Skip to content

Fix: portable skill scripts + two read-path bugs found on a JianYing 9.0.0 host - #149

Merged
renezander030 merged 6 commits into
renezander030:masterfrom
Phelim2023315:fix/skill-scripts-portability
Oct 19, 2026
Merged

renezander030 merged 6 commits into
renezander030:masterfrom
Phelim2023315:fix/skill-scripts-portability

Conversation

@Phelim2023315

Copy link
Copy Markdown
Contributor

Three commits, all from using the CLI on a Windows host where the installed app
is 剪映专业版 (JianYing) 9.0.0:

Commit Scope
fix(skills): make the recipe scripts work outside a repo checkout skills/capcut-edit/scripts/* (2 changes)
fix(caption): resolve the Whisper binary the same way doctor does src/whisper.ts, src/caption.ts, src/doctor.ts
fix(segments): tolerate a segment without target_timerange.start or speed src/index.ts

The first commit generalises what a Windows host had to patch locally to use the
skill at all; the other two are bugs the same host surfaced. No behaviour changes
on a normal checkout, no new runtime dependency.

1. anim.sh hardcoded the macOS effect-cache path

const EFFECT_CACHE_BASE = path.join(
  process.env.HOME,
  "Library/Containers/com.lemon.lvoverseas/Data/Movies/CapCut/User Data/Cache/effect"
);

On Windows that string is written into the animation material's path field, so
the effect it references has no cache the app can resolve. Resolved per platform
instead:

Platform Base
win32 %LOCALAPPDATA%\CapCut\User Data\Cache\effect (falls back to …\JianyingPro\…, preferring whichever exists)
darwin the existing container path, unchanged
other ~/.local/share/CapCut/Cache/effect

CAPCUT_EFFECT_CACHE overrides all three, which also makes the value testable.

2. The three recipe scripts computed the CLI entry from the repo layout

cli="$script_dir/../../../dist/index.js"

That is only correct inside a checkout. Used as an installed plugin — or copied
anywhere else, which is how a skill actually reaches a user — all three fail with
capcut CLI not found. They now resolve in this order:

  1. $CAPCUT_CLI (explicit path, for wrappers and CI)
  2. the repo build ($script_dir/../../../dist/index.js) — unchanged, and still
    the fallback, so a plain checkout behaves exactly as before
  3. an installed copy, node_modules/capcut-cli/dist/index.js, walking up from
    the script
  4. capcut-cli / capcut on PATH

The error message now names the ways to satisfy it instead of only the repo-relative
path.

Testing

npm test          # full suite: 1218 tests
npm run lint      # biome, clean

New coverage, all driven through the existing helpers:

  • test/skill-cli-resolution.test.mjs — a stub CLI proves $CAPCUT_CLI is
    preferred; a non-existent $CAPCUT_CLI falls through to the repo build (the
    real CLI then writes the keyframes); and with no override at all the
    repo-relative path keeps working.
  • test/anim-effect-cache.test.mjs — CAPCUT_EFFECT_CACHE is honoured and the
    written path is <cache>/<effect_id>/<md5>; on win32 the written path is a
    Windows cache base and never the macOS container.
  • test/whisper-resolution.test.mjs — the resolver takes an injectable PATH
    probe, so preference order, the whisper.cpp-only case, the faster-whisper
    fallback and the nothing-installed case are asserted without depending on the
    test host; --whisper-cmd still wins for dialect detection.
  • test/segment-table-missing-fields.test.mjs — the fixture with start and
    speed deleted renders no NaN/undefined in -H and reports the 0/1
    defaults in JSON.

Targeted run of the touched areas (4 new files + keyframe-easing,
doctor, caption): 49/49 pass.

Commit 2 — caption and doctor disagreed about Whisper

doctor probed whisper / whisper-cli / faster-whisper; caption only ever
spawned whisper, and defaulted its dialect to openai. On a host whose only
Whisper is whisper.cpp's whisper-cli.exe (the name that build has shipped since
v1.7.4), doctor printed [✓] whisper and the next caption run died with
ENOENT, hinting at --whisper-engine rather than at the command name.

Both now share resolveWhisperCommand() in a new src/whisper.ts, which also
owns the PATH lookup doctor used to define privately. Dialect detection reads
the resolved command, so a resolved whisper-cli selects the whisper-cpp
invocation — the second half of the same bug. --whisper-cmd and
--whisper-engine still win.

Closes #146.

Commit 3 — segments printed NaN:00NaN for an app-shaped segment

segmentData read target_timerange.start / .duration and speed directly
off a type that declares them required, so a segment that omits start (its 0
default) and speed rendered as:

ID        Type   Start   -End         Dur   Spd  Label
62a041c7  text   NaN:00NaN-NaN:00NaN     3.00s  undefinedx  smoke-test

Measured on a JianYing 9.0.0 Windows project (the capture is now
test/fixtures/jianying-9-0-0-windows/). The fix is on the read side
deliberately: both writers reachable in isolation write start — add-text at
start=0 writes {"start":0,...}, and init seeds an empty draft — so nothing
here guesses at a writer that omitted it. start_us, duration_us and speed
fall back to 0, 0, 1.

Closes #147.

Deliberately not included

Windows also found that --force-write has to be passed on hosts whose
JianYing/Premiere app declares a version inside the CLI's refuse band. That is a
per-host safety decision, and the repo's own invariant is "never pass
--force-write unless the user explicitly accepts overwriting changed-on-disk
state" — changing the scripts' default would trade away the guard for everyone.
It stays a local policy (CAPCUT_FORCE_WRITE=0 to restore), so nothing here
touches the default.

Environment note

CHANGELOG.md gained entries under ## [Unreleased]; no package.json changes,
no new runtime dependency.

Phelim added 3 commits October 12, 2026 17:22
The four skill scripts assumed they lived inside a capcut-cli checkout:

- `anim.sh` hardcoded the macOS effect-cache container path. On Windows that
  string is written into the animation material's `path`, where the app cannot
  resolve it, so the animation has no effect cache. Resolve the base per
  platform instead — `%LOCALAPPDATA%\CapCut\User Data\Cache\effect` (JianYing:
  `%LOCALAPPDATA%\JianyingPro\...`) on win32, the container on darwin,
  `~/.local/share/CapCut/Cache/effect` elsewhere — with `CAPCUT_EFFECT_CACHE`
  as an override.
- `ken-burns.sh`, `stamp-cta.sh` and `long-to-short.sh` computed the CLI entry
  as `$script_dir/../../../dist/index.js`. That is only correct inside a
  checkout: a skill installed as a plugin (or copied anywhere else) failed with
  "capcut CLI not found". They now resolve by priority `$CAPCUT_CLI` → the repo
  build → an installed `node_modules/capcut-cli/dist/index.js` (walking up from
  the script) → `capcut-cli`/`capcut` on `PATH`, keeping the repo-relative build
  as the fallback so existing checkouts behave exactly as before.

Tests cover the resolution order (a stub CLI proves `$CAPCUT_CLI` wins, and that
a missing `$CAPCUT_CLI` falls through to the repo build) and the effect-cache
base (`CAPCUT_EFFECT_CACHE` is honoured; on win32 the written path is a Windows
cache base, never the macOS container).

SKILL.md documents the resolution order, both environment variables and the
JianYing Windows draft store.
`doctor` probed three dialect names — `whisper`, `whisper-cli`,
`faster-whisper` — but `caption` only ever spawned `whisper`, and when the
command was not given explicitly it also assumed the openai dialect.

Reproduced on a Windows host whose only Whisper is whisper.cpp's
`whisper-cli.exe` (the name that build has shipped since v1.7.4, and the one
Homebrew's whisper-cpp installs): `capcut doctor` reported `[✓] whisper`, and
the next `caption` run died with ENOENT — with a hint suggesting
--whisper-engine openai|whisper-cpp|faster-whisper, i.e. leaving the reader to
work out that the default command name was the problem.

Both now share `resolveWhisperCommand()` in the new `src/whisper.ts`, which
also owns the PATH lookup `doctor` used to define privately. An explicit
`--whisper-cmd` and an explicit `--whisper-engine` still win over detection.
Dialect detection reads the resolved command, so a resolved `whisper-cli`
selects the whisper-cpp invocation instead of the openai one — the second half
of the same bug.

The resolver takes an injectable PATH probe, so the test asserts the preference
order, the whisper.cpp-only case, the faster-whisper fallback and the
nothing-installed case without depending on what the test host has installed,
and pins that `--whisper-cmd` still wins for dialect detection.
…peed

`TargetSegment.start` and `.duration` are typed as required, so `segmentData`
read them directly and `formatTime(undefined)` rendered `NaN`. Measured on a
JianYing 9.0.0 (Windows) project that the app opens and edits normally, where
`capcut segments <project> -H` printed:

    62a041c7  text   NaN:00NaN-NaN:00NaN   3.00s  undefinedx  smoke-test

for a segment whose `target_timerange` is `{"duration":3000000}` and which has
no `speed` — the app drops the 0 default, and `speed` is absent from segments
of that shape. The same `undefined` also reached the JSON result as
`"start_us": null`.

Both fields now fall back to their 0 / 1 defaults in `segmentData`, which is
the single source for the `segments` table, the JSON result and the text-only
table. The fix is deliberately on the read side: the two writers reachable in
isolation were checked and both write `start` — `add-text` at `start=0` writes
`{"start":0,...}`, and `init` seeds an empty draft — so nothing here guesses at
a writer that omitted it.

The test drives the fixture through `spawnCli`, deleting `start` and `speed`
first, and asserts the human table contains no `NaN`/`undefined` while the JSON
result reports the defaults.
@renezander030

Copy link
Copy Markdown
Owner

Thanks for putting this batch together! The Whisper and timing fixes look good. Before merge, please adjust the PATH fallback: node "$cli" fails for shell launchers, so it needs a separate launch path and a test with the skill outside a checkout. CI still needs to run too.

@renezander030

renezander030 commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

@Phelim2023315 Following up on the PATH launcher fix and the test outside a checkout requested above. The current branch also has merge conflicts with master, and CI has not run on this head yet.

I’m picking up the remaining integration work from the maintainer side so these fixes can be merged. Thanks again for contributing the Whisper and timing fixes.

Maintainer update: Completed the launcher fix and regression tests outside a checkout, resolved the conflict with master, and corrected the Windows test helper. All 13 CI jobs passed, including Linux, macOS and Windows. Local validation also passed: 1,281 tests, one skipped, lint clean, and all 32 wrapper smoke checks. Merged as e52f47e. Thanks again for the contribution.

@renezander030
renezander030 merged commit e52f47e into renezander030:master Oct 19, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants