Spaces:
Paused
Paused
|
Download README.md from jerrdeh/muscriptor-chords: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/spaces/jerrdeh/muscriptor-chords/resolve/main/README.md
- Command line
-
hf download hf://spaces/jerrdeh/muscriptor-chords/README.md
-
curl -L -o README.md https://huggingface.co/spaces/jerrdeh/muscriptor-chords/resolve/main/README.md
10.9 kB
| title: MuScriptor Chords | |
| emoji: πΈ | |
| colorFrom: indigo | |
| colorTo: purple | |
| sdk: docker | |
| app_port: 7860 | |
| pinned: false | |
| license: mit | |
| short_description: Chord recognition for MuScriptor, on CPU | |
| # MuScriptor chord service | |
| Chord recognition from audio, served as a small HTTP API from a free CPU Space. | |
| Upload a recording to `POST /analyze` and get back the chord track β symbol, | |
| start time, duration, and the notes each chord is made of β plus the beat grid | |
| the chord boundaries were snapped to. | |
| The model is [BTC](https://github.com/jayg996/BTC-ISMIR19) (Bi-directional | |
| Transformer for Chord Recognition, Park et al., ISMIR 2019), large-vocabulary | |
| checkpoint: 12 roots Γ 14 qualities plus "no chord". It runs on the CPU in a | |
| couple of seconds per minute of audio, which is the whole reason this endpoint | |
| exists separately β [MuScriptor](https://github.com/muscriptor/muscriptor)'s | |
| note transcription needs a GPU, chord recognition does not, so the chord track | |
| is served from here and the GPU is only rented when someone asks for notes. | |
| Nothing musical is implemented in `app.py`. Chords, beat tracking and audio | |
| decoding are imported from the `muscriptor` package installed into the image, | |
| so this Space and the main server return the same chords for the same file. | |
| ## API | |
| `https://<owner>-muscriptor-chords.hf.space` | |
| ### `GET /health`, `HEAD /health` | |
| ```json | |
| {"status": "ok", "version": "0.3.0", "model": "BTC-large-voca"} | |
| ``` | |
| Cheap, never rate limited, and answers before the model has finished loading β | |
| `"status"` is `"loading"` until it has. A free Space sleeps when it is idle, so | |
| a client should ping `/health` to wake it before uploading anything, and wait | |
| for `"ok"` before expecting `/analyze` to be quick. | |
| ### `POST /analyze` β `multipart/form-data` | |
| | field | type | default | meaning | | |
| |---|---|---|---| | |
| | `file` | audio upload | required | any format libsndfile/ffmpeg reads (wav, mp3, flac, ogg, m4a, β¦) | | |
| | `detect_tempo` | `"best-effort"ο½"true"ο½"false"` | `"best-effort"` | when a beat grid is found, chord boundaries snap to it. `"true"` fails the request when no grid is found, `"false"` skips beat tracking entirely | | |
| ```json | |
| { | |
| "chords": [ | |
| {"time": 0.0, "duration": 1.85, "label": "C", "root": 0, "intervals": [0, 4, 7]} | |
| ], | |
| "beat_grid": {"bpm": 120.0, "beats_per_bar": 4, "first_downbeat": 0.12, "onset_delay": null}, | |
| "duration": 12.34, | |
| "processing_time_ms": 4210 | |
| } | |
| ``` | |
| - `time` and `duration` are seconds in the uploaded audio; `root` is a pitch | |
| class (0 = C) and `intervals` the semitones above it that sound, so a client | |
| can play the chord without knowing any music theory. Both are `null`/empty | |
| for an `"N.C."` span. | |
| - The list starts at the first real chord. Silence a recording opens with is | |
| not a chord change; `"N.C."` spans *between* chords are kept, since they say | |
| the harmony stopped. | |
| - Chord times need no correction β they are snapped to the beats already. | |
| - `beat_grid` is `null` when no tempo was found. `onset_delay` is always `null` | |
| here: it measures how late transcribed *notes* sit against the beats, and | |
| this service transcribes none. The field is in the payload so that a client | |
| can consume one `beat_grid` shape from either backend. | |
| - `label`, `root` and `intervals` are exactly what MuScriptor's own | |
| `/transcribe` puts in its `transcription_complete` event. | |
| Errors are a sanitized `{"detail": "β¦"}`: `400` undecodable audio (or no beat | |
| grid when `detect_tempo=true`), `401` missing/incorrect shared secret when one | |
| is enforced, `413` too large or too long, `429` rate limited (with | |
| `Retry-After`), `503` all analysis slots busy, `504` analysis timed out. | |
| Details never quote a traceback, a package or a path β those go to the Space | |
| logs instead. | |
| ## Configuration | |
| Every knob is an environment variable, set under the Space's | |
| **Settings β Variables and secrets**. Changing one restarts the Space; none of | |
| them need a rebuild. | |
| | variable | default | purpose | | |
| |---|---|---| | |
| | `ALLOWED_ORIGINS` | `https://muscriptor.vercel.app,http://localhost:5173` | CSV allowlist of browser origins for CORS. `*` is ignored on purpose: with it, any page on the internet could spend this Space's CPU from a visitor's browser. Set it to the real frontend origin for your deployment. | | |
| | `RATE_LIMIT_WINDOW_SEC` | `60` | Length of the burst window, in seconds. | | |
| | `RATE_LIMIT_PER_WINDOW` | `5` | Analyses one client may start per burst window. | | |
| | `RATE_LIMIT_HOURLY` | `40` | Analyses one client may start per hour. | | |
| | `MAX_CONCURRENT_ANALYSES` | `2` | Analyses running at once; requests beyond it get `503` rather than queueing behind a busy CPU. | | |
| | `ANALYZE_TIMEOUT_SEC` | `180` | Wall-clock budget for one analysis, after which the client gets `504`. | | |
| | `MAX_UPLOAD_MB` | `25` | Upload size cap, checked while reading so an oversized body is never fully buffered. | | |
| | `MAX_AUDIO_SECONDS` | `600` | Duration cap, checked after decoding β a small file can still be an hour long. | | |
| | `ENFORCE_EDGE_SHARED_SECRET` | `false` | When true, `/analyze` requires the shared secret header. | | |
| | `EDGE_SHARED_SECRET` | *(unset)* | The secret itself. Store it as a **secret**, not a variable. | | |
| | `EDGE_SHARED_SECRET_HEADER` | `X-Edge-Auth` | Header the secret is read from. | | |
| | `ENABLE_PUBLIC_API_DOCS` | `false` | Swagger (`/docs`), ReDoc (`/redoc`) and `/openapi.json`. Off by default: they are a map of the API for anyone who finds the URL. | | |
| | `TORCH_NUM_THREADS` | *(unset)* | Cap torch's thread pool. Worth setting to `1` when `MAX_CONCURRENT_ANALYSES` is above 1, since torch otherwise takes every core for each analysis. | | |
| | `LOG_LEVEL` | `INFO` | Python logging level. | | |
| Rate limiting keys on the client IP as Hugging Face presents it: a Space never | |
| sees the caller's socket, so the address is taken from the left-most entry of | |
| `X-Forwarded-For` (`request.client.host` is the same HF router for everybody). | |
| That header is client-controllable and an abuser can rotate it β the real | |
| bounds on damage are `MAX_CONCURRENT_ANALYSES` and `ANALYZE_TIMEOUT_SEC`. | |
| No Hugging Face token is needed at runtime. The BTC checkpoint comes from | |
| GitHub (SHA-256 pinned) and the beat tracker's from a public HF repo; the gated | |
| MuScriptor transcription weights are never loaded here. | |
| ## Deploying | |
| The Space repository is generated from this one β do not edit it by hand, a | |
| sync overwrites it. | |
| 1. Create the Space once: **New Space** on Hugging Face, SDK **Docker**, | |
| hardware **CPU basic (free)**, name it `muscriptor-chords` (any name works; | |
| it only has to match what the frontend calls). | |
| 2. Set `ALLOWED_ORIGINS` under Settings β Variables to your frontend's origin. | |
| 3. Add a Hugging Face **write** token | |
| ([settings/tokens](https://huggingface.co/settings/tokens)) as the `HF_TOKEN` | |
| secret of this GitHub repository. | |
| 4. Run the **Sync chord Space** workflow | |
| (`.github/workflows/sync-hf-space.yml`) from the Actions tab, giving it the | |
| Space as `owner/name`. It assembles the Space contents and force-pushes | |
| them; Hugging Face then builds the image. | |
| The workflow lays the Space repository out as: | |
| ``` | |
| README.md β deploy/huggingface/README.md (this file; its frontmatter configures the Space) | |
| Dockerfile β deploy/huggingface/Dockerfile | |
| .dockerignore β deploy/huggingface/.dockerignore | |
| deploy/huggingface/app.py | |
| pyproject.toml, uv.lock, LICENSE, LICENSE-BTC | |
| muscriptor/ β the package the service imports | |
| ``` | |
| Hugging Face insists on `README.md` and `Dockerfile` at the repository root; | |
| everything else keeps the layout it has in this repository. **The build context | |
| is the repository root**, which is why the Dockerfile's `COPY` paths read | |
| `pyproject.toml`, `muscriptor/` and `deploy/huggingface/app.py` β the same | |
| paths work from a checkout here: | |
| ```bash | |
| docker build -f deploy/huggingface/Dockerfile -t muscriptor-chords . | |
| docker run --rm -p 7860:7860 muscriptor-chords | |
| curl -F file=@web/public/headache_by_lost_deposit_10s.mp3 http://localhost:7860/analyze | |
| ``` | |
| To push without GitHub Actions, do by hand what the workflow does: copy those | |
| files into a clone of the Space and `git push`. | |
| ### About the image | |
| - `python:3.11-slim`, ffmpeg + libsndfile1, running as uid 10001. Every cache | |
| the process writes (`HOME`, `HF_HOME`, numba's) is redirected under | |
| `/home/app`, since a Space's non-root user cannot write `/root/.cache`. | |
| - torch and torchaudio are installed from PyTorch's **CPU** index, pinned by | |
| the `TORCH_VERSION` / `TORCHAUDIO_VERSION` build args to what `uv.lock` | |
| resolves. The PyPI wheels bundle over 2 GB of CUDA that this Space would | |
| never use. Bump the two together when the lockfile moves. | |
| - The rest of the dependencies are resolved from `pyproject.toml` rather than | |
| installed from `uv.lock`: the lock pins the PyPI (CUDA) build of torch, which | |
| would undo the line above. The trade-off is that a Space image is not | |
| bit-identical to a `uv sync` install β everything but torch floats within the | |
| ranges `pyproject.toml` declares. | |
| - The BTC checkpoint is downloaded and digest-checked **during the build**, the | |
| way the repository's root Dockerfile pre-warms its soundfonts, so a cold | |
| container answers its first request without fetching weights. The beat | |
| tracker's checkpoint is pre-fetched too, best-effort. | |
| - There is no `chords` extra in `pyproject.toml`, and adding one would not make | |
| this image smaller: everything MuScriptor declares as a Python dependency is | |
| also needed here (torch, librosa, soundfile, beat-this, mido, fastapi β¦). | |
| What this Space does *not* need β MuseScore, fluidsynth, the soundfonts, the | |
| web build β are system packages and build steps, not Python dependencies, and | |
| they are simply absent from this Dockerfile. | |
| ## If the Space is being abused | |
| Symptoms: the queue is permanently full, `429`s in the logs from a handful of | |
| addresses, or the free CPU quota disappearing. In rough order of how much they | |
| cost a legitimate user, all of them Settings β Variables, no rebuild: | |
| 1. `TORCH_NUM_THREADS=1` and `MAX_CONCURRENT_ANALYSES=1` β the Space stays up | |
| and serves one analysis at a time. | |
| 2. `RATE_LIMIT_PER_WINDOW=2`, `RATE_LIMIT_HOURLY=10` β a normal session still | |
| works; a script does not. | |
| 3. `MAX_AUDIO_SECONDS=120`, `MAX_UPLOAD_MB=10`, `ANALYZE_TIMEOUT_SEC=60` β caps | |
| the cost of any single request. | |
| 4. `ALLOWED_ORIGINS` down to the production origin alone (drop | |
| `http://localhost:5173`). This only stops browsers, not `curl`. | |
| 5. `ENFORCE_EDGE_SHARED_SECRET=true` with `EDGE_SHARED_SECRET` set (and the | |
| same value configured in the frontend) β closes the endpoint to everything | |
| but the deployment. This is the switch that actually stops a determined | |
| caller, since IP-based limits are only as good as `X-Forwarded-For`. | |
| 6. Set the Space to **private** in its settings, or pause it, and let the | |
| frontend fall back to transcribing without a chord track. | |