Vibecode Swell AI
track this build4 phases, 12 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Swell AI, generate show notes, articles, social posts, and transcripts from uploaded episodes. The hard boundary is templates, integrations, hosted processing, and content history, plus audio infrastructure, distribution, and production polish.
You are building a lean indie version of Swell AI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Swell AI · indie build A local podcast production tool whose hard problems are editing without ever rewriting the source recording, hitting a published loudness target measured on the file you actually ship, and generating notes that can be traced back to the words that were said. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI and an HTMX interface | a local web interface with no build step and no accounts | | Audio | ffmpeg for decode, filtering, loudness measurement and encode | the measurement and the encode have to come from the same toolchain to agree | | Storage | SQLite for metadata, a read-only media folder for sources, a renders folder for output | the source folder and the output folder are different folders for a reason | | Editing | An ordered edit list applied at render, never applied to the source | non-destructive is not a preference here, it is the point | | Transcription | A local model by default, an API key optional and degraded cleanly without one | an hour of audio through an API costs money and leaves the machine | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the interface and the processing. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with the loudness filter and the encoders you need** · free - Why: Decode, filtering, loudness measurement and encoding all come from here, and the measurement and the encode agreeing matters more than either alone. - Get it: Your package manager or a static build. - Verify: ffmpeg -filters lists the loudness normalisation filter and -encoders lists your target codec - [ ] **A local transcription model** · free, plus the disk and the time - Why: An hour of audio per episode through a hosted service is a recurring bill and a copy of every conversation leaving the machine. - Get it: Download a speech recognition model sized for your hardware and confirm it runs on a short clip. - Verify: A one minute clip transcribes with timings - [ ] **A transcription or text model API key** (optional) · per minute of audio or per thousand tokens; check the current rate - Why: Only if you want higher quality than the local model gives; every phase must work without it. - Get it: Create a key with the narrowest scope your provider offers. - Verify: A short clip transcribes through the API and the local path still works when the key is removed - [ ] **Three real recordings, including one difficult one** · free - Why: Loudness, chunk stitching and gating all behave differently on a quiet remote guest than on a clean studio track, and a clean fixture proves nothing. - Get it: One studio recording, one noisy remote call, and one already mastered episode. - Verify: All three import and render within the stated tolerance - [ ] **Your loudness target, true peak ceiling and channel count, with sources** · free - Why: The figures are asserted by tests and the published guidance changes, so the version you built against needs recording. - Get it: Read the current audio requirements of the directories you publish to and write down the figures and the date. - Verify: The README names the figures, the source and the date ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Remote recording. - Hosting, feeds and directory analytics. - Licensed music and voice cloning. - Automatic publishing of anything. - templates, integrations, hosted processing, and content history - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support If one of those is essential to you, that is the reason to keep paying for Swell AI, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Swell AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local podcast post-production and show-notes tool that never touches your original recordings to replace Swell AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules. The original recording is read-only, permanently. Every operation produces a new file or an entry in an edit list, the source file's checksum is recorded at import and re-verified before every render, and no code path anywhere opens the source for writing. An interview you cannot re-record is the one file in this build that has no backup by definition. Second, generated text is a draft, and it is the host's name on the published version. Every generated sentence is derived from the transcript and carries the timestamps it came from, so it can be checked in seconds. The tool does not invent a guest's job title, a company name, a statistic or a quotation, and it does not publish anything without a person marking it approved. A show-notes tool that fabricates a guest's credentials has published a lie under somebody else's byline, and no amount of speed is worth that. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI and an HTMX interface · a local web interface with no build step and no accounts - Audio: ffmpeg for decode, filtering, loudness measurement and encode · the measurement and the encode have to come from the same toolchain to agree - Storage: SQLite for metadata, a read-only media folder for sources, a renders folder for output · the source folder and the output folder are different folders for a reason - Editing: An ordered edit list applied at render, never applied to the source · non-destructive is not a preference here, it is the point - Transcription: A local model by default, an API key optional and degraded cleanly without one · an hour of audio through an API costs money and leaves the machine ### Data model (create this before Phase 1) - episodes(id, title, source_path, source_sha256, duration_seconds, sample_rate, channels, imported_at) · source_path is inside the read-only folder - edits(episode_id, ordinal, kind, at_seconds, to_seconds, params_json) · kind is cut, fade, gain, gate or marker, and applying them produces a new file - renders(id, episode_id, edit_sha256, target_lufs, measured_lufs, true_peak_db, format, bitrate, channels, path, rendered_at) - transcripts(id, episode_id, engine, engine_version, language, created_at) - segments(transcript_id, ordinal, start_seconds, end_seconds, speaker, text, confidence) · timings survive a manual correction - chapters(episode_id, start_seconds, title, origin) · origin is manual or derived, and derived ones carry the segments they came from - drafts(id, episode_id, kind, engine, body, citations_json, approved_at, approved_by) · citations_json maps each passage to segment ids ### Phase 1 · Originals that cannot be damaged Build: Copy or hard link the source into a media folder, record its checksum and its real format from the decoder rather than from the extension, and mark the folder read-only. Build a peaks file for waveform display so the interface never decodes the whole recording again. Represent every edit as a row in the edit list. Re-verify the source checksum before each render and refuse if it changed. Trap: Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. Done when: The source file's checksum after a full edit and render session is identical to the one recorded at import, asserted in the suite. No code path opens a file under the media folder for writing, verified by review and by a test that makes the folder read-only and runs a full render. The format, sample rate and channel count come from the decoder, not the file extension, tested with a mislabelled file. A render refuses to start if the source checksum no longer matches. Do not build yet: No transcription, no loudness work, no generated text, no export. ### Phase 2 · A transcript with timings you can click Build: Transcribe to segments with start and end times, a speaker label where the engine offers one, and a confidence score. Split long recordings into overlapping chunks and stitch them so a sentence across a boundary is not duplicated or lost. Let a person correct text without destroying the timings, and record which engine and version produced each transcript. Trap: Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. Done when: A ninety minute recording transcribes with no duplicated or dropped words at chunk boundaries, asserted against a fixture with known text at the boundary positions. Correcting a segment's text leaves its start and end times unchanged. Every transcript records its engine and version, so a later re-run is comparable. With no API key configured the tool uses the local engine and says which one, rather than failing. Do not build yet: No loudness processing, no generated notes, no export. ### Phase 3 · Loudness measured on the file you ship Build: Normalise towards a published target and verify the result. Apple Podcasts states an overall loudness around minus sixteen LKFS with a tolerance of plus or minus one decibel and a true peak not exceeding minus one decibel relative to full scale, and recommends sixty four to one hundred and twenty eight kilobits per second for mono and one hundred and twenty eight to two hundred and fifty six for stereo at forty four point one or forty eight kilohertz for feed audio. Check the current page yourself, then measure the encoded output and store the measured figures. Trap: Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. Done when: The measured loudness of the encoded output is within the stated tolerance of the target, asserted for at least three fixtures including one very quiet and one already loud. The measured true peak of the encoded output does not exceed the stated ceiling. Every render stores its target, its measured loudness, its true peak, its channel count and its bit rate. The interface shows the measured figures next to the target rather than only claiming success. Do not build yet: No generated notes, no chapters export, no publishing. ### Phase 4 · Notes that point at what was actually said Build: Generate a title, a description, chapter suggestions and social excerpts from the transcript only, with each passage carrying the segment identifiers it drew on. Show the draft beside its citations so each claim is one click from the audio. Require an explicit approval per draft before export, and keep every draft with the engine that produced it. Work without any model configured by falling back to extraction from the transcript. Trap: A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. Done when: Every generated passage carries at least one segment citation and the interface shows the cited audio range, asserted in the suite. Exporting a draft that has not been approved is refused. With no model configured, the tool still produces chapter suggestions and excerpts by extraction and labels them as extracted. A draft is stored with its engine, so a regeneration can be compared against the previous one. Do not build yet: No publishing, feed generation or analytics. ### Phase 5 · Exporting an episode and living with the disk Build: Export the encoded audio, the transcript in a caption format with timings, chapters as JSON, the approved notes as Markdown, and a manifest naming every file with its checksum and the measured audio figures. Write chapter markers into the audio file where the format supports it. Delete old renders after a stated retention, never sources. Cap disk use and refuse a render that would exceed it, with the figure shown. Trap: Keeping every render of every episode fills a disk in a season, and the cleanup script written in a hurry then walks the media folder as well and removes the masters. Give renders a retention and a ceiling, make the delete path physically incapable of reaching the media folder, and put that in a test. Done when: An export produces audio, timed captions, chapters, notes and a manifest whose checksums match the files, verified by re-reading the manifest. Chapter markers written into the audio are read back by a separate tool. Retention deletes only files under the renders folder, asserted by a test that places a file in the media folder and confirms it survives. A render that would exceed the disk ceiling is refused with the current and configured figures shown. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time remote recording, which is a separate product with its own network, drift and failure problems and cannot be bolted on. - Podcast hosting, feed distribution and directory analytics, which are an operational commitment rather than a feature. - Licensed music beds and voice cloning, which are licensing and consent questions long before they are engineering ones. - Publishing anything automatically, since the approval step is the whole defence against a generated claim nobody checked. ### README must contain - That the media folder is read-only and every source checksum is verified before each render - That edits are a list applied at render time, so the original is never rewritten - Which loudness and true peak targets were used, the source they came from and the date it was read - That loudness is measured on the encoded output and the measured figures are stored per render - That generated text carries transcript citations and cannot be exported without approval ===== AGENTS.md ===== # Agent instructions · Swell AI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with FastAPI and an HTMX interface, ffmpeg for decode, filtering, loudness measurement and encode, SQLite for metadata, a read-only media folder for sources, a renders folder for output, An ordered edit list applied at render, never applied to the source, A local model by default, an API key optional and degraded cleanly without one. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. - Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. - Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. - A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. ===== BUILD_PLAN.md ===== # Build plan · Swell AI A local podcast production tool whose hard problems are editing without ever rewriting the source recording, hitting a published loudness target measured on the file you actually ship, and generating notes that can be traced back to the words that were said. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Originals that cannot be damaged Importing a recording makes it read-only and every later operation is additive. ### Steps 1. Import into a read-only media folder The filesystem is a better guarantee than a code review. Files: `app/media/import.py`, `tests/test_source_untouched.py` 2. Keep edits as rows, applied only at render An edit list can be undone; an overwritten file cannot. Files: `app/edits/model.py` 3. Verify the checksum before every render It catches the one path that got it wrong, whenever that appears. Files: `app/render/guard.py` ### Done when - [ ] Source checksums are unchanged after a full session - [ ] No write path exists into the media folder - [ ] Format comes from the decoder, not the extension - [ ] A changed source refuses to render ### Watch out - Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. ## Phase 2 · A transcript with timings you can click Every word has a time, so notes and chapters can point back at the audio. ### Steps 1. Chunk with overlap and stitch by matching Fixed boundaries with no overlap duplicate or lose a sentence every time. Files: `app/transcribe/chunk.py`, `tests/test_stitch.py` 2. Keep timings through manual corrections The timings are what make every later feature possible. Files: `app/transcribe/edit.py` 3. Record the engine and version per transcript Two transcripts of one episode are only comparable if you know what made them. Files: `app/transcribe/model.py` ### Done when - [ ] No duplicated or dropped words at chunk boundaries - [ ] Corrections preserve segment timings - [ ] Engine and version are recorded per transcript - [ ] No API key degrades to the local engine with a notice ### Watch out - Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. ## Phase 3 · Loudness measured on the file you ship The exported episode meets a published target, verified after encoding rather than before. ### Steps 1. Measure in two passes, then encode, then measure again The encoded file is the only file anyone will hear. Files: `app/render/loudness.py`, `tests/test_loudness.py` 2. Store measured loudness and true peak per render A claim without a measurement is a claim. Files: `app/render/model.py` 3. Cite the target's source and the date it was read The published figures change, and the README is where the next person looks. Files: `docs/audio-targets.md` ### Done when - [ ] Encoded output is within tolerance of the target - [ ] Encoded true peak stays under the ceiling - [ ] Renders store measured figures, channels and bit rate - [ ] The target cites its source and the date read ### Watch out - Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. ## Phase 4 · Notes that point at what was actually said Every generated passage can be traced to timestamps in the transcript and nothing ships unread. ### Steps 1. Cite segments for every generated passage It is the only cheap defence against a confident invention. Files: `app/drafts/generate.py`, `tests/test_citations.py` 2. Refuse to export an unapproved draft The approval is the point at which a person takes responsibility. Files: `app/export/gate.py` 3. Fall back to extraction with no model A tool that needs a key to be useful is a tool with a monthly bill. Files: `app/drafts/extract.py` ### Done when - [ ] Every passage carries a segment citation - [ ] Unapproved drafts cannot be exported - [ ] No model configured still yields labelled extraction - [ ] Drafts record the engine that produced them ### Watch out - A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. ## Not in this build - Remote recording. - Hosting, feeds and directory analytics. - Licensed music and voice cloning. - Automatic publishing of anything. ## After v1, if you want it - A second render profile for a different directory's published figures, once one has shipped a season - Speaker naming that persists across episodes, built on the existing segment labels - A diff between two transcripts of the same episode, which the recorded engine versions already make meaningful ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Read-only sources. Nothing in the application writes here. MEDIA_DIR=./media # Required. Everything the tool produces. The only folder retention can reach. RENDERS_DIR=./renders # Required. Episodes, edits, transcripts, renders and drafts. DATABASE_PATH=./data/podcast.db # Required. Taken from the directory's published requirements, with the date recorded. TARGET_LUFS=-16 # Required. Measured on the encoded file, not on the intermediate. TRUE_PEAK_CEILING_DB=-1 # Required. Applies to renders only. Sources are never deleted by any path. RENDER_RETENTION_DAYS=60 # Required. A render that would exceed this is refused before it starts. MAX_RENDERS_GB=50 # Optional · secret. Optional. Without it the local model is used and the interface says so. TRANSCRIPTION_API_KEY=sk-...redacted
You are building a lean indie version of Swell AI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Swell AI · indie build A local podcast production tool whose hard problems are editing without ever rewriting the source recording, hitting a published loudness target measured on the file you actually ship, and generating notes that can be traced back to the words that were said. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI and an HTMX interface | a local web interface with no build step and no accounts | | Audio | ffmpeg for decode, filtering, loudness measurement and encode | the measurement and the encode have to come from the same toolchain to agree | | Storage | SQLite for metadata, a read-only media folder for sources, a renders folder for output | the source folder and the output folder are different folders for a reason | | Editing | An ordered edit list applied at render, never applied to the source | non-destructive is not a preference here, it is the point | | Transcription | A local model by default, an API key optional and degraded cleanly without one | an hour of audio through an API costs money and leaves the machine | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the interface and the processing. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with the loudness filter and the encoders you need** · free - Why: Decode, filtering, loudness measurement and encoding all come from here, and the measurement and the encode agreeing matters more than either alone. - Get it: Your package manager or a static build. - Verify: ffmpeg -filters lists the loudness normalisation filter and -encoders lists your target codec - [ ] **A local transcription model** · free, plus the disk and the time - Why: An hour of audio per episode through a hosted service is a recurring bill and a copy of every conversation leaving the machine. - Get it: Download a speech recognition model sized for your hardware and confirm it runs on a short clip. - Verify: A one minute clip transcribes with timings - [ ] **A transcription or text model API key** (optional) · per minute of audio or per thousand tokens; check the current rate - Why: Only if you want higher quality than the local model gives; every phase must work without it. - Get it: Create a key with the narrowest scope your provider offers. - Verify: A short clip transcribes through the API and the local path still works when the key is removed - [ ] **Three real recordings, including one difficult one** · free - Why: Loudness, chunk stitching and gating all behave differently on a quiet remote guest than on a clean studio track, and a clean fixture proves nothing. - Get it: One studio recording, one noisy remote call, and one already mastered episode. - Verify: All three import and render within the stated tolerance - [ ] **Your loudness target, true peak ceiling and channel count, with sources** · free - Why: The figures are asserted by tests and the published guidance changes, so the version you built against needs recording. - Get it: Read the current audio requirements of the directories you publish to and write down the figures and the date. - Verify: The README names the figures, the source and the date ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Remote recording. - Hosting, feeds and directory analytics. - Licensed music and voice cloning. - Automatic publishing of anything. - templates, integrations, hosted processing, and content history - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support If one of those is essential to you, that is the reason to keep paying for Swell AI, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Swell AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local podcast post-production and show-notes tool that never touches your original recordings to replace Swell AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules. The original recording is read-only, permanently. Every operation produces a new file or an entry in an edit list, the source file's checksum is recorded at import and re-verified before every render, and no code path anywhere opens the source for writing. An interview you cannot re-record is the one file in this build that has no backup by definition. Second, generated text is a draft, and it is the host's name on the published version. Every generated sentence is derived from the transcript and carries the timestamps it came from, so it can be checked in seconds. The tool does not invent a guest's job title, a company name, a statistic or a quotation, and it does not publish anything without a person marking it approved. A show-notes tool that fabricates a guest's credentials has published a lie under somebody else's byline, and no amount of speed is worth that. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI and an HTMX interface · a local web interface with no build step and no accounts - Audio: ffmpeg for decode, filtering, loudness measurement and encode · the measurement and the encode have to come from the same toolchain to agree - Storage: SQLite for metadata, a read-only media folder for sources, a renders folder for output · the source folder and the output folder are different folders for a reason - Editing: An ordered edit list applied at render, never applied to the source · non-destructive is not a preference here, it is the point - Transcription: A local model by default, an API key optional and degraded cleanly without one · an hour of audio through an API costs money and leaves the machine ### Data model (create this before Phase 1) - episodes(id, title, source_path, source_sha256, duration_seconds, sample_rate, channels, imported_at) · source_path is inside the read-only folder - edits(episode_id, ordinal, kind, at_seconds, to_seconds, params_json) · kind is cut, fade, gain, gate or marker, and applying them produces a new file - renders(id, episode_id, edit_sha256, target_lufs, measured_lufs, true_peak_db, format, bitrate, channels, path, rendered_at) - transcripts(id, episode_id, engine, engine_version, language, created_at) - segments(transcript_id, ordinal, start_seconds, end_seconds, speaker, text, confidence) · timings survive a manual correction - chapters(episode_id, start_seconds, title, origin) · origin is manual or derived, and derived ones carry the segments they came from - drafts(id, episode_id, kind, engine, body, citations_json, approved_at, approved_by) · citations_json maps each passage to segment ids ### Phase 1 · Originals that cannot be damaged Build: Copy or hard link the source into a media folder, record its checksum and its real format from the decoder rather than from the extension, and mark the folder read-only. Build a peaks file for waveform display so the interface never decodes the whole recording again. Represent every edit as a row in the edit list. Re-verify the source checksum before each render and refuse if it changed. Trap: Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. Done when: The source file's checksum after a full edit and render session is identical to the one recorded at import, asserted in the suite. No code path opens a file under the media folder for writing, verified by review and by a test that makes the folder read-only and runs a full render. The format, sample rate and channel count come from the decoder, not the file extension, tested with a mislabelled file. A render refuses to start if the source checksum no longer matches. Do not build yet: No transcription, no loudness work, no generated text, no export. ### Phase 2 · A transcript with timings you can click Build: Transcribe to segments with start and end times, a speaker label where the engine offers one, and a confidence score. Split long recordings into overlapping chunks and stitch them so a sentence across a boundary is not duplicated or lost. Let a person correct text without destroying the timings, and record which engine and version produced each transcript. Trap: Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. Done when: A ninety minute recording transcribes with no duplicated or dropped words at chunk boundaries, asserted against a fixture with known text at the boundary positions. Correcting a segment's text leaves its start and end times unchanged. Every transcript records its engine and version, so a later re-run is comparable. With no API key configured the tool uses the local engine and says which one, rather than failing. Do not build yet: No loudness processing, no generated notes, no export. ### Phase 3 · Loudness measured on the file you ship Build: Normalise towards a published target and verify the result. Apple Podcasts states an overall loudness around minus sixteen LKFS with a tolerance of plus or minus one decibel and a true peak not exceeding minus one decibel relative to full scale, and recommends sixty four to one hundred and twenty eight kilobits per second for mono and one hundred and twenty eight to two hundred and fifty six for stereo at forty four point one or forty eight kilohertz for feed audio. Check the current page yourself, then measure the encoded output and store the measured figures. Trap: Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. Done when: The measured loudness of the encoded output is within the stated tolerance of the target, asserted for at least three fixtures including one very quiet and one already loud. The measured true peak of the encoded output does not exceed the stated ceiling. Every render stores its target, its measured loudness, its true peak, its channel count and its bit rate. The interface shows the measured figures next to the target rather than only claiming success. Do not build yet: No generated notes, no chapters export, no publishing. ### Phase 4 · Notes that point at what was actually said Build: Generate a title, a description, chapter suggestions and social excerpts from the transcript only, with each passage carrying the segment identifiers it drew on. Show the draft beside its citations so each claim is one click from the audio. Require an explicit approval per draft before export, and keep every draft with the engine that produced it. Work without any model configured by falling back to extraction from the transcript. Trap: A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. Done when: Every generated passage carries at least one segment citation and the interface shows the cited audio range, asserted in the suite. Exporting a draft that has not been approved is refused. With no model configured, the tool still produces chapter suggestions and excerpts by extraction and labels them as extracted. A draft is stored with its engine, so a regeneration can be compared against the previous one. Do not build yet: No publishing, feed generation or analytics. ### Phase 5 · Exporting an episode and living with the disk Build: Export the encoded audio, the transcript in a caption format with timings, chapters as JSON, the approved notes as Markdown, and a manifest naming every file with its checksum and the measured audio figures. Write chapter markers into the audio file where the format supports it. Delete old renders after a stated retention, never sources. Cap disk use and refuse a render that would exceed it, with the figure shown. Trap: Keeping every render of every episode fills a disk in a season, and the cleanup script written in a hurry then walks the media folder as well and removes the masters. Give renders a retention and a ceiling, make the delete path physically incapable of reaching the media folder, and put that in a test. Done when: An export produces audio, timed captions, chapters, notes and a manifest whose checksums match the files, verified by re-reading the manifest. Chapter markers written into the audio are read back by a separate tool. Retention deletes only files under the renders folder, asserted by a test that places a file in the media folder and confirms it survives. A render that would exceed the disk ceiling is refused with the current and configured figures shown. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time remote recording, which is a separate product with its own network, drift and failure problems and cannot be bolted on. - Podcast hosting, feed distribution and directory analytics, which are an operational commitment rather than a feature. - Licensed music beds and voice cloning, which are licensing and consent questions long before they are engineering ones. - Publishing anything automatically, since the approval step is the whole defence against a generated claim nobody checked. ### README must contain - That the media folder is read-only and every source checksum is verified before each render - That edits are a list applied at render time, so the original is never rewritten - Which loudness and true peak targets were used, the source they came from and the date it was read - That loudness is measured on the encoded output and the measured figures are stored per render - That generated text carries transcript citations and cannot be exported without approval ===== AGENTS.md ===== # Agent instructions · Swell AI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with FastAPI and an HTMX interface, ffmpeg for decode, filtering, loudness measurement and encode, SQLite for metadata, a read-only media folder for sources, a renders folder for output, An ordered edit list applied at render, never applied to the source, A local model by default, an API key optional and degraded cleanly without one. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. - Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. - Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. - A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. ===== BUILD_PLAN.md ===== # Build plan · Swell AI A local podcast production tool whose hard problems are editing without ever rewriting the source recording, hitting a published loudness target measured on the file you actually ship, and generating notes that can be traced back to the words that were said. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Originals that cannot be damaged Importing a recording makes it read-only and every later operation is additive. ### Steps 1. Import into a read-only media folder The filesystem is a better guarantee than a code review. Files: `app/media/import.py`, `tests/test_source_untouched.py` 2. Keep edits as rows, applied only at render An edit list can be undone; an overwritten file cannot. Files: `app/edits/model.py` 3. Verify the checksum before every render It catches the one path that got it wrong, whenever that appears. Files: `app/render/guard.py` ### Done when - [ ] Source checksums are unchanged after a full session - [ ] No write path exists into the media folder - [ ] Format comes from the decoder, not the extension - [ ] A changed source refuses to render ### Watch out - Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. ## Phase 2 · A transcript with timings you can click Every word has a time, so notes and chapters can point back at the audio. ### Steps 1. Chunk with overlap and stitch by matching Fixed boundaries with no overlap duplicate or lose a sentence every time. Files: `app/transcribe/chunk.py`, `tests/test_stitch.py` 2. Keep timings through manual corrections The timings are what make every later feature possible. Files: `app/transcribe/edit.py` 3. Record the engine and version per transcript Two transcripts of one episode are only comparable if you know what made them. Files: `app/transcribe/model.py` ### Done when - [ ] No duplicated or dropped words at chunk boundaries - [ ] Corrections preserve segment timings - [ ] Engine and version are recorded per transcript - [ ] No API key degrades to the local engine with a notice ### Watch out - Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. ## Phase 3 · Loudness measured on the file you ship The exported episode meets a published target, verified after encoding rather than before. ### Steps 1. Measure in two passes, then encode, then measure again The encoded file is the only file anyone will hear. Files: `app/render/loudness.py`, `tests/test_loudness.py` 2. Store measured loudness and true peak per render A claim without a measurement is a claim. Files: `app/render/model.py` 3. Cite the target's source and the date it was read The published figures change, and the README is where the next person looks. Files: `docs/audio-targets.md` ### Done when - [ ] Encoded output is within tolerance of the target - [ ] Encoded true peak stays under the ceiling - [ ] Renders store measured figures, channels and bit rate - [ ] The target cites its source and the date read ### Watch out - Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. ## Phase 4 · Notes that point at what was actually said Every generated passage can be traced to timestamps in the transcript and nothing ships unread. ### Steps 1. Cite segments for every generated passage It is the only cheap defence against a confident invention. Files: `app/drafts/generate.py`, `tests/test_citations.py` 2. Refuse to export an unapproved draft The approval is the point at which a person takes responsibility. Files: `app/export/gate.py` 3. Fall back to extraction with no model A tool that needs a key to be useful is a tool with a monthly bill. Files: `app/drafts/extract.py` ### Done when - [ ] Every passage carries a segment citation - [ ] Unapproved drafts cannot be exported - [ ] No model configured still yields labelled extraction - [ ] Drafts record the engine that produced them ### Watch out - A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. ## Not in this build - Remote recording. - Hosting, feeds and directory analytics. - Licensed music and voice cloning. - Automatic publishing of anything. ## After v1, if you want it - A second render profile for a different directory's published figures, once one has shipped a season - Speaker naming that persists across episodes, built on the existing segment labels - A diff between two transcripts of the same episode, which the recorded engine versions already make meaningful ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Read-only sources. Nothing in the application writes here. MEDIA_DIR=./media # Required. Everything the tool produces. The only folder retention can reach. RENDERS_DIR=./renders # Required. Episodes, edits, transcripts, renders and drafts. DATABASE_PATH=./data/podcast.db # Required. Taken from the directory's published requirements, with the date recorded. TARGET_LUFS=-16 # Required. Measured on the encoded file, not on the intermediate. TRUE_PEAK_CEILING_DB=-1 # Required. Applies to renders only. Sources are never deleted by any path. RENDER_RETENTION_DAYS=60 # Required. A render that would exceed this is refused before it starts. MAX_RENDERS_GB=50 # Optional · secret. Optional. Without it the local model is used and the interface says so. TRANSCRIPTION_API_KEY=sk-...redacted
You are building a production product version of Swell AI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Swell AI · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Swell AI, generate show notes, articles, social posts, and transcripts from uploaded episodes. The hard boundary is templates, integrations, hosted processing, and content history, plus audio infrastructure, distribution, and production polish. ## Product outcome A production tool that physically cannot damage the recording you can never make again, ships audio whose loudness you measured on the file itself, and produces show notes every sentence of which can be traced to the moment it was said. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - ffmpeg - local audio files - optional transcription API key - sufficient disk space ## Explicit non-goals for v1 - Remote recording. - Hosting, feeds and directory analytics. - Licensed music and voice cloning. - Automatic publishing of anything. - templates, integrations, hosted processing, and content history - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support ## Success criteria - Source checksums are confirmed unchanged after a full session - No write path into the media folder is confirmed to exist - Encoded loudness and true peak are confirmed within target - Every generated passage is confirmed to carry a citation - Retention is confirmed unable to reach the media folder ===== BRIEF.md ===== # Build brief · Swell AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local podcast post-production and show-notes tool that never touches your original recordings to replace Swell AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules. The original recording is read-only, permanently. Every operation produces a new file or an entry in an edit list, the source file's checksum is recorded at import and re-verified before every render, and no code path anywhere opens the source for writing. An interview you cannot re-record is the one file in this build that has no backup by definition. Second, generated text is a draft, and it is the host's name on the published version. Every generated sentence is derived from the transcript and carries the timestamps it came from, so it can be checked in seconds. The tool does not invent a guest's job title, a company name, a statistic or a quotation, and it does not publish anything without a person marking it approved. A show-notes tool that fabricates a guest's credentials has published a lie under somebody else's byline, and no amount of speed is worth that. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI and an HTMX interface · a local web interface with no build step and no accounts - Audio: ffmpeg for decode, filtering, loudness measurement and encode · the measurement and the encode have to come from the same toolchain to agree - Storage: SQLite for metadata, a read-only media folder for sources, a renders folder for output · the source folder and the output folder are different folders for a reason - Editing: An ordered edit list applied at render, never applied to the source · non-destructive is not a preference here, it is the point - Transcription: A local model by default, an API key optional and degraded cleanly without one · an hour of audio through an API costs money and leaves the machine ### Data model (create this before Phase 1) - episodes(id, title, source_path, source_sha256, duration_seconds, sample_rate, channels, imported_at) · source_path is inside the read-only folder - edits(episode_id, ordinal, kind, at_seconds, to_seconds, params_json) · kind is cut, fade, gain, gate or marker, and applying them produces a new file - renders(id, episode_id, edit_sha256, target_lufs, measured_lufs, true_peak_db, format, bitrate, channels, path, rendered_at) - transcripts(id, episode_id, engine, engine_version, language, created_at) - segments(transcript_id, ordinal, start_seconds, end_seconds, speaker, text, confidence) · timings survive a manual correction - chapters(episode_id, start_seconds, title, origin) · origin is manual or derived, and derived ones carry the segments they came from - drafts(id, episode_id, kind, engine, body, citations_json, approved_at, approved_by) · citations_json maps each passage to segment ids ### Phase 1 · Originals that cannot be damaged Build: Copy or hard link the source into a media folder, record its checksum and its real format from the decoder rather than from the extension, and mark the folder read-only. Build a peaks file for waveform display so the interface never decodes the whole recording again. Represent every edit as a row in the edit list. Re-verify the source checksum before each render and refuse if it changed. Trap: Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. Done when: The source file's checksum after a full edit and render session is identical to the one recorded at import, asserted in the suite. No code path opens a file under the media folder for writing, verified by review and by a test that makes the folder read-only and runs a full render. The format, sample rate and channel count come from the decoder, not the file extension, tested with a mislabelled file. A render refuses to start if the source checksum no longer matches. Do not build yet: No transcription, no loudness work, no generated text, no export. ### Phase 2 · A transcript with timings you can click Build: Transcribe to segments with start and end times, a speaker label where the engine offers one, and a confidence score. Split long recordings into overlapping chunks and stitch them so a sentence across a boundary is not duplicated or lost. Let a person correct text without destroying the timings, and record which engine and version produced each transcript. Trap: Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. Done when: A ninety minute recording transcribes with no duplicated or dropped words at chunk boundaries, asserted against a fixture with known text at the boundary positions. Correcting a segment's text leaves its start and end times unchanged. Every transcript records its engine and version, so a later re-run is comparable. With no API key configured the tool uses the local engine and says which one, rather than failing. Do not build yet: No loudness processing, no generated notes, no export. ### Phase 3 · Loudness measured on the file you ship Build: Normalise towards a published target and verify the result. Apple Podcasts states an overall loudness around minus sixteen LKFS with a tolerance of plus or minus one decibel and a true peak not exceeding minus one decibel relative to full scale, and recommends sixty four to one hundred and twenty eight kilobits per second for mono and one hundred and twenty eight to two hundred and fifty six for stereo at forty four point one or forty eight kilohertz for feed audio. Check the current page yourself, then measure the encoded output and store the measured figures. Trap: Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. Done when: The measured loudness of the encoded output is within the stated tolerance of the target, asserted for at least three fixtures including one very quiet and one already loud. The measured true peak of the encoded output does not exceed the stated ceiling. Every render stores its target, its measured loudness, its true peak, its channel count and its bit rate. The interface shows the measured figures next to the target rather than only claiming success. Do not build yet: No generated notes, no chapters export, no publishing. ### Phase 4 · Notes that point at what was actually said Build: Generate a title, a description, chapter suggestions and social excerpts from the transcript only, with each passage carrying the segment identifiers it drew on. Show the draft beside its citations so each claim is one click from the audio. Require an explicit approval per draft before export, and keep every draft with the engine that produced it. Work without any model configured by falling back to extraction from the transcript. Trap: A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. Done when: Every generated passage carries at least one segment citation and the interface shows the cited audio range, asserted in the suite. Exporting a draft that has not been approved is refused. With no model configured, the tool still produces chapter suggestions and excerpts by extraction and labels them as extracted. A draft is stored with its engine, so a regeneration can be compared against the previous one. Do not build yet: No publishing, feed generation or analytics. ### Phase 5 · Exporting an episode and living with the disk Build: Export the encoded audio, the transcript in a caption format with timings, chapters as JSON, the approved notes as Markdown, and a manifest naming every file with its checksum and the measured audio figures. Write chapter markers into the audio file where the format supports it. Delete old renders after a stated retention, never sources. Cap disk use and refuse a render that would exceed it, with the figure shown. Trap: Keeping every render of every episode fills a disk in a season, and the cleanup script written in a hurry then walks the media folder as well and removes the masters. Give renders a retention and a ceiling, make the delete path physically incapable of reaching the media folder, and put that in a test. Done when: An export produces audio, timed captions, chapters, notes and a manifest whose checksums match the files, verified by re-reading the manifest. Chapter markers written into the audio are read back by a separate tool. Retention deletes only files under the renders folder, asserted by a test that places a file in the media folder and confirms it survives. A render that would exceed the disk ceiling is refused with the current and configured figures shown. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time remote recording, which is a separate product with its own network, drift and failure problems and cannot be bolted on. - Podcast hosting, feed distribution and directory analytics, which are an operational commitment rather than a feature. - Licensed music beds and voice cloning, which are licensing and consent questions long before they are engineering ones. - Publishing anything automatically, since the approval step is the whole defence against a generated claim nobody checked. ### README must contain - That the media folder is read-only and every source checksum is verified before each render - That edits are a list applied at render time, so the original is never rewritten - Which loudness and true peak targets were used, the source they came from and the date it was read - That loudness is measured on the encoded output and the measured figures are stored per render - That generated text carries transcript citations and cannot be exported without approval ===== ARCHITECTURE.md ===== # Architecture · Swell AI ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI and an HTMX interface | a local web interface with no build step and no accounts | | Audio | ffmpeg for decode, filtering, loudness measurement and encode | the measurement and the encode have to come from the same toolchain to agree | | Storage | SQLite for metadata, a read-only media folder for sources, a renders folder for output | the source folder and the output folder are different folders for a reason | | Editing | An ordered edit list applied at render, never applied to the source | non-destructive is not a preference here, it is the point | | Transcription | A local model by default, an API key optional and degraded cleanly without one | an hour of audio through an API costs money and leaves the machine | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Read-only sources, checksums and decoded format. | Any storage layout, provided sources stay unwritable. | | Edits | The ordered edit list applied at render. | Not swappable for in-place processing, which is the whole risk. | | Render | Two pass loudness, encoding and measured output. | Any toolchain, provided the measurement happens after the encode. | | Transcript | Overlapped chunking, stitching and timings. | Any engine, provided timings survive manual correction. | | Drafts | Citations, approval and the extraction fallback. | Any model, provided uncited passages are still refused. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `MEDIA_DIR` · required · Read-only sources. Nothing in the application writes here. - `RENDERS_DIR` · required · Everything the tool produces. The only folder retention can reach. - `DATABASE_PATH` · required · Episodes, edits, transcripts, renders and drafts. - `TARGET_LUFS` · required · Taken from the directory's published requirements, with the date recorded. - `TRUE_PEAK_CEILING_DB` · required · Measured on the encoded file, not on the intermediate. - `RENDER_RETENTION_DAYS` · required · Applies to renders only. Sources are never deleted by any path. - `MAX_RENDERS_GB` · required · A render that would exceed this is refused before it starts. - `TRANSCRIPTION_API_KEY` · optional, secret · Optional. Without it the local model is used and the interface says so. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Swell AI product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with FastAPI and an HTMX interface, ffmpeg for decode, filtering, loudness measurement and encode, SQLite for metadata, a read-only media folder for sources, a renders folder for output, An ordered edit list applied at render, never applied to the source, A local model by default, an API key optional and degraded cleanly without one. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. - Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. - Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. - A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. - Keeping every render of every episode fills a disk in a season, and the cleanup script written in a hurry then walks the media folder as well and removes the masters. Give renders a retention and a ceiling, make the delete path physically incapable of reaching the media folder, and put that in a test. ===== MILESTONES.md ===== # Delivery milestones · Swell AI Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Originals that cannot be damaged Importing a recording makes it read-only and every later operation is additive. ### Steps 1. Import into a read-only media folder The filesystem is a better guarantee than a code review. Files: `app/media/import.py`, `tests/test_source_untouched.py` 2. Keep edits as rows, applied only at render An edit list can be undone; an overwritten file cannot. Files: `app/edits/model.py` 3. Verify the checksum before every render It catches the one path that got it wrong, whenever that appears. Files: `app/render/guard.py` ### Done when - [ ] Source checksums are unchanged after a full session - [ ] No write path exists into the media folder - [ ] Format comes from the decoder, not the extension - [ ] A changed source refuses to render ### Watch out - Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. ## M2 · A transcript with timings you can click Every word has a time, so notes and chapters can point back at the audio. ### Steps 1. Chunk with overlap and stitch by matching Fixed boundaries with no overlap duplicate or lose a sentence every time. Files: `app/transcribe/chunk.py`, `tests/test_stitch.py` 2. Keep timings through manual corrections The timings are what make every later feature possible. Files: `app/transcribe/edit.py` 3. Record the engine and version per transcript Two transcripts of one episode are only comparable if you know what made them. Files: `app/transcribe/model.py` ### Done when - [ ] No duplicated or dropped words at chunk boundaries - [ ] Corrections preserve segment timings - [ ] Engine and version are recorded per transcript - [ ] No API key degrades to the local engine with a notice ### Watch out - Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. ## M3 · Loudness measured on the file you ship The exported episode meets a published target, verified after encoding rather than before. ### Steps 1. Measure in two passes, then encode, then measure again The encoded file is the only file anyone will hear. Files: `app/render/loudness.py`, `tests/test_loudness.py` 2. Store measured loudness and true peak per render A claim without a measurement is a claim. Files: `app/render/model.py` 3. Cite the target's source and the date it was read The published figures change, and the README is where the next person looks. Files: `docs/audio-targets.md` ### Done when - [ ] Encoded output is within tolerance of the target - [ ] Encoded true peak stays under the ceiling - [ ] Renders store measured figures, channels and bit rate - [ ] The target cites its source and the date read ### Watch out - Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. ## M4 · Notes that point at what was actually said Every generated passage can be traced to timestamps in the transcript and nothing ships unread. ### Steps 1. Cite segments for every generated passage It is the only cheap defence against a confident invention. Files: `app/drafts/generate.py`, `tests/test_citations.py` 2. Refuse to export an unapproved draft The approval is the point at which a person takes responsibility. Files: `app/export/gate.py` 3. Fall back to extraction with no model A tool that needs a key to be useful is a tool with a monthly bill. Files: `app/drafts/extract.py` ### Done when - [ ] Every passage carries a segment citation - [ ] Unapproved drafts cannot be exported - [ ] No model configured still yields labelled extraction - [ ] Drafts record the engine that produced them ### Watch out - A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. ## M5 · Exporting an episode and living with the disk (production only) A finished episode leaves as a complete set of files, and renders do not quietly fill the drive. ### Steps 1. Export a manifest with checksums and measured figures It is what makes an episode reproducible six months later. Files: `app/export/manifest.py` 2. Confine deletion to the renders folder A cleanup script that can reach the masters will eventually reach them. Files: `app/retention.py`, `tests/test_retention_scope.py` 3. Refuse a render over the disk ceiling Failing before the render beats failing during it. Files: `app/render/limits.py` ### Done when - [ ] The manifest's checksums match the exported files - [ ] Chapter markers read back in another tool - [ ] Retention cannot reach the media folder - [ ] A render over the ceiling is refused with figures ### Watch out - Keeping every render of every episode fills a disk in a season, and the cleanup script written in a hurry then walks the media folder as well and removes the masters. Give renders a retention and a ceiling, make the delete path physically incapable of reaching the media folder, and put that in a test. ===== OPERATIONS.md ===== # Operations · Swell AI ## Backup The media folder first, because those recordings are irreplaceable and everything else can be regenerated from them plus the database. Back up the database with it so the edit lists and approvals survive; renders are the one thing worth leaving out. ## Restore Restore media and database together, then re-verify every source checksum before re-rendering anything. A source whose checksum no longer matches is reported rather than quietly used, because the edit list was written against the original. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks are at startup and in the suite: sources whose checksum has changed, renders whose measured loudness sits outside tolerance, the size of the renders folder against its ceiling, and drafts exported without approval, which should be zero. ## Incident checklist A damaged source is the incident this build exists to prevent, and the checksum at import is what detects it. An episode that sounds wrong is diagnosed from the stored render figures before anything is re-rendered, because those figures record what actually shipped. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Source checksums are confirmed unchanged after a full session - [ ] No write path into the media folder is confirmed to exist - [ ] Encoded loudness and true peak are confirmed within target - [ ] Every generated passage is confirmed to carry a citation - [ ] Retention is confirmed unable to reach the media folder ## Launch constraint Do not market omitted Swell AI capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Read-only sources. Nothing in the application writes here. MEDIA_DIR=./media # Required. Everything the tool produces. The only folder retention can reach. RENDERS_DIR=./renders # Required. Episodes, edits, transcripts, renders and drafts. DATABASE_PATH=./data/podcast.db # Required. Taken from the directory's published requirements, with the date recorded. TARGET_LUFS=-16 # Required. Measured on the encoded file, not on the intermediate. TRUE_PEAK_CEILING_DB=-1 # Required. Applies to renders only. Sources are never deleted by any path. RENDER_RETENTION_DAYS=60 # Required. A render that would exceed this is refused before it starts. MAX_RENDERS_GB=50 # Optional · secret. Optional. Without it the local model is used and the interface says so. TRANSCRIPTION_API_KEY=sk-...redacted
# Swell AI · indie build A local podcast production tool whose hard problems are editing without ever rewriting the source recording, hitting a published loudness target measured on the file you actually ship, and generating notes that can be traced back to the words that were said. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI and an HTMX interface | a local web interface with no build step and no accounts | | Audio | ffmpeg for decode, filtering, loudness measurement and encode | the measurement and the encode have to come from the same toolchain to agree | | Storage | SQLite for metadata, a read-only media folder for sources, a renders folder for output | the source folder and the output folder are different folders for a reason | | Editing | An ordered edit list applied at render, never applied to the source | non-destructive is not a preference here, it is the point | | Transcription | A local model by default, an API key optional and degraded cleanly without one | an hour of audio through an API costs money and leaves the machine | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the interface and the processing. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with the loudness filter and the encoders you need** · free - Why: Decode, filtering, loudness measurement and encoding all come from here, and the measurement and the encode agreeing matters more than either alone. - Get it: Your package manager or a static build. - Verify: ffmpeg -filters lists the loudness normalisation filter and -encoders lists your target codec - [ ] **A local transcription model** · free, plus the disk and the time - Why: An hour of audio per episode through a hosted service is a recurring bill and a copy of every conversation leaving the machine. - Get it: Download a speech recognition model sized for your hardware and confirm it runs on a short clip. - Verify: A one minute clip transcribes with timings - [ ] **A transcription or text model API key** (optional) · per minute of audio or per thousand tokens; check the current rate - Why: Only if you want higher quality than the local model gives; every phase must work without it. - Get it: Create a key with the narrowest scope your provider offers. - Verify: A short clip transcribes through the API and the local path still works when the key is removed - [ ] **Three real recordings, including one difficult one** · free - Why: Loudness, chunk stitching and gating all behave differently on a quiet remote guest than on a clean studio track, and a clean fixture proves nothing. - Get it: One studio recording, one noisy remote call, and one already mastered episode. - Verify: All three import and render within the stated tolerance - [ ] **Your loudness target, true peak ceiling and channel count, with sources** · free - Why: The figures are asserted by tests and the published guidance changes, so the version you built against needs recording. - Get it: Read the current audio requirements of the directories you publish to and write down the figures and the date. - Verify: The README names the figures, the source and the date ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Remote recording. - Hosting, feeds and directory analytics. - Licensed music and voice cloning. - Automatic publishing of anything. - templates, integrations, hosted processing, and content history - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support If one of those is essential to you, that is the reason to keep paying for Swell AI, and the README should say so rather than pretend.
# Build brief · Swell AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local podcast post-production and show-notes tool that never touches your original recordings to replace Swell AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules. The original recording is read-only, permanently. Every operation produces a new file or an entry in an edit list, the source file's checksum is recorded at import and re-verified before every render, and no code path anywhere opens the source for writing. An interview you cannot re-record is the one file in this build that has no backup by definition. Second, generated text is a draft, and it is the host's name on the published version. Every generated sentence is derived from the transcript and carries the timestamps it came from, so it can be checked in seconds. The tool does not invent a guest's job title, a company name, a statistic or a quotation, and it does not publish anything without a person marking it approved. A show-notes tool that fabricates a guest's credentials has published a lie under somebody else's byline, and no amount of speed is worth that. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI and an HTMX interface · a local web interface with no build step and no accounts - Audio: ffmpeg for decode, filtering, loudness measurement and encode · the measurement and the encode have to come from the same toolchain to agree - Storage: SQLite for metadata, a read-only media folder for sources, a renders folder for output · the source folder and the output folder are different folders for a reason - Editing: An ordered edit list applied at render, never applied to the source · non-destructive is not a preference here, it is the point - Transcription: A local model by default, an API key optional and degraded cleanly without one · an hour of audio through an API costs money and leaves the machine ### Data model (create this before Phase 1) - episodes(id, title, source_path, source_sha256, duration_seconds, sample_rate, channels, imported_at) · source_path is inside the read-only folder - edits(episode_id, ordinal, kind, at_seconds, to_seconds, params_json) · kind is cut, fade, gain, gate or marker, and applying them produces a new file - renders(id, episode_id, edit_sha256, target_lufs, measured_lufs, true_peak_db, format, bitrate, channels, path, rendered_at) - transcripts(id, episode_id, engine, engine_version, language, created_at) - segments(transcript_id, ordinal, start_seconds, end_seconds, speaker, text, confidence) · timings survive a manual correction - chapters(episode_id, start_seconds, title, origin) · origin is manual or derived, and derived ones carry the segments they came from - drafts(id, episode_id, kind, engine, body, citations_json, approved_at, approved_by) · citations_json maps each passage to segment ids ### Phase 1 · Originals that cannot be damaged Build: Copy or hard link the source into a media folder, record its checksum and its real format from the decoder rather than from the extension, and mark the folder read-only. Build a peaks file for waveform display so the interface never decodes the whole recording again. Represent every edit as a row in the edit list. Re-verify the source checksum before each render and refuse if it changed. Trap: Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. Done when: The source file's checksum after a full edit and render session is identical to the one recorded at import, asserted in the suite. No code path opens a file under the media folder for writing, verified by review and by a test that makes the folder read-only and runs a full render. The format, sample rate and channel count come from the decoder, not the file extension, tested with a mislabelled file. A render refuses to start if the source checksum no longer matches. Do not build yet: No transcription, no loudness work, no generated text, no export. ### Phase 2 · A transcript with timings you can click Build: Transcribe to segments with start and end times, a speaker label where the engine offers one, and a confidence score. Split long recordings into overlapping chunks and stitch them so a sentence across a boundary is not duplicated or lost. Let a person correct text without destroying the timings, and record which engine and version produced each transcript. Trap: Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. Done when: A ninety minute recording transcribes with no duplicated or dropped words at chunk boundaries, asserted against a fixture with known text at the boundary positions. Correcting a segment's text leaves its start and end times unchanged. Every transcript records its engine and version, so a later re-run is comparable. With no API key configured the tool uses the local engine and says which one, rather than failing. Do not build yet: No loudness processing, no generated notes, no export. ### Phase 3 · Loudness measured on the file you ship Build: Normalise towards a published target and verify the result. Apple Podcasts states an overall loudness around minus sixteen LKFS with a tolerance of plus or minus one decibel and a true peak not exceeding minus one decibel relative to full scale, and recommends sixty four to one hundred and twenty eight kilobits per second for mono and one hundred and twenty eight to two hundred and fifty six for stereo at forty four point one or forty eight kilohertz for feed audio. Check the current page yourself, then measure the encoded output and store the measured figures. Trap: Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. Done when: The measured loudness of the encoded output is within the stated tolerance of the target, asserted for at least three fixtures including one very quiet and one already loud. The measured true peak of the encoded output does not exceed the stated ceiling. Every render stores its target, its measured loudness, its true peak, its channel count and its bit rate. The interface shows the measured figures next to the target rather than only claiming success. Do not build yet: No generated notes, no chapters export, no publishing. ### Phase 4 · Notes that point at what was actually said Build: Generate a title, a description, chapter suggestions and social excerpts from the transcript only, with each passage carrying the segment identifiers it drew on. Show the draft beside its citations so each claim is one click from the audio. Require an explicit approval per draft before export, and keep every draft with the engine that produced it. Work without any model configured by falling back to extraction from the transcript. Trap: A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. Done when: Every generated passage carries at least one segment citation and the interface shows the cited audio range, asserted in the suite. Exporting a draft that has not been approved is refused. With no model configured, the tool still produces chapter suggestions and excerpts by extraction and labels them as extracted. A draft is stored with its engine, so a regeneration can be compared against the previous one. Do not build yet: No publishing, feed generation or analytics. ### Phase 5 · Exporting an episode and living with the disk Build: Export the encoded audio, the transcript in a caption format with timings, chapters as JSON, the approved notes as Markdown, and a manifest naming every file with its checksum and the measured audio figures. Write chapter markers into the audio file where the format supports it. Delete old renders after a stated retention, never sources. Cap disk use and refuse a render that would exceed it, with the figure shown. Trap: Keeping every render of every episode fills a disk in a season, and the cleanup script written in a hurry then walks the media folder as well and removes the masters. Give renders a retention and a ceiling, make the delete path physically incapable of reaching the media folder, and put that in a test. Done when: An export produces audio, timed captions, chapters, notes and a manifest whose checksums match the files, verified by re-reading the manifest. Chapter markers written into the audio are read back by a separate tool. Retention deletes only files under the renders folder, asserted by a test that places a file in the media folder and confirms it survives. A render that would exceed the disk ceiling is refused with the current and configured figures shown. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time remote recording, which is a separate product with its own network, drift and failure problems and cannot be bolted on. - Podcast hosting, feed distribution and directory analytics, which are an operational commitment rather than a feature. - Licensed music beds and voice cloning, which are licensing and consent questions long before they are engineering ones. - Publishing anything automatically, since the approval step is the whole defence against a generated claim nobody checked. ### README must contain - That the media folder is read-only and every source checksum is verified before each render - That edits are a list applied at render time, so the original is never rewritten - Which loudness and true peak targets were used, the source they came from and the date it was read - That loudness is measured on the encoded output and the measured figures are stored per render - That generated text carries transcript citations and cannot be exported without approval
# Agent instructions · Swell AI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with FastAPI and an HTMX interface, ffmpeg for decode, filtering, loudness measurement and encode, SQLite for metadata, a read-only media folder for sources, a renders folder for output, An ordered edit list applied at render, never applied to the source, A local model by default, an API key optional and degraded cleanly without one. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. - Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. - Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. - A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust.
# Build plan · Swell AI A local podcast production tool whose hard problems are editing without ever rewriting the source recording, hitting a published loudness target measured on the file you actually ship, and generating notes that can be traced back to the words that were said. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Originals that cannot be damaged Importing a recording makes it read-only and every later operation is additive. ### Steps 1. Import into a read-only media folder The filesystem is a better guarantee than a code review. Files: `app/media/import.py`, `tests/test_source_untouched.py` 2. Keep edits as rows, applied only at render An edit list can be undone; an overwritten file cannot. Files: `app/edits/model.py` 3. Verify the checksum before every render It catches the one path that got it wrong, whenever that appears. Files: `app/render/guard.py` ### Done when - [ ] Source checksums are unchanged after a full session - [ ] No write path exists into the media folder - [ ] Format comes from the decoder, not the extension - [ ] A changed source refuses to render ### Watch out - Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. ## Phase 2 · A transcript with timings you can click Every word has a time, so notes and chapters can point back at the audio. ### Steps 1. Chunk with overlap and stitch by matching Fixed boundaries with no overlap duplicate or lose a sentence every time. Files: `app/transcribe/chunk.py`, `tests/test_stitch.py` 2. Keep timings through manual corrections The timings are what make every later feature possible. Files: `app/transcribe/edit.py` 3. Record the engine and version per transcript Two transcripts of one episode are only comparable if you know what made them. Files: `app/transcribe/model.py` ### Done when - [ ] No duplicated or dropped words at chunk boundaries - [ ] Corrections preserve segment timings - [ ] Engine and version are recorded per transcript - [ ] No API key degrades to the local engine with a notice ### Watch out - Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. ## Phase 3 · Loudness measured on the file you ship The exported episode meets a published target, verified after encoding rather than before. ### Steps 1. Measure in two passes, then encode, then measure again The encoded file is the only file anyone will hear. Files: `app/render/loudness.py`, `tests/test_loudness.py` 2. Store measured loudness and true peak per render A claim without a measurement is a claim. Files: `app/render/model.py` 3. Cite the target's source and the date it was read The published figures change, and the README is where the next person looks. Files: `docs/audio-targets.md` ### Done when - [ ] Encoded output is within tolerance of the target - [ ] Encoded true peak stays under the ceiling - [ ] Renders store measured figures, channels and bit rate - [ ] The target cites its source and the date read ### Watch out - Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. ## Phase 4 · Notes that point at what was actually said Every generated passage can be traced to timestamps in the transcript and nothing ships unread. ### Steps 1. Cite segments for every generated passage It is the only cheap defence against a confident invention. Files: `app/drafts/generate.py`, `tests/test_citations.py` 2. Refuse to export an unapproved draft The approval is the point at which a person takes responsibility. Files: `app/export/gate.py` 3. Fall back to extraction with no model A tool that needs a key to be useful is a tool with a monthly bill. Files: `app/drafts/extract.py` ### Done when - [ ] Every passage carries a segment citation - [ ] Unapproved drafts cannot be exported - [ ] No model configured still yields labelled extraction - [ ] Drafts record the engine that produced them ### Watch out - A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. ## Not in this build - Remote recording. - Hosting, feeds and directory analytics. - Licensed music and voice cloning. - Automatic publishing of anything. ## After v1, if you want it - A second render profile for a different directory's published figures, once one has shipped a season - Speaker naming that persists across episodes, built on the existing segment labels - A diff between two transcripts of the same episode, which the recorded engine versions already make meaningful
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Read-only sources. Nothing in the application writes here. MEDIA_DIR=./media # Required. Everything the tool produces. The only folder retention can reach. RENDERS_DIR=./renders # Required. Episodes, edits, transcripts, renders and drafts. DATABASE_PATH=./data/podcast.db # Required. Taken from the directory's published requirements, with the date recorded. TARGET_LUFS=-16 # Required. Measured on the encoded file, not on the intermediate. TRUE_PEAK_CEILING_DB=-1 # Required. Applies to renders only. Sources are never deleted by any path. RENDER_RETENTION_DAYS=60 # Required. A render that would exceed this is refused before it starts. MAX_RENDERS_GB=50 # Optional · secret. Optional. Without it the local model is used and the interface says so. TRANSCRIPTION_API_KEY=sk-...redacted
# Swell AI · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Swell AI, generate show notes, articles, social posts, and transcripts from uploaded episodes. The hard boundary is templates, integrations, hosted processing, and content history, plus audio infrastructure, distribution, and production polish. ## Product outcome A production tool that physically cannot damage the recording you can never make again, ships audio whose loudness you measured on the file itself, and produces show notes every sentence of which can be traced to the moment it was said. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - ffmpeg - local audio files - optional transcription API key - sufficient disk space ## Explicit non-goals for v1 - Remote recording. - Hosting, feeds and directory analytics. - Licensed music and voice cloning. - Automatic publishing of anything. - templates, integrations, hosted processing, and content history - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support ## Success criteria - Source checksums are confirmed unchanged after a full session - No write path into the media folder is confirmed to exist - Encoded loudness and true peak are confirmed within target - Every generated passage is confirmed to carry a citation - Retention is confirmed unable to reach the media folder
# Build brief · Swell AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local podcast post-production and show-notes tool that never touches your original recordings to replace Swell AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules. The original recording is read-only, permanently. Every operation produces a new file or an entry in an edit list, the source file's checksum is recorded at import and re-verified before every render, and no code path anywhere opens the source for writing. An interview you cannot re-record is the one file in this build that has no backup by definition. Second, generated text is a draft, and it is the host's name on the published version. Every generated sentence is derived from the transcript and carries the timestamps it came from, so it can be checked in seconds. The tool does not invent a guest's job title, a company name, a statistic or a quotation, and it does not publish anything without a person marking it approved. A show-notes tool that fabricates a guest's credentials has published a lie under somebody else's byline, and no amount of speed is worth that. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI and an HTMX interface · a local web interface with no build step and no accounts - Audio: ffmpeg for decode, filtering, loudness measurement and encode · the measurement and the encode have to come from the same toolchain to agree - Storage: SQLite for metadata, a read-only media folder for sources, a renders folder for output · the source folder and the output folder are different folders for a reason - Editing: An ordered edit list applied at render, never applied to the source · non-destructive is not a preference here, it is the point - Transcription: A local model by default, an API key optional and degraded cleanly without one · an hour of audio through an API costs money and leaves the machine ### Data model (create this before Phase 1) - episodes(id, title, source_path, source_sha256, duration_seconds, sample_rate, channels, imported_at) · source_path is inside the read-only folder - edits(episode_id, ordinal, kind, at_seconds, to_seconds, params_json) · kind is cut, fade, gain, gate or marker, and applying them produces a new file - renders(id, episode_id, edit_sha256, target_lufs, measured_lufs, true_peak_db, format, bitrate, channels, path, rendered_at) - transcripts(id, episode_id, engine, engine_version, language, created_at) - segments(transcript_id, ordinal, start_seconds, end_seconds, speaker, text, confidence) · timings survive a manual correction - chapters(episode_id, start_seconds, title, origin) · origin is manual or derived, and derived ones carry the segments they came from - drafts(id, episode_id, kind, engine, body, citations_json, approved_at, approved_by) · citations_json maps each passage to segment ids ### Phase 1 · Originals that cannot be damaged Build: Copy or hard link the source into a media folder, record its checksum and its real format from the decoder rather than from the extension, and mark the folder read-only. Build a peaks file for waveform display so the interface never decodes the whole recording again. Represent every edit as a row in the edit list. Re-verify the source checksum before each render and refuse if it changed. Trap: Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. Done when: The source file's checksum after a full edit and render session is identical to the one recorded at import, asserted in the suite. No code path opens a file under the media folder for writing, verified by review and by a test that makes the folder read-only and runs a full render. The format, sample rate and channel count come from the decoder, not the file extension, tested with a mislabelled file. A render refuses to start if the source checksum no longer matches. Do not build yet: No transcription, no loudness work, no generated text, no export. ### Phase 2 · A transcript with timings you can click Build: Transcribe to segments with start and end times, a speaker label where the engine offers one, and a confidence score. Split long recordings into overlapping chunks and stitch them so a sentence across a boundary is not duplicated or lost. Let a person correct text without destroying the timings, and record which engine and version produced each transcript. Trap: Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. Done when: A ninety minute recording transcribes with no duplicated or dropped words at chunk boundaries, asserted against a fixture with known text at the boundary positions. Correcting a segment's text leaves its start and end times unchanged. Every transcript records its engine and version, so a later re-run is comparable. With no API key configured the tool uses the local engine and says which one, rather than failing. Do not build yet: No loudness processing, no generated notes, no export. ### Phase 3 · Loudness measured on the file you ship Build: Normalise towards a published target and verify the result. Apple Podcasts states an overall loudness around minus sixteen LKFS with a tolerance of plus or minus one decibel and a true peak not exceeding minus one decibel relative to full scale, and recommends sixty four to one hundred and twenty eight kilobits per second for mono and one hundred and twenty eight to two hundred and fifty six for stereo at forty four point one or forty eight kilohertz for feed audio. Check the current page yourself, then measure the encoded output and store the measured figures. Trap: Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. Done when: The measured loudness of the encoded output is within the stated tolerance of the target, asserted for at least three fixtures including one very quiet and one already loud. The measured true peak of the encoded output does not exceed the stated ceiling. Every render stores its target, its measured loudness, its true peak, its channel count and its bit rate. The interface shows the measured figures next to the target rather than only claiming success. Do not build yet: No generated notes, no chapters export, no publishing. ### Phase 4 · Notes that point at what was actually said Build: Generate a title, a description, chapter suggestions and social excerpts from the transcript only, with each passage carrying the segment identifiers it drew on. Show the draft beside its citations so each claim is one click from the audio. Require an explicit approval per draft before export, and keep every draft with the engine that produced it. Work without any model configured by falling back to extraction from the transcript. Trap: A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. Done when: Every generated passage carries at least one segment citation and the interface shows the cited audio range, asserted in the suite. Exporting a draft that has not been approved is refused. With no model configured, the tool still produces chapter suggestions and excerpts by extraction and labels them as extracted. A draft is stored with its engine, so a regeneration can be compared against the previous one. Do not build yet: No publishing, feed generation or analytics. ### Phase 5 · Exporting an episode and living with the disk Build: Export the encoded audio, the transcript in a caption format with timings, chapters as JSON, the approved notes as Markdown, and a manifest naming every file with its checksum and the measured audio figures. Write chapter markers into the audio file where the format supports it. Delete old renders after a stated retention, never sources. Cap disk use and refuse a render that would exceed it, with the figure shown. Trap: Keeping every render of every episode fills a disk in a season, and the cleanup script written in a hurry then walks the media folder as well and removes the masters. Give renders a retention and a ceiling, make the delete path physically incapable of reaching the media folder, and put that in a test. Done when: An export produces audio, timed captions, chapters, notes and a manifest whose checksums match the files, verified by re-reading the manifest. Chapter markers written into the audio are read back by a separate tool. Retention deletes only files under the renders folder, asserted by a test that places a file in the media folder and confirms it survives. A render that would exceed the disk ceiling is refused with the current and configured figures shown. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time remote recording, which is a separate product with its own network, drift and failure problems and cannot be bolted on. - Podcast hosting, feed distribution and directory analytics, which are an operational commitment rather than a feature. - Licensed music beds and voice cloning, which are licensing and consent questions long before they are engineering ones. - Publishing anything automatically, since the approval step is the whole defence against a generated claim nobody checked. ### README must contain - That the media folder is read-only and every source checksum is verified before each render - That edits are a list applied at render time, so the original is never rewritten - Which loudness and true peak targets were used, the source they came from and the date it was read - That loudness is measured on the encoded output and the measured figures are stored per render - That generated text carries transcript citations and cannot be exported without approval
# Architecture · Swell AI ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI and an HTMX interface | a local web interface with no build step and no accounts | | Audio | ffmpeg for decode, filtering, loudness measurement and encode | the measurement and the encode have to come from the same toolchain to agree | | Storage | SQLite for metadata, a read-only media folder for sources, a renders folder for output | the source folder and the output folder are different folders for a reason | | Editing | An ordered edit list applied at render, never applied to the source | non-destructive is not a preference here, it is the point | | Transcription | A local model by default, an API key optional and degraded cleanly without one | an hour of audio through an API costs money and leaves the machine | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Read-only sources, checksums and decoded format. | Any storage layout, provided sources stay unwritable. | | Edits | The ordered edit list applied at render. | Not swappable for in-place processing, which is the whole risk. | | Render | Two pass loudness, encoding and measured output. | Any toolchain, provided the measurement happens after the encode. | | Transcript | Overlapped chunking, stitching and timings. | Any engine, provided timings survive manual correction. | | Drafts | Citations, approval and the extraction fallback. | Any model, provided uncited passages are still refused. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `MEDIA_DIR` · required · Read-only sources. Nothing in the application writes here. - `RENDERS_DIR` · required · Everything the tool produces. The only folder retention can reach. - `DATABASE_PATH` · required · Episodes, edits, transcripts, renders and drafts. - `TARGET_LUFS` · required · Taken from the directory's published requirements, with the date recorded. - `TRUE_PEAK_CEILING_DB` · required · Measured on the encoded file, not on the intermediate. - `RENDER_RETENTION_DAYS` · required · Applies to renders only. Sources are never deleted by any path. - `MAX_RENDERS_GB` · required · A render that would exceed this is refused before it starts. - `TRANSCRIPTION_API_KEY` · optional, secret · Optional. Without it the local model is used and the interface says so. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Swell AI product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with FastAPI and an HTMX interface, ffmpeg for decode, filtering, loudness measurement and encode, SQLite for metadata, a read-only media folder for sources, a renders folder for output, An ordered edit list applied at render, never applied to the source, A local model by default, an API key optional and degraded cleanly without one. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. - Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. - Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. - A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. - Keeping every render of every episode fills a disk in a season, and the cleanup script written in a hurry then walks the media folder as well and removes the masters. Give renders a retention and a ceiling, make the delete path physically incapable of reaching the media folder, and put that in a test.
# Delivery milestones · Swell AI Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Originals that cannot be damaged Importing a recording makes it read-only and every later operation is additive. ### Steps 1. Import into a read-only media folder The filesystem is a better guarantee than a code review. Files: `app/media/import.py`, `tests/test_source_untouched.py` 2. Keep edits as rows, applied only at render An edit list can be undone; an overwritten file cannot. Files: `app/edits/model.py` 3. Verify the checksum before every render It catches the one path that got it wrong, whenever that appears. Files: `app/render/guard.py` ### Done when - [ ] Source checksums are unchanged after a full session - [ ] No write path exists into the media folder - [ ] Format comes from the decoder, not the extension - [ ] A changed source refuses to render ### Watch out - Normalising or trimming in place because it is one ffmpeg command overwrites the only copy of a two hour interview, and the failure is silent until somebody asks for the unedited version. Import into a folder nothing writes to, keep edits as data, and make the render read the source and write elsewhere. ## M2 · A transcript with timings you can click Every word has a time, so notes and chapters can point back at the audio. ### Steps 1. Chunk with overlap and stitch by matching Fixed boundaries with no overlap duplicate or lose a sentence every time. Files: `app/transcribe/chunk.py`, `tests/test_stitch.py` 2. Keep timings through manual corrections The timings are what make every later feature possible. Files: `app/transcribe/edit.py` 3. Record the engine and version per transcript Two transcripts of one episode are only comparable if you know what made them. Files: `app/transcribe/model.py` ### Done when - [ ] No duplicated or dropped words at chunk boundaries - [ ] Corrections preserve segment timings - [ ] Engine and version are recorded per transcript - [ ] No API key degrades to the local engine with a notice ### Watch out - Chunking an hour of audio at fixed boundaries with no overlap cuts sentences in half, and stitching by concatenation then duplicates the words either side of every boundary, which the person only notices when the show notes quote a phrase twice. Overlap the chunks, match the repeated region when stitching, and test with a recording long enough to need several. ## M3 · Loudness measured on the file you ship The exported episode meets a published target, verified after encoding rather than before. ### Steps 1. Measure in two passes, then encode, then measure again The encoded file is the only file anyone will hear. Files: `app/render/loudness.py`, `tests/test_loudness.py` 2. Store measured loudness and true peak per render A claim without a measurement is a claim. Files: `app/render/model.py` 3. Cite the target's source and the date it was read The published figures change, and the README is where the next person looks. Files: `docs/audio-targets.md` ### Done when - [ ] Encoded output is within tolerance of the target - [ ] Encoded true peak stays under the ceiling - [ ] Renders store measured figures, channels and bit rate - [ ] The target cites its source and the date read ### Watch out - Measuring loudness on the normalised audio and then encoding to a lossy format ships a file whose true peak is above the ceiling, because the encoder reconstructs a waveform that overshoots the samples it was given; a single loudness pass also gets the target wrong because it cannot see the whole file. Use a two pass measurement, encode, then measure the encoded file and store what you measured. ## M4 · Notes that point at what was actually said Every generated passage can be traced to timestamps in the transcript and nothing ships unread. ### Steps 1. Cite segments for every generated passage It is the only cheap defence against a confident invention. Files: `app/drafts/generate.py`, `tests/test_citations.py` 2. Refuse to export an unapproved draft The approval is the point at which a person takes responsibility. Files: `app/export/gate.py` 3. Fall back to extraction with no model A tool that needs a key to be useful is a tool with a monthly bill. Files: `app/drafts/extract.py` ### Done when - [ ] Every passage carries a segment citation - [ ] Unapproved drafts cannot be exported - [ ] No model configured still yields labelled extraction - [ ] Drafts record the engine that produced them ### Watch out - A model given only a rough transcript will confidently supply a guest's employer, a round number and a neat closing quotation, none of which were said, and the host publishes it because it reads well. Require citations per passage, show them beside the draft, refuse to export an unapproved draft, and treat anything with no citation as something to delete rather than to trust. ## M5 · Exporting an episode and living with the disk (production only) A finished episode leaves as a complete set of files, and renders do not quietly fill the drive. ### Steps 1. Export a manifest with checksums and measured figures It is what makes an episode reproducible six months later. Files: `app/export/manifest.py` 2. Confine deletion to the renders folder A cleanup script that can reach the masters will eventually reach them. Files: `app/retention.py`, `tests/test_retention_scope.py` 3. Refuse a render over the disk ceiling Failing before the render beats failing during it. Files: `app/render/limits.py` ### Done when - [ ] The manifest's checksums match the exported files - [ ] Chapter markers read back in another tool - [ ] Retention cannot reach the media folder - [ ] A render over the ceiling is refused with figures ### Watch out - Keeping every render of every episode fills a disk in a season, and the cleanup script written in a hurry then walks the media folder as well and removes the masters. Give renders a retention and a ceiling, make the delete path physically incapable of reaching the media folder, and put that in a test.
# Operations · Swell AI ## Backup The media folder first, because those recordings are irreplaceable and everything else can be regenerated from them plus the database. Back up the database with it so the edit lists and approvals survive; renders are the one thing worth leaving out. ## Restore Restore media and database together, then re-verify every source checksum before re-rendering anything. A source whose checksum no longer matches is reported rather than quietly used, because the edit list was written against the original. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks are at startup and in the suite: sources whose checksum has changed, renders whose measured loudness sits outside tolerance, the size of the renders folder against its ceiling, and drafts exported without approval, which should be zero. ## Incident checklist A damaged source is the incident this build exists to prevent, and the checksum at import is what detects it. An episode that sounds wrong is diagnosed from the stored render figures before anything is re-rendered, because those figures record what actually shipped. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Source checksums are confirmed unchanged after a full session - [ ] No write path into the media folder is confirmed to exist - [ ] Encoded loudness and true peak are confirmed within target - [ ] Every generated passage is confirmed to carry a citation - [ ] Retention is confirmed unable to reach the media folder ## Launch constraint Do not market omitted Swell AI capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Read-only sources. Nothing in the application writes here. MEDIA_DIR=./media # Required. Everything the tool produces. The only folder retention can reach. RENDERS_DIR=./renders # Required. Episodes, edits, transcripts, renders and drafts. DATABASE_PATH=./data/podcast.db # Required. Taken from the directory's published requirements, with the date recorded. TARGET_LUFS=-16 # Required. Measured on the encoded file, not on the intermediate. TRUE_PEAK_CEILING_DB=-1 # Required. Applies to renders only. Sources are never deleted by any path. RENDER_RETENTION_DAYS=60 # Required. A render that would exceed this is refused before it starts. MAX_RENDERS_GB=50 # Optional · secret. Optional. Without it the local model is used and the interface says so. TRANSCRIPTION_API_KEY=sk-...redacted
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Swell AI because creators pay to remove fragile audio plumbing and publishing chores from a release schedule. The recurring cost buys codec support, loudness standards, transcription, storage, feeds, analytics, and deliverability to directories, not just the visible interface.
xtemplates, integrations, hosted processing, and content history
xremote studio reliability
xlicensed music libraries
xhosting distribution
xadvanced mastering and support
Don't feel like building it? These folks already made it free.
SSpeakrPodcast repurposing with your own model endpoints instead of another monthly invoice.open source↗no votes, no pay-to-list · just what's real
Vibecode Swell AI
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Swell AI replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Swell AI cost?
Swell AI costs about $17/month (Hobby, checked 2026-07-31), which is $204 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Swell AI?
Honestly: templates, integrations, hosted processing, and content history; remote studio reliability; licensed music libraries; hosting distribution; advanced mastering and support. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Swell AI?
Yes: Speakr (Podcast repurposing with your own model endpoints instead of another monthly invoice.) The prompt is for when you want it exactly your way.