mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
701ae9e9b90d93667503f75e4107c3c96dbaf6c9
2287
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
701ae9e9b9 |
fix(cli): contrast audit reads an element's own opaque background (#1975)
The WCAG contrast audit estimated each text element's background by sampling a 4px pixel ring just OUTSIDE its bounding box. For an element that paints its OWN opaque background (a caption pill, a CTA button, a solid card), the text is composited over that solid color, not over whatever surrounds the box. Sampling the ring there measured the text against the scene behind the element (often a dark photo), producing false ~1:1 ratios and flagging perfectly readable CTAs and captions. Users reported the warning persisting no matter how they changed the background color, because the audit was never reading it. Resolve the nearest fully-opaque background-color by walking the element up its ancestor chain, and use it when present; keep sampling the ring only when the text sits over image pixels (a background-image is hit first) or no opaque background exists. The pure decision lives in a new commands/contrast-bg.ts with unit tests; contrast-audit.browser.js (injected as a raw string, so it cannot import) inlines the same logic, mirroring the existing duplicated-WCAG-math note. |
||
|
|
ab129023de |
docs(cli): fix render examples that pass a file as the project dir [P2] (#1974)
* docs(cli): fix render examples that pass a file as the project dir
The render command's positional argument is the project directory (default
"."), resolved via resolveProjectOrThrow; a specific composition file is
passed with -c/--composition. Several docs showed `hyperframes render
index.html` / `render ./my-composition.html`, which treats the HTML file as
the project dir and fails with "Not a directory". Correct the guide and the
cli README to render the project's index.html directly (or point at a file
with -c).
* docs: fix render index.html example in the Open Design guide too (R1)
R1 flagged that open-design-hyperframes.md carried the identical
`npx hyperframes render index.html` example this PR fixes in the Claude
guide — same failure vector ("Not a directory" for a file positional).
Corrected to `npx hyperframes render` run from the project directory.
|
||
|
|
e018318225 |
fix(cli): add --no-clipboard no longer throws "Unknown flag: --clipboard" (#2067)
The add command declared its flag literally as `"no-clipboard"`, but citty treats `--no-<name>` as the negation of a boolean `<name>` arg. So `--no-clipboard` parsed as negating a (nonexistent) `clipboard` arg and assertKnownFlags threw "Unknown flag: --clipboard" — even though --help advertised --no-clipboard as valid. Declare the positive `clipboard` (boolean, default true) instead and read `args.clipboard === false`; citty's built-in negation then handles `--no-clipboard` correctly. --help still lists both spellings. Verified: `hyperframes add data-chart --no-clipboard` now succeeds instead of erroring on the flag. |
||
|
|
f5f94a9495 |
fix(cli): warn when a WebM render silently drops its alpha channel [P2] (#2044)
* fix(cli): warn when a WebM render loses its requested alpha channel HyperFrames always encodes WebM with an alpha-capable pixel format (yuva420p), but some ffmpeg/libvpx builds silently emit opaque yuv420p even when handed alpha input and -pix_fmt yuva420p. The render succeeds and plays back fine, so the lost transparency is only discovered after compositing (users report shipping a solid-black clip and colorkeying it out by hand). After a WebM render, best-effort ffprobe the output's pix_fmt; if it lacks alpha, print a non-blocking warning that names the concrete remedy (--format mov / ProRes 4444). Only WebM is checked (mp4 is intentionally opaque; mov/png carry alpha through paths that don't hit libvpx-vp9), and a failed probe stays silent rather than warning speculatively. Pure decision (pixelFormatHasAlpha / webmAlphaAdvisory) unit-tested; verified end-to-end that a transparent WebM render now surfaces the warning while an MP4 render stays silent. * fix(cli): key WebM alpha check on ALPHA_MODE tag, not pix_fmt (R1 blocker) R1 (Rames/Via) correctly flagged the detection as ~100% false-positive on working builds. libvpx-vp9 stores the alpha plane in a Matroska BlockAdditional sidecar, so ffprobe ALWAYS reports pix_fmt=yuv420p for a correct transparent WebM (per docs/guides/rendering.mdx #1823 and the webm-concat-copy smoke test). The real signal is the stream-level ALPHA_MODE=1 tag: a working encode writes it; a build that can't emit the sidecar omits it and produces genuinely opaque output. Re-cut the probe to read stream_tags=alpha_mode (JSON, case-insensitive) and warn only when a probed WebM lacks ALPHA_MODE=1. Tests inverted accordingly (alphaMode:true → silent; alphaMode:false → warn). Verified end-to-end: a transparent webm render on an alpha-preserving build (ALPHA_MODE=1) now emits 0 warnings; previously it warned on every webm. |
||
|
|
6f8bf5f364 |
fix(engine): resolve relative data-start references for audio tracks (#2062)
parseAudioElements read data-start with a bare parseFloat, so a relative reference (data-start="introClip", the documented 'start when that clip ends' pattern) resolved to NaN. The mixer then silently dropped the track, rendering the whole segment as pure digital silence — even though the SAME reference on the sibling <video> placed the visual correctly (#2030 taught parseVideoElements/parseImageElements to resolve refs; audio never learned). Root fix, single source of truth: extract the Node-side reference resolver out of videoFrameExtractor into referenceResolver.ts and use it in parseAudioElements for both <audio> and <video data-has-audio> tracks. Now every media parser resolves relative timing identically, so audio and video cannot drift again. The two near-identical parse loops share one builder; end stays a numeric read (mixer derives real length downstream), NaN-guarded. Verified end-to-end: a composition with <audio data-start="clipId"> now renders an audio stream that is silent before the referenced clip ends and audible after (matches the numeric-start control); previously the output had no audio stream at all. 78 engine media tests pass (4 new). |
||
|
|
bb423dd217 |
docs: add modal deployment template to deploy guide (#2069)
Add Modal as a third official deployment template alongside Vercel and Cloudflare: comparison-table row, a Modal tab (deploy commands, what-you-get, performance, pricing, async spawned-function note), architecture/pre-baking updates, swap-the-composition steps, and a source card. Update the cloud.mdx cross-reference to include Modal. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
41ad5b4690 |
fix(engine): inject sub-composition variables on the render path (#2066)
render left window.__hyperframes.getVariables() empty inside every
sub-composition mounted via data-composition-src, so each instance rendered
its declared JS defaults instead of the per-instance data-variable-values.
preview/snapshot injected them correctly, so the composition looked right in
every authoring/QA surface and then rendered wrong content silently (exit 0).
Any template-library workflow (reusable sub-comp scenes parametrized per
video) shipped placeholder/default text in the final MP4.
The plumbing already existed on main: htmlCompiler passes
readVariableDefaults/parseHostVariables and populates result.variablesByComp,
and the CSS-custom-property path (emitRootCompositionVariableStyles) reaches
the render. But the render compiler emitted only the CSS vars and never the
JS table window.__hfVariablesByComp that the scoped getVariables reads, while
the preview bundler (htmlBundler) did -- so getVariables() returned {} only
during render.
Fix, so the paths cannot drift again: buildVariablesByCompScript, colocated
with the reader in compositionScoping.ts and shared by both compile paths.
htmlBundler now calls it instead of an inline string; htmlCompiler injects it
before the inlined sub-comp scripts, using the already-populated
result.variablesByComp.
Verified end-to-end: a sub-comp painting its background from a color variable
now renders the injected value under render, matching snapshot; previously it
rendered the default. 3 new producer tests; 89 htmlCompiler + core-compiler
tests pass.
Closes #2064.
|
||
|
|
17b852784b |
feat(skills): mode-first briefs, value-first storyboards, and destination defaults across creation workflows (#2058)
* feat(skills): add brief contract — interaction modes + shared intake fields across workflows New hyperframes-core/references/brief-contract.md, the shared intake contract every creation workflow now runs its brief against: - §1 interaction mode: collaborative (default) vs autonomous, ongoing vs one-time signals, mode set once and carried forward, and a gate taxonomy (preference / checkpoint / quality / routing) — autonomous skips waiting, never verification - §2 field registry: destination→aspect derivation (feed → 1:1, Shorts/TikTok → 9:16, else 16:9), message, angle, length, audience, language, narration — each workflow binds fields as ask or state - §3 question rules: one round with one question per asked field (native question UI mandatory when available, recommended option first with a receipt), never drop a question as inferable, and a mode legend advertised in the intro text instead of asked Wired into the surfaces: - hyperframes router: detect mode at entry, derive aspect from destination instead of stating 16:9 - product-launch-video / pr-to-video / faceless-explainer: ask/state binding tables at Step 0; Step 3/6 checkpoint-gate branches (autonomous posts a heads-up with a preview hint before render) - website-to-video: local mode definition now defers to the contract - music-to-video, general-video, embedded-captions, talking-head-recut, slideshow, motion-graphics: mode semantics wired per gate type - storyboard-format: new optional 'mode' frontmatter key - pr-to-video: length tier is a ceiling, not a floor — a one-headline PR recommends inside the 30–90s sweet spot regardless of diff size * feat(skills): story spine + mode-first brief across creation workflows Story — the reverse-iceberg feedback: - New hyperframes-creative/references/story-spine.md, three rules for the narrated workflows: the hook speaks the viewer's outcome language, the value claim lands by beat 2 (implementation is the footnote of the story, not the spine), and the storyboard is presented as a proposal — 'This video tells [audience] that [message]' plus a per-frame why: drawn from narrativeRole - pr-to-video: feature-reveal reordered promise-first (impact leads, diff/mechanism follow as evidence); hooks ban file/function names; fix-explainer, refactor-walkthrough, changelog unchanged - product-launch-video / faceless-explainer hook rules aligned to the spine; website-to-video's beat summary gains the echo line + why:; general-video points at the spine from its plan step Brief — hardened after live-test drift: - Mode is now the first question (Collaborative recommended vs Autonomous), its own round, skipped when the request carries a signal; autonomous asks nothing further until one final preview-or-render question before render - Step 0 rewritten as a literal two-round question script in each shot-sequence workflow (website-to-video's editorial register, channel-agnostic); brief-contract.md §3 reduced to invariants so the procedure lives in exactly one place * feat(skills): split type minimums by viewing context typography.md: full-screen viewing keeps body 20px / headline 60px; in-feed destinations (X / LinkedIn / Instagram — brief-contract's destination field) scale to body >=32px, headline >=90px, data labels >=24px. First-pass values, to be calibrated against real renders. * feat(skills): storyboard proposal as a table + credits close by default - story-spine § 3: the proposal presents frames as a markdown table (frame · beat · on screen · why) instead of dense paragraphs; the three shot-sequence workflows and website-to-video's beat summary reference the same shape - pr-to-video: the credits close is now the default ending — every PR video ends on a contributors frame (committers by commit count, 1-6 avatars), with no taste judgment; the only skip is when no avatar was fetched, and the user can cut the frame in the proposal * fix(skills): address review nits on the brief/story contracts - embedded-captions: the identity procedure now states both sides of the preference gate inline (user picks; autonomous picks with a stated why) - website-to-video step-2-brief: note that its mode section is the workflow's application of brief-contract.md, not a second definition - brief-contract: resuming a project reads mode from STORYBOARD.md frontmatter — a recorded mode counts as set, closing the write-only gap * docs(skills): add a non-code receipts example to the brief contract Review nit (jrusso1020, #2058): the receipts example in § 3 was PR-video-shaped only. A destination-shaped example joins it so the rule reads as workflow-neutral. |
||
|
|
81884a7495 |
fix(cli,skills): install workflow skills on demand instead of re-pulling the full set (#2012)
* fix(cli,skills): install workflow skills on demand instead of re-pulling the full set Users report every init re-pulls all 21 skills into ~/.agents/skills whenever anything is stale or missing - heavy, noisy, and it re-expands deliberate partial installs. Split the set into two tiers: - core: the /hyperframes router + hyperframes-* domain skills + media-use, which every workflow references structurally. init and bare 'skills update' keep these (plus anything already installed) fresh, and never expand the install. - on demand: the end-user workflow skills (and figma). They install at trigger time via 'skills update <name...>' - positional names are the only way update expands an install: one targeted 'skills add --skill <name>' covering only stale/missing targets, a fast no-op when current, presence-verified after install, exit 1 on unknown names, and a presence-only degrade when GitHub is unreachable. The /hyperframes router now runs 'skills update <workflow>' after routing and before reading the workflow skill, so a routed workflow is guaranteed present even on a machine that only has the core set. Each on-demand skill also opens with the same self-maintenance step (run 'npx hyperframes skills update <name>' silently), so a workflow triggered directly - without the router - still refreshes itself and restores any missing core skill before relying on it. When the manifest is unreachable (offline / rate-limited) the engine degrades honestly instead of claiming success: named runs presence-check the request plus a pinned fallback core list (unit-pinned to skills/) and blind-install whatever is absent; a bare strict update fails loudly so the 'check || update' chain can't pass while everything stays stale; init reports the skipped freshness check. --json emits structured errors on failure paths. skills check still lists every skill, but exits non-zero only for stale installed skills, an incomplete core set, or removed leftovers - workflow skills not yet installed are reported as available on demand. Bare 'hyperframes skills' (and 'skills add --all') remain the explicit full-set installs. Verified end-to-end with a sandboxed $HOME: fresh init installs the 9 core skills only; 'skills update slideshow' adds exactly that skill (no-op on re-run, exit 1 on unknown names); bare update refreshes without expanding; a live Claude Code run routed PR-to-video, executed the router's update step, and the workflow skill appeared before use; and a second live run triggered an installed workflow directly, whose opening maintenance step restored a deliberately removed core skill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(skills): clarify update-engine contracts + document lazy-install model - skills.ts: note the UpdateSkillsResult.unknown strict-mode contract, verifyInstalled's non-strict (warn-not-throw) intent, and that a partial install stays "refreshed but never expanded" (review nits). - docs/guides/skills.mdx: add a "Keeping skills current" section covering the core-eager / workflow-on-demand model and the skills check|update commands, per the repo's catalog-maintenance rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: kiritowoo <295860553+kiritowoo@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Miao Yang <miao.yang@heygen.com> |
||
|
|
e52bfc246a |
Merge pull request #2053 from heygen-com/vi/figma-loop-fixes
fix(core,cli,lint): close the figma brand-token loop — runtime CSS variables, --name, snippet lint |
||
|
|
4d3cdc3e4b |
feat(media-use): resolve official brand logos via a four-tier cascade (#2061)
* feat(media-use): resolve official brand logos via a four-tier cascade Third-party brand logos (the meeting's 'credibility signals lost' gap) had no acquisition path: capture only grabs the product's own site assets, and HeyGen asset search returns generic look-alike icons for brand queries (0/3 in testing — an X-in-a-circle for LinkedIn). Workers could only fake a mark or drop it. New resolve type 'logo', four tiers verified by a 54-brand stress test (100% cascade hit across dev tools / big tech / non-tech / CN brands): - svgl — official full-color vector SVGs + wordmark variants (40/54 first-hits); search is substring-based, so entities pass through alias normalization (nextjs → 'next.js', aws → 'amazon web services') - simple-icons (pinned CDN build) — official monochrome glyphs; catches the long tail (nike, visa, toyota, wechat, bytedance) - github org avatar — known-org map only; a brand name is not a GitHub login, guessing risks same-named personal accounts - domain favicon (DuckDuckGo ip3) — small-raster last resort; sub-500B responses are DDG's placeholder and rejected; frozen with a low_res provenance flag (chip-size use only) logo joins the icon/image equivalence group (typesMatch) and the images/ subdir, so entity cache hits interop with figma-imported marks. A total miss falls through resolve's normal failure path — no special casing. HeyGen search stays the icon provider; it is deliberately absent from the logo cascade. Docs: media-use gap/types/providers tables + example; the five workflow banners now cover logos (catalog claim kept for media, 'from their official sources' added for logos); product-launch story-design and motion-graphics logo-reveal point at the new type; catalog surfaces (CLAUDE.md / README / docs) updated in lockstep. Verified: 19 unit tests + coverage row green; live smoke across all four tiers (linkedin→svgl, nike→simple-icons, heygen→github.avatar, amazon→favicon) plus a fabricated brand exiting 1 on the default miss path. oxlint + oxfmt clean. * test(media-use): sanction the four logo providers in the registry allowlist svgl / simple-icons / github.avatar / favicon.ddg join the sanctioned list — the logo cascade added in the previous commit. Full lib suite 95/95 green. * test(media-use): gate the logo cascade behavior in CI + single-fetch favicon tier Review follow-ups (miga-heygen, jrusso1020 on #2061): - Eight mocked-network tests pin what the manual 54-brand stress test only asserted: descriptor shape, alias retry (svgl non-array payload → next query, simple-icons 404 → next slug), network-error → null fallthrough, the sub-500B placeholder rejection, github's no-guessing (zero fetches for unmapped entities), and the real cascade order landing tier by tier under a mocked network. - faviconSearch now hands its verified bytes over as a local file, so the freeze step copies instead of re-downloading — one round-trip, and the size check is authoritative over what gets frozen. - The header's hit counts are labeled as a stress-test snapshot, not a live invariant. Full lib suite 103/103; live smoke re-verified (amazon → favicon.ddg, frozen .ico). |
||
|
|
d9368ec051 |
fix(core): address PR feedback — ReDoS-safe slug trim, getVariables cleanups
- slugify: replace the anchored alternated trim regex (/^-+|-+$/g) with a character-scan trim — CodeQL js/polynomial-redos blocker. - readRenderOverrides: fold the readOverrides wrapper into the exported function (one name, no pass-through). - getVariables: deduplicate declarers with a Set, matching injectCompositionCssVariables. - Move the tokenSlug import to the top of the file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e2c88ef689 |
fix(core,producer): composition CSS variables reach the render path at eval time
Live testing of the compile-time variable emission surfaced four gaps: - The producer render path never emitted the compile-time stylesheet (only the preview bundler did), so eval-time reads — GSAP .from immediateRender, top-level getComputedStyle — saw undefined vars in rendered output. The producer's inlineSubCompositions now calls the shared emitRootCompositionVariableStyles and passes the variable hooks. - --variables overrides weren't visible at eval time. They now thread from the orchestrator / distributed plan through compileStage into the emitted rules (window.__hfVariables still covers script reads). - Per-declarer rules anchored on data-composition-id, which two inlined instances of one sub-composition share — instance A's rule restyled instance B, and a rule directly on the declarer defeated the host's inherited data-variable-values. Rules now anchor on per-instance data-hf-var-scope markers and layer nearest-host values over declared defaults, mirroring the runtime loader. - Emission ignored authored CSS; a declared default now yields to a var already defined in an authored <style> block (define-if-absent, matching the runtime injection). Also: the figma importer emits background-color (longhand) for solid fills. GSAP backgroundColor tweens cannot read a var() through the background shorthand — its pending-substitution longhands serialize empty, so .from captured nothing and settled on transparent (pre-existing GSAP interaction, reproduced with no composition variables involved). Validated live: eval-time default + override, .from + override, two-instance host branding, authored :root precedence, SDS brand-loop pixel parity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6192ed4cbd |
Merge pull request #1779 from heygen-com/feat/lint-gsap-non-transform-motion
feat(lint): add gsap_non_transform_motion rule, migrate registry comps to transforms |
||
|
|
4fc619e4f1 |
Merge remote-tracking branch 'origin/main' into feat/lint-gsap-non-transform-motion
# Conflicts: # skills-manifest.json |
||
|
|
def276524b |
fix(core,cli,lint): close the figma brand-token loop — runtime CSS variables, --name, snippet lint
Brand-loop live test (SDS duplicate, plans/figma/brand-loop-test-plan.md) proved the recolor chain end-to-end and surfaced three gaps: - runtime now defines every declared composition variable as a CSS custom property (document root at init + scoped sub-comp hosts in the loader), so imported var(--slug, literal) fills resolve live — without this the frozen literal always won and variable-driven rebranding could not propagate. Slug kept byte-compatible with the figma importer (parity test). render --variables overrides win. - figma component --name: variant frames are often all named 'Platform=Desktop' and slug-collided across imports. - imported fragments carry data-hf-snippet and the project linter skips composition-root rules for them. - /figma skill documents the field-tested non-Enterprise tokens path (MCP get_variable_defs joined with REST boundVariables ids). Shared-helper extractions (injectScopedStyles, flattenedRoot module, parseHostVariableValues, rasterizeFallback, shapeCss) satisfy the dedup/complexity audit the runtime changes tripped. Validated live: brand-loop renders purple from the attribute alone (no manual :root); 118 figma + 662 runtime/compiler + 331 lint tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
80271863d3 |
fix(media-use): explain the codex alias-vs-PATH gotcha in the unavailable message (#2042)
When codex is a shell alias (e.g. codex → /Applications/Codex.app/...), a spawned subprocess's PATH lookup can't see it, so media-use reported the bare "codex CLI not on PATH" — which reads as "binary missing" and sent people hunting for a bad install. Spell out the actual cause (aliases aren't visible to child processes) and the one-line fix (symlink the real binary onto PATH). No behavior change; clearer agent/user guidance only. |
||
|
|
cebce603df |
fix(cli): pin arm64 render Chromium to Playwright headless-shell (#2039) (#2040)
The arm64 render image had no pinned browser: chrome-for-testing publishes no linux-arm64 build, so Dockerfile.render fell back to Debian bookworm's rolling `chromium` package. Its current arm64 build (150.0.7871.46) SIGTRAPs at startup (exit 133), breaking `render --docker` 100% on Apple Silicon. Install a pinned, non-Debian chrome-headless-shell from Playwright on arm64 (Google's build, not Debian's repackage). The wrapper wires whichever binary landed into PRODUCER_HEADLESS_SHELL_PATH and now fails the build loudly if neither is present, instead of silently using the broken Debian chromium. Bonus: arm64 gains BeginFrame deterministic capture it previously lacked. amd64 path is unchanged. Verified on Apple Silicon: same arm64 image, Debian chromium 150 -> exit 133, Playwright arm64 headless-shell (Chromium 149) -> exit 0. |
||
|
|
5b9b71df25 |
fix(producer): suppress GSAP call side effects during render seeks (#2037)
* fix(producer): suppress GSAP call side effects during render seeks * fix(core): preserve GSAP root render nudge safely |
||
|
|
c6408b620e |
Merge pull request #2038 from heygen-com/release/v0.7.42
chore: release v0.7.42v0.7.42 |
||
|
|
401dd1d27f |
fix: media-use bug-bash fixes (codex gate, id race, provider/reuse/adopt guards) + CLI unknown-flag rejection (#2033)
* fix(media-use): codex gate misfires as 'not logged in' when piped codexUnavailableReason() gated generation on parsing `codex login status` stdout, but that command prints 'Logged in using ChatGPT' to stderr and exits 0 — so the piped stdout media-use captures (execFileSync returns stdout only on success) was empty, and the gate falsely reported 'not logged in'. Every headless / CI / agent run was blocked from codex image gen even when fully authed. Gate on the durable credentials file ($CODEX_HOME/auth.json) instead of the TTY/stderr-only human text. Token validity is still proven by the exec, which fails cleanly on a stale login. The stdout `features list` capability check is unchanged. Verified: reproduced the false 'not logged in' block, then after the fix generated end-to-end via `resolve -t image --provider codex` (valid 1254x1254 PNG, source=generated, provider=codex.image_gen). * fix(media-use): bug-bash fixes — id race, provider/reuse/adopt guards From the bug-bash against main: - MU-23 (HIGH): concurrent resolves raced on nextId (read-max-then-append, non-atomic), so parallel agents got duplicate ids and clobbered each other's files. Add allocateId(): a coarse per-project lock (.media/.lock, 15s stale-steal) around id allocation that scans the manifest AND the type dir for reserved ids, then O_EXCL-creates a placeholder file so the slow download between allocate and append can't collide. 5 parallel resolves now yield 5 distinct ids + files. - X4: --reuse imported across a type mismatch (bgm asset under images/). Apply typesMatch on the --reuse path; reject mismatches (icon<->image still interchangeable). - X5: --provider silently overrode --local-only and made a network call. --local-only is now a hard guard: network providers are skipped even under a forced provider; the miss message explains the conflict. - BUG-2: --provider ignored the exact-cache floor and could hand back an asset from a different provider. A forced --provider now bypasses all reuse rungs (regenerate with THIS provider); the unforced floor is intact. - MU-26/X6: 0-byte assets accepted. --adopt skips 0-byte files (loud); ingest refuses a 0-byte local file (freezeUrl already rejects empty responses). - BUG-4: unknown/unavailable --provider now errors with the available list instead of a generic 'no provider could resolve' (typo != catalog miss). - BUG-5: --reuse "" gave the wrong 'type and intent required' error; it now routes to a clear empty-sha message. - BUG-3: voice duration leaked an unrounded float into index.md; round all durations to 0.1s centrally at record build (matches probe). - Nits: whitespace-only --intent is rejected; nudge grammar (exists/exist). Tests: allocateId reservation + registry local-only-wins added; full media-use suite green. All fixes verified e2e. * fix(cli): reject unknown flags instead of silently ignoring them citty is permissive: an unrecognized flag was dropped, not rejected — so `render . --out x` (the flag is --output/-o) silently ignored --out and rendered to the default renders/<name>.mp4 path. A mistyped flag read as a render/catalog miss. Add assertKnownFlags(): validate every dash-prefixed token against the command's declared args + aliases + the global set (help/version/json) before the command runs, in the shared trackCommandFailures run-wrapper so every leaf command is covered. Handles --flag=value, --no-<bool> negation, camelCase<->kebab arg names, and combined shorts; stops at --; positionals and flag values pass through. Verified: `render . --out x` -> 'Error: Unknown flag: --out'; --output/-o/ --json/--help still accepted. Unit tests added. * docs(skills): install with --full-depth so agents get current main The documented `npx skills add heygen-com/hyperframes` fetched the skills.sh registry blob, which lags GitHub main by hours — so users following the docs got a stale skill (e.g. media-use v1: no --candidates, voice stubbed). The CLI's own `hyperframes skills` command already forces a full clone via --full-depth to bypass this; the docs didn't pass it. Add --full-depth to every documented install command (README, CLAUDE.md, docs/guides/skills.mdx) with a one-line note on the lag. Addresses the user-facing half of the publish/registry lag (#2034). * chore(media-use): collapse resolve.mjs import to satisfy oxfmt --check * fix(cli): extract longFlagName to keep flag validator under complexity gate Also regenerate skills-manifest.json (resolve.mjs formatting change re-hashed the media-use skill). Fixes the Fallow audit + skills-manifest-in-sync CI gates. |
||
|
|
f6cd711bf0 |
chore: release v0.7.42
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
924727a0b4 |
feat(producer,cli): drawElement priority inversion — single-worker streaming over auto-parallel (#2026)
* feat(producer,cli): drawElement priority inversion — single-worker streaming over auto-parallel clamp:parallel eats 50% of local renders (1,326/fortnight; DE engagement stuck at 3.8%) by routing multi-worker renders to unverified screenshot capture. Benchmarks (2026-07-08, 4 comps x W1/W2/W3/W5) show that above the ~900-frame amortization crossover, single-worker VERIFIED drawElement streaming beats screenshot-parallel at EVERY worker count (2,380f: 66s vs 109-127s; 3,600f: 33s vs 39-56s; parallel scaling flattens past W2), while below it DE's fixed init cost loses by <=2.2s. - shouldPreferSingleWorkerDrawElement (exported predicate + 7 unit tests): inverts an AUTO-resolved multi-worker render to workerCount=1 when the comp matches the benchmarked configuration — default-on DE (darwin hardware clamp upstream), no compile gate, no forced-screenshot hint, mp4 output, single-worker streaming eligible, and totalFrames >= HF_DE_SINGLE_MIN_FRAMES (default 900; 0 disables). Explicit --workers N is always honored. - Inverted renders keep the probe session and land on the worker-encode streaming drain — the ONLY path with runtime self-verification, so this moves ~40% of previously-clamped renders onto the verified fast path. Comps that later hit an init-time gate (~1.5% of local renders) render single-worker screenshot streaming; accepted trade. - Telemetry: de_worker_inversion on render_complete (orchestrator -> perfSummary.workerInversion -> CLI), plus the worker_resolution observability checkpoint now records deWorkerInversion. Validation: e2e matrix on 2,381f comp — auto->5 workers inverted to 1, DE verified 4x inf PSNR, RENDER_OK; short comp (360f) auto stays 5-worker; explicit WORKERS=3 honored; HF_DE_SINGLE_MIN_FRAMES=0 disables. Canary suite 7/7 (PSNRs identical). renderOrchestrator tests 86/86. tsc/oxlint/oxfmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(producer): review fixes — inversion routing guards, calibration skip, retry revert Max code-review round on the inversion (13 confirmed findings): - Streaming spawn-failure disk fallback now clamps default-on drawElement (deClampReason=disk_path, DE-mode probe closed) exactly like the pre-capture clamp — previously it carried useDrawElement=true onto the unverified disk path, the hole the verified-path confinement exists to close, newly reachable for every inverted render. - Predicate gained the routing knowledge it was blind to: layered/HDR and shader-transition comps (drawElement never runs there), supersampling (deviceScaleFactor>1 init gate), a probe session whose init gates already disengaged DE, and the PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true explicit parallel-DE opt-in (honored like --workers N). - Eligibility is evaluated BEFORE capture calibration and skips it when the inversion pins workers to 1 regardless of the estimate — the throwaway calibration browser + sample captures cost ~41s on the 2,381-frame benchmark comp (auto render: 111.6s -> 70.1s total). - Self-verify retry reverts the inversion: the re-render returns to the pre-inversion parallel screenshot path (disk) instead of single-worker screenshot streaming, the slowest shape for exactly the comps drawElement damages. - HF_DE_SINGLE_MIN_FRAMES="" (set-but-empty) now falls back to the 900 default instead of aliasing the 0 kill switch. - Timeout advisory uses the RESOLVED worker count — an inverted render that times out no longer prints "Retry with --workers 1" (the configuration that just failed). - Telemetry: deWorkerInversion recorded in capture observability (failed renders are attributable), emitted as literal false when not fired (queryable denominator), and the drawElement perf input shape is one exported DrawElementPerfInput type instead of three copies. - Tests: requestedWorkers undefined (the value production actually passes) + the four new predicate guards; 91/91. Validation: e2e auto render — calibration skipped (deInversionEligible), inversion fires, DE verified 4x inf, total 70.1s (was 111.6s); HF_DE_SINGLE_MIN_FRAMES=0 restores calibration + parallel; canary suite 7/7 (PSNRs identical); tsc/oxlint/oxfmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(producer,cli): review round 2 — loss-cohort telemetry, retry-plan helper, boundary tests - de_worker_inversion is now a tri-state string ("inverted" | "reverted" | "none") instead of a boolean: the self-verify retry marks the render "reverted" rather than resetting to false, so the dashboard can segment the lost-inversion cohort first-class instead of inferring it from deSelfVerifyFallback + frame-count joins (james-russo #1). - The retry rollback is extracted to resolveInversionRetryPlan (pure, exported) with unit coverage: pre-inversion worker-count restore, streaming re-resolution (multi-worker retry -> disk), "reverted" state, null when never inverted (james-russo #2). - WOULD_RESOLVE_MULTI_WORKER named constant replaces the bare sentinel 2 (james-russo #5); minFrames: -1 boundary case added (miga #3). 94/94 renderOrchestrator tests; tsc/oxlint/oxfmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(producer,cli): emit de_pre_inversion_workers for the parallel counterfactual The ramp-down decision needs "did DE beat the parallel render it displaced", not just "did DE beat single-worker screenshot". Emitting the worker count the auto-resolution chose BEFORE the inversion pinned it to 1 makes the parallel counterfactual computable per render (screenshot ms/frame from the verify samples / W x the measured parallel-efficiency curve). Set only when the inversion fired. Smoke: 2,381f auto render -> de_worker_inversion="inverted", de_pre_inversion_workers=5, mode=drawelement, verify armed 4. 99/99 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4b3c73d941 |
fix(cli): upgrade and update notice use the detected install method
* fix(cli): upgrade + update-notice use the detected install method
hyperframes upgrade hardcoded 'npm install -g', so bun/pnpm/brew users either
saw it fail or silently got a shadowed npm copy while their real (older) binary
kept running. Route the install through detectInstaller() via a new
installInvocation() argv helper; for skip kinds (ephemeral npx/bunx,
project-local, unknown) print 'npx hyperframes@latest' instead of guessing.
The passive update notice now shows the detected manager's command too. Semver
safety guard consolidated into a shared isSafeVersion(). Suppression gates and
the background auto-update flow are unchanged.
* test(cli): pin the shell:false contract of the --yes install path
Export runDetectedInstall and add a mocked-execFileSync test asserting the
detected manager binary is spawned with the exact installInvocation argv,
{stdio:inherit, shell:false}, and that an install failure sets a non-zero exit
code without throwing. Addresses review nit on the untested --yes path.
* fix(cli): guard the registry version at the boundary; execFile the auto-installer
Security (addresses review): a poisoned registry data.version (e.g.
'1.2.3; rm -rf /') was cached unvalidated and flowed into the background
auto-updater, which ran it via exec() -- a shell -- so a registry compromise
meant RCE on the next CLI run. isSafeVersion only covered the two touched
consumers (upgrade, notice), not this third sibling (scheduleBackgroundInstall).
- Guard at the registry boundary in checkForUpdate: only a strict-semver STRING
is trusted; a non-string or metachar-bearing data.version is never cached and
falls back to the last known-good version. The cache-read and fallback paths
re-validate too, so a pre-existing poisoned cache can't leak through. One gate
closes all three consumers and any future one; per-consumer checks stay as
defense in depth.
- The detached auto-installer now runs via execFile(bin, args, shell:false),
reusing installInvocation, matching the interactive runDetectedInstall path --
the shell is gone from that path entirely.
Tests: reject poisoned / non-string registry version (never cached); accept a
valid semver.
|
||
|
|
4a36655b2b |
fix(engine): resolve relative data-start references in video-frame extraction
* fix(engine): resolve relative data-start references in video-frame extraction <video data-start="intro"> (a relative reference to another clip's end) is resolved by the browser runtime but parseVideoElements/parseImageElements did a raw parseFloat, yielding NaN start/end. The FrameLookupTable active-window checks (start <= t <= end) are then always false, so the clip is never injected and composites BLANK in the final render — while lint/validate/inspect/snapshot and the live preview all look fine. The docs' Relative Timing section teaches exactly this pattern on <video>. Share the pure reference-syntax parser (parseStartExpression) out of the runtime resolver into @hyperframes/core, and resolve references in the extractor against the linkedom document it already holds: a reference resolves to the target clip's resolved start + its duration (data-duration or data-end) + offset, mirroring the runtime. Cycle-guarded; an unknown target or unknown duration falls back to the target's start / 0 (never NaN), matching runtime semantics. Natural-media-duration-only targets aren't known at parse time (same limit as the runtime's fallback). parseImageElements gets the same fix. Runtime resolver behavior is unchanged (its 25-case suite still passes). * chore: re-trigger CI to refresh a stuck CodeQL aggregate check |
||
|
|
42a209545b |
fix(media-use): create the output dir before ElevenLabs TTS writes
The ElevenLabs provider spawns a Python helper that writes straight to wavAbs
via a bare open(), which (unlike heygen/kokoro) never creates the parent dir —
so on a fresh project the save throws ENOENT and the line is silently dropped
as 'TTS failed - omitted'. mkdir -p the dir first, guarded so a mkdir failure
(EACCES/EROFS) returns { ok:false } like the rest of the branch rather than
throwing. (Migrated from #1960, whose skills/hyperframes-media path was retired
into skills/media-use; the bug moved with it.)
|
||
|
|
00b96d2eaa |
fix(producer): fall back to copying extracted frames when symlink hits EPERM
* fix(producer): fall back to copying extracted frames when symlink hits EPERM materializeExtractedFramesForCompiledDir stages each video's extracted frames into the compiled dir via a single symlink (the in-process renderer's default; distributed plan() already copies via materializeSymlinks). Windows without Developer Mode (or Administrator) cannot create symlinks and rejects with EPERM, so high/standard-quality renders failed there — while draft quality worked because it avoids the symlinked-cache path entirely. Fix: a new stageExtractedFrameDir helper catches EPERM/EACCES from symlinkSync and falls back to the same recursive cpSync the materializeSymlinks path already uses. The extra disk is far better than a hard render failure on a default Windows configuration. Non-permission errors (ENOSPC, etc.) still propagate so real failures aren't masked as silent copies. Extracting the helper also keeps the main function under the complexity gate. Test: two new cases via the injected fileSystem — symlinkSync throwing EPERM triggers exactly one recursive cpSync (frames still remapped under compiledDir), and a non-permission error (ENOSPC) rethrows without falling back to copy. Full renderOrchestrator suite (81) passes. * fix(producer): recover from a stale dangling frame-symlink (EEXIST) Follow-up to this PR's EPERM copy fallback, from a further Windows report: the symlink fails with EEXIST after the extraction cache is GC'd. A prior render's symlink at the compiled linkPath dangles once its target is removed; the caller's existsSync() guard follows the dead link and reads it as absent, so staging runs again, but the link file still exists and symlinkSync collides with EEXIST -> the render hard-fails. Catch EEXIST in stageExtractedFrameDir, clear the stale entry (rmSync), and re-stage (link, or copy on EPERM/EACCES). Factored the link-or-copy into a helper reused by both the first attempt and the retry. rmSync is an optional injected fs method (default fs supplies it; only the EEXIST path calls it). New unit test covers the dangling-symlink recovery. * fix(producer): widen symlink fallback to UNKNOWN and cover EEXIST on copy path Addresses review nits on the frame-staging fallback: - Widen the symlink no-privilege catch from EPERM/EACCES to also include UNKNOWN (some Windows builds surface a symlink privilege denial as UNKNOWN). - Wrap the EEXIST stale-entry recovery around BOTH staging branches, not just the symlink one: after #2025 Windows uses the eager cpSync path, which can collide with a dangling symlink left by a prior Linux run — now it clears the stale entry and re-stages either way. - Emit a one-time INFO log when symlinking degrades to copying, so a heavier Windows render is self-explanatory. |
||
|
|
1aa39d4653 |
fix(producer): copy extracted frames on Windows to avoid symlink EPERM
The local render path (renderOrchestrator → runExtractVideosStage) materialized each video's extracted frames into the compiled dir via symlinkSync, with no materializeSymlinks flag. On Windows without Developer Mode/Administrator, symlinkSync throws EPERM, so local video renders failed at the video_extract stage (users worked around it with materializeSymlinks patches / snapshot-frame hacks). The distributed plan() path already copies (materializeSymlinks: true). Pass materializeSymlinks: shouldCopyExtractedFrames(process.platform) at the local caller — copy on win32 (symlinks unavailable), symlink elsewhere (cheap). New pure shouldCopyExtractedFrames() helper + unit tests. |
||
|
|
de27b46680 |
fix(cli): default render fps to the composition's data-fps
* fix(cli): default render fps to the composition's data-fps
hyperframes render hard-coded fps to 30 when --fps was omitted, ignoring a
data-fps declared on the composition root — so a composition authored at
data-fps="24" silently rendered at 30fps unless the user knew to pass --fps 24.
The runtime already honors data-fps; the CLI now matches it.
Precedence: explicit --fps > composition root data-fps > 30. New pure
readCompositionFps() extracts the root data-fps via linkedom (mirrors the
runtime's root resolution: [data-composition-id][data-root=true], else the
outermost [data-composition-id]); render validates it through parseFps and
falls back to 30 on an absent/invalid value. Unit-tested.
* fix(cli): honor composition data-fps on cloud renders and --composition targets
The local render command read data-fps from project.dir/index.html even when
--composition rendered a different file, and the lambda/cloudrun render paths
ignored data-fps entirely (hardcoded ?? 30). Both are the same silently-wrong-
fps bug on other render entry points:
- render.ts resolves the entry file first, then reads data-fps from the file
actually being rendered (falling back to index.html).
- lambda render/render-batch and cloudrun render/render-batch default fps from
the composition's data-fps, accepted only when it is one of the cloud-allowed
values {24,30,60}, else the existing 30 default. Explicit --fps still wins.
* fix(cli): drop citty fps default so data-fps resolution actually runs
The fps arg had default: "30", so citty set args.fps="30" on omission and
resolveDefaultFpsArg short-circuited (explicitFps never null) — reverting the
command to always-30 and making the whole data-fps feature a no-op (caught in
review). Remove the arg default; the "30" fallback already lives at
parseFps(fpsArg ?? "30"). Adds a regression guard asserting the arg has no
default.
* test(cli): read citty args through a plain record in the fps-default guard
The regression guard accessed cmd.args.fps directly, but citty types args as
Resolvable<ArgsDef> so .fps failed typecheck in CI. Read it through a plain
record cast.
|
||
|
|
92f3116dee |
fix(cli): re-download the browser when the cached archive is corrupt
A partially-downloaded or interrupted chrome-headless-shell archive left in the cache makes @puppeteer/browsers' install() throw "invalid end-of-central-directory" during extraction. That error propagated out of the browser check and hard-blocked the render, forcing users onto the fallback renderer until they manually cleared the cache — a recurring Windows failure. Detect the corrupt-archive extraction error (isCorruptArchiveError), clear the cache to drop the bad archive, and retry the download exactly once; non-corrupt errors and a second corruption still propagate (no infinite retry). The pure predicate and the recovery wrapper are unit-tested. |
||
|
|
4834de37f4 |
fix(cli): lint sets process.exitCode instead of process.exit() to flush stdout
`hyperframes lint --json` wrote the JSON payload with console.log() and then immediately called process.exit(). process.exit() terminates the process before Node flushes an asynchronously-buffered stdout, which is what a non-TTY (piped) stdout is — so `hyperframes lint --json | tee`, `> out.json`, or any agent/CI capture silently lost the entire payload on Windows (reported on 0.7.31 non-TTY). The same console.log-then-exit pattern was on all four exit sites (both --json branches and the human-readable + thrown-error paths), so any of them could truncate. Fix: set process.exitCode and return, letting run() unwind so Node drains stdout before exiting with the code. This is exactly the pattern the other commands (publish/transcribe/upgrade/play/present) already use; lint was the outlier still calling process.exit() after writing. Test: new lint.test.ts drives the command's run() with mocked lintProject/resolveProject and a process.exit spy that throws if called. Covers the --json-with-errors, --json-clean, --json-thrown, and human-readable paths — each asserts process.exit is never called and the correct exitCode is set. Fails against the pre-fix code (the spy throws on the first process.exit). |
||
|
|
ce175ebe98 |
Merge pull request #2031 from heygen-com/release/v0.7.41
chore: release v0.7.41v0.7.41 |
||
|
|
60463d0bd5 | chore: release v0.7.41 | ||
|
|
7c0b9e5d5a |
Merge pull request #2029 from heygen-com/feat/media-use-agentic-reuse
feat(media-use): agent-driven asset reuse (candidates + reuse) |
||
|
|
7286b005b0 |
Merge pull request #2028 from heygen-com/worktree-fix-media-use-issues
fix(media-use): forgiving prompt matching + precise assets/ scan |
||
|
|
35e54cae19 |
feat(media-use): agent-driven asset reuse (candidates + reuse)
Reuse now hands the semantic judgment to the coding agent instead of a string heuristic, while keeping the deterministic normalize-exact match as an automatic dedup floor. No LLM/embedding call enters resolve; it stays offline-capable. - lib/match.mjs: shared matchTokens + typesMatch (extracted from adopt.mjs and resolve.mjs so the icon<->image equivalence and token rules can't drift between the do-path and the look-path); adds tokenOverlap ranker. - resolve --candidates: side-effect-free listing of reusable assets across the project manifest AND the global ~/.media cache, ranked by lexical overlap, capped per scope, --json or human table. Never hard-filters on zero overlap (that would pre-empt the agent's judgment); the agent decides. - resolve --reuse <sha>: import a specific global-cache asset by content sha/prefix (from --candidates) into the project via importFromCache, marked source=reused-explicit / provenance.reused_by=agent. - Adherence nudge: on a resolve that misses the exact floor and is about to fetch, print a one-line stderr hint when similar cached assets exist, pointing at --candidates. Offline, stderr (safe under --json), never auto-reuses a fuzzy match. - cache.mjs: export readGlobalManifest; add findGlobalBySha (prefix resolve with ambiguity/miss handling). - Telemetry: media_use_candidates event + reused-explicit source on media_use_resolve (type/scope/counts only, no intent text or paths). - SKILL.md: 'Reuse before you resolve' guidance + trust guardrail (prefer-fresh-when-unsure, entity-exact for brand, cross-project bleed). - Tests: lib/candidates.test.mjs (ranking, no-hard-filter, cap/truncation, icon<->image, sha resolution, formatter); adopt.mjs refactor covered by existing lib/adopt.test.mjs. Full media-use suite green; verified e2e against the live catalog (cross-project resolve->candidates->reuse; hint fires on miss). |
||
|
|
76204ec630 |
fix(engine): pre-create __render_frame__ siblings in initializeSession (#2006)
* fix(engine): commit render-frame siblings with a visual BeginFrame at init Chunk-lambda renders drop a periodic near-black frame — one every chunk_frames/worker_count frames (every 60 on a 4-worker single-video chunk), YAVG ~22 against YMAX ~240 in signalstats. Local single-process renders don't show it because they don't run under BeginFrame. It's the isNewImage branch in injectVideoFramesBatch: the first time a session paints a given videoId there's no __render_frame__ sibling yet, so it creates the <img> on the spot (createElement + insertBefore) right before capture. Under HeadlessExperimental.BeginFrame the compositor doesn't have that fresh layer in the immediately-next frame, so the first captured frame per session paints only body background + already-composited overlays. Each lambda worker is its own session, hence the worker-boundary periodicity. Pre-create the hidden sibling at the end of initializeSession, then drive one non-capture visual BeginFrame (noDisplayUpdates: false) to composite the new layers before the first real capture. The warmup ticks are noDisplayUpdates: true (they advance the clock but don't paint) and the per-frame seek doesn't tick, so this explicit visual frame is what actually commits the layers; its tick sits in the gap between warmup and frame 0 so ticks stay monotonic and no render frame is consumed. Every subsequent inject then takes the hasImg=true (src-update) path; the isNewImage branch stays as a fallback for callers that don't go through initializeSession. * fix(engine): place the render-frame commit tick before the liveness probe The commit tick at init sends its BeginFrame at `beginFrameTimeTicks - 1·interval`. The producer's liveness probe then fires right after init at `beginFrameTimeTicks - 5·interval` — an earlier tick. Per-session BeginFrame time has to be monotonic, so the probe running backwards past the commit tick stalls chrome-headless-shell indefinitely; the engine reads that timeout as a SwiftShader heavy-layer stall and routes the render to screenshot capture, which then dies relaunching and hangs the shard to the job timeout. Reproduced on a native x86 SwiftShader host and bisected: with the commit tick present the probe times out even with zero render-frame siblings created, so it's the tick ordering, not layer count. Moving the commit tick to `-6·interval` (below the probe, above the warmup ticks) keeps warmup < commit < probe < capture monotonic and clears the stall on every affected comp — sub-composition-video, chat, style-5-prod — while a healthy comp (style-18-prod) is unchanged. The commit tick itself is untouched, so the black-frame fix it exists for still holds. |
||
|
|
b5383ded42 |
fix(media-use): forgiving prompt matching + precise assets/ scan
Two defects in the resolve cascade that made cache and asset-reuse misbehave in practice: - Prompt matching was byte-exact and case-sensitive. findByPrompt and cacheGet compared provenance.prompt with ===, so "Calm piano" and "calm piano" re-searched and re-downloaded instead of reusing the cached asset (same project and cross-project). Add normalizePrompt (trim + lowercase + collapse whitespace) and key both lookups on it; the raw prompt is still stored for audit. - findExistingAsset matched with name.includes(intent) || intent.includes(name), which silently returned the WRONG local file: intent "whoosh" grabbed a stray who.mp3, and a one-letter filename matched every intent. Require a shared word token (>= 3 chars, minus stopwords) so a false negative just falls through to a catalog search rather than shipping the wrong asset. Adds lib/adopt.test.mjs and extends manifest.test.mjs. Full media-use suite green; verified e2e against the live catalog (case-variant cross-project resolve now reuses; whoosh no longer grabs who.mp3). |
||
|
|
2bd9bb6f69 | chore: release v0.7.40 (#2024) v0.7.40 | ||
|
|
4c8064d4d3 |
fix(cli): figma component import survives unrenderable nodes (#2022)
Live testing against a real community file (Ratings) found a nested instance node figma refuses to render as svg — which aborted the entire component import. The rasterize loop now retries the node as png, and only if both formats fail warns and skips THAT node (placeholder keeps its data-figma-rasterize marker, no src) instead of failing the import. On the file that surfaced this, the png retry recovers the node — 31/31 placeholders get assets. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
337d0b51bc |
refactor(engine,producer): adopt requestPaint contract, retire autoAlpha rewrite (#2021)
* refactor(engine,producer): adopt requestPaint contract, retire autoAlpha rewrite crbug 529829538 was closed "working as intended": the html-in-canvas API's contract is mutate -> canvas.requestPaint() -> await the canvas paint event -> drawElementImage, which refreshes the subtree's paint records including compositor-applied properties. Verified on the pinned 151 floor and 152 canary: root opacity, root filter, nested group opacity, and child transforms (incl. will-change-promoted) all capture exactly; the root element's own TRANSFORM is the one property still never baked. - Paint invalidation: all three paint-wait sites (serial capture, worker produce, batch produce) now call canvas.requestPaint() when available and fall back to the __hf_de_tick sentinel background toggle on builds without it. The 250ms unsynchronized-draw safety net is unchanged. - Root-opacity ratio correction REMOVED (all three draw sites + base-opacity recording at injection). Since 151 the paint wait bakes current root opacity into the snapshot as pixel alpha, so the ratio correction DOUBLE-APPLIED animated root fades: a root-fade A/B tripped the runtime self-verify at 30.1dB (frame 24, ~0.92 expected vs ~0.85 rendered). Post-removal the same comp self-verifies at inf and matches the screenshot render at PSNR=inf. The root TRANSFORM correction stays — verified still required. - autoAlpha rewrite machinery DELETED: the opt-in opacity->autoAlpha tween rewrite (default-off since the retraction fix; measured ~28dB damage on comps whose fades it touched), its flush-time transparent-target hiding, the __HF_FAST_CAPTURE_AUTOALPHA__ flag plumbing, and the deferral-time retract/re-assert dance. The stub keeps tween-target tracking (3D projection + at-risk scans depend on it). Validation: canary suite 7/7 with PSNRs identical to baseline (58.30 / 43.13 / 54.15 dB); root-fade A/B PSNR=inf vs screenshot; engine suite 905 passed (1 pre-existing color-grading failure); tsc/oxlint/oxfmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(engine,producer): review fixes — gate opacity correction by paint mechanism Max code-review findings on the requestPaint adoption: - Root-opacity ratio correction RESTORED, gated per frame on how the paint was produced: it applies on BeginFrame (sync=false) captures and on builds without canvas.requestPaint() — the two paths where the snapshot holds the root's load-time opacity — and is skipped only on requestPaint-driven paints, where the snapshot bakes the current opacity and the ratio double-applies (the proven 30.1dB root-fade failure). Base opacity is recorded at injection again. - Invalidation extracted to a page-scope helper (__hfDeInvalidate, installed by injectDrawElementCanvas) shared by all three paint-wait sites: sentinel toggle ALWAYS (a paint is guaranteed even if requestPaint elides one on a clean subtree) + requestPaint() in a try/catch (a throwing implementation degrades to sentinel-only instead of rejecting the capture). Returns whether requestPaint ran, feeding the opacity-correction gate. Also removes the triplicated inline block and its three anonymous `as T` casts. - HF_FAST_CAPTURE_AUTOALPHA now logs a retirement warning instead of being a silent no-op (the deleted rewrite's comment documented it as an operator escape hatch). - Batch producer docstring updated (still described the tick-toggle-only paint wait); stub tween observer reshaped to a void fn (observeTweenCall) so no arg-rewriting seam survives. Validation: canary suite 7/7 (58.30/43.13/54.15dB, d95f20b6 clean); root-fade A/B self-verify 4x inf + whole-video PSNR=inf; engine suite 905 passed (1 pre-existing); tsc/oxlint/oxfmt clean; stub regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: WaterrrForever <miao.yang@heygen.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
574da2b215 |
fix(studio): off-canvas indicators track live layout instead of going stale (#2018)
The dashed indicators for elements outside the canvas stayed pinned at an element's old position after a move or a seek: they were recomputed by an effect keyed on compRect/activeCompositionPath, neither of which changes on an in-place soft-reload edit or a playhead seek. They now recompute via a MutationObserver on the preview document (coalesced to one recompute per frame), so they follow the element live. Crop-hugging is preserved. Adds a regression test that mutates an element in place and asserts the indicator moves. |
||
|
|
037266e72b |
feat(studio): timeline revamp with active-clip highlighting and hide controls (#2017)
Timeline UI - Highlight clips visible at the playhead in the primary color; others share one neutral color - Minimalist rounded clips, single-color track rows, no gutter icons or superscript labels - Per-track eye toggle and a per-element hide button in the design panel - Ruler zoom fixes: sub-second tick intervals and correct label formatting at high zoom - Sticky gutter so track controls stay visible while scrolling WYSIWYG visibility (data-hidden) - Runtime honors data-hidden (display:none), so hiding affects the render, not just the preview - HTML stays the source of truth; hide state persists and round-trips on reload Split several studio files to stay under the 600-line cap; pure relocations, no behavior change. |
||
|
|
5d59835446 |
fix(studio): static-position drag no longer freezes an element beside an animated rotation (#2016)
## What Brief description of the change. ## Why Why is this change needed? ## How How was this implemented? Any notable design decisions? ## Test plan How was this tested? - [ ] Unit tests added/updated - [ ] Manual testing performed - [ ] Documentation updated (if applicable) |
||
|
|
306a291dea |
fix(skills): audit descriptions — trim routing prose, fix stale facts, add missing triggers (#1990)
* fix(skills): audit descriptions — trim routing prose, fix stale facts, add missing triggers Descriptions are the always-loaded routing tier; this audit rebuilds them on one principle: discriminate by input shape, not pipeline internals. - Trim creation-workflow descriptions to positive trigger + nearest-neighbor disambiguation + /hyperframes escape hatch; full routing prose already lives in each skill body's route-confirm block and the router - Codify the workflow-vs-domain split as ownership (owns the end-to-end deliverable vs capability layer pulled in mid-flight) in /hyperframes, and widen "make me a video" framing to deck / composition port - Fix stale facts: embedded-captions identity count (desc 32, body 17 → actual 36 = 10 classic + 26 themed), six→ten visual languages, retired RVM/Standard wording in router details, figma shader transport (MCP → MCP source / native export), keyframes "cursor demos" (no backing content), hyperframes-media scripts/audio.mjs leak - Register missing capabilities: motion-graphics maps category (was in categories/ but absent from its own table, description, and router), asset-fusion + news triggers, slideshow page-to-deck + presenter mode, general-video editing, talking-head-recut 16:9/9:16/4:5 canvas, cli feedback + lambda sites, product demos, mood-brief BGM generation - website-to-video: relabel promo-shaped video types to keep the promo boundary with /product-launch-video; drop headless-Chrome wording - music-to-video: lyric timing via /hyperframes-media transcription or user-supplied lyrics, placed on the beat grid - Sync catalogs in lockstep (CLAUDE.md, AGENTS.md, README, docs/guides/skills.mdx, CLI project templates): add music-to-video + slideshow entries, complete the domain-skill lists, and extend the catalog-maintenance rule to cover AGENTS.md and the templates Validated with a 35-case description-only routing eval: 35/35 both before and after the rewrite (including new maps / asset-fusion / news probes). * fix(skills): post-media-v2 consistency — stale media ref, router figma wording, catalog rows - music-to-video: lyric transcription now routes to /media-use (the retired /hyperframes-media was still referenced) - router capability map: figma row gains the shaders fact (MCP source / native export), matching the SKILL.md source of truth - media-use catalog rows (CLAUDE.md, README, docs/guides/skills.mdx): add image models + captioning, aligning with the v2 description - catalog rule #1: root AGENTS.md carries the workflow list only (it has no domain-skill section) — rule wording now says so |
||
|
|
3d8372f880 |
feat(cli): associate signed-in HeyGen account with telemetry (#2020)
* feat(cli): associate signed-in HeyGen account with telemetry Sign-in telemetry currently attributes everything to the anonymous install id, so the sign-in funnel can be counted but a completed sign-in can't be tied to the account it produced. This associates the two. - On a completed sign-in, emit a PostHog `$identify` alias whose `$anon_distinct_id` is the install's anonymousId, so events recorded before sign-in stitch to the same person, and tag `auth_login_completed` with the account identity (the pre-plumbed `distinctId`). - `/v3/users/me` exposes no opaque user_id, so the identity key is the account email, falling back to username (single `identityKey` helper). - Both no-op under the `telemetry disable` opt-out and only fire after the user chooses to sign in. Privacy disclosure updated in lockstep, since this is the first PII the CLI attaches: the first-run telemetry notice and the telemetry section of docs/packages/cli.mdx now state that signing in links your account email to your usage. Tests: identifyUser payload + no-op, completion attribution incl. username fallback and no-identity-on-reject/empty. Verified end-to-end against the built CLI: pre-auth events anonymous, $identify carries $anon_distinct_id, completion carries the account email. * docs(cli): disclose the username identity fallback Review gating item: identityKey is `email ?? username`, but the first-run notice and cli.mdx said only "email", so an emailless account's username would reach PostHog undisclosed. `/v3/users/me` treats email as optional (pickString), so the fallback is live code, not dead — disclose it rather than assert an unverifiable email guarantee. Both surfaces now say "email, or username if the account has no email". Also soften the identityKey comment: it implied username is "less identifying", but HeyGen usernames are often email-shaped, so the note now states username is a fallback, not a privacy win. |
||
|
|
229e88eac5 | chore: release v0.7.39 v0.7.39 | ||
|
|
b26c27576b |
feat(engine,producer,cli): verify video comps via deferred DE init + capture p50 (#2015)
* feat(engine,producer,cli): verify video comps via deferred DE init + capture p50 Closes the two biggest gaps in the first day of v0.7.38 wild data: 88% of drawElement renders (video comps initialized via probe sessions) ran with self-verification unarmed, and speedup was measurable on only 3 of 76 renders. - Deferred drawElement init: probe sessions initialize before video extraction, so they have no frame injector — ground-truth screenshots would capture black <video> boxes, and verification skipped the whole comp. DE init now stops after the gates for injector-less video comps (deInitDeferred; autoAlpha flag retracted in case no path completes it) and completeDeferredDrawElementInit finishes verification + canvas injection + worker-encode at capture time, once prepareCaptureSessionForReuse has attached the injector. Validated end-to-end: a probe-path video comp now arms 4 ground-truth frames with real video pixels (3x inf + 64.7dB) and renders drawElement verified. - capture_p50_ms: per-frame capture durations are sampled (capturePerf.frameMs; batch frames get the batch mean) and the median ships as CapturePerfSummary.p50TotalMs -> RenderPerfSummary.captureP50Ms -> render_complete capture_p50_ms. Unlike capture_avg_ms it is immune to first-frame warmup and stage-setup amortization — smoke: avg 15ms vs p50 8ms on the same render, p50 matching the measured steady-state floor. Dashboard speedup tiles can drop their frame-count floor once this ships. - video_count on render_complete: segments speedup by video-injection comps (whose per-frame gain is legitimately lower) vs pure-graphics. Canary suite 7/7; engine suite 905 passed (1 pre-existing upstream failure); tsc/oxlint/oxfmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(producer): complete deferred drawElement init on the disk capture path Review (miga): a probe-initialized video comp falling back to the disk path kept deInitDeferred and silently stayed in screenshot mode — a regression for PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true renders that previously ran drawElement there. Complete the deferred init on the sequential disk path under the same explicit-opt-in test the orchestrator clamp uses; default-on renders stay on the screenshot baseline (this path has no drain-time self-verification, per the #1998 confinement rule). Validated: video comp + PRODUCER_ENABLE_STREAMING_ENCODE=false + explicit opt-in logs "(deferred drawElement init)" completion on capture_disk and renders correct video pixels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
47276edcc6 |
feat(skills): align easing docs with motion doctrine, add baked springEase (#1989)
* feat(skills): align easing docs with motion doctrine, add baked springEase The easing adapter contradicted the workflow doctrine: it taught a power2.out entrance default and pitched back/elastic as playful defaults, while motion-language.md says power3 / smooth-beats-bouncy. A worker following the adapter produced exactly the flat, cheap motion users complain about. - gsap-easing-and-stagger: power3.out becomes the documented house default; back/elastic/bounce capped as RARE playful-only; new "Spring Eases (baked physics, seek-safe)" section — closed-form damped-spring ease springEase(response, dampingFraction), measured damping ladder, response/duration table, craft notes - gsap-timeline-and-labels: last leftover power2.out default -> power3 - spring-pop-entrance: exact-physics option (zeta=1) for the settle; playful variant now prefers spring zeta 0.6-0.7 over back.out - motion-language x3 (product-launch-video, faceless-explainer, pr-to-video): doctrine "Smooth beats bouncy" wired to the baked springEase — real physics, same doctrine, not a license for bounce Verified with node + GSAP 3.15: zeta=1 strictly monotone with zero overshoot; scrambled out-of-order seeks return bit-identical values; ease(0)=0 and ease(1)=1 exact; snippet re-extracted from the published markdown and re-run. * chore: format README.md (landed unformatted on main; unblocks the repo-wide Format check) |
||
|
|
a1abd57a60 |
Merge pull request #2010 from heygen-com/fix/studio-selection-outline-only
fix(studio): remove selection overlay fill |