muscriptor 0.2.1__tar.gz → 0.3.0__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 (120) hide show
  1. {muscriptor-0.2.1 → muscriptor-0.3.0}/.dockerignore +1 -0
  2. muscriptor-0.3.0/.github/workflows/deploy.yml +28 -0
  3. muscriptor-0.3.0/.github/workflows/id_ed25519.pub +1 -0
  4. {muscriptor-0.2.1 → muscriptor-0.3.0}/.gitignore +3 -0
  5. muscriptor-0.3.0/.python-version +1 -0
  6. {muscriptor-0.2.1 → muscriptor-0.3.0}/Dockerfile +4 -0
  7. {muscriptor-0.2.1 → muscriptor-0.3.0}/PKG-INFO +94 -16
  8. {muscriptor-0.2.1 → muscriptor-0.3.0}/README.md +87 -13
  9. muscriptor-0.3.0/muscriptor/accelerator.py +69 -0
  10. muscriptor-0.3.0/muscriptor/events.py +294 -0
  11. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/main.py +103 -19
  12. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/models/lm.py +30 -12
  13. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/modules/conditioners.py +3 -4
  14. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/modules/transformer.py +31 -16
  15. muscriptor-0.3.0/muscriptor/server.py +451 -0
  16. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/tokenizer/mt3.py +61 -0
  17. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/tokenizer/notes.py +70 -37
  18. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/transcription_model.py +177 -22
  19. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/utils/auralization.py +23 -11
  20. muscriptor-0.3.0/muscriptor/utils/beats.py +367 -0
  21. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/utils/download.py +36 -2
  22. muscriptor-0.3.0/muscriptor/utils/midi.py +55 -0
  23. muscriptor-0.3.0/muscriptor/web_dist/assets/index-ClK01VrS.js +223 -0
  24. muscriptor-0.3.0/muscriptor/web_dist/assets/index-DfGntwya.css +2 -0
  25. muscriptor-0.3.0/muscriptor/web_dist/headache_by_lost_deposit_1min.mp3 +0 -0
  26. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/web_dist/index.html +3 -3
  27. muscriptor-0.2.1/web/public/muscriptor-mark-v4-pink.svg → muscriptor-0.3.0/muscriptor/web_dist/muscriptor-mark-v4.svg +3 -0
  28. {muscriptor-0.2.1 → muscriptor-0.3.0}/pyproject.toml +15 -3
  29. {muscriptor-0.2.1 → muscriptor-0.3.0}/swarm.yml +6 -1
  30. muscriptor-0.3.0/tests/test_beats.py +243 -0
  31. {muscriptor-0.2.1 → muscriptor-0.3.0}/tests/test_cli.py +64 -1
  32. muscriptor-0.3.0/tests/test_download.py +26 -0
  33. muscriptor-0.3.0/tests/test_midi.py +174 -0
  34. muscriptor-0.3.0/tests/test_prelude_forcing.py +427 -0
  35. muscriptor-0.3.0/tests/test_server.py +463 -0
  36. muscriptor-0.3.0/tests/test_strict_instruments.py +190 -0
  37. {muscriptor-0.2.1 → muscriptor-0.3.0}/tests/test_transcription_model.py +31 -0
  38. {muscriptor-0.2.1 → muscriptor-0.3.0}/uv.lock +1895 -109
  39. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/index.html +1 -1
  40. muscriptor-0.3.0/web/public/headache_by_lost_deposit_1min.mp3 +0 -0
  41. muscriptor-0.2.1/muscriptor/web_dist/muscriptor-mark-v4-pink.svg → muscriptor-0.3.0/web/public/muscriptor-mark-v4.svg +3 -0
  42. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/App.tsx +111 -36
  43. muscriptor-0.3.0/web/src/analytics.ts +91 -0
  44. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/audio.ts +29 -1
  45. muscriptor-0.3.0/web/src/components/Button.tsx +43 -0
  46. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/components/ConditioningPanel.tsx +14 -8
  47. muscriptor-0.3.0/web/src/components/ConsentBanner.tsx +49 -0
  48. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/components/Controls.tsx +5 -4
  49. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/components/DropOverlay.tsx +2 -9
  50. muscriptor-0.3.0/web/src/components/Faq.tsx +143 -0
  51. muscriptor-0.3.0/web/src/components/FeedbackLine.tsx +68 -0
  52. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/components/Footer.tsx +27 -18
  53. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/components/InstrumentList.tsx +17 -13
  54. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/components/OutputBar.tsx +24 -13
  55. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/components/PianoRollCanvas.tsx +1 -1
  56. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/components/WelcomeScreen.tsx +71 -30
  57. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/hooks/useTranscription.ts +78 -12
  58. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/main.tsx +3 -0
  59. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/pianoroll.ts +309 -44
  60. muscriptor-0.3.0/web/src/sse.ts +138 -0
  61. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/style.css +29 -0
  62. muscriptor-0.3.0/web/src/vite-env.d.ts +10 -0
  63. muscriptor-0.2.1/.python-version +0 -1
  64. muscriptor-0.2.1/muscriptor/events.py +0 -208
  65. muscriptor-0.2.1/muscriptor/server.py +0 -279
  66. muscriptor-0.2.1/muscriptor/utils/midi.py +0 -25
  67. muscriptor-0.2.1/muscriptor/web_dist/assets/index-BZbHOTyZ.js +0 -223
  68. muscriptor-0.2.1/muscriptor/web_dist/assets/index-b8IJOPe2.css +0 -2
  69. muscriptor-0.2.1/tests/test_midi.py +0 -63
  70. muscriptor-0.2.1/tests/test_server.py +0 -220
  71. muscriptor-0.2.1/web/src/sse.ts +0 -68
  72. muscriptor-0.2.1/web/src/vite-env.d.ts +0 -1
  73. {muscriptor-0.2.1 → muscriptor-0.3.0}/.github/workflows/pypi.yml +0 -0
  74. {muscriptor-0.2.1 → muscriptor-0.3.0}/.pre-commit-config.yaml +0 -0
  75. {muscriptor-0.2.1 → muscriptor-0.3.0}/LICENSE +0 -0
  76. {muscriptor-0.2.1 → muscriptor-0.3.0}/deploy.sh +0 -0
  77. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/__init__.py +0 -0
  78. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/__main__.py +0 -0
  79. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/models/__init__.py +0 -0
  80. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/modules/__init__.py +0 -0
  81. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/modules/mel_spectrogram.py +0 -0
  82. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/modules/streaming.py +0 -0
  83. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/soundfonts.py +0 -0
  84. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/tokenizer/__init__.py +0 -0
  85. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/utils/__init__.py +0 -0
  86. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/utils/audio.py +0 -0
  87. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/utils/resample.py +0 -0
  88. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/utils/sampling.py +0 -0
  89. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/web_dist/assets/spessasynth_processor.min-tdOdhV7p.js +0 -0
  90. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/web_dist/favicon.png +0 -0
  91. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/web_dist/kyutai-logo.svg +0 -0
  92. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/web_dist/mirelo-logo.svg +0 -0
  93. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/web_dist/muscriptor-logo-v2.png +0 -0
  94. /muscriptor-0.2.1/muscriptor/web_dist/muscriptor-logo-v4-pink.svg → /muscriptor-0.3.0/muscriptor/web_dist/muscriptor-logo-v4.svg +0 -0
  95. {muscriptor-0.2.1 → muscriptor-0.3.0}/muscriptor/web_dist/muscriptor-wave.svg +0 -0
  96. {muscriptor-0.2.1 → muscriptor-0.3.0}/tests/__init__.py +0 -0
  97. {muscriptor-0.2.1 → muscriptor-0.3.0}/tests/conftest.py +0 -0
  98. {muscriptor-0.2.1 → muscriptor-0.3.0}/tests/encode_helpers.py +0 -0
  99. {muscriptor-0.2.1 → muscriptor-0.3.0}/tests/test_conditioners.py +0 -0
  100. {muscriptor-0.2.1 → muscriptor-0.3.0}/tests/test_events.py +0 -0
  101. {muscriptor-0.2.1 → muscriptor-0.3.0}/tests/test_integration.py +0 -0
  102. {muscriptor-0.2.1 → muscriptor-0.3.0}/tests/test_notes.py +0 -0
  103. {muscriptor-0.2.1 → muscriptor-0.3.0}/tests/test_sampling.py +0 -0
  104. {muscriptor-0.2.1 → muscriptor-0.3.0}/tests/test_transformer.py +0 -0
  105. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/logo_muscriptor_final.png +0 -0
  106. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/package-lock.json +0 -0
  107. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/package.json +0 -0
  108. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/pnpm-lock.yaml +0 -0
  109. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/public/favicon.png +0 -0
  110. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/public/kyutai-logo.svg +0 -0
  111. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/public/mirelo-logo.svg +0 -0
  112. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/public/muscriptor-logo-v2.png +0 -0
  113. /muscriptor-0.2.1/web/public/muscriptor-logo-v4-pink.svg → /muscriptor-0.3.0/web/public/muscriptor-logo-v4.svg +0 -0
  114. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/public/muscriptor-wave.svg +0 -0
  115. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/components/icons.tsx +0 -0
  116. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/hooks/useAudioEngine.ts +0 -0
  117. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/instruments.ts +0 -0
  118. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/src/progress.ts +0 -0
  119. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/tsconfig.json +0 -0
  120. {muscriptor-0.2.1 → muscriptor-0.3.0}/web/vite.config.ts +0 -0
@@ -3,6 +3,7 @@
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
@@ -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
@@ -4,6 +4,7 @@
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
@@ -13,3 +14,5 @@ web/tsconfig.tsbuildinfo
13
14
  *.egg-info
14
15
  __pycache__
15
16
  .DS_Store
17
+ .claude/settings.local.json
18
+ .claude/worktrees/
@@ -0,0 +1 @@
1
+ 3.13
@@ -9,6 +9,10 @@ COPY web/package.json web/pnpm-lock.yaml ./
9
9
  RUN pnpm install --frozen-lockfile
10
10
 
11
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
12
16
  # vite outDir is ../muscriptor/web_dist, i.e. /muscriptor/web_dist in this stage.
13
17
  RUN pnpm run build
14
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: muscriptor
3
- Version: 0.2.1
3
+ Version: 0.3.0
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
@@ -18,16 +18,20 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
20
20
  Requires-Python: >=3.10
21
+ Requires-Dist: beat-this>=1.1
21
22
  Requires-Dist: einops>=0.4
22
23
  Requires-Dist: fastapi>=0.136.3
23
24
  Requires-Dist: httpx>=0.28.1
24
25
  Requires-Dist: huggingface-hub>=0.13
25
26
  Requires-Dist: mido>=1.3
26
- Requires-Dist: numpy>=1.24
27
+ Requires-Dist: numpy<2,>=1.24; sys_platform == 'darwin' and platform_machine == 'x86_64'
28
+ Requires-Dist: numpy>=1.24; sys_platform != 'darwin' or platform_machine != 'x86_64'
29
+ Requires-Dist: packaging>=20.0
27
30
  Requires-Dist: python-multipart>=0.0.29
28
31
  Requires-Dist: safetensors>=0.4
29
32
  Requires-Dist: soundfile>=0.14.0
30
- Requires-Dist: torch>=2.0
33
+ Requires-Dist: torch<2.3,>=2.0; sys_platform == 'darwin' and platform_machine == 'x86_64'
34
+ Requires-Dist: torch>=2.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
31
35
  Requires-Dist: typer>=0.10
32
36
  Requires-Dist: uvicorn[standard]>=0.48.0
33
37
  Description-Content-Type: text/markdown
@@ -50,6 +54,30 @@ MuScriptor is the first music transcription model that has been trained on a lar
50
54
  </p>
51
55
  -->
52
56
 
57
+ ## HuggingFace login (required)
58
+
59
+ The model weights are hosted on [HuggingFace](https://huggingface.co/MuScriptor)
60
+ and gated behind their CC BY-NC 4.0 license, so downloading them — including
61
+ on the first `uvx muscriptor` run — requires a (free) HuggingFace account:
62
+
63
+ 1. Accept the model license on the model page, e.g.
64
+ [muscriptor-medium](https://huggingface.co/MuScriptor/muscriptor-medium)
65
+ (access is granted automatically).
66
+ 2. Authenticate on your machine:
67
+
68
+ ```bash
69
+ uvx hf auth login
70
+ ```
71
+
72
+ or set a token (create one at
73
+ [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)):
74
+
75
+ ```bash
76
+ export HF_TOKEN=hf_...
77
+ ```
78
+
79
+ The weights are then downloaded on first use and cached locally.
80
+
53
81
  ## Try it locally
54
82
 
55
83
  You can try it locally with the web UI with:
@@ -58,12 +86,29 @@ You can try it locally with the web UI with:
58
86
  uvx muscriptor serve
59
87
  ```
60
88
 
89
+ On windows, the default PyTorch backend is `cpu`, so to use the GPU on Windows, you'll need to run:
90
+
91
+ ```bash
92
+ uvx --torch-backend=cu128 muscriptor serve
93
+ ```
94
+ Use the `--torch-backend=cu128` flag every time you run a `uvx` command on Windows.
95
+
61
96
  or with the CLI:
62
97
 
63
98
  ```bash
64
99
  uvx muscriptor transcribe
65
100
  ```
66
101
 
102
+ > **Intel Macs:** PyTorch stopped shipping Intel-mac (x86_64) wheels after
103
+ > torch 2.2.2, which supports Python ≤ 3.12, so you must tell uvx to use
104
+ > Python 3.12:
105
+ >
106
+ > ```bash
107
+ > uvx --python 3.12 muscriptor serve
108
+ > ```
109
+ >
110
+ > If you install with pip/uv instead, use Python 3.10–3.12.
111
+
67
112
 
68
113
  ## Installation
69
114
 
@@ -92,7 +137,9 @@ weights are downloaded and cached automatically. The architecture is a transform
92
137
 
93
138
  `small` is the practical choice on CPU-only machines, `medium` is the default
94
139
  speed/accuracy trade-off, and `large` is the most accurate but really wants a
95
- GPU.
140
+ GPU. On Apple Silicon the model runs on Metal (MPS) automatically, in float16
141
+ by default (`--dtype float32` to override) — fast enough that `large`
142
+ transcribes several times faster than real time.
96
143
  ## Usage
97
144
 
98
145
  ```python
@@ -165,6 +212,7 @@ class TranscriptionModel:
165
212
  batch_size: int | None = None,
166
213
  no_eos_is_ok: bool = True,
167
214
  beam_size: int = 1,
215
+ prelude_forcing: bool = True,
168
216
  ) -> Generator[NoteStartEvent | NoteEndEvent | ProgressEvent, None, None]:
169
217
  """Transcribe audio into a stream of note events.
170
218
 
@@ -177,24 +225,39 @@ class TranscriptionModel:
177
225
  temperature: Sampling temperature (only used when use_sampling=True).
178
226
  cfg_coef: Classifier-free guidance coefficient. Keep to 1 for the released models (they are post-RL)
179
227
  instruments: Optional list of instrument group names to
180
- condition the model on (exact names, e.g.
228
+ restrict decoding to (exact names, e.g.
181
229
  ["acoustic_piano", "drums"]). Run `muscriptor
182
230
  list-instruments` (or GET /instruments on the server)
183
- for the full list of valid names.
231
+ for the full list of valid names. When given, this is a
232
+ hard constraint: every program/drum token outside the
233
+ list is masked out during generation, so no unlisted
234
+ instrument can appear in the output. Leave unset to let
235
+ the model decode whatever instruments it detects.
184
236
  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.
237
+ pass. `None` (default) resolves to 1 while prelude forcing
238
+ is on; with `prelude_forcing=False` it picks a value based
239
+ on the device: 1 on CPU, 4 on GPU. Values > 1 trade
240
+ chunk-boundary quality for throughput and therefore require
241
+ `prelude_forcing=False` (ValueError otherwise). Batching
242
+ also delays streaming: several chunks generate together, so
243
+ events belonging to later chunks of a batch won't arrive
244
+ until the whole batch finishes. Within a batch, events are
245
+ always yielded in temporal order; all events from chunk N
246
+ are emitted before any event from chunk N+1.
193
247
  no_eos_is_ok: If True, a chunk that doesn't emit EOS within
194
248
  the generation budget produces a warning instead of raising.
195
249
  beam_size: Beam search width. 1 (default) uses greedy decoding
196
250
  (or sampling, with use_sampling=True); >= 2 enables beam
197
251
  search, which is slower but can be more accurate.
252
+ prelude_forcing: If True (default), every chunk after the first
253
+ has its tie prologue — the tokens declaring which notes are
254
+ sustained from the previous chunk — teacher-forced from the
255
+ previous chunk's actually-unfinished notes, instead of
256
+ letting the model guess (which occasionally makes a chunk
257
+ restart with the wrong instruments). Requires chunks to be
258
+ generated strictly in order, so it only works with
259
+ batch_size=1 (the default); set prelude_forcing=False
260
+ explicitly to use larger batches.
198
261
 
199
262
  Returns:
200
263
  Generator of NoteStartEvent, NoteEndEvent and ProgressEvent
@@ -206,6 +269,12 @@ class TranscriptionModel:
206
269
  plus a tiny duration. Note: this tokenizer does not preserve
207
270
  velocity (loudness) — only onset/offset timing, pitch, and
208
271
  instrument are recovered.
272
+
273
+ Event times may all carry the same small lag (up to ~25 ms).
274
+ Correcting it needs the beat grid and the whole transcription's
275
+ onsets, which are only available once the stream has finished, so
276
+ prefer `transcribe_to_midi` when precise note timing matters — it
277
+ measures the lag and takes it out.
209
278
  """
210
279
 
211
280
  def transcribe_to_midi(
@@ -218,6 +287,7 @@ class TranscriptionModel:
218
287
  batch_size: int | None = None,
219
288
  no_eos_is_ok: bool = True,
220
289
  beam_size: int = 1,
290
+ prelude_forcing: bool = True,
221
291
  ) -> bytes:
222
292
  """Same as `transcribe`, but returns a MIDI file as bytes instead
223
293
  of a generator of events. Useful when you want to save the MIDI
@@ -237,10 +307,11 @@ muscriptor transcribe audio.wav -o out.mid
237
307
  # a local safetensors path, or an hf:// / http(s):// URL
238
308
  muscriptor transcribe audio.wav --model large
239
309
 
240
- # Tell the model which instruments to expect (comma-separated names;
310
+ # Restrict decoding to only the instruments listed (comma-separated names;
241
311
  # run `muscriptor list-instruments` for the full list). Case-insensitive,
242
312
  # and unambiguous abbreviations work: 'timp,dist' = timpani + distorted
243
- # electric guitar
313
+ # electric guitar. Every other instrument's tokens are masked out during
314
+ # generation, so nothing else can appear in the output.
244
315
  muscriptor transcribe audio.wav --instruments acoustic_piano,drums
245
316
 
246
317
  # Get the event stream instead of MIDI: json (single array) or
@@ -252,6 +323,13 @@ muscriptor transcribe audio.wav --format jsonl -o -
252
323
  muscriptor transcribe audio.wav --sampling -t 0.8
253
324
  muscriptor transcribe audio.wav --beam-size 4
254
325
 
326
+ # Notes sustained across a chunk boundary are teacher-forced into the next
327
+ # chunk's prologue by default, so a chunk can't restart them with the wrong
328
+ # instrument. This needs chunks generated in order, so the batch size
329
+ # defaults to 1; batching (faster on GPU, lower quality at chunk boundaries)
330
+ # must be opted into explicitly:
331
+ muscriptor transcribe audio.wav --no-prelude-forcing --batch-size 4
332
+
255
333
  # Render a stereo check-mix of the result (left channel = original audio,
256
334
  # right channel = synthesized MIDI; requires fluidsynth on PATH)
257
335
  muscriptor transcribe audio.wav -o out.mid --auralize check.wav
@@ -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
@@ -172,6 +231,12 @@ class TranscriptionModel:
172
231
  plus a tiny duration. Note: this tokenizer does not preserve
173
232
  velocity (loudness) — only onset/offset timing, pitch, and
174
233
  instrument are recovered.
234
+
235
+ Event times may all carry the same small lag (up to ~25 ms).
236
+ Correcting it needs the beat grid and the whole transcription's
237
+ onsets, which are only available once the stream has finished, so
238
+ prefer `transcribe_to_midi` when precise note timing matters — it
239
+ measures the lag and takes it out.
175
240
  """
176
241
 
177
242
  def transcribe_to_midi(
@@ -184,6 +249,7 @@ class TranscriptionModel:
184
249
  batch_size: int | None = None,
185
250
  no_eos_is_ok: bool = True,
186
251
  beam_size: int = 1,
252
+ prelude_forcing: bool = True,
187
253
  ) -> bytes:
188
254
  """Same as `transcribe`, but returns a MIDI file as bytes instead
189
255
  of a generator of events. Useful when you want to save the MIDI
@@ -203,10 +269,11 @@ muscriptor transcribe audio.wav -o out.mid
203
269
  # a local safetensors path, or an hf:// / http(s):// URL
204
270
  muscriptor transcribe audio.wav --model large
205
271
 
206
- # Tell the model which instruments to expect (comma-separated names;
272
+ # Restrict decoding to only the instruments listed (comma-separated names;
207
273
  # run `muscriptor list-instruments` for the full list). Case-insensitive,
208
274
  # and unambiguous abbreviations work: 'timp,dist' = timpani + distorted
209
- # electric guitar
275
+ # electric guitar. Every other instrument's tokens are masked out during
276
+ # generation, so nothing else can appear in the output.
210
277
  muscriptor transcribe audio.wav --instruments acoustic_piano,drums
211
278
 
212
279
  # Get the event stream instead of MIDI: json (single array) or
@@ -218,6 +285,13 @@ muscriptor transcribe audio.wav --format jsonl -o -
218
285
  muscriptor transcribe audio.wav --sampling -t 0.8
219
286
  muscriptor transcribe audio.wav --beam-size 4
220
287
 
288
+ # Notes sustained across a chunk boundary are teacher-forced into the next
289
+ # chunk's prologue by default, so a chunk can't restart them with the wrong
290
+ # instrument. This needs chunks generated in order, so the batch size
291
+ # defaults to 1; batching (faster on GPU, lower quality at chunk boundaries)
292
+ # must be opted into explicitly:
293
+ muscriptor transcribe audio.wav --no-prelude-forcing --batch-size 4
294
+
221
295
  # Render a stereo check-mix of the result (left channel = original audio,
222
296
  # right channel = synthesized MIDI; requires fluidsynth on PATH)
223
297
  muscriptor transcribe audio.wav -o out.mid --auralize check.wav
@@ -0,0 +1,69 @@
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() still tries to init CUDA even on CPU-only systems
57
+ # Only call it if we actually have a non-CPU accelerator
58
+ try:
59
+ current_device = torch.accelerator.current_device_index()
60
+ if current_device >= 0:
61
+ torch.accelerator.synchronize()
62
+ except RuntimeError:
63
+ # No accelerator available (CUDA not found, etc.)
64
+ pass
65
+ return
66
+ if torch.cuda.is_available():
67
+ torch.cuda.synchronize()
68
+ elif _mps_available():
69
+ torch.mps.synchronize()