Build InVideo

KINDAback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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-daythe files for this build are in the project pack

RuntimePython with FastAPI and a browser interface, everything on your own machineRendererFFmpeg invoked with an explicit filter graph written to a file per renderProject formatOne JSON project file referencing assets by path and content hashProvenanceA required per-asset record of origin, licence, consent and whether it was generatedPreviewA low-resolution proxy rendered from the same filter graph as the final

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the service that drives FFmpeg and holds the project.

    Get it python.org or your package manager. open ↗

    Verify python3 --version prints 3.12 or higher

  2. installfree

    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. open ↗

    Verify ffmpeg -encoders lists the video and audio encoders your presets name

  3. have readyfree

    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

  4. have readyfree

    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

  5. decidefree

    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

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- 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.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
PROJECT_DIRrequired./data/projectsProject JSON and per-project asset folders. This is what you back up.
WORK_DIRrequired./data/workProxies, intermediates and render logs. Disposable by design.
DISK_CEILING_GBrequired40A render that would exceed this is refused before it starts.
FFMPEG_BINrequired/usr/bin/ffmpegPinned, so you know which build produced a given output.
TTS_API_KEYsecretoptionalsk-...redactedGenerated speech only. Without it, narration is recorded rather than generated.
ALLOW_LIKENESS_ASSETSrequiredfalseEven when true, a consent row is still required. This only gates the import path.
RENDER_CONCURRENCYrequired1One at a time. Two renders on one machine is two slow renders.

The build, in order

  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.

    1. The hash is the only thing that notices a file changed under you.

      Files app/assets.pytests/test_hash_guard.py

    2. A nullable licence column becomes a null licence column.

      Files app/models.pymigrations/0001_assets.sql

    3. A check in code, not a sentence in the README.

      Files app/consent.pytests/test_consent_gate.py

    done when · tick each as it passes
    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.
  2. A timeline and a preview that matches the export

    What you approve in the preview is what comes out of the final render.

    1. Drift you fix at the end is drift you have already approved.

      Files app/normalise.py

    2. The preview must not be a second implementation.

      Files app/graph.pytests/test_graph_parity.py

    3. This is the assertion that keeps them honest.

      Files tests/test_cut_positions.py

    done when · tick each as it passes
    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.
  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.

    1. The check belongs at the point of creation, not in a warning.

      Files app/voice.pytests/test_voice_consent.py

    2. Transcribed, written or generated. No default.

      Files app/captions.py

    3. Burned-in text is the hardest thing in the file to correct.

      Files app/captions.pytests/test_caption_review.py

    done when · tick each as it passes
    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.
  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.

    1. The flag is documented as a second pass that can be skipped in some cases.

      Files app/export.pytests/test_faststart.py

      terminal
      ffmpeg -i input.mp4 -movflags +faststart -c copy output.mp4
    2. Better than an encoder error twenty minutes into a render.

      Files app/export.py

    3. It is the only record of what is in the file.

      Files app/manifest.py

    done when · tick each as it passes
    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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more audio & video.