muscriptor 0.2.0__tar.gz → 0.2.2__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.2.0 → muscriptor-0.2.2}/.dockerignore +4 -3
- muscriptor-0.2.2/.github/workflows/deploy.yml +28 -0
- muscriptor-0.2.2/.github/workflows/id_ed25519.pub +1 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/.github/workflows/pypi.yml +9 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/.gitignore +2 -2
- muscriptor-0.2.2/.python-version +1 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/Dockerfile +18 -11
- {muscriptor-0.2.0 → muscriptor-0.2.2}/PKG-INFO +98 -23
- {muscriptor-0.2.0 → muscriptor-0.2.2}/README.md +92 -20
- muscriptor-0.2.2/muscriptor/accelerator.py +61 -0
- muscriptor-0.2.2/muscriptor/events.py +294 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/main.py +73 -17
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/models/lm.py +29 -11
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/modules/conditioners.py +3 -4
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/modules/transformer.py +31 -16
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/server.py +101 -1
- muscriptor-0.2.2/muscriptor/soundfonts.py +19 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/tokenizer/mt3.py +61 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/tokenizer/notes.py +47 -36
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/transcription_model.py +129 -16
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/auralization.py +19 -8
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/download.py +46 -3
- muscriptor-0.2.2/muscriptor/utils/midi.py +41 -0
- muscriptor-0.2.2/muscriptor/web_dist/assets/index-Cc_b-ZGX.js +223 -0
- muscriptor-0.2.2/muscriptor/web_dist/assets/index-Dsp-ORJh.css +2 -0
- muscriptor-0.2.2/muscriptor/web_dist/assets/spessasynth_processor.min-tdOdhV7p.js +18 -0
- muscriptor-0.2.2/muscriptor/web_dist/headache_by_lost_deposit_1min.mp3 +0 -0
- muscriptor-0.2.2/muscriptor/web_dist/index.html +30 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/pyproject.toml +15 -3
- {muscriptor-0.2.0 → muscriptor-0.2.2}/swarm.yml +5 -1
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_cli.py +64 -1
- muscriptor-0.2.2/tests/test_download.py +26 -0
- muscriptor-0.2.2/tests/test_prelude_forcing.py +427 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_server.py +83 -0
- muscriptor-0.2.2/tests/test_strict_instruments.py +190 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_transcription_model.py +3 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/uv.lock +291 -111
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/index.html +1 -1
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/package.json +0 -2
- muscriptor-0.2.2/web/public/favicon.png +0 -0
- muscriptor-0.2.2/web/public/headache_by_lost_deposit_1min.mp3 +0 -0
- muscriptor-0.2.2/web/public/kyutai-logo.svg +9 -0
- muscriptor-0.2.2/web/public/mirelo-logo.svg +10 -0
- muscriptor-0.2.2/web/public/muscriptor-logo-v2.png +0 -0
- muscriptor-0.2.2/web/public/muscriptor-logo-v4.svg +44 -0
- muscriptor-0.2.2/web/public/muscriptor-mark-v4.svg +33 -0
- muscriptor-0.2.2/web/public/muscriptor-wave.svg +3 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/App.tsx +14 -6
- muscriptor-0.2.2/web/src/analytics.ts +91 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/audio.ts +6 -3
- muscriptor-0.2.2/web/src/components/Button.tsx +43 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/ConditioningPanel.tsx +14 -8
- muscriptor-0.2.2/web/src/components/ConsentBanner.tsx +49 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/Controls.tsx +5 -4
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/DropOverlay.tsx +1 -1
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/InstrumentList.tsx +16 -12
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/OutputBar.tsx +24 -13
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/PianoRollCanvas.tsx +1 -1
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/WelcomeScreen.tsx +18 -15
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/hooks/useTranscription.ts +19 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/main.tsx +3 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/pianoroll.ts +36 -7
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/style.css +22 -0
- muscriptor-0.2.2/web/src/vite-env.d.ts +10 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/vite.config.ts +10 -2
- muscriptor-0.2.0/.python-version +0 -1
- muscriptor-0.2.0/muscriptor/events.py +0 -208
- muscriptor-0.2.0/muscriptor/utils/midi.py +0 -25
- muscriptor-0.2.0/web/scripts/prepare-soundfonts.mjs +0 -69
- muscriptor-0.2.0/web/src/vite-env.d.ts +0 -1
- {muscriptor-0.2.0 → muscriptor-0.2.2}/.pre-commit-config.yaml +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/LICENSE +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/deploy.sh +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/__init__.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/__main__.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/models/__init__.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/modules/__init__.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/modules/mel_spectrogram.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/modules/streaming.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/tokenizer/__init__.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/__init__.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/audio.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/resample.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/sampling.py +0 -0
- {muscriptor-0.2.0/web/public → muscriptor-0.2.2/muscriptor/web_dist}/favicon.png +0 -0
- {muscriptor-0.2.0/web/public → muscriptor-0.2.2/muscriptor/web_dist}/kyutai-logo.svg +0 -0
- {muscriptor-0.2.0/web/public → muscriptor-0.2.2/muscriptor/web_dist}/mirelo-logo.svg +0 -0
- {muscriptor-0.2.0/web/public → muscriptor-0.2.2/muscriptor/web_dist}/muscriptor-logo-v2.png +0 -0
- /muscriptor-0.2.0/web/public/muscriptor-logo-v4-pink.svg → /muscriptor-0.2.2/muscriptor/web_dist/muscriptor-logo-v4.svg +0 -0
- /muscriptor-0.2.0/web/public/muscriptor-mark-v4-pink.svg → /muscriptor-0.2.2/muscriptor/web_dist/muscriptor-mark-v4.svg +0 -0
- {muscriptor-0.2.0/web/public → muscriptor-0.2.2/muscriptor/web_dist}/muscriptor-wave.svg +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/__init__.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/conftest.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/encode_helpers.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_conditioners.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_events.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_integration.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_midi.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_notes.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_sampling.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_transformer.py +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/logo_muscriptor_final.png +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/package-lock.json +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/pnpm-lock.yaml +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/Footer.tsx +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/icons.tsx +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/hooks/useAudioEngine.ts +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/instruments.ts +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/progress.ts +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/sse.ts +0 -0
- {muscriptor-0.2.0 → muscriptor-0.2.2}/web/tsconfig.json +0 -0
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*.safetensors
|
|
4
4
|
*.wav
|
|
5
5
|
*.mp3
|
|
6
|
+
!web/public/headache_by_lost_deposit_1min.mp3
|
|
6
7
|
*.ogg
|
|
7
8
|
*.mid
|
|
8
9
|
.venv
|
|
9
10
|
web/node_modules
|
|
10
|
-
web
|
|
11
|
-
#
|
|
12
|
-
|
|
11
|
+
# Built fresh in the web-builder stage; a stale local build must not leak
|
|
12
|
+
# into the runtime image via `COPY muscriptor/`.
|
|
13
|
+
muscriptor/web_dist
|
|
13
14
|
.git
|
|
14
15
|
.gitignore
|
|
15
16
|
__pycache__
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Deploy demo
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
concurrency:
|
|
7
|
+
group: deploy-demo
|
|
8
|
+
cancel-in-progress: false
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
deploy:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- name: Set up SSH access to the deployment host
|
|
16
|
+
run: |
|
|
17
|
+
mkdir -p ~/.ssh
|
|
18
|
+
chmod 700 ~/.ssh
|
|
19
|
+
# The public key is at .github/workflows/id_ed25519.pub
|
|
20
|
+
# If the machine changes, add this key to the authorized_keys of the new machine.
|
|
21
|
+
echo "${{ secrets.DEPLOYMENT_SSH_KEY }}" > ~/.ssh/id_ed25519
|
|
22
|
+
chmod 600 ~/.ssh/id_ed25519
|
|
23
|
+
ssh-keyscan muscriptor.kyutai.org >> ~/.ssh/known_hosts
|
|
24
|
+
echo "${{ secrets.DEPLOYMENT_REGISTRY_SECRET }}" | docker login rg.fr-par.scw.cloud -u nologin --password-stdin
|
|
25
|
+
- name: Deploy
|
|
26
|
+
env:
|
|
27
|
+
HF_TOKEN: ${{ secrets.DEPLOYMENT_HF_TOKEN }}
|
|
28
|
+
run: ./deploy.sh
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOTt3l4T8vJXJWxDPQrWjXFNOf3Z4ww/vtUXBbn5reJU root@tf-muscriptor-0
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -8,14 +8,12 @@ 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
|
+
# The GA4 measurement ID is inlined into the JS bundle by Vite; left unset
|
|
13
|
+
# (the default), the frontend is built with analytics disabled entirely.
|
|
14
|
+
ARG VITE_GA_MEASUREMENT_ID=
|
|
15
|
+
ENV VITE_GA_MEASUREMENT_ID=$VITE_GA_MEASUREMENT_ID
|
|
16
|
+
# vite outDir is ../muscriptor/web_dist, i.e. /muscriptor/web_dist in this stage.
|
|
19
17
|
RUN pnpm run build
|
|
20
18
|
|
|
21
19
|
|
|
@@ -38,15 +36,24 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
|
|
38
36
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
|
39
37
|
uv sync --no-install-project
|
|
40
38
|
|
|
39
|
+
# Pre-fetch the soundfonts (253 MB: .sf2 for /auralize, .sf3 for the web UI)
|
|
40
|
+
# into the HF hub cache so a fresh container doesn't download them on first
|
|
41
|
+
# use. Runs off just the two files it needs, before the full source COPY,
|
|
42
|
+
# so day-to-day code changes don't re-trigger the download.
|
|
43
|
+
COPY muscriptor/soundfonts.py muscriptor/utils/download.py /tmp/prewarm/
|
|
44
|
+
RUN /app/.venv/bin/python -c "\
|
|
45
|
+
import sys; sys.path.insert(0, '/tmp/prewarm'); \
|
|
46
|
+
import soundfonts, download; \
|
|
47
|
+
download.download_if_necessary(soundfonts.SF2_URL); \
|
|
48
|
+
download.download_if_necessary(soundfonts.SF3_URL)" \
|
|
49
|
+
&& rm -rf /tmp/prewarm
|
|
50
|
+
|
|
41
51
|
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
52
|
COPY muscriptor/ ./muscriptor/
|
|
46
53
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
47
54
|
uv sync
|
|
48
55
|
|
|
49
|
-
COPY --from=web-builder /
|
|
56
|
+
COPY --from=web-builder /muscriptor/web_dist ./muscriptor/web_dist
|
|
50
57
|
|
|
51
58
|
|
|
52
59
|
EXPOSE 8000
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: muscriptor
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -23,11 +23,14 @@ Requires-Dist: fastapi>=0.136.3
|
|
|
23
23
|
Requires-Dist: httpx>=0.28.1
|
|
24
24
|
Requires-Dist: huggingface-hub>=0.13
|
|
25
25
|
Requires-Dist: mido>=1.3
|
|
26
|
-
Requires-Dist: numpy
|
|
26
|
+
Requires-Dist: numpy<2,>=1.24; sys_platform == 'darwin' and platform_machine == 'x86_64'
|
|
27
|
+
Requires-Dist: numpy>=1.24; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
|
28
|
+
Requires-Dist: packaging>=20.0
|
|
27
29
|
Requires-Dist: python-multipart>=0.0.29
|
|
28
30
|
Requires-Dist: safetensors>=0.4
|
|
29
31
|
Requires-Dist: soundfile>=0.14.0
|
|
30
|
-
Requires-Dist: torch
|
|
32
|
+
Requires-Dist: torch<2.3,>=2.0; sys_platform == 'darwin' and platform_machine == 'x86_64'
|
|
33
|
+
Requires-Dist: torch>=2.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
|
31
34
|
Requires-Dist: typer>=0.10
|
|
32
35
|
Requires-Dist: uvicorn[standard]>=0.48.0
|
|
33
36
|
Description-Content-Type: text/markdown
|
|
@@ -50,6 +53,30 @@ MuScriptor is the first music transcription model that has been trained on a lar
|
|
|
50
53
|
</p>
|
|
51
54
|
-->
|
|
52
55
|
|
|
56
|
+
## HuggingFace login (required)
|
|
57
|
+
|
|
58
|
+
The model weights are hosted on [HuggingFace](https://huggingface.co/MuScriptor)
|
|
59
|
+
and gated behind their CC BY-NC 4.0 license, so downloading them — including
|
|
60
|
+
on the first `uvx muscriptor` run — requires a (free) HuggingFace account:
|
|
61
|
+
|
|
62
|
+
1. Accept the model license on the model page, e.g.
|
|
63
|
+
[muscriptor-medium](https://huggingface.co/MuScriptor/muscriptor-medium)
|
|
64
|
+
(access is granted automatically).
|
|
65
|
+
2. Authenticate on your machine:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uvx hf auth login
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
or set a token (create one at
|
|
72
|
+
[huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export HF_TOKEN=hf_...
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The weights are then downloaded on first use and cached locally.
|
|
79
|
+
|
|
53
80
|
## Try it locally
|
|
54
81
|
|
|
55
82
|
You can try it locally with the web UI with:
|
|
@@ -58,12 +85,29 @@ You can try it locally with the web UI with:
|
|
|
58
85
|
uvx muscriptor serve
|
|
59
86
|
```
|
|
60
87
|
|
|
88
|
+
On windows, the default PyTorch backend is `cpu`, so to use the GPU on Windows, you'll need to run:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uvx --torch-backend=cu128 muscriptor serve
|
|
92
|
+
```
|
|
93
|
+
Use the `--torch-backend=cu128` flag every time you run a `uvx` command on Windows.
|
|
94
|
+
|
|
61
95
|
or with the CLI:
|
|
62
96
|
|
|
63
97
|
```bash
|
|
64
98
|
uvx muscriptor transcribe
|
|
65
99
|
```
|
|
66
100
|
|
|
101
|
+
> **Intel Macs:** PyTorch stopped shipping Intel-mac (x86_64) wheels after
|
|
102
|
+
> torch 2.2.2, which supports Python ≤ 3.12, so you must tell uvx to use
|
|
103
|
+
> Python 3.12:
|
|
104
|
+
>
|
|
105
|
+
> ```bash
|
|
106
|
+
> uvx --python 3.12 muscriptor serve
|
|
107
|
+
> ```
|
|
108
|
+
>
|
|
109
|
+
> If you install with pip/uv instead, use Python 3.10–3.12.
|
|
110
|
+
|
|
67
111
|
|
|
68
112
|
## Installation
|
|
69
113
|
|
|
@@ -92,7 +136,9 @@ weights are downloaded and cached automatically. The architecture is a transform
|
|
|
92
136
|
|
|
93
137
|
`small` is the practical choice on CPU-only machines, `medium` is the default
|
|
94
138
|
speed/accuracy trade-off, and `large` is the most accurate but really wants a
|
|
95
|
-
GPU.
|
|
139
|
+
GPU. On Apple Silicon the model runs on Metal (MPS) automatically, in float16
|
|
140
|
+
by default (`--dtype float32` to override) — fast enough that `large`
|
|
141
|
+
transcribes several times faster than real time.
|
|
96
142
|
## Usage
|
|
97
143
|
|
|
98
144
|
```python
|
|
@@ -165,6 +211,7 @@ class TranscriptionModel:
|
|
|
165
211
|
batch_size: int | None = None,
|
|
166
212
|
no_eos_is_ok: bool = True,
|
|
167
213
|
beam_size: int = 1,
|
|
214
|
+
prelude_forcing: bool = True,
|
|
168
215
|
) -> Generator[NoteStartEvent | NoteEndEvent | ProgressEvent, None, None]:
|
|
169
216
|
"""Transcribe audio into a stream of note events.
|
|
170
217
|
|
|
@@ -177,24 +224,39 @@ class TranscriptionModel:
|
|
|
177
224
|
temperature: Sampling temperature (only used when use_sampling=True).
|
|
178
225
|
cfg_coef: Classifier-free guidance coefficient. Keep to 1 for the released models (they are post-RL)
|
|
179
226
|
instruments: Optional list of instrument group names to
|
|
180
|
-
|
|
227
|
+
restrict decoding to (exact names, e.g.
|
|
181
228
|
["acoustic_piano", "drums"]). Run `muscriptor
|
|
182
229
|
list-instruments` (or GET /instruments on the server)
|
|
183
|
-
for the full list of valid names.
|
|
230
|
+
for the full list of valid names. When given, this is a
|
|
231
|
+
hard constraint: every program/drum token outside the
|
|
232
|
+
list is masked out during generation, so no unlisted
|
|
233
|
+
instrument can appear in the output. Leave unset to let
|
|
234
|
+
the model decode whatever instruments it detects.
|
|
184
235
|
batch_size: Number of 5-second chunks processed per forward
|
|
185
|
-
pass. `None` (default)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
events
|
|
192
|
-
|
|
236
|
+
pass. `None` (default) resolves to 1 while prelude forcing
|
|
237
|
+
is on; with `prelude_forcing=False` it picks a value based
|
|
238
|
+
on the device: 1 on CPU, 4 on GPU. Values > 1 trade
|
|
239
|
+
chunk-boundary quality for throughput and therefore require
|
|
240
|
+
`prelude_forcing=False` (ValueError otherwise). Batching
|
|
241
|
+
also delays streaming: several chunks generate together, so
|
|
242
|
+
events belonging to later chunks of a batch won't arrive
|
|
243
|
+
until the whole batch finishes. Within a batch, events are
|
|
244
|
+
always yielded in temporal order; all events from chunk N
|
|
245
|
+
are emitted before any event from chunk N+1.
|
|
193
246
|
no_eos_is_ok: If True, a chunk that doesn't emit EOS within
|
|
194
247
|
the generation budget produces a warning instead of raising.
|
|
195
248
|
beam_size: Beam search width. 1 (default) uses greedy decoding
|
|
196
249
|
(or sampling, with use_sampling=True); >= 2 enables beam
|
|
197
250
|
search, which is slower but can be more accurate.
|
|
251
|
+
prelude_forcing: If True (default), every chunk after the first
|
|
252
|
+
has its tie prologue — the tokens declaring which notes are
|
|
253
|
+
sustained from the previous chunk — teacher-forced from the
|
|
254
|
+
previous chunk's actually-unfinished notes, instead of
|
|
255
|
+
letting the model guess (which occasionally makes a chunk
|
|
256
|
+
restart with the wrong instruments). Requires chunks to be
|
|
257
|
+
generated strictly in order, so it only works with
|
|
258
|
+
batch_size=1 (the default); set prelude_forcing=False
|
|
259
|
+
explicitly to use larger batches.
|
|
198
260
|
|
|
199
261
|
Returns:
|
|
200
262
|
Generator of NoteStartEvent, NoteEndEvent and ProgressEvent
|
|
@@ -218,6 +280,7 @@ class TranscriptionModel:
|
|
|
218
280
|
batch_size: int | None = None,
|
|
219
281
|
no_eos_is_ok: bool = True,
|
|
220
282
|
beam_size: int = 1,
|
|
283
|
+
prelude_forcing: bool = True,
|
|
221
284
|
) -> bytes:
|
|
222
285
|
"""Same as `transcribe`, but returns a MIDI file as bytes instead
|
|
223
286
|
of a generator of events. Useful when you want to save the MIDI
|
|
@@ -237,10 +300,11 @@ muscriptor transcribe audio.wav -o out.mid
|
|
|
237
300
|
# a local safetensors path, or an hf:// / http(s):// URL
|
|
238
301
|
muscriptor transcribe audio.wav --model large
|
|
239
302
|
|
|
240
|
-
#
|
|
303
|
+
# Restrict decoding to only the instruments listed (comma-separated names;
|
|
241
304
|
# run `muscriptor list-instruments` for the full list). Case-insensitive,
|
|
242
305
|
# and unambiguous abbreviations work: 'timp,dist' = timpani + distorted
|
|
243
|
-
# electric guitar
|
|
306
|
+
# electric guitar. Every other instrument's tokens are masked out during
|
|
307
|
+
# generation, so nothing else can appear in the output.
|
|
244
308
|
muscriptor transcribe audio.wav --instruments acoustic_piano,drums
|
|
245
309
|
|
|
246
310
|
# Get the event stream instead of MIDI: json (single array) or
|
|
@@ -252,6 +316,13 @@ muscriptor transcribe audio.wav --format jsonl -o -
|
|
|
252
316
|
muscriptor transcribe audio.wav --sampling -t 0.8
|
|
253
317
|
muscriptor transcribe audio.wav --beam-size 4
|
|
254
318
|
|
|
319
|
+
# Notes sustained across a chunk boundary are teacher-forced into the next
|
|
320
|
+
# chunk's prologue by default, so a chunk can't restart them with the wrong
|
|
321
|
+
# instrument. This needs chunks generated in order, so the batch size
|
|
322
|
+
# defaults to 1; batching (faster on GPU, lower quality at chunk boundaries)
|
|
323
|
+
# must be opted into explicitly:
|
|
324
|
+
muscriptor transcribe audio.wav --no-prelude-forcing --batch-size 4
|
|
325
|
+
|
|
255
326
|
# Render a stereo check-mix of the result (left channel = original audio,
|
|
256
327
|
# right channel = synthesized MIDI; requires fluidsynth on PATH)
|
|
257
328
|
muscriptor transcribe audio.wav -o out.mid --auralize check.wav
|
|
@@ -276,11 +347,15 @@ uv sync
|
|
|
276
347
|
cd web && pnpm install && pnpm run build && cd ..
|
|
277
348
|
```
|
|
278
349
|
|
|
279
|
-
`pnpm run build` is required once —
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
350
|
+
`pnpm run build` is required once — it outputs to `muscriptor/web_dist/`,
|
|
351
|
+
which the FastAPI server auto-mounts if it exists (and which ships inside
|
|
352
|
+
the PyPI wheel, so `uvx muscriptor serve` works without a checkout).
|
|
353
|
+
|
|
354
|
+
The soundfonts are not bundled: the server fetches
|
|
355
|
+
`MuseScore_General.sf2` (215 MB, used by `/auralize`) and
|
|
356
|
+
`MuseScore_General.sf3` (38 MB, the compressed build the UI plays) from
|
|
357
|
+
[MuScriptor/assets](https://huggingface.co/MuScriptor/assets) on first use
|
|
358
|
+
and caches them locally (see `muscriptor/soundfonts.py`).
|
|
284
359
|
|
|
285
360
|
### Run
|
|
286
361
|
|
|
@@ -305,8 +380,8 @@ a WAV onto the page.
|
|
|
305
380
|
is local-only.
|
|
306
381
|
- Playback runs a full SoundFont synthesizer ([SpessaSynth](https://github.com/spessasus/spessasynth_lib))
|
|
307
382
|
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.
|
|
383
|
+
`/auralize` endpoint uses, served by the app itself from `/soundfonts/`
|
|
384
|
+
(cached server-side), no third-party CDN.
|
|
310
385
|
|
|
311
386
|
## License
|
|
312
387
|
|
|
@@ -16,6 +16,30 @@ MuScriptor is the first music transcription model that has been trained on a lar
|
|
|
16
16
|
</p>
|
|
17
17
|
-->
|
|
18
18
|
|
|
19
|
+
## HuggingFace login (required)
|
|
20
|
+
|
|
21
|
+
The model weights are hosted on [HuggingFace](https://huggingface.co/MuScriptor)
|
|
22
|
+
and gated behind their CC BY-NC 4.0 license, so downloading them — including
|
|
23
|
+
on the first `uvx muscriptor` run — requires a (free) HuggingFace account:
|
|
24
|
+
|
|
25
|
+
1. Accept the model license on the model page, e.g.
|
|
26
|
+
[muscriptor-medium](https://huggingface.co/MuScriptor/muscriptor-medium)
|
|
27
|
+
(access is granted automatically).
|
|
28
|
+
2. Authenticate on your machine:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uvx hf auth login
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
or set a token (create one at
|
|
35
|
+
[huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export HF_TOKEN=hf_...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The weights are then downloaded on first use and cached locally.
|
|
42
|
+
|
|
19
43
|
## Try it locally
|
|
20
44
|
|
|
21
45
|
You can try it locally with the web UI with:
|
|
@@ -24,12 +48,29 @@ You can try it locally with the web UI with:
|
|
|
24
48
|
uvx muscriptor serve
|
|
25
49
|
```
|
|
26
50
|
|
|
51
|
+
On windows, the default PyTorch backend is `cpu`, so to use the GPU on Windows, you'll need to run:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uvx --torch-backend=cu128 muscriptor serve
|
|
55
|
+
```
|
|
56
|
+
Use the `--torch-backend=cu128` flag every time you run a `uvx` command on Windows.
|
|
57
|
+
|
|
27
58
|
or with the CLI:
|
|
28
59
|
|
|
29
60
|
```bash
|
|
30
61
|
uvx muscriptor transcribe
|
|
31
62
|
```
|
|
32
63
|
|
|
64
|
+
> **Intel Macs:** PyTorch stopped shipping Intel-mac (x86_64) wheels after
|
|
65
|
+
> torch 2.2.2, which supports Python ≤ 3.12, so you must tell uvx to use
|
|
66
|
+
> Python 3.12:
|
|
67
|
+
>
|
|
68
|
+
> ```bash
|
|
69
|
+
> uvx --python 3.12 muscriptor serve
|
|
70
|
+
> ```
|
|
71
|
+
>
|
|
72
|
+
> If you install with pip/uv instead, use Python 3.10–3.12.
|
|
73
|
+
|
|
33
74
|
|
|
34
75
|
## Installation
|
|
35
76
|
|
|
@@ -58,7 +99,9 @@ weights are downloaded and cached automatically. The architecture is a transform
|
|
|
58
99
|
|
|
59
100
|
`small` is the practical choice on CPU-only machines, `medium` is the default
|
|
60
101
|
speed/accuracy trade-off, and `large` is the most accurate but really wants a
|
|
61
|
-
GPU.
|
|
102
|
+
GPU. On Apple Silicon the model runs on Metal (MPS) automatically, in float16
|
|
103
|
+
by default (`--dtype float32` to override) — fast enough that `large`
|
|
104
|
+
transcribes several times faster than real time.
|
|
62
105
|
## Usage
|
|
63
106
|
|
|
64
107
|
```python
|
|
@@ -131,6 +174,7 @@ class TranscriptionModel:
|
|
|
131
174
|
batch_size: int | None = None,
|
|
132
175
|
no_eos_is_ok: bool = True,
|
|
133
176
|
beam_size: int = 1,
|
|
177
|
+
prelude_forcing: bool = True,
|
|
134
178
|
) -> Generator[NoteStartEvent | NoteEndEvent | ProgressEvent, None, None]:
|
|
135
179
|
"""Transcribe audio into a stream of note events.
|
|
136
180
|
|
|
@@ -143,24 +187,39 @@ class TranscriptionModel:
|
|
|
143
187
|
temperature: Sampling temperature (only used when use_sampling=True).
|
|
144
188
|
cfg_coef: Classifier-free guidance coefficient. Keep to 1 for the released models (they are post-RL)
|
|
145
189
|
instruments: Optional list of instrument group names to
|
|
146
|
-
|
|
190
|
+
restrict decoding to (exact names, e.g.
|
|
147
191
|
["acoustic_piano", "drums"]). Run `muscriptor
|
|
148
192
|
list-instruments` (or GET /instruments on the server)
|
|
149
|
-
for the full list of valid names.
|
|
193
|
+
for the full list of valid names. When given, this is a
|
|
194
|
+
hard constraint: every program/drum token outside the
|
|
195
|
+
list is masked out during generation, so no unlisted
|
|
196
|
+
instrument can appear in the output. Leave unset to let
|
|
197
|
+
the model decode whatever instruments it detects.
|
|
150
198
|
batch_size: Number of 5-second chunks processed per forward
|
|
151
|
-
pass. `None` (default)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
events
|
|
158
|
-
|
|
199
|
+
pass. `None` (default) resolves to 1 while prelude forcing
|
|
200
|
+
is on; with `prelude_forcing=False` it picks a value based
|
|
201
|
+
on the device: 1 on CPU, 4 on GPU. Values > 1 trade
|
|
202
|
+
chunk-boundary quality for throughput and therefore require
|
|
203
|
+
`prelude_forcing=False` (ValueError otherwise). Batching
|
|
204
|
+
also delays streaming: several chunks generate together, so
|
|
205
|
+
events belonging to later chunks of a batch won't arrive
|
|
206
|
+
until the whole batch finishes. Within a batch, events are
|
|
207
|
+
always yielded in temporal order; all events from chunk N
|
|
208
|
+
are emitted before any event from chunk N+1.
|
|
159
209
|
no_eos_is_ok: If True, a chunk that doesn't emit EOS within
|
|
160
210
|
the generation budget produces a warning instead of raising.
|
|
161
211
|
beam_size: Beam search width. 1 (default) uses greedy decoding
|
|
162
212
|
(or sampling, with use_sampling=True); >= 2 enables beam
|
|
163
213
|
search, which is slower but can be more accurate.
|
|
214
|
+
prelude_forcing: If True (default), every chunk after the first
|
|
215
|
+
has its tie prologue — the tokens declaring which notes are
|
|
216
|
+
sustained from the previous chunk — teacher-forced from the
|
|
217
|
+
previous chunk's actually-unfinished notes, instead of
|
|
218
|
+
letting the model guess (which occasionally makes a chunk
|
|
219
|
+
restart with the wrong instruments). Requires chunks to be
|
|
220
|
+
generated strictly in order, so it only works with
|
|
221
|
+
batch_size=1 (the default); set prelude_forcing=False
|
|
222
|
+
explicitly to use larger batches.
|
|
164
223
|
|
|
165
224
|
Returns:
|
|
166
225
|
Generator of NoteStartEvent, NoteEndEvent and ProgressEvent
|
|
@@ -184,6 +243,7 @@ class TranscriptionModel:
|
|
|
184
243
|
batch_size: int | None = None,
|
|
185
244
|
no_eos_is_ok: bool = True,
|
|
186
245
|
beam_size: int = 1,
|
|
246
|
+
prelude_forcing: bool = True,
|
|
187
247
|
) -> bytes:
|
|
188
248
|
"""Same as `transcribe`, but returns a MIDI file as bytes instead
|
|
189
249
|
of a generator of events. Useful when you want to save the MIDI
|
|
@@ -203,10 +263,11 @@ muscriptor transcribe audio.wav -o out.mid
|
|
|
203
263
|
# a local safetensors path, or an hf:// / http(s):// URL
|
|
204
264
|
muscriptor transcribe audio.wav --model large
|
|
205
265
|
|
|
206
|
-
#
|
|
266
|
+
# Restrict decoding to only the instruments listed (comma-separated names;
|
|
207
267
|
# run `muscriptor list-instruments` for the full list). Case-insensitive,
|
|
208
268
|
# and unambiguous abbreviations work: 'timp,dist' = timpani + distorted
|
|
209
|
-
# electric guitar
|
|
269
|
+
# electric guitar. Every other instrument's tokens are masked out during
|
|
270
|
+
# generation, so nothing else can appear in the output.
|
|
210
271
|
muscriptor transcribe audio.wav --instruments acoustic_piano,drums
|
|
211
272
|
|
|
212
273
|
# Get the event stream instead of MIDI: json (single array) or
|
|
@@ -218,6 +279,13 @@ muscriptor transcribe audio.wav --format jsonl -o -
|
|
|
218
279
|
muscriptor transcribe audio.wav --sampling -t 0.8
|
|
219
280
|
muscriptor transcribe audio.wav --beam-size 4
|
|
220
281
|
|
|
282
|
+
# Notes sustained across a chunk boundary are teacher-forced into the next
|
|
283
|
+
# chunk's prologue by default, so a chunk can't restart them with the wrong
|
|
284
|
+
# instrument. This needs chunks generated in order, so the batch size
|
|
285
|
+
# defaults to 1; batching (faster on GPU, lower quality at chunk boundaries)
|
|
286
|
+
# must be opted into explicitly:
|
|
287
|
+
muscriptor transcribe audio.wav --no-prelude-forcing --batch-size 4
|
|
288
|
+
|
|
221
289
|
# Render a stereo check-mix of the result (left channel = original audio,
|
|
222
290
|
# right channel = synthesized MIDI; requires fluidsynth on PATH)
|
|
223
291
|
muscriptor transcribe audio.wav -o out.mid --auralize check.wav
|
|
@@ -242,11 +310,15 @@ uv sync
|
|
|
242
310
|
cd web && pnpm install && pnpm run build && cd ..
|
|
243
311
|
```
|
|
244
312
|
|
|
245
|
-
`pnpm run build` is required once —
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
313
|
+
`pnpm run build` is required once — it outputs to `muscriptor/web_dist/`,
|
|
314
|
+
which the FastAPI server auto-mounts if it exists (and which ships inside
|
|
315
|
+
the PyPI wheel, so `uvx muscriptor serve` works without a checkout).
|
|
316
|
+
|
|
317
|
+
The soundfonts are not bundled: the server fetches
|
|
318
|
+
`MuseScore_General.sf2` (215 MB, used by `/auralize`) and
|
|
319
|
+
`MuseScore_General.sf3` (38 MB, the compressed build the UI plays) from
|
|
320
|
+
[MuScriptor/assets](https://huggingface.co/MuScriptor/assets) on first use
|
|
321
|
+
and caches them locally (see `muscriptor/soundfonts.py`).
|
|
250
322
|
|
|
251
323
|
### Run
|
|
252
324
|
|
|
@@ -271,8 +343,8 @@ a WAV onto the page.
|
|
|
271
343
|
is local-only.
|
|
272
344
|
- Playback runs a full SoundFont synthesizer ([SpessaSynth](https://github.com/spessasus/spessasynth_lib))
|
|
273
345
|
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.
|
|
346
|
+
`/auralize` endpoint uses, served by the app itself from `/soundfonts/`
|
|
347
|
+
(cached server-side), no third-party CDN.
|
|
276
348
|
|
|
277
349
|
## License
|
|
278
350
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Device-agnostic accelerator helpers.
|
|
2
|
+
|
|
3
|
+
``torch.accelerator`` (``is_available``/``current_accelerator``/``synchronize``)
|
|
4
|
+
was only added in PyTorch 2.6; on older versions we fall back to checking CUDA
|
|
5
|
+
then MPS directly, in that order.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import platform
|
|
9
|
+
|
|
10
|
+
from packaging.version import Version
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
|
|
14
|
+
_HAS_TORCH_ACCELERATOR = Version(torch.__version__.split("+")[0]) >= Version("2.6")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _mps_available() -> bool:
|
|
18
|
+
"""Whether MPS is available and worth auto-selecting.
|
|
19
|
+
|
|
20
|
+
torch <= 2.2 also reports MPS as available on Intel Macs with AMD GPUs, a
|
|
21
|
+
backend that was never solid and has since been abandoned. Passing
|
|
22
|
+
``device="mps"`` explicitly still works there for those who want to try
|
|
23
|
+
it; this only affects auto-detection.
|
|
24
|
+
"""
|
|
25
|
+
return torch.backends.mps.is_available() and platform.machine() == "arm64"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_available() -> bool:
|
|
29
|
+
"""Whether an accelerator (GPU) is available."""
|
|
30
|
+
if _HAS_TORCH_ACCELERATOR:
|
|
31
|
+
return torch.accelerator.is_available()
|
|
32
|
+
return torch.cuda.is_available() or _mps_available()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def current_accelerator() -> torch.device:
|
|
36
|
+
"""The current accelerator device.
|
|
37
|
+
|
|
38
|
+
Raises ``RuntimeError`` if no accelerator is available; check
|
|
39
|
+
:func:`is_available` first.
|
|
40
|
+
"""
|
|
41
|
+
if _HAS_TORCH_ACCELERATOR:
|
|
42
|
+
return torch.accelerator.current_accelerator()
|
|
43
|
+
if torch.cuda.is_available():
|
|
44
|
+
return torch.device("cuda")
|
|
45
|
+
if _mps_available():
|
|
46
|
+
return torch.device("mps")
|
|
47
|
+
raise RuntimeError("No available accelerator detected.")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def synchronize() -> None:
|
|
51
|
+
"""Wait for all kernels on the current accelerator to complete.
|
|
52
|
+
|
|
53
|
+
No-op if no accelerator is available.
|
|
54
|
+
"""
|
|
55
|
+
if _HAS_TORCH_ACCELERATOR:
|
|
56
|
+
torch.accelerator.synchronize()
|
|
57
|
+
return
|
|
58
|
+
if torch.cuda.is_available():
|
|
59
|
+
torch.cuda.synchronize()
|
|
60
|
+
elif _mps_available():
|
|
61
|
+
torch.mps.synchronize()
|