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.
Files changed (111) hide show
  1. {muscriptor-0.2.0 → muscriptor-0.2.2}/.dockerignore +4 -3
  2. muscriptor-0.2.2/.github/workflows/deploy.yml +28 -0
  3. muscriptor-0.2.2/.github/workflows/id_ed25519.pub +1 -0
  4. {muscriptor-0.2.0 → muscriptor-0.2.2}/.github/workflows/pypi.yml +9 -0
  5. {muscriptor-0.2.0 → muscriptor-0.2.2}/.gitignore +2 -2
  6. muscriptor-0.2.2/.python-version +1 -0
  7. {muscriptor-0.2.0 → muscriptor-0.2.2}/Dockerfile +18 -11
  8. {muscriptor-0.2.0 → muscriptor-0.2.2}/PKG-INFO +98 -23
  9. {muscriptor-0.2.0 → muscriptor-0.2.2}/README.md +92 -20
  10. muscriptor-0.2.2/muscriptor/accelerator.py +61 -0
  11. muscriptor-0.2.2/muscriptor/events.py +294 -0
  12. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/main.py +73 -17
  13. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/models/lm.py +29 -11
  14. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/modules/conditioners.py +3 -4
  15. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/modules/transformer.py +31 -16
  16. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/server.py +101 -1
  17. muscriptor-0.2.2/muscriptor/soundfonts.py +19 -0
  18. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/tokenizer/mt3.py +61 -0
  19. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/tokenizer/notes.py +47 -36
  20. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/transcription_model.py +129 -16
  21. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/auralization.py +19 -8
  22. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/download.py +46 -3
  23. muscriptor-0.2.2/muscriptor/utils/midi.py +41 -0
  24. muscriptor-0.2.2/muscriptor/web_dist/assets/index-Cc_b-ZGX.js +223 -0
  25. muscriptor-0.2.2/muscriptor/web_dist/assets/index-Dsp-ORJh.css +2 -0
  26. muscriptor-0.2.2/muscriptor/web_dist/assets/spessasynth_processor.min-tdOdhV7p.js +18 -0
  27. muscriptor-0.2.2/muscriptor/web_dist/headache_by_lost_deposit_1min.mp3 +0 -0
  28. muscriptor-0.2.2/muscriptor/web_dist/index.html +30 -0
  29. {muscriptor-0.2.0 → muscriptor-0.2.2}/pyproject.toml +15 -3
  30. {muscriptor-0.2.0 → muscriptor-0.2.2}/swarm.yml +5 -1
  31. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_cli.py +64 -1
  32. muscriptor-0.2.2/tests/test_download.py +26 -0
  33. muscriptor-0.2.2/tests/test_prelude_forcing.py +427 -0
  34. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_server.py +83 -0
  35. muscriptor-0.2.2/tests/test_strict_instruments.py +190 -0
  36. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_transcription_model.py +3 -0
  37. {muscriptor-0.2.0 → muscriptor-0.2.2}/uv.lock +291 -111
  38. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/index.html +1 -1
  39. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/package.json +0 -2
  40. muscriptor-0.2.2/web/public/favicon.png +0 -0
  41. muscriptor-0.2.2/web/public/headache_by_lost_deposit_1min.mp3 +0 -0
  42. muscriptor-0.2.2/web/public/kyutai-logo.svg +9 -0
  43. muscriptor-0.2.2/web/public/mirelo-logo.svg +10 -0
  44. muscriptor-0.2.2/web/public/muscriptor-logo-v2.png +0 -0
  45. muscriptor-0.2.2/web/public/muscriptor-logo-v4.svg +44 -0
  46. muscriptor-0.2.2/web/public/muscriptor-mark-v4.svg +33 -0
  47. muscriptor-0.2.2/web/public/muscriptor-wave.svg +3 -0
  48. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/App.tsx +14 -6
  49. muscriptor-0.2.2/web/src/analytics.ts +91 -0
  50. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/audio.ts +6 -3
  51. muscriptor-0.2.2/web/src/components/Button.tsx +43 -0
  52. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/ConditioningPanel.tsx +14 -8
  53. muscriptor-0.2.2/web/src/components/ConsentBanner.tsx +49 -0
  54. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/Controls.tsx +5 -4
  55. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/DropOverlay.tsx +1 -1
  56. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/InstrumentList.tsx +16 -12
  57. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/OutputBar.tsx +24 -13
  58. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/PianoRollCanvas.tsx +1 -1
  59. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/WelcomeScreen.tsx +18 -15
  60. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/hooks/useTranscription.ts +19 -0
  61. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/main.tsx +3 -0
  62. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/pianoroll.ts +36 -7
  63. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/style.css +22 -0
  64. muscriptor-0.2.2/web/src/vite-env.d.ts +10 -0
  65. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/vite.config.ts +10 -2
  66. muscriptor-0.2.0/.python-version +0 -1
  67. muscriptor-0.2.0/muscriptor/events.py +0 -208
  68. muscriptor-0.2.0/muscriptor/utils/midi.py +0 -25
  69. muscriptor-0.2.0/web/scripts/prepare-soundfonts.mjs +0 -69
  70. muscriptor-0.2.0/web/src/vite-env.d.ts +0 -1
  71. {muscriptor-0.2.0 → muscriptor-0.2.2}/.pre-commit-config.yaml +0 -0
  72. {muscriptor-0.2.0 → muscriptor-0.2.2}/LICENSE +0 -0
  73. {muscriptor-0.2.0 → muscriptor-0.2.2}/deploy.sh +0 -0
  74. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/__init__.py +0 -0
  75. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/__main__.py +0 -0
  76. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/models/__init__.py +0 -0
  77. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/modules/__init__.py +0 -0
  78. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/modules/mel_spectrogram.py +0 -0
  79. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/modules/streaming.py +0 -0
  80. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/tokenizer/__init__.py +0 -0
  81. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/__init__.py +0 -0
  82. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/audio.py +0 -0
  83. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/resample.py +0 -0
  84. {muscriptor-0.2.0 → muscriptor-0.2.2}/muscriptor/utils/sampling.py +0 -0
  85. {muscriptor-0.2.0/web/public → muscriptor-0.2.2/muscriptor/web_dist}/favicon.png +0 -0
  86. {muscriptor-0.2.0/web/public → muscriptor-0.2.2/muscriptor/web_dist}/kyutai-logo.svg +0 -0
  87. {muscriptor-0.2.0/web/public → muscriptor-0.2.2/muscriptor/web_dist}/mirelo-logo.svg +0 -0
  88. {muscriptor-0.2.0/web/public → muscriptor-0.2.2/muscriptor/web_dist}/muscriptor-logo-v2.png +0 -0
  89. /muscriptor-0.2.0/web/public/muscriptor-logo-v4-pink.svg → /muscriptor-0.2.2/muscriptor/web_dist/muscriptor-logo-v4.svg +0 -0
  90. /muscriptor-0.2.0/web/public/muscriptor-mark-v4-pink.svg → /muscriptor-0.2.2/muscriptor/web_dist/muscriptor-mark-v4.svg +0 -0
  91. {muscriptor-0.2.0/web/public → muscriptor-0.2.2/muscriptor/web_dist}/muscriptor-wave.svg +0 -0
  92. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/__init__.py +0 -0
  93. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/conftest.py +0 -0
  94. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/encode_helpers.py +0 -0
  95. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_conditioners.py +0 -0
  96. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_events.py +0 -0
  97. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_integration.py +0 -0
  98. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_midi.py +0 -0
  99. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_notes.py +0 -0
  100. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_sampling.py +0 -0
  101. {muscriptor-0.2.0 → muscriptor-0.2.2}/tests/test_transformer.py +0 -0
  102. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/logo_muscriptor_final.png +0 -0
  103. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/package-lock.json +0 -0
  104. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/pnpm-lock.yaml +0 -0
  105. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/Footer.tsx +0 -0
  106. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/components/icons.tsx +0 -0
  107. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/hooks/useAudioEngine.ts +0 -0
  108. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/instruments.ts +0 -0
  109. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/progress.ts +0 -0
  110. {muscriptor-0.2.0 → muscriptor-0.2.2}/web/src/sse.ts +0 -0
  111. {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/dist
11
- # Regenerated inside the web-builder stage from MuseScore_General.sf2.
12
- web/public/soundfonts
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
@@ -4,11 +4,11 @@
4
4
  !headache_mastered.mid
5
5
  *.wav
6
6
  *.mp3
7
+ !headache_by_lost_deposit_1min.mp3
7
8
  *.ogg
8
9
  *.sf2
9
10
  web/node_modules
10
- web/dist
11
- web/public/soundfonts/
11
+ muscriptor/web_dist/
12
12
  web/tsconfig.tsbuildinfo
13
13
  /dist
14
14
  *.egg-info
@@ -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 /web/dist ./web/dist
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.0
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>=1.24
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>=2.0
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
- condition the model on (exact names, e.g.
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) picks a value based on the device:
186
- 1 on CPU, 4 on GPU. Use `batch_size=1` for the lowest
187
- streaming latency — larger batches process several chunks
188
- together, so events belonging to later chunks of a batch
189
- won't arrive until the whole batch finishes. Within a
190
- batch, events are always yielded in temporal order; all
191
- events from chunk N are emitted before any event from
192
- chunk N+1.
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
- # Tell the model which instruments to expect (comma-separated names;
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 — the FastAPI server auto-mounts
280
- `web/dist/` if it exists. The first build downloads two soundfonts:
281
- `MuseScore_General.sf2` (215 MB, to the repo root, used by `/auralize`) and
282
- `MuseScore_General.sf3` (38 MB, the compressed build the UI plays); later
283
- builds skip files already on disk.
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
- condition the model on (exact names, e.g.
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) picks a value based on the device:
152
- 1 on CPU, 4 on GPU. Use `batch_size=1` for the lowest
153
- streaming latency — larger batches process several chunks
154
- together, so events belonging to later chunks of a batch
155
- won't arrive until the whole batch finishes. Within a
156
- batch, events are always yielded in temporal order; all
157
- events from chunk N are emitted before any event from
158
- chunk N+1.
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
- # Tell the model which instruments to expect (comma-separated names;
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 — the FastAPI server auto-mounts
246
- `web/dist/` if it exists. The first build downloads two soundfonts:
247
- `MuseScore_General.sf2` (215 MB, to the repo root, used by `/auralize`) and
248
- `MuseScore_General.sf3` (38 MB, the compressed build the UI plays); later
249
- builds skip files already on disk.
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()