Build Kapwing
KINDAreplaces $24/mosaves $288/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/editor.db | Media records, projects, edit lists and render history. |
MEDIA_DIR | required | ./data/media | Where imported files are referenced from. They are never written to. |
PROXY_DIR | required | ./data/proxies | Low resolution previews. Safe to delete at any time. |
TEMP_DIR | required | ./data/tmp | Render intermediates. Swept by age, and never on the same volume as your only copy of anything. |
OUTPUT_DIR | required | ./data/exports | Finished renders. A failed render leaves nothing here. |
MIN_FREE_DISK_GB | required | 20 | A render below this is refused with the actual figure. |
PROXY_HEIGHT | required | 360 | Preview resolution. Lower makes scrubbing quicker and changes nothing about exports. |
FFMPEG_PATH | required | /usr/bin/ffmpeg | Recorded on every render row, because a version change explains a different output. |
The build, in order
Probe everything, trust nothing
Import records what a file actually is, and refuses anything it cannot describe.
The parse will be wrong about something and you will want the original.
Files
src/media/probe.pytests/test_probe.pyThe player was lying to you comfortably.
Files
src/render/graph.pytests/fixtures/portrait.mp4A media row with unknown fields poisons every render that touches it.
Files
src/media/import.py
done when · tick each as it passeswatch 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.
An edit list, and sources opened read-only
Trimming, splitting and reordering change rows only, and the source files are provably untouched.
Nothing past the edge should ever see a decimal second.
Files
src/timeline/units.pytests/test_units.pyBoth, because read-only is a habit and the hash is the proof.
Files
src/media/open.pytests/test_untouched.pyIt is the cheapest test of whether the model is complete.
Files
src/project/serialise.py
done when · tick each as it passeswatch 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.
A preview that is the export, only smaller
One function builds the filter graph and both the preview and the export use it.
Two graphs is two products with one name.
Files
src/render/graph.pytests/test_graph_parity.pyAnd assert the export command never mentions a proxy path.
Files
src/render/preview.pysrc/render/export.pyThey are cache, and treating them as cache keeps them honest.
Files
src/media/proxy.py
done when · tick each as it passeswatch 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.
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.
So an old render's arguments can be read back exactly.
Files
src/presets.pydata/presets.jsonA silent duration drift is the fault you find on upload day.
Files
src/render/verify.pytests/test_duration.pyA render you cannot reproduce is a render you cannot fix.
Files
src/render/run.py
done when · tick each as it passeswatch 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.
Disk, cancellation, and jobs that outlive the tabproduct builder
A long render can be cancelled cleanly, cannot fill the disk, and the project can move machines.
The browser tab is not where the job should live.
Files
src/queue.pytests/test_queue.pyA full disk mid-render corrupts the output and the project directory.
Files
src/render/disk.pySo reopening it elsewhere fails loudly on a missing file.
Files
src/project/export.pytests/test_portable.py
done when · tick each as it passeswatch out- Assembling an export by rendering each clip separately and joining them with the concat demuxer looks efficient and drifts the audio of everything after the first slightly wrong duration, because the documentation requires that 'all files must have the same streams (same codecs, same time base, etc.)' and adjusts each file's timestamps using its stored duration, which it also says 'can cause artifacts' when that duration is incorrect. Checked at ffmpeg.org on 15 September 2026. Render the timeline as one graph and keep concat for parts you produced yourself with identical settings.
That is the whole plan for Kapwing. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Real-time collaborative editing.
- Automatic subtitles.
- Professional colour management and broadcast codecs.
- Hosting and distribution.
- distribution and podcast hosting
- professional codecs, effects, and collaboration
- high-fidelity color, format, and export handling
- hosted recording reliability
- large-scale rendering
- A waveform view generated from the proxy audio, which makes cutting on speech possible
- Render presets per destination stored as data, so a new target is a row rather than a branch
- A watch folder that probes and imports automatically, refusing anything it cannot describe
Need the files? The project pack on the verdict page hands your agent the whole brief · more audio & video.