# Edit JSON

`preview` and `apply` take one JSON object with exactly one key: `range` (cut or restore speech), `speed` (play parts faster or slower), `track` (tracks and clips), `placement` (camera and screen layout), `canvas` (the size of the video frame), `offset` (the sync of one recording to the reference audio) or `gain` (the volume of an inserted video). Unknown keys are refused with `invalid_edit`.

Always `preview` first. It uses the same checks as `apply` and writes nothing.

## Range edits: cut and restore

Cut the lunch sentence, using output times from `cutzal transcript`:

```json
{ "range": { "action": "cut", "coordinate": "output", "ranges": [{ "start": 2.84, "end": 4.84 }] } }
```

Bring a cut part back. Restores must use source time, because cut parts have no output time:

```json
{ "range": { "action": "restore", "coordinate": "source", "ranges": [{ "start": 4.5, "end": 6.5 }] } }
```

- `action`: `cut` or `restore`.
- `coordinate`: `output` or `source` (see [coordinates.md](https://cutzal.ai/docs/en/agents/coordinates.md)).
- `ranges`: 1 to 1000 items of `{ "start", "end" }` in seconds, `start` at least 0 and less than `end`.
- Put the edges on word boundaries from `transcript` (a word's `start` and `end`). An edge inside a word can keep part of it (`partial: true` in `transcript`).
- One apply is one checkpoint. Put all ranges of one decision (for example every filler word) in one edit.
- Optional `rippleTrackIds` on `cut` or `restore` lists the overlay visual tracks that should move with the edit. Use IDs from `context.tracks`; duplicates, audio/source tracks, and this field on swap are refused. Without it, output-anchored clips keep their existing times.

```json
{ "range": { "action": "cut", "coordinate": "output", "ranges": [{ "start": 0.3, "end": 0.5 }], "rippleTrackIds": ["graphics"] } }
```

The participating clips follow the actual change in rendered output, including padding and existing cuts. Internal cuts split video graphics and advance `sourceInS` so the remaining footage stays synchronized. Content-anchored clips and native camera/audio tracks retain their existing projection rules. Affected locks, missing media, unsupported linked sound-effect trims, or fragments that only hold an exhausted video frame reject the entire cut. Preview shows those conflicts without writing. Undo/rewind restores both the cut and graphics. In the CutZal app, every edit that only shortens or only lengthens the video (keys, timeline, phone script and chat) includes every additional visual track automatically when the project has an output-anchored clip. `cutzal` commands keep the default above: only the tracks you list move.

A `restore` with `rippleTrackIds` pushes later participating clips back by the actual increase in rendered output, and a clip that runs to the end of the video grows to the new end. This restores the speech that the cut above removed and moves the graphics back with it:

```json
{ "range": { "action": "restore", "coordinate": "source", "ranges": [{ "start": 0.3, "end": 0.5 }], "rippleTrackIds": ["graphics"] } }
```

When the restored speech lands inside a clip, an image or text clip continues through it, and a video clip splits there and leaves the restored part empty. A video clip is refilled instead where an earlier ripple cut split that same graphic (restoring the same speech with the same tracks puts its footage back), keeps playing when it runs to the end of the video, and simply grows while it is already holding its last frame. A linked sound effect that ends after the restored spot inside its parent rejects the whole restore.

An output-anchored clip that runs to the end of the video (a background, watermark, frame or a header text clip) makes a shortening `cut` without `rippleTrackIds` fail with `track.outOfRange`, because the clip would pass the new end. List its track in `rippleTrackIds` whenever you cut, so the clip shrinks with the video and later clips move back with the speech.

## Speed edits: fast-forward or slow down

`speed` plays parts of the video faster or slower. The screen, the camera and clips that follow the speech (content anchors) change speed together, the edited video gets shorter (or longer), and later speech and clips move with it. Use it for a long process that should be seen but not heard in full, such as reading a prompt aloud, typing or waiting for a result. Cut what nobody needs to see.

Fast-forward the lunch sentence 4 times, using output times from `cutzal transcript`. Its sound is muted:

```json
{ "speed": { "coordinate": "output", "ranges": [{ "start": 2.4, "end": 4.6 }], "rate": 4 } }
```

Speed up the last sentence a little and keep its sound:

```json
{ "speed": { "coordinate": "source", "ranges": [{ "start": 8, "end": 10 }], "rate": 1.5, "audio": "keep" } }
```

Bring the last sentence back to its original speed with `rate: 1`:

```json
{ "speed": { "coordinate": "source", "ranges": [{ "start": 8, "end": 10 }], "rate": 1 } }
```

Speed up the whole video a little. It multiplies the speed of every part, so the 4x part above now plays at 4.8x:

```json
{ "speed": { "global": 1.2 } }
```

- `rate`: 0.25 to 16 with up to two decimals. Above 1 is faster, below 1 is slower, and `1` removes the speed from those ranges.
- `coordinate`: `output` or `source` (see [coordinates.md](https://cutzal.ai/docs/en/agents/coordinates.md)). Output ranges cover only what is in the edited video now. Source ranges also cover parts that are cut, so speech you restore inside them later plays at the same speed.
- `audio`: `keep` (the speech speeds up, pitch preserved) or `mute`. Without it, 0.5 to 2 keeps the sound and other speeds mute it. `keep` is allowed from 0.5 to 4. A muted part has no captions. Sound effect clips on audio tracks still play, so add a fast-forward sound effect there if the person wants one.
- Setting a speed on a range again replaces it only where the ranges overlap. Several ranges in one edit get the same speed and are one checkpoint.
- `global`: the speed of the whole video, 0.5 to 2 with up to two decimals. It multiplies every part's speed (1.2 on a 4x part plays it at 4.8x), so parts keep their relative speed. A part whose resulting speed is above 4 or below 0.5 plays without sound. `{ "speed": { "global": 1 } }` brings the whole video back to its original speed. In chat it needs a whole-video request.
- Clips pinned to output time (`anchor.kind: "output"`) move with the speech around the change. Over a part whose speed changes, an image or text clip gets shorter or longer with it, while a video clip or sound effect keeps its length and plays at normal speed. A clip that ran to the end of the video still runs to the new end. An edit that would move a clip on a locked track, or make a clip pass the new end or overlap another, is refused as a whole with `blocked` and the `clipIds`.
- `context.speed` lists the current speeds: `spans` in source time and `rate` for the whole video. `context.output.segments` carries `rate` (and `mute` when the sound is off) on those parts. `transcript` and `capture` already use the new output times.
- A project with a speed is saved as file version 19. Older CutZal versions refuse to open it instead of playing it at normal speed.

## Track edits

`context` returns the track document in `result.tracks`:

- `tracks`: in drawing order, bottom first. A later track is drawn on top of an earlier one.
  - `binding.kind: "source"`: the recording (screen, camera, microphone, system audio).
  - `binding.kind: "transcript"`: the subtitle track.
  - `binding.kind: "overlay"`: tracks for your clips. `kind: "video"` holds image and video clips, `kind: "audio"` holds sound effects.
- `clips`: image and video clips. `audioClips`: sound effect clips (only when an audio track exists).

The accepted `track` edits are `track.add`, `track.update`, `track.remove`, `track.reorder`, `clip.put`, `clip.remove` and `track.batch`. Use `rewind` to undo; there is no undo edit.

### Add a track

A video track for images and B-roll:

```json
{ "track": { "kind": "track.add", "track": { "id": "graphics", "kind": "video", "name": "Graphics", "locked": false, "visible": true, "binding": { "kind": "overlay" } } } }
```

An audio track for sound effects. Audio tracks need `"muted"` and are always `"visible": true`:

```json
{ "track": { "kind": "track.add", "track": { "id": "sfx", "kind": "audio", "name": "Sound effects", "locked": false, "visible": true, "muted": false, "binding": { "kind": "overlay" } } } }
```

- `id`: your choice, unique, up to 200 characters. `name`: what the person sees.
- Optional `"beforeTrackId": "<id>"` inserts the track before that one (so it is drawn below it). Without it the track goes last (on top).
- Images can be registered with `asset add` before adding a track. Registration preserves the existing tracks and clips; `clip.put` places the image on a video track.
- Sound files (`asset add` of an audio file, `sfx use`) are refused until the project has an audio track. Add the audio track first.

### Put a clip

`clip.put` adds a clip, or replaces the clip with the same `id`. To change a clip, copy it from `context`, change fields, and put it again.

An image over the whole frame from 1 s to 2 s of the output:

```json
{ "track": { "kind": "clip.put", "clip": { "id": "title-card", "trackId": "graphics", "assetId": "title", "sourceInS": 0, "anchor": { "kind": "output", "startS": 1, "endS": 2 }, "placement": "full", "endBehavior": "hold" } } }
```

B-roll in a box at the top right from 3 s to 4.5 s, starting 2 s into the B-roll file:

```json
{ "track": { "kind": "clip.put", "clip": { "id": "broll-1", "trackId": "graphics", "assetId": "city", "sourceInS": 2, "anchor": { "kind": "output", "startS": 3, "endS": 4.5 }, "placement": "frame", "frame": { "pos": "tr", "sizePct": 0.35, "radiusPx": 12, "aspect": "media", "mirror": false }, "endBehavior": "return" } } }
```

A sound effect at 1 s, 9 dB quieter than the file:

```json
{ "track": { "kind": "clip.put", "clip": { "id": "ding-1", "trackId": "sfx", "assetId": "ding", "sourceInS": 0, "gainDb": -9, "anchor": { "kind": "output", "startS": 1, "endS": 1.6 } } } }
```

Image and video clip fields:

- `id`: your choice, unique across all clips, up to 200 characters.
- `trackId`: an overlay video track. `assetId`: an image or video asset from `context.assets` or `asset add`.
- `sourceInS`: where in the file to start, in seconds. Always `0` for images.
- `anchor`: `{ "kind": "output", "startS", "endS" }` pins the clip to output time for ordinary range edits. Explicit cuts with `rippleTrackIds` (and cuts made in the CutZal app) update participating clips in that transaction. Opening a project never repositions them. Clips the person placed in CutZal may have `{ "kind": "content", ... }` anchors that follow the speech; keep those as they are when you put such a clip again.
- `placement`: `full` (fills the frame), `screen` (where the screen recording sits in the layout) or `frame` (a box set by `frame`).
- `frame` (needed for `placement: "frame"`): `pos` is a 9-grid spot (`tl`, `tc`, `tr`, `ml`, `mc`, `mr`, `bl`, `bc`, `br`) or `{ "x", "y" }` from 0 to 1 measured from the top left. `sizePct` is the box width as a fraction of the frame width (0.35 is 35%). `radiusPx` rounds the corners. `aspect` is `media` (the file's own shape) or `1:1`, `4:3`, `3:4`, `16:9`. `mirror` flips it horizontally. `focus` (optional) picks which part of the file shows when the box crops it ([coordinates.md](https://cutzal.ai/docs/en/agents/coordinates.md#frame-fields)).
- `endBehavior`: what happens when the file ends before `endS`. `return` goes back to what was showing before; `hold` keeps its last frame until `endS`.

Sound effect clip fields: `id`, `trackId` (an overlay audio track), `assetId` (an audio asset), `sourceInS`, `gainDb` (from -40 to 12, one decimal place) and `anchor` as above.

### Other track edits

```json
{ "track": { "kind": "clip.remove", "clipId": "title-card" } }
```

```json
{ "track": { "kind": "track.update", "trackId": "sfx", "changes": { "muted": true } } }
```

- `track.update` `changes` takes at least one of `name`, `locked`, `visible`, `muted` (audio tracks, and the main voice track: the microphone, or the main video when there is no microphone), `solo` (the same tracks: when any track is soloed, only soloed tracks play, in previews and exports), `gainDb` (system audio tracks), `denoise` and `denoiseMix` (the microphone track). Muting the voice or soloing another track exports the video with no speech.
- `track.remove` (`{ "kind": "track.remove", "trackId": "<id>" }`) removes an overlay track that has no clips left.
- `track.reorder` (`{ "kind": "track.reorder", "trackIds": [...] }`) takes every track id in the new order, bottom first. The recording tracks (`binding.kind: "source"`) keep their order among themselves, so the camera stays above the screen. Overlay tracks can go anywhere, for example between the screen and the camera so the camera stays on top of a B-roll clip.

### Background noise removal

Turn on `denoise` on the microphone recording track (`binding.sourceId` is the microphone source, usually `source:microphone`) to remove background noise such as fans and air conditioners from the voice:

```json
{ "track": { "kind": "track.update", "trackId": "source:microphone", "changes": { "denoise": true } } }
```

- It applies only while the microphone is the master audio. `false` turns it off. Sound effects and system audio are never processed.
- The app processes the whole microphone recording on this computer in the background (a long recording can take a few minutes, no cost). Preview plays the original sound until it is ready, and export waits for it. Transcripts and AI cut decisions keep using the original sound.

`denoiseMix` sets the strength, an integer from 1 to 10 (like Premiere's Mix Amount). 10 is the default and uses only the cleaned voice. Lower values blend in more of the original sound, which keeps a voice natural when the cleaned result sounds over-processed:

```json
{ "track": { "kind": "track.update", "trackId": "source:microphone", "changes": { "denoiseMix": 7 } } }
```

- Changing the strength does not process the recording again. Turning `denoise` off keeps the strength for the next time it is turned on. `10` removes the field.

### Several changes at once

`track.batch` applies up to 100 track edits together as one checkpoint. Batches cannot be nested.

```json
{ "track": { "kind": "track.batch", "edits": [
  { "kind": "clip.put", "clip": { "id": "ding-2", "trackId": "sfx", "assetId": "ding", "sourceInS": 0, "gainDb": -9, "anchor": { "kind": "output", "startS": 5, "endS": 5.6 } } },
  { "kind": "clip.put", "clip": { "id": "ding-3", "trackId": "sfx", "assetId": "ding", "sourceInS": 0, "gainDb": -9, "anchor": { "kind": "output", "startS": 6.5, "endS": 7.1 } } }
] } }
```

## Layout edits: camera and screen

`placement` changes how the screen recording and the camera are arranged, like the layout (화면 구성) controls in CutZal. It needs a project with a screen and a camera recording. `context.placement` names the source tracks that hold the screen and the camera (`null` when there is none), and the clips on those tracks hold the current layout: `placement` (`full` fills the picture, `frame` uses `frame`) and `frame`.

Show only the camera while the person talks from 2 s to 4 s of the edited video:

```json
{ "placement": { "preset": "cam-full", "coordinate": "output", "ranges": [{ "start": 2, "end": 4 }] } }
```

Show the screen with the camera in the top left corner for the whole video:

```json
{ "placement": { "preset": "cam-pip", "corner": "tl" } }
```

Make the camera a little bigger everywhere:

```json
{ "placement": { "camera": { "sizePct": 0.3 } } }
```

Shrink the screen and round its corners for one part, using source time:

```json
{ "placement": { "screen": { "sizePct": 0.9, "radiusPx": 16 }, "coordinate": "source", "ranges": [{ "start": 5, "end": 7 }] } }
```

The face sits left of the middle of the camera recording, so a narrow camera frame shows it off center. Keep the face at the center of the frame for the whole video (`focus` is the face's position in the recording, before mirroring):

```json
{ "placement": { "camera": { "focus": { "x": 0.42, "y": 0.5 } } } }
```

- Exactly one of these:
  - `preset`: `cam-pip` (screen with the camera in a corner; `corner` picks it, default `br`), `split` (screen left, camera right), `cam-full` (camera only) or `screen-full` (screen only).
  - Presets look at the shape of each recording and of the frame. A recording whose orientation differs from the frame (a vertical screen recording in a horizontal video, a horizontal camera in a vertical short) is shown whole with `screen-full`, `cam-full` and the screen part of `cam-pip`, with the background around it, instead of being cropped. In a horizontal frame with a vertical screen recording, `split` puts the camera on the left and the whole vertical screen on the right.
  - `camera` or `screen`: only the frame fields to change. The other fields keep their value in each part. The fields are in [coordinates.md](https://cutzal.ai/docs/en/agents/coordinates.md#frame-fields).
- `coordinate` and `ranges` go together, with the same rules as range edits. Leave both out to change the whole video. Parts cut out between your ranges are included, the same as selecting blocks in CutZal.
- The layout follows the speech: later cuts and restores keep it on the same words.
- `camera.mirror` also works where the camera fills the picture (`cam-full`). Position, size, rounding and aspect changes still skip full-picture camera parts; a full-picture-only range without a mirror change is refused with `invalid_edit`. A range with no camera is also refused.
- Switching between `cam-pip`, `split` and `cam-full` preserves each existing clip's mirror setting. Newly created clips without an existing camera segment use `mirror: false`; `screen-full` removes the camera from the selected range.
- `focus` follows the same rules as the other frame fields and also carries over when `cam-pip` or `split` rebuilds the camera frame. A full-picture camera part has no `focus`, so a part that goes through `cam-full` comes back centered.
- When nothing would change, `apply` returns `noop` and `preview` shows `changed: false`.
- One edit is one checkpoint. To pick a preset and then change the camera size, send two edits.
- A clip that fills the frame can carry `"conform": "fit"` (track edits, `placement: "full"` only): the whole recording is shown inside the frame instead of filling it and cropping the edges. Clips without `conform` fill the frame as before.

## Canvas: the size of the video frame

`canvas` sets the frame size of the whole video: `"1920x1080"` (horizontal), `"1080x1920"` (vertical) or `"original"` (the size of the screen recording, the default). One video has one frame size; vertical and horizontal recordings are then placed inside it with `placement`.

Make a horizontal video from a project whose screen was recorded on a vertical monitor:

```json
{ "canvas": "1920x1080" }
```

- Layouts that showed a recording whole keep showing it whole: a full-frame clip that would now be cropped gets `conform: "fit"`, and a box that would now run off the frame shrinks in place. Clips that were already cropped stay as they are, and locked tracks are not changed. `apply` returns how many clips it changed as `fixed`.
- Then pick the layout per part with `placement` presets, which follow the new frame shape.
- `preview` only says whether the size changes (`changed`). Check the result with `capture` after `apply`.
- One edit is one checkpoint, and rewinding to an earlier checkpoint also restores the frame size. Same size is `noop`.

## Offset: sync a recording to the reference audio

`offset` moves one take of one source in time against the reference audio. `sourceTime = masterTime + offsetS`, so a positive value shows later parts of that recording at the same moment. Read the current values from `context.sources` (each source has `isMaster` and its `takes` with `id`, `offsetS` and `syncStatus`).

The camera shows the mouth about 0.3 seconds before the voice is heard, so move the camera 0.3 seconds back from its current value (here from 0.27):

```json
{ "offset": { "sourceId": "camera", "takeId": "camera-1", "offsetS": -0.03 } }
```

- `offsetS` is the new value in seconds, not a change (within ±3600). Compute it from `context.sources`. Sending the same value again is `noop`.
- `takeId` can be left out when the source has one take. With several takes it is required, and a missing or unknown take is refused with the list of takes.
- The reference audio source (`isMaster: true`) always has offset 0 and is refused. Move the other sources instead.
- The take becomes `syncStatus: "manual"`. The person can undo it in CutZal. It is listed in `checkpoints`, but `rewind` does not bring offsets back: to go back, send another `offset` edit with the previous value. Works before transcription too.
- `preview` only says that the value changes. Check the result with `capture` at a moment where the mouth and the voice are clear.

## Inserted videos

`cutzal insert` adds a video with its own sound to the edited video (see [commands.md](https://cutzal.ai/docs/en/agents/commands.md)). Its part of source time comes after the end of the recording, and the edited video plays it at the insert point. Cut and restore it like any other part: a `range` `cut` with `coordinate: "output"` over the inserted block takes it out of the edited video. A `content` clip can be anchored to the inserted part in source time, and it follows that part when the person moves it.

### Gain: the volume of an inserted video

`gain` sets the volume of one inserted video in dB. Inserted takes are the ones with `inserted: true` in `context.sources`, and their `gainDb` is the current value (0 is the original volume of the file). It starts at the value CutZal matched to the main audio when the video was inserted.

The inserted interview plays a little loud next to the talk, so make it 3 dB quieter than its current 0 dB:

```json
{ "gain": { "takeId": "interview", "gainDb": -3 } }
```

- `gainDb` is the new value, not a change: from −40 to +20 dB in steps of 0.1. `0` brings back the original volume. Sending the current value again is `noop`.
- Only inserted videos take a volume. Any other take is refused with `invalid_edit` and the list of inserted takes. To change the loudness of the whole video, use the person's target loudness setting in CutZal instead.
- The same volume is used by playback in CutZal, by exports, and by the loudness measurement for the target loudness. The person can undo it in CutZal. It is listed in `checkpoints`. `preview` only says that the value changes.

## What gets refused

- Clips on the same track that overlap in output time.
- Clip changes on a locked track.
- An asset kind that does not match the track (images and videos go on video tracks, audio on audio tracks).
- A `rewind`-like edit (`track.restore`) or a drag transform (`clip.transform`): use `rewind`, or put the clip again.

When an edit would change clips in a way the person should decide, `preview` shows `status: "blocked"` with `conflicts`, and `apply` returns the error `blocked` without applying. Change the edit or ask the person.

## A full example: title card on a new project

1. `cutzal context --project clip.mp4` gives `revision: 0` on a project that was just opened.
2. Add a video track (the `track.add` above) with `apply --revision 0 --id add-graphics`. The result has `revision: 1`.
3. `cutzal asset add --project clip.mp4 title.png` registers asset `title`. The revision is now 2 (run `context` to see it).
4. Put the title card (the first `clip.put` above) with `apply --revision 2 --id put-title --label "Title card at 1 s"`.
5. `cutzal capture --project clip.mp4 --at 1.5` and look at the PNG.

### Captions on video

Use the existing transcript track and a `track.update` edit. Captions are off until explicitly enabled. Read the track ID from `inspect` rather than inventing a second track. The full `caption` object is required:

```json
{ "track": { "kind": "track.update", "trackId": "transcript", "changes": { "caption": { "enabled": true, "x": 0.5, "y": 0.88, "size": 0.045, "appearance": "outline" } } } }
```

`x` and `y` are fractions of the canvas (0.05–0.95); `size` is a fraction of canvas height (0.02–0.09). `appearance` is `outline`, `box`, or `plain` (text only). Locked transcript tracks reject caption changes. Placement is clamped to keep the text in frame. Both preview and the composed video use these settings; source stems and SRT/VTT files remain separate. Burning captions re-encodes the composed video. No transcription or model call is started by this edit.

The caption look uses the same words as title text. These fields are optional; omit them to keep the default look (white text, `#111111` outline or a black box at 80%):

- `weight`: `semibold` or `black`. The outline is thicker with `black`.
- `color`: text color, `#RRGGBB`.
- `edgeColor`: outline color, or box color with `box`, `#RRGGBB`.
- `boxOpacity`: box opacity with `box`, 0.2–1.
- `shadow`: `true` adds a soft drop shadow.

The app offers four looks and names them by `appearance`, `weight`, and `shadow` only (colors and opacity can change without changing the look). To match one, send its values:

| Look         | `appearance` | `weight`   | `shadow` | colors                                                   |
| ------------ | ------------ | ---------- | -------- | -------------------------------------------------------- |
| Outline      | `outline`    | `semibold` | `false`  | `color` `#FFFFFF`, `edgeColor` `#111111`                 |
| Box          | `box`        | `semibold` | `false`  | `color` `#FFFFFF`, `edgeColor` `#000000`, `boxOpacity` 1 |
| Bold outline | `outline`    | `black`    | `false`  | `color` `#FFFFFF`, `edgeColor` `#000000`                 |
| Shadow       | `plain`      | `semibold` | `true`   | `color` `#FFFFFF`                                        |

Lines wrap between words. A word longer than the line, or text without spaces, wraps by character.

### Title text on the video

On-screen titles (for example the two-line header of a vertical short) are text clips, not images, so the person can edit the words in CutZal. Put one with `clip.put` on a video overlay track, with `title` instead of `assetId`: `{ "kind": "clip.put", "clip": { "id": "header", "trackId": "<overlay track>", "placement": "full", "anchor": { "kind": "output", "startS": 0, "endS": <end> }, "title": { "text": "first line\nsecond line", "style": { "x": 0.5, "y": 0.24, "size": 0.058, "colors": ["#F3EFE8", "#EC8A62"], "weight": "black", "shadow": true } } } }`.

- `text`: up to 3 lines (`\n`), 40 characters per line. `colors` is one `#RRGGBB` per line; extra lines reuse the last color. `weight` is `black` or `semibold`.
- `x` and `y` are the center of the text block as fractions of the canvas (0.02–0.98); `size` is the letter height as a fraction of canvas height (0.02–0.12). A line wider than 88% of the canvas shrinks on its own.
- Text clips take `placement: "full"` only, and no `assetId`, `sourceInS`, `endBehavior`, `frame` or `mirror`. To change the words, send the same clip again with a new `title`. Preview, capture and export draw the same image.
- A project with a text clip is saved as file version 16. Older CutZal versions refuse to open it instead of showing the video without the title.
- Optional style fields: `sizes` sets the size of each line (same range as `size`; lines without a value use `size`). Wrap words in asterisks, like `*word*`, to draw them in `accent` (`#RRGGBB`, default `#FF3B30`); the asterisks are not drawn and do not count toward the 40 characters, and they must come in pairs. `progress: { "y": <top of the bar as a fraction of canvas height>, "color": "#RRGGBB", "track": "#RRGGBB" }` adds a bar that fills from left to right while the text clip is on screen. CutZal's Properties panel sets `track` to the bar color darkened (each channel x 0.48).
- Example: a two-line short header (small top line, big bottom line, key words in the accent color, progress bar under the text) on a 1080x1920 canvas is `{ "x": 0.5, "y": 0.2318, "size": 0.074, "sizes": [0.0344, 0.074], "colors": ["#F3F3EF"], "accent": "#FF3B30", "weight": "black", "shadow": false, "progress": { "y": 0.2943, "color": "#FF3B30", "track": "#7A1C17" } }` with text like `first line\n*key words* rest`.

### Subtitle term fixes

Transcripts spell product names the way they sound (for example "클라우드 코드"). To show the standard spelling in subtitles, set `captionTerms` on the transcript track with `track.update`: `{ "kind": "track.update", "trackId": "transcript", "changes": { "captionTerms": [{ "from": "클라우드 코드", "to": "Claude Code" }, { "from": "깃허브", "to": "GitHub" }] } }`.

- Both sides need the same number of words. The last word keeps what follows it, so "클라우드 코드에서" becomes "Claude Code에서". Longer terms match first, up to 100 terms.
- Preview, burned-in captions and SRT/VTT use the same list. Word timings do not change. `"captionTerms": null` removes the list.
