## What
Normalize both padded and trimmed render audio on the decoded sample timeline, encode the result as AAC in M4A, and keep the no-op path on stream copy. Final video muxing remains stream-copy only.
## Why
AAC packet-copy operations cannot guarantee the requested presentation duration. Trimming could retain a packet-boundary tail, while padding by concatenating a separately encoded raw-ADTS silence segment could create a timestamp/bitrate discontinuity that surfaced as roughly 606 ms of apparent audio drift in the production-style regression.
## How
- Trim with `atrim` plus `asetpts`, then AAC-encode into M4A.
- Pad with `apad=whole_dur`, cap the output at the target duration, and AAC-encode the continuous sample timeline into M4A.
- Preserve the M4A encoder-delay edit list when the normalized audio is copied into the final MP4.
- Use the normalized M4A path in both local and distributed assembly; leave already-correct audio unchanged.
- Remove the obsolete raw-ADTS concat helpers and the disproven final-mux duration workaround.
## Test plan
- [x] 18 focused pad/trim unit tests
- [x] Real-media integration regression covering the packet-timeline boundary
- [x] Producer typecheck and repository commit hooks
- [x] Faithful Docker `style-3-prod` regression: video/audio approximately 16.07 s, 1 ms drift, zero visual failures
- [x] Exact-head required CI was green before the review-cleanup commit; fresh exact-head CI is running
- [ ] Documentation updated (not applicable)
The regression was reproduced before the fix with 0.563416 s of reported audio/video drift. The sample-timeline normalization removes the malformed tail while preserving stream-copy final muxing and preview behavior.
* feat(lint): dense motion re-sampling for content_overlap
Transient text-on-text collisions during continuous motion (e.g. an
orbiting label card crossing the center card) overlap for a fraction of
a second that the sparse 9-point layout grid seeks straight past. The
content_overlap detector is correct; it just never gets a sample at the
crossing moment. Rerun ONLY content_overlap on an 8fps grid (text-only,
cheap) when the composition animates; findings feed the existing
persistence tiering unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(lint): unconditional dense content_overlap pass + honor 500ms floor
Round-1 blocker: the dense motion-overlap re-pass was gated on sparse-grid
geometry fingerprints changing, so an animation aliased to the sparse grid
(identical fingerprints, yet colliding between samples) bypassed the pass —
exactly the transient false-negative it was built to catch. Remove the gate:
the dense pass now runs unconditionally (bounded, text-only), driven by the
composition timeline rather than a fingerprint heuristic.
Round-2 follow-ups:
- Persistence-tier drift: at 8fps, occurrences>=2 spans only ~125ms, not the
~500ms the design intends, and it short-circuited before the ms floor.
content_overlap promotion now requires BOTH occurrences>=2 AND a literal
firstSeen..lastSeen span >= 500ms, so the wall-clock floor is honored at any
sampling density. Comment block updated to match.
- Sample cap scales to hold a true 8fps grid up to ~75s (raised 120 -> 600)
with an explicit note that longer comps degrade below 8fps to stay bounded.
Tests:
- Replaced the trivial "warning at every sample" test with a real between-grid
regression: a collision living only inside (3.5,4.5) — a gap the sparse grid
seeks past — is detected and, held ~750ms, promoted to error.
- Replaced the now-invalid "skips when static" test with one asserting the
dense pass runs even when sparse fingerprints are identical (aliased motion).
- Added a tiering regression: two dense occurrences spanning ~125ms stay a
warning (not error). Both new guards verified red before the fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* perf(lint): settle-free geometry seek for dense content_overlap pass
The dense overlap re-pass did up to OVERLAP_MAX_SAMPLES full-settle seeks
(120ms paint settle each, ~72s of pure sleep at the ceiling) even though
collectOverlap only reads getBoundingClientRect geometry, valid
synchronously after the timeline setTime. Add a settle-free
DENSE_GEOMETRY_SEEK_OPTIONS + driver.seekGeometry used only by the dense
loop; the base grid keeps full-settle driver.seek.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(lint): document + cover content_overlap 500ms boundary for sparse callers
The occurrences>=2 AND heldMs>=500 promotion rule is a semantics change for
sparse callers (--samples 20, --at, short comps) whose two samples can land
<500ms apart. Document the change in the tiering comment and add boundary
tests: 499ms span stays warning, 500ms span promotes to error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(lint): make dense content_overlap seek genuinely geometry-only
Per review: DENSE_GEOMETRY_SEEK_OPTIONS only overrode settleMs, still
inheriting animationFrameSettle:double + waitForFontsMs:500 → ~3 frame
waits + font wait per seek → ~30s at the 600-sample cap. Geometry
(getBoundingClientRect) is valid synchronously post-setTime, so drop all
post-seek waits (animationFrameSettle:none, waitForFontsMs:0, settleMs:0).
Add options-level regression locking the geometry-only contract. Also fix
a stale comment name (detectMotionTextOverlap → collectMotionOverlapSamples).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style: collapse multi-line comments to single lines
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## What it catches
A gauge needle / clock hand / dial pointer / radar sweep that rotates about the **wrong pivot** — the recovered center-of-rotation sits far from the dial hub (e.g. `transform-origin` at the needle base or SVG element edge instead of the dial center). Visually the needle "wobbles" or orbits off-axis instead of sweeping cleanly about the hub.
This is a genuine gap in the current checks: `rotation_pivot_drift` (#2741) provably **cannot** catch it — a correct sweeping needle's bbox-center orbits identically to a broken one, so only a **dial-hub reference** distinguishes them. This is the separate hub-referenced check that analysis called for.
## How it works
- Sampler maps 2 material endpoints per frame via `getScreenCTM` (honors the actual rendered transform, independent of `svgOrigin`).
- Resolves the dial hub = shared center of the modal set of static concentric circles, or the arc-center of the largest static near-circular path (Kasa circle fit).
- Fits a circle to the endpoint trajectory to recover the true center-of-rotation; flags drift `> 0.35 * pointer_length`. One warning per hub.
- Never fires without a resolvable hub. Walks the rotation reference to the composition root (not the `<svg>`) so a pointer rotated by a `div` ancestor is measured correctly.
- Multi-body guard: `>= 2` bodies at distinct angular positions on one hub = orbit/atom system, not a dial → suppressed.
## Corpus evidence (autonomous geometry-fuzz run, 81 fuzzed diagrams)
- **7 / 7 true positives, 0 false positives across all 81 samples.**
- Assigned TPs: fuzz005, fuzz017, fuzz032. Bonus TPs: fuzz044, fuzz056, fuzz068, fuzz080.
- **The Gemini-3.6 video-judge itself MISSED all 4 bonus TPs** (`vlm_has_defects: false`) — the deterministic hub-reference check beats the VLM on this defect class.
- FPs driven to 0 by the two principled guards above: fuzz016 (planet arc rotated by a `div` ancestor) cleared by root-walk; fuzz055 (atom) cleared by the multi-body guard.
- fuzz080 reads as a false positive to the connector check but is a true positive here — confirms the architectural boundary between the two checks is drawn correctly.
## Validation
- Autonomous Gemini-3.6 **video**-judge fuzz run to surface candidate defects, then a **deterministic FP sweep** across all 81 rendered compositions (not VLM-gated — code inspection is the arbiter, since the VLM both over- and under-calls this class).
- 9 unit tests (`checkPipeline.offPivotRotation.test.ts`) + full check suite pass; `bun run build` green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The media_in_subcomposition rule blanket-errored every <video>/<audio>
inside a sub-composition, claiming nested media is "never seeked/decoded
and renders blank/black". This is false: the runtime discovers media with
a flat document.querySelectorAll("video, audio"), resolves each element's
host composition via closest("[data-composition-id]"), and rebases its
local data-start by the accumulated absolute start of every ancestor
composition (packages/core/src/runtime/{media,startResolver}.ts). Media
seeks and decodes at any nesting depth, verified end to end through the
producer render path.
- Remove the rule and flip its test to assert nested media is NOT flagged.
- Drop the now-dead media_in_subcomposition clause from the registry
components test.
- Drop the equivalent pre-render guard from the faceless-explainer and
pr-to-video assemble scripts.
- Correct the reference docs (hyperframes-core SKILL, data-attributes,
variables-and-media, composition-patterns; hyperframes-cli
lint-validate-inspect): media works at any depth. Preserve the one real
constraint, that a sub-comp timeline cannot reach host-root elements, so
host-root media motion is authored on the main timeline.
## What
Allow Studio Preview to serve an asset reached through a project-local symlink whose target is in a shared directory outside the project, including browser-hostile video assets that need an authoring proxy.
## Why
Preview rejected these assets with a 404 while the renderer accepted the same path. The initial static-route fix still failed for HEVC, ProRes, AV1, and VP9 assets because the proxy transcoder rejected the external target.
## How
Use lexical project-root containment for the read-only static asset route and proxy source request. The transcoder canonicalizes the target for ffmpeg and includes that identity in its cache key, while keeping the proxy cache inside the project. Composition source paths retain canonical containment because preview can persist their data-hf-id values.
## Test plan
- [x] Unit tests added/updated
- [x] `bun run --cwd packages/studio-server test` (397 tests)
- [x] Studio Server typecheck, oxlint, and oxfmt
- [x] External-symlinked hostile-video proxy route regression
- [x] Static-route traversal regression
- [ ] Documentation updated (not applicable)