Commit Graph
100 Commits
Author SHA1 Message Date
Vance IngallsandClaude Opus 5 23854f7c6a feat(producer): surface init telemetry from parallel workers — the band's missing motion axis
The routing surface the short-comp benchmarks validated is (motion x DOM
size x frames). After the baseline release, fleet telemetry carries DOM
size (composition_element_count) and frames on every render — but the
motion proxy, observability_init_tween_count, has 0% coverage on the
exact renders the band routes: parallel workers' console buffers (and so
the [FrameCapture:INIT] line the summary parses) only propagate to the
orchestrator on FAILURE. Single-worker screenshot renders report it;
the multi-worker clamp bucket never does. Verified against 7d of fleet
data: 35k screenshot renders carry tween counts, 0 of 9,600 band renders.

Fix rides the one channel parallel workers already return on success —
the per-worker CapturePerfSummary. Sessions record initTelemetry on
every init path; the perf summary now carries it; the orchestrator
max-merges across workers (same multi-session semantics the console
parser uses) and feeds it to the observability summary as a structured
fallback, console lines still refining when present.

With this, every band render carries full coordinates — (elements,
tweens, frames, path, speed) — which buys two reads: regressing wild DE
speed against element count on the existing 900+ inversions validates
the bench's 0.50ms/element slope BEFORE the routing flip, and any
post-flip misroute can be reproduced locally by feeding its telemetry
row straight into gen-crossover-comp's knobs (--movers ~ tween count,
--static ~ element count) and re-benching.

(Also drops a now-stale fallow suppression in render.ts — the test-only
reset export it guarded gained real test importers, so the issue it
suppressed no longer exists and the gate flags the leftover.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 10:54:24 -07:00
Vance IngallsandClaude Opus 5 e9de2fa14f refactor(producer): make the short-comp band baseline-first and its attribution decisive
Recut after pre-registering the read exposed two flaws in the first cut:

1. Attribution was wrong. de_short_band keyed on frame count + element
   ceiling alone, so a webm render, a compile-gated comp, or a forced
   screenshot at 400f reported "applied" while its routing was untouched —
   poisoning the measurement cohort with unaffected renders and diluting
   any effect toward zero. Now the predicate is evaluated twice (900 floor
   vs band floor) and the band is DECISIVE only when the calls disagree:
   every other eligibility condition passed and only the floor differed.
   The cohort contains exactly the renders whose routing the band decides.

2. A same-release flip is unfalsifiable. composition_element_count ships
   WITH the routing change, so the before-period cannot be filtered to the
   same cohort as the after-period — the comparison would show a speedup
   even if the change did nothing (the after-cohort excludes big comps by
   construction; the before-cohort includes them). Routing is therefore
   gated behind HF_DE_SHORT_BAND_ROUTE, default OFF: this release computes
   and emits the full band decision on every render ("applied" is the
   counterfactual "would have inverted"), a follow-up flips the default.
   Identical cohort selector on both sides of the boundary, and the
   skipped/oversize renders in the same frame band form a concurrent
   control — a difference-in-differences that absorbs secular drift
   (content mix, version-correlated populations, hardware), which a plain
   before/after cannot.

Also: countElementTags now counts HTML void elements. Counting only
closers read an image gallery as a tiny comp and opened the band on
exactly the content most likely to lose it (images skew expensive to
paint). Opening tags stay uncounted — inline scripts' `a < b` would
false-positive. Counter semantics are frozen while the baseline is read:
the distribution the baseline release records must be measured by the
same counter that later gates.

Revert-rate baseline for the pre-registered read, measured over 14d
fleet-wide: the 900+ inversion runs 31,756 inverted / 1,705 reverted =
5.1%. At the benched 1.16-1.24x win and ~1.8x revert cost, expected net
for the band is ~12%. Kill criteria for the flip release: DiD <= 0,
in-band revert rate > 5.1% baseline, or band fallback rate > DE baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 10:02:05 -07:00
Vance IngallsandClaude Opus 5 0749cd9ff8 feat(producer): open the DE single-worker inversion to short comps under an element ceiling
31% of fleet renders (24h, v0.7.78+) are DE-eligible comps clamped to
parallel screenshot purely because they sit under the 900-frame inversion
floor — the median fleet render is ~250-600 frames, below every DE entry
threshold. This opens a 250-899 frame band, gated on composition size.

Measured, not assumed. A controlled sweep (fixed synthetic content,
{250,400,600,900}f, single-DE vs parallel-screenshot-W4, 3 reps, capture
mode verified per row, AC power, load-gated) showed single-DE winning
1.16-1.24x at every size — but only for content in constant motion. A
follow-up 2x2 found motion and DOM size pull in OPPOSITE directions, so
neither alone predicts the winner (ratio = ss4/de1, >1 means DE wins):

     24 movers /     0 nodes -> 1.05
    320 movers /     0 nodes -> 1.24
    320 movers /  7000 nodes -> 1.09
     24 movers /  7000 nodes -> 0.96
     24 movers / 20000 nodes -> 0.71
     24 movers / 40000 nodes -> 0.55

DE's wall-clock scales ~0.50ms/element against parallel screenshot's
~0.22ms — drawElement repaints the whole tree per frame while fan-out
amortizes it — so the downside is NOT bounded and a bare floor drop would
have handed a 1.8x regression to large comps. Since motion only ever helps
DE, an element ceiling calibrated at the lowest-motion case is safe at
every motion level; crossover there is ~3.9k, and the default sits at 2500.

The predicate is untouched; the call site picks the floor. Above the
ceiling, or at 900+ frames, behaviour is bit-identical to today — the
change can only add inversions in the new band, never remove one.

Instrumentation, since this ships at full exposure rather than cohorted:
`composition_element_count` on EVERY render (the fleet distribution of the
gate variable is unknown — without it we cannot tell whether 2500 opens the
band for most short comps or almost none, nor re-derive the threshold from
real content), and `de_short_band` = applied | skipped_elements, unset when
the frame count made the band irrelevant, so a fleet perf shift is
attributable to this change rather than to content mix.

Safety is unchanged and already proven on this path: per-frame PSNR
self-verify with screenshot fallback, exactly as the 900+ band has shipped
default-on. Knobs: HF_DE_SHORT_MIN_FRAMES, HF_DE_SHORT_MAX_ELEMENTS (0
disables the band).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 02:36:40 -07:00
Vance Ingalls c691869e22 chore: release v0.7.78 2026-07-28 05:27:29 -07:00
Vance Ingalls a557aea1f7 Merge pull request #2838 from heygen-com/07-27-feat_producer_lower_parallel-de_router_floor_to_700_frames_power-state_telemetry
feat(producer): lower parallel-DE router floor to 700 frames + power-state telemetry
2026-07-28 05:24:18 -07:00
Vance Ingalls 011f46bc18 Merge remote-tracking branch 'origin/main' into 07-27-feat_producer_lower_parallel-de_router_floor_to_700_frames_power-state_telemetry
# Conflicts:
#	packages/cli/src/telemetry/events.ts
2026-07-28 05:01:24 -07:00
Vance Ingalls 59b259d039 Merge pull request #2841 from heygen-com/07-27-feat_engine_open_drawelement_fast_capture_to_win32_hardware_gpu
feat(engine): open drawElement fast capture to Windows hardware GPU
2026-07-28 04:47:07 -07:00
Vance IngallsandClaude Opus 5 cddc90ae37 fix(cli): add required gpu prop to power-state test calls
trackRenderComplete requires `gpu: boolean`; the two new opt-out test
calls omitted it, failing Typecheck in CI. The fix already existed on the
stacked branch, so only this base branch was broken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 01:29:58 -07:00
Vance IngallsandClaude Opus 5 b99c803898 fix(producer): guard pmset behind shouldTrack + don't pin workers without streaming (review)
Two review findings on the floor/telemetry PR:

1. powerStateFields() is spread into the properties object at the CALL SITE,
   so it ran before trackEvent's own `if (!shouldTrack()) return` guard —
   telemetry-disabled installs paid two blocking `pmset` subprocess spawns
   per render for an event that was then discarded. Now short-circuits on
   shouldTrack() (memoized, so no cost on the tracked path). Regression test
   asserts pmset is not sampled when telemetry is off; fault-injection
   verified it fails without the guard.

2. The DE parallel router pinned workerCount to 3 and skipped calibration
   even when verified parallel DE STREAMING — the entire reason for the pin
   — could not run for that render. The common case is a composition over
   streamingEncodeMaxDurationSeconds (240 s default): the duration cap
   disables streaming before the router's force flag is consulted, so the
   render got a hard-coded 3 workers chosen by a benchmark for a path it was
   not on, instead of the calibrated count. shouldPreferParallelDrawElement
   now takes parallelStreamingAvailable and withholds the bet without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 00:59:22 -07:00
Vance IngallsandClaude Opus 5 4520cd240b fix(engine): bucket gpu_renderer + cover the failure cohort (review)
Three review findings on the win32 drawElement PR:

1. gpu_renderer shipped the raw UNMASKED_RENDERER_WEBGL string — unbounded,
   driver-authored, GPU-model-specific, and |-joined across parallel
   sessions, i.e. high cardinality by construction, against this file's own
   convention of sanitizing engine-sourced strings (deGateReason is a
   bucket; error messages go through redactTelemetryString). Now bucketed at
   the source by classifyGpuRenderer to <backend>/<vendor>
   (metal/apple, d3d11/nvidia, swiftshader/other, ...), which is the whole
   analytic signal the win32 rollout needs and nothing else. The raw string
   never leaves the engine.

2. gpu_renderer reached render_complete only, so a crashed render — the
   cohort the field exists to attribute — carried no backend. It now rides
   RenderCaptureObservability (deGpuRenderer, sourced from the live probe
   session like the de_* counters), so both render_complete and
   render_error carry it and a hard failure still reports its GPU backend.
   On render_complete the perfSummary value still wins by spread order.

3. Restore the fallow-ignore-next-line suppression above
   __resetDeParallelRouterTrialStateForTests: CLI test files are not fallow
   entry points, so removing it fails the CI dead-code audit (local
   pre-commit passed only because of its changed-file scope).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 00:55:10 -07:00
Vance IngallsandClaude Opus 5 cb30157ebb feat(engine): open drawElement fast capture to Windows hardware GPU
Widen the default-on drawElement clamp from darwin-only to darwin|win32
(still requiring a non-software-GPU browser). The darwin restriction was a
validation envelope, not an architectural limit — the CanvasDrawElement
Chrome flag ships on every platform, and every safety layer that made the
macOS default-on release (v0.7.38) survivable is platform-neutral:
compile-time gates, the SwiftShader init gate, per-render worker-encode
self-verification with screenshot fallback, and the blank guard. Worst case
on an unvalidated D3D11 backend is the same as on Metal: verify catches a
bad frame and the render re-runs on the screenshot baseline.

Why now: 30-day telemetry shows ~206k non-CI hardware-GPU Windows renders
(~78% of the win32 fleet, 18k installs) held on the slow screenshot path by
the clamp — the second-largest perf population after macOS, carrying ~1,550
capture-hours/month in the DE-eligible >=700-frame band alone at a measured
~2x speedup opportunity.

Instrumentation for the new cohort: drawElement session init now records the
raw WebGL UNMASKED_RENDERER_WEBGL string (detectSwiftShader generalized to
detectGpuBackend — same single evaluate, the string was previously read and
discarded) and threads it session -> CapturePerfSummary -> RenderPerfSummary
-> render_complete as `gpu_renderer`. drawElement damage proved
compositor-backend-specific throughout the macOS rollout, so D3D11-cohort
failures must cluster by ANGLE backend + GPU vendor (NVIDIA/AMD/Intel), not
just `os`.

The two DE clamp branches are extracted into a pure, unit-tested
`resolveDefaultDrawElement` (platform + GPU mode + worker-encode + explicit
opt-in), which also drops resolveConfig's cyclomatic complexity. The win32
streaming-encode compound tests collapse onto one shared helper.

Linux stays excluded: that fleet is headless/Docker SwiftShader, where DE
has no speedup and known rendering defects. Kill switches unchanged:
PRODUCER_EXPERIMENTAL_FAST_CAPTURE=false, --experimental-fast-capture=false.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 00:00:21 -07:00
Vance IngallsandClaude Opus 5 3da31e399b feat(producer): lower parallel-DE router floor to 700 frames + power-state telemetry
HF_DE_PARALLEL_MIN_FRAMES default 2000 -> 700, re-calibrated by a controlled
crossover sweep (fixed content-per-frame, three synthetic profiles x
{350..3000f} x {single,par2,par3} x 3 reps, resolved worker counts and capture
modes verified per run): par3 beats single at EVERY size in every profile —
+17-21% at 700f rising to +28-34% at 3000f. That includes a
24-sub-composition profile built specifically to reproduce the 'workers
re-pay init' failure the original 2000 floor guarded against (92k tweens,
~2.5s pollSubCompositionTimelines per worker): workers initialize
concurrently, so duplicated init costs CPU, not wall-clock, and the comp
still parallelizes +19% at 700f. Below ~700f the win thins toward +10%
while paying three hardware-GPU browsers, so a floor remains. par2 loses to
par3 in every cell of every profile — the router's existing 3-worker pin is
confirmed, not changed. Harness:
plans/drawelement-fast-capture/de-crossover-bench.sh (docs repo).

Also adds on_battery / low_power_mode to render_complete and render_error.
The DE fleet is macOS laptops, and bench sweeps on an M4 Pro caught the SAME
render flipping between ~9.6 and ~17.2 ms/frame power-management regimes
with no existing telemetry signal to segment by — the router soak reading
this change needs that dimension to interpret perf on the machines users
actually render on. Sampled per event (volatile), pmset-based, darwin-only,
null-safe on failure.

Router stays default-off behind HF_DE_PARALLEL_ROUTER; this tunes what it
will do when the soak clears it to flip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 22:51:28 -07:00
Vance Ingalls 9d4c3493d0 Merge pull request #2750 from heygen-com/feat/caption-data-driven
feat(registry): make the 5 caption catalog components data-driven
2026-07-24 00:00:44 -07:00
vanceingallsandClaude Fable 5 8bf939043f fix(registry): liberal emoji-pop brand colors, weight-shift fit fixes, 8192 clamp
- caption-emoji-pop: shadowForColor now builds its glow via color-mix()
  instead of hex-pair slicing, so the strict 6-digit brand-color gate is
  gone — any CSS color the sibling templates accept (#fff, rgb(), named)
  now renders instead of silently falling back to the default palette

- caption-weight-shift: fitFontSize now sizes against the WIDEST split
  line rather than the joined group text (two-line groups no longer shrink
  unnecessarily), and avoidSingleWordGroups' merges re-check fitsInTwoLines
  like makeGroups' first pass does (merged groups can no longer overflow
  the split budget)

- all 5: hfApplyStageConfig clamps resolution to the published validator's
  <=8192 bound (validator is optional pre-flight; unbounded stages OOM
  render workers), and fit floors carry a comment documenting that the
  minimum size is returned unverified by design

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 06:10:00 +00:00
vanceingallsandClaude Fable 5 5c2981d066 fix(registry): reject non-numeric caption-data versions; boot fetch never clobbers a manual attach
Two review findings on the caption-data runtime, applied to all 5 templates:
- hfValidate's version gate used Math.floor(Number(v)) > HF_CONTRACT_VERSION,
  and Number("v2") is NaN — NaN comparisons are always false, so malformed
  versions slid through with no unsupported-version signal. An explicit
  Number.isFinite check closes it.
- hfBoot's sibling-fetch .then called hfAttach unconditionally; a manual
  window.__HF_CAPTION_ATTACH__ call landing while the fetch or fonts.ready
  was still pending got clobbered by the late boot payload. Boot now yields
  if a timeline already exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 05:55:21 +00:00
Vance Ingalls 688500f2d6 Merge pull request #2757 from heygen-com/release/v0.7.70
chore: release v0.7.70
2026-07-23 22:40:07 -07:00
vanceingallsandClaude Fable 5 e2846eb7cc fix(registry): encapsulate caption template runtimes in IIFEs
caption-highlight was already IIFE-wrapped; the other four leaked their
runtime (hfAttach/hfBuild/...) as script-scope globals. Harmless when boot
was synchronous, but the data-driven retrofit defers attach behind
fonts.ready + the sibling fetch — with two caption components pasted into
one composition document, every script finishes before any deferred boot
runs, the last script's definitions win the shared scope, and the first
component never registers its timeline (a renderer waiting on it hangs to
timeout). Wrapping each template's script keeps its internals private so
each boot attaches its own component. window.__HF_CAPTION_ATTACH__ remains
intentionally window-scoped (last-defined-wins).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 05:25:41 +00:00
Vance Ingalls b9c3e3a4b6 chore: release v0.7.70 2026-07-23 20:59:30 -07:00
Vance Ingalls fec3bba6b9 Merge pull request #2749 from heygen-com/07-23-fix_engine_verify_disk_path_parallel_drawelement_samples
fix(engine): self-verify parallel disk drawElement samples
2026-07-23 20:35:28 -07:00
Vance Ingalls 5fd28e1149 style: format probe-cleanup retry code (oxfmt) 2026-07-23 18:43:28 -07:00
Via b8e1015476 fix(producer): close orphaned probe session before verify-triggered retries
On a parallel-capture disk-verify or streaming-drain breach, the outer
catch cleared probeSession without first closing the still-owned session,
orphaning the probe Chrome process precisely when the retry was recovering
from GPU/memory pressure. Introduce closeOrphanedProbeForRetry so both
retry catches close the session (with defensive .catch that logs on close
error) before releasing the reference, and cover it with a focused unit
test asserting closure-before-clear and the swallow-and-warn behaviour.

Addresses Magi's REQUEST_CHANGES on #2749; also closes Rames' sibling
concern at the streaming-retry path (renderOrchestrator.ts:3093).

— Via
2026-07-24 01:38:42 +00:00
Vance Ingalls 4889b779c9 fix(engine): lazy-promisify execFile in psnr so partial child_process mocks don't crash at import 2026-07-23 17:57:15 -07:00
vanceingallsandClaude Sonnet 5 4f69947191 fix(registry): clear brand CSS custom properties on unbranded re-attach; remove dead italic path
hfApplyStageConfig (Block B, verbatim across all 5 retrofitted caption
identities) only ever set --hf-caption-primary/--hf-caption-accent when the
corresponding brand.primaryColor/accentColor key was present, with no else
branch to clear it when absent. Re-attaching a brand-less payload after a
branded one left the custom property (and any JS-cached color derived from
it, e.g. caption-pill-karaoke's hfColorActive and caption-emoji-pop's
hfAccentColors) stuck at the stale value instead of reverting to the CSS
fallback, violating idempotent re-attach.

Also removes caption-editorial-emphasis's dead .word--italic CSS rule and
CLASS_MAP.i entry — hfMakeBlocks only ever emits "n"/"e" tags, "i" was only
reachable via the old hand-authored BLOCKS literal this task replaced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 00:03:46 +00:00
Vance Ingalls 92ffd0476d refactor(producer): share DE verify-fallback telemetry mapping across capture paths 2026-07-23 16:52:57 -07:00
Vance Ingalls c85cfae8fa fix(producer): rethrow disk drawElement verify failure past completeness check 2026-07-23 16:06:22 -07:00
vanceingalls 5af6203ae7 fix(caption-weight-shift): remove O(n^2) redundant hide-all-others loop 2026-07-23 22:55:37 +00:00
vanceingalls 18de2b1f1d fix(caption-editorial-emphasis,caption-emoji-pop): remove O(n^2) redundant hide-all-others loop
Both identities looped over every OTHER group/block to force its opacity
to 0 at each group's own start time, in addition to each group already
setting its own opacity to 0 at its own end. Since groups/blocks occupy
non-overlapping time windows and already own their full opacity
lifecycle, that loop was dead weight — but it made timeline construction
O(n^2) in the number of groups/blocks. Under the Task 7 stress transcript
(long, frequent "emphasis" words forcing near single-word blocks/groups),
n reached ~2000 and the page hung well past a 30s test timeout for both
identities.

Verified behavior-preserving: full templates.test.ts (41 tests, incl.
opacity/seek assertions) and the new limits.test.ts stress suite pass
against both identities after the removal.
2026-07-23 22:42:14 +00:00
vanceingallsandClaude Sonnet 5 ed8973952d feat(registry): make caption-editorial-emphasis data-driven with emphasis heuristic
Replaces the hand-authored BLOCKS literal with hfMakeBlocks, a heuristic that
groups words into blocks/lines from timing (pauses, punctuation, max words per
block) and hfIsEmphasisWord (long, non-stopword content words) to decide which
word gets the large Playfair Display emphasis treatment and its own slide-in
line. computeLineSize/buildBlocks/fitBlocks and timeline construction now live
inside hfBuild, closing over layout-scaled font sizes and widths. Adds the
shared Blocks A-E caption-data runtime (attach/gate/brand config) and the
.word/.word--emphasis CSS brand hook for --hf-caption-primary. This is the
last of the five caption identities to go data-driven.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:15:26 +00:00
Vance Ingalls 9fc1c2f159 fix(producer): verify sequential disk drawElement samples too 2026-07-23 15:07:35 -07:00
Vance Ingalls ec791e91d9 fix(producer): screenshot-retry recovery for disk-path drawElement verify failures 2026-07-23 15:00:26 -07:00
vanceingalls 7d4e71d10b feat(registry): make caption-highlight data-driven with automatic grouping
Replaces the hardcoded RAW_GROUPS index-pair array with hfMakeGroups, a
real fitting/pause/punctuation-based grouper, and wires the component
into the shared caption-data runtime (validate/gate/attach). Adds CSS
brand hooks: .hl-word text color from --hf-caption-primary, .hl-word-bg
gradient from --hf-caption-accent (second stop via color-mix()).
2026-07-23 21:52:56 +00:00
Vance Ingalls 060b6f8ae5 fix(engine): self-verify parallel disk drawElement samples (PRINFRA-352) 2026-07-23 14:41:25 -07:00
vanceingallsandClaude Sonnet 5 392a9d251a feat(registry): make caption-emoji-pop data-driven with generic emoji lexicon
Retrofits caption-emoji-pop onto the shared HyperFrames caption-data runtime
(Blocks A-D) and the emphasis heuristic (Block E), replacing the hardcoded
7-word emoji map, fixed KEYWORDS set, and TRANSITION_WORDS set with a
generic ~40-entry lexicon and HF_STOPWORDS/hfIsEmphasisWord. The component
now consumes brand.primaryColor (hfPrimaryColor) and brand.accentColor
(hfAccentColors) and rescales stage/font/emoji sizing to the runtime
resolution via layout.fontScale/scaleX/scaleY.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 21:31:46 +00:00
vanceingalls 020c8986f4 fix(registry): force GSAP render at attach — seek(0) is a no-op on a fresh timeline 2026-07-23 21:01:24 +00:00
vanceingallsandClaude Sonnet 5 08620b75df feat(registry): make caption-pill-karaoke data-driven via caption-data runtime
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 20:46:26 +00:00
vanceingalls c67e9dc1f8 feat(registry): make caption-weight-shift data-driven via caption-data runtime 2026-07-23 20:18:39 +00:00
Vance Ingalls c39f3cf924 Merge pull request #2737 from heygen-com/07-22-fix_hooks_pre-commit_gate_denies_commit_instead_of_ending_the_turn
fix(hooks): pre-commit gate denies instead of ending the turn
2026-07-22 15:45:01 -07:00
Vance IngallsandClaude Opus 4.8 97ec7db5cc fix(hooks): pre-commit gate denies commit instead of ending the turn
The pre-commit build/lint/typecheck PreToolUse hook emitted {continue:false,
stopReason}, which halts the agent's turn — the dev had to type 'continue'
after every failed gate. Switch to a PreToolUse deny decision so the failing
commit is still blocked but the failure text is fed back to the agent as tool
feedback, letting it fix and retry without a manual nudge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 15:35:44 -07:00
Vance Ingalls 84e4eafacd Merge pull request #2723 from heygen-com/07-21-fix_engine_worker_autoscaler_memory_budget
fix(engine): realistic worker memory budget + sizing/feedback telemetry
2026-07-21 21:44:35 -07:00
Vance Ingalls c6462a0a22 fix(producer): emit heap advisory at orchestrator, lock message + telemetry props with tests 2026-07-21 20:39:06 -07:00
Vance Ingalls 12e599a6ba fix(engine): realistic worker memory budget + sizing/feedback telemetry 2026-07-21 14:31:14 -07:00
Vance Ingalls ed32898439 Merge pull request #2681 from heygen-com/07-21-feat_engine_warn_on_live_map_viewports_at_capture_init
feat(engine): warn when a live map viewport is detected at capture init
2026-07-21 14:07:43 -07:00
Vance Ingalls 89d4f63396 Merge pull request #2677 from heygen-com/via/anonymize-cli-feedback-repro
fix(skills): anonymize CLI feedback repro guidance
2026-07-21 13:58:29 -07:00
Vance Ingalls 8f171433f9 Merge pull request #2679 from heygen-com/07-21-fix_engine_carry_framestride_onto_workerresult_fixes_interleaved_worker_false-positive_
fix(engine): carry frameStride onto WorkerResult
2026-07-21 02:10:55 -07:00
Vance Ingalls 30ca51c615 feat(engine): warn when a live map viewport is detected at capture init 2026-07-21 01:08:02 -07:00
Vance Ingalls 4f53dd4f2c fix(engine): carry frameStride onto WorkerResult (fixes interleaved worker false-positive) 2026-07-21 00:26:24 -07:00
ViaandClaude Opus 4.7 78ab9bc889 fix(skills): anonymize CLI feedback repro guidance
The reproduction packet template in hyperframes-cli previously said
"Include the rerunnable command and working directory" and shipped a
`REPRO COMMAND: cd <project path> && ...` skeleton. Agents faithfully
followed both, so user home-directory prefixes (`/home/<user>/projects/...`,
`/Users/<user>/Documents/...`) have been landing verbatim in the public
CLI feedback channel — leaking user + machine identity that maintainers
don't need to reproduce a bug.

Fix, docs-only:

- SKILL.md: change "rerunnable command and working directory" to
  "rerunnable command (relative to the project directory)" plus an
  explicit note that feedback is public and absolute paths must not be
  pasted. Add a matching redaction rule for EXACT ERROR stack traces
  (keep basename+line, drop leading directory).
- references/preview-render.md: replace the `cd <project path>` skeleton
  with a bare `<HF_*/PRODUCER_* env> npx hyperframes <exact command>`
  template + inline comment reminding to run from the project directory
  without pasting absolute paths. Rewrite the "Preserve paths / redact
  secrets" line to lead with the anonymization rule and give concrete
  before/after examples (`./renders/out.mp4` vs
  `/Users/<user>/Documents/…/out.mp4`).

The COMPOSITION_STRUCTURE block was already privacy-preserving (counts
+ presence flags only, "no file paths, no src URLs, no user text") — no
change needed there.

skills-manifest.json regenerated for the hyperframes-cli hash.

Signed-off-by: Via <noreply@anthropic.com>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-21 05:04:18 +00:00
Vance Ingalls 32c41ea7dc Merge pull request #2661 from heygen-com/07-20-staticguard_compiler_recognition_5_facet
fix(lint): recognize compiler-derived data-end as legitimate
2026-07-20 13:23:15 -07:00
ViaandVia ac9f463108 fix(lint): recognize compiler-derived data-end as legitimate
`bundleToSingleHtml` compiles `data-duration` into `data-end` (in
`compileTimingAttrs`), then calls `validateHyperframeHtmlContract` against
the compiled HTML. The linter's `deprecated_data_end` rule fired on the
compiler's own consistent output — `<audio data-start="0" data-duration="18"
data-end="18">` — because `diagnoseDerivedEnd` unconditionally emitted a
`deprecated-end` diagnostic whenever both attributes were present, ignoring
whether the derived value matched.

Reporters routed this as "raw-source lint passes with 0 errors and 0
warnings, but `check --strict` still logs StaticGuard noise about
data-end without data-duration." Field cluster: cli-feedback crons 61-68,
n=25+ across darwin/arm64, darwin/x64, linux/x64, win32/x64, and versions
0.7.56 through 0.7.64. L3 reporter cite (ts=1784519869): "bundleToSingleHtml
compiles data-duration into data-end, then validates the compiled HTML and
reports its own generated data-end as deprecated."

Fix: `diagnoseDerivedEnd` now stays silent when the paired `data-end`
matches `data-start + data-duration` (within a 1ns epsilon to absorb
IEEE-754 residuals like `0.1 + 0.2 = 0.30000000000000004`). Truly-legacy
authoring shapes — `data-end` alone with no `data-duration`, or a
`data-end` that disagrees with `data-duration` — still fire
`deprecated_data_end`, with a refined message that names the drift on the
conflicting variant.

Facets covered: (a) validator treats compiler-derived `data-end` as legal
when paired with `data-duration`, and (e) recognizes the compile-time
rewrite site (`bundleToSingleHtml` → `compileHtml` → `compileTimingAttrs`).
Facets (b) stderr gating, (c) terminal JSON verdict, and (d) audio-not-
dropped are unblocked transitively — StaticGuard's `console.warn` is
already gated on `!isValid` and never drops audio; the check command
already emits JSON on every terminal path; so once the false-positive
diagnostic stops firing on compiler output, the noisy stderr line and the
misleading "check appears to fail" reporter framing both go away without
further wiring.

Co-Authored-By: Via <noreply@heygen.com>
2026-07-20 13:26:51 +00:00
Vance Ingalls c268f5ba85 chore: release v0.7.61 2026-07-17 01:07:09 -07:00
Vance Ingalls f084a7217d Merge pull request #2502 from heygen-com/task4-start-resolver-export
feat(core): export runtime start resolver with documentRef parameter
2026-07-17 00:47:42 -07:00
Vance Ingalls ac51891eb0 Merge pull request #2600 from heygen-com/fix/studio-slideshow-tab-gate
fix(studio): hide Slideshow tab for non-slideshow comps + default the flat inspector on
2026-07-17 00:46:50 -07:00
Vance Ingalls 1ddf9cc331 Merge pull request #2563 from heygen-com/via/thumbnail-id-escape
fix(studio,runtime): CSS.escape ids so digit-leading selectors don't crash
2026-07-17 00:43:31 -07:00
Vance Ingalls a7c0fa4fe5 Merge pull request #2564 from heygen-com/via/win-workdir-env
feat(cli): surface extract-cache dir in doctor + add --frames-cache-dir sugar
2026-07-17 00:43:06 -07:00
Vance Ingalls cf64f24e63 Merge pull request #2569 from heygen-com/via/macos12-dyld-crash-hint
fix(cli): surface HYPERFRAMES_BROWSER_PATH hint on macOS <13 chrome-headless-shell dyld crash
2026-07-17 00:40:14 -07:00
Vance Ingalls 53b3621437 fix(studio): short-circuit slideshow-island detection with a substring check (review N2)
slideshowIslandRegex scanned the full file content on every editingFile
change even for the common non-slideshow case. Gate it behind a plain
substring check on SLIDESHOW_ISLAND_TYPE first — cheap, and avoids the
full-content RegExp pass for files that plainly have no island.

Added a test for the still-open behavior this preserves: a malformed
island (invalid JSON) still trips the substring check and the regex,
so the tab stays discoverable rather than silently disappearing.
2026-07-17 00:37:39 -07:00
Vance Ingalls a4167ede07 feat(studio): default the flat inspector on
Flip STUDIO_FLAT_INSPECTOR_ENABLED's default from false to true — the
bug-fix pass on the flat inspector is complete (right-aligned values,
Stroke width/style split, promote-badge overlap, Layout/Style section
gating for non-visual elements like audio). VITE_STUDIO_FLAT_INSPECTOR_ENABLED=false
still opts back into the legacy panel.

Updates the two tests that asserted the old false default: the flag's
own default test, and the "classic PropertyPanel input coverage" suite,
which relied on that default to reach the legacy panel and now mocks it
explicitly (mirroring the adjacent "flat" suite's existing pattern).
2026-07-17 00:37:39 -07:00
Vance Ingalls 67696cd8de fix(studio): hide the Slideshow tab and panel for non-slideshow compositions
The Slideshow tab rendered unconditionally, showing the branching editor
for any composition regardless of whether it was actually a slideshow —
a plain video comp offered a tab with nothing meaningful to edit.

Gate it on the composition carrying the slideshow JSON island
(<script type="application/hyperframes-slideshow+json">), the same
definitive marker the CLI's `present` command already requires (it
refuses to run without one). Presence-only, not full manifest
validation, so a malformed island still surfaces the tab rather than
disappearing entirely. Also bounce rightPanelTab off "slideshow" to
"renders" if the active composition stops being a slideshow while that
tab is open (e.g. switching files), since its button would otherwise
vanish with no way back to it.

Extracted the gating + scene-list derivation into useSlideshowTabState
to keep StudioRightPanel.tsx under the 600-LOC gate.
2026-07-17 00:37:39 -07:00
Vance Ingalls 8eccc6e9a3 Merge pull request #2599 from heygen-com/via/composition-structure-mandate
feat(skills): add COMPOSITION_STRUCTURE to feedback repro packet + soft-warn lint
2026-07-17 00:36:36 -07:00
Via 490642b78a fix(feedback-lint): tighten census value scoping and defect-keyword matching
Addresses C1-C7 from Rames's adversarial review + N2/N3 nits:

- C1 (positionFixed): inline probe now value-scopes to 'fixed' — previously
  fired on any position value (absolute, relative, sticky), producing a
  false-positive anatomy that would mislead maintainers pattern-matching
  the 'sub-comp + position:fixed capture' bug family.

- C2 (overflowHidden): symmetric fix — inline path now catches
  style='overflow: hidden' via the value-scoped probe, matching the
  <style>-tag branch. Also handles overflow-x/overflow-y variants.

- C3 (VISUAL_DEFECT_KEYWORDS): drop 'render' — CLI's primary command is
  'hyperframes render', so build/perf/hang reports were triggering an
  inappropriate COMPOSITION_STRUCTURE: nudge on the most common failure
  mode. Rely on the more specific tokens (black, blank, flicker, corrupt,
  wrong frame) to identify actual visual defects.

- C4 (mentionsVisualDefect): compile keywords into a word-bounded regex.
  'blackboard', 'blanket', 'visualize', 'corruptible' no longer false-
  positive. Accepted tradeoff: plural forms ('flickers') don't match.

- C5 (marker case-normalization): REPRO COMMAND: / COMPOSITION_STRUCTURE:
  checks now case-insensitive, matching mentionsVisualDefect's
  normalization. Reporters using 'Repro command:' or lowercase
  'composition_structure:' get credit for compliance.

- C6 (background/mask shorthand): inline branch previously required the
  longhand 'background-image:' / 'mask-image:' — style='background:
  url(bg.png)' silently returned false. Now checks both longhand AND
  shorthand-with-url() inline forms.

- C7 (usesGsap docstring): trim promise of data-gsap-* attribute scanning
  that detectGsap never implemented — attribute scan lives outside the
  <script>-only detection path.

- N2 (EMPTY_VALUES): include 'inherit', 'revert', 'revert-layer' — a
  style='position: inherit' is authored intent to defer, not authored
  intent to place.

- N3 (input size cap): early-exit to a zero census on HTML > 20 MB
  rather than feeding linkedom a hostile input. Not expected in normal
  usage; guard for future callers that might pass raw user uploads.

Extends the test locks: 6 new census tests (value-scoping, size cap) and
5 new lint tests (word-boundary rejections, render-noise rejections,
lowercase-marker acceptance). All existing tests unchanged in intent —
only the 'flickers' plural in one test updated to 'flicker' to reflect
the new word-boundary rule.

No behavior change to the wire path: lint is still soft-warn, census is
still never called from feedback.ts, no new dependencies.
2026-07-17 06:22:00 +00:00
Via 8f90fd9ec1 refactor(feedback): extract lint-warning loop to satisfy Fallow CRAP threshold
The added `for...of` loop over `lintFeedbackComment` warnings pushed the
`run` function's cyclomatic complexity from 4 to 5, landing the CRAP
score at exactly the 30.0 Fallow threshold. Extract the loop into
`printFeedbackLintWarnings` so `run` stays a flat driver — the helper
carries the incidental complexity.

No behavior change; all 29 unit tests + typecheck + oxlint + oxfmt +
local `fallow audit --base origin/main` pass clean.
2026-07-17 03:12:04 +00:00
Via 0aaac7aa30 feat(skills): add composition-structure block + soft-warn feedback lint
Extend the CLI feedback reproduction packet (#2498) with a fifth
mandated field, `COMPOSITION_STRUCTURE:`, and enforce presence of
`REPRO COMMAND:` / `COMPOSITION_STRUCTURE:` at feedback-submit time.

- Skill + reference now specify `COMPOSITION_STRUCTURE:` — a
  privacy-preserving structural anatomy (element census + attribute
  presence + timeline shape + delta + defect location) — required for
  any rating <=7 that describes a visual defect.
- `buildCompositionCensus()` + `renderCompositionCensusBlock()`
  auto-fill the block from composition HTML so agents don't ask the
  human user to hand-count `<video>` / `<img>` / sub-comp mounts.
  Counts + presence flags only — no file paths, no src URLs, no user
  text.
- `hyperframes feedback` soft-warns (never blocks) when a non-10
  `--comment` is missing `REPRO COMMAND:`, and when a rating-<=7
  visual-defect comment is missing `COMPOSITION_STRUCTURE:`. The
  warning points at the auto-census helper so agents remediate
  themselves.
- `coreSkillContent.test.ts` locks the new literal in both the skill
  and the reference file, following #2498's pattern.

Extends #2498. Follow-up: no change to `doctorSummary` generation, no
change to the feedback-submission API endpoint, no refactor of
#2498's doc-content Jest test.

Signed-off-by: Via
2026-07-17 02:58:11 +00:00
Via d05c899c88 fix(fallow): suppress line-shifted pre-existing findings + simplify CSS.escape polyfill
Fallow audit failed on the parent PR (#2563) with 8 findings, all of them
tracing back to line-shift fingerprint invalidation on pre-existing complexity/
duplication, plus one new-but-easily-simplified CRAP finding on the CSS.escape
polyfill in picker.test.ts.

Actions:

- picker.ts: 5 pre-existing inherited-complexity findings (isEffectivelyHidden,
  isPickableElement, buildElementLabel, getPickCandidatesFromPoint,
  pickManyAtPoint). All in the file at the parent SHA. The one-line
  buildElementSelector edit (+ 3-line comment) shifted every function below
  it, re-triggering the fingerprint. Added to health.ignore with rationale.

- screenshotClip.ts + vite.browser.ts: 19-line clip-computation clone that
  pre-dates this PR — the try/catch guard around querySelectorAll shifted
  screenshotClip.ts's clone-start line, re-flagging the inherited duplication.
  Added both files to duplicates.ignore with rationale (splitting the clone
  would require crossing puppeteer's page.evaluate serialization boundary).

- picker.test.ts CSS.escape polyfill: simplified from a 15-line char-by-char
  loop (CRAP 56.3, cyclo 14) to a compact regex + leading-digit special case
  (~4 cyclo). Still handles the digit-leading case this PR's regression test
  needs (`#0` -> `#\30 `); the round-trip through querySelector still asserts
  the element is picked back. All 16 picker tests + 3 screenshotClip tests
  still pass locally.

Change by Via
2026-07-17 02:53:07 +00:00
Via 9fc7390c2b fix(cli): import EXTRACT_CACHE_DIR_DISABLED_ALIASES in render.ts
Addresses Miga's SSOT review on #2564. The render command was
inlining the disable-alias list (["off","none","false","0"]) instead
of importing the exported constant, defeating the drift-safety the
constant exists to provide. Also switches the flag description string
to interpolate the alias set from the constant for consistency.

_— Via_
2026-07-17 02:52:04 +00:00
Via 0d16f19b07 fix(cli): surface HYPERFRAMES_BROWSER_PATH hint on macOS <13 chrome-headless-shell dyld crash
Field feedback (#hyperframes-cli-feedback ts=1784227832, darwin/x64,
macOS 12, HyperFrames CLI 0.7.60) hit
`dyld: Symbol not found: _kVTCompressionPropertyKey_ReferenceBufferCount`
from VideoToolbox when launching the pinned chrome-headless-shell
mac-152.0.7928.2. The symbol is macOS-13-only, so older hosts abort
the binary at dyld load before any browser process starts.

The reporter recovered by installing an older shell
(`@puppeteer/browsers install chrome-headless-shell@150`) and pointing
`PRODUCER_HEADLESS_SHELL_PATH` at it. Their check/snapshot commands
accepted that older cached shell (they do not force the pinned build),
but the render command requires v152 via `preferManagedChrome: true`
and could not fall back on its own. The generic "Try --docker" hint
didn't name any of the browser-path env vars.

Sibling failure mode to the download-time hint added in #2443 and the
closed-with-invite #2078 (SIGTRAP at launch on macOS arm64), and the
in-flight #2481 (Windows STATUS_STACK_BUFFER_OVERRUN); same
`HYPERFRAMES_BROWSER_PATH` remediation, different trigger + platform.

The match is gated on:
1. Puppeteer launch-failure wrapper text
2. dyld Symbol-not-found signal
3. a macOS-13-only symbol OR the VideoToolbox framework

so unrelated darwin launch failures do not mis-fire the hint. The
symbol name is macOS-version-specific by construction — if a user's
dyld cannot find `_kVTCompressionPropertyKey_ReferenceBufferCount`
their host is <13, no separate `os.release()` gate needed.

- Signed-off-by: Via -
2026-07-16 19:25:45 +00:00
ViaandVia ca35227506 feat(cli): surface extract-cache dir in doctor + add --frames-cache-dir sugar
Windows users with the OS temp dir on a small system drive have hit
C: exhaustion mid-render (Slack ts=1784219488 · CLI v0.7.58 · win32
15 GB / 8-core, ~5500 frames). The engine already honors
HYPERFRAMES_EXTRACT_CACHE_DIR for relocation, but the knob was
undocumented and invisible in diagnostics — the reporter had to piece
together a 4-flag compound workaround including EXTRACT_CACHE_DIR=off.

Changes:
- Extract the env-var resolver into a public engine API
  (resolveExtractCacheDir, defaultExtractCacheDir,
  EXTRACT_CACHE_DIR_DISABLED_ALIASES) with a typed resolution shape
  distinguishing "disabled by user" vs "default" vs "env override".
- Add a Frames-cache check to `hyperframes doctor` that reports the
  effective directory, its free space, source (env or default), and
  fails with a relocation hint when <2 GB free at that mount.
- Add `hyperframes render --frames-cache-dir <path>` as discoverable
  CLI sugar for the env var, including the opt-out aliases
  (off/none/false/0) and CWD-safe absolute-path resolution.
- Document the flag in docs/packages/cli.mdx with the field-signal
  citation, and add a render example row for the Windows workflow.
- Cover both surfaces with unit tests (6 doctor cases + 4 engine
  cases including all disabled-alias variants).

Refs Slack #hyperframes-cli-feedback ts=1784219488 (win32 v0.7.58).

Co-authored-by: Via <via-heygen[bot]@users.noreply.github.com>
2026-07-16 18:24:43 +00:00
Via 9bbdcc4ec9 fix(studio,runtime): CSS.escape ids so digit-leading selectors don't crash
The runtime picker built raw `#${id}` selectors while its sibling
attribute-selector branches (data-composition-id, data-composition-src,
data-track-index) already CSS.escape'd their values. When a user
composition has an element with a digit-leading id (e.g. `id="0"`),
the picker emits the selector `#0` which is invalid per the CSS spec —
downstream `document.querySelector` throws SyntaxError.

Same failure mode reached the Studio thumbnail: getElementScreenshotClip
called `document.querySelectorAll(selector)` unguarded, so an invalid
selector bubbling out of page.evaluate failed the whole thumbnail and
returned 500 to the browser (broken thumbnail image).

Fixes:
- packages/core/src/runtime/picker.ts — CSS.escape the id, matching the
  sibling branches on lines 100/102/104.
- packages/studio-server/src/helpers/screenshotClip.ts — catch
  SyntaxError from an invalid selector and return undefined so the
  caller falls back to a full-page screenshot, so the user still sees
  a thumbnail instead of a broken image.

Regression tests for both.

Reported via #hf-cli-feedback (Slack ts=1784218060, darwin/arm64,
CLI 0.7.60): "digit-leading worker IDs broke Studio thumbnail
querySelectorAll".

— Via
2026-07-16 18:07:58 +00:00
Vance IngallsandClaude Fable 5 21ab83320f chore: retrigger ci
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:01:58 -07:00
Vance IngallsandClaude Fable 5 94929815fa fix(core): re-export createRuntimeStartTimeResolver from index so dist emits it
src/runtime is excluded from core's tsconfig include set — runtime files
only reach dist when an included module imports them. Without a root
re-export, the ./runtime/start-resolver publishConfig entry pointed at
dist/runtime/startResolver.js which tsc never emitted, failing
verify:packed-manifests in the Build job. Same precedent as
parseStartExpression's index re-export.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:01:58 -07:00
Vance Ingalls fd19a1563d feat(core): export runtime start resolver with documentRef parameter 2026-07-16 11:01:27 -07:00
Vance Ingalls dbb684fa6c Merge pull request #2535 from heygen-com/fix/runtime-partial-timeline-pause-guard
fix(runtime): tolerate registry timelines without pause() in interactive transport
2026-07-16 03:03:22 -07:00
Vance Ingalls 74f52fcd96 Merge pull request #2534 from heygen-com/fix/panel-sections-audio-layout-style-gate
fix(studio): gate Layout and Style panel sections on the element having a rendered box
2026-07-16 02:43:57 -07:00
Vance IngallsandClaude Fable 5 7e0bd401c8 chore(core): regenerate position-edits-render artifact with current esbuild
The committed artifact was generated with an older esbuild than the
current lockfile resolves; CI's check:position-edits-render regen now
produces different (equivalent) minifier variable naming and fails the
diff gate. Regenerate to match — no source change to positionEdits.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 02:17:36 -07:00
Vance Ingalls eb967f1542 Merge pull request #2533 from heygen-com/fix/studio-promotable-badge-overlap
fix(studio): reserve a gutter for the promote-to-variable badge
2026-07-16 02:06:28 -07:00
Vance Ingalls 1a2e7524e2 Merge pull request #2532 from heygen-com/fix/studio-flat-stroke-select
fix(studio): make flat inspector's Stroke width free of style-name typing
2026-07-16 02:06:07 -07:00
Vance Ingalls f96b78165e Merge pull request #2528 from heygen-com/fix/studio-flat-row-values-right-aligned
fix(studio): right-align FlatRow's value input in the flat inspector
2026-07-16 02:05:48 -07:00
Vance IngallsandClaude Fable 5 8826d2d71d fix(runtime): tolerate registry timelines without pause() in interactive transport
A window.__timelines entry is authored content and may be a partial
RuntimeTimelineLike (duration/seek only, no pause). Timeline resolution
is deliberately permissive — duration-based — and such compositions
render fine, because the render path only seeks. But every interactive
transport path (play/pause/seek, bind, rebind-tick, boot) called
capturedTimeline.pause() unguarded, crashing studio playback with
'tl.pause is not a function' — the top recurring studio:unhandled_error
in telemetry across versions 0.6.121 through 0.7.59 (~150-175/day).

Guard all pause sites through one helper (typeof check + swallow, plus
a once-per-page timeline_missing_pause analytics event so composition
authors can find the partial timeline), matching the safeVoid pattern
player.ts already uses. In the rebind restore path, pause is guarded
separately so a missing pause() no longer aborts the seek/play restore
behind it in the same try/catch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 01:58:39 -07:00
Vance Ingalls ff26e5f2c8 Merge pull request #2529 from heygen-com/via/resolution-portrait-fix
fix(cli): accept portrait aspects for --resolution alias flag
2026-07-16 01:54:55 -07:00
Vance Ingalls 8a793ba2b2 fix(studio): gate Layout and Style panel sections on the element having a rendered box
Both panels showed Layout (X/Y/W/H/Rotation/Z-index) unconditionally —
no gate existed for it at all — and Style was gated only on
canEditStyles (a permission check), never on the element's tag. Neither
gate accounted for `<audio>`, which never paints a visual frame, so a
music track's inspector showed a full set of position/size/fill/shadow
controls with zero visual effect.

Add `layout`/`style` applicability to resolveEditingSections (core),
keyed on tag !== "audio", and gate both panels' Layout section and the
existing Style gate on it. Media/Motion/Grade/Text were already
correctly gated (verified via a research pass across both panels) and
are untouched.
2026-07-16 01:47:46 -07:00
Vance Ingalls 14dbfca5a7 fix(studio): give flat Text section rows more vertical breathing room
6px between rows left little clearance above a row's value for the
promote-to-variable badge (now positioned above the row). Widen the
row gap to 10px.
2026-07-16 01:17:04 -07:00
Vance Ingalls 5f045b6bdf fix(studio): move the promote-to-variable badge above the row instead of a padding gutter
Shrinking the wrapped control's width to make room for the badge (previous
commit) fixed the overlap but pushed the value left unnecessarily. Move the
badge to sit above the row instead, clearing the value without touching its
layout.
2026-07-16 01:11:30 -07:00
Vance Ingalls 9fa1b26570 fix(studio): reserve a gutter for the promote-to-variable badge
PromotableControl absolutely-positions its "◇ var" / "◆ {id}" badge over
the wrapped control without reserving any space, so on rows where the
value renders flush to the right edge (flat Font/Color rows) the badge
sat directly on top of the value text instead of beside it. Add a
right-padding gutter on the wrapper sized to each badge state, and cap
the bound chip to a fixed max-width so it always fits inside its gutter.
2026-07-16 01:06:11 -07:00
ViaandClaude 2d398ed274 fix(cli): wire aspect-agnostic resolution through cloudrun/lambda/batch + preflight recompute
Addresses R2 CHANGES_REQUESTED from Miga + Rames on PR #2529:

1. Sibling-surface gap (blocker): `hyperframes cloudrun render{,-batch}`,
   `hyperframes lambda render{,-batch}` all advertised the same tier-only
   aliases (`1080p` / `hd` / `4k` / `uhd`) but normalized them to `landscape`
   and never set `outputResolutionAspectAgnostic`. The distributed plumbing
   PR #2529 added received `undefined` from those callers, so portrait `1080p`
   still hit the original aspect-mismatch on Cloud Run / Lambda.

   Fix: introduce `resolveResolutionFlagPair` in `@hyperframes/parsers` (the
   single source of truth for the two-step normalize + aspect-agnostic
   detect) and route every distributed entrypoint through a shared
   `parseOutputResolutionFlag` CLI util so the alias signal now reaches
   `SerializableDistributedRenderConfig`. Studio Server keeps its
   canonical-only HTTP contract; that intent is now pinned in tests.

2. Preflight recompute (hardening): the earlier "downgrade aspect-mismatch"
   preflight cleared un-remapped mismatches, so IG 4:5 (non-preset aspect,
   no sibling) and portrait-4K comp + `--resolution 1080p` (remap +
   downsample) both slipped through to fail late in `resolveDeviceScaleFactor`.
   Now `checkRenderResolutionPreflight` computes the effective preset via
   `suggestMatchingPreset` (mirroring the compile stage's
   `adaptAspectAgnosticResolution`) and re-checks against that — only
   genuinely-fixable mismatches clear early. New tests pin both regressed
   input classes.

3. Docker forwarding boundary test (Miga's important #2): pinned
   `1080p` survives verbatim as `--resolution 1080p` in the Docker args
   so the in-container CLI can re-run `isAspectAgnosticResolutionAlias`.

4. Doc-nit (Miga): parsers/src/types.ts no longer references the
   nonexistent `resolveResolutionForComposition` — points at the actual
   remap helpers.

Fallow: cloudrun.ts / lambda.ts share 390 lines of pre-existing structural
symmetry (parallel AWS + GCP dispatchers), and lambda/render.ts +
render-batch.ts declare parallel RenderArgs interfaces. Both re-flagged
after threading the aspect-agnostic field through each surface; ignored
with justification in .fallowrc.jsonc. lambda.ts's `run` and
lambda/render.ts's `waitForCompletion` are pre-existing CRAP-score
hotspots untouched by this PR — added under health.ignore.

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

— Via
2026-07-16 08:01:56 +00:00
Vance Ingalls d5a259a3d2 fix(studio): make flat inspector's Stroke width free of style-name typing
Stroke width committed border-width and border-style together from one
free-text field, so setting a style meant typing an exact CSS keyword
(e.g. "dashed") with no indication of which ones were valid — the row
also duplicated the discoverable Stroke style select directly below it.
Stroke width now only commits border-width; style changes go exclusively
through the existing select.
2026-07-16 00:48:53 -07:00
Vance Ingalls b32f9a3e86 fix(studio): right-align FlatRow's value input in the flat inspector
FlatRow lays out label…gap…value across a `justify-between` row, but the
shared CommitField input it wraps had no text-align, so its text hugged
the LEFT edge of the value's own (often much wider) right-hand box —
looking left-aligned relative to the row, out of step with FlatSelectRow
and FlatSlider, which already right-align.

Added an optional `align` prop to CommitField (default "left", preserving
the legacy panel's MetricField/DetailField layouts where label-then-value
sits inline and left reads naturally) and pass `align="right"` from
FlatRow. Left the Motion Timing row's Start/End/Duration cells alone —
those stack label-above-value in a grid, a different pattern from the
inline label…value row this fix targets.

New tests: FlatRow's input has `text-right` (not `text-left`); the legacy
MetricField's input keeps `text-left` (not `text-right`), pinning
CommitField's default so the shared component doesn't drift for the
panel that didn't ask for this.

Full studio suite (2645 tests) green; typecheck/oxlint/oxfmt clean.
2026-07-16 00:48:53 -07:00
Vance Ingalls b0bb468877 Merge pull request #2519 from heygen-com/fix/studio-variable-promote-shared-session-sync
fix(studio): resync the shared SDK session after a Design-panel variable promote
2026-07-16 00:36:10 -07:00
Vance Ingalls d0d281de50 Merge pull request #2525 from heygen-com/via/audiopad-ffmpeg8-compat
fix(producer): audioPadTrim FFmpeg-8.x-compatible apad invocation
2026-07-16 00:35:53 -07:00
Vance Ingalls 3e4c761842 Merge pull request #2527 from heygen-com/via/artifact-validated-exit-gate
fix(cli): don't override exit code after artifact validated
2026-07-16 00:35:30 -07:00
Vance Ingalls 9d36bc9d95 Merge pull request #2526 from heygen-com/via/transcribe-timeout
feat(cli): configurable transcribe timeout with duration-scaled default
2026-07-16 00:35:08 -07:00
Vance Ingalls 578d6202b4 fix(studio): resync the shared SDK session after a Design-panel variable promote
Reported as "template variables are broken": binding an element's field to a
variable via the flat inspector's "◇ var" promote chip (or editing an
already-bound field's value) wrote the correct bytes to disk, but the
Variables tab kept showing the pre-edit value until the whole Studio page
was hard-reloaded.

Root cause: DesignPanelPromoteProvider deliberately opens its OWN SDK
session (`useSdkSession(projectId, selection.sourceFile ?? activeCompPath)`)
so that promoting inside a sub-composition binds the variable in the
sub-comp's own file, not the host's. For the common case — a top-level
element, same file as `activeCompPath` — this session is a SEPARATE
in-memory `Composition` instance from the shared one `VariablesPanel`
(Variables tab, Slideshow, etc.) reads. A persist through the promote
provider's session never fires the shared session's own "change" event.

Worse, the shared session's file-change listener runs
`isSelfWriteEcho(path, content)` to decide whether to reload — but
`sdkSelfWriteRegistry` is keyed by file path only, not by session instance
(its own doc comment assumes "the studio process has a single SDK session
lifecycle at a time"). It sees the promote provider's write registered
under the same path and concludes it's its own echo, permanently
suppressing the reload it actually needs.

Threaded `forceReloadSdkSession` (the same mechanism every other
server-side-write path in Studio already uses for exactly this "resync
after a write I didn't make myself" case) from App.tsx through
StudioRightPanel into DesignPanelPromoteProvider, and call it after every
successful promote/setDefault persist — unconditionally, not gated on the
promote target matching activeCompPath, since re-opening a file that
didn't change is a harmless no-op re-parse and a path-equality guard here
already produced one subtly wrong comparison (activeCompPath can be null
while the shared session still defaults to "index.html") before landing on
this simpler version. Verified live: editing a variable-bound field's
value now updates the Variables tab immediately, no reload required.

App.tsx crossed the 600-line file-size gate after threading the new prop;
extracted the tiny handleAddAssetAtPlayhead wrapper into its own
useAddAssetAtPlayhead hook (with a regression test) to bring it back under.

Full studio suite (2639 tests) green against a fresh main; typecheck/
oxlint/oxfmt clean.
2026-07-16 00:18:36 -07:00
ViaandClaude f3b941f1e3 fix(cli): reduce CRAP score by extracting exit-path helpers; bump contactSheet Windows timeout
Fallow flagged the `uncaughtException` arrow at CRAP 30.0 (right at
threshold). Extracting the three exit paths — emitCliErrorEvent,
reportPostRenderTerminationEvent, exitAfterPostRenderTermination,
exitAfterCliFailure — pulls the two `_flushSync?.()` optional-chain
branches out of the arrow body and drops cyclomatic to 3. Same shape
already used on compileStage in a sibling PR. Behavior preserved:
EPIPE → exit(0), renderSucceeded → report + flush + exit(0),
default → commandFailed + track + flush + exit(1) for uncaught;
renderSucceeded → report + return, default → commandFailed + track
for rejection.

Windows contactSheet flake: `createContactSheet > writes PNG output`
timed out at 20025ms (default ceiling 20000ms) on Windows. Test does
milliseconds of real work — the wall-clock overhead is sharp's native
binary fork under runner I/O jitter. PR #2492 already tried lightening
the test; the flake persists at the default ceiling. Bumping the
per-test timeout to 60s absorbs runner jitter without hiding real
slowdowns (a genuine sharp regression would blow through 60s just as
readily as 20s). Targeted per-test bump, not a global suite bump.

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

— Via
2026-07-16 07:08:45 +00:00
ViaandClaude 40f4cfe92f fix(producer): materialize audioPadTrim concat script to real file
The prior fix (dc410ca) dropped `pathToFileURL` from the pad-concat step
to make FFmpeg 8.x on Windows stop rejecting `file:///C:/…` URLs — but
kept feeding the concat script via `pipe:0` stdin. That combination
broke Linux CI: FFmpeg's concat demuxer resolves bare paths in the
script against the base URL of the script's own source, and when the
script is fed via `pipe:0` the base URL is `pipe:`. Absolute POSIX
paths (`/tmp/foo.aac`) then join to `pipe:/tmp/foo.aac`, which the
demuxer tries to open as a pipe and fails with:

    [concat @ 0x…] Impossible to open 'pipe:/tmp/…/audio.aac'
    pipe:0: End of file

Manually reproduced with `ffmpeg-static@7.0.2` on this repo's binary.

Fix: write the concat script to a real temp file (`<outputPath>.concat-
list.txt`) and pass `-i concatListPath` — matching the sibling concat
in `distributed/assemble.ts:180-186` exactly. A real file's directory
becomes the base URL, so absolute paths in the script resolve as-is on
both Linux and Windows. The `file://` scheme prefix stays out of the
script (Windows FFmpeg 8.x fix preserved) and no `pipe:` prefix gets
prepended (Linux regression fixed). Cleanup path list now covers both
the silence tail and the concat list script.

Also drops the now-unused `runFfmpegWithStdin` helper — no consumer
needs stdin plumbing anymore.

Regression pins in `audioPadTrim.test.ts`:
  - `does not emit file:// URLs …` — Windows arg-shape pin (unchanged
    intent, moved from `stdin` to `concatListContent` field).
  - `materializes the pad-concat script to a real file …` — new pin
    that asserts `-i` is not `pipe:0` and points at the concat list
    path, so the Linux failure mode can't regress.

CI failures fixed:
  - CI / Producer: integration tests (assemble.test.ts pad case)
  - regression / regression-shards shard-1 (style-3-prod field-signal
    end-to-end render exercising the assemble pad path)

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

— Via
2026-07-16 06:54:07 +00:00
ViaandClaude 7e58d050f8 fix(fallow): resolve audit findings for portrait --resolution PR
Three findings, all resolved:

- packages/producer/src/server.ts `render` (CRAP 31.6, cyclo 10 — minor):
  pre-existing complexity; the PR only threads
  `outputResolutionAspectAgnostic` through parseRenderOverrides /
  RenderInput and does not touch `render`. Line-shift fingerprint —
  exempted via health.ignore with justification comment.

- packages/producer/src/services/distributed/plan.ts `plan` (CRAP 36.7,
  cyclo 33 — major): pre-existing complexity; the PR only adds one
  optional field spread inside `plan` and does not add branches.
  Line-shift fingerprint — exempted via health.ignore with justification.

- packages/producer/src/services/render/stages/compileStage.ts
  `runCompileStage` (cyclo 23, cognitive 19 — minor): this one is a
  real complexity bump from the two-branch aspect-agnostic re-target
  block added in the fix. Extracted the block into a local helper
  `adaptAspectAgnosticResolution` so `runCompileStage` stays under both
  the cyclomatic (20) and cognitive (15) thresholds.

Verified locally with `fallow audit --base origin/main --fail-on-issues`
(exit 0, "No GitHub PR/MR findings") and `tsc --noEmit` on the producer
package.

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

— Via
2026-07-16 06:52:11 +00:00
ViaandClaude 46e9ecf3f2 fix(cli): accept portrait aspects for --resolution alias flag
The aspect-agnostic resolution aliases (`--resolution 1080p` / `hd` / `4k` / `uhd`) previously all normalized to a landscape preset, which rejected portrait 1080x1920 compositions with 'Output resolution incompatible'. Users had to specify the orientation-bearing alias (`1080p-portrait`) or render at native.

This threads two new fields (`outputResolutionAspectAgnostic` + `outputResolutionRaw`) through the render pipeline. At the CLI layer we detect whether the user's flag was an aspect-agnostic alias; at the compile stage we re-map the preset to the composition's orientation via the existing `suggestMatchingPreset` sibling-lookup (formerly private). Explicit orientation-bearing aliases and canonical presets stay strict.

Field signal: ts=1784176662 (darwin/arm64, CLI 0.7.59, `--resolution 1080p` on a 1080x1920 portrait comp).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

— Via
2026-07-16 06:23:14 +00:00
ViaandClaude 25a04d7fc8 fix(cli): don't override exit code after artifact validated
The render command's post-artifact-validated cleanup (telemetry flush,
feedback prompt, worker/browser teardown, stray promise rejections) can
throw AFTER the producer has committed a valid MP4 to disk. Field signal
ts=1784169760, ts=1784171150, ts=1784172467 (all win32/x64, CLI 0.7.58,
ffmpeg=no, 1080x1920): ffprobe + visual QA confirmed the outputs are
valid, but the CLI exited 1 after the terminal "artifact validated" log
with no final error message.

Introduce a `renderSucceeded` sentinel that flips after `executeRenderJob`
(or the Docker child render) resolves cleanly. From that point on:

  - Post-render steps in the render command (trackRenderMetrics,
    printRenderComplete, warnIfWebmAlphaDropped, maybePromptRenderFeedback)
    run through `runPostRenderStep`/`runPostRenderStepAsync` guards that
    swallow throws, log a compact warning to stderr, and sanitize a stray
    `process.exitCode` back to 0.

  - The CLI's top-level `uncaughtException` handler logs the throw for
    diagnosis but exits 0 instead of 1 when the render already succeeded.

  - The CLI's `unhandledRejection` handler stops flipping `commandFailed`
    (which drove the success:false telemetry field) when the render
    already succeeded.

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

— Via
2026-07-16 05:56:35 +00:00
ViaandClaude Opus 4.7 f8210d96da feat(cli): configurable transcribe timeout with duration-scaled default
Adds a `--timeout <ms>` CLI flag (and `HYPERFRAMES_TRANSCRIBE_TIMEOUT_MS`
env var) plus a model-slowdown factor in the auto-scaled default so
`hyperframes transcribe` doesn't hard-fail with `spawnSync ETIMEDOUT`
on slow CPUs running heavier whisper models.

Field-signal ts=1784165471 (win32/arm64 emulating x64 on Snapdragon,
CLI 0.7.59) reported the failure on a 63s wav with `-m medium` at ~13x
realtime — the historical 10x-realtime scale (PR #2463) gave 10.5 min
while the machine needed ~13.7 min. Splitting audio and merging offsets
was the manual workaround.

- Add `--timeout <ms>` and `HYPERFRAMES_TRANSCRIBE_TIMEOUT_MS` (min 5000).
  Explicit override bypasses auto-scaling; still capped at 12h.
- Add per-model slowdown factor (tiny 0.5, base 0.7, small 1, medium 2,
  large 4, large-v3-turbo 2). Multiplied into the 10s/audio-second
  baseline so medium/large get proportional headroom while `small.en`
  (the default) preserves the historical safety window.
- Wrap whisper's spawn error with a discoverability hint naming
  `--timeout`, the env var, and the effective timeout when the child
  was killed by SIGTERM/ETIMEDOUT (mirrors PR #2504 protocol-timeout).
- Docs: new `--timeout` row in `docs/packages/cli.mdx` Flags table.

Regression coverage in `packages/cli/src/whisper/transcribe.test.ts`
(56 tests) and `packages/cli/src/commands/transcribe.test.ts` (5 tests):
- Model factor per known name + case-insensitive + safe unknown fallback.
- 63s field-signal case on medium.en → 1_260_000ms (was 630_000ms).
- Explicit override honored below the auto floor + capped at 12h.
- Model factor ignored when overrideMs is set.
- SIGTERM/ETIMEDOUT detection + augmented message contract.
- CLI rejects below-minimum `--timeout` with error naming both the flag
  and the 5000ms floor.

— Via

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-16 05:49:46 +00:00
ViaandClaude dc410ca990 fix(producer): audioPadTrim FFmpeg-8.x-compatible apad invocation
The `audioPadTrim` module's pad-concat step generates a concat script
whose file directives use `file://` URLs (built via Node's
`pathToFileURL`). FFmpeg 8.x on Windows rejects these with
"Impossible to open file:///C:/…" — its `file:` protocol handler strips
the scheme leaving `///C:/…`, which Windows path parsing then rejects.

Field-signal (4 reports over ~24h, all win32/x64, CLI 0.7.59):
  - ts=1784169914 (Baoyu, 60s render, native audio assembly failed)
  - ts=1784177061 (andre 22cores, 345.87s composition, 9 WAV audio elements)
  - ts=1784177375 (KEY DIAGNOSTIC: 13 mono 44.1kHz mp3 tracks, ffmpeg
    8.1.1-full_build gyan.dev, "same project rendered fine in July with
    an older ffmpeg"; manual `ffmpeg -i track.mp3 -af apad=whole_dur=16
    -t 16 -c:a aac out.aac` works with the same binary, so the tool's
    audioPadTrim invocation is the incompatible part)
  - ts=1784177375 (duplicate reporter follow-up)

The concat approach itself is fine — the sibling concat scripts in
`assemble.ts` and `chunkEncoder.ts` pass raw paths (no `pathToFileURL`)
and work on Windows. `audioPadTrim.ts` was the outlier introduced in
PR #1615 (2026-06-20). Aligns with the codebase convention.

Regression pin: unit test asserts the pad-concat stdin never contains
the `file://` scheme, including for a Windows-shaped input path.

End-to-end verification requires a Windows + FFmpeg 8.x reviewer; the
unit test snapshots the arg shape.

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

— Via
2026-07-16 05:46:56 +00:00
Vance Ingalls a10e462d02 Merge pull request #2518 from heygen-com/fix/studio-flat-input-affordance
fix(studio): give flat-inspector value fields a resting-state input affordance
2026-07-15 21:24:13 -07:00
Vance Ingalls 399438b963 Merge pull request #2497 from heygen-com/fix/studio-flat-layers-panel-standalone
fix(studio): show Layers full-height in the flat inspector, not split with Design
2026-07-15 21:19:29 -07:00
Vance Ingalls 2b51c52634 fix(studio): give flat-inspector value fields a resting-state input affordance
Every editable value in the flat inspector (FlatRow's CommitField, the
Motion Timing row's Start/End/Duration cells, and every raw <select> —
Style/Text dropdowns, Grade's Custom LUT and Copy-grade-to scope) rendered
its underline/border only on hover (`border-transparent
group-hover:border-...` or no border at all). At rest a value looked like
plain static text, with nothing distinguishing it from a label — testers
reported not being able to tell which fields were editable.

Give each a dim-but-visible resting border (`border-panel-border-input/50`,
or `border-panel-accent/30` for the explicitCustom tier) that brightens on
hover/focus, instead of a fully transparent one. Purely visual — no
behavior change.

Full studio suite (2641 tests) green; typecheck/oxlint/oxfmt clean.
2026-07-15 20:54:07 -07:00