Commit Graph
2700 Commits
Author SHA1 Message Date
Miguel Ángel 8bf7bc3af5 fix(cli): require explicit non-interactive init source (#2395) 2026-07-13 23:28:46 -04:00
Vance Ingalls f44bc3a525 fix(engine,producer): drive forceScreenshot from one authoritative local
Miguel R4 blocker on #2359: my R3 fix at renderOrchestrator only updated
the observability copy, leaving the authoritative captureForceScreenshot
local at compileResult.forceScreenshot (false for auto→software). The
frameCapture side clamped its own local and correctly routed screenshot,
but downstream orchestrator code overwrote observability back to
beginframe from the still-false local at two sites:

  - Parallel-stream label at renderOrchestrator.ts:2293 mis-labelled the
    stream as 'beginframe' when actual capture was 'screenshot'.
  - capture_strategy telemetry at renderOrchestrator.ts:2440-2450
    overwrote the earlier observability correction, so the final
    captureMode observation flipped back to 'beginframe' while the
    engine actually captured screenshot.

Fix: extract the clamp into a caller-facing helper
applyConcreteGpuScreenshotClamp(current, resolvedGpuMode, cfg) that
returns the (possibly-promoted) new boolean. Callers assign it back to
their authoritative local, so routing + telemetry + strategy code read
one value.

Changes:

  - packages/engine/src/config.ts: new exported
    applyConcreteGpuScreenshotClamp; delegates to
    shouldClampToScreenshotForConcreteGpu but computes the caller's
    final value, not just the clamp decision. Reads the programmatic
    opt-out from cfg.forceScreenshotExplicitlyOptedOut. Idempotent on
    already-true input.
  - packages/engine/src/index.ts: export the new helper.
  - packages/engine/src/services/frameCapture.ts: replace the inline
    OR expression with applyConcreteGpuScreenshotClamp.
  - packages/producer/src/services/renderOrchestrator.ts: assign result
    into the AUTHORITATIVE captureForceScreenshot local (was updating
    only observability). Downstream parallel-stream label at :2293 and
    capture_strategy telemetry at :2440-2450 now read the corrected
    value.

Tests: 6 new caller-level cases for applyConcreteGpuScreenshotClamp
covering the exact matrix Miguel called out:

  - resolved software + default false → promotes to true (screenshot)
  - resolved software + programmatic opt-out → stays false (BeginFrame)
  - resolved hardware + default false → stays false
  - resolved software + already-true → stays true (idempotent)
  - resolved software + env PRODUCER_FORCE_SCREENSHOT=false → stays false
  - resolved software + undefined cfg → promotes to true (frameCapture path)

Local: 67/67 engine config tests pass (was 61). oxfmt clean.
2026-07-14 03:17:29 +00:00
Vance Ingalls 72daac2a1d fix(engine): carry programmatic forceScreenshot opt-out to concrete-resolved site
Miguel R3 blocker on #2359: the runtime helper only checked the env opt-out
(PRODUCER_FORCE_SCREENSHOT=false), silently defeating the documented
programmatic escape hatch (overrides.forceScreenshot === false) on the
browserGpuMode:'auto' → software probe path. At the concrete-resolution
site the boolean forceScreenshot === false is ambiguous between default
and explicit opt-out — resolveConfig sees the provenance but the runtime
helper does not.

Fix: persist provenance on the resolved config.

- New INTERNAL EngineConfig field forceScreenshotExplicitlyOptedOut, set
  by resolveConfig when EITHER env or programmatic explicit-false is
  present. Purpose-documented in the type as 'not intended to be set by
  callers'.
- shouldClampToScreenshotForConcreteGpu gains an opts.programmaticOptOut
  parameter; returns false early when set. Env stays as the third arg
  (backward compatibility with existing tests).
- frameCapture.ts and renderOrchestrator.ts pass
  config.forceScreenshotExplicitlyOptedOut through at both call sites, so
  the auto→software probe path preserves the same escape hatches as
  literal browserGpuMode:'software'.

New tests: 5 additional cases across the helper (programmatic opt-out
alone; programmatic beats missing env) and resolveConfig provenance
(programmatic sets flag; env sets flag; neither leaves it undefined).
Local: 61/61 engine config tests pass (was 56).
2026-07-14 03:06:34 +00:00
Miguel Ángel 2a7a5e6236 feat(preview): keep review sessions alive (#2384)
* feat(preview): keep review sessions alive

* fix(preview): harden background lifecycle state

* refactor(preview): satisfy lifecycle quality gate
2026-07-13 22:55:19 -04:00
Miguel Ángel 4495cb7355 feat(cli): stable public URL for hyperframes publish (re-publish updates the same link) (#2363)
* feat(cli): stable-URL re-publish (send owned id, honest UX, --update, team id file)

Resolve a stable project id (committed team id > machine store > mint), send it
when authenticated so an owned re-publish updates the same URL in place, and
persist the server id+url. Report updated-vs-created honestly, add --update to
target a project explicitly, write a committable .hyperframes/project.json so a
team shares one link, and show the prior URL on re-publish.

* test(cli): env-gated E2E round-trip for stable-URL re-publish

Publish -> edit -> re-publish asserts one URL with updated content against a
live EF (HYPERFRAMES_E2E_API_URL + an authenticated runner); skipped otherwise.

* fix(cli): real team space, auth-gate --update/--space, safe team-file write

- --space + committed .hyperframes/project.json (projectId+spaceId) send X-Space-Id so a
  team converges on one link; personal space stays the default for solo users
- --update/--space error when unauthenticated, and warn loudly when a resolved-but-invalid
  token silently downgrades to a new anonymous URL (no more generic-tip-only)
- team-file write in its own try/catch so a read-only dir can't fake 'Publish failed'
- parseUpdateTarget handles scheme-less URLs + query/hash; X-Space-Id on metadata only (not S3 PUT)
- share readJsonRecord across the local + team descriptors

* test(cli): e2e team-space convergence + cross-space hijack guard

* test(cli): unit-test parseUpdateTarget url shapes (export for test)

* fix(cli): warn on committed-team miss too, not just --update
2026-07-13 22:47:28 -04:00
Miguel Ángel ca7c017e49 fix(media-use): tag avatar-video heygen recipes with X-HeyGen-Client-Source (#2391)
The agent-driven video recipes (heygen video create) went out untagged, so
media-use avatar / image-to-video usage landed as generic cli traffic and
wasn't attributable to media-use. Add --headers "X-HeyGen-Client-Source:
media-use" to the generating recipes (persistent flag, allowlisted by the
CLI) so those videos carry client_source in master_video_table meta and show
up in the API dashboards alongside the TTS path (#2365) and CLI (#2368).
2026-07-13 22:43:58 -04:00
Miguel Ángel 852d1891d3 fix(pr-to-video): enforce offline frame contract (#2383)
* fix(pr-to-video): enforce offline frame contract

* fix(pr-to-video): tighten frame attribute contract
2026-07-13 22:39:32 -04:00
Miguel Ángel fba5cb9c93 fix(pr-to-video): bound first-run workspace and context (#2382)
* fix(pr-to-video): bound first-run workspace and context

* fix(cli): expose validation gate in help

* fix(pr-to-video): harden workflow guardrails

* style(pr-to-video): apply repository formatting

* chore(skills): refresh pr-to-video manifest
2026-07-13 22:33:21 -04:00
Miguel Ángel 9ac4ab8dee test(engine): write HDR fixture color tags into the H.264 VUI (#2389)
The SDR-to-HDR extraction tests synthesize their HDR fixture with
-color_trc/-color_primaries flags and rely on the encoder propagating
them into the bitstream. The pinned Windows CI ffmpeg build drops the
transfer on that path, so after #2377 narrowed HDR detection to the
transfer function, the fixture probes as SDR on Windows and both tests
fail (hdrPreflightCount 0). Write the VUI directly with the
h264_metadata bitstream filter so the tag survives on every build.
2026-07-13 22:20:41 -04:00
Miguel Ángel 6f4c6308f0 fix(lint): isolate IIFE timing constants (#2378) 2026-07-13 20:53:24 -04:00
Miguel Ángel b231b6f963 fix(cli): surface keyframes from scripts and styles inside <template> (#2374)
Sub-compositions are required to wrap markup, script, and style in <template>, but template content is an inert DocumentFragment that document-level querySelectorAll does not traverse — so 'hyperframes keyframes' silently surfaced zero tweens and zero CSS keyframes for every spec-conformant sub-composition. Extract from the document and every template content fragment (nested templates included, walked iteratively). Documents the extraction ordering contract and the deliberately document-root-only sub-composition discovery scan; pins template, multi-template, nested-template, and mixed-script cases with tests.
2026-07-13 20:47:50 -04:00
Miguel Ángel 9639824f4e fix(render): require HDR transfer metadata (#2377) 2026-07-13 20:46:59 -04:00
Miguel Ángel 3a020bf543 fix(cli): reject missing init example values (#2376) 2026-07-13 20:42:11 -04:00
Ular Kimsanov 403ecb56d9 Merge pull request #2347 from heygen-com/fix/studio-restore-golden-behaviors
fix(studio): restore golden-branch timeline behaviors dropped by the stack rebuild
2026-07-13 17:13:37 -07:00
Miguel Ángel dd938c7a16 feat(media-use): tag HeyGen calls with X-HeyGen-Client-Source (#2365)
Send `X-HeyGen-Client-Source: media-use` on every media-use HeyGen API
request (both auth types, via heygenAuthHeaders + the heygenJSON transport),
so backend billing meta can isolate media-use consumption from other free
TTS and avatar-video usage. Unconditional of auth type — a paying user's
media-use call is still media-use — unlike the OAuth-only cli-source header
that gates the free allowance.
2026-07-13 20:03:31 -04:00
ukimsanov 512560b4c3 test(studio): delete-path duration rollback coverage, shared dismiss predicate on preview open 2026-07-13 16:48:52 -07:00
ukimsanov a33b3f35e1 fix(studio): address PR #2347 review findings (rounds 1-2)
Review 1 (restore commit):
- asset reveal now clears any open preview overlay (stuck-overlay repro:
  preview on A, click already-added B — A stayed open over the reveal)
- duration readout rolls back on failed persist: captureDurationRollback
  snapshots store + live root data-duration before the optimistic sync and
  restores both in every move/resize/delete/group catch (golden's
  previousDuration pattern)
- asset preview opened during running playback dismisses immediately (the
  RAF loop bypasses the store, so the subscription alone never fired)
- persistTimelineBatchEdit resolves the target (findTagByTarget) before
  treating identical output as a no-op — a mistargeted member now throws
  like the single-element path instead of being silently dropped
- a post-mutation history-fold failure no longer suppresses the preview
  sync: fold errors are surfaced separately and the rewritten script still
  syncs (previously the preview kept stale GSAP positions with no recovery)
- timelineRevealScroll guards degenerate viewports (windowSize <= 0)
- CodeQL: encodeURIComponent(projectId) at all timelineTimingSync fetches

Review 2 (single-source-of-truth pass):
- createTimelineElementFromManifestClip — the one manifest->element
  boundary — now carries authoredTrack and stackingContextId; expanded
  sub-comp children preserve both (authoredTrack in their OWN file's space)
- authoredTrackForLane scopes occupants to the dragged clip's sourceFile
  (a foreign file's authored values are a different coordinate space);
  nearest-same-file-lane offset fallback
- optimistic store updates mirror the persisted track into authoredTrack
  (and roll it back on failure), so consecutive drags before a reload
  resolve from fresh data
- spill sub-lanes: documented decision — dropping onto a spill lane is a
  legitimate same-track join (occupants share the authored track by
  construction); false 'never a lane-move target' docstring rewritten
- single-element fallback persists vertical-only moves (early return now
  requires neither start nor track changed; live DOM patch includes
  data-track-index)
- canonical contextKey helper for stacking-context normalization
- new pipeline test crosses the REAL factory boundary (sparse authored
  tracks -> factory -> expansion -> normalize -> drag commit -> persisted
  attribute), no injected fields
2026-07-13 16:48:52 -07:00
ukimsanov 760b88a6f3 fix(studio): flashless z-order commits and visible-overlap stepping
Two legibility fixes for the canvas z-order menu, from user feel-testing:

- z-only commits no longer remount the preview iframe. The commit hook
  already applies the inline z (+ injected position) to the live elements and
  updates the store synchronously; the post-commit reloadPreview() was a
  redundant full remount that read as a canvas 'blink' on every action.
  commitDomEditPatchBatches gains skipReload, engaged only when provably
  safe: every op is an inline-style patch AND the server reports every patch
  matched — anything else falls back to the reload so the preview reconverges
  with disk. The file-watcher's own reload stays suppressed by the existing
  domEditSaveTimestampRef window, so the skip is real.
- Bring Forward / Send Backward step over the next VISIBLY overlapping
  sibling. The nearest z-neighbor in a composition is often invisible at the
  current frame (runtime hides time-inactive clips with inline
  visibility/display; GSAP parks elements at opacity 0), so the step crossed
  something the user couldn't see — 'enabled but nothing happens'. The
  forward/backward set now filters on element-level computed visibility
  (display/visibility/opacity, injectable for tests); enable/disable shares
  the resolver so the menu is honest: actions disable when no visible
  neighbor exists. Front/back keep the full painting family.
- The neighbor that was stepped over gets a 600ms accent flash, drawn in the
  studio overlay layer (never in the iframe DOM), so the action shows its
  work.
2026-07-13 16:48:52 -07:00
ukimsanov 84963ea8ba fix(studio): persist canvas z-order actions correctly for static elements
An adversarial review of the canvas context-menu z-order pipeline (Bring to
Front / Forward / Backward / Send to Back) found the resolver math sound but
the glue between the menu and the commit hook broken:

- The menu optimistically wrote style.zIndex AND position: relative to the
  live elements BEFORE the commit hook ran. The hook decides whether to
  persist position by checking getComputedStyle(el).position === 'static' —
  always false after the pre-apply — so the position patch was never
  persisted on the menu path and the reorder silently reverted at the
  post-commit reload for any nested/static element (root clips survive only
  because the runtime forces position:absolute). The same pre-apply made the
  failure rollback capture the already-mutated values, restoring the broken
  state on persist errors. The menu no longer pre-applies; the hook owns the
  live writes (it already applied both synchronously) and now sees true
  priors. Siblings without a persistable identity still get their z applied
  live-only so a renumber stays visually coherent.
- The commit hook's entry.key store-sync plumbing had zero production
  callers; the store zIndex went stale until full reload. All three callers
  (canvas menu via PreviewOverlays, timeline lane z-sync, LayersPanel) now
  derive and pass the timeline store key (new deriveTimelineStoreKey helper).
- patchElementBatch discarded the server's per-patch matched[]; unresolvable
  siblings persisted partially and silently. Unmatched targets now warn and
  report save-failure telemetry (z-reorder-unmatched) without rolling back
  the matched subset.
- template/noscript elements counted as painting siblings, so renumber
  fallbacks wrote z-index/position into <template> tags in the source file.
  Excluded from the sibling family.
- The default undo coalesce key merged DISTINCT z actions within 300ms into
  one undo entry; the action kind is now part of the key (LayersPanel drags
  keep coalescing within a drag; explicit lane-move gesture keys untouched).
- rectsIntersect comment claimed touching rects intersect; the strict
  inequalities say otherwise — comment fixed.
2026-07-13 16:48:52 -07:00
ukimsanov 19139b91ed fix(studio): make vertical lane moves persist correctly and harden the z/lane pipeline
Vertical clip moves committed in the store but never survived: two persist
bugs plus a runtime renumber all fought the stable-track-lanes model.

- timelineMoveAdapter deliberately stripped the track from lane-reorder
  persists ('z-only reorder path' — the old z-driven lane model). Lane =
  authored data-track-index now: lane-reorder and track-insert both persist
  the track; plain timing moves omit it to stay SDK-fast-path eligible.
- Display lanes and file tracks are different coordinate spaces:
  normalizeToZones packs sparse authored tracks (1,2,... or gaps, or DOM-index
  fallbacks) onto contiguous display lanes, and lane edits persisted the LANE
  number — silently re-targeting the wrong row in any non-0-contiguous file.
  Elements now record their authoredTrack when remapped; a lane change
  persists the target lane's authored track (store stays in lane space).
- The runtime split same-track clips of different kinds (video vs caption
  div) onto separate renumbered tracks at discovery, so authored indices
  never round-tripped ('drop onto an existing track' bounced back). Removed:
  data-track-index is honored verbatim (render never reads it); kind-based
  row presentation belongs in the display layer if ever wanted.

Adversarial review fixes on the same pipeline:
- runtime: parseInt(attr) || fallback dropped authored track 0 for GSAP and
  overlay clips (parseAuthoredTrack helper honors 0)
- single-clip move fallback persisted only data-start — lane changes snapped
  back on reload (now passes the track to the patch builder)
- lane-change z-sync candidate ignored a multi-selection's time shift, so
  patches were computed against stale overlap sets
- track insert around a locked clip persisted a colliding renumber (the next
  normalize merged lanes); the insert is now refused with a warning
- computeStackingPatches compared leaf z across CSS stacking contexts, where
  ancestor z decides paint order; the sync now partitions by
  stackingContextId and never patches across contexts

Timeline geometry (user-reported):
- fit zoom leaves 20% trailing headroom (FIT_ZOOM_HEADROOM in
  timelineLayout.ts; single fit-pps source, so ruler/lanes/playhead/drag all
  inherit it)
- playhead line center now sits exactly on GUTTER + t*pps at every zoom
  (wrapper had shrink-wrapped to the 9px diamond, off-centering the line);
  ruler ticks center on their timestamp
- ruler: frame-mode steps snap to whole frames (no duplicate labels), hour
  steps added for far zoom-out, tick positions computed as exact multiples
  (no float drift)
2026-07-13 16:48:52 -07:00
ukimsanov 54f41b41b6 fix(studio): restore golden-branch timeline behaviors dropped by the stack rebuild
The Studio stack rebuild (#2291) landed the remaining NLE layers but dropped
or regressed several final-wave behaviors from the reviewed studio-dnd stack,
and never repaired the stale timelineZones.ts that #2279 introduced. Restores:

- TimelineRuler: sticky under vertical scroll, full-height gridlines removed
  (beat lines only), frame-number tick labels via a persisted timeDisplayMode
  store preference (PlayerControls toggle now store-backed)
- timelineZones: stable track lanes — lane = authored data-track-index
  ascending; z is paint order only (replaces the stale z-driven lane pack,
  which broke track insert-band commits that contractually depend on it)
- persistTimelineBatchEdit: a batch member whose patch is a no-op (attributes
  already at target values, e.g. in a track-insert renumber) is skipped
  instead of aborting and rolling back the whole batch — this alone made
  new-track creation (incl. the top insert band) fail silently
- useTimelineStackingSync: unresolvable clips read as NaN again so
  timelineStackingSync's Number.isFinite exclusion contract holds (z=0
  fabrications skewed stacking boundaries)
- timelineAssetDrop: drops land on the drop track (no overlap bump to
  max-track+1), data-hf-id stamped, audio gets data-volume
- timing edits: soft-reload the server's rewritten GSAP script instead of a
  full iframe remount (no all-clips flash on move/resize); full reload only
  when no scriptText or the soft path can't apply, and one full reload when a
  group edit touches non-active files (new hooks/timelineTimingSync.ts)
- duration: content-driven grow-AND-shrink on move/resize/delete, synced
  optimistically to the store and the live root data-duration at release
  (was a grow-only ratchet; shrink never updated the readout)

New UX: sidebar asset click opens a compact non-modal preview over the canvas
(dismiss on outside click, Escape, playback, or seek), and clicking an
already-added asset reveals its clip in the timeline (smooth minimal scroll
to its time and lane; vertical-only in fit zoom).

Verified by pointer-driving a real project: sticky ruler + gridline removal,
no iframe remount on move/resize (marker survives, GSAP tween positions
rewritten in place), duration readout 40->37->40 on shrink/stretch, and
top-insert-band track creation renumbering lanes correctly on disk.
2026-07-13 16:48:51 -07:00
Vance Ingalls d04bcbf7c4 Merge pull request #2360 from heygen-com/via/issue-1-webm-alpha-probe
fix(cli): detect WebM alpha lost when ALPHA_MODE tag is written without side data
2026-07-13 16:44:57 -07:00
Vance Ingalls c3b0b91feb chore(fallow): allow sampledAlphaIsFullyOpaque test-only export
Matches the isPathInside pattern in fileServer.ts. The helper is called from
webmAlphaCheck.ts's own webmAlphaAdvisory (same file), but fallow doesn't
count intra-file consumption or test-file imports. Suppress the false
positive rather than dropping the direct unit test — the 1/2/3-frame
byte-count gate + per-frame stride logic is load-bearing enough to warrant
independent tests, not just coverage through the outer advisory function.
2026-07-13 23:21:18 +00:00
Vance Ingalls c6da79cbdb Merge pull request #2358 from heygen-com/07-13-fix_core_cli_attribute_figma_rest_failures_to_the_endpoint_that_failed
fix(core,cli): attribute figma REST failures to the endpoint that failed
2026-07-13 16:16:24 -07:00
Vance Ingalls 2e44602f99 fix(engine,producer): apply software-GPU screenshot invariant at concrete-resolved point
Addresses Miguel's R1 blockers:

1. `browserGpuMode: "auto"` that runtime-probes to software slipped past the
   `resolveConfig` clamp — that clamp only sees the pre-resolve string. Add
   `shouldClampToScreenshotForConcreteGpu(resolvedGpuMode, currentForceScreenshot, env)`
   in `packages/engine/src/config.ts` and apply it at BOTH concrete-resolution
   sites:
   - `packages/engine/src/services/frameCapture.ts`: downgrades `preMode`
     from "beginframe" to "screenshot" when resolved GPU is software (respects
     `PRODUCER_FORCE_SCREENSHOT=false` env opt-out), fixing the routing.
   - `packages/producer/src/services/renderOrchestrator.ts`: updates
     `captureObservability.forceScreenshot` (and thus `captureMode`) at the
     same call site, fixing the observability truth on the auto → software
     case.

2. New unit tests in `config.test.ts`:
   - Documents the auto-branch gap (resolveConfig leaves auto as
     forceScreenshot=false — the runtime companion closes it).
   - 5 branch tests on `shouldClampToScreenshotForConcreteGpu` covering
     software / hardware / already-forced / env-opt-out / non-"false" env
     values.
   Full suite: 56/56 pass.

Scope narrowing on Blocker 2: the distributed rendering path at
`packages/producer/src/services/distributed/plan.ts:753-754` and
`renderChunk.ts:462-466` explicitly hardcodes `browserGpuMode:"software",
forceScreenshot:false` post-resolveConfig and stays outside this PR's
invariant boundary. `compileStage` may still flip it to true for alpha
formats, but generic MP4 distributed renders on SwiftShader hosts remain
BeginFrame. That's a separate architectural cleanup (needs its own
behavior-change trace); the PR body now scopes the invariant to the
in-process CLI/orchestrator path.
2026-07-13 23:13:30 +00:00
James Russo cb69d3fe00 fix(render): surface structured outcomes (#2153) 2026-07-13 19:07:39 -04:00
Miguel Ángel 6892b62662 docs(media-use): drop the HeyGen generative use-cases table (#2370)
Keep the image-to-video recipe; remove the broader capability table (photo
avatar, digital twin, cinematic, translate, lipsync, ai-clipping, voice). It
drifted toward the API-reference surface OP1 says media-use shouldn't carry, and
--request-schema already self-documents each command. Fix the now-dangling
'(below)' pointer in the image-to-video note.
2026-07-13 19:02:35 -04:00
Miguel Ángel 746b6d5b75 feat(cli): tag HeyGen API calls with X-HeyGen-Client-Source (#2368)
Send `X-HeyGen-Client-Source: hyperframes` from buildAuthHeaders on every
HeyGen API call (both OAuth and API-key), so backend billing meta can
isolate hyperframes CLI usage. The single buildAuthHeaders chokepoint covers
the core CLI + cloud client. Mirrors the media-use tagging; the OAuth-only
X-HeyGen-Source cli free-gate header is unchanged.
2026-07-13 19:02:22 -04:00
Miguel Ángel 78b9a814d5 docs(skills): add cloud render + variables to CLI skill, media-use generative use cases (#2356)
* docs(skills): add cloud render + variables to CLI skill, media-use generative use cases

The hyperframes-cli skill only documented self-managed AWS Lambda rendering; the
zero-infra HeyGen-hosted `cloud render` path (a real, shipped command with its
own docs page) was absent from every skill, so agents never surfaced it.

- hyperframes-cli: add `cloud` to the frontmatter verb list + entry point; new
  Cloud and Variables sections; new references/cloud.md distilled from
  docs/deploy/cloud.mdx; routing + workflow rows.
- media-use: add image-to-video recipe (heygen video create type:image) plus a
  table of other HeyGen generative use cases (photo avatar, digital twin, video
  translation, lipsync, voice design) in references/operations.md; surface them
  in the SKILL coverage/provider rows.
- Sync README + docs/guides/skills.mdx catalog entries to mention cloud render.

* docs(media-use): point HeyGen generative use cases at --request-schema

Verified against the installed heygen CLI (v0.3.0): no capability gap that would
need the raw API. Replace hardcoded body-field lists with a pointer to
`heygen video create --request-schema` (self-documenting, can't rot), correct
the image-to-video motion_prompt/expressiveness support, and add the
cinematic_avatar, ai-clipping, and photo-avatar creation paths.

* fix(skills): correct media-use manifest hash (clean-tree regen)

The prior regen was polluted by the gitignored skills/media-use/eval-report.html
(a suppressed mv error left it present), so the committed media-use hash didn't
match a clean checkout. Regenerate with no untracked artifacts present.
2026-07-13 18:53:19 -04:00
Xuanru Li 7f4eaeb568 feat(cli): coordinate-frame layout findings in check (#2354)
* feat(cli): coordinate-frame layout findings in check

Four production compositions shipped with 100-600px layout drift, each a
different coordinate-frame confusion the check graded info or missed
entirely: viewport pixels written as container left/top, gsap x/y
treated as absolute position, a -350px margin fighting flex centering,
and stage-relative path coords drawn into a nested SVG.

Three new layout findings close the class:
- positioned_out_of_parent: an absolute/fixed element rendering mostly
  outside its positioning ancestor (warning) — the parent needs no
  overflow clipping, which is what let container_overflow miss it.
- box_out_of_canvas: a painted panel breaching the canvas (warning) —
  text is canvas_overflow's, media is frame_out_of_frame's, painted
  boxes were nobody's.
- connector_detached: a connector path whose endpoints land far from
  every anchorable element (warning) — measured coordinates drawn into
  an SVG with a different origin.

canvas_overflow additionally promotes from info to warning when held
across samples AND the breach exceeds 5% of the canvas.

All three are persistence-tiered and respect data-layout-allow-overflow.
Verified against the four incident compositions: every one now surfaces
its drift as held warnings (previously: info or silence).

* fix(cli): harden coordinate-frame findings against review false positives

Reworks all three findings after two-lens review (adversarial FP hunt in
real Chrome + maintainer pass):

- escaped_container (was positioned_out_of_parent): uses offsetParent
  (transform-aware, skips fixed-as-canvas), exempts fully-detached
  callouts within an attachment allowance while still flagging
  touching-but-mostly-outside drift.
- panel_out_of_canvas (was box_out_of_canvas): paint alone qualifies
  (flat solid panels were a false negative), fully off-canvas rects are
  parked entrances and stay silent, pointer-events:none marks decorative
  layers, hero-sized breaches warn while small bleeds stay info.
- connector_detached: endpoints via getPointAtLength + getScreenCTM
  (viewBox, preserveAspectRatio, group transforms, every command type),
  defs/marker/clipPath subtrees skipped, word-boundary connector naming,
  containment tier limited to opaque non-ancestor targets (a text-bearing
  wrapper contains its own diagram's endpoints).
- canvas_overflow promotion requires partial visibility — a fully
  off-canvas rect is a parked entrance, not drift.

Verified: the four incident compositions still surface their drift as
held warnings; the review's false-positive repros (fixed HUD, callout,
parked entrance, corner bleed, marker arrowheads, g-transform and
viewBox-scaled connectors) are clean at warning level. Docs and the CLI
skill reference now describe the coordinate-frame findings.

* fix(cli): panel ownership is geometric — direct-text panels were a silent false negative

A painted panel whose direct text stays in-bounds while its box breaches
the canvas produced neither finding: canvas_overflow measures the text
range and panel_out_of_canvas skipped every own-text element. Skip the
panel finding only when the element's own text ALSO breaches (that
geometry belongs to canvas_overflow); pin the message/fixHint wording of
all three findings with positive assertions; document the SVG-internal
anchor blind spot.

* fix(cli): classify panel decoration by paint kind, not pointer-events

pointer-events:none exempted the framed-painting incident's gold frame
layers — hero content that happens to disable hit-testing. Decoration is
now gradient-only paint (spotlights, textures, vignettes); url() images,
solid fills and borders are content regardless of pointer-events.

* fix(cli): add fixHint to the test-local AuditIssue shape

* fix(cli): gradient stops decide content vs decoration; ownership matches canvas_overflow's tolerance

A gradient with any solid stop (alpha >= 0.6) is content — heroes and
cards painted with linear-gradient were invisible under the blanket
gradient exemption; all-translucent stops (spotlights, vignettes) stay
decoration. The text-ownership check now uses the audit tolerance that
canvas_overflow itself fires at, making the contract strict-mutex: any
text breach past that tolerance cedes the element, so a shallow 20px
text breach no longer double-reports.
2026-07-13 15:35:03 -07:00
Miguel Ángel 8e50e8477f fix(cli): skip contrast for intentionally covered text (#2366) 2026-07-13 18:22:30 -04:00
Vance Ingalls 56dcc4e47a fix(cli): accept 1–3-frame short WebMs + add direct pixel-probe tests
Address Miguel's R1 blocker + Rames/Miga's testability nit:

- The `-frames:v 3` decode samples AT MOST 3 frames; a legitimate 1- or
  2-frame WebM (256 or 512 bytes) was returned as `undefined` (probe
  failure), silently skipping the advisory even when every available
  pixel was opaque. Accept any positive whole-frame byte count ≤ 768
  (multiples of 256), distinguishing successful short-EOF from partial/
  malformed decode.
- Export `sampledAlphaIsFullyOpaque` and add 11 direct tests covering:
  3/2/1-frame opaque decodes → true; transparent pixel at pos 0 or
  final byte → false (guards the alpha-byte stride); non-frame-multiple
  / over-3-frame / zero byte counts → undefined; execFileSync throw →
  undefined; findFFmpeg missing → undefined; and one args-shape guard
  pinning the load-bearing `-c:v libvpx-vp9` before `-i` (without which
  the default decoder silently discards VP9 alpha and the whole check
  would false-positive on genuinely-transparent WebMs).
- Update advisory wording from "3 sampled decoded frames" to "every
  sampled decoded pixel" so the message is honest for short WebMs.

18/18 tests pass locally under `vitest run`.
2026-07-13 22:03:11 +00:00
Miguel Ángel 74fb4dbcc3 docs(cli): sync SRT upload docs from EF (#2364) 2026-07-13 17:51:25 -04:00
Vance Ingalls bd8ee05ae2 fix(cli): label the batch asset-import RENDER_FAILED with the images endpoint
Review feedback on #2358: the batch-miss RENDER_FAILED in
runAssetImportMany (asset.ts) throws the same typed error as
client.ts's single-node renderNode, but wasn't labeled — so
cli_error.endpoint would silently come back undefined for the
flow that most heavily exercises /v1/images.
2026-07-13 21:41:55 +00:00
Vance Ingalls 4ca1552e20 fix(core,cli): attribute figma REST failures to the endpoint that failed
cli_error had no way to tell which figma REST call (images, files_nodes,
variables_local, styles, ...) actually hit RATE_LIMITED/FORBIDDEN/etc, so
the dashboard could see failures spike but not which call caused them.

FigmaClientError now carries a low-cardinality endpoint label (never the
raw fileKey/nodeId), threaded through to cli_error's endpoint property.
2026-07-13 21:41:55 +00:00
Vance Ingalls b8562c91d5 fix(cli): detect WebM alpha lost when the ALPHA_MODE tag is written without side data
Extends webmAlphaCheck.ts (from #2044) with a pixel-level decode probe.
After the tag check passes, decodes 3 sampled frames via
`ffmpeg -c:v libvpx-vp9 -pix_fmt rgba -f rawvideo` at 8x8 and emits a
distinct advisory if every alpha byte reads 255.

#2044 detects the "tag absent" failure mode (ffprobe shows no
`alpha_mode` in stream tags). It doesn't catch a stricter case reported
on CLI 0.7.56 / Windows 11: ALPHA_MODE=1 present but BlockAdditional
alpha side data empty. Under current logic webmAlphaAdvisory sees
`alphaMode: true` and stays silent, so the render ships as opaque
without any signal.

The -metadata:s:v:0 alpha_mode=1 push is a muxer directive that some
ffmpeg builds write unconditionally, independent of whether libvpx-vp9
emitted the alpha plane. Tag presence is necessary but not sufficient
evidence of preserved alpha.

Advisory text names both possibilities (opaque composition OR silent
alpha drop) plus the concrete workaround (png-sequence + prores repack).
Fast path (no tag or missing tag) is unchanged. Probe adds ~1s per
WebM render only when the tag says alpha.
2026-07-13 21:38:12 +00:00
Vance Ingalls ba5168293f fix(engine): software-GPU browsers imply screenshot capture
When `browserGpuMode === "software"`, set `forceScreenshot = true` in
`resolveConfig`. Explicit opt-outs (`PRODUCER_FORCE_SCREENSHOT=false`
or `overrides.forceScreenshot === false`) are honored.

This is defense-in-depth on top of the existing platform gates:

1. Linux + software (SwiftShader host) skips BeginFrame, avoiding the
   compositor stall on shader-heavy frames under CPU raster (same
   motivation as the closed PR #822).
2. `renderOrchestrator`'s reported `captureMode` field is derived from
   `cfg.forceScreenshot ? "screenshot" : "beginframe"` — without this
   clamp it misreports `"beginframe"` for the actual screenshot capture
   on darwin + software.
3. Any new BeginFrame or drawElement entry point that forgets to gate
   on GPU mode still routes to screenshot here.

Does NOT fix SwiftShader-on-darwin text-rasterization artifacts (an
ANGLE-SwiftShader issue on macOS text — the fix there is to use
`--browser-gpu`, which routes to `--use-angle=metal`).
2026-07-13 21:38:10 +00:00
Vance Ingalls 9b71bc2103 Merge pull request #2361 from heygen-com/via/flake-fix-manager-progress-notice
test(cli): stabilize flaky install-lock wait-notice test with fake timers
2026-07-13 14:37:40 -07:00
Vance Ingalls e47af77c5a test(cli): stabilize flaky install-lock wait-notice test with fake timers
Adopt the fake-timer pattern the sibling "recovers when a crashed reclaimer
leaves both lock directories" test in the same file already uses. Without
fake timers, if the dynamic `import("./manager.js")` beat between
`installFsMocks({ initialMtimeMs: Date.now() })` and the `withInstallLock`
call exceeds `staleMs` (50 ms on a busy shared runner with `vi.resetModules()`
per beforeEach), the new immediate-stale short-circuit added in #2328 fires
on iteration 1, breaks out before `waitedMs` reaches `waitNoticeMs=20`, and
no "Waiting for another hyperframes process" warn ever emits — the assertion
at `manager.test.ts:428` (`expected false to be true`) then fails.

Under fake timers, `Date.now()` is frozen at the mtime seed, so the lock
stays non-stale across the dynamic import and the wait-notice branch
observes real polling; `vi.advanceTimersByTimeAsync(staleMs + pollMs * 5)`
then drives the loop past both the wait-notice threshold and the stale
deadline so the reclaim + acquisition still resolves.

Test-only change; no production-code diff. Verified 27/27 in
`packages/cli/src/browser/manager.test.ts` under `vitest run`.
2026-07-13 21:30:44 +00:00
Xuanru LiandClaude Fable 5 3e3b37d37f fix(cli): occlusion-probe false positives — pointer-events blindness, low-alpha gradients, not-yet-entered text (#2357)
* fix(cli): three occlusion-probe false-positive sources in text_occluded

- pointer-events:none text is invisible to elementFromPoint, so the probe
  always hit whatever paints beneath and misread visible text as buried;
  restore hit-testing on the element for the duration of the probe
- a backgroundImage counted as opaque regardless of alpha, so a 4%-alpha
  grid/scrim gradient qualified as an occluder; gradients now occlude only
  when their colours reach alpha > 0.6 (url() images unchanged)
- a visible container whose every text-bearing descendant is still at
  opacity 0 (entrance not started) was probed anyway; skip when no text
  ink is on screen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): address review — document-wide hit-testing restore, gradient compositing, whitespace ink

- restore hit-testing for ALL pointer-events:none elements during the text
  audit pass (not just the probed text): an occluder that itself carries
  pointer-events:none is invisible to elementFromPoint, which made truly
  buried text read as clean once the text alone became hittable
- hasVisibleTextInk ignores whitespace-only text nodes (indented markup
  defeated the gate) and uses a 0.05 floor so mid-fade text keeps its
  persistence occurrences
- hasOpaqueBackground composites gradient alpha with background-color
  (two 0.5-alpha layers paint at ~0.75); gradientMaxAlpha returns opaque
  for any colour function it cannot score (oklch/lab/...); percentage
  alpha values now parse as fractions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): walk the elementsFromPoint stack in occluderAt

A transparent layer that becomes hittable (pointer-events restored) must
not mask an opaque occluder painting beneath it — single-point
elementFromPoint returned the transparent top and dropped two genuinely
buried cases in the census acceptance run; the stack walk keeps 10/10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): composite stacked background-image layers when judging occluder opacity

Two 0.5-alpha gradient layers paint at 0.75 combined; taking the max
color-stop alpha across the whole declaration under-counted them and
suppressed real text_occluded findings. Split layers at top-level commas
(paren-aware), score each, composite as 1-prod(1-a_i). Also pins the
0.05 text-ink floor with a boundary test (review feedback on #2357).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): keep walking the occlusion stack past pair-specific exemptions

sharedPreserve3d and isCrossSceneTransitionOverlap excuse one hit, not
the whole probe; returning null let a transparent decorative layer in
the text's 3D context mask a real occluder below it (review feedback).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:27:39 -07:00
Miguel Ángel 3df59fc0a4 fix(engine): support current FFmpeg filter scripts (#2324) 2026-07-13 16:52:15 -04:00
Miguel Ángel 81a6edcb67 fix(browser): recover abandoned install locks (#2328) 2026-07-13 16:52:04 -04:00
Miguel Ángel 5ff4ba13f4 fix(render): retry explicit parallel capture timeouts (#2331) 2026-07-13 16:51:58 -04:00
James Russo e523a29f59 fix(player): own connection and media resources (#2152)
## What

Make Player connection resources symmetric and scope slideshow media ownership.

## Why

Reconnect could retain destroyed handles, while slideshow operations scanned and mutated unrelated document media.

## How

Null and recreate connection-owned resources idempotently and introduce an OwnedMediaRegistry with abortable cleanup.

## Test plan

- [x] Player reconnect, media-scope, slideshow, and owned-media registry tests
- [x] Stack-wide lint, format, build, typecheck, and relevant integration gates
2026-07-13 16:15:06 -04:00
Miguel Ángel 648e4e8c54 fix(cli): declare Apache-2.0 license (#2351) 2026-07-13 14:56:51 -04:00
James 16942c0c12 fix(player): own connection and media resources 2026-07-13 14:34:54 -04:00
James Russo 242cb3234e Merge pull request #2151 from heygen-com/07-10-fix_core_preserve_runtime_transport_contract
fix(core): preserve runtime transport contract
2026-07-13 14:34:20 -04:00
James cc3ca2f9f7 fix(core): preserve runtime transport contract 2026-07-13 14:08:52 -04:00
James Russo a364f69568 fix(player): version runtime protocol (#2150)
## What

Version the Player/runtime protocol and pin injected core runtime code to the Player release.

## Why

An unversioned CDN runtime and untyped messages allowed silent host/runtime drift and ambiguous frame timing.

## How

Define RuntimeProtocolV1 with capabilities and rational fps, generate the versioned runtime URL, validate major versions, and retain a tested legacy fallback.

## Test plan

- [x] Player, Core, and Studio protocol tests; Player build-time runtime-pin verification
- [x] Stack-wide lint, format, build, typecheck, and relevant integration gates
2026-07-13 14:08:17 -04:00
James dcefdd98ca fix(player): version runtime protocol 2026-07-13 13:28:11 -04:00