Commit Graph
5626 Commits
Author SHA1 Message Date
Miguel Ángel 09c0307c3f feat(producer): render at 720p and 480p from a larger composition (#5724)
## What a user can do now

Render a video at 720p or 480p: `hyperframes render --resolution 720p` (or `480p`, or the presets `landscape-720p`, `portrait-480p`, `square-720p` and so on) gives a 1280×720 or 854×480 file from a 1080p composition, and the same presets work through the studio-server render route. Before, any preset smaller than the composition was refused ("Downsampling via --resolution is not supported").

## How

- `parsers/src/types.ts`: six presets, `{landscape,portrait,square}-{720p,480p}`, and the `720p` / `480p` aliases (aspect-agnostic, like `1080p` and `4k`). 480p at 16:9 is 854×480: 480 × 16/9 is 853.33, and 4:2:0 encoders need even sides.
- `checkOutputResolutionCompatibility` now allows a preset smaller than the composition. A smaller preset may be off by under one pixel on its long side (the 854); a larger one must still match exactly and be a whole multiple, as before. The aspect remap that `--resolution 1080p` / `4k` apply on their own (`suggestMatchingPreset`, used by the CLI preflight and the compile stage) returns only a preset in the asked tier, never another: with the looser match for smaller presets, a 1922×1080 composition asked for `4k` would otherwise have been remapped to 480p. It is refused instead, as before.
- `resolveDeviceScaleFactor` returns the long side's ratio, so a smaller preset renders at a device scale factor under 1: Chrome lays the page out at composition size and rasterizes each frame straight at 1280×720 or 854×480, the way 4K already renders at 2. For 480p that is 854/1920: the width lands on 854 exactly and the height's 480.375 rounds to 480, so no pad column is added. Output sizes round as Chrome rounds the screenshot.
- The streaming encoder was given the composition's size instead of the output size on both streaming paths (`renderOrchestrator.ts`). 4K never noticed, as both sizes are even; at 480p the encoder skipped its even-size pad and libx264 refused an odd frame (`width not divisible by 2 (853x480)`). It now gets the output size, as the disk-frame encode already did.
- The paths that refused DPR > 1 (BeginFrame, drawElement, the Linux fast-capture gate) now refuse any DPR other than 1, since none of them honor a scale; scaled renders take the screenshot path, as 4K does.
- `render/outputScaleRoute.ts`: the layered compositor (HDR content, and shader transitions in webm and mov) sizes its layers and encoder from the composition and never scales, so a scaled output on that route now stops with a named error, checked where every route and replan is settled (`syncCapturePlan`, beside motion blur's route check). Before, a 1080p request from a 4K composition with shader transitions as mov would have encoded 3840×2160. HDR with `--resolution` was already refused in preflight; 4K on this route had the same gap on main and gets the same error.

## What I measured

- Real renders from source on a Linux box (`bun src/cli.ts render registry/examples/nyt-graph --resolution 720p|480p|1080p`), checked with ffprobe: 720p `h264, 1280x720, yuv420p, 15.0 s`; 480p `h264, 854x480, yuv420p, 15.0 s`; 1080p unchanged at 1920x1080. A frame of each, looked at: same chart and layout, crisp text, and the 480p right-edge column is the page's own color (250,248,242), not a pad.
- Before the encoder-size fix the 480p render failed with `[libx264] width not divisible by 2 (853x480)`; after it, it renders.
- `outputScaleRoute.test.ts`: the real route choice for a 4K composition with shader transitions as mov (`outputSupportsPageSideShaderCompositing`, `shouldUseLayeredComposite`, `createCapturePlan`, `resolveDeviceScaleFactor`) lands on `hdr_layered` at scale 0.5 and is refused. CLI preflight and the compile stage both refuse `4k` on a 1922×1080 composition.
- fails without the fix: without the same-tier rule, the preflight and compile tests for 1922×1080 at `4k` pass the render through at 854×480; without the route check, the mov test does not throw.
- At d33c4458: producer `outputScaleRoute` + `compileStage` 18/18 three runs in a row, `shared` (bun) 9/9, cli `render` 104/104 once and its new test three runs in a row; tsc clean in producer and cli. Earlier at e72f6507: parsers `outputResolutionCompatibility` 20/20, core `index` 26/26, producer `resolveDeviceScaleFactor` 14/14, cli `render` + `parseOutputResolution` 122/122, studio-server `render` 53/53. The new cases fail on main (the presets don't exist; smaller presets are refused). tsc clean in parsers, engine, producer and cli; oxlint and oxfmt clean.

## What I did NOT exercise

- Distributed (Lambda / Cloud Run) renders at 720p/480p: the chunk worker's sizes round the same way, but I did not run a distributed render.
- Studio's render queue keeps its own copy of the rule (`RenderQueue.tsx` `scaleApplies`) and offers no 720p/480p option; unchanged here.
2026-10-16 20:58:15 -07:00
Miguel Ángel e6ce733796 fix(lint): stop flagging dir on <html>; a render test now guards RTL export (#5744) 2026-10-17 03:25:12 +00:00
Miguel Ángel 40c64255e8 fix(skills): regenerate manifest after HyperFrames guidance update (#5747) 2026-10-16 19:57:12 -07:00
kevin 6dba67a0c3 fix(cli): honour HYPERFRAMES_SKIP_SKILLS in skills update and bare skills (#5743)
skills update (bare or named) and bare skills now exit 0 with a one-line
notice instead of installing or pruning when HYPERFRAMES_SKIP_SKILLS=1, so a
pinned skills install survives the router's trigger-time update. Closes #5695.
2026-10-17 02:34:40 +00:00
Miguel Ángel eab6ca15b1 fix(cli): timeline lists clips nested in a timed element and lint flags ones that never show (#5732)
* fix(cli): timeline lists clips nested in a timed element and lint flags ones that never show

* docs(lint): wrap the nested-clip entries

* docs(skills): format the nested-clip note and refresh the skills manifest

* fix(cli): keep timeline edits from colliding with a clip's own wrapper

* fix(cli): cut clips nested in a wrapper at their sub-composition's in-point

* refactor(lint): split the nested-clip visibility checks to keep the audit green

* style(cli): restore the timeline test's formatting
2026-10-17 02:27:17 +00:00
Miguel Ángel 4e6ee6d230 fix(engine): a WebGPU block mounted as a sub-composition gets WebGPU like a root one (#5735)
compositionRequiresWebGpu read only the first data-composition-id tag, the
page root. In bundled HTML a mounted composition's root keeps
data-requires-webgpu but carries data-hf-inner-root instead of a composition
id, so a pasted-in WebGPU block launched without WebGPU and failed with
'WebGPU unavailable' even with PRODUCER_ALLOW_SOFTWARE_WEBGPU set.
2026-10-17 00:52:55 +00:00
Miguel Ángel c38e9f350b fix(core): a composition mounted twice keeps each mount's own timeline (#5729) 2026-10-17 00:47:28 +00:00
Miguel Ángel 35b17c3fe6 chore: release v0.8.167 (#5730) v0.8.167 2026-10-17 00:44:30 +00:00
Miguel Ángel b1fbc16b6c fix(cli): check finishes on video-heavy projects, its geometry seeks skip only video-frame waits (#5715)
* fix(cli): check's dense geometry seeks stop waiting for video frames they never read

* test(cli): race the dense seek against the barrier ask instead of a timer

* fix(cli): keep the geometry seek's comment share at its base

* fix(cli): dense geometry seeks still wait for frame sources, only video frames are skipped
2026-10-16 23:50:49 +00:00
Miguel Ángel 13ea834959 perf(core): hold a clip at 0 inside a later scene until it is due (#5716) 2026-10-16 23:16:13 +00:00
Miguel Ángel 12d8df37f4 perf(core): a preview's composition scripts give the page a turn every 50 ms, not one long task (#5713) 2026-10-16 23:16:08 +00:00
Miguel Ángel b1cf6e3c51 fix(core): bundling a project no longer blocks the host's thread on esbuild (#5714) 2026-10-16 21:23:28 +00:00
Miguel Ángel 41486b9725 fix(cli): page loads stop timing out while a busy machine's Chrome warms up (#5705)
* fix(cli): page loads stop timing out while a busy machine's Chrome warms up

* fix(cli): keep the page-load helper comment-free and its files in the bench filter
2026-10-16 19:35:44 +00:00
Miguel Ángel eace3d28b6 test(cli): caption image test checks the close itself, not a reusable file number (#5707) 2026-10-16 19:12:50 +00:00
Rudy Mizrahi Celekli 0bff8342c3 perf(lint): strip stylesheet comments once per canvas zoom scan (#5712) 2026-10-16 19:09:14 +00:00
Vance IngallsandClaude Opus 5.5 d05bc4215d perf(core): preview runtime scales with what plays, not with the film's clip count (#5674)
* perf(core): probe media volume with one timeline walk per pass, not one per element

Each bind, rebind and scene-swap pass probes every media element for volume
automation, and each probe flattened the whole captured timeline
(getChildren(true, true, false)) to ask whether any tween could move that one
element's volume. A pass cost elements x tweens. Elements with no automation are
never cached, so the cost came back on every rebind pass.

On a two-hour podcast episode (2,354 media clips, ~100k caption tweens) that was
several seconds of preview main thread per pass (CPU profile of the project's
open in Desktop: IE/PE -> getChildren, ~6 s).

volumeMoversOf() now walks the tweens once and lists the elements a tween names
volume on, plus whether any non-DOM setter is tweened. Each pass builds it lazily
and shares it with every probe; a probe without one walks as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(core): find the master bus from the first composition root

The top-level root is the first [data-composition-id] in document order, so
one match replaces a scan of every composition node, read once per clip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(core): lay out a clip again only when its layout inputs change

applyClipLayout ran on every timeline post and, for every root clip, lifted
display:none, read computed style and restored it. A clip whose inline
layout, class and data-width/height are as last laid out gets the same
answer, so it is skipped. On a 2,354-clip episode that was 4 of every 10 s
of playback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(core): a clip is laid out again only when its layout inputs change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(core): play a long film without re-walking it every frame

- The visibility pass skips root-level media far from the playhead during
  steady playback: hidden last pass, not loading or armed, and its window
  (widened by every look-ahead) clear of the span played and the loop start.
  Seeks, forced syncs and renders visit every node.
- The GSAP re-render saves and restores only the landing marks near the
  playhead, placed once per timeline structure; the zero-duration-callback
  check is cached the same way. A caption track of 38k sets was walked twice
  a frame.
- Registered timeline hosts, the root composition and the per-frame
  whole-film queries ([data-start], audio groups, timed audio) are kept for
  one timing revision; a render reads fresh.
- The runtime's own media release (src reset) and load events of clips with
  an authored length no longer count as timing changes, which re-posted the
  timeline about twice a second.
- The timeline payload resolves a stacking context once per parent.

2,354-clip episode in Desktop: playback 15 -> 67-89 fps (unminified runtime).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(core): check each element once per batch of raster-hint records, and cache the root from init

- A script building a large tree (a caption track) made each attribute record
  re-walk its subtree with getComputedStyle; a batch now checks each element
  once, an element whose own attributes changed still reading its own hints.
- The root composition cache installs the timing observer itself: a root
  with a declared length never reads the duration floors that installed it,
  so init looked the root up once per media clip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(core): type the playback visibility skip for any element, and cache clip layout only once loaded

The skip's predicate took an HTMLElement where the pass hands it a
StylableElement (tsconfig.runtime.json). The layout cache records a clip only
once the document has loaded, so a pass before stylesheets apply is redone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(core): realm-safe element checks in the timing caches; cut added comments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:33:19 +00:00
Miguel Ángel a43cdd93bf chore: release v0.8.166 (#5709) v0.8.166 2026-10-16 18:16:28 +00:00
Miguel Ángel 7266646264 fix(studio): a failed preview reload says which step it was still waiting for (#5700)
* fix(studio): a failed preview reload says which step it was still waiting for

* fix(studio): a failed reload of a cross-origin preview still cleans up and reports

Naming the step the reload waited for read the hidden frame's player, which throws
for a cross-origin frame and skipped the cleanup and the failure report.

* refactor(studio): name the reachable-player check instead of commenting it
2026-10-16 17:56:55 +00:00
Miguel Ángel 19a8d44b48 fix(core): scenes stop resetting during playback when the root has no animation of its own (#5693) 2026-10-16 17:32:02 +00:00
Miguel Ángel 1714f7c7b4 chore: release v0.8.165 (#5703) v0.8.165 2026-10-16 17:30:38 +00:00
Miguel Ángel 6f0e1f1185 test(cli): stop the dotenv startup test failing at random under load (#5701)
Each case left the CLI's background imports in flight. Under load they resolved
vitest's pending mock queue at the same time as the next case's import, which
then loaded the real command module. Wait for dynamic imports to settle before
resetting the mocks.
2026-10-16 17:07:16 +00:00
Miguel Ángel fff3636fe7 fix(core): exported scenes keep cards that fly in, instead of one frame of each (#5692)
* fix(core): exported scenes keep cards that fly in, instead of one frame of each

Under the producer's GSAP stub every timeline is a stand-in. GSAP's bundle sets
window.gsap twice, so the stub wrapped twice; and the runtime compared those
stand-ins against GSAP's real children. Nesting a scene into the root never
read back as nested, so the scene ended up parked under an abandoned composite
at time 0, and each render seek's play() snapped it back to 0. Tweens whose
from-values set opacity lost them after their first frame.

The stub now wraps a GSAP instance once, and the runtime compares the timeline
behind a stand-in.

* fix(core): keep the stand-in note to one line
2026-10-16 16:48:41 +00:00
Miguel Ángel 4e6fcdb3a9 fix(core): a caption line shows again after the playhead jumps back over it (#5690)
* fix(core): a caption line shows again after the playhead jumps back over it

The runtime hides an off-screen clip with an inline visibility:hidden that its
children inherit. A seek that jumps over a clip renders that clip's GSAP tweens
for the first time while it is hidden, so a set or autoAlpha tween records the
inherited "hidden" as the value to restore. Seeking back then writes it back,
and the element stays hidden for the rest of the session.

Interactive seeks now render GSAP with hidden clips shown, so first reads see
the authored style. Play ticks and render captures move forward and are
unchanged.

* fix(core): keep the files' comment share where it was

* fix(core): show hidden clips for any seek that jumps past what has rendered

Clicks and scrubs were covered, but render seeks jump too (hover frames,
thumbnails, stills at several times). The decision now lives in the seek
itself: a seek more than a frame and a half from the last one, to a time the
timelines have not rendered through yet, renders GSAP with hidden clips shown.
Frame-by-frame renders, play ticks and scrubs over already-rendered time pay
nothing.
2026-10-16 10:54:31 +00:00
Miguel Ángel 4d6b5be33b chore: release v0.8.164 (#5679) v0.8.164 2026-10-16 08:42:19 +00:00
Miguel Ángel 8be76fc86c fix(cli): pin website capture connections to public peers (#5668)
* fix(cli): pin website capture connections to public peers

* style(cli): keep capture policy explanations within comment limits

* test(cli): separate case-sensitive dotenv assertions

* fix(cli): honor capture deadlines and bootstrap isolation

* chore: trim redundant capture dotenv comments

* fix(capture): join signal cleanup and guard leading options

* docs(capture): trim redundant cleanup comment
2026-10-16 02:37:14 +00:00
Miguel Ángel 490ecafbc4 chore: release v0.8.163 (#5676) v0.8.163 2026-10-16 01:52:29 +00:00
Miguel Ángel c03532b285 fix(producer): an ffmpeg failure names how ffmpeg ended and keeps its last line (#5678)
* test: ffmpeg failures name how ffmpeg ended and keep its last line

* fix(producer): an ffmpeg failure names how ffmpeg ended and keeps its last line

* refactor(producer): say why in names, not comments
2026-10-16 01:38:20 +00:00
Miguel Ángel 8e17ee9300 fix(engine): preserve authored audio mix order (#5672) 2026-10-16 00:13:04 +00:00
Miguel Ángel 917ee5d313 fix(cli): ask to log in when publishing needs an account (#5670) 2026-10-15 23:43:46 +00:00
Miguel Ángel 1ee6754f71 fix(core): timed svg clips hide outside their window (#5661)
* fix(core): timed svg clips hide outside their window

* fix(core): type the clip visibility path for SVG as well as HTML elements

* chore(core): regenerate the position edits render script
2026-10-15 23:22:42 +00:00
Miguel Ángel 0d6bdd453b fix(cli): capture writes inline SVG logos whole and parakeet takes long audio (#5662)
* fix(cli): capture writes inline SVG logos whole and parakeet takes long audio

* fix(cli): keep the decode timeout comment under the line limit
2026-10-15 21:16:20 +00:00
WaterrrForeverandClaude Opus 5.5 cea8aa8a7d feat(cli): judge text behind a transparent video by its decoded alpha in check (#5657)
* feat(cli): judge text behind a transparent video by its decoded alpha in check

The layout audit treated every <video> as opaque over its whole box, so a
caption between footage and a remove-background cutout always failed
text_occluded and needed data-layout-allow-occlusion, which also hid a word
the subject's head fully covered.

check now draws the video's current (seeked) frame into one reused,
downscaled scratch canvas, once per sample and only for a video a text probe
lands under. A fully opaque frame keeps today's behaviour. A frame with
transparency is judged per rendered word against its alpha: partial cover is
an info finding carrying the most covered word's share, and a word 80% hidden
or two neighbouring words each half hidden is an error. A frame that cannot
be read falls back to opaque with a video_alpha_unread info note.

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

* test(cli): serve the video-alpha fixtures from memory, not request paths

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

* fix(cli): check finds words under narrow, mirrored or backed cutouts

Review follow-ups on #5657:
- A matte the probe walk passes over, even at clear-alpha points, now triggers
  the per-word check, so a feature between two probe columns (an arm, a mic
  stand) still fails the word it hides.
- Mirrored videos and images map viewport points through the flip; a rotated,
  skewed or 3D-transformed video falls back to opaque with video_alpha_unread.
- A video with an opaque CSS background keeps its whole box occluding.
- cli.mdx lists every unread cause and why a fully transparent frame is one.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 18:53:16 +00:00
WaterrrForeverandClaude Opus 5.5 08ee1bd5ad fix(cli): skip runtime by-products when fingerprinting a skill bundle (#5654)
The skill hash walked every file in a skill directory, so Python
__pycache__/*.pyc left by running a skill script (or node_modules, editor
swap files) changed it. Locally gen-skills-manifest then wrote a hash CI's
clean checkout could not reproduce, and an installed skill that had run a
script read as outdated forever. Skip those by-products on both the source
and the installed side so the hash stays a function of authored content.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 18:24:12 +00:00
Miguel Ángel a403ea1e8b ci(studio): re-run an edit accuracy shard on a fresh runner when its runner is too slow (#5659) 2026-10-15 17:43:09 +00:00
Miguel Ángel 0e9f380c2e feat(cli): render telemetry reports how busy the machine's CPU was (#5646)
* feat(cli): render telemetry reports how busy the machine's CPU was

render_complete and render_error now carry system-wide CPU busy share (average
and busiest 2 s window), cores busy in that window, and the one-minute load
average at start and end, for CLI and Studio renders. Nothing is sampled when
telemetry is off.

* test(cli): render tests use a private project folder instead of /tmp/project
2026-10-15 16:08:52 +00:00
Miguel Ángel 76222e41d7 chore: release v0.8.162 (#5649) v0.8.162 2026-10-15 11:54:09 +00:00
Xuanru Li 34776f3def fix(cli): analyze finalized audio assets after direct upload (#5647) 2026-10-15 09:23:10 +00:00
Xuanru Li ce060c7acc chore: release v0.8.161 (#5643) v0.8.161 2026-10-15 08:49:20 +00:00
Miguel Ángel 49c49504a4 fix(studio-server): an edit aimed at a clip never lands on an audio group sharing its id (#5645)
* fix(studio-server): an edit aimed at a clip never lands on an audio group sharing its id

* refactor(studio-server): one comment line, the rest in names
2026-10-15 08:19:28 +00:00
Xuanru Li 695110e46b feat(core): signal rhythm is rate-independent, refuses grids in the tracker, follows chords (#5644)
* fix(core): signal beats read the same at any decode rate and refuse a grid in the tracker

The tracker resamples every channel to 22.05 kHz and averages their band power, so the
CLI's native-rate decode and Studio's AudioContext decode get the same beats, and a mono
file and the same sound on two channels get identical ones.

The no-steady-beat floor moves from analyzeMusicFromSignal into the tracker, so
hyperframes audio, grep_audio and beats share one floor. It is 0.15: above every one of
66 synthetic ambient, speech, scattered-effect and noise clips (max 0.122), where the
wrapper's 0.2 also refused a third of GTZAN's produced music.

* feat(core): signal downbeats follow chord changes, and sections hear harmony

The tracker now keeps a 10 fps chroma beside its band energies. Downbeats weigh where the
chords change, over one beat and over two, with the low-end hit, instead of the low end
alone. Sections compare timbre and harmony equally.

Tuned on GTZAN's dev half: the tempo prior narrows to 0.8 octave, the local tempo stays
within 10% of the global one, and beats sit 15 ms after the onset peak, where annotators
put them.

* chore(core): trim redundant audio analysis comments

* refactor(core): keep the rhythm files under their comment share

Drops doc comments whose function or field name already says the same thing.
2026-10-15 07:31:04 +00:00
Xuanru Li 124492e427 feat(cli): the api provider speaks the HeyGen audio analysis contract (#5641)
* feat(cli): the api provider speaks the HeyGen audio analysis contract

The api rung now sends its key as x-api-key, a new Idempotency-Key on each
request, and 16 kHz mono 16-bit WAV: the channels are averaged and resampled
with a windowed sinc whose cutoff sits under 8 kHz, so nothing folds back in.
It reads the answer from a { "data": ... } envelope, or bare. Times stay in
seconds, and the answered times still snap onto beats measured on the signal
as decoded. The response type gains time_resolution_seconds.

* fix(cli): reject redirects from audio analysis requests

* fix(cli): preserve audio when stereo downmix cancels
2026-10-15 06:54:29 +00:00
Xuanru Li 1544b519a9 feat(cli): beats tracks the music with the TypeScript beat tracker, without Chrome (#5634)
* feat(cli): audio answers beats, downbeats, bpm, sections and describe through a provider ladder

Beats and downbeats try api, apple-mu, beat-this, then signal; sections try api, apple-mu,
then signal; describe tries api, then apple-sound. bpm is read off whichever provider placed
the beats. Each provider says whether it can run here and why not, and the ladder warns
when it passes one over.

The signal rung is a TypeScript tracker: log-band spectral flux, autocorrelation tempo with
a log-normal prior, Ellis dynamic-programming beats with a windowed local tempo, a low-band
bar phase, and Foote novelty sections snapped to beats. It reports no steady beat when the
onsets do not repeat.

The api rung posts the audio to HYPERFRAMES_AUDIO_API_URL with HYPERFRAMES_AUDIO_API_KEY and
moves each answered time onto the nearest signal beat within a quarter beat, reporting what
moved and what did not. beat-this runs Beat This! from a configured Python and local weights
and never downloads. apple-sound runs macOS's sound classifier through a Swift helper.
apple-mu reports that Music Understanding needs macOS 27.

words are no longer accepted: they come from hyperframes transcribe.

* docs(cli): audio's beats, downbeats, bpm, sections, describe and their provider ladder

* feat(cli): beats tracks the music with the TypeScript beat tracker, without Chrome

hyperframes beats and Studio's beat detection now share analyzeMusicFromSignal: band energies,
then the spectral-flux tracker in audioRhythm.ts, then each beat's loudness for its strength.
The CLI decodes with the same ffmpeg path as hyperframes audio, so beats no longer launches
headless Chrome. Studio still decodes in the browser and passes every channel.

The old analyzer averaged bpm-detective's tempo with an energy-peak tempo it had only compared
after folding both into one octave, and phased a rigid grid from noisy peaks. On 22 real tracks
with exact script grids it found the tempo 18% of the time with beat F 0.437; this finds it
91% of the time with F 0.887, and refuses the other two rather than guess.

Below a tracker confidence of 0.2 no beats are reported, so ambient or rubato music gets no
invented grid and the CLI still writes nothing. bpm-detective, the energy-peak grid, the
headless analyzer and its prebuilt bundle are gone.

* fix(core): tempo reads 96 BPM off beats on 10 ms frames, not 95.2

The tracker places beats on 10 ms frames, so a 625 ms beat lands 620 or 630 ms apart and the
median interval reported 95.2 or 96.8 BPM. Tempo now averages the intervals within 10% of the
median: as robust to a skipped or doubled beat, without the frame-sized step. Both
hyperframes audio and hyperframes beats read it.

* fix(cli): remove redundant audio context comment

* fix(core): remove redundant tempo interval comment

* fix(ci): record beat analyzer removals and browser evidence

* fix(core): keep flat audio novelty in one section

* chore: keep PR capture evidence out of the source tree

* fix(cli): honor BPM provider pins before analysis
2026-10-15 06:23:03 +00:00
James RussoandClaude Opus 5.5 f55b366175 fix(build): ship NodeNext-resolvable declarations from every package (#5532)
* fix(build): ship NodeNext-resolvable declarations from every package

tsc copies relative specifiers verbatim, so packages whose sources use
bundler-style extensionless imports (core, inspector) published .d.ts files
that a module/moduleResolution nodenext consumer cannot resolve. Core already
rewrote its emitted JavaScript for this, but not its declarations.

Move that rewrite to scripts/rewrite-relative-import-extensions.mjs, make it
cover .d.ts too (adding .js or /index.js, and failing the build when a
declaration import has no declaration), and run it after every tsc emit:
core, engine, sdk, producer, aws-lambda, gcp-cloud-run and inspector.

verify:packed-manifests now typechecks the clean packed consumer with
skipLibCheck false and type-only imports of every typed subpath, so an
unresolvable import inside a published declaration fails the gate and is
attributed to its package.

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

* chore(ci): allow the moved core rewrite-esm-extensions script deletion

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

* ci: rebuild the edit-accuracy bench when the import-extension rewriter changes

The package builds now run scripts/rewrite-relative-import-extensions.mjs, so main's
edit_accuracy paths filter has to include it (check-edit-accuracy-filter test).

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 05:59:41 +00:00
Xuanru Li ec97816a92 chore: release v0.8.160 (#5636) v0.8.160 2026-10-15 05:43:55 +00:00
Miguel Ángel d4c8d76fff fix(lint): warn on unbounded cosine powers in shaders (#5640) 2026-10-15 05:40:38 +00:00
Xuanru Li 8e340d220e feat(cli): audio answers beats, downbeats, bpm, sections and describe through a provider ladder (#5632)
* feat(cli): audio answers beats, downbeats, bpm, sections and describe through a provider ladder

Beats and downbeats try api, apple-mu, beat-this, then signal; sections try api, apple-mu,
then signal; describe tries api, then apple-sound. bpm is read off whichever provider placed
the beats. Each provider says whether it can run here and why not, and the ladder warns
when it passes one over.

The signal rung is a TypeScript tracker: log-band spectral flux, autocorrelation tempo with
a log-normal prior, Ellis dynamic-programming beats with a windowed local tempo, a low-band
bar phase, and Foote novelty sections snapped to beats. It reports no steady beat when the
onsets do not repeat.

The api rung posts the audio to HYPERFRAMES_AUDIO_API_URL with HYPERFRAMES_AUDIO_API_KEY and
moves each answered time onto the nearest signal beat within a quarter beat, reporting what
moved and what did not. beat-this runs Beat This! from a configured Python and local weights
and never downloads. apple-sound runs macOS's sound classifier through a Swift helper.
apple-mu reports that Music Understanding needs macOS 27.

words are no longer accepted: they come from hyperframes transcribe.

* docs(cli): audio's beats, downbeats, bpm, sections, describe and their provider ladder

* fix(cli): remove redundant audio context comment

* fix(core): keep flat audio novelty in one section

* fix(cli): honor BPM provider pins before analysis
2026-10-15 05:16:55 +00:00
Miguel Ángel db845d9409 fix(cli): retire old snapshot run folders (#5635)
* fix(cli): retire old snapshot run folders

* chore: satisfy snapshot comment limit

* fix(history): read and release snapshot leases through verified handles

* test(history): run lease races in Node and check released markers

* fix(studio): verify snapshot leases from open descriptors
2026-10-15 04:59:59 +00:00
Xuanru Li c694785df5 fix(core): faint SVG grain is not a click target, and a page reset is not an element's CSS (#5628)
* fix(core): a faint SVG grain is not the click target, and a page reset is not an element's CSS

A filter rect under an svg at opacity 0.07 was still picked. The veil check
required that element's own opacity and a background-image. A `*` rule matched
every element, so a rect was given the page reset as its CSS.

fails without the fix: clicking the grain selects the rect, and inspection attaches `*`.

* fix(core): keep picker comments within the source ratchet

* fix(core): preserve selector literals and SVG veil boundaries
2026-10-15 04:04:13 +00:00
Miguel Ángel 8f8f91c9de fix(runtime): prepare trimmed video frames before preview cuts (#5633) 2026-10-15 03:31:39 +00:00
Miguel Ángel 916ec37755 fix(studio-server): history copies a changing file without stalling its sweep (#5627)
* fix(studio-server): history copies a changing file without stalling its sweep

* fix(studio-server): a failed history copy leaves no partial file and keeps the mode

* style(studio-server): name the history copy for what it does
2026-10-15 03:16:32 +00:00