Spaces:
Running on Zero
Running on Zero
Commit ·
b89b02d
1
Parent(s): 3b2141d
Run inline on Space instead of via FastAPI
Browse filesDrop 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>
- app.py +196 -50
- 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 |
-
|
| 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
|
| 21 |
import os
|
| 22 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
from xml.sax.saxutils import escape
|
| 24 |
-
import spaces
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 101 |
-
"""
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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=
|
|
|
|
| 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=
|
|
|
|
| 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=
|
|
|
|
| 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
|