mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-06 14:47:29 +00:00
sync/hyperframes-codegen-17ff6eb1
881
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ebc613b818 | codegen: resync cloud client from EF 17ff6eb1 | ||
|
|
67c363b9cf |
fix(cli): keep live reload working after an atomic save on Linux (#4346)
* fix(cli): keep hearing a file after an atomic save on Linux * fix(cli): skip a subdirectory the watcher cannot open * test(cli): pin that one unwatchable subdirectory leaves the project watched * test(cli): assert the tree keeps its other watches when one subdirectory fails * test(cli): cover the manifest directory and prefix-named siblings * test(cli): reset fake directories per test and cover an unwatchable root |
||
|
|
8cb7d06db6 |
perf(studio): re-render only the edited scene's thumbnails after an edit (#4315)
* perf(studio): refresh only the edited scene's thumbnails and let the preview load first One scene edit regenerated every sidebar and timeline thumbnail, and those renders starved the new preview's requests on the Studio server. The thumbnail cache now keys on what a composition renders from, the server names the compositions a write affects, only those thumbnails refresh, and composition thumbnails wait while a shadow preview reload is in flight. * test(studio): pin scene-scoped thumbnail reuse and the preview-reload hold Memoizes the scoped thumbnail signature on the project signature so requests keep reusing the adapter's cached one, keeps the fresh-signature check that catches a write the watcher has not reported yet, and adds tests for sibling-scene reuse, root-head invalidation, the affected-composition set, per-path revisions and the scheduler hold. * refactor(studio): share the file-change payload guard * fix(studio): match thumbnail revisions for scenes mounted with a ./ path * perf(studio): keep signature cache keys constant-size * style(studio): format thumbnail-scope changes * docs(studio): trim thumbnail input comments |
||
|
|
b32b9a7dab |
feat(runtime): data-vfx-chain — a seek-deterministic per-pixel effect element (#4306)
* feat(core): data-vfx-chain defs and chain parsing (mirrors data-fx-chain)
Mirrors audioFx.ts's shape for the per-pixel VFX chain: a versioned
JSON chain attribute (data-vfx-chain), a def registry with typed
params (number/enum/bool/ref), and parse/serialize/normalize helpers.
HF_VFX holds one def, fractal-noise, with a placeholder frag; the
kernel and its consumers (packages/core/src/runtime/vfx.ts) land in
Task 1.2/1.3 of the vfx-chain-runtime plan, so vfx.ts is added to
.fallowrc.jsonc's entry list for now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(runtime): paint data-vfx-chain canvases on every seek
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): fractal-noise kernel (Basic/fBm approximation) with CPU reference
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): self capture through layoutsubtree with the page-composite readiness protocol
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): wave-warp kernel (Sine)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): displacement-map kernel (self-referential map)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(vfx): chain order is nesting order; preceding CSS effects are captured
...and following ones apply outside. The plan's one-line message for Task 2.4
is 105 characters; commitlint caps the header at 100, so the tail moved here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(vfx): seek determinism and CPU-reference PSNR for fractal-noise
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): size the output canvas from the host's layout box, not its transformed AABB
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(telemetry): vfx node count, capture class and types on render_complete
Threads vfxNodeCount, vfxCapture and vfxTypes from a static scan of the
compiled composition's data-vfx-chain hosts (mirrors the existing
compositionElementCount/colorGradingCount scan) through
RenderPerfSummary.drawElement into the render_complete event, next to
de_capture_mode. Exports chainCapture/parseVfxChain/HfVfxCapture from
@hyperframes/core's index for the producer to consume. The PostHog
dashboard tile is not created here — left for the coordinator.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(vfx): data-vfx-chain guide — element shape, capture table, Studio flag, known constraints
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): register chain hosts that arrive with a sub-composition mount
`initVfx(document.body, fps)` ran once, synchronously, three lines after
`void loadExternalCompositions(...)` — so every `data-vfx-chain` host that
enters the DOM only when a `data-composition-src` mount resolves was never
registered. An unregistered host paints nothing and reports nothing: no
`[HyperFrames] composition script error`, the `.hf-vfx-out` canvas keeps its
300x150 default, and `paintVfx` never even arms the page-composite flag.
Measured on a real multi-file export served raw: `#main-l5-text`'s output
canvas stayed 300x150 and `window.__timelines` never gained the sub-comp.
Re-scan in the loader's `.finally`, before `maybePublishRenderReady()`, so
registration precedes readiness. `initVfx` now releases the outgoing
registry's programs, capture textures and ping-pong targets first — the
output canvas hands back the same WebGL2 context on a re-scan, so the
previous pass's GL objects would otherwise leak.
Two browser cases in `vfxDeterminism.test.ts`, both against the real runtime
bundle: a host whose `.hf-vfx-in` IS the mount (already worked — the mount
replaces the wrapper's children, not the wrapper, so the registered source
node survives), and a host that arrives INSIDE a mounted sub-composition
(failed with `armed: false` and no error; passes now).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): runtime tsconfig typecheck passes
Under noUncheckedIndexedAccess, resolveCaptureSource's null return
widened VfxEntry.src past its VfxCaptureSource | undefined type; the
function is internal-only, so its return type moves to
VfxCaptureSource | undefined instead. Indexing PingPong.framebuffers
by a non-literal index produced | undefined against
WebGLFramebuffer | null, coalesced to null. No behavior change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): structural DOM predicates instead of realm-bound instanceof
vfx.ts checked `instanceof HTMLCanvasElement`/`HTMLElement` directly, which
returns false for inner-realm nodes when the Studio previews across an
iframe realm. domRealm.ts had no canvas predicate yet, so this adds
isCanvasElement mirroring the existing isImageElement pattern, then swaps
all three vfx.ts checks to the structural predicates the lint guard requires.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): paint self-capture hosts under hyperframes snapshot, not only the engine path
`paintVfx(t, { engineMode: true })` armed the page-composite protocol and
returned, betting that every capturing chain runs under `frameCapture.ts`.
`hyperframes snapshot` (and check/compare/validate/layout, and Studio's
thumbnail capture) seek through the same `renderSeek`, never read
`__hf_page_composite_pending` and never call `__hf_page_composite_resolve` —
so a `self` chain painted nothing there, in silence, because its content
lives in a `layoutsubtree` canvas the browser never paints directly.
Engine mode now arms the preview-side capture as well and paints on whichever
completes first. A seek token stops a superseded async capture from repainting
a newer frame, and a registry-identity check stops it painting an entry
`initVfx` already released. `resolveVfxCapture` stays non-idempotent on
purpose: the engine resolves only after its own before-capture hooks have
mutated the DOM, so its capture is still the authoritative one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): an exporter-emitted .hf-vfx-out gets the box the spec fixes for it
findOrCreateOut adopted a canvas the exporter had already emitted without
applying the layout cssText, so it kept the default position:static;
display:inline and wrapped to the line AFTER the layer-wide .hf-vfx-src
canvas — painting a second, misplaced copy of the layer one full layer
height below the host. Measured on retro-wave #main-l6-text: .hf-vfx-src
rect top 315.1 h 93.0, .hf-vfx-out rect top 409.2, both style=null,
position static (ae-mcp findings Task 3.5b). Hosts with no emitted
out-canvas were never affected, which is why only this one host moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(telemetry): vfx_node_count counts hosts, not chain nodes — rename to vfx_host_count
The field increments once per `data-vfx-chain` attribute occurrence, never
per node inside a chain's JSON, so the name promised something the scan does
not measure. Renames the internal `vfxNodeCount` and the wire property
`vfx_node_count` to `vfxHostCount`/`vfx_host_count` across the orchestrator
scan, the perf summary, the CLI render path and the telemetry event. No
dashboard consumes the field yet, so the wire rename costs nothing now and
would cost a migration later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): join the preview capture to the seek-completion barrier, not race it
`paintVfx` fired `capturePreviewThenPaint` and forgot it, so nothing the CLI
awaits waited for a `self` chain's paint. Every snapshot-family command
(`snapshot`, `check`, `compare`, `validate`, `layout`) seeks through
`seekCompositionTimeline`, which drains `window.__hfWaitForSeekCompletion`
and then allows one settle race before screenshotting — and only the
`"hf-seek"` event's `waitUntil()` could put anything in that barrier. The
screenshot therefore landed on however many hosts happened to have finished,
which varied with host count and machine speed.
Extracts the "track, untrack on settle, retain the first rejection" block
`dispatch()` built inline into an exported `registerSeekCompletion`, and has
`paintVfx` register the preview capture through it. Also awaits the per-host
paints with `Promise.all` instead of sequentially: N hosts cost about two
animation frames now, not 2N.
Measured on the pre-fix bundle with two `self` hosts, both output canvases
read `[0,0,0,0]` at the moment the barrier returned; they painted only
because the settle race that follows happened to be long enough. Both tests
added here fail on that bundle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): report WebGL context loss loudly instead of silently no-op painting
One WebGL2 context per `data-vfx-chain` host, against a browser cap that is
commonly 16 and shared with `colorGrading.ts`'s per-element contexts. A lost
context makes every later call on it do nothing, per spec — so the chain kept
"painting" a blank canvas with no error, which is exactly what the module's
loud-failure contract exists to prevent.
Adds the `webglcontextlost` listener pair's first half, following
`colorGrading.ts`'s precedent: `preventDefault()`, mark the entry, report
once through `reportVfxError`. The entry is then skipped by `paintVfx`,
`capturePreviewThenPaint` and `resolveVfxCapture` — silently, since the one
loud report already happened and repeating it per frame is spam.
Restore is left unhandled on purpose, with the reasoning in the doc comment:
`initVfx`'s existing re-scan already rebuilds every GL object, and a second
narrower rebuild path for `webglcontextrestored` would duplicate it. The
shared-context redesign the review floated is likewise out of scope here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* perf(vfx): cache uniform locations at pass-build time instead of querying every paint
`setPassUniforms` asked the driver for `u_size`, `u_t`, `u_fps` and every
param's location on every paint of every pass, and `bindPass` did the same
for `u_src` on every bind. `buildPasses` already constructs each `VfxPass`
once at init, right after the program links, and a linked program's
locations never change — so they are resolved there now and read from
`pass.locations` in the hot path.
Same values, same `null`-means-optimised-out semantics (`gl.uniform*(null,
…)` is a defined no-op, which is what the per-frame query produced too), so
the existing vfx.test.ts and the browser determinism suite cover it
unchanged — both still green, the latter against a rebuilt runtime bundle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): bound the paint wait per host so a stalled compositor can't hang the seek barrier
`paintVfx` registers `capturePreviewThenPaint` into the shared seek-completion
barrier (
|
||
|
|
42eb6e9ad2 |
fix(cli): replace pre-planted links when writing capture files (#4298)
* fix(cli): replace pre-planted links when writing capture files writeCaptureFileSync wrote through whatever sat at the target path, so a symlink or hard link planted in the capture directory redirected the write to its target. Stage each file in a private directory next to the target and rename it over the target instead, the same publication pattern the font cache uses since #3669. Rename swaps the directory entry, so a planted link is replaced rather than followed, while intentional overwrites such as the deadline partial bundle keep working. Cleanup only removes the staging directory this call created. A caller's own flag (wx for meta.json) is passed through unchanged. Two capture writes bypassed the writer and stayed exposed: the contact sheets (sharp's toFile opens the path itself) and the BLOCKED.md failure note. Route both through writeCaptureFileSync. Fixes #4243 * fix(cli): address CodeQL findings on capture writes Read the captured file before lstat-ing it in the regression tests so CodeQL does not report a check-then-use race, and route both write modes of writeCaptureFileSync through a single writeFileSync call, the one write site the writer already had on main. * fix(cli): route media previews through the capture writer The Lottie and video previews in mediaCapture.ts still let Puppeteer write the PNG itself via page.screenshot({ path }), which opens whatever sits at that path, so a pre-planted link there redirected the write. Take the screenshot as a buffer and write it with writeCaptureFileSync, like screenshotCapture.ts already does. Also restructure writeCaptureFileSync so its only write is the same writeFileSync(path, ...) call main has. Publication now stages through a separate helper that calls back into the writer with O_EXCL (wx) on a fresh staging path, then renames over the target. Behavior is unchanged, and CodeQL keeps tracking the existing network-data-to-file finding on that line instead of reporting it as new. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2KDat7mo5Dr8wFkFzobyb --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
1b8f8a43d4 |
fix(cli): stopping the preview server no longer leaves a Chrome running (#4183)
* fix(cli): stop the preview server's browser when the server exits Cancel in-flight renders and thumbnail launches before draining the browser pool on shutdown, instead of only closing whatever browser was already registered. A render whose Chrome died from the shutdown signal itself was being misclassified as a transient failure and retried with a fresh, untracked browser that outlived the process. Reject new render and thumbnail requests once shutdown has begun, and await an in-flight thumbnail launch before closing it. * fix(cli): close preview browsers before a hung render, keep SIGINT armed shutdown() awaited renders before closing browsers, so a render slower than preview.ts 3s exit watchdog left Chrome running when it fired. Close the thumbnail browser and drain the pool concurrently with, not after, the render wait, and bound the wait under that watchdog. A second Ctrl+C/SIGTERM during shutdown removed the one-shot signal handlers, so it hit the OS default and killed the process before cleanup ran. Use persistent handlers guarded by the existing shuttingDown flag instead. Also: getThumbnailBrowser could still hand a live lease to a request that lands after shuttingDown flips true; trim a comment over budget; replace a fixed-sleep test race with a drain-signal barrier. * fix(engine): make browser pool shutdown terminal, not just draining drain() resets its drainPromise to null once it settles, so acquire() only waits for an in-flight drain -- a render still unwinding after shutdown could relaunch Chrome the instant that drain resolved (probeStage.ts:449-465 has exactly this gap between an abort check and a later acquireBrowser call). No non-shutdown caller reuses the pool after draining it (checked every drainBrowserPool()/drain() call site), but added a separate terminal close() rather than changing drain()'s own semantics, so a future reuse caller stays safe by default. BrowserLeasePool.close() sets a permanent closed flag before draining, and acquire() checks it both before and after its one await point, so a request already mid-await when close() lands still sees it once that await resolves. studioServer's shutdown() now calls the new closeBrowserPool() instead of drainBrowserPool(). Also bounds drain()'s own wait: a close() that hangs past 1s now gets escalated to a force-close instead of blocking the caller indefinitely, keeping total shutdown time under preview.ts's 3s exit watchdog alongside the existing render-wait bound. * fix(engine): trim closeBrowserPool JSDoc to house comment length |
||
|
|
cc19b8c812 |
fix(server): contain static file requests within their served roots (#4217)
* fix(server): contain static file requests within their served roots
Static asset handlers joined the raw request path onto their root
directory without checking where it landed. Hono decodes percent
escapes while routing, so an encoded backslash (%5c) arrives as a real
separator on Windows and dot segments arrive already collapsed —
"/assets/..%5c..%5csecret" and "/%2e%2e%2fsecret" could read files
above the studio bundle root (embedded Studio server) and above
compiledDir/projectDir (engine render file server). Browsers never
request those shapes, but anything that can reach the bound port can:
raw local clients, LAN peers when the preview binds 0.0.0.0, and a
shared-machine process hitting the render server.
Both handlers now resolve the request path first and refuse anything
that collapses outside its root, using the same lexical containment
the preview asset route already applies — bundle/project assets behind
symlinked directories keep being served, only dot-segment escapes are
rejected. Route-level pathSafety tests pin the behavior in both
packages, including the Windows backslash shape and the
symlinked-asset cases from the descriptor-pinning regressions.
Validated: new containment suites fail on the prior handlers (200 +
marker file content served from outside the root) and pass with the
guards; cli server (140), engine services (1024+, matching the main
baseline incl. pre-existing browserManager failures), typecheck,
oxlint, oxfmt and the full workspace build all clean.
* fix: close fallow audit findings, prove containment cross-platform
Fallow audit (health/complexity): createFileServer's request handler
exceeded the CRAP-score threshold once the new containment checks were
added. Extracted resolveContainedPaths, a small pure function that
resolves and validates both roots, cutting the handler's own branching
back down.
Fallow audit (duplication): the new pathSafety test files copied their
temp-root/studioDir setup and teardown from the existing staticAssets
test wholesale. Extracted the shared parts into
studioServerTestFixture.ts (makeStudioServerRoot, writeStudioIndexHtml,
cleanupStudioServerRoot); only the per-file vi.mock("node:path", ...)
call stays duplicated, since Vitest requires it declared statically in
each test file and it cannot be moved behind a shared function -- that
one block keeps a narrow, targeted ignore comment instead of a
file-wide one.
Security claim: isWithinProjectRoot's %5c-decoded-backslash escape is
real, but only on Windows -- POSIX never treats a literal backslash as
a separator, and Hono leaves %2f undecoded in c.req.path, so none of
the new tests' encoded-traversal payloads produce an actual escape on
Linux (verified empirically: they pass identically with the fix
reverted). isWithinProjectRoot now takes an optional path-module
override (defaulting to the host's native node:path, so every existing
caller is unchanged) so its win32 and posix branches can be unit
tested directly from one OS, the same pattern already used by
producer/fileServer.ts's isPathInside. New tests cover a backslash
traversal, a mixed backslash/forward-slash traversal, and a
drive-letter path escaping the root under path.win32, plus the
existing posix case -- confirmed each one fails when the check is
removed. The HTTP-level tests stay as-is: real regression coverage for
them comes from the windows-latest CI job, which runs both new
pathSafety suites unfiltered.
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-authored-by: orphan4183 <kingenmaww@gmail.com>
|
||
|
|
dd97478175 |
fix(media-use): remove clicks and ~25ms gaps at every transcript-cut splice (#4179)
* fix(media-use): remove clicks and ~25ms gaps at every transcript-cut splice transcript-cut extracts each kept range to its own final-codec file, then concat-copies them. That produces two audible artifacts at every cut: 1. No boundary fades. Concat splices raw segment edges together, so the waveform steps discontinuously and each cut clicks. 2. Encoder priming silence. Each segment is encoded to AAC independently, so the encoder pads every one with priming samples. Stream-copy concat bakes that padding in as a silent gap at each splice. Measured 24.4 ms and 24.1 ms at the two splices of a three-range cut. It accumulates: the nth splice lands roughly 25n ms late, so splice positions drift away from the sum of the preceding segment durations. The second survives the first -- adding fades alone leaves the gap. Fix: intermediates carry PCM (.mkv for video, .wav for audio-only) instead of the final codec, so audio is encoded exactly once, at concat. Boundary fades of 30 ms are applied per segment, scaled down on segments shorter than 4x the ramp and skipped on degenerate ones. Measured on the same three-range cut, widest run of digital silence at each splice: before 24.4 ms / 24.1 ms after 0.1 ms / 0.1 ms --copy is unchanged: stream copy cannot filter, so it keeps producing final-codec segments and a copy concat. Audio-only output is byte-identical in duration before and after. scripts/lib test suite: 34/35, the one failure (lut-preset-provider) pre-exists on main and is unrelated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(skills): regenerate manifest for media-use * fix(media-use): honor the fadeIn/fadeOut flags fadeFilterFor was passed The caller already computed {fadeIn, fadeOut} per segment, but fadeFilterFor still took one parameter and always emitted both afade ramps -- so a single kept segment (no splice at all) and every segment's true start/end got faded regardless. Extracted the pure function into lib/transcriptCutFade.mjs, made it honor both flags, and added its first unit test: first/middle/last/single-segment filter strings, no ffmpeg needed, fails against the previous version. Also installs ffmpeg in the Test: skills CI job (same action the Producer job uses), so transcript-cut.test.mjs's real-tone RED/GREEN proof runs in CI instead of skipping silently for lack of the binary. * fix(media-use): fix the packed-CLI import and lint on transcript-cut.mjs build-copy.mjs replaces dist/skills/media-use/scripts/lib with a copy of packages/cli/src/media-use/lib, so transcriptCutFade.mjs needed a byte-identical twin there (and in MEDIA_USE_COPY_NAMES) or the packed CLI's transcript-cut.mjs imported a file that no longer existed. Moved its test alongside, matching every other lib helper's tests. Also drops three redundant Boolean(args.copy) wrappers (a ternary condition already coerces) that oxlint's no-extra-boolean-cast flagged. * style(media-use): reflow the shortened ext ternary to oxfmt's shape Dropping the Boolean() wrapper shortened the line enough that oxfmt 0.41 now collapses the ternary onto one line; format:check was failing on the stale multi-line version. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |
||
|
|
105984cac4 | feat(cli): add IBM Bob runtime support (#3930) | ||
|
|
05d09a7452 |
chore(registry): remove low-use catalog items and variant tails (#4274)
* chore(registry): remove low-use catalog items and variant tails * fix(registry): remove stale references to deleted catalog items * style: format catalog fixture * fix(ci): use surviving catalog preview canary * chore(ci): allow intentional catalog deletions * fix(ci): scope fallow audit for catalog fixtures * chore(catalog): regenerate local search vectors * chore(catalog): update vector revision * fix(catalog): validate registry set delta |
||
|
|
24c3e197ef |
fix(studio): cutover gate classification, SSE cross-project bleed, benign races (#4269)
Implements the fixes from the 2026-09-21 cutover dashboard read
(plans/sdk/2026-09-21-cutover-dashboard-fixes-plan.md):
- sdk_cutover_failed now carries `family` and `error_kind`
(network/conflict/http/sdk). The one production gate event was a raw
`TypeError: Failed to fetch` from an unwrapped fetch (readProjectFile /
writeProjectFile's preflight), which an instanceof-only classifier would
still file as "sdk" and trip the rollback gate — added a message-based
fallback for the browser's fetch-rejection strings.
- studio:sdk_session_unavailable's `network` reason now carries `elapsed_ms`
and `hidden` (document.visibilityState at rejection), separating a
near-instant policy block (CSP/PNA/extension) from a dropped connection
the tab or server going away mid-flight.
- The ownership-race branch in useSdkSession's open effect now emits
sdk_session_superseded instead of sdk_session_unavailable{stage:ownership}
— it's a benign race (identity moved on mid-open), not a broken
precondition, and was being counted as one on the dashboard.
- Root-caused and fixed the read-404 "storm": /api/events (CLI host) and
the vite-dev hot-reload channel are one connection per SERVER, not per
project. A stale tab left open after `hyperframes preview` restarts on a
different folder (same port) received every save the CURRENT project
made, reloading its preview (which then 404s) and re-reading its own
composition on each one. Both transports now carry `projectId`; the
client-side coordinator drops a delivery whose projectId doesn't match
the tab's (absent field still passes, for one release of skew).
- studio-server's file routes report `why` on 403/404: `project_dir_missing`
(new — the CLI host's static resolveProject kept answering for a project
whose folder was renamed/deleted under a running server, which used to
fail closed as an indistinguishable path-traversal 403) vs `nul` vs
`outside_project`.
- absent_or_empty reads now carry `path_in_tree` (against the caller's
loaded file tree). Instrumentation only, per the graveyard page
(clearing-activecomppath-on-tree-absence) that refuted the "clear
activeCompPath" fix this was the proposed next step for.
Tests: unit coverage added for every change (sdkCutover.test.ts,
useSdkSession.lifecycle.test.tsx, useExternalFileChangeCoordinator.test.tsx,
studioServer.test.ts, files.pathSafety.test.ts). oxlint/oxfmt clean,
tsc --noEmit clean on studio/studio-server/cli.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
d5e00c0e66 |
feat(registry): week in merges leaderboard block (#4264)
* feat(registry): add a week-in-merges leaderboard block Authors race across the row at a speed set by their merge count, bouncing at both edges. * chore(registry): cover week-in-merges in the catalog search index * style(registry): format the week-in-merges block Wrap the lines oxfmt rejects so the format check passes. * fix(registry): regenerate the week-in-merges catalog payload The format pass left the committed payload off the generator output. * feat(registry): cap week-in-merges at twelve rows The block races at most rows_max authors, highest merge count first, and writes one line for everyone left out. The default is 12, so a long list cannot shrink the rows. * fix(registry): make week-in-merges speed follow merge count A row now travels the leader's pace times its merges over the leader's count. Only a zero stays on the floor, so a near-tie no longer looks like last place. |
||
|
|
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> |
||
|
|
839ae426dd |
feat(studio): typed audio values, clip fades, audible video drops (#4250)
* feat(studio): typed audio values, clip fades, audible video drops Audio rows in the media inspector (Volume, Speed, Media start) take a typed value: click the readout, type "-6 dB", "1.5x" or "2.5s", Enter. Speed presets are hidden on <audio> clips. Clip-edge fades: data-fade-in / data-fade-out in seconds, honoured by the preview runtime (per-tick gain), the render mixer (afade after volume) and Studio (Fade in / Fade out rows, and drag dots at the top corners of audio clips and audible videos). Linear on both sides so preview and render agree; fades that outrun the clip scale to meet inside it. Dropped videos with an audio stream now land with data-has-audio="true" instead of muted, so the mixer keeps their sound. The media metadata endpoint reports hasAudio from the ffprobe call it already makes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(audio): one owner for fades, dB, and afade-after-volume Illegal fade values become no-fade at fadeGain; typed inspector input still returns null so the readout stays red. Master and group mixes share one filter chain with volume then afade. Fade-out stays anchored to the trimmed clip end. * fix(studio): share audio helpers and bound typed media values * fix(studio): reserve sibling fade duration when editing --------- Co-authored-by: Jake Moran <jakemoran2011@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |
||
|
|
3091dcb787 |
feat(cli): route media search through media-use (#4230)
* feat(cli): move media-use engine into the CLI * fix(cli): replace media-use compatibility asset * fix(cli): replace moved media-use tree * fix(cli): copy moved media-use files cleanly * fix(cli): copy media-use engine files individually * fix(cli): keep audio helper compatibility import * fix(cli): clear copied skill destinations * test(cli): preserve moved media-use test paths * fix(cli): preserve media-use script compatibility imports * fix(cli): preserve media-use telemetry shim * fix(cli): resolve packaged media-use LUT assets * fix(cli): resolve packaged bundled SFX assets * chore(skills): regenerate media-use manifest * fix(cli): preserve media-use parity coverage * fix(cli): cover relocated media-use tests * chore(skills): regenerate media-use manifest * fix(cli): format bundled media provider * fix(ci): run relocated media-use tests with node * fix(ci): preserve fallow script exemptions * fix(ci): scope media-use fallow exemption * refactor(cli): trim media-use essay comments * feat(cli): index media-use assets for catalog search * feat(catalog): commit media-use search vectors * chore(catalog): track media vector metadata * fix(catalog): validate media vector row order * fix(catalog): validate media artifact freshness * fix(catalog): cover media metadata and credits * fix(cli): reject malformed media rows * fix(catalog): compare complete media rows * feat(cli): route media search through media-use * refactor(cli): trim relocated media-use comments * fix(cli): make media search runtime-safe * fix(cli): preserve sfx fallback diagnostics * fix(cli): probe sfx fallback provider explicitly * chore(skills): regenerate manifest * refactor(cli): keep typed media ranker source * fix(cli): align media search checks with local-first flow * fix(cli): scope vector audit exceptions * fix(cli): ship resolver registry beside skill * fix(cli): keep media-use skill scripts standalone * test(cli): cover local vector validation * test(cli): correct media vector fixtures * fix(cli): prefer exact media keys * fix(cli): stem exact media keys * test(cli): skip media lint for snippets without media * fix(cli): align skills test discovery Keep the CI skills job's bash discovery as the single source of truth so its empty-list guard covers the exact files Node executes. Make hasMediaVectors private because it has no external callers and needs no Fallow suppression. * fix(cli): close media-use review gaps Prioritize literal ids before stemmed ids and cover the collision alongside the plural regression. Add command seam tests for engine discovery, actionable failure, and argv passthrough. Add a deterministic parity check for standalone media-use copies, with documented allowlists for the standalone media-fetch shim and self-contained npx resolver. Keep the bash-discovered skills test list as the CI source of truth. * fix(cli): lower parity check complexity Split missing-copy and unallowlisted-drift checks so the parity guard keeps its behavior without a threshold CRAP score. * fix(cli): close media-use review seams Run the parity test in the skills job so skills-only edits exercise the same check. Export the verb mapping and inject command dispatch for direct coverage. Keep missing-copy diagnostics separate from drift reads. |
||
|
|
4e9059ad59 |
feat(cli): add agent timeline workflows (#4241)
* feat(cli): add timeline agent mutation commands * fix(cli): type timeline a2 mutations * style(cli): format timeline a2 commands * test(cli): cover timeline a2 verbs * test(cli): assert ids keep named refs * refactor: centralize timeline duplicate mutation * fix(cli): narrow set assignment field * style(cli): format timeline mutation exports * refactor(cli): split apply plan decisions * fix(cli): tag apply plan refusals * refactor(cli): move A2 runners beside timeline helpers * fix(cli): remove stale A2 imports * style(cli): format A2 command module * fix(cli): avoid A2 command import cycle * refactor(cli): isolate shared timeline mutation logic * fix(cli): restore timeline mutation command factory * fix(cli): remove stale command-surface remnants * fix(cli): complete shared timeline support extraction * fix(cli): close shared conflict helper * fix(cli): import shared timeline formatter * style(cli): format shared mutation runner * refactor(cli): split mutation command orchestration * fix(cli): keep shared mutation primitives in support module * fix(cli): use shared mutation support helpers * refactor(cli): centralize A2 refusals * style(cli): format A2 refusal helpers * fix(cli): clean shared A2 imports * fix(cli): make A2 runner imports explicit * refactor(cli): isolate apply plan target validation * fix(cli): tag prepared plan edits * style(cli): format plan target helper * fix(cli): validate sequential plans and duplicate ids * test(cli): account for stamped composition id * fix(studio): stamp ids on patched html * fix(studio): preserve no-op patch bytes * test(studio): expect stamped patch markup * fix(studio-server): duplicate elements in nested templates * refactor(cli): share timeline mutation receipts * fix(studio-server): harden duplicate and no-op mutations * fix(studio-server): reduce duplicate helper complexity |
||
|
|
952e9228b5 |
fix(cli): bound capture runs and degrade WebGL navigation (#4215)
* fix(cli): bound capture runs and degrade WebGL navigation * fix(cli): keep partial capture on deadline * fix(cli): persist partial capture artifacts * fix(cli): reduce capture phase complexity * chore(cli): tighten capture comments * fix(cli): restore post-capture phase calls * fix(cli): parse extracted scripts before filtering * fix(cli): remove next data through dom filter * refactor(cli): move capture attempt into module * refactor(cli): extract capture phases into modules * refactor(cli): split navigation checks * refactor(cli): deduplicate capture phase modules * fix(cli): preserve bounded capture output on deadline * fix(cli): secure capture artifact writes * fix(cli): close remaining capture file alerts * fix(cli): mark deadline captures unsuccessful * refactor(cli): centralize capture phase schema * style(cli): format capture watchdog test * docs(cli): condense capture attempt header |
||
|
|
f92d017b99 |
feat(cli): add timeline editing verbs (#4221)
* feat(cli): add timeline editing verbs * chore: satisfy fallow complexity gate * test(cli): cover snap rounding at known fps * test(cli): cover snap rounding at known fps * test(cli): cover snap rounding at known fps * test(cli): cover snap rounding at known fps * fix(cli): derive split ids from stable refs * fix(cli): remove timeline gate blockers * fix(cli): close freshness and output gate gaps * fix(cli): close remaining timeline review findings * refactor(cli): simplify mutation decision flow * refactor(cli): split trim and apply paths * refactor(cli): isolate trim validation * fix(cli): satisfy timeline lint gate * style: format timeline mutation files * fix(cli): preserve mutation result types * fix(cli): close timeline receipt and nested bounds gaps * fix(studio): rollback writes that fail during write * fix(cli): bound moved clips by end time |
||
|
|
0c158be165 |
feat(studio): edit a clip's speed ramp with a rate lane and presets (#4153)
* feat(studio): edit a clip's speed ramp with a rate lane and presets * fix(studio): keep the live preview attribute in sync with what gets persisted * fix(cli): snapshot picks the source frame through a rate lane and up to 10x hyperframes snapshot clamped the constant playback rate at 5 and ignored a rate lane in data-automation, so a sped-up or ramped clip showed the wrong frame. It now reads the rate through the runtime's own reader and maps time with the shared source-time function. * docs(skills): teach speed ramps as a rate lane in one reference page Adds the speed-ramps reference, corrects the 0.1 to 5 rate range to 0.1 to 10 and the 'speed ramps are not supported' lines in the skills, and regenerates the skills manifest. * docs(skills): say what fixes a ramped clip's length and how the render approximates pitch * fix(studio): hide the speed preset until the clip has a known duration A preset on a clip with no data-duration was stretched over a made-up one second. The panel now offers presets only when the duration is known. * docs(skills): fix the last stale ramp line in the editing recipes and the slice-cap wording |
||
|
|
0cde884330 |
fix(cli): macOS 12 users get Chrome 150 automatically and can render (#4185)
* fix(cli): pick a macOS 12 capable Chrome on macOS 12 instead of the pinned build * test(engine): stop building subprocess eval scripts from strings |
||
|
|
c610d8491c |
fix(cli): the contact sheet png test no longer waits on a Windows font cache build (#4181)
* fix(cli): keep the contact sheet png test within its budget on slow runners sharp's bundled Pango/Fontconfig backend enumerates the OS font directories on first text render and builds an on-disk cache before it can draw the sheet's labels. On Windows CI that first-render scan is unbounded (measured 12s-103s here for otherwise-millisecond work), which is what the 60s ceiling was really absorbing. Point Fontconfig at an empty, per-run config on win32 so label rendering has nothing to scan, and bring the budget down to reflect the real raster work. * test(cli): keep the createContactSheet timing label to measure the Windows font fix Diagnostic: the Windows job on this branch prints createContactSheet's wall time with the one-font Fontconfig override, to compare against 3.7-18.4s without it. * test(cli): temporary diagnostic, time the first svg and first svg text render * test(cli): temporary diagnostic, first text render in a child process * test(cli): set the Windows Fontconfig file in the vitest config, before workers start * test(cli): drop the contact sheet timing diagnostics * fix(cli): the vitest Fontconfig dir is created with an unpredictable name and removed on exit |
||
|
|
aa05278b78 | feat(cli): render events carry the browser's build and install-path shape (#4180) | ||
|
|
2d8d44151a |
fix(cli): a Studio render refuses with the browser check's own message (#4176)
* fix(cli): a Studio render refuses with the browser check's own message * test(cli): the Studio browser refusal test no longer loads the real producer |
||
|
|
ac0d8ced2a |
fix(cli): timeline lists every clip of a kind in one flat list, nested ones included (#4173)
Text: one absolute-start list per kind, nested clips marked with host and file. JSON: a track's rows hold every clip of that kind; children are index pointers, not copies. |
||
|
|
7597b80c95 |
fix(cli): timeline reports real media durations instead of unauthored (#4169)
* fix(cli): timeline resolves real media durations instead of printing unauthored Media rows call the shared resolveMediaDuration with an ffprobe source length. Each row shows its duration source, or pending with the reason a probe failed. Absolute time (absStart/absEnd) is carried across sub-compositions. Remote, absolute and parent-relative srcs are never probed. * fix(cli): timeline probe leaves the authored-duration rule to the resolver The gate test now launches jobs while slots are held. * fix(cli): timeline reports a zero-length probe as pending instead of a measurement A still image or duration-less container is pending with a reason, never duration=media. The runOneLiner test helper now cleans up its own fixture directory. * fix(cli): timeline probe is injectable and the fixture path works on windows Two unit tests used real ffprobe, which the Ubuntu Test job lacks, and the audio fixture path doubled the drive letter on Windows. describeProject now takes the measure function, the tests pass a recorded fake, and one real-ffprobe test remains. |
||
|
|
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. |
||
|
|
0a5c235173 |
feat(cli): hyperframes timeline prints a project's tracks and clips (#4138)
* feat(cli): hyperframes timeline prints a project's tracks and clips * docs(cli): list the timeline command * feat(cli): timeline command reports unreadable lanes; add skill docs * fix(cli): timeline stays inside the project, shares one start cache, keeps small volumes * fix(cli): timeline resolves symlinks before the project containment check * fix(cli): timeline accepts project folders whose name starts with two dots * refactor(cli): timeline and compositions share the engine's start resolver * docs(skills): point agents at the timeline command where they decide how to read a project * fix(cli): timeline prints absolute main-timeline times and the owning file for nested clips Nested rows carry absStart/absEnd on the main timeline and their owning file, alongside existing local start/end. Text prints absolute time first, local time in parentheses. Adds jq/node one-liners for common queries, tested against a fixture. * fix(cli): timeline places nested media at the runtime start and tests its query one-liners A nested media clip with a negative literal data-start was reported later than it plays, because the raw start was clamped before the host offset was added. The skill's jq and node one-liners now run against the fixture in the test suite. * fix(cli): timeline clamps a negative media start to 0 when its host starts at 0 The runtime skips the host-offset formula for a host at t=0 and clamps the start, so the timeline command now does the same. * refactor(core): one media-start rule shared by the runtime and the timeline command The runtime and the CLI each carried a copy of the rule for when a nested media clip's start takes the host offset, and the copies had drifted. Both now call resolveMediaStartSeconds. * test(cli): timeline fixture for a media start written as an expression parseNumeric and the runtime's parseStrictFiniteTimingNumber are the same function, so the two callers cannot disagree on what counts as a literal; this pins the expression case. |
||
|
|
752b552789 |
feat(registry): blur an element by marking it, not by calling a function (#4045)
attachMotionBlur(el, tl) has an ordering contract: it has to run after every tween is defined and before the composition registers its timeline. Getting it wrong makes the blur silently do nothing, which is the worst shape a failure can take in a renderer nobody watches. An attribute has no order to get wrong. data-hf-motion-blur on any element is now the contract. Empty means defaults; anything else is a JSON object of the same options. The element finds its own timeline: its nearest [data-composition-id] ancestor names the key, and the snippet reads window.__timelines[key]. attachMotionBlur stays exported for a target built after the page has settled. claim(el) is the only thing that attaches, and two triggers call it. An accessor on window.__timelines wraps the registry in a Proxy whose set trap sweeps on every per-key registration, which is what attaches before the renderer captures frame zero. A bounded poll, 32 ms by 250 ticks, reads the registry directly and needs no cooperation from whoever wrote it. The poll is not belt and braces: a registry reference captured before the snippet installs bypasses the trap outright, and a composition that mounts its DOM after registering its timeline has no marked element to sweep at trap time. It runs its whole budget every time rather than stopping the first tick nothing is pending, because on an ordinary page the top-level target is claimed on the registration write, so an early exit kills the poll one tick in, before any late composition arrives. Every refusal is by name, once, and never silent: a value that is not JSON, a value that parses but is not an options object, an option name that is not one of the four, an option value that is not a finite number, a target inside another target, and a second timeline registered under one key. Parsing runs before the timeline lookup, so a malformed value is reported as malformed rather than as a composition that never registered. Two properties of the primitive this change made load-bearing: - Frame rate is resolved from the target's own composition root, not from the first root in the document. The document-wide lookup was survivable while every caller could pass fps explicitly; the declarative form has no per-call argument. Two compositions on one page at different rates now each get their own shutter window, and one call naming both says it cannot. - An attach is all-or-nothing. The mark, the copies and the resize observer go in together and come back out together, so a throw partway through leaves the element untouched rather than marked blurred with copies in the page and no tracker driving them. The sweep is also bounded per element, because the write trap runs it inside the author's own registration statement. Copies are stripped of data-hf-motion-blur, data-composition-id and data-fps, on the clone and every descendant: the first would make a copy a target of its own, and the other two would let a copy win the fps lookup, since copies are inserted before the original. skills/hyperframes-animation/references/motion-blur.md is the agent-facing half, mirroring how colour grading is surfaced. It leads with when NOT to blur, because the attribute makes blurring everything trivial and a smear on motion that was never fast enough reads as a soft, cheap render. Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> Co-authored-by: Vance Ingalls <vance@heygen.com> Co-authored-by: Claude Code <noreply@anthropic.com> |
||
|
|
e7c4eb52b9 |
fix(producer,cli): hash every byte-affecting segment input; pin the resume-flag plumbing
Review findings on the Phase 2b PR. Three inputs that change segment bytes were missing from computeSegmentPlanHash, so --resume could splice mismatched segments: the hardware encoder (h264_nvenc and libx264 both report codec "h264"), the device-scaled capture size (the hash used the composition's CSS size), and the motion-blur options (sample count changes every averaged frame). All three are hashed now, each with a test that fails if it is dropped. The hash and the encoder read the image format from one binding. The plan -> options -> request -> config chain that once dropped --resume and --keep-segments had no test. renderOptionsFromPlan is now a pure, exported hop with a test, renderConfigFromRequest has one for the same flags, and the batch path, which was still dropping both flags, carries them too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
ae6aedf1ca |
fix(cli): pass the segmented resume flags from the plan into render options
execute.ts is where the plan becomes RenderOptions, and it dropped both new flags, so --keep-segments deleted the segment directory anyway and --resume never resumed. Nothing failed: the render just silently re-captured everything. Caught by the Phase 2b gate, which is the only check that spans plan -> options -> request -> config; the gate doc now says so. Gate result recorded: killed at 2 of 6 segments, resume skipped both, finished in 3m32 against 5m13, and the output was byte-identical to the uninterrupted render. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a560f498cd |
feat(render): --resume and --keep-segments for segmented capture
Segments and their manifest live under renders/.hf-segments/<planHash>. A resumed run re-validates every recorded segment before skipping it; a run without --resume deletes the directory first, because a previous run's segments must never be spliced into this output. The directory survives a failure on purpose — that is the resume point — and is removed after a successful render unless --keep-segments. The frame-count check derives from probed duration x fps: the engine has no exact-count reader and -count_frames decodes the whole file, while the only thing this must catch is a segment missing frames, which shortens duration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ddbc9a8a98 |
feat(cli): emit Chrome memory, capture path and segment fields on render events
Both events carry the live observability values, so a render_error that never built a perf summary still reports the last known Chrome memory. render_complete prefers the aggregate from RenderPerfSummary.chromeMemory, which covers every worker rather than whichever session sampled last. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a25da94b77 |
refactor(studio): remove the storyboard view and its Storyboard/Preview switch (#4140)
* refactor(studio): remove the storyboard view and its Storyboard/Preview switch * refactor(studio): fold the thumbnail output branches left after the storyboard mode went * docs(studio): drop leftover storyboard references after the view was removed * ci: allow the storyboard view deletions in the no-deletions guard * docs(studio): wrap an over-long comment line in the caption overlay helper * style(studio): reindent App.tsx after the view-mode wrapper was removed |
||
|
|
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 |
||
|
|
b44f1f85dc |
feat(lint): flag nested structure, missing timing and caption tracks on the root composition (#4146)
* feat(lint): flag nested structure, missing timing and caption tracks on the root composition One rule set for the root composition: nested layout inside a top-level element, a timed element or media without data-duration, captions not marked data-track-kind=captions, and captions spread over more than one track. Findings are errors when the host is Studio and warnings elsewhere. Sub-composition files are left free to hold layout. * test(lint): keep the structure rules silent on the shipped catalog and skills Runs the rules over every registry and skills html file and asserts no findings, and treats a bare media element with no timing as a static layer. * fix(lint): run the structure rules as Studio in both adapters and skip non-root files The Vite Studio adapter now lints as the studio host, and HTML the project lint did not treat as a root (components, snippets) is linted as a sub-composition. Style, script and template content no longer count as nested layout, sub-composition hosts need no duration, and a caption row without a lane is not a second track. * fix(lint): count legacy data-end as a length and pin the catalog test to real roots |
||
|
|
93921dccc8 | fix(cli): stop the workspace build check test from depending on wall-clock mtimes (#4098) | ||
|
|
9981d9afb7 |
fix(capture): never silently render a webgpu composition blank (#4091)
* fix(capture): never silently render a webgpu composition blank A composition declaring data-requires-webgpu only got the Chrome WebGPU flag when hardware GPU auto-detection succeeded. On a software-GPU host it launched without the flag and rendered blank with no error. compositionRequiresWebGpu (static declaration check) and assertWebGpuAdapterAvailable (live per-page adapter probe) now live once in packages/engine's browserManager, the shared choke point every launch site already goes through. All six launch sites (frame capture, the four CLI diagnostic commands, the studio thumbnail server, and the render pipeline's capture-options builder) route through those two functions; the old CLI-only static-heuristic guard is deleted, not kept alongside the new one. A software host that cannot obtain any WebGPU adapter now fails loudly naming the requirement instead of producing a blank frame. * fix(capture): thread requiresWebGpu through the distributed chunk-render path The distributed renderChunk() path built its CaptureOptions without requiresWebGpu, so it fell through to frameCapture's own fetch-based detection, whose failure path silently treats "requires webgpu" as "doesn't" if that fetch ever fails. Every other launch site (frame capture, all four CLI diagnostic commands, the studio thumbnail server, and the in-process render pipeline) already reads the value from HTML already in memory or on disk instead of relying on that fallback. Reads the compiled composition's index.html the same way probeStage already does and derives requiresWebGpu from it before building CaptureOptions. Adds a regression test that pins the exact path renderChunk() reads at render time, since the composition doesn't actually exercise WebGPU so an end-to-end render can't distinguish the two cases on this host. * test(producer): observe the real CaptureOptions renderChunk() builds The previous regression test called plan() and compositionRequiresWebGpu() directly, bypassing renderChunk() entirely, so it could not detect a regression to the wiring it was meant to protect (confirmed: it stayed green with the fix reverted). Exports the existing createCaptureSession dependency seam so a test can substitute a spy that delegates to the real implementation while capturing the CaptureOptions object renderChunk() actually constructs. Verified non-vacuous by reverting the fix and confirming the test fails. * fix(gcp-cloud-run): resolve puppeteer-core from a real file in the webgpu probe The image build's WebGPU adapter probe imported puppeteer-core from an inline `bun -e` eval string. The eval's synthetic /app/[eval] location can't resolve the workspace-hoisted dependency, so every build of this stage failed closed with "Cannot find package 'puppeteer-core'" before the probe itself ever ran. Moves the probe into a real script file next to probe-beginframe.ts, which already imports puppeteer-core dynamically the same way and resolves fine from its real workspace location. Verified locally against a cached chrome-headless-shell binary. * fix(engine): bound the webgpu adapter probe so a stuck driver can't hang the caller requestAdapter() has no native timeout. A broken software GPU driver that never settles it would hang assertWebGpuAdapterAvailable indefinitely, including inside the GCP image build's probe RUN step, producing an opaque build timeout instead of a clean probe failure. Races the in-page request against a bounded timeout so a stuck adapter reads as unavailable and fails the same way a genuinely absent one does. * test(cli): mock the engine's webgpu exports in the browser-fallback test * test(producer): run the typegpu adapter fixture only on hosts with a hardware WebGPU adapter The fixture declares data-requires-webgpu, so a software-only host now refuses it instead of silently rendering the shader blank. The old golden had no shader in it. Tag it webgpu, skip that tag in the CI image and the test scripts, and re-record the golden from a Metal render with the shader visible. * fix(engine): bound the composition fetch that decides the webgpu flag createCaptureSession fetched the served page with no timeout before launching the browser, so a hung server stalled a render forever and hung a fake-timer test. Give it a five second limit, and let that test say it has no WebGPU requirement the way the producer does. * fix(capture): refuse a software fallback WebGPU adapter instead of rendering on it swiftshader reports a WebGPU adapter but cannot render canvas WebGPU, so a data-requires-webgpu composition on a software-only host crashed on its first frame. Refuse fallback adapters with the same clear WebGPU-unavailable error. The GCP image probe now serves its page over http (file:// hides navigator.gpu) and reports a software-only host instead of failing the image build. * fix(engine): parse the composition root tag without a backtracking regex * fix(engine): keep quoted attribute values whole when reading the composition root tag |
||
|
|
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. |
||
|
|
a7cc1d2cff |
fix(cli): stop the preflight cancel test reading a pid file mid-write (#4077)
The test polled a pid file written by `echo $$ > path`, whose shell redirection creates (truncates) the file before the echo writes to it. A read landing in that window parsed an empty string to 0, which satisfied "not undefined" and exited the wait loop early, failing the next assertion with "expected 0 to be greater than 0" — the exact shape of the intermittent CI failure. The loop now only accepts a parsed value that's a positive integer, reproduced with a busy-wait harness that hit the empty-file race in most iterations 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. |
||
|
|
3867d8da5d |
test(cli): time each native Sharp call in the flaky Windows contact-sheet test (#4070)
This test has hit its 60s ceiling on Windows CI runners before, and the log only ever shows the bare timeout, not which of the four native Sharp calls actually stalled. Wrapping each one in console.time/console.timeEnd costs nothing when the test is fast and names the slow step the next time it isn't. No behavior change. |
||
|
|
2a559714ee | fix(cli): distinguish agent and execution harness telemetry (#4071) | ||
|
|
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 |
||
|
|
82f49262ec |
feat(registry): blur every transform driver, and ship the reference case as shutter-slam (#4038)
The motion-blur snippet integrated translation only. Its output stage was one SVG filter per target whose copies were feOffset, and feOffset only translates, so a beat that only scaled or only turned sampled a displacement of zero and rendered sharp. No SVG filter primitive can give a copy its own affine transform, so the sampling loop was never the problem: the output stage was. Copies are now DOM clones of the element in an isolation:isolate group, each at 1/N opacity with mix-blend-mode plus-lighter, each carrying the element's resolved transform at its sample time. plus-lighter adds premultiplied colour, so N + 1 copies at 1/N sum to the average the shutter integral asks for; the isolation is load-bearing rather than tidy, because without a transparent backdrop of its own the first copy adds onto the page and blows a light background out to white. Reading the resolved matrix instead of a list of named GSAP properties is what makes every driver work at once: translation, scale, rotation, 3D rotation and skew arrive in the same one value. Measured against the After Effects export the shutter model was fitted to, per beat, previous stage to this one: scale +0.62 dB, rotate X +0.43, rotate Y +0.84, rotate Z +1.15, translate X -0.18, translate Y -0.04. The two small translate regressions are inherent and not tunable: the filter offset one rasterisation so every copy was pixel-identical, where each copy is now rasterised independently at its own fractional transform with its own subpixel antialiasing. Three consequences of the new stage, each carrying its own fix: - mix-blend-mode flattens preserve-3d, so a copy cannot inherit the parent's 3D context. Each copy carries the parent's perspective as its own first transform function instead. - A copy is styled by nothing that selected the original, since dropping the id also drops every #id rule that gave it size, colour and font. Each copy carries its own resolved style inline, read once and replayed. Those values are px, so a container-relative element needs them again when its box changes: a ResizeObserver re-snapshots rather than paying a subtree walk every frame. - A beat that moved and faded at once kept a full-strength smear behind a vanishing element. The element's opacity now rides the group; the 1/N weight stays on the copies. The axis option is gone. It selected which of x and y to sample, a question the resolved matrix does not have. shutter-slam is the new component: one word through six single-property beats, two translate slams, a scale punch, then a full turn about each axis, which exercises every driver the model covers in one mount. Easing is power2.inOut because it measured best, not because it was the default. A bezier fitted to the reference's own bbox centres looks like the better curve and renders worse: a blurred bbox centre is the shutter window's average rather than the instant, so on an accelerating segment the fit measures its own blur. Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
71bb88719e |
feat(telemetry): render output-shape, adapter, count, and environment properties (#4017)
* feat(telemetry): add output-shape request facts to render events output_resolution_preset, output_format, hdr_mode, video_frame_format, and gif_fps_capped join render_complete/render_error, all threaded from already-resolved RenderOptions/RenderPlan fields rather than recomputed: createRenderPlan already computes gifFpsCapped (clamping a requested --fps above 30 to 30 for --format gif) but only used it for a console warning; this threads that same decision into RenderOptions and telemetry. All five are known before the pipeline starts, so both events carry them even when a render fails before a perfSummary exists. Also wires output_format/output_resolution_preset into Studio's separate emitStudioRenderComplete/emitStudioRenderError path, since Studio's request genuinely varies those two per-request. hdr_mode, video_frame_format, and gif_fps_capped stay CLI-only: Studio exposes no HDR-mode, video-frame-format, or GIF output control to users at all, so there is no per-request value to report for those three. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * feat(telemetry): add adapters_used to render events adapters_used lists the runtime deterministic adapters (packages/core/src/runtime/adapters/) a composition exercises. Investigated the actual codebase rather than the ticket's guessed 7-adapter list and found 12 real registered adapters (gsap, css, waapi, animejs, lottie, three, typegpu, d3, leaflet, mapbox, maplibre, google-maps) — the map/data-source adapters share the exact same window.__hf<Name> registration mechanism and are equally real "adapters used" facts, so all 12 are included. Detection unions a live page.evaluate probe (piggybacked on the same probeSession resolveCompositionElementCount already uses) with a static regex fallback over the compiled HTML, since this is purely observational with no gating role (unlike compositionElementCount) — union strictly increases recall rather than needing one source of truth. Threaded through the same plumbing chain as the already-merged composition_element_tags/aroll_video_count/heygen_video_count. Studio's emitStudioRenderComplete/emitStudioRenderError get this field automatically, with no Studio-side code change, via the same shared capture-observability spread heygen_video_count already uses. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * feat(telemetry): add audio/image/sub-composition/color-grading counts audio_count, image_count, sub_composition_count, audio_group_count, color_grading_count, and has_lut join composition_element_tags in the same single scanElementTags pass — audio/image/audio-group counts read from the scan's uncapped tag map so a 50+-distinct-tag composition can't push a named count into the "other" bucket. sub_composition_count counts data-composition-src mounts directly rather than reusing collectSubCompositionSrcs, which dedupes by src and drops placeholder/remote mounts (a different, smaller fact than the mount count this field reports). has_lut decodes the HTML-entity-escaped form of data-color-grading before parsing, since linkedom's serializer re-emits it "-escaped on every compile-pipeline round-trip — without the decode this silently reported false for every LUT-graded composition. The LUT-presence check now mirrors normalizeLut's own definition of "has a LUT" exactly, closing a gap where a blank or empty lut value would have reported true. Threaded through the same plumbing chain as the already-merged per-tag/provenance/adapter fields; Studio's emitter picks these up automatically via the shared capture-observability spread. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * feat(telemetry): add browser_version_major and ffmpeg_version_major Both are already-run subprocess calls whose output was discarded: readToolVersion (checkFFmpeg's preflight) already runs `ffmpeg -version`, and chromeLaunchOutcome already runs `--version` as a Chrome launch sanity probe on every render. A shared extractMajorVersion helper pulls the first X.Y-shaped number from either banner, threaded onto EnvironmentCheckOutcome/ EnvironmentCheckResult, then RenderOptions right after runEnvironmentChecks resolves in the local render path only. Docker renders never populate these two RenderOptions fields (the containerized producer runs its own preflight the host CLI never sees), so both properties come back absent there rather than reporting the host's own toolchain versions for a render that didn't use them. Studio does not call runEnvironmentChecks or this ffmpeg/Chrome preflight at all before creating a render job, so its render_complete/ render_error events will not carry these two properties unless Studio's own startup path is separately instrumented — a genuine parity gap, not an oversight in this change. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * feat(telemetry): add authoring_skill_source and authoring_skill_invalid Both derive from resolution logic createRenderPlan already runs: authoring_skill_source names whichever step actually won attribution (an explicit --skill flag, or the skill persisted in hyperframes.json) using the same flagSkill/projectConfigSkill values already computed for authoringSkill itself, not a second lookup. authoring_skill_invalid surfaces the raw --skill value when it fails slug-shape validation (normalizeSkillSlug rejects malformed input, not unregistered skill names) — this value already exists as RenderPlan.invalidAuthoringSkill and already drives a CLI warning in present.ts; this change threads the same field into telemetry rather than adding a parallel one. Threaded through both RenderOptions builders in execute.ts (single and batch render) and all three telemetry call sites in render.ts, including the Docker path — skill attribution is a request-level fact independent of which backend executes the render. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * feat(telemetry): add root_body_mismatch and root_body_delta_px_bucket Measures the composition root's authored data-width/data-height against the scaffold's own html/body CSS size, straight from the compiled document string scanElementTags already scans — the source-level fact, not runtime DOM state. init.ts's applyCompositionSizing (the #4001 fix) only patches the live DOM after the browser loads this same HTML, so this measurement stays meaningful whether or not that fix ran for a given render. Reuses @hyperframes/parsers' canvasScaffoldPatterns regexes (the same ones packages/lint's root_dimensions_mismatch rule reads) for the CSS side. Root-tag detection here is a lighter first-data-composition-id-match heuristic, not lint's more thorough findRootTag (which also handles a leading decorative <svg> defs block) — that helper isn't part of @hyperframes/lint's public API, and expanding it is out of scope here. The delta is bucketed (0 / 1-10 / 11-50 / 51+ px) rather than reported raw, and both fields come back absent — not a false/"0" default — when the root or the CSS block can't be read at all. Threaded through the same plumbing as every other static-scan field this week, including the live-capture-observability fallback used on a crash that never reaches perfSummary. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * feat(telemetry): add hf_env_overrides (names only, capped at 20) Reports the sorted names of HF_*/HYPERFRAMES_* env vars present when createRenderPlan resolves — never their values, since several of these carry filesystem paths. Snapshotting at plan-resolution time (the very first line of the render command, before any pipeline code runs) matters: this render's own preflight later injects HYPERFRAMES_FFMPEG_PATH/HYPERFRAMES_FFPROBE_PATH into its own process.env, which would otherwise make those two always read back as "operator overrides" whether or not the operator ever set them. Reports an empty array, never an absent key, when nothing is set, matching every other capped/bucketed field added this week. Capped at 20 names, documented alongside the cap constant. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(cli): stop clobbering studio render_complete fields, fix an env leak trackRenderComplete's explicit audio_count/image_count/root_body_mismatch/ adapters_used/etc. keys, sourced from perfSummary.drawElement and only ever populated on the CLI's own render.ts path, always won the object spread over the observability-capture fallback those same keys share with trackRenderError, even when the direct value was undefined. studioRenderTelemetry.ts's emitStudioRenderComplete never populates drawElement, only the capture fallback, so 24 fields silently came back missing from every studio-triggered render_complete event despite Studio having computed and sent the capture value. Each field now falls back to its captureXxx counterpart via a new directOrCapture() helper when the direct field is absent, with regression tests proving both the fallback and that the direct value still wins when both are present. Also excludes HF_SHADER_WORKER_ENTRY from hf_env_overrides: cli.ts sets this on every invocation to point the bundled shader worker pool at its own file, sharing the HF_ prefix by coincidence rather than because an operator set it. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * style: remove em and en dashes from added code comments No behavior change. Rewrites 22 comment lines this PR's own diff introduced, using a comma or a period where the dash separated two clauses. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * test(producer): add a temporary diagnostic for a CI-only rootBodyMismatch failure Not a real fix. The rootBodyMismatch test group fails deterministically in CI (confirmed on two runs of the same head) but will not reproduce locally by any method tried, including an isolated file run, the exact combined producer:test:unit command, a worker-count-constrained run, and a fresh clone with a clean install and build matching CI's own steps. This dumps the intermediate regex-match state for the simplest failing fixture to stderr so the next CI run's log says what actually differs. To be removed once that answer lands. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * test(producer): widen the rootBodyMismatch diagnostic to compare import paths Still not a real fix. The previous diagnostic ( |
||
|
|
e4b1832fad | feat(cli): add hls output format (#4022) | ||
|
|
9aa60a844f |
fix(registry): make motion-blur match After Effects shutter-based blur (#4008)
* fix(registry): make motion-blur match After Effects shutter-based blur The motion-blur catalog snippet estimated speed from the x/y delta between consecutive timeline updates and drew three one-sided ghost copies with decaying alpha plus a Gaussian on top. After Effects does temporal supersampling: each frame integrates the layer over a shutter window (shutter angle, shutter phase, samples per frame), producing a symmetric box smear of length speed x shutter time. The snippet now does the same inside the page. After every timeline update a deferred pass seeks the GSAP timeline to N sub-frame times across the shutter window (events suppressed), reads each target's x/y, restores the frame time, and drives an SVG filter that averages N feOffset copies with equal weight (feComposite arithmetic, 1/N, sRGB). Defaults follow AE: shutterAngle 180, shutterPhase -90, samplesPerFrame 16; fps comes from the render config, then the root data-fps, then 30. Displacements are mapped through the inverse of the element's own scale/rotation; percent x/y values are resolved to px; SVG targets use their bbox; an existing inline filter is kept and the blur appended to it. The pass runs in a microtask rather than inside the tracker tween's onUpdate, because seeking GSAP from within its own render reads stale tween state and suppresses the composition's callbacks on forward seeks. The heuristic options blurScale/blurMax/stretchScale/stretchMax are removed. Reference: an AE-algorithm reconstruction of the same composition rendered without the component at an effective 960 fps and averaged over the 16 sub-frames of the 180 degree window per output frame. Against it (1920x1080, 30 fps, solid + text translating linear then power2.inOut): old mean PSNR 53.2 dB (min 26.7), SSIM 0.9930; new mean PSNR 72.4 dB (min 52.6), SSIM 0.9998. The 10-90 edge ramp at 66.7 px/frame is 26.8 px in the reference, 27.5 px new, 14.5/6.3 px (one-sided) old; at the ease peak 79.3 vs 79.5 vs 7.9/17.7 px, with smear centres matching to 0.1 px. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(registry): drop render-config fps probe from motion-blur and refresh its docs The snippet resolved fps by reconstructing the renderer's private seek config key so the compiler's inline-script guard would not strip it. A public catalog snippet must not depend on that key, so fps is now the explicit option, then the root data-fps, then 30; renders with an fps override pass fps explicitly. The catalog page's Usage, How it works and Options sections still described the old velocity heuristic and are rewritten for the shutter model. The 1/N weight keeps full precision. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(registry): match the After Effects reference export's shutter window The shutter model landed on this branch was built against a reconstruction of After Effects' algorithm, not against After Effects. A real 1920x1080 / 30 fps export of translating text disagrees with it on three points, all measurable off the export itself. The window is two frames wide, not half a frame. On the measured frame the outermost duplicate on the trailing side sits 128 px back while the previous frame is 125.8 px back, and the outermost leading duplicate sits 189 px ahead while the next frame is 184.9 px ahead: the window ends land on the neighbouring frames. Defaults become shutterAngle 720, shutterPhase -360. Duplicates sit on the sub-interval boundaries, both ends included, not at sub-interval centres. The staircase across a stroke steps every 15.6 px on a 125.8 px displacement and every 23.0 px on a 184.9 px one, i.e. an eighth of a frame, with the run closing exactly on the neighbouring frame's position. samplesPerFrame now means sub-intervals of the window, so there are N+1 copies at k/N rather than N copies at (k+0.5)/N. The frame-time instance is drawn sharp over the smear. At full ink the core bounding box is the whole word at every frame and total ink rises to 1.44x the unblurred frame's, neither of which an average can produce. A final feComposite over of SourceGraphic reproduces it. Each duplicate stays at 1/N: the staircase plateaus step by 0.063 +- 0.002 of the sharp text intensity, flat, with no taper toward the window edges, and triangle weighting scores 1.6 dB worse against the export. Verified by replaying the export's translating section as a composition whose sprite is the export's own rest-frame ink and whose eight legs follow its own measured per-frame positions, rendered at 1920x1080 / 30 fps and compared frame by frame over the text band. Mean PSNR against the export rises from 24.19 dB (min 17.07) to 32.51 dB (min 27.57) across 72 frames, and from 19.83 dB (min 17.07) to 32.03 dB (min 27.57) across the 26 frames that actually move. Ghost extents match to a few px and the rendered staircase plateaus match the export's to two decimal places. The export's later scale, skew, flip and 3D rotation sections are out of reach here: integrating anything but translation inside the shutter is engine work tracked in issue #4010. The filter region now clamps its bounds through zero. It was derived from the duplicate offsets alone, which was correct while the output was nothing but duplicates; the sharp instance draws at offset zero, so a window sitting entirely on one side of the frame time - any shutterPhase above 0 or below -shutterAngle - produced a region starting inside the element's own box and cut the instance off. The defaults straddle the frame time and were never affected, so the measured numbers above are unchanged. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
2229d0d6a6 |
fix(lint): root_dimensions_mismatch sub-composition false positive + regex SSOT consolidation (#4012)
* fix(lint): guard root_dimensions_mismatch against sub-compositions Sub-compositions mounted via data-composition-src are fetched with DOMParser and only their matched <template>/<body> subtree is imported into the live document. A sub-composition authored as a full standalone HTML document still carries its own <html>/<head>/<meta viewport>, but none of that ever reaches the rendering document, so a mismatch there was always a false positive. Guard the rule with options.isSubComposition, matching the sibling missing_timeline_registry rule's existing pattern. Split the warning wording: html/body CSS actually sizes the element that clips the root, but the CDP viewport is set from the root's own data-width/data-height (frameCapture.ts), never from <meta viewport>. A viewport-only mismatch is real drift but not a clipping risk. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(cli,parsers): consolidate scaffold resolution regexes into one definition applyResolutionPreset kept its own prefix-capturing regex literals for the html/body CSS size and meta viewport content, duplicating the same three patterns already defined in @hyperframes/parsers for lint's read-only root_dimensions_mismatch comparison. Merge to one definition per pattern that captures both the prefix (for in-place replace) and the digits (for comparison), so the two callers can no longer drift on where a scaffold's resolution lives. Existing applyResolutionPreset tests stay green. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
3704863d96 |
feat(telemetry): per-tag, provenance, and failure-taxonomy properties on render events (#4004)
* feat(telemetry): add per-tag element and a-roll counts to render_complete
countElementTags's single regex scan (the ~83% of renders that get no
live probe session) is extended into scanElementTags, which returns a
per-tag breakdown and an a-roll video count alongside the same total,
all from one pass over the same script/style-stripped markup — the
total is the sum of the per-tag map by construction, so the two
properties can't drift apart. countElementTags itself is removed; it
had zero production callers after this change.
New render_complete properties: composition_element_tags ({tag: count},
raw lowercase tag names, capped at the top 50 by count with the rest
folded into an "other" key) and aroll_video_count (<video
data-aroll="true"> elements from the same scan). Both are static-scan-
only, mirroring the existing composition_element_count_source split —
the live probe-DOM path never runs this scan.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* feat(telemetry): count HeyGen-provenance video from the same tag scan
heygenVideoCount joins arollVideoCount/byTag as a third property
derived from scanElementTags's single pass: <video
data-media-source="heygen"> elements. media-use's resolve.md now
documents that the skill stamps this attribute on a mounted video only
when the ledger record's provider is "heygen.video" (the mount itself
is always the agent's own composition edit, never written by resolve
scripts) — the ledger stays the single source of truth for provenance,
the HTML attribute is a projection of it, and no other provider value
is invented beyond this one signal.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* feat(telemetry): add a bucketable failure taxonomy to render_error
error_name (error.name, e.g. one of the ~20 typed producer error
classes) and failed_stage_code (job.currentStage/failedStage
normalized to a stable snake_case code via normalizeStageCode)
join the existing free-text error_message/failed_stage, so a fleet
query can bucket failures without parsing prose. Error-conditional by
nature — render_complete has no equivalent, since a successful render
has no error to name or stage to blame.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
|