Roadmap
The v0 web prototype (Vite/React) has been removed from the tree β see git history (tag v0.13.0 and earlier) for the reference implementation for UX and the dual-editing sync model. The next phase moves rendering and media off the DOM entirely.
Decisions so far (the compromises)β
- No DOM rendering. GPU acceleration wherever possible. No WebKitGTK anywhere in the stack (this also rules out Tauri on Linux, which embeds it).
- Implementation language: Rust (rationale below).
- Scripting: TypeScript first, but the scripting surface is a stable document + API boundary, so other languages can drive the editor too.
- This is a GNOME app: GTK4 + libadwaita, GNOME HIG. The deliverable
is a Flatpak (starter manifest:
engine/build-aux/org.tunaos.dualcut.json).
Why Rust (ecosystem survey)β
| Need | Rust | Go | GJS |
|---|---|---|---|
| NLE engine (timeline/clips/effects/render) | GES β GStreamer Editing Services has maintained Rust bindings: Timeline, Layer, Clip, Group, transitions, rendering β a whole editing engine for free | none; cgoβFFmpeg only, no timeline layer | GES via GObject-Introspection, but JS-side perf ceiling |
| Media pipeline | gstreamer-rs is first-class (GStreamer itself ships official plugins written in Rust) | weak | good (GI) |
| GPU video display | gtk4paintablesink: GL textures + DMABuf zero-copy on GTK β₯ 4.14 | no story | possible, less control |
| GPU 2D (shapes/titles) | wgpu; Vello (alpha but advancing β Linebender status); skia-safe as the boring fallback | Gio/Fyne (small) | GSK only |
| Embedded TS runtime | deno_core / rustyscript (V8 + TS transpile in-process) | no | is JS, but can't embed user TS cleanly |
Go has no serious media-editing ecosystem. GJS gets GTK4+GES but leaves no good path for custom GPU compositing or embedding user TypeScript. Rust uniquely has all four pillars, and the GStreamer/GTK combo keeps us native GNOME-adjacent without WebKitGTK.
Stack: gtk4-rs + libadwaita (UI, GPU-rendered via GSK) Β·
GStreamer + GES (decode, timeline, audio, export) Β·
gtk4paintablesink DMABuf (preview) Β·
wgpu + Vello for the shapes/titles/motion-graphics compositor (rendered to
GL textures fed into the GES pipeline as a source; swap in skia-safe if
Vello's alpha gaps bite) Β·
deno_core for in-process TypeScript.
Document model v2β
Project
ββ meta width, height, fps, background
ββ defs reusable compositions (templates), keyed by name
β ββ Composition { params: {name, type, default}[], scene-or-layers }
ββ scenes[] sequential β the narrative spine; no gaps, order = time
β ββ Scene { duration, transition-in?, layers[] }
β ββ layers: text | video | audio | image | shape | comp-ref
ββ overlays[] tracks that span scene boundaries β solves the
ββ OverlayTrack { clips[] } subtitle/music overlap problem
- Scenes answer "what happens next": cut-to-cut structure like CapCut's main track. Elements inside a scene are timed relative to the scene.
- Overlays answer the concern raised about the scene model: subtitles, background music, watermarks, and lower-thirds don't respect scene cuts, so they live on composition-level overlay tracks with absolute timing β scenes for structure, overlays for anything that crosses cuts.
- Reusable compositions (
defs): a named, parameterised set of layers β a text template ("lower third:{name},{title}"), a motion template (logo sting), an intro card. Instantiated viacomp-refwith arguments; editing the def updates every instance. Maps 1:1 onto GES's nested-timeline/Groupsupport. - Shapes: circle, ellipse, rectangle (rounded), star, polygon, line, arrow, and arbitrary SVG paths β all GPU-drawn vectors, animatable (fill, stroke, path morph later).
- Keep v0's animation primitive (
from/totween windows per property with easing incl. spring); add transform-origin and per-scene transitions (cut, crossfade, slide, wipe).
Scripting modelβ
- In-app TS console + script panel:
deno_coreruns user TypeScript against a typededitor.*API (query/mutate the document, not pixels). - External agents, any language: same as v0 β the document is
serialized JSON on disk plus a local HTTP/Unix-socket API
(
GET/POST /composition, plus granular ops later:patch,addScene,renderFramefor screenshot feedback). TS is first-class; anything that can speak JSON-over-HTTP is supported. - Typed schema published as both TS declarations (
.d.ts) and JSON Schema so agents and humans get completion/validation in either world.
Milestonesβ
- M0 β Pipeline spike (de-risk). Rust bin: build a GES timeline with two
video clips + a title, preview via gtk4paintablesink in a bare GTK4
window, render to MP4. Proves decodeβtimelineβdisplayβexport before any
editor code. Also: Vello-to-GL-texture-into-GES proof, and a
deno_core "hello editor API" embed.
Status: complete (2026-07-18), all four pillars proven in
engine/: (1) GES timeline (test source + URI clip + transparent title) renders to H.264/AAC MP4 headless βcargo run --bin render, verified by frame extraction; (2) GTK4/libadwaita preview window with gtk4paintablesink builds and launches βcargo run --features preview --bin preview(smoke-tested under Xvfb; GPU frame display still to be eyeballed on a real desktop session); (3) in-process TypeScript via rustyscript/deno_core drives aneditor.addClip()API and renders its own MP4 βcargo run --features scripting --bin script; (4) Vello 0.9 renders the shape set (rounded rect, circle, star) headless on wgpu/Vulkan βcargo run --features vector --bin vello_spike. Note: GES objects are not Send β scripting ops collect document mutations, applied on the engine thread (this is the right M1 architecture anyway: scripts edit the document, not GES). Vello-textureβGES source integration moved to M3. - M1 β Engine + document. Document model v2 (serde), documentβGES
mapping layer, undo/redo as document diffs, autosave, the HTTP agent API
(port the v0 contract + AGENTS.md).
Status: core complete (2026-07-18) β
document.rs(scenes/overlays/defs with validation),mapping.rs(documentβGES: sequential scenes, absolute overlays, parameterised def instantiation, per-property animation control sources with easing),render <project.json>and theserveagent API (GET/POST /project, POST /render, /status) verified end-to-end: agent edit round-trip persisted + rendered. Undo/redo as document diffs moves to M2 with the UI. - M2 β Editor UI. GTK4/libadwaita shell: preview, scene strip + overlay tracks timeline (drag/trim/reorder), inspector sidebar β feature parity with the v0 web prototype, but native.
- M3 β Shapes & motion. Vector layer types, animation editor, scene transitions, transform handles in the preview. Status: shapes + transitions done (2026-07-18) β all seven shape kinds render via Vello to cached transparent PNGs entering GES as image clips (feature "vector"); scene crossfades via GES auto-transition. Remaining: in-app animation editor, transform handles, live vector morphs.
- M4 β Scripting. Embedded TS runtime + script panel, typed
editor.*API,.d.ts/JSON Schema publishing, agent recipes. Status: core done (2026-07-18) β POST /script runs TS (export function edit(p: Project): Project) in-process, validated;schema/dualcut.d.ts+dualcut.schema.jsonpublished. Remaining: in-app script panel, V8 offline source for the Flatpak bundle. - M5 β Templates.
defs/comp-refwith params, "save selection as template", a small built-in template library (lower third, title card, caption style). Status: library done (2026-07-18) β starter defs (lower-third, title-card, caption) embedded;render new <path> [title]scaffolds. Remaining: "save selection as template" UI op. - M6 β Export & polish. Render queue (GES render profiles: MP4/WebM, resolution/bitrate presets), audio gain/fades, waveforms + thumbnails on clips, snapping, multi-select. Status: profiles done (2026-07-18) β MP4 (H.264/AAC) and WebM (VP8/Vorbis) by name or extension, CLI + /render. Remaining: queue UI, bitrate presets, waveforms/thumbnails, snapping, multi-select.
- M7 β Flatpak distribution. The shipping artifact: manifest
(GNOME runtime, rust-stable SDK extension,
flatpak-cargo-generatoroffline sources, GES module), portals for media access, appstream metainfo + screenshots. No Flathub submission β Flathub's current AI policy rules it out (decided 2026-07-18); GitHub Releases is the distribution channel (install one-liner in the README). Status: automated releases live (2026-07-18) β everyv*tag buildsdualcut.flatpak(GNOME 50 runtime + GES module) and attaches it to a GitHub Release; verified end-to-end with the README install one-liner (v0.1.0). Remaining: appstream metainfo, desktop file + icon, screenshots, Flathub submission.
Open questionsβ
Scene audiodecided: a video clip's own audio is scene-local; music/VO lives on overlays. Plus a detach-audio op (like other editors): splits a video's audio into an independent audio clip (schema supports it today via videovolume: 0+ anaudioclip with the same src/offset; the one-click op arrives with the M2 UI).- Vello alpha risk β spike passed (0.9 renders the shape set headless on
llvmpipe); final Vello vs
skia-safecall once M3 measures real scenes on real GPUs. - Name: Chop / Clip / Compose / Frame / Collect / Pack β pending.
- Does v0 (web) stay maintained as a thin remote UI, or freeze as reference once M2 reaches parity?