Project format¶
A ZeyoCut project is a folder. project.json in it is the single source of truth: every tool call validates the change, saves it atomically and adds an undo step. Before each save, the file it replaces is copied to backups\, and the last 20 are kept; restore_backup lists them and puts one back.
This page describes the folder, the file, and how ZeyoCut reads what is in it: space and time, transforms, the camera, animation, text, video, captions, languages, the library and audio. The guide shows the tools that change it.
The folder¶
my-project\
project.json the model
assets\ imported images, audio, video and fonts (layers refer to them by asset id),
plus analysis results next to each analyzed file:
<name>.analysis.json, <name>.transcript.json, <name>.srt,
and <name>.beats.json from detect_beats
renders\ PNGs from render_frame / render_contact_sheet / view_source
exports\ MP4s, SRTs, chapter lists, .otio and .fcpxml timelines, and in
<project>_prerender\ the ProRes files of prerender_layers (prerender.json lists them)
tracks\ the boxes of each track (track_object), one file per version
backups\ project.json before each of the last 20 saves (project-<time> before <edit>.json),
and as an older ZeyoCut saved it (project-v<format>-<time>.json)
cache\chunks\ the 2 s chunks of earlier exports, reused by the next export (at most 2 GB)
cache\preview\ the sound mix the preview page plays (mix-<key>.m4a, the latest one)
proxies\ 1920 px copies of footage over 2560 px, for previews and tracking (exports read the originals)
project.json¶
project.json is indented UTF-8 JSON. Names are camelCase, and so are enum values (easeOutBack, voice). It's meant to be readable and to sit well in version control. Change it through the tools, which check every edit; if you edit it by hand, open_project lists the problems it finds.
Here is the demo project (ZeyoCut.Mcp.exe demo) cut down to one asset and one layer:
{
"formatVersion": 6,
"name": "Tora Jumps",
"width": 1280,
"height": 720,
"fps": 30,
"duration": 3,
"backgroundColor": "#BFE6FF",
"audio": {"loudnessTarget": -14, "truePeak": -1, "ducking": true},
"assets": [
{
"id": "clouds",
"type": "image",
"file": "assets/clouds.png",
"width": 1400,
"height": 240
}
],
"layers": [
{
"type": "image",
"id": "clouds",
"name": "clouds",
"start": 0,
"end": null,
"zIndex": 1,
"parentId": null,
"visible": true,
"anchorX": 0,
"anchorY": 0,
"flipX": false,
"drawings": {"normal": "clouds"},
"drawing": {"value": "normal"},
"x": {
"value": 0,
"keyframes": [
{"time": 0, "value": 0, "easing": "linear"},
{"time": 3, "value": -80, "easing": "linear"}
]
},
"y": {"value": 0},
"rotation": {"value": 0},
"scaleX": {"value": 1},
"scaleY": {"value": 1},
"opacity": {"value": 1},
"brightness": {"value": 0},
"contrast": {"value": 1},
"saturation": {"value": 1}
}
]
}
Top level¶
| Field | Default | What it is |
|---|---|---|
formatVersion |
6 | The format the file was saved in; see Format versions |
name |
The project's name | |
width, height |
1920, 1080 | The canvas in pixels |
fps |
30 | Frames per second |
duration |
10 | Length in seconds |
backgroundColor |
#FFFFFF |
Shown where no layer draws |
audio |
loudnessTarget: the loudness of the exported mix in LUFS, -14 by default, null for off. truePeak: the highest true peak, -1 dBTP. ducking: lower music under voice, on by default |
|
camera |
Written once set_camera, a camera keyframe or camera_shake makes one: animated x, y, zoom and rotation, and the shake channels shakeX, shakeY and shakeRotation |
|
assets |
[] |
The files the project uses |
layers |
[] |
Everything on the timeline, as one flat list; parentId makes the hierarchy |
tracks |
Boxes followed through footage; written only when there are some | |
markers |
Chapters and scenes in time order; written only when there are some |
Assets¶
Each asset has an id, which layers refer to it by, a type (image, audio, font or video) and a file. The file is always assets/<file name>; one that points anywhere else is an error. The rest is what was found when the file was imported: width and height, duration, frameRate, frameCount, hasAudio, codec and, for fonts, fontFamily. A video that was transcoded on import keeps the file the user imported in originalFile and the reason in normalized.
Layers¶
Every layer has these fields:
| Field | What it is |
|---|---|
type |
image, text, video, audio, cutout or redaction |
id |
Unique in the project; tools refer to layers by it |
name |
A display name |
rigRole |
The part's name in a rig (head, arm_l); actions match parts by it |
start, end |
Where the layer plays, in seconds; end null plays to the end of the project |
zIndex |
Draw order across all layers; higher draws in front |
parentId |
The layer this one moves with |
visible |
Hidden layers aren't drawn or heard |
language |
Puts the layer and its children in one language's version only; see Languages |
Image, text and video layers add the anchor (anchorX, anchorY), ignoreCamera, and the animated x, y, rotation, scaleX, scaleY and opacity. They can have a motionPath, a mask, and the effects tint, outline, shadow and blur. Each type then has fields of its own:
| Type | Its own fields |
|---|---|
image |
drawings (drawing name → image asset id), drawing (which drawing shows, animated), flipX, the animated color adjustments brightness, contrast and saturation, and matte (background removal). An image layer without drawings is a null layer |
text |
text, fontFamily or fontAsset, fontSize, bold, color, strokeColor, strokeWidth, align, maxWidth, lineHeight, and box (color, padding, cornerRadius) |
video |
asset, trimIn, trimOut, speed, volume, muted, role, fadeIn, fadeOut, a crop (cropWidth, cropHeight and the animated cropX, cropY), the color adjustments, and matte |
audio |
asset, trimIn, trimOut, volume, role (voice, music or effects), fadeIn and fadeOut |
cutout |
Made by text_behind_subject: the subject of its sources (a clip and the pieces splits made of it) drawn again above the text, cut out with its matte. It follows its sources, so only name, zIndex, visible and opacity are its own. captionGroup names the captions generate_captions made it for |
redaction |
Made by apply_redaction: hides the boxes of its tracks with a style (pixelate, blur or solid), strength, paddingPercent, feather, holdFrames and color. After tracks_to_keyframes, boxes holds a track's box as keyframes |
Animated values¶
A property that can be animated is an object with a value and a list of keyframes:
"x": {
"value": 0,
"keyframes": [
{ "time": 0, "value": 0, "easing": "linear" },
{ "time": 3, "value": -80, "easing": "linear" }
]
}
value is used while there are no keyframes. A keyframe's easing shapes the segment that starts at it; see Animation. An image layer's drawing works the same way with drawing names, and always holds: "drawing": { "value": "normal", "keyframes": [{ "time": 1.2, "value": "happy" }] }.
Tracks¶
A track is a box followed through a video asset by track_object, or found on every frame by detect_and_track. It is kept in the asset's own frames and pixels, so trimming, speeding up or splitting the clips that play it never breaks it.
| Field | What it is |
|---|---|
id, asset |
The track and the video asset it follows |
layer |
The clip the box was drawn on |
method |
csrt, template or opticalFlow for a followed box; faces or plates for detections |
direction, origin, firstFrame, lastFrame |
Which way tracking ran from the frame it started at, and the frames it covers |
anchors |
The boxes set by hand (frame, x, y, width, height), in frame order |
file |
tracks/<id>-<hash>.json: the box on every frame, with how sure the tracker was. Every change writes a new file, so undo brings back the old one |
Markers¶
A marker has an id, a time, a name and a kind, chapter or scene. export_markers writes the chapters as a YouTube chapter list.
Format versions¶
| Version | What it added |
|---|---|
| 1 | The first format |
| 2 | Video clips, sound roles and loudness settings |
| 3 | The camera, effects, masks and motion paths |
| 4 | Subject cutouts |
| 5 | Tracks and redactions |
| 6 | Markers and language tags |
A project saved by an older ZeyoCut opens too: it's updated to the current format as it opens, and the file as it was is kept in backups\ for good. So far every version has only added fields, and a field an older file lacks gets its default.
Space and time¶
- Pixels, origin at the top-left, y points down, rotation in degrees clockwise.
- Times are in seconds. Frame n is rendered at t = n / fps. The frame count is
round(duration × fps).
Transforms¶
- Each visual layer has an anchor (
anchorX,anchorY) in its own pixels.x,yis where that anchor lands in the parent's pixel space, or on the canvas for top-level layers. local = translate(x,y) · rotate(rotation) · scale(scaleX,scaleY) · translate(−anchor), andworld = parentWorld · local.- Children inherit the transform, and opacity and visibility multiply down the chain. A parent's
start/endalso limits its children. - Parent cycles are rejected.
Camera¶
- One camera per project moves the whole scene:
x,yis the scene point at the frame's center,zoom2 is twice as close, androtationturns the camera clockwise. screen = translate(canvasCenter + shake) · rotate(−rotation) · scale(zoom) · translate(−x,−y) · world, applied to every top-level layer except those withignoreCamera(captions get it).- Animate it with
set_keyframesonlayerId"camera".camera_shakeadds seeded shake on separate channels, so the same seed always gives the same frames.
Draw order and layer types¶
- Draw order is
zIndexacross all layers; it doesn't follow the parent hierarchy. Higher draws in front, and ties keep layer order. To put a tail behind its body, give it a lowerzIndex; negative values are fine. - An image layer without drawings is a null layer. It is invisible and only carries a transform, which makes it a good rig root.
flipXmirrors a layer's own pixels aroundanchorX. Its children are not flipped.
Animation¶
- Animatable properties are
x,y,rotation,scaleX,scaleYandopacity, plusdrawingon image layers,brightness,contrastandsaturationon image and video layers, andcropX/cropYon cropped video layers. - A keyframe's easing shapes the segment that starts at that keyframe:
linear,hold,easeIn,easeOut,easeInOut,easeOutBack,easeOutBounce,easeOutElastic. - Before the first keyframe and after the last, a property holds that keyframe's value. Once a property has keyframes, its static value is ignored.
drawingalways holds, and before its first keyframe it shows the first keyframe's drawing. Add a keyframe at 0 for the starting drawing.
Effects, masks and motion paths¶
See the guide.
set_effectgives one layer, not its children, a drop shadow, outline, blur or tint. Sizes are pixels at the layer's scale and directions are on screen, so a shadow keeps its look when the layer or the camera zooms. The numbers animate asshadow.x,shadow.y,shadow.blur,shadow.opacity,outline.width,blur.x,blur.yandtint.amount.set_maskshows a layer only inside, or only outside, a rectangle, an ellipse or an image's alpha. The box is in the layer's own pixels and has a softfeatheredge;mask.x,mask.y,mask.width,mask.heightandmask.featheranimate.- A layer with effects or a mask is drawn into a layer of its own: content, mask, tint, outline, shadow, blur, and then its opacity.
set_motion_pathmoves a layer's anchor through timed points on cubic bezier curves, smooth where no handles are given. It moves at an even speed along each segment, eased per point, and can turn with the path (autoOrient). The path replacesxandy;xorykeyframes later take it off.
Text¶
- One font per text layer, with no automatic fallback. Use
fontFamilyfor an installed font, orfontAssetfor an imported.ttf/.otf/.ttc. If the font lacks glyphs for the text, every tool that touches the layer warns and names a font that covers it: "Myanmar Text" for Burmese, "Yu Gothic" for Japanese. - Shaping uses HarfBuzz, which handles Burmese stacking and reordering (kinzi, medials, pre-base vowels) and Japanese.
maxWidthwraps text:- Breaks go at spaces first.
- Only a phrase too wide for a line is split further: Burmese at syllable boundaries, Japanese per character with kinsoku rules (no 。、」 at a line start, no 「 at a line end).
- A single grapheme never splits.
\nalways starts a new line.
lineHeightis a multiple of the font's natural line spacing, not offontSize. The default is 1.0, which keeps Burmese stacked marks from colliding.align(left/center/right) aligns lines within the text box. The box ismaxWidthwide when set, otherwise as wide as the longest line.bolduses the font's bold face, or synthetic emboldening if it has none.strokeColor/strokeWidthdraw an outline ofstrokeWidthpixels outside the glyphs.boxdraws a background (color,padding,cornerRadius) behind the text's ink.
Video¶
import_videocopies clean files as they are. It transcodes anything else in the background: variable frame rate, rotation, 10-bit or HDR, or non-square pixels. The output is constant frame rate, upright, 8-bit BT.709 H.264, so frame n of a clip is always at n / fps.- A video layer plays
trimIn–trimOutof its source atspeed. Its timeline length is(trimOut − trimIn) / speed. render_frameseeks to the exact frame. Export decodes each clip once, in order, with one decoder per clip.- Footage over 2560 px (4K and up) gets a 1920 px proxy in the background when it is imported, and when a project that lacks one opens. Previews and object tracking read it; exports read the original. On a 20 s 4K clip in a 1080p project, the proxy took 2.5 s to make, and
render_framethen took 220–270 ms instead of 425–440 ms, a 12-frame contact sheet 1.7 s instead of 4.8 s. A preview drawn from a proxy says so. - Previews are cached in memory (up to 128 MB per open project) under the same per-frame key as the export's chunks: a frame or contact sheet cell shown before comes back without drawing while nothing it shows has changed, so an edit at 12 s leaves the previews at 2 s cached. On a 1080p clip,
render_frametook 312 ms the first time and 1 ms again; a 12-frame contact sheet took 2.3 s, then 44 ms, and 129 ms after an edit that changed 7 of its cells. preview_serverstarts a read-only page for the browser athttp://127.0.0.1:<port>/<token>/. It shows a timeline with the layers and markers, and plays frames of up to 640 px, about 15 a second, with the export's sound mix. It follows every edit within a second and draws again only the frames the edit changed. Only that address answers, and other sites can't read it. On a 1080p clip, the first play showed 11 frames a second and the second the full 15.- The editing tools (
split_layer,remove_range,remove_silences,cut_by_transcript, …) cut clips into pieces that play exactly as before. They ripple later layers, keyframes and children along, so a cut never changes a frame it doesn't touch. add_transitionbakes crossfade, fade-to-black, wipe, slide, zoom and whip-pan transitions into keyframes, masks and blur effects that you can adjust afterwards.reframe_to_verticalcrops landscape clips to the vertical canvas;cropXcan be keyframed to follow the subject.
Captions¶
generate_captions builds text layers from the transcript under a captions null layer, timed through the clips that play the asset. Cuts made before generating are followed; for cuts made after, run it again and it replaces the old captions. With behindSubject they go behind the person in the clip; auto brings them in front, with a short fade, wherever the person would cover too much of a caption. export_srt writes the same cues as subtitles.
Languages¶
A layer's language (en, ja, my, pt-BR) puts it and its children in that language's version only; untagged layers are in every version. A video's tag is the language of its sound: its picture stays in every version, and the other versions mute it.
add_caption_trackadds one language's captions as a track of their own (captions_ja), from cues such as a translation, an SRT file or a transcript.import_srtdoes the same with a.srtfile, and takes the language from a name liketalk.ja.srt.render_frame,render_contact_sheet,start_exportandexport_thumbnailtakelanguageto show one version.export_language_versionswrites<name>_<language>_<preset>.mp4for every language and preset in one job.- A text with a language and no font gets the language's font (
LanguageFonts), andcheck_projectshapes every text with HarfBuzz to find characters its font can't show.
See the guide.
Library¶
The library is a folder shared by all projects (see the guide):
- Rigs (
save_rig,add_rig): a layer and everything parented under it, with drawings, anchors and draw order. Each copy gets its own ids (tora_head,mimi_head). - Actions (
save_action,apply_action): a rig's keyframes over a time window, by part role rather than layer id. The root moves relative to where the rig stands, so the same walk cycle plays on any copy of the rig, at any speed and for any number of loops. - Styles (
save_style,apply_style): a text look, scaled to the canvas, usable byadd_text_layerandgenerate_captions. - Templates (
save_template,new_project_from_template): fixed layers such as an intro, watermark or end screen. Full-length layers stretch to the new duration, and pinned layers keep their distance from the end. - Sound effects (
list_sfx,add_sfx): your own audio files insfx\, with subfolders as categories.add_sfximports a sound once per project and places it with roleeffects. - Episode scripts (
build_from_script): a JSON or Markdown script of scenes, each with rigs, actions, sound effects and captions at their times, built into a new project in one call. What it can't map is listed and the rest is built.
Saving to the library doesn't change the project and isn't an undo step. Placing or applying an item is one undo step.
Audio¶
Audio layers and the sound of video layers are mixed at export, and for the preview page. Each one has:
start/end: placement on the timelinetrimIn/trimOut: the range used from the source filevolume: linear gainfadeIn/fadeOut: fade lengths in secondsrole:voice,musicoreffects. Audio layers default tomusic, video sound tovoice.
At export:
- Ducking. Music is lowered under voice by a sidechain compressor, roughly 12–18 dB while someone speaks, depending on how loud the voice is. It comes back within about a second after they stop.
- Loudness. The mix is normalized with a two-pass
loudnormto the project's target, -14 LUFS by default. The first pass measures the mix; the second applies the gain linearly, with true peak at most -1 dBTP. Set the target withupdate_project(loudnessTarget,0= off).
The mix is 48 kHz stereo AAC at 192 kbps (128 kbps for the preview page). Hidden audio layers are skipped.
Animating to sound (see the guide):
detect_beatsfinds the onsets of a music asset from its spectral flux (a hand-written FFT, no extra library) and the tempo from how they repeat. It fits a steady beat grid to the loudest hits, so kicks win over off-beat hi-hats. The BPM, beats and onsets are saved as<name>.beats.json, in source seconds.auto_lip_flapmeasures the loudness of every frame of a voice as it plays on the timeline. It gives quiet frames the closed mouth drawing and shares the open drawings evenly by loudness among the rest. Every drawing holds at least 2 frames, and the result is written asdrawingkeyframes in one undo step.