← Writing

How Vit versions a video timeline

Most merge conflicts in Vit are never resolved. They're designed out of existence by the file layout, and an LLM only sees what's left.

March 2026 · 7 min

Read the teardown

A Git-backed version control system where the thing being versioned is a DaVinci Resolve timeline. Companion to the product teardown, which covers why it was built this way. This one covers how.


The constraint that shapes everything

Git is good at merging text and useless at merging video. A DaVinci Resolve project file is binary, and the media it references runs to hundreds of gigabytes. Version controlling either one directly is a non-starter.

So Vit versions neither. It versions the edit decisions: which clip sits where, what grade is on it, how loud it is, where the markers are. Those are small, structured, and text-representable. Serialized to JSON, a full timeline snapshot is a few kilobytes, and Git handles it the way it handles source code.

Raw footage never enters the repository. Teams keep sharing it however they already do, on a drive or in Dropbox, and manifest.json records paths and checksums so Resolve can relink. Vit owns the coordination layer and stays out of the storage layer.


Domain-split JSON

This is the design decision the rest of the system rests on.

A naive implementation writes one timeline.json. Every collaborator then edits the same file, and every parallel edit is a merge conflict. You'd spend the whole project resolving conflicts that a smarter file layout would never have produced.

Vit splits the timeline into six files along the lines the team already divides along:

FileContentsTypical owner
cuts.jsonClip placements, in/out points, transforms, speedEditor
color.jsonColor grading per clipColorist
audio.jsonLevels, panningSound designer
effects.jsonEffects, transitionsEditor / VFX
markers.jsonMarkers, notesAnyone
metadata.jsonFrame rate, resolution, track countsRarely changed

The result is that most merges have no conflict to resolve. The colorist touches color.json, the sound designer touches audio.json, the editor touches cuts.json. Git sees three disjoint changesets and merges them without being asked to make a judgment call.

The file layout is doing the work that a conflict resolution algorithm would otherwise have to do. Structuring the data so the problem doesn't arise beats solving the problem well.

Clips are linked across files by a shared id field, which is what makes cross-domain reasoning possible when it's needed.


Architecture

Resolve Panel (primary UI)  ─┐
                             ├─→ vit-core (Python) ─→ git (system binary) ─→ GitHub
vit CLI (setup, power use)  ─┘

Vit shells out to the real git binary rather than reimplementing version control. Branches are Git branches, commits are Git commits, the remote is an ordinary GitHub repository. That means the history is inspectable with any Git tool, and nothing is locked inside a proprietary format.

vit-core modules:

ModuleResponsibility
serializer.pyResolve timeline to JSON
deserializer.pyJSON back to Resolve timeline
json_writer.pyDomain-split file I/O, deterministic formatting
core.pyGit operations wrapper
validator.pyPost-merge rule-based validation
ai_merge.pySemantic conflict resolution
differ.pyHuman-readable timeline diffs

vit-core (Python)

User interfaces

optional, degrades gracefully

relinked manually in Resolve

Vit Panel in DaVinci Resolve
Tkinter default, Qt optional
primary interface

vit CLI
setup, scripting, power users

serializer.py
timeline to JSON

deserializer.py
JSON to timeline

json_writer.py
domain-split file I/O

core.py
git operations wrapper

differ.py
human-readable diffs

validator.py
post-merge rule checks

ai_merge.py
semantic conflict resolution

DaVinci Resolve
scripting API

git
system binary

GitHub remote
JSON only, no media

Gemini 2.5 Flash

Shared drive or Dropbox
raw footage, never touched by Vit

JSON is always written with indent=2, sort_keys=True. Deterministic serialization matters more here than it looks: without it, reopening a project and re-serializing produces spurious diffs, and the history fills with commits that changed nothing.

gitjson_writer.pyResolve APIserializer.pyVit Panelgitjson_writer.pyResolve APIserializer.pyVit Panelcuts.json, color.json, audio.json,effects.json, markers.json, metadata.jsonindent=2, sort_keys=TrueEditorclicks Save Versionserialize current timelineread clips, grades, levels, markerstimeline statewrite domain-split JSONgit add timeline/ assets/git commit -m "message"commit hashversion saved, shown in HistoryEditor

one time, in Terminal

everything after this
is in the Resolve panel

click Pull

click Switch Branch

click Save Version

click Push

keep working

lead merges branches

vit init or vit clone

Vit Panel open in Resolve

fetch teammates' latest

timeline restored to your branch

edit in Resolve as normal

timeline serialized and committed

work shared with the team

Combine Workstreams

The only step that
touches a terminal


Where Git stops being enough

Domain-split JSON eliminates the common conflicts. It does not eliminate a subtler class of problem, where Git merges cleanly and produces an invalid timeline.

ProblemHow it happensWhy Git misses it
Orphaned referenceEditor deletes item_003 in cuts.json, colorist graded item_003 in color.jsonBoth files merge fine, the grade now points at nothing
Audio/video desyncEditor trims a clip, sound designer mixed for the old lengthDifferent files, no textual conflict
Overlapping clipsTwo editors add clips to the same track at the same timecodeGit takes both, the timeline is invalid
Track count mismatchOne branch adds a V3 track, the other doesn'tmetadata.json conflicts, cuts.json is structurally wrong
Speed/audio mismatchSpeed changed on one branch, audio adjusted for the old speed on anotherValues diverge silently
Stale durationSpeed changed while a parallel branch edited the same clip's durationrecord_end_frame no longer matches the speed-adjusted source

Every one of these is a case where the merge succeeds and the editor opens a broken timeline.

validator.py catches them with rule-based checks after every merge. Detection is deterministic, not model-driven, which matters because this is the layer that decides whether anything is wrong at all.

But the timeline is now invalid

Orphaned reference
clip deleted in cuts.json,
still graded in color.json

Audio/video sync
clip trimmed, audio still
cut for the old length

Overlapping clips
two editors, same track,
same timecode

Track count mismatch
one branch added V3

Speed/audio mismatch
speed changed, audio
adjusted for old speed

Stale duration
record_end_frame no longer
matches speed-adjusted source

Git merges these cleanly

cuts.json changed

color.json changed

Different files, no overlap

validator.py
rule-based detection

Issues feed the LLM prompt
as explicit context


AI-assisted semantic merge

Only when validation finds an issue, or Git itself conflicts, does an LLM get involved.

The flow:

  1. Attempt the Git merge
  2. Run validator.py on the result
  3. If clean, done. The AI is never called in the common case.
  4. If not, extract BASE, OURS and THEIRS for the affected files
  5. Send all domain JSON, the schema context, and the specific detected issues to Gemini 2.5 Flash
  6. Receive structured per-domain decisions with confidence levels
  7. On low confidence, present the options to the user instead of choosing
  8. Show the user exactly what changed
  9. Write and commit only on approval

Yes
the common case:
different domains touched

No

No

Yes

Yes

No

Yes

No

vit merge branch

Attempt git merge

Git merge
clean?

Post-merge validation
validator.py

Extract BASE / OURS / THEIRS
for conflicting files

Issues
found?

Merge complete
AI never invoked

Build LLM prompt:
all domain JSON, both versions
+ schema context
+ detected issues
+ resolution rules

Gemini 2.5 Flash

Structured per-domain decisions
with confidence levels

Low
confidence?

Present options to user
interactive clarification

Show user exactly
what the AI changed

User
approves?

Write resolved files, commit

Fall back to
manual resolution

The resolution rules live in the prompt:

  • A clip deleted in one branch has its references removed from all domain files
  • Audio clip boundaries must match their corresponding video clip boundaries
  • No two clips may overlap on the same track at the same timecode
  • Preserve as much work from both branches as possible
  • When ambiguous, prefer the more recent commit

Two deliberate limits on how much the model is trusted. It never writes without user approval. And in the Resolve panel, AI is enrichment only: branch comparison and commit classification fall back to heuristics when no API key is present, and full AI conflict resolution runs only in the CLI. The panel works completely without an API key.

The honest tradeoff: these are prompt instructions, not a trained model, so the same conflict can resolve differently on two runs and there's no way to unit test the behavior the way a merge algorithm can be tested. Acceptable for the cases that reach it, which are rare, user-reviewed, and reversible. Not acceptable for anything running unattended.


Resolve API constraints

The Resolve scripting API sets hard limits on what can be captured and restored, and the serializer is shaped around them:

  • Extended properties (rotation, crop, flip) are static only. No keyframe data is reachable.
  • Retiming is constant-speed only. Speed ramps can't be read.
  • Color is write-only. There is no GetCDL or GetLUT, so grades can be applied but not fully read back.
  • No deletion API for timelines or clips.
  • Timeline restore triggers clip duplication if SetName() is called on the existing timeline, so restore uses a three-phase create, populate, rename flow.

The last one is the kind of constraint that only shows up in practice. The obvious implementation silently duplicates the user's clips.


Testing

The serializer is tested against a mock Resolve object, which is what makes any of this testable without a running copy of Resolve. Coverage spans the Git wrapper, merge behavior, validation rules, AI merge, diff formatting, and full serialize/deserialize roundtrips.

python -m pytest tests/

Scope boundaries

Deliberately out of scope: web UI, hosted platform, database, media storage or sync, file locking, real-time collaboration, authentication, LUT versioning, and other editing applications.

Every one of those is a real product. None of them is this one.