mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-03 12:17:03 +00:00
5383e486bbc6781300ea3e6491affc4231ca337a
238
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
7eb00137a2 |
fix(lint): a missing image, audio or mask file names the composition that references it (#4616)
* 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 |
||
|
|
693c385508 |
fix(cli): commands exit right away even when the network or DNS is broken (#4604)
* 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 |
||
|
|
09ab947e1a |
fix(layout): flag text a clipping card cuts off (#4577)
* fix(layout): flag text a clipping card cuts off A short card with overflow hidden cut the words off, and the check dropped text_box_overflow because that parent clips. The text element's own box does not clip, so clipped_text stayed quiet. A deliberate mask still clears with data-layout-allow-overflow. * fix(layout): ignore a text line parked fully outside its box A reel row that never enters the window was failing with the cut-off card. Only a line that still meets the box and sticks out of it is an overflow. * fix(layout): report visible text outside non-clipping cards * fix(layout): distinguish line boxes from parked font extents * test(layout): type browser audit test interfaces |
||
|
|
5cbef29d3a |
feat(cli): transcribe with Parakeet on macOS, Linux and Windows after an opt-in install (#4583)
* 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 |
||
|
|
ff6e2103d7 |
fix(skills): keep plugin workflows on their installed release (#4607)
* fix(skills): keep plugin workflows on their installed release * docs(skills): clarify plugin manifest discovery * fix(plugins): suppress stale notices and isolate plugin context |
||
|
|
627e9476af |
fix(cli): a failed command sends one error report, naming its real cause (#4586)
* fix(cli): a failed command sends one cli_error, naming its real cause * chore(cli): drop a comment the parameter name already says * fix(cli): a failure caught by its own command is still reported, and only once * test(cli): normalize-audio test passes the flags it requires * style(cli): format the failure tracking test * fix(cli): one function unwraps and reports a command failure, once * docs(cli): shorten the command failure reporter's comment |
||
|
|
e00baefb5c |
fix(studio): reload the preview only when a file it loaded changes (#4570)
* fix(studio): reload the preview only when a file it loaded changes * test(studio): read the owner id from the new change-owner shape * refactor(studio): drop a comment the parser name already says * fix(studio): keep reloading until the preview is built, and count what its document names * style(studio): format the reference pattern and name the kept reload * fix(studio-server): scan preview references in linear time and refresh all thumbnails on a merged reload * fix(studio): keep a waiting film change when a file the preview never loaded arrives behind it * test(studio): name the preview ownership test for what it checks * chore(ci): allow the watchIgnore removals and trim the new comments * fix(studio-server): reload the preview when a folder holding a file it loaded moves in or out * perf(studio-server): check a changed folder in one lookup and cover every reference form * fix(studio-server): record read folders for a project at the filesystem root * fix(studio-server): reload the preview when the file a symlinked asset points at changes * fix(studio-server): resolve symlinks on every preview read and key projects by their real folder * fix(studio-server): record every link a preview read passes through, resolved as the system does |
||
|
|
950cccdac0 |
test: cli and studio-server tests remove the temp folders they create (#4566)
* test: cli and studio-server tests remove the temp folders they create * test(cli): each temp folder is removed where it is made, even when a test fails * test(cli): the one-liner helper relies on its project's own cleanup |
||
|
|
31c00d2f0e |
fix(studio): avoid preview reloads for temporary and excluded files (#4553)
* fix(studio): avoid preview reloads for temporary and excluded files * docs: capture public preview reload reproduction * fix(studio-server): normalize watch paths in linear time * fix(studio): register dynamically opened preview projects |
||
|
|
16dc05e9fb |
fix(cli): stop background update and skills checks from holding a finished command open (#4556)
* fix(cli): skip background checks in unattended runs and never hold exit on a silent DNS * fix(cli): probe DNS only on background checks so foreground upgrade and skills commands still wait * fix(cli): keep update checks for agents and skip them only for CI, dev and the opt-out * refactor(cli): give the update-check opt-outs one owner shared with auto-install * docs(cli): stop the auto-install notes claiming runs without a terminal are skipped * fix(cli): run telemetry flushes one at a time so an overlapping flush never resends an event * fix(cli): let a telemetry flush run even when the one before it threw * fix(cli): cap exit wait at one telemetry attempt when an overlapping flush already failed |
||
|
|
d290c05b09 |
fix(cli): --yes in an unattended run no longer overrides a no saved by another process (#4547)
* fix(cli): --yes in an unattended run no longer overrides a no saved by another process * fix(cli): every settings writer takes the lock, and --yes needs a real terminal to reverse a no * fix(cli): the thin-results download offer also needs a real terminal * fix(cli): one owner-checked settings lock, and an unreadable settings file never counts as a yes * fix(cli): settings lock removals go through a reaper, and a non-boolean answer counts as a no * fix(cli): a settings lock that cannot be released no longer fails a write that landed * fix(cli): a leftover settings lock is never taken over, and doctor names it * fix(cli): a background update that cannot be recorded is not launched again every run * fix(cli): every on-device install re-reads the saved answer first, and an unsaved yes installs nothing * fix(cli): the thin-results offer stops when a yes cannot be saved * fix(cli): only the consent-checked step installs the on-device runtime, and a leftover lock warns once * fix(cli): the leftover-lock warning follows the attended-terminal rule |
||
|
|
7eb6430e4e | fix(studio-server): history refuses once closed and reopens when a new project takes its folder (#4537) | ||
|
|
7c017a08de |
feat(studio-server): history can copy the files before or after any change into a folder (#4493)
* feat(studio-server): history can copy the files before or after any change into a folder ProjectHistory.checkout(entryId, side, emptyDir) writes the project as entry entryId found it (before) or left it (after) into an empty folder, leaving the project and its history untouched, so a host can play a change's before and after side by side. The entry's own files come from its recorded change and the rest from the entries logged before it, so a change stays exact when entries were logged out of write order. * fix(studio-server): checkout refuses an unreadable folder and every replay names its side * fix(studio-server): checkout lays out only trees that existed and never writes into the project An entry's tree now includes later-logged entries whose writes it continues (an outside change that turned a file into a folder before an agent turn edited inside it), each file replayed in the order its versions chain. Checkout refuses a folder inside the project (symlinks included), refuses a tree with a path that is both a file and a folder before writing anything, and empties the folder again if a copy fails. manifestAt is back to main's replay. * fix(studio-server): history logs every change in the order it was written A window now settles pending writes, the held claim and the outside group before it opens, and commits a held claim before its own entry when it closes, so the log order is the write order and every replay (checkout, restore, peek, reopen) sees files as they were. Removes the dependency replay added for checkout. Visible change: an outside burst that spans an agent turn is two entries, and a drag held across a turn ends at the turn. * fix(studio-server): checkout recognises its project folder whatever letter case the path uses * fix(studio-server): overlapping turns, operations and claims log every pending change in write order settle() (a window opening, an undo, restore or step) and every claim commit the held claim, the outside group and each open window's pending changes as a part, oldest first; the windows stay open. A held drag ends when another write is pending. One commitPending owner replaces the claimed-paths-only cut. * feat(studio-server): undo is per author, and a turn part undoes alone keeping later edits * test(studio-server): pin per-author redo and name a person's open window on Cmd+Z * refactor(studio-server): split Cmd+Z target lookup and claim steps under the audit's thresholds * fix(studio-server): a file replaced by a folder logs its removal before the folder's files * fix(studio-server): undo across a file/folder swap works, and a later swap stays the person's * fix(studio-server): an undo blocked by a file added since refuses before touching any file * fix(studio-server): an undo never writes through or replaces a link standing in its way * fix(studio-server): undo and redo across a letter-case file/folder swap work in every direction * fix(studio-server): a letter-case file/folder swap after an idle turn stays one checkable entry * fix(studio-server): decide letter-case folding from the project's own disk * docs(studio-server): keep three history comments under the line limit * fix(studio-server): checkout checks and fills one resolved folder, so it never overwrites a nonempty one |
||
|
|
2be82f0099 |
fix(cli): use onnxruntime-node installed beside the CLI when it is the pinned version (#4507)
* fix(cli): use onnxruntime-node installed beside the CLI when it is the pinned version An optional package (onnxruntime-node, @google/genai) installed next to the CLI at the pinned version is now loaded from there, before the ~/.cache/hyperframes/optional copy and without an npm install. Any other version is ignored and the cache path runs as before. doctor reports that copy's version. * refactor(cli): name the beside-the-CLI lookup instead of commenting it * fix(cli): a bad optional package copy beside the CLI no longer breaks doctor The lookup reads the first copy Node would load and treats an unreadable manifest as not the pinned copy, so doctor and the loader fall back to the cache instead of throwing or checking a different copy. * fix(cli): let Node pick the optional package copy before checking its version The beside-the-CLI lookup now asks require.resolve which copy it would load and checks that copy's manifest, so an empty folder or a same-named file earlier on the path can no longer make the check and the loader disagree. * test(cli): cover a symlinked optional package copy beside the CLI * fix(cli): find a symlinked optional package when Node preserves symlinks Under --preserve-symlinks require.resolve returns the symlink path while the candidate folder was compared by its real path, so a pinned copy Node loads was rejected. Both sides are now real paths. A test runs the lookup in a Node subprocess with --preserve-symlinks. |
||
|
|
2734ee2c02 |
feat(cli): hyperframes history lists, undoes and restores a project's recorded changes (#4449)
* feat(cli): hyperframes history lists, undoes and restores a project's recorded changes * fix(cli): an agent's history turn ends after 10 idle minutes on every path A turn left open no longer takes the person's later edits: the engine files a CLI turn's writes by the same idle rule as a preview window. Studio claims take their paths back from an agent's window; refusals, --json shapes, --limit and time parsing follow the review. * refactor(cli): the project-history opener lives in its own module The history command and other commands open a project's history the same way: through a running preview, or directly under the engine's lock. * docs(skills): the history loop lines sit in one marked trial block per skill * fix(cli): a turn marker without its last write time has ended Idle-limit tests set file times instead of racing the clock, and the restore refusal test picks a ref no entry id starts with. * docs(cli): the history entry shape comment names no other app * refactor(cli): history reuses the busy error class and its who helper * fix(cli): undo --who reverts the agent's whole last turn and nothing older When the open turn recorded nothing, undo fell back to the agent's earlier turn; when a mid-turn command split the turn, it reverted only the newest part. The turn now keeps the entries it filed, and undo --who reverts exactly those or refuses. Engine tests pin the write timing rules, and a date that rolls over (Feb 31) is refused. * fix(cli): an undo --who that meets a conflict leaves the whole turn as it was It redoes the parts it already undid, lists the newer entries as they are now, and offers --just-this and --back-to-before for the whole turn. The last turn is recorded atomically before its marker is cleared. * chore(cli): history keeps each file's comment share at or under main's * fix(cli): history keeps log order past a turn's idle limit and undoes a turn's cut parts * fix(cli): history drops unused imports and splits undo and reopen steps |
||
|
|
f873734cc4 |
perf(studio): open a large film ready to play in about 4 s instead of 32 s (#4401)
* perf(studio): keep lint off the preview open path The bundler linted every build only to print a StaticGuard warning; a preview bundle now skips it through a staticGuard option, since Studio lints on its own route. That route now lints in the CLI child-process worker the render path already uses, so seconds of parsing no longer stall the preview and every other Studio request on the server event loop. * perf(parsers): parse identical GSAP source once Studio re-requests the parsed animations of unchanged source on every open, and several lint rules parse the same script in one run. A small memo keyed by the script text serves repeats; each caller gets its own copy. * perf(studio): build each preview document once per content version The preview route rebuilt the document for every browser without a cached copy. It now serves one build per ETag from memory, and the CLI keeps the last build on disk salted with its own file, so a restarted server serves the previous document without stale code. * perf(studio): build the opening film's preview while the browser starts The preview server built the document only when the player asked for it, after Studio had booted, so a first open waited about a second of idle server time. The CLI now requests the opening project's preview right after listening, and requests that arrive while a build runs share it instead of starting another. * perf(studio): request the preview document at page load Studio asked for the preview only once the player mounted, about 640 ms into a first open. It now requests it the moment the page loads, and again when the hash names another project, so the build or cache read overlaps Studio's boot. * feat(studio): show the cached frame 0 while the live preview boots Studio covers the live preview with frame 0 as the thumbnail route last rendered it, sized from the image so the stage does not shift, and removes it when the live frame is ready to show. The request is cache-only, so a miss never starts a render on the open path; the missing poster is rendered after the live frame shows, for the next open. * perf(cli): start Studio before the startup lint finishes hyperframes preview linted the whole project before it listened, 4 to 6 s on a large film, and did so in --json mode where the result is discarded. The lint now runs in the CLI worker while Studio starts and prints its summary when done; --json skips it. * perf(studio): render thumbnails from one loaded page, after the preview boots Every thumbnail opened a page and loaded the whole composition to shoot one frame, and the Compositions card asked for one while the live preview was still booting. The server now keeps one loaded page per preview document and seeks it per frame, reloading only when the project changes; card thumbnails wait for the project first ready preview. * perf(studio): parse GSAP animations after the preview boots Studio asked the server to parse the composition animations while the preview was opening; on a fresh server that cold parse ran on the event loop ahead of the preview build. The request now waits for the live preview to boot, or to fail loading, since only keyframes need it. * perf(cli): find orphaned browsers with one process-table read preview ran pgrep three times and then ps once per matching browser, all synchronously before listening: about half a second on a machine running other headless Chrome. One ps listing now finds every orphan in ~20 ms. * perf(studio): keep lint off the boot Opening a project started two lint processes while Studio booted: the terminal's startup lint and Studio's background lint, ~4 s of CPU next to the boot. Studio's background lint now waits until the live preview has booted, and lint processes run at the lowest CPU priority so they only take idle CPU. * perf(studio): open the editing session after the preview boots Opening a project read the composition source and parsed it for the editing session (linkedom plus hf-id assignment) on the main thread while the live preview was booting on the same thread: ~300 ms on a large film. The session now opens once the preview has booted; nothing the first frame shows needs it. * perf(studio): park the editor overlays until the preview boots The overlay frame loop polled preview geometry and off-canvas indicators from the first frame, forcing layout on a preview document that was still loading, on the thread its boot runs on: ~200 ms before the first frame on a large film. The loop now skips its subscribers until the preview has booted, then wakes. * perf(studio): treat the preview as booted once it can play Work deferred until the boot (editing session, lint, overlays, GSAP parse, card thumbnails) started at timeline-ready, which fires before the asset gate, so the editing-session parse ran inside the wait for play. The Player now latches the boot when the preview can show and play, or fails to load. * fix(studio): keep a left project's deferred work out of the one now open Deferred open-time work (source parse, background lint, editing session) waited on one global boot flag, so leaving a project before its preview booted let the next project's boot run the old project's work and write its results. The wait is now per project and resolves false once another project opens. A preview stuck without an error stops deferring that work after 5 s. * perf(cli): let the play page revalidate instead of refetching hyperframes play served the composition, its assets, the player and the runtime with no validator, so every reload refetched everything, the mp4 in full three times. They now carry an ETag and stay no-cache, so unchanged bytes cost a 304 and an edit is still seen. * fix(cli): serve an edited text asset even when its size and mtime match The play page tagged every asset by mtime and size, so a same-size rewrite within one filesystem tick kept the old tag and the browser kept stale bytes. Text assets now get a content-hash tag; media keeps the mtime and size tag Studio already uses. * fix(studio): close idle thumbnail pages so an idle Studio runs no composition Kept-loaded thumbnail pages stayed open for the life of the server, each running a live composition, so an idle Studio cost server CPU and memory that main does not. A page now closes after 10 s unused, and a page is closed only after the frame still running on it finishes. * fix(studio): stop a first open from reloading the preview once per scene file Studio stamped hf-ids into a composition the first time that composition was served, so opening a film of external scenes wrote to index.html and every scene file mid-session. Each write reached Studio as an outside edit and reloaded the preview, which failed for a large film. The server now stamps every composition once before it starts watching the project. * fix(studio): ignore play until the preview can play, so it is never undone Space (and any play() caller) could start playback before the preview frame finished loading. Its load step then paused and rewound to the restore point, so the play was silently lost. play() now waits for timelineReady, the same gate the Play button already uses. * fix(studio): hold play on a composition switch until the new preview loads Readiness was reset only when the project changed. Drilling into another composition kept the old ready flag while the new preview loaded, so Play or Space could start a play that its load step then undid. * refactor(studio): split background lint and thumbnail page reuse into single-purpose steps Keeps the boot wait in the background lint only, moves page lookup and idle close out of withPage, and drops a suppression the startup worker no longer needs. Behaviour unchanged. * chore(cli): declare the render-setup worker as a fallow entry It is spawned by path from cancellableProcess.ts, so it has no import-graph referrer; the entry list is where the repo records that, replacing the per-file ignore removed in the previous commit. * fix(cli): close a thumbnail page whose load fails The page never leaves the promise that loads it, so when the load throws (a WebGPU composition with no adapter, or the navigation timeout on a big film) nothing could close it. * fix(cli): match text assets by exact type and keep Range on them The pattern anchored only its first alternative, and text assets skipped buildRangeResponse, so a ranged request for a stylesheet or SVG got a full 200. * test: open the next project as Studio does, and read the stamped file once The absent-read guard test switched project without beginning its timeline session, so the per-project boot wait held the read. The stamp test checked then read the same file. * test(cli): rewrite the scene file without opening it first Opening the file by path counted as a check before the later read of the same path. * perf(cli): take a thumbnail's version from the cached project signature Each thumbnail walked the project and hashed every text file for its page version; the adapter already caches that signature and clears it from the watcher. An aborted request now also stops before its screenshot. * fix(studio): keep Play off until the preview's load step has run Play could enable before the preview document's load event, from the runtime's timeline message or from the blank page's load step settling on the preview's first message. The load step then paused and rewound the preview, so a Play pressed in between did nothing; a film with large images loads late enough for this to hit a cold open. Readiness now waits for the document's load step, whichever path asks for it. * refactor(studio): move the preview poster's state into usePreviewPoster NLEPreview.tsx passed the 600-line limit once main grew it; the poster's state and callbacks belong beside PreviewPoster. No behaviour change. * fix(cli): take an earlier thumbnail on a fresh page A composition need not draw the same frame when seeked back, so a kept page that had shown 20 s drew a different 10 s thumbnail than a fresh load. A kept page now only seeks forward, as a render does; an earlier time loads a fresh page. * perf(cli): close a kept thumbnail page a second after its last frame Held for 10 s, the page went on running its composition after the open's thumbnail burst, about 5 % of a core in the thumbnail browser while Studio sat idle. A burst's requests arrive back to back, so it still loads the page once. * fix(studio): count the preview as booted at its first frame The boot waited for every video and audio in the preview to finish buffering, which on a project with media took until the 5 s fallback. The thumbnails, editing session and lint that wait for the boot then started seconds late and kept an idle Studio busy. They now start once the first frame is up; media finishes buffering alongside. * perf(studio): render one thumbnail per composition for its card and clip A composition's sidebar card asked for its frame at 3.00 s and its timeline clip for the clip's midpoint, so opening a project rendered every scene twice. The clip now asks for the card's frame (the midpoint still, for compositions under 3 s), and the thumbnail cache serves both from one render: 20 scene renders drop to 12 on a 12-scene project. * chore: keep each changed file's comment share at or under main's The comment ratchet counts the comment lines this branch added. Each is cut, or kept as one line where it states an invariant (the GSAP parse cache returns copies; the boot starts waiting work at the first frame). The orphan-cleanup docblock is condensed and now names the process-table read it uses instead of the pgrep scan it replaced. No code line changed. * fix(studio): keep a 3 s clip off its end frame and give card and clip one encoded URL A composition exactly 3 s long asked for its thumbnail at 3.00 s, its end instant, which after a fade-out is an empty frame; compositions of 3 s or less now use their midpoint. The card built its thumbnail URL from the raw path while the clip encoded it, so names with & + [ or % asked for two different URLs, and a % in the name made the card's request fail on the server; both now encode. The readiness comment says the boot latches at the first frame, as it has since the boot moved. * fix(studio): encode the card's live preview URL too, and document the bundler's static guard The card's hover preview built its URL from the raw composition path, so a % in the file name made the preview route fail to decode it. The card now builds its composition URL in one place, encoded, for both its thumbnail and its hover preview. The static-guard bundle option keeps a one-line doc (its default and what it warns about); a neighbouring docblock is rewrapped to keep the comment share. * test(cli): mock studio-server once so the signature spy and the history hook both apply The main merge brought a second vi.mock of @hyperframes/studio-server into studioServer.test.ts; the later one replaced the first, so createProjectSignature was no longer a spy and the cached signature test read .mock of undefined. One mock now carries both overrides. |
||
|
|
7556b95ac6 |
feat(cli): warn when canvas content touches the frame edge (#4427)
* feat(cli): warn when canvas content touches the frame edge * test(cli): pin which frame edge the canvas warning reads * test(cli): type the canvas edge stub like the file's other getContext stubs |
||
|
|
7e8d2bd1f3 |
fix(cli): stop check --at false alarms and name a stale CLI version (#4417)
* fix(cli): stop check --at failing still frames as a frozen timeline * fix(cli): warn when the running CLI is older than the project's pin * fix(cli): keep judging spread samples when check --at is combined with other gates |
||
|
|
d27435c070 | fix(lint): report original source line and column locations (#4353) | ||
|
|
105984cac4 | feat(cli): add IBM Bob runtime support (#3930) | ||
|
|
e5689782c0 |
fix(cli): require non-empty feedback repro commands (#4263)
Co-authored-by: GPT-5.6 Sol <noreply@openai.com> Co-authored-by: miguel.sierra <miguel.sierra@heygen.com> Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com> |
||
|
|
45f92e5572 | fix(cli): a failed docker render names the container's last error (#4168) | ||
|
|
e8fdfd0d65 |
fix(cli): install onnxruntime-node and google-genai on first use (#4156)
* fix(cli): install onnxruntime-node and google-genai on first use Both carry deprecated transitive dependencies (boolean, node-domexception) that showed up as npm warnings on every npx hyperframes install. They move to devDependencies (types and the Intel-binding check still need them at build time; the bundler externalizes them) and load through a new on-demand loader that installs into the CLI's own cache dir the first time a feature needs them, with one stderr line, no prompt, and a manual-install command named in the error if the network is unavailable. hyperframes doctor lists each as installed (with version) or not-installed-yet. * fix(cli): sweep stale install staging and bound network retries * fix(cli): optional-package install only sweeps staging dirs whose process is dead Staging dirs now carry a per-call suffix after the pid, and the sweep skips any dir whose pid is alive, so concurrent first runs no longer delete each other's install. The install error suggests the network only for network failures, and the manifest path has one owner. install() takes an injectable npm runner so tests drive the real code. * fix(cli): optional-package sweep also clears old-format staging dirs and tests a live foreign dir The concurrent-callers test shared one pid, so it never exercised the sweep rule; a live foreign staging dir test now does. Old-format dirs of a dead pid are swept, and a failed cleanup no longer aborts a viable install. * fix(cli): optional-package sweep names a stale dir it could not remove A locked leftover is still skipped so the install proceeds, but the warning makes it visible. * docs(cli): shorten the install docblock to the comment limit Comment-only change. |
||
|
|
2db126d4c8 |
docs(skills): one owner doc for everyday edits and a hyperframes-studio skill (#4125)
* docs(skills): one owner doc for everyday edits and a hyperframes-studio skill Agents got contradictory advice on volume fades and were blocked from splits and copies by the Ask-agent guardrails. - creator-editing-recipes.md is the single owner: adds align-to-event, copy-a-group and swap-media recipes and the one volume rule (use the lane) - variables-and-media, data-attributes (skill and CLI copy), the gsap adapter and tracks-and-clips now defer to it - Ask-agent guardrails allow the neighbour edits a split, retime, copy or swap requires and point at the owner doc - new hyperframes-studio skill: sub-compositions for every scene, one caption track, one element kind per track, safe zones - drift test lints every html example in the owner doc with the real linter and fails on any doc teaching a volume tween * refactor(skills): drop unused tween allowlist from recipes drift test * docs(skills): name the track kind attribute in the studio skill * docs(skills): stop claiming lint rules that do not exist yet and fix track numbering Review fixes: - studio skill: drop the severity claim and the preview-overlay claim, group tracks by data-track-kind instead of numeric ranges that clashed with the recipes - recipes: audio data-start is root time only in the root composition; scene-local inside a sub-composition file - guardrails: timeline edits are exempt from the element-only and timing lines; the pointer names the skill reference, not a repo path - montage doc defers to the volume lane; skill count is 21 everywhere - drift test also scans the montage doc * fix(skills): the ducking generator emits a volume lane and captions defer to the studio safe zones - audio-duck.mjs prints a data-automation lane (clip-local times from the target data-start) instead of a timeline volume tween; --json adds the lane - operations.md shows the generated lane; the drift test scans it - embedded-captions safe-zone lines point at the studio skill, which owns the values * chore(skills): regenerate the skills manifest after rebase * docs(skills): remotion translation docs point volume ramps at the automation lane * docs(skills): studio safe zones use the action-safe 90 and title-safe 80 boxes * docs(skills): caption docs defer to the studio safe boxes * chore(skills): regenerate the skills manifest after rebase * fix(skills): studio skill names the safe margins; guardrail test matches the timeline-edit wording |
||
|
|
93921dccc8 | fix(cli): stop the workspace build check test from depending on wall-clock mtimes (#4098) | ||
|
|
2a9912ec99 |
fix(cli): stop the cancellable-process test reading a pid file mid-write (#4080)
Same defect shape as the earlier preflight cancel test fix: the wait condition only checked that the ready file existed, which goes true as soon as the write opens (and truncates) it, before its content lands. A read landing in that window parsed an empty string to 0. The wait condition now also validates the parsed value is a real positive pid before accepting it, retrying otherwise. Reproduced with a busy-wait harness that hit the empty-file race in roughly two of five attempts and confirmed clean after the fix. |
||
|
|
de9206e0f8 |
fix(cli): detect a missing or stale studio build before it crashes (#4076)
* fix(cli): name the missing or stale workspace package before preview crashes Running hyperframes preview from source with an unbuilt or stale package previously crashed deep inside that package's own import graph with a raw resolver error naming neither the package nor the fix. Add a preflight that runs before the preview command's own module import, so it also catches the crash that happens at CLI load time before the studio dev server ever spawns. * test(cli): cover the preview workspace-build gate wiring Adds a test that exercises the real CLI dispatch path (mocked module loaders, not source-text matching) to prove the preview command's build-check runs before its module loads and that a failure exits non-zero without double-printing. Also guards the exports-map reader against a malformed package.json instead of crashing raw. * fix(cli): correct why the audio-fx generated file is checked The comment claimed core/src/index.ts imports both generated files directly. Only runtime-inline.ts is; audio-fx-runtime-inline.ts is reachable solely via core's own audio-fx-runtime export subpath, which the engine package imports. The check itself was already correct to require both, only the stated reason was wrong. |
||
|
|
096459e76d |
fix(cli): collapse startup lint output to a one-line summary (#4052)
* fix(cli): collapse startup lint output to a one-line summary preview, publish, and render's non-abort lint check all dumped every finding to the terminal before their real result line, drowning it in noise unrelated to whether the project is in good shape. Full findings already surface in Studio's own Lint badge; all three now print a one-line summary by default, with --lint-verbose for the old full output. One formatter (formatLintStartupMessage), three call sites. * fix(cli): show lint findings in full on every abort path Any path that calls presentRenderLintAbort now prints full findings even without --lint-verbose, since the one-line collapse was hiding the reason a render stopped. * fix(cli): scope the lint summary pointer to what each command has open The one-line lint summary always pointed at "the Lint badge in Studio", even from render and publish where no Studio is running. It now points at --lint-verbose for those, and preview additionally names the hyperframes lint command since its own stdout is what an agent reads, not the badge. publish now forces full findings on its own entry-mismatch abort, the same fix render already shipped, so the reason for the abort isn't hidden behind a bare count. * docs(cli): trim a doc comment past the 4-line limit |
||
|
|
e4b1832fad | feat(cli): add hls output format (#4022) | ||
|
|
08d48ab753 |
fix(cli): guard process snapshot capture (#3974)
Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com> Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com> |
||
|
|
5d2478c6fa |
fix: report HDR auto-promotion (#3985)
Report HDR auto-promotion consistently from the render media set, including remote media, and make its regression coverage portable across supported paths. Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com> Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com> |
||
|
|
4c6fa9a790 |
fix(studio): the preview stays paused, survives typing, and opens without a re-encode (#3910)
* chore(studio): remove the write-only self-write timestamp ref
`domEditSaveTimestampRef` was assigned at 26 sites across 54 files and read
nowhere. It used to feed a 2 s "suppress the watcher reload after our own
write" window; that mechanism was replaced by content-hash identity in
sdkSelfWriteRegistry, whose header still says why (a clock cannot tell an
SDK self-write echo from an undo landing in the same window). The reader
went with that change, the writers did not, and three comments kept
describing the timestamp as protection that no longer existed.
Pure deletion: the ref, every prop and parameter that threaded it, every
assignment, the five comments citing it, and the orphaned test helpers.
No behaviour change; the studio suite, typecheck, lint and format are
green.
Not removed: the __hfSuppressSceneMutations wrapper in gsapSoftReload.ts.
It looked undefined from inside the studio package, but shader-transitions
installs it on the preview window (hyper-shader.ts) so soft reloads do not
invalidate cached transitions. It is live.
* docs(studio): plan for loading the preview during the shell's first layout
* fix(runtime): a paused preview stays still after any seek
Sub-compositions kept animating while the transport was paused and the
Studio button showed play. A render-seek unpauses every sibling timeline so
GSAP propagates the root's totalTime into them, and nothing paused them
again; parented to the global ticker, they free-ran at 1x while the master
timeline and the clock stayed stopped. The Studio reaches that seek path
through its seek-driven fallback adapter right after a preview reload,
which is why editing text was what set it off. Captured live in the user's
preview: seven child timelines advancing 0.210s per 200ms sample, all
gsapPaused=false, transport not playing.
Three fixes under one invariant: while the clock is paused nothing runs,
and the button never disagrees with the runtime.
- The sibling rearm is a lease, not a resting state: seekTimelineAndAdapters
now returns every timeline it unpaused to paused in a finally. The second
rearm after the child re-seek only ever changed the leaked state and is
gone, with the comment that justified the leak by describing one caller.
- The paused side of the transport tick policed nothing; it now stops any
timed media element found running. A parked transport runs no ticks, so a
capture-phase play listener wakes it.
- Studio's canvas click resumed by setting the store flag alone; it now
requests playback through the player so adapter, rAF loop and flag agree.
Regression tests fail without each of the three changes.
* perf(studio-server): stop re-encoding videos the browser already plays on open
Opening a project kicked off a background transcode for every asset whose
codec some browser might not decode. VP9 is on that list for Safari's sake,
so a Chrome user opening a project with a VP9 avatar paid a ~10s, ~60
CPU-second re-encode across six cores on every open, concurrently with the
browser's first layout, for an output nothing ever requested: across four
recorded sessions the browser asked for the proxy zero times.
One predicate was doing two jobs. "Could some browser fail on this" is a
property of the asset and decides what gets injected into the page;
"will this client request the substitute" decides whether to spend CPU
before being asked. The codec table now carries an explicit prewarm flag:
HEVC and ProRes (no browser decodes them) still warm; VP9 and AV1 are
injected but transcoded lazily by the existing ?hf-proxy= request, which
already reports failure as a 502.
Pre-warms requested and proxies served are now counted on the existing
structured-stderr telemetry channel, because a speculative job with a 0%
hit rate emits no error and had been invisible. A single lookup helper
also closes a prototype-key gap where one path used Object.hasOwn and the
other a bare index.
Regression test: a VP9 and an HEVC asset in one composition yield exactly
one pre-warm, for the HEVC. Fails on the previous gate.
* perf(cli): serve the studio bundle compressed and cache hashed assets
The 4.1MB studio bundle was served uncompressed with Cache-Control:
no-store, so every open of the studio re-downloaded and re-parsed it. Vite
names built assets with an 8-character content hash, so their bytes can
never change: those now ship gzip-compressed (1.25MB on the wire,
byte-identical after inflation) with a one-year immutable cache policy.
Unhashed files under public/ keep no-store, and the HTML shell, which
previously sent no Cache-Control at all, now sends no-store explicitly:
it is the only thing that names the current hashed bundle, and it must
keep revalidating for the immutable policy to be safe.
The hash test only accepts the segment after the last hyphen and requires
a digit, underscore or capital, so an ordinary hyphenated name like
user-Guide-v2.js is not mistaken for a hash and served forever.
* perf(cli): warm the preview route before the browser opens
The browser's first request for a project's preview paid the server's cold
compiler import and first bundle in the request path, after the studio
shell's own first-layout stall, so both costs landed on the user's
time-to-first-frame in series. The CLI now issues one fire-and-forget
request for the preview route from openStudioBrowser, the single funnel
for every launch path, before the --no-open early return so pasted URLs
benefit too. Measured on the demo project: first client request 231ms
before, 101ms after; the ETag 304 path afterwards is under 1ms.
The fetch carries a 10s abort so an unsettled connection cannot keep an
otherwise-finished CLI process alive, matching the package's existing
convention. The shader query params the player appends are not part of
the route's cache key, so the plain route URL warms the same entry.
* perf(studio): request the player chunk before the shell's first layout
The dynamic import of @hyperframes/player ran inside the preview's mount
effect, which React schedules after the shell's first layout. On a cold
open that layout stalls the main thread for seconds, so the chunk request
waited behind it for no reason. The import is now kicked at module scope,
behind a typeof window guard that preserves the documented SSR contract
(the module registers a custom element at load), and the mount effect
awaits the already-in-flight promise. Verified in the built bundle: the
preload call sits at module top level, so the request goes out on bundle
evaluation.
* fix(studio): editing a text property no longer reloads the preview
Every keystroke in the Design panel writes the composition file, the file
watcher announces the change to the studio, and the studio decides whether
the change was its own. Over the CLI's event stream that decision has
never worked: the browser hands the handler a MessageEvent whose data is
a JSON string, and three of the four payload readers (version, write
token, content) only understood an already-parsed object, so they read
every field as absent. An absent token means "someone else edited the
file", and the studio hard-reloaded the preview iframe on its own edit,
blanking it for seconds. The path reader alone knew how to unwrap the
string, which is why the event was recognised well enough to reload and
never well enough to suppress.
The envelope is now decoded once, at the boundary, by one function that
all three transports feed; the readers share one field accessor so they
cannot diverge again. The production event-stream rung is extracted into
an exported channel so a test can drive a real MessageEvent through the
listener it registers, which was impossible before because vitest defines
import.meta.hot and the selection never reached that rung under test.
A second, smaller cause: the server attached the write receipt to the
first subscriber only and deleted it on read, so any other listener saw
an unlabelled change. Reads are now non-destructive with the TTL as the
only eviction, scanning newest-first because identical bytes written
twice inside the TTL (undo, retyping a value) share a version and the
older token was already spent. The file version now ships with every
event, receipt or not, so duplicate deliveries of one change dedupe
instead of reloading once each.
shouldReloadSdkSession had no production callers and a signature that
invited an undecoded delivery straight back into this bug; it is removed.
consumeFileWriteReceipt stays as a deprecated alias for one release.
Regression tests: a Studio write delivered as a real SSE MessageEvent is
suppressed; two subscribers of one watcher event reload once; a genuinely
external write still reloads; a repeat of earlier bytes gets the newest
token; a receipt past the TTL is not recognised. Each fails on the code
before it.
* fix(runtime): paused-time media playback is borrowed, not banned
The paused-side enforcement added in the previous commit had no notion of
provenance, so it stopped two features that legitimately play media while the
transport clock is paused. Both were deterministic, not racy: the capture-phase
`play` listener means the very play() that starts them wakes the transport that
stops them.
- The colour-grading preview (colorGrading.ts startPreviewPlayback) plays a
video while paused to render grading previews. It went dark on the first
paused tick.
- The Studio's scrub audition (timelineIframeHelpers.ts applyScrub) plays the
music track for ~140 ms while paused so a playhead drag is audible. Same path
killed it.
A runtime-owned lease fixes both without weakening the enforcement. One owner: a
WeakSet in the runtime closure, with lease/release published on the existing
window.__hf surface for the Studio, which reaches the element across the iframe
boundary and cannot call into the closure. The grading runtime is constructed
with the pair directly. Both the cheap probe and the sync path skip leased
elements while the clock is paused; during playback the transport owns everything
again. Anything that plays while paused without a lease is, by definition, the
defect the enforcement exists for, and is still stopped.
Two corrections to the previous commit's reasoning:
The old leak broke the render path too, deterministically, not only the preview.
packages/producer/src/services/fileServer.ts:375-379 seekToTime flushes the
virtualized rAF queue, and GSAP's global ticker with it, after renderSeek and
before the frame screenshot (fileServer.ts:657-664, hf.seek). A sibling left
unpaused therefore advanced by the full inter-frame delta into the captured
frame. The finally added in the previous commit fixes that as well.
Deleting the second activateSiblingTimelines is safe because nothing between
frames reads a sibling's paused(), verified by grep over the deterministic
adapters, syncTimedElementVisibility, the hf-timelines-built handler and
__hfReseekGpu. Not merely because seekStandaloneRegisteredTimelines pauses each
child. The code comment now gives that reason.
Tests, each proven non-vacuous by reverting the piece it guards:
- a leased element survives repeated paused ticks and is stopped once released
- the colour-grading preview survives, through the real init wiring
- the scrub borrows the element and gives it back on stop
- the existing test that an unleased element is still stopped keeps passing
* test(runtime): a lease ends when the transport plays or the borrower stops
Two assertions the review found not load-bearing. Dropping the isPlaying
branch so leased media stayed exempt during playback left every test
green; a leased out-of-window clip is now asserted stopped the moment the
transport plays. Deleting the release in the grading stop closure also
left the suite green, because the pause on the next line satisfied the
assertion; a restart after stop is now asserted stopped, which only the
release makes true.
* test(studio): the event-stream channel must open /api/events
* fix(studio-server): stop printing a proxy diagnostic line per clip on every render
Review of the pre-warm commit found the diagnostic louder than the thing it
diagnoses, and the two counters measuring different things under one name.
The per-asset `prewarm_requested` line is now behind
HYPERFRAMES_DEBUG_MEDIA_PROXY, matching isGpuProbeDebugEnabled in
packages/engine/src/utils/gpuEncoder.ts. A composition with fifty hostile
clips printed fifty JSON lines into a clack-formatted terminal on every
re-render. One summary line is written at process exit instead, using the
same process.on("exit") shutdown hook as packages/cli/src/cli.ts.
The counters now share a unit. prewarmsRequested counts per asset per render;
proxyRequests counted every HTTP request, including 304s. It now increments
once per resolveProxy call, after the ETag shortcut, so a revalidated repeat
no longer reads as fresh demand. An unconditional Range refill still counts,
and the docstring says so rather than claiming otherwise.
The HEVC justification was false on macOS Chrome, which answers canPlayType
for hvc1 with "probably" and keeps the source, so the pre-warm is redeemed
there only through the reactive zero-videoWidth path. prewarm stays true
because Chrome on Windows/Linux and Firefox do need the substitute; the
comment and test names now say "no cross-platform decode" instead of
"browsers never decode it".
Two test gaps closed. Nulling vp9's representativeMime left the suite green
while making the client skip canPlayType and proxy on every browser, which
would reinstate exactly the transcodes this work removed; the mimes are now
pinned. The Object.prototype test passed with the hasOwn guard deleted, so it
now goes through probeAssetCodec, the input that actually misbehaves without
it. Both fail when the change is reverted.
* perf(cli): stop gzipping the studio bundle on loopback
Compression made the cold open slower on the only transport this server has.
It binds 127.0.0.1 with no --host, and measured there the six bundle assets
took 69.8 ms with gzip against 7.9 ms raw; the 4.1 MB chunk alone was 49.4 ms
against 3.1 ms. hono/compress is removed, which also retires the Vary header
question it raised. If remote serving ever matters, compress at build time
rather than per request.
The cache policy is now decided by route instead of by filename. Reading a
content hash out of a name cannot work: rollup's alphabet is base64url and
includes a hyphen, so roughly one hashed file in ten was misread as unhashed,
and the immutable header also leaked onto hand-authored public/ files served
by the same handler. packages/studio/vite.config.ts sets neither
build.assetsDir nor publicDir, so dist/assets holds only rollup's hashed
emits and every public/ file lands at the dist root. /assets/* is therefore
immutable and /icons/* and /favicon.svg keep revalidating, with no heuristic
in between.
The shell is no-cache rather than no-store. It carries no ETag, so both force
the same full refetch, but no-store puts the document on Chrome's bfcache
blocklist: leaving Studio and pressing Back would cold-boot the app instead
of restoring it.
* docs(runtime): say what the paused-media probe actually filters
* revert(cli): drop the preview prewarm that held the CLI event loop
The fire-and-forget warm added in
|
||
|
|
9c84277d28 |
fix(cli): let .hyperframesignore negation re-include hidden directories (#3859)
## What `.hyperframesignore` negation rules (e.g. `!/.media/`) could never re-include a hidden (dot-prefixed) directory in the publish or cloud-render project archive, even though negation works correctly for every other kind of path. ## Why `collectProjectFiles`'s walker called `shouldIgnoreSegment` first for every directory entry, and that check unconditionally excluded any name starting with `.` — before the project's ignore matcher (built from `DEFAULT_PROJECT_IGNORE` + `.hyperframesignore`, where negation is evaluated) ever ran on that path. A hidden directory was discarded at the walk step, so no negation rule downstream could ever reach it. ## How - Moved the dot-prefix exclusion out of the hard `shouldIgnoreSegment` short-circuit and into the same ignore matcher that already parses `.hyperframesignore`, as a new default pattern (`.*`) in `DEFAULT_PROJECT_IGNORE`. Dot-prefixed paths are still excluded by default, but now via the same gitignore-style negation path as everything else, so a project's `.hyperframesignore` can override it. - `shouldIgnoreSegment` is now reserved for the fixed, non-negotiable exclusions only (`.git`, `node_modules`, `dist`, `.next`, `coverage`, `.DS_Store`, `Thumbs.db`) — the set no `.hyperframesignore` rule should ever be able to reach. - Added regression coverage for both directions: a `.hyperframesignore` negation re-including a hidden directory, and an unmatched hidden directory still being excluded by default (no behavior change for existing projects without an explicit negation rule). ## Testing - `bunx vitest run packages/cli/src/utils/publishProject.test.ts` — 40/40 passing (2 new) - `bunx vitest run packages/cli/src/commands/cloud/render.test.ts` — 10/10 passing (cloud render reuses the same archive builder) - `bunx tsc --noEmit` in `packages/cli` — clean - `bunx oxlint` / `bunx oxfmt --write` on changed files — clean, no changes needed 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
e5a6023228 | fix(skills): guard canonical stores from mirror aliases (#3714) | ||
|
|
00c91d6807 | fix(check): allow missing caption overrides (#3716) | ||
|
|
e5d89f770f |
feat(check): flag connectors that point at nothing and stylesheets that leak into the frame (#3736)
* feat(lint): catch leftover marker heads and guessed marked shafts Dash-draw on a marked path shows the arrowhead before the shaft exists. A long marked path that misses every node in both user and screen space is the same detach as the CTM-paste bug, without the counterfactual. * feat(check): flag orphan connectors and unbalanced style tags Catch a visible shaft while fewer than two nodes are on stage (enter-early / exit-late), and extra </style> that dumps CSS onto the frame. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(check): judge connector_orphan by the shaft's own endpoints The rule counted anchors stage-wide and skipped whenever two were on. That tier is every visible text-bearing or opaque element under 15% of the stage, so a title and a footer alone satisfied it — the check could not fire on a composition that had any chrome, which is all of them. Endpoints are now resolved per shaft, against candidates gathered by layout instead of by visibility, so a node hidden with opacity:0 still owns the endpoint that meets it and its visibility is what the rule tests. An endpoint that meets no node is left to connector_detached. * revert(lint): drop the marker rules this repo would only duplicate marker_orient_typo and marker_dash_draw_on already run in Zephyr's Python connector lint, down to the same regexes and the same message and fixHint strings. Zephyr concatenates its local findings onto this bridge's result with no dedupe, so shipping them here reports each one twice and counts it twice, and every later edit has to land in two places. The browser-check work stays: a marked shaft that meets no node, and an orphan shaft, are both runtime facts this repo can see and a source-reading rule cannot. * chore(check): drop the comments added with the orphan rewrite * fix(check): score orphan and detach on overflow-opted connector layers Allow-overflow is a clip opt-out, not a lifecycle skip. Orphan now uses the same connector names as detach so flow and arrow shafts are judged. * fix(check): see axis-aligned shafts, and script tags closed with a space connector_orphan gated on isVisibleElement, which rejects anything whose bounding box has no height or width. A straight horizontal or vertical connector is exactly that, and it is the common shape, so the rule was blind to most of what it exists to catch: across 47 production compositions it produced nothing. It now tests paint -- display, visibility and the opacity chain -- and leaves size to the 80px chord floor it already had. The same 47 compositions now yield three findings, each naming the endpoint that is still dark. unbalanced_style_tags stripped scripts with a regex that only matched `</script>`. `</script >` is valid, so the script survived the strip and a `"</style>"` string literal inside it counted toward the tag balance, reporting an error on a composition whose tags are paired. Both the strip and the closer count now tolerate whitespace before the `>`, which is also what CodeQL flagged on this branch. * fix(lint): count style tags in one pass instead of stripping scripts CodeQL reads the `source.replace(/<script.../)` as an incomplete HTML sanitizer. It was never one — the rule only needs to not count `<style` tokens that live inside a script string. A single alternation scan, where the script branch consumes the whole block, gets the same counts without a replace. * test(check): cover the two orphan guards that no fixture was crossing Neutering `shaftIsPainted`'s opacity check, or the endpoint proximity threshold, left the suite green. Both are false-positive guards: a connector staged hidden before its reveal, and a hidden element that happens to be the nearest thing to an endpoint without belonging to it. The hidden-shaft case now runs over all four ways a shaft can be invisible, so the display and visibility clauses are crossed too. * test: pin the two boundaries raised in review A closer inside an html comment is already dropped upstream by stripHtmlComments, so the rule never sees it. The test fails if that stripping regresses; adding a comment branch to the rule's own scan would have reintroduced the polynomial-redos pattern that stripper exists to avoid. A node hidden with display:none has no geometry, so nothing can place it near an endpoint and connector_orphan stays silent. connector_detached does not pick it up either, since it allows a half-attached shaft by design. That case is uncovered, and the fixture says so. * fix(check): type the hidden-shaft fixture table as a style record * fix(check): key connector_orphan by geometry and let a live node win the endpoint Two defects found in review. connector_orphan was persistence-tiered but absent from the geometry key, so several id-less shafts orphaning at one sample each collapsed into a single finding that then read as held rather than transient. On the 47 corpus compositions this recovers 9 findings across 5 compositions where 3 across 2 were reported; one composition fades its nodes and its connectors on a shared stagger, so four shafts each outlive a different node and only one of the four survived the collapse. The endpoint scan took the nearest candidate and asked whether that one was hidden, without asking whether a visible box was also in range. A staged halo sitting on the node it belongs to produced a finding naming the halo. A visible candidate within threshold now settles the endpoint. Fixtures added for both, and for the two guards a mutation sweep found unpinned: the connector-shape test and the dash-offset skip. * test(check): kill every surviving mutant in the connector_orphan path Seven guards still passed the suite when deleted: the four candidate filters, the connector layer's own visibility gate, the defs/marker skip and the chord floor. Each now has a fixture where that guard alone decides the outcome. The earlier sweep that missed them was mutating the wrong copy of a shared line — connectorAnchorRects and connectorEndpointCandidates carry the same filter text, and a first-match replace edited the detached one. Mutations are scoped by enclosing function now. * test(check): order the halo before the node it hides under The halo fixture proved nothing. Both boxes contain the endpoint, so both score gap 0, and the strict tie-break keeps whichever comes first in document order — which was the visible node. The old code picked the same winner and stayed silent too. Putting the halo first makes the tie-break hand it the slot, so only the visible-candidate check keeps the endpoint attached. --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
c59fd162aa |
fix(check): key content_overlap and text_occluded collapse by pair, and bill a clipped text box once (#2801)
Combines two fixes to the layout audit: - `staticIssueKey` no longer includes `issue.text` for `content_overlap` and `text_occluded`; both builders now emit a unique selector for both ends, so the selector pair is the identity. An animating subject (count-up, typewriter, rotating word) no longer splits one held collision into transient groups. - A clipping box that is its own nearest constraint reports `clipped_text` only, instead of also reporting `text_box_overflow`. A non-clipping self-constraint still reports. |
||
|
|
39ad2721b3 |
fix(check): keep auditing when a motion spec is partly unusable (#2805)
Combines two fixes to the motion sidecar path: - An ambiguous selector no longer aborts the whole spec. It is reported as a finding, the assertions that depend on it are skipped and named, and every other assertion still evaluates. `assertionTargets` is now exhaustive over the assertion kinds so a new kind cannot silently fall back to an arbitrary first match. - A sidecar that will not parse is reported as a spec finding instead of ending the run, so the composition is still audited. |
||
|
|
b49fba8dea |
fix(cli): preserve concurrent scaffold config creation (#3721)
* fix(cli): preserve concurrent package metadata during init * fix(cli): preserve concurrent project config creation |
||
|
|
e147613542 | fix(cli): upgrade project wrapper pins (#3042) | ||
|
|
2dc4bbfa75 |
feat(cli): forward a reported catalog gap to the feedback channel (#3204)
A search miss already reached PostHog, where nobody was watching it. It now also goes to the same channel a CLI rating goes to, by the same best-effort POST, so a move the catalog is missing is read rather than queried. The forward is bounded and swallowed, and the ack prints before it, so a gap report can never fail or delay the command that sent it. |
||
|
|
45cc343525 |
fix(cli): flag caption-zone by DOM box overlap (#3580)
A card centered at y=.860 can still cover the painted V2A pill. Intersect the element's getBoundingClientRect with the keepout instead of testing whether its center sits inside the band. |
||
|
|
9097d539b1 |
fix(cli): hide Windows child process consoles (#3529)
Rebase #3529 onto current main. Preserve all 16 issue-scoped Studio server, lint, and CLI child-process windowsHide options, including main's PowerShell null guards and stderr suppression in orphanCleanup. Regression tests continue to assert windowsHide at each scoped spawn site. #3476 and #3430 remain out of scope. Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com> Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com> |
||
|
|
73aa71c9ec |
fix(cli): guard PowerShell process queries against exited PIDs (#3571)
## Summary
`processIdentity` and `processParentPid` call `Get-CimInstance Win32_Process` to look up process metadata on Windows. When the target process has already exited, `Get-CimInstance` returns null and calling `.CreationDate.ToFileTimeUtc()` or `.ParentProcessId` on it throws `InvokeMethodOnNull`. The try/catch handles it, but PowerShell writes the error to stderr, which pollutes the test runner's output and causes spurious exit code 1 on Windows CI.
Two fixes per call site:
- Null-check the CimInstance before accessing properties (`$p = ...; if ($p) { $p.Property }`)
- `-ErrorAction SilentlyContinue` + `stdio: ["pipe", "pipe", "ignore"]` to suppress any residual stderr
Fixes the recurring `Tests on windows-latest` flake on main.
## Test plan
- [x] All 196 CLI test files pass locally
- [ ] Windows CI should no longer exit 1 from PowerShell stderr noise
— Miga
|
||
|
|
b28747df0f | fix(cli): respect timeline-free static compositions (#3533) | ||
|
|
0c9d234bd8 |
Merge pull request #3481 from heygen-com/fix/web-audio-cross-origin-silence-v2
fix(core): prevent cross-origin Web Audio capture from silencing audio |
||
|
|
cce17da5a9 |
fix(core): prevent cross-origin Web Audio capture from silencing audio
Classify each <audio> element before Web Audio capture: same-origin, CORS-opted-in, or a non-http(s) scheme stays on the primary createMediaElementSource() path; cross-origin media without a crossorigin opt-in withholds that call (the Web Audio spec makes such a node output silence without throwing) and falls back to fetch + decodeAudioData, preserving the FX graph whenever the server allows CORS. Recheck the route at the transport's irreversible capture boundary, and account for currentSrc, src, and <source> candidates the same way the HTML resource-selection algorithm does. Emit a stable preview diagnostic (`runtime_web_audio_bypass`) at media discovery time, not only from playback scheduling, so `hyperframes check` — which seeks but never plays — can surface it as a `web_audio_bypass` finding. Diagnostics are suppressed during export rendering, where the producer mixes audio offline and already applies the FX chain. The existing non-unit-rate fail-closed rule stays scoped to fx-chain/automation so this fix does not newly mute grouped or above-unity tracks. Takes over #3459 with the data-native-audio escape hatch removed per review feedback: the automatic cross-origin detection already covers the cases that mattered, so the extra per-element opt-in attribute, its route-classifier branch, and its diagnostic path are dropped in favor of a single automatic behavior. Fixes #3458 Original-Author: desenmeng Co-Authored-By: desenmeng <desenmeng@users.noreply.github.com> Co-Authored-By: Miga <noreply@anthropic.com> |
||
|
|
045b3a4fd7 |
feat(cli): report which catalog items a render actually used (#3470)
`registry_item_added` fires when a catalog block is installed and `render_complete` fires when a video is produced, but nothing joined them, so "did this video use the catalog?" had no answer. `hyperframes add` now records each installed item in `hyperframes.json` (installed files are plain composition HTML with no provenance marker, so this manifest is the only record that a file came from the registry), and `render_complete` reports both the items the project installed and the blocks the rendered composition actually reaches. An item installed and then never mounted was tried and dropped, which no add-time event can express. The scan answering "which sub-compositions does this file mount" now has one owner, `collectSubCompositionSrcs` in `@hyperframes/parsers`, shared with lint's `lintMissingOrEmptySubComposition`. It holds two invariants that were previously restated per call site and got re-derived wrongly: it is a text scan rather than a DOM query, because `<template>` content is inert and every sub-composition except the render entry is wrapped in one; and references resolve root-relative at every nesting level, matching `parseSubCompositions`. It walks tag by tag rather than running open-ended spans across the whole file, so a malformed composition cannot stall the render plan. Also: `registryItems` is declared in the config schema, which closes with `additionalProperties: false`, with an ajv-backed test pinning every key the CLI writes; counts are never truncated by the name cap, and the reported used blocks stay a subset of the reported installed ones, with `registry_items_truncated` marking a windowed list; and an unreadable manifest reports itself rather than posing as a project that never used the catalog. |
||
|
|
95e1ac9f04 | fix(skills): skip mirror fan-out to agents that read the universal store (#3325) |