mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 13:07:11 +00:00
f87dc5fa0dae00a2be6e7e3474749a2cdab9d42e
979
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bd0dc8c3ae |
feat(studio,cli): add an Edit with Framey button and hyperframes open for HyperFrames Studio (#4951)
* feat(studio,cli): add an Edit with Framey button and `hyperframes open` for HyperFrames Studio
Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app.
- Studio header: an "Edit with Framey" button with Framey, the app's cursor mascot (the eye
follows the pointer, a wiggle on hover, honours reduced motion). Until the app opens
handed-over projects (HANDOFF_READY), a press shows a "Meet Framey" card with the download.
The button shows only where the preview server answers GET /api/open-in-desktop on macOS,
so the desktop app's own embedded Studio never shows it.
- `hyperframes open [dir]`: hands the project folder to the app with `open -b` (released app,
then Canary) and points to the download when neither is installed. When run by Claude Code
or Codex it leaves .hyperframes/agent-handoff.json (CLAUDE_CODE_SESSION_ID / CODEX_THREAD_ID)
so the app's first chat can pick up that conversation.
- render and preview print one line about the app: the download until HANDOFF_READY, then
`hyperframes open` when the app is installed. Nothing for a draft render, a batch row, or a
run inside the app (HYPERFRAMES_DESKTOP_PROJECT).
* fix(studio,cli): gate `hyperframes open` too, guard the open-in-desktop POST, keep one gate
Review of #4951 at
|
||
|
|
44b5836250 |
fix(core): composition scripts start after web fonts are ready, so renders repeat (#4980)
* fix(core): composition scripts start after web fonts are ready A scene script that measures text (offsetHeight, getBoundingClientRect) ran while the page parsed, often before its web font loaded, so it laid the scene out with fallback metrics. Renders of the same film differed run to run, and preview could differ from export. The compilers now give every body script a type the browser does not run (text/hf-after-fonts, or +module), through one helper in scriptRuns.ts called at the end of the bundler, the producer compile and the sub-composition preview. The Studio motion script is emitted the same way so it still runs after them. The runtime entry owns the wait: on DOMContentLoaded it forces a layout, waits for document.fonts.ready or FONT_WAIT_TIMEOUT_MS (5 s), then re-creates each script in its own place in parser order (classic in order, awaiting src loads; then module and defer scripts in insertion order), boots as before, and calls the DOMContentLoaded and load listeners the scripts added. On timeout the scripts run anyway and runtime_font_wait_timeout names the families still loading. Scene swaps and the composition loader use the same wait. * fix(core): a page whose runtime predates the font gate still runs its scripts A bundle can be served with a runtime loaded from HYPERFRAME_RUNTIME_URL. If that runtime is older than the font gate, nothing would run the deferred composition scripts and the film would be blank. Whenever the marking helper defers scripts, it also puts one small inline script first in the head. At DOMContentLoaded it runs the deferred scripts in parser order itself, without waiting for fonts and with a console warning, unless a runtime has claimed them. The gate's runtime claims them when it loads, before DOMContentLoaded, so the scripts run exactly once either way. * fix(studio-server): motion paths animate in Studio preview when gsap loads in the body The MotionPathPlugin tag Studio inserts after a body gsap script now carries that script's type, so the runtime runs it right after gsap once fonts are ready, instead of before gsap exists. * fix(cli): layout, motion-shot and validate wait for the composition scripts to run The three commands waited on the timeline registry, which exists as soon as the runtime file loads, or on a flat sleep. They now wait for the runtime's render-ready flag through one shared helper, keeping each command's timeout and proceeding as before when it passes. * fix(core): a script that wraps addEventListener keeps its wrapper after its scripts run While composition scripts run, the runtime patches addEventListener to hold load events that already passed. Putting the original back now happens only if the patch is still in place, so a library wrapper installed meanwhile survives. * docs: comments on script timing match scripts that wait for web fonts The early stub's flush and the player's iframe-load handler described page scripts as done by DOMContentLoaded and the runtime as ready before load. Composition scripts now wait for web fonts, so both can come later. Comment-only. * test(core): pin script order after an external script and the scene swap's font wait An inline composition script that follows an external one sees its global, and a scene swap runs the new scene's script only once web fonts are ready. * docs(producer): the early stub comment names what drains late font-gated work * fix(studio): motion paths add without a flash when gsap loads after web fonts Studio preloads MotionPathPlugin on iframe load. A body gsap now runs after web fonts, often after that load, so the preload retries once the runtime reports ready. * test(cli): pin the runtime wait before each command first samples the page * fix(studio): the motion path preload stops waiting once the preview runtime has booted A composition with no global gsap posts ready before the preload runs; the retry listener then never detached and kept each retired preview document alive. It now waits only while the runtime has not booted, and the tests pin the cleanup and the ready filter. * fix(studio): a preview frame that never boots is not kept alive by the motion path preload Only the latest preview waits for the ready message of its runtime; a newer load drops the older wait, so a crashed or missing composition frame can be collected. * style: comments added by the font gate move into names |
||
|
|
18a922f433 |
fix(media-use): a slow download no longer leaves an empty asset that undo brings back (#4989)
* fix(media-use): a slow download no longer leaves an empty asset that undo brings back * docs(media-use): tell the person an OAuth sign-in makes voice and avatar video free * docs(media-use): the OAuth free usage line names the monthly allowance * docs(media-use): the OAuth line says a free allowance |
||
|
|
0ca69472d9 |
feat(cli): feedback says whether a person or an agent wrote it, with the email an app attaches (#4984)
* 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 |
||
|
|
de9bf487d4 |
fix(cli): publish with a refused login refreshes or asks you to log in again (#4964)
* fix(cli): publish with an expired login stops and asks you to log in again The server now answers 401 to a rejected credential instead of publishing anonymously, so the CLI says so and drops the warning for the old fallback. * fix(cli): publish refreshes an expired login first, and names a rejected API key An expired access token with a refresh token is refreshed (the cloud commands already do) instead of being sent and refused. Only the requests that carry the credential map a 401; an environment API key is named, since logging in would not replace it. * fix(cli): an expired login without a refresh token asks you to log in again A stored login past expiry with no refresh token now resolves as LOGIN_EXPIRED instead of "not signed in", so publish stops with the re-login message before sending anything. A signed-in publish that the server answers with an unclaimed project fails the same way and links nothing. * fix(cli): keep the resolver comment share at its base * fix(cli): an API-key publish still succeeds, and only an owned publish is linked * fix(cli): --update and --space refuse an API key instead of being ignored * fix(cli): publish uploads with the credential it checked, so --update and --space are never dropped * fix(cli): a login that expires during the bake is refreshed, not reported as expired * fix(cli): an expired login that cannot refresh stops publish before anything is sent * fix(cli): a token refresh never writes over a login that replaced it * fix(cli): a refresh that cannot read the saved login reports that, not a changed login |
||
|
|
cd1a34ceca |
fix(studio): one owner for gsap CDN URLs, so MotionPathPlugin matches the composition's gsap (#4976)
* fix(studio): load MotionPathPlugin at the composition's own gsap version * fix(core): list the gsap-cdn subpath where core's exports are generated from * fix(core): every default gsap URL comes from the one gsap-cdn owner * fix(core): the gsap a new composition or first tween gets comes from the gsap-cdn owner |
||
|
|
8c81efb7c2 |
fix(cli): close every browser on SIGTERM and SIGINT via one launch owner (#4910)
* fix(cli): close every browser on SIGTERM and SIGINT through one launch owner * fix(cli): own SIGTERM before the GPU probe browser launches * fix(cli): keep tests on the managed GPU mode and drop the engine signal flag * test(cli): give the browser fallback fake a process handle * test(cli): signal the GPU probe case only once the signal owner is installed * test(cli): mark the launching case only after the signal owner is installed |
||
|
|
9123628d94 |
fix(studio-server): time history idle limits on one clock so a slow machine cannot flip them (#4924)
* test(cli): time history turns on one injected clock instead of real waits * fix(studio-server): end an idle history window by its own clock, not a bare timer * fix(studio-server): cap a history window's idle timer at Node's timer range |
||
|
|
7348933111 |
feat(cli): expose harness subscription usage to agents (#4919)
* feat(cli): plan video creation around remaining harness usage * feat(cli): read Codex and Grok subscription allowances * test(cli): load usage fixtures through portable file URLs * test(cli): keep fixture writes on filesystem paths * refactor(cli): separate usage window and login decisions * fix(cli): guard parsed credential array entries * refactor(cli): report harness usage without workflow policy * fix(cli): preserve usage when subscription tier is null * test(cli): separate usage fixture code from inputs * style(cli): format usage transport fixture * refactor(cli): isolate usage fixture credential checks |
||
|
|
ada4e0d289 |
fix(studio): show each timeline row its own layer instead of a flat bar (#4917)
* fix(studio): show each timeline row its own layer instead of a grey bar A row got a frame only if it spanned under 92% of the film and its id did not look like a backdrop; every other row drew a plain grey bar. Every element row now gets its frame, and the capture fades the row element's siblings first, so the frame shows that layer and not the whole film. Studio's dev server and the CLI share one capture helper; Studio's own copy is gone. * fix(studio): read timeline rows from any realm so each keeps its selector * refactor(studio-server): always isolate the element in a thumbnail clip * fix(cli): undo a thumbnail's isolation before its page takes the next capture * chore(core): generate the dom-realm export and trim thumbnail clip comments * fix(studio-server): restore sibling opacity after a clip, hash selectors in cache keys * fix(studio-server): keep faded siblings in a page-owned list so restore is exact * feat(studio): draw a text layer row as its own live text instead of a captured picture * refactor(studio): move timeline text reading into its own module * fix(studio): keep text rows readable and current, and leave painted layers as pictures * fix(studio): refresh a canvas-edited text row by its own identity and source file * fix(studio): refresh a canvas-edited text row known only by its data-hf-id |
||
|
|
0d3d423200 |
fix(cli): select containing source frames in snapshots (#4809)
* fix(cli): select containing source frames in snapshots * fix(cli): find the containing frame from packet timestamps, not a full decode Probing decoded frames and selecting by index decoded the source from its start, so a sample 290 s into a 1080p clip took about 35 s and hit the 30 s extraction bound. Packet timestamps need only a demux, and an accurate -ss to the midpoint before the containing frame decodes from the nearest keyframe. * fix(cli): end snapshot's ffprobe options with -- before the input * fix(cli): seek snapshot frames from their keyframe and skip unreadable videos Fragmented MP4 seeks by dts and MPEG-TS seeks imprecisely, so an input seek to the frame midpoint returned the next frame before each keyframe (fMP4) or a corrupt or missing frame (TS). Seek to the keyframe that starts the frame's decode instead and pick the frame by decoded time with a select filter. A failed or unparsable ffprobe now returns null, so one unreadable video is blanked with a warning instead of aborting the whole snapshot. * fix(cli): keep open-GOP leading frames and extract without ffprobe The keyframe walk went by decode order, so an open-GOP leading frame seeked to the keyframe that follows it, the decoder dropped it and the next frame was returned. Only keyframes at or before the frame in presentation order count. Without ffprobe, extraction falls back to a plain seek instead of failing the snapshot, as it worked before frame selection needed ffprobe. * fix(cli): measure snapshot frame times from the exact video start ffprobe prints format.start_time rounded to microseconds. When it rounds down, t=0 found no frame and every exact frame time picked the previous frame (24 fps fragmented MP4 with B-frames, many MPEG-TS files). Subtract the stream's integer start_pts in time-base ticks when the video sets the format start. * fix(cli): probe snapshot packets to an absolute end past edit-list preroll A stream-copy trim keeps preroll packets before the first visible frame. The packet probe read a window relative to the first packet, so it stopped before the requested time and picked an earlier frame. Probe the stream start first, then up to start + time + 1 s. * fix(cli): keep snapshot ffprobe argv a literal ending in -- before the input The producer ffprobe argv contract reads each argv literal and requires -- right before the input; the probe helper spliced flags in from its callers, so the contract saw argv without it. * fix(cli): select snapshot frames on the video stream clock when audio leads When the video starts after the container (TS with audio, late video), FFmpeg puts zero at the first video frame after a seek to 0, so the threshold picked the next frame, and times before the first frame gave none. Select on raw stream time (-copyts) and hold the first frame. * fix(cli): measure snapshot frame selection from the first decoded frame -copyts kept raw time in the filter but changed which frames survive the seek trim, so a file whose timestamps start below zero froze on a late frame. Seek to the keyframe display time and select relative to start_t; the no-ffprobe path keeps the plain seek. * fix(cli): anchor snapshot frame selection on the keyframe a seek lands on MPEG-TS seeks can land a whole keyframe group late, and an open-GOP keyframe is not a clean start, so a predicted start_t picked later frames. Read the landing keyframe with a stream copy, seek exactly to it so its leading frames are trimmed, and select relative to it. --------- Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |
||
|
|
fd2f90c7d8 |
fix(cli): a background preview launch keeps only the server its own child started (#4914)
* fix(cli): a background preview launch keeps only the server its own child started * fix(cli): the detached preview child always starts its own server |
||
|
|
4178a8f95d |
fix(cli): reject extra positional arguments on every command (#4905)
* fix(cli): reject extra positional arguments on every command * fix(cli): read the command name without assuming a resolved meta * fix(cli): join free-text words for catalog and tts, reject a bad repo root * test(cli): load the listed commands through static imports * fix(cli): keep a tts file path out of the word join and read the parsed json flag * fix(cli): apply the file exception only to a tts .txt path * fix(cli): keep usageLine private and test the json fallback for undeclared json * fix(cli): keep extra arguments and the repo root value out of telemetry * test(cli): type the skills update lookup * fix(cli): read json mode from the parsed flag, reject tts words with --text-file, restore add's hint * fix(cli): check tts words with --text-file before --list and test --json=false |
||
|
|
fcc637068c |
fix(cli): honor an explicit --port for background preview reuse and launch (#3915)
* fix: honor explicit --port when reusing a background preview server startBackgroundPreview never compared a reuse candidate's actual bound port against the caller's explicitly requested --port before returning it, so a second `preview --port <N>` call could silently return an existing server on a different port instead of honoring the request. Reuses the existing PreviewServerPortMismatchError (already used by findPreviewServerForProject for the same discipline) instead of adding a new error type: an explicit --port that doesn't match the reuse candidate now throws a clear conflict rather than substituting the wrong port silently. A bare launch with no --port keeps reusing any project-matching server, unchanged. Deliberately out of scope: the interactive/embedded launch path (runEmbeddedMode -> findPortAndServe) has the same silent-substitution shape when a same-project server is found mid-scan at a port other than the one requested. That path's output isn't the JSON lifecycle schema this bug was reported against, and fixing it would mean touching the interactive dev-server bind/scan loop rather than a simple pre-return check, so it's left as a follow-up candidate. Co-Authored-By: Miguel Angel <miguel.sierra@heygen.com> * fix: honor explicit --port on every background preview exit path `startBackgroundPreview` already refused to reuse a same-project server on the wrong port when --port was explicit, but the fresh-launch path still trusted whatever port the detached child bound. The child scans upward from --port and takes the first free port, so `--background --port N` with N busy reported `{ type: "started", port: N+1 }` and recorded ownership of it. - After the child comes up, compare its port with the explicit --port. On a mismatch, reap the wrapper, wait until the substitute stops answering, and throw `PreviewPortUnavailableError` (JSON code `preview-port-unavailable`). If the substitute never stops, fail loudly instead of reporting it. - Share one `unmetPreferredPort` predicate between the reuse and launch paths, and one `awaitServerGone` wait between the reap path and `stopBackgroundPreview`. - When several same-project servers are running, prefer the one on the explicit --port as the reuse candidate instead of the lowest port. - Map launch failures to JSON codes in one place (`backgroundStartFailureCode`). Tests: preferred port bound → reported as-is; preferred port taken → reaped and rejected with both ports, no session record; substitute that keeps serving → distinct error; bare launch keeps the next free port; explicit port picks the matching sibling. Deleting the launch-path check, the candidate ordering, or the post-kill wait each turns its test red. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(cli): reuse the same-project server on the explicit --port `startBackgroundPreview` picked its reuse candidate before honouring `--port`, so with an owned server on 3002 and an unmanaged same-project server on 3003, `--port 3003` rejected with "No Studio preview server … on port 3003. Matching server port: 3002" even though 3003 was running. - A policy-matching same-project server already on the explicit port is now the reuse candidate, owned or not; the owned server stays authoritative otherwise, and an explicit GPU-policy change on the requested port still replaces it. - `PreviewServerPortMismatchError` receives every policy-matching same-project server, so the ports it reports are the ones running. - `policyMatchingServers` is the single owner of the same-project + GPU-policy predicate; `matchingServer` derives from it. - `BackgroundPreviewResult` names the launch result union once; `PREVIEW_PORT_MISMATCH_CODE` replaces three literal error codes. Tests: unmanaged sibling on the explicit port is reused without touching the owned server; mismatch error lists every same-project port and no foreign-project port; owned server on the explicit port is replaced on a GPU-policy change. Each new check fails when its guard is removed. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * refactor(cli): keep the --port honour comments short * fix(cli): drop the scan binding stopBackgroundPreview no longer reads * refactor(cli): keep comment share at base in the preview lifecycle files * fix(cli): reuse a same-project server on an explicit --port beyond the scan window * test(cli): pin the JSON codes for a background start that misses --port * refactor(cli): probe an out-of-window --port in its own helper --------- Co-authored-by: Miguel Angel <miguel.sierra@heygen.com> |
||
|
|
821e8c8ad0 |
fix(cli): hide console windows on Windows for background/detached spawns (#3996)
* fix(cli): hide console windows on Windows for background/detached spawns Add windowsHide: true to 8 spawn/execFile call sites that were missing it: ffprobe/ffmpeg probes in the producer and engine packages, the telemetry exit-flush and studio dev-server spawns, and the browser-open and background-preview detached launches in the CLI. previewLifecycle.ts's spawnDetachedPreview redirects stdout/stderr to a raw log-file descriptor; libuv only adds Windows' CREATE_NO_WINDOW flag when no stdio slot inherits a raw handle, so windowsHide alone does not fully suppress a console flash there (documented inline) — it's still added for consistency and because it correctly sets the process's show-window state regardless. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(cli): correct windowsHide flash-risk claims and add missing dev-server spawn tests - The previewLifecycle.ts comment (and its mirrored test comment) overstated the residual console-flash risk on the detached background preview spawn: DETACHED_PROCESS already leaves that child without a console regardless of whether CREATE_NO_WINDOW gets applied. - Add the two missing regression tests for preview.ts's runDevMode and runLocalStudioMode spawns, exporting both for test access. - Drop an unused export in audioPadTrim.ts flagged by the repo's dead-code gate; the constant is only ever used within its own file. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(engine): hide console window for the drawElement self-verify PSNR comparison psnr.ts's execFile call runs in-process inside the Studio server's render path (parallelCoordinator.ts's drawElement disk self-verify), so a render started from Studio's UI under a detached preview hits this call from a console-less parent — the same flash case the other windowsHide sites in this PR already cover. psnrFilterAvailability.ts's probe is on the same in-process render path and already had the flag. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(cli): keep only the spawn sites main still misses and leave browser windows visible windowsHide maps to libuv UV_PROCESS_WINDOWS_HIDE, which also starts GUI apps with SW_HIDE, so the detached browser launch must not set it. The background preview child already has no console (DETACHED_PROCESS with inherited log fds), so its flag was a no-op. The telemetry, psnr and audioPadTrim sites shipped on main via #3839 and #4604. * fix(cli): stop the studio dev server when the terminal closes With windowsHide the dev server runs in its own hidden console on Windows, so closing the terminal only signals the wrapper (SIGHUP). Handle it like SIGINT and SIGTERM and kill the process tree. * fix(studio-server): hide the console window for loudness measurement The background preview has no console, so its ffmpeg loudness run opened a visible window on Windows. * refactor(cli): drop the comment on the studio shutdown signals --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
f16e509832 |
fix(cli): stop browser --force from deleting chrome under running renders (#4901)
* fix(cli): stop browser --force from deleting chrome under running renders * style(cli): format browser manager test * fix(cli): sweep leftovers of killed browser installs and on browser clear * docs(cli): state what browser clear returns without history wording |
||
|
|
d865b5a7cb |
fix(skills): stop mirroring skills into Codex and remove old mirror links (#4105)
* fix(skills): avoid duplicate Codex skill mirrors * fix(skills): remove Codex mirror links left by earlier skill updates * fix(skills): guard the Codex mirror cleanup and keep links without a universal copy --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
b31fb02c7e |
fix(cli): distinguish a skipped browser check from a genuinely clean one (#3901)
* 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> |
||
|
|
a7e95c0e37 |
fix(cli): stop lambda deploy from pointing at a nonexistent --sam-template flag (#3879)
* fix(cli): stop lambda deploy/destroy from pointing at a nonexistent --sam-template flag locateSamTemplate()'s error message and doc comment both promised a --sam-template CLI flag as an installed-package fallback, but no such flag was ever wired up anywhere in lambda.ts/deploy.ts/destroy.ts, and examples/aws-lambda/template.yaml isn't shipped in any published package either -- a dead-end escape hatch that just wasted users' time. Replace it with the honest requirement (matches repoRoot()'s own message): these commands need a HyperFrames monorepo checkout, or HYPERFRAMES_REPO_ROOT pointed at one. * fix(cli): scope the missing sam template error to lambda deploy --------- Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |
||
|
|
bad0bc7ce2 |
fix(cli): recapture snapshot composites after video injection (#4767)
* fix(cli): recapture snapshot composites after video injection * test(cli): guard snapshot composite recapture ordering |
||
|
|
ded5fa8661 |
fix(cli): reject extra positional arguments passed to add (#3891)
* fix(cli): reject extra positional arguments passed to add `add <a> <b> <c>` silently installed only `<a>` and exited 0, discarding `<b>` and `<c>` with no warning. citty binds only the first positional token to `name`; every token after it still lands in `args._`, but nothing read it. `add` now rejects the extra arguments up front, before touching the registry, naming exactly what was dropped. Co-Authored-By: Miguel Angel <miguel.sierra@heygen.com> * refactor(cli): inline the add extra-argument message and test it through citty parsing --------- Co-authored-by: Miguel Angel <miguel.sierra@heygen.com> |
||
|
|
e8659cb653 |
fix(cli): recognize textual motion in sweep checks (#3707)
* 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> |
||
|
|
1a619785d5 |
fix(cli): keep check --json stdout pure by sending proxy pre-resolve diagnostics to stderr (#3836)
* fix(cli): keep check --json stdout pure by sending proxy pre-resolve diagnostics to stderr preResolveHostileMediaProxies() logged its summary and scan-failure lines via console.info, which writes to stdout. With autoProxy on (the default), any project containing HEVC/ProRes/AV1 media emitted that line ahead of the JSON envelope from `check --json`, so JSON.parse(stdout) failed for consumers of the machine-readable output. Route both diagnostics through console.error (stderr), matching how the check command already reports its own failures. Add regression tests asserting the summary and scan-failure lines land on stderr and that stdout stays untouched. * refactor(cli): shorten the pre-resolve stderr comments * refactor(cli): drop the pre-resolve doc line the comment ratchet rejects --------- Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |
||
|
|
5aa3c8f72f |
fix(cli): connector dash gate honours pathLength (#3954)
* fix(cli): connector dash gate honours pathLength `shaftDashHidden` compared the stroke's dash window against `getTotalLength()`, but a `pathLength` attribute rescales every distance along the path, so the CSS draw-on idiom (`<path pathLength="1">` with `stroke-dasharray: 1; stroke-dashoffset: 1`) — which renders zero ink — still fired `connector_detached`. The window is now judged in the author's units: `pathLength` when set (`SVGAnimatedNumber.baseVal`, 0 when unset), the real length otherwise. `dashLength`/`dashArrayLengths` stay the only owners of parsing and units. Two adjacent changes to the gate's arithmetic: - The `total > period` shortcut is removed so the 10% rule applies uniformly. A window that spans a whole period without enclosing a whole dash (`2 97` on a 100-long path at offset 0.5 paints 3%) is hidden, like any other stroke painting under 10%; any pattern that does enclose a whole dash is still caught by the in-loop check. - The dash-offset wrap adds the period only to negative remainders. The previous `((offset % period) + period) % period` is off by one ulp for fractional offsets (`0.9 % 2` → 0.8999…), which flipped the exact 10% boundary once dashes are expressed in `pathLength` units. Tests: rows for `pathLength="1"` (`1/1` hidden, `1/0.9` on the boundary hidden, `1/0.5` painted, `0.1 0.9` round dot painted), a negative offset that only the wrap gets right (`100` at -150), a percentage row (`10%` resolves against the viewport, not as a bare number), and the `2 97` full-period row. `installConnectorGeometry` mirrors the DOM's `pathLength` for happy-dom, which has no SVGGeometryElement. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(cli): treat a zero pathLength as a solid stroke in the dash gate A present `pathLength` that resolves to 0 (`"0"` or an unparseable value) zeroes the browser's dash scale factor, so the stroke paints solid. The gate saw `baseVal` 0, fell back to the real length, judged the window inside a gap and dropped the shaft, suppressing a real `connector_detached` finding. Return painted for that case. Add a `pathLength=0` row to the dash table and make the happy-dom `pathLength` mirror parse like the DOM (finite SVG number only; `Infinity`, hex, `1.` read as 0). Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(cli): check a zero pathLength before the butt-cap dots shortcut A pathLength resolving to 0 paints the stroke solid, so a butt-capped `0 4` pattern on it is visible; the dots shortcut used to call it hidden first. Also trims the dash-table comment. * test(cli): keep the pathLength helper apart from the geometry installer comment --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
d2cdad418a |
fix(cli): refuse capture folders that resolve outside the output root (#4789)
* fix(cli): refuse capture directories that resolve outside the output root writeCaptureFileSync guards the final path entry, but a directory in the path that is itself a planted symlink (assets -> ~/somewhere) still carried the staging directory and the rename into wherever it pointed, with a filename the captured page chose. Every capture directory is now created through ensureCaptureDirSync(root, dir), which walks from the output root one level at a time and refuses any directory whose real path is not the one it has under the root, before creating anything inside it. The root itself stays trusted as given, so a symlinked root like /tmp on macOS keeps working. Fixes #4304 * fix(cli): vet assets/svgs before the SVG contact sheet writes into it Under --skip-assets, downloadAssets never runs, so nothing checked assets/svgs before createSvgContactSheet wrote its thumbnails and contact-sheet.jpg into it; a planted assets/svgs link received them. The sheet now runs ensureCaptureDirSync on the directory first, and a refusal just skips the sheet. A dangling or looping directory link is now refused with the same message instead of a raw ENOENT/ELOOP, and a comment records why the check uses the JS realpathSync rather than realpathSync.native. Refs #4304 * fix(cli): vet capture --video's download folder Video mode created assets/videos (or capture/assets/videos in a W2H project) with a plain mkdirSync, so a planted assets or capture link carried the downloaded clip outside the project. It now goes through ensureCaptureDirSync with the project folder as the trusted root, and a refused folder is reported before anything is downloaded. * fix(cli): warn when a best-effort capture phase refuses a folder Folder refusals now throw CaptureDirRefusedError. The contact sheets and the video manifest still skip on any error, but a refusal adds a warning so a planted link doesn't go unnoticed. saveLottieAnimations now vets assets/lottie itself, taking the output root like renderLottiePreviews, and every folder check in the capture pipeline is pinned by a test. * style(cli): trim the ensureCaptureDirSync doc comment to two lines --------- Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |
||
|
|
8aefd91bf8 |
fix(cli): capture snapshot --at at the exact requested instant (#4827)
snapshot --at captures the frame at the exact requested instant instead of the nearest seek step. |
||
|
|
efdabb758d |
fix(cli): connector_detached now skips fully dash-hidden connector shafts (#3912)
* fix(cli): connector_detached now skips dash-hidden connector shafts connector_detached fired on SVG connector paths that were 100% hidden via stroke-dashoffset/stroke-dasharray draw-on entrances, evaluating rendered/user-space geometry unconditionally. The sibling connector_orphan check already gates on shaftDashHidden(path); add the same gate here, in the same position. Co-Authored-By: Miguel Angel <miguel.sierra@heygen.com> * fix(cli): connector dash gate reads the whole stroke-dasharray; one owner for connector enumeration `shaftDashHidden` read only the first `stroke-dasharray` entry, so any pattern starting with 0 (`0 4` dotted) or a long first dash counted as hidden and suppressed both connector findings on a visible stroke. It now resolves the full computed list (comma/space separated, units, percentages against the SVG viewport diagonal, odd lists repeated; `none`, all-zero and negative lists render solid) and treats the shaft as hidden only when the window `[dashoffset, dashoffset + length]` sits in a single gap, with up to 10% of the length overlapping a neighbouring dash. Zero-length dashes paint only under `stroke-linecap: round | square`; under the default `butt` they render nothing, which also covers the `0px, 999999px` state a finished draw-off tween leaves behind. `connector_detached` and `connector_orphan` each walked SVG paths with their own copy of the candidate/gate logic, which had already drifted. A single `connectorShafts(root)` generator now owns enumeration, the dash gate and endpoint resolution, and yields the `painted` verdict; both findings consume it. The path length is queried once per shaft (`pathUserEndpoints` returns it) instead of twice, and the attach threshold formula has one owner. Tests pin `0 4` (butt and round), `0, 4`, `4 0`, `0`, `none`, px units, negative offsets, a draw-off end state, and both sides of the 10% boundary. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * refactor(cli): shorten the connector and dash-window comments * test(cli): shorten the dash-gate table comment --------- Co-authored-by: Miguel Angel <miguel.sierra@heygen.com> |
||
|
|
0eba8d6a06 |
fix(cli): route FFmpeg exec-failure hint through getFFmpegInstallHint() (#3877)
* fix(cli): route FFmpeg exec-failure hint through getFFmpegInstallHint() checkFFmpeg()'s "cannot start" branch (binary found but fails to run -version) hardcoded a Windows/DLL-flavored install hint on every platform instead of delegating to the already platform-aware getFFmpegInstallHint() used by every other branch in this file. * test(cli): pin the ffmpeg cannot-start hint through the async probe mock * fix(cli): keep the runtime-DLL cannot-start hint on Windows --------- Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |
||
|
|
0b88cd0537 |
chore(deps): update node.js (#4245)
* chore(deps): update node.js * fix(deps): keep the Node 22 engines floor at >=22 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
45b8e171e2 |
feat(studio): sync origin, out-of-sync repair, linked selection toggle and grouped clip menu (7/8) (#4820)
* feat(studio): order the clip menu into time, sound, picture and delete groups Split and Freeze frame; Normalize, Voice, link items and Duck; Look and Crop; then Delete beside Delete this clip only. Each item file renders its pieces separately, empty groups collapse, and the viewport clamp uses the measured menu height instead of a row-count estimate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio-server): mint freeze-frame right-half links with the shared lk-N helper Freeze frame now calls core relinkSplitHalves, so split halves and freeze halves share one link-id convention instead of L -> L-2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(studio): apply speed edits to every data-link member in one undo step The inspector speed slider, rate lane edits and ramp presets on a linked clip now commit once, writing the selection and, via expandToLinkedMembers, each partner from its own current value. Unlinked clips keep their usual path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(studio): drop the audio parent tick in favour of the data-link chain badge A linked clip now shows one link indicator: the chain badge driven by data-link. The waveform no longer draws its 1px parent tick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): follow late clip-menu rows and target link partners like S2 The menu height follows a ResizeObserver so rows that read the live preview after mount still clamp inside the viewport. Speed fan-out builds partner targets with buildPatchTarget, matching the link edit plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): show the sound strip and audio menu items without studio's shell The audible-video waveform strip drops AudioWaveform's 16px label band, which left a 0px canvas on short lanes. Normalize loudness and Duck under voice fall back to the timeline session's project, the live preview iframe store and a new onNotice edit callback when no StudioShellProvider is mounted. usePreviewIframeStore is exported so a host can publish its preview. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): show duck under voice only when a voice overlaps The item now appears only when an overlapping voice exists or the bed is already ducked. Its check mark moves to the right edge so unchecked rows align with siblings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(studio): name the partner in the clip menu link and unlink items Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): clip menu stops offering unlink once the clips are unlinked The timeline skipped store updates when only data-link changed, so the menu kept reading the stale link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(core): sync origin, offset math and move/slip into sync for linked pairs data-sync-origin survives Unlink; split gives right halves a fresh origin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio-server): split re-mints the sync origin of unlinked pairs too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(studio): detach, link and keep-sound cutout write a sync origin that unlink keeps TimelineElement.syncOrigin is read on both parse paths; merge back drops it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(studio): expect sync-origin on keep-sound cutout write Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(studio): linked selection toggle in the timeline toolbar, on by default Off: a click selects one clip; drag, trim, razor, speed and delete stay on it (delete unlinks the survivor). Alt-click still selects one half when on. Right-click holds the out-of-sync indicator preference. Both persist in UI prefs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(studio): link groups share a label colour; out-of-sync badges with move and slip Red badge on both halves of a sync-origin pair that drifted (frames, s:ff past a second); hidden when rates differ or the preference is off. The whole badge opens Move / Slip into Sync; each edits that clip alone in one undo step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): take link label colours from theme tokens Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(sdk): syncOffset, moveIntoSync and slipIntoSync for sync-origin pairs Core gains findSyncPartner, the DOM pairing shared by the SDK and the CLI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(cli): timeline rows report syncOffsetFrames and print out-of-sync Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cli): the timeline row fixture carries syncOffsetFrames Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): data-sync-origin in hyperframes-core Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(core,sdk): keep a one-sided cut's sync origin and slip the in-point playback reads relinkSplitHalves reminted the right half's sync origin even when only one member of an unlinked source pair was cut, so the uncut partner kept the old origin and the new half lost its badge. The origin is now reminted only when both the video and the audio were cut. Slip into Sync wrote data-media-start whenever it existed, but playback reads data-playback-start first, so a clip carrying both never moved. The link timing reader now uses playback's precedence and the slip writes the attribute that wins; lint compares the same field. A sync partner is also looked up only in the clip's own composition (core) or source file (Studio). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): flip the out-of-sync menu above the pointer near the bottom edge Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: trim comments the sync-origin layer added Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(core): split the scoped candidate lookup out of findSyncPartner Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(core,studio): find sync partners inside the link scope findSyncPartner and Studio's syncPartnerOf now scope by the same rule as link groups: the nearest data-composition-id or data-composition-file ancestor, plus the row's compositionScope in Studio. An origin reused in a file host or an inline composition no longer pairs across it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
824e4100c6 |
feat(studio): waveform strip, clip light, peak marks and video beat source (5/8) (#4817)
* feat(studio): draw a waveform strip under video clips that carry sound An audible video keeps its thumbnails and gains the audio waveform along the bottom 38% of the clip (also when thumbnails are off); muted and silent video are unchanged. The waveform route already decodes video audio; a test now proves it on an mp4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(studio): latch a CLIP light on the master meter at the export ceiling The light turns red when the preview master sample peak reaches -1 dBFS (the export true-peak ceiling) and stays lit until clicked. Mutation-tested threshold and latch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): paint the CLIP light from theme colour tokens Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(studio): mark where each clip redlines on its waveform studio-server computes an absolute per-50ms sample-peak map once per media file (native channels, cached by size+mtime next to the waveform cache); the timeline paints red marks where the source at the clip's own volume reaches -1 dBFS and names the loudest point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(studio): let a video with sound be the beat source Studio's isMusicSourceElement accepts an audible, unmuted video for the music track and the beat fallback (lane zoning unchanged); core findMusicAudioSrc accepts <video data-has-audio="true"> without muted; the beats CLI message mentions video. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): decode each peak map once and never offer Duck on a carve source Concurrent peak requests for one file share a single ffmpeg decode; Duck under voice is withheld on a clip another bed already carves against. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio-server): decode the same audio stream the peak map probes The channel probe read stream a:0 while the decode left stream choice to ffmpeg, which picks a later stereo stream over a mono first one; every frame was then read at the wrong width, doubling the bins and taking the other stream's level. Both commands now select a:0, and the cache key moves to peaks-v2 so maps built from the wrong stream are rebuilt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio-server): terminate the peak map's ffprobe options before the input Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio-server): reject a peak map whose decode exits nonzero A decode that emits some samples and then fails no longer resolves as a complete map, so /peaks cannot cache a truncated clipping map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9244c47328 |
feat(studio): normalize loudness and duck under voice, with a limiter report (4/8) (#4816)
* feat(studio-server): share loudness measurement with an absolute target and video support normalize-audio moves its measure/plan code into @hyperframes/studio-server/loudness, gains an absolute mode (--lufs, default -16, clamped at +12 dB and -1.5 dBTP) and accepts <video> clips with sound. Adds the @hyperframes/core/audible-video subpath. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(studio-server): add a measure-only loudness normalize endpoint POST /projects/:id/loudness/normalize measures a clip's played window (audio or video) and answers the -16 LUFS data-volume plan; Studio writes the attribute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: register the loudness and audible-video subpaths in their descriptors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style: format the loudness and audible-video subpath descriptors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(studio): add Normalize loudness and Duck under voice to the clip menu Normalize measures the clip on studio-server and writes data-volume as one undoable edit; Duck toggles a carve against every overlapping voice (audio or video with sound) at the default strength. Both live in clipMenuAudioItems with one hook-in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(studio): tighten carveOutput docs to the package comment share Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(producer): report how far the true-peak limiter lowered the export The AAC true-peak pass now returns its attenuation; the render logs "Audio lowered by X.X dB to stay under -1 dBTP" and sets RenderJob.audioLoweredDb, which Studio servers carry in the complete SSE event and the /renders record. Also exports useAudioMetersVisible from @hyperframes/studio for hosts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio-server): scan HTML start tags without a backtracking regex The start-tag regex backtracked quadratically on '<' followed by many spaces (CodeQL js/polynomial-redos). A linear character scan reads the same name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): refuse to normalize a volume-automated clip and honour save outcomes A volume lane owns the clip's gain in preview and render, so writing data-volume changed nothing while the toast claimed success; Normalize now refuses with a clear message. Normalize and Duck also check each quiet-save outcome, stop at the first failed or refused write and report it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(core): drop the duplicate audible-video subpath descriptor Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
37f30b1d06 |
feat: keep a video's sound on the clip by default (1/8) (#4813)
* 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> |
||
|
|
c72b735a8c |
fix: a save's temp file no longer reaches Studio as an outside change (#4857)
* fix: a save's temp file no longer reaches Studio as an outside change * refactor: the project file walk and its tests carry no new duplication or complexity * test(studio): the edit accuracy gate judges the two nudge sequences again |
||
|
|
567525a8c2 |
fix: readers never see an empty or half-written project file (#4777)
* fix: readers never see an empty or half-written project file * test: pin the create routes and block install to whole-file writes * fix(sdk): a write through a link to a missing file creates that file * fix: saves through a linked folder reach the real file, atomic writes handle long names * test(sdk): check for the stray file, not the folder listing * fix(core): creates work on Windows FAT drives, and links through folders resolve as the system does * fix: agree the helper move in the deletion check, and match temp paths without a path regex * test: check POSIX file modes only off Windows |
||
|
|
481dae1ed9 |
fix: render names a folder composition source instead of crashing (#4613)
* fix(producer): render names a data-composition-src that points at a folder instead of crashing * fix(lint): name a data-composition-src that points at a folder instead of crashing * fix(cli): compositions, keyframes and Studio insertion survive a folder composition source * fix(lint): tell a folder composition source to point at the HTML file inside it * fix: read a composition source through one file handle so its type check and read cannot race * fix(parsers): report a named pipe as a composition source instead of blocking on it * revert(parsers): open composition sources read-only again, which CodeQL reads as no file creation * fix(parsers): open composition sources with a private mode, skip pipes, and treat bad paths as missing |
||
|
|
f361ee4bc4 | codegen: resync cloud client from EF d1d64f05 (#4716) | ||
|
|
9e1dbca7a2 |
fix(studio): undo and redo update the preview in place instead of reloading it (#4718)
* fix(studio): undo and redo swap the restored scene in place instead of reloading * fix(studio-server): a preview asked for right after an undo shows the undone state * fix(studio): an undo that restores a page outside every scene still reloads in full * fix(studio-server): a preview built across a write is not cached under the old signature * test(studio): a soft undo keeps the scene hashes so the next edit still swaps |
||
|
|
2195db5e1d |
fix(media-use): never replace a voice, music or sound file the person put in the project (#4665)
* fix(media-use): record agent-made voice, music and sound effects in the project's media manifest * fix(media-use): record in place only when --source says the agent made the file * test(media-use): a rewritten agent file keeps one manifest record * fix(media-use): leave a person's own sound effect unlabelled and mark locally made music generated * refactor(media-use): record agent audio in-process through the shared manifest library * style(media-use): format the in-process recording and refresh the skills manifest * chore(skills): refresh the skills manifest for the media-use changes * fix(media-use): report a broken ffprobe setting plainly and document the project sfx source * style(skills): format the pipeline manifest assertion * chore(skills): refresh the skills manifest * fix(media-use): never replace a voice, music or sound file the person put in the project * style(media-use): format the keep-person-files change * chore(skills): refresh the skills manifest * fix(media-use): a regenerated file gets a fresh record, and lookups skip the replaced one * test(media-use): a replaced record no longer answers a prompt lookup in the CLI * chore(skills): refresh the skills manifest * chore(skills): refresh the skills manifest from a clean tree * fix(media-use): never give a recorded file the id of a download in flight * chore(skills): refresh the media-use manifest hash * fix(media-use): say when audio lands on a new name, and never let two lines share one * style(media-use): format the keep-files change * chore(skills): refresh the media-use manifest hash * fix(media-use): keep two sound effects off one file when a name is taken * refactor(media-use): drop a set the one-at-a-time downloads never needed * chore(skills): refresh the media-use manifest hash * fix(media-use): keep two sound effects off one recorded file on a rerun * style(media-use): format the rerun test * chore(skills): refresh the media-use manifest hash * fix(media-use): list only a file's newest record in candidates and the media index * revert(media-use): leave promote as it was, nothing calls it * test(media-use): give the index test's two assets their own files * chore(skills): refresh the media-use manifest hash * chore(skills): regenerate the manifest after merging the base branch * fix(core): a figma import lists each media file once, like media-use |
||
|
|
663b02297d |
feat(cli): add hyperframes clean to find and remove what HyperFrames left on disk (#4701)
* 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 |
||
|
|
e07ea1fc70 |
perf(cli): load each command's code only when that command runs (#4656)
* perf(cli): load each command's code only when that command runs * fix(cli): honest bench percentiles, and no unused registry entry in the split build * fix(cli): the startup bench is a package script and stops on a failed run * style(cli): format the bench's failed-run check * fix(cli): a notice module that fails to load cannot fail the command |
||
|
|
e25bc3a806 |
fix(media-use): record agent-made voice, music and sound effects in the media manifest (#4648)
* fix(media-use): record agent-made voice, music and sound effects in the project's media manifest * fix(media-use): record in place only when --source says the agent made the file * test(media-use): a rewritten agent file keeps one manifest record * fix(media-use): leave a person's own sound effect unlabelled and mark locally made music generated * refactor(media-use): record agent audio in-process through the shared manifest library * style(media-use): format the in-process recording and refresh the skills manifest * chore(skills): refresh the skills manifest for the media-use changes * fix(media-use): report a broken ffprobe setting plainly and document the project sfx source * style(skills): format the pipeline manifest assertion * chore(skills): refresh the skills manifest * fix(media-use): a regenerated file gets a fresh record, and lookups skip the replaced one * test(media-use): a replaced record no longer answers a prompt lookup in the CLI * chore(skills): refresh the skills manifest * chore(skills): refresh the skills manifest from a clean tree * fix(media-use): never give a recorded file the id of a download in flight * chore(skills): refresh the media-use manifest hash * fix(media-use): list only a file's newest record in candidates and the media index * revert(media-use): leave promote as it was, nothing calls it * test(media-use): give the index test's two assets their own files * chore(skills): refresh the media-use manifest hash * fix(core): a figma import lists each media file once, like media-use |
||
|
|
590c192498 |
feat(catalog): add 3D objects, cursors, snow, starfield and paper transitions (#4349)
* feat(catalog): shelve 3D objects and cursors in their own catalog sections Carries the generator grouping rules from #4310 and #4308. * feat(registry): add a starfield ambient overlay From #4313. * feat(registry): add a falling snow ambient overlay From #4331. * feat(registry): add a torn-paper-rip transition From #4321. * feat(registry): add a paper-collage-reveal transition From #4322. * feat(registry): add a curtain-reveal transition From #4323. * feat(registry): add 3D objects: product turntable, glass hero, exploded stack, extruded logo From #4310. * feat(registry): add eight cursor animations and the dart cursor From #4308. * fix(catalog): find items by everyday editor words and accept a bare search word From #4311. * chore(registry): remove the simulated-cursor component The Cursors section's new pointer animations replace it (from #4308). * fix(registry): take cursor-zoom-follow's screenshot from runtime variables only The declared default is empty, so page markup never becomes an image URL. Clears the code-scanning alert raised on #4308. * docs(cli): show a catalog search example that finds items "smoke" matches nothing in the catalog; "crossfade" finds fade-through. * chore(ci): allow the simulated-cursor removal in the deletion guard * fix(ci): skip tracked files missing from disk in the player CDN pin check Catalog regeneration removes a deleted item's pages before the publish PR untracks them, so the check crashed on every catalog deletion PR. * chore(cli): drop comments that narrate catalog search history * fix(registry): drag-drop keeps the family arrow and shows the drop slot only while hovering * fix(registry): agent cursor labels its steps in an accent tag, no glow or sparkles, in 4.8 s * fix(registry): annotate cursor draws a tapered marker stroke and clears the circled value * fix(registry): show real file thumbnails and end the context menu demo sooner * fix(registry): give marquee select real photos, a Finder-blue marquee and a tighter loop * fix(registry): centre the text-select toolbar on the selection and tighten its timing * fix(registry): selection cursors default to the macOS system blue * style(registry): format the agent and annotate cursor sources * fix(registry): dart-cursor stops covering the choice it makes and shows the export finishing * fix(registry): frame the product turntable fully and light it with two studio softboxes * fix(registry): touch-indicator taps like a real phone and the post it taps gets liked * fix(registry): press-ripple confirms with a check and small click rings at the cursor * fix(registry): exploded stack separates at once and marks each layer in blue as it is labelled * fix(registry): extruded logo settles facing the camera, set in Geist Bold with a neutral rim * fix(registry): magnetic cursor docks its toolbar on the selection and ends after the press * style(registry): format the exploded stack source * fix(registry): glyph trail becomes a tutorial cursor path with numbered steps on a settings screen * fix(registry): make glass-hero a clear lens that shows the headline through it * fix(registry): snow reads as real snowfall with bokeh flakes over a winter title card * fix(registry): oversized cursor is a 3.5x arrow that clicks a real editor toggle on * fix(registry): starfield reads as a drifting night sky over a real title card * style(registry): format the glass hero source * fix(registry): make the paper tear show a white fibrous edge and lift the torn sheet away * fix(registry): curtain reveal draws velvet folds that gather onto a title card * fix(registry): make every zoom-follow click change the screen and read at zoom * fix(registry): paper collage pieces stay opaque, tear with deckled grain and lift away on an arc * refactor(registry): drop dead code and correct stale comments in reworked items * fix(registry): agent cursor scrubs cleanly, docs match press-ripple, fictional demo names * docs(registry): catalog notes say oversized-cursor exits and how it differs from press-ripple * docs(registry): plain lowercase comments in oversized-cursor and touch-indicator * docs(registry): oversized-cursor names CLICK_AT as the click cue |
||
|
|
f7ef18dca8 |
fix(producer): renders leave no captured frames behind when interrupted (#4694)
Ctrl+C during a render left its whole work dir of captured frames (and a staging dir) in the renders folder. Puppeteer's default SIGINT handler kills Chrome and exits the process before the render's own cancel path can remove them. The engine keeps that default unless the host owns Ctrl+C; the CLI's render cancellation scope claims it while a render runs, so the render aborts and cleans up after itself. A render killed outright (SIGKILL, crash) cannot clean up, so each work dir and staging dir now records its owner process (pid, hostname, boot, and on Linux the pid namespace), and the next render into the same folder removes the ones whose owner has exited. Cancelling a render while its browsers start no longer leaves anything running that kept a batch render alive after Ctrl+C: the BeginFrame warm-up loop, the probe browser and file server, or a capture browser whose launch finished after the cancel (now closed before the worker settles, since Puppeteer's exit hook skips every other live browser). |
||
|
|
46eedd4dfb |
fix(fonts): resolve declared family names with missing spaces (#4670)
* fix(producer): a misspelled font-family resolves to the family the document declares "Saira ExtraCondensed" in CSS next to an @import of family=Saira+Extra+Condensed was unresolved (Google Fonts answers the typo with HTTP 400), so fail-closed compiles threw FontFetchError. Once every existing path fails, a family whose spelling (case, spaces, -, _, +) uniquely matches a family the document declares in a Google Fonts @import/<link> URL or its own @font-face resolves to it. Faces are emitted under the authored name, so every authored usage matches them. Zero or ambiguous matches keep today's behavior. FontFetchError gains a structured unresolvedFamilies list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): font localization failures name the unresolved font families The failure line keeps its prefix and appends the sanitized names from FontFetchError.unresolvedFamilies, e.g. font localization failed (FontFetchError): unresolved font families: "Foo Bar" The error message, URLs and HTML are still never printed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(producer): keep conditional font declarations out of alias fallback * refactor(producer): bound declared font collection complexity * test(producer): model multi-family Google font responses * test(producer): preserve optional font policy through alias fallback --------- Co-authored-by: terence.cho_tai2 <terence.cho_tai2@heygen.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
26bfd38136 |
fix(studio-server): a save never recreates a project folder renamed away (#4602)
* fix(studio-server): a save never recreates a project folder renamed away * style(cli): wrap imports after merging main * fix(studio-server): proxy and upload answer that the project folder is gone instead of 502 or an empty 201 * test(studio-server): rename the project folder while an upload is being read * fix(studio-server): a proxy or backup answers that a vanished project folder is gone * test(studio-server): clean up the renamed folder after the proxy test * refactor(studio-server): drop an unused re-export and share the folder-gone test setup * fix(core): no folder outside a project is created once the project folder is gone * test(studio-server): a refused render leaves no outside renders folder behind * docs(core): wrap the mkdirWithinProject comment under the line limit * test(studio-server): pass the project folder to the preview document store |
||
|
|
f39b179030 |
fix(cli): an update never replaces the CLI's files while a hyperframes command is running (#4637)
* fix(cli): an update never replaces the CLI's files while a hyperframes command is running * fix(cli): a killed command's leftover pid file cannot hold updates back * fix(cli): judge a stale pid file by the installer's own clock, so sleep cannot drop a live command * fix(cli): after a sleep the installer watches every command's file afresh * docs(cli): shorten the installer script comment * docs(cli): keep the new files within their comment share * refactor(cli): split the upgrade eligibility check out of the installer scheduler * docs(cli): keep the scheduler's doc on the scheduler |
||
|
|
bee6a8e17d |
fix(studio-server): remove the history of a project that no longer exists (#4634)
* fix(studio-server): remove the history of a project that no longer exists * fix(studio-server): keep a project's history while its drive is disconnected * fix(studio-server): keep an older history whose drive may only be unmounted |
||
|
|
27f1cfd23e |
fix(lint): Studio's lint panel and the CLI label a finding by the composition it names (#4618)
* fix(lint): a missing image, audio or mask file names the composition that references it * test(cli): a missing file is reported once per composition that references it * fix(lint): Studio's lint panel and the CLI label a finding by the composition it names * refactor(lint): drop a comment and unused first-entry lines from the grouping change |
||
|
|
7fedc689b8 |
fix: run the configured ffmpeg and ffprobe everywhere and say when an upload was not checked (#4611)
* fix(studio-server): ffmpeg and ffprobe run from the configured binary; unchecked uploads say so * fix(studio-server): the upload result type lists unchecked files * fix(media-use): spawn the configured ffmpeg and ffprobe, and name a missing configured ffprobe * style: format the ffprobe path test, doctor spawns and upload check * fix(studio): show the not-checked upload notice as info, not an error * fix: a configured ffmpeg or ffprobe path that cannot run fails loudly * fix: refuse a folder as the ffprobe path, and report broken paths through --json * test: reuse the file manager test setup and split the upload stream check * fix(media-use): a configured ffmpeg or ffprobe must run -version, and a relative one runs itself * fix(media-use): a configured ffprobe path must name ffprobe, not another tool |