Build Kapwing

KINDAreplaces $24/mosaves $288/yrback to the verdict

0%0 of 16 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 editor whose hard problems are describing an edit in rational time so cuts land on frame boundaries, keeping the preview and the export on one filter graph so faults appear early, and producing a container that starts playing before it has finished downloading.

estimated effort multi-daythe files for this build are in the project pack

RuntimePython 3.12 with a small local web interface, FFmpeg invoked as a subprocessEdit modelA non-destructive edit list in SQLite, with source files opened read-onlyTimesIntegers in the probed time base, never floating point secondsPreviewLow resolution proxies generated on import, rendered through the same filter graphExportOne graph, one encode, then a delivery pass for the container

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 interface, the queue and the FFmpeg wrapper.

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

    Verify python3 --version prints 3.12 or higher

  2. installfree

    Why It is the entire renderer, and an old build will be missing options this project assumes.

    Get it Your package manager or a static build from the project. open ↗

    Verify Both binaries run and report a version, and the mp4 muxer lists the faststart option

  3. have readyfree

    Why Rotation metadata and an irregular frame rate are the two inputs that break a naive editor, and neither appears in a clean test clip.

    Get it Record ten seconds on a phone held upright and ten seconds of your screen.

    Verify Both import, probe correctly and export without rotation or drift

  4. have readyfree

    Why An intermediate render plus proxies plus the output can be several times the input, and a disk that fills mid-render takes the project directory with it.

    Get it Point the temporary directory at a volume with room and set the ceiling below its free space.

    Verify A render is refused when free space is below the configured figure

Data model

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

- media(id, path, sha256, probe_json, duration_ms, video_codec, audio_codec, time_base, avg_fps, is_variable_fps, rotation, sample_rate, channels, imported_at)
- proxies(media_id, path, width, height, generated_at) · regenerable, and never an input to an export
- projects(id, name, width, height, fps_num, fps_den, sample_rate, created_at) · the frame rate is a fraction because many real ones are not integers
- clips(id, project_id, track, media_id, source_in, source_out, timeline_in, ordinal, transform_json) · times as integers in the project time base
- renders(id, project_id, preset_id, started_at, finished_at, state, command_text, stderr_tail, output_path, expected_duration_ms, actual_duration_ms)
- presets(id, name, container, video_args_json, audio_args_json, delivery_pass) · the arguments live in data so an old render can be reproduced exactly
- Nothing in this schema writes to a source file. The edit list plus the probe record is enough to reproduce any export, which is what makes a bad render debuggable a week later.

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
DATABASE_PATHrequired./data/editor.dbMedia records, projects, edit lists and render history.
MEDIA_DIRrequired./data/mediaWhere imported files are referenced from. They are never written to.
PROXY_DIRrequired./data/proxiesLow resolution previews. Safe to delete at any time.
TEMP_DIRrequired./data/tmpRender intermediates. Swept by age, and never on the same volume as your only copy of anything.
OUTPUT_DIRrequired./data/exportsFinished renders. A failed render leaves nothing here.
MIN_FREE_DISK_GBrequired20A render below this is refused with the actual figure.
PROXY_HEIGHTrequired360Preview resolution. Lower makes scrubbing quicker and changes nothing about exports.
FFMPEG_PATHrequired/usr/bin/ffmpegRecorded on every render row, because a version change explains a different output.

The build, in order

  1. Probe everything, trust nothing

    Import records what a file actually is, and refuses anything it cannot describe.

    1. The parse will be wrong about something and you will want the original.

      Files src/media/probe.pytests/test_probe.py

    2. The player was lying to you comfortably.

      Files src/render/graph.pytests/fixtures/portrait.mp4

    3. A media row with unknown fields poisons every render that touches it.

      Files src/media/import.py

    done when · tick each as it passes
    watch out
    • A clip from a phone carries its orientation as rotation metadata and plays upright in every player you check it in, so you build the entire timeline before discovering the export is on its side, because the encoder applied your filters to the stored orientation rather than the displayed one. Read the rotation at import, normalise it in the filter graph, and put a portrait phone clip in the fixture set on the first day rather than the last.
  2. An edit list, and sources opened read-only

    Trimming, splitting and reordering change rows only, and the source files are provably untouched.

    1. Nothing past the edge should ever see a decimal second.

      Files src/timeline/units.pytests/test_units.py

    2. Both, because read-only is a habit and the hash is the proof.

      Files src/media/open.pytests/test_untouched.py

    3. It is the cheapest test of whether the model is complete.

      Files src/project/serialise.py

    done when · tick each as it passes
    watch out
    • Storing cut points as floating point seconds survives every test you write and then splits a clip at twelve point three seconds one frame early on one render and one frame late on the next, because the container's time base is not a decimal fraction, and the two halves stop adding up to the original. Convert to the project time base on entry, keep integers everywhere, and assert that splitting a clip and rejoining the halves reproduces the original duration exactly.
  3. A preview that is the export, only smaller

    One function builds the filter graph and both the preview and the export use it.

    1. Two graphs is two products with one name.

      Files src/render/graph.pytests/test_graph_parity.py

    2. And assert the export command never mentions a proxy path.

      Files src/render/preview.pysrc/render/export.py

    3. They are cache, and treating them as cache keeps them honest.

      Files src/media/proxy.py

    done when · tick each as it passes
    watch out
    • Writing the preview with one set of filters and the export with another means every fault appears only at the end of a forty minute render, and the one you chase longest is the one the preview could never have shown you. Generate both graphs from one function and put a test that asserts the two differ only in the scale and the output arguments.
  4. A file that starts playing before it has finished downloading

    The delivery export carries its index at the front and its duration is checked, not assumed.

    1. So an old render's arguments can be read back exactly.

      Files src/presets.pydata/presets.json

    2. A silent duration drift is the fault you find on upload day.

      Files src/render/verify.pytests/test_duration.py

    3. A render you cannot reproduce is a render you cannot fix.

      Files src/render/run.py

    done when · tick each as it passes
    watch out
    • Leave the faststart option off and the container's index sits at the end, which the FFmpeg documentation describes explicitly: the option will 'run a second pass moving the index (moov atom) to the beginning of the file', and without it a player fetching the file over http has to reach the tail before it can begin, so your export plays perfectly from disk and stalls on the web. Checked at ffmpeg.org on 15 September 2026; note the same page says it 'will not work in various situations such as fragmented output' and that it needs a seekable output rather than a pipe.
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.