[pull] main from heygen-com:main#69
Merged
Merged
Conversation
…ut_font_face (#1859) Two independent post-release feedback reports of this rule hard-erroring on OS system fonts (Hiragino Sans, Microsoft YaHei) that have no downloadable file. Both asked for the same thing, in slightly different words: a documented way to satisfy the check for a font that's genuinely OS-bundled, not missing. That way already exists and already works — extractFontFaceFamilies only looks at the font-family declaration inside @font-face, never the src value, so `@font-face { font-family: 'X'; src: local('X'); }` already passes. One report found this themselves; the other didn't. The gap was discoverability: the fixHint only described bundling a real font file, so nobody would think to try `local()` unless they already knew about it. Considered and rejected a broader fix: adding CJK system-font names to the shared FONT_ALIAS_MAP (the mechanism that already exempts Latin system fonts like Segoe UI/Verdana by aliasing them to a bundled fallback font). That map has no CJK-equivalent bundled font to alias to (only Japanese has one, noto-sans-jp) — aliasing "Microsoft YaHei" (Simplified Chinese) to a Japanese font would silently swap in the wrong glyph shapes for shared Han characters, and would specifically break distributed/Lambda rendering (where system-font capture is disabled, per system_font_will_alias's own comment) by removing the warning that currently prompts a real fix. The local() message fix has none of that risk: it changes no detection logic, only points at an already-correct existing escape hatch. Tests: local() font-face no longer flags (proves the advice is accurate, not just documented); fixHint contains "local(". 308 lint tests pass.
…1874) WINDOW_TIMELINE_ASSIGN_PATTERN only matched window.__timelines["literal"] or window.__timelines.prop, so registrations via a computed key like window.__timelines[spec.id] (used by the code-particle-assemble and code-3d-extrude registry blocks) went undetected. That made gsap_timeline_not_registered false-fire on correctly registered timelines, and let root_composition_missing_duration_source wrongly demand an explicit data-duration on compositions that already have one.
…e composition root (#1867) Two independent post-release feedback reports of the same mechanism: a leading <svg> block (icon/gradient/filter <defs>, referenced by url(#id) elsewhere in the document) placed before the real [data-composition-id] root manufactures root_missing_composition_id + root_missing_dimensions on an otherwise-correct composition. Moving the <svg> after the root cleared both findings for each reporter. findRootTag returned the first body child that wasn't script/style/meta/ link/title, unconditionally — <svg> was never in that skip list, so a leading defs-only <svg> got treated as the root. Fix: skip a leading <svg> when it carries none of the composition markers itself (data-composition-id/data-width/data-height), so an intentionally SVG-rooted composition is still eligible as the root. The first attempt at this only skipped the <svg> open tag, which surfaced a second bug: extractOpenTags is a flat, nesting-unaware scan, so the very next tag it returns after skipping <svg> is the svg's own nested child (<defs>, <filter>, ...), not the sibling after </svg>. Track the svg's closing tag position and skip every tag before it, not just the <svg> tag itself. Tests: skips a leading svg defs block (no false root findings); still treats an <svg> as the root when data-composition-id/data-width/ data-height are declared directly on it. Full lint suite (308 tests) passes.
…imates_clip_element (#1846) Two independent post-release feedback reports of the same contradiction: scene_layer_missing_visibility_kill / gsap_exit_missing_hard_kill tell you to add `tl.set(selector, { visibility: "hidden" }, t)` on an exiting scene element, but when that element is also class="clip", the exact tl.set they recommend is then flagged by gsap_animates_clip_element (the framework already owns visibility/display on clip elements). One report worked around it by wrapping the scene's content in an inner non-clip div and asked that the fix hint mention that pattern. Both rules now detect when the exiting/flagged selector is a clip element (scene_layer_missing_visibility_kill checks the tag's class list directly; gsap_exit_missing_hard_kill reuses the clipIds/clipClasses maps already built in its enclosing rule) and, only in that case, point at the inner-wrapper pattern instead of a tl.set on the clip element itself. Non-clip targets are unaffected — same fix hint as before.
…1858) Two independent post-release feedback reports of hitting ffmpegEncodeTimeout (600000ms default) on long or high-frame-count renders, both resolved by setting FFMPEG_ENCODE_TIMEOUT_MS to a higher value and/or PRODUCER_ENABLE_CHUNKED_ENCODE=true — env vars that already exist and already solve this, but that neither user found from the error message itself. appendEncodeTimeoutMessage only stated what happened ("FFmpeg killed after exceeding ffmpegEncodeTimeout"), not what to do about it. Name both existing knobs in the message so the fix is immediately visible at the point of failure instead of requiring a source dive. One function, six call sites, all fixed at once. Existing tests assert with toContain, so the appended text doesn't break them; added two assertions confirming both env var names appear in the message.
#1849) Two independent post-release feedback reports of validate warning about audio duration despite an explicit, correct data-duration slot, one of them naming a timeout explicitly. Root cause: auditClipDurations reads each <video>/<audio> element's intrinsic .duration via a single page.evaluate() snapshot taken after a flat, unconditional page-settle sleep (opts.timeout ?? 3000ms, shared with other audits). Per the HTML spec, HTMLMediaElement.duration is NaN until metadata loads. A slow-loading audio file (large narration WAV, remote source) can still be mid-fetch when that sleep elapses — el.duration is NaN at that exact instant, which the audit permanently records as "could not read the duration" even though the render pipeline (which properly awaits media readiness) handles the same file fine. Fix: race each not-yet-ready element's loadedmetadata/error event against a deadline instead of taking one fixed-time snapshot. Elements already ready resolve immediately (no added latency in the common case); only genuinely slow elements get a real second chance before the warning fires. The race/cleanup wiring lives twice by necessity — once inline inside the page.evaluate() closure (Puppeteer serializes and re-runs that closure in an isolated browser realm with no access to this module), and once as the exported, duck-typed raceMediaReady for a real, deterministic unit test via Node's built-in EventTarget (no browser or DOM library needed). The comment on raceMediaReady flags that both copies must move together.
…ction races (#1866) * fix(cli): lock chrome-headless-shell install against concurrent extraction races A detailed post-release feedback report of `render` producing a fully black 15s MP4 despite lint/validate/inspect/snapshot all passing and Studio preview playing correctly. Root cause traced by the reporter: chrome-headless-shell had been manually re-extracted after `browser ensure`'s own download got stuck mid-extraction when two concurrent invocations raced on the same cache dir. The manual extraction lost a macOS Gatekeeper/quarantine or GPU/Metal entitlement bit that a clean install sets, so headless GPU frame capture silently returned all-black frames — invisible to every existing health check, since they only confirm the binary *exists*, not that it captures real pixels. `--no-browser-gpu` fixed it completely, confirming the GPU-capture path specifically. A related, vaguer report of the same race the prior loop run ("'browser ensure' hung mid-extraction after a race from two concurrent invocations") was deferred pending a clearer repro; this report supplied one. @puppeteer/browsers' install() has no concurrency guard of its own — confirmed by reading its source: two concurrent installs for the same browser/buildId both proceed straight to download+unpack with no existing-install check, no lock. Two ensureBrowser()/findBrowser() calls that both miss the cache at the same time (the common case on a fresh machine, or right after `browser clear`) race on the same extract target. Fix: mkdirSync as an atomic cross-process mutex around the download — recursive:false makes it throw EEXIST when another process already holds it (that's load-bearing: recursive:true would silently no-op instead). Zero new dependencies. A concurrent caller polls until the lock releases, then re-checks the cache before deciding whether to download at all — the common case (loser waits, then reuses the winner's completed install) never re-downloads. A lock held past a generous timeout is reclaimed rather than left to wedge every future render if the holder crashed mid-extraction. Applied to both call sites that reach the racy downloadBrowser() (ensureBrowser's two paths, and findBrowser's stale- cache re-download — the file already carries a code-duplication suppression between these two near-identical functions). Not doing (out of scope for this fix): the reporter's second suggestion, a deeper `doctor` check that actually captures a test frame rather than checking binary existence. That's a real gap but a separate, larger feature — this fix prevents the corruption that caused it, which matters more than detecting it after the fact. Tests: two new cases (lock releases after a successful download; a lock held past its timeout is reclaimed rather than hanging — exercised via withInstallLock's injectable timeoutMs/pollMs with tiny real waits, avoiding fake-timer mocking through the full async ensureBrowser call graph). All 13 tests in manager.test.ts, 22 across packages/cli/src/browser, and the full CLI suite (1115 tests) pass. * test(cli): isolate browser install lock test from system chrome * fix(cli): guard stale browser lock reclaim
…silently shipping video-only (#1854) At least 4 independent post-release feedback reports of a render completing successfully (exit 0) with audio elements correctly authored and detected at compile time (audioCount > 0), but the final MP4 having no audio track — discovered only via ffprobe or manual playback, with the CLI giving no indication anything went wrong. Users worked around it by muxing the generated audio in manually with ffmpeg. Root cause: runAudioStage sets hasAudio from processCompositionAudio's success flag, but discarded its error field — the actual reason a per-element audio prep step or the final mix failed (source not found, extract failed, ffmpeg error) was computed and then thrown away. A real audio-mix failure was therefore indistinguishable from "no audio was authored": both just produced hasAudio: false with zero diagnostic output. Thread the mixer's error through as audioError (only set when audios.length > 0 but the mix failed) and log.warn it from both call sites (the main render path in renderOrchestrator.ts and the distributed plan() path) so a real failure is loud instead of silently downgrading to a video-only render. Tests: 4 new cases for runAudioStage (mixer error surfaced, generic fallback message when the mixer doesn't provide one, no audioError on success, no audioError when there's no audio to mix). renderOrchestrator.test.ts (68 tests) unaffected. plan.test.ts's one failure (an audio-bearing planHash determinism test timing out at 30s) is pre-existing — reproduces identically on unmodified main with these changes stashed.
…ng every line at once (#1862) Two independent post-release feedback reports of the same mechanism from two different skills (both delegate to this one shared engine): audio.mjs fired every line's Kokoro TTS + whisper-transcribe subprocess concurrently via a bare Promise.all with no cap. - One OOM'd 12/13 lines on a resource-constrained laptop (32GB total, ~7GB free), requiring a manual patch to a sequential for-loop. - The other saw 7/8 lines fail on first run, then pass on retry once the model was cached — concurrent cold-start model loads overwhelming the machine, not a real synthesis failure. Kokoro/Whisper each load their own local model per subprocess, so firing every line at once multiplies that cost by the line count. Extracted the concurrency cap into lib/concurrency.mjs (audio.mjs is a script — it runs CLI/exit side effects on import, so it can't be unit-tested directly; the cap is small enough to pull out and test in isolation). Default 4, overridable via HYPERFRAMES_TTS_CONCURRENCY, floored at 1 (matching one report's own manual workaround). hyperframes-media/scripts/audio.mjs is the single canonical engine per its own header comment; product-launch-video, faceless-explainer, and pr-to-video each carry a thin wrapper that spawns this file as a subprocess (confirmed via their DEFAULT_ENGINE path), so this one fix covers all four skills without touching the other three. Tests: 4 new cases for mapWithConcurrency (order preserved regardless of completion order, cap actually enforced, limit > item count doesn't hang, empty input). Full skills test suite (514 tests) shows no new failures — the 444 pre-existing failures are environment-dependent and reproduce identically on unmodified main.
…1877) * fix(hyperframes-media): fall back to ffmpeg when ffprobe is missing ffprobeDuration() returned NaN whenever the ffprobe spawn failed for any reason, conflating "ffprobe binary not installed" with "file is corrupt". Some ffmpeg-only distributions (common in curated Windows installs) ship ffmpeg.exe without ffprobe.exe, so every TTS line hit the missing-binary case and audio.mjs read the NaN as a bad WAV, silently dropping an already successfully synthesized line. Now falls back to parsing ffmpeg's own `Duration:` stderr banner when ffprobe specifically ENOENTs, and only returns NaN when the file itself can't be probed by either tool. * fix(hyperframes-media): update skills manifest for ffprobe fallback
…n Windows (#1845) * fix(hyperframes-media): npx spawn without shell:true fails silently on Windows Two independent user reports of Kokoro TTS silently failing on Windows, both naming the same site: lib/tts.mjs synthesizeOne() spawns "npx" via plain spawn(cmd, args). On Windows npx resolves to npx.cmd, which Node's spawn() cannot exec without shell:true — it fails ENOENT, and spawnP's "error" listener turns that into a plain ok:false ("TTS failed") with no indication of the real cause. Scope the fix to the npx call specifically (python3/ffmpeg are real binaries and don't need it), with the platform/spawn function injectable so the win32 branch is testable without mocking node:child_process (its ESM exports are non-configurable, so mock.method can't patch it) or the real process.platform. * fix(hyperframes-media): avoid shell true for windows npx
…1879) The design-style extractor now captures a site's signature color grounds and materials that a flat background-color misses: - Capture gradient background-image + backdrop-filter on buttons/cards/nav. - backgrounds[]: dominant gradient / mesh washes ranked by on-screen area (includes ::before/::after glow orbs), chroma-weighted so a small vivid brand wash outranks a large neutral scrim. - glass[]: frosted-glass panels (backdrop-filter blur) with their raw translucent fill, border, radius, shadow — ranked by area. - nav CTA capture: keep filled buttons inside <nav> (a page's primary "Sign up" / "Start for free" CTA that the old nav-drop lost), including gradient-filled CTAs whose background-COLOR is transparent. - Dedup keys for buttons/cards now include gradient + glass so a gradient/frosted variant is not collapsed into its flat sibling. - Fix: a fully-transparent fill rgba(...,0) now reports "transparent" instead of #000000 — the old bug turned every transparent wrapper into a phantom black button/card. types: ComponentStyle gains backgroundImage/backdropFilter; DesignStyles gains backgrounds[] and glass[]. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )