multimodalart HF Staff Claude Opus 4.7 (1M context) commited on
Commit
b89b02d
·
1 Parent(s): 3b2141d

Run inline on Space instead of via FastAPI

Browse files

Drop the HTTP client to /generate and load AudioProcessor in-process
so the Space works as a single Gradio app: download checkpoints,
clone the seed-vc and ComfyUI-MelBandRoFormer source trees, pin every
checkpoint env var (incl. GEMMA_QUANTIZE=nf4) before importing
audio_core, then call processor.startup() at module load. _run_job
monkey-patches _download_reference so file:// uploads from the Voice
Cloning tab get copied to a throwaway temp path. Whisper Validation
defaults off pending cuBLAS/cuDNN preload; ship the nvidia-cu12 wheels
in requirements so the libs are at least installed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +196 -50
  2. requirements.txt +2 -0
app.py CHANGED
@@ -2,32 +2,162 @@
2
  # https://scenema.ai
3
  # SPDX-License-Identifier: MIT
4
 
5
- """Gradio web UI for Scenema Audio.
6
 
7
- Thin HTTP client that talks to the FastAPI server at /generate.
8
- Mount into the FastAPI app via gr.mount_gradio_app() or run standalone.
9
-
10
- Usage (standalone):
11
- python app.py
12
-
13
- Usage (mounted, via ENABLE_GRADIO=1):
14
- ENABLE_GRADIO=1 python -m server
15
- # UI available at http://localhost:8000/ui
16
- """
17
-
18
- import base64
19
  import io
20
- import json
21
  import os
22
- import urllib.request
 
 
 
 
23
  from xml.sax.saxutils import escape
24
- import spaces
25
 
26
- import gradio as gr
27
- import numpy as np
28
- import soundfile as sf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- # API_URL = os.environ.get("SCENEMA_API_URL", "http://localhost:8000")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
 
33
  # ── Helpers ────────────────────────────────────────────────────
@@ -97,30 +227,43 @@ def _build_xml(
97
  return f"<speak {attrs}>\n{text.strip()}\n</speak>"
98
 
99
 
100
- def _call_api(payload: dict) -> tuple:
101
- """POST to /generate, return (sample_rate, np_array), metadata_str."""
102
- data = json.dumps(payload).encode()
103
- req = urllib.request.Request(
104
- f"{API_URL}/generate",
105
- data=data,
106
- headers={"Content-Type": "application/json"},
107
- )
108
- try:
109
- with urllib.request.urlopen(req, timeout=600) as resp:
110
- result = json.loads(resp.read())
111
- except urllib.error.URLError as e:
112
- raise gr.Error(
113
- f"Cannot reach API at {API_URL}/generate. "
114
- f"Is the server running? ({e})"
115
- )
116
-
117
- if result.get("status") != "succeeded":
118
- raise gr.Error(result.get("error", "Generation failed"))
119
-
120
- wav_bytes = base64.b64decode(result["audio"])
121
- audio_data, sample_rate = sf.read(io.BytesIO(wav_bytes))
122
 
123
- meta = result.get("metadata", {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  meta_display = {
125
  "duration": f"{meta.get('duration_s', 0):.1f}s",
126
  "processing_time": f"{meta.get('processing_ms', 0) / 1000:.1f}s",
@@ -163,7 +306,7 @@ def generate(
163
  "validate": validate,
164
  "skip_vc": skip_vc,
165
  }
166
- audio, meta = _call_api(payload)
167
  return audio, meta, prompt
168
 
169
 
@@ -188,7 +331,7 @@ def voice_design(
188
  "mode": "voice_design",
189
  "seed": seed,
190
  }
191
- audio, meta = _call_api(payload)
192
  return audio, meta, prompt
193
 
194
 
@@ -236,7 +379,7 @@ def voice_clone(
236
  "background_sfx": background_sfx,
237
  "validate": validate,
238
  }
239
- audio, meta = _call_api(payload)
240
  return audio, meta, prompt
241
 
242
 
@@ -264,7 +407,7 @@ def generate_raw(
264
  "validate": validate,
265
  "skip_vc": skip_vc,
266
  }
267
- audio, meta = _call_api(payload)
268
  return audio, meta
269
 
270
 
@@ -488,7 +631,8 @@ def create_demo() -> gr.Blocks:
488
  )
489
  gen_validate = gr.Checkbox(
490
  label="Whisper Validation",
491
- value=True,
 
492
  )
493
  gen_skip_vc = gr.Checkbox(
494
  label="Skip Voice Conversion",
@@ -678,7 +822,8 @@ def create_demo() -> gr.Blocks:
678
  )
679
  vc_validate = gr.Checkbox(
680
  label="Whisper Validation",
681
- value=True,
 
682
  )
683
  vc_btn = gr.Button("Generate with Voice Cloning", variant="primary")
684
 
@@ -756,7 +901,8 @@ def create_demo() -> gr.Blocks:
756
  )
757
  raw_validate = gr.Checkbox(
758
  label="Whisper Validation",
759
- value=True,
 
760
  )
761
  raw_skip_vc = gr.Checkbox(
762
  label="Skip VC",
 
2
  # https://scenema.ai
3
  # SPDX-License-Identifier: MIT
4
 
5
+ """Gradio web UI for Scenema Audio (ZeroGPU, single-process)."""
6
 
7
+ import asyncio
 
 
 
 
 
 
 
 
 
 
 
8
  import io
9
+ import logging
10
  import os
11
+ import shutil
12
+ import sys
13
+ import tempfile
14
+ import uuid
15
+ from pathlib import Path
16
  from xml.sax.saxutils import escape
 
17
 
18
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
19
+
20
+ # ── Model paths ───────────────────────────────────────────────
21
+ # These env vars must be set before audio_core is imported — vocal_separator
22
+ # and seedvc capture MELBAND_*/SEEDVC_PATH at module import time.
23
+
24
+ _APP_DIR = Path(__file__).parent.resolve()
25
+ MODEL_DIR = (_APP_DIR / "models").resolve()
26
+ MODEL_DIR.mkdir(parents=True, exist_ok=True)
27
+ os.environ["MODEL_DIR"] = str(MODEL_DIR)
28
+
29
+ os.environ.setdefault(
30
+ "AUDIO_CKPT", str(MODEL_DIR / "scenema-audio-transformer-int8.safetensors")
31
+ )
32
+ os.environ.setdefault(
33
+ "PIPELINE_CKPT", str(MODEL_DIR / "scenema-audio-pipeline.safetensors")
34
+ )
35
+ os.environ.setdefault(
36
+ "VAE_ENCODER_CKPT", str(MODEL_DIR / "scenema-audio-vae-encoder.safetensors")
37
+ )
38
+ os.environ.setdefault("GEMMA_ROOT", str(MODEL_DIR / "gemma-3-12b-it"))
39
+ os.environ.setdefault(
40
+ "MELBAND_MODEL_PATH", str(MODEL_DIR / "MelBandRoformer_fp16.safetensors")
41
+ )
42
+ os.environ.setdefault("SEEDVC_PATH", str(_APP_DIR / "seed-vc"))
43
+ os.environ.setdefault("MELBAND_NODE_PATH", str(_APP_DIR / "melband_roformer_node"))
44
+ os.environ.setdefault("HF_HUB_CACHE", str(MODEL_DIR / "hf_cache"))
45
+ os.environ.setdefault("GEMMA_QUANTIZE", "nf4")
46
+
47
+ sys.path.insert(0, str(_APP_DIR / "src"))
48
+
49
+ import gradio as gr # noqa: E402
50
+ import numpy as np # noqa: E402
51
+ import soundfile as sf # noqa: E402
52
+ import spaces # noqa: E402
53
+ from huggingface_hub import hf_hub_download, snapshot_download # noqa: E402
54
+
55
+ logging.basicConfig(
56
+ level=logging.INFO,
57
+ format="%(asctime)s %(name)s %(levelname)s %(message)s",
58
+ )
59
+ logger = logging.getLogger("scenema-space")
60
+
61
+
62
+ # ── Repo + weight bootstrap ───────────────────────────────────
63
+
64
+ HF_REPO = "ScenemaAI/scenema-audio"
65
+ GEMMA_REPO = "google/gemma-3-12b-it"
66
+ SEEDVC_REPO = "Plachta/Seed-VC"
67
+ BIGVGAN_REPO = "nvidia/bigvgan_v2_22khz_80band_256x"
68
+ WHISPER_REPO = "openai/whisper-small"
69
+
70
+
71
+ def _ensure_seedvc_repo():
72
+ """Clone seed-vc + ComfyUI-MelBandRoFormer source if missing."""
73
+ seedvc = Path(os.environ["SEEDVC_PATH"])
74
+ if not (seedvc / "modules").exists():
75
+ logger.info("Cloning seed-vc source...")
76
+ os.system(
77
+ f"git clone --depth 1 https://github.com/Plachtaa/seed-vc.git {seedvc}"
78
+ )
79
 
80
+ melband_node = Path(os.environ["MELBAND_NODE_PATH"])
81
+ if not melband_node.exists():
82
+ logger.info("Cloning ComfyUI-MelBandRoFormer source...")
83
+ os.system(
84
+ f"git clone --depth 1 https://github.com/kijai/ComfyUI-MelBandRoFormer {melband_node}"
85
+ )
86
+
87
+
88
+ def _download_all():
89
+ token = os.environ.get("HF_TOKEN")
90
+
91
+ audio_ckpt = Path(os.environ["AUDIO_CKPT"])
92
+ if not audio_ckpt.exists():
93
+ logger.info("Downloading audio transformer INT8 (~4.9 GB)...")
94
+ hf_hub_download(
95
+ HF_REPO, "scenema-audio-transformer-int8.safetensors",
96
+ local_dir=str(audio_ckpt.parent), token=token,
97
+ )
98
+
99
+ pipeline_ckpt = Path(os.environ["PIPELINE_CKPT"])
100
+ if not pipeline_ckpt.exists():
101
+ logger.info("Downloading pipeline checkpoint (~6.7 GB)...")
102
+ hf_hub_download(
103
+ HF_REPO, "scenema-audio-pipeline.safetensors",
104
+ local_dir=str(pipeline_ckpt.parent), token=token,
105
+ )
106
+
107
+ vae = Path(os.environ["VAE_ENCODER_CKPT"])
108
+ if not vae.exists():
109
+ logger.info("Downloading VAE encoder (~42 MB)...")
110
+ hf_hub_download(
111
+ HF_REPO, "scenema-audio-vae-encoder.safetensors",
112
+ local_dir=str(vae.parent), token=token,
113
+ )
114
+
115
+ melband = Path(os.environ["MELBAND_MODEL_PATH"])
116
+ if not melband.exists():
117
+ logger.info("Downloading MelBandRoFormer (~436 MB)...")
118
+ hf_hub_download(
119
+ "Kijai/MelBandRoFormer_comfy", "MelBandRoformer_fp16.safetensors",
120
+ local_dir=str(melband.parent), token=token,
121
+ )
122
+
123
+ gemma = Path(os.environ["GEMMA_ROOT"])
124
+ if not gemma.exists() or not any(gemma.glob("*.safetensors")):
125
+ logger.info("Downloading Gemma 3 12B IT (~24 GB, gated)...")
126
+ snapshot_download(
127
+ GEMMA_REPO, local_dir=str(gemma),
128
+ ignore_patterns=["*.gguf"], token=token,
129
+ )
130
+
131
+ seedvc_path = Path(os.environ["SEEDVC_PATH"])
132
+ seedvc_ckpts = seedvc_path / "checkpoints"
133
+ if not seedvc_ckpts.exists() or not any(seedvc_ckpts.glob("*.pth")):
134
+ logger.info("Downloading SeedVC checkpoints (~1.6 GB)...")
135
+ seedvc_ckpts.mkdir(parents=True, exist_ok=True)
136
+ hf_cache = seedvc_ckpts / "hf_cache"
137
+ hf_cache.mkdir(parents=True, exist_ok=True)
138
+ os.environ["HF_HUB_CACHE"] = str(hf_cache)
139
+ hf_hub_download(
140
+ SEEDVC_REPO,
141
+ "DiT_seed_v2_uvit_whisper_small_wavenet_bigvgan_pruned.pth",
142
+ local_dir=str(seedvc_ckpts), token=token,
143
+ )
144
+ hf_hub_download(
145
+ SEEDVC_REPO,
146
+ "config_dit_mel_seed_uvit_whisper_small_wavenet.yml",
147
+ local_dir=str(seedvc_ckpts), token=token,
148
+ )
149
+ snapshot_download(BIGVGAN_REPO, local_dir=str(hf_cache / "bigvgan"))
150
+ snapshot_download(WHISPER_REPO, local_dir=str(hf_cache / "whisper-small"))
151
+
152
+
153
+ _ensure_seedvc_repo()
154
+ _download_all()
155
+
156
+ from audio_core.processor import AudioProcessor # noqa: E402
157
+ from common.handlers.base import ProcessJob # noqa: E402
158
+
159
+ processor = AudioProcessor()
160
+ processor.startup()
161
 
162
 
163
  # ── Helpers ────────────────────────────────────────────────────
 
227
  return f"<speak {attrs}>\n{text.strip()}\n</speak>"
228
 
229
 
230
+ def _run_job(payload: dict) -> tuple:
231
+ """Run a generation job inline against the global processor.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
+ Returns ((sample_rate, np_array), metadata_dict).
234
+ """
235
+ async def _go():
236
+ # Voice Cloning passes the upload as file://… — httpx can't fetch
237
+ # that, and the processor unlinks the path it gets back. Copy the
238
+ # user's file to a throwaway temp file so cleanup is harmless.
239
+ original = processor._download_reference
240
+
241
+ async def patched(url):
242
+ if url.startswith("file://"):
243
+ src = url[len("file://"):]
244
+ suffix = Path(src).suffix or ".wav"
245
+ tmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
246
+ tmp.close()
247
+ shutil.copyfile(src, tmp.name)
248
+ return tmp.name
249
+ return await original(url)
250
+
251
+ processor._download_reference = patched
252
+ try:
253
+ job = ProcessJob(job_id=str(uuid.uuid4()), input=payload)
254
+ return await processor.process(job)
255
+ finally:
256
+ processor._download_reference = original
257
+
258
+ result = asyncio.run(_go())
259
+
260
+ if not result.success:
261
+ raise gr.Error(result.error or "Generation failed")
262
+
263
+ output = result.output
264
+ audio_data, sample_rate = sf.read(io.BytesIO(output.data))
265
+
266
+ meta = output.metadata or {}
267
  meta_display = {
268
  "duration": f"{meta.get('duration_s', 0):.1f}s",
269
  "processing_time": f"{meta.get('processing_ms', 0) / 1000:.1f}s",
 
306
  "validate": validate,
307
  "skip_vc": skip_vc,
308
  }
309
+ audio, meta = _run_job(payload)
310
  return audio, meta, prompt
311
 
312
 
 
331
  "mode": "voice_design",
332
  "seed": seed,
333
  }
334
+ audio, meta = _run_job(payload)
335
  return audio, meta, prompt
336
 
337
 
 
379
  "background_sfx": background_sfx,
380
  "validate": validate,
381
  }
382
+ audio, meta = _run_job(payload)
383
  return audio, meta, prompt
384
 
385
 
 
407
  "validate": validate,
408
  "skip_vc": skip_vc,
409
  }
410
+ audio, meta = _run_job(payload)
411
  return audio, meta
412
 
413
 
 
631
  )
632
  gen_validate = gr.Checkbox(
633
  label="Whisper Validation",
634
+ value=False,
635
+ interactive=True
636
  )
637
  gen_skip_vc = gr.Checkbox(
638
  label="Skip Voice Conversion",
 
822
  )
823
  vc_validate = gr.Checkbox(
824
  label="Whisper Validation",
825
+ value=False,
826
+ interactive=True,
827
  )
828
  vc_btn = gr.Button("Generate with Voice Cloning", variant="primary")
829
 
 
901
  )
902
  raw_validate = gr.Checkbox(
903
  label="Whisper Validation",
904
+ value=False,
905
+ interactive=True,
906
  )
907
  raw_skip_vc = gr.Checkbox(
908
  label="Skip VC",
requirements.txt CHANGED
@@ -27,3 +27,5 @@ bitsandbytes==0.49.2
27
  kokoro==0.9.4
28
  faster-whisper==1.2.1
29
  ctranslate2==4.7.1
 
 
 
27
  kokoro==0.9.4
28
  faster-whisper==1.2.1
29
  ctranslate2==4.7.1
30
+ nvidia-cublas-cu12
31
+ nvidia-cudnn-cu12