LosCaquitos commited on
Commit
c5aa3eb
·
verified ·
1 Parent(s): d9ee227

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +294 -92
app.py CHANGED
@@ -2,6 +2,7 @@
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
  """
6
  from __future__ import annotations
7
 
@@ -14,6 +15,7 @@ import json
14
  import uuid
15
  from datetime import datetime
16
  import traceback
 
17
 
18
  import gradio as gr
19
  import numpy as np
@@ -27,9 +29,11 @@ DEVICE_LABEL = "CPU (Stable)"
27
 
28
  MODELS_DIR = Path("models")
29
  OUTPUTS_DIR = Path("outputs")
 
30
 
31
  MODELS_DIR.mkdir(exist_ok=True)
32
  OUTPUTS_DIR.mkdir(exist_ok=True)
 
33
 
34
  CONFIG = {
35
  "sample_rate": 16000,
@@ -40,6 +44,73 @@ CONFIG = {
40
  AUDIO_FORMATS = ["wav", "mp3", "flac", "ogg", "m4a", "aac", "wma", "mp4", "mkv", "webm", "avi", "mov", "flv"]
41
  VIDEO_FORMATS = ["mp4", "mkv", "webm", "avi", "mov", "flv", "m4v"]
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  # ============================================================================
44
  # FUNÇÕES
45
  # ============================================================================
@@ -102,7 +173,7 @@ def normalize_audio(audio: np.ndarray) -> np.ndarray:
102
  return audio
103
 
104
  def extract_audio_from_video(video_path: str) -> str:
105
- """Extrai áudio de vídeo (MP4, MKV, WebM, etc)"""
106
  try:
107
  output_audio = Path(tempfile.gettempdir()) / f"temp_audio_{uuid.uuid4().hex[:8]}.wav"
108
 
@@ -139,14 +210,21 @@ def convert_voice_simple(
139
  pitch: int = 0,
140
  clean: bool = False,
141
  reverb: bool = False,
 
142
  ) -> dict:
143
  """Conversão de voz SIMPLIFICADA"""
144
 
145
- job_id = str(uuid.uuid4())[:8]
 
 
146
  output_dir = OUTPUTS_DIR / job_id
147
  output_dir.mkdir(exist_ok=True)
148
 
149
  try:
 
 
 
 
150
  # 1. Carregar áudio
151
  audio, sr = load_audio(audio_path)
152
  if audio is None:
@@ -156,6 +234,9 @@ def convert_voice_simple(
156
  entrada_path = output_dir / "entrada.wav"
157
  sf.write(entrada_path, audio, sr)
158
 
 
 
 
159
  # 2. Separar vocais e instrumentação
160
  vocals, instrumental = separate_vocals(audio, sr)
161
 
@@ -167,6 +248,9 @@ def convert_voice_simple(
167
  entrada_instrumental_path = output_dir / "entrada_instrumental.wav"
168
  sf.write(entrada_instrumental_path, instrumental, sr)
169
 
 
 
 
170
  # 3. Processar vocais
171
  vocals_converted = vocals.copy()
172
 
@@ -189,6 +273,9 @@ def convert_voice_simple(
189
  saida_acapella_path = output_dir / "saida_acapella.wav"
190
  sf.write(saida_acapella_path, vocals_converted, sr)
191
 
 
 
 
192
  # 4. Mixar vocal processado com instrumental original
193
  min_len = min(len(vocals_converted), len(instrumental))
194
  mix = vocals_converted[:min_len] + instrumental[:min_len]
@@ -198,6 +285,9 @@ def convert_voice_simple(
198
  saida_path = output_dir / "saida.wav"
199
  sf.write(saida_path, mix, sr)
200
 
 
 
 
201
  return {
202
  "status": "success",
203
  "entrada": str(entrada_path),
@@ -211,6 +301,8 @@ def convert_voice_simple(
211
  except Exception as e:
212
  print(f"Erro na conversão: {e}")
213
  traceback.print_exc()
 
 
214
  return {
215
  "status": "error",
216
  "error": str(e),
@@ -221,15 +313,21 @@ def process_video_simple(
221
  pitch: int = 0,
222
  clean: bool = False,
223
  reverb: bool = False,
 
224
  ) -> dict:
225
  """Processamento de vídeo SIMPLIFICADO"""
226
 
227
- job_id = str(uuid.uuid4())[:8]
 
 
228
  output_dir = OUTPUTS_DIR / job_id
229
  output_dir.mkdir(exist_ok=True)
230
 
231
  try:
232
  # 1. Extrair áudio do vídeo
 
 
 
233
  temp_audio = output_dir / "temp_audio.wav"
234
  cmd = [
235
  "ffmpeg", "-i", video_path, "-q:a", "0", "-map", "a",
@@ -237,12 +335,18 @@ def process_video_simple(
237
  ]
238
  subprocess.run(cmd, check=True, capture_output=True, timeout=600)
239
 
 
 
 
240
  # 2. Processar áudio
241
- result = convert_voice_simple(str(temp_audio), pitch=pitch, clean=clean, reverb=reverb)
242
 
243
  if result["status"] != "success":
244
  return result
245
 
 
 
 
246
  # 3. Remixar vídeo com áudio novo
247
  output_video = output_dir / "saida_video.mp4"
248
  cmd = [
@@ -255,6 +359,9 @@ def process_video_simple(
255
  # Limpar temporário
256
  temp_audio.unlink(missing_ok=True)
257
 
 
 
 
258
  return {
259
  **result,
260
  "video_output": str(output_video),
@@ -263,6 +370,8 @@ def process_video_simple(
263
  except Exception as e:
264
  print(f"Erro no processamento de vídeo: {e}")
265
  traceback.print_exc()
 
 
266
  return {
267
  "status": "error",
268
  "error": str(e),
@@ -282,73 +391,125 @@ def create_zip(output_dir: Path) -> str:
282
  print(f"Erro ao criar ZIP: {e}")
283
  return None
284
 
285
- def submit_audio(audio_mic, audio_file, pitch, clean, reverb):
286
- """Handler de conversão de áudio"""
287
  audio_path = audio_mic or audio_file
288
 
289
  if not audio_path:
290
- return "❌ Nenhum áudio fornecido", None, None, None, None, None, None
291
 
292
- try:
293
- # Validar duração
294
- duration = librosa.get_duration(filename=audio_path)
295
- if duration > 600: # 10 minutos max
296
- return "❌ Áudio muito longo (máx 10 min)", None, None, None, None, None, None
297
-
298
- result = convert_voice_simple(audio_path, pitch=pitch, clean=clean, reverb=reverb)
299
-
300
- if result["status"] != "success":
301
- return f"❌ Erro: {result.get('error', 'Desconhecido')}", None, None, None, None, None, None
302
-
303
- zip_path = create_zip(Path(result["output_dir"]))
304
-
305
- return (
306
- f"✅ Conversão completa!",
307
- result["entrada"],
308
- result["entrada_acapella"],
309
- result["entrada_instrumental"],
310
- result["saida_acapella"],
311
- result["saida"],
312
- zip_path,
313
- )
314
 
315
- except Exception as e:
316
- print(f"Erro: {e}")
317
- traceback.print_exc()
318
- return f"❌ Erro: {str(e)}", None, None, None, None, None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
- def submit_video(video_file, pitch, clean, reverb):
321
- """Handler de conversão de vídeo"""
322
 
323
  if not video_file:
324
- return "❌ Nenhum vídeo fornecido", None, None, None, None, None, None, None
325
 
326
- try:
327
- result = process_video_simple(video_file, pitch=pitch, clean=clean, reverb=reverb)
328
-
329
- if result["status"] != "success":
330
- return f"❌ Erro: {result.get('error', 'Desconhecido')}", None, None, None, None, None, None, None
331
-
332
- zip_path = create_zip(Path(result["output_dir"]))
333
-
334
- return (
335
- f"✅ Conversão de vídeo completa!",
336
- result["entrada"],
337
- result["entrada_acapella"],
338
- result["entrada_instrumental"],
339
- result["saida_acapella"],
340
- result["saida"],
341
- result["video_output"],
342
- zip_path,
343
- )
344
 
345
- except Exception as e:
346
- print(f"Erro: {e}")
347
- traceback.print_exc()
348
- return f"❌ Erro: {str(e)}", None, None, None, None, None, None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
  # ============================================================================
351
- # INTERFACE GRADIO (GRADIO 6.0 COMPATIBLE)
352
  # ============================================================================
353
 
354
  with gr.Blocks(title="RVC Voice Conversion") as demo:
@@ -381,22 +542,10 @@ with gr.Blocks(title="RVC Voice Conversion") as demo:
381
  reverb = gr.Checkbox(value=False, label="Reverb")
382
 
383
  convert_btn = gr.Button("🚀 Converter Áudio", variant="primary", size="lg")
384
-
385
- with gr.Column(scale=1):
386
- status_audio = gr.Textbox(label="Status", interactive=False)
387
-
388
- gr.Markdown("### 📦 Outputs (5 Áudios)")
389
- entrada_aud = gr.Audio(label="entrada.wav", interactive=False)
390
- entrada_acap = gr.Audio(label="entrada_acapella.wav", interactive=False)
391
- entrada_inst = gr.Audio(label="entrada_instrumental.wav", interactive=False)
392
- saida_acap = gr.Audio(label="saida_acapella.wav", interactive=False)
393
- saida_aud = gr.Audio(label="saida.wav", interactive=False)
394
-
395
- gr.Markdown("### 📥 Download")
396
- audio_zip = gr.File(label="Baixar ZIP", interactive=False)
397
 
398
- # ─ TAB 2: VIDEO ──────────────────────────────────────────
399
- with gr.Tab("🎬 Converter Vídeo (MP4, MKV, WebM...)"):
400
 
401
  with gr.Row():
402
  with gr.Column(scale=1):
@@ -410,39 +559,92 @@ with gr.Blocks(title="RVC Voice Conversion") as demo:
410
  video_reverb = gr.Checkbox(value=False, label="Reverb")
411
 
412
  video_btn = gr.Button("🎬 Converter Vídeo", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
 
414
- with gr.Column(scale=1):
415
- status_video = gr.Textbox(label="Status", interactive=False)
416
-
417
- gr.Markdown("### 📦 Outputs (5 Áudios)")
418
- v_entrada_aud = gr.Audio(label="entrada.wav", interactive=False)
419
- v_entrada_acap = gr.Audio(label="entrada_acapella.wav", interactive=False)
420
- v_entrada_inst = gr.Audio(label="entrada_instrumental.wav", interactive=False)
421
- v_saida_acap = gr.Audio(label="saida_acapella.wav", interactive=False)
422
- v_saida_aud = gr.Audio(label="saida.wav", interactive=False)
423
-
424
- gr.Markdown("### 🎬 Vídeo")
 
 
425
  video_output = gr.Video(label="saida_video.mp4", interactive=False)
426
-
 
427
  gr.Markdown("### 📥 Download")
428
- video_zip = gr.File(label="Baixar ZIP", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
 
430
  # Wire eventos
431
  convert_btn.click(
432
- fn=submit_audio,
433
  inputs=[audio_mic, audio_file, pitch, clean, reverb],
434
- outputs=[status_audio, entrada_aud, entrada_acap, entrada_inst, saida_acap, saida_aud, audio_zip],
435
  )
436
 
437
  video_btn.click(
438
- fn=submit_video,
439
  inputs=[video_file, video_pitch, video_clean, video_reverb],
440
- outputs=[status_video, v_entrada_aud, v_entrada_acap, v_entrada_inst, v_saida_acap, v_saida_aud, video_output, video_zip],
441
  )
442
 
443
  if __name__ == "__main__":
444
  demo.launch(
445
- share=True, # ✅ IMPORTANTE: share=True para Hugging Face Spaces
446
  server_name="0.0.0.0",
447
  server_port=7860,
448
  show_error=True
 
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
 
 
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
 
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,
 
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
  # ============================================================================
 
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
 
 
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:
 
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
 
 
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
 
 
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]
 
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),
 
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),
 
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",
 
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 = [
 
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),
 
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),
 
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:
 
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):
 
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