Vibecode Alitu
track this build4 phases, 12 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Alitu, clean, assemble, publish, and package a recurring spoken-word podcast episode. The hard boundary is production automation, hosting integrations, support, and creator-specific polish, plus audio infrastructure, distribution, and production polish.
You are building a lean indie version of Alitu. 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 ===== # Alitu · indie build A podcast production pipeline that levels and assembles episodes and then publishes a feed which behaves correctly in every client, where the discipline is that identifiers and addresses are permanent commitments rather than implementation details. 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 | | --- | --- | --- | | Audio | ffmpeg with loudness normalisation to a stated target | consistency between episodes matters more than any single episode's polish | | Assembly | A declarative episode structure of segments | intro, content and outro assembled reproducibly rather than by hand each time | | Hosting | Object storage with a stable address scheme decided up front | a moved file breaks every existing subscriber | | Feed | Generated with immutable identifiers and validated before publish | the feed is consumed by clients you cannot fix | | Runtime | Python 3.12 with a job queue | renders are long and must be resumable | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Orchestrates rendering, upload and feed generation. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with loudness measurement** · free - Why: Normalisation, assembly and measurement all run through it, and reimplementing any of it is not the project. - Get it: Your package manager, confirming the loudness filter is available. - Verify: It measures and normalises a file to a stated target - [ ] **Object storage with range request support** · pennies per month plus bandwidth, which is the real cost - Why: Podcast clients resume partial downloads, and storage that does not support ranges produces failures that look like corrupt files. - Get it: Any provider serving range requests with correct content types. Decide the address scheme before uploading anything. - Verify: A partial download resumes correctly from the stored file - [ ] **Two real podcast clients** · free - Why: A feed passing validation is not the same as a client rendering it correctly, and the differences between clients are where the surprises live. - Get it: Install two different podcast apps and subscribe to your feed by address before publishing publicly. - Verify: Both clients show artwork, episode order and descriptions correctly - [ ] **Your identifier and address scheme, written first** · free - Why: Both are permanent commitments visible to every subscriber, and changing either after publishing is a migration your audience experiences as broken episodes. - Get it: Decide how identifiers are generated and how media addresses are formed, from immutable values only. Write it down before the first publish. - Verify: A written document states both schemes and confirms neither derives from mutable data ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Transcription and show notes. - Directory submission and management. - Dynamic ad insertion. - Meaningful analytics. - production automation, hosting integrations, support, and creator-specific polish - 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 Alitu, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Alitu 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 podcast production and publishing where the feed is correct and stays correct to replace Alitu. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The feed is the product. Everything else here is audio processing that can be redone, but a podcast feed is consumed by apps that cache it and by directories that index it, and getting it wrong is visible to every listener at once. The specific thing to get right is the episode identifier. It must never change once published. Apps use it to know what a listener has already heard, so changing it makes an existing episode appear as a new one, which re-downloads it for everyone and can produce duplicates across directories. Generate a stable identifier at publish and treat it as immutable, including when a file is replaced or a title is corrected. The same discipline applies to the enclosure address. If the file moves, listeners with the old address get nothing, and clients that already downloaded may re-download. Decide the hosting layout before publishing the first episode, because changing it afterwards is a migration visible to your audience. ### Stack (fixed, do not substitute) - Audio: ffmpeg with loudness normalisation to a stated target · consistency between episodes matters more than any single episode's polish - Assembly: A declarative episode structure of segments · intro, content and outro assembled reproducibly rather than by hand each time - Hosting: Object storage with a stable address scheme decided up front · a moved file breaks every existing subscriber - Feed: Generated with immutable identifiers and validated before publish · the feed is consumed by clients you cannot fix - Runtime: Python 3.12 with a job queue · renders are long and must be resumable ### Data model (create this before Phase 1) - shows(id, title, author, category, explicit, artwork_path, feed_url, base_media_url) - episodes(id, show_id, number, season, title, description, guid, published_at, duration_seconds, state) - segments(episode_id, ordinal, kind, source_path, start_ms, end_ms, gain_db) · kind is intro, content, ad or outro - renders(episode_id, revision, output_path, bytes, loudness_lufs, rendered_at) - publications(episode_id, feed_revision, published_at, enclosure_url) · what the feed said and when - feed_revisions(id, show_id, xml_path, generated_at, validated, validator_output) - An episode's guid is written once at first publish and has no update path anywhere in the schema. ### Phase 1 · Audio that sounds the same every week Build: Normalise loudness to a stated target with a true peak ceiling. Report the measured loudness before and after. Handle a recording that is far too quiet without amplifying the noise floor to match. Trap: Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. Done when: Episodes measure within a stated tolerance of the loudness target and respect the true peak ceiling. A recording requiring excessive gain is reported rather than normalised into noise. Before and after measurements are recorded per render. Two consecutive episodes sound consistent when played back to back. Do not build yet: No assembly, hosting or feed. ### Phase 2 · Assembling an episode reproducibly Build: Assemble episodes from a declarative segment list with crossfades and per-segment gain. Make the render deterministic. Keep source files untouched and every render revision reproducible from its description. Trap: Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. Done when: The same segment list renders byte-identically twice. Source files are unmodified, asserted by checksum in the test suite. Changing the intro re-renders from original sources rather than from a previous render. Crossfades between segments produce no click, verified by inspecting the waveform. Do not build yet: No hosting or feed yet. ### Phase 3 · Addresses that will not move Build: Upload to object storage using an address scheme decided in advance and documented. Serve with correct content type and range request support. Verify a partial download resumes, since podcast clients rely on it. Trap: Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. Done when: Media addresses derive from an immutable identifier rather than from the title, verified by renaming an episode and confirming the address is unchanged. Range requests work and a partial download resumes. Content type and length headers are correct. The address scheme is documented before the first publication. Do not build yet: No feed generation yet. ### Phase 4 · A feed that is right the first time Build: Generate the feed with the required namespace tags, an immutable identifier per episode written once at publish, and validate before publishing. Verify in at least two real podcast clients. Trap: Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. Done when: An episode identifier is written once and has no update path, enforced by the schema. Correcting a title or replacing a file does not change the identifier, tested directly. The feed passes validation before publishing. The feed loads correctly in two real podcast clients including artwork and episode ordering. Do not build yet: No scheduling or analytics yet. ### Phase 5 · Scheduling, corrections and knowing it worked Build: Schedule publication with the feed updating at the right moment. Support correcting an episode's file or metadata without changing its identifier. Record download requests from your own storage for a basic sense of reach. Trap: Replacing an episode's audio file at the same address means clients that already downloaded keep the old version and new listeners get the new one, with no way to tell which anyone has. Decide whether corrections replace in place or publish a new address, and state the consequence either way rather than discovering it. Done when: A scheduled episode appears in the feed at the intended moment. Correcting a file follows a stated rule with its consequence documented. The identifier is unchanged by any correction. Download counts are recorded from storage access logs with their limitations stated. Do not build yet: Nothing after this. ### Out of scope (and why) - Transcription and show notes generation, which is a separate tool and a separate quality question. - Directory submission and management, which is manual and per directory. - Dynamic ad insertion, which requires serving audio per request and is a different hosting model entirely. - Meaningful analytics, which requires either a serving layer you control end to end or a hosting provider that specialises in it. ### README must contain - That an episode identifier is written once and has no update path, and why changing it re-downloads for everyone - That media addresses derive from an immutable identifier rather than from the title - That the address scheme is documented before the first episode because changing it is a migration your audience sees - That source recordings are never modified and renders are reproducible from their description - That an excessively quiet recording is reported rather than normalised into noise ===== AGENTS.md ===== # Agent instructions · Alitu indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg with loudness normalisation to a stated target, A declarative episode structure of segments, Object storage with a stable address scheme decided up front, Generated with immutable identifiers and validated before publish, Python 3.12 with a job queue. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. - Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. - Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. - Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. ===== BUILD_PLAN.md ===== # Build plan · Alitu A podcast production pipeline that levels and assembles episodes and then publishes a feed which behaves correctly in every client, where the discipline is that identifiers and addresses are permanent commitments rather than implementation details. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Audio that sounds the same every week Episodes are consistent in level between themselves and against the platform. ### Steps 1. Normalise to a stated target with a peak ceiling Consistency between episodes is what listeners notice. Files: `src/loudness.py` 2. Measure and record before and after So a problem episode is explicable rather than mysterious. Files: `src/measure.py` 3. Refuse excessive gain Loud and hissy is worse than quiet. Report it instead. Files: `src/loudness.py` ### Done when - [ ] Episodes measure within tolerance of the target - [ ] Excessive gain is refused and reported - [ ] Measurements are recorded per render - [ ] Consecutive episodes sound consistent ### Watch out - Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. ## Phase 2 · Assembling an episode reproducibly The same segments produce the same file every time. ### Steps 1. Describe the assembly declaratively So a render is reproducible rather than a one-off result. Files: `src/assemble.py` 2. Keep sources read only Checksummed in the suite. Processing twice is not recoverable. Files: `tests/test_sources_untouched.py` 3. Crossfade rather than butt-joining A hard join between segments is a click. Files: `src/assemble.py` ### Done when - [ ] The same segment list renders identically - [ ] Source files are unmodified - [ ] Re-rendering uses original sources - [ ] Crossfades produce no click ### Watch out - Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. ## Phase 3 · Addresses that will not move Where an episode file lives is decided once and never changes. ### Steps 1. Derive addresses from something immutable Not the title. A typo correction should not move the file. Files: `src/storage.py` 2. Support range requests Podcast clients resume partial downloads constantly. Files: `src/storage.py` 3. Document the scheme before publishing Changing it later is a migration your audience experiences. Files: `docs/media-urls.md` ### Done when - [ ] Addresses are unaffected by a title change - [ ] Range requests and resumption work - [ ] Content type and length are correct - [ ] The address scheme is documented ### Watch out - Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. ## Phase 4 · A feed that is right the first time The feed validates and its identifiers are permanent. ### Steps 1. Write the identifier once, no update path In the schema. This is the commitment that cannot be walked back. Files: `src/schema.sql`, `tests/test_guid_immutable.py` 2. Validate before publishing The feed is consumed by clients you cannot fix. Files: `src/validate_feed.py` 3. Test in real clients A validator passing is not the same as a client rendering it correctly. Files: `tests/clients.md` ### Done when - [ ] The identifier has no update path - [ ] Corrections do not change identifiers - [ ] The feed passes validation before publishing - [ ] Two real clients render the feed correctly ### Watch out - Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. ## Not in this build - Transcription and show notes. - Directory submission and management. - Dynamic ad insertion. - Meaningful analytics. ## After v1, if you want it - Chapter markers, which several clients support and few shows use - A transcript published alongside, which helps discovery and accessibility together - Trailer and bonus episode types, once the feed handling is proven ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The media address prefix. Changing it later breaks every existing subscriber. SHOW_BASE_URL=https://media.example.com/show # Required. Where the feed lives. Directories store this and it should never move. FEED_URL=https://example.com/feed.xml # Required. Stated, and consistent across every episode. LOUDNESS_TARGET_LUFS=-16 # Required. So encoding does not introduce clipping after normalisation. TRUE_PEAK_CEILING_DB=-1.0 # Required. Above this a recording is reported rather than normalised into noise. MAX_GAIN_DB=20 # Required · secret. Scoped to the media bucket only. STORAGE_CREDENTIALS=a scoped key
You are building a lean indie version of Alitu. 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 ===== # Alitu · indie build A podcast production pipeline that levels and assembles episodes and then publishes a feed which behaves correctly in every client, where the discipline is that identifiers and addresses are permanent commitments rather than implementation details. 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 | | --- | --- | --- | | Audio | ffmpeg with loudness normalisation to a stated target | consistency between episodes matters more than any single episode's polish | | Assembly | A declarative episode structure of segments | intro, content and outro assembled reproducibly rather than by hand each time | | Hosting | Object storage with a stable address scheme decided up front | a moved file breaks every existing subscriber | | Feed | Generated with immutable identifiers and validated before publish | the feed is consumed by clients you cannot fix | | Runtime | Python 3.12 with a job queue | renders are long and must be resumable | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Orchestrates rendering, upload and feed generation. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with loudness measurement** · free - Why: Normalisation, assembly and measurement all run through it, and reimplementing any of it is not the project. - Get it: Your package manager, confirming the loudness filter is available. - Verify: It measures and normalises a file to a stated target - [ ] **Object storage with range request support** · pennies per month plus bandwidth, which is the real cost - Why: Podcast clients resume partial downloads, and storage that does not support ranges produces failures that look like corrupt files. - Get it: Any provider serving range requests with correct content types. Decide the address scheme before uploading anything. - Verify: A partial download resumes correctly from the stored file - [ ] **Two real podcast clients** · free - Why: A feed passing validation is not the same as a client rendering it correctly, and the differences between clients are where the surprises live. - Get it: Install two different podcast apps and subscribe to your feed by address before publishing publicly. - Verify: Both clients show artwork, episode order and descriptions correctly - [ ] **Your identifier and address scheme, written first** · free - Why: Both are permanent commitments visible to every subscriber, and changing either after publishing is a migration your audience experiences as broken episodes. - Get it: Decide how identifiers are generated and how media addresses are formed, from immutable values only. Write it down before the first publish. - Verify: A written document states both schemes and confirms neither derives from mutable data ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Transcription and show notes. - Directory submission and management. - Dynamic ad insertion. - Meaningful analytics. - production automation, hosting integrations, support, and creator-specific polish - 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 Alitu, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Alitu 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 podcast production and publishing where the feed is correct and stays correct to replace Alitu. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The feed is the product. Everything else here is audio processing that can be redone, but a podcast feed is consumed by apps that cache it and by directories that index it, and getting it wrong is visible to every listener at once. The specific thing to get right is the episode identifier. It must never change once published. Apps use it to know what a listener has already heard, so changing it makes an existing episode appear as a new one, which re-downloads it for everyone and can produce duplicates across directories. Generate a stable identifier at publish and treat it as immutable, including when a file is replaced or a title is corrected. The same discipline applies to the enclosure address. If the file moves, listeners with the old address get nothing, and clients that already downloaded may re-download. Decide the hosting layout before publishing the first episode, because changing it afterwards is a migration visible to your audience. ### Stack (fixed, do not substitute) - Audio: ffmpeg with loudness normalisation to a stated target · consistency between episodes matters more than any single episode's polish - Assembly: A declarative episode structure of segments · intro, content and outro assembled reproducibly rather than by hand each time - Hosting: Object storage with a stable address scheme decided up front · a moved file breaks every existing subscriber - Feed: Generated with immutable identifiers and validated before publish · the feed is consumed by clients you cannot fix - Runtime: Python 3.12 with a job queue · renders are long and must be resumable ### Data model (create this before Phase 1) - shows(id, title, author, category, explicit, artwork_path, feed_url, base_media_url) - episodes(id, show_id, number, season, title, description, guid, published_at, duration_seconds, state) - segments(episode_id, ordinal, kind, source_path, start_ms, end_ms, gain_db) · kind is intro, content, ad or outro - renders(episode_id, revision, output_path, bytes, loudness_lufs, rendered_at) - publications(episode_id, feed_revision, published_at, enclosure_url) · what the feed said and when - feed_revisions(id, show_id, xml_path, generated_at, validated, validator_output) - An episode's guid is written once at first publish and has no update path anywhere in the schema. ### Phase 1 · Audio that sounds the same every week Build: Normalise loudness to a stated target with a true peak ceiling. Report the measured loudness before and after. Handle a recording that is far too quiet without amplifying the noise floor to match. Trap: Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. Done when: Episodes measure within a stated tolerance of the loudness target and respect the true peak ceiling. A recording requiring excessive gain is reported rather than normalised into noise. Before and after measurements are recorded per render. Two consecutive episodes sound consistent when played back to back. Do not build yet: No assembly, hosting or feed. ### Phase 2 · Assembling an episode reproducibly Build: Assemble episodes from a declarative segment list with crossfades and per-segment gain. Make the render deterministic. Keep source files untouched and every render revision reproducible from its description. Trap: Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. Done when: The same segment list renders byte-identically twice. Source files are unmodified, asserted by checksum in the test suite. Changing the intro re-renders from original sources rather than from a previous render. Crossfades between segments produce no click, verified by inspecting the waveform. Do not build yet: No hosting or feed yet. ### Phase 3 · Addresses that will not move Build: Upload to object storage using an address scheme decided in advance and documented. Serve with correct content type and range request support. Verify a partial download resumes, since podcast clients rely on it. Trap: Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. Done when: Media addresses derive from an immutable identifier rather than from the title, verified by renaming an episode and confirming the address is unchanged. Range requests work and a partial download resumes. Content type and length headers are correct. The address scheme is documented before the first publication. Do not build yet: No feed generation yet. ### Phase 4 · A feed that is right the first time Build: Generate the feed with the required namespace tags, an immutable identifier per episode written once at publish, and validate before publishing. Verify in at least two real podcast clients. Trap: Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. Done when: An episode identifier is written once and has no update path, enforced by the schema. Correcting a title or replacing a file does not change the identifier, tested directly. The feed passes validation before publishing. The feed loads correctly in two real podcast clients including artwork and episode ordering. Do not build yet: No scheduling or analytics yet. ### Phase 5 · Scheduling, corrections and knowing it worked Build: Schedule publication with the feed updating at the right moment. Support correcting an episode's file or metadata without changing its identifier. Record download requests from your own storage for a basic sense of reach. Trap: Replacing an episode's audio file at the same address means clients that already downloaded keep the old version and new listeners get the new one, with no way to tell which anyone has. Decide whether corrections replace in place or publish a new address, and state the consequence either way rather than discovering it. Done when: A scheduled episode appears in the feed at the intended moment. Correcting a file follows a stated rule with its consequence documented. The identifier is unchanged by any correction. Download counts are recorded from storage access logs with their limitations stated. Do not build yet: Nothing after this. ### Out of scope (and why) - Transcription and show notes generation, which is a separate tool and a separate quality question. - Directory submission and management, which is manual and per directory. - Dynamic ad insertion, which requires serving audio per request and is a different hosting model entirely. - Meaningful analytics, which requires either a serving layer you control end to end or a hosting provider that specialises in it. ### README must contain - That an episode identifier is written once and has no update path, and why changing it re-downloads for everyone - That media addresses derive from an immutable identifier rather than from the title - That the address scheme is documented before the first episode because changing it is a migration your audience sees - That source recordings are never modified and renders are reproducible from their description - That an excessively quiet recording is reported rather than normalised into noise ===== AGENTS.md ===== # Agent instructions · Alitu indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg with loudness normalisation to a stated target, A declarative episode structure of segments, Object storage with a stable address scheme decided up front, Generated with immutable identifiers and validated before publish, Python 3.12 with a job queue. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. - Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. - Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. - Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. ===== BUILD_PLAN.md ===== # Build plan · Alitu A podcast production pipeline that levels and assembles episodes and then publishes a feed which behaves correctly in every client, where the discipline is that identifiers and addresses are permanent commitments rather than implementation details. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Audio that sounds the same every week Episodes are consistent in level between themselves and against the platform. ### Steps 1. Normalise to a stated target with a peak ceiling Consistency between episodes is what listeners notice. Files: `src/loudness.py` 2. Measure and record before and after So a problem episode is explicable rather than mysterious. Files: `src/measure.py` 3. Refuse excessive gain Loud and hissy is worse than quiet. Report it instead. Files: `src/loudness.py` ### Done when - [ ] Episodes measure within tolerance of the target - [ ] Excessive gain is refused and reported - [ ] Measurements are recorded per render - [ ] Consecutive episodes sound consistent ### Watch out - Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. ## Phase 2 · Assembling an episode reproducibly The same segments produce the same file every time. ### Steps 1. Describe the assembly declaratively So a render is reproducible rather than a one-off result. Files: `src/assemble.py` 2. Keep sources read only Checksummed in the suite. Processing twice is not recoverable. Files: `tests/test_sources_untouched.py` 3. Crossfade rather than butt-joining A hard join between segments is a click. Files: `src/assemble.py` ### Done when - [ ] The same segment list renders identically - [ ] Source files are unmodified - [ ] Re-rendering uses original sources - [ ] Crossfades produce no click ### Watch out - Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. ## Phase 3 · Addresses that will not move Where an episode file lives is decided once and never changes. ### Steps 1. Derive addresses from something immutable Not the title. A typo correction should not move the file. Files: `src/storage.py` 2. Support range requests Podcast clients resume partial downloads constantly. Files: `src/storage.py` 3. Document the scheme before publishing Changing it later is a migration your audience experiences. Files: `docs/media-urls.md` ### Done when - [ ] Addresses are unaffected by a title change - [ ] Range requests and resumption work - [ ] Content type and length are correct - [ ] The address scheme is documented ### Watch out - Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. ## Phase 4 · A feed that is right the first time The feed validates and its identifiers are permanent. ### Steps 1. Write the identifier once, no update path In the schema. This is the commitment that cannot be walked back. Files: `src/schema.sql`, `tests/test_guid_immutable.py` 2. Validate before publishing The feed is consumed by clients you cannot fix. Files: `src/validate_feed.py` 3. Test in real clients A validator passing is not the same as a client rendering it correctly. Files: `tests/clients.md` ### Done when - [ ] The identifier has no update path - [ ] Corrections do not change identifiers - [ ] The feed passes validation before publishing - [ ] Two real clients render the feed correctly ### Watch out - Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. ## Not in this build - Transcription and show notes. - Directory submission and management. - Dynamic ad insertion. - Meaningful analytics. ## After v1, if you want it - Chapter markers, which several clients support and few shows use - A transcript published alongside, which helps discovery and accessibility together - Trailer and bonus episode types, once the feed handling is proven ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The media address prefix. Changing it later breaks every existing subscriber. SHOW_BASE_URL=https://media.example.com/show # Required. Where the feed lives. Directories store this and it should never move. FEED_URL=https://example.com/feed.xml # Required. Stated, and consistent across every episode. LOUDNESS_TARGET_LUFS=-16 # Required. So encoding does not introduce clipping after normalisation. TRUE_PEAK_CEILING_DB=-1.0 # Required. Above this a recording is reported rather than normalised into noise. MAX_GAIN_DB=20 # Required · secret. Scoped to the media bucket only. STORAGE_CREDENTIALS=a scoped key
You are building a production product version of Alitu. 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 ===== # Alitu · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Alitu, clean, assemble, publish, and package a recurring spoken-word podcast episode. The hard boundary is production automation, hosting integrations, support, and creator-specific polish, plus audio infrastructure, distribution, and production polish. ## Product outcome A podcast pipeline that produces consistently levelled episodes and a feed whose identifiers and addresses never move, so a correction never costs your listeners a re-download. ## 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 - Transcription and show notes. - Directory submission and management. - Dynamic ad insertion. - Meaningful analytics. - production automation, hosting integrations, support, and creator-specific polish - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support ## Success criteria - The episode identifier is confirmed to have no update path - A title correction is confirmed not to change the media address - The feed is confirmed to validate before publishing - Two real clients are confirmed to render the feed correctly - Source recordings are confirmed unmodified by checksum ===== BRIEF.md ===== # Build brief · Alitu 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 podcast production and publishing where the feed is correct and stays correct to replace Alitu. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The feed is the product. Everything else here is audio processing that can be redone, but a podcast feed is consumed by apps that cache it and by directories that index it, and getting it wrong is visible to every listener at once. The specific thing to get right is the episode identifier. It must never change once published. Apps use it to know what a listener has already heard, so changing it makes an existing episode appear as a new one, which re-downloads it for everyone and can produce duplicates across directories. Generate a stable identifier at publish and treat it as immutable, including when a file is replaced or a title is corrected. The same discipline applies to the enclosure address. If the file moves, listeners with the old address get nothing, and clients that already downloaded may re-download. Decide the hosting layout before publishing the first episode, because changing it afterwards is a migration visible to your audience. ### Stack (fixed, do not substitute) - Audio: ffmpeg with loudness normalisation to a stated target · consistency between episodes matters more than any single episode's polish - Assembly: A declarative episode structure of segments · intro, content and outro assembled reproducibly rather than by hand each time - Hosting: Object storage with a stable address scheme decided up front · a moved file breaks every existing subscriber - Feed: Generated with immutable identifiers and validated before publish · the feed is consumed by clients you cannot fix - Runtime: Python 3.12 with a job queue · renders are long and must be resumable ### Data model (create this before Phase 1) - shows(id, title, author, category, explicit, artwork_path, feed_url, base_media_url) - episodes(id, show_id, number, season, title, description, guid, published_at, duration_seconds, state) - segments(episode_id, ordinal, kind, source_path, start_ms, end_ms, gain_db) · kind is intro, content, ad or outro - renders(episode_id, revision, output_path, bytes, loudness_lufs, rendered_at) - publications(episode_id, feed_revision, published_at, enclosure_url) · what the feed said and when - feed_revisions(id, show_id, xml_path, generated_at, validated, validator_output) - An episode's guid is written once at first publish and has no update path anywhere in the schema. ### Phase 1 · Audio that sounds the same every week Build: Normalise loudness to a stated target with a true peak ceiling. Report the measured loudness before and after. Handle a recording that is far too quiet without amplifying the noise floor to match. Trap: Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. Done when: Episodes measure within a stated tolerance of the loudness target and respect the true peak ceiling. A recording requiring excessive gain is reported rather than normalised into noise. Before and after measurements are recorded per render. Two consecutive episodes sound consistent when played back to back. Do not build yet: No assembly, hosting or feed. ### Phase 2 · Assembling an episode reproducibly Build: Assemble episodes from a declarative segment list with crossfades and per-segment gain. Make the render deterministic. Keep source files untouched and every render revision reproducible from its description. Trap: Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. Done when: The same segment list renders byte-identically twice. Source files are unmodified, asserted by checksum in the test suite. Changing the intro re-renders from original sources rather than from a previous render. Crossfades between segments produce no click, verified by inspecting the waveform. Do not build yet: No hosting or feed yet. ### Phase 3 · Addresses that will not move Build: Upload to object storage using an address scheme decided in advance and documented. Serve with correct content type and range request support. Verify a partial download resumes, since podcast clients rely on it. Trap: Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. Done when: Media addresses derive from an immutable identifier rather than from the title, verified by renaming an episode and confirming the address is unchanged. Range requests work and a partial download resumes. Content type and length headers are correct. The address scheme is documented before the first publication. Do not build yet: No feed generation yet. ### Phase 4 · A feed that is right the first time Build: Generate the feed with the required namespace tags, an immutable identifier per episode written once at publish, and validate before publishing. Verify in at least two real podcast clients. Trap: Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. Done when: An episode identifier is written once and has no update path, enforced by the schema. Correcting a title or replacing a file does not change the identifier, tested directly. The feed passes validation before publishing. The feed loads correctly in two real podcast clients including artwork and episode ordering. Do not build yet: No scheduling or analytics yet. ### Phase 5 · Scheduling, corrections and knowing it worked Build: Schedule publication with the feed updating at the right moment. Support correcting an episode's file or metadata without changing its identifier. Record download requests from your own storage for a basic sense of reach. Trap: Replacing an episode's audio file at the same address means clients that already downloaded keep the old version and new listeners get the new one, with no way to tell which anyone has. Decide whether corrections replace in place or publish a new address, and state the consequence either way rather than discovering it. Done when: A scheduled episode appears in the feed at the intended moment. Correcting a file follows a stated rule with its consequence documented. The identifier is unchanged by any correction. Download counts are recorded from storage access logs with their limitations stated. Do not build yet: Nothing after this. ### Out of scope (and why) - Transcription and show notes generation, which is a separate tool and a separate quality question. - Directory submission and management, which is manual and per directory. - Dynamic ad insertion, which requires serving audio per request and is a different hosting model entirely. - Meaningful analytics, which requires either a serving layer you control end to end or a hosting provider that specialises in it. ### README must contain - That an episode identifier is written once and has no update path, and why changing it re-downloads for everyone - That media addresses derive from an immutable identifier rather than from the title - That the address scheme is documented before the first episode because changing it is a migration your audience sees - That source recordings are never modified and renders are reproducible from their description - That an excessively quiet recording is reported rather than normalised into noise ===== ARCHITECTURE.md ===== # Architecture · Alitu ## Stack | Part | Choice | Why | | --- | --- | --- | | Audio | ffmpeg with loudness normalisation to a stated target | consistency between episodes matters more than any single episode's polish | | Assembly | A declarative episode structure of segments | intro, content and outro assembled reproducibly rather than by hand each time | | Hosting | Object storage with a stable address scheme decided up front | a moved file breaks every existing subscriber | | Feed | Generated with immutable identifiers and validated before publish | the feed is consumed by clients you cannot fix | | Runtime | Python 3.12 with a job queue | renders are long and must be resumable | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Audio | Loudness normalisation with a refusal threshold. | Any tooling, provided excessive gain is refused rather than applied. | | Assembly | Declarative, reproducible renders. | Any renderer, provided sources stay read only. | | Storage | Immutable addresses and range support. | Any provider, provided addresses do not derive from mutable data. | | Feed | Write-once identifiers and validation. | Not swappable for regenerated identifiers, which re-download for everyone. | | Publication | Scheduling and corrections with stated consequences. | Any implementation, provided identifiers survive corrections. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `SHOW_BASE_URL` · required · The media address prefix. Changing it later breaks every existing subscriber. - `FEED_URL` · required · Where the feed lives. Directories store this and it should never move. - `LOUDNESS_TARGET_LUFS` · required · Stated, and consistent across every episode. - `TRUE_PEAK_CEILING_DB` · required · So encoding does not introduce clipping after normalisation. - `MAX_GAIN_DB` · required · Above this a recording is reported rather than normalised into noise. - `STORAGE_CREDENTIALS` · required, secret · Scoped to the media bucket only. ## 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 · Alitu product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: ffmpeg with loudness normalisation to a stated target, A declarative episode structure of segments, Object storage with a stable address scheme decided up front, Generated with immutable identifiers and validated before publish, Python 3.12 with a job queue. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. - Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. - Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. - Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. - Replacing an episode's audio file at the same address means clients that already downloaded keep the old version and new listeners get the new one, with no way to tell which anyone has. Decide whether corrections replace in place or publish a new address, and state the consequence either way rather than discovering it. ===== MILESTONES.md ===== # Delivery milestones · Alitu Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Audio that sounds the same every week Episodes are consistent in level between themselves and against the platform. ### Steps 1. Normalise to a stated target with a peak ceiling Consistency between episodes is what listeners notice. Files: `src/loudness.py` 2. Measure and record before and after So a problem episode is explicable rather than mysterious. Files: `src/measure.py` 3. Refuse excessive gain Loud and hissy is worse than quiet. Report it instead. Files: `src/loudness.py` ### Done when - [ ] Episodes measure within tolerance of the target - [ ] Excessive gain is refused and reported - [ ] Measurements are recorded per render - [ ] Consecutive episodes sound consistent ### Watch out - Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. ## M2 · Assembling an episode reproducibly The same segments produce the same file every time. ### Steps 1. Describe the assembly declaratively So a render is reproducible rather than a one-off result. Files: `src/assemble.py` 2. Keep sources read only Checksummed in the suite. Processing twice is not recoverable. Files: `tests/test_sources_untouched.py` 3. Crossfade rather than butt-joining A hard join between segments is a click. Files: `src/assemble.py` ### Done when - [ ] The same segment list renders identically - [ ] Source files are unmodified - [ ] Re-rendering uses original sources - [ ] Crossfades produce no click ### Watch out - Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. ## M3 · Addresses that will not move Where an episode file lives is decided once and never changes. ### Steps 1. Derive addresses from something immutable Not the title. A typo correction should not move the file. Files: `src/storage.py` 2. Support range requests Podcast clients resume partial downloads constantly. Files: `src/storage.py` 3. Document the scheme before publishing Changing it later is a migration your audience experiences. Files: `docs/media-urls.md` ### Done when - [ ] Addresses are unaffected by a title change - [ ] Range requests and resumption work - [ ] Content type and length are correct - [ ] The address scheme is documented ### Watch out - Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. ## M4 · A feed that is right the first time The feed validates and its identifiers are permanent. ### Steps 1. Write the identifier once, no update path In the schema. This is the commitment that cannot be walked back. Files: `src/schema.sql`, `tests/test_guid_immutable.py` 2. Validate before publishing The feed is consumed by clients you cannot fix. Files: `src/validate_feed.py` 3. Test in real clients A validator passing is not the same as a client rendering it correctly. Files: `tests/clients.md` ### Done when - [ ] The identifier has no update path - [ ] Corrections do not change identifiers - [ ] The feed passes validation before publishing - [ ] Two real clients render the feed correctly ### Watch out - Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. ## M5 · Scheduling, corrections and knowing it worked (production only) Episodes release on time and a mistake can be fixed without breaking anyone. ### Steps 1. Publish on schedule The feed changes at the moment, not when someone remembers. Files: `src/schedule.py` 2. Define the correction rule Replace in place or new address. Both have consequences worth stating. Files: `docs/corrections.md` 3. Count downloads honestly From your own logs, with the limitations stated rather than implied away. Files: `src/analytics.py` ### Done when - [ ] Scheduled episodes appear at the right moment - [ ] Corrections follow a stated rule - [ ] Identifiers survive every correction - [ ] Download counts state their limitations ### Watch out - Replacing an episode's audio file at the same address means clients that already downloaded keep the old version and new listeners get the new one, with no way to tell which anyone has. Decide whether corrections replace in place or publish a new address, and state the consequence either way rather than discovering it. ===== OPERATIONS.md ===== # Operations · Alitu ## Backup Source recordings are irreplaceable and large; the feed and its identifiers are small and critical. Back up both, and treat the identifier records as the ones you cannot lose, since regenerating them would republish your entire back catalogue. ## Restore Restore and confirm every episode identifier matches what was published, since a restore that regenerates them would present the whole back catalogue as new to every subscriber. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Feed validation on every publish, and storage errors on media requests. A feed that stops validating is usually a metadata field, and it will be silently rejected by a directory rather than reported. ## Incident checklist Listeners re-downloading old episodes is the failure that matters, and it means an identifier changed. The write-once schema is the control, and the publication records say exactly what the feed asserted and when. 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 - [ ] The episode identifier is confirmed to have no update path - [ ] A title correction is confirmed not to change the media address - [ ] The feed is confirmed to validate before publishing - [ ] Two real clients are confirmed to render the feed correctly - [ ] Source recordings are confirmed unmodified by checksum ## Launch constraint Do not market omitted Alitu 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. The media address prefix. Changing it later breaks every existing subscriber. SHOW_BASE_URL=https://media.example.com/show # Required. Where the feed lives. Directories store this and it should never move. FEED_URL=https://example.com/feed.xml # Required. Stated, and consistent across every episode. LOUDNESS_TARGET_LUFS=-16 # Required. So encoding does not introduce clipping after normalisation. TRUE_PEAK_CEILING_DB=-1.0 # Required. Above this a recording is reported rather than normalised into noise. MAX_GAIN_DB=20 # Required · secret. Scoped to the media bucket only. STORAGE_CREDENTIALS=a scoped key
# Alitu · indie build A podcast production pipeline that levels and assembles episodes and then publishes a feed which behaves correctly in every client, where the discipline is that identifiers and addresses are permanent commitments rather than implementation details. 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 | | --- | --- | --- | | Audio | ffmpeg with loudness normalisation to a stated target | consistency between episodes matters more than any single episode's polish | | Assembly | A declarative episode structure of segments | intro, content and outro assembled reproducibly rather than by hand each time | | Hosting | Object storage with a stable address scheme decided up front | a moved file breaks every existing subscriber | | Feed | Generated with immutable identifiers and validated before publish | the feed is consumed by clients you cannot fix | | Runtime | Python 3.12 with a job queue | renders are long and must be resumable | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Orchestrates rendering, upload and feed generation. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with loudness measurement** · free - Why: Normalisation, assembly and measurement all run through it, and reimplementing any of it is not the project. - Get it: Your package manager, confirming the loudness filter is available. - Verify: It measures and normalises a file to a stated target - [ ] **Object storage with range request support** · pennies per month plus bandwidth, which is the real cost - Why: Podcast clients resume partial downloads, and storage that does not support ranges produces failures that look like corrupt files. - Get it: Any provider serving range requests with correct content types. Decide the address scheme before uploading anything. - Verify: A partial download resumes correctly from the stored file - [ ] **Two real podcast clients** · free - Why: A feed passing validation is not the same as a client rendering it correctly, and the differences between clients are where the surprises live. - Get it: Install two different podcast apps and subscribe to your feed by address before publishing publicly. - Verify: Both clients show artwork, episode order and descriptions correctly - [ ] **Your identifier and address scheme, written first** · free - Why: Both are permanent commitments visible to every subscriber, and changing either after publishing is a migration your audience experiences as broken episodes. - Get it: Decide how identifiers are generated and how media addresses are formed, from immutable values only. Write it down before the first publish. - Verify: A written document states both schemes and confirms neither derives from mutable data ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Transcription and show notes. - Directory submission and management. - Dynamic ad insertion. - Meaningful analytics. - production automation, hosting integrations, support, and creator-specific polish - 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 Alitu, and the README should say so rather than pretend.
# Build brief · Alitu 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 podcast production and publishing where the feed is correct and stays correct to replace Alitu. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The feed is the product. Everything else here is audio processing that can be redone, but a podcast feed is consumed by apps that cache it and by directories that index it, and getting it wrong is visible to every listener at once. The specific thing to get right is the episode identifier. It must never change once published. Apps use it to know what a listener has already heard, so changing it makes an existing episode appear as a new one, which re-downloads it for everyone and can produce duplicates across directories. Generate a stable identifier at publish and treat it as immutable, including when a file is replaced or a title is corrected. The same discipline applies to the enclosure address. If the file moves, listeners with the old address get nothing, and clients that already downloaded may re-download. Decide the hosting layout before publishing the first episode, because changing it afterwards is a migration visible to your audience. ### Stack (fixed, do not substitute) - Audio: ffmpeg with loudness normalisation to a stated target · consistency between episodes matters more than any single episode's polish - Assembly: A declarative episode structure of segments · intro, content and outro assembled reproducibly rather than by hand each time - Hosting: Object storage with a stable address scheme decided up front · a moved file breaks every existing subscriber - Feed: Generated with immutable identifiers and validated before publish · the feed is consumed by clients you cannot fix - Runtime: Python 3.12 with a job queue · renders are long and must be resumable ### Data model (create this before Phase 1) - shows(id, title, author, category, explicit, artwork_path, feed_url, base_media_url) - episodes(id, show_id, number, season, title, description, guid, published_at, duration_seconds, state) - segments(episode_id, ordinal, kind, source_path, start_ms, end_ms, gain_db) · kind is intro, content, ad or outro - renders(episode_id, revision, output_path, bytes, loudness_lufs, rendered_at) - publications(episode_id, feed_revision, published_at, enclosure_url) · what the feed said and when - feed_revisions(id, show_id, xml_path, generated_at, validated, validator_output) - An episode's guid is written once at first publish and has no update path anywhere in the schema. ### Phase 1 · Audio that sounds the same every week Build: Normalise loudness to a stated target with a true peak ceiling. Report the measured loudness before and after. Handle a recording that is far too quiet without amplifying the noise floor to match. Trap: Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. Done when: Episodes measure within a stated tolerance of the loudness target and respect the true peak ceiling. A recording requiring excessive gain is reported rather than normalised into noise. Before and after measurements are recorded per render. Two consecutive episodes sound consistent when played back to back. Do not build yet: No assembly, hosting or feed. ### Phase 2 · Assembling an episode reproducibly Build: Assemble episodes from a declarative segment list with crossfades and per-segment gain. Make the render deterministic. Keep source files untouched and every render revision reproducible from its description. Trap: Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. Done when: The same segment list renders byte-identically twice. Source files are unmodified, asserted by checksum in the test suite. Changing the intro re-renders from original sources rather than from a previous render. Crossfades between segments produce no click, verified by inspecting the waveform. Do not build yet: No hosting or feed yet. ### Phase 3 · Addresses that will not move Build: Upload to object storage using an address scheme decided in advance and documented. Serve with correct content type and range request support. Verify a partial download resumes, since podcast clients rely on it. Trap: Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. Done when: Media addresses derive from an immutable identifier rather than from the title, verified by renaming an episode and confirming the address is unchanged. Range requests work and a partial download resumes. Content type and length headers are correct. The address scheme is documented before the first publication. Do not build yet: No feed generation yet. ### Phase 4 · A feed that is right the first time Build: Generate the feed with the required namespace tags, an immutable identifier per episode written once at publish, and validate before publishing. Verify in at least two real podcast clients. Trap: Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. Done when: An episode identifier is written once and has no update path, enforced by the schema. Correcting a title or replacing a file does not change the identifier, tested directly. The feed passes validation before publishing. The feed loads correctly in two real podcast clients including artwork and episode ordering. Do not build yet: No scheduling or analytics yet. ### Phase 5 · Scheduling, corrections and knowing it worked Build: Schedule publication with the feed updating at the right moment. Support correcting an episode's file or metadata without changing its identifier. Record download requests from your own storage for a basic sense of reach. Trap: Replacing an episode's audio file at the same address means clients that already downloaded keep the old version and new listeners get the new one, with no way to tell which anyone has. Decide whether corrections replace in place or publish a new address, and state the consequence either way rather than discovering it. Done when: A scheduled episode appears in the feed at the intended moment. Correcting a file follows a stated rule with its consequence documented. The identifier is unchanged by any correction. Download counts are recorded from storage access logs with their limitations stated. Do not build yet: Nothing after this. ### Out of scope (and why) - Transcription and show notes generation, which is a separate tool and a separate quality question. - Directory submission and management, which is manual and per directory. - Dynamic ad insertion, which requires serving audio per request and is a different hosting model entirely. - Meaningful analytics, which requires either a serving layer you control end to end or a hosting provider that specialises in it. ### README must contain - That an episode identifier is written once and has no update path, and why changing it re-downloads for everyone - That media addresses derive from an immutable identifier rather than from the title - That the address scheme is documented before the first episode because changing it is a migration your audience sees - That source recordings are never modified and renders are reproducible from their description - That an excessively quiet recording is reported rather than normalised into noise
# Agent instructions · Alitu indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg with loudness normalisation to a stated target, A declarative episode structure of segments, Object storage with a stable address scheme decided up front, Generated with immutable identifiers and validated before publish, Python 3.12 with a job queue. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. - Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. - Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. - Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema.
# Build plan · Alitu A podcast production pipeline that levels and assembles episodes and then publishes a feed which behaves correctly in every client, where the discipline is that identifiers and addresses are permanent commitments rather than implementation details. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Audio that sounds the same every week Episodes are consistent in level between themselves and against the platform. ### Steps 1. Normalise to a stated target with a peak ceiling Consistency between episodes is what listeners notice. Files: `src/loudness.py` 2. Measure and record before and after So a problem episode is explicable rather than mysterious. Files: `src/measure.py` 3. Refuse excessive gain Loud and hissy is worse than quiet. Report it instead. Files: `src/loudness.py` ### Done when - [ ] Episodes measure within tolerance of the target - [ ] Excessive gain is refused and reported - [ ] Measurements are recorded per render - [ ] Consecutive episodes sound consistent ### Watch out - Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. ## Phase 2 · Assembling an episode reproducibly The same segments produce the same file every time. ### Steps 1. Describe the assembly declaratively So a render is reproducible rather than a one-off result. Files: `src/assemble.py` 2. Keep sources read only Checksummed in the suite. Processing twice is not recoverable. Files: `tests/test_sources_untouched.py` 3. Crossfade rather than butt-joining A hard join between segments is a click. Files: `src/assemble.py` ### Done when - [ ] The same segment list renders identically - [ ] Source files are unmodified - [ ] Re-rendering uses original sources - [ ] Crossfades produce no click ### Watch out - Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. ## Phase 3 · Addresses that will not move Where an episode file lives is decided once and never changes. ### Steps 1. Derive addresses from something immutable Not the title. A typo correction should not move the file. Files: `src/storage.py` 2. Support range requests Podcast clients resume partial downloads constantly. Files: `src/storage.py` 3. Document the scheme before publishing Changing it later is a migration your audience experiences. Files: `docs/media-urls.md` ### Done when - [ ] Addresses are unaffected by a title change - [ ] Range requests and resumption work - [ ] Content type and length are correct - [ ] The address scheme is documented ### Watch out - Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. ## Phase 4 · A feed that is right the first time The feed validates and its identifiers are permanent. ### Steps 1. Write the identifier once, no update path In the schema. This is the commitment that cannot be walked back. Files: `src/schema.sql`, `tests/test_guid_immutable.py` 2. Validate before publishing The feed is consumed by clients you cannot fix. Files: `src/validate_feed.py` 3. Test in real clients A validator passing is not the same as a client rendering it correctly. Files: `tests/clients.md` ### Done when - [ ] The identifier has no update path - [ ] Corrections do not change identifiers - [ ] The feed passes validation before publishing - [ ] Two real clients render the feed correctly ### Watch out - Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. ## Not in this build - Transcription and show notes. - Directory submission and management. - Dynamic ad insertion. - Meaningful analytics. ## After v1, if you want it - Chapter markers, which several clients support and few shows use - A transcript published alongside, which helps discovery and accessibility together - Trailer and bonus episode types, once the feed handling is proven
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The media address prefix. Changing it later breaks every existing subscriber. SHOW_BASE_URL=https://media.example.com/show # Required. Where the feed lives. Directories store this and it should never move. FEED_URL=https://example.com/feed.xml # Required. Stated, and consistent across every episode. LOUDNESS_TARGET_LUFS=-16 # Required. So encoding does not introduce clipping after normalisation. TRUE_PEAK_CEILING_DB=-1.0 # Required. Above this a recording is reported rather than normalised into noise. MAX_GAIN_DB=20 # Required · secret. Scoped to the media bucket only. STORAGE_CREDENTIALS=a scoped key
# Alitu · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Alitu, clean, assemble, publish, and package a recurring spoken-word podcast episode. The hard boundary is production automation, hosting integrations, support, and creator-specific polish, plus audio infrastructure, distribution, and production polish. ## Product outcome A podcast pipeline that produces consistently levelled episodes and a feed whose identifiers and addresses never move, so a correction never costs your listeners a re-download. ## 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 - Transcription and show notes. - Directory submission and management. - Dynamic ad insertion. - Meaningful analytics. - production automation, hosting integrations, support, and creator-specific polish - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support ## Success criteria - The episode identifier is confirmed to have no update path - A title correction is confirmed not to change the media address - The feed is confirmed to validate before publishing - Two real clients are confirmed to render the feed correctly - Source recordings are confirmed unmodified by checksum
# Build brief · Alitu 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 podcast production and publishing where the feed is correct and stays correct to replace Alitu. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The feed is the product. Everything else here is audio processing that can be redone, but a podcast feed is consumed by apps that cache it and by directories that index it, and getting it wrong is visible to every listener at once. The specific thing to get right is the episode identifier. It must never change once published. Apps use it to know what a listener has already heard, so changing it makes an existing episode appear as a new one, which re-downloads it for everyone and can produce duplicates across directories. Generate a stable identifier at publish and treat it as immutable, including when a file is replaced or a title is corrected. The same discipline applies to the enclosure address. If the file moves, listeners with the old address get nothing, and clients that already downloaded may re-download. Decide the hosting layout before publishing the first episode, because changing it afterwards is a migration visible to your audience. ### Stack (fixed, do not substitute) - Audio: ffmpeg with loudness normalisation to a stated target · consistency between episodes matters more than any single episode's polish - Assembly: A declarative episode structure of segments · intro, content and outro assembled reproducibly rather than by hand each time - Hosting: Object storage with a stable address scheme decided up front · a moved file breaks every existing subscriber - Feed: Generated with immutable identifiers and validated before publish · the feed is consumed by clients you cannot fix - Runtime: Python 3.12 with a job queue · renders are long and must be resumable ### Data model (create this before Phase 1) - shows(id, title, author, category, explicit, artwork_path, feed_url, base_media_url) - episodes(id, show_id, number, season, title, description, guid, published_at, duration_seconds, state) - segments(episode_id, ordinal, kind, source_path, start_ms, end_ms, gain_db) · kind is intro, content, ad or outro - renders(episode_id, revision, output_path, bytes, loudness_lufs, rendered_at) - publications(episode_id, feed_revision, published_at, enclosure_url) · what the feed said and when - feed_revisions(id, show_id, xml_path, generated_at, validated, validator_output) - An episode's guid is written once at first publish and has no update path anywhere in the schema. ### Phase 1 · Audio that sounds the same every week Build: Normalise loudness to a stated target with a true peak ceiling. Report the measured loudness before and after. Handle a recording that is far too quiet without amplifying the noise floor to match. Trap: Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. Done when: Episodes measure within a stated tolerance of the loudness target and respect the true peak ceiling. A recording requiring excessive gain is reported rather than normalised into noise. Before and after measurements are recorded per render. Two consecutive episodes sound consistent when played back to back. Do not build yet: No assembly, hosting or feed. ### Phase 2 · Assembling an episode reproducibly Build: Assemble episodes from a declarative segment list with crossfades and per-segment gain. Make the render deterministic. Keep source files untouched and every render revision reproducible from its description. Trap: Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. Done when: The same segment list renders byte-identically twice. Source files are unmodified, asserted by checksum in the test suite. Changing the intro re-renders from original sources rather than from a previous render. Crossfades between segments produce no click, verified by inspecting the waveform. Do not build yet: No hosting or feed yet. ### Phase 3 · Addresses that will not move Build: Upload to object storage using an address scheme decided in advance and documented. Serve with correct content type and range request support. Verify a partial download resumes, since podcast clients rely on it. Trap: Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. Done when: Media addresses derive from an immutable identifier rather than from the title, verified by renaming an episode and confirming the address is unchanged. Range requests work and a partial download resumes. Content type and length headers are correct. The address scheme is documented before the first publication. Do not build yet: No feed generation yet. ### Phase 4 · A feed that is right the first time Build: Generate the feed with the required namespace tags, an immutable identifier per episode written once at publish, and validate before publishing. Verify in at least two real podcast clients. Trap: Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. Done when: An episode identifier is written once and has no update path, enforced by the schema. Correcting a title or replacing a file does not change the identifier, tested directly. The feed passes validation before publishing. The feed loads correctly in two real podcast clients including artwork and episode ordering. Do not build yet: No scheduling or analytics yet. ### Phase 5 · Scheduling, corrections and knowing it worked Build: Schedule publication with the feed updating at the right moment. Support correcting an episode's file or metadata without changing its identifier. Record download requests from your own storage for a basic sense of reach. Trap: Replacing an episode's audio file at the same address means clients that already downloaded keep the old version and new listeners get the new one, with no way to tell which anyone has. Decide whether corrections replace in place or publish a new address, and state the consequence either way rather than discovering it. Done when: A scheduled episode appears in the feed at the intended moment. Correcting a file follows a stated rule with its consequence documented. The identifier is unchanged by any correction. Download counts are recorded from storage access logs with their limitations stated. Do not build yet: Nothing after this. ### Out of scope (and why) - Transcription and show notes generation, which is a separate tool and a separate quality question. - Directory submission and management, which is manual and per directory. - Dynamic ad insertion, which requires serving audio per request and is a different hosting model entirely. - Meaningful analytics, which requires either a serving layer you control end to end or a hosting provider that specialises in it. ### README must contain - That an episode identifier is written once and has no update path, and why changing it re-downloads for everyone - That media addresses derive from an immutable identifier rather than from the title - That the address scheme is documented before the first episode because changing it is a migration your audience sees - That source recordings are never modified and renders are reproducible from their description - That an excessively quiet recording is reported rather than normalised into noise
# Architecture · Alitu ## Stack | Part | Choice | Why | | --- | --- | --- | | Audio | ffmpeg with loudness normalisation to a stated target | consistency between episodes matters more than any single episode's polish | | Assembly | A declarative episode structure of segments | intro, content and outro assembled reproducibly rather than by hand each time | | Hosting | Object storage with a stable address scheme decided up front | a moved file breaks every existing subscriber | | Feed | Generated with immutable identifiers and validated before publish | the feed is consumed by clients you cannot fix | | Runtime | Python 3.12 with a job queue | renders are long and must be resumable | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Audio | Loudness normalisation with a refusal threshold. | Any tooling, provided excessive gain is refused rather than applied. | | Assembly | Declarative, reproducible renders. | Any renderer, provided sources stay read only. | | Storage | Immutable addresses and range support. | Any provider, provided addresses do not derive from mutable data. | | Feed | Write-once identifiers and validation. | Not swappable for regenerated identifiers, which re-download for everyone. | | Publication | Scheduling and corrections with stated consequences. | Any implementation, provided identifiers survive corrections. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `SHOW_BASE_URL` · required · The media address prefix. Changing it later breaks every existing subscriber. - `FEED_URL` · required · Where the feed lives. Directories store this and it should never move. - `LOUDNESS_TARGET_LUFS` · required · Stated, and consistent across every episode. - `TRUE_PEAK_CEILING_DB` · required · So encoding does not introduce clipping after normalisation. - `MAX_GAIN_DB` · required · Above this a recording is reported rather than normalised into noise. - `STORAGE_CREDENTIALS` · required, secret · Scoped to the media bucket only. ## 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 · Alitu product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: ffmpeg with loudness normalisation to a stated target, A declarative episode structure of segments, Object storage with a stable address scheme decided up front, Generated with immutable identifiers and validated before publish, Python 3.12 with a job queue. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. - Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. - Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. - Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. - Replacing an episode's audio file at the same address means clients that already downloaded keep the old version and new listeners get the new one, with no way to tell which anyone has. Decide whether corrections replace in place or publish a new address, and state the consequence either way rather than discovering it.
# Delivery milestones · Alitu Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Audio that sounds the same every week Episodes are consistent in level between themselves and against the platform. ### Steps 1. Normalise to a stated target with a peak ceiling Consistency between episodes is what listeners notice. Files: `src/loudness.py` 2. Measure and record before and after So a problem episode is explicable rather than mysterious. Files: `src/measure.py` 3. Refuse excessive gain Loud and hissy is worse than quiet. Report it instead. Files: `src/loudness.py` ### Done when - [ ] Episodes measure within tolerance of the target - [ ] Excessive gain is refused and reported - [ ] Measurements are recorded per render - [ ] Consecutive episodes sound consistent ### Watch out - Normalising a quiet recording to the target by applying enormous gain brings the room noise up with it, producing an episode that is loud and hissy rather than quiet and clean. Measure first, and report that a recording needs re-recording rather than fixing it beyond what the material supports. ## M2 · Assembling an episode reproducibly The same segments produce the same file every time. ### Steps 1. Describe the assembly declaratively So a render is reproducible rather than a one-off result. Files: `src/assemble.py` 2. Keep sources read only Checksummed in the suite. Processing twice is not recoverable. Files: `tests/test_sources_untouched.py` 3. Crossfade rather than butt-joining A hard join between segments is a click. Files: `src/assemble.py` ### Done when - [ ] The same segment list renders identically - [ ] Source files are unmodified - [ ] Re-rendering uses original sources - [ ] Crossfades produce no click ### Watch out - Editing the source recording in place to fix a level means the original is gone and the next time you need to re-render with a different intro you are working from a file that has already been processed twice. Keep sources read only, describe the assembly, and render from both. ## M3 · Addresses that will not move Where an episode file lives is decided once and never changes. ### Steps 1. Derive addresses from something immutable Not the title. A typo correction should not move the file. Files: `src/storage.py` 2. Support range requests Podcast clients resume partial downloads constantly. Files: `src/storage.py` 3. Document the scheme before publishing Changing it later is a migration your audience experiences. Files: `docs/media-urls.md` ### Done when - [ ] Addresses are unaffected by a title change - [ ] Range requests and resumption work - [ ] Content type and length are correct - [ ] The address scheme is documented ### Watch out - Building the media address from the episode title means correcting a typo in the title moves the file, breaking every client that already has the old address and potentially causing re-downloads. Derive the address from something immutable, and document the scheme before the first episode goes out. ## M4 · A feed that is right the first time The feed validates and its identifiers are permanent. ### Steps 1. Write the identifier once, no update path In the schema. This is the commitment that cannot be walked back. Files: `src/schema.sql`, `tests/test_guid_immutable.py` 2. Validate before publishing The feed is consumed by clients you cannot fix. Files: `src/validate_feed.py` 3. Test in real clients A validator passing is not the same as a client rendering it correctly. Files: `tests/clients.md` ### Done when - [ ] The identifier has no update path - [ ] Corrections do not change identifiers - [ ] The feed passes validation before publishing - [ ] Two real clients render the feed correctly ### Watch out - Regenerating identifiers from episode data means any correction changes them, and a changed identifier makes an already-heard episode appear as new to every subscriber, re-downloading it and sometimes duplicating it in directories. Write the identifier once at first publish and give it no update path in the schema. ## M5 · Scheduling, corrections and knowing it worked (production only) Episodes release on time and a mistake can be fixed without breaking anyone. ### Steps 1. Publish on schedule The feed changes at the moment, not when someone remembers. Files: `src/schedule.py` 2. Define the correction rule Replace in place or new address. Both have consequences worth stating. Files: `docs/corrections.md` 3. Count downloads honestly From your own logs, with the limitations stated rather than implied away. Files: `src/analytics.py` ### Done when - [ ] Scheduled episodes appear at the right moment - [ ] Corrections follow a stated rule - [ ] Identifiers survive every correction - [ ] Download counts state their limitations ### Watch out - Replacing an episode's audio file at the same address means clients that already downloaded keep the old version and new listeners get the new one, with no way to tell which anyone has. Decide whether corrections replace in place or publish a new address, and state the consequence either way rather than discovering it.
# Operations · Alitu ## Backup Source recordings are irreplaceable and large; the feed and its identifiers are small and critical. Back up both, and treat the identifier records as the ones you cannot lose, since regenerating them would republish your entire back catalogue. ## Restore Restore and confirm every episode identifier matches what was published, since a restore that regenerates them would present the whole back catalogue as new to every subscriber. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Feed validation on every publish, and storage errors on media requests. A feed that stops validating is usually a metadata field, and it will be silently rejected by a directory rather than reported. ## Incident checklist Listeners re-downloading old episodes is the failure that matters, and it means an identifier changed. The write-once schema is the control, and the publication records say exactly what the feed asserted and when. 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 - [ ] The episode identifier is confirmed to have no update path - [ ] A title correction is confirmed not to change the media address - [ ] The feed is confirmed to validate before publishing - [ ] Two real clients are confirmed to render the feed correctly - [ ] Source recordings are confirmed unmodified by checksum ## Launch constraint Do not market omitted Alitu 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. The media address prefix. Changing it later breaks every existing subscriber. SHOW_BASE_URL=https://media.example.com/show # Required. Where the feed lives. Directories store this and it should never move. FEED_URL=https://example.com/feed.xml # Required. Stated, and consistent across every episode. LOUDNESS_TARGET_LUFS=-16 # Required. So encoding does not introduce clipping after normalisation. TRUE_PEAK_CEILING_DB=-1.0 # Required. Above this a recording is reported rather than normalised into noise. MAX_GAIN_DB=20 # Required · secret. Scoped to the media bucket only. STORAGE_CREDENTIALS=a scoped key
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Alitu 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.
xproduction automation, hosting integrations, support, and creator-specific polish
xremote studio reliability
xlicensed music libraries
xhosting distribution
xadvanced mastering and support
Alitu pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| podcaster | $38/workspace | $20.58/workspace | 1 podcast; editing, cleanup, transcription, publishing and hosting; hosting includes 1,000 downloads/month |
| done-for-you editing | $295/workspace | — | Professional recurring episode editing; exact episode-frequency and length allowance depends on the selected service |
free tierno free tier; 7-day trial
billingmonthly + annual for Podcaster; 30-day money-back guarantee
hidden costsHosting above 1,000 downloads/month costs $10/month up to 10,000 downloads; a second podcast normally doubles the subscription, with a discounted two-show price available.
verified 2026-08-14 · source ↗
Vibecode Alitu
Kinda. The core of Alitu is buildable in a weekend with the prompt on this page, but there are real gaps: production automation, hosting integrations, support, and creator-specific polish, remote studio reliability. Read the honest list above before committing.
How much does Alitu cost?
Alitu costs about $38/month (Podcaster, checked 2026-07-31), which is $456 per year.
What do I lose by replacing Alitu?
Honestly: production automation, hosting integrations, support, and creator-specific polish; 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 Alitu?
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.