harp-dev commited on
Commit
d8a8deb
·
verified ·
1 Parent(s): 4e6714e

Pin muscriptor 0.3.0, whose 0.4.0 renamed transcribe_to_midi, fetch the weights at startup, run every size on the GPU, and check requests before the GPU is reserved.

Browse files
Files changed (2) hide show
  1. app.py +70 -7
  2. requirements.txt +2 -1
app.py CHANGED
@@ -17,11 +17,14 @@ except ImportError:
17
  func = args[0]
18
  return func
19
 
 
20
  import tempfile
21
  from pathlib import Path
22
 
23
  import gradio as gr
 
24
  import torch
 
25
  from pyharp import ModelCard, build_endpoint
26
  from pyharp.tags import Subcategory
27
 
@@ -43,18 +46,46 @@ model_card = ModelCard(
43
 
44
  _VALID_INSTRUMENTS = ", ".join(MT3_FULL_PLUS_GROUP_NAMES)
45
 
 
 
 
 
 
 
 
 
 
46
  _models: dict[str, TranscriptionModel] = {}
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  def _get_model(variant: str) -> TranscriptionModel:
50
- """Load and cache a TranscriptionModel for a given size, one per variant.
51
 
52
- small/medium run on CPU; large runs on GPU. Built directly on its target
53
- device inside this GPU-decorated call, per ZeroGPU rules.
54
  """
55
  if variant not in _models:
56
- device = "cuda" if variant == "large" else "cpu"
57
- _models[variant] = TranscriptionModel.load_model(variant, device=device)
58
  return _models[variant]
59
 
60
 
@@ -88,8 +119,16 @@ def _resolve_instruments(text: str) -> tuple[list[str] | None, str]:
88
  return (resolved or None), note
89
 
90
 
91
- @spaces.GPU
92
- @torch.inference_mode()
 
 
 
 
 
 
 
 
93
  def process_fn(
94
  input_audio_path: str,
95
  variant: str,
@@ -98,6 +137,30 @@ def process_fn(
98
  temperature: float,
99
  ) -> tuple[str, str]:
100
  """Transcribe the input audio to MIDI, plus a note on the Instruments field."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  model = _get_model(variant)
102
  instruments, instrument_note = _resolve_instruments(instruments_text)
103
 
 
17
  func = args[0]
18
  return func
19
 
20
+ import contextlib
21
  import tempfile
22
  from pathlib import Path
23
 
24
  import gradio as gr
25
+ import soundfile as sf
26
  import torch
27
+ from huggingface_hub import hf_hub_download, snapshot_download
28
  from pyharp import ModelCard, build_endpoint
29
  from pyharp.tags import Subcategory
30
 
 
46
 
47
  _VALID_INSTRUMENTS = ", ".join(MT3_FULL_PLUS_GROUP_NAMES)
48
 
49
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
50
+
51
+ # Each size's weights, pinned, with the config.json that load_model reads beside them
52
+ _WEIGHT_REVISIONS = {
53
+ "small": "8c127f603b807520fa465c838e9bfee8a91ada4e",
54
+ "medium": "f32236969308476e01fd3aae67357de5feb05a2d",
55
+ "large": "8809fdfbed2affa7ade94a7059e746e3880720e7",
56
+ }
57
+ _weights: dict[str, Path] = {}
58
  _models: dict[str, TranscriptionModel] = {}
59
 
60
+ # Fetched in the worker, which runs process_fn and starts as the server comes up, so that no
61
+ # request downloads inside its GPU reservation. The server never runs process_fn.
62
+ if __name__ != "__main__":
63
+ for _size, _revision in _WEIGHT_REVISIONS.items():
64
+ _weights[_size] = Path(snapshot_download(
65
+ f"MuScriptor/muscriptor-{_size}", revision=_revision
66
+ )) / "model.safetensors"
67
+
68
+ # Tempo detection runs beat_this, which downloads its checkpoint from the JKU cloud on first use,
69
+ # and that has failed both inside a GPU call (no name resolution) and at startup (503). The same
70
+ # file is mirrored on the Hub and linked where torch.hub looks before downloading.
71
+ _beat_checkpoint = Path(torch.hub.get_dir()) / "checkpoints" / "beat_this-final0.ckpt"
72
+ if not _beat_checkpoint.exists():
73
+ _beat_checkpoint.parent.mkdir(parents=True, exist_ok=True)
74
+ with contextlib.suppress(FileExistsError):
75
+ _beat_checkpoint.symlink_to(hf_hub_download(
76
+ "teamup-tech/Beat-This-weights", "final0.ckpt",
77
+ revision="4afbe376535aaab71b76ae2ca2d7c16b08877321",
78
+ ))
79
+
80
 
81
  def _get_model(variant: str) -> TranscriptionModel:
82
+ """Builds the TranscriptionModel of one size, on the GPU the call holds.
83
 
84
+ Built inside the call, so ZeroGPU builds it again on every call, which takes seconds from local
85
+ weights. Holding all three sizes in the worker instead would keep about 7 GB there.
86
  """
87
  if variant not in _models:
88
+ _models[variant] = TranscriptionModel.load_model(_weights[variant], device=DEVICE)
 
89
  return _models[variant]
90
 
91
 
 
119
  return (resolved or None), note
120
 
121
 
122
+ # The audio is transcribed in five-second segments, so the cost grows with its length, and with the
123
+ # size of the model
124
+ MAX_INPUT_SECONDS = 600
125
+ SECONDS_PER_INPUT_SECOND = {"small": 0.5, "medium": 1.0, "large": 2.0}
126
+
127
+
128
+ def gpu_seconds(input_audio_path, variant, instruments_text, use_sampling, temperature):
129
+ return 20 + SECONDS_PER_INPUT_SECOND[variant] * sf.info(input_audio_path).duration
130
+
131
+
132
  def process_fn(
133
  input_audio_path: str,
134
  variant: str,
 
137
  temperature: float,
138
  ) -> tuple[str, str]:
139
  """Transcribe the input audio to MIDI, plus a note on the Instruments field."""
140
+ # Checked before the GPU is reserved, so a request the model cannot take spends none of the
141
+ # caller's ZeroGPU allowance
142
+ if not input_audio_path:
143
+ raise gr.Error("Provide the music to transcribe.")
144
+ try:
145
+ duration = sf.info(input_audio_path).duration
146
+ except Exception as exc:
147
+ raise gr.Error(f"Could not read the audio file: {exc}") from exc
148
+ if duration > MAX_INPUT_SECONDS:
149
+ raise gr.Error(
150
+ f"The music is {duration:.1f} s long. Trim it to {MAX_INPUT_SECONDS // 60} minutes "
151
+ "or less."
152
+ )
153
+ if gpu_seconds(input_audio_path, variant, instruments_text, use_sampling, temperature) > 200:
154
+ raise gr.Error(
155
+ f"The {variant} model would need more GPU time for {duration:.0f} s of music than a "
156
+ "request can reserve. Choose a smaller model or trim the music."
157
+ )
158
+ return transcribe(input_audio_path, variant, instruments_text, use_sampling, temperature)
159
+
160
+
161
+ @spaces.GPU(duration=gpu_seconds)
162
+ @torch.inference_mode()
163
+ def transcribe(input_audio_path, variant, instruments_text, use_sampling, temperature):
164
  model = _get_model(variant)
165
  instruments, instrument_note = _resolve_instruments(instruments_text)
166
 
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
  git+https://github.com/TEAMuP-dev/pyharp.git@v0.4.2
2
- muscriptor
 
3
  spaces>=0.51.1
 
1
  git+https://github.com/TEAMuP-dev/pyharp.git@v0.4.2
2
+ # 0.4.0 renamed transcribe_to_midi, which this app calls
3
+ muscriptor==0.3.0
4
  spaces>=0.51.1