techfreakworm commited on
Commit
a5459fd
·
unverified ·
1 Parent(s): a30ec7c

fix(deploy): align m7 with claude.md — models/<org>/<repo> layout + gpu estimator

Browse files
Files changed (1) hide show
  1. app.py +109 -45
app.py CHANGED
@@ -29,9 +29,11 @@ DO NOT switch this back to ``gr.Tabs`` — that produces top-positioned
29
  horizontal tabs which contradicts the wireframes.
30
 
31
  On HF Spaces (``SPACE_ID`` env present), ``_bootstrap_spaces_cache()``
32
- runs once on import to symlink HF hub cache snapshots into the
33
- acestep-apple-silicon fork's ``<site-packages>/checkpoints/`` layout.
34
- On Mac/Linux locally, it's a no-op.
 
 
35
  """
36
 
37
  from __future__ import annotations
@@ -46,6 +48,8 @@ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
46
 
47
  import hashlib
48
  import random
 
 
49
  from pathlib import Path
50
 
51
  import gradio as gr
@@ -68,64 +72,124 @@ def get_backend() -> be.ACEStepStudioBackend:
68
  return _BACKEND
69
 
70
 
71
- def _bootstrap_spaces_cache() -> None:
72
- """On HF Spaces, mirror the HF hub cache into the site-packages checkpoints/ dir.
 
 
 
 
 
 
73
 
74
- The acestep-apple-silicon fork resolves checkpoints relative to its own install
75
- location (``.venv/.../site-packages/checkpoints/``). HF Spaces puts model weights
76
- in the HF hub cache. This bootstrap snapshot-downloads the two required repos
77
- into the cache (using preload mirror if available) and then symlinks each
78
- snapshot child into ``checkpoints/`` so the fork's resolver finds them.
79
 
80
- Local Mac/CUDA: no-op (guarded by ``SPACE_ID`` env var).
 
 
 
 
 
 
81
  """
82
- if not os.getenv("SPACE_ID"):
 
 
 
 
 
 
83
  return
 
 
84
 
85
- import site
86
 
 
 
87
  from huggingface_hub import snapshot_download
88
 
89
- site_pkgs = site.getsitepackages()[0]
90
- target_dir = Path(site_pkgs) / "checkpoints"
 
 
 
 
 
 
 
 
91
 
92
- if target_dir.exists():
93
- return # already bootstrapped
94
 
95
- hf_home = os.getenv("HF_HOME", "/home/user/.cache/huggingface")
 
96
 
97
- # Download Ace-Step1.5 umbrella (vae + encoder).
98
- umbrella_path = snapshot_download(
99
- repo_id="ACE-Step/Ace-Step1.5",
100
- cache_dir=hf_home,
101
- )
 
 
 
102
 
103
- # Download the XL SFT diffusion variant.
104
- xl_sft_path = snapshot_download(
105
- repo_id="ACE-Step/acestep-v15-xl-sft",
106
- cache_dir=hf_home,
107
- )
108
 
109
- # Merge both snapshots into ``checkpoints/`` via symlinks.
110
- target_dir.mkdir(parents=True, exist_ok=True)
111
- for src_path in [umbrella_path, xl_sft_path]:
112
- for child in Path(src_path).iterdir():
113
- link = target_dir / child.name
114
- if not link.exists():
115
- link.symlink_to(child)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
 
118
- def _maybe_spaces_gpu():
119
- """Return ``@spaces.GPU(duration=180)`` on HF Spaces, otherwise a no-op decorator.
120
 
121
- The decorator MUST be applied at module load time ZeroGPU's startup
122
- analyzer doesn't see runtime decoration. Local dev is a transparent pass-through.
123
  """
124
  if os.getenv("SPACE_ID"):
125
  try:
126
  import spaces
127
 
128
- return spaces.GPU(duration=180)
129
  except ImportError:
130
  pass
131
 
@@ -263,7 +327,7 @@ def on_lora_strength_change(state, strength: float):
263
  return new_state, _active_md(new_state["name"], float(strength), kind)
264
 
265
 
266
- @_maybe_spaces_gpu()
267
  def on_generate_click(
268
  prompt: str,
269
  lyrics: str,
@@ -292,7 +356,7 @@ def on_generate_click(
292
  return out_path, meta, new_history
293
 
294
 
295
- @_maybe_spaces_gpu()
296
  def on_cover_click(
297
  ref_audio,
298
  prompt: str,
@@ -324,7 +388,7 @@ def on_cover_click(
324
  return out_path, meta, new_history
325
 
326
 
327
- @_maybe_spaces_gpu()
328
  def on_extend_click(
329
  seed_audio,
330
  extra_prompt: str,
@@ -364,7 +428,7 @@ def on_extend_click(
364
  return out_path, meta, new_history
365
 
366
 
367
- @_maybe_spaces_gpu()
368
  def on_draft_lyrics(
369
  brief: str,
370
  structure: str,
@@ -442,7 +506,7 @@ def on_export_mp3(audio_path):
442
  return gr.File(value=str(out), visible=True)
443
 
444
 
445
- @_maybe_spaces_gpu()
446
  def on_edit_click(
447
  source_audio,
448
  sub_mode: str,
 
29
  horizontal tabs which contradicts the wireframes.
30
 
31
  On HF Spaces (``SPACE_ID`` env present), ``_bootstrap_spaces_cache()``
32
+ runs once on import to (a) hardlink-mirror the build-user-owned HF hub
33
+ cache into a runtime-writable ``~/hf-cache-rw/`` and (b) symlink the
34
+ preloaded snapshots into ``./models/<org>/<repo>/`` so ACE-Step's
35
+ checkpoint resolver finds them. On Mac/Linux locally, it's a no-op —
36
+ local dev uses ``setup.sh``'s site-packages symlink instead.
37
  """
38
 
39
  from __future__ import annotations
 
48
 
49
  import hashlib
50
  import random
51
+ import shutil # noqa: F401 (reserved for future cleanup paths)
52
+ import subprocess
53
  from pathlib import Path
54
 
55
  import gradio as gr
 
72
  return _BACKEND
73
 
74
 
75
+ _PRELOAD_REPOS = (
76
+ "ACE-Step/Ace-Step1.5",
77
+ "ACE-Step/acestep-v15-xl-sft",
78
+ )
79
+
80
+
81
+ def _hf_cache_rw_dir() -> Path:
82
+ return Path.home() / "hf-cache-rw"
83
 
 
 
 
 
 
84
 
85
+ def _mirror_hf_cache() -> None:
86
+ """Hardlink-mirror the build-user HF hub cache to a runtime-writable location.
87
+
88
+ HF Spaces ships the preloaded weights under ~/.cache/huggingface/hub owned by
89
+ the build user (read-only at runtime). Hardlink them to ~/hf-cache-rw so the
90
+ runtime user can write new files alongside the preloaded snapshots without
91
+ paying the storage cost twice.
92
  """
93
+ src = Path.home() / ".cache" / "huggingface"
94
+ dst = _hf_cache_rw_dir()
95
+ if dst.exists():
96
+ return
97
+ if not src.exists():
98
+ # Nothing preloaded yet — create the empty target so HF_HOME points somewhere valid.
99
+ dst.mkdir(parents=True, exist_ok=True)
100
  return
101
+ # `cp -al` = archive + hardlinks → fast, no duplicate bytes.
102
+ subprocess.run(["cp", "-al", str(src), str(dst)], check=True)
103
 
 
104
 
105
+ def _symlink_snapshots_into_models() -> None:
106
+ """Create ./models/<org>/<repo>/ → latest snapshot dir for each preloaded repo."""
107
  from huggingface_hub import snapshot_download
108
 
109
+ project_models = Path("./models").resolve()
110
+ for repo_id in _PRELOAD_REPOS:
111
+ # snapshot_download is a no-op when the files are already cached. It returns
112
+ # the resolved snapshot dir on disk.
113
+ snap = Path(snapshot_download(repo_id=repo_id, cache_dir=os.environ.get("HF_HOME")))
114
+ target = project_models / repo_id # e.g. ./models/ACE-Step/Ace-Step1.5
115
+ target.parent.mkdir(parents=True, exist_ok=True)
116
+ if target.exists() or target.is_symlink():
117
+ continue
118
+ target.symlink_to(snap)
119
 
 
 
120
 
121
+ def _bootstrap_spaces_cache() -> None:
122
+ """On HF Spaces, prepare ./models/<org>/<repo>/ so ACE-Step finds preloaded weights.
123
 
124
+ Skipped locally local dev uses setup.sh's site-packages symlink instead, since
125
+ the apple-silicon fork hardcodes its checkpoint resolver to its own install dir.
126
+ """
127
+ if not os.getenv("SPACE_ID"):
128
+ return
129
+ _mirror_hf_cache()
130
+ os.environ["HF_HOME"] = str(_hf_cache_rw_dir())
131
+ _symlink_snapshots_into_models()
132
 
 
 
 
 
 
133
 
134
+ _GPU_BASE_BY_MODE = {
135
+ "generate": 30,
136
+ "cover": 40,
137
+ "extend": 30,
138
+ "edit": 30,
139
+ "lyrics": 15, # CPU-only typically — lyrics LM runs short on GPU too
140
+ }
141
+ _GPU_CLAMP_MIN = 60
142
+ _GPU_CLAMP_MAX = 300
143
+
144
+
145
+ def _estimate_gpu_duration(mode: str, params: dict, multiplier: float = 1.0) -> int:
146
+ """Estimate per-call GPU duration in seconds.
147
+
148
+ Inputs:
149
+ mode: one of generate/cover/extend/edit/lyrics
150
+ params: dict that may contain "duration_s" — the requested audio length
151
+ multiplier: safety factor (1.0 = nominal, 1.5 = pessimistic)
152
+
153
+ Returns int seconds, clamped to [60, 300].
154
+ """
155
+ base = _GPU_BASE_BY_MODE.get(mode, 30)
156
+ duration_s = float(params.get("duration_s") or 30)
157
+ # Roughly 2x realtime on a ZeroGPU L4 — generation > playback length.
158
+ estimated = base + duration_s * 2.0 * float(multiplier)
159
+ return max(_GPU_CLAMP_MIN, min(_GPU_CLAMP_MAX, int(estimated)))
160
+
161
+
162
+ def _gpu_call_to_estimator(mode: str, *, duration_arg_index: int = 2):
163
+ """Bridge spaces.GPU's per-call (*args, **kwargs) → our (mode, params, multiplier) estimator.
164
+
165
+ spaces.GPU(duration=callable) invokes the callable with the handler's actual
166
+ runtime args. The handlers here have signature roughly:
167
+ on_<mode>_click(prompt_or_seed, lyrics_or_other, duration_s, ...)
168
+ so duration_s is at position 2 by default. The kwargs path also works.
169
+ """
170
+
171
+ def from_call(*args, **kwargs):
172
+ duration_s = kwargs.get("duration_s")
173
+ if duration_s is None and len(args) > duration_arg_index:
174
+ candidate = args[duration_arg_index]
175
+ if isinstance(candidate, (int, float)):
176
+ duration_s = candidate
177
+ return _estimate_gpu_duration(mode, {"duration_s": duration_s})
178
+
179
+ return from_call
180
 
181
 
182
+ def _maybe_spaces_gpu(mode: str):
183
+ """Return ``@spaces.GPU(duration=<callable>)`` on HF Spaces, otherwise a no-op decorator.
184
 
185
+ The callable estimator gives long extends/edits the time they need (up to 300s)
186
+ while keeping short clips fast (60s floor). Off-Spaces this returns identity.
187
  """
188
  if os.getenv("SPACE_ID"):
189
  try:
190
  import spaces
191
 
192
+ return spaces.GPU(duration=_gpu_call_to_estimator(mode))
193
  except ImportError:
194
  pass
195
 
 
327
  return new_state, _active_md(new_state["name"], float(strength), kind)
328
 
329
 
330
+ @_maybe_spaces_gpu("generate")
331
  def on_generate_click(
332
  prompt: str,
333
  lyrics: str,
 
356
  return out_path, meta, new_history
357
 
358
 
359
+ @_maybe_spaces_gpu("cover")
360
  def on_cover_click(
361
  ref_audio,
362
  prompt: str,
 
388
  return out_path, meta, new_history
389
 
390
 
391
+ @_maybe_spaces_gpu("extend")
392
  def on_extend_click(
393
  seed_audio,
394
  extra_prompt: str,
 
428
  return out_path, meta, new_history
429
 
430
 
431
+ @_maybe_spaces_gpu("lyrics")
432
  def on_draft_lyrics(
433
  brief: str,
434
  structure: str,
 
506
  return gr.File(value=str(out), visible=True)
507
 
508
 
509
+ @_maybe_spaces_gpu("edit")
510
  def on_edit_click(
511
  source_audio,
512
  sub_mode: str,