LosCaquitos commited on
Commit
0d49186
·
verified ·
1 Parent(s): 4415153

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +569 -594
app.py CHANGED
@@ -1,651 +1,626 @@
1
  """
2
- RVC Voice Conversion – Simplified & Working Version
3
- Suporta: MP3, WAV, FLAC, OGG, M4A, MP4, MKV, WebM, AVI, MOV, FLV
4
- Gradio 6.0 Compatible
5
- Com aba Jobs para mostrar status
6
  """
7
  from __future__ import annotations
8
 
9
  import os
10
  import subprocess
11
  import tempfile
12
- import shutil
13
  from pathlib import Path
14
- import json
15
- import uuid
16
- from datetime import datetime
17
- import traceback
18
- import threading
19
 
20
  import gradio as gr
21
- import numpy as np
22
- import librosa
23
- import soundfile as sf
24
- from scipy import signal
25
- import zipfile
26
-
27
- DEVICE = "cpu"
28
- DEVICE_LABEL = "CPU (Stable)"
29
-
30
- MODELS_DIR = Path("models")
31
- OUTPUTS_DIR = Path("outputs")
32
- JOBS_DIR = Path("conversion_jobs")
33
-
34
- MODELS_DIR.mkdir(exist_ok=True)
35
- OUTPUTS_DIR.mkdir(exist_ok=True)
36
- JOBS_DIR.mkdir(exist_ok=True)
37
-
38
- CONFIG = {
39
- "sample_rate": 16000,
40
- "n_fft": 800,
41
- "hop_length": 200,
42
- }
43
-
44
- AUDIO_FORMATS = ["wav", "mp3", "flac", "ogg", "m4a", "aac", "wma", "mp4", "mkv", "webm", "avi", "mov", "flv"]
45
- VIDEO_FORMATS = ["mp4", "mkv", "webm", "avi", "mov", "flv", "m4v"]
46
-
47
- # ============================================================================
48
- # JOB MANAGER PARA CONVERSÃO
49
- # ============================================================================
50
-
51
- class ConversionJobManager:
52
- def __init__(self):
53
- self.jobs = {}
54
- self.load_jobs()
55
-
56
- def load_jobs(self):
57
- """Carrega jobs salvos"""
58
- for job_file in JOBS_DIR.glob("*.json"):
59
- try:
60
- with open(job_file) as f:
61
- job = json.load(f)
62
- self.jobs[job["id"]] = job
63
- except:
64
- pass
65
-
66
- def create_job(self, job_type, filename):
67
- """Cria novo job"""
68
- job_id = str(uuid.uuid4())[:8]
69
- job = {
70
- "id": job_id,
71
- "type": job_type, # "audio" ou "video"
72
- "filename": filename,
73
- "status": "waiting", # waiting, converting, done
74
- "progress": 0,
75
- "created_at": datetime.now().isoformat(),
76
- "updated_at": datetime.now().isoformat(),
77
- "result_files": {
78
- "entrada": None,
79
- "entrada_acapella": None,
80
- "entrada_instrumental": None,
81
- "saida_acapella": None,
82
- "saida": None,
83
- "video_output": None,
84
- "zip": None,
85
- },
86
- "error": None,
87
- }
88
- self.jobs[job_id] = job
89
- self.save_job(job_id)
90
- return job_id
91
-
92
- def save_job(self, job_id):
93
- """Salva job"""
94
- job_file = JOBS_DIR / f"{job_id}.json"
95
- with open(job_file, "w") as f:
96
- json.dump(self.jobs[job_id], f, indent=2)
97
-
98
- def update_job(self, job_id, **kwargs):
99
- """Atualiza job"""
100
- if job_id in self.jobs:
101
- self.jobs[job_id].update(kwargs)
102
- self.jobs[job_id]["updated_at"] = datetime.now().isoformat()
103
- self.save_job(job_id)
104
-
105
- def get_job(self, job_id):
106
- return self.jobs.get(job_id)
107
-
108
- def list_jobs(self):
109
- """Retorna jobs ordenados por data (mais recentes primeiro)"""
110
- return sorted(self.jobs.values(), key=lambda x: x["updated_at"], reverse=True)
111
 
112
- job_manager = ConversionJobManager()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
- # ============================================================================
115
- # FUNÇÕES
116
- # ============================================================================
 
 
 
 
 
 
117
 
118
- def load_audio(path: str) -> tuple:
119
- """Carrega áudio"""
120
- try:
121
- y, sr = librosa.load(path, sr=16000, mono=True)
122
- return y, sr
123
- except Exception as e:
124
- print(f"Erro ao carregar áudio: {e}")
125
- return None, None
126
 
127
- def separate_vocals(audio: np.ndarray, sr: int) -> tuple:
128
- """Separa vocais e instrumentação"""
129
- try:
130
- sos = signal.butter(5, 300, 'hp', fs=sr, output='sos')
131
- vocals = signal.sosfilt(sos, audio)
132
- instrumental = audio - vocals * 0.5
133
- return vocals, instrumental
134
- except Exception as e:
135
- print(f"Erro ao separar vocais: {e}")
136
- return audio, np.zeros_like(audio)
137
-
138
- def apply_pitch_shift(audio: np.ndarray, sr: int, n_steps: int) -> np.ndarray:
139
- """Aplica pitch shift"""
140
- try:
141
- if n_steps == 0:
142
- return audio
143
- return librosa.effects.pitch_shift(audio, sr=sr, n_steps=n_steps)
144
- except Exception as e:
145
- print(f"Erro no pitch shift: {e}")
146
- return audio
147
-
148
- def apply_reverb_simple(audio: np.ndarray, sr: int, wet: float = 0.15) -> np.ndarray:
149
- """Aplica reverb"""
150
- try:
151
- delay_samples = int(sr * 0.05)
152
- delayed = np.concatenate([np.zeros(delay_samples), audio[:-delay_samples]])
153
- return audio * (1 - wet) + delayed * wet
154
- except Exception as e:
155
- print(f"Erro no reverb: {e}")
156
- return audio
157
-
158
- def apply_noise_reduction(audio: np.ndarray, strength: float = 0.5) -> np.ndarray:
159
- """Remove ruído"""
160
- try:
161
- threshold = np.percentile(np.abs(audio), strength * 100)
162
- audio_copy = audio.copy()
163
- audio_copy[np.abs(audio_copy) < threshold] *= 0.5
164
- return audio_copy
165
- except Exception as e:
166
- print(f"Erro na redução de ruído: {e}")
167
- return audio
168
-
169
- def normalize_audio(audio: np.ndarray) -> np.ndarray:
170
- """Normaliza volume"""
171
- if np.max(np.abs(audio)) > 0:
172
- return audio / np.max(np.abs(audio)) * 0.95
173
- return audio
174
 
175
  def extract_audio_from_video(video_path: str) -> str:
176
- """Extrai áudio de vídeo"""
 
 
 
177
  try:
178
- output_audio = Path(tempfile.gettempdir()) / f"temp_audio_{uuid.uuid4().hex[:8]}.wav"
179
-
180
- cmd = [
181
- "ffmpeg", "-i", video_path,
182
- "-q:a", "0", "-map", "a",
183
- "-y", str(output_audio)
184
- ]
185
-
186
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
187
-
188
- if result.returncode != 0:
189
- print(f"Erro FFmpeg: {result.stderr}")
190
- return None
191
-
192
- return str(output_audio)
193
  except Exception as e:
194
- print(f"Erro ao extrair áudio de vídeo: {e}")
195
- return None
196
 
197
- def convert_to_wav_16k(audio_path: str) -> str:
198
- """Converte para WAV 16kHz"""
199
- try:
200
- y, sr = librosa.load(audio_path, sr=16000, mono=True)
201
- output_path = Path(audio_path).with_suffix(".wav")
202
- sf.write(output_path, y, 16000)
203
- return str(output_path)
204
- except Exception as e:
205
- print(f"Erro ao converter: {e}")
206
- return None
207
-
208
- def convert_voice_simple(
209
- audio_path: str,
210
- pitch: int = 0,
211
- clean: bool = False,
212
- reverb: bool = False,
213
- job_id: str = None,
214
- ) -> dict:
215
- """Conversão de voz SIMPLIFICADA"""
216
-
217
- if job_id is None:
218
- job_id = str(uuid.uuid4())[:8]
219
-
220
- output_dir = OUTPUTS_DIR / job_id
221
- output_dir.mkdir(exist_ok=True)
222
-
223
  try:
224
- # Atualizar status
225
- if job_id:
226
- job_manager.update_job(job_id, status="converting", progress=10)
227
-
228
- # 1. Carregar áudio
229
- audio, sr = load_audio(audio_path)
230
- if audio is None:
231
- raise ValueError("Erro ao carregar áudio")
232
-
233
- # Salvar entrada original
234
- entrada_path = output_dir / "entrada.wav"
235
- sf.write(entrada_path, audio, sr)
236
-
237
- if job_id:
238
- job_manager.update_job(job_id, progress=20)
239
-
240
- # 2. Separar vocais e instrumentação
241
- vocals, instrumental = separate_vocals(audio, sr)
242
-
243
- # Salvar entrada acapella (vocais originais)
244
- entrada_acapella_path = output_dir / "entrada_acapella.wav"
245
- sf.write(entrada_acapella_path, vocals, sr)
246
-
247
- # Salvar entrada instrumental
248
- entrada_instrumental_path = output_dir / "entrada_instrumental.wav"
249
- sf.write(entrada_instrumental_path, instrumental, sr)
250
-
251
- if job_id:
252
- job_manager.update_job(job_id, progress=40)
253
-
254
- # 3. Processar vocais
255
- vocals_converted = vocals.copy()
256
 
257
- # Aplicar pitch shift
258
- if pitch != 0:
259
- vocals_converted = apply_pitch_shift(vocals_converted, sr, pitch)
260
 
261
- # Aplicar ruído reduction
262
- if clean:
263
- vocals_converted = apply_noise_reduction(vocals_converted, strength=0.5)
264
-
265
- # Aplicar reverb
266
- if reverb:
267
- vocals_converted = apply_reverb_simple(vocals_converted, sr, wet=0.15)
268
-
269
- # Normalizar
270
- vocals_converted = normalize_audio(vocals_converted)
271
-
272
- # Salvar saida acapella (vocais processados)
273
- saida_acapella_path = output_dir / "saida_acapella.wav"
274
- sf.write(saida_acapella_path, vocals_converted, sr)
275
-
276
- if job_id:
277
- job_manager.update_job(job_id, progress=70)
278
-
279
- # 4. Mixar vocal processado com instrumental original
280
- min_len = min(len(vocals_converted), len(instrumental))
281
- mix = vocals_converted[:min_len] + instrumental[:min_len]
282
- mix = normalize_audio(mix)
283
-
284
- # Salvar mix final
285
- saida_path = output_dir / "saida.wav"
286
- sf.write(saida_path, mix, sr)
287
-
288
- if job_id:
289
- job_manager.update_job(job_id, progress=85)
290
-
291
- return {
292
- "status": "success",
293
- "entrada": str(entrada_path),
294
- "entrada_acapella": str(entrada_acapella_path),
295
- "entrada_instrumental": str(entrada_instrumental_path),
296
- "saida_acapella": str(saida_acapella_path),
297
- "saida": str(saida_path),
298
- "output_dir": str(output_dir),
299
- }
300
-
301
- except Exception as e:
302
- print(f"Erro na conversão: {e}")
303
- traceback.print_exc()
304
- if job_id:
305
- job_manager.update_job(job_id, status="error", error=str(e))
306
- return {
307
- "status": "error",
308
- "error": str(e),
309
- }
310
-
311
- def process_video_simple(
312
- video_path: str,
313
- pitch: int = 0,
314
- clean: bool = False,
315
- reverb: bool = False,
316
- job_id: str = None,
317
- ) -> dict:
318
- """Processamento de vídeo SIMPLIFICADO"""
319
-
320
- if job_id is None:
321
- job_id = str(uuid.uuid4())[:8]
322
-
323
- output_dir = OUTPUTS_DIR / job_id
324
- output_dir.mkdir(exist_ok=True)
325
-
326
- try:
327
- # 1. Extrair áudio do vídeo
328
- if job_id:
329
- job_manager.update_job(job_id, status="converting", progress=5)
330
-
331
- temp_audio = output_dir / "temp_audio.wav"
332
- cmd = [
333
- "ffmpeg", "-i", video_path, "-q:a", "0", "-map", "a",
334
- "-y", str(temp_audio)
335
- ]
336
- subprocess.run(cmd, check=True, capture_output=True, timeout=600)
337
-
338
- if job_id:
339
- job_manager.update_job(job_id, progress=15)
340
-
341
- # 2. Processar áudio
342
- result = convert_voice_simple(str(temp_audio), pitch=pitch, clean=clean, reverb=reverb, job_id=job_id)
343
-
344
- if result["status"] != "success":
345
- return result
346
-
347
- if job_id:
348
- job_manager.update_job(job_id, progress=75)
349
-
350
- # 3. Remixar vídeo com áudio novo
351
- output_video = output_dir / "saida_video.mp4"
352
  cmd = [
353
- "ffmpeg", "-i", video_path, "-i", result["saida"],
354
- "-c:v", "copy", "-c:a", "aac", "-map", "0:v:0", "-map", "1:a:0",
355
- "-y", str(output_video)
 
 
 
356
  ]
357
- subprocess.run(cmd, check=True, capture_output=True, timeout=600)
358
 
359
- # Limpar temporário
360
- temp_audio.unlink(missing_ok=True)
361
-
362
- if job_id:
363
- job_manager.update_job(job_id, progress=90)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
 
365
- return {
366
- **result,
367
- "video_output": str(output_video),
368
- }
369
-
370
  except Exception as e:
371
- print(f"Erro no processamento de vídeo: {e}")
372
- traceback.print_exc()
373
- if job_id:
374
- job_manager.update_job(job_id, status="error", error=str(e))
375
- return {
376
- "status": "error",
377
- "error": str(e),
378
- }
379
 
380
- def create_zip(output_dir: Path) -> str:
381
- """Cria ZIP com todos os arquivos"""
382
- try:
383
- zip_path = output_dir.parent / f"{output_dir.name}.zip"
384
-
385
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
386
- for file in sorted(output_dir.glob("*.wav")) + sorted(output_dir.glob("*.mp4")):
387
- zf.write(file, arcname=file.name)
388
-
389
- return str(zip_path)
390
- except Exception as e:
391
- print(f"Erro ao criar ZIP: {e}")
392
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
 
394
- def submit_audio_async(audio_mic, audio_file, pitch, clean, reverb):
395
- """Handler de conversão de áudio em thread"""
396
- audio_path = audio_mic or audio_file
397
-
398
- if not audio_path:
399
- return "❌ Nenhum áudio fornecido"
400
-
401
- # Criar job
402
- filename = Path(audio_path).name
403
- job_id = job_manager.create_job("audio", filename)
404
-
405
- # Processar em thread
406
- def process():
407
- try:
408
- result = convert_voice_simple(audio_path, pitch=pitch, clean=clean, reverb=reverb, job_id=job_id)
409
-
410
- if result["status"] != "success":
411
- job_manager.update_job(job_id, status="error", error=result.get("error", "Erro desconhecido"))
412
- return
413
-
414
- zip_path = create_zip(Path(result["output_dir"]))
415
-
416
- job_manager.update_job(
417
- job_id,
418
- status="done",
419
- progress=100,
420
- result_files={
421
- "entrada": result["entrada"],
422
- "entrada_acapella": result["entrada_acapella"],
423
- "entrada_instrumental": result["entrada_instrumental"],
424
- "saida_acapella": result["saida_acapella"],
425
- "saida": result["saida"],
426
- "zip": zip_path,
427
- }
428
- )
429
- except Exception as e:
430
- job_manager.update_job(job_id, status="error", error=str(e))
431
-
432
- thread = threading.Thread(target=process, daemon=False)
433
- thread.start()
434
-
435
- return f"✅ Conversão iniciada! Job ID: {job_id}\n\nVá para a aba 'Jobs' para acompanhar."
436
-
437
- def submit_video_async(video_file, pitch, clean, reverb):
438
- """Handler de conversão de vídeo em thread"""
439
-
440
- if not video_file:
441
- return "❌ Nenhum vídeo fornecido"
442
-
443
- # Criar job
444
- filename = Path(video_file).name
445
- job_id = job_manager.create_job("video", filename)
446
-
447
- # Processar em thread
448
- def process():
449
- try:
450
- result = process_video_simple(video_file, pitch=pitch, clean=clean, reverb=reverb, job_id=job_id)
451
-
452
- if result["status"] != "success":
453
- job_manager.update_job(job_id, status="error", error=result.get("error", "Erro desconhecido"))
454
- return
455
-
456
- zip_path = create_zip(Path(result["output_dir"]))
457
-
458
- job_manager.update_job(
459
- job_id,
460
- status="done",
461
- progress=100,
462
- result_files={
463
- "entrada": result["entrada"],
464
- "entrada_acapella": result["entrada_acapella"],
465
- "entrada_instrumental": result["entrada_instrumental"],
466
- "saida_acapella": result["saida_acapella"],
467
- "saida": result["saida"],
468
- "video_output": result.get("video_output"),
469
- "zip": zip_path,
470
- }
471
  )
472
- except Exception as e:
473
- job_manager.update_job(job_id, status="error", error=str(e))
474
-
475
- thread = threading.Thread(target=process, daemon=False)
476
- thread.start()
477
-
478
- return f"✅ Conversão de vídeo iniciada! Job ID: {job_id}\n\nVá para a aba 'Jobs' para acompanhar."
479
-
480
- def refresh_jobs():
481
- """Retorna lista de jobs com status"""
482
- job_manager.load_jobs()
483
- jobs = job_manager.list_jobs()
484
-
485
- if not jobs:
486
- return "Nenhuma conversão realizada ainda."
487
-
488
- output = ""
489
- for job in jobs:
490
- status_icon = {
491
- "waiting": "⏳ Esperando",
492
- "converting": "🔄 Convertendo",
493
- "done": " Concluído",
494
- "error": "❌ Erro",
495
- }.get(job["status"], "❓")
496
-
497
- output += f"**{status_icon}** - {job['filename']} (ID: `{job['id']}`)\n"
498
- output += f"- Tipo: {job['type'].upper()}\n"
499
- output += f"- Progresso: {job['progress']}%\n"
500
- output += f"- Criado: {job['created_at']}\n"
501
-
502
- if job["status"] == "done":
503
- output += f"- 📦 ZIP: Disponível para download\n"
504
- elif job["status"] == "error":
505
- output += f"- Erro: {job['error']}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
506
 
507
- output += "\n"
508
-
509
- return output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
 
511
- # ============================================================================
512
- # GRADIO UI (GRADIO 6.0 COMPATIBLE)
513
- # ============================================================================
514
 
515
- with gr.Blocks(title="RVC Voice Conversion") as demo:
516
-
517
- gr.HTML("""
518
- <div style="text-align: center; padding: 20px;">
519
- <h1>🎙️ RVC Voice Conversion</h1>
520
- <p>Conversão de voz simplificada · 5 áudios de saída · Processamento de vídeo</p>
521
- <p style="color: #666;">CPU (Estável)</p>
522
  </div>
 
523
  """)
524
-
525
  with gr.Tabs():
526
-
527
- # ── TAB 1: AUDIO ──────────────────────────────────────────
528
- with gr.Tab("🎤 Converter Áudio"):
529
-
530
  with gr.Row():
531
  with gr.Column(scale=1):
532
- gr.Markdown("### 🔊 Entrada")
 
 
 
 
 
 
 
 
533
  with gr.Tabs():
534
  with gr.Tab("🎙️ Microfone"):
535
- audio_mic = gr.Audio(sources=["microphone"], type="filepath", label="Gravar")
536
- with gr.Tab("📁 Upload"):
537
- audio_file = gr.Audio(sources=["upload"], type="filepath", label="Upload de áudio")
 
 
 
 
 
 
 
 
538
 
539
- gr.Markdown("### ⚙️ Configurações")
540
- pitch = gr.Slider(-24, 24, value=0, step=1, label="Pitch Shift (semitons)")
541
- clean = gr.Checkbox(value=False, label="Redução de Ruído")
542
- reverb = gr.Checkbox(value=False, label="Reverb")
 
 
 
543
 
544
- convert_btn = gr.Button("🚀 Converter Áudio", variant="primary", size="lg")
545
- status_output = gr.Textbox(label="Status", interactive=False)
546
-
547
- # ─��� TAB 2: VIDEO ──────────────────────────────────────────
548
- with gr.Tab("🎬 Converter Vídeo"):
549
-
550
- with gr.Row():
 
 
 
 
 
 
551
  with gr.Column(scale=1):
552
- gr.Markdown("### 🎥 Entrada")
553
- gr.Markdown("Suporta: MP4, MKV, WebM, AVI, MOV, FLV")
554
- video_file = gr.Video(label="Upload de vídeo", sources=["upload"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
 
556
- gr.Markdown("### ⚙Configurações")
557
- video_pitch = gr.Slider(-24, 24, value=0, step=1, label="Pitch Shift (semitons)")
558
- video_clean = gr.Checkbox(value=False, label="Redução de Ruído")
559
- video_reverb = gr.Checkbox(value=False, label="Reverb")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
 
561
- video_btn = gr.Button("🎬 Converter Vídeo", variant="primary", size="lg")
562
- video_status_output = gr.Textbox(label="Status", interactive=False)
563
-
564
- # ── TAB 3: JOBS ──────────────────────────────────────────
565
- with gr.Tab("📋 JOBS"):
566
- gr.Markdown("### 📊 Status de Conversões")
567
- gr.Markdown("Clique em **Refresh** ou espere auto-atualizar")
568
-
569
- refresh_btn = gr.Button("🔄 Refresh", size="lg", variant="primary")
570
- jobs_output = gr.Markdown()
571
-
572
- refresh_btn.click(refresh_jobs, outputs=jobs_output)
573
- demo.load(refresh_jobs, outputs=jobs_output)
574
-
575
- # Auto-atualizar a cada 3 segundos
576
- demo.load(
577
- lambda: (refresh_jobs(),),
578
- outputs=[jobs_output],
579
- every=3
580
- )
581
-
582
- gr.Markdown("### ⬇️ Clique na setinha para expandir e ver os 5 áudios")
583
-
584
- with gr.Accordion("📦 Resultados (Clique para expandir ⬇️)"):
585
- job_id_input = gr.Textbox(label="🔑 Cole o Job ID aqui para ver os resultados", placeholder="Ex: a1b2c3d4")
586
- show_results_btn = gr.Button("Mostrar Resultados", variant="primary")
587
-
588
- with gr.Group():
589
- gr.Markdown("### 🎵 Áudios de Entrada")
590
- entrada_aud = gr.Audio(label="entrada.wav", interactive=False)
591
- entrada_acap = gr.Audio(label="entrada_acapella.wav", interactive=False)
592
- entrada_inst = gr.Audio(label="entrada_instrumental.wav", interactive=False)
593
-
594
- with gr.Group():
595
- gr.Markdown("### 🎵 Áudios de Saída")
596
- saida_acap = gr.Audio(label="saida_acapella.wav", interactive=False)
597
- saida_aud = gr.Audio(label="saida.wav", interactive=False)
598
-
599
- with gr.Group():
600
- gr.Markdown("### 🎬 Vídeo de Saída")
601
- video_output = gr.Video(label="saida_video.mp4", interactive=False)
602
-
603
- with gr.Group():
604
- gr.Markdown("### 📥 Download")
605
- download_file = gr.File(label="Baixar ZIP com todos os arquivos")
606
-
607
- def show_job_results(job_id_str):
608
- if not job_id_str or not job_id_str.strip():
609
- return None, None, None, None, None, None, None
610
 
611
- job = job_manager.get_job(job_id_str.strip())
612
- if not job or job["status"] != "done":
613
- return None, None, None, None, None, None, None
 
614
 
615
- files = job["result_files"]
616
- return (
617
- files["entrada"],
618
- files["entrada_acapella"],
619
- files["entrada_instrumental"],
620
- files["saida_acapella"],
621
- files["saida"],
622
- files.get("video_output"),
623
- files["zip"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  )
625
-
626
- show_results_btn.click(
627
- show_job_results,
628
- inputs=job_id_input,
629
- outputs=[entrada_aud, entrada_acap, entrada_inst, saida_acap, saida_aud, video_output, download_file]
630
- )
631
-
632
- # Wire eventos
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
633
  convert_btn.click(
634
- submit_audio_async,
635
- inputs=[audio_mic, audio_file, pitch, clean, reverb],
636
- outputs=status_output,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
  )
638
 
639
- video_btn.click(
640
- submit_video_async,
641
- inputs=[video_file, video_pitch, video_clean, video_reverb],
642
- outputs=video_status_output,
 
 
 
 
643
  )
644
 
 
645
  if __name__ == "__main__":
 
646
  demo.launch(
647
- share=True,
648
  server_name="0.0.0.0",
649
- server_port=7860,
650
- show_error=True
 
 
651
  )
 
1
  """
2
+ RVC Voice Conversion – HuggingFace Space
3
+
4
+ Simple, fast, GPU/CPU auto-detected. Now with video upload and 5-output generation!
 
5
  """
6
  from __future__ import annotations
7
 
8
  import os
9
  import subprocess
10
  import tempfile
 
11
  from pathlib import Path
 
 
 
 
 
12
 
13
  import gradio as gr
14
+ import torch
15
+ from moviepy.video.io.VideoFileClip import VideoFileClip
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ # Imports do seu código original
18
+ from lib.config import (
19
+ BUILTIN_MODELS,
20
+ CSS,
21
+ DEVICE_LABEL,
22
+ MAX_INPUT_DURATION,
23
+ logger,
24
+ )
25
+ from lib.jobs import (
26
+ get_jobs_table,
27
+ get_queue_info,
28
+ poll_job,
29
+ submit_job,
30
+ )
31
+ from lib.models import list_models, startup_downloads
32
+ from lib.ui import refresh_models, toggle_autotune, upload_model
33
 
34
+ # ── Startup (original) ───────────────────────────────────────────────────────
35
+ startup_status = ""
36
+ default_model = ""
37
+ try:
38
+ default_model = startup_downloads()
39
+ startup_status = f"✅ Ready &nbsp;·&nbsp; {DEVICE_LABEL}"
40
+ except Exception as e:
41
+ startup_status = f"⚠️ Some assets unavailable: {e} &nbsp;·&nbsp; {DEVICE_LABEL}"
42
+ logger.warning("Startup download issue: %s", e)
43
 
44
+ initial_models = list_models()
45
+ initial_value = default_model if default_model in initial_models else (
46
+ initial_models[0] if initial_models else None
47
+ )
 
 
 
 
48
 
49
+ # ── NOVAS FUNÇÕES DE PROCESSAMENTO ──────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  def extract_audio_from_video(video_path: str) -> str:
52
+ """
53
+ Extrai o áudio de um arquivo de vídeo usando moviepy.
54
+ Retorna o caminho do arquivo de áudio extraído (.mp3).
55
+ """
56
  try:
57
+ # Define um nome temporário para o arquivo de áudio
58
+ audio_path = video_path.replace('.mp4', '.mp3') if video_path.endswith('.mp4') else video_path + '.mp3'
59
+ if os.path.exists(audio_path):
60
+ return audio_path
61
+ with VideoFileClip(video_path) as video:
62
+ audio = video.audio
63
+ if audio is None:
64
+ raise ValueError("O arquivo de vídeo não possui nenhuma faixa de áudio.")
65
+ audio.write_audiofile(audio_path, logger=None)
66
+ return audio_path
 
 
 
 
 
67
  except Exception as e:
68
+ logger.error(f"Erro ao extrair áudio do vídeo: {e}")
69
+ raise gr.Error(f"Falha ao processar o vídeo: {e}")
70
 
71
+ def separate_audio_stems(audio_path: str, output_dir: str) -> tuple[str, str]:
72
+ """
73
+ Usa o Demucs para separar o áudio em vocal (acapella) e instrumental.
74
+ Retorna o caminho para o acapella e para o instrumental.
75
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  try:
77
+ # Define os caminhos de saída esperados
78
+ acapella_path = os.path.join(output_dir, "entrada_acapella.mp3")
79
+ instrumental_path = os.path.join(output_dir, "entrada_instrumental.mp3")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
+ # Se os arquivos já existirem, retorna eles (cache)
82
+ if os.path.exists(acapella_path) and os.path.exists(instrumental_path):
83
+ return acapella_path, instrumental_path
84
 
85
+ # Configura o Demucs para separar apenas os vocais
86
+ # O modelo 'htdemucs' é o mais recente e de alta qualidade
87
+ # O parâmetro '--two-stems=vocals' faz o Demucs separar apenas vocais e o resto
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  cmd = [
89
+ "python3", "-m", "demucs.separate",
90
+ "--two-stems=vocals",
91
+ "-n", "htdemucs",
92
+ "-d", "cpu" if not torch.cuda.is_available() else "cuda",
93
+ "-o", output_dir,
94
+ audio_path
95
  ]
 
96
 
97
+ logger.info(f"Executando separação Demucs: {' '.join(cmd)}")
98
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
99
+
100
+ # O Demucs cria uma estrutura de pastas. Precisamos localizar os arquivos gerados.
101
+ base_name = Path(audio_path).stem
102
+ demucs_output = Path(output_dir) / "htdemucs" / base_name
103
+ if not demucs_output.exists():
104
+ # Se a estrutura for diferente, tenta encontrar recursivamente
105
+ for wav_file in Path(output_dir).rglob("vocals.wav"):
106
+ demucs_output = wav_file.parent
107
+ break
108
+
109
+ # Converte os .wav gerados para .mp3
110
+ vocals_wav = demucs_output / "vocals.wav"
111
+ no_vocals_wav = demucs_output / "no_vocals.wav"
112
+
113
+ if vocals_wav.exists():
114
+ # Converte para MP3 usando ffmpeg (mais leve e rápido)
115
+ subprocess.run([
116
+ "ffmpeg", "-y", "-i", str(vocals_wav), "-acodec", "libmp3lame", "-b:a", "192k", acapella_path
117
+ ], check=True, capture_output=True)
118
+ else:
119
+ raise FileNotFoundError(f"Arquivo vocals.wav não encontrado em {demucs_output}")
120
+
121
+ if no_vocals_wav.exists():
122
+ subprocess.run([
123
+ "ffmpeg", "-y", "-i", str(no_vocals_wav), "-acodec", "libmp3lame", "-b:a", "192k", instrumental_path
124
+ ], check=True, capture_output=True)
125
+ else:
126
+ raise FileNotFoundError(f"Arquivo no_vocals.wav não encontrado em {demucs_output}")
127
+
128
+ # Limpa os arquivos .wav temporários para economizar espaço
129
+ os.remove(vocals_wav)
130
+ os.remove(no_vocals_wav)
131
+ # Remove o diretório temporário do Demucs, se estiver vazio
132
+ if demucs_output.exists() and not any(demucs_output.iterdir()):
133
+ demucs_output.rmdir()
134
+
135
+ return acapella_path, instrumental_path
136
 
137
+ except subprocess.CalledProcessError as e:
138
+ logger.error(f"Erro na separação do Demucs: {e.stderr}")
139
+ raise gr.Error(f"Falha na separação das faixas. Verifique se o Demucs está instalado corretamente.")
 
 
140
  except Exception as e:
141
+ logger.error(f"Erro inesperado na separação: {e}")
142
+ raise
 
 
 
 
 
 
143
 
144
+ def process_full_pipeline(
145
+ video_file: str | None,
146
+ audio_mic: str | None,
147
+ audio_file: str | None,
148
+ model: str,
149
+ pitch: int,
150
+ f0_method: str,
151
+ index_rate: float,
152
+ protect: float,
153
+ vol_env: float,
154
+ clean_cb: bool,
155
+ clean_strength: float,
156
+ split_cb: bool,
157
+ autotune_cb: bool,
158
+ autotune_strength: float,
159
+ filter_radius: int,
160
+ fmt: str,
161
+ reverb_cb: bool,
162
+ reverb_room: float,
163
+ reverb_damp: float,
164
+ reverb_wet: float,
165
+ ) -> tuple[str, str, str, str, str, str, str]:
166
+ """
167
+ Função principal que orquestra todo o novo pipeline:
168
+ 1. Obtém o áudio de entrada (vídeo, microfone ou upload)
169
+ 2. Extrai áudio se for vídeo
170
+ 3. Aplica Demucs para separar acapella e instrumental
171
+ 4. Converte o áudio original e o acapella usando RVC
172
+ 5. Retorna os 5 arquivos de áudio mais o status
173
+ """
174
+ # Cria um diretório temporário único para esta execução
175
+ with tempfile.TemporaryDirectory() as tmp_dir:
176
+ # --- PASSO 1: Determinar o arquivo de áudio fonte ---
177
+ input_audio_path = None
178
+ if video_file:
179
+ # É um vídeo, extrai o áudio
180
+ input_audio_path = extract_audio_from_video(video_file)
181
+ logger.info(f"Áudio extraído do vídeo: {input_audio_path}")
182
+ elif audio_mic:
183
+ input_audio_path = audio_mic
184
+ logger.info(f"Usando áudio do microfone: {input_audio_path}")
185
+ elif audio_file:
186
+ input_audio_path = audio_file
187
+ logger.info(f"Usando áudio enviado: {input_audio_path}")
188
+ else:
189
+ return "Erro: Nenhuma fonte de áudio ou vídeo foi fornecida.", "", "", "", "", "", ""
190
 
191
+ # Converte o áudio de entrada para um formato padrão (MP3) para facilitar
192
+ base_audio = os.path.join(tmp_dir, "entrada_original.mp3")
193
+ subprocess.run([
194
+ "ffmpeg", "-y", "-i", input_audio_path, "-acodec", "libmp3lame", "-b:a", "192k", base_audio
195
+ ], check=True, capture_output=True)
196
+
197
+ # --- PASSO 2: Separar acapella e instrumental com Demucs ---
198
+ logger.info("Iniciando separação Demucs...")
199
+ entrada_acapella, entrada_instrumental = separate_audio_stems(base_audio, tmp_dir)
200
+ logger.info(f"Separação concluída. Acapella: {entrada_acapella}, Instrumental: {entrada_instrumental}")
201
+
202
+ # --- PASSO 3: Aplicar RVC no áudio original e no acapella ---
203
+ # Precisamos de uma função de callback para o submit_job, pois ele espera um arquivo e retorna um job_id
204
+ # Como o submit_job é assíncrono e usa uma fila, vamos usá-lo diretamente.
205
+ # Para simplificar, vamos chamar submit_job duas vezes e esperar os resultados.
206
+
207
+ # Nota: submit_job retorna uma mensagem de status e um job_id. Precisamos de uma função que espere o job terminar.
208
+ # Vou criar uma função auxiliar para aguardar a conclusão.
209
+
210
+ def run_rvc_conversion(audio_path, model_name, output_name):
211
+ status, job_id = submit_job(
212
+ None, audio_path, model_name, # inp_mic, inp_file, model
213
+ pitch, f0_method,
214
+ index_rate, protect, vol_env,
215
+ clean_cb, clean_strength,
216
+ split_cb, autotune_cb, autotune_strength,
217
+ filter_radius,
218
+ "mp3", # formato fixo para consistência
219
+ reverb_cb, reverb_room, reverb_damp, reverb_wet
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  )
221
+ # Aguarda o job terminar (polling)
222
+ import time
223
+ import re
224
+ job_id_match = re.search(r"[a-f0-9]{8}", job_id)
225
+ if not job_id_match:
226
+ raise Exception(f"Falha ao obter job_id: {status}")
227
+ job_id = job_id_match.group(0)
228
+ logger.info(f"Job {job_id} submetido para {output_name}")
229
+ # Poll até que o job esteja completo
230
+ while True:
231
+ time.sleep(1)
232
+ poll_status, output_file = poll_job(job_id)
233
+ if "completed" in poll_status.lower() or "done" in poll_status.lower():
234
+ if output_file and os.path.exists(output_file):
235
+ # Converte para MP3 se necessário
236
+ final_output = os.path.join(tmp_dir, output_name)
237
+ subprocess.run([
238
+ "ffmpeg", "-y", "-i", output_file, "-acodec", "libmp3lame", "-b:a", "192k", final_output
239
+ ], check=True, capture_output=True)
240
+ return final_output
241
+ else:
242
+ raise Exception("Job concluído mas nenhum arquivo foi gerado.")
243
+ elif "failed" in poll_status.lower():
244
+ raise Exception(f"Job {job_id} falhou: {poll_status}")
245
+
246
+ # Executa as duas conversões em paralelo para maior eficiência
247
+ from concurrent.futures import ThreadPoolExecutor
248
+ with ThreadPoolExecutor(max_workers=2) as executor:
249
+ future_original = executor.submit(run_rvc_conversion, base_audio, model, "saida.mp3")
250
+ future_acapella = executor.submit(run_rvc_conversion, entrada_acapella, model, "saida_acapella.mp3")
251
+ saida_path = future_original.result()
252
+ saida_acapella_path = future_acapella.result()
253
+
254
+ # Os arquivos estão em MP3, prontos para serem retornados
255
+ # Precisamos copiá-los para fora do diretório temporário, ou o Gradio não conseguirá acessá-los.
256
+ # Vamos usar o diretório de saída permanente do espaço.
257
+ output_dir = Path("outputs")
258
+ output_dir.mkdir(exist_ok=True)
259
+
260
+ # Nomes finais
261
+ final_files = {
262
+ "entrada_acapella.mp3": entrada_acapella,
263
+ "entrada.mp3": base_audio,
264
+ "entrada_instrumental.mp3": entrada_instrumental,
265
+ "saida.mp3": saida_path,
266
+ "saida_acapella.mp3": saida_acapella_path,
267
+ }
268
 
269
+ # Copia para o diretório de saída
270
+ final_paths = {}
271
+ for name, src_path in final_files.items():
272
+ dest = output_dir / name
273
+ # Se o arquivo já existe, remove para garantir uma cópia nova
274
+ if dest.exists():
275
+ dest.unlink()
276
+ # Copia o arquivo (usando shutil para preservar metadados)
277
+ import shutil
278
+ shutil.copy2(src_path, dest)
279
+ final_paths[name] = str(dest)
280
+
281
+ status_msg = "✅ Conversão concluída com sucesso! Os 5 arquivos estão disponíveis abaixo."
282
+
283
+ return (
284
+ status_msg,
285
+ final_paths["entrada_acapella.mp3"],
286
+ final_paths["entrada.mp3"],
287
+ final_paths["entrada_instrumental.mp3"],
288
+ final_paths["saida.mp3"],
289
+ final_paths["saida_acapella.mp3"],
290
+ "processamento-finalizado"
291
+ )
292
 
293
+ # ── Gradio UI (modificada) ───────────────────────────────────────────────────
294
+ with gr.Blocks(title="RVC Voice Conversion - Full Suite", delete_cache=(3600, 3600)) as demo:
 
295
 
296
+ gr.HTML(f"""
297
+ <div id="header">
298
+ <h1>🎙️ RVC Voice Conversion - Full Suite</h1>
299
+ <p>Conversão de voz com suporte a vídeos, extração de acapella/instrumental e 5 saídas!</p>
 
 
 
300
  </div>
301
+ <p id="status">{startup_status}</p>
302
  """)
303
+
304
  with gr.Tabs():
305
+ with gr.Tab("🎤 Convert"):
 
 
 
306
  with gr.Row():
307
  with gr.Column(scale=1):
308
+ gr.Markdown("### 🔊 Entrada de Áudio/Vídeo")
309
+ # NOVO: Componente de vídeo
310
+ video_input = gr.Video(
311
+ label="Upload de Vídeo (MP4, WebM, etc.)",
312
+ sources=["upload"],
313
+ format="mp4",
314
+ height=300,
315
+ )
316
+ gr.Markdown("--- OU ---")
317
  with gr.Tabs():
318
  with gr.Tab("🎙️ Microfone"):
319
+ inp_mic = gr.Audio(
320
+ sources=["microphone"],
321
+ type="filepath",
322
+ label="Gravar Áudio",
323
+ )
324
+ with gr.Tab("📁 Upload de Áudio"):
325
+ inp_file = gr.Audio(
326
+ sources=["upload"],
327
+ type="filepath",
328
+ label="Enviar Arquivo (wav, mp3, flac, etc.)",
329
+ )
330
 
331
+ gr.Markdown("### 🤖 Modelo")
332
+ model_dd = gr.Dropdown(
333
+ choices=initial_models,
334
+ value=initial_value,
335
+ label="Modelo de Voz Ativo",
336
+ interactive=True,
337
+ )
338
 
339
+ gr.Markdown("### 🎚️ Configurações Básicas")
340
+ pitch_sl = gr.Slider(
341
+ minimum=-24, maximum=24, value=0, step=1,
342
+ label="Pitch Shift (semitons)",
343
+ info="0 = sem alteração · positivo = mais agudo · negativo = mais grave",
344
+ )
345
+ f0_radio = gr.Radio(
346
+ choices=["rmvpe", "fcpe", "crepe", "crepe-tiny"],
347
+ value="rmvpe",
348
+ label="Método de Extração de Pitch",
349
+ info="rmvpe = mais rápido · crepe = maior qualidade (mais lento)",
350
+ )
351
+
352
  with gr.Column(scale=1):
353
+ gr.Markdown("### ⚙️ Configurações Avançadas")
354
+ with gr.Accordion("Expandir opções avançadas", open=False):
355
+ index_rate_sl = gr.Slider(
356
+ 0.0, 1.0, value=0.75, step=0.05,
357
+ label="Index Rate",
358
+ info="Força com que o índice FAISS influencia o timbre (0 = desligado)",
359
+ )
360
+ protect_sl = gr.Slider(
361
+ 0.0, 0.5, value=0.5, step=0.01,
362
+ label="Proteção de Consoantes",
363
+ info="0.5 = proteção máxima",
364
+ )
365
+ filter_radius_sl = gr.Slider(
366
+ 0, 7, value=3, step=1,
367
+ label="Raio do Filtro de Respiração",
368
+ info="Valores mais altos suavizam mais, reduzindo ruído de respiração",
369
+ )
370
+ vol_env_sl = gr.Slider(
371
+ 0.0, 1.0, value=0.25, step=0.05,
372
+ label="Mistura de Envelope de Volume",
373
+ info="0.25 = mistura natural · 1 = mantém volume original · 0 = saída do modelo",
374
+ )
375
+ with gr.Row():
376
+ clean_cb = gr.Checkbox(value=False, label="Redução de Ruído")
377
+ clean_sl = gr.Slider(
378
+ 0.0, 1.0, value=0.5, step=0.05,
379
+ label="Intensidade",
380
+ )
381
+ with gr.Row():
382
+ split_cb = gr.Checkbox(value=False, label="Dividir Áudio Longo")
383
+ autotune_cb = gr.Checkbox(value=False, label="Autotune")
384
+ autotune_sl = gr.Slider(
385
+ 0.0, 1.0, value=1.0, step=0.05,
386
+ label="Intensidade do Autotune",
387
+ visible=False,
388
+ )
389
+ autotune_cb.change(
390
+ fn=toggle_autotune,
391
+ inputs=autotune_cb,
392
+ outputs=autotune_sl,
393
+ )
394
 
395
+ gr.Markdown("**🎛Reverb**")
396
+ reverb_cb = gr.Checkbox(value=False, label="Habilitar Reverb")
397
+ with gr.Group(visible=False) as reverb_group:
398
+ reverb_room_sl = gr.Slider(
399
+ 0.0, 1.0, value=0.15, step=0.05,
400
+ label="Tamanho da Sala",
401
+ )
402
+ reverb_damp_sl = gr.Slider(
403
+ 0.0, 1.0, value=0.7, step=0.05,
404
+ label="Atenuação",
405
+ )
406
+ reverb_wet_sl = gr.Slider(
407
+ 0.0, 1.0, value=0.15, step=0.05,
408
+ label="Nível Úmido",
409
+ )
410
+ reverb_cb.change(
411
+ fn=lambda v: gr.update(visible=v),
412
+ inputs=reverb_cb,
413
+ outputs=reverb_group,
414
+ )
415
 
416
+ fmt_radio = gr.Radio(
417
+ choices=["WAV", "MP3", "FLAC", "OPUS"],
418
+ value="MP3",
419
+ label="Formato de Saída",
420
+ info="Para este pipeline, o formato MP3 é recomendado.",
421
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
+ convert_btn = gr.Button(
424
+ "🚀 Iniciar Processamento Completo",
425
+ variant="primary",
426
+ )
427
 
428
+ gr.Markdown("### 🎧 5 Saídas de Áudio")
429
+ out_status = gr.Markdown(value="")
430
+ with gr.Row():
431
+ with gr.Column():
432
+ gr.Markdown("#### 🎤 Entrada Original (sem RVC)")
433
+ entrada_acapella = gr.Audio(label="Acapella Extraído", type="filepath", interactive=False)
434
+ entrada_audio = gr.Audio(label="Áudio Original", type="filepath", interactive=False)
435
+ entrada_instrumental = gr.Audio(label="Instrumental Extraído", type="filepath", interactive=False)
436
+ with gr.Column():
437
+ gr.Markdown("#### 🎙️ Saída com RVC")
438
+ saida_audio = gr.Audio(label="Saída (RVC sobre áudio original)", type="filepath", interactive=False)
439
+ saida_acapella = gr.Audio(label="Saída (RVC sobre acapella)", type="filepath", interactive=False)
440
+
441
+ gr.Markdown("#### 🔍 Verificar Status do Job")
442
+ with gr.Row():
443
+ job_id_box = gr.Textbox(
444
+ label="Job ID",
445
+ placeholder="ex: a3f2b1c9",
446
+ scale=3,
447
+ )
448
+ poll_btn = gr.Button("🔄 Verificar", scale=1)
449
+ poll_status = gr.Markdown(value="")
450
+ poll_audio = gr.Audio(label="Resultado", type="filepath", interactive=False)
451
+
452
+ # As outras abas permanecem EXATAMENTE como estavam (Models, Jobs, Help)
453
+ with gr.Tab("📦 Models"):
454
+ # ... (conteúdo original) ...
455
+ gr.Markdown("""
456
+ ### Upload de Modelo Customizado
457
+ Forneça um **`.zip`** contendo:
458
+ - **`model.pth`** — pesos (obrigatório)
459
+ - **`model.index`** — índice FAISS (opcional)
460
+
461
+ **Modelos integrados** (pré-baixados):
462
+ Vestia Zeta v1 · Vestia Zeta v2 · Ayunda Risu · Gawr Gura
463
+ """)
464
+ with gr.Row():
465
+ with gr.Column(scale=1):
466
+ up_zip = gr.File(label="ZIP do Modelo", file_types=[".zip"])
467
+ up_name = gr.Textbox(
468
+ label="Nome do Modelo",
469
+ placeholder="Deixe em branco para usar o nome do arquivo",
470
  )
471
+ up_btn = gr.Button("📤 Carregar Modelo", variant="primary")
472
+ up_status = gr.Textbox(label="Status", interactive=False, lines=2)
473
+ with gr.Column(scale=1):
474
+ gr.Markdown("### Modelos Carregados")
475
+ models_table = gr.Dataframe(
476
+ col_count=(1, "fixed"),
477
+ value=[[m] for m in initial_models],
478
+ interactive=False,
479
+ label="",
480
+ )
481
+ refresh_btn = gr.Button("🔄 Atualizar")
482
+
483
+ up_btn.click(
484
+ fn=upload_model,
485
+ inputs=[up_zip, up_name],
486
+ outputs=[up_status, model_dd, models_table],
487
+ )
488
+ refresh_btn.click(
489
+ fn=refresh_models,
490
+ outputs=[models_table, model_dd],
491
+ )
492
+
493
+ with gr.Tab("📋 Jobs"):
494
+ # ... (conteúdo original) ...
495
+ gr.Markdown("Todos os jobs submetidos, do mais novo ao mais antigo. Clique em **Atualizar**.")
496
+ queue_status = gr.Markdown(value=get_queue_info, every=10)
497
+ jobs_table = gr.Dataframe(
498
+ headers=["Job ID", "Model", "Status", "Time", "Download"],
499
+ col_count=(5, "fixed"),
500
+ value=get_jobs_table,
501
+ interactive=False,
502
+ wrap=True,
503
+ datatype=["str", "str", "str", "str", "markdown"],
504
+ every=10,
505
+ )
506
+ refresh_jobs_btn = gr.Button("🔄 Atualizar")
507
+
508
+ def _refresh_jobs():
509
+ return get_queue_info(), get_jobs_table()
510
+
511
+ refresh_jobs_btn.click(fn=_refresh_jobs, outputs=[queue_status, jobs_table])
512
+
513
+ with gr.Tab("ℹ️ Help"):
514
+ # ... (conteúdo original) ...
515
+ gr.Markdown(f"""
516
+ ## Como Funciona
517
+ RVC (Retrieval-Based Voice Conversion) transforma uma gravação de voz para soar como um falante alvo, utilizando o modelo desse falante.
518
+
519
+ ---
520
+
521
+ ## Guia Rápido
522
+ 1. Abra a aba **Convert**
523
+ 2. Escolha uma fonte: **vídeo (MP4)**, **microfone** ou **upload de áudio**
524
+ 3. Selecione um **modelo** no dropdown
525
+ 4. Ajuste o **Pitch Shift** se necessário (ex: voz masculina → feminina: tente +12 semitons)
526
+ 5. Clique em **Iniciar Processamento Completo** e aguarde
527
+ 6. Os 5 arquivos de saída aparecerão na tela para ouvir e baixar
528
+
529
+ ---
530
+
531
+ ## Novidades na Versão Full Suite
532
+ * **Upload de vídeo**: Suporte a MP4 e outros formatos de vídeo (o áudio é extraído automaticamente).
533
+ * **Separação de faixas**: Usando Demucs, extraímos o acapella e o instrumental da sua entrada.
534
+ * **5 saídas**:
535
+ * `entrada_acapella.mp3` (vocal extraído)
536
+ * `entrada.mp3` (áudio original)
537
+ * `entrada_instrumental.mp3` (instrumental extraído)
538
+ * `saida.mp3` (RVC aplicado ao áudio original)
539
+ * `saida_acapella.mp3` (RVC aplicado apenas ao vocal)
540
+ * **Pipeline otimizado**: Processamento em paralelo para maior velocidade.
541
+
542
+ ---
543
+
544
+ ## Modelos Integrados
545
+ | Modelo | Descrição |
546
+ |---|---|
547
+ | **Vestia Zeta v1** | VTuber da Hololive ID, modelo v1 |
548
+ | **Vestia Zeta v2** | VTuber da Hololive ID, modelo v2 (recomendado) |
549
+ | **Ayunda Risu** | VTuber da Hololive ID |
550
+ | **Gawr Gura** | VTuber da Hololive EN |
551
+
552
+ ---
553
+
554
+ ## Métodos de Extração de Pitch
555
+ | Método | Velocidade | Qualidade | Melhor para |
556
+ |---|---|---|---|
557
+ | **rmvpe** | ⚡⚡⚡ | ★★★★ | Uso geral (padrão) |
558
+ | **fcpe** | ⚡⚡ | ★★★★ | Canto |
559
+ | **crepe** | ⚡ | ★★★★★ | Máxima qualidade, mais lento |
560
+ | **crepe-tiny** | ⚡⚡ | ★★★ | Baixo recurso |
561
+
562
+ ---
563
+
564
+ ## Configurações Avançadas
565
+ | Parâmetro | Descrição |
566
+ |---|---|
567
+ | **Index Rate** | Influência do índice FAISS (0.75 recomendado) |
568
+ | **Protect Consonants** | Protege consoantes não vozeadas (0.5 = máximo) |
569
+ | **Respiration Filter Radius** | Suaviza a curva de pitch — valores maiores reduzem ruído de respiração |
570
+ | **Volume Envelope Mix** | 0.25 = mistura natural · 1 = mantém volume original |
571
+ | **Noise Reduction** | Remove ruído de fundo antes da conversão |
572
+ | **Split Long Audio** | Divide áudios longos (>60s) em segmentos |
573
+ | **Autotune** | Ajusta o pitch para a nota musical mais próxima |
574
+
575
+ ---
576
+
577
+ **Dispositivo:** `{DEVICE_LABEL}`
578
+ **Duração máxima de entrada:** {MAX_INPUT_DURATION // 60} minutos
579
+
580
+ ---
581
+
582
+ ## Créditos
583
+ Engine: [Ultimate RVC](https://github.com/JackismyShephard/ultimate-rvc) · Separação: [Demucs](https://github.com/facebookresearch/demucs)
584
+ """)
585
+
586
+ # --- Conexão dos Eventos (novo botão de conversão) ---
587
  convert_btn.click(
588
+ fn=process_full_pipeline,
589
+ inputs=[
590
+ video_input, inp_mic, inp_file, model_dd,
591
+ pitch_sl, f0_radio,
592
+ index_rate_sl, protect_sl, vol_env_sl,
593
+ clean_cb, clean_sl,
594
+ split_cb, autotune_cb, autotune_sl,
595
+ filter_radius_sl,
596
+ fmt_radio,
597
+ reverb_cb, reverb_room_sl, reverb_damp_sl, reverb_wet_sl,
598
+ ],
599
+ outputs=[
600
+ out_status,
601
+ entrada_acapella, entrada_audio, entrada_instrumental,
602
+ saida_audio, saida_acapella,
603
+ job_id_box
604
+ ],
605
  )
606
 
607
+ def _poll_and_refresh(job_id):
608
+ status, file = poll_job(job_id)
609
+ return status, file, get_queue_info(), get_jobs_table()
610
+
611
+ poll_btn.click(
612
+ fn=_poll_and_refresh,
613
+ inputs=[job_id_box],
614
+ outputs=[poll_status, poll_audio, queue_status, jobs_table],
615
  )
616
 
617
+ # ── Launch ────────────────────────────────────────────────────────────────────
618
  if __name__ == "__main__":
619
+ demo.queue(default_concurrency_limit=5)
620
  demo.launch(
 
621
  server_name="0.0.0.0",
622
+ server_port=int(os.getenv("PORT", 7860)),
623
+ max_threads=10,
624
+ ssr_mode=False,
625
+ css=CSS,
626
  )