muscriptor 0.2.1__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.1 → muscriptor-0.2.2}/.dockerignore +1 -0
  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.1 → muscriptor-0.2.2}/.gitignore +1 -0
  5. muscriptor-0.2.2/.python-version +1 -0
  6. {muscriptor-0.2.1 → muscriptor-0.2.2}/Dockerfile +4 -0
  7. {muscriptor-0.2.1 → muscriptor-0.2.2}/PKG-INFO +87 -16
  8. {muscriptor-0.2.1 → muscriptor-0.2.2}/README.md +81 -13
  9. muscriptor-0.2.2/muscriptor/accelerator.py +61 -0
  10. muscriptor-0.2.2/muscriptor/events.py +294 -0
  11. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/main.py +70 -14
  12. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/models/lm.py +29 -11
  13. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/modules/conditioners.py +3 -4
  14. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/modules/transformer.py +31 -16
  15. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/server.py +88 -0
  16. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/tokenizer/mt3.py +61 -0
  17. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/tokenizer/notes.py +47 -36
  18. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/transcription_model.py +129 -16
  19. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/utils/download.py +36 -2
  20. muscriptor-0.2.2/muscriptor/utils/midi.py +41 -0
  21. muscriptor-0.2.1/muscriptor/web_dist/assets/index-BZbHOTyZ.js → muscriptor-0.2.2/muscriptor/web_dist/assets/index-Cc_b-ZGX.js +4 -4
  22. muscriptor-0.2.2/muscriptor/web_dist/assets/index-Dsp-ORJh.css +2 -0
  23. muscriptor-0.2.2/muscriptor/web_dist/headache_by_lost_deposit_1min.mp3 +0 -0
  24. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/web_dist/index.html +3 -3
  25. {muscriptor-0.2.1 → muscriptor-0.2.2}/pyproject.toml +10 -3
  26. {muscriptor-0.2.1 → muscriptor-0.2.2}/swarm.yml +5 -1
  27. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/test_cli.py +64 -1
  28. muscriptor-0.2.2/tests/test_download.py +26 -0
  29. muscriptor-0.2.2/tests/test_prelude_forcing.py +427 -0
  30. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/test_server.py +83 -0
  31. muscriptor-0.2.2/tests/test_strict_instruments.py +190 -0
  32. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/test_transcription_model.py +3 -0
  33. {muscriptor-0.2.1 → muscriptor-0.2.2}/uv.lock +291 -111
  34. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/index.html +1 -1
  35. muscriptor-0.2.2/web/public/headache_by_lost_deposit_1min.mp3 +0 -0
  36. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/App.tsx +14 -6
  37. muscriptor-0.2.2/web/src/analytics.ts +91 -0
  38. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/audio.ts +3 -1
  39. muscriptor-0.2.2/web/src/components/Button.tsx +43 -0
  40. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/components/ConditioningPanel.tsx +14 -8
  41. muscriptor-0.2.2/web/src/components/ConsentBanner.tsx +49 -0
  42. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/components/Controls.tsx +5 -4
  43. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/components/DropOverlay.tsx +1 -1
  44. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/components/InstrumentList.tsx +16 -12
  45. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/components/OutputBar.tsx +24 -13
  46. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/components/PianoRollCanvas.tsx +1 -1
  47. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/components/WelcomeScreen.tsx +18 -15
  48. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/hooks/useTranscription.ts +19 -0
  49. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/main.tsx +3 -0
  50. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/pianoroll.ts +36 -7
  51. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/style.css +22 -0
  52. muscriptor-0.2.2/web/src/vite-env.d.ts +10 -0
  53. muscriptor-0.2.1/.python-version +0 -1
  54. muscriptor-0.2.1/muscriptor/events.py +0 -208
  55. muscriptor-0.2.1/muscriptor/utils/midi.py +0 -25
  56. muscriptor-0.2.1/muscriptor/web_dist/assets/index-b8IJOPe2.css +0 -2
  57. muscriptor-0.2.1/web/src/vite-env.d.ts +0 -1
  58. {muscriptor-0.2.1 → muscriptor-0.2.2}/.github/workflows/pypi.yml +0 -0
  59. {muscriptor-0.2.1 → muscriptor-0.2.2}/.pre-commit-config.yaml +0 -0
  60. {muscriptor-0.2.1 → muscriptor-0.2.2}/LICENSE +0 -0
  61. {muscriptor-0.2.1 → muscriptor-0.2.2}/deploy.sh +0 -0
  62. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/__init__.py +0 -0
  63. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/__main__.py +0 -0
  64. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/models/__init__.py +0 -0
  65. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/modules/__init__.py +0 -0
  66. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/modules/mel_spectrogram.py +0 -0
  67. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/modules/streaming.py +0 -0
  68. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/soundfonts.py +0 -0
  69. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/tokenizer/__init__.py +0 -0
  70. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/utils/__init__.py +0 -0
  71. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/utils/audio.py +0 -0
  72. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/utils/auralization.py +0 -0
  73. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/utils/resample.py +0 -0
  74. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/utils/sampling.py +0 -0
  75. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/web_dist/assets/spessasynth_processor.min-tdOdhV7p.js +0 -0
  76. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/web_dist/favicon.png +0 -0
  77. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/web_dist/kyutai-logo.svg +0 -0
  78. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/web_dist/mirelo-logo.svg +0 -0
  79. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/web_dist/muscriptor-logo-v2.png +0 -0
  80. /muscriptor-0.2.1/muscriptor/web_dist/muscriptor-logo-v4-pink.svg → /muscriptor-0.2.2/muscriptor/web_dist/muscriptor-logo-v4.svg +0 -0
  81. /muscriptor-0.2.1/muscriptor/web_dist/muscriptor-mark-v4-pink.svg → /muscriptor-0.2.2/muscriptor/web_dist/muscriptor-mark-v4.svg +0 -0
  82. {muscriptor-0.2.1 → muscriptor-0.2.2}/muscriptor/web_dist/muscriptor-wave.svg +0 -0
  83. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/__init__.py +0 -0
  84. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/conftest.py +0 -0
  85. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/encode_helpers.py +0 -0
  86. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/test_conditioners.py +0 -0
  87. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/test_events.py +0 -0
  88. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/test_integration.py +0 -0
  89. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/test_midi.py +0 -0
  90. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/test_notes.py +0 -0
  91. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/test_sampling.py +0 -0
  92. {muscriptor-0.2.1 → muscriptor-0.2.2}/tests/test_transformer.py +0 -0
  93. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/logo_muscriptor_final.png +0 -0
  94. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/package-lock.json +0 -0
  95. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/package.json +0 -0
  96. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/pnpm-lock.yaml +0 -0
  97. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/public/favicon.png +0 -0
  98. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/public/kyutai-logo.svg +0 -0
  99. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/public/mirelo-logo.svg +0 -0
  100. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/public/muscriptor-logo-v2.png +0 -0
  101. /muscriptor-0.2.1/web/public/muscriptor-logo-v4-pink.svg → /muscriptor-0.2.2/web/public/muscriptor-logo-v4.svg +0 -0
  102. /muscriptor-0.2.1/web/public/muscriptor-mark-v4-pink.svg → /muscriptor-0.2.2/web/public/muscriptor-mark-v4.svg +0 -0
  103. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/public/muscriptor-wave.svg +0 -0
  104. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/components/Footer.tsx +0 -0
  105. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/components/icons.tsx +0 -0
  106. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/hooks/useAudioEngine.ts +0 -0
  107. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/instruments.ts +0 -0
  108. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/progress.ts +0 -0
  109. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/src/sse.ts +0 -0
  110. {muscriptor-0.2.1 → muscriptor-0.2.2}/web/tsconfig.json +0 -0
  111. {muscriptor-0.2.1 → muscriptor-0.2.2}/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
@@ -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.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
@@ -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
@@ -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()