mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-14 18:01:20 +08:00
main
4324 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f6041d7597 |
fix(lint): preserve opening-tag source bounds after multiline closes (#3917)
Capture explicit source name bounds before attributes are parsed. Preserve malformed names, Unicode lowercase expansion, and existing implied-tag ranges while retaining malformed-attribute lint failures. |
||
|
|
dc6d78b462 |
fix(cli): assign w{index} ids when loading JSON transcripts (#3770)
loadTranscript assigns id: w{index} on the srt/vtt branches but never on
the JSON branches: parseWhisperCpp and parseOpenAI drop the field and the
words-json branch defaults it to "". Every engine funnels through
loadTranscript, and transcribeAudio rewrites transcript.json from its
output, so CLI-produced transcripts ship without the stable word ids that
transcribe.md documents for caption overrides — per-word overrides have
nothing to key on.
Assign id: w.id || `w{index}` across the JSON branches, matching the
srt/vtt behavior. || also repairs the empty-string ids older CLIs wrote
to words-json files, which otherwise collapse every word onto one key.
Signed-off-by: Santhi Prakash <b.santhiprakash@gmail.com>
|
||
|
|
558c11b019 |
chore(deps): update github-actions (#3932)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
10e8447ac0 | chore: release v0.8.38 (#3929) v0.8.38 | ||
|
|
f059a6eb53 |
feat(cli): default BeginFrame, agent blank, and looks quality (#3927)
* feat(cli): render summary names the capture path, gpu mode and stage timings * fix(lint): drop gsap_exit_missing_hard_kill, its hazard does not reproduce on cold workers * fix(cli): render slow-path hint fires only when the gpu was auto-probed * test(cli): isolate agent hint suite from ambient agent env keys The suite runs inside agent sessions. Ambient env keys fill the 16-key hint cap and fail assertions that a plain shell would pass. * docs(music-to-video): drop leftover hard-kill mandate after lint rule removal Frame workers still required a tl.set hard-kill that the dropped lint rule no longer enforces. Align the self-check with montage.md. * refactor(cli): share hint-key pattern and collapse render summary args Export HINT_KEY_PATTERN for the test isolation strip. Read duration and frame count from the perf summary instead of passing them beside it. * test(cli): lock capture-mode preference and 16-key hint isolation Session mode, including drawelement, must print over observability. The hint suite now fails if ambient keys already fill the 16-key cap. * feat(cli): default BeginFrame, agent blank, and looks quality Local auto render opts out of the software-GPU screenshot clamp so BeginFrame can run. init --agent scaffolds a centered Inter blank with no prompts. --quality looks (now the default) is CRF 16; delivery is high. * fix(cli): honor screenshot env and teach agent pitfalls on the happy path Local auto still requests BeginFrame unless PRODUCER_FORCE_SCREENSHOT=true. The agent blank fills its canvas so --resolution can resize it. Skills now name the four pitfalls and tell agents to read the render summary line. * feat(cli): default init to the centered blank Bare init scaffolds the centered Inter stage. --agent is a hidden alias. * fix(cli): pack from-file and use it for video init TTY --video with no --example selects from-file. Pack copies that template. * fix(cli): wire --video onto from-file even with --example blank One resolver picks the scaffold. Spawned init --video asserts a-roll src. * fix(cli): narrow looks CRF on the quality alias Only looks carries crf. Typecheck failed on the union without a guard. * style(cli): shorten render-pipeline summary comment |
||
|
|
95bea1631f |
refactor(skills): keep hyperframes-core as the HTML contract (#3928)
* refactor(skills): keep hyperframes-core as the HTML contract Move brief, storyboard, review, production, dispatch, and frame-worker docs to hyperframes. Slim remaining core references. * fix(skills): restore camera recipes and root sizing Restore Zoom, Ken Burns, crop, and clip-path recipes. #root is 100 percent. * fix(skills): stamp size on the composition root Runtime sizes the composition root, not html/body. Overlap pin requires is valid. |
||
|
|
e30992fdb8 |
Merge pull request #3925 from heygen-com/release/v0.8.37
chore: release v0.8.37v0.8.37 |
||
|
|
7a5d2bf769 | chore: release v0.8.37 | ||
|
|
cd975d8c28 |
fix(core/bundler): inline fonts and images so a lone bundle renders (#3590)
`bundleToSingleHtml` documented itself as producing "a single self-contained HTML file", but `INLINE_MIME` covered only `.svg`, `.json`, `.txt`, `.cube` and `.xml`. Every font and raster image stayed a live project-relative reference. That is invisible to every consumer in this repo, because each one serves the bundled string from a server rooted at the project directory, so the relative paths resolve. It breaks the moment the bundle is stored on its own, with no sibling asset directory: the font 404s and the page silently reflows in a fallback face, which is worse than a visible failure. Widen the inline set to fonts (woff2/woff/ttf/otf) and raster images (png/jpg/jpeg/gif/webp/avif), behind a 2 MiB per-asset cap. The cap is measured against this repo's own assets rather than guessed: the largest of 164 tracked `.woff2` files is 105 KB, and the largest of 284 tracked raster images is 2.00 MB, so everything in-tree inlines while a video-sized file cannot. Oversized assets keep their relative URL and warn, reusing the existing "may not be self-contained" wording. Audio and video stay external on purpose: they are large, streamed rather than laid out, and their absence is obvious rather than silent. Scripts already had a better path (`script[src]` is folded in as source), so they are deliberately not added to the MIME table. The five rebasing tests that asserted a relative path survived now assert the data URL's decoded content instead. That is a stronger check: resolving from the wrong base directory finds no file, so nothing inlines and the assertion fails. |
||
|
|
4c26ffe8ce |
fix(render): keep verification fallback storage-aware (#3703)
* fix(render): keep verification fallback storage-aware * fix(render): honour storage-aware verification fallback for every routing kind The disk-headroom flag was computed by the orchestrator for both worker_inversion and parallel_router routings but only honoured by replanAfterFailure under worker_inversion, so the default-on parallel_router path still reverted to an sdr_disk plan that the 90% gate would reject and assertDiskCaptureHeadroom aborted the render. Give the predicate a single owner: drawElementVerificationFailure decides when headroom can change the fallback (non-default routing, sdr_disk preferred fallback, off-disk memory-exhaustion fallback) and consults the shared inspector only then. replanAfterFailure now honours the resulting flag for any routing kind, and the orchestrator merges the two revert log branches keyed on that flag instead of on plan kind, which also stops OOM retries from being logged as headroom diversions. inspectDiskCaptureHeadroom returns a discriminated union so the assert no longer needs a redundant null guard to narrow freeBytes. Tests: parallel_router headroom diversion and restoration, the failure constructor across default / streaming-only / disk-only routings, and an exact-once statfs count. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * test(render): prove non-verification capture failures skip the disk inspector Extract the streaming drain's failure construction into `streamingCaptureFailure` so the retry classification is unit-testable, and assert that a drawElement capture failure or OOM on a routed plan never calls the disk headroom inspector — only a self-verification failure may, via `drawElementVerificationFailure`. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> --------- Co-authored-by: miga-heygen <miguel.sierra_miga@heygen.com> |
||
|
|
fdf9ffac95 | fix(engine): preserve nested sibling video layout (#3639) | ||
|
|
1acc65dbf8 |
fix(core): un-hide a later root-level clip instead of leaving it display:none forever (#3893)
A root-level `[data-start]` clip with no authored `position` starts out `position: static` until the runtime forces it to `position: absolute`. A visibility pass over the clip while it's still inactive can observe the pre-forcing `static` value and cache it as in-flow, which correctly hides the clip with `display: none`. But by the time the clip later becomes active, that same cache has been invalidated and recomputed against the now-forced `absolute` position — so the un-hide check, which re-derived the same cached fact instead of tracking what it had actually done, saw "not in-flow" and skipped the removal. The clip stayed `display: none` (and therefore zero-sized) for the rest of the render. Tracks whether this code applied `display: none` to a given element in its own WeakSet instead of re-deriving the in-flow cache, so the un-hide check can no longer disagree with the hide check that set it. Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
58707afaa0 | fix(producer): reserve AAC correction headroom (#3693) | ||
|
|
4aa6e17b76 |
docs: fix skill documentation cross-references (#3920)
Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com> Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com> |
||
|
|
f86aae655a | chore: release v0.8.36 (#3911) v0.8.36 | ||
|
|
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
|
||
|
|
51a88b9566 |
docs(catalog): put the install command above the preview (#3888)
The install command is the one line a reader comes to a catalog page to copy, and it sat below the whole variables explorer, under its Customize panel, a screen or more down on long items. Move the Install section above the preview and regenerate all 399 pages. |
||
|
|
63574a7c7f |
Merge pull request #3887 from heygen-com/release/v0.8.35
chore: release v0.8.35v0.8.35 |
||
|
|
4ba8396c27 |
chore: release v0.8.35
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
b848d81681 |
Merge pull request #3878 from heygen-com/docs/require-voiceover-carve
feat(audio): require the voiceover carve, default strength 0.8, slower release |
||
|
|
849ce0176a |
fix(catalog): name the better search tier when a search finds nothing (#3872)
* fix(catalog): name the better search tier when a search finds nothing The hint that tells a scripted caller the on-device tier exists was wired to the branch where the search succeeded, and was silent on the branch where it found nothing. The caller with the most reason to hear it was the only one who never did. It also only ever reached stderr, so a --json caller never saw it at all. The hint is now a returned sentence pushed into the same warnings array the zero-result envelope already serializes, and printed from that one value, so the terminal and the envelope cannot drift. Withheld when the query parsed to no searchable tokens, where the advice is already to search in English, and when a warning has already explained why the tier cannot run here. The consent gate is untouched: nothing downloads, and the sentence still asks the caller to check with a person before enabling it. * fix(review): pass the resolved model status, and pin both hint guards Deriving the status inside the hint helper re-read and re-hashed the on-device model from disk to answer a question the caller had already answered a few lines earlier. It now takes the resolved value. Both guards below the status check were invisible to the suite: every other case pinned a status that already returned null, so deleting either guard passed everything. Two tests now hold them, and each fails for its own mutation. * docs(skills): tell agents to read the tier hint the envelope now carries The CLI skill said that under --json nothing about the offline tier is printed at all. An agent following that sentence would never read the warnings array this branch writes the tier sentence into, so the fix would land in the code and never reach the caller it was written for. It also described warnings as firing only when a tier was asked for and could not run. That is now too narrow: a zero-result search emits the hint with no tier requested. Regenerate skills-manifest.json for the changed content hash. |
||
|
|
06897fe0be |
style(audio): format carve.test.mjs and re-sync the skills manifest
The pre-commit format hook's glob does not include .mjs, so the new test file reached CI unformatted while oxfmt --check . covers it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
26513984f3 |
fix(producer): non-DE parallel-stream router silently refused on macOS multi-worker renders (#3886)
* fix(producer): read useDrawElement post-clamp for the parallel-stream router The non-DE parallel-streaming router read cfg.useDrawElement before the default-on drawElement clamp ran, so on every macOS/GPU-default host with multi-worker renders it saw a stale true and refused to stream even though the render was about to fall back to non-DE screenshot capture anyway. * refactor: trim fix-narrating comments to durable invariants Adversarial simplify pass flagged two comments that described the diff itself (what used to happen, what changed) instead of the invariant that matters going forward. Kept only the forward-looking reasoning. * fix(producer): capture probe console before closing on the DE clamp path; add regression coverage - Extract the default-on drawElement clamp condition into a named, exported, unit-tested pure predicate (shouldClampDefaultDrawElement), matching this file's existing convention for the sibling router predicate. - Preserve lastBrowserConsole when the clamp closes a drawElement probe session, matching every other probe-close site in this function (an adversarial review caught this: on the auto-worker-calibration path, a render that throws after the clamp would report the calibration browser's console instead of the probe's). - Add tests proving the router requires the clamp's post-clamp output, not its input — the exact ordering bug PRINFRA-689 was about. * test: drop internal link and fix-narrating comment from new regression test A test comment I added carried an internal Slack archive link (this is a public repo) and described the diff instead of the invariant — the user caught it in review after my own simplify pass had already run and missed it, since that pass ran before this test file existed. * refactor: collapse duplicated ordering-invariant comment into one place Two comment blocks explained the same 'why deParallelStreamForced alone is safe here' reasoning, one forward-referencing the other. Stated once. |
||
|
|
e488871ce1 |
fix(audio): pin the slower release and the carve default, fold the requirement, sync the manifest
Review at
|
||
|
|
42a3e7a519 |
fix(skills): loop-extended BGM writes .mp3, not MP3-in-a-.wav (#3884)
ensureBgmCovers() (duplicated in faceless-explainer, pr-to-video, and product-launch-video's assemble-index.mjs) always re-encodes a short BGM track with libmp3lame, but preserved the source asset's own extension when naming the loop-extended output — so a "bgm.wav" source produced "bgm.loop.wav" containing genuine MP3 audio. ffprobe/ffmpeg/Chromium all decode this correctly via the WAVE_FORMAT_MPEGLAYER3 tag, but a naive/strict WAV parser (e.g. Python's stdlib `wave` module) hard-fails on it. The function's own doc comment already documented the intended output as "*.loop.mp3" — the code just didn't match it. Fix: always emit a .loop.mp3 path regardless of the source extension. music-to-video's assemble-index.mjs does not have this function (it uses the raw BGM asset directly, no loop-extension), so only 3 of the 4 skills needed the change. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
fe53f51adb |
fix(core): slow the voiceover carve's level release to 2.4s
At 1.6s the bed audibly came back at every sentence break on narrated builds, which reads as the effect switching off rather than the mix breathing. 2.4s lets it swell back over a breath. Band-filter release is unchanged; a notch closing is inaudible. Test timings move with the constant. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
2e2b5c8530 |
feat(hyperframes-audio): default the voiceover carve strength to 0.8
0.25 left the bed audibly fighting the voice; a build carved at 0.25 had to be redone at 0.8 before it was accepted. Make carve.mjs default to 0.8 and rewrite the strength prose and the sample output to match. The core DEFAULT_CARVE used by the Studio panel is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
0b9852fee4 |
docs(skills): require the voiceover carve whenever music plays under a voice
The hyperframes-audio skill described the carve as one option among several, so builds with BGM under narration shipped with only a volume duck. Make it a requirement in the three places an agent reads while assembling audio: the carve section of hyperframes-audio, general-video's Assemble step, and the production loop's Audio stage. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
ea7e1dbd0b |
fix(cli): surface a warning when doctor's HYPERFRAMES_PYTHON override is rejected (#3870)
## Summary - `findPython()` correctly resolves `HYPERFRAMES_PYTHON` before falling back to the PATH probe — that part already works, confirmed by direct testing. But when the override is set and fails validation (nonexistent path, non-executable, non-Python-3 output, timeout), it silently falls through to the PATH probe with zero diagnostic. A user whose override had any subtle issue got a plain "Not installed" from `doctor` with no signal the variable was even seen. - This is a corrected, narrower version of a report that originally claimed `doctor` ignores `HYPERFRAMES_PYTHON` entirely — that claim was refuted directly (the override resolution works). The real defect is the silent validation-failure path. - Extracts the override-validation logic into `validatePythonOverride()` and adds `describeRejectedPythonOverride()`, which `doctor`'s TTS (Kokoro) and BGM (MusicGen) checks now call to append the rejection reason to their `detail` when applicable. `findPython()`'s own behavior (including its fallback) is unchanged. PRINFRA-669 ## Test plan - [x] New `packages/cli/src/tts/python.test.ts`: `describeRejectedPythonOverride` returns null when unset / when the override validates; names the override + exception message when the override can't run; names the override + actual output when it isn't Python 3; `findPython` still falls back to the PATH probe when the override is rejected (unchanged behavior) and still uses a valid override directly. - [x] Confirmed RED against the pre-fix source (tagged stash) — all 4 new `describeRejectedPythonOverride` tests failed with "not a function"; GREEN after restoring the fix. - [x] `bunx tsc --noEmit`, `bunx oxlint`, `bunx oxfmt --write` clean on changed files. - [x] `bunx fallow audit --base origin/main --fail-on-issues`: no issues in the 3 changed files. - [x] Full `packages/cli` vitest suite: 3033/3038 passing (2 pre-existing unrelated failures — a PID/socket sandbox quirk and an agent-env-var-pollution test — plus 4 browser-test files failing at collection on a pre-existing `node:` builtin import issue under this sandbox's happy-dom setup; all confirmed identical on pristine `origin/main` and unrelated to this change, consistent with every other fix from this backlog-drain session). 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
2ef76bf97b |
fix(lint): flag an unclosed tag that swallows a nested element as bogus attribute text (#3869)
A start tag missing its closing `>` before the next `<` (e.g. a bad string-replace that leaves `<img ... <div class="hl"></div>` behind) is parsed leniently by the browser's HTML5 tokenizer: the `<div` text is consumed as a bogus attribute name on the still-open `img` tag, and the intended element never becomes a real DOM node. No existing lint/check gate catches this — they all operate on the resolved DOM, which looks structurally valid once the browser has already dropped the element. Adds `unclosed_tag_swallowed_element`, a new core lint rule that flags any parsed tag whose attribute text contains a `<` outside a quoted value (a legitimate attribute value may itself contain a raw `<`, e.g. `data-expr="x < y"`, which htmlparser2 parses correctly and is not flagged). PRINFRA-668 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
bafc7b4e00 |
fix(cli): stop promising a universal per-time filmstrip for --layout strip (#3867)
## What `keyframes --shot --layout strip`'s help text, its type's inline doc comment, and the CLI reference docs all described `strip` as an unqualified "filmstrip by time." The tool doesn't actually do that for the overwhelmingly common case. ## Why A real per-time pixel filmstrip is only produced when the sampled selector is an SVG element (gated by an internal shape check — `typeof element.getBBox === "function" && typeof element.getScreenCTM === "function"`). Any other selector — including every nested sub-composition host, which is always a `<div data-composition-src>` — silently falls back to one live screenshot plus vector position markers instead. This isn't a capture bug: for a non-SVG selector, real per-time pixel compositing was never implemented, only 3D bbox/marker sampling. But the documented behavior over-promised what the tool does, so a user following the docs on the common case (a DOM/sub-composition selector) sees root captions and empty image boxes where they expected the nested composition's actual content to move across frames — the diagnostic strip is misleading, even though the real render is correct. ## How Reworded all three descriptions (CLI help text, `ShotOptions.layout` TSDoc, and the reference docs table) to state the SVG-only condition and the DOM/sub-composition fallback explicitly. No behavior changed — this is a documentation-accuracy fix, per the ticket's own framing that a doc-only fix fully resolves the reported symptom (a silent, misleading omission) for a P3. ## Testing Added a test asserting the CLI help text no longer makes the unqualified "filmstrip by time" claim and does disclose the SVG-only condition — guards against a future regression back to the misleading wording. Verified RED (fails against the pre-fix string) and GREEN (passes after the fix) via a local before/after comparison. - `bunx vitest run src/commands/keyframes.test.ts src/commands/motionShotLayout.test.ts` — 46/46 passing - `bunx tsc --noEmit` in `packages/cli` — clean - `bunx oxlint` / `bunx oxfmt --write` on changed files — clean - Full CLI suite (excluding known-broken-in-sandbox browser-launch tests, unrelated to this change): 217 test files / 3025 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
5a95211e36 |
fix(cli): apply the canvas/video pixel-hash carve-out to img (#3865)
## What
`check`'s sweep_static guard false-positives on a composition that swaps between equal-size, equal-position opaque `<img>` elements — a common authoring pattern for revealing frame N of a still sequence from a paused GSAP cursor. The render itself is correct; only `check`'s verdict is wrong.
## Why
The sweep guard fingerprints every visible element's box + opacity + font-variation-settings per seeked sample. That's deliberately blind to pixel-only motion (a canvas repainting, a video playing) with no element moving, so an existing carve-out downsamples each visible `canvas`/`video` to 8x8 and folds its pixels into the fingerprint specifically to catch that class of motion.
That carve-out's element selector (`root.querySelectorAll("canvas, video")`) never included `img`. An img src/visibility swap between equal-size opaque images moves zero geometry and zero opacity, so it stays outside both the base fingerprint and the pixel-hash carve-out — the whole-run fingerprint reads byte-identical across every sample and `sweep_static` fires on an animating composition.
## How
Widened the selector to `canvas, video, img`. No other change was needed: `mediaPixelHash` already handles `img` correctly — `drawImage` accepts any `CanvasImageSource`, and its width/height detection already falls back to the element's bounding rect the same way it does for `canvas`/`video`.
**Scope note:** this repo has an open PR (#3707) touching the same function (`collectLayoutGeometry` in this same file) for a different, unrelated bug (text/counter fingerprinting). This change is deliberately isolated to the `canvas, video` → `canvas, video, img` selector line and a new comment above it — verified against #3707's current diff that neither touches this exact loop, so the two PRs shouldn't conflict regardless of merge order.
## Testing
Added a test mirroring the existing "changes the sweep fingerprint when visible video pixels advance" test, using an `<img>` element instead of `<video>` with the same pixel-mock approach.
- `bunx tsc --noEmit` in `packages/cli` — clean
- `bunx oxlint` / `bunx oxfmt --write` on changed files — clean, no changes needed
- Full CLI suite (excluding known-broken browser-launch tests unrelated to this change): 217 test files / 3023 tests passing
One local-environment caveat, disclosed for transparency: `layout-audit.browser.test.ts` (the file the new test lives in) can't execute in my local sandbox — it fails identically with or without this change (`No such built-in module: node:`, a happy-dom + Vite externalization issue affecting every test file in this repo that imports Node builtins at the top under `@vitest-environment happy-dom`, not specific to this change). I verified the new test's logic and mocking approach are structurally identical to the existing, CI-passing video test it's modeled on, and will confirm via this PR's CI run rather than a local one.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
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) |
||
|
|
c93c765403 |
Merge pull request #3805 from heygen-com/fix/prinfra-600-fresh-screenshot-fallback
fix(render): retry drawElement failures on a fresh screenshot page |
||
|
|
14b9e2039b |
fix(studio): tolerate WebMCP execute calls without an options object (#3860)
* fix(studio): tolerate WebMCP execute calls without an options object
Every Studio write tool (studio_set_text, studio_set_style,
studio_transform, studio_add_animation, studio_update_animation,
studio_add_keyframe, studio_delete_animation) registered its handler as
`execute: (input, { signal }) => ...`. The W3C shape passes an options
object, but the bundled `@mcp-b/global` polyfill invokes a registered
`execute` with the input alone, both from its in-page BrowserMcpServer
wrapper and from the descriptor it mirrors into a native
`document.modelContext`. The destructure therefore threw
`TypeError: Cannot destructure property 'signal' of 'undefined'` before
the handler ran, while the read tools, which ignore the second
parameter, kept working.
Route the seven signal-taking tools through one `writeTool` helper in
`buildStudioTools` that reads `options?.signal`, so a missing options
object yields an undefined signal at a single boundary. The handlers
already default an undefined signal to a never-aborted one, so nothing
downstream changes. `ModelContextTool.execute` now declares `options`
optional to match what callers actually do.
Upstream check: `@mcp-b/global` 5.0.1 (pinned), 5.0.3 and 5.1.0 ship a
byte-identical `@mcp-b/webmcp-polyfill` chunk and the same one-argument
call in `@mcp-b/webmcp-ts-sdk`, so a dependency bump would not fix this.
Tests: drive `studio_set_text` through the real `@mcp-b/global`
package under jsdom (registry entry `execute` and Chromium-style
`executeTool`) and assert a saved result; call every write tool with
one argument against a fake model context and assert none reports an
`internal` failure. The existing spec-shaped tests, including the early
abort path, still pass, which proves a provided signal still reaches
the handler. Shared inert deps builders move to `webmcpTestUtils.ts`.
Closes #3858
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(studio): cover the native-mirror WebMCP execute path
Install a native-looking `document.modelContext` before importing
`@mcp-b/global` so the bridge wraps it and mirrors every Studio
registration into it. One setup then exercises both one-argument call
paths the package ships: the in-page BrowserMcpServer wrapper (registry
entry and Chromium-style `executeTool`) and the descriptor mirrored into
the native context. Both fail with "Cannot destructure property
'signal' of 'undefined'" without the fix and save with it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
8820a01090 | fix(skills): exclude repo-native skills from public installs (#3856) | ||
|
|
59def1b66d | chore(release): v0.8.34 (#3854) v0.8.34 | ||
|
|
9549e038c4 |
perf(studio): a paused editor stops redrawing its overlays sixty times a second (#3846)
* perf(studio): run the editor overlays on one parkable frame loop The composition rect, the selection and hover boxes, the off-canvas indicators and the snap guides each owned an animation-frame loop that re-armed unconditionally. On a paused, untouched editor that is four callbacks per frame reading layout, and the snap-guide loop writes style on every one of them, so the compositor kept committing 55 frames a second with nothing moving. None of the four has a clock of its own; each only changes when something observable happens. They now share one loop that runs at full rate for a moment after a pointer, key, wheel, scroll, resize or visibility change, after a preview message reporting a frame the overlays have not drawn, or after a mutation inside the preview document, and otherwise polls four times a second so a wake source nobody thought of costs a quarter second of staleness rather than a frozen overlay. The frame comparison on preview messages is load-bearing rather than an optimisation: the paused preview posts an unchanged status message every 80 ms so any listener can confirm its position, and treating that as news held the overlays at 60 fps for the life of the tab. * fix(studio): one overlay's throw must not stop the other four The shared frame loop re-armed after running its subscribers, so a subscriber that threw took down the loop and its idle-poll safety net permanently. The four loops it replaced each re-armed first, which kept that failure to the one overlay that caused it. It now re-arms before running anything and isolates each subscriber, rethrowing out of band so the error still reaches the page's error reporting. The wake also sat outside the recognised-message check, so postMessage traffic from an extension, devtools or any other embed on the page held the overlays awake for 400ms at a time. Only the preview's own messages wake it now, and a repeated paused status post still does not. useMotionPathData had the same unconditional loop and is live whenever a keyframed element is selected; it joins the shared one. * fix(studio): wake the overlays only for the preview |
||
|
|
eae4892ae8 |
chore(deps): update dependency vitest to v4 [security] (#3789)
* chore(deps): update dependency vitest to v4 [security] * fix(test): preserve test behavior on Vitest 4 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: James <james.russo@heygen.com> |
||
|
|
c98d6fbac8 |
fix: keep caption transcript data inside generated scripts (#3847)
* fix: keep caption transcript data inside generated scripts * test: locate caption data without an HTML filtering regex |
||
|
|
c752b89bc9 |
fix(runtime): the editor keeps waking during a drag on a preview it did not build (#3850)
The manual-edit gesture watch tested mutation targets with `instanceof Element`. The composition body is adopted into the preview frame, so its nodes answer to another realm's Element and the check is false for every one of them: the watch never sees a gesture, and the paused transport it gates does not wake while the user drags. Routes the check through the runtime's structural predicate, which is what the preview-guard lint added alongside it now requires. Main is red on that lint for this line, so this also unbreaks it. Test adopts an element from a second realm and asserts the marker is seen and cleared; it fails with the identity check restored. |
||
|
|
d4fba55a4e |
perf(runtime): a paused preview stops burning CPU (#3845)
* perf(runtime): stop the preview transport ticking when the editor is paused A paused, untouched preview asked the browser for a fresh frame sixty times a second and re-read the page on each one. Nothing it looked at could change without some observable event firing, so the loop now stands down and wakes on that event instead, with a slow timer as the safety net. Parked, the loop keeps two jobs the 60 Hz version did implicitly: the control bridge's paused heartbeat, on the same interval as before, and a re-read of the timeline registry, which is a plain object no observer and no event can report. Everything else arrives by an event now: timing-attribute edits, mounted or removed timed elements and media metadata through the composition-timing observer that already existed; a manual-edit gesture starting or ending through a new attribute-filtered observer, which also replaces a whole-document query that ran on every paused frame; and playhead or play-state changes through the forced state post every transport mutation already ends in. Three periodic jobs (re-binding the root timeline, posting the clip manifest, binding media-metadata listeners) used a frame counter as a proxy for "the document may have changed". They now ask that question directly, because the counter stops advancing while the loop is parked. The render path is untouched: the loop never parks while an export render is driving frames. That test is the pair of renderCaptureSeekStarted and the producer's injected seek config, not the flag alone, because Studio's own preview falls back to renderSeek for overhanging timelines. Idle, paused, no input, on a 1689-element project: main-thread self time 128 -> 8.5 ms/s and animation-frame callbacks 282 -> 4.3 per second, both measured with the runtime and editor changes in place. * fix(runtime): keep the rebind policy the only owner of "may rebind now" The parked-loop change let a composition-timing change OR its way past shouldAttemptPeriodicTimelineBind, which removed the hold that keeps an async rebind off the first two seconds of playback, and let the clip manifest post on every frame of a composition that mutates the DOM every frame (measured: 30 posts in 30 frames against one). The change is now an input to that policy, which still applies the hold, and the change-driven path is confined to the paused path and rate-limited to the posts per second the frame counter already produced. A change it defers stays pending, and a pending change keeps the loop awake, so deferring can never drop it. Two more holes from the same review: The parked poll compared only the composition timing revision, so an adapter duration floor that grew was never noticed. Adapters infer duration from live animation objects that change with no DOM mutation and no media event, which makes it the second input nothing can push; both are now in one witness. Draining the gesture observer's records to answer within a task suppressed the observer's own callback for them, so a reader could consume the notification that un-parks the transport. Draining now notifies. The watch reports whether it is observing at all, and the loop refuses to park when it is not. That path is unreachable today because the colour-grading runtime constructs a MutationObserver unconditionally during the same init; the flag is the explicit statement of the invariant for the day that changes. * fix(runtime): clear the pending-change latch after the post, not before postTimeline walks author DOM and can throw. The tail scheduler runs in the tick's finally either way, so clearing the latch first let it see nothing owed and park with the change undelivered — and nothing would deliver it until some unrelated change happened to wake the loop again. Clearing after the post means a throw leaves the change owed, the loop stays awake, and the frame counter retries it within twenty frames. Same rule the shared editor loop already follows for its own re-arm. Also rewords the note on draining the gesture observer's records: that is hardening, not a fix for a live lost wake. isActive has exactly one caller today, inside transportTick, and a tick is its own task, so the observer's microtask has already run by then. * fix(runtime): stop the parked heartbeat when the page has gone away The parked transport holds a timer where the old loop held only an animation frame, and a frame is discarded when a page or a test environment is torn down while a timer is not. A test that initialises a runtime and abandons it therefore left an 80ms timer to fire into a dead global, which CI reported as an unhandled ReferenceError attributed to whichever file was running when it landed. Two changes, because the leak has two ends. The heartbeat stops instead of re-arming when window or document is gone: nothing is left to report a state change to, so stopping is the answer rather than throwing. And the one test that initialised a runtime without ever tearing it down now tears it down. |
||
|
|
73dfe35e46 |
fix(preview): show the correct scene instead of every scene at once on some loads (#3848)
* fix(runtime): show the right scene when the preview DOM comes from another window The Studio preview sometimes builds the composition body in the editor window and adopts it into the preview frame's document. Adopted nodes keep the prototypes of the realm that created them, so `node instanceof HTMLElement` is false for every element in the composition even though the elements are ordinary HTML sitting in that document. Every guard in the runtime written as `if (!(node instanceof HTMLElement))` then skipped the whole document, silently: nothing threw, nothing logged, and readiness still reported success. The timed-element visibility pass wrote no inline visibility at all, so on those loads the preview painted every scene on top of every other and the editor drew off-canvas markers for elements the user could not see. The auto-stamp pass stopped stamping too, which is why the composition came up one timeline clip short. Replace every realm-sensitive element check in the runtime with structural predicates that ask what a node IS (node type, namespace, tag name) rather than which window's constructor made it. Two local `doc.defaultView.HTMLElement` workarounds are deleted with it: they fix only the case where the nodes belong to the document's own realm, and adoption is exactly what breaks that. * fix(studio): scrub the music track the timeline named, not the first audio `resolveScrubAudioEl` tested `byId instanceof HTMLAudioElement` on a node from the preview iframe's document. That node is an instance of the IFRAME's `HTMLAudioElement`, never this module's, so the check was false on every load and the `musicId` hint was dead. Scrub fell through to the first `<audio>` in the document, which the comment right above it warns can be the voiceover, so dragging the playhead could preview the wrong track. Ask what the node is instead. Also close the gaps an independent review found in the runtime fix: - the cross-realm predicate test had no `<audio>` and no `<img>`, so reverting `isAudioElement` or `isImageElement` to `instanceof` left it green. It no longer does, and the audio case also pins `isMediaElement`, which composes from it and gates the media sync path. - nothing stopped the runtime regressing. `lint-runtime-preview-guards.ts` gains a second check kind: patterns that must be ABSENT under a directory, seeded with DOM-typed `instanceof` under `src/runtime`, pointing at domRealm.ts. Comment lines and tests are exempt, both on purpose. - domRealm.ts stated the adoption mechanism as settled fact. The mixed prototypes are measured; how the nodes get into the frame is not identified, and the docstring now says which is which. Its ownership claim is scoped to the runtime, since packages/studio still hand-rolls its own checks. |
||
|
|
0408fbcd99 |
perf(studio): scrubbing a large composition no longer stalls the editor (#3842)
* perf(studio): ask each ancestor once per off-canvas rebuild, not once per element The dashed off-canvas markers are rebuilt whenever anything in the preview changes, which during playback or a scrub is several times a second. Most of what a rebuild asked about an element was really a question about its ANCESTORS: does each node up to the root render, what does each contribute to the composed transform, which node is the source-file boundary. Two siblings share their whole chain, so a preview of a thousand elements asked the platform the same questions about the same ancestors a thousand times over. A rebuild now threads one measure pass through the walk, and each node answers once. On a 1689-element carousel that takes computed-style reads from 11,159 to 2,526 per rebuild and the rebuild itself from 59 to 12-18 ms per seek, with the rendered marker set byte-identical. The pass is deliberately not a cache. A cache would have to say what could have changed since last time, and for a measurement the answer is anything: an image finishing decode, a font swapping in, a transition frame, a container query, an inserted stylesheet rule all move an element's box with nothing written to the DOM and no record to invalidate on. A pass lives inside one synchronous measurement that only reads, so nothing can move under it, and it is dropped when the measurement ends. Every rebuild still measures every element. Layout reads are untouched on purpose: each element still takes its own client rect every rebuild, because that is the read that cannot be shared and must not be remembered. * test(studio): give one PropertyPanel case the file's own render timeout Every other render test in that file passes RENDER_TIMEOUT_MS; this it.each was left on vitest's 5s default and times out when the whole suite runs on a loaded machine. Unrelated to any behaviour: it passes on either side of the change when the machine is quiet, and fails in a full-suite run on either side when it is not. * refactor(studio): share the source-boundary walk and dedupe the rebuild fixtures Three follow-ups from review of the measure pass, none of them behaviour: The source boundary was memoized against the element that asked for it, so two siblings still walked their whole chain separately and only the answer was reused. It now memoizes every node on the way up, like the other two walks: an element's boundary IS its parent's unless the element is one itself. `isElementVisibleThroughAncestors` returns what it always returned, but it now resolves top-down, so the set of nodes it takes a style read for is different (smaller for a subtree hidden near the root). Three callers use it with no memo, so that is written down next to it rather than left to be discovered. The three rebuild fixtures in the indicator tests each carried their own copy of the iframe/overlay/layout-stub scaffolding, which is four clone groups and 126 duplicated lines. One `mountPreview` helper now serves all of them and the pre-existing selector-index fixture too. * test(studio): pin the measure pass to one rebuild through an ancestor-derived input The existing no-mutation-record test varies an element's own box, and the pass never memoizes a box — so hoisting the pass to module scope, which is exactly the mistake that would reintroduce cross-rebuild staleness, left every test green. This one fades a wrapper between two rebuilds and asserts the marker under it goes away. Visibility through ancestors IS memoized, so the pass surviving the rebuild serves the stale answer: with `createOverlayMeasurePass()` hoisted the suite exits 1 on this test alone, and exits 0 at head. |
||
|
|
cd1a996ba4 |
fix(catalog): confine hosted preview asset downloads (#3843)
* fix(catalog): confine hosted preview asset downloads * test(catalog): pin credential and port refusal with finite streams |
||
|
|
96637aeb1c | fix(capture): share the render host policy and block localhost aliases (#3844) | ||
|
|
a7e2e3853a |
fix(media): validate every download redirect target (#3841)
* fix(media): validate every download redirect target * fix(media): apply redirect policy to logo HEAD probes |
||
|
|
d393541c2b |
fix(cloud): preserve existing output when downloads fail (#3840)
* fix(cloud): preserve existing output when downloads fail * test(cloud): pin same-directory download staging |
||
|
|
962c95406d |
fix(registry): verify staged asset content before publishing manifests (#3832)
* fix(registry): verify staged asset content before publishing manifests * test(registry): run asset staging regression in script CI * refactor(registry): separate verified manifest publication |
||
|
|
4c7fbde51e |
fix(engine): emit pipelined frame diagnostics before the recoverable wrapper
Review feedback on #3805. captureFrameToBufferPipelined checked isRecoverableDrawElementError and threw DrawElementCaptureError before reaching captureFrameErrorDiagnostics, so the NCPR/canvas failures that now abort the whole attempt produced no frame-error PNG/HTML/JSON bundle — the exact case worth debugging, and the one the adjacent comment promised mirrored the serial path. The serial path was unaffected because its own DrawElementCaptureError throw propagates through captureFrameCore's outer diagnostics catch. Run diagnostics first, then the recoverable wrapper. Bounded to at most one bundle per attempt, since a recoverable error fails the whole attempt, and captureFrameErrorDiagnostics self-catches, so a dead page cannot mask the structural error the producer's fresh-page retry depends on. Covered by a new test asserting the bundle lands for a recoverable pipelined failure; verified by mutation (restoring the old order fails it). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |