Commit Graph
1135 Commits
Author SHA1 Message Date
James RussoandClaude Opus 4.7 21f5066832 feat(producer): enable webm in distributed mode via concat-copy (#951)
* feat(producer): enable webm in distributed mode via concat-copy

PR 8.2 of the WebM distributed-rendering plan (v1.5 backlog #1; see
DISTRIBUTED-RENDERING-PLAN.md §7.2). Wires libvpx-vp9 webm through the
distributed pipeline now that PR 8.1 proved concat-copy works.

Architectural decision: Path A (concat-copy) — based on PR 8.1's smoke
test result (9/9 tests pass for both yuv420p and yuva420p VP9 streams).
The simpler architecture wins; no re-encode in assemble, no encode-
parallelism loss.

Changes:

- plan.ts:
  - DistributedRenderConfig.format and PlanResult.format now include
    "webm" — type-level acceptance matches the runtime gate.
  - rejectUnsupportedDistributedFormat() no longer trips on webm. HDR
    mp4 remains the only refused configuration.
  - resolveEncoderTriple() returns libvpx-vp9-software + yuva420p +
    preset="good" for format="webm". yuva420p preserves alpha — the
    format's main reason for existing for web delivery.
  - codec= remains rejected for non-mp4 formats (mov is always ProRes
    4444; webm is always libvpx-vp9). The error message lists all four
    distributed-supported formats.
  - FormatNotSupportedInDistributedError docstring updated to reflect
    the new reality (only HDR is unsupported).

- freezePlan.ts: LockedRenderConfig.encoder gains "libvpx-vp9-software".
  Mirrors libx265-software / prores-software / png-sequence in shape;
  the chunk worker reads this discriminant to decide encode args.

- renderChunk.ts: drops the now-incorrect cast that excluded webm from
  buildSyntheticRenderJob's format input; tightens the preset-format
  cast to include webm.

- assemble.ts: docstring + comment updates. The mp4/mov concat-copy
  path is format-agnostic — webm uses the exact same code (applyFaststart
  is a no-op for webm via the existing chunkEncoder.ts gate;
  muxVideoWithAudio already routes webm to libopus audio).

- planFormatBanlist.test.ts: webm-rejection tests removed; replaced with
  "accepts webm" tests + a HDR+webm combo test that verifies HDR is the
  trip regardless of format.

- plan.test.ts: new describe block pins the webm wiring contract:
  format="webm" produces an encoder=libvpx-vp9-software /
  pixelFormat=yuva420p planDir with closedGop=true and gopSize=chunkSize.

- webm-concat-copy.test.ts (smoke): extended with a yuva420p variant
  that proves the alpha pixel format the distributed pipeline actually
  emits also round-trips through concat-copy. 9/9 tests pass locally.

§8 format support matrix in DISTRIBUTED-RENDERING-PLAN.md is intentionally
left unchanged at this PR — it flips to ✓ in PR 8.4 once the end-to-end
fixture (PR 8.3) is green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(producer): include webm in plan-time needsAlpha + strengthen alpha smoke

PR review feedback from Miguel and Vai on #951 caught a real bug:
`plan.ts`'s `needsAlpha` disjunction excluded `"webm"`, so the plan
stage froze `forceScreenshot: false` into the `LockedRenderConfig`
even though distributed webm uses `yuva420p`. Every chunk worker
captured opaque RGB via BeginFrame (which doesn't preserve alpha on
Linux headless-shell), and libvpx-vp9 encoded uniformly-opaque alpha
that the encoder then dropped — producing un-keyable webm.

Two changes:

1. **plan.ts**: include `"webm"` in `needsAlpha`. Matches the
   in-process renderer's logic at `renderOrchestrator.ts:1469`
   (`const needsAlpha = isWebm || isMov || isPngSequence`); the two
   sites must stay in sync since the distributed pipeline's PSNR
   regression compares against the in-process baseline.

2. **Smoke test (yuva420p describe)**: source frames now use a real
   alpha gradient (`geq=a='X*255/W'` on top of `testsrc2`) instead of
   `testsrc2 + format=rgba` which was uniformly opaque. The decode-
   pix_fmt assertion is dropped (ffprobe reports `yuv420p` for
   VP9-with-alpha because the alpha lives in a Matroska
   `BlockAdditional` sidecar) and replaced with two stronger checks:
   - `TAG:ALPHA_MODE=1` is present on the stream — proves the
     encoder was actually configured for alpha
   - alpha plane variance after `-c:v libvpx-vp9 -i ... -pix_fmt rgba
     -vf extractplanes=a,signalstats` — proves the alpha sub-stream
     round-trips through concat-copy with spatially-varying content,
     not uniform/dropped alpha
   - decode-test gate is now exit-code-only (was `exitCode || stderr`
     which would flake on chatty ffmpeg `-v error` builds emitting
     non-fatal DTS/container notes)

These checks would have caught the `needsAlpha` bug before review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(aws-lambda): widen narrow format types to include webm

CI on PR #951 was failing at typecheck/build because the producer's
`DistributedRenderConfig.format` widened to include webm in this PR
but the aws-lambda package's narrow `"mp4" | "mov" | "png-sequence"`
type literals in `events.ts`, `handler.ts`, and `validateConfig.ts`
hadn't kept up. `renderToLambda.ts:87` passed `config.format` (now
including webm) into a parameter typed against the narrow union,
producing TS2345.

This widening originally landed in PR #952 (test fixture PR) but
needs to be atomic with the producer's widening here to keep each
PR independently typecheck-clean.

Also refactor `formatExtension` from a switch dispatch to a
`Record<DistributedFormat, string>` lookup. Adding the webm case
tipped the switch's CRAP to the 30.0 fallow threshold; the lookup
table drops cyclomatic from 5 to 1 with the same compile-time
exhaustiveness guarantee (TS errors on missing entries when
`DistributedFormat` adds a new format). The runtime
`_exhaustive: never` throw was only protecting against a string
slipping past TS; `validateConfig.ts`'s `ALLOWED_FORMATS` already
gates untrusted input at the SDK boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 02:46:21 -04:00
James Russo 07de7e61ed feat(engine): closed-GOP VP9 encoder args + concat-copy smoke test (#950)
## Description

PR 1 of 4 in the WebM (VP9) distributed-rendering series. A gating
experiment that proves closed-GOP libvpx-vp9 chunks survive
`ffmpeg -f concat -c copy` losslessly, so the rest of the stack can
ship Path A (concat-copy) rather than the slower
re-encode-in-assemble fallback.

Two changes:

1. **Closed-GOP VP9 encoder args.** `buildEncoderArgs` now lays
   `-g <chunkSize>`, `-keyint_min <chunkSize>`, `-auto-alt-ref 0`, and
   `-cpu-used 2` on libvpx-vp9 when `lockGopForChunkConcat=true`.
   Mirrors the existing libx264/libx265 branches. The alt-ref disable
   is load-bearing — libvpx-vp9's default non-displayable alt-ref
   frames can reach across chunk seams and break concat-copy.
   `-cpu-used 2` pins the speed/quality tradeoff so chunks encoded on
   workers with different libvpx-vp9 defaults produce visually
   consistent output across seams. Default (`lockGopForChunkConcat`
   unset) preserves the existing in-process VP9 path unchanged.

2. **Concat-copy smoke test** at
   `packages/producer/tests/distributed/_smoke/webm-concat-copy.test.ts`.
   Generates 60 PNGs via lavfi `testsrc2`, encodes them as 4 VP9 chunks
   of 15 frames using `buildEncoderArgs` with
   `lockGopForChunkConcat=true`, concat-copies via `ffmpeg -f concat -c
   copy`, then runs three independent verifications:
   `ffprobe -show_streams`, `ffmpeg -f null -` decode test, and
   `ffprobe -count_frames`. Each verification surfaces its failure
   fingerprint in the error message.

Smoke test passes 6/6 locally → Path A works; the rest of the stack
takes it.

Also exports `buildEncoderArgs` from `@hyperframes/engine` so
adapters / tests can construct args without re-implementing the
contract.

## Testing

- [x] `bunx vitest run --root packages/engine src/services/chunkEncoder.test.ts` — 62/62 pass (new VP9 closed-GOP tests included)
- [x] `bun test packages/producer/tests/distributed/_smoke/webm-concat-copy.test.ts` — passes
- [x] `bunx oxlint` + `bunx oxfmt --check` on all changed files — clean
- [x] `bunx tsc --noEmit -p packages/engine/tsconfig.json` — clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-19 01:53:08 -04:00
James Russo efc5f0584b ci: post sticky PR comment with fallow audit findings (#954)
* ci: run fallow audit in lefthook pre-commit

Mirrors the same `fallow audit --base ... --fail-on-issues` check that
runs in CI, but locally against HEAD so issues surface at commit time
instead of after the push round-trip.

Scoped to `packages/**` source files via the glob — non-code edits
(README, docs, top-level configs) skip the hook entirely.

Measured locally: ~5s in parallel with the existing lint/format/typecheck
checks. Doesn't extend wall-clock time because typecheck (~11s) is the
long pole, and lefthook runs commands in parallel.

The default `--gate new-only` means inherited findings don't block the
commit — same gate behavior as CI, so local pre-commit and PR audit
agree.

* refactor: delete orphan declarations flagged by fallow

After fallow's auto-fix de-exports unused symbols, oxlint surfaces them
as no-unused-vars. This PR deletes those orphan declarations outright.

Biggest cleanup: studio/src/icons/SystemIcons.tsx shrinks from 132 to 57
lines — 33 unused icon wrappers and their phosphor-icon imports deleted.

Other deletions across 14 more files covering paired getter/setters,
helper functions, dead env constants, internal components with no
callers, and cascading unused imports.

Cascade-causing files held back for follow-up PRs: renderOrchestrator
barrel of captureCost re-exports, telemetry/portUtils/remote barrels,
Button.tsx + ui/index.ts (would orphan whole file), studioMotion
type re-exports.

Test plan: typecheck clean across 8 packages, oxlint + oxfmt clean,
fallow audit exit 0 (remaining findings inherited), cli + studio
vitest suites pass.

* ci: post sticky PR comment with fallow audit findings

Reviewers shouldn't have to dig through CI logs to see what fallow
flagged. With this change, on every PR the fallow job posts (or
updates) a sticky comment containing the full audit report formatted
as a collapsible markdown table.

The comment uses fallow's built-in `pr-comment-github` format, which
already emits a `<!-- fallow-id: fallow-results -->` sentinel.
`marocchino/sticky-pull-request-comment@v2.9.1` matches that header so
each run replaces the previous comment instead of stacking new ones.

The job now runs in three steps:
1. Run `fallow audit ... --format pr-comment-github` with
   `continue-on-error: true` so the comment posts even when the audit
   fails. Exit code is captured.
2. Post (or update) the sticky comment with the captured output.
3. Re-emit the audit exit code so the job still fails-the-build on
   new findings.

Bumps the workflow's `pull-requests` permission from read to write,
needed for the sticky-comment poster to call the issues API.
2026-05-18 21:28:41 -07:00
James Russo 2729ee5087 refactor: delete orphan declarations flagged by fallow (#949)
* ci: run fallow audit in lefthook pre-commit

Mirrors the same `fallow audit --base ... --fail-on-issues` check that
runs in CI, but locally against HEAD so issues surface at commit time
instead of after the push round-trip.

Scoped to `packages/**` source files via the glob — non-code edits
(README, docs, top-level configs) skip the hook entirely.

Measured locally: ~5s in parallel with the existing lint/format/typecheck
checks. Doesn't extend wall-clock time because typecheck (~11s) is the
long pole, and lefthook runs commands in parallel.

The default `--gate new-only` means inherited findings don't block the
commit — same gate behavior as CI, so local pre-commit and PR audit
agree.

* refactor: delete orphan declarations flagged by fallow

After fallow's auto-fix de-exports unused symbols, oxlint surfaces them
as no-unused-vars. This PR deletes those orphan declarations outright.

Biggest cleanup: studio/src/icons/SystemIcons.tsx shrinks from 132 to 57
lines — 33 unused icon wrappers and their phosphor-icon imports deleted.

Other deletions across 14 more files covering paired getter/setters,
helper functions, dead env constants, internal components with no
callers, and cascading unused imports.

Cascade-causing files held back for follow-up PRs: renderOrchestrator
barrel of captureCost re-exports, telemetry/portUtils/remote barrels,
Button.tsx + ui/index.ts (would orphan whole file), studioMotion
type re-exports.

Test plan: typecheck clean across 8 packages, oxlint + oxfmt clean,
fallow audit exit 0 (remaining findings inherited), cli + studio
vitest suites pass.
2026-05-18 21:11:03 -07:00
James Russo b6b7bcb51a ci: run fallow audit in lefthook pre-commit (#948)
Mirrors the same `fallow audit --base ... --fail-on-issues` check that
runs in CI, but locally against HEAD so issues surface at commit time
instead of after the push round-trip.

Scoped to `packages/**` source files via the glob — non-code edits
(README, docs, top-level configs) skip the hook entirely.

Measured locally: ~5s in parallel with the existing lint/format/typecheck
checks. Doesn't extend wall-clock time because typecheck (~11s) is the
long pole, and lefthook runs commands in parallel.

The default `--gate new-only` means inherited findings don't block the
commit — same gate behavior as CI, so local pre-commit and PR audit
agree.
2026-05-18 19:39:34 -07:00
James 2dc2531cf7 chore: release v0.6.25 v0.6.25 2026-05-19 02:33:31 +00:00
James Russo 17f47f30dd fix(distributed): gate per-worker SwiftShader probe to worker 0 only (#956)
After #916 moved `assertSwiftShader` from `renderChunk()`'s eager probe
session into `executeWorkerTask`, every parallel worker began running its
own `chrome://gpu` / canvas-WebGL probe. At `chunkWorkerCount=6` (texture
launch at chunks=3) that's 6 concurrent CDP page-loads per chunk × 3
chunks = 18 simultaneous probes. Bench data on dev (12 producer pods × 22
vCPU) showed c=3 worst-case wall-clock at 67.3s, 24.7s above c=6 worst
(42.6s) — pod_total inflates 100s → 147s uniformly across all three
chunks per slow iter, the signature of cluster-level CDP contention
rather than within-pod contention.

Workers within a chunk share the same Chrome binary, flags, and OS/driver
state on a single pod, so worker 0's success is representative for the
rest. Gate the probe via `shouldVerifyWorkerGpu(workerId, config)` so
only worker 0 navigates to the probe page; workers 1..N-1 skip it. The
fail-fast contract still holds at the chunk level (worker 0 still aborts
the chunk if SwiftShader didn't load) — just without the concurrent CDP
traffic.

Expected wall-clock impact: c=3 worst drops from ~67s to in line with
c=6 worst (~42-44s). c=6 (3 workers/pod) and c=8 (2 workers/pod) should
see smaller wins; c=12 (1 worker/pod, sequential branch) is unaffected.

Closes #955.
2026-05-18 22:30:13 -04:00
Miguel Ángel 7354d61371 chore: release v0.6.24 v0.6.24 2026-05-18 22:00:56 -04:00
Miguel Ángel 72a18a0116 Merge pull request #947 from heygen-com/feat/studio-blocks-panel
feat(studio): full Blocks panel — browse, search, add, drag-and-drop registry items
2026-05-19 03:58:25 +02:00
James Russo 2d566338ae Merge pull request #944 from heygen-com/cleanup/fallow-auto-fix
refactor: drop unused exports detected by fallow auto-fix
2026-05-18 21:19:44 -04:00
Miguel Ángel ffbc18ad31 feat(studio): full Blocks panel — browse, search, add, drag-and-drop registry items
Adds a Blocks tab to the Studio left sidebar with the full 78-item registry
catalog (58 blocks + 20 components). Users can browse by category, search by
title/description, preview CDN-hosted poster thumbnails with video-on-hover,
and install items on-demand with one click or drag-to-timeline.

Core changes:
- BlockCategory type + resolveBlockCategory() for 7 categories (Captions, VFX,
  Transitions, Effects, Social, Data, Scenes)
- Registry API routes: GET /api/registry/blocks (catalog) + POST install
- StudioApiAdapter extended with listRegistryCatalog + installRegistryBlock
- Vite adapter reads from disk; CLI adapter fetches from GitHub (24h cache)
- BlockParam interface + params on 6 blocks for future parameter controls

Studio UI:
- 4th sidebar tab "Blocks" with responsive grid, category pills, search bar
- BlockCard: CDN poster thumbnail, video autoplay on hover, duration + WebGL badges
- On-demand install: blocks append as sub-compositions on timeline; components
  overlay at start=0 spanning full duration with transparent background patching
- TIMELINE_BLOCK_MIME drag-and-drop to timeline
- BlockParamsPanel (Phase 3 scaffold) auto-opens for parameterized blocks

Registry manifests:
- All 58 blocks backfilled with preview: { video, poster } CDN URLs
- All 20 components normalized to object format + poster URLs added
- 6 blocks annotated with params (Liquid Glass/Background, Portal, Chart,
  Logo Outro, Magnetic)
- flowchart-vertical preview generated and uploaded to CDN
2026-05-18 21:15:15 -04:00
James 7e0a447325 refactor: drop unused exports detected by fallow auto-fix
Run `fallow fix --auto-fixable` to remove `export` keywords from symbols
fallow's reachability analysis identifies as unused. Keeps only the cases
where the symbol is still referenced internally in its own file (so
removing `export` doesn't surface a new oxlint `no-unused-vars` error).

Result: fallow dead-code findings drop from 276 → 208 (68 fewer unused
exports), with no behavior change — each symbol is still defined and used
exactly the same way within its file.

Reverted ~20 files where fallow's auto-fix would have created cascading
"declared but never used" lint errors — those are cases where the symbol
isn't used at all, and properly cleaning them up means deleting the
declaration, not just dropping `export`. Better to land that as a
separate, narrower PR rather than mixing it into a mechanical de-export.

Also reverted four false positives where fallow missed real consumers:
- `captureCost.ts` (renderOrchestrator has two separate import blocks
  from the same module; fallow only saw the first)
- `propertyPanelHelpers.ts`, `domEditingLayers.ts` (real internal uses
  fallow's reachability missed)
- `render.ts` (functions imported via `await import()` dynamic import,
  which fallow's static analysis doesn't follow)

Test plan: bun run --filter '*' typecheck (clean), oxlint + oxfmt clean,
cli/core/studio/engine vitest suites pass (335 + 917 + 576 + 605 tests).
2026-05-19 00:51:56 +00:00
James Russo 3eec777a29 Merge pull request #942 from heygen-com/05-18-ci_add_fallow_audit_job_pr-scoped_new-only_gate_
ci: add fallow audit job (PR-scoped, new-only gate)
2026-05-18 20:46:55 -04:00
Miguel Ángel d873364746 test(producer): regenerate 7 stale regression baselines (#946)
## Summary

- Regenerate golden baselines for 7 regression tests that were deterministically failing due to stale baselines from before the recent renderer fixes
- Root cause: renderer changes (revert flattenInnerRoot, late-bind polling removal, conditional rebind, sub-comp timeline readiness polling) changed visual output, but baselines were never regenerated
- CI's `Detect changes` job was skipping shards on non-engine commits, masking the failures — making it look like flaky tests when in reality they failed every time shards ran

## Tests regenerated (all in Docker with pinned Chrome 148.0.7778.167)

| Test | Previously failed frames | Baseline age |
|---|---|---|
| `gsap-letters-render-compat` | 86/100 | old |
| `typegpu-adapter` | 76/100 | May 13 |
| `style-7-prod` | 60/100 | PR #368 (very old) |
| `style-15-prod` | 59/100 | May 17 (pre-renderer-fixes) |
| `style-18-prod` | 19/100 | old |
| `style-3-prod` | 7/100 | May 17 (pre-renderer-fixes) |
| `style-8-prod` | 1/100 | borderline |

## Test plan

- [x] All 7 tests pass locally in Docker with 0 failed frames
- [x] Verified deterministic — ran style-7-prod twice, same result both times
- [x] All 8 CI regression shards green
- [x] Other tests (style-1, 2, 4, 5, 6, 9, 10, 11, 12, 13, 16, 17, overlay-montage, sub-composition-video, vignelli-stacking, etc.) still pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-19 02:38:07 +02:00
Miguel ÁngelandClaude Sonnet 4.6 13846554f5 test(producer): regenerate gsap-letters-render-compat baseline
Missed in the initial batch — 86/100 frames were failing. Regenerated
in Docker with the same pinned Chrome build. Now passes with 0 failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:43:13 +00:00
Miguel Ángel d9183ba27d Merge pull request #940 from heygen-com/worktree-fix+seek-slider-duration
fix(studio): seek slider respects effective timeline duration with appended blocks
2026-05-19 00:39:13 +02:00
Miguel Ángel d0669fa86c Merge pull request #943 from CypherPoet/docs/canonical-pipeline-page
docs: add canonical pipeline guide (#937)
2026-05-19 00:38:25 +02:00
Miguel Ángel 1a63a945e0 fix(studio): address review — GSAP-only fallback, drop dead alias, add tests
- [blocker] When playerAdapter is null (GSAP-only runtimes with no
  win.__player), the fallback path now uses the best available timeline
  adapter instead of returning null. Track timelineAdapter across the
  __timeline and __timelines paths, then use it as the base for
  createStaticSeekPlaybackAdapter.

- [nit] Remove dead baseAdapter alias — use bestAdapter directly.

- [tests] Add 4 tests: readTimelineDurationFromDocument with
  data-hf-authored-duration fallback, createStaticSeekPlaybackAdapter
  with seek-only adapter (no renderSeek), and pause lifecycle.
2026-05-18 18:28:34 -04:00
Miguel ÁngelandClaude Sonnet 4.6 5a6c3dd45f test(producer): regenerate 6 stale regression baselines in Docker
Renderer fixes (revert flattenInnerRoot, late-bind polling removal,
conditional rebind, sub-comp timeline readiness polling) changed visual
output but baselines were never regenerated. CI's Detect changes job
was skipping shards on non-engine commits, masking the failures.

Regenerated in Docker (Dockerfile.test) with pinned Chrome 148.0.7778.167:
- style-7-prod (60/100 frames failed, baseline from PR #368)
- style-15-prod (59/100 frames failed)
- style-18-prod (19/100 frames failed)
- style-3-prod (7/100 frames failed)
- style-8-prod (1/100 frames failed, borderline)
- typegpu-adapter (76/100 frames failed)

All 6 now pass locally with 0 failed frames.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:28:10 +00:00
Miguel Ángel 10af9655f2 fix(studio): restore root-id resolution comment in __timelines path 2026-05-18 18:20:34 -04:00
Miguel Ángel b32a62d4d1 fix(studio): seek slider respects full document timeline duration
getAdapter() returned the runtime player or GSAP timeline adapter directly
when its duration was > 0, even when the document's timeline (computed from
sub-composition data-start + data-hf-authored-duration attributes) extended
beyond that duration. This capped the seek slider, seek clamping, and
sub-composition visibility at the adapter's shorter value.

Now each adapter path checks whether the document duration exceeds the
adapter's own duration. When it does, the adapter falls through to
createStaticSeekPlaybackAdapter which wraps the runtime player with the
correct effective duration, allowing seeking and preview across the full
timeline range.
2026-05-18 18:15:33 -04:00
Miguel Ángel ca6069763d fix(studio): patch runtime clock when document duration exceeds player duration
When the runtime player's clock duration is smaller than the effective
timeline (computed from data-start + data-hf-authored-duration on
sub-composition elements), the seek is clamped too early and sub-compositions
beyond the clock duration are invisible.

Detect this mismatch in getAdapter() and pad the root GSAP timeline to the
document duration, then force a timeline rebind so the clock updates.
2026-05-18 18:10:43 -04:00
Miguel Ángel 1a4e534d0f fix(studio): uncap seek clamp to include timeline element range
The seek function clamped to adapter.getDuration() which only knows the
root composition's authored duration. Appended timeline elements extend
beyond this range. Compute the effective max from both the adapter duration
and the store's element boundaries so scrubbing reaches the full timeline.
2026-05-18 17:28:45 -04:00
Miguel Ángel 837ffdcb5d Merge pull request #941 from heygen-com/worktree-feat+media-design-panel
feat(studio): media properties panel for video/audio elements
2026-05-18 23:28:08 +02:00
Miguel Ángel 1098f3af84 fix(studio): polish media panel UX and preserve selection on undo
- Move Timing + Media sections above Layout in the Design panel
- Remove LayerTree from Design panel (redundant with Layers tab)
- Replace Rate and Media Start with sliders matching Volume's UX
- Replace Position DetailField with SelectField to match Fit height
- Remove Poster field (not useful for HyperFrames compositions)
- Show absolute filesystem path for Source (resolves symlinks)
- Add Copy button for source path with checkmark feedback
- Preserve element selection on undo/redo instead of clearing it
2026-05-18 17:26:38 -04:00
James Russo 2fd161b943 Merge pull request #939 from heygen-com/feat/auto-size-chunk-size-when-undefined
feat(producer): auto-size chunkSize from maxParallelChunks when undefined
2026-05-18 17:25:08 -04:00
CypherPoet c088588507 docs: add canonical pipeline guide (#937)
Move the 7-step production pipeline (Capture → Design → Script → Storyboard
→ VO + Timing → Build → Validate) into its own dedicated guide so it serves
any Hyperframes project, not just website-to-video. Expand each step with
file contents, project layout, gates, and iteration patterns. Reference the
new page from website-to-video, quickstart, prompting, and launch-videos.
2026-05-18 16:18:54 -05:00
James 62c800aec3 ci: add fallow audit job (PR-scoped, new-only gate) 2026-05-18 21:18:01 +00:00
JamesandClaude Opus 4.7 a8499632c6 test(producer): preserve single-chunk-path coverage + add auto-size integration
Address PR review feedback on #939:

- Pin chunkSize=240 on the golden planDir layout test so the 1-chunk path
  through plan() stays exercised after the auto-sizer change. Assert
  chunkCount === 1 explicitly (previously just >= 1).
- Add an integration test that runs plan() with chunkSize=undefined and
  asserts the auto-sizer produces multi-chunk output end-to-end
  (chunkCount=3, encoder.gopSize=10, encoder.chunkSize=10) for the same
  30-frame fixture.
- Document the GOP/file-size trade-off on the chunkSize docstring so
  adopters who optimize for output bytes know to pin chunkSize.
- Update the resolveChunkPlan docstring formula to reference the operative
  variable (resolvedChunkSize) instead of the now-ambiguous chunkSize.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:09:31 +00:00
James Russo 1edea8afe4 Merge pull request #938 from heygen-com/chore/fallow-config-and-cleanup
chore: add fallow config and fix high-signal findings
2026-05-18 16:52:18 -04:00
James Russo cc7622cc42 Merge pull request #926 from heygen-com/fix/ci-chrome-pin-and-psnr-harness
fix(ci): pin chrome-headless-shell + clamp PSNR checkpoint to a valid frame
2026-05-18 16:51:29 -04:00
JamesandClaude Opus 4.7 fd3fce9955 refactor(producer): tighten resolveChunkPlan assertion + trim comments
Address self-review findings:
- assertPositiveInteger now only runs on the caller-supplied path so the
  error message names `configChunkSize` only when the caller actually
  passed one. Previously, the assertion fired against `resolvedChunkSize`
  on both paths and would have lied about the offending input.
- Drop the call-site comment that narrated the diff/history; the
  function docstring already covers the contract.
- Drop the internal-track name and date from the MIN_CHUNK_SIZE rationale
  and the test block header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:37:25 +00:00
Miguel Ángel f66c47c812 fix(studio): derive effective duration in PlayerControls instead of useEffect sync
Replace the useEffect that pushed effectiveTimelineDuration into the player
store with an inline derived selector in PlayerControls. The selector computes
Math.max(duration, maxElementEnd) directly from store state, avoiding the
effect-based sync anti-pattern entirely.
2026-05-18 16:35:59 -04:00
Miguel Ángel 96f9462e42 feat(studio): add media properties panel for video/audio elements
Adds a new Media section to the Design panel that appears when a <video>
or <audio> element is selected. Controls include volume (slider),
playback rate, media start offset, loop/muted toggles, and for video:
object-fit, object-position, poster, and has-audio-track toggle.

Extends the source patcher with an "html-attribute" operation type for
native HTML attributes (loop, muted, poster) that don't use the data-
prefix. Adds coalesceKey to attribute commits so rapid slider/scrub
edits merge into a single undo entry.
2026-05-18 16:34:41 -04:00
Miguel Ángel 43f0a2b2bb fix(studio): seek slider respects effective timeline duration with appended blocks
The seek slider read duration from the player store, which was set from the
iframe adapter's getDuration() — only aware of the root composition's authored
data-duration. Appended sub-compositions (via Blocks panel) extend the timeline
but the slider stayed capped at the original duration.

Sync effectiveTimelineDuration (which accounts for all timeline elements) into
the player store, and prevent adapter callbacks from overwriting a larger
effective duration back down to the authored value.
2026-05-18 16:32:03 -04:00
JamesandClaude Opus 4.7 e79a8faa42 feat(producer): auto-size chunkSize from maxParallelChunks when undefined
Previously, plan() defaulted chunkSize to 240 on a `?? DEFAULT_CHUNK_SIZE`
line, so a 660-frame composition with maxParallelChunks=16 ended up at 3
chunks (ceil(660/240)) regardless of the caller's fan-out intent.

When config.chunkSize is undefined, auto-size from maxParallelChunks:
  effectiveChunkSize = max(MIN_CHUNK_SIZE, ceil(totalFrames / maxParallelChunks))

MIN_CHUNK_SIZE=10 keeps per-chunk fixed overhead from swamping the
parallelism gain on tiny renders. Explicit numbers, including 240, take
precedence over the auto-sizer — no behavior change for callers that set
chunkSize explicitly.

Surfaced by the lever-1 chunk-scaling benchmark on 2026-05-17.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:29:34 +00:00
James 030a2b32ef chore: oxfmt .fallowrc.jsonc 2026-05-18 20:07:08 +00:00
James 21e68d9b50 ci: re-trigger regression on PR #926 (suspected shard-3 flake) 2026-05-18 18:58:39 +00:00
James 2087d5dab2 chore: add fallow config and fix high-signal findings
Configure fallow via .fallowrc.jsonc so its analysis reflects this repo's
real entry surface, then fix the genuine issues it found.

Fallow noise reduction (601 → 276 dead-code findings):
- Ignore docs/, test fixtures, skill test-corpora, registry/, examples/
- Declare worker entry points loaded dynamically by file path
  (pngDecodeBlitWorker.ts, shaderTransitionWorker.ts)
- Declare runtime IIFE entry (core/src/runtime/entry.ts) built outside the
  import graph by build-hyperframes-runtime-artifact.ts
- Declare bun:test files in producer + aws-lambda as test entries
- Ignore dynamically-resolved deps: tsup external (puppeteer-core, esbuild,
  giget), peer/static-file (gsap in player perf tests), workspace deps
  hoisted by bun (happy-dom, @hyperframes/*), and @fontsource/* packages
  read via readFileSync in generate-font-data.ts

Extract inline build:fonts scripts:
- packages/{cli,producer}/package.json had multi-line `node -e ...` blobs
  containing braces that fallow mis-parsed as glob alternate groups. Moved
  to dedicated build-fonts.mjs scripts.

Fix duplicate exports:
- Remove dead FileIcon alias in studio/SystemIcons.tsx (FileTreeIcons.tsx
  has the real, used one)
- Consolidate ValidationResult: drop the identical duplicate in
  gsapParser.ts; both parsers now import from core.types
- Suppress intentional namespace patterns (per-namespace ML manager
  exports; CLI per-command 'examples' convention; fileServer.ts test-only
  isPathInside which has different symlink semantics from utils/paths.ts)

Break circular dep (studio/components/editor):
- manualEditsDom.ts re-exported clearStudioPathOffset / clearStudioRotation
  / clearStudioBoxSize from manualEditsSnapshot.ts, which imports four
  helpers from manualEditsDom.ts — back-edge cycle
- Re-export moved to manualEdits.ts (the package-public barrel) where the
  rest of the snapshot re-exports already live; underlying files now form
  a clean DAG

Remove genuinely unused deps:
- studio: motion (no imports anywhere), codemirror (umbrella package; the
  @codemirror/* sub-packages are used directly)
- cli: mime-types (plus its only consumer src/utils/mime.ts, which was a
  hardcoded mime table that didn't use the package), and its now-stale
  tsup external entry

Verified: typecheck across core/cli/producer/studio is clean, oxlint
+ oxfmt pass, manualEdits.test.ts (18 tests) and core parser tests (69
tests) still pass.

Deferred follow-ups (real findings, separate PRs):
- 8 circular deps in producer/services/render/stages/ — renderOrchestrator
  ↔ captureHdr* / captureStage / extractVideosStage form a hub cycle
- ~14 unused files in producer/src/services/ that look like dead
  re-export shims to @hyperframes/engine, but aren't in the public
  exports map — need to confirm no deep-import consumers before deletion
- waveform.ts complexity hotspot
2026-05-18 18:57:21 +00:00
Miguel Ángel 27efcd0f80 chore: release v0.6.22 v0.6.22 2026-05-18 14:23:43 -04:00
Miguel Ángel 4c29b43b85 Merge pull request #935 from heygen-com/worktree-fix+canvas-zoom-improvements
fix(studio): canvas zoom improvements — zoom to cursor, reset button, border fix
2026-05-18 20:07:20 +02:00
Miguel Ángel e25bcf989a fix(studio): address PR review — split pan clamp, pin invariant, drop dead guard
- Split pan clamping: clampPreviewPan (drag/wheel-pan) stays narrow
  (Math.max(0,...) — content pins to center when smaller than viewport).
  New clampPreviewPanForZoom (Math.abs) gives the wide range only to
  cursor-anchored zoom, preventing middle-mouse drag from pushing content
  off-screen at low zoom levels.

- Pin transform-origin invariant: comment on the stage div noting that
  resolvePreviewWheelZoom cursor math depends on center-center pivot.
  New test verifies a non-center cursor keeps the same content-space
  point fixed across a zoom step.

- Remove dead Math.abs(oldScale) > 1e-6 guard — oldScale >= 0.25 always
  (clampPreviewZoomPercent floors at MIN_PREVIEW_ZOOM_PERCENT = 25).

- Skip setSettledZoom re-render when the value didn't change — uses a
  functional updater that returns the previous state object when all
  three fields match, avoiding a React re-render cascade through Player.
2026-05-18 14:04:53 -04:00
Miguel Ángel 3af7f1cba9 Merge pull request #934 from heygen-com/feat/texture-caption-any-texture
feat(registry): make caption-texture-lava work with any texture
2026-05-18 20:00:48 +02:00
Miguel Ángel 0150a0a1b4 fix(studio): canvas zoom improvements — zoom to cursor, reset button, border fix
- Zoom anchors to cursor position instead of always zooming toward center.
  The resolvePreviewWheelZoom function now accepts cursorX/cursorY (offset
  from viewport center) and uses the standard zoom-to-point formula to
  adjust pan so the content point under the cursor stays fixed.

- Add visible "Reset" button (bottom-right) showing current zoom % when
  not at fit zoom. Driven by settledZoom state that updates after the
  200ms settle debounce, so no re-renders during active zoom gestures.

- Fix border-expands-inward bug: scaleIframeToFit in the player now uses
  offsetWidth/offsetHeight instead of getBoundingClientRect. The latter
  returns values inflated by ancestor CSS zoom, causing double-scaling
  that made the iframe appear smaller than its container.

- Fix zoom HUD appearing during pan: split applyZoom (shows HUD) from
  applyPan (silent) so trackpad/middle-mouse panning no longer flashes
  the zoom percentage overlay.

- Fix stale closure performance regression: replace stageSize in effect
  dependency arrays with stageSizeRef pattern. The old deps caused wheel
  and pointer handlers to re-register on every viewport resize.

- Widen pan clamp range (Math.abs instead of Math.max(0,...)) so content
  can float within the viewport when zoomed below fit — required for
  zoom-to-cursor to work correctly at any zoom level.

Closes #900
2026-05-18 13:24:17 -04:00
Miguel Ángel 09af2f9cc1 feat(registry): rename caption-texture-lava to caption-texture with multi-texture support
Rename component to caption-texture and bundle 6 popular textures
(lava, marble, metal, wood, concrete, rock). The texture is configurable
via the texture composition variable (default: lava).

Usage: hyperframes render --variables '{"texture":"marble-012"}'

Tested: lava, marble-012, metal-046-b all render correctly with
distinct visual patterns.
2026-05-18 12:40:48 -04:00
Miguel Ángel 8163f38077 chore: release v0.6.21 v0.6.21 2026-05-18 11:29:01 -04:00
Miguel Ángel 4b6ed3197f Merge pull request #928 from heygen-com/worktree-feat+registry-us-map
feat(registry): add us-map choropleth block
2026-05-18 17:17:09 +02:00
Miguel Ángel 9f18425eae fix(lint): detect Three.js loaded via importmap or ES module import
The missing_three_script rule only checked <script src> attributes
for Three.js. Now also detects:
- importmap entries defining "three"
- ES module import/from statements referencing "three"

Closes #931
2026-05-18 11:06:51 -04:00
Miguel Ángel 15c2d7644d ci: re-trigger regression shards 2026-05-18 10:42:16 -04:00
Miguel Ángel 4bf2fa5cd1 fix: revert flattenInnerRoot, use host-level authored-id + count-based rebind
Three changes to fix regression failures without breaking baselines:

1. Revert flattenInnerRoot in producer — use the original innerHTML
   inlining that preserves the existing DOM structure. Instead, set
   data-hf-authored-id on the HOST element so the scoped proxy can
   still rewrite #id selectors for sub-composition scripts.

2. Revert compiled.html baselines to main (no DOM structure changes).

3. Use timeline-count comparison instead of poll duration to decide
   whether to call __hfForceTimelineRebind. Compare timeline count
   before vs after the poll — rebind only when new timelines appeared
   during polling. This correctly identifies async compositions
   regardless of fetch speed, while leaving sync compositions
   untouched.
2026-05-18 10:20:17 -04:00