Skip to content

A podcast artifact is WAV; the bundled ffmpeg for MP3 is not built #2001

Closed

Description

@MODSetter

The gap

docs/architecture/studio.md lists this under Known gaps:

A podcast is WAV. The design encodes MP3 with a bundled ffmpeg, which is not built.

Why it matters

A generated episode is a single WAV joined from audio.cpp's output for each turn, so a twenty-minute podcast is a few hundred megabytes sitting in the user's data directory. Everything downstream pays for that: the artifact file, the download button, and anything the user does with the file afterwards. MP3 at the same length is an order of magnitude smaller, and it is what someone expects when they download a podcast rather than an audio asset.

Nothing else in the app is blocked by this, which is why it has sat open — but it is the difference between a Studio podcast being shareable and being a file people re-encode by hand.

Where

What done looks like

  • A podcast artifact is audio/mpeg, stored as .mp3, and the player and download in the artifact panel still work.
  • A development checkout with nothing staged still produces a podcast. The other native runtimes degrade rather than fail (stage.mjs writes an empty folder and says why, and the app runs without that feature); WAV is the honest fallback here, so decide deliberately what happens when ffmpeg is absent and say so in the PR.
  • The docs/architecture/packaging.md extraResources table gains its row, and the Studio doc's line "There is no Electron printToPDF and no ffmpeg in surfsense_local" is corrected along with the Voicing a podcast section. Delete the Known gaps line in the same PR.

What not to change

Settle the binary with a maintainer before you write the staging script. ffmpeg's licence depends on how it was configured — an LGPL build and a GPL build are different obligations for a distributed desktop app, and it is tens of megabytes on every installer for all three platforms. Which build, which pin, and whether it ships at all is a maintainer's call, not a contributor's; the code is the easy half. Comment here with what you propose before starting.

Do not reach for ffmpeg on the user's PATH as a shortcut. The app is offline-first and self-contained by design, and a feature that works only on machines that happen to have ffmpeg installed is worse than one that does not exist.

How to verify

surfsense_local/backend/tests/unit/worker/test_studio_podcast.py drives the pipeline with a fake voice provider, and tests/unit/llm/providers/audiocpp/test_speech.py covers synthesize() and the WAV joining. tests/packaging/test_audiocpp_voicing.py is the pattern for a test that needs the staged binary. Write the failing test first: a synthesized episode whose media type is audio/mpeg.

cd surfsense_local/backend
uv run pytest -m unit
uv run pytest -m integration
uv run ruff check .
cd ../.. && python scripts/check_docs.py

Before you start

Read CONTRIBUTING.md, the Studio doc and the packaging doc, which describes how every other native runtime is staged and frozen. Open your PR against dev, not main. Comment here to claim this.

Activity

  1. added
    enhancementNew feature or request
    help wantedExtra attention is needed
    desktopSurfSense desktop app (surfsense_local)
    on Sep 28, 2026
  2. Cedric921 commented on Oct 6, 2026

    @Cedric921
    Contributor

    I'd like to take this one. As asked, here is the binary question before any staging script, with a proposal that may avoid staging ffmpeg at all.

    Where the encode sits: right after joined_wav(voiced) in audiocpp/speech.py, as encoded_or_wav(pcm) -> SynthesizedAudio. The podcast pipeline's _EXTENSIONS and persist.py already map audio/mpeg to .mp3, so nothing downstream changes. The remote voice path in joined_replies.py already returns MP3.

    The options, smallest first:

    1. lameenc as a backend dependency (my proposal).
      • LAME statically linked into a Python extension: LGPL, well under 1 MB.
      • It takes the 16-bit PCM joined_wav already has, with no subprocess, no new staged binary, and no change to electron-builder.yml or SidecarContext.
      • The LGPL relinking duty holds because it is a separate shared object in PyInstaller's onedir. LAME's licence text ships alongside, as eSpeak-ng's COPYING does.
      • To verify before committing to it: wheels for the backend's Python on darwin-arm64, win-amd64 and manylinux-x64. I'll confirm that in the PR rather than assume it.
    2. soundfile: its wheels bundle an LGPL libsndfile 1.2.x built with LAME and mpg123, which can write MP3. About 1.5 MB, and PyInstaller has to collect its data folder.
    3. A staged ffmpeg as a last resort: an LGPL build configured with only --enable-libmp3lame (about 3–8 MB per platform), pinned and SHA-checked per platform, smoke-run with -version, and given an ffmpeg row in extraResources. Generic prebuilt binaries are mostly GPL and 70–90 MB, so I would not take one of those.

    Fallback: with no encoder importable, a dev checkout that skipped the dependency or a failed import, the podcast stays WAV. That is deliberate and tested: one test pins audio/mpeg with the encoder, one pins audio/wav without it. Nothing on the user's PATH is used.

    Which build ships is your call: lameenc, soundfile, or a staged LGPL ffmpeg. With option 1 or 2, the packaging.md change is a dependency line rather than an extraResources row. I'll update the Studio doc's ffmpeg sentence and Voicing a podcast section, and delete the Known gaps line, in the same PR.

  3. added a commit that references this issue on Oct 15, 2026
    32a0c0d
  4. MODSetter commented on Oct 15, 2026

    @MODSetter
    OwnerAuthor

    Fixed by #2183, merged into dev, by @Cedric921. The Known gaps line this issue quoted is deleted in the same PR.

    Closing by hand: GitHub only auto-closes on a merge to the default branch, and we merge through dev.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    desktopSurfSense desktop app (surfsense_local)enhancementNew feature or requesthelp wantedExtra attention is needed

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions