* fix(studio): keep a preview reload waiting while a slow project boots
Studio gives a reload up to three 15 s budgets, but the player failed it at
8 s and Studio's own 5 s safety net stopped listening for the runtime. A host
can now own the load deadline (no-load-timeout); script and runtime errors
still fail at once.
* fix(player): send the 8 s load deadline through the probe's one error path
Lambda and Cloud Run renders refused 25 fps although it is an integer
rate like 24, 30 and 60, and PAL footage is 25 fps. The distributed fps
set is now {24, 25, 30, 60} in the producer's config validation, the
plan v2 manifest, the render request mapping, the Lambda and Cloud Run
event types, the CLI's lambda and cloudrun commands and the regression
harnesses.
Co-authored-by: Cursor <cursoragent@cursor.com>
skills update (bare or named) and bare skills now exit 0 with a one-line
notice instead of installing or pruning when HYPERFRAMES_SKIP_SKILLS=1, so a
pinned skills install survives the router's trigger-time update. Closes#5695.
* feat(cli): judge text behind a transparent video by its decoded alpha in check
The layout audit treated every <video> as opaque over its whole box, so a
caption between footage and a remove-background cutout always failed
text_occluded and needed data-layout-allow-occlusion, which also hid a word
the subject's head fully covered.
check now draws the video's current (seeked) frame into one reused,
downscaled scratch canvas, once per sample and only for a video a text probe
lands under. A fully opaque frame keeps today's behaviour. A frame with
transparency is judged per rendered word against its alpha: partial cover is
an info finding carrying the most covered word's share, and a word 80% hidden
or two neighbouring words each half hidden is an error. A frame that cannot
be read falls back to opaque with a video_alpha_unread info note.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cli): serve the video-alpha fixtures from memory, not request paths
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): check finds words under narrow, mirrored or backed cutouts
Review follow-ups on #5657:
- A matte the probe walk passes over, even at clear-alpha points, now triggers
the per-word check, so a feature between two probe columns (an arm, a mic
stand) still fails the word it hides.
- Mirrored videos and images map viewport points through the flip; a rotated,
skewed or 3D-transformed video falls back to opaque with video_alpha_unread.
- A video with an opaque CSS background keeps its whole box occluding.
- cli.mdx lists every unread cause and why a fully transparent frame is one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(cli): the api provider speaks the HeyGen audio analysis contract
The api rung now sends its key as x-api-key, a new Idempotency-Key on each
request, and 16 kHz mono 16-bit WAV: the channels are averaged and resampled
with a windowed sinc whose cutoff sits under 8 kHz, so nothing folds back in.
It reads the answer from a { "data": ... } envelope, or bare. Times stay in
seconds, and the answered times still snap onto beats measured on the signal
as decoded. The response type gains time_resolution_seconds.
* fix(cli): reject redirects from audio analysis requests
* fix(cli): preserve audio when stereo downmix cancels
* feat(cli): audio answers beats, downbeats, bpm, sections and describe through a provider ladder
Beats and downbeats try api, apple-mu, beat-this, then signal; sections try api, apple-mu,
then signal; describe tries api, then apple-sound. bpm is read off whichever provider placed
the beats. Each provider says whether it can run here and why not, and the ladder warns
when it passes one over.
The signal rung is a TypeScript tracker: log-band spectral flux, autocorrelation tempo with
a log-normal prior, Ellis dynamic-programming beats with a windowed local tempo, a low-band
bar phase, and Foote novelty sections snapped to beats. It reports no steady beat when the
onsets do not repeat.
The api rung posts the audio to HYPERFRAMES_AUDIO_API_URL with HYPERFRAMES_AUDIO_API_KEY and
moves each answered time onto the nearest signal beat within a quarter beat, reporting what
moved and what did not. beat-this runs Beat This! from a configured Python and local weights
and never downloads. apple-sound runs macOS's sound classifier through a Swift helper.
apple-mu reports that Music Understanding needs macOS 27.
words are no longer accepted: they come from hyperframes transcribe.
* docs(cli): audio's beats, downbeats, bpm, sections, describe and their provider ladder
* feat(cli): beats tracks the music with the TypeScript beat tracker, without Chrome
hyperframes beats and Studio's beat detection now share analyzeMusicFromSignal: band energies,
then the spectral-flux tracker in audioRhythm.ts, then each beat's loudness for its strength.
The CLI decodes with the same ffmpeg path as hyperframes audio, so beats no longer launches
headless Chrome. Studio still decodes in the browser and passes every channel.
The old analyzer averaged bpm-detective's tempo with an energy-peak tempo it had only compared
after folding both into one octave, and phased a rigid grid from noisy peaks. On 22 real tracks
with exact script grids it found the tempo 18% of the time with beat F 0.437; this finds it
91% of the time with F 0.887, and refuses the other two rather than guess.
Below a tracker confidence of 0.2 no beats are reported, so ambient or rubato music gets no
invented grid and the CLI still writes nothing. bpm-detective, the energy-peak grid, the
headless analyzer and its prebuilt bundle are gone.
* fix(core): tempo reads 96 BPM off beats on 10 ms frames, not 95.2
The tracker places beats on 10 ms frames, so a 625 ms beat lands 620 or 630 ms apart and the
median interval reported 95.2 or 96.8 BPM. Tempo now averages the intervals within 10% of the
median: as robust to a skipped or doubled beat, without the frame-sized step. Both
hyperframes audio and hyperframes beats read it.
* fix(cli): remove redundant audio context comment
* fix(core): remove redundant tempo interval comment
* fix(ci): record beat analyzer removals and browser evidence
* fix(core): keep flat audio novelty in one section
* chore: keep PR capture evidence out of the source tree
* fix(cli): honor BPM provider pins before analysis
* feat(cli): audio answers beats, downbeats, bpm, sections and describe through a provider ladder
Beats and downbeats try api, apple-mu, beat-this, then signal; sections try api, apple-mu,
then signal; describe tries api, then apple-sound. bpm is read off whichever provider placed
the beats. Each provider says whether it can run here and why not, and the ladder warns
when it passes one over.
The signal rung is a TypeScript tracker: log-band spectral flux, autocorrelation tempo with
a log-normal prior, Ellis dynamic-programming beats with a windowed local tempo, a low-band
bar phase, and Foote novelty sections snapped to beats. It reports no steady beat when the
onsets do not repeat.
The api rung posts the audio to HYPERFRAMES_AUDIO_API_URL with HYPERFRAMES_AUDIO_API_KEY and
moves each answered time onto the nearest signal beat within a quarter beat, reporting what
moved and what did not. beat-this runs Beat This! from a configured Python and local weights
and never downloads. apple-sound runs macOS's sound classifier through a Swift helper.
apple-mu reports that Music Understanding needs macOS 27.
words are no longer accepted: they come from hyperframes transcribe.
* docs(cli): audio's beats, downbeats, bpm, sections, describe and their provider ladder
* fix(cli): remove redundant audio context comment
* fix(core): keep flat audio novelty in one section
* fix(cli): honor BPM provider pins before analysis
* feat(cli): hyperframes audio measures where sounds land, in film time
Adds @hyperframes/core/audio-analysis: an energy envelope, spectral-flux
and energy onsets, the moments a sound lands (onset, steepest rise, peak,
tail end), BS.1770 loudness with true peak, silence spans and a first
guess at sfx/voice/music, all pure functions of PCM. Each capability runs
through a provider ladder so model or service providers can join later.
The radix-2 FFT moves out of audioCarve so both share it.
`hyperframes audio` decodes with FFmpeg and analyzes files, or the film's
own audio and video clips by element, second, range or boundary, reading
only the part of each file that plays. Timeline rows now carry
`mediaStart`, moved by an in-point host's cut, so file time maps to film
time.
* feat(cli): audio placements carry the element's own data-start, local to its composition
* feat(cli): audio placements carry the element's own data-start, local to its composition
* feat(cli): audio places a file with --place-at, drops --boundary
--start becomes --place-at and --media-start becomes --skip-file-seconds,
which only goes with --place-at, refuses to skip the whole file, and warns
when the skipped part held the file's loudest moment. --boundary is gone:
--at and --range cover the sounds a cut touches.
* fix(cli): pass a sound file to ffprobe as a file, on every platform
ffprobe treated a name starting with a dash as an option because the argument
list never ended the flags. The path check compared a Windows drive path to a
POSIX string, and a new timeline comment raised that file's comment share.
* fix(cli): hear a reversed stereo channel, and warn when its peak was skipped
Averaging the channels cancelled a reversed one, so a real hit was reported
as silence. The skip warning also compared against the decode margin, so a
peak just before the kept window produced no warning.
* feat(cli): snapshot --keep redoes one board still and leaves the others
* fix(skills): the board recipe waits for a released snapshot --keep
* fix(cli): snapshot --keep refuses two times with one name and replaces linked stills
* feat(cli): init --keep-original copies a clip as is, without transcoding
`hyperframes init --keep-original` copies the --video file under its own
name and wires it into the project the way a web-safe clip is wired today:
no transcode and no prompt, interactive or not. Default behaviour is unchanged.
Theora leaves init's web-safe codecs and joins the preview proxy's
browser-hostile codecs, so init converts it and the preview proxy serves an
H.264 copy instead of a blank picture.
* fix(studio-server): keep the codec table's comment share at its base level
* feat(cli): save check, snapshot and render artifacts per revision
* fix(cli): keep clean --snapshots inside the project
A symlinked .hyperframes or artifacts folder was followed, so the command
deleted snapshot and check files outside the project.
* test(cli): keep check, snapshot, and render successful when recording fails
A held history or a corrupt output registry warns and leaves the command result alone.
* fix(cli): drop snapshot records when a replaced run is not recorded
A reused --output rewrites the files even when history is busy.
Those records are removed so the old revision does not keep the new pixels.
The preview checkpoint stops waiting after five seconds.
A bad PNG header no longer fails the snapshot.
* fix(studio-server): keep the video-state comment share from rising
The forget helper's doc comment pushed the file above its base share.
* fix(cli): post the preview checkpoint and keep cleanup inside artifacts
A preview checkpoint with no body was sent as GET, so Studio never ran it.
Cleanup now requires the real path to stay under the artifacts folder.
A failed follow-up drop does not warn twice.
The studio-server unpacked budget covers the record-dropping helper.
* docs(cli): shorten the snapshot skill notes
The command examples already show range and composition.
The prose only keeps the output path and the flags that are easy to misuse.
* chore: leave package headroom for artifact cleanup
* test(cli): compare artifact paths portably
* style(cli): format portable artifact assertion
* feat(cli): guard history turns with --exclusive, --who and --if-open
* fix(cli): make exclusive history turns atomic
Re-read the open turn only after acquiring history ownership so
concurrent exclusive begins cannot both claim the same project.
Cover the race with a deterministic two-process regression test.
* fix(cli): refuse a second exclusive turn while a preview is open
The preview's begin window now rejects another agent's live turn, and an
idle turn no longer blocks exclusive begin. The two-process test waits on
pids collected from claim drafts instead of seeing both files at once.
* 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.
* docs(lint): add a reference page for every lint finding code
docs/reference/lint-rules.mdx lists every code emitted by the linter
(132: the 121 rule codes in packages/lint/src/rules/*.ts plus the 11
project-level probes that run as part of a project lint) with its
severity, what it catches, and how to fix it, grouped by the rule file
the code comes from. Conditional severities (Studio-host structure
checks, GSAP seek-order hazards) say when they escalate.
Link it from docs/packages/lint.mdx and add it to the Composition
reference group in docs.json.
Issue: #5385
* docs(lint): document the color grading codes and correct four entries
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>
Add the four commands that `hyperframes --help` lists but the CLI reference
page left out, with their options taken from each command's args and
examples checked against a sample project.
Closes#5386
Co-authored-by: Claude Opus 5.5 <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): 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
* 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
* 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
* fix(cli): enforce capture screenshot limits and report interruptions
* refactor(cli): split screenshot step out of capture animation phase
Keeps the screenshot limit outside the mirrored core-extraction argument
list and moves scroll screenshots into their own step, so both stay under
the audit's duplication and complexity gates with no behaviour change.
* fix(core): write only authored hue curves, so a single hue curve passes lint
* fix(cli): apply over a grading saved with empty hue curves instead of refusing it
* fix(core): read grading properties from computed style, and stop advising an inline start
* fix(cli): preserve validation during legacy hue repair
* fix(cli): preview grading writes without apply
* fix(auth): accept host-managed HeyGen access tokens
* fix(auth): share environment header validation
* docs(auth): condense credential resolver guidance
* fix(auth): report the account a browser login just signed in
Identify the account from the tokens the login issued, not a fresh credential
lookup that an environment token or API key outranks.
* docs(auth): list the host access token in the CLI credential order
* fix(cli): name a rejected host access token on publish
Publish told a host-token user to log in again, which cannot help while the token outranks the login.
* docs(auth): show the media credential order main resolves
* fix(cli): warn at logout that a host access token still signs commands
One map of environment credential variables, owned by the resolver, now feeds the logout warning and both publish messages.
* docs(auth): scope the media credential order to the audio scripts
* refactor(cli): read every env credential name from the resolver's map
* docs(skills): ask agents inside a host app to report its friction as feedback
* feat(cli): send missing features and host-app friction as ratingless feedback
* fix(cli): keep the rating error, refuse blank reports and document ratingless feedback
* docs(skills): leave names and figures out of missing-feature reports
* docs(skills): keep anything private out of missing-feature reports
* fix(check): see color, filter and media time under seek, and name data-no-timeline for stills
* fix(check): warn instead of fail when only audio advances, and judge text overlap on glyphs
* fix(check): judge glyph overlap by the glyph box, and see capitalized words and one-side borders
* fix(check): capitalize past leading punctuation, test outline color, trim a comment
* fix(check): capitalize words the way Chromium does, so digit-led words stay lowercase
* fix(cli): let parakeet-mlx detect the language and name the Parakeet install under auto
* docs(cli): say what --language does under each engine
* docs(cli): whisper transcribes as the --language, it does not filter
* chore(cli): drop a comment the install-hint condition already says
* feat(cli): feedback says whether a person or an agent wrote it, with the email an app attaches
* refactor(cli): split the .env loader so each part stays simple
* fix(cli): a broken .env loader fails loudly; only a missing .env is ignored
* fix(studio): an edit on a GSAP-animated layer writes its value at the playhead into the file's tween
* fix(studio): undo pressed while an edit saves undoes that edit, not one started after the key
* fix(studio): an edit stays saved when the history reply after it cannot be read
* feat(studio): export ColorField and GradientField; a session style commit takes a map
* refactor(studio): name the map form instead of commenting it
* feat(studio): a host can move layers by exact film px, one undo per call
handleDomGroupMoveBy(moves) on useDomEditSession and DomEditValue moves each
selection by its delta in composition px, for Desktop's align and distribute.
It reuses the group drag's own path: a drag member per element (press-time
route, measured offset map), the delta carried to screen px through the
preview frames, then handleGsapAwareGroupPathOffsetCommit, so one shared
coalesce key gives one undo entry and GSAP members take the GSAP write.
A member that can't move refuses the call before anything is written; the
refusal rejects with its reason and shows no Studio toast (the group commit
takes refusalToast: false; telemetry stays). Zero deltas are skipped.
Tests: a pre-offset element and one under a half-scale parent move by exactly
{10,0} and {0,-5} under one group key; a half-scale parent with a 30 deg
rotated ancestor lands within the 0.001 px a translate is written with; a
GSAP member goes through the GSAP write; three refusal kinds write nothing,
leave the boxes where they were, show no toast and leave no gesture mark,
while a canvas group drag still toasts; the session hands the call to the
group commit with refusalToast: false. Each mutation (no frame mapping, no
zero skip, no restore, no teardown, no capability check, toast on refusal)
fails at least one of them.
* fix(studio): an outside change during a drag waits for the drop to show
While a gesture mark is live in the preview, refreshPlayer takes the shadow load instead of
swapping scenes in place. A swap replaced the node under the pointer mid-drag; the shadow load
is then held by the existing gesture hold and, if the drop saved, reloaded fresh by the stale
rule, so this adds no second "released" signal.
Test: an outside change during a live drag on a preview that can swap scenes leaves the dragged
node in place, shows nothing under the pointer, and after the drop loads fresh with the dropped
position. It fails on the parent without the condition ("swapped under the pointer").
Cost case, measured in a real headless preview on the edit-accuracy bench's per-frame teleport
check. Every GSAP instant patch was forced to miss and fall back to reloadPreview during its own
commit; move and rotate of a held element (gsap.set and tl.set holds), px and xPercent
placement, r0 and r30, root and nested, at 100%:
- The gesture mark was live when the fallback asked for the reload, but had always ended by the
time refreshPlayer ran (refreshKey is consumed in an effect after the commit settles). So the
new condition never fired there: root previews offer no scene swap and load shadow on both
builds, and nested previews swap on both builds.
- Worst post-release frame offset, 32 cases on this build and 32 on the parent: 0.00 px over
76-106 frames each (release to settle). No frame was off by more than 0.5 px.
- Sensitivity: a 3 px nudge injected 50 ms after the fallback reads back as 3.00 px.
* fix(studio): an edit no longer reopens the live-updates stream, and thumbnails ask one at a time
* fix(studio): an undo no longer waits behind paused preview videos for a connection
* feat(studio): a host whose API needs same-origin cookies can have Studio send them
* docs(studio-server): a host on same-origin cookies sets the Studio credentials opt-out
* test(studio): an undo is not held up by the preview's paused videos
* fix(studio): a resize inside one gesture writes a mixed tween once, in place, never beside it
* chore(studio): keep the carried fixes inside the comment ratchet; telemetry posts through studioApiFetch
* test(studio): a drag on a mixed tween is one write under the gesture key
* fix(studio): freeze-frame, loudness and peak-map requests go through studioApiFetch too
* fix(studio): an undo with no project open reloads in full instead of skipping nested files
* refactor(studio): drop the soft reload's reparse seeding, the held-element sweep covers it
* test(studio): keep the undo-restore browser test off the network and inside its timeout
* test(studio): nested composition reads expect the cookie-free request studioApiFetch sends
* fix(studio): an edit at the playhead rewrites its tween in place, so later tweens keep their times
* fix(studio): a rotate on a tween that turns another axis holds its angle and never turns early
* fix(studio): plan every group member before the first write so one refusal moves none
A group drag or nudge preflighted each member against the move refusal rule only. The value-at-playhead planner ran later, in the commit loop, after earlier members were already flushed to disk, so a tween with extras, eased keyframes or an unknown ease left the group half moved.
The group preflight now runs the commit's own route and keyframe plan for every member with its real offset, so any refusal refuses the whole group with nothing written and one toast attributed to the refusing member. The plan is not reused in the commit: earlier members' writes change the file under it, so each member re-plans against the fresh tween, from the same planValueEdit.
* chore(studio): the fetch lint rule also catches window.fetch, globalThis.fetch and self.fetch
* docs(studio-server): set the credentials flag before Studio loads; a mounting host injects it
* test(studio): the undo socket test needs six pinned videos under Vite and checks each undo restores
* fix(studio): Studio leaves cookies out of its requests only when served from loopback
* fix(studio): undo takes back the first edit, and the first save never waits on media
* fix(studio): undo of a move whose save failed stays undone and steps back no older edit
* fix(studio): a style set by its JavaScript name saves under its CSS name
* test(studio): the undo socket test holds round 0 to a full restore too
* fix(studio): the host revert keeps returning what shows the edit again
* fix(studio): an entrance rewritten at the playhead still shows its start state before it starts
* fix(studio): undo takes back the edit pending at the key even after a later edit claims
* chore(studio): lift the late-write quarantine now that undo never waits on videos; format
* test(studio): pin an entrance's start render on the written tween, not a happy-dom GSAP run
* revert(studio): leave entrances as on main, immediateRender does not render a keyframed start
This reverts commit ee9d130d3c and its written-vars test.
* revert(studio): undo goes back to the newest claim after the key, as on main
This reverts commit 5b9009a823.
* revert(studio): first-undo history opening moves to the next PR
This reverts commit 4e1e75d7f4.
* revert(studio): the undo socket test exempts round 0 again until first-undo lands
This reverts commit 1f8f97047c.
* test(studio): bank the edit accuracy cases this PR makes pass
* refactor(studio): share one project file fetch between the plain and optional reads
* test(studio): share the WebMCP host shim between the browser gates
* test(studio-server): share one stub adapter across the route tests
* refactor(studio): type the preview slot props from the NLE context
* test(studio): share the setup the new editing tests copied between files
* refactor(studio): split playhead tween picking and undo path merging into small tested helpers
* test(studio): pin when a resize at the playhead also writes the anchor move
* test(parsers): pin rewriting a tween in place as keyframes
* chore(studio): cut comments the new helper names already say
* fix(studio): composition thumbnails keep two fetches in flight, as the document lifetime fix expects
* fix(studio): a playhead edit parses a tween not yet played and matches only its own tween
* fix(parsers): a new position argument goes after the last argument, so trailing commas stay valid
* fix(studio): a second undo pressed during one save steps back to the edit before it
* test(studio): pin the slot an aborted thumbnail frees and the shared tween refusal
* refactor(studio): split the unplayed-tween parse into small helpers and name the claim release
* fix(studio): a playhead edit keeps the tween's default ease, delay timing and loop settings
* fix(studio): a group move refuses layers animated in two files and plans at the real drop
* style(studio): format two test files
* refactor(studio): check a group's files inside the preflight helper
* refactor(studio): move the group preflight checks out of the editing hook, under 600 lines
* fix(studio): read an unreached tween without redrawing other layers, and compare write files
* test(studio): use the shared selection helper in main's keyframe-at-time telemetry test
* chore(studio): fix a comment that read as a repo path and cut one added comment
* fix(studio): read a later tween's start as playback does, and count every keyframe add
* fix(studio): two undo presses during one save step back two edits, not one twice
* fix(studio): an undo that succeeds by any route retires the claim on the edit it undid
* fix(studio): a claim is never handed back to an edit another undo has already undone
* fix(studio): an undo picks its claimed edit at the press and skips it once undone
* fix(studio): a press refuses, as main did, once its edit is undone and a newer edit exists
* fix(studio): an unplayed keyframe tween clears every keyframe's channels before it is read
* fix(studio): reading an unplayed tween puts back every transform value its clear removed
* fix(studio): reading an unplayed tween restores each layer's whole style and GSAP state
* fix(studio): reading an unplayed tween skips plain-object targets and restores attributes
* fix(studio): shared tween-target filter uses GSAP's own styled-element test
* fix(studio): match a parsed tween on any target, and check a later keyframe channel's start
* docs(studio): say the unplayed-tween read restores styled elements only
* test(studio): a drag on a fromTo start shows the dropped x through the runtime seek
* fix(lint): flag seek-unsafe CSS transitions
* fix(lint): allow zero-duration transition resets in css_transition_used
The registry's reduced-motion and render-lock resets use transition-duration: 0s and
transition-delay: 0s, which never run on the browser clock. Also drops the one real
seek-unsafe transition the rule finds in the product-promo example.
* refactor(lint): split the zero-time check out of isSeekUnsafeTransition
* fix(lint): report seek-unsafe CSS transitions as a warning
---------
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
* fix(cli): distinguish a skipped browser check from a genuinely clean one
`hyperframes check`'s runtime/layout/motion/contrast JSON sections reported
the exact same ok:true/zero-findings shape whether the browser session
actually ran and found nothing, or never ran at all (a blocking lint error,
a lint-step crash, or an exception thrown before the audit could start all
took the same "empty result" fallback). Only the top-level ok field and an
undocumented layout.duration === 0 tell separated the two cases apart.
Add a skipped boolean to CheckBrowserResult, set at the only two places one
is constructed (false in the real audit result, true in the empty-result
fallback), and mirror it to a new top-level browserSkipped field on
CheckReport. The human-readable report now prints a warning when the
browser never ran; the JSON envelope carries the same signal for callers
that only check individual sections.
Co-Authored-By: Miguel Angel <miguel.sierra@heygen.com>
* docs(cli): document browserSkipped in the check --json envelope
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* refactor(cli): trim browserSkipped docs and cover the linter-crash skip path
* chore(skills): refresh the hyperframes-cli manifest hash
* refactor(cli): drop the browserSkipped type comments to keep the comment share flat
---------
Co-authored-by: Miguel Angel <miguel.sierra@heygen.com>
* fix(cli): fingerprint textual sweep state
* fix(cli): share one motion classifier between sweep guard and liveness
The frozen-sweep guard (sweep_static) and keepsMoving liveness (motion_frozen)
each owned their own definition of "what counts as motion": the sweep
fingerprint in layout-audit.browser.js read nine channels while
motion-sample.browser.js compared box + opacity only. They meet in one decision
in checkPipeline.ts, so a fixed-width attr() countdown was accepted by the
sweep guard and still reported frozen by liveness.
Move the classifier into motion-signature.browser.js, injected before both
samplers, and make it the single owner:
- Per-element channels (box, opacity, font-variation axes, clip-path, direct
text, form-control state, ::before/::after content, media pixels) as a
reader list so further channels (e.g. SVG dash) plug in.
- Liveness consumes the same signature with 2px / 0.08 bucketing; the sweep
guard consumes it exact. motion-sample throws at install if the shared
script is missing.
- Counter state is a composition-level channel over box-generating elements
and root ancestors. display:none subtrees generate no boxes and are
excluded, and only declarations naming a counter that some painted
generated content (::before/::after/::marker on a non-ignored, paintable
host) consumes are folded in, so a decoy hidden counter cannot make a
frozen composition read as live.
- clip-path is a channel, replacing the fingerprint's former reliance on the
clipped-away visibility probe, so a wipe over a static box still counts.
- data-layout-ignore layers are excluded from the signature but not from the
per-selector `visible` bit reported for explicit motion assertions.
- Root resolution for the "*" liveness scope now matches the sweep guard and
the root element is part of its own signature.
Tests: motion-signature.browser.test.ts (happy-dom, per-element fake computed
style so ancestor handling comes from the classifier) and
motion-signature.browser.chromium.test.ts (real Chromium via puppeteer-core;
skips without a system browser) pin every branch: attr() substitution,
display:none decoys, named-counter gating, ::marker, zero-box hosts, ancestor
counters, textarea/indeterminate, root text, clip-path, ignore layers, and
sweep/liveness agreement on the countdown fixture.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): prune unrendered content from the motion signature and measure asserted scopes
The shared motion classifier pruned only display:none subtrees, so text,
pseudo content, media pixels, and counter decoys inside skipped
content-visibility contents changed the frozen-sweep and liveness
signatures although nothing painted. Pruning is now platform-owned where
checkVisibility exists (display:none, skipped hidden/auto contents; a
display:contents host keeps its painting pseudo-elements), with the
computed-style fallback for happy-dom. A content-visibility:hidden host
signs its own box, opacity, clip-path, font axes, and widget state, but
not its text, pseudo content, control value, or media pixels; hosts where
size containment does not apply (non-atomic inline, display:contents,
table and internal table boxes, ruby) skip nothing. isVisibleElement
regains the checkVisibility floor layout-audit uses on its opacity-floor
path, so an asserted selector inside skipped contents reports hidden.
The layout-audit opt-out no longer applies to the element passed as the
signature root: a keepsMoving scope that is itself data-layout-ignore is
measured (an assertion naming an element outranks the opt-out) instead of
producing an empty signature that motionAudit reports as a missing
selector. Opt-outs inside the scope still apply.
motion-sample defers the missing-classifier error to the first call so
the driver's evaluate surfaces it instead of an opaque "is not a
function" page error; round() is shared.
The real-Chromium suite is opt-in on Windows (HYPERFRAMES_BROWSER_TESTS=1):
in the shared Windows package lane the runner's Chrome did not launch
within the package hookTimeout, and the suite already runs on Linux CI
where the system Chrome is found. The launch hook has its own 120s ceiling.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): prune display:contents in skipped contents; table-cell skips, caption paints
A display:contents child of a content-visibility:hidden host has no box
of its own, so the platform check could not tell it apart from an
ordinary display:contents owner and it stayed a counter consumer: a
change of the owner's counter-reset altered the sweep signature although
nothing paints. The parent's skipsContents verdict now prunes it; boxed
descendants keep being decided by the platform.
NOT_CONTAINABLE_DISPLAY had table-cell and table-caption inverted
versus Chromium 152, where a table-cell host skips its contents and a
table-caption host paints them.
Four Chromium pins: the display:contents-in-skipped-contents counter, a
table-cell host's own text and pseudo content (frozen), a table-caption
host's own text and pseudo content (motion), and a counter reachable
only through a skipped host's hidden ::marker / ::after (frozen; also
asserts the skipped child is not visible for motion-sample).
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* chore(cli): trim sweep motion comments to the comment ratchet
* chore(cli): shorten sweep motion comments to the merge-hook limit
---------
Co-authored-by: miga-heygen <miguel.sierra_miga@heygen.com>
* feat(cli): one owner for init media placeholders, audio stays on the video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): init keeps a video's sound on the clip; silent files scaffold muted
Supersedes the patchVideoSrc changes in #3977 and #3942 and carries their tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: miga-heygen <miga-heygen@users.noreply.github.com>
Co-Authored-By: kapelame <kapelame@users.noreply.github.com>
* feat(registry): example templates keep the a-roll's sound on the video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(lint): video audio rules recommend keeping sound on the video
Update video_missing_muted and video_audio_double_source message and
fixHint text to recommend the new default: keep audio on the video with
data-has-audio="true" (not muted). Tests verify the new wording is in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(parsers): a video is either audible (data-has-audio) or muted, never both
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): follow the compiler's audio rule in the audio-track toggle
Confirm before background removal drops a video's sound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): hyperframes-core teaches sound on the video by default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): workflows and scaffold guidance keep a video's sound on the clip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: footage with sound keeps it on the video clip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): init from-file a-roll is timed, muted strip is attribute-exact
The from-file a-roll video gets data-start="0" so a scaffold lints clean, and
the orphaned audio-only slot comment is dropped. setVideoAudio now walks the
tag attribute by attribute, so a class value containing the word muted
survives. The ffprobe-missing warning says the video is assumed to have sound
and how to switch to muted, and DEFAULT_META notes the failure surfaces at
render until lint support lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep has-audio in step with the muted toggle, test cutout confirm
Muting a video now clears has-audio and unmuting sets it to true, in both the
Design and flat media panels, matching the Has audio track toggle. Audio
elements are unchanged. Adds coverage for both panels plus the cutout
window.confirm guard: declined leaves src alone, accepted swaps src and mutes,
a silent video never prompts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(parsers): updateElementInHtml hasAudio only applies to videos, true or false
An audio element or an undefined hasAudio no longer picks up a muted attribute;
only an explicit false on a video writes muted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: video_missing_muted rule text, silent-footage notes, consistent audible examples
lint.mdx and cli.mdx describe the current video_missing_muted rule instead of
the retired unmuted-video wording. The editing recipes say how to adapt for
silent footage and require the outgoing clip's sound to end at a J-cut. The
Remotion media example shows the audible mapping consistently, data-attributes
states that has-audio is required unless muted, and the hairline frame note no
longer claims the video is muted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): encode init media filenames and probe --audio duration
Function replacers stop dollar-sign pattern expansion in filenames; src is percent-encoded.
--audio now passes the probed track length as the composition duration in both init paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(lint): audio-state-aware media rules and time-aware double-source check
video_missing_muted honors any explicit data-has-audio.
video_audio_double_source only considers audible videos and only fires
when the same-src clips overlap in time.
duplicate_audio_track now covers audible videos.
audio_file_without_element no longer claims silence when an audible video
carries the sound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): audio-stream check for remotion ports, followable J-cut recipe
Remotion Video maps to data-has-audio only when the file has an audio stream.
The J/L-cut recipe now shows both shots with muted pictures and separate audio.
embedded-captions removes the audio.mp3 scratch file it extracts.
skills-manifest.json regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(lint): trim comments to satisfy the comment ratchet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): atomic muted edits, cutout keeps sound on a linked audio
Muted and Has audio track commit one batched write (one persist, one
undo entry) and remove data-has-audio instead of writing an empty
marker. Unmuting a video probes its src: audible -> true, silent ->
false, probe failed -> absent. Remove background on an audible video
inserts a linked sibling <audio> with the original sound in the same
write, replacing the drop-the-sound confirm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(producer): skip hidden videos when auto-adding video audio
A video hidden in Studio is dropped by the mixer, so its src never reached
existingAudioSrcs and the render re-added it at full volume. The video parser
now records the shared hidden predicate and the auto-detect step skips hidden
videos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep useDomEditSession under the 600-line cap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: miga-heygen <miga-heygen@users.noreply.github.com>
Co-authored-by: kapelame <kapelame@users.noreply.github.com>
* feat(cli): add hyperframes clean to find and remove what HyperFrames left on disk
`hyperframes clean` lists leftover render work dirs, debug renders, the history of projects
that no longer exist, idle video proxies and extracted video frames, with sizes, and removes
them. `--dry-run` removes nothing, `--json` is for agents, `--snapshots` also removes
snapshot folders (otherwise listed as reclaimable). It never touches outputs, sources or
anything a running render or preview still owns.
* docs(cli): say that clean does not search a project's subfolders
* fix(cli): clean test compares folder names on Windows and the skills manifest is regenerated
* fix(studio-server): the proxy cache sweep removes only files the transcoder named
It removed any idle .mp4 or .webm in .transcode-cache, so a user file placed there, or reached through
a symlinked cache folder, could go. Proxies are always <sha256>.<ext> and their temp files
.tmp-<uuid>-<proxy name>; nothing else is swept.
* style(producer): keep a renderDirOwner doc comment within 120 columns
* feat(player): play and loop a range of the film with range-start and range-end
The runtime takes a set-play-range control and stops on the range end frame itself.
The player wraps to range-start with loop, fires ended without it, and reports a
range cut to the film with rangeclamped. Video and __timelines players check it per tick.
* fix(player): hold a range on its last frame and let the runtime own range jumps
A runtime with the play-range capability jumps to range-start itself when a new range leaves
the playhead outside, so a stale state no longer ends the film; older runtimes keep the
player fallback, guarded until a state lands inside. Every range stop shows the last frame
before range-end; a paused playhead re-parks only when it falls outside the new range.
* fix(player): stop a range a check early and enforce its end on a video in a background tab
* test(player): share the range video setup
* fix(player): a slow frame no longer cuts a range short; a video without a range is unchanged
* fix(player): a range still stops early where video time moves once per frame
* refactor(player): cut the range look-ahead comment to one line
* fix(player): a per-frame video time rounded down still stops a range early
* test(player): pin no rounding tolerance at a film's own end
* test(studio): expect the play-range capability on control messages
* feat(player): play video files with type="video/..."
A src with a video type now plays in a <video playsinline> behind the same
API and events as a composition: play/pause/seek, currentTime, duration,
ready with the video's size, timeupdate, ended, loop, durationchange and
resize. A load failure fires error at once with the MediaError code, and a
blocked play fires playbackerror and leaves the player paused.
The video is driven through the direct-timeline adapter and clock, and every
src load goes through one _loadSrc that picks the video or the iframe.
* fix(player): keep video playback steady across option changes, pauses and errors
* fix(player): keep video playback in step with the video after seek, pause and srcdoc
* fix(player): limit video-mode changes to video players and pause on srcdoc switches
* refactor(player): cut the video-source header comment to two lines
* perf(studio-server): light entries for safe paths, hf-id stamping and project history
* docs(studio-server): list the light entries a host can import
* perf(studio): light ui and player entries for a host's first screen
* perf(studio): the ui entry's logo no longer loads the studio header
* test(studio): refuse only packages studio itself depends on
* test(studio): the light entries also refuse the hf-ids parser
* feat(cli): tag telemetry and feedback with the app that launched the CLI
* fix(cli): keep the client tag to a short slug and correct the telemetry docs
* test(cli): read the delivered batch through the shared helper
* refactor(cli): let the tag pattern alone bound the client tag's length
* docs(cli): describe the client tag generically as the launching app's tag
* style(cli): format the client tag test cases
* test(cli): use a neutral example value for the client tag
* chore(skills): regenerate the skills manifest for the telemetry reference
* docs(cli): render help says when page-side compositing is actually used
* docs(cli): page-side compositing help names webm and mov and the fast-capture flag
* docs(cli): say plainly that gif keeps page-side compositing
* fix(cli): commands exit without waiting on update checks or telemetry
The update and skills checks run in a detached child that refreshes the
caches for the next run; the parent only reads the caches. finalizeCli no
longer awaits the telemetry flush: the exit handler flushSync() delivers
what is queued. The events beacon, feedback and figma error paths stop
awaiting flush() too.
* fix(cli): retry a failed background check at most once an hour
Before starting the background worker, the CLI stamps a per-check attempt
time and skips a check it attempted within the hour, so a dead network no
longer starts a worker on every command. The stale-pin notice writes onto a
fresh read, detached spawns set windowsHide, and a test ties the worker path
to its build entry.
* fix(cli): a failed-check stamp dated in the future no longer silences update checks
* feat(cli): download Parakeet with models install parakeet and transcribe with it
`hyperframes models install parakeet` installs the sherpa-onnx runtime from
npm and the Parakeet TDT 0.6B v3 int8 model from Hugging Face, pinned to a
revision. Every file is checked against its pinned size and sha256 under a
temp name before it is renamed into place. Re-running it is a no-op. On
Linux with glibc older than 2.32 it refuses up front.
`transcribe --engine auto|parakeet` now prefers that engine when it is
installed, then an installed parakeet-mlx runner, then whisper. Audio is cut
into about 60 s windows at the quietest 100 ms near each cut, and decoding
runs in a child process. If the child cannot load the runtime or crashes,
transcribe falls back with one stderr line saying why. The JSON output and
transcript.json shape are unchanged.
* fix(skills): report the real word count and engine from transcribe fallbacks
The media-use transcribe script read `words` from the CLI's transcript,
which is a flat array, so its whisper path always reported an undefined
word count. The embedded-captions script labelled every CLI transcript
whisper.cpp even when `--engine auto` ran Parakeet; it now uses the engine
the CLI reports.
* fix(cli): pin the sherpa-onnx native package to the runtime's exact version
* test(cli): type the sherpa install test doubles
* fix(cli): transcribe audio with no speech as 0 words instead of failing
Parakeet writes an empty word list for a music bed, a silent intro or a
very short clip, and loading it failed with "Unrecognized JSON transcript
format". An empty list is now read as a transcript with no words, as
whisper's empty output already is. A non-empty list still has to look like
words.
* fix(cli): re-decode a Parakeet window that skipped the start of its speech
At some window lengths the model silently drops the first clause: on a
looped speech clip, 4 of 167 lengths between 55 and 60 s, and a 61.91 s
window, lost their first 9 words. A window with a token gap of at least 1 s
that is as loud as speech is decoded again with 0.5 s of leading silence,
and the result with more tokens wins. Clean windows are unchanged.
Decoding with 1.5 s of overlap before each cut was measured too and was
worse on 573 s of audio (2.62% WER against 0.98%), so it is not used.
Empty audio no longer makes a zero-length window, which aborted the
decoder.
* fix(cli): stop Parakeet on Ctrl-C and name the repair when its install breaks
- Ctrl-C during a Parakeet transcribe now stops with exit 130 instead of
starting whisper, and a SIGTERM to the CLI also stops its decode child.
- Ctrl-C during models install parakeet aborts the download and the npm
install, keeps no partial file, and exits 130. A staging dir left by a
killed install is swept by the next run.
- When Parakeet fails under --engine auto, the warning leads with the
Parakeet error and names hyperframes models install parakeet. If the
fallback fails too, the Parakeet error is reported rather than
whisper_unavailable, so --optional no longer hides a broken install.
- An explicit --engine parakeet no longer falls back: it fails with the
Parakeet error and the repair command.
- The install refuses up front on systems with no native package
(naming the six supported ones) and on musl, instead of a raw npm error.
- 24-bit and float WAV go through ffmpeg, since sherpa-onnx reads only
16-bit PCM. The decode timeout scales with the audio length.
* docs(cli): point Parakeet setup at models install parakeet
The transcribe docs and the media-use references named parakeet-mlx as the
only way to get Parakeet. They now name hyperframes models install
parakeet, and the CLI reference gets a models section.
* test(cli): cover the npm cancel signal and decode timeout, and fix a doubled period
The Parakeet failure line printed "failed.. To repair" when the decoder's
own message ended in a period. Tests now also pin that Ctrl-C reaches the
npm install and that the decode timeout scales with the audio length.
* fix(cli): report unreadable audio as an input error, not a Parakeet failure
Prepare the WAV once before the Parakeet fallback and hand the same file to whisper,
so ffmpeg runs once. Ctrl-C while ffmpeg runs now ends as a cancel (exit 130)
instead of starting whisper.
* fix(cli): recover a clause Parakeet drops after a pause by decoding the gap alone
Each loud gap without tokens is decoded on its own with 0.5 s either side, and only
its new tokens are spliced in. The padded whole-window retry stays as the fallback.
A window with no tokens at all is no longer decoded twice.
* perf(cli): size the decode timeout from the WAV header instead of reading the file
* docs(cli): say a broken Parakeet install still fails transcribe --optional
* fix(cli): treat Ctrl-C during audio preparation as a cancel and drop the partial WAV
ffmpeg traps SIGINT and exits 255 rather than dying by the signal, so that status now
counts as a cancel unless the timeout fired. A failed or stopped ffmpeg removes the
temp WAV it was writing. The decode timeout now sizes a prepared WAV from its file
size, so a large header can no longer hide its length.
* fix(cli): splice a re-decoded gap by whole words so its neighbours are never doubled
The gap decode splits edge words into other tokens than the window decode did. Match
neighbours as whole words within 0.3 s of each gap edge, add only whole words that start
inside the gap, and decode the gap behind 0.5 s of silence like the padded retry.
* fix(cli): count a cancel only when ffmpeg says it received a signal
ffmpeg 7 and later also exit 255 when a file is not permitted (EPERM), so exit 255
alone read a permission error as a cancel. ffmpeg's stderr is now captured, a cancel
needs its received-signal line, and any other failure reports ffmpeg's last line.
The output path is passed to the shared runner explicitly.
* fix(cli): keep a spliced gap word that has no leading space
The filter dropped such a word; it is now kept and given its word boundary.
* refactor(cli): trim three long comments in the Parakeet decoder and installer
* fix(cli): repair a Parakeet runtime missing its native binary, and pick Parakeet by language
The runtime dir is per platform and arch. Installed now needs the runtime
manifest and the native sherpa-onnx.node; a half-installed dir is removed
before npm runs, since install() keeps any dir holding the runtime manifest.
The decode worker flattens a multi-line loader error onto its prefixed line
and stops between windows once orphaned. transcribe opens its cancellation
scope before audio prep, so Ctrl-C under --json cleans up. auto uses
Parakeet only for its 25 languages and only where the host can run it;
--engine parakeet with another language fails.
* fix(cli): sweep reused-pid staging, name failed model downloads, keep ffmpeg's cause
sweepStaleStaging also removes a staging dir older than 6 hours whose pid
is alive, since containers reuse pids. A failed model download names the
file, the host and a retry. runFfmpeg keeps ffmpeg's stderr on its error,
so transcribe shows "Output file does not contain any stream" for a video
without audio, and reads a cancel only from ffmpeg's own signal line.
* fix(skills): make media-use --engine whisper run whisper and report the engine that ran
The whisper path now passes --engine whisper when asked, or --engine auto,
and reads the engine from the CLI's --json line, so a CLI Parakeet run is
no longer labelled whisper. Docs state the language rule, the real repair
behaviour and the platforms Parakeet runs on.
* test(cli): pin that a long Parakeet decoder error is kept whole up to a bound
* fix(cli): report a broken Parakeet runtime with the repair instead of skipping it
* refactor(cli): name the staging age limit instead of commenting it
* fix(skills): let media-use --engine parakeet use the CLI Parakeet when parakeet-mlx is absent
* fix(cli): repair a Parakeet runtime that fails to load, and read Ctrl-C after whisper
models install parakeet now loads the runtime in a child process, the way
the decode worker does, and reinstalls it when that fails, so a missing
shared library, a corrupt binary or missing runtime JS is repaired. It
reports changed: false when the runtime loads. transcribe keeps its cheap
manifest check. After a failure, transcribe lets the event loop turn once
before reading the Ctrl-C scope, since a signal that landed during the
synchronous whisper fallback is only handled then.
* fix(cli): read Ctrl-C from the whisper child, and re-check the Parakeet runtime after a repair
A whisper fallback killed by SIGINT or SIGTERM is a cancel (exit 130), the
same rule decode and runFfmpeg use; the loop-turn wait is gone. The
runtime probe treats a child stopped by Ctrl-C as a cancel and keeps the
runtime; a reinstall is probed again and fails with the loader's error if
it still does not load. media-use keeps the CLI's last lines, stdout
included, when hyperframes transcribe fails.
* fix(cli): give the user-stop rule one owner, SIGHUP included, and end a cancelled install at 130
stoppedByCancelSignal in renderCancellation.ts uses the scope's own signal
list (SIGINT, SIGTERM, SIGHUP) and is false whenever Node stopped the child
(a code, killed, or a spawn error). decode, runFfmpeg, the runtime probe and
the whisper fallback all route through it, so a probe killed by SIGHUP keeps
the runtime. isWhisperTimeoutError now keys on ETIMEDOUT only, which Node
sets on execFileSync's own timeout, so a SIGTERM reaches the cancel rule.
A cancelled models install sets exit 130 and returns instead of failing, and
the probe says what went wrong (timed out, crashed, no output).
* refactor(cli): let the stop-rule helper name carry its meaning
* fix(cli): Studio's catalog install goes through add, so it records the item and honours paths
* fix(cli): Studio's catalog lists the project's registry and install paths survive symlinks
* refactor(cli): name the Studio catalog's registry option plainly
* feat(cli): registry_item_added says whether add or Studio installed the item
* fix(cli): dropping the same catalog block in Studio twice installs it again
* chore(cli): drop a comment that restates add's source option
* fix(cli): Studio's re-record finds lock entries by their resolved path
* test(cli): the symlinked-folder install test checks the resize happened
* fix(cli): Studio mounts the installed item's own file by the path add recorded
* fix(cli): Studio's install never hands back a file the project kept for its own edit
* feat(player): expose picture size, durationchange and a click-to-play opt-out
- compositionWidth/compositionHeight are public, ready carries them, and a
resize event fires when the picture size changes.
- durationchange fires when the duration changes after ready, on both the
runtime and the direct-timeline paths.
- disable-click-to-play leaves clicks on the picture to the host.
Nothing changes for a player that does not use the new attribute; ready's
detail only gains two fields.
* docs(player): say composition size, one doc line per size getter
* fix(player): apply scenes before ready fires, keep the zero-size warning, document the new API
* fix(player): fire a message's events after the whole message is applied
* fix(player): queue every player event behind the update that raised it
* fix(player): decide autoplay after ready listeners run, and restart a loop at time 0
* fix(player): keep raising events after a listener throws
* refactor(player): drop a comment the ordering test already pins
* fix(player): fire the rest of an update's events when a listener throws
* perf(player): let paused players check in once a second with low-power-idle
* refactor(player): trim low-power-idle comments and tidy its test
* fix(core): keep the fast paused heartbeat until a timeline is bound
* fix(core): wait for sub-compositions to bind before slowing the paused heartbeat