Vibecode Vyravid
track this build5 phases, 15 steps, beginner friendly0%A local version of the core workflow is buildable: project dashboard, script-to-scenes generation, voiceover, media generation, timeline assembly, and local ffmpeg rendering. The hard parts are not the CRUD shell but the media edge cases, provider integrations, long-running render jobs, and the editor polish that makes the timeline pleasant instead of fragile.
You are building a lean indie version of Vyravid. 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 ===== # Vyravid · indie build A local pipeline turning a script into a short video: synthetic voiceover with word timings, the script split into scenes, media assigned per scene from a library whose licences you recorded, captions generated from the real word timings rather than estimated, and a deterministic render you can re-run after a change. 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 | | --- | --- | --- | | Render | ffmpeg driven by a filter graph built from the timeline | one render pass from a described timeline, reproducible and scriptable | | Runtime | Python 3.12 with SQLite | the work is orchestration and bookkeeping over media tools | | Voice | A speech engine that returns word-level timings | captions must come from when words were actually spoken, not from an estimate | | Media | A local library with licence recorded per asset | the licence question has to be answered at import, because it cannot be answered later | | Timeline | Scenes and clips as data, never a rendered intermediate | a correction should re-render rather than require remaking | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the pipeline and builds the filter graph. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with the encoders you need** · free - Why: The render is one filter graph through it. A build missing an encoder fails at the last step. - Get it: brew install ffmpeg or your package manager. Confirm your target encoder is present. - Verify: ffmpeg -encoders lists your video and audio encoders - [ ] **A speech engine returning word timings** · local engines are free and slower; hosted ones are priced per character - Why: Captions and scene boundaries both derive from them, and estimated timings drift visibly within a minute. - Get it: Choose an engine that reports word-level timings, or plan a forced alignment step. Confirm the timings exist before building on them. - Verify: Generated speech comes with per-word start and end times - [ ] **Media whose licences you have read** · free with openly licensed material - Why: A render is blocked on a licence mismatch by design, so material with unknown terms cannot be used at all. - Get it: Openly licensed libraries, material you created, or stock you have bought. Record the terms per asset as you import. - Verify: At least ten assets are imported with licences and check dates - [ ] **A phone to check the output on** · free - Why: Caption safe areas and legibility are only judgeable where the video will be watched, and a desktop player hides the platform interface that covers the edges. - Get it: Any phone. Play the export and watch for captions behind interface elements. - Verify: An export has been checked on a phone ## 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: - The stock media library. - Motion design and visual polish. - Direct publishing to platforms. - Any claim that generated video performs. - someone else's servers & support - provider-specific voice/image/video integration details - timeline rendering polish - custom voice and character workflows If one of those is essential to you, that is the reason to keep paying for Vyravid, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Vyravid The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a script to short video pipeline with synthetic voiceover, media and captions to replace Vyravid. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things have to be settled before this produces anything you publish. Every piece of media you use has a licence: stock footage, music and images each have terms about commercial use, modification and attribution, and a video assembled from material you did not check is a problem that surfaces after it does well rather than before. And a synthetic voice should be identifiable as one. Disclosure requirements for synthetic media are appearing in several jurisdictions and platform policies already require it in places. Label the output and keep a record of what was generated, because you cannot reconstruct that later. Build the timeline as data with a deterministic render, so a video can be corrected and re-rendered rather than remade. ### Stack (fixed, do not substitute) - Render: ffmpeg driven by a filter graph built from the timeline · one render pass from a described timeline, reproducible and scriptable - Runtime: Python 3.12 with SQLite · the work is orchestration and bookkeeping over media tools - Voice: A speech engine that returns word-level timings · captions must come from when words were actually spoken, not from an estimate - Media: A local library with licence recorded per asset · the licence question has to be answered at import, because it cannot be answered later - Timeline: Scenes and clips as data, never a rendered intermediate · a correction should re-render rather than require remaking ### Data model (create this before Phase 1) - projects(id, title, aspect, fps, created_at) - scripts(id, project_id, text, version, created_at) - scenes(id, project_id, ordinal, text, start_ms, end_ms, media_asset_id, effect_json) - voice_words(scene_id, ordinal, word, start_ms, end_ms) · the spine captions and scene timing both use - assets(id, path, kind, source, licence, commercial_use, attribution_required, checked_on) - renders(id, project_id, settings_hash, output_path, rendered_at, duration_ms) - provenance(project_id, note) · what was synthesised, recorded because it cannot be reconstructed - Scene boundaries come from word timings rather than from a guess at reading speed, which is what keeps captions and cuts aligned. ### Phase 1 · Voice with real word timings Build: Generate speech from the script with a engine that returns word-level timings, or generate the audio and align it to the script afterwards. Store timings per word. Verify by playing a single word's range. Trap: Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. Done when: Playing a single word's range plays that word. Word timings are monotonic with no overlaps. The total duration matches the audio file. A script with numbers and abbreviations produces timings covering how they were actually spoken rather than how they were written. Do not build yet: No scenes, media or render. ### Phase 2 · Scenes derived from the speech Build: Split the script into scenes at sentence boundaries, and set each scene's time range from the word timings rather than by dividing the duration. Enforce a minimum and maximum scene length by merging or splitting. Trap: Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. Done when: Scene boundaries fall in gaps between spoken words, verified by checking three against the audio. A scene shorter than the minimum merges with its neighbour. A scene longer than the maximum splits at an internal sentence boundary. Scene times sum to the audio duration. Do not build yet: No media, captions or render yet. ### Phase 3 · Media with its licence recorded Build: Import media into a library recording source, licence, commercial use and attribution requirement per asset, with a check date. Assign assets to scenes. Refuse to render a project containing an asset whose licence does not cover your declared use. Trap: Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. Done when: Importing an asset requires a licence and a check date. A project using an asset whose licence excludes your declared use is blocked from rendering with the asset named. Attribution text is collected for assets that require it. The declared use is recorded on the project. Do not build yet: No captions or render yet. ### Phase 4 · Captions from the timings you already have Build: Group words into caption lines of a readable length and duration, using the word timings for their start and end. Render captions with a safe area margin so nothing sits under a platform's interface. Export a subtitle file alongside the video. Trap: Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. Done when: Captions appear at the word timings, verified by watching a section. No caption falls outside the safe area, checked by playing the export on a phone. Lines respect a maximum length and minimum duration. A separate subtitle file is exported that matches the burned captions. Do not build yet: No render pipeline or batch yet. ### Phase 5 · A render you can repeat Build: Build one ffmpeg filter graph from the timeline and render in a single pass. Record the settings hash on the render. Re-render after changing one scene and confirm only that changed. Never overwrite a previous render. Trap: Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. Done when: A project renders in a single pass with no intermediate files. The same timeline renders byte-identical output twice. Changing one scene's text and re-rendering produces a video differing only where expected. Renders are kept rather than overwritten, each with its settings hash. Do not build yet: Disclosure and batch come last. ### Phase 6 · Disclosure, batch and the honest limits Build: Record what was synthesised per project and label the output, in the video and in its metadata. Batch render a directory of scripts. Then write down honestly what this produces and what it does not. Trap: A synthetic voice presented as a person's is a problem that grows rather than fades, and disclosure requirements for synthetic media are appearing in more places rather than fewer. Label at render time so it cannot be forgotten, and keep the provenance record whatever you decide about the label. Done when: Every rendered video carries a synthetic media label in its metadata and, where you chose, on screen. The provenance record lists what was generated per project. A directory of scripts renders unattended with per-project failures reported. The documentation states what this produces and its limits. Do not build yet: Nothing after this. ### Out of scope (and why) - The stock media library, which is a licensing operation with a continuing bill and is a large part of what the hosted product provides. - Motion design and the visual polish that distinguishes a good short video from an assembled one. - Publishing directly to platforms, each of which is an API and an account relationship. - Any claim that generated video performs. Assembly is cheap; the reason a video works is the script, which this does not write for you. ### README must contain - That every asset carries a recorded licence and a render is blocked on a mismatch - That captions come from real word timings rather than an estimated reading speed - That scene boundaries land in gaps between spoken words - That renders are deterministic and kept with their settings hash - That synthetic content is labelled in metadata and what the provenance record holds ===== AGENTS.md ===== # Agent instructions · Vyravid indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg driven by a filter graph built from the timeline, Python 3.12 with SQLite, A speech engine that returns word-level timings, A local library with licence recorded per asset, Scenes and clips as data, never a rendered intermediate. 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 - Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. - Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. - Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. - Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. - Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. ===== BUILD_PLAN.md ===== # Build plan · Vyravid A local pipeline turning a script into a short video: synthetic voiceover with word timings, the script split into scenes, media assigned per scene from a library whose licences you recorded, captions generated from the real word timings rather than estimated, and a deterministic render you can re-run after a change. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Voice with real word timings The voiceover exists and you know when every word was spoken. ### Steps 1. Generate the voiceover With word timings if the engine offers them, or align afterwards if it does not. Files: `src/voice.py` 2. Store timings per word The spine that scenes and captions both derive from. Files: `src/timings.py` 3. Verify by playing one word From the middle of a sentence. Estimated timings fail this immediately. ### Done when - [ ] A single word's range plays that word - [ ] Timings are monotonic with no overlaps - [ ] Total duration matches the audio - [ ] Numbers and abbreviations are timed as spoken ### Watch out - Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. ## Phase 2 · Scenes derived from the speech The script splits into scenes whose boundaries land at real pauses. ### Steps 1. Split at sentence boundaries Then take times from the word timings for those sentences. Files: `src/scenes.py` 2. Enforce length bounds Merge short scenes, split long ones at an internal boundary rather than arbitrarily. Files: `src/scenes.py` 3. Verify cuts land in gaps Check three boundaries against the audio. A cut mid-word is the visible failure. ### Done when - [ ] Boundaries fall in gaps between words - [ ] Short scenes merge and long ones split at sentence boundaries - [ ] Scene times sum to the audio duration - [ ] No cut lands mid-word ### Watch out - Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. ## Phase 3 · Media with its licence recorded Every clip has an answer to where it came from and what you may do with it. ### Steps 1. Require licence at import Source, terms, commercial use, attribution, check date. Not optional. Files: `src/assets.py` 2. Block on mismatch Against the project's declared use. A warning gets clicked through. Files: `src/render.py` 3. Collect attribution Automatically, so the requirement is met by construction rather than by memory. Files: `src/attribution.py` ### Done when - [ ] Import requires a licence and a check date - [ ] A licence mismatch blocks the render and names the asset - [ ] Attribution text is collected where required - [ ] The declared use is recorded on the project ### Watch out - Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. ## Phase 4 · Captions from the timings you already have Captions appear exactly when the words are spoken and are readable. ### Steps 1. Group words into lines Maximum characters and minimum on-screen time. Two constraints, both needed. Files: `src/captions.py` 2. Respect a safe area Margins from every edge. Verify on a phone, not in a desktop player. Files: `src/captions.py` 3. Export a subtitle file too Not everyone wants burned captions, and the file is also an accessibility artefact. Files: `src/captions.py` ### Done when - [ ] Captions appear at the word timings - [ ] No caption falls outside the safe area on a phone - [ ] Lines respect length and duration limits - [ ] The subtitle file matches the burned captions ### Watch out - Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. ## Phase 5 · A render you can repeat The video renders from the timeline, and the same timeline renders the same video. ### Steps 1. Build one filter graph Concatenation, overlays and audio in a single invocation. Files: `src/render.py` 2. Pin anything nondeterministic Seeds and encoder settings, so two renders match byte for byte. Files: `tests/test_determinism.py` 3. Keep every render With its settings hash. Overwriting loses the version someone already saw. Files: `src/renders.py` ### Done when - [ ] A project renders in a single pass - [ ] Two renders of one timeline are byte-identical - [ ] Changing one scene changes only that part - [ ] Renders are kept with their settings hash ### Watch out - Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. ## Not in this build - The stock media library. - Motion design and visual polish. - Direct publishing to platforms. - Any claim that generated video performs. ## After v1, if you want it - A second voice for dialogue, which the timing model already supports - Automatic media suggestion per scene from your own library, proposed rather than applied - A review pass that plays the video with the script alongside for checking before publish ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where renders and reports are written. Never the source directory. OUTPUT_DIR=./renders # Required. Output aspect. Vertical for most short video destinations. ASPECT=9:16 # Required. Frame rate. Fixed per project so captions and cuts land on frames. FPS=30 # Required. Shortest scene before merging with a neighbour. SCENE_MIN_MS=1800 # Required. Longest scene before splitting at an internal sentence boundary. SCENE_MAX_MS=7000 # Required. Margin from every edge, so captions clear platform interface elements. CAPTION_SAFE_MARGIN_PCT=12 # Required. Checked against asset licences. A mismatch blocks the render. DECLARED_USE=commercial
You are building a lean indie version of Vyravid. 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 ===== # Vyravid · indie build A local pipeline turning a script into a short video: synthetic voiceover with word timings, the script split into scenes, media assigned per scene from a library whose licences you recorded, captions generated from the real word timings rather than estimated, and a deterministic render you can re-run after a change. 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 | | --- | --- | --- | | Render | ffmpeg driven by a filter graph built from the timeline | one render pass from a described timeline, reproducible and scriptable | | Runtime | Python 3.12 with SQLite | the work is orchestration and bookkeeping over media tools | | Voice | A speech engine that returns word-level timings | captions must come from when words were actually spoken, not from an estimate | | Media | A local library with licence recorded per asset | the licence question has to be answered at import, because it cannot be answered later | | Timeline | Scenes and clips as data, never a rendered intermediate | a correction should re-render rather than require remaking | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the pipeline and builds the filter graph. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with the encoders you need** · free - Why: The render is one filter graph through it. A build missing an encoder fails at the last step. - Get it: brew install ffmpeg or your package manager. Confirm your target encoder is present. - Verify: ffmpeg -encoders lists your video and audio encoders - [ ] **A speech engine returning word timings** · local engines are free and slower; hosted ones are priced per character - Why: Captions and scene boundaries both derive from them, and estimated timings drift visibly within a minute. - Get it: Choose an engine that reports word-level timings, or plan a forced alignment step. Confirm the timings exist before building on them. - Verify: Generated speech comes with per-word start and end times - [ ] **Media whose licences you have read** · free with openly licensed material - Why: A render is blocked on a licence mismatch by design, so material with unknown terms cannot be used at all. - Get it: Openly licensed libraries, material you created, or stock you have bought. Record the terms per asset as you import. - Verify: At least ten assets are imported with licences and check dates - [ ] **A phone to check the output on** · free - Why: Caption safe areas and legibility are only judgeable where the video will be watched, and a desktop player hides the platform interface that covers the edges. - Get it: Any phone. Play the export and watch for captions behind interface elements. - Verify: An export has been checked on a phone ## 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: - The stock media library. - Motion design and visual polish. - Direct publishing to platforms. - Any claim that generated video performs. - someone else's servers & support - provider-specific voice/image/video integration details - timeline rendering polish - custom voice and character workflows If one of those is essential to you, that is the reason to keep paying for Vyravid, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Vyravid The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a script to short video pipeline with synthetic voiceover, media and captions to replace Vyravid. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things have to be settled before this produces anything you publish. Every piece of media you use has a licence: stock footage, music and images each have terms about commercial use, modification and attribution, and a video assembled from material you did not check is a problem that surfaces after it does well rather than before. And a synthetic voice should be identifiable as one. Disclosure requirements for synthetic media are appearing in several jurisdictions and platform policies already require it in places. Label the output and keep a record of what was generated, because you cannot reconstruct that later. Build the timeline as data with a deterministic render, so a video can be corrected and re-rendered rather than remade. ### Stack (fixed, do not substitute) - Render: ffmpeg driven by a filter graph built from the timeline · one render pass from a described timeline, reproducible and scriptable - Runtime: Python 3.12 with SQLite · the work is orchestration and bookkeeping over media tools - Voice: A speech engine that returns word-level timings · captions must come from when words were actually spoken, not from an estimate - Media: A local library with licence recorded per asset · the licence question has to be answered at import, because it cannot be answered later - Timeline: Scenes and clips as data, never a rendered intermediate · a correction should re-render rather than require remaking ### Data model (create this before Phase 1) - projects(id, title, aspect, fps, created_at) - scripts(id, project_id, text, version, created_at) - scenes(id, project_id, ordinal, text, start_ms, end_ms, media_asset_id, effect_json) - voice_words(scene_id, ordinal, word, start_ms, end_ms) · the spine captions and scene timing both use - assets(id, path, kind, source, licence, commercial_use, attribution_required, checked_on) - renders(id, project_id, settings_hash, output_path, rendered_at, duration_ms) - provenance(project_id, note) · what was synthesised, recorded because it cannot be reconstructed - Scene boundaries come from word timings rather than from a guess at reading speed, which is what keeps captions and cuts aligned. ### Phase 1 · Voice with real word timings Build: Generate speech from the script with a engine that returns word-level timings, or generate the audio and align it to the script afterwards. Store timings per word. Verify by playing a single word's range. Trap: Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. Done when: Playing a single word's range plays that word. Word timings are monotonic with no overlaps. The total duration matches the audio file. A script with numbers and abbreviations produces timings covering how they were actually spoken rather than how they were written. Do not build yet: No scenes, media or render. ### Phase 2 · Scenes derived from the speech Build: Split the script into scenes at sentence boundaries, and set each scene's time range from the word timings rather than by dividing the duration. Enforce a minimum and maximum scene length by merging or splitting. Trap: Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. Done when: Scene boundaries fall in gaps between spoken words, verified by checking three against the audio. A scene shorter than the minimum merges with its neighbour. A scene longer than the maximum splits at an internal sentence boundary. Scene times sum to the audio duration. Do not build yet: No media, captions or render yet. ### Phase 3 · Media with its licence recorded Build: Import media into a library recording source, licence, commercial use and attribution requirement per asset, with a check date. Assign assets to scenes. Refuse to render a project containing an asset whose licence does not cover your declared use. Trap: Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. Done when: Importing an asset requires a licence and a check date. A project using an asset whose licence excludes your declared use is blocked from rendering with the asset named. Attribution text is collected for assets that require it. The declared use is recorded on the project. Do not build yet: No captions or render yet. ### Phase 4 · Captions from the timings you already have Build: Group words into caption lines of a readable length and duration, using the word timings for their start and end. Render captions with a safe area margin so nothing sits under a platform's interface. Export a subtitle file alongside the video. Trap: Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. Done when: Captions appear at the word timings, verified by watching a section. No caption falls outside the safe area, checked by playing the export on a phone. Lines respect a maximum length and minimum duration. A separate subtitle file is exported that matches the burned captions. Do not build yet: No render pipeline or batch yet. ### Phase 5 · A render you can repeat Build: Build one ffmpeg filter graph from the timeline and render in a single pass. Record the settings hash on the render. Re-render after changing one scene and confirm only that changed. Never overwrite a previous render. Trap: Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. Done when: A project renders in a single pass with no intermediate files. The same timeline renders byte-identical output twice. Changing one scene's text and re-rendering produces a video differing only where expected. Renders are kept rather than overwritten, each with its settings hash. Do not build yet: Disclosure and batch come last. ### Phase 6 · Disclosure, batch and the honest limits Build: Record what was synthesised per project and label the output, in the video and in its metadata. Batch render a directory of scripts. Then write down honestly what this produces and what it does not. Trap: A synthetic voice presented as a person's is a problem that grows rather than fades, and disclosure requirements for synthetic media are appearing in more places rather than fewer. Label at render time so it cannot be forgotten, and keep the provenance record whatever you decide about the label. Done when: Every rendered video carries a synthetic media label in its metadata and, where you chose, on screen. The provenance record lists what was generated per project. A directory of scripts renders unattended with per-project failures reported. The documentation states what this produces and its limits. Do not build yet: Nothing after this. ### Out of scope (and why) - The stock media library, which is a licensing operation with a continuing bill and is a large part of what the hosted product provides. - Motion design and the visual polish that distinguishes a good short video from an assembled one. - Publishing directly to platforms, each of which is an API and an account relationship. - Any claim that generated video performs. Assembly is cheap; the reason a video works is the script, which this does not write for you. ### README must contain - That every asset carries a recorded licence and a render is blocked on a mismatch - That captions come from real word timings rather than an estimated reading speed - That scene boundaries land in gaps between spoken words - That renders are deterministic and kept with their settings hash - That synthetic content is labelled in metadata and what the provenance record holds ===== AGENTS.md ===== # Agent instructions · Vyravid indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg driven by a filter graph built from the timeline, Python 3.12 with SQLite, A speech engine that returns word-level timings, A local library with licence recorded per asset, Scenes and clips as data, never a rendered intermediate. 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 - Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. - Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. - Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. - Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. - Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. ===== BUILD_PLAN.md ===== # Build plan · Vyravid A local pipeline turning a script into a short video: synthetic voiceover with word timings, the script split into scenes, media assigned per scene from a library whose licences you recorded, captions generated from the real word timings rather than estimated, and a deterministic render you can re-run after a change. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Voice with real word timings The voiceover exists and you know when every word was spoken. ### Steps 1. Generate the voiceover With word timings if the engine offers them, or align afterwards if it does not. Files: `src/voice.py` 2. Store timings per word The spine that scenes and captions both derive from. Files: `src/timings.py` 3. Verify by playing one word From the middle of a sentence. Estimated timings fail this immediately. ### Done when - [ ] A single word's range plays that word - [ ] Timings are monotonic with no overlaps - [ ] Total duration matches the audio - [ ] Numbers and abbreviations are timed as spoken ### Watch out - Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. ## Phase 2 · Scenes derived from the speech The script splits into scenes whose boundaries land at real pauses. ### Steps 1. Split at sentence boundaries Then take times from the word timings for those sentences. Files: `src/scenes.py` 2. Enforce length bounds Merge short scenes, split long ones at an internal boundary rather than arbitrarily. Files: `src/scenes.py` 3. Verify cuts land in gaps Check three boundaries against the audio. A cut mid-word is the visible failure. ### Done when - [ ] Boundaries fall in gaps between words - [ ] Short scenes merge and long ones split at sentence boundaries - [ ] Scene times sum to the audio duration - [ ] No cut lands mid-word ### Watch out - Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. ## Phase 3 · Media with its licence recorded Every clip has an answer to where it came from and what you may do with it. ### Steps 1. Require licence at import Source, terms, commercial use, attribution, check date. Not optional. Files: `src/assets.py` 2. Block on mismatch Against the project's declared use. A warning gets clicked through. Files: `src/render.py` 3. Collect attribution Automatically, so the requirement is met by construction rather than by memory. Files: `src/attribution.py` ### Done when - [ ] Import requires a licence and a check date - [ ] A licence mismatch blocks the render and names the asset - [ ] Attribution text is collected where required - [ ] The declared use is recorded on the project ### Watch out - Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. ## Phase 4 · Captions from the timings you already have Captions appear exactly when the words are spoken and are readable. ### Steps 1. Group words into lines Maximum characters and minimum on-screen time. Two constraints, both needed. Files: `src/captions.py` 2. Respect a safe area Margins from every edge. Verify on a phone, not in a desktop player. Files: `src/captions.py` 3. Export a subtitle file too Not everyone wants burned captions, and the file is also an accessibility artefact. Files: `src/captions.py` ### Done when - [ ] Captions appear at the word timings - [ ] No caption falls outside the safe area on a phone - [ ] Lines respect length and duration limits - [ ] The subtitle file matches the burned captions ### Watch out - Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. ## Phase 5 · A render you can repeat The video renders from the timeline, and the same timeline renders the same video. ### Steps 1. Build one filter graph Concatenation, overlays and audio in a single invocation. Files: `src/render.py` 2. Pin anything nondeterministic Seeds and encoder settings, so two renders match byte for byte. Files: `tests/test_determinism.py` 3. Keep every render With its settings hash. Overwriting loses the version someone already saw. Files: `src/renders.py` ### Done when - [ ] A project renders in a single pass - [ ] Two renders of one timeline are byte-identical - [ ] Changing one scene changes only that part - [ ] Renders are kept with their settings hash ### Watch out - Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. ## Not in this build - The stock media library. - Motion design and visual polish. - Direct publishing to platforms. - Any claim that generated video performs. ## After v1, if you want it - A second voice for dialogue, which the timing model already supports - Automatic media suggestion per scene from your own library, proposed rather than applied - A review pass that plays the video with the script alongside for checking before publish ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where renders and reports are written. Never the source directory. OUTPUT_DIR=./renders # Required. Output aspect. Vertical for most short video destinations. ASPECT=9:16 # Required. Frame rate. Fixed per project so captions and cuts land on frames. FPS=30 # Required. Shortest scene before merging with a neighbour. SCENE_MIN_MS=1800 # Required. Longest scene before splitting at an internal sentence boundary. SCENE_MAX_MS=7000 # Required. Margin from every edge, so captions clear platform interface elements. CAPTION_SAFE_MARGIN_PCT=12 # Required. Checked against asset licences. A mismatch blocks the render. DECLARED_USE=commercial
You are building a production product version of Vyravid. 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 ===== # Vyravid · product brief ## Problem A local version of the core workflow is buildable: project dashboard, script-to-scenes generation, voiceover, media generation, timeline assembly, and local ffmpeg rendering. The hard parts are not the CRUD shell but the media edge cases, provider integrations, long-running render jobs, and the editor polish that makes the timeline pleasant instead of fragile. ## Product outcome A pipeline that turns a script into a captioned video whose cuts and captions land where words were actually spoken, whose media is all licensed for what you are doing, and which re-renders deterministically after a correction. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - AI provider API keys - local filesystem storage - ffmpeg ## Explicit non-goals for v1 - The stock media library. - Motion design and visual polish. - Direct publishing to platforms. - Any claim that generated video performs. - someone else's servers & support - provider-specific voice/image/video integration details - timeline rendering polish - custom voice and character workflows ## Success criteria - Word timings are verified by playing a single word - Scene cuts are confirmed to land in gaps between words - A licence mismatch is confirmed to block the render - Captions are confirmed inside the safe area on a phone - Two renders of one timeline are byte-identical ===== BRIEF.md ===== # Build brief · Vyravid The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a script to short video pipeline with synthetic voiceover, media and captions to replace Vyravid. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things have to be settled before this produces anything you publish. Every piece of media you use has a licence: stock footage, music and images each have terms about commercial use, modification and attribution, and a video assembled from material you did not check is a problem that surfaces after it does well rather than before. And a synthetic voice should be identifiable as one. Disclosure requirements for synthetic media are appearing in several jurisdictions and platform policies already require it in places. Label the output and keep a record of what was generated, because you cannot reconstruct that later. Build the timeline as data with a deterministic render, so a video can be corrected and re-rendered rather than remade. ### Stack (fixed, do not substitute) - Render: ffmpeg driven by a filter graph built from the timeline · one render pass from a described timeline, reproducible and scriptable - Runtime: Python 3.12 with SQLite · the work is orchestration and bookkeeping over media tools - Voice: A speech engine that returns word-level timings · captions must come from when words were actually spoken, not from an estimate - Media: A local library with licence recorded per asset · the licence question has to be answered at import, because it cannot be answered later - Timeline: Scenes and clips as data, never a rendered intermediate · a correction should re-render rather than require remaking ### Data model (create this before Phase 1) - projects(id, title, aspect, fps, created_at) - scripts(id, project_id, text, version, created_at) - scenes(id, project_id, ordinal, text, start_ms, end_ms, media_asset_id, effect_json) - voice_words(scene_id, ordinal, word, start_ms, end_ms) · the spine captions and scene timing both use - assets(id, path, kind, source, licence, commercial_use, attribution_required, checked_on) - renders(id, project_id, settings_hash, output_path, rendered_at, duration_ms) - provenance(project_id, note) · what was synthesised, recorded because it cannot be reconstructed - Scene boundaries come from word timings rather than from a guess at reading speed, which is what keeps captions and cuts aligned. ### Phase 1 · Voice with real word timings Build: Generate speech from the script with a engine that returns word-level timings, or generate the audio and align it to the script afterwards. Store timings per word. Verify by playing a single word's range. Trap: Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. Done when: Playing a single word's range plays that word. Word timings are monotonic with no overlaps. The total duration matches the audio file. A script with numbers and abbreviations produces timings covering how they were actually spoken rather than how they were written. Do not build yet: No scenes, media or render. ### Phase 2 · Scenes derived from the speech Build: Split the script into scenes at sentence boundaries, and set each scene's time range from the word timings rather than by dividing the duration. Enforce a minimum and maximum scene length by merging or splitting. Trap: Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. Done when: Scene boundaries fall in gaps between spoken words, verified by checking three against the audio. A scene shorter than the minimum merges with its neighbour. A scene longer than the maximum splits at an internal sentence boundary. Scene times sum to the audio duration. Do not build yet: No media, captions or render yet. ### Phase 3 · Media with its licence recorded Build: Import media into a library recording source, licence, commercial use and attribution requirement per asset, with a check date. Assign assets to scenes. Refuse to render a project containing an asset whose licence does not cover your declared use. Trap: Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. Done when: Importing an asset requires a licence and a check date. A project using an asset whose licence excludes your declared use is blocked from rendering with the asset named. Attribution text is collected for assets that require it. The declared use is recorded on the project. Do not build yet: No captions or render yet. ### Phase 4 · Captions from the timings you already have Build: Group words into caption lines of a readable length and duration, using the word timings for their start and end. Render captions with a safe area margin so nothing sits under a platform's interface. Export a subtitle file alongside the video. Trap: Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. Done when: Captions appear at the word timings, verified by watching a section. No caption falls outside the safe area, checked by playing the export on a phone. Lines respect a maximum length and minimum duration. A separate subtitle file is exported that matches the burned captions. Do not build yet: No render pipeline or batch yet. ### Phase 5 · A render you can repeat Build: Build one ffmpeg filter graph from the timeline and render in a single pass. Record the settings hash on the render. Re-render after changing one scene and confirm only that changed. Never overwrite a previous render. Trap: Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. Done when: A project renders in a single pass with no intermediate files. The same timeline renders byte-identical output twice. Changing one scene's text and re-rendering produces a video differing only where expected. Renders are kept rather than overwritten, each with its settings hash. Do not build yet: Disclosure and batch come last. ### Phase 6 · Disclosure, batch and the honest limits Build: Record what was synthesised per project and label the output, in the video and in its metadata. Batch render a directory of scripts. Then write down honestly what this produces and what it does not. Trap: A synthetic voice presented as a person's is a problem that grows rather than fades, and disclosure requirements for synthetic media are appearing in more places rather than fewer. Label at render time so it cannot be forgotten, and keep the provenance record whatever you decide about the label. Done when: Every rendered video carries a synthetic media label in its metadata and, where you chose, on screen. The provenance record lists what was generated per project. A directory of scripts renders unattended with per-project failures reported. The documentation states what this produces and its limits. Do not build yet: Nothing after this. ### Out of scope (and why) - The stock media library, which is a licensing operation with a continuing bill and is a large part of what the hosted product provides. - Motion design and the visual polish that distinguishes a good short video from an assembled one. - Publishing directly to platforms, each of which is an API and an account relationship. - Any claim that generated video performs. Assembly is cheap; the reason a video works is the script, which this does not write for you. ### README must contain - That every asset carries a recorded licence and a render is blocked on a mismatch - That captions come from real word timings rather than an estimated reading speed - That scene boundaries land in gaps between spoken words - That renders are deterministic and kept with their settings hash - That synthetic content is labelled in metadata and what the provenance record holds ===== ARCHITECTURE.md ===== # Architecture · Vyravid ## Stack | Part | Choice | Why | | --- | --- | --- | | Render | ffmpeg driven by a filter graph built from the timeline | one render pass from a described timeline, reproducible and scriptable | | Runtime | Python 3.12 with SQLite | the work is orchestration and bookkeeping over media tools | | Voice | A speech engine that returns word-level timings | captions must come from when words were actually spoken, not from an estimate | | Media | A local library with licence recorded per asset | the licence question has to be answered at import, because it cannot be answered later | | Timeline | Scenes and clips as data, never a rendered intermediate | a correction should re-render rather than require remaking | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Voice | Speech and word timings. | Any engine providing timings, or a forced alignment step. Estimated timings are not an option. | | Scenes | Boundaries derived from speech. | Any splitting logic, provided boundaries come from timings rather than division. | | Assets | Media and their licences. | Blocking on a licence mismatch is a correctness property, not a preference. | | Captions | Lines, timing and safe areas. | Any renderer. The safe area margin is what makes them readable where people watch. | | Render | One deterministic pass from the timeline. | ffmpeg is the tool. Single pass rather than concatenated intermediates is the quality decision. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `OUTPUT_DIR` · required · Where renders and reports are written. Never the source directory. - `ASPECT` · required · Output aspect. Vertical for most short video destinations. - `FPS` · required · Frame rate. Fixed per project so captions and cuts land on frames. - `SCENE_MIN_MS` · required · Shortest scene before merging with a neighbour. - `SCENE_MAX_MS` · required · Longest scene before splitting at an internal sentence boundary. - `CAPTION_SAFE_MARGIN_PCT` · required · Margin from every edge, so captions clear platform interface elements. - `DECLARED_USE` · required · Checked against asset licences. A mismatch blocks the render. ## 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 · Vyravid product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: ffmpeg driven by a filter graph built from the timeline, Python 3.12 with SQLite, A speech engine that returns word-level timings, A local library with licence recorded per asset, Scenes and clips as data, never a rendered intermediate. - 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 - Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. - Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. - Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. - Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. - Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. - A synthetic voice presented as a person's is a problem that grows rather than fades, and disclosure requirements for synthetic media are appearing in more places rather than fewer. Label at render time so it cannot be forgotten, and keep the provenance record whatever you decide about the label. ===== MILESTONES.md ===== # Delivery milestones · Vyravid Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Voice with real word timings The voiceover exists and you know when every word was spoken. ### Steps 1. Generate the voiceover With word timings if the engine offers them, or align afterwards if it does not. Files: `src/voice.py` 2. Store timings per word The spine that scenes and captions both derive from. Files: `src/timings.py` 3. Verify by playing one word From the middle of a sentence. Estimated timings fail this immediately. ### Done when - [ ] A single word's range plays that word - [ ] Timings are monotonic with no overlaps - [ ] Total duration matches the audio - [ ] Numbers and abbreviations are timed as spoken ### Watch out - Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. ## M2 · Scenes derived from the speech The script splits into scenes whose boundaries land at real pauses. ### Steps 1. Split at sentence boundaries Then take times from the word timings for those sentences. Files: `src/scenes.py` 2. Enforce length bounds Merge short scenes, split long ones at an internal boundary rather than arbitrarily. Files: `src/scenes.py` 3. Verify cuts land in gaps Check three boundaries against the audio. A cut mid-word is the visible failure. ### Done when - [ ] Boundaries fall in gaps between words - [ ] Short scenes merge and long ones split at sentence boundaries - [ ] Scene times sum to the audio duration - [ ] No cut lands mid-word ### Watch out - Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. ## M3 · Media with its licence recorded Every clip has an answer to where it came from and what you may do with it. ### Steps 1. Require licence at import Source, terms, commercial use, attribution, check date. Not optional. Files: `src/assets.py` 2. Block on mismatch Against the project's declared use. A warning gets clicked through. Files: `src/render.py` 3. Collect attribution Automatically, so the requirement is met by construction rather than by memory. Files: `src/attribution.py` ### Done when - [ ] Import requires a licence and a check date - [ ] A licence mismatch blocks the render and names the asset - [ ] Attribution text is collected where required - [ ] The declared use is recorded on the project ### Watch out - Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. ## M4 · Captions from the timings you already have Captions appear exactly when the words are spoken and are readable. ### Steps 1. Group words into lines Maximum characters and minimum on-screen time. Two constraints, both needed. Files: `src/captions.py` 2. Respect a safe area Margins from every edge. Verify on a phone, not in a desktop player. Files: `src/captions.py` 3. Export a subtitle file too Not everyone wants burned captions, and the file is also an accessibility artefact. Files: `src/captions.py` ### Done when - [ ] Captions appear at the word timings - [ ] No caption falls outside the safe area on a phone - [ ] Lines respect length and duration limits - [ ] The subtitle file matches the burned captions ### Watch out - Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. ## M5 · A render you can repeat The video renders from the timeline, and the same timeline renders the same video. ### Steps 1. Build one filter graph Concatenation, overlays and audio in a single invocation. Files: `src/render.py` 2. Pin anything nondeterministic Seeds and encoder settings, so two renders match byte for byte. Files: `tests/test_determinism.py` 3. Keep every render With its settings hash. Overwriting loses the version someone already saw. Files: `src/renders.py` ### Done when - [ ] A project renders in a single pass - [ ] Two renders of one timeline are byte-identical - [ ] Changing one scene changes only that part - [ ] Renders are kept with their settings hash ### Watch out - Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. ## M6 · Disclosure, batch and the honest limits (production only) Synthetic content is labelled, and a series can be produced without supervision. ### Steps 1. Label at render time In metadata always, on screen if you chose. Structural rather than remembered. Files: `src/render.py` 2. Keep the provenance record Voice, images, anything generated. It cannot be reconstructed afterwards. Files: `src/provenance.py` 3. Batch with failure isolation One bad script must not stop a series. Files: `src/batch.py` ### Done when - [ ] Every render carries a synthetic media label in metadata - [ ] The provenance record lists what was generated - [ ] A batch completes with failures isolated - [ ] The documentation states the limits honestly ### Watch out - A synthetic voice presented as a person's is a problem that grows rather than fades, and disclosure requirements for synthetic media are appearing in more places rather than fewer. Label at render time so it cannot be forgotten, and keep the provenance record whatever you decide about the label. ===== OPERATIONS.md ===== # Operations · Vyravid ## Backup Scripts, timelines and the asset library with its licence records are the durable material. Renders regenerate from them plus a settings hash, so backing up video files is optional and backing up licence records is not. ## Restore Restore the database and assets, then re-render and compare against a stored output. Byte equality proves it; the file opening does not. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For batch runs, watch the licence block rate and render failures. A rising block rate means assets are being imported without terms, which is the habit that has to be caught early. ## Incident checklist A published video using an asset outside its licence is the incident that matters, and blocking rather than warning is the control. The provenance record is what answers a question about a synthetic voice later. 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 - [ ] Word timings are verified by playing a single word - [ ] Scene cuts are confirmed to land in gaps between words - [ ] A licence mismatch is confirmed to block the render - [ ] Captions are confirmed inside the safe area on a phone - [ ] Two renders of one timeline are byte-identical ## Launch constraint Do not market omitted Vyravid 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. Where renders and reports are written. Never the source directory. OUTPUT_DIR=./renders # Required. Output aspect. Vertical for most short video destinations. ASPECT=9:16 # Required. Frame rate. Fixed per project so captions and cuts land on frames. FPS=30 # Required. Shortest scene before merging with a neighbour. SCENE_MIN_MS=1800 # Required. Longest scene before splitting at an internal sentence boundary. SCENE_MAX_MS=7000 # Required. Margin from every edge, so captions clear platform interface elements. CAPTION_SAFE_MARGIN_PCT=12 # Required. Checked against asset licences. A mismatch blocks the render. DECLARED_USE=commercial
# Vyravid · indie build A local pipeline turning a script into a short video: synthetic voiceover with word timings, the script split into scenes, media assigned per scene from a library whose licences you recorded, captions generated from the real word timings rather than estimated, and a deterministic render you can re-run after a change. 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 | | --- | --- | --- | | Render | ffmpeg driven by a filter graph built from the timeline | one render pass from a described timeline, reproducible and scriptable | | Runtime | Python 3.12 with SQLite | the work is orchestration and bookkeeping over media tools | | Voice | A speech engine that returns word-level timings | captions must come from when words were actually spoken, not from an estimate | | Media | A local library with licence recorded per asset | the licence question has to be answered at import, because it cannot be answered later | | Timeline | Scenes and clips as data, never a rendered intermediate | a correction should re-render rather than require remaking | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the pipeline and builds the filter graph. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg with the encoders you need** · free - Why: The render is one filter graph through it. A build missing an encoder fails at the last step. - Get it: brew install ffmpeg or your package manager. Confirm your target encoder is present. - Verify: ffmpeg -encoders lists your video and audio encoders - [ ] **A speech engine returning word timings** · local engines are free and slower; hosted ones are priced per character - Why: Captions and scene boundaries both derive from them, and estimated timings drift visibly within a minute. - Get it: Choose an engine that reports word-level timings, or plan a forced alignment step. Confirm the timings exist before building on them. - Verify: Generated speech comes with per-word start and end times - [ ] **Media whose licences you have read** · free with openly licensed material - Why: A render is blocked on a licence mismatch by design, so material with unknown terms cannot be used at all. - Get it: Openly licensed libraries, material you created, or stock you have bought. Record the terms per asset as you import. - Verify: At least ten assets are imported with licences and check dates - [ ] **A phone to check the output on** · free - Why: Caption safe areas and legibility are only judgeable where the video will be watched, and a desktop player hides the platform interface that covers the edges. - Get it: Any phone. Play the export and watch for captions behind interface elements. - Verify: An export has been checked on a phone ## 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: - The stock media library. - Motion design and visual polish. - Direct publishing to platforms. - Any claim that generated video performs. - someone else's servers & support - provider-specific voice/image/video integration details - timeline rendering polish - custom voice and character workflows If one of those is essential to you, that is the reason to keep paying for Vyravid, and the README should say so rather than pretend.
# Build brief · Vyravid The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a script to short video pipeline with synthetic voiceover, media and captions to replace Vyravid. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things have to be settled before this produces anything you publish. Every piece of media you use has a licence: stock footage, music and images each have terms about commercial use, modification and attribution, and a video assembled from material you did not check is a problem that surfaces after it does well rather than before. And a synthetic voice should be identifiable as one. Disclosure requirements for synthetic media are appearing in several jurisdictions and platform policies already require it in places. Label the output and keep a record of what was generated, because you cannot reconstruct that later. Build the timeline as data with a deterministic render, so a video can be corrected and re-rendered rather than remade. ### Stack (fixed, do not substitute) - Render: ffmpeg driven by a filter graph built from the timeline · one render pass from a described timeline, reproducible and scriptable - Runtime: Python 3.12 with SQLite · the work is orchestration and bookkeeping over media tools - Voice: A speech engine that returns word-level timings · captions must come from when words were actually spoken, not from an estimate - Media: A local library with licence recorded per asset · the licence question has to be answered at import, because it cannot be answered later - Timeline: Scenes and clips as data, never a rendered intermediate · a correction should re-render rather than require remaking ### Data model (create this before Phase 1) - projects(id, title, aspect, fps, created_at) - scripts(id, project_id, text, version, created_at) - scenes(id, project_id, ordinal, text, start_ms, end_ms, media_asset_id, effect_json) - voice_words(scene_id, ordinal, word, start_ms, end_ms) · the spine captions and scene timing both use - assets(id, path, kind, source, licence, commercial_use, attribution_required, checked_on) - renders(id, project_id, settings_hash, output_path, rendered_at, duration_ms) - provenance(project_id, note) · what was synthesised, recorded because it cannot be reconstructed - Scene boundaries come from word timings rather than from a guess at reading speed, which is what keeps captions and cuts aligned. ### Phase 1 · Voice with real word timings Build: Generate speech from the script with a engine that returns word-level timings, or generate the audio and align it to the script afterwards. Store timings per word. Verify by playing a single word's range. Trap: Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. Done when: Playing a single word's range plays that word. Word timings are monotonic with no overlaps. The total duration matches the audio file. A script with numbers and abbreviations produces timings covering how they were actually spoken rather than how they were written. Do not build yet: No scenes, media or render. ### Phase 2 · Scenes derived from the speech Build: Split the script into scenes at sentence boundaries, and set each scene's time range from the word timings rather than by dividing the duration. Enforce a minimum and maximum scene length by merging or splitting. Trap: Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. Done when: Scene boundaries fall in gaps between spoken words, verified by checking three against the audio. A scene shorter than the minimum merges with its neighbour. A scene longer than the maximum splits at an internal sentence boundary. Scene times sum to the audio duration. Do not build yet: No media, captions or render yet. ### Phase 3 · Media with its licence recorded Build: Import media into a library recording source, licence, commercial use and attribution requirement per asset, with a check date. Assign assets to scenes. Refuse to render a project containing an asset whose licence does not cover your declared use. Trap: Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. Done when: Importing an asset requires a licence and a check date. A project using an asset whose licence excludes your declared use is blocked from rendering with the asset named. Attribution text is collected for assets that require it. The declared use is recorded on the project. Do not build yet: No captions or render yet. ### Phase 4 · Captions from the timings you already have Build: Group words into caption lines of a readable length and duration, using the word timings for their start and end. Render captions with a safe area margin so nothing sits under a platform's interface. Export a subtitle file alongside the video. Trap: Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. Done when: Captions appear at the word timings, verified by watching a section. No caption falls outside the safe area, checked by playing the export on a phone. Lines respect a maximum length and minimum duration. A separate subtitle file is exported that matches the burned captions. Do not build yet: No render pipeline or batch yet. ### Phase 5 · A render you can repeat Build: Build one ffmpeg filter graph from the timeline and render in a single pass. Record the settings hash on the render. Re-render after changing one scene and confirm only that changed. Never overwrite a previous render. Trap: Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. Done when: A project renders in a single pass with no intermediate files. The same timeline renders byte-identical output twice. Changing one scene's text and re-rendering produces a video differing only where expected. Renders are kept rather than overwritten, each with its settings hash. Do not build yet: Disclosure and batch come last. ### Phase 6 · Disclosure, batch and the honest limits Build: Record what was synthesised per project and label the output, in the video and in its metadata. Batch render a directory of scripts. Then write down honestly what this produces and what it does not. Trap: A synthetic voice presented as a person's is a problem that grows rather than fades, and disclosure requirements for synthetic media are appearing in more places rather than fewer. Label at render time so it cannot be forgotten, and keep the provenance record whatever you decide about the label. Done when: Every rendered video carries a synthetic media label in its metadata and, where you chose, on screen. The provenance record lists what was generated per project. A directory of scripts renders unattended with per-project failures reported. The documentation states what this produces and its limits. Do not build yet: Nothing after this. ### Out of scope (and why) - The stock media library, which is a licensing operation with a continuing bill and is a large part of what the hosted product provides. - Motion design and the visual polish that distinguishes a good short video from an assembled one. - Publishing directly to platforms, each of which is an API and an account relationship. - Any claim that generated video performs. Assembly is cheap; the reason a video works is the script, which this does not write for you. ### README must contain - That every asset carries a recorded licence and a render is blocked on a mismatch - That captions come from real word timings rather than an estimated reading speed - That scene boundaries land in gaps between spoken words - That renders are deterministic and kept with their settings hash - That synthetic content is labelled in metadata and what the provenance record holds
# Agent instructions · Vyravid indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg driven by a filter graph built from the timeline, Python 3.12 with SQLite, A speech engine that returns word-level timings, A local library with licence recorded per asset, Scenes and clips as data, never a rendered intermediate. 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 - Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. - Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. - Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. - Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. - Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better.
# Build plan · Vyravid A local pipeline turning a script into a short video: synthetic voiceover with word timings, the script split into scenes, media assigned per scene from a library whose licences you recorded, captions generated from the real word timings rather than estimated, and a deterministic render you can re-run after a change. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Voice with real word timings The voiceover exists and you know when every word was spoken. ### Steps 1. Generate the voiceover With word timings if the engine offers them, or align afterwards if it does not. Files: `src/voice.py` 2. Store timings per word The spine that scenes and captions both derive from. Files: `src/timings.py` 3. Verify by playing one word From the middle of a sentence. Estimated timings fail this immediately. ### Done when - [ ] A single word's range plays that word - [ ] Timings are monotonic with no overlaps - [ ] Total duration matches the audio - [ ] Numbers and abbreviations are timed as spoken ### Watch out - Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. ## Phase 2 · Scenes derived from the speech The script splits into scenes whose boundaries land at real pauses. ### Steps 1. Split at sentence boundaries Then take times from the word timings for those sentences. Files: `src/scenes.py` 2. Enforce length bounds Merge short scenes, split long ones at an internal boundary rather than arbitrarily. Files: `src/scenes.py` 3. Verify cuts land in gaps Check three boundaries against the audio. A cut mid-word is the visible failure. ### Done when - [ ] Boundaries fall in gaps between words - [ ] Short scenes merge and long ones split at sentence boundaries - [ ] Scene times sum to the audio duration - [ ] No cut lands mid-word ### Watch out - Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. ## Phase 3 · Media with its licence recorded Every clip has an answer to where it came from and what you may do with it. ### Steps 1. Require licence at import Source, terms, commercial use, attribution, check date. Not optional. Files: `src/assets.py` 2. Block on mismatch Against the project's declared use. A warning gets clicked through. Files: `src/render.py` 3. Collect attribution Automatically, so the requirement is met by construction rather than by memory. Files: `src/attribution.py` ### Done when - [ ] Import requires a licence and a check date - [ ] A licence mismatch blocks the render and names the asset - [ ] Attribution text is collected where required - [ ] The declared use is recorded on the project ### Watch out - Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. ## Phase 4 · Captions from the timings you already have Captions appear exactly when the words are spoken and are readable. ### Steps 1. Group words into lines Maximum characters and minimum on-screen time. Two constraints, both needed. Files: `src/captions.py` 2. Respect a safe area Margins from every edge. Verify on a phone, not in a desktop player. Files: `src/captions.py` 3. Export a subtitle file too Not everyone wants burned captions, and the file is also an accessibility artefact. Files: `src/captions.py` ### Done when - [ ] Captions appear at the word timings - [ ] No caption falls outside the safe area on a phone - [ ] Lines respect length and duration limits - [ ] The subtitle file matches the burned captions ### Watch out - Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. ## Phase 5 · A render you can repeat The video renders from the timeline, and the same timeline renders the same video. ### Steps 1. Build one filter graph Concatenation, overlays and audio in a single invocation. Files: `src/render.py` 2. Pin anything nondeterministic Seeds and encoder settings, so two renders match byte for byte. Files: `tests/test_determinism.py` 3. Keep every render With its settings hash. Overwriting loses the version someone already saw. Files: `src/renders.py` ### Done when - [ ] A project renders in a single pass - [ ] Two renders of one timeline are byte-identical - [ ] Changing one scene changes only that part - [ ] Renders are kept with their settings hash ### Watch out - Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. ## Not in this build - The stock media library. - Motion design and visual polish. - Direct publishing to platforms. - Any claim that generated video performs. ## After v1, if you want it - A second voice for dialogue, which the timing model already supports - Automatic media suggestion per scene from your own library, proposed rather than applied - A review pass that plays the video with the script alongside for checking before publish
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where renders and reports are written. Never the source directory. OUTPUT_DIR=./renders # Required. Output aspect. Vertical for most short video destinations. ASPECT=9:16 # Required. Frame rate. Fixed per project so captions and cuts land on frames. FPS=30 # Required. Shortest scene before merging with a neighbour. SCENE_MIN_MS=1800 # Required. Longest scene before splitting at an internal sentence boundary. SCENE_MAX_MS=7000 # Required. Margin from every edge, so captions clear platform interface elements. CAPTION_SAFE_MARGIN_PCT=12 # Required. Checked against asset licences. A mismatch blocks the render. DECLARED_USE=commercial
# Vyravid · product brief ## Problem A local version of the core workflow is buildable: project dashboard, script-to-scenes generation, voiceover, media generation, timeline assembly, and local ffmpeg rendering. The hard parts are not the CRUD shell but the media edge cases, provider integrations, long-running render jobs, and the editor polish that makes the timeline pleasant instead of fragile. ## Product outcome A pipeline that turns a script into a captioned video whose cuts and captions land where words were actually spoken, whose media is all licensed for what you are doing, and which re-renders deterministically after a correction. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - AI provider API keys - local filesystem storage - ffmpeg ## Explicit non-goals for v1 - The stock media library. - Motion design and visual polish. - Direct publishing to platforms. - Any claim that generated video performs. - someone else's servers & support - provider-specific voice/image/video integration details - timeline rendering polish - custom voice and character workflows ## Success criteria - Word timings are verified by playing a single word - Scene cuts are confirmed to land in gaps between words - A licence mismatch is confirmed to block the render - Captions are confirmed inside the safe area on a phone - Two renders of one timeline are byte-identical
# Build brief · Vyravid The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a script to short video pipeline with synthetic voiceover, media and captions to replace Vyravid. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things have to be settled before this produces anything you publish. Every piece of media you use has a licence: stock footage, music and images each have terms about commercial use, modification and attribution, and a video assembled from material you did not check is a problem that surfaces after it does well rather than before. And a synthetic voice should be identifiable as one. Disclosure requirements for synthetic media are appearing in several jurisdictions and platform policies already require it in places. Label the output and keep a record of what was generated, because you cannot reconstruct that later. Build the timeline as data with a deterministic render, so a video can be corrected and re-rendered rather than remade. ### Stack (fixed, do not substitute) - Render: ffmpeg driven by a filter graph built from the timeline · one render pass from a described timeline, reproducible and scriptable - Runtime: Python 3.12 with SQLite · the work is orchestration and bookkeeping over media tools - Voice: A speech engine that returns word-level timings · captions must come from when words were actually spoken, not from an estimate - Media: A local library with licence recorded per asset · the licence question has to be answered at import, because it cannot be answered later - Timeline: Scenes and clips as data, never a rendered intermediate · a correction should re-render rather than require remaking ### Data model (create this before Phase 1) - projects(id, title, aspect, fps, created_at) - scripts(id, project_id, text, version, created_at) - scenes(id, project_id, ordinal, text, start_ms, end_ms, media_asset_id, effect_json) - voice_words(scene_id, ordinal, word, start_ms, end_ms) · the spine captions and scene timing both use - assets(id, path, kind, source, licence, commercial_use, attribution_required, checked_on) - renders(id, project_id, settings_hash, output_path, rendered_at, duration_ms) - provenance(project_id, note) · what was synthesised, recorded because it cannot be reconstructed - Scene boundaries come from word timings rather than from a guess at reading speed, which is what keeps captions and cuts aligned. ### Phase 1 · Voice with real word timings Build: Generate speech from the script with a engine that returns word-level timings, or generate the audio and align it to the script afterwards. Store timings per word. Verify by playing a single word's range. Trap: Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. Done when: Playing a single word's range plays that word. Word timings are monotonic with no overlaps. The total duration matches the audio file. A script with numbers and abbreviations produces timings covering how they were actually spoken rather than how they were written. Do not build yet: No scenes, media or render. ### Phase 2 · Scenes derived from the speech Build: Split the script into scenes at sentence boundaries, and set each scene's time range from the word timings rather than by dividing the duration. Enforce a minimum and maximum scene length by merging or splitting. Trap: Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. Done when: Scene boundaries fall in gaps between spoken words, verified by checking three against the audio. A scene shorter than the minimum merges with its neighbour. A scene longer than the maximum splits at an internal sentence boundary. Scene times sum to the audio duration. Do not build yet: No media, captions or render yet. ### Phase 3 · Media with its licence recorded Build: Import media into a library recording source, licence, commercial use and attribution requirement per asset, with a check date. Assign assets to scenes. Refuse to render a project containing an asset whose licence does not cover your declared use. Trap: Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. Done when: Importing an asset requires a licence and a check date. A project using an asset whose licence excludes your declared use is blocked from rendering with the asset named. Attribution text is collected for assets that require it. The declared use is recorded on the project. Do not build yet: No captions or render yet. ### Phase 4 · Captions from the timings you already have Build: Group words into caption lines of a readable length and duration, using the word timings for their start and end. Render captions with a safe area margin so nothing sits under a platform's interface. Export a subtitle file alongside the video. Trap: Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. Done when: Captions appear at the word timings, verified by watching a section. No caption falls outside the safe area, checked by playing the export on a phone. Lines respect a maximum length and minimum duration. A separate subtitle file is exported that matches the burned captions. Do not build yet: No render pipeline or batch yet. ### Phase 5 · A render you can repeat Build: Build one ffmpeg filter graph from the timeline and render in a single pass. Record the settings hash on the render. Re-render after changing one scene and confirm only that changed. Never overwrite a previous render. Trap: Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. Done when: A project renders in a single pass with no intermediate files. The same timeline renders byte-identical output twice. Changing one scene's text and re-rendering produces a video differing only where expected. Renders are kept rather than overwritten, each with its settings hash. Do not build yet: Disclosure and batch come last. ### Phase 6 · Disclosure, batch and the honest limits Build: Record what was synthesised per project and label the output, in the video and in its metadata. Batch render a directory of scripts. Then write down honestly what this produces and what it does not. Trap: A synthetic voice presented as a person's is a problem that grows rather than fades, and disclosure requirements for synthetic media are appearing in more places rather than fewer. Label at render time so it cannot be forgotten, and keep the provenance record whatever you decide about the label. Done when: Every rendered video carries a synthetic media label in its metadata and, where you chose, on screen. The provenance record lists what was generated per project. A directory of scripts renders unattended with per-project failures reported. The documentation states what this produces and its limits. Do not build yet: Nothing after this. ### Out of scope (and why) - The stock media library, which is a licensing operation with a continuing bill and is a large part of what the hosted product provides. - Motion design and the visual polish that distinguishes a good short video from an assembled one. - Publishing directly to platforms, each of which is an API and an account relationship. - Any claim that generated video performs. Assembly is cheap; the reason a video works is the script, which this does not write for you. ### README must contain - That every asset carries a recorded licence and a render is blocked on a mismatch - That captions come from real word timings rather than an estimated reading speed - That scene boundaries land in gaps between spoken words - That renders are deterministic and kept with their settings hash - That synthetic content is labelled in metadata and what the provenance record holds
# Architecture · Vyravid ## Stack | Part | Choice | Why | | --- | --- | --- | | Render | ffmpeg driven by a filter graph built from the timeline | one render pass from a described timeline, reproducible and scriptable | | Runtime | Python 3.12 with SQLite | the work is orchestration and bookkeeping over media tools | | Voice | A speech engine that returns word-level timings | captions must come from when words were actually spoken, not from an estimate | | Media | A local library with licence recorded per asset | the licence question has to be answered at import, because it cannot be answered later | | Timeline | Scenes and clips as data, never a rendered intermediate | a correction should re-render rather than require remaking | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Voice | Speech and word timings. | Any engine providing timings, or a forced alignment step. Estimated timings are not an option. | | Scenes | Boundaries derived from speech. | Any splitting logic, provided boundaries come from timings rather than division. | | Assets | Media and their licences. | Blocking on a licence mismatch is a correctness property, not a preference. | | Captions | Lines, timing and safe areas. | Any renderer. The safe area margin is what makes them readable where people watch. | | Render | One deterministic pass from the timeline. | ffmpeg is the tool. Single pass rather than concatenated intermediates is the quality decision. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `OUTPUT_DIR` · required · Where renders and reports are written. Never the source directory. - `ASPECT` · required · Output aspect. Vertical for most short video destinations. - `FPS` · required · Frame rate. Fixed per project so captions and cuts land on frames. - `SCENE_MIN_MS` · required · Shortest scene before merging with a neighbour. - `SCENE_MAX_MS` · required · Longest scene before splitting at an internal sentence boundary. - `CAPTION_SAFE_MARGIN_PCT` · required · Margin from every edge, so captions clear platform interface elements. - `DECLARED_USE` · required · Checked against asset licences. A mismatch blocks the render. ## 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 · Vyravid product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: ffmpeg driven by a filter graph built from the timeline, Python 3.12 with SQLite, A speech engine that returns word-level timings, A local library with licence recorded per asset, Scenes and clips as data, never a rendered intermediate. - 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 - Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. - Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. - Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. - Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. - Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. - A synthetic voice presented as a person's is a problem that grows rather than fades, and disclosure requirements for synthetic media are appearing in more places rather than fewer. Label at render time so it cannot be forgotten, and keep the provenance record whatever you decide about the label.
# Delivery milestones · Vyravid Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Voice with real word timings The voiceover exists and you know when every word was spoken. ### Steps 1. Generate the voiceover With word timings if the engine offers them, or align afterwards if it does not. Files: `src/voice.py` 2. Store timings per word The spine that scenes and captions both derive from. Files: `src/timings.py` 3. Verify by playing one word From the middle of a sentence. Estimated timings fail this immediately. ### Done when - [ ] A single word's range plays that word - [ ] Timings are monotonic with no overlaps - [ ] Total duration matches the audio - [ ] Numbers and abbreviations are timed as spoken ### Watch out - Estimating word timings from a reading speed drifts, and by the end of a sixty second video the captions are a phrase behind. Get real timings, either from the speech engine or from a forced alignment pass, and check by playing one word from the middle. ## M2 · Scenes derived from the speech The script splits into scenes whose boundaries land at real pauses. ### Steps 1. Split at sentence boundaries Then take times from the word timings for those sentences. Files: `src/scenes.py` 2. Enforce length bounds Merge short scenes, split long ones at an internal boundary rather than arbitrarily. Files: `src/scenes.py` 3. Verify cuts land in gaps Check three boundaries against the audio. A cut mid-word is the visible failure. ### Done when - [ ] Boundaries fall in gaps between words - [ ] Short scenes merge and long ones split at sentence boundaries - [ ] Scene times sum to the audio duration - [ ] No cut lands mid-word ### Watch out - Dividing the total duration by the number of scenes produces cuts in the middle of sentences, which reads as a mistake to a viewer even when they cannot say why. Take boundaries from the word timings, so a cut always lands in a gap between words. ## M3 · Media with its licence recorded Every clip has an answer to where it came from and what you may do with it. ### Steps 1. Require licence at import Source, terms, commercial use, attribution, check date. Not optional. Files: `src/assets.py` 2. Block on mismatch Against the project's declared use. A warning gets clicked through. Files: `src/render.py` 3. Collect attribution Automatically, so the requirement is met by construction rather than by memory. Files: `src/attribution.py` ### Done when - [ ] Import requires a licence and a check date - [ ] A licence mismatch blocks the render and names the asset - [ ] Attribution text is collected where required - [ ] The declared use is recorded on the project ### Watch out - Recording the licence later never happens, and a video that did well is exactly when someone checks. Require it at import, block the render on a mismatch rather than warning, and collect attribution text automatically so the requirement is met rather than remembered. ## M4 · Captions from the timings you already have Captions appear exactly when the words are spoken and are readable. ### Steps 1. Group words into lines Maximum characters and minimum on-screen time. Two constraints, both needed. Files: `src/captions.py` 2. Respect a safe area Margins from every edge. Verify on a phone, not in a desktop player. Files: `src/captions.py` 3. Export a subtitle file too Not everyone wants burned captions, and the file is also an accessibility artefact. Files: `src/captions.py` ### Done when - [ ] Captions appear at the word timings - [ ] No caption falls outside the safe area on a phone - [ ] Lines respect length and duration limits - [ ] The subtitle file matches the burned captions ### Watch out - Burning captions into the frame without a safe area margin puts them behind the platform's own interface elements on a phone, where they are unreadable exactly where most people will watch. Keep a margin from every edge and check on a real device rather than in a desktop player. ## M5 · A render you can repeat The video renders from the timeline, and the same timeline renders the same video. ### Steps 1. Build one filter graph Concatenation, overlays and audio in a single invocation. Files: `src/render.py` 2. Pin anything nondeterministic Seeds and encoder settings, so two renders match byte for byte. Files: `tests/test_determinism.py` 3. Keep every render With its settings hash. Overwriting loses the version someone already saw. Files: `src/renders.py` ### Done when - [ ] A project renders in a single pass - [ ] Two renders of one timeline are byte-identical - [ ] Changing one scene changes only that part - [ ] Renders are kept with their settings hash ### Watch out - Rendering scene by scene into intermediate files and concatenating them re-encodes the material twice and introduces a quality loss plus a seam at every join. Build one filter graph and render once, which is faster and better. ## M6 · Disclosure, batch and the honest limits (production only) Synthetic content is labelled, and a series can be produced without supervision. ### Steps 1. Label at render time In metadata always, on screen if you chose. Structural rather than remembered. Files: `src/render.py` 2. Keep the provenance record Voice, images, anything generated. It cannot be reconstructed afterwards. Files: `src/provenance.py` 3. Batch with failure isolation One bad script must not stop a series. Files: `src/batch.py` ### Done when - [ ] Every render carries a synthetic media label in metadata - [ ] The provenance record lists what was generated - [ ] A batch completes with failures isolated - [ ] The documentation states the limits honestly ### Watch out - A synthetic voice presented as a person's is a problem that grows rather than fades, and disclosure requirements for synthetic media are appearing in more places rather than fewer. Label at render time so it cannot be forgotten, and keep the provenance record whatever you decide about the label.
# Operations · Vyravid ## Backup Scripts, timelines and the asset library with its licence records are the durable material. Renders regenerate from them plus a settings hash, so backing up video files is optional and backing up licence records is not. ## Restore Restore the database and assets, then re-render and compare against a stored output. Byte equality proves it; the file opening does not. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For batch runs, watch the licence block rate and render failures. A rising block rate means assets are being imported without terms, which is the habit that has to be caught early. ## Incident checklist A published video using an asset outside its licence is the incident that matters, and blocking rather than warning is the control. The provenance record is what answers a question about a synthetic voice later. 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 - [ ] Word timings are verified by playing a single word - [ ] Scene cuts are confirmed to land in gaps between words - [ ] A licence mismatch is confirmed to block the render - [ ] Captions are confirmed inside the safe area on a phone - [ ] Two renders of one timeline are byte-identical ## Launch constraint Do not market omitted Vyravid 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. Where renders and reports are written. Never the source directory. OUTPUT_DIR=./renders # Required. Output aspect. Vertical for most short video destinations. ASPECT=9:16 # Required. Frame rate. Fixed per project so captions and cuts land on frames. FPS=30 # Required. Shortest scene before merging with a neighbour. SCENE_MIN_MS=1800 # Required. Longest scene before splitting at an internal sentence boundary. SCENE_MAX_MS=7000 # Required. Margin from every edge, so captions clear platform interface elements. CAPTION_SAFE_MARGIN_PCT=12 # Required. Checked against asset licences. A mismatch blocks the render. DECLARED_USE=commercial
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay for hosted services, managed storage, fewer setup steps, reliable provider orchestration, templates, collaboration, asset libraries, support, and polished timeline UX. A local rebuild can replace the workflow for technical users, but hosted products still win on convenience and reliability.
xsomeone else's servers & support
xprovider-specific voice/image/video integration details
xtimeline rendering polish
xcustom voice and character workflows
Don't feel like building it? These folks already made it free.
MMoneyPrinterTurboTurns a topic or script into a narrated, captioned stock-footage short; tasteful restraint is not bundled.open source↗no votes, no pay-to-list · just what's real
Vyravid pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | Unlimited projects; the official pricing page does not expose a numeric monthly scene-generation allowance. |
| starter | $19.99 | — | Up to 1,000 scene generations/month, advertised as about 2.5 hours of generated video; all aspect ratios. |
| growth | $39.99 | — | Growth plan is sold for scaled production, but its numeric monthly generation quota is not exposed in the official parsed page. |
free tierUnlimited projects; the official page does not publish the numeric free generation allowance.
billingmonthly only; no annual plan is displayed
verified 2026-08-14 · source ↗
Vibecode Vyravid
Kinda. The core of Vyravid is buildable in a weekend with the prompt on this page, but there are real gaps: someone else's servers & support, provider-specific voice/image/video integration details. Read the honest list above before committing.
How much does Vyravid cost?
Vyravid costs about $19.99/month (Starter, checked 2026-08-14), which is $239.88 per year.
What do I lose by replacing Vyravid?
Honestly: someone else's servers & support; provider-specific voice/image/video integration details; timeline rendering polish; custom voice and character workflows. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Vyravid?
Yes: MoneyPrinterTurbo (Turns a topic or script into a narrated, captioned stock-footage short; tasteful restraint is not bundled.) The prompt is for when you want it exactly your way.