* feat(cli): scope check to a time range or a set of components
* fix(cli): keep ranged check gates truthful
Refuse ranged caption and frame gates when none of their seek times
falls inside the requested range. Make ranged motion reporting,
overlap timestamps, filtering tests, and scope documentation match.
A GSAP tween that outlives a later tween on the same target and property by a
moment takes the property back for that tail and leaves its own end value on
screen. A banner word whose drift ended 20 ms after its exit stayed in the
middle of the frame for the rest of the scene, in playback and in renders.
hyperframes check now reads every tween off the live timelines and warns on
that shape (tail up to 0.25 s), with the fix: end the earlier tween first, or
add overwrite: "auto" to the later one. Longer tails, like a slow drift that
resumes after a short move, are left alone.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(inspector): query timelines and inspect the live preview
* fix(inspector): align live timeline contracts
Keep realm-safe target detection through the restack and align
floating-point interval boundaries. Update the live group fixture and
make the private runtime entry build itself before use.
* fix(inspector): declare generated build inputs
* fix(engine): encode and tag ProRes mov output as BT.709
* test(engine): read the ProRes colour tags from the first frame
* test(engine): end the ProRes tag probe's options with --
* fix(media-use): keep frame precision on transcript-cut seek boundaries
Fixes#5416.
A kept range whose boundary sits on a frame got rounded to the millisecond
twice on its way to ffmpeg, and at 29.97/30/60 fps that is enough to land just
past the frame: frame 386 at 30 fps is 12.866666 s, and -ss 12.867 starts that
segment on frame 387 instead. ffmpeg's accurate seek drops the frame, so the cut
came out a frame short from that join on and the picture drifted behind the
sound.
cutlist.mjs rounded the emitted range bounds to 3 decimals, so fmt() in
transcript-cut.mjs never saw more precision than that in the first place; fixing
only fmt() leaves the bug in place. Both now keep frame precision -- 6 decimals
in the cut list, full precision for the -ss/-to actually handed to ffmpeg --
while the 3-decimal fmt() is kept for the times that are only printed.
Measured at 30 fps: a cut of 0-1,1.0666666-3 produced 87 frames before and 88
after (frames 0-29 plus frames 32-89), with no timestamp gap at the join. 25 fps
is unaffected, as the issue notes.
* test(media-use): end ffprobe options with -- in transcript-cut test
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: James <james.russo@heygen.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(media-use): honor telemetryEnabled: false from shared config in optedOut
The CLI's 'hyperframes telemetry disable' writes telemetryEnabled: false to ~/.hyperframes/config.json, but the media-use scripts' optedOut() only checked env vars, so transcribe.mjs, transcript-cut.mjs and audio-duck.mjs kept sending PostHog events for users who had opted out through the CLI.
optedOut() now also returns true when the shared config has telemetryEnabled === false, reusing the readSharedConfig() helper already in the same file. Applied byte-identically to both copies of telemetry.mjs (skills/media-use/scripts/lib and packages/cli/src/media-use/lib, per the copy-parity rule), with tests in both test files.
Closes#5465
* test(media-use): sandbox HOME in the telemetry opt-out test and regenerate the skills manifest
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: James <james.russo@heygen.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(cli): add hyperframes/api entry with transcribe and removeBackground
HyperFrames Desktop runs the CLI as a child process, relaunching Electron
with ELECTRON_RUN_AS_NODE=1. A programmatic entry lets it import plain
functions and run them in a utilityProcess instead, so the RunAsNode fuse
can be turned off.
packages/cli/src/api.ts exports transcribe() and removeBackground(): typed
options in, typed result out, progress through callbacks, failures thrown
as TranscribeError / RemoveBackgroundError with a code and the original
error as cause. Neither prints, reads argv or exits.
The transcribe and remove-background commands now keep only flag
handling, printing and exit codes, and call these functions. Their
output, JSON and exit codes are unchanged.
The package gains an exports map with "./api" (built dist plus d.ts) and
"./package.json", generated from package-subpaths.json like the sibling
packages.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): trim api.ts comments under the package's comment share
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): trim trailing dots without a quadratic regex
CodeQL flags the trailing-dots regex on the Parakeet error now that it sits
behind a library entry. Same result, linear time.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): make the transcribe API's signal stop every stage
Review follow-ups on the api entry:
- parakeet-mlx now runs as an async child that the caller's signal can
kill. Its failures keep the synchronous run's message, status and stderr.
- A pre-aborted signal throws before any setup. Whisper checks the signal
before each setup stage, and the model download gets it.
- onEngine reports the model whisper runs (small for Spanish, not small.en).
The resolver moved to whisper/modelForLanguage.ts so both share it.
- api/transcribe.ts imports everything at module scope. The command test
hoists its whisper mock, since that module now loads during imports.
- whisper's internal TranscribeOptions/TranscribeResult types are renamed
WhisperOptions/RunnerResult, so the API's public names are unique.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): let an AbortSignal stop whisper's install and audio prep
Review follow-ups on the api entry:
- whisper's runtime install (brew, git clone, cmake) and its ffmpeg audio
prep run through a new stoppableExecFile when an API caller passes a
signal. The child runs off the event loop, an abort ends it and every
process it started, and the call rejects with a "cancelled"
TranscribeError once they are gone. A failure throws the same error
execFileSync throws (message, code, errno, status, signal, stdout,
stderr), compared field by field in stoppableExecFile.test.ts.
parakeet-mlx uses the same helper.
- Without a signal the helper is execFileSync, and the CLI passes none, so
the CLI output stays byte for byte the same as hyperframes 0.8.146.
- TranscribeOptions.signal now says what it covers: a tool lookup that is
already running (capped at 10 seconds) finishes first.
- New regressions in api.test.ts abort during a stand-in install step and
during a stand-in ffmpeg convert. Both end in under 10 seconds with no
process left, and both fail with the old synchronous calls.
- Tests load every module at the top of the file: namespace spies replace
the importOriginal partial mocks, and node:fs and node:child_process are
spied through their default exports and synced with
syncBuiltinESMExports. remove-background --info imports its manager
statically. The scanner in api.test.ts now reads every file this PR
touches and catches importOriginal and vi.importActual.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): media-treatment applies one treatment to every matched image or video
* fix(cli): media-treatment reports its action from every target and documents the per-element merge
* fix(cli): media-treatment patches each match by its stable id so one write cannot redirect another
* fix(cli): media-treatment sets every match in one parse instead of re-finding elements
* fix(cli): media-treatment refuses media that is not an HTML element, as before
* chore(skills): refresh the media-use manifest hash for the treatment reference edit
* fix(cli): the browser download unpacks on machines without an unzip command
* fix(cli): a corrupt browser download on the yauzl path is retried and named
* revert(cli): drop the cause-chain reader; @puppeteer/browsers never passes the cause through
* fix(cli): check warns when a video's browser-playable copy cannot be made
* fix(cli): big projects stop failing proxy prep, and a failed copy is reported once
* fix(cli): proxy failures name the real ffmpeg error, and a broken ffmpeg is reported once
* chore(studio-server): shorten the batch proxy resolver doc comment
* fix(cli): history reads answer while another app has the project open
* refactor: Fewer comment lines in the history read path
* refactor: Keep a history comment within the line limit
* fix(core): preview plays a split sub-composition's videos from its in-point
* fix(cli): timeline shows a split sub-composition's clips from its in-point
* fix(core): split halves cut nested media and keep each clip's loop, ramp and fades
* fix(core): a scene's media plays past a shorter root animation again
* fix(core): only a host with an in-point shifts or cuts what it holds
* test(core): a split half with no id stops at its own end
* fix(core): the playhead and Studio's clip list skip what an in-point cuts
* fix(core): keep clips under a cut-away nested host nested in studio
* refactor(core): name why a cut-away nested host stays listed
* fix(core): a sub-composition timeline with a non-function duration no longer aborts the runtime
* fix(core): every registered-timeline duration read survives a partial or throwing entry
* fix(core): a runtime that fails to start reports its real error instead of a half-built page
* fix(core): a timeline post that throws after start-up is named too, and a later good post clears it
* fix(cli): check skips a custom clock whose time() returns null instead of reading it as stuck at 0
* fix(core): a recorded runtime start-up error stays set instead of being cleared by a later post
* fix(core): the runtime failure message says what to fix
* fix(core): the runtime failure is recorded even when the thrown value cannot be printed
* chore(core): format and regenerate the position-edits bundle for the startup error recorder
* fix(cli): keyframes and validate close the browser when the runtime fails to start
* fix(cli): preview --background reports a child that exits instead of waiting it out
* fix(cli): a preview that cannot listen exits so its launcher reports why
* fix(cli): keep preview's comment share within budget
* test(cli): the start failure test uses a project path outside the temp dir
* fix(studio): text in Studio reads without em or en dashes
* fix(studio): the dash check also catches HTML entities, and peak reads Unavailable
* test(studio): the dash check test covers every entity form
* fix(studio): messages Studio shows from lint, sdk, core and the renderer drop dashes too
* fix(producer): the gate check stops early on the engine's new fallback wording
* perf(packages): ship npm packages without source maps and guard their size in CI
No package or consumer reads the published source maps, so they are no
longer built. The CLI copies only the studio app it serves instead of the
whole studio dist. verify:packed-manifests now holds every published package
to a size budget that ratchets down, and CI rejects pull requests that add a
large binary outside LFS.
* fix(ci): large-binary check reads every filename and fails on git errors
* ci: run only Build when a skill bundled into the CLI changes
* fix(ci): size guards also catch type changes, odd file names, unreadable blobs and bad budgets
* fix(ci): large-binary check reads blobs by id and skips submodules
* fix(ci): exempt only exact registry copies under docs/public/catalog
* test(ci): cover the staged catalog-copy exemption; drop the local-only override hint in CI
* fix(ci): producer budget follows its slimmed package; size checks name their real cause
* fix(cli): list valid families when media-treatment gets an unknown capability
* test(cli): the capability list test typechecks under strict index access
* fix(cli): the unknown capability error calls its list areas and claims only what holds
* fix(cli): the unknown capability error lists only the areas it accepts
* fix(producer): the npm package ships only producer's own code, without source maps
* fix(cli): the npm package leaves out Studio's source maps, like its own
* fix(producer): the published package finds its workers and pins its own packages exactly
* fix(skills): skill scripts run when the skills dir is a symlink
* test(skills): plugin launcher fixture ships the shared main-module helper like an install
* fix(skills): the CLI's cube validator runs through a symlink too, and the guard check is stricter
* docs(skills): long agent-started renders run detached in their own process group
* docs(cli): detached renders start in their own process group, on macOS too
* docs(cli): the detached render recipe never waits on terminal input
* feat(studio): export can encode on the GPU, like render --gpu
* test(studio): the 4K export assertion carries the GPU flag
* refactor(studio): the GPU flag needs no comment beyond its name
* fix(studio-server): only MP4 exports encode on the GPU, and render errors report it
* fix(studio): the disabled GPU option dims like the other export controls
* fix(cli): check passes a still title card whose timeline follows the seek
* fix(cli): check warns on a still frame and fails only when an animation is stuck
* fix(cli): count a seek clock once per sample and point the error at the registered timeline
* fix(cli): judge a finished animation by its own completion state, not its end time
* test(cli): pin a Web Animation paused at its end as finished
* fix(cli): judge a reversed animation's end in its own direction, and read plain timelines
* test(cli): pin a plain timeline held at its end as finished
* test(cli): keep an unfinished plain timeline beside the finished one
* fix(cli): skip a timeline whose time cannot be read instead of calling it stuck
* fix(cli): skip an animation whose time is not a plain number, such as a scroll-driven one
* fix(cli): a partial timeline with plain values no longer stops the browser checks
* fix(cli): snapshot, check, layout and thumbnails honour software WebGPU like render
* test(cli): stub the local WebGPU helper in the studio server tests
* fix(engine): webgpu compositions are recognised after the bundled runtime script
* fix(engine): a hung GPU probe falls back to software in 15 s instead of 120 s
* fix(engine): the GPU probe keeps the configured launch timeout and bounds only its page
* build(parsers): declare the html-attribute-spans subpath in its descriptor
* build(gcp-cloud-run): the BeginFrame contract stage copies parsers, which the engine now imports
* test(cli): the screenshot launch pin follows the shared local WebGPU helper
* fix(engine): the GPU probe's wait for Chrome's first page falls inside its 15 s limit
* feat(cli): let transcribe --to write srt or vtt straight from a media file
* fix(cli): fail a missing transcribe --output folder before decoding
* fix(cli): reject a transcribe --output that names a folder
* test(cli): check CJK caption grouping on the parsed cues
* fix(cli): keep transcribe --output from overwriting the media or its transcript
- Refuse an --output that resolves to the input file or the transcript.json the run writes,
before the engine starts.
- When the caption file cannot be written after transcribing, say the transcript was saved
and name the caption path and reason, instead of reporting a failed transcription.
- Docs: a one-run export groups cues for languages without spaces; exporting the saved
transcript later may split them differently.
* fix(cli): refuse caption paths that alias the media or transcript
- The overwrite check compares files, not path text: an existing target is matched by device
and inode, a new one by its real folder and its name ignoring letter case. Links, folder
aliases such as /var and /private/var, and other letter case are all refused.
- The check also covers the default caption path, so exporting transcript.srt to srt no
longer rewrites its own input.
- A trailing -o with no path fails before transcribing.
- One constant names transcript.json for the engines, the import and the check.
* fix(cli): refuse caption links to the transcript before it exists
- A caption path that is a link is followed to where the write would land, so a link to the
transcript.json the run is about to create is refused before transcribing.
- A test pins the folder letter-case check on a case-insensitive disk, and says which branch ran.
* fix(cli): check the caption file against the transcript right before writing it
- Right before the caption write, by when the transcript exists, a destination that is the
same file as the input or transcript.json is refused. The OS resolves every link, relative
link and alias, so the hand-written link following is gone.
- The run keeps the transcript and says the caption file could not be written and why.
An export whose caption write fails now says so too, instead of crashing.
- The check before transcribing stays as an early refusal for the common cases.
* fix(cli): compare caption paths by name when the disk reports no inode
- Some network shares report inode 0 for every file; such a pair is compared by folder and
name instead of being called the same file. The check before transcribing and the one at
write time now share one comparison.
- The link-through-a-file tests accept ENOENT as well as ENOTDIR, the code Windows reports.
* fix(cli): resolve both caption paths fully when the disk reports inode 0
- When either file reports inode 0 and both exist, compare their fully resolved real paths,
so a link to the media or the transcript is still refused. The folder and name compare
stays for a path that does not exist yet.
- Tests zero the inode on every path, on the caption path only, or on the protected file only.
* test(cli): say plainly why the caption-write test accepts two error codes
* fix(cli): reject a swallowed --json when catalog's string flags have no value
catalog --query --json (missing --query value) parsed as query="--json",
json=false, since citty's parser (node:util.parseArgs, strict: false)
unconditionally consumes the next token as a string arg's value with no
guard against it being another flag -- JSON-mode consumers silently got
human-readable stdout instead of a clear error.
Added an opt-in rejectSwallowedFlagValues() check (packages/cli/src/utils/
reject-unknown-flags.ts), called from catalog's own run() for its
query/type/tag string args. Kept it opt-in rather than wiring it into the
global assertKnownFlags pre-parse gate every command goes through: check.ts's
--frame-check is a string flag that's documented and tested to work bare or
immediately followed by another flag, handled by that command's own raw-args
preprocessing before citty parses it -- a blanket pre-parse check can't tell
that case apart from this bug, so review caught it as a real regression
before it shipped.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): unify swallowed-flag-value handling into one raw-args gate
catalog --query --json (round 1's fix) turned out to be one instance of a
codebase-wide gap: any command with a type:"string"|"enum" flag followed by
--json (or any other declared flag) has its value swallowed by citty's parser
(node:util.parseArgs, strict: false unconditionally consumes the next token),
confirmed still reproducing on check --layout --json. Round 1's post-parse,
catalog-only check also duplicated check.ts's own existing --frame-check
raw-args mechanism instead of replacing it, and couldn't distinguish the
always-legitimate --query=--json (equals form) from the actual --query --json
swallow, since node:util.parseArgs parses both to the same object.
Replaced with a single raw-args scan (guardSwallowedFlagValues), wired into
the global wrapCommand gate next to assertKnownFlags so every command is
covered. Default: throw CliUsageError. Two narrow per-command opt-outs where a
command already has its own bespoke handling for this exact bug shape:
check's --frame-check (rewritten to --flag=, folding in and removing check.ts's
own normalizeFrameCheckRawArgs/swallowedOptionError) and upgrade's --project
(left untouched, since it already recovers the swallowed value itself). A
third, previously-undiscovered collision surfaced during testing: init.ts had
its own bespoke --example dash-value guard that the new global gate now
preempts; folded that in too and updated its one affected test's expectation.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): stop the swallowed-flag usage error from leaking to stdout
Round 2's guardSwallowedFlagValues threw CliUsageError without
presented:true, the first non-presented CliUsageError construction in the
CLI -- executeCli's dead stdout-usage-dump branch (every other CliUsageError
goes through failUsage(), which always sets presented:true) fired for the
first time, leaking 1.4-5.7KB of ANSI usage text onto stdout on top of the
correct stderr message, breaking --json callers' clean-stdout contract on
exactly the class of error this ticket exists to fix.
Split into two functions instead of one, since the two throw sites have
genuinely different reach: throwSwallowedFlagError (used by
guardSwallowedFlagValues, thrown before any command's own run() executes)
prints the message itself and sets presented:true, matching every other
print-then-failUsage() usage-error site in this codebase. swallowedFlagUsageError
(used by check.ts's parseFrameCheck) is a plain, side-effect-free CliUsageError
factory, because that throw is always caught by check.ts's own run() try/catch,
which already prints and presents every error uniformly -- using the printing
variant there instead produced a real, verified regression (a doubled stderr
line in non-JSON mode, an extra stderr line even in --json mode), caught by
adversarial review before it shipped.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* test(cli): assert literal stdout bytes stay empty for the swallowed-flag error
The prior test for this only asserted the internal presented:true flag, with
a comment noting the actual 0-byte-stdout behavior was checked by hand
against the built CLI. Added an automated spawn-based test (same pattern as
init.test.ts's runInit) that runs the real CLI and asserts stdout is
literally empty, not just that the presented mechanism is engaged.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* refactor(cli): split the swallow guard into small helpers to clear the audit gate
Moves the leaf flag checks out of the wrapCommand run arrow, drops ignored args
from the owner map, and pulls the per-token decision out of the guard loop.
Trims two comments to keep check.ts under its comment share. No behaviour change.
* fix(cli): swallow guard hints a value and lets --no- negations through
* fix(cli): swallow guard looks past --no- negations that citty drops
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* fix(lint): check passes valid sub-compositions and scene-relative asset paths
* fix(lint): keep the browser lint build free of node modules
* refactor(lint): tidy sub-composition lint helpers and guard bare node imports
* fix(lint): resolve stylesheet asset paths from the stylesheet's own folder
* fix(lint): catch every written form of a path that climbs out of the project
* fix(parsers): sub-composition asset paths written with backslashes load in preview
* fix(parsers): an encoded backslash stays in the file name and a leading backslash stays absolute
* fix(lint): an encoded %2f counts as a separator when checking for a climb out of the project
* fix(cli): renders on Windows stop cancelling when an old parent's pid is reused
* test(cli): a Windows parent created in the same tick as its child stays an ancestor
* fix(cli): name the unzip errors when every Windows unzip tool fails
* fix(cli): a cached empty or folder Chrome counts as missing, not found
* fix(engine): skip an empty cached Chrome the same way the CLI does
* fix(engine): keep scanning the Chrome cache past a build it cannot read
* test(engine): a folder at the cached Chrome path is skipped
* fix(cli): preserve subtitle cues across line endings
* refactor(cli): preserve newline handling while reconciling subtitle tests
* refactor(cli): normalize WebVTT line endings at the parser
* style(cli): format the retained subtitle test block
* fix(cli): browser ensure fails when the unzip leaves no Chrome executable
* refactor(cli): keep install debug output and report only the last unzip attempt
* fix(cli): browser ensure exits after a failure or a --force download
* fix(cli): browser checks name why Chrome could not be installed
* fix(cli): render shows why its setup step failed instead of a crash dump
* refactor(cli): build the browser download label and staged test path once
* refactor(cli): one owner for the render setup result line
* fix(cli): render setup failures report through one error line
* refactor(cli): one reader and writer for both render setup lines
* feat(registry): eight more WebGPU shader backgrounds and a bundle with thirty new shaders
* refactor(registry): simpler bundle copy step in the shader build
* test(registry): every shader block ships the same bundle and names a shader in it
* refactor(registry): one block loader for the shader block tests
* test(registry): name the stale block and match only shader names
* chore(registry): shorter licence line at the top of the shader bundle
* test(registry): every shader block installs the bundle's licences
* feat(cli): models list shows which speech models transcribe can use
* refactor(cli): models list and transcribe share one Parakeet runner check
* fix(cli): models list skips whisper links transcribe would download again
* style(cli): keep models list comments under the package share