Commit Graph
774 Commits
Author SHA1 Message Date
James Russo d92d1d4f51 fix(cli): keep diagnostics on stderr for --json commands (#2522) 2026-07-16 09:14:39 -04:00
James Russo 6a1585df27 refactor(render): centralize execution ownership (#2158) 2026-07-16 05:00:40 -04: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
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 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
Miguel Ángel 0f287ee0b6 chore: release v0.7.60 2026-07-16 05:26:57 +00:00
Miguel Ángel 9c25e27da6 docs(renderer): link SwiftShader workaround tracker 2026-07-16 05:19:00 +00:00
Miguel Ángel 54a3ef2000 fix(renderer): prevent stale SwiftShader layers 2026-07-16 04:56:36 +00:00
Miguel Ángel 4cbde38301 test(producer): use intrinsic duration in style fixtures 2026-07-16 02:15:02 +00:00
Miguel Ángel 1284703adb test(producer): update remaining video slot goldens 2026-07-16 02:15:01 +00:00
Miguel Ángel 31cc2fe875 test(producer): update video slot regression golden 2026-07-16 02:15:01 +00:00
Miguel Ángel 2e8f871bc8 fix(video): hold final frame through composition 2026-07-16 02:15:01 +00:00
Via 1c46eadceb fix(producer): propagate --exclude-tags to Dockerfile.test ENTRYPOINT
Follow-up on Miga's review of #2512. The regression fixture
`escape-hatch-fatal-fallback` is tagged `field-signal-reproducer` and
`known-broken` so it's skipped from the default sweep via
`--exclude-tags transparency,field-signal-reproducer` in the
`test:regression*` scripts in packages/producer/package.json. But
`Dockerfile.test`'s ENTRYPOINT invoked the harness directly (`bunx tsx
src/regression-harness.ts -- --sequential`), bypassing those scripts —
so `bun run docker:test*` and the aws-lambda smoke tests would still
try to run the known-broken fixture and fail. CI's own regression sweep
was insulated only because it hardcodes per-shard positional test names
that don't include this fixture, but that's incidental, not by design.

Bake the exclude-tags into the Dockerfile.test ENTRYPOINT itself so
every user of the image (local `docker:test*`, aws-lambda smoke, any
adopter running the reference image) picks up the same skip contract.
Docker CMD args appended after the entrypoint (e.g. matrix shard
positional test names in .github/workflows/regression.yml, or
`--mode=distributed-simulated`) still parse correctly — the harness
applies excludeTags after testNames-filtering (see discoverTestSuites
in regression-harness.ts).

Also exports `parseArgs()` from regression-harness.ts and adds
regression-harness-parse.test.ts to pin the `--exclude-tags` comma-parse
contract, so any future change to the parser or the values baked into
the Dockerfile / package.json will trip a red test rather than silently
diverging.

Verification A (harness comma-parses `--exclude-tags transparency,
field-signal-reproducer`) already worked pre-fix; the new test file
codifies it. Verification B (Docker ENTRYPOINT propagates the same
skip) is what this commit fixes.

Signed-off-by: Via
2026-07-16 00:33:33 +00:00
Via e6cdf4abb1 feat(engine): opt-in per-frame timing on fast-capture fallback path
Field-signal baseline: >=2 fallbacks/hr on darwin/arm64 from filter:blur
and filter:drop-shadow triggers. Fallback path perf is currently untimed,
so we can't know if the overhead is 10% or 10x. This PR adds opt-in
per-frame timing (HF_PROFILE_FALLBACK_CAPTURE=true) that emits p50/p95/p99
+ trigger reason via the observeRenderStage telemetry channel extended in
#2510. Diagnostic surface only -- no perf fix, no behavior change on
healthy paths.

Stack: PR #9 (final) of 9 (base via/escape-hatch-fallback-reproducer).

Signed-off-by: Via
2026-07-16 00:28:40 +00:00
Vance Ingalls 4740288050 Merge pull request #2512 from heygen-com/via/escape-hatch-fallback-reproducer
test(producer): reproducer fixture for both-escape-hatches-fail case
2026-07-15 17:23:22 -07:00
Vance Ingalls 4e9047018d Merge pull request #2510 from heygen-com/via/parallel-capture-observability
feat(producer): calibration-aware heartbeat + worker-death terminal-error contract
2026-07-15 17:22:43 -07:00
Vance Ingalls 0ab153e0c5 Merge pull request #2507 from heygen-com/via/parity-telemetry-gate
feat(producer): parity telemetry gate for per-clip frame-count invariant
2026-07-15 17:22:13 -07:00
Vance Ingalls 9db5f67972 Merge pull request #2506 from heygen-com/via/darwin-goto-nav-timeout-hint
feat(engine): surface escape hatches in page.goto Nav timeout errors
2026-07-15 17:22:01 -07:00
Vance Ingalls fda62cae08 Merge pull request #2504 from heygen-com/via/protocol-timeout-discoverability
feat(engine): surface protocolTimeout env + flag in Puppeteer timeout errors
2026-07-15 17:21:30 -07:00
Via e5c4e1970c test(producer): reproducer fixture for both-escape-hatches-fail case
Field signal ts=1784039841 (win32/x64, CLI 0.7.57): shifted-DOM-image-
layer bug at frame 120 reproduces with BOTH PRODUCER_FORCE_SCREENSHOT=true
AND HF_DE_PARALLEL_ROUTER=false set. First case where both known escape
hatches fail simultaneously. Standalone 1920x1080 GSAP paused timeline,
absolute PNG layers, 4 independent scenes.

Ships a skipped regression fixture to codify the shape. NOT a fix — no
root cause identified. The composition is preserved so a future
diagnostic pass has a real, checked-in repro and a proposed fix can be
validated against the same shape the field reported.

Skip mechanism (belt-and-suspenders):
  1. meta.json tagged `field-signal-reproducer` and `known-broken`;
     producer/package.json test:regression* scripts add the tag to
     --exclude-tags alongside the existing `transparency` skip.
  2. Not registered in any .github/workflows/regression.yml shard's
     args, so the CI regression sweep won't pick it up either.

Un-skip when a fix lands: drop the tags from meta.json AND add the
fixture id to a shard's args in the workflow. See src/README.md for
the field-signal envelope and diagnostic starting points.

Stack: PR #8 of 9 (base via/gpu-parity-gate).

Signed-off-by: Via
2026-07-15 23:35:58 +00:00
Via 971bcf39ae feat(producer): calibration-aware heartbeat + worker-death terminal-error contract
Field signals ts=1784019503 (heartbeat reports 0 frames during 64s
browser calibration — reads as broken but is healthy) and ts=1784042064
(1292s Windows render hard-exited during video frame extraction with
no final error string — silent worker crash).

Add calibrating/capturing state to heartbeat labels; surface synthetic
terminal error on unexpected worker exit when no explicit error was
emitted.

Stack: PR #6 of 9 (base via/overlay-count-lint).
Signed-off-by: Via <vance@heygen.com>
2026-07-15 23:14:49 +00:00
Vance Ingalls c2764e0888 Merge pull request #2473 from heygen-com/07-15-fix_producer_extend_de_stall_watchdog_to_the_single-worker_streaming_path
fix(producer): extend DE stall watchdog to the single-worker streaming path
2026-07-15 15:55:52 -07:00
Via 69b393865a feat(producer): parity telemetry gate for per-clip frame-count invariant
Field signals ts=1784139267 (win32 15-injection blank later clips,
injection-count-scaled) + ts=1784144554 (darwin 147-authored-clip
visibility gap, authored-clip-count-scaled). check/snapshot passes,
final MP4 has blank/missing clips. Extends #2474's videoCount:0 probe
with per-clip captured-vs-expected frame ratio + threshold fail-loud
gate at render finalization.

Stack: PR #4 of 9 (base via/darwin-goto-nav-timeout-hint).
Signed-off-by: Via <via@heygen.com>
2026-07-15 22:46:45 +00:00
Via 58cff5f6d5 feat(engine): surface escape hatches in page.goto Nav timeout errors
Field signal ts=1784146416 (darwin/arm64, CLI 0.7.58, 7/10): host
page.goto hit Navigation timeout of 60000ms twice on a CSS 3D + audio
composition; Docker rendered the same composition successfully.
Puppeteer's stock "Navigation timeout of 60000 ms exceeded" text names
none of HyperFrames' existing escape hatches, so the reporter had no
signal that the failure had knobs.

Wraps main-render Puppeteer `page.goto` errors matching
/Navigation timeout|net::ERR_TIMED_OUT/i with an augmented message that
names:

- The effective timeout currently applied (`cfg.pageNavigationTimeout`).
- Raise-the-timeout: `PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS` env,
  `--browser-timeout` CLI flag (seconds).
- Browser-binary escape hatch: `HYPERFRAMES_BROWSER_PATH` env.
- Field-signal shape: darwin/arm64 + CSS 3D + audio compound Docker
  hint — gated on all three inputs being explicitly true; falls back
  to generic hints when any input is unknown.

Mirrors #2443's HYPERFRAMES_BROWSER_PATH surfacing pattern (which
covered download-time failures) at the runtime `page.goto` layer.
Non-matching errors flow through unchanged. Original error preserved
via `err.cause`.

Wired into `renderOrchestrator.executeRenderJob`'s top-level catch,
composed after `augmentProtocolTimeoutError` so the two augmenters
never both fire on the same error (mutually exclusive regexes).
Current wire-up passes no `hasCss3D` / `hasAudio` context — no
compile-time CSS-3D signal is threaded through the render pipeline,
and `hasAudio` is block-scoped inside the try. Per the helper's
fallback docs, unknown flags route to the generic env + browser-path
hints. A future compile-time CSS-3D scan can thread both flags to
enable the full compound Docker hint without touching this helper's
signature.

Stack: PR #3 of 9 (base via/win32-streaming-encode-autodisable).

Signed-off-by: Via <vance@heygen.com>
2026-07-15 22:33:37 +00:00
Via 6944a1c2d0 feat(engine): surface protocolTimeout env + flag in Puppeteer timeout errors
Field signal ts=1784047847 (darwin/arm64, 8GB M1, 9 videos + 22 images):
reporter hit Runtime.callFunctionOn timeout and switched to FFmpeg
because the error didn't surface HyperFrames' existing knobs
(PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS env, --protocol-timeout CLI).

Wraps main-render Puppeteer errors matching /Runtime\.callFunctionOn
timed out|Target closed|protocolTimeout/i with an augmented message that
names the effective timeout, the env var, the CLI flag, and the
field-signal shape. Non-matching errors pass through unchanged
(returned as the same instance). Original error preserved via err.cause.

Also adds a dedicated --protocol-timeout row to the CLI docs Flags table
so PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS is discoverable via search.

Signed-off-by: Via <noreply@heygen.com>
2026-07-15 22:11:44 +00:00
James Russo 2417293dab fix(studio): enforce optimistic file concurrency (#2156)
* fix(studio): enforce optimistic file concurrency

* fix(studio): harden conditional file writes

* fix(studio): honor explicit file preconditions

* test(producer): allow zero-ms encode timing
2026-07-15 18:07:04 -04:00
Miguel Ángel 35e623b4f3 fix(cli): align video output boundaries (#2490)
* fix(cli): align video output boundaries

* test(cli): honor CI ffmpeg fixture path

* test(cli): decouple duration precedence from ffmpeg

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

* test(producer): refresh style 7 boundary golden
2026-07-15 16:07:44 -04:00
Miguel Ángel 850f57ea04 fix(render): ignore favicon probe noise (#2487) 2026-07-15 15:26:25 -04:00
Vance Ingalls ff3b1541e5 chore: release v0.7.59 2026-07-15 11:57:49 -07:00
Vance Ingalls b70d849a47 fix(producer): bridge stall-timeout env var rename, disambiguate abort from stall on sequential path
Review feedback on #2473: HF_DE_PARALLEL_STALL_MS had no backwards-compat
shim after the rename to HF_DE_STALL_MS, and a parent abort during a wedged
sequential capture would surface as "stalled" instead of "aborted" in
downstream logs/telemetry (functionally harmless since isCancellation is
gated on abortSignal.aborted, not message text, but misleading to read).
2026-07-15 11:53:54 -07:00
Vance Ingalls 650977d1d8 fix(producer): extend DE stall watchdog to the single-worker streaming path 2026-07-15 11:53:54 -07:00
Miguel Ángel 882c203241 fix(render): avoid Windows output path limit for work dirs (#2479) 2026-07-15 10:30:00 -04:00
WaterrrForever 7d21cc9b8a fix(skills,cli): close four reproduced contract gaps from the CLI feedback digest (#2476)
* fix(cli): invalidate the skills nudge cache after a successful install/update/check

The passive "N skills out of date or missing" nudge reads a 24h config
cache that only the background check (on non-skills commands) ever wrote.
The skills commands themselves are excluded from the nudge pipeline, so a
successful `skills update`/install/check never refreshed or dropped the
cached verdict — the pre-install count kept printing on every other
command for up to 24h.

Reconcile commands now drop the cached verdict (counts + timestamp) so
the next command's background check re-runs for real. The offline
presence-only path deliberately keeps the cache: that run learned nothing
about freshness.

* fix(skills): win32-safe npx spawns in media-use + accurate whisper wording

The Whisper transcribe fallback and the Kokoro local-TTS delegation both
spawned a bare "npx" via execFileSync — on Windows npx is npx.cmd, which
spawn cannot exec, so both paths died with `spawnSync npx ENOENT`. Route
them through the skill's existing resolveSpawnCommand (node + npx-cli.js
on win32, no shell:true), same as the audio engine's TTS spawns.

Also corrects the "bundled with the hyperframes CLI" claim about
whisper.cpp: it is resolved from PATH / installed via Homebrew / built
from source with git+cmake on first use, and models download from
HuggingFace — nothing whisper is shipped in the package.

* feat(skills): canonical fully-silent marker + auth status exit-code docs

product-launch's Step 3.1 gate said "or the project is marked silent"
but nothing defined how to mark one, and audio.mjs unconditionally
retrieved BGM. Define the canonical marker — `music: none` in the
storyboard's top YAML block, plus no SCRIPT.md — and honor it:
audio generate produces nothing (removing stale audio_meta.json, since
absence is what assemble treats as silent), and `music: none` with
narration keeps TTS while turning BGM off.

Also documents the `auth status` exit-code contract (exit 1 while
signed out is the normal offline state, not a failure) in the
product-launch Step 0 note and the CLI skill's cloud reference.

* fix(skills): transient-init retry for standalone animation-map and contrast-report

The standalone helpers called initializeSession exactly once, so a valid
modular project — whose sub-composition timelines register asynchronously
— could hit the readiness deadline and die with the transient
"zero duration / Runtime ready: false" diagnostic the render pipeline
retries (probeStage). Add initializeSessionWithRetry to the shared
package-loader (both byte-identical copies): close the crashed session
and retry once with a fresh browser, gated by the engine's canonical
isTransientBrowserError — now re-exported from @hyperframes/producer,
with a frozen fallback pattern list for older published packages. The
"Runtime ready: true" fast-fail (a genuine authoring bug) still fails
without a retry.

* feat(skills): extend the fully-silent marker to faceless-explainer and pr-to-video

Both workflows reuse product-launch's audio model — their Step 3.1 gates
carried the same undefined "marked silent" phrase, and their (intentionally
identical) audio.mjs copies had the same unconditional BGM retrieve. Port
the `music: none` marker handling into both copies, define the marker in
their SKILL.md Step 3.1 and story-design references, and turn the
copies' "intentionally identical" header claim into a byte-identity pin
test so the next fix can't silently miss one of them.

* test(cli): reset the prune mock explicitly instead of relying on restoreAllMocks

The converge test's toHaveBeenCalledTimes(1) held only because vitest 3's
vi.restoreAllMocks() clears vi.fn() call state; vitest 4 restores spies
only, so the count would accumulate across tests and fail. Reset
pruneOrphanedLockEntries in beforeEach like the other manifest mocks —
passes under both vitest 3.2.4 (pinned) and vitest 4.

* test(skills): close review findings — package-loader pin, whisper win32 parity, quoted-none

Review follow-ups on #2476:

- package-loader.mjs byte-identity pin (the elevated concern): the two
  copies now carry initializeSessionWithRetry + FALLBACK_TRANSIENT_PATTERNS,
  exactly the shared-logic shape a future fix could land in one copy and
  miss in the other — same enforcement as the audio.mjs pin.
- whisper win32 call-site parity: runWhisper's npx resolution lifted into
  lib/npx-sync.mjs (resolveNpxInvocation, injectable params matching the
  localTtsGenerate idiom) with the same three-branch coverage as the
  Kokoro site — plus the hard-fail contract (throws actionably, since the
  whisper fallback has no next provider to fall through to).
- quoted music: "none" pin: the vendored storyboard parser strips matching
  quotes at parse time (stripQuotes), so the silent marker already accepts
  the quoted spelling — pinned so that stays true.
2026-07-15 22:22:16 +08:00
Miguel Ángel 1895286189 fix(render): normalize local AAC duration before mux (#2472)
* fix(render): normalize local AAC duration before mux

* style(render): apply repository formatter

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

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

* fix(render): isolate normalized audio temp path

* fix(engine): centralize AAC packet duration

* test(producer): refresh AAC duration golden
2026-07-15 10:07:07 -04:00
Miguel Ángel 47e4b18f99 fix(render): discover runtime-inserted media before extraction (#2474)
* fix(render): discover runtime-inserted media before extraction

* fix(render): detect runtime Audio constructors

* test(render): cover runtime audio insertion paths
2026-07-15 08:13:17 -04:00
Vance Ingalls 36075c6797 fix(engine,producer): classify blank vs psnr from a structural field, not the error message
Deepwork's request-changes on #2411 (twice): deFallbackReason's blank/psnr
split still ran /blank/i.test(err.message) even after this PR's stated goal
of moving off message-text parsing — a reworded message, a translated
string, or a differently-shaped error crossing a module boundary could
silently relabel a blank failure as psnr (or vice versa), corrupting the
soak's telemetry taxonomy.

DrawElementVerificationDetails now carries a required `kind: "blank" | "psnr"`
field, set at all three real throw sites in captureStreamingStage.ts. The
orchestrator derives deFallbackReason from getDrawElementVerificationDetails's
kind instead of regexing the message. Making `kind` a required constructor
argument means any future throw site that omits it fails to compile, closing
the gap for good rather than just at today's three call sites.

New tests in frameCapture.test.ts prove message-independence directly: kind
survives a reworded message that says neither "blank" nor "psnr", and stays
correctly "psnr" even when the message adversarially contains the substring
"blank" — the exact scenario a regex-based classifier would get wrong.
2026-07-14 23:29:49 -07:00
Vance Ingalls af923d947c fix(producer,cli): round the fallback dB consistently, thread the verify threshold through
Review feedback on #2411 (Rames): the crash-survival RenderCaptureObservability
mirror passed deFallbackFailedDb raw/unrounded while the render_complete
perfSummary path rounded to 1 decimal — the same underlying PSNR could ship two
different values to PostHog depending on which event fired. Extracted the
existing inline round/clamp expression (previously duplicated for verifyMinDb
and fallbackFailedDb) into a shared roundDb helper, applied once at the single
point deFallbackFailedDb is derived from the thrown error so both downstream
consumers agree.

Also threads verifyThresholdDb (captured on the error but never propagated,
per the nit) through DrawElementPerfInput/RenderCaptureObservability/render.ts/
telemetry as de_fallback_threshold_db on both events — the HF_DE_VERIFY_MIN_DB
value the failing dB breached, letting ops read "28.4dB failed a 32dB
threshold" directly instead of cross-referencing config.
2026-07-14 23:29:49 -07:00
Vance Ingalls a5cbb78ff9 feat(engine,producer,cli): capture failing dB/frame index on drawElement verify fallback
de_fallback_reason only told you the fallback happened (blank/psnr/oom/
capture_error), not the failing PSNR or frame index — that data existed as
text inside the thrown error's message and was discarded on the way to
telemetry. DrawElementVerificationError now carries structured
frameIndex/failedDb/verifyThresholdDb; the orchestrator reads them via the
new getDrawElementVerificationDetails helper instead of regexing message
text, and both telemetry surfaces (the render_complete perfSummary path and
the crash-survival RenderCaptureObservability mirror) emit
de_fallback_failed_db / de_fallback_frame_index.

Needed to distinguish "32dB vs the 32dB threshold, tune it" from "12dB real
corruption, investigate" during the parallel-router soak — currently that
distinction is invisible.
2026-07-14 23:29:49 -07:00
Miguel Ángel 6813eaeece fix(producer): fail renders when audio mixing fails (#2429) 2026-07-14 23:27:20 -04:00
Miguel Ángel 7382fabab9 fix(core): consolidate external asset and dependency preservation (#2410)
## Summary

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

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

## Verification

- core compiler/runtime tests: 89 passed
- producer file-server tests: 48 passed
- core, producer, engine, and CLI typechecks passed
- `git diff --check`
2026-07-14 21:55:51 -04:00
Miguel Ángel 37e1b26434 fix(fonts): consolidate composition-aware compilation (#2406)
* fix(fonts): subset Google Fonts to composition text

* fix(producer): skip slow TTC recompression

* fix(fonts): include decoded composition text in subsets
2026-07-14 20:12:30 -04:00
Miguel Ángel 0b3dfb3f84 fix(render): consolidate duration and timing correctness (#2405)
* fix(producer): pass variables to duration probe

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

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

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

Resolve relative references the same way the extractor does (parseStartExpression
from @hyperframes/core + a findReferenceTargetEl/resolveReferencedStart port,
since the engine's referenceResolver isn't a public export across the package
boundary). Verified: host duration now 6; 3 tests pass.
(Implemented via Codex; verified independently.)
2026-07-14 17:12:36 -04:00
Miguel Ángel 4b0b89e8b1 chore: release v0.7.58 (#2446) 2026-07-14 16:15:59 -04:00
Miguel Ángel d2c8c2d808 fix(producer): probe variable-bound media sources (#2444) 2026-07-14 15:42:00 -04:00
James Russo fb0d823500 fix(render): publish artifacts atomically (#2154) 2026-07-14 12:54:13 -04:00
Miguel Ángel 90be05019b chore: release v0.7.57 (#2393) 2026-07-14 00:29:29 -04:00
Miguel Ángel 3f6ea0a1ab perf(producer): cache local font compression (#2397) 2026-07-14 00:26:37 -04:00
Vance Ingalls 58cfa0c655 Merge pull request #2359 from heygen-com/via/issue-3-software-gpu-screenshot
fix(engine): software-GPU browsers imply screenshot capture
2026-07-13 20:37:12 -07:00