Skip to content

Commit 648f469

Browse files
Release v0.29.0: aligner word timings, reviewed batch plans, render fidelity and lint checks (#142)
caption --words, batch --plan/--apply-plan, render fidelity census with --strict and --verify, tts --lexicon, segment-overlap and segment-offscreen lint checks, and a write journal shown by restore --list.
1 parent cb348ed commit 648f469

27 files changed

Lines changed: 2867 additions & 64 deletions

‎CHANGELOG.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@ All notable changes to capcut-cli are documented here. The format follows [Keep
44

55
## [Unreleased]
66

7+
## [0.29.0] — 2026-10-09
8+
9+
### Added
10+
11+
- `caption --words <file.json|->` builds captions from word timings produced by an external aligner, so Whisper does not have to be installed. The format is detected automatically: Whisper / whisper.cpp `segments[].words[]`, WhisperX `word_segments[]`, or a plain `{word|text|char, start, end}` array with times in seconds, `start_ms`/`end_ms` or `start_time`/`end_time` (per-character forced-aligner output). The result reports `words_format`, `words_skipped` and `source_words`. Entries without timing are skipped; entries that end before they start or go back in time are refused with `refused [words-invalid]` naming the entry. Grouping, CJK joining without spaces, `--script`, `--karaoke` and `--word-reveal` behave exactly as with Whisper words. `--words` cannot be combined with `--audio`, `--from-segment`, `--audio-stream`, `--ffmpeg-cmd` or `--whisper-*`.
12+
- `batch <project> --plan <plan.json> < ops.jsonl` validates the operations under dry-run without touching the draft and writes a reviewable plan: `project`, `draft_sha256`, the normalized `operations`, `operations_sha256` (over key-sorted JSON) and a per-operation `preview`. `batch <project> --apply-plan <plan.json>` reads no stdin and applies the plan through the normal transactional batch. It refuses with `refused [plan-draft-changed]` when the draft changed since the plan was written, `refused [plan-tampered]` when the operations no longer match their hash, and `refused [plan-project-mismatch]` when the plan names another project. Without `--continue-on-error` a failing operation writes no plan.
13+
- `render` (including `--dry-run`) reports a `fidelity` census of what the proxy leaves out compared with the app: `faithful`, `expected_duration_us`, the `checked` categories, and per dropped category a count, up to 20 segment ids and a hint (missing media, closed main-track gaps, uncomposited overlay tracks, unburned text, stickers, transitions, effects, filters, masks, keyframes, text/video animations, blend modes, chroma key, matting). `render --strict` refuses with `refused [render-unfaithful]` before anything is written.
14+
- After a real render, `render` probes the written file with ffprobe (`--ffprobe-cmd` selects the binary) and reports `verification`: `{ verified, duration_us, expected_duration_us, drift_us, tolerance_us, within_tolerance }`, with a tolerance of one frame at the render fps. When ffprobe is missing or cannot read the file it reports `verified: false` with a reason. `render --verify` exits non-zero on drift beyond tolerance or an unverifiable file; the file stays on disk and the JSON result is still printed.
15+
- `tts --lexicon <file.json>` applies pronunciation rules (`{"rules":[{"text","say","case_sensitive"?}]}` or a bare array) to the text handed to the TTS engine only: longest match first, at word boundaries for rule edges that are word characters (`SQL` never fires inside `MySQLite`; CJK rules match anywhere). Malformed lexicons refuse with `refused [lexicon-invalid]`, and equal-length rules that say different things at the same position refuse with `refused [lexicon-ambiguous]`, both before any engine runs. The result reports `lexicon: { rules, applied, spoken_text }` with offsets in UTF-16 code units into the trimmed text. Works with `--text` and `--text-file`.
16+
- `lint` reports `segment-overlap` (error) when two segments on the same non-text track (video, audio, sticker, effect, filter …) overlap, naming the track, both segment ids and the overlap in µs. `lint --fix` repairs overlaps of at most one frame at the draft's fps (the independent-rounding overlap) by ending the earlier segment where the next begins, keeping its source duration in proportion to speed. Wider overlaps are reported, not fixed. Text tracks keep `caption-overlap`.
17+
- `lint` reports `segment-offscreen` (warning) for video, photo, sticker and text segments that cannot be seen: the clip's box lies entirely outside the canvas (material size fitted into `canvas_config`, rotation allowed for), a scale axis is 0, or opacity is 0. Segments whose position, scale or alpha is keyframed are not judged on that property. No `--fix`.
18+
- Every undo-history snapshot now records what made it: `.capcut-cli-history/<draft>.journal.jsonl` gets `{ index, time, command, argv, before_sha256, after_sha256 }` per write (library callers record `command: null`), trimmed together with the snapshots. `restore --list` shows `command`, `argv` and `time` per step; older snapshots without a journal line list with nulls. A torn or unreadable journal never breaks a write or a restore.
19+
- Whisper-free caption routes are documented in `docs/quickstart.zh-CN.md` (不装 Whisper 的字幕路线) and `examples/short-video-narration.md`: the app's own auto captions followed by `restyle` / `export-srt`, `import-srt`, and `caption --words`.
20+
21+
### Changed
22+
23+
- `doctor`'s Whisper hint names `caption --words` and `import-srt` as routes that work without Whisper. The check's status is unchanged.
24+
- `lint --frame-grid --fix` derives repaired source durations through the same helper as the new overlap repair: at speed 1 nothing changes; on clips at other speeds whose source duration matched target × speed (within 1%), the source duration now follows the snapped target duration instead of being left as it was.
25+
726
## [0.28.0] — 2026-10-03
827

928
### Added

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,10 @@ The host reads a draft and passes its JSON as tool input. The component itself h
105105

106106
## Release notes
107107

108+
> **New in v0.29.0:** captions from any aligner's word timings without Whisper (`caption --words`); reviewed batch plans bound to the draft (`batch --plan` / `--apply-plan`); a render fidelity census and output-duration check (`render --strict` / `--verify`); TTS pronunciation lexicons (`tts --lexicon`); `segment-overlap` and `segment-offscreen` lint checks; and `restore --list` naming the command behind each step. Full details in the [changelog](./CHANGELOG.md).
109+
108110
> **New in v0.28.0:** opt-in active-timeline selection; compilation into empty app-created projects; compact command discovery and selection by name for agents; Windows command-path quoting fixed in Python client v0.1.3; and Python client CI on Linux, macOS, and Windows. Full details in the [changelog](./CHANGELOG.md).
109111
110-
> **New in v0.27.0:** content-safe media replacement; automatic import registration after replacement and relink; recursive, ambiguity-safe relinking; exact fractional-second compile boundaries; operation preflight and failed-build cleanup; payload-bound queue IDs; canonical project locks; bounded queue results; and local OTIO file/relative references. Full details in the [changelog](./CHANGELOG.md).
111112

112113
For an existing nested project, use `capcut diagnose <project> --active-timeline` to inspect the selected document before editing. The opt-in follows the pointer on unverified builds and refuses invalid or conflicting selected documents; it does not bypass write guards.
113114

‎README.zh-CN.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@ Claude Code 也可以把它作为插件加载:
8282

8383
## 发布说明
8484

85+
> **v0.29.0 新增:** 用任意对齐工具的逐字/逐词时间戳生成字幕,无需安装 Whisper(`caption --words`);与草稿哈希绑定、可先审后执行的批量计划(`batch --plan` / `--apply-plan`);渲染保真度清单与输出时长校验(`render --strict` / `--verify`);TTS 发音词典(`tts --lexicon`);`segment-overlap` 与 `segment-offscreen` 两项 lint 检查;`restore --list` 显示每一步由哪条命令产生。完整说明见[更新日志](./CHANGELOG.md)。
86+
8587
> **v0.28.0 新增:** 显式选择活动时间线、向应用创建的空项目编译,以及精简的命令发现索引与按命令名筛选;Python 客户端 v0.1.3 修复 Windows 命令路径的引号解析,并在 Linux、macOS、Windows 上运行客户端 CI。详见 [更新日志](./CHANGELOG.md)。
8688
87-
> **v0.27.0 新增:** 按文件内容安全替换媒体;替换和重链接后自动更新媒体导入登记;递归搜索并报告同名歧义;精确处理小数秒时间边界;编译前校验操作并清理失败输出;将队列 ID 绑定到任务参数;统一项目锁;限制队列输出;以及解析 OTIO 的本地文件 URL 和相对路径。完整说明见[更新日志](./CHANGELOG.md)。
8889

8990
## 使用 capcut-cli 构建
9091

‎docs/command-reference.json‎

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "capcut-cli",
3-
"version": "0.28.0",
3+
"version": "0.29.0",
44
"schema_version": 2,
55
"description": "Edit CapCut/JianYing draft_content.json directly. JSON in, JSON out.",
66
"global_flags": [
@@ -1405,7 +1405,7 @@
14051405
{
14061406
"name": "tts",
14071407
"summary": "Synthesize a voiceover from text via a local TTS command (--tts-cmd) and add it as an audio segment.",
1408-
"usage": "capcut tts <project> [start] [duration] (--text <string> | --text-file <path>) --tts-cmd <template> [options]",
1408+
"usage": "capcut tts <project> [start] [duration] (--text <string> | --text-file <path>) --tts-cmd <template> [--lexicon <file>] [options]",
14091409
"positionals": [
14101410
{
14111411
"name": "project",
@@ -1417,6 +1417,11 @@
14171417
"type": "string",
14181418
"required": true
14191419
},
1420+
{
1421+
"name": "file",
1422+
"type": "path",
1423+
"required": false
1424+
},
14201425
{
14211426
"name": "start",
14221427
"type": "time",
@@ -1456,6 +1461,15 @@
14561461
"required": false,
14571462
"description": "TTS command template, run without a shell: {out} (required) is replaced with the .wav path the tool must write, {text} with the text as one argument; without {text} the text is piped to stdin."
14581463
},
1464+
{
1465+
"name": "lexicon",
1466+
"flags": [
1467+
"--lexicon"
1468+
],
1469+
"type": "path",
1470+
"required": false,
1471+
"description": "Pronunciation lexicon JSON ({\"rules\": [{\"text\", \"say\", \"case_sensitive\"?}]} or a bare array) applied to the spoken text only: longest match first, at word boundaries (CJK rules match anywhere). Equal-length rules that say different things refuse [lexicon-ambiguous]; lexicon.applied offsets are UTF-16 code unit indices into the trimmed text."
1472+
},
14591473
{
14601474
"name": "volume",
14611475
"flags": [
@@ -3588,12 +3602,17 @@
35883602
{
35893603
"name": "batch",
35903604
"summary": "Run multiple edits from stdin (JSONL), one file write.",
3591-
"usage": "capcut batch <project> [--continue-on-error] < operations.jsonl",
3605+
"usage": "capcut batch <project> [--continue-on-error] [--plan <plan.json>] < operations.jsonl | capcut batch <project> --apply-plan <plan.json>",
35923606
"positionals": [
35933607
{
35943608
"name": "project",
35953609
"type": "path",
35963610
"required": true
3611+
},
3612+
{
3613+
"name": "plan.json",
3614+
"type": "string",
3615+
"required": false
35973616
}
35983617
],
35993618
"options": [
@@ -3606,6 +3625,24 @@
36063625
"required": false,
36073626
"description": "Commit only successful operations and exit 1 if any fail."
36083627
},
3628+
{
3629+
"name": "plan",
3630+
"flags": [
3631+
"--plan"
3632+
],
3633+
"type": "path",
3634+
"required": false,
3635+
"description": "Validate the stdin operations as a real run would and write a reviewable plan file (draft and operations sha256, per-operation preview) instead of changing the draft."
3636+
},
3637+
{
3638+
"name": "apply_plan",
3639+
"flags": [
3640+
"--apply-plan"
3641+
],
3642+
"type": "path",
3643+
"required": false,
3644+
"description": "Apply a plan written by --plan (no stdin). Refused if the draft changed since the plan or its operations were edited."
3645+
},
36093646
{
36103647
"name": "active_timeline",
36113648
"flags": [
@@ -4343,7 +4380,7 @@
43434380
{
43444381
"name": "caption",
43454382
"summary": "Transcribe audio via whisper into real caption-track segments.",
4346-
"usage": "capcut caption <project> (--audio <path> | --from-segment <id>) [options]",
4383+
"usage": "capcut caption <project> (--audio <path> | --from-segment <id> | --words <file.json|->) [options]",
43474384
"positionals": [
43484385
{
43494386
"name": "project",
@@ -4375,6 +4412,15 @@
43754412
"required": false,
43764413
"description": "Audio input."
43774414
},
4415+
{
4416+
"name": "words",
4417+
"flags": [
4418+
"--words"
4419+
],
4420+
"type": "path",
4421+
"required": false,
4422+
"description": "Word timings from an external aligner instead of running Whisper (path or - for stdin): Whisper/whisper.cpp segments[].words[], WhisperX word_segments[], or an array of {word|text|char, start, end} (seconds; start_ms/end_ms and start_time/end_time also read). The result reports words_format and words_skipped. Not combinable with --audio, --from-segment, --audio-stream or --whisper-*."
4423+
},
43784424
{
43794425
"name": "audio_stream",
43804426
"flags": [
@@ -5956,7 +6002,7 @@
59566002
],
59576003
"type": "boolean",
59586004
"required": false,
5959-
"description": "List snapshots."
6005+
"description": "List snapshots, newest first, with the command that made each write."
59606006
},
59616007
{
59626008
"name": "active_timeline",
@@ -6584,6 +6630,33 @@
65846630
"required": false,
65856631
"description": "Stream ffmpeg's progress to stderr instead of buffering it."
65866632
},
6633+
{
6634+
"name": "strict",
6635+
"flags": [
6636+
"--strict"
6637+
],
6638+
"type": "boolean",
6639+
"required": false,
6640+
"description": "Refuse (refused [render-unfaithful], nothing rendered) when the fidelity census finds anything the proxy drops: transitions, effects, filters, masks, keyframes, uncomposited tracks, stickers, text, animations, blend modes, chroma, matting, gaps or missing media."
6641+
},
6642+
{
6643+
"name": "verify",
6644+
"flags": [
6645+
"--verify"
6646+
],
6647+
"type": "boolean",
6648+
"required": false,
6649+
"description": "Probe the written file with ffprobe and exit non-zero when its duration drifts from the draft's by more than one frame, or when it cannot be probed. The file is kept."
6650+
},
6651+
{
6652+
"name": "ffprobe_cmd",
6653+
"flags": [
6654+
"--ffprobe-cmd"
6655+
],
6656+
"type": "path",
6657+
"required": false,
6658+
"description": "ffprobe binary for output verification."
6659+
},
65876660
{
65886661
"name": "active_timeline",
65896662
"flags": [

‎docs/command-reference.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
| `add-audio` | `capcut add-audio <project> <file-or-url> <start> [duration] [options]` | yes | Add a local or Wikimedia audio file on an audio track. |
2828
| `add-video` | `capcut add-video <project> <file-or-url> <start> [duration] [options]` | yes | Add a local or Wikimedia video/image on a video track. |
2929
| `add-text` | `capcut add-text <project> <start> <duration> <text> [options]` | yes | Add a text segment with font/color/position options. |
30-
| `tts` | `capcut tts <project> [start] [duration] (--text <string> \| --text-file <path>) --tts-cmd <template> [options]` | yes | Synthesize a voiceover from text via a local TTS command (--tts-cmd) and add it as an audio segment. |
30+
| `tts` | `capcut tts <project> [start] [duration] (--text <string> \| --text-file <path>) --tts-cmd <template> [--lexicon <file>] [options]` | yes | Synthesize a voiceover from text via a local TTS command (--tts-cmd) and add it as an audio segment. |
3131
| `crop` | `capcut crop <project> <segment-id> [--ratio <r> \| --rect <x,y,w,h> \| --reset]` | yes | Read or set a video/photo segment's source-material crop (--ratio preset, --rect x,y,w,h, or --reset). |
3232
| `cut` | `capcut cut <project> <start> <end> --out <path>` | yes | Extract a time range into a new standalone draft. |
3333
| `duplicate` | `capcut duplicate <project> <segment-id> [--track <track-name>] [--new-track]` | yes | Duplicate a segment at its same timeline position onto a track above the source. |
@@ -51,11 +51,11 @@
5151
| `apply-template` | `capcut apply-template <project> <template> <start> <duration> [text] [options]` | yes | Stamp a template into a project with new timing/text. |
5252
| `make-preset` | `capcut make-preset <project> <text-segment-id> --out <preset.json>` | no | Extract a text segment's styling as a reusable preset JSON (apply via --preset). |
5353
| `templates` | `capcut templates <project>` | no | List bundled reusable templates. |
54-
| `batch` | `capcut batch <project> [--continue-on-error] < operations.jsonl` | yes | Run multiple edits from stdin (JSONL), one file write. |
54+
| `batch` | `capcut batch <project> [--continue-on-error] [--plan <plan.json>] < operations.jsonl \| capcut batch <project> --apply-plan <plan.json>` | yes | Run multiple edits from stdin (JSONL), one file write. |
5555
| `import-srt` | `capcut import-srt <project> <srt-or-> [options]` | yes | Import an SRT file/stdin as one text segment per cue. |
5656
| `import-ass` | `capcut import-ass <project> <ass-or-> [options]` | yes | Import an ASS/SSA subtitle file as text segments, keeping inline overrides as per-range styles. |
5757
| `text-ranges` | `capcut text-ranges <project> <id> --styles <json-or-@file>` | yes | Apply byte-accurate multi-style ranges to a text segment. |
58-
| `caption` | `capcut caption <project> (--audio <path> \| --from-segment <id>) [options]` | yes | Transcribe audio via whisper into real caption-track segments. |
58+
| `caption` | `capcut caption <project> (--audio <path> \| --from-segment <id> \| --words <file.json\|->) [options]` | yes | Transcribe audio via whisper into real caption-track segments. |
5959
| `translate` | `capcut translate <project> --to <language> --out <path> [options]` | yes | Clone a draft into another language via the Anthropic API. |
6060
| `migrate` | `capcut migrate <project> (--from <version> --to <version> \| --like <project> \| --from-store)` | yes | Apply known schema migrations across version boundaries. |
6161
| `add-sfx` | `capcut add-sfx <project> <slug> <start> <duration> [options]` | yes | Add a sound effect on a dedicated track. |

0 commit comments

Comments
 (0)