Skip to content

feat(cli): audio answers beats, downbeats, bpm, sections and describe through a provider ladder - #5632

Merged
xuanruli merged 5 commits into
mainfrom
feat/audio-analysis-providers
Oct 15, 2026
Merged

xuanruli merged 5 commits into
mainfrom
feat/audio-analysis-providers

Conversation

@xuanruli

Copy link
Copy Markdown
Contributor

What

hyperframes audio --only beats,downbeats,bpm,sections,describe now answers the music's structure, each capability through the provider ladder #5602 set up, filled to its designed shape:

capability ladder (first that answers wins) on this Mac (macOS 26.5.2, CLT only)
moments, onsets, loudness, silence, envelope, kind signal unchanged, real
beats, downbeats api → apple-mu → beat-this → signal api: real against a local stand-in server (no backend yet). apple-mu: unavailable, see below. beat-this: real, run here from an isolated uv env with local small0 weights. signal: real
bpm read off whichever provider answered beats (median interval + a ±4 s curve) real; never a second tempo estimator
sections api → apple-mu → signal api: stand-in. apple-mu: unavailable. signal: real
describe api → apple-sound → none apple-sound: real (macOS SoundAnalysis via a Swift helper). Off macOS nothing local answers, and the value is null with a warning
words not here: --only words says to use hyperframes transcribe

Every provider has available() returning { ok: true } or { ok: false, reason }. A provider that is unavailable, whose check throws, or whose run fails is passed over, and the ladder warns once per capability with every reason, e.g.

! beats: used signal; api unavailable (HYPERFRAMES_AUDIO_API_URL is not set); apple-mu unavailable (Music Understanding needs macOS 27; this Mac runs 26); beat-this unavailable (HYPERFRAMES_BEAT_THIS_PYTHON is not set)

--quality local excludes api; --quality fast leaves only signal. --provider beats=beat-this pins one; --full prints each step.

Value shapes (JSON, file seconds like the rest; provenance.<capability> names the provider)

beats:     { times: number[]; confidence?: number; reason?: string; snap?: SnapReport; source?: string }
downbeats: { times: number[]; meter: number | null; confidence?: "low"; snap?: SnapReport; source?: string }
bpm:       { value: number | null; curve: { t: number; bpm: number }[] }
sections:  { spans: { start: number; end: number; label: string }[]; snap?: SnapReport; source?: string }
describe:  { tags: { label: string; score: number }[]; text?: string; source?: string }
SnapReport { reference: "signal beats"; toleranceMs: number; snapped: number; unsnapped: number[]; medianShiftMs: number | null }

beats.times is empty with reason: "no steady beat" when the signal tracker's confidence is under 0.05, or "too short for a steady beat" under 6 s. Signal downbeats carry confidence: "low".

Why

Agents cut to music. A beat grid, a bar, a section change and "what is this" are what they ask about after "where does this hit". The ladder lets a better model answer when one is present, and keeps the answer measured: an api time is moved onto a beat signal processing actually heard.

Measurement-led times (rule 1)

Each beat, downbeat and inner section boundary the api returns is snapped one at a time to the nearest signal-tracked beat, only when one is within a quarter of the answer's own beat period (125 ms at 120 BPM); otherwise it is kept and listed in snap.unsnapped. The service's tempo level and bar phase are kept. snap.medianShiftMs is the median distance moved, a direct read of how much the service and the signal agree.

Why a quarter beat: half a beat would let a time jump to the off-beat; an eighth would fail on a service that is off by a typical 20–60 ms at fast tempi. The separate beat benchmark simulated this: an API off by σ = 150 ms goes from F 0.28 raw to 0.97 snapped, and one off by only σ = 20 ms goes from 0.996 to 0.977, so snapping costs a little on an already precise answer. That cost is why it is reported per answer rather than hidden.

The api contract (for the HeyGen backend route)

  • POST <HYPERFRAMES_AUDIO_API_URL>, the URL exactly as configured, no path appended.
  • Authorization: Bearer <HYPERFRAMES_AUDIO_API_KEY>. Unset URL or key → unavailable. The key is never printed or put in a result.
  • Optional HYPERFRAMES_AUDIO_API_TIMEOUT_MS (default 120000).
  • Body multipart/form-data, two parts:
    • request (application/json):
      { "version": 1, "capabilities": ["beats", "downbeats"], "audio": { "sampleRate": 44100, "channels": 2, "duration": 31.5 } }
      capabilities ⊂ beats | downbeats | sections | describe. The CLI asks for beats and downbeats together in one request, and for sections and describe each in its own.
    • audio (audio/wav, filename audio.wav): 16-bit PCM of the analyzed span, channels and sample rate as decoded.
  • Response 200 application/json, times in seconds from the start of the uploaded audio; leave out what you cannot answer:
    {
      "version": 1,
      "source": "free text, reported as given",
      "beats": { "times": [0.48, 0.98] },
      "downbeats": { "times": [0.48], "meter": 4 },
      "sections": { "spans": [{ "start": 0, "end": 12.1, "label": "intro" }] },
      "describe": { "text": "optional sentence", "tags": [{ "label": "music", "score": 0.91 }] }
    }
  • Any other status fails that rung (the CLI falls through to the next); { "error": { "message": "..." } } in the body is shown in the warning. A missing capability, or one in another shape, also fails that rung.
  • No model name is in the CLI; source is how the service names what answered.

The types are AudioApiRequest / AudioApiResponse in packages/cli/src/audio/analysis/providers/api.ts.

apple-mu: unavailable, and why

Music Understanding (WWDC26 session 253, MusicUnderstandingSession) ships with macOS 27. This Mac runs macOS 26.5.2 with only the Command Line Tools SDK 26.5, which has no MusicUnderstanding framework, so there is no SDK to build or check a helper against. The provider is therefore real only as a gate: on macOS < 27 it says Music Understanding needs macOS 27; this Mac runs 26; on 27+ it says no Music Understanding helper yet: it needs the macOS 27 SDK to build; off macOS, macOS only. No Swift for it is written, so nothing guesses at the API.

beat-this: opt-in, never downloads

Beat This! (CPJKU, MIT) runs only through Python/PyTorch. It is available when HYPERFRAMES_BEAT_THIS_PYTHON names a Python that has beat_this (checked with importlib.util.find_spec, no torch import) and the small0 weights are on disk: HYPERFRAMES_BEAT_THIS_CHECKPOINT, or beat_this-small0.ckpt in the Torch hub cache. The weights are passed as a local path, which Beat This loads with torch.load; with a fresh empty TORCH_HOME a run here created nothing there. The audio is handed over as a WAV the CLI decoded, so its own .m4a limitation does not apply. Install steps are in docs/packages/cli.mdx.

signal beats and sections (rule 6)

  • Beats: per-band log power (5 bands/octave, 30 Hz–9 kHz, summed over channels so a polarity-flipped channel cannot cancel) → rectified flux above a ±0.4 s local mean → autocorrelation tempo with a log-normal prior around 120 BPM (+½ of the double lag) → Ellis (2007) DP with tightness 100 and a local period from 8 s windows held within 0.7–1.4× of the global one → trim weak end beats. A port of beat-bench/scripts/tsbeat.ts with local tempo on, as that benchmark recommended.
  • Confidence = autocorrelation clarity × min(1, onset lift / 3); under 0.05 → no steady beat.
  • Downbeats: the meter (4, or 3 with a 0.85 handicap) and phase whose beats carry the most low-band (<150 Hz) onset energy. Marked confidence: "low".
  • Sections: band energies in 0.5 s blocks, z-scored per band, Foote checkerboard novelty (±8 s, Gaussian taper), peaks a standard deviation over the mean and ≥ 4 s apart, boundaries snapped to the nearest beat within half a beat; a section whose mean features match an earlier one (cosine ≥ 0.9) gets its letter.

Benchmark numbers

This PR's synthetic grids (scripts in the session scratchpad, not committed), built from the real corpus: real sfx hits (kick / snare / hat kits) on a known grid, 30 s, 70–170 BPM, a quarter in 3/4, over no bed, a real voice bed at −3 dB or a real music bed at −9 dB; F at ±70 ms, Acc1 ±4 %, Acc2 also allows ×2, ×½, ×3, ×⅓.

bed n beat F Acc1 Acc2 downbeat F meter right
none 30 0.905 0.733 1.000 0.903 0.967
voice 30 0.668 0.500 0.867 0.620 0.767
music 30 0.739 0.467 0.800 0.675 0.833
all 90 0.771 0.567 0.889 0.732 0.856

Sections, 40 cases of 3–5 real music excerpts (12–30 s each) joined end to end: boundary F 0.741 at ±3 s, 0.648 at ±0.5 s, 1.32 boundaries found per true one (some of the extra ones are real changes inside an excerpt). Speed: 3.3 ms of compute per second of audio for beats, 3.4 ms/s for sections.

On the beat benchmark's own 200-file synthetic set (read only; swing, tempo steps and drift, rhythmic beds): beat F 0.692, downbeat F 0.387; Acc1 0.484 / Acc2 0.694 on its 124 constant-tempo files.

The separate beat benchmark (its REPORT.md was not written; these numbers came from its run, scripts and outputs in the session's beat-bench/). R1 = 22 agent-synthesized scores with script grids, 15 of them at 96 BPM, not produced music:

tracker R1 beat F R1 Acc1 R1 downbeat F synthetic F
TS DP tracker (this signal rung) 0.986 1.0 0.959 0.693
Beat This! small0 (beat-this rung) 0.995 0.998
Chrome analyzer behind hyperframes beats 0.437 0.18

It confirmed the designed order for beats and downbeats (api → apple-mu → beat-this small0 → signal). It left out madmom (its models are CC BY-NC-SA, non-commercial) and librosa (no advantage).

Overlap with hyperframes beats

hyperframes beats is unchanged by this PR. It still runs the headless-Chrome analyzer, which scored F 0.437 and Acc1 0.18 on R1. Its tempo averages two unfolded estimates (Math.round((onsetBpm + detectiveBpm) / 2) in analyzeMusicFromBuffer, packages/core/src/beats/beatDetection.ts), and it phases the grid from noisy energy peaks. Next step: have beats write rhythm()'s beats (or the ladder's) to the Studio beat file.

Tests

  • core audioRhythm.test.ts: a steady 120 BPM grid (beats, tempo, 4/4 bars), a tempo change partway through, no beat in silence or noise or a 3 s clip, sections A B A split on beats, the snapper.
  • core audioAnalysis.test.ts: falls through an unavailable provider and one whose check throws, with one warning carrying every reason.
  • cli providers/api.test.ts against a local node:http stand-in: method, Bearer header, the multipart request JSON and WAV part; per-beat snapping with an invented off-grid beat kept in unsnapped; downbeats; bpm from api beats; sections and describe asked on their own; a 503 and a malformed answer fall through to signal with warnings; the key never appears in a result.
  • cli providers/localProviders.test.ts: beat-this availability (unset, missing file, missing weights, package missing) with a stand-in Python script, beats + downbeats + meter from one run, a crashing run falling through; apple-mu reasons; apple-sound unavailable off macOS and a real classification on macOS.
  • cli analyze.test.ts: bpm follows the beats' provider and is in file seconds; --quality fast keeps a service off.

Mutations that fail without the fix:

  • Make the api keep its own beat times (return { times: rounded(times), snap: … } in snappedBeats): posts the audio and the capabilities, and snaps each answered beat onto the signal's own fails, expected [0.271, 0.772, …] to deeply equal [0.241, 0.742, …].
  • Turn off the local tempo (cumulativeScore(local, () => period)): follows a tempo that changes partway through fails, recall 0.74 < 0.85.

Runs here: core src/audio* 398 passed; cli src/audio + src/commands 1683 passed, 33 skipped; typecheck, oxlint, oxfmt, fallow audit (no new findings) clean. Full suites: core has 1 failure and cli has 5, all in files this PR does not touch. They also fail when run alone on this machine: core fonts/deterministicFonts-googleSubsets (5 s timeout), cli whisper/sherpa, whisper/manager.install and server/fileWatcher.atomic.

Not exercised

  • A real HeyGen backend: it does not exist yet. The contract was exercised only against the stand-in.
  • apple-mu analysis: there is no macOS 27 here.
  • Beat This! in CI: the tests use a stand-in Python. A real run here used a uv env with torch 2.5.1 and the small0 weights.
  • apple-sound on CI runners without Xcode CLT. It is skipped off macOS, and it compiles its helper into ~/.cache/hyperframes/audio-helpers on first use (about 3 s).
  • A cross-platform local audio tagger for describe off macOS. None was added.
  • Uploads of very long files to the api: there is no size cap or chunking, and the full analyzed span is sent as 16-bit WAV.
  • Follow-up, not in this PR: fusing beats into onsets (each hit's beat index, signed offset, on-beat flag, subdivision, downbeat).

This was meant to stack on #5602, but #5602 merged and its branch was deleted before this opened, so this PR targets main.

… 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.
@mintlify

mintlify Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 15, 2026, 3:07 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 174 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

@xuanruli
xuanruli marked this pull request as ready for review October 15, 2026 04:27
@xuanruli
xuanruli added this pull request to stack #5637 October 15, 2026 04:29

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes at 5b8a8d09 for one correctness bug in signal sections. The ladder, the providers and the rhythm math look right to me.

Blocking: signal sections splits silent or flat audio into a new section every 4 s

In boundaryBlocks (packages/core/src/audioSections.ts:92-103), a flat novelty curve makes mean and spread both 0. Then neither curve[i] < max(neighbourhood) nor curve[i] < mean + spread ever skips a block, so every block minBlocks apart becomes a boundary. labels then gives each span a new letter: unit() of a zero vector is zero, its dot with any earlier span is 0, which is below SAME_SECTION.

Running sections() on 30 s of digital silence gives A 0–4, B 4–8, C 8–12, D 12–16, E 16–20, F 20–24, G 24–30. A steady 440 Hz tone correctly gives one span. Silent or near-silent stretches are common in real input: intros, padding, voice-only tracks. So hyperframes audio --sections (and the fallback any provider fails over to) would report structure that isn't there.

A small fix: return no boundaries when spread is 0 (or within an epsilon), or require curve[i] > mean. Then add a silence case for sections to audioRhythm.test.ts, which has one for beats already.

Checked, looks right

  • Ladder (audioAnalysisLadder.ts):
    • An available() that throws becomes unavailable, and a failed analyze is recorded and falls through.
    • Every skipped step is listed in one warning, and no failure comes back looking like a success.
    • perSignal caches the rejected promise, so beats and downbeats share one failed call.
  • API provider:
    • The key goes only in the authorization header, and the URL is used exactly as configured. Errors carry only the status and the service's message.
    • The default timeout is 120 s and also covers reading the body.
    • api runs only when both env vars are set. It is excluded by --quality local, and fast leaves only signal, so nothing makes a paid call implicitly.
  • Subprocesses:
    • execFile with argument arrays and no shell.
    • Temp WAVs are removed in a finally.
    • Off macOS, apple-mu returns before spawning anything.
    • beat-this loads its weights from a local path and never downloads.
  • Rhythm:
    • Empty, sub-6 s and silent input return empty beats with a reason.
    • The beat backtrack always points strictly backward.
    • (k - phase) % meter is correct for negative values.
  • Back-compat:
    • The default capabilities reach only signal.
    • shiftedTimes maps the same way the removed toFileTime did.
    • The only importers of @hyperframes/core/audio-analysis are the CLI's own audio files.
    • The available() signature change isn't in a release tag yet.
  • PR-body claims checked in source:
    • One request for beats and downbeats, separate ones for sections and describe (asserted in api.test.ts).
    • The "no steady beat" / "too short for a steady beat" reasons.
    • Snapping within a quarter of the answer's median interval.

Non-blocking

  1. api answers are only loosely validated (providers/api.ts:58-80):
    • Beat times aren't checked to be ascending and non-negative. A descending list makes the median interval negative, which sends every beat to unsnapped and gives tempoOf a negative BPM (tempoOf([2,1.5,1,0.5]) → -120).
    • A section without label passes, though the docs say label: string. api.test.ts:150,164 does exactly this, and the text output then prints undefined 6.250–12.000.
    • describe is passed through whole.
  2. HYPERFRAMES_AUDIO_API_TIMEOUT_MS set to a fraction or above 2³²−1 makes AbortSignal.timeout throw ERR_OUT_OF_RANGE. The rung fails over, but the warning is confusing. Round and clamp at api.ts:54-55.
  3. http:// URLs are accepted, so the bearer key can go out in cleartext. Worth a warning.
  4. --provider bpm=X is accepted by pins (commands/audio.ts:84) but ignored: bpm comes from the beats ladder (analyze.ts:131). Reject it, or treat it as beats=.
  5. The upload buffer is copied an extra time: new Uint8Array(encodeWav(signal)) (api.ts:109). new Blob([wav]) takes the Buffer as is.
  6. Error messages from the service reach the terminal as is. A JSON error.message from the service isn't truncated (api.ts:84-86).

Not run: I couldn't run vitest locally (no node_modules in my checkout). I ran the core rhythm/sections code directly under Node 22's type stripping. Some CI jobs were still pending at this head. The API and Beat This paths are covered by stand-ins, as the PR says.

— Rames

@xuanruli

Copy link
Copy Markdown
Contributor Author

Addressed the blocking silence finding in 4e45ab310. Section candidates must strictly exceed mean + spread, so a flat zero novelty curve cannot create boundaries. Added 30-second silence regressions at 1-, 4-, and 8-second minimum sections: all three fail before the fix and pass afterward. Existing A–B–A segmentation still passes.

Validation: core rhythm suite 11 passed; core rebuilt; CLI analysis/API suites 11 passed; lint, format, comment gates, typecheck and fallow pre-commit checks passed. API coverage remains a local HTTP fixture. The separately marked non-blocking API contract suggestions are not changed in this correction.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 4e45ab310058e60a8b07f49eb31482f49ba7bfc8. The silent-section successor adds a strict threshold and passes its regression. The shared ladder catches availability/run failures; subprocess WAV cleanup is in finally. I independently ran the five affected core/provider/analysis suites: 50 passed, one native macOS test skipped.

Blocker — an accepted provider pin can still upload audio (packages/cli/src/audio/analysis/analyze.ts:119-131). As Rames noted, --provider bpm=signal is ignored, but the consequence warrants fixing before merge: --only bpm --provider bpm=signal delegates to ladderFor("beats"), which reads only provider.beats, so a configured API still receives the audio. I exercised the actual analyzeSignal and actual API adapter against a loopback HTTP server with an 8-second synthetic signal: one multipart POST containing 353,257 bytes, provenance bpm: api, despite provider: {bpm: "signal"}. The beats=signal control made no additional request. This is a transport/selection witness, not a real backend call.

Reject unsupported BPM pins before any analysis/upload, or normalize them to the beat provider with explicit handling for conflicting beats=/bpm= pins. Add a no-request regression. The current silent-section repair does not change this path.

CI on the new head has just started; no all-green claim. No real backend, Beat This model, Apple native analysis, subjective music evaluation or Desktop/Studio run was performed. The benchmark figures remain author evidence.

— Magi (desktop-review)

Verdict: REQUEST CHANGES
Reasoning: A user-selected local provider is silently ignored and the API receives the signal. Preserve or reject that pin before any network request.

@xuanruli xuanruli mentioned this pull request Oct 15, 2026
4 of 7 tasks
@xuanruli

Copy link
Copy Markdown
Contributor Author

Fixed the provider-pin blocker in 54dfc960c. bpm=signal now pins the existing shared beats ladder to signal; BPM still derives from those beats. Different explicit beats= and bpm= pins are rejected before any capability starts, including when describe precedes them.

The actual API adapter against a local HTTP server proves zero requests for BPM-only and both beats/BPM orderings. Both conflicting pin directions also reject with zero requests. All five new cases failed before the fix; all 38 analysis tests pass afterward. Built CLI + ffmpeg probes confirm zero API requests for --only bpm --provider bpm=signal with signal provenance, and zero requests for conflicting pins. The existing JSON per-file error path exits zero for the conflict; exit behavior was not changed.

CLI build, lint, format, comment checks and all commit hooks passed. No paid/backend/model calls; HTTP coverage is a loopback transport fixture. Merged into child 2d6903ce7 with a signed normal merge. Please re-review both current heads; fresh required CI still applies.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 54dfc960c4bcfb003f2a9f09fcdc570bfe638868. Supersedes my provider-pin change request.

packages/cli/src/audio/analysis/analyze.ts:106-108,124 now normalizes BPM and beats onto the same provider before analysis, and rejects conflicting pins before any capability runs. I reran the actual API adapter against a loopback HTTP server on this head: BPM-only and both beats/BPM orderings with bpm=signal made zero requests; both conflicting pin directions, with describe first, rejected with zero requests. Matching API pins made exactly one shared request, proving the transport witness is non-vacuous. My prior-head witness made an unwanted upload; the successor fixes it.

Independent focused tests: 55 passed, one native macOS test skipped. The complete successor diff is the shared-ladder pin correction plus five regressions; prior silence/beat-file fixes remain intact. No new own-code blocker found. All required checks now pass, including Windows render, build, typecheck and runtime contract. I read the completed aggregate accuracy log: 271/271 accurate, matching the measured base. One non-required regression shard remains running; no failure is reported, so this does not claim every check is finished.

Limits: synthetic loopback/provider tests, not a real backend, installed Beat This model, Apple native inference, subjective corpus evaluation, or live Studio/Desktop certification. The author’s benchmark and browser captures remain author evidence. No merge, queue, release or deployment performed.

— Magi (desktop-review)

Verdict: APPROVE
Reasoning: The ignored provider pin is fixed on this exact head with discriminating transport controls; current-head validation supports the code verdict.

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 54dfc960. My silence blocker from 5b8a8d09 was fixed at 4e45ab31, Magi's bpm= blocker is fixed at this head, and required CI on this head is green.

4e45ab31..54dfc960 changes only analyze.ts and api.test.ts:

  • analyzeSignal reads beats and bpm pins up front. If both are set and differ, it throws beats and bpm must use the same provider before any ladder runs, so nothing is uploaded.
  • Otherwise the beats ladder runs on beats ?? bpm. Since bpm is always computed by tempoFromBeats(ladderFor("beats")), a bpm=signal pin now keeps the whole tempo path local. --only bpm, --only beats,bpm and --only bpm,beats all share the one beats ladder.
  • analyzeSignal is the only place that hands pins to runLadder, so there's no second path that ignores them.
  • The new loopback tests configure the API and assert zero requests for each of the three capability orders. They also assert zero requests for both conflicting-pin directions, including when describe comes first.

The silence fix (<= in boundaryBlocks, plus the it.each([1, 4, 8]) silence case) is unchanged from 4e45ab31.

Non-blocking, still open from my first review: loose validation of api answers (beat order and sign, a missing section label), the timeout env value, and http:// URLs.

Not exercised: I didn't run vitest locally, and the API and Beat This paths use stand-ins, as before.

— Rames

@xuanruli
xuanruli added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 8e340d2 Oct 15, 2026
81 checks passed
@xuanruli
xuanruli deleted the feat/audio-analysis-providers branch October 15, 2026 05:36
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.

3 participants