> ## Documentation Index
> Fetch the complete documentation index at: https://cue.vibeset.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Map a track's beats

> Beat and downbeat times for any audio you can point us at.

Send Cue a track and get back every beat, and separately every **downbeat**: the first beat of
each bar, the one you'd count to. Beats tell you where the pulse is. Downbeats tell you where a cut
can land without sounding wrong.

That second grid is the hard one, and it's the one the rest of Cue depends on. Every sync point we
return is a bar line, so a grid off by a single beat produces edits that pass every automated check
and sound wrong to everyone.

## Map a track

```bash theme={null}
curl https://cue.vibeset.ai/v1/beats \
  -H "x-api-key: vbsk_live_..." \
  -H "content-type: application/json" \
  -d '{
    "audio_url": "https://your-storage/track.mp3",
    "track_id": "your-own-id"
  }'
```

Only `audio_url` is required.

`known_bpm` is the one optional field worth knowing about. It is worth **+6.7 downbeat CMLt**, but
only if the number is right, meaning a measured or carefully entered catalog tempo. A guess buys nothing,
and a value that disagrees with what we read is discarded. Leave it out for live recordings, for
anything with a rubato or free-time section, and for a catalog you inherited and haven't checked.
[More on when to send it](#if-your-catalog-knows-the-tempo-send-it).

### Getting us the audio

Send a **signed HTTPS URL** from wherever your audio already lives. Nothing has to be public, and
you do not need to be on any particular cloud. Every major store signs URLs:

| your storage                | send                          |
| --------------------------- | ----------------------------- |
| Amazon S3                   | presigned URL                 |
| Google Cloud Storage        | V4 signed URL                 |
| Azure Blob Storage          | SAS URL                       |
| Cloudflare R2, Backblaze B2 | presigned URL (S3-compatible) |
| your own server or CDN      | any expiring signed URL       |

All we need is a link that is reachable from the public internet, returns the audio file itself
rather than a web page, and responds within 120 seconds.

<Warning>
  **Sign for 12–24 hours when you are mapping a catalog, not the usual one hour.** Jobs queue, so a
  link signed for an hour can expire while its job is still waiting. That surfaces as a fetch
  error that looks like a problem on your side and isn't.
</Warning>

We fetch from the public internet with no fixed source address, so you cannot allow-list us by IP.
The signature on the URL is the access control.

`s3://` URIs are also accepted, but only for buckets **outside** our own account and only once
you have granted our worker read access with a bucket policy. [Talk to us](mailto:support@vibeset.ai)
if you want that. A signed HTTPS URL needs no setup and is the path we recommend.

Mapping a track is real work on real audio, and a ten-minute mix is a few minutes of it, so every
call is a job:

```json 202 theme={null}
{
  "job_id": "9f2c…",
  "track_id": "your-own-id",
  "status": "processing",
  "poll": "/v1/beats/9f2c…",
  "next": "Poll the job until status is 'complete', then read beats/downbeats."
}
```

The submit itself only checks that `audio_url` is present and is an `https://` or `s3://` URL.
Everything about the audio is decided by the worker after it fetches the file, and reaches you on
the poll: that it is audio, that it decodes, that it's inside the length limit.

## Poll for the grid

```bash theme={null}
curl https://cue.vibeset.ai/v1/beats/9f2c… -H "x-api-key: vbsk_live_..."
```

While it's working you get a `202` carrying `stage`: `queued` means it hasn't been picked up yet,
`running` means a worker has it. A backlog is a normal state when you're mapping a catalog, and
those two are the difference between waiting your turn and a hang.

When it's done:

```json 200 theme={null}
{
  "job_id": "9f2c…",
  "track_id": "your-own-id",
  "status": "complete",
  "duration_seconds": 184.2,
  "tempo_bpm": 128.02,
  "beats": [0.412, 0.881, 1.350, 1.819],
  "downbeats": [0.412, 2.288, 4.164],
  "billed_minutes": 4,
  "cached": false
}
```

Times are seconds from the start of the audio. `downbeats` is a subset of `beats`, the ones that
open a bar, so one response drives both a beat grid and a bar grid.

`track_id` is yours, not ours. Whatever you send comes back on the job, so you can map a whole
catalog without keeping your own `job_id → track` table.

`cached` is `true` when you have mapped these exact bytes before. We keep the grid against a hash
of the audio, per account, so a re-run skips the analysis. It does not change what the call costs.

A job that fails answers **`422`** with the reason, and a failed job is never billed. So does a job
that started but will not finish, and one that sat in the queue past its retention.

## Retrying safely, and re-running a catalog

Set an `Idempotency-Key` header on `POST /v1/beats` and a retry after a network blip returns the
**original job** instead of starting a second one, so a timeout you never saw the response to cannot
bill you twice. That replay is marked with `"idempotent_replay": true`. Keys are honoured for 24
hours. Reusing one for a genuinely different request is a `422` rather than a silent alias.

```bash theme={null}
curl https://cue.vibeset.ai/v1/beats \
  -H "x-api-key: vbsk_live_..." \
  -H "Idempotency-Key: your-track-0001" \
  -H "content-type: application/json" \
  -d '{ "audio_url": "https://your-storage/track.mp3", "track_id": "your-own-id" }'
```

Re-signing the URL between attempts is fine. We identify the request by the object it points at,
not by the signature on it.

If you already know the SHA-256 of the audio, send it as `audio_sha256`. When you have mapped that
track before, we answer from the cache **without fetching the file at all**. That is what you want
when you are re-running a catalog and would rather not serve it twice.

<Note>
  `/v1/beats` is the only endpoint where the key returns the original result. Everywhere else an
  `Idempotency-Key` only stops a duplicate billing row. See
  [the reference](/docs/api-reference/overview#idempotency).
</Note>

## If your catalog knows the tempo, send it

`known_bpm` is optional and it is the single largest decoder lever we have. Constraining the
decoder to ±20% around a known tempo stops it hunting, and on GTZAN 993 that is worth **+6.73
downbeat CMLt**, moving 0.7507 to 0.8180. That is several times more than any change we have made
to the model itself.

It tolerates an imprecise value well, because what matters is that the true tempo stays inside the
window we search:

| your BPM is               | measured effect           |
| ------------------------- | ------------------------- |
| exact                     | +6.73                     |
| within 5%                 | +6.65                     |
| within 10%                | +4.76                     |
| **half the real tempo**   | **−60.91, if we used it** |
| **double the real tempo** | **−58.91, if we used it** |

That last pair is why this is opt-in. A wrong *octave*, 64 for a 128 BPM track or 256, is far
worse than sending nothing, because it puts the true tempo outside the window and forces an error
the tracker would never have made alone. A catalog with only 10% of its rows octave-wrong nets
−0.26: the entire benefit, erased.

So we check before we trust it. We decode once without the hint, and if your number sits about an
octave away from what we read, we discard it and map the track normally. You cannot make the result
worse by sending a bad tempo. At worst you have paid for a hint we ignored.

Percentage error is fine. Octave error is the one to care about, and if your catalog has any doubt
about doubling, leave the field out.

`known_bpm` accepts 20 to 400.

### Songs that change tempo

A window around one number fits a song with one tempo, not one that speeds up. Measured on our
training corpora, 9.9% of anime and game tracks and 15.9% of RWC have a tempo curve that will not
fit inside a ±20% window at all. GTZAN is 1.1%, which is exactly why the standard benchmark cannot
see this.

So we read how much the tempo actually moves in a first pass and widen the window to cover it,
never past an octave. A window spanning 2× would re-admit the half/double error the octave check
exists to exclude. A track whose own curve is wider than an octave falls back to an unconstrained
decode. Send the tempo you have. Nothing changes on your side.

## Why the downbeat is the hard part

Beats are comparatively easy. A metronome finds most of them, and trackers have been good at them
for years. Knowing which beat *starts the bar* is where they fail, and it's the failure that matters
commercially: a grid shifted by one beat looks correct in every metric that isn't measuring bar
position.

**CMLt** is the measure that catches it: the longest stretch of the track you can trust before the
grid drifts off the bar. It is the number to hold us to.

On GTZAN 993, held out of our training:

|                                       | downbeat CMLt |
| ------------------------------------- | ------------- |
| Beat This + DBN, as published         | 0.733         |
| Beat This + DBN, our own reproduction | 0.7355        |
| **Cue**                               | **0.7538**    |

Scoring the same predictions with our own harness reproduces published figures to within 0.002, so
those rows are on one scale.

Two things to read carefully. That +2.1 comes from the fine-tune, not from anything clever in the
decoder. And run-to-run variance from the random seed alone is about **1.0 CMLt** on this
benchmark, so treat any margin under roughly 1.5 as unresolved, including margins we quote.

GTZAN is made of 30-second excerpts, which is what every published beat-tracking number is measured
on, ours included. Longer tracks are split into 30-second chunks and stitched, using the reference
implementation's own chunk size and overlap handling, so a served grid is the same computation as a
benchmarked one.

## Global repertoire

The public research corpora this field standardises on are overwhelmingly Western, and a model
trained only on them slips on Asian popular and game music. Dense percussive layers, frequent
half-time and double-time feels, and tempi well above the Western pop band are all thin in the
standard datasets, and bar-position errors concentrate exactly there.

Cue is fine-tuned on 5,293 tracks: the sixteen public corpora plus roughly 740 tracks of East Asian
popular and rhythm-game repertoire absent from them.

<Note>
  **That is a statement about training data, not a measured accuracy claim.** Our only reading on
  held-out material of that kind is 99 anime tracks, where Cue scored 0.7730 downbeat CMLt against
  0.7694 for the generalist model. At n=99, and against \~1.0 CMLt of seed variance, that difference
  is inside the noise. We would rather score your catalog than estimate it.
</Note>

If your catalog is Western pop, the GTZAN table above is the one that applies.

## Very fast and very slow music

Beat trackers search a tempo range, and the default here is 55 to 215 BPM. Outside it the right
answer is not on the menu, and a 240 BPM track comes back at 120. No model quality fixes that.

We widen the search when the evidence says a track belongs outside, taken from your `known_bpm` or
from the track's own first pass. On GTZAN's 34 tracks above 215 BPM, widening is worth **+41.25
downbeat CMLt**. It holds on stock Beat This, which we never trained, and on all five of our own
checkpoints, which makes it a property of the decoder rather than of our weights.

The floor moves the same way. A track whose evidence puts it under 55 BPM gets a lower floor rather
than being forced to double tempo.

Widening only where it's needed is the point. Applied unconditionally it costs 0.29 to 0.83 CMLt on
the 95% of tracks that never needed it, so ordinary songs keep the original window.

## What it costs

**\$0.008 per minute of audio**, rounded up, one-minute minimum. Billed once, when the job completes.
A job that fails isn't billed.

| track  | you pay |
| ------ | ------- |
| 1 min  | \$0.008 |
| 3 min  | \$0.024 |
| 5 min  | \$0.040 |
| 10 min | \$0.080 |

Tracks longer than 30 minutes are refused, but not at submit time. The submit returns `202` like
any other. The worker fetches the file, reads its duration and refuses it there, and you learn
about it as a `422` on your next poll of `GET /v1/beats/{job_id}`. Nothing is billed for it.

See [Metering & pricing](/docs/metering-and-pricing) for the rest of the rate card, and
[Errors & limits](/docs/errors-and-limits) for the failure shapes.
