Skip to content

Sync upstream PR #549 + upstream/main, Studio polish (HOOK_BIGTEXT, chromatic-glow), surface catalog#42

Open
cuio wants to merge 179 commits intomainfrom
claude/interesting-nightingale-d35fa6
Open

Sync upstream PR #549 + upstream/main, Studio polish (HOOK_BIGTEXT, chromatic-glow), surface catalog#42
cuio wants to merge 179 commits intomainfrom
claude/interesting-nightingale-d35fa6

Conversation

@cuio
Copy link
Copy Markdown
Owner

@cuio cuio commented May 6, 2026

Summary

Three layered phases to bring the fork onto upstream/main and ship the concrete Studio polish work from the studio-polish-and-cinematic-aesthetic.md handoff.

1. Skill sync — e45c91c

Pulls upstream PR heygen-com#549's design.md-first architecture: renumbered 1/2/3 flow, 6 new shared references (beat-direction, design-picker, narration, prompt-expansion, techniques, video-composition), package-loader.mjs, design-picker.html template. Fork-only files preserved (chart-pack data-in-motion.md, visual-styles.md, retention-ladder.md, retention-overdrive.md, plus transcript-guide.md / tts.md since this fork has NOT adopted upstream's hyperframes-media skill split).

2. Upstream merge — 3236a19

Pulls 96 upstream commits / 386 files. 11 conflicts resolved as deliberate unions:

  • bun.lock, packages/{cli,core}/package.json — dep unions, lockfile regenerated
  • skills/hyperframes/SKILL.md — re-merged Phase 1 fork entries against post-PR-feat(skills): design.md integration, shared video references, Claude Design gaps heygen-com/hyperframes#549 upstream cleanup
  • packages/cli/src/help.ts — fork's el-tts / script / score / optimize / images preserved alongside upstream's new remove-background
  • packages/cli/src/commands/render.ts — fork's logRenderCost + upstream's exit-after-complete / scheduleRenderProcessExit
  • packages/core/src/studio-api/createStudioApi.ts — fork's elevenlabs/anthropic/script/costs/images/storyline routes + upstream's new waveform route
  • packages/studio/src/player/hooks/useTimelinePlayer.ts — kept upstream's createTimelineElementFromManifestClip + findTimelineDomNodeForClip refactor, re-attached fork's data-timeline-group passthrough as post-processing
  • packages/studio/src/player/components/Timeline.test.ts — fork's computeEffectiveTimelineDuration + deriveTimelineLaneLabel suites kept alongside upstream's formatTimelineTickLabel suite
  • packages/studio/src/components/sidebar/LeftSidebar.tsx — kept fork's mode-driven 7-tab system + fullscreen-expand, accepted upstream's onToggleCollapse as additional optional prop
  • packages/studio/src/App.tsx — union: fork's Direct/Edit toggle + sidebar-collapse + Timeline visibility toggle restored alongside upstream's Capture frame link (auto-merge had silently dropped the Timeline button); states unioned; LeftSidebar receives both mode and onToggleCollapse

Brings: bundler runtime fixes (heygen-com#641), audio-desync-on-scrub fix (heygen-com#639), shader-transition cache (heygen-com#634), SDR shader transitions (heygen-com#640), PSNR sampling fix (heygen-com#627), --background-output flag (heygen-com#637), v0.4.45 release, PR heygen-com#546 Studio Design panel scaffolding, PR heygen-com#495 + heygen-com#496 docs reorgs.

3. Studio polish — 37e7a5a

Three of the four handoff items:

  • HOOK_BIGTEXT letter-cascade safety net — fixes the dropped-letters bug. .hb-letter defaults to opacity:1 (graceful degradation), pre-cascade tl.set(letters, {opacity:0,y:50}, 0) so seeks observe a real tween, defensive final-frame snap at sceneDur - 0.01 so the last captured frame always has every letter visible. 8-test regression suite pins the structure.
  • chromatic-glow atmosphere preset — Vision-Pro / Apple-keynote spotlight halo + chromatic-aberration ring. Pure CSS, scoped per sceneId, intensifies for hooks, uses the active theme's accent. Opt-in only — defaultAtmosphereForTemplate does not yet route any template to it, so existing renders are unchanged. Foundation for the cinematic-card upgrade. 8-test regression suite.
  • Catalog reference docskills/hyperframes/references/catalog.md groups the 44 blocks + 3 components + 8 examples by purpose (social/UI mockups, hooks/openers, logo/outro, CSS transitions, shader transitions). SKILL.md links to it so an authoring agent picks the right block to hyperframes add before scaffolding empty.

Deferred: the fourth handoff item — intermittent Timeline editor flicker on play / tab-switch. Three hypotheses on file (cleanup race, ResizeObserver/layout-effect timing, missing min-height: 0) but no confirmed repro path. Held back until a fresh repro session.

Verified

  • 1164 / 1164 core tests passing (was 1148; +16 new tests from HOOK_BIGTEXT and chromatic-glow regression suites)
  • 257 / 257 cli tests, 331 / 331 studio tests = 1752 total passing
  • tsc clean across cli / core / studio
  • oxlint + oxfmt clean across all changed files
  • npx tsx packages/cli/src/cli.ts --help lists every command including fork-only el-tts / script / score / optimize / images and upstream's new remove-background

Test plan

  • bun install && bun run --cwd packages/core test && bun run --cwd packages/cli test && bun run --cwd packages/studio test
  • npx tsx packages/cli/src/cli.ts catalog lists all 44 blocks
  • Render a project with a HOOK_BIGTEXT scene + long headline — confirm no letters dropped at any frame
  • Assemble a scene with atmosphere: "chromatic-glow" — confirm halo renders with active theme accent
  • npx hyperframes preview — Studio header has Direct/Edit + sidebar + Capture + Timeline + Renders, all functional

🤖 Generated with Claude Code

ukimsanov and others added 30 commits April 25, 2026 17:02
- Link to claude.ai/design instead of claude.ai
- Remove raw.githubusercontent download links (just GitHub with ↓ button)
- Fix stale SKILL.md link text in prompting guide

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ign-links

fix(docs): use claude.ai/design link, remove raw download option
The snapshot command resolved the HyperFrames runtime IIFE via a
relative path that walked up three directories to packages/core/dist/.
This only worked in the monorepo dev layout — npm/npx installs have
a flat dist/ folder with cli.js and the runtime side by side.

Without the runtime, window.__player was never created and the
snapshot fell back to seeking every __timelines entry to the same
absolute time. Sub-composition timelines expect relative time
(offset from their data-start), so all beats rendered beat-1 content.

Fix: resolve("hyperframe.runtime.iife.js") from __dirname (the dist/
folder itself), where the build already copies the runtime IIFE.
…eview-handoff

docs(skills): clarify preview handoff URL
Change build order from concurrent to staged to prevent @hyperframes/engine
from starting TypeScript compilation before @hyperframes/core generates
src/generated/runtime-inline.ts.

This fixes intermittent "Cannot find module './generated/runtime-inline'"
errors when running bun run build.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
fix: resolve build race condition by ensuring core builds first
Adds the directory + SKILL.md frontmatter for a new skill that translates
Remotion (React) compositions to HyperFrames (HTML+GSAP). This is the
foundation PR; subsequent PRs in the stack add the eval harness, test
corpus, translation references, and finally the SKILL.md body.

The frontmatter description enumerates trigger phrases and explicit
out-of-scope cases (useState/useEffect, async metadata, @remotion/lambda)
so the skill bows out cleanly when a Remotion composition isn't a clean
translation target — those should use the runtime interop pattern from
PR heygen-com#214 instead.

Validated with skill-creator's package_skill.py.
Extends RenderConfig.format with "png-sequence" and patches two correctness
gaps so the existing "webm" / "mov" values actually preserve the alpha
channel end-to-end.

Engine fixes:
- screenshotService.pageScreenshotCapture: drop optimizeForSpeed for PNG
  captures. The fast path uses an alpha-unaware codec that crushes real
  alpha values; kept for opaque jpeg captures where it is harmless.
- frameCapture: replace the inline setDefaultBackgroundColorOverride
  block (which fired pre-navigation and was reset by page.goto) with a
  proper initTransparentBackground() call inside initializeSession,
  after the window.__hf readiness poll. This also injects the
  html/body/[data-composition-id]{background:transparent !important}
  stylesheet so compositions with custom body / #root backgrounds do not
  defeat the override. Wired into both screenshot-mode and beginframe-mode
  branches.

Producer:
- RenderConfig.format extended to "mp4" | "webm" | "mov" | "png-sequence"
  with full JSDoc.
- Streaming encode is bypassed for png-sequence (frames go straight to
  disk). FORMAT_EXT extended.
- New Stage-5 png-sequence branch: mkdir outputPath, copy captured PNGs as
  frame_NNNNNN.png, copy audio.aac sidecar when audio is present.
- Stage-6 mux/faststart and the debug copy are wrapped in !isPngSequence.
- README.md: new "Transparent Video Output" section.

Tests:
- New fixture tests/transparency-regression/ tagged "transparency".
- New tsx script src/transparency-test.ts asserts pixel-level alpha for
  webm + png-sequence outputs. Wired as "test:transparency".
- Default "test" / "test:update" scripts pass --exclude-tags transparency
  so the golden-MP4 harness ignores the new fixture.

Verified locally on macOS arm64: typecheck clean across engine + producer,
producer renderOrchestrator vitest 10/10, transparency-test passes for
both webm and png-sequence with end-to-end pixel assertions.
…ails on the base SHA

Per Miguel's review: the previous fixture had no body / root background, so it
passed against both the buggy and fixed code. The fix this PR makes (the
initTransparentBackground stylesheet injection in initializeSession) only
matters when a composition paints over the CDP default-background-color
override — exactly what we tell users not to do, but exactly what a
regression test must do.

Reproduced locally:

- base SHA (2935be6): pixel (10,10) decodes as rgba [16,16,16,255]
  (opaque heygen-com#111 body bg leaks through the pre-navigation override that
  Chrome resets on goto)
- this head: pixel (10,10) decodes as rgba [0,0,0,0]
  (initTransparentBackground injects [data-composition-id]{background:transparent !important}
  AFTER navigation, force-overriding the body bg)

The pixel-level assertions in transparency-test.ts are unchanged — they
already require alpha=0 at (10,10). With the body bg painted, that
assertion now fails on any code path that doesn't actually preserve alpha
end-to-end.
…et-next

docs: clarify alpha PR target branch
…ackage-resolution

fix(skills): load helper dependencies outside repo installs
…-export

feat(producer): true alpha output for webm, mov, and png-sequence
jrusso1020 and others added 30 commits May 4, 2026 20:27
… extraction misses

A <video src='../assets/foo.mp4'> inside a sub-composition silently dropped
from extraction; the rendered output froze on the first decoded frame for
the entire clip, with no error in stdout.

Root cause: browser URL resolver clamps '..' at origin root (studio preview
loads fine), but path.join(projectDir, '../assets/foo.mp4') normalizes to
parent-of-project/assets/foo.mp4, which usually doesn't exist. existsSync
returns false, extraction is skipped, no frame lookup is built, the
per-frame injector has nothing to swap, and the <video> element's first
decoded frame paints every screenshot.

- Adds resolveProjectRelativeSrc in videoFrameExtractor that mirrors browser
  clamping (literal join first, then leading '..' stripped).
- Surfaces a loud stderr warning when the resolver misses.
- Mirrors fix in audioMixer.ts (same bug for <audio src='../'>) and
  renderOrchestrator HDR probe loop.
- +6 regression tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Skill (hyperframes-cli): three-pattern table (cutout-over-different-scene
vs over-its-own-source vs over-different-take) + the two non-obvious rules
(wrap video in non-timed div for opacity control, both videos data-start=0
for sync). Skill (hyperframes/patterns): worked text-behind-subject example.
Docs: --quality flag, compositing pitfalls section, quality preset table.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Newer libavformat builds write the VP9-alpha sidecar tag as 'ALPHA_MODE'
(uppercase); older builds write 'alpha_mode'. ffprobe.ts only checked the
lowercase form, so files produced by recent ffmpeg encoders (including the
output of 'hyperframes remove-background' itself) were misclassified as
having no alpha channel. Knock-on effect: the producer extracted them as
JPGs (no alpha), the injected <img> overlays were fully opaque rectangles,
and any element below them on the z-stack (text, captions, other layers)
silently disappeared from the rendered output — even though the studio
preview rendered the same composition correctly via native <video> playback.

Symptom in our repro: a text-behind-subject composition showed the
headline correctly in studio preview but the production render covered
the headline entirely with the opaque avatar image.

Fix: read videoStream.tags.alpha_mode OR videoStream.tags.ALPHA_MODE.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Locks in the case-insensitive behavior alongside the existing alpha_mode
(lowercase) test. If either path regresses, the producer would silently
extract alpha-having webms as opaque JPGs and the injected <img> overlays
would cover every element below them on the z-stack — a bug that doesn't
surface in the studio preview, only in production renders.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ng on tags

Tag-based alpha detection (alpha_mode / ALPHA_MODE / pix_fmt yuva*) is
fundamentally brittle. Failure modes seen in the wild:
- case-sensitivity across ffmpeg versions (alpha_mode vs ALPHA_MODE)
- older muxers that omit the sidecar tag entirely
- mp4-as-webm rewraps that drop the tag
- ffprobe reporting yuv420p for VP9-with-alpha because the alpha plane
  lives in a Matroska BlockAdditional sidecar, not the main pix_fmt

Each of those silently strips alpha at extraction time. The bug doesn't
surface until the rendered output is missing layers — frustrating to debug,
silent in stdout. The previous case-insensitive fix patched one of the
failure modes; this commit removes the class.

The robust alternative is codec-based: any bitstream that CAN carry alpha
(VP9, VP8, ProRes 4444) gets the alpha-aware decoder and PNG output by
default, regardless of what the tag says. The cost is a small file-size
increase on opaque VP9/VP8 sources (cached PNGs vs JPGs); the benefit is
no class of silent alpha loss from tag misdetection.

- Adds codecMayHaveAlpha() + decoderForCodec() helpers and exports them.
- Updates extractVideoFramesRange to force libvpx-vp9 / libvpx for VP9 / VP8
  unconditionally (was: only when metadata.hasAlpha).
- Updates resolveFrameFormat to default to PNG for any alpha-capable codec
  (was: only when metadata.hasAlpha).
- +4 unit tests covering the codec table.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- engine/chunkEncoder, engine/streamingEncoder: extend `-bf 0` to GPU h264
  paths (nvenc, qsv, vaapi) and `-b_strategy 0` for qsv so GPU-encoded
  outputs avoid negative-DTS freezes too — not just SW libx264.
- engine/videoFrameExtractor: detect mid-path traversal (e.g.
  `assets/../../foo.mp4`) by normalizing first and re-anchoring at the
  project root. Adds a regression test.
- engine/videoFrameExtractor: dedupe stderr "src not resolvable" warnings
  by `video.src` so a comp with N broken sources logs once, not N times.
- engine/videoFrameExtractor.test: drop dynamic `require("node:fs")`,
  use ES `import { writeFileSync } from "node:fs"`.
- engine/ffprobe: extract `readTagCI` helper for case-insensitive ffprobe
  tag reads (will recur for other libavformat-versioned sidecar tags).
- cli/background-removal/pipeline: collapse Quality / QUALITIES /
  QUALITY_CRF / DEFAULT_QUALITY / isQuality surface using
  `Quality = keyof typeof QUALITY_CRF`.
- producer/renderOrchestrator: replace `v.src.startsWith("/")` with
  `isAbsolute(v.src)` in the HDR probe path so Windows absolute paths
  (`C:\...`) aren't treated as relative — matches the audioMixer guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…person video

The model removes background from any video with a person — we tested
with avatars because they were convenient, but anyone can bring a
talking-head clip, presenter footage, vlog, etc. Replace avatar-specific
filenames (avatar.mp4 / brandon.mp4) with neutral subject.mp4 (or
presenter.mp4 in the text-behind-subject example) and rephrase
copy that read as if avatars were the only use case.

Touches docs/guides/remove-background.mdx, hyperframes-media SKILL.md,
and hyperframes/patterns.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ed+snapshot

Four regression tests (font-variant-numeric, many-cuts, missing-host-comp-id,
variables-prod) failed on this PR with `Unable to parse PSNR output at <last
checkpoint>s`. Root cause: the harness derived all 100 checkpoints from the
*rendered* video's container duration, then asked ffmpeg's PSNR filter to
compare the same frame index from both videos.

The encoder changes earlier in this PR add `-avoid_negative_ts make_zero` to
the mux step. With AAC audio that shifts the first audio sample to t=0
instead of the encoder-delay offset, extending reported container duration
by ~20ms without changing video frame count. For the four failing tests,
the i=99 checkpoint then landed on a frame index that exists in the rendered
video but not in the snapshot baseline (e.g. round(2.98998 * 24) = 72 in a
72-frame baseline). ffmpeg's PSNR filter ran on zero matched frames and
emitted no `average:` line, so the parser threw.

Fix: probe both videos and use min(rendered, snapshot) duration when
spreading checkpoints. This is the correct semantics for symmetric PSNR
comparison anyway — both videos must have a frame at every sampled time.
The change is local to the harness; no encoder behavior changes, no
baselines regenerated.

Other regression tests with audio (chat, sub-composition-video,
vignelli-stacking) passed because their checkpoint-99 frame index landed
inside the baseline's frame range with several frames of slack. The four
failing tests had round-number durations where a 20ms drift was enough to
push the last checkpoint past `nb_frames - 1`.
…path-resolution-and-render-fixes

fix: render robustness — sub-comp src paths, alpha tag case, encoder + matter improvements
## Problem

The Blue Sweater intro HyperFrames project was only available as a standalone exported project zip. It was not installable from the public registry or visible in the Catalog Showcases group.

## What this fixes

Adds `blue-sweater-intro-video` as a registry block with its composition, avatar image, and sound mix asset. The block is exposed through the generated catalog page, `docs/public/catalog-index.json`, and the Showcases navigation.

The manifest and generated catalog page credit the creator as [Joe Sai](https://x.com/_blue_sweater_).

## Root cause

Catalog-visible blocks are driven by `registry/registry.json`, each block's `registry-item.json`, generated docs/catalog files, and CDN-hosted preview media. The exported project had a valid standalone composition, but it had not been converted into that registry/catalog contract or uploaded to the docs preview CDN.

## Verification

### Local checks

- `bun install`
- `bun run build`
- `bunx tsx scripts/generate-catalog-pages.ts`
- `bun run generate:catalog-previews -- --only blue-sweater-intro-video`
- `bun packages/cli/src/cli.ts add blue-sweater-intro-video --dir /tmp/hf-blue-sweater-install-test --no-clipboard --json` against a locally served registry
- `bun packages/cli/src/cli.ts lint /tmp/hf-blue-sweater-install-test` returned 0 errors and 3 static GSAP overlap warnings from the supplied timeline/parser path
- `bun packages/cli/src/cli.ts validate /tmp/hf-blue-sweater-install-test --timeout 5000` returned 0 runtime errors and 0 warnings, with contrast audit warnings only
- `bun packages/cli/src/cli.ts inspect /tmp/hf-blue-sweater-install-test --at 0.5,2.5,5.5,9.8,11.2 --json` returned 0 layout issues
- `bun packages/cli/src/cli.ts render /tmp/hf-blue-sweater-install-test --output /tmp/hf-blue-sweater-install-test/blue-sweater-intro-video-render.mp4 --fps 24 --quality draft --workers 3`
- `ffprobe` reported the installed render duration as `12.000000`
- `bunx oxfmt --check registry/registry.json registry/blocks/blue-sweater-intro-video/registry-item.json registry/blocks/blue-sweater-intro-video/blue-sweater-intro-video.html docs/docs.json docs/public/catalog-index.json docs/catalog/blocks/blue-sweater-intro-video.mdx`
- `git diff --check`
- `bunx vitest run packages/cli/src/commands/add.test.ts packages/core/src/registry/types.test.ts`

### Browser verification

- Started a real local HyperFrames preview for the installed test project.
- Used `agent-browser` to open `http://localhost:5198/api/projects/hf-blue-sweater-install-test/preview` at 1920x1080.
- Verified the runtime registered `install-test` and `blue-sweater-intro-video` timelines.
- Sought the block to the final card and verified `@_blue_sweater_` and the following state were visible.
- Recorded an `agent-browser`-driven full animation pass; `ffprobe` confirmed a 1920x1080 WebM with 110 video frames.
- Checked the fresh `agent-browser` session for page errors after the direct preview flow: `errors: []`.
- Used `agent-browser` to load an HTML page with the exact generated CDN `video`/`poster` URLs; the browser reported `readyState: 4`, `videoWidth: 1920`, `videoHeight: 1080`, and `paused: false`.

### CDN upload

Uploaded the generated preview media with AWS CLI to the existing docs image bucket path:

- `s3://heygen-public/hyperframes-oss/docs/images/catalog/blocks/blue-sweater-intro-video.mp4`
- `s3://heygen-public/hyperframes-oss/docs/images/catalog/blocks/blue-sweater-intro-video.png`

Verified both public CDN URLs return `HTTP 200` with correct content type and immutable cache headers:

- `https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/blue-sweater-intro-video.mp4` (`video/mp4`)
- `https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/blue-sweater-intro-video.png` (`image/png`)

## Notes

- Local-only browser proof artifacts:
  - `/tmp/hf-blue-sweater-browser-proof/fresh-final-card.png`
  - `/tmp/hf-blue-sweater-browser-proof/fresh-browser-flow.webm`
  - `/tmp/hf-blue-sweater-cdn-check.png`
- Local-only installed render artifact:
  - `/tmp/hf-blue-sweater-install-test/blue-sweater-intro-video-render.mp4`
)

## Summary
- Replace the flat 30s upload timeout with a size-adaptive calculation: `max(120s, bytes / 500KB/s)`
- Metadata requests (presigned URL, complete) keep the original 30s timeout
- Companion to the backend change removing the 64 MB upload limit in experiment-framework

## Context
With the backend size limit removed, large projects (78 MB+) need proportionally longer to upload. A 78 MB project now gets ~164s, a 500 MB project ~17 min. The old 30s timeout would abort any upload over ~15 MB on a typical connection.

## Test plan
- [x] All 4 existing vitest tests pass
- [x] Build succeeds, no type errors
- [x] Lint + format pass (oxlint + oxfmt)
- [x] Timeout values verified for 10/78/200/500/1000 MB archives
Emit an inverse-alpha background plate alongside the cutout in a single
inference pass. Same source RGB, alpha = 255 − mask. Dual-encoder pipeline
runs in parallel; both outputs share the same --quality preset.

This is a hole-cut plate (subject region transparent), not an inpainted
clean plate — composite something opaque under it to fill the hole.
Docs and skill cover when each is the right tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… reasoning

Both `composition_file_too_large` and `timeline_track_too_dense` previously
said "Agents produce better results when large scenes are split into smaller
sub-compositions." The audience-flavored framing ("Agents produce better
results") doesn't tell a reader (agent or human) WHY smaller is better.

Reframe to concrete properties of smaller compositions: easier to read,
iterate on, and diff. The fixHint already covers the inspect/revise/validate
detail; the message now leads with a tight reason.

Per Abhay in #C0ACCNHLG3U:
> "an agent reading 'Agents produce better results' sounds weird. We should
>  give the agent an actual reason why smaller is better for them."

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…rding

fix(lint): rephrase too-large composition warnings to give actionable reasoning
- Extract applyMask helper from postprocess and add 5 unit tests pinning
  the contract this PR is selling: fg.alpha + bg.alpha === 255 per pixel,
  RGB triples byte-identical between fg and bg, and bg=null path leaves
  the bg buffer untouched. Without these, a future postprocess change
  (mask threshold, premultiplied alpha, gamma) could silently break the
  inverse-alpha relationship and the existing plumbing tests would all
  still pass.
- Add stdin 'error' listener inside spawnFfmpeg. If either encoder dies
  mid-render, Node emits an unhandled error on the dead writable on the
  next .write() and crashes the CLI before waitForExit's reject path
  can surface the encoder's stderr tail. Doubled encoder count = doubled
  failure surface, so this is worth pinning down.
- Tighten stdio param to a 3-tuple so an accidental 1-element array fails
  at type-check.
- Sharpen backpressure comment: write→true means "highWaterMark not
  exceeded," not "libuv flushed." Reuse-without-corruption is safe only
  because session.process is slow enough that libuv drains in between.

Addresses review on PR heygen-com#637.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…und-bg-output

feat(cli): add --background-output to remove-background
…atch binding

Three issues in `bundleToSingleHtml` reported via Abhay's LLM-based code-validity
eval against the bundled output. Each is independently small; they share a single
PR because they're all artifacts of the bundler-output shape.

1. Empty `src=""` runtime placeholder (real bug)

`htmlBundler.ts:injectInterceptor` emitted
`<script data-hyperframes-preview-runtime="1" src=""></script>`
when no `HYPERFRAME_RUNTIME_URL` was configured. Empty `src` resolves to the
page URL itself; Chrome flags this as an infinite-fetch hazard. Three other
consumers (studioServer, validate, snapshot) post-process the placeholder to
substitute either a real URL or an inlined body — `bundleToSingleHtml` did
not, so the bundle wasn't actually self-contained despite the function name.

Fix: when no URL is configured, inline the runtime IIFE directly via
`getHyperframeRuntimeScript()`. Otherwise emit `src=…` as before.

2. Bare-semicolon lines between joined JS chunks (cosmetic)

Three sites used `chunks.join("\n;\n")` (body-script coalesce, local JS,
composition scripts) which produced a lone `;` on its own line between
chunks. Valid JS but a code smell. Replace with a `joinJsChunks()` helper
that ensures each chunk ends in `;` and joins on `\n`.

3. Empty `catch (_err) {}` in compositionScoping.ts (lint-noisy)

The `_err` underscore prefix signals "intentionally swallowed" but bundle-time
linters often don't honor that convention. Replaced with `catch { /* ... */ }`
(no binding, explanatory comment) — same behavior, no rule fires.

Tests: 2 new regression guards (runtime-not-empty-src, no-bare-semi) plus
existing tests updated to reflect the new inlined-runtime shape (the previous
"runtime block must not contain getElementById" assertion no longer holds
because the inlined body itself uses getElementById; replaced with a more
specific "author script not merged into runtime tag" check).

Issue #4 from the original report (Unterminated string at line 1111 col 18,
char 65497) was not directly reproducible after applying these fixes — esbuild
parses all 4 inline scripts in the rebundled output cleanly. The unterminated-
string symptom was likely a downstream artifact of the bare-semicolon joining
or the empty-src placeholder confusing the lint tool. If the original symptom
persists on a clean re-run against the fixed bundle, will open a follow-up PR
with a focused repro.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…subs

Per @vai-bot's review on hf#641:

Important #1: dead `src=""` substitution sites
=============================================

Now that `bundleToSingleHtml` inlines the runtime IIFE by default, the empty
`src=""` placeholder is never emitted in the no-env-var path — the 5 downstream
substitution sites that grep for `src=""` were dead.

Two of them (studio dev server + studio vite preview) genuinely WANT the
placeholder so they can hot-reload a local /api/runtime.js endpoint without
re-inlining ~150 KB on every composition edit. Three of them (CLI validate,
snapshot, layout) were just doing the same inlining the bundler already does.

Resolution:
- Add a `runtime: "inline" | "placeholder"` option to `BundleOptions`. Default
  is "inline" (matches the self-contained-bundle promise the function name
  makes). The two studio surfaces explicitly pass `{ runtime: "placeholder" }`
  to opt in.
- studioServer.ts + studio/vite.config.ts: pass the option, keep their
  existing string-replace logic unchanged.
- validate.ts + snapshot.ts + layout.ts: delete the now-redundant runtime
  substitution code (regex never matches the new inlined-runtime shape).

Important #2: joinJsChunks ASI hazard
======================================

The new helper appended `;` to chunks not already ending in `;` and joined
on `\n`. If a chunk ended with a `// line comment`, the appended semicolon
was eaten by the comment, leaving the next chunk's first statement attached
to the previous chunk's last expression — exactly the ASI hazard the helper
exists to prevent.

Fix: append `\n;` instead of `;` for chunks not already terminated. The
newline closes the line comment, the standalone `;` becomes the statement
separator. For typical chunks (already ending in `;`), output is unchanged
— still clean `\n`-joined chunks with no bare-semicolon lines.

Also added a trailing `;` to `wrapScopedCompositionScript`'s IIFE close
(`})()` → `})();`) so composition scripts join cleanly without falling
through to the `\n;` fallback.

New test: regression guard at the chunk boundary verifies every inline
script body in the bundle parses cleanly via esbuild even when a source JS
file ends with a line comment.

Verification
============

- `bun run --filter @hyperframes/core test` — 653/653 pass
- `bun run --filter @hyperframes/cli test` — 243/243 pass
- `bun run --filter @hyperframes/{core,cli,studio} typecheck` — clean
- `bunx oxfmt --check` + `bunx oxlint` on all touched files — clean

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
CodeQL flagged the inline `<script>...</script>` regex as case-sensitive,
which would miss `<SCRIPT>` tags. The bundler always emits lowercase, so
this is a defense-in-depth fix matching the `/i` flag already used by the
sibling regexes in this file (lines 37 & 75).

Addresses CodeQL review on heygen-com#641.
CodeQL's `js/bad-tag-filter` rule flagged `</script>` as too strict —
`</script >` (with whitespace before `>`) is valid HTML and would slip
past the matcher. Changed to `</script\s*>` for full defense-in-depth.

The bundler always emits the canonical form, so no real-traffic miss —
this is hardening the test's parse-loop, not fixing a downstream bug.

Addresses CodeQL alert on heygen-com#641.
CodeQL still flagged `</script\s*>` as too narrow — the rule wants
tolerance for `</script\t\n bar>` (HTML parser treats trailing content
in a close tag as part of the tag). Switched to `</script[^>]*>` for
full coverage.

The bundler still always emits the canonical `</script>`; this is
test-side hardening, not a runtime fix.
Per CodeQL's `js/bad-tag-filter` recommendation, replace the regex-based
`<script>` body extraction with a `parseHTML` + `querySelectorAll`
walk. The rule explicitly says "use a parser library" — and linkedom
is already imported in this file, so the diff is small.

This eliminates the regex entirely, so the rule can no longer fire on
this site (instead of chasing whitespace / case / trailing-content
edge cases one at a time).
…c on scrub (heygen-com#639)

* fix(runtime): clear play guard after hard seek to prevent audio desync on scrub

When scrubbing the timeline during playback, syncRuntimeMedia detects
the offset jump and hard-seeks the media element. But the in-flight
play() guard (playRequested WeakSet) from the previous play() call
prevented the next sync tick from re-issuing play() — leaving the
element paused at the new position for 50-150ms while the GSAP timeline
continued advancing. This caused audible audio desync after every scrub.

Fix: clear playRequested on the element after a hard seek so the very
next sync tick can re-issue play().

Also adds a lint rule (video_audio_double_source) that catches
compositions where an unmuted <video> and a separate <audio> point to
the same source — a pattern that causes double playback at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(runtime): detect failed seeks past MP3 buffer and force full fetch

Root cause: streaming MP3 with preload="metadata" only buffers the first
~15 seconds. Seeking past the buffered range silently fails — currentTime
stays at 0 while the timeline advances, causing permanent audio desync
that only a page refresh fixes.

Three changes:
1. Move preload="auto" enforcement to run for ALL active elements on
   every sync tick (not just during play). This catches elements whose
   preload was overridden after init.ts set it.
2. After a hard seek, check if currentTime actually reached the target.
   If not (drift > 0.5s), call load() once to force the browser to
   fully fetch the media and build a complete seek index.
3. Clear the load-retry guard when the clip leaves its active window
   so re-entry can retry if needed.

Reproduced on hyperframes.dev Hermes launch video: vo.mp3 buffered to
15.96s, seeking to 20s failed silently. bg-music.wav (fully buffered)
was unaffected.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
…-and-joins

fix(bundler): inline runtime body, drop bare-semi joins, drop empty catch binding
* feat: cache shader transition preview frames

* fix: move shader transition loading to player
* feat: cache shader transition preview frames

* fix: move shader transition loading to player

* fix: render shader transitions for sdr compositions
…into hyperframes skill

Cherry-pick the PR heygen-com#549 design-first pipeline from upstream and renumber the
authoring flow to discovery → step 1 (design) → step 2 (prompt expansion) →
step 3 (plan), so this fork's retention work can sit on the same intermediate
brief that upstream agents produce.

Adds shared references the fork was missing: video-composition (density,
color presence, scale), beat-direction (rhythm templates), techniques (11
visual technique recipes), narration (voiceover pacing), prompt-expansion
(step-2 brief format), design-picker (visual design.md generator + HTML
template). Updates motion-principles with image-treatment and load-bearing
GSAP rules, patterns with the text-behind-subject transparent-webm pattern,
and the contrast-report and animation-map scripts to bootstrap their
dependencies via the new package-loader.

SKILL.md is the upstream version with three fork-specific entry points
spliced into the references list: tts.md (kept here because we don't yet
ship a hyperframes-media skill), retention-ladder.md, and
retention-overdrive.md (previously orphaned — never linked from SKILL.md).

Preserved as fork-only:
  - data-in-motion.md — the editorial chart-pack vocabulary
    (lollipop-timeline, grouped-bars, annotated-area, cyber-counter-burst)
  - visual-styles.md — the lighter style-library variant
  - references/transcript-guide.md, references/tts.md — full CLI guidance,
    since this fork does not (yet) ship a hyperframes-media skill split
  - retention-ladder.md, retention-overdrive.md — fork-only retention work

Verified: 24/24 SKILL.md markdown links resolve, oxlint clean on the .mjs
scripts, oxfmt clean across all changed files, npx tsx packages/cli/src/cli.ts
--help lists every command including the fork-only score/optimize/el-tts/
script/images set.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Brings the production-grade engine, runtime, and Studio fixes from
upstream that the fork was missing, on top of Phase 1's PR heygen-com#549 skill sync.

Conflicts resolved (11 files):
  - bun.lock, packages/{cli,core}/package.json: dep unions, lockfile
    regenerated via bun install
  - skills/hyperframes/SKILL.md: re-merged Phase 1 fork entry points
    (retention-ladder, retention-overdrive, prompt-expansion, tts) on top
    of upstream's post-heygen-com#549 skill cleanup
  - packages/cli/src/help.ts: union — fork's el-tts/script/score/optimize/
    images preserved, upstream's remove-background added
  - packages/cli/src/commands/render.ts: union — kept fork's logRenderCost
    accounting, added upstream's exit-after-complete + scheduleRenderProcessExit
  - packages/core/src/studio-api/createStudioApi.ts: union — fork's
    elevenlabs/anthropic/script/costs/images/storyline routes + upstream's
    new waveform route
  - packages/studio/src/player/hooks/useTimelinePlayer.ts: kept upstream's
    refactor to createTimelineElementFromManifestClip + findTimelineDomNodeForClip,
    re-attached fork's data-timeline-group passthrough as post-processing
  - packages/studio/src/player/components/Timeline.test.ts: union — fork's
    computeEffectiveTimelineDuration + deriveTimelineLaneLabel suites kept
    alongside upstream's formatTimelineTickLabel suite
  - packages/studio/src/components/sidebar/LeftSidebar.tsx: kept fork's
    mode-driven tab system, added upstream's onToggleCollapse as optional
    Hide button
  - packages/studio/src/App.tsx: union — fork's Direct/Edit toggle, sidebar
    toggle, Timeline visibility toggle restored alongside upstream's Capture
    frame link; states unioned; LeftSidebar receives both mode and
    onToggleCollapse

Post-merge fixes:
  - packages/studio/src/player/store/playerStore.ts: dropped duplicate
    label field that auto-merge introduced
  - packages/cli/src/commands/render.ts: cost-log meta uses hdrMode (typed
    field) instead of dropped legacy hdr key
  - packages/studio/src/App.tsx: removed dead timelineEditorHintDismissed
    useState and matching import (was never read)

Verified: 1736/1736 tests passing (core 1148, cli 257, studio 331); tsc,
oxlint, oxfmt all clean; CLI prints fork's commands (el-tts, script, score,
optimize, images) and upstream's new remove-background side by side.

Brings: bundler runtime fixes (heygen-com#641), audio-desync-on-scrub fix (heygen-com#639),
shader-transition cache (heygen-com#634), SDR shader transitions (heygen-com#640), PSNR
sampling fix (heygen-com#627), --background-output flag (heygen-com#637), v0.4.45 release,
PR heygen-com#546 Studio Design panel scaffolding, PR heygen-com#495 claude-design move,
PR heygen-com#496 link fixes. Fork has chosen NOT to adopt the upstream
hyperframes-media skill split — fork's tts.md and transcript-guide.md
remain inside the hyperframes skill.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…re, surface catalog

Three targeted Studio-polish improvements layered on the freshly-merged
upstream/main + PR heygen-com#546 Design panel scaffolding. The fourth handoff item
(intermittent Timeline editor flicker on play / tab-switch) is deferred —
the handoff filed three hypotheses but no confirmed repro, so any "fix"
would be speculative; needs a fresh repro session.

1. HOOK_BIGTEXT — letter-cascade safety net
   The user's render of `my-first-video` showed the kinetic-text scene
   dropping arbitrary letters mid-headline ("y failing", "compre e si e
   fede al", "framewo k", "neithe goal") for the entire scene. The
   handoff filed two hypotheses — stagger-overruns-duration and layout-
   clipping. The fix below makes both failure modes structurally
   impossible regardless of which one was the cause:
     - `.hb-letter` defaults to `opacity: 1` (graceful degradation —
       if GSAP fails to register, every letter still renders visible)
     - A pre-cascade `tl.set(letters, { opacity:0, y:50 }, 0)` records
       the initial state at t=0 so seeks into the cascade window observe
       a real tween rather than a default-state element
     - A defensive `tl.set(letters, { opacity:1, y:0, clearProps:'transform' },
       sceneDur - 0.01)` at the absolute end of the scene window
       guarantees the last captured frame has every letter at opacity:1
       even if the earlier cascade math drifts under load
   Also adds an 8-test regression suite that pins the safety net's
   structure: one `.hb-letter` span per character, opacity:1 default in
   CSS, pre-tween set at t=0, final-frame snap regex, no stagger overrun
   at minimum scene duration, single-letter edge case, accent-word
   highlight preservation.

2. chromatic-glow atmosphere preset (cinematic-card foundation)
   Adds the eleventh atmosphere — a Vision-Pro / Apple-keynote spotlight
   halo built from a single intense radial gradient at the focal point
   plus a soft chromatic-aberration ring. Pure CSS, scoped per sceneId
   for cross-scene independence, intensifies for hook scenes, uses the
   active theme's accent color so each design.md gets its own ambient
   hue. Opt-in only — defaultAtmosphereForTemplate intentionally does
   not yet route any template to this preset, so existing renders are
   unchanged. This is the foundation for the upcoming spotlight-card /
   cinematic-card aesthetic upgrade tracked in the Studio polish
   handoff (Apple-keynote / Vision-Pro reference imagery). Adds an
   8-test regression suite covering registry uniqueness, sceneId
   scoping, theme-color usage, hook-vs-body intensity, and the
   opt-in-only pin.

3. Catalog reference (skills surfacing)
   The upstream merge brought 44 catalog blocks (hyperframes.dev/Apple
   Money Count, Blue Sweater, Spotify, North Korea Locked Down, all
   transitions categories, all shader transitions, etc.) and 3 components
   (Grain Overlay, Grid Pixelate Wipe, Shimmer Sweep) into registry/.
   The new skills/hyperframes/references/catalog.md groups them by
   purpose (social/UI mockups, hooks/openers, logo/outro, CSS
   transitions, shader transitions, components, examples) with a short
   "use when" line per entry. SKILL.md now links to it from the
   References section so an authoring agent picks the right block to
   `hyperframes add` before scaffolding from empty.

Verified: core 1164/1164 tests passing (was 1148; +16 new tests),
tsc + oxlint + oxfmt clean across all changed files. CLI + Studio
typecheck unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants