diff --git a/CHANGELOG.md b/CHANGELOG.md index d1d7315..4b29fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,29 @@ All notable changes to capcut-cli are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.33.0] — 2026-11-06 ### Added +- `diff --since-write` shows what changed in a draft since capcut-cli last wrote it, such as what CapCut rewrote when it opened or saved a tool-written draft. It prints the usual `diff` result with `a` as written and `b` as the draft is now, plus `since_write` with `written_at`, `written_by` (the command that made the write), `written_sha256`, `current_sha256` and `changed` (false when the file is exactly what the CLI wrote, which stderr also says), `top_level_changed` (the document fields that differ, such as `update_time`) and `track_order_changed`. Every write now keeps a copy of what it wrote as `.last-write` in the draft's `.capcut-cli-history/` directory, one file per draft file, replaced atomically on each write and on `restore`; snapshots, `.bak`, `restore` and `restore --list` are unchanged. A draft this version has not written yet has no record, and `--since-write` exits 1 saying so. `diff ` is unchanged. +- `fixture --anonymize` builds a bundle you can share without showing what the draft is about. Media file names become `media-001.mp4`, `media-002.mp3`, … (each keeps its extension, and the same file gets the same name in every mirror the bundle carries: `draft_content.json`, `draft_info.json`, `template-2.tmp`, `draft_meta_info.json` and the nested `Timelines/` documents). Folder names on media and project paths become `folder-001`, … (standard folders such as `Desktop` and the app's own stay), and the project name becomes `project`. Every text is replaced character by character with a placeholder of the same length and script (CJK, kana and Hangul with 字, Latin letters with x/X by case, digits with 0; spaces, punctuation and style ranges kept), so line wrapping and style range offsets behave as in the original. Only the replaced strings change; every other byte of each file stays as it was. `fixture` never copies media, so no media file is included either way. The report records `anonymized: true` and counts what was replaced, and the redaction check runs on the anonymized bundle. - `translate --cloud` and `caption --translate ` (experimental, unofficial) translate with the subtitle translation behind CapCut's auto-captions, on ByteDance's servers and without an API key. `translate --cloud` writes the translated draft as the Anthropic path does; `caption --translate` translates the cues before they are written and keeps CapCut's timings for the translated words, so `--karaoke` and `--word-reveal` work in the target language. Language names such as `Vietnamese`, `deutsch` or `simplified chinese` map to CapCut's codes. Like `caption --cloud`, it calls CapCut's private endpoints as the desktop app does, so ByteDance can change or refuse them at any time. Every run says on stderr how many lines go to ByteDance, and `--dry-run` sends nothing (`caption --translate --dry-run` reports the lines under `translation`). A reply with more or fewer lines than were sent is refused instead of pairing translations with the wrong timings, and a line that comes back without a translation keeps its original text and is counted in `kept_original` ([#169](https://github.com/renezander030/capcut-cli/pull/169), contributed by @datnt-97). +- `loudness` can gate a delivery, so a script stops before exporting a mix that misses its target. `--tolerance ` (with `--target`) fails the run when the estimated loudness sits further than that from the target, and `--max-true-peak ` fails it when the loudest clip's true peak, at its volume, is above the ceiling. With `--apply` the mix after this run's gain is judged, `--max-gain-db` cap included; without it, the mix as it stands. A miss exits 1 with `ok: false` and the reason on one stderr line, and the result's `gate` carries the target, tolerance, estimate and each verdict. With `--apply` the gain is written either way (journaled, `restore` undoes it). Without either flag the result and exit code are unchanged. +- `lint --max-still-secs ` and `--max-shot-secs ` check pacing. A still image (a photo material) that plays longer than n seconds on the timeline is reported as `still-too-long`, and a video clip longer than n seconds, overlay tracks included, as `shot-too-long`. Both are warnings (exit 1) that name the segment, its track, where it starts and how long it plays; neither is fixable, since where to cut is an editing decision. Neither check runs unless its flag is given, so lint's default result is unchanged. +- `lint --min-silent-secs ` reports `silent-stretch` (warning) for every range of n seconds or longer, up to the end of the timeline, where nothing audible plays: no audio clip above volume 0 and no video clip with its own sound. A clip counts as silent only when the draft says so (volume 0 without volume keyframes, a muted track, a photo, or a video whose sound was separated to an audio track), so a video clip whose file may have no sound still counts as sound and the check never reports silence that might not be there. The message gives the range's start, end and length. Not fixable, and off unless the flag is given. +- `caption` checks that its cues cover the speech it transcribed. After transcribing (local Whisper or `--cloud`), ffmpeg's silencedetect measures the non-silent spans of the audio that was transcribed (the extracted stream with `--audio-stream`), the cue spans are subtracted in the audio's own time, and the result carries `coverage: {speech_s, captioned_s, ratio, uncaptioned: [{start_s, end_s}], tail_uncaptioned_s}`. `uncaptioned` lists the speech no cue covers that lasts at least `--min-uncaptioned` seconds (default 1.5), and `tail_uncaptioned_s` is the part of it after the last cue. When any span is listed, one `[uncaptioned-speech]` warning on stderr names them, so a transcription that stops before the end of the audio, or drops quiet speech, no longer passes unnoticed. The exit code is unchanged. Without ffmpeg the block is `{skipped: "ffmpeg not found"}` and the run goes on; `--no-coverage` skips the check, and `--words` runs, which read no audio, have none. +- `caption` caches what it transcribed, so a run that only changes the styling, the grouping, `--script` or `--translate` does not transcribe again, and with `--cloud` does not upload again. The raw transcription (cues, words, engine) is stored under the sha256 of the exact audio bytes that were transcribed (after `--audio-stream` extraction) together with the engine, Whisper command, model, language, word-timing mode and `--cloud`, so changing any of those transcribes afresh. The result carries `transcript_cache: "hit" | "miss" | "off"`. Entries live in `CAPCUT_CLI_CACHE_DIR` if set, else `$XDG_CACHE_HOME/capcut-cli/transcripts`, else `~/.cache/capcut-cli/transcripts` (`%LOCALAPPDATA%\capcut-cli\cache\transcripts` on Windows), one JSON file per key written atomically; an entry that cannot be read or does not match its key is ignored and rewritten. `--no-cache` neither reads nor writes it, and `--dry-run` reads it but never writes it. ### Changed +- `render --burn-captions` draws each caption in its text material's own font file instead of ffmpeg's default face. The font is the first style block's `font.path` in the material's content (what CapCut renders from), else the material's `font_path`, and it is passed to drawtext as `fontfile=` when the file exists and is a `.ttf`, `.otf` or `.ttc`; the path is escaped so commas, brackets, colons and backslashes in it stay inside the option. A caption whose font is missing, of another type or not named keeps the default face as before. The render result, `--dry-run` plans included, reports `caption_fonts: {from_draft, default}`. - The Python client's 3.9 job runs on `ubuntu-24.04`. `ubuntu-latest` moved to 26.04, for which `setup-python` has no 3.9 build, so the job failed before it ran a test; the package still supports Python 3.9. ### Fixed - `text-style` applies `--font-size`, `--color`, `--align`, `--x` and `--y` without `--preset`. The flags were declared (and shared with `add-text`) but only honoured through a preset, so a plain `text-style --font-size 22 --color #FFD60A` exited 0 and left the text unchanged. Font size and colour are written to the material and to every style block CapCut renders from, `--x`/`--y` move the segment as `restyle` does, and `--color` must be `#RRGGBB`. `compile`'s `text-style` operation accepts and validates `fontSize`, `alignment` and `color` too. +- Writes under Volta no longer stop with `[editor-open]`. The editor-open guard compared process names only, so the shim named `capcut` that starts node was taken for the CapCut editor and every `--apply` was refused. The guard now leaves out the CLI's own process and each of its parents (on Windows, where `tasklist` lists no parent ids, the direct parent) and still finds an editor running beside it ([#168](https://github.com/renezander030/capcut-cli/issues/168)). +- `fixture --out` requires a new or empty directory outside the source project. It refuses occupied destinations and output symlinks instead of modifying original drafts, overwriting unrelated files, or leaving private media in a bundle reported as media-free. Bundles are built in a temporary sibling directory and published only after generation and redaction inspection finish; a generation failure leaves a new destination absent or an existing empty destination empty. Existing redaction findings remain available for manual review. ## [0.32.0] — 2026-10-29 diff --git a/README.md b/README.md index 3b8d75b..872c277 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ The host reads a draft and passes its JSON as tool input. The component itself h ## Release notes -> **New in v0.32.0:** time arguments reject malformed or unsafe values before writing; translation responses must contain exactly one string per input; and subtitle imports reject malformed UTF-8 instead of silently corrupting caption text. Full details in the [changelog](./CHANGELOG.md). +> **New in v0.33.0:** `caption` reports speech its cues leave uncaptioned and reuses a cached transcription when only the styling changes; `loudness --tolerance` and `--max-true-peak` gate a delivery with an exit code; `lint` gains opt-in pacing and silent-stretch checks; `diff --since-write` shows what CapCut changed after the CLI's last write; `fixture --anonymize` makes a shareable bundle; and `render --burn-captions` draws captions in the draft's own font. Full details in the [changelog](./CHANGELOG.md). > **New in v0.31.0:** experimental, unofficial `caption --cloud` that transcribes without a local Whisper install by uploading the audio to ByteDance's CapCut servers ([#157](https://github.com/renezander030/capcut-cli/pull/157)), uploads nothing in a dry run, retries flaky requests and waits as long as the audio needs; loudness to a delivery target (`loudness --target -14`, applied as segment volume and undoable); faster compiles that reuse media hashes across repeated cuts; and three `lint` checks for clips that play silent after a copy, keyframes on trimmed or re-timed clips, and `UNIFORM_SCALE` zooms on JianYing 11.6+. Full details in the [changelog](./CHANGELOG.md). diff --git a/README.zh-CN.md b/README.zh-CN.md index 97dc0ea..7f9b666 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -82,7 +82,7 @@ Claude Code 也可以把它作为插件加载: ## 发布说明 -> **v0.32.0 更新:** 时间参数会在写入前拒绝格式错误或超出安全范围的值;翻译结果必须为每条输入提供一个字符串;字幕导入拒绝无效的 UTF-8,避免静默破坏字幕文本。完整说明见[更新日志](./CHANGELOG.md)。 +> **v0.33.0 更新:** `caption` 会报告字幕未覆盖的语音,并在只改样式时复用缓存的转写结果;`loudness --tolerance` 和 `--max-true-peak` 以退出码把关交付响度;`lint` 新增可选的节奏与静音段检查;`diff --since-write` 显示 CapCut 在 CLI 上次写入后改动了什么;`fixture --anonymize` 生成可分享的匿名包;`render --burn-captions` 使用草稿自身的字体绘制字幕。完整说明见[更新日志](./CHANGELOG.md)。 > **v0.31.0 新增:** 实验性、非官方的 `caption --cloud`:无需本地 Whisper,将音频上传到字节跳动的 CapCut 服务器转写([#157](https://github.com/renezander030/capcut-cli/pull/157)),dry run 中不上传任何内容,请求失败会自动重试,等待时间随音频长度增加;按发布平台的响度目标调整音量(`loudness --target -14`,写入片段音量,可撤销);同一素材的重复剪辑复用内容哈希,编译更快;新增三项 `lint` 检查:复制后无声的片段、裁剪或变速片段上的关键帧,以及剪映 11.6 及以上版本中的 `UNIFORM_SCALE` 缩放。完整说明见[更新日志](./CHANGELOG.md)。 diff --git a/docs/command-reference.json b/docs/command-reference.json index ee6f70d..732877c 100644 --- a/docs/command-reference.json +++ b/docs/command-reference.json @@ -233,6 +233,33 @@ "required": false, "description": "Skip ffprobe media checks (VFR / unreadable media)." }, + { + "name": "max_still_secs", + "flags": [ + "--max-still-secs" + ], + "type": "number", + "required": false, + "description": "Report still-too-long (warning) for each still image that plays longer than this many seconds. Off unless given." + }, + { + "name": "max_shot_secs", + "flags": [ + "--max-shot-secs" + ], + "type": "number", + "required": false, + "description": "Report shot-too-long (warning) for each video clip that plays longer than this many seconds. Off unless given." + }, + { + "name": "min_silent_secs", + "flags": [ + "--min-silent-secs" + ], + "type": "number", + "required": false, + "description": "Report silent-stretch (warning) for every range of this many seconds or more where nothing audible plays. A clip counts as silent only when the draft says so (volume 0, muted track, photo, sound separated). Off unless given." + }, { "name": "pip", "flags": [ @@ -4437,7 +4464,7 @@ ], "type": "path", "required": false, - "description": "FFmpeg binary used by --audio-stream." + "description": "FFmpeg binary used by --audio-stream and the coverage check." }, { "name": "from_segment", @@ -4561,6 +4588,34 @@ "required": false, "description": "Maximum gap inside a karaoke cue." }, + { + "name": "min_uncaptioned", + "flags": [ + "--min-uncaptioned" + ], + "type": "number", + "required": false, + "description": "Shortest stretch of speech without a cue, in seconds, that the coverage check lists in coverage.uncaptioned and warns about.", + "default": 1.5 + }, + { + "name": "no_coverage", + "flags": [ + "--no-coverage" + ], + "type": "boolean", + "required": false, + "description": "Skip the coverage check. By default, after transcribing (not with --words), ffmpeg measures the speech in the transcribed audio (silencedetect) and the result's `coverage` block reports speech_s, captioned_s, ratio, the uncaptioned spans and tail_uncaptioned_s (speech after the last cue); any uncaptioned span prints one `[uncaptioned-speech]` warning on stderr. Without ffmpeg the block is {skipped: \"ffmpeg not found\"} and the run does not fail." + }, + { + "name": "no_cache", + "flags": [ + "--no-cache" + ], + "type": "boolean", + "required": false, + "description": "Neither read nor write the transcript cache. By default the raw transcription (before --script, grouping, styling and --translate) is cached, keyed by the sha256 of the transcribed audio bytes, the engine, whisper command, model, language and word-timing mode, so a run that only changes those later steps does not transcribe (or, with --cloud, upload) again. The result reports transcript_cache: hit, miss or off. Cache directory: CAPCUT_CLI_CACHE_DIR, else $XDG_CACHE_HOME/capcut-cli/transcripts, else ~/.cache/capcut-cli/transcripts (%LOCALAPPDATA%\\capcut-cli\\cache\\transcripts on Windows). --dry-run reads it but never writes it." + }, { "name": "track_name", "flags": [ @@ -5387,8 +5442,8 @@ }, { "name": "diff", - "summary": "Compare two drafts (segments/materials/tracks added/removed/changed).", - "usage": "capcut diff ", + "summary": "Compare two drafts (segments/materials/tracks added/removed/changed), or with --since-write a draft with what capcut-cli last wrote to it.", + "usage": "capcut diff ( | --since-write)", "positionals": [ { "name": "project-a", @@ -5401,7 +5456,17 @@ "required": true } ], - "options": [], + "options": [ + { + "name": "since_write", + "flags": [ + "--since-write" + ], + "type": "boolean", + "required": false, + "description": "Compare with what capcut-cli itself last wrote to it instead of with a second draft, to see what the app changed when it opened or saved a tool-written draft (a = as written, b = now). Adds since_write: {file, written_at, written_by, written_sha256, current_sha256, changed, top_level_changed, track_order_changed}; changed is false when the file is byte-identical to the write. Every write records its result in the draft's history directory; a draft with no recorded write exits 1." + } + ], "mutates": false, "prerequisites": [], "output": { @@ -5898,7 +5963,7 @@ { "name": "fixture", "summary": "Build a shareable, redacted compatibility bundle (timeline JSON only) for a version-support issue, including the mask-keyframe evidence report (#44).", - "usage": "capcut fixture --out [--check]", + "usage": "capcut fixture --out [--anonymize] [--check]", "positionals": [ { "name": "project", @@ -5914,7 +5979,16 @@ ], "type": "path", "required": false, - "description": "Output directory for the sanitized bundle." + "description": "Output directory for the sanitized bundle: a new or empty directory outside the source project." + }, + { + "name": "anonymize", + "flags": [ + "--anonymize" + ], + "type": "boolean", + "required": false, + "description": "Also replace what the draft is about: media file names become media-001. and folder names on media and project paths folder-001 (consistently across every mirror; standard folders such as Desktop are kept), the project name becomes project, and every text is replaced character by character with placeholders of the same length and script (CJK 字, Latin x/X, digits 0; spaces, punctuation and style ranges kept). The report records anonymized: true and counts what was replaced. Media files are never copied." }, { "name": "check", @@ -6629,7 +6703,7 @@ ], "type": "boolean", "required": false, - "description": "Burn captions." + "description": "Burn captions (ffmpeg drawtext). Each caption uses its text material's own font file (the first style block's font.path, else font_path) when that file exists and is a .ttf, .otf or .ttc, otherwise ffmpeg's default face; the result's caption_fonts counts both." }, { "name": "soft_captions", @@ -6711,7 +6785,7 @@ { "name": "loudness", "summary": "Measure each audible clip's loudness (EBU R128) and, with --target, set the gain that brings the export there.", - "usage": "capcut loudness [--target ] [--per-clip] [--max-gain-db ] [--apply]", + "usage": "capcut loudness [--target ] [--per-clip] [--max-gain-db ] [--apply] [--tolerance ] [--max-true-peak ]", "positionals": [ { "name": "project", @@ -6727,6 +6801,16 @@ "name": "db", "type": "string", "required": false + }, + { + "name": "lu", + "type": "string", + "required": false + }, + { + "name": "dbtp", + "type": "string", + "required": false } ], "options": [ @@ -6767,6 +6851,24 @@ "required": false, "description": "Write the gain as segment volume (default: report only)." }, + { + "name": "tolerance", + "flags": [ + "--tolerance" + ], + "type": "number", + "required": false, + "description": "Delivery gate (needs --target): exit 1 when the judged loudness sits more than this many LU from the target. With --apply the mix after this run's gain is judged, without it the mix as it stands." + }, + { + "name": "max_true_peak", + "flags": [ + "--max-true-peak" + ], + "type": "number", + "required": false, + "description": "Delivery gate: exit 1 when the loudest clip's true peak, at the judged volume, is above this level in dBTP." + }, { "name": "ffmpeg_cmd", "flags": [ diff --git a/docs/command-reference.md b/docs/command-reference.md index aa6cbcb..84fc485 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -68,7 +68,7 @@ | `replace-media` | `capcut replace-media [--retime]` | yes | Swap a segment's source file (placeholder > final) keeping its timing, effects, and keyframes. | | `timeline` | `capcut timeline [--cols ]` | no | Show the track/segment layout (JSON, or -H ASCII bars). | | `projects` | `capcut projects [query] [--drafts ] [--names]` | no | List CapCut/JianYing draft folders on disk. | -| `diff` | `capcut diff ` | no | Compare two drafts (segments/materials/tracks added/removed/changed). | +| `diff` | `capcut diff ( \| --since-write)` | no | Compare two drafts (segments/materials/tracks added/removed/changed), or with --since-write a draft with what capcut-cli last wrote to it. | | `concat` | `capcut concat [--out ]` | yes | Append one draft onto another's timeline (id-safe), write to --out or in place. | | `config` | `capcut config` | no | Show the resolved config (.capcutrc + effective defaults). | | `describe` | `capcut describe [--compact] [--command ]` | no | Emit command contracts as JSON, optionally filtered by name or reduced to a compact discovery index. | @@ -78,7 +78,7 @@ | `harvest-enums` | `capcut harvest-enums [ \| --sync \| --add ] [--apply] [--catalogue ]` | no | Learn store resource ids into the per-user catalogue: from one draft, the whole library (--sync), or by hand (--add). | | `doctor` | `capcut doctor [--drafts ]` | no | Environment preflight (Node, whisper, API key, project dir). | | `diagnose` | `capcut diagnose [--bundle ]` | no | Inspect canonical draft files, divergence, and editor-write safety. | -| `fixture` | `capcut fixture --out [--check]` | no | Build a shareable, redacted compatibility bundle (timeline JSON only) for a version-support issue, including the mask-keyframe evidence report (#44). | +| `fixture` | `capcut fixture --out [--anonymize] [--check]` | no | Build a shareable, redacted compatibility bundle (timeline JSON only) for a version-support issue, including the mask-keyframe evidence report (#44). | | `sync-timelines` | `capcut sync-timelines [--nested] [--apply]` | yes | Reconcile drifted timeline mirrors (template-2.tmp, draft_info.json) from a read-only draft_content.json (plan with mtimes by default; --apply rewrites only the drifted mirrors). | | `restore` | `capcut restore [--step \| --list]` | yes | Undo writes from .bak / snapshot history (--step N, --list). | | `serve` | `capcut serve [--queue ] [options]` | no | Run a stateless JSONL job queue from stdin/--queue. | @@ -88,7 +88,7 @@ | `quickstart` | `capcut quickstart [--video ] [--audio ] [--srt ] [--drafts ] [--template auto\|bundled\|] [--ratio \| --width --height ]` | yes | One-command first draft: create + add one input + lint + print the open-in-CapCut step. | | `compile` | `capcut compile [--out \| --into ] [--template auto\|bundled\|] [--data ] [--check \| --plan]` | yes | Build a draft from a declarative JSON spec (the inverse of describe). | | `render` | `capcut render [--out ] [options]` | no | Render a low-res ffmpeg proxy preview (trim+speed+audio, --burn-captions); not CapCut's final render. | -| `loudness` | `capcut loudness [--target ] [--per-clip] [--max-gain-db ] [--apply]` | no | Measure each audible clip's loudness (EBU R128) and, with --target, set the gain that brings the export there. | +| `loudness` | `capcut loudness [--target ] [--per-clip] [--max-gain-db ] [--apply] [--tolerance ] [--max-true-peak ]` | no | Measure each audible clip's loudness (EBU R128) and, with --target, set the gain that brings the export there. | | `detect-scenes` | `capcut detect-scenes