Vibecode InVideo
track this build4 phases, 12 steps, beginner friendly0%The visible AI video creation loop is buildable, but a credible replacement needs more than the first screen. InVideo earns its keep through media pipeline, hosting, distribution, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of InVideo. 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 ===== # InVideo · indie build A local video assembler whose hard problems are keeping a render reproducible from a file rather than from a session, refusing likeness and voice work without a consent record, and exporting a file that actually starts playing when someone uploads it. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with FastAPI and a browser interface, everything on your own machine | video is large, and shipping it somewhere to render is what the subscription is really charging for | | Renderer | FFmpeg invoked with an explicit filter graph written to a file per render | a render you cannot reproduce from a text file is a render you cannot debug | | Project format | One JSON project file referencing assets by path and content hash | the project has to outlive the application that made it | | Provenance | A required per-asset record of origin, licence, consent and whether it was generated | this is the part free tools skip and the part that decides whether you can publish | | Preview | A low-resolution proxy rendered from the same filter graph as the final | previewing by seeking in source files shows you something the export will not match | ## 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 service that drives FFmpeg and holds the project. - Get it: python.org or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg with the encoders you intend to use** · free - Why: Every render is an FFmpeg invocation, and a build missing an encoder fails at export time rather than at setup time. - Get it: Your package manager, or a static build. - Verify: ffmpeg -encoders lists the video and audio encoders your presets name - [ ] **Working disk space with a number attached** · free - Why: Proxies, intermediates and failed renders fill a disk quietly, and the ceiling in this build is only useful if you decided what it should be. - Get it: Choose a working directory on a volume with room and write the ceiling into the environment. - Verify: The application refuses a render that would exceed the ceiling - [ ] **Your own footage, including one variable frame rate clip** · free - Why: Phone video with a variable frame rate is where timeline drift comes from, and a project that only ever sees clean twenty-five frame files will not show you the bug. - Get it: Record something on a phone and import it unmodified. - Verify: The probe reports an average rather than a constant frame rate and the timeline still holds - [ ] **Your rule for likeness and voice** · free - Why: Deciding after you have built the generation path means deciding it under pressure, and the schema needs the consent table before anything references it. - Get it: Write down what evidence counts as consent, how long it lasts and what happens when it expires. - Verify: The rule is in the README and the consent table enforces the expiry ## Quick start ```sh ffmpeg -i input.mp4 -movflags +faststart -c copy output.mp4 ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Faces or voices of real people without a consent record. - A stock media library. - Direct publishing to video platforms. - Collaborative editing. - production codecs, rendering speed, and media templates - hosted recording reliability - large-scale rendering - distribution and podcast hosting - professional codecs, effects, and collaboration If one of those is essential to you, that is the reason to keep paying for InVideo, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · InVideo The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local video assembler that turns a script and your own footage into an exported file, with every source, licence and generated element recorded to replace InVideo. 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 A tool that generates video runs straight into three questions, and the answers shape the schema. Likeness first. Do not build a path that produces the face or the voice of a real person without their permission. Tennessee's ELVIS Act, in force since the first of July 2024, made a person's voice a property right and defines voice to include a simulation of it, so a cloned voice is actionable whether or not any real recording was used, and other places are moving the same way. The build therefore refuses to accept a reference face or a voice sample without a consent record naming the person, the scope and the date. Check the law where you actually are before you rely on any of this; the point is that the schema has a consent row, not that one statute covers you. Licensing second. Every asset carries where it came from and under what licence, as required fields. A tool that pulls in free stock footage and exports a file with no record of any of it is a tool whose output you cannot safely publish, which is the whole reason you made the file. Disclosure third. Anything generated is marked as generated in the project and in whatever provenance you attach to the export, using the established action vocabulary rather than a field you invented. Removing that mark is not a feature and there is no setting for it. ### Stack (fixed, do not substitute) - Runtime: Python with FastAPI and a browser interface, everything on your own machine · video is large, and shipping it somewhere to render is what the subscription is really charging for - Renderer: FFmpeg invoked with an explicit filter graph written to a file per render · a render you cannot reproduce from a text file is a render you cannot debug - Project format: One JSON project file referencing assets by path and content hash · the project has to outlive the application that made it - Provenance: A required per-asset record of origin, licence, consent and whether it was generated · this is the part free tools skip and the part that decides whether you can publish - Preview: A low-resolution proxy rendered from the same filter graph as the final · previewing by seeking in source files shows you something the export will not match ### Data model (create this before Phase 1) - projects(id, name, created_at, width, height, fps, format_version) - assets(id, project_id, path, sha256, kind, duration_ms, source, licence, licence_url, generated_by, consent_id) · source and licence are NOT NULL, because an asset with unknown provenance is a liability rather than a file - consents(id, person_name, scope, evidence_path, obtained_at, expires_at) · required before any asset carrying a real face or voice is accepted at all - timeline(id, project_id, track, ordinal, asset_id, in_ms, out_ms, transform_json) - captions(id, project_id, start_ms, end_ms, text, origin) · origin is transcribed, written or generated, and it is never blank - renders(id, project_id, at, preset, filtergraph_path, output_path, duration_ms, exit_code, log_tail, asset_hashes_json) - Every render writes its filter graph to a file beside the output and records the hash of every asset it used. A video you cannot regenerate from a text file and a folder is not reproducible, and six months later that is the only question you will have about it. ### Phase 1 · Assets that know where they came from Build: Import files by copying or referencing, hash the bytes, probe duration, resolution and frame rate. Require source and licence on import with no default. Detect assets flagged as carrying a person's face or voice and refuse them until a consent row exists and is in date. Trap: Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. Done when: An import with a blank source or licence is rejected, tested. An asset marked as carrying a likeness or a voice cannot be added without a consent row that is in date, tested at the expiry boundary. Changing an asset's bytes after import causes the next render to fail with a named mismatch rather than producing a file, tested. The probe records duration, resolution and frame rate, including for variable frame rate phone footage where the reported rate is an average. Do not build yet: No timeline, preview, narration or export yet. ### Phase 2 · A timeline and a preview that matches the export Build: Build the timeline as ordered clips per track with in and out points and transforms. Generate the preview by rendering a low-resolution proxy from the same filter graph the final render uses, at the project frame rate, rather than by seeking in the source files. Trap: Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. Done when: A cut placed in the preview lands within one frame in the final export, tested by rendering both and comparing frame positions. Mixing a sixty and a twenty-five frame source produces a timeline with no drift over a five minute sequence, tested. The proxy render and the final render are produced by the same graph builder with different parameters, asserted by review and by a test that diffs the two graphs. Deleting the proxy cache and rebuilding gives an identical proxy. Do not build yet: No narration, captions, disclosure or final export yet. ### Phase 3 · Narration, captions and what they are Build: Accept recorded narration or generated speech. Generated speech from a stock voice records the engine and the voice identifier; anything cloned from a person requires a consent row and is refused otherwise. Captions carry an origin of transcribed, written or generated. Trap: Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. Done when: A voice asset flagged as cloned from a person cannot be generated or imported without a consent row, tested. Every caption row has a non-blank origin and the editor shows it, asserted in the schema and in a test. Generated captions cannot be burned in until marked reviewed, tested. The engine and voice identifier are recorded for every piece of generated speech and appear in the project export. Do not build yet: No final export settings, provenance attachment or render queue yet. ### Phase 4 · An export that actually starts playing Build: Export to a widely supported codec pair in an MP4 container with the index moved to the front. Keep dimensions even, record the exact command, and write a sidecar manifest listing every asset, its licence, its consent record and every generated element in the file. Trap: The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. Done when: The exported file begins playing in a browser before it has fully downloaded, tested by serving it with a throttled connection. Odd dimensions from a crop are corrected and the correction is logged rather than causing an encoder failure, tested with a deliberately odd crop. The exact FFmpeg command and filter graph are written beside the output. The sidecar manifest lists every asset with licence and consent references and every generated element, and is written even when the render fails partway. Do not build yet: No provenance signing, render queue, disk limits or project export yet. ### Phase 5 · Provenance, disk and the render queue Build: Attach content provenance to the export where you can sign it, and keep the sidecar manifest regardless. Run one render at a time with a disk ceiling, clean up proxies and failed outputs, and export the whole project as a folder of assets plus the JSON that references them. Trap: Treating an attached provenance manifest as proof the viewer will see is wrong twice over, because any tool downstream can strip it during a re-encode and because the specification itself notes that a manifest without a time stamp stops being valid once its signing credential expires or is revoked; attach it if you can, keep your own sidecar manifest in the project whatever happens to the file, and describe it in the README as a record you hold rather than a guarantee you offer. Done when: A project exports as a self-contained folder and reimports on another machine with every asset hash matching, tested. A render that would take the working directory past the disk ceiling is refused before it starts, with the shortfall named, tested. Failed renders and orphaned proxies are cleaned by a routine that never touches an asset referenced by a project, tested with a deliberately orphaned file. Provenance attachment failures are recorded as a warning and never silently skipped. Do not build yet: Nothing after this. ### Out of scope (and why) - Generating the face or the voice of a real person without a consent record, which is refused in code rather than discouraged in documentation. - A stock media library, because licensing somebody else's footage is a contract and a catalogue rather than a feature you can write. - Publishing directly to video platforms, which needs the same credential and quota machinery as a social scheduler and belongs in that project. - Collaborative editing, which means a server, locking and a merge model for a data type that does not merge well. ### README must contain - That likeness and voice assets require a consent record, and that you should check the law where you are rather than relying on the one this build was designed around - That every asset carries a source and a licence as required fields, and what happens when one is missing - That every render writes its filter graph and asset hashes beside the output, so any export can be reproduced - That the export moves the file index to the front, why that matters for playback, and how to verify it happened - That attached provenance can be stripped downstream, and that the sidecar manifest is the copy you keep ===== AGENTS.md ===== # Agent instructions · InVideo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with FastAPI and a browser interface, everything on your own machine, FFmpeg invoked with an explicit filter graph written to a file per render, One JSON project file referencing assets by path and content hash, A required per-asset record of origin, licence, consent and whether it was generated, A low-resolution proxy rendered from the same filter graph as the final. 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 - Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. - Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. - Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. - The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. ===== BUILD_PLAN.md ===== # Build plan · InVideo A local video assembler whose hard problems are keeping a render reproducible from a file rather than from a session, refusing likeness and voice work without a consent record, and exporting a file that actually starts playing when someone uploads it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Assets that know where they came from Nothing enters a project without a hash, a source, a licence and, where a person appears, a consent record. ### Steps 1. Hash on import and verify before every render The hash is the only thing that notices a file changed under you. Files: `app/assets.py`, `tests/test_hash_guard.py` 2. Make source and licence required fields A nullable licence column becomes a null licence column. Files: `app/models.py`, `migrations/0001_assets.sql` 3. Gate likeness and voice assets on a consent row A check in code, not a sentence in the README. Files: `app/consent.py`, `tests/test_consent_gate.py` ### Done when - [ ] Blank source or licence is rejected on import - [ ] A likeness asset without consent cannot be added - [ ] A changed asset fails the render, not the check - [ ] Probe data includes frame rate for variable rate footage ### Watch out - Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. ## Phase 2 · A timeline and a preview that matches the export What you approve in the preview is what comes out of the final render. ### Steps 1. Normalise frame rate at import, not at render Drift you fix at the end is drift you have already approved. Files: `app/normalise.py` 2. Build one filter graph function, two presets The preview must not be a second implementation. Files: `app/graph.py`, `tests/test_graph_parity.py` 3. Compare preview and final frame positions in a test This is the assertion that keeps them honest. Files: `tests/test_cut_positions.py` ### Done when - [ ] A preview cut lands within one frame in the export - [ ] Mixed frame rates produce no drift over five minutes - [ ] Preview and final come from one graph builder - [ ] Proxies rebuild identically ### Watch out - Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. ## Phase 3 · Narration, captions and what they are Every spoken word and every caption carries a typed origin, and a cloned voice is impossible without consent. ### Steps 1. Refuse cloned voices without consent, in code The check belongs at the point of creation, not in a warning. Files: `app/voice.py`, `tests/test_voice_consent.py` 2. Type the origin of every caption track Transcribed, written or generated. No default. Files: `app/captions.py` 3. Require a review pass before burning captions in Burned-in text is the hardest thing in the file to correct. Files: `app/captions.py`, `tests/test_caption_review.py` ### Done when - [ ] A cloned voice without consent is refused - [ ] Every caption has a non-blank origin - [ ] Generated captions need review before burn-in - [ ] Speech engine and voice identifier are recorded ### Watch out - Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. ## Phase 4 · An export that actually starts playing The exported file plays immediately in a browser and on the platforms you named, with a manifest beside it. ### Steps 1. Move the index to the front and verify it moved The flag is documented as a second pass that can be skipped in some cases. Files: `app/export.py`, `tests/test_faststart.py` ```sh ffmpeg -i input.mp4 -movflags +faststart -c copy output.mp4 ``` 2. Correct odd dimensions and log the correction Better than an encoder error twenty minutes into a render. Files: `app/export.py` 3. Write the manifest with the output, always It is the only record of what is in the file. Files: `app/manifest.py` ### Done when - [ ] The file starts playing before it finishes downloading - [ ] Odd dimensions are corrected and logged - [ ] The command and filter graph are saved beside the output - [ ] The manifest lists assets, licences and generated elements ### Watch out - The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. ## Not in this build - Faces or voices of real people without a consent record. - A stock media library. - Direct publishing to video platforms. - Collaborative editing. ## After v1, if you want it - A second export preset for a platform you actually use, verified by uploading rather than by reading a specification - Scene detection on long source clips, which turns the tedious part of assembling into a review task - A licence report for a finished project, generated from the manifest, which is the document you would want if anyone ever asked ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Project JSON and per-project asset folders. This is what you back up. PROJECT_DIR=./data/projects # Required. Proxies, intermediates and render logs. Disposable by design. WORK_DIR=./data/work # Required. A render that would exceed this is refused before it starts. DISK_CEILING_GB=40 # Required. Pinned, so you know which build produced a given output. FFMPEG_BIN=/usr/bin/ffmpeg # Optional · secret. Generated speech only. Without it, narration is recorded rather than generated. TTS_API_KEY=sk-...redacted # Required. Even when true, a consent row is still required. This only gates the import path. ALLOW_LIKENESS_ASSETS=false # Required. One at a time. Two renders on one machine is two slow renders. RENDER_CONCURRENCY=1
You are building a lean indie version of InVideo. 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 ===== # InVideo · indie build A local video assembler whose hard problems are keeping a render reproducible from a file rather than from a session, refusing likeness and voice work without a consent record, and exporting a file that actually starts playing when someone uploads it. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with FastAPI and a browser interface, everything on your own machine | video is large, and shipping it somewhere to render is what the subscription is really charging for | | Renderer | FFmpeg invoked with an explicit filter graph written to a file per render | a render you cannot reproduce from a text file is a render you cannot debug | | Project format | One JSON project file referencing assets by path and content hash | the project has to outlive the application that made it | | Provenance | A required per-asset record of origin, licence, consent and whether it was generated | this is the part free tools skip and the part that decides whether you can publish | | Preview | A low-resolution proxy rendered from the same filter graph as the final | previewing by seeking in source files shows you something the export will not match | ## 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 service that drives FFmpeg and holds the project. - Get it: python.org or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg with the encoders you intend to use** · free - Why: Every render is an FFmpeg invocation, and a build missing an encoder fails at export time rather than at setup time. - Get it: Your package manager, or a static build. - Verify: ffmpeg -encoders lists the video and audio encoders your presets name - [ ] **Working disk space with a number attached** · free - Why: Proxies, intermediates and failed renders fill a disk quietly, and the ceiling in this build is only useful if you decided what it should be. - Get it: Choose a working directory on a volume with room and write the ceiling into the environment. - Verify: The application refuses a render that would exceed the ceiling - [ ] **Your own footage, including one variable frame rate clip** · free - Why: Phone video with a variable frame rate is where timeline drift comes from, and a project that only ever sees clean twenty-five frame files will not show you the bug. - Get it: Record something on a phone and import it unmodified. - Verify: The probe reports an average rather than a constant frame rate and the timeline still holds - [ ] **Your rule for likeness and voice** · free - Why: Deciding after you have built the generation path means deciding it under pressure, and the schema needs the consent table before anything references it. - Get it: Write down what evidence counts as consent, how long it lasts and what happens when it expires. - Verify: The rule is in the README and the consent table enforces the expiry ## Quick start ```sh ffmpeg -i input.mp4 -movflags +faststart -c copy output.mp4 ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Faces or voices of real people without a consent record. - A stock media library. - Direct publishing to video platforms. - Collaborative editing. - production codecs, rendering speed, and media templates - hosted recording reliability - large-scale rendering - distribution and podcast hosting - professional codecs, effects, and collaboration If one of those is essential to you, that is the reason to keep paying for InVideo, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · InVideo The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local video assembler that turns a script and your own footage into an exported file, with every source, licence and generated element recorded to replace InVideo. 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 A tool that generates video runs straight into three questions, and the answers shape the schema. Likeness first. Do not build a path that produces the face or the voice of a real person without their permission. Tennessee's ELVIS Act, in force since the first of July 2024, made a person's voice a property right and defines voice to include a simulation of it, so a cloned voice is actionable whether or not any real recording was used, and other places are moving the same way. The build therefore refuses to accept a reference face or a voice sample without a consent record naming the person, the scope and the date. Check the law where you actually are before you rely on any of this; the point is that the schema has a consent row, not that one statute covers you. Licensing second. Every asset carries where it came from and under what licence, as required fields. A tool that pulls in free stock footage and exports a file with no record of any of it is a tool whose output you cannot safely publish, which is the whole reason you made the file. Disclosure third. Anything generated is marked as generated in the project and in whatever provenance you attach to the export, using the established action vocabulary rather than a field you invented. Removing that mark is not a feature and there is no setting for it. ### Stack (fixed, do not substitute) - Runtime: Python with FastAPI and a browser interface, everything on your own machine · video is large, and shipping it somewhere to render is what the subscription is really charging for - Renderer: FFmpeg invoked with an explicit filter graph written to a file per render · a render you cannot reproduce from a text file is a render you cannot debug - Project format: One JSON project file referencing assets by path and content hash · the project has to outlive the application that made it - Provenance: A required per-asset record of origin, licence, consent and whether it was generated · this is the part free tools skip and the part that decides whether you can publish - Preview: A low-resolution proxy rendered from the same filter graph as the final · previewing by seeking in source files shows you something the export will not match ### Data model (create this before Phase 1) - projects(id, name, created_at, width, height, fps, format_version) - assets(id, project_id, path, sha256, kind, duration_ms, source, licence, licence_url, generated_by, consent_id) · source and licence are NOT NULL, because an asset with unknown provenance is a liability rather than a file - consents(id, person_name, scope, evidence_path, obtained_at, expires_at) · required before any asset carrying a real face or voice is accepted at all - timeline(id, project_id, track, ordinal, asset_id, in_ms, out_ms, transform_json) - captions(id, project_id, start_ms, end_ms, text, origin) · origin is transcribed, written or generated, and it is never blank - renders(id, project_id, at, preset, filtergraph_path, output_path, duration_ms, exit_code, log_tail, asset_hashes_json) - Every render writes its filter graph to a file beside the output and records the hash of every asset it used. A video you cannot regenerate from a text file and a folder is not reproducible, and six months later that is the only question you will have about it. ### Phase 1 · Assets that know where they came from Build: Import files by copying or referencing, hash the bytes, probe duration, resolution and frame rate. Require source and licence on import with no default. Detect assets flagged as carrying a person's face or voice and refuse them until a consent row exists and is in date. Trap: Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. Done when: An import with a blank source or licence is rejected, tested. An asset marked as carrying a likeness or a voice cannot be added without a consent row that is in date, tested at the expiry boundary. Changing an asset's bytes after import causes the next render to fail with a named mismatch rather than producing a file, tested. The probe records duration, resolution and frame rate, including for variable frame rate phone footage where the reported rate is an average. Do not build yet: No timeline, preview, narration or export yet. ### Phase 2 · A timeline and a preview that matches the export Build: Build the timeline as ordered clips per track with in and out points and transforms. Generate the preview by rendering a low-resolution proxy from the same filter graph the final render uses, at the project frame rate, rather than by seeking in the source files. Trap: Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. Done when: A cut placed in the preview lands within one frame in the final export, tested by rendering both and comparing frame positions. Mixing a sixty and a twenty-five frame source produces a timeline with no drift over a five minute sequence, tested. The proxy render and the final render are produced by the same graph builder with different parameters, asserted by review and by a test that diffs the two graphs. Deleting the proxy cache and rebuilding gives an identical proxy. Do not build yet: No narration, captions, disclosure or final export yet. ### Phase 3 · Narration, captions and what they are Build: Accept recorded narration or generated speech. Generated speech from a stock voice records the engine and the voice identifier; anything cloned from a person requires a consent row and is refused otherwise. Captions carry an origin of transcribed, written or generated. Trap: Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. Done when: A voice asset flagged as cloned from a person cannot be generated or imported without a consent row, tested. Every caption row has a non-blank origin and the editor shows it, asserted in the schema and in a test. Generated captions cannot be burned in until marked reviewed, tested. The engine and voice identifier are recorded for every piece of generated speech and appear in the project export. Do not build yet: No final export settings, provenance attachment or render queue yet. ### Phase 4 · An export that actually starts playing Build: Export to a widely supported codec pair in an MP4 container with the index moved to the front. Keep dimensions even, record the exact command, and write a sidecar manifest listing every asset, its licence, its consent record and every generated element in the file. Trap: The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. Done when: The exported file begins playing in a browser before it has fully downloaded, tested by serving it with a throttled connection. Odd dimensions from a crop are corrected and the correction is logged rather than causing an encoder failure, tested with a deliberately odd crop. The exact FFmpeg command and filter graph are written beside the output. The sidecar manifest lists every asset with licence and consent references and every generated element, and is written even when the render fails partway. Do not build yet: No provenance signing, render queue, disk limits or project export yet. ### Phase 5 · Provenance, disk and the render queue Build: Attach content provenance to the export where you can sign it, and keep the sidecar manifest regardless. Run one render at a time with a disk ceiling, clean up proxies and failed outputs, and export the whole project as a folder of assets plus the JSON that references them. Trap: Treating an attached provenance manifest as proof the viewer will see is wrong twice over, because any tool downstream can strip it during a re-encode and because the specification itself notes that a manifest without a time stamp stops being valid once its signing credential expires or is revoked; attach it if you can, keep your own sidecar manifest in the project whatever happens to the file, and describe it in the README as a record you hold rather than a guarantee you offer. Done when: A project exports as a self-contained folder and reimports on another machine with every asset hash matching, tested. A render that would take the working directory past the disk ceiling is refused before it starts, with the shortfall named, tested. Failed renders and orphaned proxies are cleaned by a routine that never touches an asset referenced by a project, tested with a deliberately orphaned file. Provenance attachment failures are recorded as a warning and never silently skipped. Do not build yet: Nothing after this. ### Out of scope (and why) - Generating the face or the voice of a real person without a consent record, which is refused in code rather than discouraged in documentation. - A stock media library, because licensing somebody else's footage is a contract and a catalogue rather than a feature you can write. - Publishing directly to video platforms, which needs the same credential and quota machinery as a social scheduler and belongs in that project. - Collaborative editing, which means a server, locking and a merge model for a data type that does not merge well. ### README must contain - That likeness and voice assets require a consent record, and that you should check the law where you are rather than relying on the one this build was designed around - That every asset carries a source and a licence as required fields, and what happens when one is missing - That every render writes its filter graph and asset hashes beside the output, so any export can be reproduced - That the export moves the file index to the front, why that matters for playback, and how to verify it happened - That attached provenance can be stripped downstream, and that the sidecar manifest is the copy you keep ===== AGENTS.md ===== # Agent instructions · InVideo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with FastAPI and a browser interface, everything on your own machine, FFmpeg invoked with an explicit filter graph written to a file per render, One JSON project file referencing assets by path and content hash, A required per-asset record of origin, licence, consent and whether it was generated, A low-resolution proxy rendered from the same filter graph as the final. 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 - Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. - Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. - Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. - The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. ===== BUILD_PLAN.md ===== # Build plan · InVideo A local video assembler whose hard problems are keeping a render reproducible from a file rather than from a session, refusing likeness and voice work without a consent record, and exporting a file that actually starts playing when someone uploads it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Assets that know where they came from Nothing enters a project without a hash, a source, a licence and, where a person appears, a consent record. ### Steps 1. Hash on import and verify before every render The hash is the only thing that notices a file changed under you. Files: `app/assets.py`, `tests/test_hash_guard.py` 2. Make source and licence required fields A nullable licence column becomes a null licence column. Files: `app/models.py`, `migrations/0001_assets.sql` 3. Gate likeness and voice assets on a consent row A check in code, not a sentence in the README. Files: `app/consent.py`, `tests/test_consent_gate.py` ### Done when - [ ] Blank source or licence is rejected on import - [ ] A likeness asset without consent cannot be added - [ ] A changed asset fails the render, not the check - [ ] Probe data includes frame rate for variable rate footage ### Watch out - Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. ## Phase 2 · A timeline and a preview that matches the export What you approve in the preview is what comes out of the final render. ### Steps 1. Normalise frame rate at import, not at render Drift you fix at the end is drift you have already approved. Files: `app/normalise.py` 2. Build one filter graph function, two presets The preview must not be a second implementation. Files: `app/graph.py`, `tests/test_graph_parity.py` 3. Compare preview and final frame positions in a test This is the assertion that keeps them honest. Files: `tests/test_cut_positions.py` ### Done when - [ ] A preview cut lands within one frame in the export - [ ] Mixed frame rates produce no drift over five minutes - [ ] Preview and final come from one graph builder - [ ] Proxies rebuild identically ### Watch out - Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. ## Phase 3 · Narration, captions and what they are Every spoken word and every caption carries a typed origin, and a cloned voice is impossible without consent. ### Steps 1. Refuse cloned voices without consent, in code The check belongs at the point of creation, not in a warning. Files: `app/voice.py`, `tests/test_voice_consent.py` 2. Type the origin of every caption track Transcribed, written or generated. No default. Files: `app/captions.py` 3. Require a review pass before burning captions in Burned-in text is the hardest thing in the file to correct. Files: `app/captions.py`, `tests/test_caption_review.py` ### Done when - [ ] A cloned voice without consent is refused - [ ] Every caption has a non-blank origin - [ ] Generated captions need review before burn-in - [ ] Speech engine and voice identifier are recorded ### Watch out - Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. ## Phase 4 · An export that actually starts playing The exported file plays immediately in a browser and on the platforms you named, with a manifest beside it. ### Steps 1. Move the index to the front and verify it moved The flag is documented as a second pass that can be skipped in some cases. Files: `app/export.py`, `tests/test_faststart.py` ```sh ffmpeg -i input.mp4 -movflags +faststart -c copy output.mp4 ``` 2. Correct odd dimensions and log the correction Better than an encoder error twenty minutes into a render. Files: `app/export.py` 3. Write the manifest with the output, always It is the only record of what is in the file. Files: `app/manifest.py` ### Done when - [ ] The file starts playing before it finishes downloading - [ ] Odd dimensions are corrected and logged - [ ] The command and filter graph are saved beside the output - [ ] The manifest lists assets, licences and generated elements ### Watch out - The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. ## Not in this build - Faces or voices of real people without a consent record. - A stock media library. - Direct publishing to video platforms. - Collaborative editing. ## After v1, if you want it - A second export preset for a platform you actually use, verified by uploading rather than by reading a specification - Scene detection on long source clips, which turns the tedious part of assembling into a review task - A licence report for a finished project, generated from the manifest, which is the document you would want if anyone ever asked ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Project JSON and per-project asset folders. This is what you back up. PROJECT_DIR=./data/projects # Required. Proxies, intermediates and render logs. Disposable by design. WORK_DIR=./data/work # Required. A render that would exceed this is refused before it starts. DISK_CEILING_GB=40 # Required. Pinned, so you know which build produced a given output. FFMPEG_BIN=/usr/bin/ffmpeg # Optional · secret. Generated speech only. Without it, narration is recorded rather than generated. TTS_API_KEY=sk-...redacted # Required. Even when true, a consent row is still required. This only gates the import path. ALLOW_LIKENESS_ASSETS=false # Required. One at a time. Two renders on one machine is two slow renders. RENDER_CONCURRENCY=1
You are building a production product version of InVideo. 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 ===== # InVideo · product brief ## Problem The visible AI video creation loop is buildable, but a credible replacement needs more than the first screen. InVideo earns its keep through media pipeline, hosting, distribution, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A video assembler where every asset carries its origin and licence, a likeness or a voice needs a consent record before it can exist, every render is reproducible from a filter graph on disk, and the exported file starts playing rather than downloading. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.12 - FFmpeg - desktop with sufficient storage - optional model or API key in .env ## Explicit non-goals for v1 - Faces or voices of real people without a consent record. - A stock media library. - Direct publishing to video platforms. - Collaborative editing. - production codecs, rendering speed, and media templates - hosted recording reliability - large-scale rendering - distribution and podcast hosting - professional codecs, effects, and collaboration ## Success criteria - An import with a blank source or licence is confirmed rejected - A likeness or voice asset without consent is confirmed refused - A preview cut is confirmed to land within one frame in the export - The exported file is confirmed to start playing before it downloads - A project is confirmed to round-trip to another machine by hash ===== BRIEF.md ===== # Build brief · InVideo The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local video assembler that turns a script and your own footage into an exported file, with every source, licence and generated element recorded to replace InVideo. 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 A tool that generates video runs straight into three questions, and the answers shape the schema. Likeness first. Do not build a path that produces the face or the voice of a real person without their permission. Tennessee's ELVIS Act, in force since the first of July 2024, made a person's voice a property right and defines voice to include a simulation of it, so a cloned voice is actionable whether or not any real recording was used, and other places are moving the same way. The build therefore refuses to accept a reference face or a voice sample without a consent record naming the person, the scope and the date. Check the law where you actually are before you rely on any of this; the point is that the schema has a consent row, not that one statute covers you. Licensing second. Every asset carries where it came from and under what licence, as required fields. A tool that pulls in free stock footage and exports a file with no record of any of it is a tool whose output you cannot safely publish, which is the whole reason you made the file. Disclosure third. Anything generated is marked as generated in the project and in whatever provenance you attach to the export, using the established action vocabulary rather than a field you invented. Removing that mark is not a feature and there is no setting for it. ### Stack (fixed, do not substitute) - Runtime: Python with FastAPI and a browser interface, everything on your own machine · video is large, and shipping it somewhere to render is what the subscription is really charging for - Renderer: FFmpeg invoked with an explicit filter graph written to a file per render · a render you cannot reproduce from a text file is a render you cannot debug - Project format: One JSON project file referencing assets by path and content hash · the project has to outlive the application that made it - Provenance: A required per-asset record of origin, licence, consent and whether it was generated · this is the part free tools skip and the part that decides whether you can publish - Preview: A low-resolution proxy rendered from the same filter graph as the final · previewing by seeking in source files shows you something the export will not match ### Data model (create this before Phase 1) - projects(id, name, created_at, width, height, fps, format_version) - assets(id, project_id, path, sha256, kind, duration_ms, source, licence, licence_url, generated_by, consent_id) · source and licence are NOT NULL, because an asset with unknown provenance is a liability rather than a file - consents(id, person_name, scope, evidence_path, obtained_at, expires_at) · required before any asset carrying a real face or voice is accepted at all - timeline(id, project_id, track, ordinal, asset_id, in_ms, out_ms, transform_json) - captions(id, project_id, start_ms, end_ms, text, origin) · origin is transcribed, written or generated, and it is never blank - renders(id, project_id, at, preset, filtergraph_path, output_path, duration_ms, exit_code, log_tail, asset_hashes_json) - Every render writes its filter graph to a file beside the output and records the hash of every asset it used. A video you cannot regenerate from a text file and a folder is not reproducible, and six months later that is the only question you will have about it. ### Phase 1 · Assets that know where they came from Build: Import files by copying or referencing, hash the bytes, probe duration, resolution and frame rate. Require source and licence on import with no default. Detect assets flagged as carrying a person's face or voice and refuse them until a consent row exists and is in date. Trap: Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. Done when: An import with a blank source or licence is rejected, tested. An asset marked as carrying a likeness or a voice cannot be added without a consent row that is in date, tested at the expiry boundary. Changing an asset's bytes after import causes the next render to fail with a named mismatch rather than producing a file, tested. The probe records duration, resolution and frame rate, including for variable frame rate phone footage where the reported rate is an average. Do not build yet: No timeline, preview, narration or export yet. ### Phase 2 · A timeline and a preview that matches the export Build: Build the timeline as ordered clips per track with in and out points and transforms. Generate the preview by rendering a low-resolution proxy from the same filter graph the final render uses, at the project frame rate, rather than by seeking in the source files. Trap: Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. Done when: A cut placed in the preview lands within one frame in the final export, tested by rendering both and comparing frame positions. Mixing a sixty and a twenty-five frame source produces a timeline with no drift over a five minute sequence, tested. The proxy render and the final render are produced by the same graph builder with different parameters, asserted by review and by a test that diffs the two graphs. Deleting the proxy cache and rebuilding gives an identical proxy. Do not build yet: No narration, captions, disclosure or final export yet. ### Phase 3 · Narration, captions and what they are Build: Accept recorded narration or generated speech. Generated speech from a stock voice records the engine and the voice identifier; anything cloned from a person requires a consent row and is refused otherwise. Captions carry an origin of transcribed, written or generated. Trap: Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. Done when: A voice asset flagged as cloned from a person cannot be generated or imported without a consent row, tested. Every caption row has a non-blank origin and the editor shows it, asserted in the schema and in a test. Generated captions cannot be burned in until marked reviewed, tested. The engine and voice identifier are recorded for every piece of generated speech and appear in the project export. Do not build yet: No final export settings, provenance attachment or render queue yet. ### Phase 4 · An export that actually starts playing Build: Export to a widely supported codec pair in an MP4 container with the index moved to the front. Keep dimensions even, record the exact command, and write a sidecar manifest listing every asset, its licence, its consent record and every generated element in the file. Trap: The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. Done when: The exported file begins playing in a browser before it has fully downloaded, tested by serving it with a throttled connection. Odd dimensions from a crop are corrected and the correction is logged rather than causing an encoder failure, tested with a deliberately odd crop. The exact FFmpeg command and filter graph are written beside the output. The sidecar manifest lists every asset with licence and consent references and every generated element, and is written even when the render fails partway. Do not build yet: No provenance signing, render queue, disk limits or project export yet. ### Phase 5 · Provenance, disk and the render queue Build: Attach content provenance to the export where you can sign it, and keep the sidecar manifest regardless. Run one render at a time with a disk ceiling, clean up proxies and failed outputs, and export the whole project as a folder of assets plus the JSON that references them. Trap: Treating an attached provenance manifest as proof the viewer will see is wrong twice over, because any tool downstream can strip it during a re-encode and because the specification itself notes that a manifest without a time stamp stops being valid once its signing credential expires or is revoked; attach it if you can, keep your own sidecar manifest in the project whatever happens to the file, and describe it in the README as a record you hold rather than a guarantee you offer. Done when: A project exports as a self-contained folder and reimports on another machine with every asset hash matching, tested. A render that would take the working directory past the disk ceiling is refused before it starts, with the shortfall named, tested. Failed renders and orphaned proxies are cleaned by a routine that never touches an asset referenced by a project, tested with a deliberately orphaned file. Provenance attachment failures are recorded as a warning and never silently skipped. Do not build yet: Nothing after this. ### Out of scope (and why) - Generating the face or the voice of a real person without a consent record, which is refused in code rather than discouraged in documentation. - A stock media library, because licensing somebody else's footage is a contract and a catalogue rather than a feature you can write. - Publishing directly to video platforms, which needs the same credential and quota machinery as a social scheduler and belongs in that project. - Collaborative editing, which means a server, locking and a merge model for a data type that does not merge well. ### README must contain - That likeness and voice assets require a consent record, and that you should check the law where you are rather than relying on the one this build was designed around - That every asset carries a source and a licence as required fields, and what happens when one is missing - That every render writes its filter graph and asset hashes beside the output, so any export can be reproduced - That the export moves the file index to the front, why that matters for playback, and how to verify it happened - That attached provenance can be stripped downstream, and that the sidecar manifest is the copy you keep ===== ARCHITECTURE.md ===== # Architecture · InVideo ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with FastAPI and a browser interface, everything on your own machine | video is large, and shipping it somewhere to render is what the subscription is really charging for | | Renderer | FFmpeg invoked with an explicit filter graph written to a file per render | a render you cannot reproduce from a text file is a render you cannot debug | | Project format | One JSON project file referencing assets by path and content hash | the project has to outlive the application that made it | | Provenance | A required per-asset record of origin, licence, consent and whether it was generated | this is the part free tools skip and the part that decides whether you can publish | | Preview | A low-resolution proxy rendered from the same filter graph as the final | previewing by seeking in source files shows you something the export will not match | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Assets | Import, hashing, probe, licence and consent gates. | Any importer, provided source and licence stay required. | | Timeline | Clips, tracks, transforms, frame rate normalisation. | Any editor model, provided normalisation happens at import. | | Graph | One filter graph builder for proxy and final. | Not swappable for two builders, which is how previews start lying. | | Export | Encoding presets, index placement, the manifest. | Any preset set, provided the manifest is always written. | | Queue | One render at a time, disk ceiling, cleanup. | Any scheduler, provided cleanup never touches referenced assets. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PROJECT_DIR` · required · Project JSON and per-project asset folders. This is what you back up. - `WORK_DIR` · required · Proxies, intermediates and render logs. Disposable by design. - `DISK_CEILING_GB` · required · A render that would exceed this is refused before it starts. - `FFMPEG_BIN` · required · Pinned, so you know which build produced a given output. - `TTS_API_KEY` · optional, secret · Generated speech only. Without it, narration is recorded rather than generated. - `ALLOW_LIKENESS_ASSETS` · required · Even when true, a consent row is still required. This only gates the import path. - `RENDER_CONCURRENCY` · required · One at a time. Two renders on one machine is two slow renders. ## 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 · InVideo product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python with FastAPI and a browser interface, everything on your own machine, FFmpeg invoked with an explicit filter graph written to a file per render, One JSON project file referencing assets by path and content hash, A required per-asset record of origin, licence, consent and whether it was generated, A low-resolution proxy rendered from the same filter graph as the final. - 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 - Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. - Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. - Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. - The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. - Treating an attached provenance manifest as proof the viewer will see is wrong twice over, because any tool downstream can strip it during a re-encode and because the specification itself notes that a manifest without a time stamp stops being valid once its signing credential expires or is revoked; attach it if you can, keep your own sidecar manifest in the project whatever happens to the file, and describe it in the README as a record you hold rather than a guarantee you offer. ===== MILESTONES.md ===== # Delivery milestones · InVideo Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Assets that know where they came from Nothing enters a project without a hash, a source, a licence and, where a person appears, a consent record. ### Steps 1. Hash on import and verify before every render The hash is the only thing that notices a file changed under you. Files: `app/assets.py`, `tests/test_hash_guard.py` 2. Make source and licence required fields A nullable licence column becomes a null licence column. Files: `app/models.py`, `migrations/0001_assets.sql` 3. Gate likeness and voice assets on a consent row A check in code, not a sentence in the README. Files: `app/consent.py`, `tests/test_consent_gate.py` ### Done when - [ ] Blank source or licence is rejected on import - [ ] A likeness asset without consent cannot be added - [ ] A changed asset fails the render, not the check - [ ] Probe data includes frame rate for variable rate footage ### Watch out - Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. ## M2 · A timeline and a preview that matches the export What you approve in the preview is what comes out of the final render. ### Steps 1. Normalise frame rate at import, not at render Drift you fix at the end is drift you have already approved. Files: `app/normalise.py` 2. Build one filter graph function, two presets The preview must not be a second implementation. Files: `app/graph.py`, `tests/test_graph_parity.py` 3. Compare preview and final frame positions in a test This is the assertion that keeps them honest. Files: `tests/test_cut_positions.py` ### Done when - [ ] A preview cut lands within one frame in the export - [ ] Mixed frame rates produce no drift over five minutes - [ ] Preview and final come from one graph builder - [ ] Proxies rebuild identically ### Watch out - Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. ## M3 · Narration, captions and what they are Every spoken word and every caption carries a typed origin, and a cloned voice is impossible without consent. ### Steps 1. Refuse cloned voices without consent, in code The check belongs at the point of creation, not in a warning. Files: `app/voice.py`, `tests/test_voice_consent.py` 2. Type the origin of every caption track Transcribed, written or generated. No default. Files: `app/captions.py` 3. Require a review pass before burning captions in Burned-in text is the hardest thing in the file to correct. Files: `app/captions.py`, `tests/test_caption_review.py` ### Done when - [ ] A cloned voice without consent is refused - [ ] Every caption has a non-blank origin - [ ] Generated captions need review before burn-in - [ ] Speech engine and voice identifier are recorded ### Watch out - Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. ## M4 · An export that actually starts playing The exported file plays immediately in a browser and on the platforms you named, with a manifest beside it. ### Steps 1. Move the index to the front and verify it moved The flag is documented as a second pass that can be skipped in some cases. Files: `app/export.py`, `tests/test_faststart.py` ```sh ffmpeg -i input.mp4 -movflags +faststart -c copy output.mp4 ``` 2. Correct odd dimensions and log the correction Better than an encoder error twenty minutes into a render. Files: `app/export.py` 3. Write the manifest with the output, always It is the only record of what is in the file. Files: `app/manifest.py` ### Done when - [ ] The file starts playing before it finishes downloading - [ ] Odd dimensions are corrected and logged - [ ] The command and filter graph are saved beside the output - [ ] The manifest lists assets, licences and generated elements ### Watch out - The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. ## M5 · Provenance, disk and the render queue (production only) The project survives the application, renders cannot fill the disk, and provenance is kept as a record rather than promised as a guarantee. ### Steps 1. Export the project as assets plus JSON, and reimport it The test is another machine, not another folder. Files: `app/project_io.py`, `tests/test_roundtrip.py` 2. Refuse a render that would fill the disk Check before, not after twenty gigabytes of temporary files. Files: `app/queue.py` 3. Keep the sidecar manifest whatever happens to the file It is the copy nobody downstream can strip. Files: `app/manifest.py`, `README.md` ### Done when - [ ] A project round-trips to another machine with matching hashes - [ ] A render over the disk ceiling is refused up front - [ ] Cleanup never removes a referenced asset - [ ] Provenance failures are warnings, never silent ### Watch out - Treating an attached provenance manifest as proof the viewer will see is wrong twice over, because any tool downstream can strip it during a re-encode and because the specification itself notes that a manifest without a time stamp stops being valid once its signing credential expires or is revoked; attach it if you can, keep your own sidecar manifest in the project whatever happens to the file, and describe it in the README as a record you hold rather than a guarantee you offer. ===== OPERATIONS.md ===== # Operations · InVideo ## Backup The project directory, which is the JSON plus the assets it references. The working directory is explicitly not backed up, because everything in it can be rebuilt from the project and it is the largest thing on the disk. ## Restore Copy the project directory to the new machine, reimport, and let the hash check confirm every asset arrived intact. A restore where hashes mismatch tells you which files are wrong before you render, which is the whole reason they are stored. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Working directory size against the ceiling, failed render exit codes, and assets whose hash no longer matches. A growing count of hash mismatches usually means something else on the machine is rewriting your media, which is worth knowing early. ## Incident checklist The incident is publishing something you should not have: an unlicensed clip, an unconsented voice, an undisclosed generated element. The manifest beside each render is what lets you answer what was in a given file on a given day, which is why it is written even when the render fails. 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 - [ ] An import with a blank source or licence is confirmed rejected - [ ] A likeness or voice asset without consent is confirmed refused - [ ] A preview cut is confirmed to land within one frame in the export - [ ] The exported file is confirmed to start playing before it downloads - [ ] A project is confirmed to round-trip to another machine by hash ## Launch constraint Do not market omitted InVideo 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. Project JSON and per-project asset folders. This is what you back up. PROJECT_DIR=./data/projects # Required. Proxies, intermediates and render logs. Disposable by design. WORK_DIR=./data/work # Required. A render that would exceed this is refused before it starts. DISK_CEILING_GB=40 # Required. Pinned, so you know which build produced a given output. FFMPEG_BIN=/usr/bin/ffmpeg # Optional · secret. Generated speech only. Without it, narration is recorded rather than generated. TTS_API_KEY=sk-...redacted # Required. Even when true, a consent row is still required. This only gates the import path. ALLOW_LIKENESS_ASSETS=false # Required. One at a time. Two renders on one machine is two slow renders. RENDER_CONCURRENCY=1
# InVideo · indie build A local video assembler whose hard problems are keeping a render reproducible from a file rather than from a session, refusing likeness and voice work without a consent record, and exporting a file that actually starts playing when someone uploads it. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with FastAPI and a browser interface, everything on your own machine | video is large, and shipping it somewhere to render is what the subscription is really charging for | | Renderer | FFmpeg invoked with an explicit filter graph written to a file per render | a render you cannot reproduce from a text file is a render you cannot debug | | Project format | One JSON project file referencing assets by path and content hash | the project has to outlive the application that made it | | Provenance | A required per-asset record of origin, licence, consent and whether it was generated | this is the part free tools skip and the part that decides whether you can publish | | Preview | A low-resolution proxy rendered from the same filter graph as the final | previewing by seeking in source files shows you something the export will not match | ## 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 service that drives FFmpeg and holds the project. - Get it: python.org or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg with the encoders you intend to use** · free - Why: Every render is an FFmpeg invocation, and a build missing an encoder fails at export time rather than at setup time. - Get it: Your package manager, or a static build. - Verify: ffmpeg -encoders lists the video and audio encoders your presets name - [ ] **Working disk space with a number attached** · free - Why: Proxies, intermediates and failed renders fill a disk quietly, and the ceiling in this build is only useful if you decided what it should be. - Get it: Choose a working directory on a volume with room and write the ceiling into the environment. - Verify: The application refuses a render that would exceed the ceiling - [ ] **Your own footage, including one variable frame rate clip** · free - Why: Phone video with a variable frame rate is where timeline drift comes from, and a project that only ever sees clean twenty-five frame files will not show you the bug. - Get it: Record something on a phone and import it unmodified. - Verify: The probe reports an average rather than a constant frame rate and the timeline still holds - [ ] **Your rule for likeness and voice** · free - Why: Deciding after you have built the generation path means deciding it under pressure, and the schema needs the consent table before anything references it. - Get it: Write down what evidence counts as consent, how long it lasts and what happens when it expires. - Verify: The rule is in the README and the consent table enforces the expiry ## Quick start ```sh ffmpeg -i input.mp4 -movflags +faststart -c copy output.mp4 ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Faces or voices of real people without a consent record. - A stock media library. - Direct publishing to video platforms. - Collaborative editing. - production codecs, rendering speed, and media templates - hosted recording reliability - large-scale rendering - distribution and podcast hosting - professional codecs, effects, and collaboration If one of those is essential to you, that is the reason to keep paying for InVideo, and the README should say so rather than pretend.
# Build brief · InVideo The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local video assembler that turns a script and your own footage into an exported file, with every source, licence and generated element recorded to replace InVideo. 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 A tool that generates video runs straight into three questions, and the answers shape the schema. Likeness first. Do not build a path that produces the face or the voice of a real person without their permission. Tennessee's ELVIS Act, in force since the first of July 2024, made a person's voice a property right and defines voice to include a simulation of it, so a cloned voice is actionable whether or not any real recording was used, and other places are moving the same way. The build therefore refuses to accept a reference face or a voice sample without a consent record naming the person, the scope and the date. Check the law where you actually are before you rely on any of this; the point is that the schema has a consent row, not that one statute covers you. Licensing second. Every asset carries where it came from and under what licence, as required fields. A tool that pulls in free stock footage and exports a file with no record of any of it is a tool whose output you cannot safely publish, which is the whole reason you made the file. Disclosure third. Anything generated is marked as generated in the project and in whatever provenance you attach to the export, using the established action vocabulary rather than a field you invented. Removing that mark is not a feature and there is no setting for it. ### Stack (fixed, do not substitute) - Runtime: Python with FastAPI and a browser interface, everything on your own machine · video is large, and shipping it somewhere to render is what the subscription is really charging for - Renderer: FFmpeg invoked with an explicit filter graph written to a file per render · a render you cannot reproduce from a text file is a render you cannot debug - Project format: One JSON project file referencing assets by path and content hash · the project has to outlive the application that made it - Provenance: A required per-asset record of origin, licence, consent and whether it was generated · this is the part free tools skip and the part that decides whether you can publish - Preview: A low-resolution proxy rendered from the same filter graph as the final · previewing by seeking in source files shows you something the export will not match ### Data model (create this before Phase 1) - projects(id, name, created_at, width, height, fps, format_version) - assets(id, project_id, path, sha256, kind, duration_ms, source, licence, licence_url, generated_by, consent_id) · source and licence are NOT NULL, because an asset with unknown provenance is a liability rather than a file - consents(id, person_name, scope, evidence_path, obtained_at, expires_at) · required before any asset carrying a real face or voice is accepted at all - timeline(id, project_id, track, ordinal, asset_id, in_ms, out_ms, transform_json) - captions(id, project_id, start_ms, end_ms, text, origin) · origin is transcribed, written or generated, and it is never blank - renders(id, project_id, at, preset, filtergraph_path, output_path, duration_ms, exit_code, log_tail, asset_hashes_json) - Every render writes its filter graph to a file beside the output and records the hash of every asset it used. A video you cannot regenerate from a text file and a folder is not reproducible, and six months later that is the only question you will have about it. ### Phase 1 · Assets that know where they came from Build: Import files by copying or referencing, hash the bytes, probe duration, resolution and frame rate. Require source and licence on import with no default. Detect assets flagged as carrying a person's face or voice and refuse them until a consent row exists and is in date. Trap: Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. Done when: An import with a blank source or licence is rejected, tested. An asset marked as carrying a likeness or a voice cannot be added without a consent row that is in date, tested at the expiry boundary. Changing an asset's bytes after import causes the next render to fail with a named mismatch rather than producing a file, tested. The probe records duration, resolution and frame rate, including for variable frame rate phone footage where the reported rate is an average. Do not build yet: No timeline, preview, narration or export yet. ### Phase 2 · A timeline and a preview that matches the export Build: Build the timeline as ordered clips per track with in and out points and transforms. Generate the preview by rendering a low-resolution proxy from the same filter graph the final render uses, at the project frame rate, rather than by seeking in the source files. Trap: Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. Done when: A cut placed in the preview lands within one frame in the final export, tested by rendering both and comparing frame positions. Mixing a sixty and a twenty-five frame source produces a timeline with no drift over a five minute sequence, tested. The proxy render and the final render are produced by the same graph builder with different parameters, asserted by review and by a test that diffs the two graphs. Deleting the proxy cache and rebuilding gives an identical proxy. Do not build yet: No narration, captions, disclosure or final export yet. ### Phase 3 · Narration, captions and what they are Build: Accept recorded narration or generated speech. Generated speech from a stock voice records the engine and the voice identifier; anything cloned from a person requires a consent row and is refused otherwise. Captions carry an origin of transcribed, written or generated. Trap: Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. Done when: A voice asset flagged as cloned from a person cannot be generated or imported without a consent row, tested. Every caption row has a non-blank origin and the editor shows it, asserted in the schema and in a test. Generated captions cannot be burned in until marked reviewed, tested. The engine and voice identifier are recorded for every piece of generated speech and appear in the project export. Do not build yet: No final export settings, provenance attachment or render queue yet. ### Phase 4 · An export that actually starts playing Build: Export to a widely supported codec pair in an MP4 container with the index moved to the front. Keep dimensions even, record the exact command, and write a sidecar manifest listing every asset, its licence, its consent record and every generated element in the file. Trap: The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. Done when: The exported file begins playing in a browser before it has fully downloaded, tested by serving it with a throttled connection. Odd dimensions from a crop are corrected and the correction is logged rather than causing an encoder failure, tested with a deliberately odd crop. The exact FFmpeg command and filter graph are written beside the output. The sidecar manifest lists every asset with licence and consent references and every generated element, and is written even when the render fails partway. Do not build yet: No provenance signing, render queue, disk limits or project export yet. ### Phase 5 · Provenance, disk and the render queue Build: Attach content provenance to the export where you can sign it, and keep the sidecar manifest regardless. Run one render at a time with a disk ceiling, clean up proxies and failed outputs, and export the whole project as a folder of assets plus the JSON that references them. Trap: Treating an attached provenance manifest as proof the viewer will see is wrong twice over, because any tool downstream can strip it during a re-encode and because the specification itself notes that a manifest without a time stamp stops being valid once its signing credential expires or is revoked; attach it if you can, keep your own sidecar manifest in the project whatever happens to the file, and describe it in the README as a record you hold rather than a guarantee you offer. Done when: A project exports as a self-contained folder and reimports on another machine with every asset hash matching, tested. A render that would take the working directory past the disk ceiling is refused before it starts, with the shortfall named, tested. Failed renders and orphaned proxies are cleaned by a routine that never touches an asset referenced by a project, tested with a deliberately orphaned file. Provenance attachment failures are recorded as a warning and never silently skipped. Do not build yet: Nothing after this. ### Out of scope (and why) - Generating the face or the voice of a real person without a consent record, which is refused in code rather than discouraged in documentation. - A stock media library, because licensing somebody else's footage is a contract and a catalogue rather than a feature you can write. - Publishing directly to video platforms, which needs the same credential and quota machinery as a social scheduler and belongs in that project. - Collaborative editing, which means a server, locking and a merge model for a data type that does not merge well. ### README must contain - That likeness and voice assets require a consent record, and that you should check the law where you are rather than relying on the one this build was designed around - That every asset carries a source and a licence as required fields, and what happens when one is missing - That every render writes its filter graph and asset hashes beside the output, so any export can be reproduced - That the export moves the file index to the front, why that matters for playback, and how to verify it happened - That attached provenance can be stripped downstream, and that the sidecar manifest is the copy you keep
# Agent instructions · InVideo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with FastAPI and a browser interface, everything on your own machine, FFmpeg invoked with an explicit filter graph written to a file per render, One JSON project file referencing assets by path and content hash, A required per-asset record of origin, licence, consent and whether it was generated, A low-resolution proxy rendered from the same filter graph as the final. 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 - Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. - Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. - Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. - The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag.
# Build plan · InVideo A local video assembler whose hard problems are keeping a render reproducible from a file rather than from a session, refusing likeness and voice work without a consent record, and exporting a file that actually starts playing when someone uploads it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Assets that know where they came from Nothing enters a project without a hash, a source, a licence and, where a person appears, a consent record. ### Steps 1. Hash on import and verify before every render The hash is the only thing that notices a file changed under you. Files: `app/assets.py`, `tests/test_hash_guard.py` 2. Make source and licence required fields A nullable licence column becomes a null licence column. Files: `app/models.py`, `migrations/0001_assets.sql` 3. Gate likeness and voice assets on a consent row A check in code, not a sentence in the README. Files: `app/consent.py`, `tests/test_consent_gate.py` ### Done when - [ ] Blank source or licence is rejected on import - [ ] A likeness asset without consent cannot be added - [ ] A changed asset fails the render, not the check - [ ] Probe data includes frame rate for variable rate footage ### Watch out - Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. ## Phase 2 · A timeline and a preview that matches the export What you approve in the preview is what comes out of the final render. ### Steps 1. Normalise frame rate at import, not at render Drift you fix at the end is drift you have already approved. Files: `app/normalise.py` 2. Build one filter graph function, two presets The preview must not be a second implementation. Files: `app/graph.py`, `tests/test_graph_parity.py` 3. Compare preview and final frame positions in a test This is the assertion that keeps them honest. Files: `tests/test_cut_positions.py` ### Done when - [ ] A preview cut lands within one frame in the export - [ ] Mixed frame rates produce no drift over five minutes - [ ] Preview and final come from one graph builder - [ ] Proxies rebuild identically ### Watch out - Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. ## Phase 3 · Narration, captions and what they are Every spoken word and every caption carries a typed origin, and a cloned voice is impossible without consent. ### Steps 1. Refuse cloned voices without consent, in code The check belongs at the point of creation, not in a warning. Files: `app/voice.py`, `tests/test_voice_consent.py` 2. Type the origin of every caption track Transcribed, written or generated. No default. Files: `app/captions.py` 3. Require a review pass before burning captions in Burned-in text is the hardest thing in the file to correct. Files: `app/captions.py`, `tests/test_caption_review.py` ### Done when - [ ] A cloned voice without consent is refused - [ ] Every caption has a non-blank origin - [ ] Generated captions need review before burn-in - [ ] Speech engine and voice identifier are recorded ### Watch out - Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. ## Phase 4 · An export that actually starts playing The exported file plays immediately in a browser and on the platforms you named, with a manifest beside it. ### Steps 1. Move the index to the front and verify it moved The flag is documented as a second pass that can be skipped in some cases. Files: `app/export.py`, `tests/test_faststart.py` ```sh ffmpeg -i input.mp4 -movflags +faststart -c copy output.mp4 ``` 2. Correct odd dimensions and log the correction Better than an encoder error twenty minutes into a render. Files: `app/export.py` 3. Write the manifest with the output, always It is the only record of what is in the file. Files: `app/manifest.py` ### Done when - [ ] The file starts playing before it finishes downloading - [ ] Odd dimensions are corrected and logged - [ ] The command and filter graph are saved beside the output - [ ] The manifest lists assets, licences and generated elements ### Watch out - The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. ## Not in this build - Faces or voices of real people without a consent record. - A stock media library. - Direct publishing to video platforms. - Collaborative editing. ## After v1, if you want it - A second export preset for a platform you actually use, verified by uploading rather than by reading a specification - Scene detection on long source clips, which turns the tedious part of assembling into a review task - A licence report for a finished project, generated from the manifest, which is the document you would want if anyone ever asked
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Project JSON and per-project asset folders. This is what you back up. PROJECT_DIR=./data/projects # Required. Proxies, intermediates and render logs. Disposable by design. WORK_DIR=./data/work # Required. A render that would exceed this is refused before it starts. DISK_CEILING_GB=40 # Required. Pinned, so you know which build produced a given output. FFMPEG_BIN=/usr/bin/ffmpeg # Optional · secret. Generated speech only. Without it, narration is recorded rather than generated. TTS_API_KEY=sk-...redacted # Required. Even when true, a consent row is still required. This only gates the import path. ALLOW_LIKENESS_ASSETS=false # Required. One at a time. Two renders on one machine is two slow renders. RENDER_CONCURRENCY=1
# InVideo · product brief ## Problem The visible AI video creation loop is buildable, but a credible replacement needs more than the first screen. InVideo earns its keep through media pipeline, hosting, distribution, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A video assembler where every asset carries its origin and licence, a likeness or a voice needs a consent record before it can exist, every render is reproducible from a filter graph on disk, and the exported file starts playing rather than downloading. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.12 - FFmpeg - desktop with sufficient storage - optional model or API key in .env ## Explicit non-goals for v1 - Faces or voices of real people without a consent record. - A stock media library. - Direct publishing to video platforms. - Collaborative editing. - production codecs, rendering speed, and media templates - hosted recording reliability - large-scale rendering - distribution and podcast hosting - professional codecs, effects, and collaboration ## Success criteria - An import with a blank source or licence is confirmed rejected - A likeness or voice asset without consent is confirmed refused - A preview cut is confirmed to land within one frame in the export - The exported file is confirmed to start playing before it downloads - A project is confirmed to round-trip to another machine by hash
# Build brief · InVideo The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local video assembler that turns a script and your own footage into an exported file, with every source, licence and generated element recorded to replace InVideo. 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 A tool that generates video runs straight into three questions, and the answers shape the schema. Likeness first. Do not build a path that produces the face or the voice of a real person without their permission. Tennessee's ELVIS Act, in force since the first of July 2024, made a person's voice a property right and defines voice to include a simulation of it, so a cloned voice is actionable whether or not any real recording was used, and other places are moving the same way. The build therefore refuses to accept a reference face or a voice sample without a consent record naming the person, the scope and the date. Check the law where you actually are before you rely on any of this; the point is that the schema has a consent row, not that one statute covers you. Licensing second. Every asset carries where it came from and under what licence, as required fields. A tool that pulls in free stock footage and exports a file with no record of any of it is a tool whose output you cannot safely publish, which is the whole reason you made the file. Disclosure third. Anything generated is marked as generated in the project and in whatever provenance you attach to the export, using the established action vocabulary rather than a field you invented. Removing that mark is not a feature and there is no setting for it. ### Stack (fixed, do not substitute) - Runtime: Python with FastAPI and a browser interface, everything on your own machine · video is large, and shipping it somewhere to render is what the subscription is really charging for - Renderer: FFmpeg invoked with an explicit filter graph written to a file per render · a render you cannot reproduce from a text file is a render you cannot debug - Project format: One JSON project file referencing assets by path and content hash · the project has to outlive the application that made it - Provenance: A required per-asset record of origin, licence, consent and whether it was generated · this is the part free tools skip and the part that decides whether you can publish - Preview: A low-resolution proxy rendered from the same filter graph as the final · previewing by seeking in source files shows you something the export will not match ### Data model (create this before Phase 1) - projects(id, name, created_at, width, height, fps, format_version) - assets(id, project_id, path, sha256, kind, duration_ms, source, licence, licence_url, generated_by, consent_id) · source and licence are NOT NULL, because an asset with unknown provenance is a liability rather than a file - consents(id, person_name, scope, evidence_path, obtained_at, expires_at) · required before any asset carrying a real face or voice is accepted at all - timeline(id, project_id, track, ordinal, asset_id, in_ms, out_ms, transform_json) - captions(id, project_id, start_ms, end_ms, text, origin) · origin is transcribed, written or generated, and it is never blank - renders(id, project_id, at, preset, filtergraph_path, output_path, duration_ms, exit_code, log_tail, asset_hashes_json) - Every render writes its filter graph to a file beside the output and records the hash of every asset it used. A video you cannot regenerate from a text file and a folder is not reproducible, and six months later that is the only question you will have about it. ### Phase 1 · Assets that know where they came from Build: Import files by copying or referencing, hash the bytes, probe duration, resolution and frame rate. Require source and licence on import with no default. Detect assets flagged as carrying a person's face or voice and refuse them until a consent row exists and is in date. Trap: Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. Done when: An import with a blank source or licence is rejected, tested. An asset marked as carrying a likeness or a voice cannot be added without a consent row that is in date, tested at the expiry boundary. Changing an asset's bytes after import causes the next render to fail with a named mismatch rather than producing a file, tested. The probe records duration, resolution and frame rate, including for variable frame rate phone footage where the reported rate is an average. Do not build yet: No timeline, preview, narration or export yet. ### Phase 2 · A timeline and a preview that matches the export Build: Build the timeline as ordered clips per track with in and out points and transforms. Generate the preview by rendering a low-resolution proxy from the same filter graph the final render uses, at the project frame rate, rather than by seeking in the source files. Trap: Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. Done when: A cut placed in the preview lands within one frame in the final export, tested by rendering both and comparing frame positions. Mixing a sixty and a twenty-five frame source produces a timeline with no drift over a five minute sequence, tested. The proxy render and the final render are produced by the same graph builder with different parameters, asserted by review and by a test that diffs the two graphs. Deleting the proxy cache and rebuilding gives an identical proxy. Do not build yet: No narration, captions, disclosure or final export yet. ### Phase 3 · Narration, captions and what they are Build: Accept recorded narration or generated speech. Generated speech from a stock voice records the engine and the voice identifier; anything cloned from a person requires a consent row and is refused otherwise. Captions carry an origin of transcribed, written or generated. Trap: Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. Done when: A voice asset flagged as cloned from a person cannot be generated or imported without a consent row, tested. Every caption row has a non-blank origin and the editor shows it, asserted in the schema and in a test. Generated captions cannot be burned in until marked reviewed, tested. The engine and voice identifier are recorded for every piece of generated speech and appear in the project export. Do not build yet: No final export settings, provenance attachment or render queue yet. ### Phase 4 · An export that actually starts playing Build: Export to a widely supported codec pair in an MP4 container with the index moved to the front. Keep dimensions even, record the exact command, and write a sidecar manifest listing every asset, its licence, its consent record and every generated element in the file. Trap: The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. Done when: The exported file begins playing in a browser before it has fully downloaded, tested by serving it with a throttled connection. Odd dimensions from a crop are corrected and the correction is logged rather than causing an encoder failure, tested with a deliberately odd crop. The exact FFmpeg command and filter graph are written beside the output. The sidecar manifest lists every asset with licence and consent references and every generated element, and is written even when the render fails partway. Do not build yet: No provenance signing, render queue, disk limits or project export yet. ### Phase 5 · Provenance, disk and the render queue Build: Attach content provenance to the export where you can sign it, and keep the sidecar manifest regardless. Run one render at a time with a disk ceiling, clean up proxies and failed outputs, and export the whole project as a folder of assets plus the JSON that references them. Trap: Treating an attached provenance manifest as proof the viewer will see is wrong twice over, because any tool downstream can strip it during a re-encode and because the specification itself notes that a manifest without a time stamp stops being valid once its signing credential expires or is revoked; attach it if you can, keep your own sidecar manifest in the project whatever happens to the file, and describe it in the README as a record you hold rather than a guarantee you offer. Done when: A project exports as a self-contained folder and reimports on another machine with every asset hash matching, tested. A render that would take the working directory past the disk ceiling is refused before it starts, with the shortfall named, tested. Failed renders and orphaned proxies are cleaned by a routine that never touches an asset referenced by a project, tested with a deliberately orphaned file. Provenance attachment failures are recorded as a warning and never silently skipped. Do not build yet: Nothing after this. ### Out of scope (and why) - Generating the face or the voice of a real person without a consent record, which is refused in code rather than discouraged in documentation. - A stock media library, because licensing somebody else's footage is a contract and a catalogue rather than a feature you can write. - Publishing directly to video platforms, which needs the same credential and quota machinery as a social scheduler and belongs in that project. - Collaborative editing, which means a server, locking and a merge model for a data type that does not merge well. ### README must contain - That likeness and voice assets require a consent record, and that you should check the law where you are rather than relying on the one this build was designed around - That every asset carries a source and a licence as required fields, and what happens when one is missing - That every render writes its filter graph and asset hashes beside the output, so any export can be reproduced - That the export moves the file index to the front, why that matters for playback, and how to verify it happened - That attached provenance can be stripped downstream, and that the sidecar manifest is the copy you keep
# Architecture · InVideo ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with FastAPI and a browser interface, everything on your own machine | video is large, and shipping it somewhere to render is what the subscription is really charging for | | Renderer | FFmpeg invoked with an explicit filter graph written to a file per render | a render you cannot reproduce from a text file is a render you cannot debug | | Project format | One JSON project file referencing assets by path and content hash | the project has to outlive the application that made it | | Provenance | A required per-asset record of origin, licence, consent and whether it was generated | this is the part free tools skip and the part that decides whether you can publish | | Preview | A low-resolution proxy rendered from the same filter graph as the final | previewing by seeking in source files shows you something the export will not match | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Assets | Import, hashing, probe, licence and consent gates. | Any importer, provided source and licence stay required. | | Timeline | Clips, tracks, transforms, frame rate normalisation. | Any editor model, provided normalisation happens at import. | | Graph | One filter graph builder for proxy and final. | Not swappable for two builders, which is how previews start lying. | | Export | Encoding presets, index placement, the manifest. | Any preset set, provided the manifest is always written. | | Queue | One render at a time, disk ceiling, cleanup. | Any scheduler, provided cleanup never touches referenced assets. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PROJECT_DIR` · required · Project JSON and per-project asset folders. This is what you back up. - `WORK_DIR` · required · Proxies, intermediates and render logs. Disposable by design. - `DISK_CEILING_GB` · required · A render that would exceed this is refused before it starts. - `FFMPEG_BIN` · required · Pinned, so you know which build produced a given output. - `TTS_API_KEY` · optional, secret · Generated speech only. Without it, narration is recorded rather than generated. - `ALLOW_LIKENESS_ASSETS` · required · Even when true, a consent row is still required. This only gates the import path. - `RENDER_CONCURRENCY` · required · One at a time. Two renders on one machine is two slow renders. ## 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 · InVideo product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python with FastAPI and a browser interface, everything on your own machine, FFmpeg invoked with an explicit filter graph written to a file per render, One JSON project file referencing assets by path and content hash, A required per-asset record of origin, licence, consent and whether it was generated, A low-resolution proxy rendered from the same filter graph as the final. - 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 - Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. - Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. - Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. - The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. - Treating an attached provenance manifest as proof the viewer will see is wrong twice over, because any tool downstream can strip it during a re-encode and because the specification itself notes that a manifest without a time stamp stops being valid once its signing credential expires or is revoked; attach it if you can, keep your own sidecar manifest in the project whatever happens to the file, and describe it in the README as a record you hold rather than a guarantee you offer.
# Delivery milestones · InVideo Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Assets that know where they came from Nothing enters a project without a hash, a source, a licence and, where a person appears, a consent record. ### Steps 1. Hash on import and verify before every render The hash is the only thing that notices a file changed under you. Files: `app/assets.py`, `tests/test_hash_guard.py` 2. Make source and licence required fields A nullable licence column becomes a null licence column. Files: `app/models.py`, `migrations/0001_assets.sql` 3. Gate likeness and voice assets on a consent row A check in code, not a sentence in the README. Files: `app/consent.py`, `tests/test_consent_gate.py` ### Done when - [ ] Blank source or licence is rejected on import - [ ] A likeness asset without consent cannot be added - [ ] A changed asset fails the render, not the check - [ ] Probe data includes frame rate for variable rate footage ### Watch out - Referencing assets by path alone means the day someone re-exports a clip or moves a folder your project renders different footage without telling you, and you find out from the output rather than from an error; hash on import, store the hash, verify it before every render, and report a mismatch as a failed render rather than rendering whatever is at that path now. ## M2 · A timeline and a preview that matches the export What you approve in the preview is what comes out of the final render. ### Steps 1. Normalise frame rate at import, not at render Drift you fix at the end is drift you have already approved. Files: `app/normalise.py` 2. Build one filter graph function, two presets The preview must not be a second implementation. Files: `app/graph.py`, `tests/test_graph_parity.py` 3. Compare preview and final frame positions in a test This is the assertion that keeps them honest. Files: `tests/test_cut_positions.py` ### Done when - [ ] A preview cut lands within one frame in the export - [ ] Mixed frame rates produce no drift over five minutes - [ ] Preview and final come from one graph builder - [ ] Proxies rebuild identically ### Watch out - Previewing by seeking in the original files gives you positions that disagree with the render when the sources have different frame rates, and phone footage with a variable frame rate disagrees with itself, so the cut you carefully placed lands a few frames elsewhere in the export; build the proxy from the same graph as the final at the project frame rate, and normalise every source to that rate on import rather than at render time. ## M3 · Narration, captions and what they are Every spoken word and every caption carries a typed origin, and a cloned voice is impossible without consent. ### Steps 1. Refuse cloned voices without consent, in code The check belongs at the point of creation, not in a warning. Files: `app/voice.py`, `tests/test_voice_consent.py` 2. Type the origin of every caption track Transcribed, written or generated. No default. Files: `app/captions.py` 3. Require a review pass before burning captions in Burned-in text is the hardest thing in the file to correct. Files: `app/captions.py`, `tests/test_caption_review.py` ### Done when - [ ] A cloned voice without consent is refused - [ ] Every caption has a non-blank origin - [ ] Generated captions need review before burn-in - [ ] Speech engine and voice identifier are recorded ### Watch out - Presenting a model-generated caption track as a transcript is a small lie that becomes a large one when the video is about anything factual, because a transcription model will confidently produce words nobody said and they end up burned into the picture; type the origin of every caption track, show the origin in the editor, and require a pass over generated captions before they can be burned in. ## M4 · An export that actually starts playing The exported file plays immediately in a browser and on the platforms you named, with a manifest beside it. ### Steps 1. Move the index to the front and verify it moved The flag is documented as a second pass that can be skipped in some cases. Files: `app/export.py`, `tests/test_faststart.py` ```sh ffmpeg -i input.mp4 -movflags +faststart -c copy output.mp4 ``` 2. Correct odd dimensions and log the correction Better than an encoder error twenty minutes into a render. Files: `app/export.py` 3. Write the manifest with the output, always It is the only record of what is in the file. Files: `app/manifest.py` ### Done when - [ ] The file starts playing before it finishes downloading - [ ] Odd dimensions are corrected and logged - [ ] The command and filter graph are saved beside the output - [ ] The manifest lists assets, licences and generated elements ### Watch out - The MP4 muxer writes its index at the end of the file by default, and the documentation for the faststart flag says plainly that it is a second pass that moves the index to the beginning and is not enabled by default, so without it a viewer must download the whole file before anything appears and your upload looks broken rather than slow; add the flag, confirm it ran, and note that it does not work with fragmented output so check the output rather than trusting the flag. ## M5 · Provenance, disk and the render queue (production only) The project survives the application, renders cannot fill the disk, and provenance is kept as a record rather than promised as a guarantee. ### Steps 1. Export the project as assets plus JSON, and reimport it The test is another machine, not another folder. Files: `app/project_io.py`, `tests/test_roundtrip.py` 2. Refuse a render that would fill the disk Check before, not after twenty gigabytes of temporary files. Files: `app/queue.py` 3. Keep the sidecar manifest whatever happens to the file It is the copy nobody downstream can strip. Files: `app/manifest.py`, `README.md` ### Done when - [ ] A project round-trips to another machine with matching hashes - [ ] A render over the disk ceiling is refused up front - [ ] Cleanup never removes a referenced asset - [ ] Provenance failures are warnings, never silent ### Watch out - Treating an attached provenance manifest as proof the viewer will see is wrong twice over, because any tool downstream can strip it during a re-encode and because the specification itself notes that a manifest without a time stamp stops being valid once its signing credential expires or is revoked; attach it if you can, keep your own sidecar manifest in the project whatever happens to the file, and describe it in the README as a record you hold rather than a guarantee you offer.
# Operations · InVideo ## Backup The project directory, which is the JSON plus the assets it references. The working directory is explicitly not backed up, because everything in it can be rebuilt from the project and it is the largest thing on the disk. ## Restore Copy the project directory to the new machine, reimport, and let the hash check confirm every asset arrived intact. A restore where hashes mismatch tells you which files are wrong before you render, which is the whole reason they are stored. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Working directory size against the ceiling, failed render exit codes, and assets whose hash no longer matches. A growing count of hash mismatches usually means something else on the machine is rewriting your media, which is worth knowing early. ## Incident checklist The incident is publishing something you should not have: an unlicensed clip, an unconsented voice, an undisclosed generated element. The manifest beside each render is what lets you answer what was in a given file on a given day, which is why it is written even when the render fails. 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 - [ ] An import with a blank source or licence is confirmed rejected - [ ] A likeness or voice asset without consent is confirmed refused - [ ] A preview cut is confirmed to land within one frame in the export - [ ] The exported file is confirmed to start playing before it downloads - [ ] A project is confirmed to round-trip to another machine by hash ## Launch constraint Do not market omitted InVideo 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. Project JSON and per-project asset folders. This is what you back up. PROJECT_DIR=./data/projects # Required. Proxies, intermediates and render logs. Disposable by design. WORK_DIR=./data/work # Required. A render that would exceed this is refused before it starts. DISK_CEILING_GB=40 # Required. Pinned, so you know which build produced a given output. FFMPEG_BIN=/usr/bin/ffmpeg # Optional · secret. Generated speech only. Without it, narration is recorded rather than generated. TTS_API_KEY=sk-...redacted # Required. Even when true, a consent row is still required. This only gates the import path. ALLOW_LIKENESS_ASSETS=false # Required. One at a time. Two renders on one machine is two slow renders. RENDER_CONCURRENCY=1
$ choose a build depth, inspect the files, then open the complete pack in your agent
InVideo: People pay to avoid codec failures, upload limits, rendering surprises, lost recordings, and distribution chores.
xproduction codecs, rendering speed, and media templates
xhosted recording reliability
xlarge-scale rendering
xdistribution and podcast hosting
xprofessional codecs, effects, and collaboration
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
InVideo pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | $0/workspace | Limited credits reset weekly; exact numeric credit allowance is not publicly disclosed on the current official help page |
| plus | — | $16.67 | 75 credits/month |
| max | — | $83.33 | 390 credits/month |
| generative | — | $166.67 | 800 credits/month |
| elite | — | $900 | 4,250 credits/month |
| team & enterprise | custom | — | Custom seats, credits, security and support |
free tierpermanent free account with a weekly-resetting but numerically undisclosed credit allowance; watermarked exports
billingpublic paid prices shown as annual commitments; unused monthly credits do not roll over
hidden costsOn-demand credit top-ups are sold; unused plan credits expire, purchased extras are time-limited, and model/agent credit costs may change without notice.
verified 2026-08-14 · source ↗
Vibecode InVideo
Kinda. The core of InVideo is buildable in a weekend with the prompt on this page, but there are real gaps: production codecs, rendering speed, and media templates, hosted recording reliability. Read the honest list above before committing.
How much does InVideo cost?
InVideo's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing InVideo?
Honestly: production codecs, rendering speed, and media templates; hosted recording reliability; large-scale rendering; distribution and podcast hosting; professional codecs, effects, and collaboration. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to InVideo?
Yes: MoneyPrinterTurbo (Give it a topic and it assembles stock footage, voice, music and captions; you supply the model keys and patience.) The prompt is for when you want it exactly your way.