Vibecode Castmagic
track this build5 phases, 15 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Castmagic, turn a recording into a transcript, summary, clips, quotes, and reusable content assets. The hard boundary is large prompt library, integrations, team workflow, and hosted processing, plus audio infrastructure, distribution, and production polish.
You are building a lean indie version of Castmagic. 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 ===== # Castmagic · indie build Import an episode, clean the audio, transcribe with word timestamps and speakers, generate show notes with real timestamps, find clip candidates from the transcript, and export the audio, the transcript, the notes and the clips as production files, with every generated claim traceable to a moment in the recording. 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 driving ffmpeg and a transcriber | the work is orchestration over media tools | | Audio | ffmpeg for cleanup and clip extraction | loudness normalisation and cutting are both solved | | Transcription | whisper.cpp with word-level timestamps and speaker labels | timestamps are what let every generated artefact point back at the recording | | Generation | A model applied to the transcript, never to the audio | text is cheap to process and the transcript is already the artefact you trust | | Storage | Source audio untouched, outputs and a manifest alongside | everything is regenerable from the source plus settings | ## 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 pipeline. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with loudnorm and afftdn** · free - Why: Cleanup, measurement and clip extraction all run through it. - Get it: brew install ffmpeg or your package manager. Confirm the filters exist rather than assuming. - Verify: ffmpeg -filters lists loudnorm and afftdn - [ ] **whisper.cpp with a model supporting word timestamps** · free, but transcription takes real time on your machine - Why: Word timestamps are the spine of chapters, clips and citations. Segment-level output cannot support any of them. - Get it: Build whisper.cpp and download a model. Confirm word-level output before building on it. - Verify: Output contains per-word start and end times - [ ] **A model API key, or a local model** · pay per token; an hour-long transcript summarised is typically a few cents - Why: Phase 3 onward. Cleanup and transcription work with no model at all. - Get it: A provider key with a spend limit, or Ollama for a local model with lower quality output. - Verify: A generated note returns with valid citations - [ ] **Three real episodes, one with two speakers** · free - Why: Speaker labelling, chapter detection and clip scoring all need real conversation. A monologue hides most of the difficulty. - Get it: Use your own recordings. At least one over forty minutes with two voices. - Verify: Three episodes are available with one two-speaker recording over forty minutes ## Quick start ```sh ffmpeg -i episode.wav -af loudnorm=print_format=json -f null - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The accumulated prompt library of the commercial product. - Publishing integrations to hosts and social platforms. - Team review and approval workflow. - Hosted processing that runs while your machine sleeps. - large prompt library, integrations, team workflow, and hosted processing - 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 Castmagic, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Castmagic 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 pipeline that turns a recorded episode into a transcript, show notes, clips and quotes to replace Castmagic. 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. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 driving ffmpeg and a transcriber · the work is orchestration over media tools - Audio: ffmpeg for cleanup and clip extraction · loudness normalisation and cutting are both solved - Transcription: whisper.cpp with word-level timestamps and speaker labels · timestamps are what let every generated artefact point back at the recording - Generation: A model applied to the transcript, never to the audio · text is cheap to process and the transcript is already the artefact you trust - Storage: Source audio untouched, outputs and a manifest alongside · everything is regenerable from the source plus settings ### Data model (create this before Phase 1) - episodes(id, source_path, title, duration_ms, imported_at, content_hash) - words(episode_id, ordinal, text, start_ms, end_ms, speaker) · the spine everything else references - segments(id, episode_id, start_ms, end_ms, speaker, text) · sentence-level, derived from words - chapters(id, episode_id, start_ms, title, source) · source is silence, transcript or manual - notes(id, episode_id, kind, body, cited_segment_ids_json, generated_at) · kind is summary, show_notes or description - clips(id, episode_id, start_ms, end_ms, reason, score, exported_path, approved) - Every generated artefact records the segment identifiers it drew from, so a claim in the show notes can be played back. ### Phase 1 · Import and clean, without touching the source Build: Import an episode, measure loudness, normalise to a stated target with a true peak ceiling using a two pass process, and apply gentle noise reduction. Write outputs alongside and record the before and after figures. Trap: Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. Done when: A processed episode measures within tolerance of the configured target and its loudness range is close to the source's rather than much smaller. The source file checksum is unchanged. Before and after figures are recorded per episode. Do not build yet: No transcript, notes or clips. ### Phase 2 · Transcript with timestamps and speakers Build: Transcribe with word-level timestamps. Assign speaker labels, correctable by hand, since automatic assignment will be imperfect. Build sentence segments from words. Verify by playing a single word and hearing that word. Trap: Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. Done when: Playing one word's range plays that word cleanly. Word timestamps are monotonic with no overlaps. Speaker labels are assigned and correctable, and a correction persists. Sentence segments align with audible sentence boundaries. Do not build yet: No generation yet. ### Phase 3 · Show notes that cite the recording Build: Generate a summary, show notes and a description from the transcript. Require every claim to cite segment identifiers. Validate that every cited identifier exists, and reject the generation if one does not. Trap: A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. Done when: Generated notes cite segment identifiers and each resolves to real text. A crafted output citing a non-existent segment fails validation, tested by feeding one to the validator directly. Clicking a citation plays that moment. A transcript with nothing to summarise produces a short honest output rather than invention. Do not build yet: No clips or chapters yet. ### Phase 4 · Chapters and clip candidates Build: Detect chapter boundaries from long silences and topic shifts in the transcript. Propose clip candidates scored on self-contained meaning and length. Snap every boundary to a word boundary with a small pad, and require approval before export. Trap: A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. Done when: Chapter boundaries land at real topic or silence boundaries rather than mid sentence. Clip candidates start and end at word boundaries with padding, verified by listening to three. No clip is exported without approval. A candidate can be nudged at either end and re-previewed. Do not build yet: No export bundle yet. ### Phase 5 · The export bundle Build: Export the processed audio, the transcript in a subtitle format and as plain text, the show notes as Markdown, chapter markers in a format your host accepts, and approved clips as separate files. Write a manifest listing everything and the settings used. Trap: Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. Done when: A single export produces audio, transcript, notes, chapters and approved clips. Chapter timings match the audio when loaded in a player. The manifest records the settings and the source hash. Re-running the export with unchanged settings produces identical files. Do not build yet: Publishing and repurposing come last. ### Phase 6 · Repurposing, batching and the honest limits Build: Batch process a directory of episodes. Generate social posts and a newsletter section from the notes, with the same citation validation. Then review a sample honestly and record what needed editing before it was publishable. Trap: Generated social copy reads fluently and is wrong often enough that publishing it unread will eventually misquote a guest. Sample and record the edit rate rather than assuming, because that number tells you whether this saves time or moves the work. Done when: A directory of episodes processes unattended with per-episode failures reported and not fatal. Generated social copy carries citations validated the same way. A sample of ten generated outputs has been reviewed and the proportion needing edits is recorded. Cost per episode is reported. Do not build yet: Nothing after this. ### Out of scope (and why) - The prompt library behind the commercial product, which is accumulated tuning across many shows and is most of what the subscription buys. - Integrations that publish directly to hosts and social platforms, each of which is an API and a maintenance burden. - Team workflow with review and approval across several people. - Hosted processing, so an hour of audio takes an hour of your machine's time rather than someone else's. ### README must contain - That the source audio is never modified and every output is regenerable from it plus settings - That normalisation is two pass with linear correction, and the target used - That word-level timestamps are required and how to verify them - That every generated claim cites a segment and citations are validated - The measured edit rate on generated copy, so nobody publishes it unread ===== AGENTS.md ===== # Agent instructions · Castmagic indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 driving ffmpeg and a transcriber, ffmpeg for cleanup and clip extraction, whisper.cpp with word-level timestamps and speaker labels, A model applied to the transcript, never to the audio, Source audio untouched, outputs and a manifest alongside. 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 - Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. - Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. - A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. - A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. - Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. ===== BUILD_PLAN.md ===== # Build plan · Castmagic Import an episode, clean the audio, transcribe with word timestamps and speakers, generate show notes with real timestamps, find clip candidates from the transcript, and export the audio, the transcript, the notes and the clips as production files, with every generated claim traceable to a moment in the recording. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Import and clean, without touching the source An episode is loudness-normalised and ready, and the original is untouched. ### Steps 1. Measure loudness first Analysis pass parsed from the filter's structured output rather than its human readable text. Files: `src/measure.py` ```sh ffmpeg -i episode.wav -af loudnorm=print_format=json -f null - ``` 2. Apply with linear correction Feed the measured values back in. Around -16 LUFS with a -1.5 dBTP ceiling is a common spoken word target; state yours. Files: `src/normalise.py` 3. Never modify the source Outputs alongside, checksum the source before and after as a test. Files: `tests/test_source_untouched.py` ### Done when - [ ] The output measures within tolerance of the target - [ ] Loudness range is preserved rather than compressed - [ ] The source checksum is unchanged - [ ] Before and after figures are recorded ### Watch out - Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. ## Phase 2 · Transcript with timestamps and speakers Every word knows when it was said and, where there are two voices, who said it. ### Steps 1. Convert for the transcriber Mono 16 kHz 16 bit from the source, not from the normalised output. ```sh ffmpeg -i episode.wav -ar 16000 -ac 1 -c:a pcm_s16le for-transcription.wav ``` 2. Request word-level output Explicitly. Verify by playing a single word before building anything on it. Files: `src/transcribe.py` 3. Label speakers and allow correction Automatic assignment is a starting point. Corrections must persist and propagate to segments. Files: `src/speakers.py` ### Done when - [ ] A single word's range plays that word - [ ] Word timestamps are monotonic - [ ] Speaker corrections persist - [ ] Segments align with audible sentence boundaries ### Watch out - Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. ## Phase 3 · Show notes that cite the recording Every claim in the generated notes points at a moment you can play. ### Steps 1. Pass numbered segments and demand citations The model sees numbered segments and must cite them. Request structured output so parsing is not a regular expression. Files: `src/notes.py` 2. Validate every citation A pure function from output plus segment set to a verdict. Unit test it with a crafted bad citation. Files: `src/validate.py`, `tests/test_validate.py` 3. Make citations playable Clicking one seeks the audio. This is also how you check the notes before publishing. Files: `src/review.py` ### Done when - [ ] Every citation resolves to real segment text - [ ] A crafted bad citation is rejected by the validator - [ ] Clicking a citation plays that moment - [ ] A thin transcript produces an honest short output ### Watch out - A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. ## Phase 4 · Chapters and clip candidates The episode is chaptered, and the moments worth clipping are surfaced rather than hunted for. ### Steps 1. Detect chapters two ways Long silences from the audio and topic shifts from the transcript, reconciled rather than one overriding the other. Files: `src/chapters.py` ```sh ffmpeg -i episode.wav -af silencedetect=n=-35dB:d=1.5 -f null - ``` 2. Score clip candidates Self-contained meaning, length within bounds, single speaker where you want that. Score, then propose. Files: `src/clips.py` 3. Snap and pad boundaries To word boundaries, padded into surrounding silence. Listen to three before trusting the rule. Files: `src/clips.py` ### Done when - [ ] Chapter boundaries fall at real transitions - [ ] Clip boundaries are on word boundaries with padding - [ ] No clip exports without approval - [ ] A candidate can be nudged and re-previewed ### Watch out - A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. ## Phase 5 · The export bundle One action produces everything you need to publish the episode. ### Steps 1. Export each artefact Audio, subtitle file, plain transcript, Markdown notes, chapters, clips. Files: `src/export.py` 2. Target your host's chapter format Confirm what it accepts first. Record the choice in the manifest. Files: `docs/host.md` 3. Write a manifest Settings, source hash and every output. This is what makes the bundle reproducible. Files: `src/manifest.py` ### Done when - [ ] One export produces every artefact - [ ] Chapter timings match the audio in a player - [ ] The manifest records settings and source hash - [ ] Re-export with unchanged settings is byte-identical ### Watch out - Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. ## Not in this build - The accumulated prompt library of the commercial product. - Publishing integrations to hosts and social platforms. - Team review and approval workflow. - Hosted processing that runs while your machine sleeps. ## After v1, if you want it - A guest-facing page generated from the transcript and clips - Cross-episode search over your whole back catalogue - Automatic detection of a repeated segment such as an intro, so it is excluded from clips ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Loudness target for spoken word. State it rather than assuming a default. TARGET_LUFS=-16 # Required. True peak ceiling, leaving headroom so lossy encoding does not clip. TRUE_PEAK_DB=-1.5 # Required. The transcription model. Larger is more accurate and considerably slower. WHISPER_MODEL_PATH=./models/ggml-base.en.bin # Required. Shortest clip candidate. Below this a clip rarely carries a self-contained idea. CLIP_MIN_SECONDS=20 # Required. Longest clip candidate. CLIP_MAX_SECONDS=90 # Optional · secret. For generation. Leave unset to run the audio and transcript phases only. MODEL_API_KEY=sk-... # Required. Where processed audio, transcripts, notes and clips are written. Never the source directory. OUTPUT_DIR=./out
You are building a lean indie version of Castmagic. 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 ===== # Castmagic · indie build Import an episode, clean the audio, transcribe with word timestamps and speakers, generate show notes with real timestamps, find clip candidates from the transcript, and export the audio, the transcript, the notes and the clips as production files, with every generated claim traceable to a moment in the recording. 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 driving ffmpeg and a transcriber | the work is orchestration over media tools | | Audio | ffmpeg for cleanup and clip extraction | loudness normalisation and cutting are both solved | | Transcription | whisper.cpp with word-level timestamps and speaker labels | timestamps are what let every generated artefact point back at the recording | | Generation | A model applied to the transcript, never to the audio | text is cheap to process and the transcript is already the artefact you trust | | Storage | Source audio untouched, outputs and a manifest alongside | everything is regenerable from the source plus settings | ## 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 pipeline. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with loudnorm and afftdn** · free - Why: Cleanup, measurement and clip extraction all run through it. - Get it: brew install ffmpeg or your package manager. Confirm the filters exist rather than assuming. - Verify: ffmpeg -filters lists loudnorm and afftdn - [ ] **whisper.cpp with a model supporting word timestamps** · free, but transcription takes real time on your machine - Why: Word timestamps are the spine of chapters, clips and citations. Segment-level output cannot support any of them. - Get it: Build whisper.cpp and download a model. Confirm word-level output before building on it. - Verify: Output contains per-word start and end times - [ ] **A model API key, or a local model** · pay per token; an hour-long transcript summarised is typically a few cents - Why: Phase 3 onward. Cleanup and transcription work with no model at all. - Get it: A provider key with a spend limit, or Ollama for a local model with lower quality output. - Verify: A generated note returns with valid citations - [ ] **Three real episodes, one with two speakers** · free - Why: Speaker labelling, chapter detection and clip scoring all need real conversation. A monologue hides most of the difficulty. - Get it: Use your own recordings. At least one over forty minutes with two voices. - Verify: Three episodes are available with one two-speaker recording over forty minutes ## Quick start ```sh ffmpeg -i episode.wav -af loudnorm=print_format=json -f null - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The accumulated prompt library of the commercial product. - Publishing integrations to hosts and social platforms. - Team review and approval workflow. - Hosted processing that runs while your machine sleeps. - large prompt library, integrations, team workflow, and hosted processing - 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 Castmagic, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Castmagic 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 pipeline that turns a recorded episode into a transcript, show notes, clips and quotes to replace Castmagic. 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. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 driving ffmpeg and a transcriber · the work is orchestration over media tools - Audio: ffmpeg for cleanup and clip extraction · loudness normalisation and cutting are both solved - Transcription: whisper.cpp with word-level timestamps and speaker labels · timestamps are what let every generated artefact point back at the recording - Generation: A model applied to the transcript, never to the audio · text is cheap to process and the transcript is already the artefact you trust - Storage: Source audio untouched, outputs and a manifest alongside · everything is regenerable from the source plus settings ### Data model (create this before Phase 1) - episodes(id, source_path, title, duration_ms, imported_at, content_hash) - words(episode_id, ordinal, text, start_ms, end_ms, speaker) · the spine everything else references - segments(id, episode_id, start_ms, end_ms, speaker, text) · sentence-level, derived from words - chapters(id, episode_id, start_ms, title, source) · source is silence, transcript or manual - notes(id, episode_id, kind, body, cited_segment_ids_json, generated_at) · kind is summary, show_notes or description - clips(id, episode_id, start_ms, end_ms, reason, score, exported_path, approved) - Every generated artefact records the segment identifiers it drew from, so a claim in the show notes can be played back. ### Phase 1 · Import and clean, without touching the source Build: Import an episode, measure loudness, normalise to a stated target with a true peak ceiling using a two pass process, and apply gentle noise reduction. Write outputs alongside and record the before and after figures. Trap: Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. Done when: A processed episode measures within tolerance of the configured target and its loudness range is close to the source's rather than much smaller. The source file checksum is unchanged. Before and after figures are recorded per episode. Do not build yet: No transcript, notes or clips. ### Phase 2 · Transcript with timestamps and speakers Build: Transcribe with word-level timestamps. Assign speaker labels, correctable by hand, since automatic assignment will be imperfect. Build sentence segments from words. Verify by playing a single word and hearing that word. Trap: Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. Done when: Playing one word's range plays that word cleanly. Word timestamps are monotonic with no overlaps. Speaker labels are assigned and correctable, and a correction persists. Sentence segments align with audible sentence boundaries. Do not build yet: No generation yet. ### Phase 3 · Show notes that cite the recording Build: Generate a summary, show notes and a description from the transcript. Require every claim to cite segment identifiers. Validate that every cited identifier exists, and reject the generation if one does not. Trap: A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. Done when: Generated notes cite segment identifiers and each resolves to real text. A crafted output citing a non-existent segment fails validation, tested by feeding one to the validator directly. Clicking a citation plays that moment. A transcript with nothing to summarise produces a short honest output rather than invention. Do not build yet: No clips or chapters yet. ### Phase 4 · Chapters and clip candidates Build: Detect chapter boundaries from long silences and topic shifts in the transcript. Propose clip candidates scored on self-contained meaning and length. Snap every boundary to a word boundary with a small pad, and require approval before export. Trap: A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. Done when: Chapter boundaries land at real topic or silence boundaries rather than mid sentence. Clip candidates start and end at word boundaries with padding, verified by listening to three. No clip is exported without approval. A candidate can be nudged at either end and re-previewed. Do not build yet: No export bundle yet. ### Phase 5 · The export bundle Build: Export the processed audio, the transcript in a subtitle format and as plain text, the show notes as Markdown, chapter markers in a format your host accepts, and approved clips as separate files. Write a manifest listing everything and the settings used. Trap: Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. Done when: A single export produces audio, transcript, notes, chapters and approved clips. Chapter timings match the audio when loaded in a player. The manifest records the settings and the source hash. Re-running the export with unchanged settings produces identical files. Do not build yet: Publishing and repurposing come last. ### Phase 6 · Repurposing, batching and the honest limits Build: Batch process a directory of episodes. Generate social posts and a newsletter section from the notes, with the same citation validation. Then review a sample honestly and record what needed editing before it was publishable. Trap: Generated social copy reads fluently and is wrong often enough that publishing it unread will eventually misquote a guest. Sample and record the edit rate rather than assuming, because that number tells you whether this saves time or moves the work. Done when: A directory of episodes processes unattended with per-episode failures reported and not fatal. Generated social copy carries citations validated the same way. A sample of ten generated outputs has been reviewed and the proportion needing edits is recorded. Cost per episode is reported. Do not build yet: Nothing after this. ### Out of scope (and why) - The prompt library behind the commercial product, which is accumulated tuning across many shows and is most of what the subscription buys. - Integrations that publish directly to hosts and social platforms, each of which is an API and a maintenance burden. - Team workflow with review and approval across several people. - Hosted processing, so an hour of audio takes an hour of your machine's time rather than someone else's. ### README must contain - That the source audio is never modified and every output is regenerable from it plus settings - That normalisation is two pass with linear correction, and the target used - That word-level timestamps are required and how to verify them - That every generated claim cites a segment and citations are validated - The measured edit rate on generated copy, so nobody publishes it unread ===== AGENTS.md ===== # Agent instructions · Castmagic indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 driving ffmpeg and a transcriber, ffmpeg for cleanup and clip extraction, whisper.cpp with word-level timestamps and speaker labels, A model applied to the transcript, never to the audio, Source audio untouched, outputs and a manifest alongside. 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 - Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. - Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. - A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. - A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. - Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. ===== BUILD_PLAN.md ===== # Build plan · Castmagic Import an episode, clean the audio, transcribe with word timestamps and speakers, generate show notes with real timestamps, find clip candidates from the transcript, and export the audio, the transcript, the notes and the clips as production files, with every generated claim traceable to a moment in the recording. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Import and clean, without touching the source An episode is loudness-normalised and ready, and the original is untouched. ### Steps 1. Measure loudness first Analysis pass parsed from the filter's structured output rather than its human readable text. Files: `src/measure.py` ```sh ffmpeg -i episode.wav -af loudnorm=print_format=json -f null - ``` 2. Apply with linear correction Feed the measured values back in. Around -16 LUFS with a -1.5 dBTP ceiling is a common spoken word target; state yours. Files: `src/normalise.py` 3. Never modify the source Outputs alongside, checksum the source before and after as a test. Files: `tests/test_source_untouched.py` ### Done when - [ ] The output measures within tolerance of the target - [ ] Loudness range is preserved rather than compressed - [ ] The source checksum is unchanged - [ ] Before and after figures are recorded ### Watch out - Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. ## Phase 2 · Transcript with timestamps and speakers Every word knows when it was said and, where there are two voices, who said it. ### Steps 1. Convert for the transcriber Mono 16 kHz 16 bit from the source, not from the normalised output. ```sh ffmpeg -i episode.wav -ar 16000 -ac 1 -c:a pcm_s16le for-transcription.wav ``` 2. Request word-level output Explicitly. Verify by playing a single word before building anything on it. Files: `src/transcribe.py` 3. Label speakers and allow correction Automatic assignment is a starting point. Corrections must persist and propagate to segments. Files: `src/speakers.py` ### Done when - [ ] A single word's range plays that word - [ ] Word timestamps are monotonic - [ ] Speaker corrections persist - [ ] Segments align with audible sentence boundaries ### Watch out - Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. ## Phase 3 · Show notes that cite the recording Every claim in the generated notes points at a moment you can play. ### Steps 1. Pass numbered segments and demand citations The model sees numbered segments and must cite them. Request structured output so parsing is not a regular expression. Files: `src/notes.py` 2. Validate every citation A pure function from output plus segment set to a verdict. Unit test it with a crafted bad citation. Files: `src/validate.py`, `tests/test_validate.py` 3. Make citations playable Clicking one seeks the audio. This is also how you check the notes before publishing. Files: `src/review.py` ### Done when - [ ] Every citation resolves to real segment text - [ ] A crafted bad citation is rejected by the validator - [ ] Clicking a citation plays that moment - [ ] A thin transcript produces an honest short output ### Watch out - A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. ## Phase 4 · Chapters and clip candidates The episode is chaptered, and the moments worth clipping are surfaced rather than hunted for. ### Steps 1. Detect chapters two ways Long silences from the audio and topic shifts from the transcript, reconciled rather than one overriding the other. Files: `src/chapters.py` ```sh ffmpeg -i episode.wav -af silencedetect=n=-35dB:d=1.5 -f null - ``` 2. Score clip candidates Self-contained meaning, length within bounds, single speaker where you want that. Score, then propose. Files: `src/clips.py` 3. Snap and pad boundaries To word boundaries, padded into surrounding silence. Listen to three before trusting the rule. Files: `src/clips.py` ### Done when - [ ] Chapter boundaries fall at real transitions - [ ] Clip boundaries are on word boundaries with padding - [ ] No clip exports without approval - [ ] A candidate can be nudged and re-previewed ### Watch out - A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. ## Phase 5 · The export bundle One action produces everything you need to publish the episode. ### Steps 1. Export each artefact Audio, subtitle file, plain transcript, Markdown notes, chapters, clips. Files: `src/export.py` 2. Target your host's chapter format Confirm what it accepts first. Record the choice in the manifest. Files: `docs/host.md` 3. Write a manifest Settings, source hash and every output. This is what makes the bundle reproducible. Files: `src/manifest.py` ### Done when - [ ] One export produces every artefact - [ ] Chapter timings match the audio in a player - [ ] The manifest records settings and source hash - [ ] Re-export with unchanged settings is byte-identical ### Watch out - Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. ## Not in this build - The accumulated prompt library of the commercial product. - Publishing integrations to hosts and social platforms. - Team review and approval workflow. - Hosted processing that runs while your machine sleeps. ## After v1, if you want it - A guest-facing page generated from the transcript and clips - Cross-episode search over your whole back catalogue - Automatic detection of a repeated segment such as an intro, so it is excluded from clips ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Loudness target for spoken word. State it rather than assuming a default. TARGET_LUFS=-16 # Required. True peak ceiling, leaving headroom so lossy encoding does not clip. TRUE_PEAK_DB=-1.5 # Required. The transcription model. Larger is more accurate and considerably slower. WHISPER_MODEL_PATH=./models/ggml-base.en.bin # Required. Shortest clip candidate. Below this a clip rarely carries a self-contained idea. CLIP_MIN_SECONDS=20 # Required. Longest clip candidate. CLIP_MAX_SECONDS=90 # Optional · secret. For generation. Leave unset to run the audio and transcript phases only. MODEL_API_KEY=sk-... # Required. Where processed audio, transcripts, notes and clips are written. Never the source directory. OUTPUT_DIR=./out
You are building a production product version of Castmagic. 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 ===== # Castmagic · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Castmagic, turn a recording into a transcript, summary, clips, quotes, and reusable content assets. The hard boundary is large prompt library, integrations, team workflow, and hosted processing, plus audio infrastructure, distribution, and production polish. ## Product outcome A pipeline that turns a raw episode into publishable artefacts where every generated claim can be played back, clips cut cleanly at word boundaries, and the source recording is never touched. ## 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 - The accumulated prompt library of the commercial product. - Publishing integrations to hosts and social platforms. - Team review and approval workflow. - Hosted processing that runs while your machine sleeps. - large prompt library, integrations, team workflow, and hosted processing - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support ## Success criteria - Source checksums are unchanged after a full run - Single word playback confirms transcript alignment - The citation validator rejects a crafted bad citation - Three clip boundaries are confirmed clean by listening - The edit rate on generated copy is recorded ===== BRIEF.md ===== # Build brief · Castmagic 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 pipeline that turns a recorded episode into a transcript, show notes, clips and quotes to replace Castmagic. 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. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 driving ffmpeg and a transcriber · the work is orchestration over media tools - Audio: ffmpeg for cleanup and clip extraction · loudness normalisation and cutting are both solved - Transcription: whisper.cpp with word-level timestamps and speaker labels · timestamps are what let every generated artefact point back at the recording - Generation: A model applied to the transcript, never to the audio · text is cheap to process and the transcript is already the artefact you trust - Storage: Source audio untouched, outputs and a manifest alongside · everything is regenerable from the source plus settings ### Data model (create this before Phase 1) - episodes(id, source_path, title, duration_ms, imported_at, content_hash) - words(episode_id, ordinal, text, start_ms, end_ms, speaker) · the spine everything else references - segments(id, episode_id, start_ms, end_ms, speaker, text) · sentence-level, derived from words - chapters(id, episode_id, start_ms, title, source) · source is silence, transcript or manual - notes(id, episode_id, kind, body, cited_segment_ids_json, generated_at) · kind is summary, show_notes or description - clips(id, episode_id, start_ms, end_ms, reason, score, exported_path, approved) - Every generated artefact records the segment identifiers it drew from, so a claim in the show notes can be played back. ### Phase 1 · Import and clean, without touching the source Build: Import an episode, measure loudness, normalise to a stated target with a true peak ceiling using a two pass process, and apply gentle noise reduction. Write outputs alongside and record the before and after figures. Trap: Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. Done when: A processed episode measures within tolerance of the configured target and its loudness range is close to the source's rather than much smaller. The source file checksum is unchanged. Before and after figures are recorded per episode. Do not build yet: No transcript, notes or clips. ### Phase 2 · Transcript with timestamps and speakers Build: Transcribe with word-level timestamps. Assign speaker labels, correctable by hand, since automatic assignment will be imperfect. Build sentence segments from words. Verify by playing a single word and hearing that word. Trap: Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. Done when: Playing one word's range plays that word cleanly. Word timestamps are monotonic with no overlaps. Speaker labels are assigned and correctable, and a correction persists. Sentence segments align with audible sentence boundaries. Do not build yet: No generation yet. ### Phase 3 · Show notes that cite the recording Build: Generate a summary, show notes and a description from the transcript. Require every claim to cite segment identifiers. Validate that every cited identifier exists, and reject the generation if one does not. Trap: A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. Done when: Generated notes cite segment identifiers and each resolves to real text. A crafted output citing a non-existent segment fails validation, tested by feeding one to the validator directly. Clicking a citation plays that moment. A transcript with nothing to summarise produces a short honest output rather than invention. Do not build yet: No clips or chapters yet. ### Phase 4 · Chapters and clip candidates Build: Detect chapter boundaries from long silences and topic shifts in the transcript. Propose clip candidates scored on self-contained meaning and length. Snap every boundary to a word boundary with a small pad, and require approval before export. Trap: A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. Done when: Chapter boundaries land at real topic or silence boundaries rather than mid sentence. Clip candidates start and end at word boundaries with padding, verified by listening to three. No clip is exported without approval. A candidate can be nudged at either end and re-previewed. Do not build yet: No export bundle yet. ### Phase 5 · The export bundle Build: Export the processed audio, the transcript in a subtitle format and as plain text, the show notes as Markdown, chapter markers in a format your host accepts, and approved clips as separate files. Write a manifest listing everything and the settings used. Trap: Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. Done when: A single export produces audio, transcript, notes, chapters and approved clips. Chapter timings match the audio when loaded in a player. The manifest records the settings and the source hash. Re-running the export with unchanged settings produces identical files. Do not build yet: Publishing and repurposing come last. ### Phase 6 · Repurposing, batching and the honest limits Build: Batch process a directory of episodes. Generate social posts and a newsletter section from the notes, with the same citation validation. Then review a sample honestly and record what needed editing before it was publishable. Trap: Generated social copy reads fluently and is wrong often enough that publishing it unread will eventually misquote a guest. Sample and record the edit rate rather than assuming, because that number tells you whether this saves time or moves the work. Done when: A directory of episodes processes unattended with per-episode failures reported and not fatal. Generated social copy carries citations validated the same way. A sample of ten generated outputs has been reviewed and the proportion needing edits is recorded. Cost per episode is reported. Do not build yet: Nothing after this. ### Out of scope (and why) - The prompt library behind the commercial product, which is accumulated tuning across many shows and is most of what the subscription buys. - Integrations that publish directly to hosts and social platforms, each of which is an API and a maintenance burden. - Team workflow with review and approval across several people. - Hosted processing, so an hour of audio takes an hour of your machine's time rather than someone else's. ### README must contain - That the source audio is never modified and every output is regenerable from it plus settings - That normalisation is two pass with linear correction, and the target used - That word-level timestamps are required and how to verify them - That every generated claim cites a segment and citations are validated - The measured edit rate on generated copy, so nobody publishes it unread ===== ARCHITECTURE.md ===== # Architecture · Castmagic ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 driving ffmpeg and a transcriber | the work is orchestration over media tools | | Audio | ffmpeg for cleanup and clip extraction | loudness normalisation and cutting are both solved | | Transcription | whisper.cpp with word-level timestamps and speaker labels | timestamps are what let every generated artefact point back at the recording | | Generation | A model applied to the transcript, never to the audio | text is cheap to process and the transcript is already the artefact you trust | | Storage | Source audio untouched, outputs and a manifest alongside | everything is regenerable from the source plus settings | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Audio | Cleanup, measurement and extraction. | ffmpeg is the tool. The two pass normalisation and the never-modify-source rule survive any change. | | Transcript | Words, timestamps and speakers. | A hosted transcriber fits the same interface and gives up the offline property. Word-level output is non-negotiable. | | Generation | Notes, descriptions and social copy. | Any model. The citation validator is what makes swapping safe. | | Clips | Candidate scoring and boundary snapping. | Scoring can improve; approval before export should not, because a bad clip published is not recoverable. | | Export | The bundle and its manifest. | Host formats vary, so the exporter is the part most likely to need changing. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `TARGET_LUFS` · required · Loudness target for spoken word. State it rather than assuming a default. - `TRUE_PEAK_DB` · required · True peak ceiling, leaving headroom so lossy encoding does not clip. - `WHISPER_MODEL_PATH` · required · The transcription model. Larger is more accurate and considerably slower. - `CLIP_MIN_SECONDS` · required · Shortest clip candidate. Below this a clip rarely carries a self-contained idea. - `CLIP_MAX_SECONDS` · required · Longest clip candidate. - `MODEL_API_KEY` · optional, secret · For generation. Leave unset to run the audio and transcript phases only. - `OUTPUT_DIR` · required · Where processed audio, transcripts, notes and clips are written. Never the source directory. ## 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 · Castmagic product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 driving ffmpeg and a transcriber, ffmpeg for cleanup and clip extraction, whisper.cpp with word-level timestamps and speaker labels, A model applied to the transcript, never to the audio, Source audio untouched, outputs and a manifest alongside. - 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 - Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. - Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. - A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. - A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. - Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. - Generated social copy reads fluently and is wrong often enough that publishing it unread will eventually misquote a guest. Sample and record the edit rate rather than assuming, because that number tells you whether this saves time or moves the work. ===== MILESTONES.md ===== # Delivery milestones · Castmagic Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Import and clean, without touching the source An episode is loudness-normalised and ready, and the original is untouched. ### Steps 1. Measure loudness first Analysis pass parsed from the filter's structured output rather than its human readable text. Files: `src/measure.py` ```sh ffmpeg -i episode.wav -af loudnorm=print_format=json -f null - ``` 2. Apply with linear correction Feed the measured values back in. Around -16 LUFS with a -1.5 dBTP ceiling is a common spoken word target; state yours. Files: `src/normalise.py` 3. Never modify the source Outputs alongside, checksum the source before and after as a test. Files: `tests/test_source_untouched.py` ### Done when - [ ] The output measures within tolerance of the target - [ ] Loudness range is preserved rather than compressed - [ ] The source checksum is unchanged - [ ] Before and after figures are recorded ### Watch out - Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. ## M2 · Transcript with timestamps and speakers Every word knows when it was said and, where there are two voices, who said it. ### Steps 1. Convert for the transcriber Mono 16 kHz 16 bit from the source, not from the normalised output. ```sh ffmpeg -i episode.wav -ar 16000 -ac 1 -c:a pcm_s16le for-transcription.wav ``` 2. Request word-level output Explicitly. Verify by playing a single word before building anything on it. Files: `src/transcribe.py` 3. Label speakers and allow correction Automatic assignment is a starting point. Corrections must persist and propagate to segments. Files: `src/speakers.py` ### Done when - [ ] A single word's range plays that word - [ ] Word timestamps are monotonic - [ ] Speaker corrections persist - [ ] Segments align with audible sentence boundaries ### Watch out - Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. ## M3 · Show notes that cite the recording Every claim in the generated notes points at a moment you can play. ### Steps 1. Pass numbered segments and demand citations The model sees numbered segments and must cite them. Request structured output so parsing is not a regular expression. Files: `src/notes.py` 2. Validate every citation A pure function from output plus segment set to a verdict. Unit test it with a crafted bad citation. Files: `src/validate.py`, `tests/test_validate.py` 3. Make citations playable Clicking one seeks the audio. This is also how you check the notes before publishing. Files: `src/review.py` ### Done when - [ ] Every citation resolves to real segment text - [ ] A crafted bad citation is rejected by the validator - [ ] Clicking a citation plays that moment - [ ] A thin transcript produces an honest short output ### Watch out - A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. ## M4 · Chapters and clip candidates The episode is chaptered, and the moments worth clipping are surfaced rather than hunted for. ### Steps 1. Detect chapters two ways Long silences from the audio and topic shifts from the transcript, reconciled rather than one overriding the other. Files: `src/chapters.py` ```sh ffmpeg -i episode.wav -af silencedetect=n=-35dB:d=1.5 -f null - ``` 2. Score clip candidates Self-contained meaning, length within bounds, single speaker where you want that. Score, then propose. Files: `src/clips.py` 3. Snap and pad boundaries To word boundaries, padded into surrounding silence. Listen to three before trusting the rule. Files: `src/clips.py` ### Done when - [ ] Chapter boundaries fall at real transitions - [ ] Clip boundaries are on word boundaries with padding - [ ] No clip exports without approval - [ ] A candidate can be nudged and re-previewed ### Watch out - A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. ## M5 · The export bundle One action produces everything you need to publish the episode. ### Steps 1. Export each artefact Audio, subtitle file, plain transcript, Markdown notes, chapters, clips. Files: `src/export.py` 2. Target your host's chapter format Confirm what it accepts first. Record the choice in the manifest. Files: `docs/host.md` 3. Write a manifest Settings, source hash and every output. This is what makes the bundle reproducible. Files: `src/manifest.py` ### Done when - [ ] One export produces every artefact - [ ] Chapter timings match the audio in a player - [ ] The manifest records settings and source hash - [ ] Re-export with unchanged settings is byte-identical ### Watch out - Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. ## M6 · Repurposing, batching and the honest limits (production only) A back catalogue can be processed unattended, and you know what the output is worth. ### Steps 1. Batch with failure isolation One bad file must not stop a back catalogue run. Files: `src/batch.py` 2. Reuse the citation validator Social copy gets the same treatment as show notes. A misquote is worse on social. Files: `src/social.py` 3. Measure the edit rate Ten samples, reviewed honestly, with the proportion recorded. This is the number that decides whether the pipeline is worth running. Files: `docs/quality.md` ### Done when - [ ] A batch completes with failures isolated and reported - [ ] Social copy citations are validated - [ ] The edit rate over ten samples is recorded - [ ] Cost per episode is reported ### Watch out - Generated social copy reads fluently and is wrong often enough that publishing it unread will eventually misquote a guest. Sample and record the edit rate rather than assuming, because that number tells you whether this saves time or moves the work. ===== OPERATIONS.md ===== # Operations · Castmagic ## Backup Source recordings are irreplaceable and belong in a backup. Everything else regenerates from the source plus the manifest's settings, which is why the manifest exists. ## Restore Restore sources and settings, then re-run. Byte-identical output is what makes this a restore rather than a reprocessing. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For batch runs, watch citation validation failures. A rising rate means the transcript quality dropped or the segments passed to the model became less coherent, and it is the early signal of unusable output. ## Incident checklist The failure that matters is publishing a generated claim that was never said. The validator is the control, and if something gets through, the fix belongs in the validator rather than in remembering to read more carefully. 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 unchanged after a full run - [ ] Single word playback confirms transcript alignment - [ ] The citation validator rejects a crafted bad citation - [ ] Three clip boundaries are confirmed clean by listening - [ ] The edit rate on generated copy is recorded ## Launch constraint Do not market omitted Castmagic 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. Loudness target for spoken word. State it rather than assuming a default. TARGET_LUFS=-16 # Required. True peak ceiling, leaving headroom so lossy encoding does not clip. TRUE_PEAK_DB=-1.5 # Required. The transcription model. Larger is more accurate and considerably slower. WHISPER_MODEL_PATH=./models/ggml-base.en.bin # Required. Shortest clip candidate. Below this a clip rarely carries a self-contained idea. CLIP_MIN_SECONDS=20 # Required. Longest clip candidate. CLIP_MAX_SECONDS=90 # Optional · secret. For generation. Leave unset to run the audio and transcript phases only. MODEL_API_KEY=sk-... # Required. Where processed audio, transcripts, notes and clips are written. Never the source directory. OUTPUT_DIR=./out
# Castmagic · indie build Import an episode, clean the audio, transcribe with word timestamps and speakers, generate show notes with real timestamps, find clip candidates from the transcript, and export the audio, the transcript, the notes and the clips as production files, with every generated claim traceable to a moment in the recording. 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 driving ffmpeg and a transcriber | the work is orchestration over media tools | | Audio | ffmpeg for cleanup and clip extraction | loudness normalisation and cutting are both solved | | Transcription | whisper.cpp with word-level timestamps and speaker labels | timestamps are what let every generated artefact point back at the recording | | Generation | A model applied to the transcript, never to the audio | text is cheap to process and the transcript is already the artefact you trust | | Storage | Source audio untouched, outputs and a manifest alongside | everything is regenerable from the source plus settings | ## 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 pipeline. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with loudnorm and afftdn** · free - Why: Cleanup, measurement and clip extraction all run through it. - Get it: brew install ffmpeg or your package manager. Confirm the filters exist rather than assuming. - Verify: ffmpeg -filters lists loudnorm and afftdn - [ ] **whisper.cpp with a model supporting word timestamps** · free, but transcription takes real time on your machine - Why: Word timestamps are the spine of chapters, clips and citations. Segment-level output cannot support any of them. - Get it: Build whisper.cpp and download a model. Confirm word-level output before building on it. - Verify: Output contains per-word start and end times - [ ] **A model API key, or a local model** · pay per token; an hour-long transcript summarised is typically a few cents - Why: Phase 3 onward. Cleanup and transcription work with no model at all. - Get it: A provider key with a spend limit, or Ollama for a local model with lower quality output. - Verify: A generated note returns with valid citations - [ ] **Three real episodes, one with two speakers** · free - Why: Speaker labelling, chapter detection and clip scoring all need real conversation. A monologue hides most of the difficulty. - Get it: Use your own recordings. At least one over forty minutes with two voices. - Verify: Three episodes are available with one two-speaker recording over forty minutes ## Quick start ```sh ffmpeg -i episode.wav -af loudnorm=print_format=json -f null - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The accumulated prompt library of the commercial product. - Publishing integrations to hosts and social platforms. - Team review and approval workflow. - Hosted processing that runs while your machine sleeps. - large prompt library, integrations, team workflow, and hosted processing - 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 Castmagic, and the README should say so rather than pretend.
# Build brief · Castmagic 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 pipeline that turns a recorded episode into a transcript, show notes, clips and quotes to replace Castmagic. 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. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 driving ffmpeg and a transcriber · the work is orchestration over media tools - Audio: ffmpeg for cleanup and clip extraction · loudness normalisation and cutting are both solved - Transcription: whisper.cpp with word-level timestamps and speaker labels · timestamps are what let every generated artefact point back at the recording - Generation: A model applied to the transcript, never to the audio · text is cheap to process and the transcript is already the artefact you trust - Storage: Source audio untouched, outputs and a manifest alongside · everything is regenerable from the source plus settings ### Data model (create this before Phase 1) - episodes(id, source_path, title, duration_ms, imported_at, content_hash) - words(episode_id, ordinal, text, start_ms, end_ms, speaker) · the spine everything else references - segments(id, episode_id, start_ms, end_ms, speaker, text) · sentence-level, derived from words - chapters(id, episode_id, start_ms, title, source) · source is silence, transcript or manual - notes(id, episode_id, kind, body, cited_segment_ids_json, generated_at) · kind is summary, show_notes or description - clips(id, episode_id, start_ms, end_ms, reason, score, exported_path, approved) - Every generated artefact records the segment identifiers it drew from, so a claim in the show notes can be played back. ### Phase 1 · Import and clean, without touching the source Build: Import an episode, measure loudness, normalise to a stated target with a true peak ceiling using a two pass process, and apply gentle noise reduction. Write outputs alongside and record the before and after figures. Trap: Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. Done when: A processed episode measures within tolerance of the configured target and its loudness range is close to the source's rather than much smaller. The source file checksum is unchanged. Before and after figures are recorded per episode. Do not build yet: No transcript, notes or clips. ### Phase 2 · Transcript with timestamps and speakers Build: Transcribe with word-level timestamps. Assign speaker labels, correctable by hand, since automatic assignment will be imperfect. Build sentence segments from words. Verify by playing a single word and hearing that word. Trap: Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. Done when: Playing one word's range plays that word cleanly. Word timestamps are monotonic with no overlaps. Speaker labels are assigned and correctable, and a correction persists. Sentence segments align with audible sentence boundaries. Do not build yet: No generation yet. ### Phase 3 · Show notes that cite the recording Build: Generate a summary, show notes and a description from the transcript. Require every claim to cite segment identifiers. Validate that every cited identifier exists, and reject the generation if one does not. Trap: A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. Done when: Generated notes cite segment identifiers and each resolves to real text. A crafted output citing a non-existent segment fails validation, tested by feeding one to the validator directly. Clicking a citation plays that moment. A transcript with nothing to summarise produces a short honest output rather than invention. Do not build yet: No clips or chapters yet. ### Phase 4 · Chapters and clip candidates Build: Detect chapter boundaries from long silences and topic shifts in the transcript. Propose clip candidates scored on self-contained meaning and length. Snap every boundary to a word boundary with a small pad, and require approval before export. Trap: A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. Done when: Chapter boundaries land at real topic or silence boundaries rather than mid sentence. Clip candidates start and end at word boundaries with padding, verified by listening to three. No clip is exported without approval. A candidate can be nudged at either end and re-previewed. Do not build yet: No export bundle yet. ### Phase 5 · The export bundle Build: Export the processed audio, the transcript in a subtitle format and as plain text, the show notes as Markdown, chapter markers in a format your host accepts, and approved clips as separate files. Write a manifest listing everything and the settings used. Trap: Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. Done when: A single export produces audio, transcript, notes, chapters and approved clips. Chapter timings match the audio when loaded in a player. The manifest records the settings and the source hash. Re-running the export with unchanged settings produces identical files. Do not build yet: Publishing and repurposing come last. ### Phase 6 · Repurposing, batching and the honest limits Build: Batch process a directory of episodes. Generate social posts and a newsletter section from the notes, with the same citation validation. Then review a sample honestly and record what needed editing before it was publishable. Trap: Generated social copy reads fluently and is wrong often enough that publishing it unread will eventually misquote a guest. Sample and record the edit rate rather than assuming, because that number tells you whether this saves time or moves the work. Done when: A directory of episodes processes unattended with per-episode failures reported and not fatal. Generated social copy carries citations validated the same way. A sample of ten generated outputs has been reviewed and the proportion needing edits is recorded. Cost per episode is reported. Do not build yet: Nothing after this. ### Out of scope (and why) - The prompt library behind the commercial product, which is accumulated tuning across many shows and is most of what the subscription buys. - Integrations that publish directly to hosts and social platforms, each of which is an API and a maintenance burden. - Team workflow with review and approval across several people. - Hosted processing, so an hour of audio takes an hour of your machine's time rather than someone else's. ### README must contain - That the source audio is never modified and every output is regenerable from it plus settings - That normalisation is two pass with linear correction, and the target used - That word-level timestamps are required and how to verify them - That every generated claim cites a segment and citations are validated - The measured edit rate on generated copy, so nobody publishes it unread
# Agent instructions · Castmagic indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 driving ffmpeg and a transcriber, ffmpeg for cleanup and clip extraction, whisper.cpp with word-level timestamps and speaker labels, A model applied to the transcript, never to the audio, Source audio untouched, outputs and a manifest alongside. 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 - Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. - Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. - A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. - A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. - Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest.
# Build plan · Castmagic Import an episode, clean the audio, transcribe with word timestamps and speakers, generate show notes with real timestamps, find clip candidates from the transcript, and export the audio, the transcript, the notes and the clips as production files, with every generated claim traceable to a moment in the recording. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Import and clean, without touching the source An episode is loudness-normalised and ready, and the original is untouched. ### Steps 1. Measure loudness first Analysis pass parsed from the filter's structured output rather than its human readable text. Files: `src/measure.py` ```sh ffmpeg -i episode.wav -af loudnorm=print_format=json -f null - ``` 2. Apply with linear correction Feed the measured values back in. Around -16 LUFS with a -1.5 dBTP ceiling is a common spoken word target; state yours. Files: `src/normalise.py` 3. Never modify the source Outputs alongside, checksum the source before and after as a test. Files: `tests/test_source_untouched.py` ### Done when - [ ] The output measures within tolerance of the target - [ ] Loudness range is preserved rather than compressed - [ ] The source checksum is unchanged - [ ] Before and after figures are recorded ### Watch out - Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. ## Phase 2 · Transcript with timestamps and speakers Every word knows when it was said and, where there are two voices, who said it. ### Steps 1. Convert for the transcriber Mono 16 kHz 16 bit from the source, not from the normalised output. ```sh ffmpeg -i episode.wav -ar 16000 -ac 1 -c:a pcm_s16le for-transcription.wav ``` 2. Request word-level output Explicitly. Verify by playing a single word before building anything on it. Files: `src/transcribe.py` 3. Label speakers and allow correction Automatic assignment is a starting point. Corrections must persist and propagate to segments. Files: `src/speakers.py` ### Done when - [ ] A single word's range plays that word - [ ] Word timestamps are monotonic - [ ] Speaker corrections persist - [ ] Segments align with audible sentence boundaries ### Watch out - Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. ## Phase 3 · Show notes that cite the recording Every claim in the generated notes points at a moment you can play. ### Steps 1. Pass numbered segments and demand citations The model sees numbered segments and must cite them. Request structured output so parsing is not a regular expression. Files: `src/notes.py` 2. Validate every citation A pure function from output plus segment set to a verdict. Unit test it with a crafted bad citation. Files: `src/validate.py`, `tests/test_validate.py` 3. Make citations playable Clicking one seeks the audio. This is also how you check the notes before publishing. Files: `src/review.py` ### Done when - [ ] Every citation resolves to real segment text - [ ] A crafted bad citation is rejected by the validator - [ ] Clicking a citation plays that moment - [ ] A thin transcript produces an honest short output ### Watch out - A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. ## Phase 4 · Chapters and clip candidates The episode is chaptered, and the moments worth clipping are surfaced rather than hunted for. ### Steps 1. Detect chapters two ways Long silences from the audio and topic shifts from the transcript, reconciled rather than one overriding the other. Files: `src/chapters.py` ```sh ffmpeg -i episode.wav -af silencedetect=n=-35dB:d=1.5 -f null - ``` 2. Score clip candidates Self-contained meaning, length within bounds, single speaker where you want that. Score, then propose. Files: `src/clips.py` 3. Snap and pad boundaries To word boundaries, padded into surrounding silence. Listen to three before trusting the rule. Files: `src/clips.py` ### Done when - [ ] Chapter boundaries fall at real transitions - [ ] Clip boundaries are on word boundaries with padding - [ ] No clip exports without approval - [ ] A candidate can be nudged and re-previewed ### Watch out - A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. ## Phase 5 · The export bundle One action produces everything you need to publish the episode. ### Steps 1. Export each artefact Audio, subtitle file, plain transcript, Markdown notes, chapters, clips. Files: `src/export.py` 2. Target your host's chapter format Confirm what it accepts first. Record the choice in the manifest. Files: `docs/host.md` 3. Write a manifest Settings, source hash and every output. This is what makes the bundle reproducible. Files: `src/manifest.py` ### Done when - [ ] One export produces every artefact - [ ] Chapter timings match the audio in a player - [ ] The manifest records settings and source hash - [ ] Re-export with unchanged settings is byte-identical ### Watch out - Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. ## Not in this build - The accumulated prompt library of the commercial product. - Publishing integrations to hosts and social platforms. - Team review and approval workflow. - Hosted processing that runs while your machine sleeps. ## After v1, if you want it - A guest-facing page generated from the transcript and clips - Cross-episode search over your whole back catalogue - Automatic detection of a repeated segment such as an intro, so it is excluded from clips
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Loudness target for spoken word. State it rather than assuming a default. TARGET_LUFS=-16 # Required. True peak ceiling, leaving headroom so lossy encoding does not clip. TRUE_PEAK_DB=-1.5 # Required. The transcription model. Larger is more accurate and considerably slower. WHISPER_MODEL_PATH=./models/ggml-base.en.bin # Required. Shortest clip candidate. Below this a clip rarely carries a self-contained idea. CLIP_MIN_SECONDS=20 # Required. Longest clip candidate. CLIP_MAX_SECONDS=90 # Optional · secret. For generation. Leave unset to run the audio and transcript phases only. MODEL_API_KEY=sk-... # Required. Where processed audio, transcripts, notes and clips are written. Never the source directory. OUTPUT_DIR=./out
# Castmagic · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Castmagic, turn a recording into a transcript, summary, clips, quotes, and reusable content assets. The hard boundary is large prompt library, integrations, team workflow, and hosted processing, plus audio infrastructure, distribution, and production polish. ## Product outcome A pipeline that turns a raw episode into publishable artefacts where every generated claim can be played back, clips cut cleanly at word boundaries, and the source recording is never touched. ## 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 - The accumulated prompt library of the commercial product. - Publishing integrations to hosts and social platforms. - Team review and approval workflow. - Hosted processing that runs while your machine sleeps. - large prompt library, integrations, team workflow, and hosted processing - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support ## Success criteria - Source checksums are unchanged after a full run - Single word playback confirms transcript alignment - The citation validator rejects a crafted bad citation - Three clip boundaries are confirmed clean by listening - The edit rate on generated copy is recorded
# Build brief · Castmagic 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 pipeline that turns a recorded episode into a transcript, show notes, clips and quotes to replace Castmagic. 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. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 driving ffmpeg and a transcriber · the work is orchestration over media tools - Audio: ffmpeg for cleanup and clip extraction · loudness normalisation and cutting are both solved - Transcription: whisper.cpp with word-level timestamps and speaker labels · timestamps are what let every generated artefact point back at the recording - Generation: A model applied to the transcript, never to the audio · text is cheap to process and the transcript is already the artefact you trust - Storage: Source audio untouched, outputs and a manifest alongside · everything is regenerable from the source plus settings ### Data model (create this before Phase 1) - episodes(id, source_path, title, duration_ms, imported_at, content_hash) - words(episode_id, ordinal, text, start_ms, end_ms, speaker) · the spine everything else references - segments(id, episode_id, start_ms, end_ms, speaker, text) · sentence-level, derived from words - chapters(id, episode_id, start_ms, title, source) · source is silence, transcript or manual - notes(id, episode_id, kind, body, cited_segment_ids_json, generated_at) · kind is summary, show_notes or description - clips(id, episode_id, start_ms, end_ms, reason, score, exported_path, approved) - Every generated artefact records the segment identifiers it drew from, so a claim in the show notes can be played back. ### Phase 1 · Import and clean, without touching the source Build: Import an episode, measure loudness, normalise to a stated target with a true peak ceiling using a two pass process, and apply gentle noise reduction. Write outputs alongside and record the before and after figures. Trap: Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. Done when: A processed episode measures within tolerance of the configured target and its loudness range is close to the source's rather than much smaller. The source file checksum is unchanged. Before and after figures are recorded per episode. Do not build yet: No transcript, notes or clips. ### Phase 2 · Transcript with timestamps and speakers Build: Transcribe with word-level timestamps. Assign speaker labels, correctable by hand, since automatic assignment will be imperfect. Build sentence segments from words. Verify by playing a single word and hearing that word. Trap: Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. Done when: Playing one word's range plays that word cleanly. Word timestamps are monotonic with no overlaps. Speaker labels are assigned and correctable, and a correction persists. Sentence segments align with audible sentence boundaries. Do not build yet: No generation yet. ### Phase 3 · Show notes that cite the recording Build: Generate a summary, show notes and a description from the transcript. Require every claim to cite segment identifiers. Validate that every cited identifier exists, and reject the generation if one does not. Trap: A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. Done when: Generated notes cite segment identifiers and each resolves to real text. A crafted output citing a non-existent segment fails validation, tested by feeding one to the validator directly. Clicking a citation plays that moment. A transcript with nothing to summarise produces a short honest output rather than invention. Do not build yet: No clips or chapters yet. ### Phase 4 · Chapters and clip candidates Build: Detect chapter boundaries from long silences and topic shifts in the transcript. Propose clip candidates scored on self-contained meaning and length. Snap every boundary to a word boundary with a small pad, and require approval before export. Trap: A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. Done when: Chapter boundaries land at real topic or silence boundaries rather than mid sentence. Clip candidates start and end at word boundaries with padding, verified by listening to three. No clip is exported without approval. A candidate can be nudged at either end and re-previewed. Do not build yet: No export bundle yet. ### Phase 5 · The export bundle Build: Export the processed audio, the transcript in a subtitle format and as plain text, the show notes as Markdown, chapter markers in a format your host accepts, and approved clips as separate files. Write a manifest listing everything and the settings used. Trap: Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. Done when: A single export produces audio, transcript, notes, chapters and approved clips. Chapter timings match the audio when loaded in a player. The manifest records the settings and the source hash. Re-running the export with unchanged settings produces identical files. Do not build yet: Publishing and repurposing come last. ### Phase 6 · Repurposing, batching and the honest limits Build: Batch process a directory of episodes. Generate social posts and a newsletter section from the notes, with the same citation validation. Then review a sample honestly and record what needed editing before it was publishable. Trap: Generated social copy reads fluently and is wrong often enough that publishing it unread will eventually misquote a guest. Sample and record the edit rate rather than assuming, because that number tells you whether this saves time or moves the work. Done when: A directory of episodes processes unattended with per-episode failures reported and not fatal. Generated social copy carries citations validated the same way. A sample of ten generated outputs has been reviewed and the proportion needing edits is recorded. Cost per episode is reported. Do not build yet: Nothing after this. ### Out of scope (and why) - The prompt library behind the commercial product, which is accumulated tuning across many shows and is most of what the subscription buys. - Integrations that publish directly to hosts and social platforms, each of which is an API and a maintenance burden. - Team workflow with review and approval across several people. - Hosted processing, so an hour of audio takes an hour of your machine's time rather than someone else's. ### README must contain - That the source audio is never modified and every output is regenerable from it plus settings - That normalisation is two pass with linear correction, and the target used - That word-level timestamps are required and how to verify them - That every generated claim cites a segment and citations are validated - The measured edit rate on generated copy, so nobody publishes it unread
# Architecture · Castmagic ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 driving ffmpeg and a transcriber | the work is orchestration over media tools | | Audio | ffmpeg for cleanup and clip extraction | loudness normalisation and cutting are both solved | | Transcription | whisper.cpp with word-level timestamps and speaker labels | timestamps are what let every generated artefact point back at the recording | | Generation | A model applied to the transcript, never to the audio | text is cheap to process and the transcript is already the artefact you trust | | Storage | Source audio untouched, outputs and a manifest alongside | everything is regenerable from the source plus settings | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Audio | Cleanup, measurement and extraction. | ffmpeg is the tool. The two pass normalisation and the never-modify-source rule survive any change. | | Transcript | Words, timestamps and speakers. | A hosted transcriber fits the same interface and gives up the offline property. Word-level output is non-negotiable. | | Generation | Notes, descriptions and social copy. | Any model. The citation validator is what makes swapping safe. | | Clips | Candidate scoring and boundary snapping. | Scoring can improve; approval before export should not, because a bad clip published is not recoverable. | | Export | The bundle and its manifest. | Host formats vary, so the exporter is the part most likely to need changing. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `TARGET_LUFS` · required · Loudness target for spoken word. State it rather than assuming a default. - `TRUE_PEAK_DB` · required · True peak ceiling, leaving headroom so lossy encoding does not clip. - `WHISPER_MODEL_PATH` · required · The transcription model. Larger is more accurate and considerably slower. - `CLIP_MIN_SECONDS` · required · Shortest clip candidate. Below this a clip rarely carries a self-contained idea. - `CLIP_MAX_SECONDS` · required · Longest clip candidate. - `MODEL_API_KEY` · optional, secret · For generation. Leave unset to run the audio and transcript phases only. - `OUTPUT_DIR` · required · Where processed audio, transcripts, notes and clips are written. Never the source directory. ## 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 · Castmagic product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 driving ffmpeg and a transcriber, ffmpeg for cleanup and clip extraction, whisper.cpp with word-level timestamps and speaker labels, A model applied to the transcript, never to the audio, Source audio untouched, outputs and a manifest alongside. - 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 - Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. - Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. - A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. - A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. - Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. - Generated social copy reads fluently and is wrong often enough that publishing it unread will eventually misquote a guest. Sample and record the edit rate rather than assuming, because that number tells you whether this saves time or moves the work.
# Delivery milestones · Castmagic Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Import and clean, without touching the source An episode is loudness-normalised and ready, and the original is untouched. ### Steps 1. Measure loudness first Analysis pass parsed from the filter's structured output rather than its human readable text. Files: `src/measure.py` ```sh ffmpeg -i episode.wav -af loudnorm=print_format=json -f null - ``` 2. Apply with linear correction Feed the measured values back in. Around -16 LUFS with a -1.5 dBTP ceiling is a common spoken word target; state yours. Files: `src/normalise.py` 3. Never modify the source Outputs alongside, checksum the source before and after as a test. Files: `tests/test_source_untouched.py` ### Done when - [ ] The output measures within tolerance of the target - [ ] Loudness range is preserved rather than compressed - [ ] The source checksum is unchanged - [ ] Before and after figures are recorded ### Watch out - Single pass loudness normalisation compresses dynamics to reach the target and makes speech sound pumped. Measure first, then apply the measured values with linear correction, which applies a fixed gain instead. On a spoken word episode the difference is audible immediately. ## M2 · Transcript with timestamps and speakers Every word knows when it was said and, where there are two voices, who said it. ### Steps 1. Convert for the transcriber Mono 16 kHz 16 bit from the source, not from the normalised output. ```sh ffmpeg -i episode.wav -ar 16000 -ac 1 -c:a pcm_s16le for-transcription.wav ``` 2. Request word-level output Explicitly. Verify by playing a single word before building anything on it. Files: `src/transcribe.py` 3. Label speakers and allow correction Automatic assignment is a starting point. Corrections must persist and propagate to segments. Files: `src/speakers.py` ### Done when - [ ] A single word's range plays that word - [ ] Word timestamps are monotonic - [ ] Speaker corrections persist - [ ] Segments align with audible sentence boundaries ### Watch out - Segment-level timestamps are the default and are useless for clips, because a segment is a sentence and a clip boundary needs a word. Request word-level output explicitly and check it by playing a single word's range from the middle of a sentence. ## M3 · Show notes that cite the recording Every claim in the generated notes points at a moment you can play. ### Steps 1. Pass numbered segments and demand citations The model sees numbered segments and must cite them. Request structured output so parsing is not a regular expression. Files: `src/notes.py` 2. Validate every citation A pure function from output plus segment set to a verdict. Unit test it with a crafted bad citation. Files: `src/validate.py`, `tests/test_validate.py` 3. Make citations playable Clicking one seeks the audio. This is also how you check the notes before publishing. Files: `src/review.py` ### Done when - [ ] Every citation resolves to real segment text - [ ] A crafted bad citation is rejected by the validator - [ ] Clicking a citation plays that moment - [ ] A thin transcript produces an honest short output ### Watch out - A model summarising an hour of conversation will confidently state something that was never said, and it will be the most quotable line in the notes. Require citations by segment identifier and validate them, because the failure mode here is publishing a quote your guest did not say. ## M4 · Chapters and clip candidates The episode is chaptered, and the moments worth clipping are surfaced rather than hunted for. ### Steps 1. Detect chapters two ways Long silences from the audio and topic shifts from the transcript, reconciled rather than one overriding the other. Files: `src/chapters.py` ```sh ffmpeg -i episode.wav -af silencedetect=n=-35dB:d=1.5 -f null - ``` 2. Score clip candidates Self-contained meaning, length within bounds, single speaker where you want that. Score, then propose. Files: `src/clips.py` 3. Snap and pad boundaries To word boundaries, padded into surrounding silence. Listen to three before trusting the rule. Files: `src/clips.py` ### Done when - [ ] Chapter boundaries fall at real transitions - [ ] Clip boundaries are on word boundaries with padding - [ ] No clip exports without approval - [ ] A candidate can be nudged and re-previewed ### Watch out - A clip cut mid-word is unusable and a clip cut exactly at a word boundary often clips the breath before it, which sounds abrupt. Snap to word boundaries and pad into the surrounding silence, and always let a person hear a candidate before it is exported. ## M5 · The export bundle One action produces everything you need to publish the episode. ### Steps 1. Export each artefact Audio, subtitle file, plain transcript, Markdown notes, chapters, clips. Files: `src/export.py` 2. Target your host's chapter format Confirm what it accepts first. Record the choice in the manifest. Files: `docs/host.md` 3. Write a manifest Settings, source hash and every output. This is what makes the bundle reproducible. Files: `src/manifest.py` ### Done when - [ ] One export produces every artefact - [ ] Chapter timings match the audio in a player - [ ] The manifest records settings and source hash - [ ] Re-export with unchanged settings is byte-identical ### Watch out - Chapter marker formats differ between podcast hosts and a file the host rejects is discovered at upload time, usually late. Check the format your host accepts before building the exporter, and record which one you targeted in the manifest. ## M6 · Repurposing, batching and the honest limits (production only) A back catalogue can be processed unattended, and you know what the output is worth. ### Steps 1. Batch with failure isolation One bad file must not stop a back catalogue run. Files: `src/batch.py` 2. Reuse the citation validator Social copy gets the same treatment as show notes. A misquote is worse on social. Files: `src/social.py` 3. Measure the edit rate Ten samples, reviewed honestly, with the proportion recorded. This is the number that decides whether the pipeline is worth running. Files: `docs/quality.md` ### Done when - [ ] A batch completes with failures isolated and reported - [ ] Social copy citations are validated - [ ] The edit rate over ten samples is recorded - [ ] Cost per episode is reported ### Watch out - Generated social copy reads fluently and is wrong often enough that publishing it unread will eventually misquote a guest. Sample and record the edit rate rather than assuming, because that number tells you whether this saves time or moves the work.
# Operations · Castmagic ## Backup Source recordings are irreplaceable and belong in a backup. Everything else regenerates from the source plus the manifest's settings, which is why the manifest exists. ## Restore Restore sources and settings, then re-run. Byte-identical output is what makes this a restore rather than a reprocessing. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For batch runs, watch citation validation failures. A rising rate means the transcript quality dropped or the segments passed to the model became less coherent, and it is the early signal of unusable output. ## Incident checklist The failure that matters is publishing a generated claim that was never said. The validator is the control, and if something gets through, the fix belongs in the validator rather than in remembering to read more carefully. 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 unchanged after a full run - [ ] Single word playback confirms transcript alignment - [ ] The citation validator rejects a crafted bad citation - [ ] Three clip boundaries are confirmed clean by listening - [ ] The edit rate on generated copy is recorded ## Launch constraint Do not market omitted Castmagic 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. Loudness target for spoken word. State it rather than assuming a default. TARGET_LUFS=-16 # Required. True peak ceiling, leaving headroom so lossy encoding does not clip. TRUE_PEAK_DB=-1.5 # Required. The transcription model. Larger is more accurate and considerably slower. WHISPER_MODEL_PATH=./models/ggml-base.en.bin # Required. Shortest clip candidate. Below this a clip rarely carries a self-contained idea. CLIP_MIN_SECONDS=20 # Required. Longest clip candidate. CLIP_MAX_SECONDS=90 # Optional · secret. For generation. Leave unset to run the audio and transcript phases only. MODEL_API_KEY=sk-... # Required. Where processed audio, transcripts, notes and clips are written. Never the source directory. OUTPUT_DIR=./out
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Castmagic 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.
xlarge prompt library, integrations, team workflow, and hosted processing
xremote studio reliability
xlicensed music libraries
xhosting distribution
xadvanced mastering and support
Castmagic pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| hobby | — | $19.92/workspace | 30 transcribed library hours total; 3 spaces; 50 GB storage; 3 seats; 10 GB/file |
| starter | — | $48.25/workspace | 100 transcribed library hours total; 10 spaces; 250 GB; 5 seats; API access |
| team | — | $139.08/workspace | 400 transcribed library hours total; 25 spaces; 1 TB; 5 seats included; unlimited team size |
| business & scale | $699/workspace | — | Custom transcription volume, seats, storage, security and support |
free tierno permanent free tier; a trial is offered, but its numeric allowance is not published on the live pricing page
billingannual self-serve plans (advertised as 30% lower) plus monthly/custom Business & Scale
hidden costsLibrary-hour caps are cumulative rather than simple monthly allowances; more hours can be purchased, and Team seats beyond 5 cost $19/month or $159/year each.
verified 2026-08-14 · source ↗
Vibecode Castmagic
Kinda. The core of Castmagic is buildable in a weekend with the prompt on this page, but there are real gaps: large prompt library, integrations, team workflow, and hosted processing, remote studio reliability. Read the honest list above before committing.
How much does Castmagic cost?
Castmagic costs about $19.92/month (Hobby, checked 2026-08-14), which is $239.04000000000002 per year.
What do I lose by replacing Castmagic?
Honestly: large prompt library, integrations, team workflow, and hosted processing; 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 Castmagic?
Yes: Audacity (Long-running open-source multitrack audio editor and useful implementation prior art.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.