muscriptor 0.1.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {muscriptor-0.1.0 → muscriptor-0.2.1}/.dockerignore +3 -3
- {muscriptor-0.1.0 → muscriptor-0.2.1}/.github/workflows/pypi.yml +9 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/.gitignore +1 -2
- {muscriptor-0.1.0 → muscriptor-0.2.1}/Dockerfile +14 -11
- {muscriptor-0.1.0 → muscriptor-0.2.1}/PKG-INFO +22 -19
- {muscriptor-0.1.0 → muscriptor-0.2.1}/README.md +20 -17
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/main.py +3 -3
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/server.py +13 -1
- muscriptor-0.2.1/muscriptor/soundfonts.py +19 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/utils/auralization.py +19 -8
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/utils/download.py +10 -1
- muscriptor-0.2.1/muscriptor/web_dist/assets/index-BZbHOTyZ.js +223 -0
- muscriptor-0.2.1/muscriptor/web_dist/assets/index-b8IJOPe2.css +2 -0
- muscriptor-0.2.1/muscriptor/web_dist/assets/spessasynth_processor.min-tdOdhV7p.js +18 -0
- muscriptor-0.2.1/muscriptor/web_dist/index.html +30 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/pyproject.toml +7 -2
- {muscriptor-0.1.0 → muscriptor-0.2.1}/uv.lock +2 -2
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/package.json +0 -2
- muscriptor-0.2.1/web/public/favicon.png +0 -0
- muscriptor-0.2.1/web/public/kyutai-logo.svg +9 -0
- muscriptor-0.2.1/web/public/mirelo-logo.svg +10 -0
- muscriptor-0.2.1/web/public/muscriptor-logo-v2.png +0 -0
- muscriptor-0.2.1/web/public/muscriptor-logo-v4-pink.svg +44 -0
- muscriptor-0.2.1/web/public/muscriptor-mark-v4-pink.svg +33 -0
- muscriptor-0.2.1/web/public/muscriptor-wave.svg +3 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/audio.ts +3 -2
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/vite.config.ts +10 -2
- muscriptor-0.1.0/web/scripts/prepare-soundfonts.mjs +0 -69
- {muscriptor-0.1.0 → muscriptor-0.2.1}/.pre-commit-config.yaml +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/.python-version +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/LICENSE +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/deploy.sh +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/__init__.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/__main__.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/events.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/models/__init__.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/models/lm.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/modules/__init__.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/modules/conditioners.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/modules/mel_spectrogram.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/modules/streaming.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/modules/transformer.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/tokenizer/__init__.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/tokenizer/mt3.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/tokenizer/notes.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/transcription_model.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/utils/__init__.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/utils/audio.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/utils/midi.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/utils/resample.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/muscriptor/utils/sampling.py +0 -0
- {muscriptor-0.1.0/web/public → muscriptor-0.2.1/muscriptor/web_dist}/favicon.png +0 -0
- {muscriptor-0.1.0/web/public → muscriptor-0.2.1/muscriptor/web_dist}/kyutai-logo.svg +0 -0
- {muscriptor-0.1.0/web/public → muscriptor-0.2.1/muscriptor/web_dist}/mirelo-logo.svg +0 -0
- {muscriptor-0.1.0/web/public → muscriptor-0.2.1/muscriptor/web_dist}/muscriptor-logo-v2.png +0 -0
- {muscriptor-0.1.0/web/public → muscriptor-0.2.1/muscriptor/web_dist}/muscriptor-logo-v4-pink.svg +0 -0
- {muscriptor-0.1.0/web/public → muscriptor-0.2.1/muscriptor/web_dist}/muscriptor-mark-v4-pink.svg +0 -0
- {muscriptor-0.1.0/web/public → muscriptor-0.2.1/muscriptor/web_dist}/muscriptor-wave.svg +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/swarm.yml +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/__init__.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/conftest.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/encode_helpers.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/test_cli.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/test_conditioners.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/test_events.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/test_integration.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/test_midi.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/test_notes.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/test_sampling.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/test_server.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/test_transcription_model.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/tests/test_transformer.py +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/index.html +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/logo_muscriptor_final.png +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/package-lock.json +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/pnpm-lock.yaml +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/App.tsx +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/components/ConditioningPanel.tsx +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/components/Controls.tsx +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/components/DropOverlay.tsx +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/components/Footer.tsx +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/components/InstrumentList.tsx +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/components/OutputBar.tsx +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/components/PianoRollCanvas.tsx +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/components/WelcomeScreen.tsx +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/components/icons.tsx +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/hooks/useAudioEngine.ts +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/hooks/useTranscription.ts +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/instruments.ts +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/main.tsx +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/pianoroll.ts +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/progress.ts +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/sse.ts +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/style.css +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/src/vite-env.d.ts +0 -0
- {muscriptor-0.1.0 → muscriptor-0.2.1}/web/tsconfig.json +0 -0
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*.mid
|
|
8
8
|
.venv
|
|
9
9
|
web/node_modules
|
|
10
|
-
web
|
|
11
|
-
#
|
|
12
|
-
|
|
10
|
+
# Built fresh in the web-builder stage; a stale local build must not leak
|
|
11
|
+
# into the runtime image via `COPY muscriptor/`.
|
|
12
|
+
muscriptor/web_dist
|
|
13
13
|
.git
|
|
14
14
|
.gitignore
|
|
15
15
|
__pycache__
|
|
@@ -15,6 +15,15 @@ jobs:
|
|
|
15
15
|
- uses: actions/checkout@v2
|
|
16
16
|
- name: Install uv
|
|
17
17
|
uses: astral-sh/setup-uv@v7
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 22
|
|
21
|
+
- name: Build frontend into muscriptor/web_dist
|
|
22
|
+
run: |
|
|
23
|
+
corepack enable
|
|
24
|
+
pnpm install --frozen-lockfile
|
|
25
|
+
pnpm run build
|
|
26
|
+
working-directory: web
|
|
18
27
|
- name: Build wheels
|
|
19
28
|
run: uv build
|
|
20
29
|
- name: Publish package distributions to PyPI
|
|
@@ -8,14 +8,8 @@ RUN corepack enable
|
|
|
8
8
|
COPY web/package.json web/pnpm-lock.yaml ./
|
|
9
9
|
RUN pnpm install --frozen-lockfile
|
|
10
10
|
|
|
11
|
-
# Download the soundfonts: the .sf3 the frontend streams from /soundfonts/,
|
|
12
|
-
# and the .sf2 for /auralize (to /MuseScore_General.sf2, one level above the
|
|
13
|
-
# package root — the runtime stage copies it from here). Runs in its own
|
|
14
|
-
# layer so day-to-day source changes don't re-trigger the 253 MB download.
|
|
15
|
-
COPY web/scripts/prepare-soundfonts.mjs scripts/
|
|
16
|
-
RUN node scripts/prepare-soundfonts.mjs
|
|
17
|
-
|
|
18
11
|
COPY web/ ./
|
|
12
|
+
# vite outDir is ../muscriptor/web_dist, i.e. /muscriptor/web_dist in this stage.
|
|
19
13
|
RUN pnpm run build
|
|
20
14
|
|
|
21
15
|
|
|
@@ -38,15 +32,24 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
|
|
38
32
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
|
39
33
|
uv sync --no-install-project
|
|
40
34
|
|
|
35
|
+
# Pre-fetch the soundfonts (253 MB: .sf2 for /auralize, .sf3 for the web UI)
|
|
36
|
+
# into the HF hub cache so a fresh container doesn't download them on first
|
|
37
|
+
# use. Runs off just the two files it needs, before the full source COPY,
|
|
38
|
+
# so day-to-day code changes don't re-trigger the download.
|
|
39
|
+
COPY muscriptor/soundfonts.py muscriptor/utils/download.py /tmp/prewarm/
|
|
40
|
+
RUN /app/.venv/bin/python -c "\
|
|
41
|
+
import sys; sys.path.insert(0, '/tmp/prewarm'); \
|
|
42
|
+
import soundfonts, download; \
|
|
43
|
+
download.download_if_necessary(soundfonts.SF2_URL); \
|
|
44
|
+
download.download_if_necessary(soundfonts.SF3_URL)" \
|
|
45
|
+
&& rm -rf /tmp/prewarm
|
|
46
|
+
|
|
41
47
|
COPY pyproject.toml uv.lock README.md ./
|
|
42
|
-
# SoundFont for auralization; auralize() defaults to <repo-root>/MuseScore_General.sf2.
|
|
43
|
-
# Reuse the copy the web-builder stage downloaded — no need for it in the context.
|
|
44
|
-
COPY --from=web-builder /MuseScore_General.sf2 ./
|
|
45
48
|
COPY muscriptor/ ./muscriptor/
|
|
46
49
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
47
50
|
uv sync
|
|
48
51
|
|
|
49
|
-
COPY --from=web-builder /
|
|
52
|
+
COPY --from=web-builder /muscriptor/web_dist ./muscriptor/web_dist
|
|
50
53
|
|
|
51
54
|
|
|
52
55
|
EXPOSE 8000
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: muscriptor
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Audio-to-MIDI transcription using a transformer language model
|
|
5
5
|
Project-URL: Homepage, https://github.com/muscriptor/muscriptor
|
|
6
6
|
Project-URL: Repository, https://github.com/muscriptor/muscriptor
|
|
@@ -27,7 +27,7 @@ Requires-Dist: numpy>=1.24
|
|
|
27
27
|
Requires-Dist: python-multipart>=0.0.29
|
|
28
28
|
Requires-Dist: safetensors>=0.4
|
|
29
29
|
Requires-Dist: soundfile>=0.14.0
|
|
30
|
-
Requires-Dist: torch
|
|
30
|
+
Requires-Dist: torch>=2.0
|
|
31
31
|
Requires-Dist: typer>=0.10
|
|
32
32
|
Requires-Dist: uvicorn[standard]>=0.48.0
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
@@ -41,7 +41,7 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
MuScriptor is a multi-instrument music transcription model developed by [Kyutai](https://kyutai.org) and [Mirelo](https://www.mirelo.ai).
|
|
42
42
|
MuScriptor is the first music transcription model that has been trained on a large scale dataset of 170k songs from classical music to heavy metal.
|
|
43
43
|
|
|
44
|
-
[Online Demo](https://muscriptor.kyutai.org) | [Paper]
|
|
44
|
+
[Online Demo](https://muscriptor.kyutai.org) | [Paper](https://arxiv.org/abs/2607.08168v1) | [HuggingFace](https://huggingface.co/MuScriptor)
|
|
45
45
|
|
|
46
46
|
<!-- TODO: record the demo GIF (web UI piano roll), save it as assets/demo.gif,
|
|
47
47
|
then uncomment:
|
|
@@ -276,11 +276,15 @@ uv sync
|
|
|
276
276
|
cd web && pnpm install && pnpm run build && cd ..
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
-
`pnpm run build` is required once —
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
`pnpm run build` is required once — it outputs to `muscriptor/web_dist/`,
|
|
280
|
+
which the FastAPI server auto-mounts if it exists (and which ships inside
|
|
281
|
+
the PyPI wheel, so `uvx muscriptor serve` works without a checkout).
|
|
282
|
+
|
|
283
|
+
The soundfonts are not bundled: the server fetches
|
|
284
|
+
`MuseScore_General.sf2` (215 MB, used by `/auralize`) and
|
|
285
|
+
`MuseScore_General.sf3` (38 MB, the compressed build the UI plays) from
|
|
286
|
+
[MuScriptor/assets](https://huggingface.co/MuScriptor/assets) on first use
|
|
287
|
+
and caches them locally (see `muscriptor/soundfonts.py`).
|
|
284
288
|
|
|
285
289
|
### Run
|
|
286
290
|
|
|
@@ -305,8 +309,8 @@ a WAV onto the page.
|
|
|
305
309
|
is local-only.
|
|
306
310
|
- Playback runs a full SoundFont synthesizer ([SpessaSynth](https://github.com/spessasus/spessasynth_lib))
|
|
307
311
|
in the browser, fed with `MuseScore_General.sf3` — the same soundfont the
|
|
308
|
-
`/auralize` endpoint uses, served by the app itself from `/soundfonts
|
|
309
|
-
no third-party CDN.
|
|
312
|
+
`/auralize` endpoint uses, served by the app itself from `/soundfonts/`
|
|
313
|
+
(cached server-side), no third-party CDN.
|
|
310
314
|
|
|
311
315
|
## License
|
|
312
316
|
|
|
@@ -322,15 +326,14 @@ distributed under its own (MIT) license.
|
|
|
322
326
|
|
|
323
327
|
## Citation
|
|
324
328
|
|
|
325
|
-
<!-- TODO: fill in the BibTeX entry when the paper is out:
|
|
326
329
|
```bibtex
|
|
327
|
-
@
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
@misc{rouard2026muscriptoropenmodelmultiinstrument,
|
|
331
|
+
title={MuScriptor: An Open Model for Multi-Instrument Music Transcription},
|
|
332
|
+
author={Simon Rouard and Michael Krause and Axel Roebel and Carl-Johann Simon-Gabriel and Alexandre Défossez},
|
|
333
|
+
year={2026},
|
|
334
|
+
eprint={2607.08168},
|
|
335
|
+
archivePrefix={arXiv},
|
|
336
|
+
primaryClass={cs.SD},
|
|
337
|
+
url={https://arxiv.org/abs/2607.08168},
|
|
332
338
|
}
|
|
333
339
|
```
|
|
334
|
-
-->
|
|
335
|
-
|
|
336
|
-
The paper is coming soon — a citation entry will be added here with it.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
MuScriptor is a multi-instrument music transcription model developed by [Kyutai](https://kyutai.org) and [Mirelo](https://www.mirelo.ai).
|
|
8
8
|
MuScriptor is the first music transcription model that has been trained on a large scale dataset of 170k songs from classical music to heavy metal.
|
|
9
9
|
|
|
10
|
-
[Online Demo](https://muscriptor.kyutai.org) | [Paper]
|
|
10
|
+
[Online Demo](https://muscriptor.kyutai.org) | [Paper](https://arxiv.org/abs/2607.08168v1) | [HuggingFace](https://huggingface.co/MuScriptor)
|
|
11
11
|
|
|
12
12
|
<!-- TODO: record the demo GIF (web UI piano roll), save it as assets/demo.gif,
|
|
13
13
|
then uncomment:
|
|
@@ -242,11 +242,15 @@ uv sync
|
|
|
242
242
|
cd web && pnpm install && pnpm run build && cd ..
|
|
243
243
|
```
|
|
244
244
|
|
|
245
|
-
`pnpm run build` is required once —
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
`pnpm run build` is required once — it outputs to `muscriptor/web_dist/`,
|
|
246
|
+
which the FastAPI server auto-mounts if it exists (and which ships inside
|
|
247
|
+
the PyPI wheel, so `uvx muscriptor serve` works without a checkout).
|
|
248
|
+
|
|
249
|
+
The soundfonts are not bundled: the server fetches
|
|
250
|
+
`MuseScore_General.sf2` (215 MB, used by `/auralize`) and
|
|
251
|
+
`MuseScore_General.sf3` (38 MB, the compressed build the UI plays) from
|
|
252
|
+
[MuScriptor/assets](https://huggingface.co/MuScriptor/assets) on first use
|
|
253
|
+
and caches them locally (see `muscriptor/soundfonts.py`).
|
|
250
254
|
|
|
251
255
|
### Run
|
|
252
256
|
|
|
@@ -271,8 +275,8 @@ a WAV onto the page.
|
|
|
271
275
|
is local-only.
|
|
272
276
|
- Playback runs a full SoundFont synthesizer ([SpessaSynth](https://github.com/spessasus/spessasynth_lib))
|
|
273
277
|
in the browser, fed with `MuseScore_General.sf3` — the same soundfont the
|
|
274
|
-
`/auralize` endpoint uses, served by the app itself from `/soundfonts
|
|
275
|
-
no third-party CDN.
|
|
278
|
+
`/auralize` endpoint uses, served by the app itself from `/soundfonts/`
|
|
279
|
+
(cached server-side), no third-party CDN.
|
|
276
280
|
|
|
277
281
|
## License
|
|
278
282
|
|
|
@@ -288,15 +292,14 @@ distributed under its own (MIT) license.
|
|
|
288
292
|
|
|
289
293
|
## Citation
|
|
290
294
|
|
|
291
|
-
<!-- TODO: fill in the BibTeX entry when the paper is out:
|
|
292
295
|
```bibtex
|
|
293
|
-
@
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
@misc{rouard2026muscriptoropenmodelmultiinstrument,
|
|
297
|
+
title={MuScriptor: An Open Model for Multi-Instrument Music Transcription},
|
|
298
|
+
author={Simon Rouard and Michael Krause and Axel Roebel and Carl-Johann Simon-Gabriel and Alexandre Défossez},
|
|
299
|
+
year={2026},
|
|
300
|
+
eprint={2607.08168},
|
|
301
|
+
archivePrefix={arXiv},
|
|
302
|
+
primaryClass={cs.SD},
|
|
303
|
+
url={https://arxiv.org/abs/2607.08168},
|
|
298
304
|
}
|
|
299
305
|
```
|
|
300
|
-
-->
|
|
301
|
-
|
|
302
|
-
The paper is coming soon — a citation entry will be added here with it.
|
|
@@ -137,8 +137,8 @@ def transcribe(
|
|
|
137
137
|
typer.Option(
|
|
138
138
|
"--soundfont",
|
|
139
139
|
help=(
|
|
140
|
-
"Path to a .sf2 SoundFont for auralization. "
|
|
141
|
-
"
|
|
140
|
+
"Path to a .sf2 SoundFont for auralization. Defaults to "
|
|
141
|
+
"MuseScore_General.sf2, downloaded once and cached locally."
|
|
142
142
|
),
|
|
143
143
|
),
|
|
144
144
|
] = None,
|
|
@@ -307,7 +307,7 @@ def serve(
|
|
|
307
307
|
_device = None if device == "auto" else device
|
|
308
308
|
typer.echo("Loading model…")
|
|
309
309
|
model = TranscriptionModel.load_model(weights_path=model_path, device=_device)
|
|
310
|
-
web_dir = Path(__file__).resolve().parent
|
|
310
|
+
web_dir = Path(__file__).resolve().parent / "web_dist"
|
|
311
311
|
fastapi_app = create_app(model, web_dir=web_dir if web_dir.is_dir() else None)
|
|
312
312
|
uvicorn.run(fastapi_app, host=host, port=port)
|
|
313
313
|
|
|
@@ -22,14 +22,16 @@ from pathlib import Path
|
|
|
22
22
|
from typing import Annotated
|
|
23
23
|
|
|
24
24
|
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
|
25
|
-
from fastapi.responses import Response, StreamingResponse
|
|
25
|
+
from fastapi.responses import FileResponse, Response, StreamingResponse
|
|
26
26
|
from fastapi.staticfiles import StaticFiles
|
|
27
27
|
from starlette.background import BackgroundTask
|
|
28
28
|
|
|
29
29
|
from muscriptor.events import NoteEndEvent, NoteStartEvent, ProgressEvent
|
|
30
|
+
from muscriptor.soundfonts import SF3_URL
|
|
30
31
|
from muscriptor.tokenizer.mt3 import MT3_FULL_PLUS_GROUP_NAMES
|
|
31
32
|
from muscriptor.transcription_model import TranscriptionModel
|
|
32
33
|
from muscriptor.utils.audio import _read_non_wav_file, _read_wav_file
|
|
34
|
+
from muscriptor.utils.download import download_if_necessary
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
def _make_release_once(lock: threading.Lock):
|
|
@@ -85,6 +87,16 @@ def create_app(model: TranscriptionModel, web_dir: str | Path | None = None) ->
|
|
|
85
87
|
async def list_instruments():
|
|
86
88
|
return {"instruments": list(MT3_FULL_PLUS_GROUP_NAMES.keys())}
|
|
87
89
|
|
|
90
|
+
@app.get("/soundfonts/MuseScore_General.sf3")
|
|
91
|
+
async def soundfont() -> FileResponse:
|
|
92
|
+
"""Compressed soundfont for the web UI's in-browser synthesizer.
|
|
93
|
+
|
|
94
|
+
Fetched from SF3_URL on first request (in a worker thread, so the
|
|
95
|
+
event loop keeps serving) and cached locally.
|
|
96
|
+
"""
|
|
97
|
+
path = await asyncio.to_thread(download_if_necessary, SF3_URL)
|
|
98
|
+
return FileResponse(path, media_type="application/octet-stream")
|
|
99
|
+
|
|
88
100
|
@app.post("/transcribe")
|
|
89
101
|
async def transcribe(
|
|
90
102
|
file: Annotated[UploadFile, File()],
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Runtime sources for the MuseScore General soundfonts.
|
|
2
|
+
|
|
3
|
+
Neither file ships in the package: both are fetched lazily through
|
|
4
|
+
:func:`muscriptor.utils.download.download_if_necessary` and cached locally
|
|
5
|
+
(``hf://`` URLs land in the HuggingFace hub cache). The upstream origin is
|
|
6
|
+
MuseScore's distribution mirror,
|
|
7
|
+
https://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/, re-hosted
|
|
8
|
+
at https://huggingface.co/MuScriptor/assets (MIT license).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Full soundfont (215 MB), rendered server-side by fluidsynth — the
|
|
12
|
+
# /auralize endpoint and the CLI's --auralize flag.
|
|
13
|
+
# SHA-256: ee51d2c4b1525e70f19a45909c4fd7a2e26d91d115fa89dbf5a6bc413d8b9bf3
|
|
14
|
+
SF2_URL = "hf://MuScriptor/assets/MuseScore_General.sf2"
|
|
15
|
+
|
|
16
|
+
# Vorbis-compressed build (38 MB) of the same soundfont, served to the web
|
|
17
|
+
# UI's in-browser spessasynth_lib synthesizer (GET /soundfonts/…).
|
|
18
|
+
# SHA-256: 5b85b6c2c61d10b2b91cddd41efcce7b25cd31c8271d511c73afafbef20b6fa3
|
|
19
|
+
SF3_URL = "hf://MuScriptor/assets/MuseScore_General.sf3"
|
|
@@ -16,9 +16,14 @@ from pathlib import Path
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
import soundfile as sf
|
|
18
18
|
|
|
19
|
+
from muscriptor.soundfonts import SF2_URL
|
|
19
20
|
from muscriptor.utils.audio import load_audio
|
|
21
|
+
from muscriptor.utils.download import download_if_necessary
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
# Pre-downloaded copy at the repo root (kept for checkouts and Docker images
|
|
24
|
+
# that already have one); absent that, the soundfont is fetched from SF2_URL
|
|
25
|
+
# and cached under ~/.cache/muscriptor/.
|
|
26
|
+
_LOCAL_SOUNDFONT = Path(__file__).parent.parent.parent / "MuseScore_General.sf2"
|
|
22
27
|
_SAMPLE_RATE = 44100
|
|
23
28
|
|
|
24
29
|
|
|
@@ -29,12 +34,16 @@ def _load_mono_44k(path: Path) -> np.ndarray:
|
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
def _resolve_soundfont(soundfont_path: str | Path | None) -> Path:
|
|
32
|
-
|
|
37
|
+
if soundfont_path is None:
|
|
38
|
+
if _LOCAL_SOUNDFONT.exists():
|
|
39
|
+
return _LOCAL_SOUNDFONT
|
|
40
|
+
return download_if_necessary(SF2_URL)
|
|
41
|
+
soundfont_path = Path(soundfont_path)
|
|
33
42
|
if not soundfont_path.exists():
|
|
34
43
|
raise FileNotFoundError(
|
|
35
44
|
f"SoundFont not found: {soundfont_path}\n"
|
|
36
|
-
"Pass --soundfont /path/to/file.sf2 or
|
|
37
|
-
"MuseScore_General.sf2
|
|
45
|
+
"Pass --soundfont /path/to/file.sf2, or omit it to use "
|
|
46
|
+
"MuseScore_General.sf2 (downloaded once and cached)."
|
|
38
47
|
)
|
|
39
48
|
return soundfont_path
|
|
40
49
|
|
|
@@ -85,8 +94,9 @@ def synthesize(
|
|
|
85
94
|
Args:
|
|
86
95
|
midi_path: Path to the MIDI file to synthesize.
|
|
87
96
|
output_path: Destination WAV file path.
|
|
88
|
-
soundfont_path: Path to a ``.sf2`` SoundFont file.
|
|
89
|
-
|
|
97
|
+
soundfont_path: Path to a ``.sf2`` SoundFont file. Defaults to
|
|
98
|
+
MuseScore_General.sf2, downloaded on first use and cached
|
|
99
|
+
locally (see :mod:`muscriptor.soundfonts`).
|
|
90
100
|
|
|
91
101
|
Raises:
|
|
92
102
|
RuntimeError: If fluidsynth is not available or fails.
|
|
@@ -112,8 +122,9 @@ def auralize(
|
|
|
112
122
|
midi_path: Path to the MIDI file to synthesize.
|
|
113
123
|
original_audio_path: Path to the source audio file (any format soundfile supports).
|
|
114
124
|
output_path: Destination WAV file path.
|
|
115
|
-
soundfont_path: Path to a ``.sf2`` SoundFont file.
|
|
116
|
-
|
|
125
|
+
soundfont_path: Path to a ``.sf2`` SoundFont file. Defaults to
|
|
126
|
+
MuseScore_General.sf2, downloaded on first use and cached
|
|
127
|
+
locally (see :mod:`muscriptor.soundfonts`).
|
|
117
128
|
|
|
118
129
|
Raises:
|
|
119
130
|
RuntimeError: If fluidsynth is not available or fails.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Weight download utility with caching in ~/.cache/muscriptor/."""
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
|
+
import os
|
|
4
5
|
import urllib.request
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from huggingface_hub import hf_hub_download
|
|
@@ -40,7 +41,15 @@ def download_if_necessary(url: str | Path) -> Path:
|
|
|
40
41
|
if dest.exists():
|
|
41
42
|
return dest
|
|
42
43
|
print(f"Downloading {filename} …")
|
|
43
|
-
|
|
44
|
+
# Download to a per-process temp file, then rename: an interrupted or
|
|
45
|
+
# concurrent download must never leave a partial file at `dest`, where
|
|
46
|
+
# it would be mistaken for a complete one forever after.
|
|
47
|
+
tmp = dest.with_name(f"{dest.name}.part{os.getpid()}")
|
|
48
|
+
try:
|
|
49
|
+
urllib.request.urlretrieve(url, tmp)
|
|
50
|
+
os.replace(tmp, dest)
|
|
51
|
+
finally:
|
|
52
|
+
tmp.unlink(missing_ok=True)
|
|
44
53
|
return dest
|
|
45
54
|
|
|
46
55
|
# Local file — nothing to download, just check it's there.
|