Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,130 @@ All notable changes to capcut-cli are documented here. The format follows [Keep

## [Unreleased]

## [0.20.0] — 2026-08-21

Two threads run through this release. Subtitles now carry their styling across
the draft boundary in both directions — `export-ass` writes it out,
`import-ass` stops throwing it away. And the raw-recording pipeline closes:
find the speech (`detect-silence`), voice the script (`tts`), and the ffmpeg
fail-fast work from 0.19.0 is finished on the audio chain, with hardware
encoders selectable where the build has them. `diagnose` now captures, in
sanitized form, the evidence the two open store-layout questions are waiting
on. No command was removed and no existing flag changed meaning.

### Fixed

- **`render` no longer reaches ffmpeg's raw parser error when a base filter is missing** — the render chain applies `fps`/`scale`/`pad`/`setsar`/`format`/`concat`/`trim`/`setpts` to every segment unconditionally, but `probeFfmpegCapabilities` only checked `drawtext`/`overlay`/`libx264`, the flag-gated ones. A build missing one of the unconditional filters — reported on Remotion's bundled compositor ffmpeg binary, a minimal build that compiles in only an explicit `--enable-filter=` allowlist — reached `spawnSync` anyway and surfaced ffmpeg's own parse error verbatim (`No option name near '30'`, `Failed to set value '...' for option 'filter_complex': Invalid argument`), naming a fragment of the filter graph rather than the missing filter ([#89](https://github.com/renezander030/capcut-cli/issues/89)). The probe now checks all eight against the same `-filters` output already fetched, and `render` fails fast — before building the plan — naming exactly which filter(s) are missing and pointing at `--ffmpeg-cmd`, the same style as the existing "ffmpeg lacks drawtext" fallback message. `--dry-run` is unaffected by design: `cmdRender` routes it through `buildRenderPlan` directly rather than `renderDraft`, so a plan stays inspectable on a machine with no ffmpeg at all — the same reason the pre-existing `--dry-run` test already ran ungated on ffmpeg-less machines. Swapping `fps=` for an output-level `-r` is deliberately out of scope here (would change per-segment CFR normalization ahead of `concat`); this PR is the fail-fast fix only.

- **`render` probed the video chain but not the audio chain**
([#91](https://github.com/renezander030/capcut-cli/issues/91)). The audio
side of the render graph applies `atrim`/`asetpts`/`adelay` plus `anull`
(one audio segment) or `amix` (several) unconditionally, with
`atempo`/`volume`/`afade` joining when a draft carries a speed change,
volume or fades — none of them probed, so a minimal ffmpeg build missing one
still reached `spawnSync` and died on the same raw parser error class #89
eliminated for video. The probe now reads the audio filter names from the
same single `-filters` output (no extra spawn); `render` fails fast up front
for the unconditional five, and for the conditional three exactly when the
built plan uses them — refusing rather than silently dropping a retime or a
fade. A guard test builds a plan from a draft exercising speed, volume,
fades, captions, overlays and multi-track audio, extracts every filter name
from the generated `filter_complex`, and asserts each one is probed or
explicitly allowlisted — for both chains, so chain and probe cannot drift
apart again.
- **`import-ass` dropped every inline override tag.** A styled ASS file —
bold or italic spans, per-word colour, size changes — flattened to plain
text on import: the parser deleted `{...}` blocks wholesale and never read
`[V4+ Styles]`. Inline `\b`/`\i`/`\u`/`\c`/`\1c`/`\fs`/`\r` overrides now
become per-range styles through the same writer `text-ranges` uses (ranges
in UTF-16 code units of the stored text — the #85 rule), and the Dialogue's
referenced Style line seeds font size, colour and alignment unless flags
override them. Unknown tags (`\pos`, `\an`, karaoke `\k`, `\2c`–`\4c`) are
still stripped, now deliberately. The round-trip is pinned by test:
`export-ass` output re-imported reproduces the same `styles[]` arrays and
timings exactly.

### Added

- **`export-ass` — styled subtitles can finally leave a draft.** `export-srt`
stays bare by design; `export-ass <project>` writes the styling too: PlayRes
from the draft canvas, one `[V4+ Styles]` line per distinct text styling
(size, colour and alpha, bold/italic/underline, alignment, border, shadow
and background), one Dialogue per text segment, and `styles[].range` blocks
becoming inline override tags with explicit resets — ASS colour order is
`&HAABBGGRR`, tested in both directions. `--karaoke` emits `{\k}`
centisecond word timing from the same word timestamps the WebVTT karaoke
writer uses, with the highlight colour as PrimaryColour over the base
SecondaryColour. CapCut's border/shadow numbers pass through unscaled — they
are text-size-relative and ASS wants PlayRes pixels, and inventing a scale
would be worse than none (documented in the code).
- **`detect-silence` — the audio twin of `detect-scenes`.** ffmpeg's
silencedetect filter run deterministically over any media file, no draft
required: the silence spans and the complementary keep segments (the
speech), in seconds and draft-native microseconds, directly consumable by
`cut`/`compile` the way `detect-scenes` segments already are.
`--threshold-db` (default −30 dBFS) and `--min-silence` (default 0.5 s) map
onto the filter's noise/duration; `--pad` (default 0.1 s) shrinks every
silence span on both ends so a cut keeps a margin around speech and never
clips a word mid-syllable — a pad wider than a span makes the span
disappear, never a negative time. Silence running into end-of-file is an
open span, `--limit` keeps the N longest, and `--ffmpeg-cmd`/`--json`/`-H`
behave exactly as in `detect-scenes`, including the actionable no-ffmpeg
error.
- **`tts` — voiceover without leaving the CLI.** `capcut tts <project>
--text "..." --tts-cmd "<template>"` runs any local TTS tool (piper, macOS
`say`, espeak-ng — the missing-flag error carries working examples) and
lands the result as a real audio segment through the exact `add-audio`
path: same ffprobe duration probing with the same `--no-probe`/
`--ffprobe-cmd` escape hatches, same `--volume`/`--track-name`,
collision-safe `voiceover[-N].wav` naming. The template never passes
through a shell: `{out}` and `{text}` substitute as single argv tokens, and
a template without `{text}` gets the text on stdin instead. A tool that
exits non-zero or writes an empty file reports a bounded stderr tail and
cleans up after itself. `doctor` reports the configuration the way it
reports whisper.
- **`harvest-enums --sync` and `--add`.** `--sync` sweeps every draft the
`projects` listing can see (honouring `--drafts`) into one merged catalogue
write: unreadable or damaged drafts are skipped with a one-line note
instead of aborting, cross-draft repeats merge to one entry, the report
counts drafts scanned and skipped plus entries new and already known, and a
second sync adds nothing. `--add <kind> <slug> <resource-id>
[--effect-id <id>]` registers an entry whose witness draft is gone —
refusing unknown kinds, the deliberately excluded ambiguous kinds, unclean
slugs (suggesting the clean form) and duplicate ids (naming the entry that
owns them). Both keep the command's plan-by-default/`--apply` convention
and both funnel through the existing fsynced atomic catalogue writer.
- **`render --encoder`** picks the video encoder for proxy renders —
`h264_videotoolbox`, `h264_nvenc`, `h264_qsv`, anything the build
enumerates. Validation is lazy: only when the flag is given does `render`
spawn `ffmpeg -encoders`, failing fast with the encoder name and how to
list what is available; without the flag the ffmpeg invocation is
byte-identical to 0.19.1 (a test diffs the two argument arrays at exactly
one slot).
- **`diagnose` captures the nested-Timelines evidence #50 is waiting on**
([#50](https://github.com/renezander030/capcut-cli/issues/50)). When a
draft carries the nested `Timelines/` layout, the report — and the
`--bundle`/`fixture` output, through the existing #59 redaction rules —
attaches the `Timelines/project.json` pointer, the draft-file tree with
sizes and mtimes, the app version and OS marker, and a root-vs-nested
divergence comparison per nested document: content hashes, which side is
mtime-newer, per-track segment and text counts and a text hash — never raw
text. That is precisely the before/after that decides whether the nested
pointer is authoritative; the write-path question stays open until it
arrives. A non-nested draft's diagnose output is unchanged, byte for byte.
- **`diagnose` notes unregistered timeline media in `draft_meta_info.json`.**
Newer CapCut builds (reported on CapCut International 9.1.0, macOS) mark
every clip "file inaccessible" and demand per-clip relinking when the
sidecar's `draft_materials` does not register the timeline's media — even
with valid paths in `draft_content.json`, and this CLI has never written
`draft_materials`. `diagnose` now says so, read-only, when the condition
provably holds (sidecar missing, key missing, or every group empty while
the timeline references local media), and asks for the one thing that lets
the write side be built from evidence rather than guesswork: a
`capcut fixture` bundle from an app-authored draft on such a build — the
bundle already carries `draft_meta_info.json`. Nothing writes or invents
`draft_materials` content anywhere.

## [0.19.1] — 2026-08-16

One bug, reported with the measurement that settled it, plus the repair for
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ JSON in, JSON out: every command reads and writes the local draft store directly
- **Library** — `import { loadDraft, lintDraft, saveDraft } from "capcut-cli"` (typed, zero-dep)
- **Queue runner** — `capcut serve` reads JSONL jobs from stdin, for [n8n / Make / Coze](./examples/serve-automation.md)

> **New in v0.19.1:** every multi-range text highlight was being written past the end of the text it styled. `styles[].range` holds UTF-16 code units, not UTF-16LE bytes, so `text-ranges`, `caption --karaoke`, `--highlight-words` and any preset carrying `text_ranges` stored offsets twice as large as they should be — a plain `add-text` looked fine only because a full-span range clamps back to the end of the text ([#85](https://github.com/renezander030/capcut-cli/issues/85), measured by [@hillimited](https://github.com/hillimited) across 38 app-authored drafts). Fixed everywhere those offsets are read or written, and `lint --fix` repairs drafts written by earlier versions (`text-range-doubled`). Full details in the [changelog](./CHANGELOG.md).
> **New in v0.20.0:** subtitles now carry their styling across the draft boundary in both directions — `export-ass` writes `[V4+ Styles]`, per-range override tags and `--karaoke` word timing, and `import-ass` keeps inline bold/italic/colour/size spans instead of flattening them to plain text, pinned by a round-trip test. The raw-recording pipeline closes: `detect-silence` finds dead air (with a `--pad` so no word gets clipped mid-syllable) and `tts` voices a script through any local TTS tool (piper, `say`, espeak-ng) straight onto an audio track. `render` finishes 0.19.0's fail-fast work — the audio filter chain is probed like the video chain ([#91](https://github.com/renezander030/capcut-cli/issues/91)) — and `--encoder` unlocks hardware encoders. Plus `harvest-enums --sync`/`--add` for the whole library, and `diagnose` now captures sanitized evidence for the open store-layout questions ([#50](https://github.com/renezander030/capcut-cli/issues/50)). No command was removed and no existing flag changed meaning. Full details in the [changelog](./CHANGELOG.md).

> **New in v0.19.0:** a long `render` no longer fails with `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` on a draft that was fine, and a render that does fail now names the missing decoder, encoder or filter instead of dumping ffmpeg's output; `render --progress` streams progress so a ten-minute job stops looking hung. `lint` gained three checks — caption reading speed (`--max-cps`), vertical safe-area (`--safe-area`) and speed consistency — and `lint --fix` can finally re-wrap CJK captions without disturbing per-character styling. Plus `export-timeline` no longer rounds a sub-half-frame clip to a zero-length OTIO clip ([#82](https://github.com/renezander030/capcut-cli/issues/82)), and the keyframe docs now name the `property_type` the code actually writes ([#80](https://github.com/renezander030/capcut-cli/issues/80)). No command was removed and no existing flag changed meaning. Full details in the [changelog](./CHANGELOG.md).
> **New in v0.19.1:** every multi-range text highlight was being written past the end of the text it styled. `styles[].range` holds UTF-16 code units, not UTF-16LE bytes, so `text-ranges`, `caption --karaoke`, `--highlight-words` and any preset carrying `text_ranges` stored offsets twice as large as they should be — a plain `add-text` looked fine only because a full-span range clamps back to the end of the text ([#85](https://github.com/renezander030/capcut-cli/issues/85), measured by [@hillimited](https://github.com/hillimited) across 38 app-authored drafts). Fixed everywhere those offsets are read or written, and `lint --fix` repairs drafts written by earlier versions (`text-range-doubled`). Full details in the [changelog](./CHANGELOG.md).

## Install

Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ JSON 进、JSON 出:每个命令都直接读写本地草稿存储,不用 MCP
- **库(Library)** —— `import { loadDraft, lintDraft, saveDraft } from "capcut-cli"`(带类型、零依赖)
- **队列执行器** —— `capcut serve` 从 stdin 读取 JSONL 任务,对接 [n8n / Make / Coze](./examples/serve-automation.md)

> **v0.19.1 新增:** 此前每一处多区间文字高亮都被写到了文本末尾之外。`styles[].range` 存的是 UTF-16 码元(code unit)而非 UTF-16LE 字节,因此 `text-ranges`、`caption --karaoke`、`--highlight-words` 以及任何带 `text_ranges` 的预设,写入的偏移量都是应有值的两倍——普通的 `add-text` 看起来正常,只是因为整段区间会被裁回文本末尾([#85](https://github.com/renezander030/capcut-cli/issues/85),由 [@hillimited](https://github.com/hillimited) 在 38 个由 App 创建的草稿上实测得出)。所有读写该偏移量的位置均已修正,`lint --fix` 可修复旧版本写出的草稿(`text-range-doubled`)。详见[更新日志](./CHANGELOG.md)。
> **v0.20.0 新增:** 字幕样式现在可以双向跨越草稿边界——`export-ass` 写出 `[V4+ Styles]`、逐区间覆盖标签与 `--karaoke` 逐词计时,`import-ass` 则保留内联的粗体/斜体/颜色/字号区间,不再压平为纯文本,并有往返测试兜底。原始录音的处理链路就此闭环:`detect-silence` 找出静音段(`--pad` 留出余量,不会把词切在半个音节上),`tts` 可通过任意本地 TTS 工具(piper、`say`、espeak-ng)把文稿直接配音到音频轨上。`render` 补完了 0.19.0 的快速失败工作——音频滤镜链与视频链一样接受预检([#91](https://github.com/renezander030/capcut-cli/issues/91))——并新增 `--encoder` 以启用硬件编码器。另有 `harvest-enums --sync`/`--add` 支持整库扫描与手工登记,`diagnose` 现在能为悬而未决的存储布局问题采集脱敏证据([#50](https://github.com/renezander030/capcut-cli/issues/50))。没有删除任何命令,现有参数含义均未改变。详见[更新日志](./CHANGELOG.md)。

> **v0.19.0 新增:** 长时间的 `render` 不再在本身正常的草稿上以 `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` 失败;渲染确实失败时,现在会直接指出缺失的解码器、编码器或滤镜,而不是抛出一整段 ffmpeg 输出;`render --progress` 会实时输出进度,十分钟的任务不再看起来像卡死。`lint` 新增三项检查——字幕阅读速度(`--max-cps`)、竖屏安全区(`--safe-area`)与变速一致性;`lint --fix` 终于可以在不破坏逐字样式的前提下为中日韩字幕换行。此外,`export-timeline` 不再把不足半帧的片段导出为零长度的 OTIO 片段([#82](https://github.com/renezander030/capcut-cli/issues/82)),关键帧文档也改为标注代码实际写入的 `property_type`([#80](https://github.com/renezander030/capcut-cli/issues/80))。没有删除任何命令,现有参数含义均未改变。详见[更新日志](./CHANGELOG.md)。
> **v0.19.1 新增:** 此前每一处多区间文字高亮都被写到了文本末尾之外。`styles[].range` 存的是 UTF-16 码元(code unit)而非 UTF-16LE 字节,因此 `text-ranges`、`caption --karaoke`、`--highlight-words` 以及任何带 `text_ranges` 的预设,写入的偏移量都是应有值的两倍——普通的 `add-text` 看起来正常,只是因为整段区间会被裁回文本末尾([#85](https://github.com/renezander030/capcut-cli/issues/85),由 [@hillimited](https://github.com/hillimited) 在 38 个由 App 创建的草稿上实测得出)。所有读写该偏移量的位置均已修正,`lint --fix` 可修复旧版本写出的草稿(`text-range-doubled`)。详见[更新日志](./CHANGELOG.md)。

## 安装

Expand Down
Loading