Commit Graph
100 Commits
Author SHA1 Message Date
Miguel Ángel feb675a89d chore: release v0.7.63 (#2628) 2026-07-18 04:44:56 -04:00
Miguel Ángel 49113eb084 fix(media-use): clean failed asset reservations (#2627)
* fix(media-use): clean failed asset reservations

* test(media-use): pin installer guidance exactly
2026-07-18 04:36:36 -04:00
Miguel Ángel 7f76170956 chore: release v0.7.62 (#2626) 2026-07-17 22:02:49 -04:00
Miguel Ángel e64a22893a fix(studio): preserve alpha proxy playback (#2625)
* fix(studio): preserve alpha proxy playback

* test(cli): pin VP8 alpha proxy pre-resolution

* fix(studio): emit textarea field-sizing CSS

* test(studio): verify textarea CSS generation
2026-07-17 21:56:27 -04:00
Miguel Ángel 2577aaffeb fix(studio): ignore stale failed sidecars for existing renders (#2621)
* fix(studio): ignore stale failed sidecars for existing renders

* style: format stale render metadata test
2026-07-17 16:21:33 -04:00
Miguel Ángel 209784ab27 fix(producer): credit held video tails in coverage gate (#2606)
* fix(producer): credit held video tails in coverage gate

* test(producer): decouple makeExtracted's durationSeconds default from delivered frame count

Defaulting durationSeconds to delivered/fps made any test modeling a
delivery shortfall silently report full coverage unless it remembered
to override durationSeconds afterward. Default to Infinity instead so
callers fall into the 'no usable source duration' branch (full slot
required) unless they explicitly pass a duration.
2026-07-17 14:16:17 -04:00
Miguel Ángel 2b65b4efce fix(studio): harden composition timeline reliability (#2615)
* fix(studio): preserve composition playback continuity

* feat(studio): drag compositions into the timeline

* fix(studio): collapse expanded composition move aliases

* fix(studio): make timeline cuts atomic

* fix(studio): group inspector gesture history

* test(studio): cover masked text selection

* fix(studio): harden composition timeline reliability

* fix(studio): satisfy CI source gates

* fix(studio): harden composition mutation requests
2026-07-17 14:15:30 -04:00
Miguel Ángel 2be8a62c00 fix(engine): stop compositing phantom duplicates on captureBeyondViewport (#2607)
* fix(core): stop the async media-metadata rebind once render capture starts seeking

scheduleMetadataDurationHydration re-resolves and can swap the captured
GSAP timeline off a debounced loadedmetadata/durationchange event, fully
uncoordinated with the producer's own per-frame renderSeek calls. When a
full-length <video>'s metadata resolves after capture has already begun
(slow I/O, Docker), this races the deterministic BeginFrame capture loop
and can reflow sub-composition state mid-render, producing phase-offset
duplicate content in captured frames (#2550).

Render-mode duration correction already happens deterministically during
the probe stage before capture starts, so once renderSeek has been called
once there is nothing left for this self-correction to do — gate it off
for the rest of the session.

* fix(core): scope the metadata-rebind guard to actual render/export pages

renderSeek isn't capture-exclusive — Studio's own preview iframe falls
back to it for compositions whose timeline overhangs every native
adapter's duration. Gating the HF#2550 fix on renderCaptureSeekStarted
alone silently disabled the metadata-driven duration self-correction for
that live-scrub case too, where it's still needed. Require the render/
export page signal (window.__HF_EXPORT_RENDER_SEEK_CONFIG, set only by
the producer's fileServer.ts) alongside it, and add a regression test
covering the Studio-preview case.

* fix(engine): stop requesting beyond-viewport capture for video comps that don't need it

Root-caused HF#2550 by reproducing the reporter's public repro end-to-end
(not just the timeline-rebind mechanism from the earlier commits in this
branch) on native Linux: instrumented the actual DOM state during a real
capture session and confirmed the sub-composition never double-mounts —
getBoundingClientRect and the timeline's own local time both match the
single, correct DOM tree throughout. The phantom second copy only exists
in the captured screenshot pixels.

Bisected it to captureBeyondViewport: resolveVideoCaptureBeyondViewport
(#1094's tall-portrait fix) forces `Page.captureScreenshot`'s beyond-viewport
path on for any render with a native <video>, regardless of whether the
page's content actually overflows the declared capture height. On
SwiftShader that beyond-viewport path can composite a stale, vertically
offset paint of the page alongside the fresh one for content that fits
entirely within the viewport — producing exactly the reported phase-offset
duplicate. Disabling captureBeyondViewport (repro's video still present)
eliminates the duplicate outright; re-enabling it reproduces the duplicate
byte-for-byte, isolating it as the actual cause.

Adds pageContentExceedsCaptureHeight, a ground-truth measurement of the
page's actual scrollHeight against the requested capture height, and wires
it into initializeSession to downgrade captureBeyondViewport back to false
once the page is settled and it's confirmed unnecessary — the "reliable
clip predictor" the original #1094 fix's ponytail comment flagged as
missing. This keeps #1094's fix intact for content that genuinely
overflows while closing the SwiftShader ghosting hazard for the (common)
case of video that fits inside its own viewport.

* test(producer): add HF#2550 video+sub-composition regression fixture

Checks in the reporter's confirmed real-world reproduction (media
regenerated via ffmpeg testsrc2, matching their public repro repo) as a
regression fixture, with a golden baseline rendered against the fix.

Verified end-to-end via the project's own Docker regression harness:
- Rendering this fixture with the fix produces the golden baseline
  (clean, single flowchart instance, captureBeyondViewport correctly
  downgraded).
- Direct CLI renders (not through this harness) against unpatched code
  reproduce the reported phantom-duplicate artifact reliably (10/10).

Caveat documented in meta.json: the underlying bug is timing-dependent.
Two harness runs against unpatched code, using this same fixture, did
not reproduce the artifact (0/2) — the harness's in-process render path
apparently doesn't hit the same race window a direct CLI process does on
this host. This fixture is a best-effort regression guard and a
preserved real-world repro, not the sole protection — the deterministic
guard is packages/engine/src/services/screenshotService.test.ts's
pageContentExceedsCaptureHeight unit tests, which exercise the actual
fix logic directly.

Also adds an .gitattributes LFS rule for this fixture's source
index.html (744 KB — carries the real project's embedded base64
assets, over the largefiles hook's 500 KB non-LFS limit).

* fix: route HF#2550 fixture binaries through LFS (were committed raw)

filter.lfs.clean/smudge were locally configured as a no-op "cat" in
this repo's shared .git/config, silently disabling LFS filtering for
every worktree. The previous commit's large binaries (output.mp4,
compiled.html, source index.html, source video) landed as raw blobs
instead of LFS pointers as a result. Ran `git lfs install --local
--force` to restore the correct filter commands, then re-staged the
affected files so they commit as proper LFS pointers.

* fix(engine): address capture viewport review feedback
2026-07-17 11:59:45 -04:00
Miguel Ángel 0a66671fc5 feat(media-use): add video generation (HeyGen avatar-video + local LTX fallback) (#2614)
* fix(media-use): tag HeyGen TTS generation with attribution header

Centralizes the X-HeyGen-Client-Source header into HEYGEN_CLIENT_SOURCE_ARGV
in heygen-cli.mjs and reuses it in heygen-search.mjs (dropping the duplicated
inline literal) so voice-provider's `voice speech create` call carries it too.
The generation call was previously untagged, making media-use TTS usage
invisible in HeyGen's billing/analytics warehouse; the read-only `voice list`
discovery call intentionally stays untagged.

* feat(media-use): add local LTX video generate provider

* feat(media-use): add HeyGen avatar-video generate provider

* feat(media-use): register video as a real provider type

* docs(media-use): document the wired video type and full HeyGen tagging coverage

resolve --type video is now the default path (HeyGen avatar video first,
local LTX fallback, sign-in nudge on auth failure) instead of a manual
recipe; correct the claim that only search requests are tagged now that
TTS and avatar-video generation carry the attribution header too.

* fix(media-use): wire --avatar-id/--voice-id CLI flags and close video-provider auth/cache gaps

- resolve.mjs never implemented the --avatar-id/--voice-id override that
  operations.md documented, so following the docs crashed with
  ERR_PARSE_ARGS_UNKNOWN_OPTION; wire the flags through to ctx.
- defaultAvatarId/defaultStarfishVoiceId cached a failed discovery lookup
  as a permanent null, disabling heygen.video after one transient miss;
  cache only a truthy id, matching the same fix in voice-provider.mjs's
  defaultVoiceId.
- the avatar-video onboarding nudge only fired on a video-create failure,
  never when avatar/voice discovery itself was unauthenticated (the
  common unauthenticated case) -- propagate the discovery failure reason
  so onboarding fires either way.
- dedupe the CLI-shelling JSON helper (heygen-cli.mjs's new runHeygenJson)
  and the local-model argv-template builder (local-models.mjs's new
  buildArgv) instead of leaving byte-identical copies in each provider.

* fix(media-use): address avatar-video PR review feedback

- heygenVideoGenerate short-circuits after the first discovery-call
  failure instead of always attempting both avatar list and voice list,
  so an unauthenticated caller gets one onboarding message and one
  provider-error telemetry ping instead of a double-fire.
- runHeygenJson logs a diagnostic when a CLI call succeeds but returns
  unparseable JSON, instead of silently returning null.
- dedupe the "avatar video is free" onboarding string into one constant
  (was duplicated across three call sites).

* fix(media-use): match review-requested naming and message conventions

- export AVATAR_VIDEO_SIGNIN_MESSAGE from heygen-video-provider.mjs so
  the test imports the canonical string instead of redeclaring it.
- runHeygenJson's non-JSON diagnostic now matches heygen-search.mjs's
  existing wording ("returned non-JSON output").
2026-07-17 03:56:57 -04:00
Miguel Ángel e8371a7acc feat(media): alpha-capable authoring proxies (#2598)
* feat(media): alpha-capable authoring proxies

Alpha sources were refused a proxy before the codec map ever asked whether the
browser could decode them, so a ProRes 4444 alpha file (which no browser
previews at all) rendered black forever, while an alpha WebM (which previews
fine) was already covered by the browser-safe check on the next line. The alpha
veto earned nothing and cost the one case that needed help.

Alpha is now a target-codec choice rather than a veto: alpha sources transcode
to VP9 + yuva420p in WebM, everything else keeps the existing H.264/MP4 path
byte for byte. Only files no browser can preview are proxied, which is the rule
the runtime already followed everywhere else.

WebM cannot carry AAC, so the VP9 path uses Opus and drops the MP4-only
faststart flag. PROXY_PARAMS_VERSION moves to v3 so clients stop serving the
previously cached proxies.

Safari does not decode VP9 alpha and still shows black for alpha sources, as it
does today: this is better on Chromium and Firefox and no worse anywhere.

* fix(media): infer proxy variant for rescue

* fix(media): preserve alpha proxy hardening after restack
2026-07-17 03:26:55 -04:00
Miguel Ángel 8c1b6c5154 docs(media): document automatic proxying for hostile codecs (#2596)
* docs(media): document automatic proxying for hostile codecs

Describes the shipped behavior: which input codecs render, that live preview
auto-proxies what the browser cannot decode, where the cache lives, and how to
turn it off. Carries the skills notes and the hardening design documents.

* docs(media): align proxy guidance with runtime
2026-07-17 03:01:56 -04:00
Miguel Ángel 35eff5038b feat(cli): bake proxies into published archives (#2595)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly

* feat(cli): let projects opt out of automatic proxying

Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and
forwards the resolved value into the studio and preview servers and the vite
adapter. Lands before the runtime slice that turns auto-proxying on, so the
switch exists before there is any behavior to switch off.

* fix(cli): align media config schema

* feat(core): swap undecodable video to its proxy at runtime

Adds the browser-side half: before first load the runtime consults the injected
codec map and swaps a hostile source to its proxy, and if a video still reports
zero decodable width it rescues it reactively. An HEVC file carrying AAC fires
no error event, so zero videoWidth, not the error event, is the reliable signal.
Audio elements and alpha sources are never proxied, render mode never proxies,
and each swap evicts the element's stale sync state and reports once.

This completes the loop: auto-proxying is live for preview and studio from here.
The opt-out (media.autoProxy, --no-proxy) shipped in the previous slice.

* feat(cli): serve proxies from play, present, and the static project server

Adds proxy negotiation to the CLI-side servers and gives play byte-range
serving it never had, so a swapped video can seek. The static project server
behind check, snapshot, compare and friends injects the codec map once, so all
of its callers inherit the behavior; snapshot forwards its own proxy flag.

* fix(cli): serve proxies for camera formats

* feat(cli): resolve proxies before check's timed browser phase

check pre-resolves hostile assets so a cold transcode cannot exhaust the
render-ready budget, and surfaces the runtime's proxy diagnostics as findings
so a swap is visible rather than silent.

* feat(cli): bake proxies into published archives

Published pages are static, so there is no server to negotiate with: publish
transcodes proxies for hostile assets into the archive and rewrites the video
sources that point at them. Audio elements keep their originals, since audio
decodes independently of the video codec.

Splits the archive build from the zip step so publish can transform between
them. cloud render keeps calling the unchanged composition and still uploads
originals, which its regression test pins.

* fix(cli): harden proxy pre-resolution

* fix(cli): surface publish proxy outcomes

* test(cli): remove ffmpeg from archive guard

* test(cli): normalize publish fixture path
2026-07-17 02:50:28 -04:00
Miguel Ángel 0561adc11c feat(cli): resolve proxies before check's timed browser phase (#2594)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly

* feat(cli): let projects opt out of automatic proxying

Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and
forwards the resolved value into the studio and preview servers and the vite
adapter. Lands before the runtime slice that turns auto-proxying on, so the
switch exists before there is any behavior to switch off.

* fix(cli): align media config schema

* feat(core): swap undecodable video to its proxy at runtime

Adds the browser-side half: before first load the runtime consults the injected
codec map and swaps a hostile source to its proxy, and if a video still reports
zero decodable width it rescues it reactively. An HEVC file carrying AAC fires
no error event, so zero videoWidth, not the error event, is the reliable signal.
Audio elements and alpha sources are never proxied, render mode never proxies,
and each swap evicts the element's stale sync state and reports once.

This completes the loop: auto-proxying is live for preview and studio from here.
The opt-out (media.autoProxy, --no-proxy) shipped in the previous slice.

* feat(cli): serve proxies from play, present, and the static project server

Adds proxy negotiation to the CLI-side servers and gives play byte-range
serving it never had, so a swapped video can seek. The static project server
behind check, snapshot, compare and friends injects the codec map once, so all
of its callers inherit the behavior; snapshot forwards its own proxy flag.

* fix(cli): serve proxies for camera formats

* feat(cli): resolve proxies before check's timed browser phase

check pre-resolves hostile assets so a cold transcode cannot exhaust the
render-ready budget, and surfaces the runtime's proxy diagnostics as findings
so a swap is visible rather than silent.

* fix(cli): harden proxy pre-resolution
2026-07-17 02:05:25 -04:00
Miguel Ángel a6df35f891 fix(gcp-cloud-run): include all workspace build dependencies (#2608)
Fixes #2601.

Reproduced v0.7.60 from the tag: Docker frozen install failed because five workspace manifests were omitted; after adding manifests, a fresh build failed because dependency dist artifacts were absent.

Changes:
- Copy all workspace manifests and required source trees.
- Build core, parsers, lint, SDK, sdk-playground, studio-server, and engine before producer/adapter.
- Add deterministic manifest/source coverage check.

Verification:
- `bun run --cwd packages/gcp-cloud-run test:dockerfile-workspaces`
- `docker build --progress=plain -f packages/gcp-cloud-run/Dockerfile -t hf-2601-fixed3 .` (success; image built).

Source issue: https://github.com/heygen-com/hyperframes/issues/2601
2026-07-17 01:18:52 -04:00
Miguel Ángel 8e162921bc fix(cli): resolve the producer alias in the CLI vitest config (#2609)
`main`'s Test job is red: studioServer.test.ts fails to collect with "Failed to
resolve entry for package @hyperframes/producer".

studioServer.ts has long carried `await import("@hyperframes/producer")`, but
nothing under test imported that module until #2591 added a suite that does.
Vite's import analysis resolves the specifier at transform time, and the CI test
job builds only parsers, lint, studio-server and core, so the package has no
dist to resolve and the whole file fails to collect. It passes locally only
because a full `bun run build` happens to build the producer first.

The tsup build already aliases this specifier to the producer's source, because
the CLI bundles the producer rather than depending on it at runtime. The test
config now resolves it the same way, so the tooling agrees with the build. The
neighbouring @hyperframes/core alias exists for the same class of reason.

Config only: no source change, so the shipped bundle still inlines the producer
and installed users are unaffected.

Verified by reproducing the CI condition locally (producer dist removed):
studioServer.test.ts fails without this change and passes with it, and the built
cli.js still contains no runtime import of @hyperframes/producer.
2026-07-16 23:43:04 -04:00
Miguel Ángel 74b4f1e8c3 feat(cli): serve proxies from play and the static project server (#2593)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly

* feat(cli): let projects opt out of automatic proxying

Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and
forwards the resolved value into the studio and preview servers and the vite
adapter. Lands before the runtime slice that turns auto-proxying on, so the
switch exists before there is any behavior to switch off.

* fix(cli): align media config schema

* feat(core): swap undecodable video to its proxy at runtime

Adds the browser-side half: before first load the runtime consults the injected
codec map and swaps a hostile source to its proxy, and if a video still reports
zero decodable width it rescues it reactively. An HEVC file carrying AAC fires
no error event, so zero videoWidth, not the error event, is the reliable signal.
Audio elements and alpha sources are never proxied, render mode never proxies,
and each swap evicts the element's stale sync state and reports once.

This completes the loop: auto-proxying is live for preview and studio from here.
The opt-out (media.autoProxy, --no-proxy) shipped in the previous slice.

* feat(cli): serve proxies from play, present, and the static project server

Adds proxy negotiation to the CLI-side servers and gives play byte-range
serving it never had, so a swapped video can seek. The static project server
behind check, snapshot, compare and friends injects the codec map once, so all
of its callers inherit the behavior; snapshot forwards its own proxy flag.

* fix(cli): serve proxies for camera formats
2026-07-16 23:02:01 -04:00
Miguel Ángel 39b588cbd0 feat(core): swap undecodable video to its proxy at runtime (#2592)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly

* feat(cli): let projects opt out of automatic proxying

Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and
forwards the resolved value into the studio and preview servers and the vite
adapter. Lands before the runtime slice that turns auto-proxying on, so the
switch exists before there is any behavior to switch off.

* fix(cli): align media config schema

* feat(core): swap undecodable video to its proxy at runtime

Adds the browser-side half: before first load the runtime consults the injected
codec map and swaps a hostile source to its proxy, and if a video still reports
zero decodable width it rescues it reactively. An HEVC file carrying AAC fires
no error event, so zero videoWidth, not the error event, is the reliable signal.
Audio elements and alpha sources are never proxied, render mode never proxies,
and each swap evicts the element's stale sync state and reports once.

This completes the loop: auto-proxying is live for preview and studio from here.
The opt-out (media.autoProxy, --no-proxy) shipped in the previous slice.
2026-07-16 23:01:39 -04:00
Miguel Ángel 6458807066 feat(cli): let projects opt out of automatic proxying (#2591)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly

* feat(cli): let projects opt out of automatic proxying

Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and
forwards the resolved value into the studio and preview servers and the vite
adapter. Lands before the runtime slice that turns auto-proxying on, so the
switch exists before there is any behavior to switch off.

* fix(cli): align media config schema
2026-07-16 23:01:14 -04:00
Miguel Ángel 67eab59f44 feat(studio-server): serve H.264 proxies from the preview route (#2590)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly
2026-07-16 23:00:48 -04:00
Miguel Ángel 9d148d288a feat(studio-server): transcode bounded H.264 proxies on demand (#2589)
* feat(studio-server): bound the proxy cache with LRU accounting

Adds cache accounting and bounded cleanup for transcoded proxies, and keeps
.transcode-cache out of git. Standalone: the transcoder consumes it next.

* feat(studio-server): transcode bounded H.264 proxies on demand

Adds the proxy transcoder: a bounded work queue with per-key dedupe, a hard
kill ceiling, TTL'd failure memory so a broken asset is not retried forever,
and pixel/color normalization for browser playback. Writes through a temp name
and renames on success, so a cache entry is never partial.

Carries a TEMP fallow ignoreExports entry: this module lands below its
consumers, so a per-PR audit sees its exports as unused until the preview weld
arrives. The entry is dropped there.
2026-07-16 21:08:03 -04:00
Miguel Ángel 1d3d20450d feat(studio-server): bound the proxy cache with LRU accounting (#2588)
Adds cache accounting and bounded cleanup for transcoded proxies, and keeps
.transcode-cache out of git. Standalone: the transcoder consumes it next.
2026-07-16 21:05:08 -04:00
Miguel Ángel 9ca1e17101 feat(studio-server): probe media codec facts for proxy decisions (#2587)
* feat(studio-server): probe media codec facts for proxy decisions

Adds the codec manifest: one ffprobe-backed answer to what codec an asset
uses, whether a browser can decode it, and whether it carries alpha. Migrates
the existing prober to the shared ff-binaries resolver and to async execFile so
a scan pool runs off the event loop. No consumer yet; the preview weld wires it
up later in the stack.

* fix(studio-server): honor injected ffprobe runners
2026-07-16 20:54:50 -04:00
Miguel Ángel d7b38da19d Merge pull request #2586 from heygen-com/carve/s1-asset-resolution
refactor(parsers): share local-asset resolution across lint and studio-server
2026-07-16 20:07:47 -04:00
Miguel Ángel 817a87056c Merge pull request #2585 from heygen-com/carve/hevc-base-rebased
refactor(parsers): single shared FFmpeg/FFprobe binary resolver
2026-07-16 20:07:14 -04:00
Miguel Ángel d4a29fd0c3 Merge pull request #2597 from heygen-com/carve/hevc-base-docs-lint
feat(lint): warn on HEVC video assets that browsers cannot preview
2026-07-16 20:06:35 -04:00
Miguel Angel Simon Sierra 053e1274b1 refactor(parsers): share local-asset resolution across lint and studio-server
Moves assetResolution.ts from packages/lint into @hyperframes/parsers behind
a ./asset-resolution subpath export, mirroring the ./ff-binaries shape, and
points lint's two importers at it. Pure move: no behavior change. Unblocks
studio-server consuming the same helpers without a dependency cycle (the CLI
already depends on lint, so importing lint from studio-server is not an option).
2026-07-16 19:16:43 -04:00
Miguel Angel Simon Sierra 7980479083 fix: preserve shared FFmpeg resolver behavior 2026-07-16 19:10:04 -04:00
Miguel Angel Simon Sierra 88c049f525 refactor(parsers): single shared FFmpeg/FFprobe binary resolver
The cli, engine, and lint packages each carried their own copy of the
ffmpeg/ffprobe lookup, annotated fallow-ignore code-duplication, and the
copies had drifted: the engine copy handled Windows PATHEXT and executed
which/where without a shell but lacked the Homebrew-dirs fallback for
GUI-spawned processes; the cli copy had the opposite. One resolver in
@hyperframes/parsers (the dependency-graph bottom) now carries the union
of both hardenings, and all three packages delegate to it. Every
consumer gets strictly more robust resolution; env-override semantics
per call site are preserved via configuredMustExist.
2026-07-16 18:36:40 -04:00
Miguel Angel Simon Sierra 247743fcdf refactor(lint): bound concurrent ffprobe processes in HEVC probe 2026-07-16 18:36:12 -04:00
Miguel Angel Simon Sierra 664c39db9e feat(lint): flag HEVC video assets with info-level preview note 2026-07-16 18:36:12 -04:00
Miguel Angel Simon Sierra 5f2819b1e7 docs: document HEVC input support and the preview-only codec caveat 2026-07-16 18:36:12 -04:00
Miguel Ángel 08dbb7db37 fix(engine): ignore benign media request aborts (#2423) 2026-07-16 18:20:25 -04:00
Miguel Ángel f0aee28551 fix(engine): reuse HyperFrames browser cache (#2459)
* fix(engine): reuse HyperFrames browser cache

* test(engine): isolate browser cache home on Windows
2026-07-16 18:20:21 -04:00
Miguel Ángel 428e571914 fix(skills): align TTS docs with CLI contract (#2483) 2026-07-16 18:20:17 -04:00
Miguel Ángel 406894061e fix(core): stop composition variables from shadowing authored CSS custom properties (#2553)
A composition variable mirrored as --<slug> for a mounted sub-composition
(default or an explicit data-variable-values value) previously overrode any
same-named custom property the document already authored elsewhere (e.g. a
:root theme token), since the mirroring had no "already defined" guard —
unlike the two other emission paths, which already skip re-emitting when the
name collides with an authored definition. Extend that guard to the
sub-composition mount path in both the compiler (htmlBundler.ts) and the
runtime loader (compositionLoader.ts / getVariables.ts), so an authored
definition always wins; render-time --variables overrides still always win.
2026-07-16 18:20:13 -04:00
Miguel Ángel 771145a5c4 fix(registry): remove invalid media from caption components (#2454)
* fix(registry): remove invalid media from caption components

* test(registry): enforce nested media contracts
2026-07-16 12:16:45 -04:00
Miguel Ángel 55ee559e40 fix(lint): catch cold-seek opacity reveals (#2503)
* fix(lint): catch cold-seek opacity reveals

* fix(lint): resolve hidden selector aliases

* style(lint): format gsap rule
2026-07-16 12:03:25 -04:00
Miguel Ángel 9b17a5ad7e fix(cli): avoid Python whisper CLI collision (#2414) 2026-07-16 12:03:21 -04:00
Miguel Ángel ed1f38124b fix(engine): preserve mono audio level (#2392) 2026-07-16 12:03:17 -04:00
Miguel Ángel e846cd6004 fix(cli): fail clearly on unsupported Node versions (#2388) 2026-07-16 12:03:13 -04:00
Miguel Ángel 584be6d64a fix(transcribe): select multilingual model before download (#2303) 2026-07-16 12:03:09 -04:00
Miguel Ángel 75eedf5cc1 fix(cli): time out stalled model downloads (#2415) 2026-07-16 11:45:49 -04:00
Miguel Ángel 335e7483b5 fix(talking-head): preserve source audio (#2260) 2026-07-16 11:30:49 -04:00
Miguel Ángel b8a44e49c9 fix(lint): ignore !important in font families (#2538) 2026-07-16 10:43:17 -04:00
Miguel Ángel 9ed255c0ef Merge pull request #2524 from heygen-com/release/v0.7.60
chore: release v0.7.60
2026-07-16 01:32:57 -04:00
Miguel Ángel 0f287ee0b6 chore: release v0.7.60 2026-07-16 05:26:57 +00:00
Miguel Ángel 160f142d15 Merge pull request #2521 from heygen-com/fix/stale-compositor-layer
fix(renderer): prevent stale SwiftShader layers
2026-07-16 01:23:59 -04:00
Miguel Ángel 9c25e27da6 docs(renderer): link SwiftShader workaround tracker 2026-07-16 05:19:00 +00:00
Miguel Ángel 54a3ef2000 fix(renderer): prevent stale SwiftShader layers 2026-07-16 04:56:36 +00:00
Miguel Ángel 33a4f82dca Merge pull request #2516 from heygen-com/fix/2514-hold-last-video-frame
fix(video): hold final frame through composition
2026-07-15 22:58:16 -04:00
Miguel Ángel 4cbde38301 test(producer): use intrinsic duration in style fixtures 2026-07-16 02:15:02 +00:00
Miguel Ángel 1284703adb test(producer): update remaining video slot goldens 2026-07-16 02:15:01 +00:00
Miguel Ángel 31cc2fe875 test(producer): update video slot regression golden 2026-07-16 02:15:01 +00:00
Miguel Ángel 2e8f871bc8 fix(video): hold final frame through composition 2026-07-16 02:15:01 +00:00
Miguel Ángel 3a71a03de5 fix(skills): require actionable CLI feedback repros (#2498) 2026-07-15 18:14:06 -04:00
Miguel Ángel 35e623b4f3 fix(cli): align video output boundaries (#2490)
* fix(cli): align video output boundaries

* test(cli): honor CI ffmpeg fixture path

* test(cli): decouple duration precedence from ffmpeg

* test(cli): pin half-open video boundaries

* test(producer): refresh style 7 boundary golden
2026-07-15 16:07:44 -04:00
Miguel Ángel f45f762473 fix(skills): preserve caption skin contrast states (#2486)
* fix(skills): preserve caption skin contrast states

* chore(skills): refresh caption contrast manifest

* fix(skills): keep caption skin ownership aligned

* style(skills): format shared caption builders
2026-07-15 16:01:31 -04:00
Miguel Ángel 9e53c0f9b1 fix(cli): preserve snapshot timestamp precision (#2494) 2026-07-15 15:54:27 -04:00
Miguel Ángel b874c4440f fix(runtime): resume readiness after deferred GSAP batching (#2491) 2026-07-15 15:50:48 -04:00
Miguel Ángel 968c90397b fix(engine): fail partial audio track preparation (#2488) 2026-07-15 15:26:42 -04:00
Miguel Ángel 850f57ea04 fix(render): ignore favicon probe noise (#2487) 2026-07-15 15:26:25 -04:00
Miguel Ángel e038cc93c6 fix(studio): mute composition hover previews (#2478)
* fix(studio): mute composition hover previews

* fix(studio): route hover muting through bridge
2026-07-15 12:02:36 -04:00
Miguel Ángel 04954ead81 fix(cli): restore Intel macOS background removal (#2480) 2026-07-15 10:30:04 -04:00
Miguel Ángel 882c203241 fix(render): avoid Windows output path limit for work dirs (#2479) 2026-07-15 10:30:00 -04:00
Miguel Ángel 1895286189 fix(render): normalize local AAC duration before mux (#2472)
* fix(render): normalize local AAC duration before mux

* style(render): apply repository formatter

* fix(render): count AAC packets for duration normalization

Older FFmpeg versions estimate raw ADTS duration from bitrate and can undercount variable-bitrate audio, causing the normalizer to append a false silence tail. Derive the mixed AAC duration from packet count and sample rate instead.

* fix(render): isolate normalized audio temp path

* fix(engine): centralize AAC packet duration

* test(producer): refresh AAC duration golden
2026-07-15 10:07:07 -04:00
Miguel Ángel ca69b6b080 fix(cli): preserve media at snapshot end boundary (#2475)
* fix(cli): preserve media at snapshot end boundary

* test(cli): cover snapshot media boundary branches
2026-07-15 08:13:20 -04:00
Miguel Ángel 47e4b18f99 fix(render): discover runtime-inserted media before extraction (#2474)
* fix(render): discover runtime-inserted media before extraction

* fix(render): detect runtime Audio constructors

* test(render): cover runtime audio insertion paths
2026-07-15 08:13:17 -04:00
Miguel Ángel 017183ad66 fix(cli): keep render filename timestamp local (#2470)
* fix(cli): keep render filename date and time local

* fix(render): share local output timestamps
2026-07-15 08:13:13 -04:00
Miguel Ángel c54bf13fb4 fix(cli): avoid multiline overlap false positives (#2468)
* fix(cli): avoid multiline overlap false positives

* fix(cli): sample multiline occlusion fragments
2026-07-15 02:32:25 -04:00
Miguel Ángel d2aa4f8bc4 fix(cli): preflight browser archive extraction (#2469) 2026-07-15 02:29:33 -04:00
Miguel Ángel f3800f3579 fix(lint): exempt caption cues from track density (#2461)
* fix(lint): exempt caption cues from track density

* style(media-use): satisfy resolver formatting gate
2026-07-15 02:05:39 -04:00
Miguel Ángel 7cc10a9922 feat(studio): track design-panel input usage across both inspector UIs (#2467)
* feat(studio): add design-panel input usage tracking primitive

* feat(studio): track input usage in classic inspector panel

* feat(studio): track input usage in flat inspector panel

* fix(studio): attribute animation meta by field, track classic chrome, widen coverage guard
2026-07-15 01:33:27 -04:00
Miguel Ángel e3ca89e9ac fix(cli): discover project-local ffmpeg binaries (#2464)
* fix(cli): discover project-local ffmpeg binaries

* style(cli): format project-local ffmpeg discovery

* style(media-use): format resolver script

* chore(skills): refresh bundled manifest
2026-07-15 00:37:18 -04:00
Miguel Ángel a04ce6a244 fix(cli): scale whisper timeout for long recordings (#2463)
* fix(cli): scale whisper timeout with audio duration

* style(cli): format whisper timeout scaling

* style(media-use): format resolver script

* chore(skills): refresh bundled manifest

* fix(cli): scale audio preparation timeouts
2026-07-15 00:36:34 -04:00
Miguel Ángel bf8432bca8 fix(media-use): format resolver diagnostics (#2466) 2026-07-14 23:43:32 -04:00
Miguel Ángel 6813eaeece fix(producer): fail renders when audio mixing fails (#2429) 2026-07-14 23:27:20 -04:00
Miguel Ángel cada0e7c09 fix(cli): stream non-TTY render progress (#2458) 2026-07-14 22:13:43 -04:00
Miguel Ángel d047d28bb4 fix(capture): bound static dedup verification time (#2457)
## What

- cap static-dedup verification at 15 seconds of wall-clock time
- disable the optimization and continue normal capture when the verification budget is exhausted
- add a regression that models the reported 350-second / ~8,400-frame alpha render

## Why

Static-frame verification uses full-page screenshots. Its existing screenshot-count budget still scales with composition duration, so a long composition can spend minutes proving an optimization before frame capture starts. The reported 350.35-second ProRes alpha render spent about eight minutes in this phase before safely disabling dedup.

## How

The verifier now records a deadline before seeking verification frames. It checks the deadline before every full-page capture and returns the existing fail-closed `budgetExhausted` result when time is exhausted. This keeps the existing density-based safety checks while bounding their startup cost.

## Test plan

- [x] Unit tests added/updated
- [ ] Manual testing performed
- [ ] Documentation updated (not applicable)
- [x] Focused engine test: 9/9 passed
- [x] Engine typecheck passed
- [x] Pre-commit lint, format, tracked-artifact, fallow, and typecheck gates passed
- [x] Full engine suite: 988 passed, 3 skipped; 2 pre-existing environment failures because host FFmpeg 4.2 lacks `-fps_mode`
2026-07-14 22:04:49 -04:00
Miguel Ángel da19f9a692 fix(media-use): explain missing bundled SFX (#2460) 2026-07-14 21:59:44 -04:00
Miguel Ángel 7382fabab9 fix(core): consolidate external asset and dependency preservation (#2410)
## Summary

- preserve external SVG fragment references during bundling
- preserve external module scripts and serve `.mjs` with a JavaScript MIME type
- retain template-head stylesheets when mounting sub-compositions
- add regression coverage across compiler runtime and file-server paths

Consolidates and replaces #2390, #2297, and #2375.

## Verification

- core compiler/runtime tests: 89 passed
- producer file-server tests: 48 passed
- core, producer, engine, and CLI typechecks passed
- `git diff --check`
2026-07-14 21:55:51 -04:00
Miguel Ángel 5d3a7404fa fix(render): consolidate preflight and local recovery (#2403)
* fix(render): fall back when libx264 is unavailable

* fix(render): recover orphaned browsers before retry

* fix(render): check disk space on write volumes

* test(engine): accept resolved ffmpeg binary paths

* fix(render): harden H.264 capability fallback

* chore(ci): scope inherited Fallow findings

* fix(render): diagnose encoder probe failures
2026-07-14 21:55:39 -04:00
Miguel Ángel 15ca6fd129 fix(skills): bundle modular capture helpers (#2456) 2026-07-14 20:35:05 -04:00
Miguel Ángel ada878fdcd fix(lint): consolidate lint and audit correctness (#2413)
* fix(lint): stop CSS comments in <style> from manufacturing phantom root tags

extractOpenTags scans raw source text with a flat regex that has no
concept of <style>/<script> block boundaries, so a CSS comment like
`/* <g> wrapper */` inside a <style> block reads as a real open tag.
findRootTag consumes that flat tag list and only skips tags literally
named script/style/meta/link/title, so the phantom <g> tag (not in
that skip list) wins the "first non-ignored body tag" search and gets
returned as the composition root instead of the real one that follows.

This manufactured root_missing_composition_id and root_missing_dimensions
(the phantom tag has neither) plus head_leaked_text (the leaked-text
scan slices up to the phantom tag's position, landing inside the
<style> block before its real closing tag, so the raw CSS text reads
as leaked markup) on an otherwise valid sub-composition — reported
with an exact bisected repro: a <template>-wrapped SVG sub-composition
whose <style> block comments reference an inner <g> element.

Fix: compute <style>/<script> content spans up front (reusing the
existing extractBlocks + STYLE_BLOCK_PATTERN/SCRIPT_BLOCK_PATTERN) and
skip any TAG_PATTERN match that falls inside one, before it ever
reaches findRootTag or any other extractOpenTags consumer. Same shape
as the prior fix for a leading <svg> defs block being mistaken for the
root (8ee4b7df) — this closes a sibling gap in the same function.

Test: new regression case with a <style> block containing a `/* <g> */`
comment ahead of an <svg data-composition-id> root, asserting none of
the three findings fire. Full lint package suite (318 tests) passes.

* feat(lint): flag duplicate data-composition-id values

Declaring data-composition-id on more than one element (commonly the <meta>
tag from the quickstart template AND the root <div> added to satisfy
root_missing_composition_id) is a silent collision: `compositions --json`
returns two entries for the same id (one duration:0) and inspect/snapshot
crash with "Cannot read properties of undefined (reading totalDuration)".
Lint passed clean through all of it.

New rule `duplicate_composition_id`: group elements by data-composition-id
value and error on any value shared by 2+ elements, naming the id and calling
out the meta-vs-root collision in the fixHint. 3 tests: dup fires, single id
passes, two distinct ids don't collide. (Implemented via Codex; verified
independently: 111 lint tests pass, oxfmt/oxlint clean.)

* fix(audits): avoid caption false positives

* fix(lint): ignore proxy-label tween overlaps

* fix(cli): preserve the five-percent text audit floor

* fix(lint): preserve proxy identity across lexical scopes

* fix(cli): audit only directly painted text

* fix(lint): compare live composition ids canonically

* fix(lint): preserve expanded proxy identities

* fix(cli): measure directly painted text geometry

* fix(lint): preserve first duplicate attribute value

* fix(lint): keep shared proxy identity across helpers

* fix(parsers): preserve expanded proxy identity

* fix(lint): decode composition IDs consistently
2026-07-14 20:13:01 -04:00
Miguel Ángel 37e1b26434 fix(fonts): consolidate composition-aware compilation (#2406)
* fix(fonts): subset Google Fonts to composition text

* fix(producer): skip slow TTC recompression

* fix(fonts): include decoded composition text in subsets
2026-07-14 20:12:30 -04:00
Miguel Ángel 26eb5956e5 fix(registry): install liquid-glass runtimes (#2451)
* fix(registry): install liquid-glass runtime

* fix(registry): cover liquid-glass sibling runtimes

* test(registry): satisfy manifest audit
2026-07-14 19:09:41 -04:00
Miguel Ángel 14df58f017 fix(check): ignore registry component templates (#2450) 2026-07-14 18:21:25 -04:00
Miguel Ángel 9e2afbcce5 fix(engine): consolidate capture readiness and retries (#2404)
* fix(engine): await dynamic CSS backgrounds before capture

* fix(render): retry transient network changes

* fix(engine): parse CSS URLs without backtracking

* fix(engine): decode CSS backgrounds in batch capture
2026-07-14 18:10:03 -04:00
Miguel Ángel eb731b6a8a fix(skills): consolidate animation-map capture reliability (#2409)
* fix(skills): pass rational fps to capture helpers

* fix(skills): batch animation map sampling

* chore(skills): refresh animation manifests

* fix(skills): parse exact animation map frame rates
2026-07-14 18:04:32 -04:00
Miguel Ángel 9ce44b6603 fix(cli): bundle preview font localization (#2449) 2026-07-14 18:02:51 -04:00
Miguel Ángel 0b3dfb3f84 fix(render): consolidate duration and timing correctness (#2405)
* fix(producer): pass variables to duration probe

* fix(producer): tolerate rounded frame-boundary durations

* fix(cli): resolve relative data-start references in composition duration

`compositions --json` computed each timed child's start with a bare
parseFloat(data-start ?? "0") in parseCompositions (host duration) and
parseSubComposition (sub-comp duration). A relative reference like
data-start="s1" ("start when clip s1 ends") is not numeric, so parseFloat
returned NaN and that clip's contribution to the max-end was silently
dropped — a host with two 3s clips (2nd data-start="s1") reported duration 3
instead of 6, breaking compositions/inspect/snapshot for composition-clip
relative timing.

Resolve relative references the same way the extractor does (parseStartExpression
from @hyperframes/core + a findReferenceTargetEl/resolveReferencedStart port,
since the engine's referenceResolver isn't a public export across the package
boundary). Verified: host duration now 6; 3 tests pass.
(Implemented via Codex; verified independently.)
2026-07-14 17:12:36 -04:00
Miguel Ángel 6ac18fd68d fix(product-launch): consolidate media brand and audio contracts (#2408)
* fix(product-launch): preserve hoisted media offsets

* fix(product-launch): preserve brand font and accent roles

* fix(product-launch): honor TTS provider selection

* fix(product-launch): preserve approved video geometry

* fix(skills): enforce media geometry and font classification

* fix(skills): align secondary brand accents
2026-07-14 17:11:55 -04:00
Miguel Ángel 4b0b89e8b1 chore: release v0.7.58 (#2446) 2026-07-14 16:15:59 -04:00
Miguel Ángel 6c2513d5c8 fix(check): resolve color-mix() colors in contrast audit instead of false-failing (#2445) 2026-07-14 15:51:11 -04:00
Miguel Ángel 990f5c3145 feat(feedback): adopt 0–10 recommendation scale (#2438)
* feat(feedback): adopt 10-point recommendation scale

* docs(feedback): keep OSS scale contract self-contained
2026-07-14 15:46:47 -04:00
Miguel Ángel d2c8c2d808 fix(producer): probe variable-bound media sources (#2444) 2026-07-14 15:42:00 -04:00
Miguel Ángel 4cba58f5a3 fix(pr-to-video): use display names, not GitHub logins, in credits narration (#2385)
* fix(pr-to-video): use display names, not GitHub logins, in credits narration

gh pr view already returns a `name` field for the PR author, commit authors,
and mergedBy. ingest.mjs now tracks it in people.json alongside login;
fetch-people-avatars.mjs resolves a name for reviewers/commenters/assignees
gh doesn't name via the public GitHub user API, best-effort.

story-design.md now directs the credits close to speak the person's name
(TTS reading a raw handle like @miguAng18947550 aloud is the failure mode),
with the handle shown as secondary on-screen text only.

* fix(pr-to-video): resolve missing credit names via the agent, not a new script fetch

fetch-people-avatars.mjs already runs inside the orchestrating agent's turn,
which has gh available — no need for the script to duplicate a name lookup
the agent can do itself with `gh api users/<login> --jq .name`. Reverts the
script back to avatar-fetching only; SKILL.md/story-design.md now tell the
agent to resolve any missing name for the credited people itself before
writing the credits close.

* fix(pr-to-video): regenerate skills-manifest.json hash

Stale hash left over from a rebase conflict I resolved by hand — the
generator produces the correct one.
2026-07-14 15:41:33 -04:00
Miguel Ángel e05debe1af fix(engine): honor explicit render worker counts (#2439) 2026-07-14 14:52:31 -04:00
Miguel Ángel d7204ac47f test(engine): make FFmpeg path assertion platform-safe (#2433) 2026-07-14 13:22:24 -04:00
Miguel Ángel 3d7e26aabf fix(render): diagnose unlaunchable Windows FFmpeg (#2430) 2026-07-14 12:55:46 -04:00
Miguel Ángel 6933e8acda fix(cli): consolidate snapshot and frame diagnostics (#2402)
* fix(snapshot): preserve exact requested times

* fix(cli): fail video snapshots without FFmpeg

* fix(cli): honor navigation timeout in diagnostics

* fix(cli): preserve snapshot alpha and create shot dirs
2026-07-14 01:46:44 -04:00