multimodalart HF Staff commited on
Commit
73a0ac3
Β·
verified Β·
1 Parent(s): 0ec2e3f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +734 -328
app.py CHANGED
@@ -1,385 +1,791 @@
1
- """Scenema Audio - ZeroGPU Gradio Space.
 
 
2
 
3
- Wraps the ScenemaAI/scenema-audio AudioProcessor in a Gradio UI.
4
- Heavy model weights (~38 GB) are downloaded on first cold-start and
5
- cached on persistent storage; generation runs under @spaces.GPU.
 
 
 
 
 
 
 
 
6
  """
7
 
8
- import asyncio
9
  import base64
10
- import logging
 
11
  import os
12
- import shutil
13
- import sys
14
- import tempfile
15
- import uuid
16
- from pathlib import Path
17
-
18
- os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
19
-
20
- # Allow tweaking via env, but default to repo-local cache so weights persist
21
- # across worker restarts on Spaces persistent storage if mounted at /data.
22
- _APP_DIR = Path(__file__).parent.resolve()
23
- MODEL_DIR = (_APP_DIR / "models").resolve()
24
- MODEL_DIR.mkdir(parents=True, exist_ok=True)
25
- os.environ["MODEL_DIR"] = str(MODEL_DIR)
26
-
27
- # Default model paths (must be set before AudioProcessor is imported)
28
- os.environ.setdefault(
29
- "AUDIO_CKPT", str(MODEL_DIR / "scenema-audio-transformer-int8.safetensors")
30
- )
31
- os.environ.setdefault(
32
- "PIPELINE_CKPT", str(MODEL_DIR / "scenema-audio-pipeline.safetensors")
33
- )
34
- os.environ.setdefault(
35
- "VAE_ENCODER_CKPT", str(MODEL_DIR / "scenema-audio-vae-encoder.safetensors")
36
- )
37
- os.environ.setdefault("GEMMA_ROOT", str(MODEL_DIR / "gemma-3-12b-it"))
38
- os.environ.setdefault(
39
- "MELBAND_MODEL_PATH", str(MODEL_DIR / "MelBandRoformer_fp16.safetensors")
40
- )
41
- os.environ.setdefault("SEEDVC_PATH", str(_APP_DIR / "seed-vc"))
42
- os.environ.setdefault("MELBAND_NODE_PATH", str(_APP_DIR / "melband_roformer_node"))
43
- os.environ.setdefault("HF_HUB_CACHE", str(MODEL_DIR / "hf_cache"))
44
- os.environ.setdefault("GEMMA_QUANTIZE", "nf4")
45
-
46
- # Make repo source importable
47
- sys.path.insert(0, str(Path(__file__).parent / "src"))
48
 
49
  import gradio as gr
50
- import spaces
51
- from huggingface_hub import hf_hub_download, snapshot_download
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- logging.basicConfig(
54
- level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s"
55
- )
56
- logger = logging.getLogger("scenema-space")
57
 
58
 
59
- # ── Model download (CPU phase, runs at import) ────────────────────────────
 
 
 
60
 
61
- HF_REPO = "ScenemaAI/scenema-audio"
62
- GEMMA_REPO = "google/gemma-3-12b-it"
63
- SEEDVC_REPO = "Plachta/Seed-VC"
64
- BIGVGAN_REPO = "nvidia/bigvgan_v2_22khz_80band_256x"
65
- WHISPER_REPO = "openai/whisper-small"
66
 
 
 
 
 
 
 
 
 
67
 
68
- def _download_all():
69
- token = os.environ.get("HF_TOKEN")
 
 
70
 
71
- audio_ckpt = Path(os.environ["AUDIO_CKPT"])
72
- if not audio_ckpt.exists():
73
- logger.info("Downloading audio transformer INT8 (~4.9 GB)...")
74
- hf_hub_download(
75
- HF_REPO,
76
- "scenema-audio-transformer-int8.safetensors",
77
- local_dir=str(audio_ckpt.parent),
78
- token=token,
79
- )
80
-
81
- pipeline_ckpt = Path(os.environ["PIPELINE_CKPT"])
82
- if not pipeline_ckpt.exists():
83
- logger.info("Downloading pipeline checkpoint (~6.7 GB)...")
84
- hf_hub_download(
85
- HF_REPO,
86
- "scenema-audio-pipeline.safetensors",
87
- local_dir=str(pipeline_ckpt.parent),
88
- token=token,
 
 
 
 
 
 
 
 
 
 
 
89
  )
90
 
91
- vae = Path(os.environ["VAE_ENCODER_CKPT"])
92
- if not vae.exists():
93
- logger.info("Downloading VAE encoder (~42 MB)...")
94
- hf_hub_download(
95
- HF_REPO,
96
- "scenema-audio-vae-encoder.safetensors",
97
- local_dir=str(vae.parent),
98
- token=token,
99
- )
100
 
101
- melband = Path(os.environ["MELBAND_MODEL_PATH"])
102
- if not melband.exists():
103
- logger.info("Downloading MelBandRoFormer (~436 MB)...")
104
- hf_hub_download(
105
- "Kijai/MelBandRoFormer_comfy",
106
- "MelBandRoformer_fp16.safetensors",
107
- local_dir=str(melband.parent),
108
- token=token,
109
- )
110
 
111
- gemma = Path(os.environ["GEMMA_ROOT"])
112
- if not gemma.exists() or not any(gemma.glob("*.safetensors")):
113
- logger.info("Downloading Gemma 3 12B IT (~24 GB, gated)...")
114
- snapshot_download(
115
- GEMMA_REPO,
116
- local_dir=str(gemma),
117
- ignore_patterns=["*.gguf"],
118
- token=token,
119
- )
120
-
121
- seedvc_path = Path(os.environ["SEEDVC_PATH"])
122
- seedvc_ckpts = seedvc_path / "checkpoints"
123
- if not seedvc_ckpts.exists() or not any(seedvc_ckpts.glob("*.pth")):
124
- logger.info("Downloading SeedVC checkpoints (~1.6 GB)...")
125
- seedvc_ckpts.mkdir(parents=True, exist_ok=True)
126
- hf_cache = seedvc_ckpts / "hf_cache"
127
- hf_cache.mkdir(parents=True, exist_ok=True)
128
- os.environ["HF_HUB_CACHE"] = str(hf_cache)
129
- hf_hub_download(
130
- SEEDVC_REPO,
131
- "DiT_seed_v2_uvit_whisper_small_wavenet_bigvgan_pruned.pth",
132
- local_dir=str(seedvc_ckpts),
133
- token=token,
134
- )
135
- hf_hub_download(
136
- SEEDVC_REPO,
137
- "config_dit_mel_seed_uvit_whisper_small_wavenet.yml",
138
- local_dir=str(seedvc_ckpts),
139
- token=token,
140
- )
141
- snapshot_download(BIGVGAN_REPO, local_dir=str(hf_cache / "bigvgan"))
142
- snapshot_download(WHISPER_REPO, local_dir=str(hf_cache / "whisper-small"))
143
-
144
-
145
- def _ensure_seedvc_repo():
146
- """Clone the seed-vc python source if missing (architecture code)."""
147
- seedvc = Path(os.environ["SEEDVC_PATH"])
148
- if not (seedvc / "modules").exists():
149
- logger.info("Cloning seed-vc source...")
150
- os.system(f"git clone --depth 1 https://github.com/Plachtaa/seed-vc.git {seedvc}")
151
-
152
- melband_node = Path(os.environ["MELBAND_NODE_PATH"])
153
- if not melband_node.exists():
154
- logger.info("Cloning ComfyUI-MelBandRoFormer source...")
155
- os.system(
156
- f"git clone --depth 1 https://github.com/kijai/ComfyUI-MelBandRoFormer {melband_node}"
157
- )
158
 
 
159
 
160
- _ensure_seedvc_repo()
161
- _download_all()
162
 
163
- # Import processor only after model paths/env are set
164
- from audio_core.processor import AudioProcessor # noqa: E402
165
- from common.handlers.base import ProcessJob # noqa: E402
166
 
167
- # Load models at module import so ZeroGPU snapshots the warm state and
168
- # every request starts with weights already resident.
169
- processor = AudioProcessor()
170
- processor.startup()
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
- # ── Generation ────────────────────────────────────────────────────────────
174
 
 
175
 
176
- def _build_prompt(text, voice, gender, scene, language, shot, action, sound_before):
177
- attrs = [f'voice="{voice}"', f'gender="{gender}"']
178
- if scene:
179
- attrs.append(f'scene="{scene}"')
180
- if language and language != "en":
181
- attrs.append(f'language="{language}"')
182
- if shot:
183
- attrs.append(f'shot="{shot}"')
 
 
 
 
184
 
185
- inner = ""
186
- if sound_before:
187
- inner += f"<sound>{sound_before}</sound>"
188
- if action:
189
- inner += f"<action>{action}</action>"
190
- inner += text
 
 
191
 
192
- return f"<speak {' '.join(attrs)}>{inner}</speak>"
193
 
 
194
 
195
- @spaces.GPU(duration=300)
196
- def generate(
197
- text,
198
- voice,
199
- gender,
200
- scene,
201
- language,
202
- shot,
203
- action,
204
- sound_before,
205
  reference_audio,
206
- mode,
207
- seed,
208
- background_sfx,
209
- skip_vc,
210
- raw_xml,
211
- progress=gr.Progress(track_tqdm=True),
212
  ):
213
- if raw_xml and raw_xml.strip():
214
- prompt = raw_xml.strip()
215
- else:
216
- if not text.strip():
217
- raise gr.Error("Speech text is required.")
218
- prompt = _build_prompt(text, voice, gender, scene, language, shot, action, sound_before)
219
-
220
- # If reference audio is a local file (gradio path), upload-less: we copy into
221
- # a temp http-less path that AudioProcessor expects URL. Easiest: serve via
222
- # a file:// URL β€” but httpx doesn't support file://. Instead, patch path by
223
- # writing input to a known place and using a fake URL handler via temp.
224
- body = {
 
 
 
 
 
225
  "prompt": prompt,
226
- "mode": mode,
227
- "seed": int(seed) if seed is not None else -1,
228
- "background_sfx": bool(background_sfx),
229
- "skip_vc": bool(skip_vc),
230
- "validate": False,
 
 
 
231
  }
 
 
232
 
233
- # Reference voice: AudioProcessor downloads from URL. We bypass by directly
234
- # placing a local path; the _generate function uses `reference_voice_url`
235
- # and calls `_download_reference`. Workaround: monkey-patch download to
236
- # return the local path if a file:// URL is given.
237
- ref_local_path = None
238
- if reference_audio:
239
- ref_local_path = reference_audio
240
- body["reference_voice_url"] = f"file://{ref_local_path}"
241
-
242
- async def _run():
243
- # Patch _download_reference for this call to handle file:// URLs
244
- original = processor._download_reference
245
-
246
- async def patched(url):
247
- if url.startswith("file://"):
248
- # Copy to a throwaway temp file β€” AudioProcessor unlinks
249
- # ref_wav_path on completion, which would otherwise destroy
250
- # the user's uploaded gradio file and break subsequent runs.
251
- src = url[len("file://"):]
252
- suffix = Path(src).suffix or ".wav"
253
- tmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
254
- tmp.close()
255
- shutil.copyfile(src, tmp.name)
256
- return tmp.name
257
- return await original(url)
258
-
259
- processor._download_reference = patched
260
- try:
261
- job = ProcessJob(job_id=str(uuid.uuid4()), input=body)
262
- return await processor.process(job)
263
- finally:
264
- processor._download_reference = original
265
-
266
- progress(0.1, desc="Generating audio")
267
- result = asyncio.run(_run())
268
-
269
- if not result.success:
270
- raise gr.Error(result.error or "Generation failed")
271
-
272
- # Write to temp wav and return path
273
- out_path = Path(tempfile.gettempdir()) / f"scenema_{uuid.uuid4().hex}.wav"
274
- out_path.write_bytes(result.output.data)
275
- meta = result.output.metadata or {}
276
- info = (
277
- f"Duration: {meta.get('duration_s', 0)}s Β· "
278
- f"Seed: {meta.get('seed')} Β· "
279
- f"GPU: {meta.get('gpu', 'N/A')} Β· "
280
- f"Time: {meta.get('processing_ms', 0)} ms"
281
- )
282
- return str(out_path), info
283
 
284
 
285
- # ── UI ────────────────────────────────────────────────────────────────────
286
 
287
- EXAMPLES = [
 
288
  [
289
- "The old lighthouse had stood on the cliff for over a century, its beam cutting through the fog like a blade of light.",
290
  "A warm, clear male voice with a slight British accent. Measured, thoughtful pacing.",
291
- "male", "", "en", "closeup", "", "",
292
- None, "generate", 42, False, False, "",
 
 
293
  ],
294
  [
295
- "The city never really sleeps. It just closes its eyes and pretends for a while.",
296
- "A young woman with a smoky, low register voice. Intimate, confessional tone.",
297
- "female", "", "en", "closeup", "", "",
298
- None, "voice_design", 7, False, False, "",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  ],
300
  [
301
- "Get the lines! She is pulling loose! Move! I said move!",
302
  "Male, mid 40s. Weathered. Urgent, projecting over wind.",
303
- "male", "Open dock in a thunderstorm, heavy rain", "en", "scene",
304
- "He shouts over the storm", "Heavy rain and wind howling",
305
- None, "generate", 11, True, False, "",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  ],
307
  ]
308
 
309
- with gr.Blocks(title="Scenema Audio") as demo:
310
- gr.Markdown(
311
- """
312
- # Scenema Audio Β· Zero-shot Expressive TTS
313
- Generate expressive speech with emotion, scene, and voice cloning.
314
- Built on [ScenemaAI/scenema-audio](https://github.com/ScenemaAI/scenema-audio).
315
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  )
317
- with gr.Row():
318
- with gr.Column(scale=3):
319
- text = gr.Textbox(
320
- label="Speech text",
321
- lines=4,
322
- placeholder="What the voice should say...",
323
- )
324
- voice = gr.Textbox(
325
- label="Voice description",
326
- lines=2,
327
- placeholder='e.g. "A warm male voice with a slight British accent..."',
328
- )
329
- gender = gr.Radio(["male", "female"], value="male", label="Gender")
330
- reference_audio = gr.Audio(
331
- label="Voice cloning reference (optional, 10-20s)",
332
- type="filepath",
333
- )
334
- with gr.Accordion("Advanced settings", open=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  with gr.Row():
336
- mode = gr.Radio(
337
- ["generate", "voice_design"],
338
- value="generate",
339
- label="Mode",
340
- info="voice_design = 15s voice preview",
341
- )
342
- seed = gr.Number(value=42, precision=0, label="Seed (-1 = random)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  with gr.Row():
344
- language = gr.Dropdown(
345
- ["en", "es", "fr", "de", "it", "pt", "ja", "zh", "ko"],
346
- value="en", label="Language",
347
- )
348
- shot = gr.Radio(
349
- ["closeup", "wide", "scene"], value="closeup", label="Shot"
350
- )
351
- scene = gr.Textbox(label="Scene", placeholder="e.g. busy cafe at midday")
352
- action = gr.Textbox(label="Performance direction (<action>)")
353
- sound_before = gr.Textbox(label="Sound event before speech (<sound>)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  with gr.Row():
355
- background_sfx = gr.Checkbox(value=False, label="Keep background SFX")
356
- skip_vc = gr.Checkbox(value=False, label="Skip SeedVC post-processing")
357
- raw_xml = gr.Textbox(
358
- label="Raw <speak> XML (overrides all fields above when set)",
359
- lines=4,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  )
361
- run_btn = gr.Button("Generate", variant="primary")
362
- with gr.Column(scale=2):
363
- out_audio = gr.Audio(label="Output", type="filepath")
364
- info = gr.Textbox(label="Info", interactive=False)
365
-
366
- gr.Examples(
367
- examples=EXAMPLES,
368
- inputs=[
369
- text, voice, gender, scene, language, shot, action, sound_before,
370
- reference_audio, mode, seed, background_sfx, skip_vc, raw_xml,
371
- ],
372
- )
373
 
374
- run_btn.click(
375
- generate,
376
- inputs=[
377
- text, voice, gender, scene, language, shot, action, sound_before,
378
- reference_audio, mode, seed, background_sfx, skip_vc, raw_xml,
379
- ],
380
- outputs=[out_audio, info],
381
- )
382
 
 
383
 
384
  if __name__ == "__main__":
385
- demo.queue().launch()
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Scenema AI
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 ────────────────────────────────────────────────────
34
+
35
+
36
+ _FEMALE_KEYWORDS = {"female", "woman", "girl", "she", "her ", "mother", "daughter", "lady", "feminine", "actress", "queen", "princess", "grandmother", "grandma", "aunt", "sister", "wife", "soprano", "alto", "contralto", "mezzo"}
37
+
38
+ LANGUAGES = [
39
+ ("English", "en"),
40
+ ("Spanish", "es"),
41
+ ("French", "fr"),
42
+ ("German", "de"),
43
+ ("Italian", "it"),
44
+ ("Portuguese", "pt"),
45
+ ("Russian", "ru"),
46
+ ("Japanese", "ja"),
47
+ ("Korean", "ko"),
48
+ ("Chinese", "zh"),
49
+ ("Arabic", "ar"),
50
+ ("Hindi", "hi"),
51
+ ("Dutch", "nl"),
52
+ ("Polish", "pl"),
53
+ ("Turkish", "tr"),
54
+ ("Swedish", "sv"),
55
+ ("Danish", "da"),
56
+ ("Finnish", "fi"),
57
+ ("Thai", "th"),
58
+ ("Vietnamese", "vi"),
59
+ ]
60
 
61
+ LANGUAGE_CHOICES = [f"{name} ({code})" for name, code in LANGUAGES]
62
+ _LANG_TO_CODE = {f"{name} ({code})": code for name, code in LANGUAGES}
63
+ _CODE_TO_LABEL = {code: f"{name} ({code})" for name, code in LANGUAGES}
 
64
 
65
 
66
+ def _infer_gender(voice: str) -> str:
67
+ """Infer gender from voice description for pronoun assignment."""
68
+ lower = voice.lower()
69
+ return "female" if any(kw in lower for kw in _FEMALE_KEYWORDS) else "male"
70
 
 
 
 
 
 
71
 
72
+ def _build_xml(
73
+ voice: str,
74
+ text: str,
75
+ scene: str = "",
76
+ language: str = "en",
77
+ shot: str = "closeup",
78
+ ) -> str:
79
+ """Build <speak> XML from individual fields.
80
 
81
+ Text can contain inline <action> and <sound> tags. Everything
82
+ else is treated as speech content. The voice/scene/gender
83
+ attributes are escaped; inner XML is passed through so users
84
+ can write <action> tags directly in the text field.
85
 
86
+ Gender is inferred from the voice description for pronoun assignment.
87
+ """
88
+ gender = _infer_gender(voice)
89
+ language = _LANG_TO_CODE.get(language, language) # "French (fr)" -> "fr"
90
+ attrs = f'voice="{escape(voice)}" gender="{gender}"'
91
+ if scene:
92
+ attrs += f' scene="{escape(scene)}"'
93
+ if language and language != "en":
94
+ attrs += f' language="{language}"'
95
+ if shot and shot != "closeup":
96
+ attrs += f' shot="{shot}"'
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",
127
+ "seed": meta.get("seed", -1),
128
+ "sample_rate": meta.get("sample_rate", sample_rate),
129
+ "vram_peak": f"{meta.get('vram_peak_mb', 0):.0f} MB",
130
+ "gpu": meta.get("gpu", "unknown"),
131
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
+ return (sample_rate, audio_data), meta_display
134
 
 
 
135
 
136
+ # ── Generate tab ───────────────────────────────────────────────
 
 
137
 
138
+ @spaces.GPU
139
+ def generate(
140
+ voice: str,
141
+ text: str,
142
+ scene: str,
143
+ language: str,
144
+ shot: str,
145
+ seed: int,
146
+ pace: float,
147
+ background_sfx: bool,
148
+ validate: bool,
149
+ skip_vc: bool,
150
+ ):
151
+ if not voice.strip():
152
+ raise gr.Error("Voice description is required.")
153
+ if not text.strip():
154
+ raise gr.Error("Speech text is required.")
155
 
156
+ prompt = _build_xml(voice, text, scene, language, shot)
157
+ payload = {
158
+ "prompt": prompt,
159
+ "mode": "generate",
160
+ "seed": seed,
161
+ "pace": pace,
162
+ "background_sfx": background_sfx,
163
+ "validate": validate,
164
+ "skip_vc": skip_vc,
165
+ }
166
+ audio, meta = _call_api(payload)
167
+ return audio, meta, prompt
168
 
 
169
 
170
+ # ── Voice Design tab ──────────────────────────────────────────
171
 
172
+ @spaces.GPU
173
+ def voice_design(
174
+ voice: str,
175
+ text: str,
176
+ scene: str,
177
+ language: str,
178
+ seed: int,
179
+ ):
180
+ if not voice.strip():
181
+ raise gr.Error("Voice description is required.")
182
+ if not text.strip():
183
+ raise gr.Error("Speech text is required.")
184
 
185
+ prompt = _build_xml(voice, text, scene, language)
186
+ payload = {
187
+ "prompt": prompt,
188
+ "mode": "voice_design",
189
+ "seed": seed,
190
+ }
191
+ audio, meta = _call_api(payload)
192
+ return audio, meta, prompt
193
 
 
194
 
195
+ # ── Voice Cloning tab ─────────────────────────────────────────
196
 
197
+ @spaces.GPU
198
+ def voice_clone(
199
+ voice: str,
200
+ text: str,
201
+ scene: str,
202
+ language: str,
203
+ shot: str,
 
 
 
204
  reference_audio,
205
+ seed: int,
206
+ pace: float,
207
+ vc_steps: int,
208
+ vc_cfg_rate: float,
209
+ background_sfx: bool,
210
+ validate: bool,
211
  ):
212
+ if not voice.strip():
213
+ raise gr.Error("Voice description is required.")
214
+ if not text.strip():
215
+ raise gr.Error("Speech text is required.")
216
+ if reference_audio is None:
217
+ raise gr.Error(
218
+ "Reference audio is required for voice cloning. "
219
+ "Upload a few seconds of clean speech."
220
+ )
221
+
222
+ ref_path = reference_audio
223
+ if isinstance(ref_path, tuple):
224
+ ref_path = ref_path[0] if isinstance(ref_path[0], str) else None
225
+ ref_url = f"file://{os.path.abspath(ref_path)}" if ref_path else None
226
+
227
+ prompt = _build_xml(voice, text, scene, language, shot)
228
+ payload = {
229
  "prompt": prompt,
230
+ "mode": "generate",
231
+ "reference_voice_url": ref_url,
232
+ "seed": seed,
233
+ "pace": pace,
234
+ "vc_steps": vc_steps,
235
+ "vc_cfg_rate": vc_cfg_rate,
236
+ "background_sfx": background_sfx,
237
+ "validate": validate,
238
  }
239
+ audio, meta = _call_api(payload)
240
+ return audio, meta, prompt
241
 
242
+
243
+ # ── Advanced tab ──────────────────────────────────────────────
244
+
245
+ @spaces.GPU
246
+ def generate_raw(
247
+ raw_xml: str,
248
+ mode: str,
249
+ seed: int,
250
+ pace: float,
251
+ background_sfx: bool,
252
+ validate: bool,
253
+ skip_vc: bool,
254
+ ):
255
+ if not raw_xml.strip():
256
+ raise gr.Error("Prompt XML is required.")
257
+
258
+ payload = {
259
+ "prompt": raw_xml,
260
+ "mode": mode,
261
+ "seed": seed,
262
+ "pace": pace,
263
+ "background_sfx": background_sfx,
264
+ "validate": validate,
265
+ "skip_vc": skip_vc,
266
+ }
267
+ audio, meta = _call_api(payload)
268
+ return audio, meta
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
 
271
+ # ── Examples ──────────────────────────────────────────────────
272
 
273
+ GENERATE_EXAMPLES = [
274
+ # [voice, text, scene, language, shot]
275
  [
 
276
  "A warm, clear male voice with a slight British accent. Measured, thoughtful pacing.",
277
+ "The old lighthouse had stood on the cliff for over a century, its beam cutting through the fog like a blade of light.",
278
+ "",
279
+ "English (en)",
280
+ "closeup",
281
  ],
282
  [
283
+ "Male, mid 60s. Deep baritone with gravel. Slight Southern American inflection. Worn but warm. Nostalgic, firelight cadence. The voice of someone who has seen too much and chosen kindness anyway.",
284
+ "<action>Calm, almost casual. Staring at his hands.</action>\nI used to think I had all the time in the world.\n<action>Voice tightens. Swallows. Fighting to stay composed.</action>\nThen one Tuesday morning, the doctor said three words that changed everything.\n<action>Long pause. Deep breath. When he speaks again, his voice is raw but steady.</action>\nAnd I realized... I hadn't called my son in six months.\n<action>Voice breaks on the last word. Clears throat. Forces a half-laugh.</action>\nFunny how that works, isn't it?",
285
+ "Fireside, night, crickets",
286
+ "English (en)",
287
+ "closeup",
288
+ ],
289
+ [
290
+ "A soulful female alto singing with raw emotion. Blues-jazz phrasing, slight vibrato on sustained notes.",
291
+ "<action>Soft piano intro, she takes a breath.</action>\nI heard love was a losing game, played it once and lost the same.",
292
+ "",
293
+ "English (en)",
294
+ "closeup",
295
+ ],
296
+ [
297
+ "A six-year-old girl, bright and excited, speaking fast with breathless enthusiasm. Slight lisp on S sounds.",
298
+ "Mommy look! There is a rainbow and it goes all the way across the whole sky!",
299
+ "",
300
+ "English (en)",
301
+ "closeup",
302
  ],
303
  [
 
304
  "Male, mid 40s. Weathered. Urgent, projecting over wind.",
305
+ "<sound>Heavy rain and wind howling</sound>\n<action>He shouts over the storm</action>\nGet the lines! She is pulling loose!\n<sound>Thunder cracks overhead</sound>\nMove! I said move!",
306
+ "Open dock in a thunderstorm, heavy rain",
307
+ "English (en)",
308
+ "scene",
309
+ ],
310
+ [
311
+ "Female, mid 70s. Soft alto. Native French speaker, Parisian accent. Warm like wool blankets. Unhurried.",
312
+ "<action>Elle s'assied au bord du lit</action>\nAlors, mon petit. Tu veux que je te raconte l'histoire du renard qui a trompΓ© la lune?",
313
+ "Cozy bedroom, lamplight",
314
+ "French (fr)",
315
+ "closeup",
316
+ ],
317
+ # Laughing
318
+ [
319
+ "A woman in her 30s, bright and infectious laugh. She can barely get the words out between fits of giggling.",
320
+ "<action>She starts laughing before she even finishes the sentence</action>\nAnd then he just... he just walked straight into the glass door.\n<action>Completely loses it, doubled over, gasping between words</action>\nIn front of everyone! At his own wedding!",
321
+ "",
322
+ "English (en)",
323
+ "closeup",
324
+ ],
325
+ # Crying
326
+ [
327
+ "A young man, mid 20s. Thin, shaking voice. Trying not to cry but failing. Raw and unguarded.",
328
+ "<action>Voice already trembling, eyes wet</action>\nI keep thinking she is going to call.\n<action>Swallows hard, voice cracks</action>\nEvery time the phone rings I think maybe...\n<action>Breaks down, words dissolving into quiet sobs</action>\nI just miss her so much.",
329
+ "",
330
+ "English (en)",
331
+ "closeup",
332
+ ],
333
+ # Singing
334
+ [
335
+ "A male tenor with a warm folk quality. Acoustic, intimate, like a campfire performance. Gentle vibrato on held notes.",
336
+ "<action>Strums once, pauses, then begins softly</action>\nBlackbird singing in the dead of night, take these broken wings and learn to fly.\n<action>Voice swells with quiet conviction</action>\nAll your life, you were only waiting for this moment to arise.",
337
+ "",
338
+ "English (en)",
339
+ "closeup",
340
+ ],
341
+ # Long narration β€” Alice in Wonderland
342
+ [
343
+ "A deep, soothing male voice. Rich baritone, unhurried. BBC audiobook narrator quality. Warm like dark chocolate. The kind of voice that makes you drowsy.",
344
+ "Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do. Once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it. And what is the use of a book, thought Alice, without pictures or conversations? So she was considering in her own mind, as well as she could, for the hot day made her feel very sleepy and stupid, whether the pleasure of making a daisy chain would be worth the trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes ran close by her.",
345
+ "",
346
+ "English (en)",
347
+ "closeup",
348
  ],
349
  ]
350
 
351
+ VOICE_DESIGN_EXAMPLES = [
352
+ # [voice, text, scene, language]
353
+ [
354
+ "A young woman with a smoky jazz-singer quality. Low register, intimate. Like she is telling you a secret at 2am.",
355
+ "The city never really sleeps. It just closes its eyes and pretends for a while.",
356
+ "",
357
+ "English (en)",
358
+ ],
359
+ [
360
+ "Gravelly male voice, fast talking, rough. Brooklyn accent. Sounds like he has been smoking for forty years.",
361
+ "You want my advice? Stop asking for advice and start making decisions.",
362
+ "",
363
+ "English (en)",
364
+ ],
365
+ [
366
+ "A six-year-old boy, breathless with excitement. High pitched, words tumbling over each other. Slight lisp.",
367
+ "And then the dinosaur was like RAWR and everyone ran away but I did not because I am brave!",
368
+ "",
369
+ "English (en)",
370
+ ],
371
+ [
372
+ "An elderly Japanese woman speaking English with a strong accent. Quiet authority. Every word deliberate, chosen with care. Grandmotherly warmth underneath.",
373
+ "When I was young, we did not have these things. We had patience. That was enough.",
374
+ "",
375
+ "English (en)",
376
+ ],
377
+ [
378
+ "A deep, resonant female contralto. African American preacher cadence. Building intensity, rhythmic. The kind of voice that fills a cathedral.",
379
+ "I am telling you today, the storm does not last forever. The rain will stop. The sun will come.",
380
+ "",
381
+ "English (en)",
382
+ ],
383
+ [
384
+ "A tired male nurse, late 30s. Gentle but exhausted. Slight Irish lilt. The end of a double shift.",
385
+ "You are doing great, love. Just breathe. I will be right here the whole time.",
386
+ "",
387
+ "English (en)",
388
+ ],
389
+ ]
390
+
391
+
392
+ # ── Build UI ──────────────────────────────────────────────────
393
+
394
+
395
+ def create_demo() -> gr.Blocks:
396
+ theme = gr.themes.Base(
397
+ primary_hue=gr.themes.Color(
398
+ c50="#fafafa",
399
+ c100="#f5f5f5",
400
+ c200="#e5e5e5",
401
+ c300="#d4d4d4",
402
+ c400="#a3a3a3",
403
+ c500="#737373",
404
+ c600="#525252",
405
+ c700="#404040",
406
+ c800="#262626",
407
+ c900="#171717",
408
+ c950="#0a0a0a",
409
+ ),
410
+ neutral_hue="stone",
411
+ font=gr.themes.GoogleFont("Inter"),
412
+ font_mono=gr.themes.GoogleFont("JetBrains Mono"),
413
+ radius_size=gr.themes.sizes.radius_none,
414
+ ).set(
415
+ button_primary_background_fill="#171717",
416
+ button_primary_background_fill_hover="#262626",
417
+ button_primary_text_color="#ffffff",
418
+ button_primary_border_color="#171717",
419
+ block_border_width="1px",
420
+ block_border_color="#e5e5e5",
421
+ input_border_width="1px",
422
+ input_border_color="#d4d4d4",
423
+ input_background_fill="#ffffff",
424
  )
425
+ with gr.Blocks(
426
+ title="Scenema Audio",
427
+ theme=theme,
428
+ css="footer {display: none !important}",
429
+ ) as demo:
430
+ gr.Markdown(
431
+ "# Scenema Audio\n"
432
+ "Zero-shot expressive voice cloning and speech generation. "
433
+ "Describe how a voice sounds and feels, write what it should say, "
434
+ "and the model generates a full vocal performance.\n\n"
435
+ "Built by [Scenema AI](https://scenema.ai), the AI filmmaking platform. "
436
+ "**[GitHub](https://github.com/ScenemaAI/scenema-audio)** | "
437
+ "**[Demos & Samples](https://scenema.ai/audio)**"
438
+ )
439
+
440
+ with gr.Tabs():
441
+ # ── Generate tab ──────────────────────────────
442
+ with gr.Tab("Generate"):
443
+ with gr.Row():
444
+ with gr.Column(scale=1):
445
+ gen_voice = gr.Textbox(
446
+ label="Voice Description",
447
+ placeholder="Describe the voice: age, gender, accent, emotional quality, delivery style...",
448
+ lines=3,
449
+ )
450
+ gen_text = gr.Textbox(
451
+ label="Speech Text",
452
+ placeholder="Write what the voice should say. Use <action> tags for stage directions and <sound> tags for environmental audio.",
453
+ lines=6,
454
+ )
455
+ gen_language = gr.Dropdown(
456
+ choices=LANGUAGE_CHOICES,
457
+ label="Language",
458
+ info="The model has only been tested with a limited set of languages. The language tag here is used for Whisper validation.",
459
+ value="English (en)",
460
+ )
461
+ gen_scene = gr.Textbox(
462
+ label="Scene (optional)",
463
+ placeholder="Environmental context: rain, office hum, crowd noise...",
464
+ max_lines=1,
465
+ )
466
+ with gr.Row():
467
+ gen_shot = gr.Dropdown(
468
+ ["closeup", "wide", "scene"],
469
+ label="Shot Mode",
470
+ value="closeup",
471
+ )
472
+ gen_seed = gr.Number(
473
+ label="Seed",
474
+ value=-1,
475
+ precision=0,
476
+ )
477
+ gen_pace = gr.Slider(
478
+ minimum=0.5,
479
+ maximum=3.0,
480
+ value=1.5,
481
+ step=0.1,
482
+ label="Pace",
483
+ )
484
+ with gr.Row():
485
+ gen_sfx = gr.Checkbox(
486
+ label="Background SFX",
487
+ value=False,
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",
495
+ value=False,
496
+ )
497
+ gen_btn = gr.Button("Generate", variant="primary")
498
+
499
+ with gr.Column(scale=1):
500
+ gen_audio = gr.Audio(label="Output", type="numpy")
501
+ gen_meta = gr.JSON(label="Metadata")
502
+ gen_xml = gr.Code(
503
+ label="Generated XML",
504
+ language="html",
505
+ interactive=False,
506
+ )
507
+
508
+ def _auto_sfx(text, shot):
509
+ return "<sound>" in text or shot in ("wide", "scene")
510
+
511
+ gen_text.change(
512
+ fn=_auto_sfx,
513
+ inputs=[gen_text, gen_shot],
514
+ outputs=[gen_sfx],
515
+ )
516
+ gen_shot.change(
517
+ fn=_auto_sfx,
518
+ inputs=[gen_text, gen_shot],
519
+ outputs=[gen_sfx],
520
+ )
521
+
522
+ gen_btn.click(
523
+ fn=generate,
524
+ inputs=[
525
+ gen_voice, gen_text, gen_scene,
526
+ gen_language, gen_shot, gen_seed, gen_pace,
527
+ gen_sfx, gen_validate, gen_skip_vc,
528
+ ],
529
+ outputs=[gen_audio, gen_meta, gen_xml],
530
+ )
531
+
532
+ gr.Examples(
533
+ examples=GENERATE_EXAMPLES,
534
+ inputs=[
535
+ gen_voice, gen_text, gen_scene,
536
+ gen_language, gen_shot,
537
+ ],
538
+ label="Preset Prompts",
539
+ )
540
+
541
+ # ── Voice Design tab ──────────────────────────
542
+ with gr.Tab("Voice Design"):
543
+ gr.Markdown(
544
+ "Preview a voice with a 15-second sample. "
545
+ "Use this to iterate on voice descriptions quickly before "
546
+ "generating full-length audio."
547
+ )
548
  with gr.Row():
549
+ with gr.Column(scale=1):
550
+ vd_voice = gr.Textbox(
551
+ label="Voice Description",
552
+ placeholder="Describe the voice you want to hear...",
553
+ lines=3,
554
+ )
555
+ vd_text = gr.Textbox(
556
+ label="Sample Text",
557
+ placeholder="A sentence or two for the voice to perform.",
558
+ lines=3,
559
+ )
560
+ vd_language = gr.Dropdown(
561
+ choices=LANGUAGE_CHOICES,
562
+ label="Language",
563
+ info="Sets the language tag for Whisper validation.",
564
+ value="English (en)",
565
+ )
566
+ vd_scene = gr.Textbox(
567
+ label="Scene (optional)",
568
+ placeholder="Environmental context...",
569
+ max_lines=1,
570
+ )
571
+ vd_seed = gr.Number(
572
+ label="Seed",
573
+ value=-1,
574
+ precision=0,
575
+ )
576
+ vd_btn = gr.Button(
577
+ "Preview Voice", variant="primary"
578
+ )
579
+
580
+ with gr.Column(scale=1):
581
+ vd_audio = gr.Audio(label="Voice Preview", type="numpy")
582
+ vd_meta = gr.JSON(label="Metadata")
583
+ vd_xml = gr.Code(
584
+ label="Generated XML",
585
+ language="html",
586
+ interactive=False,
587
+ )
588
+
589
+ vd_btn.click(
590
+ fn=voice_design,
591
+ inputs=[
592
+ vd_voice, vd_text, vd_scene,
593
+ vd_language, vd_seed,
594
+ ],
595
+ outputs=[vd_audio, vd_meta, vd_xml],
596
+ )
597
+
598
+ gr.Examples(
599
+ examples=VOICE_DESIGN_EXAMPLES,
600
+ inputs=[
601
+ vd_voice, vd_text, vd_scene,
602
+ vd_language,
603
+ ],
604
+ label="Preset Voices",
605
+ )
606
+
607
+ # ── Voice Cloning tab ─────────────────────────
608
+ with gr.Tab("Voice Cloning"):
609
+ gr.Markdown(
610
+ "Upload a few seconds of reference audio to clone a voice. "
611
+ "The reference provides identity only. Emotional performance "
612
+ "comes from the voice description and action tags."
613
+ )
614
  with gr.Row():
615
+ with gr.Column(scale=1):
616
+ vc_voice = gr.Textbox(
617
+ label="Voice Description",
618
+ placeholder="Describe the performance style (emotion, pacing, intensity). The reference audio handles identity.",
619
+ lines=3,
620
+ )
621
+ vc_text = gr.Textbox(
622
+ label="Speech Text",
623
+ placeholder="Write what the voice should say...",
624
+ lines=6,
625
+ )
626
+ vc_ref = gr.Audio(
627
+ label="Reference Voice (upload a few seconds of clean speech)",
628
+ type="filepath",
629
+ )
630
+ vc_language = gr.Dropdown(
631
+ choices=LANGUAGE_CHOICES,
632
+ label="Language",
633
+ info="Sets the language tag for Whisper validation.",
634
+ value="English (en)",
635
+ )
636
+ vc_scene = gr.Textbox(
637
+ label="Scene (optional)",
638
+ placeholder="Environmental context...",
639
+ max_lines=1,
640
+ )
641
+ with gr.Row():
642
+ vc_shot = gr.Dropdown(
643
+ ["closeup", "wide", "scene"],
644
+ label="Shot Mode",
645
+ value="closeup",
646
+ )
647
+ vc_seed = gr.Number(
648
+ label="Seed",
649
+ value=-1,
650
+ precision=0,
651
+ )
652
+ vc_pace = gr.Slider(
653
+ minimum=0.5,
654
+ maximum=3.0,
655
+ value=1.5,
656
+ step=0.1,
657
+ label="Pace",
658
+ )
659
+ with gr.Row():
660
+ vc_steps = gr.Slider(
661
+ minimum=10,
662
+ maximum=50,
663
+ value=25,
664
+ step=1,
665
+ label="VC Diffusion Steps",
666
+ )
667
+ vc_cfg = gr.Slider(
668
+ minimum=0.0,
669
+ maximum=1.0,
670
+ value=0.5,
671
+ step=0.05,
672
+ label="VC Guidance Rate",
673
+ )
674
+ with gr.Row():
675
+ vc_sfx = gr.Checkbox(
676
+ label="Background SFX",
677
+ value=False,
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
+
685
+ with gr.Column(scale=1):
686
+ vc_audio = gr.Audio(label="Output", type="numpy")
687
+ vc_meta = gr.JSON(label="Metadata")
688
+ vc_xml = gr.Code(
689
+ label="Generated XML",
690
+ language="html",
691
+ interactive=False,
692
+ )
693
+
694
+ def _auto_sfx_vc(text, shot):
695
+ return "<sound>" in text or shot in ("wide", "scene")
696
+
697
+ vc_text.change(
698
+ fn=_auto_sfx_vc,
699
+ inputs=[vc_text, vc_shot],
700
+ outputs=[vc_sfx],
701
+ )
702
+ vc_shot.change(
703
+ fn=_auto_sfx_vc,
704
+ inputs=[vc_text, vc_shot],
705
+ outputs=[vc_sfx],
706
+ )
707
+
708
+ vc_btn.click(
709
+ fn=voice_clone,
710
+ inputs=[
711
+ vc_voice, vc_text, vc_scene,
712
+ vc_language, vc_shot, vc_ref, vc_seed,
713
+ vc_pace, vc_steps, vc_cfg, vc_sfx, vc_validate,
714
+ ],
715
+ outputs=[vc_audio, vc_meta, vc_xml],
716
+ )
717
+
718
+ # ── Advanced tab ──────────────────────────────
719
+ with gr.Tab("Advanced (Raw XML)"):
720
+ gr.Markdown(
721
+ "Write the full `<speak>` XML prompt directly. "
722
+ "See the "
723
+ "[prompt format docs](https://github.com/ScenemaAI/scenema-audio#prompt-format) "
724
+ "for the full specification."
725
+ )
726
  with gr.Row():
727
+ with gr.Column(scale=1):
728
+ raw_xml = gr.Code(
729
+ label="Prompt XML",
730
+ language="html",
731
+ value='<speak voice="A warm male voice, measured pacing." gender="male">\nHello world.\n</speak>',
732
+ lines=12,
733
+ )
734
+ with gr.Row():
735
+ raw_mode = gr.Radio(
736
+ ["generate", "voice_design"],
737
+ label="Mode",
738
+ value="generate",
739
+ )
740
+ raw_seed = gr.Number(
741
+ label="Seed",
742
+ value=-1,
743
+ precision=0,
744
+ )
745
+ raw_pace = gr.Slider(
746
+ minimum=0.5,
747
+ maximum=3.0,
748
+ value=1.5,
749
+ step=0.1,
750
+ label="Pace",
751
+ )
752
+ with gr.Row():
753
+ raw_sfx = gr.Checkbox(
754
+ label="Background SFX",
755
+ value=False,
756
+ )
757
+ raw_validate = gr.Checkbox(
758
+ label="Whisper Validation",
759
+ value=True,
760
+ )
761
+ raw_skip_vc = gr.Checkbox(
762
+ label="Skip VC",
763
+ value=False,
764
+ )
765
+ raw_btn = gr.Button("Generate", variant="primary")
766
+
767
+ with gr.Column(scale=1):
768
+ raw_audio = gr.Audio(label="Output", type="numpy")
769
+ raw_meta = gr.JSON(label="Metadata")
770
+
771
+ raw_btn.click(
772
+ fn=generate_raw,
773
+ inputs=[
774
+ raw_xml, raw_mode, raw_seed, raw_pace,
775
+ raw_sfx, raw_validate, raw_skip_vc,
776
+ ],
777
+ outputs=[raw_audio, raw_meta],
778
  )
 
 
 
 
 
 
 
 
 
 
 
 
779
 
780
+ return demo
781
+
 
 
 
 
 
 
782
 
783
+ # ── Standalone entry point ────────────────────────────────────
784
 
785
  if __name__ == "__main__":
786
+ demo = create_demo()
787
+ demo.launch(
788
+ server_name="0.0.0.0",
789
+ server_port=int(os.environ.get("GRADIO_PORT", "7860")),
790
+ share=os.environ.get("GRADIO_SHARE") == "1",
791
+ )