Skip to content

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

Open
Phelim2023315 wants to merge 3 commits into
renezander030:masterfrom
Phelim2023315:fix/skill-scripts-portability
Open

Phelim2023315 wants to merge 3 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants