Offexb commited on
Commit
ea2bc01
·
verified ·
1 Parent(s): a763286

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +249 -384
app.py CHANGED
@@ -1,475 +1,340 @@
 
1
  import os
2
- import sys
3
  import subprocess
 
4
  import tempfile
 
5
  from pathlib import Path
 
6
 
7
  import gradio as gr
8
  import spaces
9
- import torch
10
-
11
-
12
- # ============================================================
13
- # CONFIG
14
- # ============================================================
15
 
16
  MODEL_ID = "IndexTeam/IndexTTS-2"
17
  MODEL_DIR = Path("/tmp/index-tts-2")
 
18
 
19
  os.environ.setdefault("HF_HOME", "/tmp/huggingface")
20
  os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
21
 
 
 
22
  _tts = None
23
 
24
 
25
- # ============================================================
26
- # INSTALL OFFICIAL INDEXTTS
27
- # ============================================================
28
-
29
- def install_runtime_dependencies():
30
- """
31
- Install IndexTTS and its pinned transformers version
32
- without allowing pip to modify the HF Space dependency tree.
33
- """
34
-
35
- marker = Path("/tmp/index_tts_installed")
36
-
37
- if marker.exists():
38
  return
39
 
40
- print("Installing official IndexTTS...")
41
-
42
- # Official IndexTTS source
43
- subprocess.run(
44
- [
45
- sys.executable,
46
- "-m",
47
- "pip",
48
- "install",
49
- "--no-cache-dir",
50
- "--no-deps",
51
- "git+https://github.com/index-tts/index-tts.git",
52
- ],
53
- check=True,
54
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- # IndexTTS uses Transformers 4.52.1.
57
- # Install WITHOUT dependencies because HF Space already
58
- # provides a newer huggingface-hub required by Gradio.
59
- subprocess.run(
60
- [
61
- sys.executable,
62
- "-m",
63
- "pip",
64
- "install",
65
- "--no-cache-dir",
66
- "--no-deps",
67
- "transformers==4.52.1",
68
- ],
69
- check=True,
70
- )
71
 
72
- marker.touch()
 
 
 
 
 
 
 
73
 
74
- print("IndexTTS installation complete.")
75
 
 
 
 
 
 
 
 
76
 
77
- # Install before importing indextts
78
- install_runtime_dependencies()
 
79
 
 
 
 
80
 
81
- # ============================================================
82
- # LOAD MODEL
83
- # ============================================================
84
 
85
- def load_model():
86
 
 
87
  global _tts
88
 
89
  if _tts is not None:
90
  return _tts
91
 
92
- if not torch.cuda.is_available():
93
- raise RuntimeError(
94
- "CUDA GPU available नहीं है। "
95
- "HF Space में ZeroGPU hardware select करें।"
96
- )
97
 
98
- print("Downloading IndexTTS-2 model...")
99
-
100
- from huggingface_hub import snapshot_download
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- MODEL_DIR.mkdir(
103
- parents=True,
104
- exist_ok=True
105
- )
106
 
107
- snapshot_download(
108
- repo_id=MODEL_ID,
109
- local_dir=str(MODEL_DIR),
110
- )
111
 
112
- print("Loading IndexTTS-2...")
 
 
 
 
113
 
114
- from indextts.infer_v2 import IndexTTS2
115
 
116
- _tts = IndexTTS2(
117
- cfg_path=str(MODEL_DIR / "config.yaml"),
118
- model_dir=str(MODEL_DIR),
119
 
120
- use_fp16=True,
 
121
 
122
- device="cuda",
 
123
 
124
- use_cuda_kernel=False,
125
- use_deepspeed=False,
126
- use_accel=False,
127
- use_torch_compile=False,
128
 
129
- use_qwen_emo=True,
130
- )
131
 
132
- print("IndexTTS-2 loaded successfully.")
 
 
 
 
133
 
134
- return _tts
135
 
 
 
 
 
 
 
136
 
137
- # ============================================================
138
- # GENERATION
139
- # ============================================================
140
 
141
  @spaces.GPU(duration=180)
142
- def generate_voice(
143
- reference_audio,
144
- text,
145
- emotion_audio,
146
- emotion_strength,
147
- use_text_emotion,
 
148
  seed,
149
  ):
150
- # --------------------------------------------------------
151
- # Validate reference audio
152
- # --------------------------------------------------------
153
-
154
- if not reference_audio:
155
- raise gr.Error(
156
- "Reference voice upload करें।"
157
- )
158
-
159
- # --------------------------------------------------------
160
- # Validate text
161
- # --------------------------------------------------------
162
-
163
- if not text or not text.strip():
164
- raise gr.Error(
165
- "English text लिखें।"
166
- )
167
 
168
- # --------------------------------------------------------
169
- # Permission
170
- # --------------------------------------------------------
171
 
172
- # Permission is checked by the UI wrapper below.
173
- # --------------------------------------------------------
 
 
174
 
175
  try:
176
- seed = int(seed)
177
- except Exception:
178
- seed = 1234
179
-
180
- # --------------------------------------------------------
181
- # Seed
182
- # --------------------------------------------------------
183
-
184
- torch.manual_seed(seed)
185
-
186
- if torch.cuda.is_available():
187
- torch.cuda.manual_seed_all(seed)
188
-
189
- # --------------------------------------------------------
190
- # Load model
191
- # --------------------------------------------------------
192
 
193
  try:
194
- tts = load_model()
 
 
195
 
196
- except Exception as e:
197
- raise gr.Error(
198
- "Model loading failed:\n"
199
- + str(e)
200
- )
201
 
202
- # --------------------------------------------------------
203
- # Output path
204
- # --------------------------------------------------------
205
 
206
- output_path = (
207
- Path(tempfile.gettempdir())
208
- / "index_tts2_output.wav"
209
- )
210
 
211
- # Remove previous file
212
  if output_path.exists():
213
- try:
214
- output_path.unlink()
215
- except Exception:
216
- pass
217
-
218
- # --------------------------------------------------------
219
- # Inference arguments
220
- # --------------------------------------------------------
221
-
222
- kwargs = {
223
- "spk_audio_prompt": reference_audio,
224
- "text": text.strip(),
225
- "output_path": str(output_path),
226
- "verbose": False,
227
- "use_random": False,
228
- }
229
-
230
- # --------------------------------------------------------
231
- # Emotion reference
232
- # --------------------------------------------------------
233
-
234
- if emotion_audio:
235
-
236
- kwargs["emo_audio_prompt"] = emotion_audio
237
-
238
- kwargs["emo_alpha"] = float(
239
- emotion_strength
240
- )
241
-
242
- # --------------------------------------------------------
243
- # Text emotion
244
- # --------------------------------------------------------
245
-
246
- if use_text_emotion:
247
-
248
- kwargs["use_emo_text"] = True
249
-
250
- kwargs["emo_alpha"] = min(
251
- float(emotion_strength),
252
- 0.6
253
- )
254
-
255
- # --------------------------------------------------------
256
- # Generate
257
- # --------------------------------------------------------
258
-
259
- print("Generating voice...")
260
 
261
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
- tts.infer(
264
- **kwargs
265
- )
266
-
267
- except Exception as e:
 
 
268
 
269
- raise gr.Error(
270
- "IndexTTS-2 inference failed:\n"
271
- + str(e)
272
- )
273
 
274
- # --------------------------------------------------------
275
- # Check output
276
- # --------------------------------------------------------
277
 
278
- if not output_path.exists():
 
 
 
 
279
 
280
- raise gr.Error(
281
- "Audio output नहीं बना।"
282
- )
283
 
284
- print(
285
- "Generated:",
286
- output_path
 
287
  )
288
 
289
- return str(output_path)
290
-
291
-
292
- # ============================================================
293
- # PERMISSION WRAPPER
294
- # ============================================================
295
-
296
- def guarded_generate(
297
- reference_audio,
298
- text,
299
- emotion_audio,
300
- emotion_strength,
301
- use_text_emotion,
302
- seed,
303
- permission,
304
- ):
305
-
306
- if not permission:
307
-
308
- raise gr.Error(
309
- "पहले confirm करें कि आपको इस reference voice "
310
- "का इस्तेमाल करने की अनुमति है।"
311
  )
312
 
313
- return generate_voice(
314
- reference_audio,
315
- text,
316
- emotion_audio,
317
- emotion_strength,
318
- use_text_emotion,
319
- seed,
320
  )
321
 
322
-
323
- # ============================================================
324
- # UI
325
- # ============================================================
326
-
327
- with gr.Blocks(
328
- title="Offex IndexTTS-2 Voice Clone"
329
- ) as demo:
330
-
331
- gr.Markdown(
332
- """
333
- # 🎙️ Offex IndexTTS-2 Voice Clone
334
-
335
- ### Real zero-shot voice cloning powered by IndexTTS-2
336
-
337
- Reference voice upload करें और English text को उसी voice
338
- के style में generate करें।
339
-
340
- > केवल ऐसी आवाज़ इस्तेमाल करें जिसके उपयोग की आपको अनुमति है।
341
- """
342
  )
343
 
344
- # --------------------------------------------------------
345
- # MAIN ROW
346
- # --------------------------------------------------------
347
-
348
  with gr.Row():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
- # ====================================================
351
- # LEFT
352
- # ====================================================
353
-
354
- with gr.Column():
355
-
356
- reference = gr.Audio(
357
- label="🎤 Reference Voice",
358
- sources=[
359
- "upload",
360
- "microphone",
361
- ],
362
- type="filepath",
363
- )
364
-
365
- text = gr.Textbox(
366
- label="📝 English Text",
367
- lines=7,
368
- placeholder=(
369
- "Hello! This is a voice cloning test. "
370
- "The generated voice should sound natural."
371
- ),
372
- )
373
-
374
- permission = gr.Checkbox(
375
- label=(
376
- "I have permission to use this "
377
- "reference voice."
378
- ),
379
- value=False,
380
- )
381
-
382
- gr.Markdown(
383
- "### 🎭 Emotion"
384
- )
385
-
386
- emotion = gr.Audio(
387
- label="Optional Emotion Reference",
388
- sources=[
389
- "upload",
390
- "microphone",
391
- ],
392
- type="filepath",
393
- )
394
-
395
- emotion_strength = gr.Slider(
396
- minimum=0,
397
- maximum=1,
398
- value=0.6,
399
- step=0.05,
400
- label="Emotion Strength",
401
- )
402
-
403
- text_emotion = gr.Checkbox(
404
- label="Use emotion information from text",
405
- value=False,
406
- )
407
-
408
- gr.Markdown(
409
- "### ⚙️ Generation"
410
- )
411
-
412
- seed = gr.Number(
413
- label="Seed",
414
- value=1234,
415
- precision=0,
416
- )
417
-
418
- button = gr.Button(
419
- "🎧 Generate Voice",
420
- variant="primary",
421
- size="lg",
422
- )
423
-
424
- # ====================================================
425
- # RIGHT
426
- # ====================================================
427
-
428
- with gr.Column():
429
-
430
- output = gr.Audio(
431
- label="🔊 Generated Voice",
432
- type="filepath",
433
- )
434
-
435
- gr.Markdown(
436
- """
437
- ### Tips
438
-
439
- - Reference voice साफ और कम noise वाली रखें।
440
- - English reference voice बेहतर परिणाम दे सकती है।
441
- - लगभग 10–30 सेकंड की अच्छी reference recording इस्तेमाल करें।
442
- - Emotion reference optional है।
443
- """
444
- )
445
-
446
- # --------------------------------------------------------
447
- # BUTTON
448
- # --------------------------------------------------------
449
-
450
- button.click(
451
- fn=guarded_generate,
452
 
 
 
453
  inputs=[
454
- reference,
455
  text,
456
- emotion,
 
457
  emotion_strength,
458
- text_emotion,
459
  seed,
460
- permission,
461
  ],
462
-
463
  outputs=output,
464
  )
465
 
466
 
467
- # ============================================================
468
- # LAUNCH
469
- # ============================================================
470
-
471
  if __name__ == "__main__":
472
-
473
- demo.queue(
474
- max_size=4
475
- ).launch()
 
1
+ import inspect
2
  import os
3
+ import random
4
  import subprocess
5
+ import sys
6
  import tempfile
7
+ import threading
8
  from pathlib import Path
9
+ from typing import Optional
10
 
11
  import gradio as gr
12
  import spaces
 
 
 
 
 
 
13
 
14
  MODEL_ID = "IndexTeam/IndexTTS-2"
15
  MODEL_DIR = Path("/tmp/index-tts-2")
16
+ OUTPUT_DIR = Path(tempfile.gettempdir()) / "index-tts-2-outputs"
17
 
18
  os.environ.setdefault("HF_HOME", "/tmp/huggingface")
19
  os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
20
 
21
+ _runtime_lock = threading.Lock()
22
+ _runtime_ready = False
23
  _tts = None
24
 
25
 
26
+ def _install_runtime_dependencies() -> None:
27
+ """Install the official IndexTTS runtime without changing Space SDK pins."""
28
+ global _runtime_ready
29
+ if _runtime_ready:
 
 
 
 
 
 
 
 
 
30
  return
31
 
32
+ with _runtime_lock:
33
+ if _runtime_ready:
34
+ return
35
+
36
+ commands = [
37
+ [
38
+ sys.executable,
39
+ "-m",
40
+ "pip",
41
+ "install",
42
+ "--no-cache-dir",
43
+ "--no-deps",
44
+ "tokenizers==0.21.0",
45
+ ],
46
+ [
47
+ sys.executable,
48
+ "-m",
49
+ "pip",
50
+ "install",
51
+ "--no-cache-dir",
52
+ "--no-deps",
53
+ "transformers==4.52.1",
54
+ ],
55
+ [
56
+ sys.executable,
57
+ "-m",
58
+ "pip",
59
+ "install",
60
+ "--no-cache-dir",
61
+ "--no-deps",
62
+ "git+https://github.com/index-tts/index-tts.git",
63
+ ],
64
+ ]
65
+
66
+ for command in commands:
67
+ subprocess.check_call(command)
68
+
69
+ _runtime_ready = True
70
+
71
+
72
+ def _download_model() -> Path:
73
+ """Download the model once, only when the first synthesis request arrives."""
74
+ from huggingface_hub import snapshot_download
75
 
76
+ MODEL_DIR.mkdir(parents=True, exist_ok=True)
77
+ marker = MODEL_DIR / ".snapshot-complete"
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
+ if not marker.exists():
80
+ snapshot_download(
81
+ repo_id=MODEL_ID,
82
+ local_dir=str(MODEL_DIR),
83
+ )
84
+ marker.touch()
85
+
86
+ return MODEL_DIR
87
 
 
88
 
89
+ def _find_config(model_dir: Path) -> str:
90
+ candidates = [
91
+ model_dir / "config.yaml",
92
+ model_dir / "config.yml",
93
+ model_dir / "configs" / "inference.yaml",
94
+ model_dir / "configs" / "config.yaml",
95
+ ]
96
 
97
+ for candidate in candidates:
98
+ if candidate.is_file():
99
+ return str(candidate)
100
 
101
+ discovered = sorted(model_dir.rglob("*.yaml")) + sorted(model_dir.rglob("*.yml"))
102
+ if discovered:
103
+ return str(discovered[0])
104
 
105
+ raise FileNotFoundError(f"No IndexTTS configuration file was found under {model_dir}.")
 
 
106
 
 
107
 
108
+ def _get_tts():
109
  global _tts
110
 
111
  if _tts is not None:
112
  return _tts
113
 
114
+ with _runtime_lock:
115
+ if _tts is not None:
116
+ return _tts
 
 
117
 
118
+ try:
119
+ import torch
120
+
121
+ if not torch.cuda.is_available():
122
+ raise RuntimeError("CUDA GPU is not available in this Space.")
123
+
124
+ _install_runtime_dependencies()
125
+ model_dir = _download_model()
126
+ from indextts.infer_v2 import IndexTTS2
127
+
128
+ _tts = IndexTTS2(
129
+ cfg_path=_find_config(model_dir),
130
+ model_dir=str(model_dir),
131
+ use_fp16=True,
132
+ device="cuda",
133
+ use_cuda_kernel=False,
134
+ use_deepspeed=False,
135
+ use_accel=False,
136
+ use_torch_compile=False,
137
+ use_qwen_emo=True,
138
+ )
139
+ return _tts
140
+ except Exception:
141
+ _tts = None
142
+ raise
143
 
 
 
 
 
144
 
145
+ def _validate_audio(path: Optional[str], label: str) -> str:
146
+ if not path:
147
+ raise gr.Error(f"{label} is required.")
 
148
 
149
+ audio = Path(path)
150
+ if not audio.is_file():
151
+ raise gr.Error(f"{label} could not be found.")
152
+ if audio.stat().st_size == 0:
153
+ raise gr.Error(f"{label} is empty.")
154
 
155
+ return str(audio)
156
 
 
 
 
157
 
158
+ def _set_seed(seed: int) -> None:
159
+ random.seed(seed)
160
 
161
+ try:
162
+ import numpy as np
163
 
164
+ np.random.seed(seed)
165
+ except ImportError:
166
+ pass
 
167
 
168
+ try:
169
+ import torch
170
 
171
+ torch.manual_seed(seed)
172
+ if torch.cuda.is_available():
173
+ torch.cuda.manual_seed_all(seed)
174
+ except ImportError:
175
+ pass
176
 
 
177
 
178
+ def _supported_kwargs(function, values: dict) -> dict:
179
+ """Keep compatibility with small upstream inference API changes."""
180
+ parameters = inspect.signature(function).parameters
181
+ if any(parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in parameters.values()):
182
+ return values
183
+ return {key: value for key, value in values.items() if key in parameters}
184
 
 
 
 
185
 
186
  @spaces.GPU(duration=180)
187
+ def synthesize(
188
+ reference_audio: Optional[str],
189
+ text: str,
190
+ permission: bool,
191
+ emotion_audio: Optional[str],
192
+ emotion_strength: float,
193
+ use_text_emotion: bool,
194
  seed,
195
  ):
196
+ if not permission:
197
+ raise gr.Error("Please confirm that you have permission to use the reference voice.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
+ reference_audio = _validate_audio(reference_audio, "Reference audio")
200
+ text = (text or "").strip()
 
201
 
202
+ if not text:
203
+ raise gr.Error("English text is required.")
204
+ if len(text) > 2000:
205
+ raise gr.Error("Please keep the English text to 2,000 characters or fewer.")
206
 
207
  try:
208
+ numeric_seed = float(seed)
209
+ if not numeric_seed.is_integer() or numeric_seed < 0:
210
+ raise ValueError
211
+ numeric_seed = int(numeric_seed)
212
+ except (TypeError, ValueError, OverflowError) as exc:
213
+ raise gr.Error("Seed must be a non-negative integer.") from exc
 
 
 
 
 
 
 
 
 
 
214
 
215
  try:
216
+ emotion_strength = float(emotion_strength)
217
+ except (TypeError, ValueError) as exc:
218
+ raise gr.Error("Emotion strength must be a number between 0 and 1.") from exc
219
 
220
+ if not 0 <= emotion_strength <= 1:
221
+ raise gr.Error("Emotion strength must be between 0 and 1.")
 
 
 
222
 
223
+ if not isinstance(use_text_emotion, bool):
224
+ raise gr.Error("Use text emotion must be a boolean.")
 
225
 
226
+ emotion_audio = _validate_audio(emotion_audio, "Emotion audio") if emotion_audio else None
227
+ output_dir = OUTPUT_DIR
228
+ output_dir.mkdir(parents=True, exist_ok=True)
229
+ output_path = output_dir / "index-tts-2-output.wav"
230
 
 
231
  if output_path.exists():
232
+ output_path.unlink()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
  try:
235
+ _set_seed(numeric_seed)
236
+ tts = _get_tts()
237
+
238
+ values = {
239
+ "spk_audio_prompt": reference_audio,
240
+ "text": text,
241
+ "output_path": str(output_path),
242
+ "use_random": False,
243
+ }
244
+
245
+ if emotion_audio:
246
+ values.update(
247
+ {
248
+ "emo_audio_prompt": emotion_audio,
249
+ "emo_alpha": emotion_strength,
250
+ }
251
+ )
252
 
253
+ if use_text_emotion:
254
+ values.update(
255
+ {
256
+ "use_emo_text": True,
257
+ "emo_alpha": min(emotion_strength, 0.6),
258
+ }
259
+ )
260
 
261
+ tts.infer(**_supported_kwargs(tts.infer, values))
 
 
 
262
 
263
+ if not output_path.is_file() or output_path.stat().st_size == 0:
264
+ raise RuntimeError("IndexTTS-2 returned without creating an audio file.")
 
265
 
266
+ return str(output_path)
267
+ except gr.Error:
268
+ raise
269
+ except Exception as exc:
270
+ raise gr.Error(f"Synthesis failed: {type(exc).__name__}: {exc}") from exc
271
 
 
 
 
272
 
273
+ with gr.Blocks(title="Offex IndexTTS-2 Voice Clone") as demo:
274
+ gr.Markdown(
275
+ "# Offex IndexTTS-2 Voice Clone\n"
276
+ "Generate English speech from a permitted reference voice."
277
  )
278
 
279
+ with gr.Row():
280
+ reference_audio = gr.Audio(
281
+ label="Reference voice",
282
+ sources=["upload", "microphone"],
283
+ type="filepath",
284
+ )
285
+ emotion_audio = gr.Audio(
286
+ label="Optional emotion reference",
287
+ sources=["upload", "microphone"],
288
+ type="filepath",
 
 
 
 
 
 
 
 
 
 
 
 
289
  )
290
 
291
+ text = gr.Textbox(
292
+ label="English text",
293
+ placeholder="Enter the English text to synthesize...",
294
+ lines=6,
 
 
 
295
  )
296
 
297
+ permission = gr.Checkbox(
298
+ label="I have permission to use this reference voice",
299
+ value=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  )
301
 
 
 
 
 
302
  with gr.Row():
303
+ emotion_strength = gr.Slider(
304
+ minimum=0,
305
+ maximum=1,
306
+ value=0.6,
307
+ step=0.05,
308
+ label="Emotion strength",
309
+ )
310
+ use_text_emotion = gr.Checkbox(
311
+ label="Use emotion information from text",
312
+ value=False,
313
+ )
314
+ seed = gr.Number(
315
+ label="Seed",
316
+ value=1234,
317
+ precision=0,
318
+ minimum=0,
319
+ )
320
 
321
+ generate = gr.Button("Generate Voice", variant="primary", size="lg")
322
+ output = gr.Audio(label="Generated voice", type="filepath", autoplay=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
+ generate.click(
325
+ fn=synthesize,
326
  inputs=[
327
+ reference_audio,
328
  text,
329
+ permission,
330
+ emotion_audio,
331
  emotion_strength,
332
+ use_text_emotion,
333
  seed,
 
334
  ],
 
335
  outputs=output,
336
  )
337
 
338
 
 
 
 
 
339
  if __name__ == "__main__":
340
+ demo.queue(max_size=4).launch()