dimensionalpulsar commited on
Commit
cfcb530
·
verified ·
1 Parent(s): 969158e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +87 -126
app.py CHANGED
@@ -1,17 +1,10 @@
1
- """
2
- Clone Vocal - Outil web de clonage vocal base sur Seed-VC (zero-shot).
3
- Interface Gradio en francais, deploye sur HuggingFace Spaces avec ZeroGPU.
4
- """
5
-
6
  import os
7
  import sys
8
  import logging
9
  import tempfile
10
  import shutil
11
-
12
  import gradio as gr
13
 
14
- # Monkey-patch gradio_client to fix "argument of type 'bool' is not iterable"
15
  try:
16
  import gradio_client.utils as _gc_utils
17
 
@@ -38,12 +31,12 @@ try:
38
  except Exception:
39
  pass
40
 
41
- # Setup logging
42
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
43
  logger = logging.getLogger(__name__)
44
 
45
- # Startup: clone Seed-VC
46
- logger.info("Initialisation de l'application...")
47
 
48
  from pipeline.setup import setup_seed_vc
49
  from pipeline.storage import init_storage, list_models, download_model, delete_model, get_reference_path
@@ -51,29 +44,24 @@ from pipeline.storage import init_storage, list_models, download_model, delete_m
51
  try:
52
  setup_seed_vc()
53
  except Exception as e:
54
- logger.error("Erreur lors du setup: {}".format(e))
55
 
56
- # Initialize model storage
57
  HF_MODELS_REPO = os.environ.get("HF_MODELS_REPO", "")
58
  if HF_MODELS_REPO:
59
  init_storage(HF_MODELS_REPO)
60
- logger.info("Stockage HuggingFace configure: {}".format(HF_MODELS_REPO))
61
 
62
- # Import GPU-decorated functions for ZeroGPU detection
63
  from pipeline.training import save_voice_reference, _gpu_warmup
64
  from pipeline.separation import separate_audio
65
  from pipeline.inference import convert_voice
66
 
67
-
68
- # -- Training Tab --
69
-
70
  def train_voice_model(audio_file, model_name, progress=gr.Progress()):
71
- """Handler: save voice reference."""
72
  if audio_file is None:
73
- return "Erreur : Veuillez uploader un fichier audio.", None
74
 
75
  if not model_name or not model_name.strip():
76
- return "Erreur : Veuillez entrer un nom pour le modele.", None
77
 
78
  model_name = model_name.strip().replace(" ", "_")
79
 
@@ -81,31 +69,28 @@ def train_voice_model(audio_file, model_name, progress=gr.Progress()):
81
  progress(value, desc=desc)
82
 
83
  try:
84
- progress(0.0, desc="Demarrage...")
85
  pth_path, ref_path = save_voice_reference(
86
  audio_path=audio_file,
87
  model_name=model_name,
88
  progress_callback=progress_callback,
89
  )
90
 
91
- return "Reference vocale '{}' sauvegardee avec succes !".format(model_name), ref_path
92
 
93
  except Exception as e:
94
  import traceback
95
  tb = traceback.format_exc()
96
- logger.error("Erreur training: {}".format(tb))
97
- return "Erreur : {}: {}\n\nDetails:\n{}".format(
98
  type(e).__name__, str(e), tb[-500:]
99
  ), None
100
 
101
-
102
- # -- Conversion Tab --
103
-
104
  def get_model_choices():
105
- """Get list of trained model names for dropdown."""
106
  models = list_models()
107
  if not models:
108
- return ["(aucun modele)"]
109
  return models
110
 
111
 
@@ -119,34 +104,30 @@ def convert_song(
119
  instrumental_volume,
120
  progress=gr.Progress(),
121
  ):
122
- """Full pipeline: separate + convert + mix."""
123
  if song_file is None:
124
- return "Erreur : Veuillez uploader un fichier audio.", None, None, None
125
 
126
- if model_choice == "(aucun modele)" or not model_choice:
127
- return "Erreur : Veuillez d'abord enregistrer une reference vocale.", None, None, None
128
 
129
  from pipeline.mixing import mix_audio
130
 
131
  try:
132
- # Step 1: Download model / find reference audio
133
- progress(0.05, desc="Chargement du modele...")
134
  pth_path, ref_or_index = download_model(model_choice)
135
  if not pth_path:
136
- return "Erreur : Modele '{}' introuvable.".format(model_choice), None, None, None
137
 
138
- # Find the reference audio path
139
  reference_path = get_reference_path(model_choice)
140
  if not reference_path:
141
- return "Erreur : Audio de reference introuvable pour '{}'.".format(model_choice), None, None, None
142
 
143
- # Step 2: Separate vocals from instruments
144
- progress(0.10, desc="Separation des pistes (Demucs)...")
145
  vocals_path, instruments_path = separate_audio(song_file)
146
 
147
- progress(0.40, desc="Conversion vocale (Seed-VC)...")
148
 
149
- # Step 3: Convert vocals with Seed-VC
150
  converted_path = convert_voice(
151
  audio_path=vocals_path,
152
  reference_path=reference_path,
@@ -155,9 +136,8 @@ def convert_song(
155
  similarity=float(similarity),
156
  )
157
 
158
- progress(0.85, desc="Mixage final...")
159
 
160
- # Step 4: Mix converted vocals with instruments
161
  final_path = mix_audio(
162
  vocals_path=converted_path,
163
  instruments_path=instruments_path,
@@ -165,10 +145,10 @@ def convert_song(
165
  instrumental_volume=float(instrumental_volume),
166
  )
167
 
168
- progress(1.0, desc="Termine !")
169
 
170
  return (
171
- "Conversion terminee avec succes !",
172
  vocals_path,
173
  converted_path,
174
  final_path,
@@ -177,103 +157,84 @@ def convert_song(
177
  except Exception as e:
178
  import traceback
179
  tb = traceback.format_exc()
180
- logger.error("Erreur conversion: {}".format(tb))
181
- return "Erreur : {}: {}\n\nDetails:\n{}".format(
182
  type(e).__name__, str(e), tb[-500:]
183
  ), None, None, None
184
 
185
-
186
- # -- Models Tab --
187
-
188
  def refresh_models():
189
- """Refresh the model list as HTML."""
190
  models = list_models()
191
  if not models:
192
- return "<p style='color:gray;'>Aucun modele enregistre</p>"
193
  rows = "".join(
194
  "<tr><td>{}</td><td>Disponible</td></tr>".format(m) for m in models
195
  )
196
  return (
197
  "<table style='width:100%;border-collapse:collapse;'>"
198
- "<tr><th style='text-align:left;border-bottom:1px solid #555;padding:8px;'>Nom</th>"
199
- "<th style='text-align:left;border-bottom:1px solid #555;padding:8px;'>Statut</th></tr>"
200
  "{}</table>".format(rows)
201
  )
202
 
203
 
204
  def delete_selected_model(model_name_to_delete):
205
- """Delete a model."""
206
- if not model_name_to_delete or model_name_to_delete == "(aucun modele)":
207
- return "Veuillez selectionner un modele a supprimer.", refresh_models()
208
  try:
209
  delete_model(model_name_to_delete)
210
- return "Modele '{}' supprime.".format(model_name_to_delete), refresh_models()
211
  except Exception as e:
212
- return "Erreur : {}".format(e), refresh_models()
213
-
214
-
215
- # -- Build Gradio UI --
216
-
217
- DESCRIPTION = """
218
- # Clone Vocal
219
-
220
- Outil de clonage vocal **zero-shot** base sur **Seed-VC** (Diffusion Transformer).
221
-
222
- **Comment utiliser :**
223
- 1. **Onglet "Ma voix"** : Uploadez un court extrait de votre voix (3-30 sec) pour creer votre profil vocal
224
- 2. **Onglet "Convertir"** : Uploadez un morceau de musique, l'outil remplace la voix par la votre
225
- 3. **Onglet "Modeles"** : Gerez vos profils vocaux
226
-
227
- > **Zero-shot** : pas d'entrainement necessaire ! Juste 3-30 secondes de votre voix suffisent.
228
- """
229
 
230
  with gr.Blocks(
231
- title="Clone Vocal",
232
  theme=gr.themes.Soft(),
233
  ) as app:
234
 
235
- gr.Markdown(DESCRIPTION)
236
 
237
  with gr.Tabs():
238
- # Tab 1: Voice Reference
239
- with gr.TabItem("Ma voix"):
240
- gr.Markdown("### Enregistrer votre reference vocale")
241
 
242
  with gr.Row():
243
  with gr.Column(scale=2):
244
  train_audio = gr.Audio(
245
- label="Extrait de votre voix (WAV ou MP3, 3-30 secondes)",
246
  type="filepath",
247
  sources=["upload"],
248
  )
249
  train_model_name = gr.Textbox(
250
- label="Nom du profil",
251
- placeholder="ex: ma_voix",
252
  max_lines=1,
253
  )
254
  train_btn = gr.Button(
255
- "Sauvegarder",
256
  variant="primary",
257
  size="lg",
258
  )
259
 
260
  with gr.Column(scale=1):
261
  train_status = gr.Textbox(
262
- label="Statut",
263
  interactive=False,
264
  lines=3,
265
  )
266
  train_download = gr.File(
267
- label="Fichier de reference",
268
  interactive=False,
269
  )
270
 
271
  gr.Markdown(
272
- "**Conseils :**\n"
273
- "- Utilisez un enregistrement propre (pas de bruit de fond, pas de musique)\n"
274
- "- Parlez ou chantez naturellement pendant 3 a 30 secondes\n"
275
- "- Plus l'extrait est long et varie, meilleur sera le resultat\n"
276
- "- Format WAV ou MP3 accepte"
277
  )
278
 
279
  train_btn.click(
@@ -282,85 +243,85 @@ with gr.Blocks(
282
  outputs=[train_status, train_download],
283
  )
284
 
285
- # Tab 2: Conversion
286
- with gr.TabItem("Convertir un morceau"):
287
- gr.Markdown("### Remplacer la voix d'un morceau par la votre")
288
 
289
  with gr.Row():
290
  with gr.Column(scale=2):
291
  convert_model = gr.Dropdown(
292
  choices=get_model_choices(),
293
- label="Profil vocal",
294
  interactive=True,
295
  )
296
- refresh_btn = gr.Button("Rafraichir la liste", size="sm")
297
  convert_audio = gr.Audio(
298
- label="Morceau a convertir (WAV ou MP3)",
299
  type="filepath",
300
  sources=["upload"],
301
  )
302
 
303
- with gr.Accordion("Parametres avances", open=False):
304
  convert_pitch = gr.Slider(
305
  minimum=-24,
306
  maximum=24,
307
  value=0,
308
  step=1,
309
- label="Transposition (demi-tons)",
310
  )
311
  convert_similarity = gr.Slider(
312
  minimum=0.0,
313
  maximum=1.0,
314
  value=0.7,
315
  step=0.05,
316
- label="Similarite vocale (0.5=naturel, 0.7=equilibre, 0.9=plus fidele)",
317
  )
318
  convert_diffusion = gr.Slider(
319
  minimum=5,
320
  maximum=100,
321
  value=25,
322
  step=5,
323
- label="Qualite (10=rapide, 25=equilibre, 50=haute qualite)",
324
  )
325
  convert_vocal_vol = gr.Slider(
326
  minimum=0.0,
327
  maximum=2.0,
328
  value=1.0,
329
  step=0.1,
330
- label="Volume de la voix",
331
  )
332
  convert_inst_vol = gr.Slider(
333
  minimum=0.0,
334
  maximum=2.0,
335
  value=1.0,
336
  step=0.1,
337
- label="Volume des instruments",
338
  )
339
 
340
  convert_btn = gr.Button(
341
- "Convertir et mixer",
342
  variant="primary",
343
  size="lg",
344
  )
345
 
346
  with gr.Column(scale=1):
347
  convert_status = gr.Textbox(
348
- label="Statut",
349
  interactive=False,
350
  lines=3,
351
  )
352
- gr.Markdown("**Apercu des pistes :**")
353
  preview_vocals = gr.Audio(
354
- label="Voix originale (separee)",
355
  interactive=False,
356
  )
357
  preview_converted = gr.Audio(
358
- label="Voix convertie",
359
  interactive=False,
360
  )
361
- gr.Markdown("**Resultat final :**")
362
  final_output = gr.Audio(
363
- label="Morceau final (voix + instruments)",
364
  interactive=False,
365
  )
366
 
@@ -383,25 +344,25 @@ with gr.Blocks(
383
  outputs=[convert_status, preview_vocals, preview_converted, final_output],
384
  )
385
 
386
- # Tab 3: Models
387
- with gr.TabItem("Mes modeles"):
388
- gr.Markdown("### Gerer vos profils vocaux")
389
 
390
  models_table = gr.HTML(
391
  value=refresh_models(),
392
- label="Modeles enregistres",
393
  )
394
 
395
  with gr.Row():
396
- models_refresh_btn = gr.Button("Rafraichir", size="sm")
397
  models_delete_name = gr.Dropdown(
398
  choices=get_model_choices(),
399
- label="Modele a supprimer",
400
  interactive=True,
401
  )
402
- models_delete_btn = gr.Button("Supprimer", variant="stop", size="sm")
403
 
404
- models_delete_status = gr.Textbox(label="Statut", interactive=False)
405
 
406
  models_refresh_btn.click(
407
  fn=refresh_models,
@@ -418,25 +379,25 @@ with gr.Blocks(
418
  outputs=[models_delete_status, models_table],
419
  )
420
 
421
- # Tab 4: Debug (temporary)
422
- with gr.TabItem("Debug GPU"):
423
- gr.Markdown("### Logs GPU Worker (pour diagnostic)")
424
  debug_output = gr.Textbox(
425
- label="Derniers logs GPU",
426
  interactive=False,
427
  lines=20,
428
  )
429
- debug_btn = gr.Button("Lire les logs", size="sm")
430
 
431
  def read_debug_log():
432
  log_path = "/home/user/app/debug_gpu.log"
433
  if os.path.exists(log_path):
434
  with open(log_path, "r") as f:
435
  return f.read()
436
- return "Aucun log disponible. Lancez d'abord une conversion."
437
 
438
  debug_btn.click(fn=read_debug_log, outputs=[debug_output])
439
 
440
 
441
  if __name__ == "__main__":
442
- app.launch(server_name="0.0.0.0")
 
 
 
 
 
 
1
  import os
2
  import sys
3
  import logging
4
  import tempfile
5
  import shutil
 
6
  import gradio as gr
7
 
 
8
  try:
9
  import gradio_client.utils as _gc_utils
10
 
 
31
  except Exception:
32
  pass
33
 
34
+ # Configuración de logs
35
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
36
  logger = logging.getLogger(__name__)
37
 
38
+ # Inicio: clonar Seed-VC
39
+ logger.info("Inicializando la aplicación...")
40
 
41
  from pipeline.setup import setup_seed_vc
42
  from pipeline.storage import init_storage, list_models, download_model, delete_model, get_reference_path
 
44
  try:
45
  setup_seed_vc()
46
  except Exception as e:
47
+ logger.error("Error durante la configuración: {}".format(e))
48
 
 
49
  HF_MODELS_REPO = os.environ.get("HF_MODELS_REPO", "")
50
  if HF_MODELS_REPO:
51
  init_storage(HF_MODELS_REPO)
52
+ logger.info("Almacenamiento de HuggingFace configurado: {}".format(HF_MODELS_REPO))
53
 
 
54
  from pipeline.training import save_voice_reference, _gpu_warmup
55
  from pipeline.separation import separate_audio
56
  from pipeline.inference import convert_voice
57
 
 
 
 
58
  def train_voice_model(audio_file, model_name, progress=gr.Progress()):
59
+ """Controlador: guardar referencia de voz."""
60
  if audio_file is None:
61
+ return "Error: Por favor, sube un archivo de audio.", None
62
 
63
  if not model_name or not model_name.strip():
64
+ return "Error: Por favor, ingresa un nombre para el modelo.", None
65
 
66
  model_name = model_name.strip().replace(" ", "_")
67
 
 
69
  progress(value, desc=desc)
70
 
71
  try:
72
+ progress(0.0, desc="Iniciando...")
73
  pth_path, ref_path = save_voice_reference(
74
  audio_path=audio_file,
75
  model_name=model_name,
76
  progress_callback=progress_callback,
77
  )
78
 
79
+ return "¡Referencia de voz '{}' guardada con éxito!".format(model_name), ref_path
80
 
81
  except Exception as e:
82
  import traceback
83
  tb = traceback.format_exc()
84
+ logger.error("Error en el entrenamiento: {}".format(tb))
85
+ return "Error : {}: {}\n\nDetalles:\n{}".format(
86
  type(e).__name__, str(e), tb[-500:]
87
  ), None
88
 
 
 
 
89
  def get_model_choices():
90
+ """Obtener lista de nombres de modelos entrenados para el menú desplegable."""
91
  models = list_models()
92
  if not models:
93
+ return ["(ningún modelo)"]
94
  return models
95
 
96
 
 
104
  instrumental_volume,
105
  progress=gr.Progress(),
106
  ):
107
+ """Pipeline completo: separar + convertir + mezclar."""
108
  if song_file is None:
109
+ return "Error: Por favor, sube un archivo de audio.", None, None, None
110
 
111
+ if model_choice == "(ningún modelo)" or not model_choice:
112
+ return "Error: Por favor, guarda una referencia de voz primero.", None, None, None
113
 
114
  from pipeline.mixing import mix_audio
115
 
116
  try:
117
+ progress(0.05, desc="Cargando el modelo...")
 
118
  pth_path, ref_or_index = download_model(model_choice)
119
  if not pth_path:
120
+ return "Error: Modelo '{}' no encontrado.".format(model_choice), None, None, None
121
 
 
122
  reference_path = get_reference_path(model_choice)
123
  if not reference_path:
124
+ return "Error: Audio de referencia no encontrado para '{}'.".format(model_choice), None, None, None
125
 
126
+ progress(0.10, desc="Separación de pistas (Demucs)...")
 
127
  vocals_path, instruments_path = separate_audio(song_file)
128
 
129
+ progress(0.40, desc="Conversión de voz (Seed-VC)...")
130
 
 
131
  converted_path = convert_voice(
132
  audio_path=vocals_path,
133
  reference_path=reference_path,
 
136
  similarity=float(similarity),
137
  )
138
 
139
+ progress(0.85, desc="Mezcla final...")
140
 
 
141
  final_path = mix_audio(
142
  vocals_path=converted_path,
143
  instruments_path=instruments_path,
 
145
  instrumental_volume=float(instrumental_volume),
146
  )
147
 
148
+ progress(1.0, desc="¡Terminado!")
149
 
150
  return (
151
+ "¡Conversión completada con éxito!",
152
  vocals_path,
153
  converted_path,
154
  final_path,
 
157
  except Exception as e:
158
  import traceback
159
  tb = traceback.format_exc()
160
+ logger.error("Error en la conversión: {}".format(tb))
161
+ return "Error : {}: {}\n\nDetalles:\n{}".format(
162
  type(e).__name__, str(e), tb[-500:]
163
  ), None, None, None
164
 
 
 
 
165
  def refresh_models():
166
+ """Actualizar la lista de modelos como HTML."""
167
  models = list_models()
168
  if not models:
169
+ return "<p style='color:gray;'>Ningún modelo guardado</p>"
170
  rows = "".join(
171
  "<tr><td>{}</td><td>Disponible</td></tr>".format(m) for m in models
172
  )
173
  return (
174
  "<table style='width:100%;border-collapse:collapse;'>"
175
+ "<tr><th style='text-align:left;border-bottom:1px solid #555;padding:8px;'>Nombre</th>"
176
+ "<th style='text-align:left;border-bottom:1px solid #555;padding:8px;'>Estado</th></tr>"
177
  "{}</table>".format(rows)
178
  )
179
 
180
 
181
  def delete_selected_model(model_name_to_delete):
182
+ """Eliminar un modelo."""
183
+ if not model_name_to_delete or model_name_to_delete == "(ningún modelo)":
184
+ return "Por favor, selecciona un modelo para eliminar.", refresh_models()
185
  try:
186
  delete_model(model_name_to_delete)
187
+ return "Modelo '{}' eliminado.".format(model_name_to_delete), refresh_models()
188
  except Exception as e:
189
+ return "Error : {}".format(e), refresh_models()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
  with gr.Blocks(
192
+ title="Clon de Voz",
193
  theme=gr.themes.Soft(),
194
  ) as app:
195
 
196
+ gr.Markdown("# Aplicación de Clonación de Voz") # Aquí iría DESCRIPTION si estuviera definida globalmente
197
 
198
  with gr.Tabs():
199
+ # Pestaña 1: Referencia de voz
200
+ with gr.TabItem("Mi voz"):
201
+ gr.Markdown("### Guardar tu referencia de voz")
202
 
203
  with gr.Row():
204
  with gr.Column(scale=2):
205
  train_audio = gr.Audio(
206
+ label="Extracto de tu voz (WAV o MP3, 3-30 segundos)",
207
  type="filepath",
208
  sources=["upload"],
209
  )
210
  train_model_name = gr.Textbox(
211
+ label="Nombre del perfil",
212
+ placeholder="ej: mi_voz",
213
  max_lines=1,
214
  )
215
  train_btn = gr.Button(
216
+ "Guardar",
217
  variant="primary",
218
  size="lg",
219
  )
220
 
221
  with gr.Column(scale=1):
222
  train_status = gr.Textbox(
223
+ label="Estado",
224
  interactive=False,
225
  lines=3,
226
  )
227
  train_download = gr.File(
228
+ label="Archivo de referencia",
229
  interactive=False,
230
  )
231
 
232
  gr.Markdown(
233
+ "**Consejos:**\n"
234
+ "- Usa una grabación limpia (sin ruido de fondo, sin música)\n"
235
+ "- Habla o canta naturalmente durante 3 a 30 segundos\n"
236
+ "- Mientras más largo y variado sea el extracto, mejor será el resultado\n"
237
+ "- Se aceptan formatos WAV o MP3"
238
  )
239
 
240
  train_btn.click(
 
243
  outputs=[train_status, train_download],
244
  )
245
 
246
+ # Pestaña 2: Conversión
247
+ with gr.TabItem("Convertir una canción"):
248
+ gr.Markdown("### Reemplazar la voz de una canción por la tuya")
249
 
250
  with gr.Row():
251
  with gr.Column(scale=2):
252
  convert_model = gr.Dropdown(
253
  choices=get_model_choices(),
254
+ label="Perfil de voz",
255
  interactive=True,
256
  )
257
+ refresh_btn = gr.Button("Actualizar lista", size="sm")
258
  convert_audio = gr.Audio(
259
+ label="Canción a convertir (WAV o MP3)",
260
  type="filepath",
261
  sources=["upload"],
262
  )
263
 
264
+ with gr.Accordion("Parámetros avanzados", open=False):
265
  convert_pitch = gr.Slider(
266
  minimum=-24,
267
  maximum=24,
268
  value=0,
269
  step=1,
270
+ label="Transposición (semitonos)",
271
  )
272
  convert_similarity = gr.Slider(
273
  minimum=0.0,
274
  maximum=1.0,
275
  value=0.7,
276
  step=0.05,
277
+ label="Similitud de voz (0.5=natural, 0.7=equilibrado, 0.9=más fiel)",
278
  )
279
  convert_diffusion = gr.Slider(
280
  minimum=5,
281
  maximum=100,
282
  value=25,
283
  step=5,
284
+ label="Calidad (10=rápido, 25=equilibrado, 50=alta calidad)",
285
  )
286
  convert_vocal_vol = gr.Slider(
287
  minimum=0.0,
288
  maximum=2.0,
289
  value=1.0,
290
  step=0.1,
291
+ label="Volumen de la voz",
292
  )
293
  convert_inst_vol = gr.Slider(
294
  minimum=0.0,
295
  maximum=2.0,
296
  value=1.0,
297
  step=0.1,
298
+ label="Volumen de los instrumentos",
299
  )
300
 
301
  convert_btn = gr.Button(
302
+ "Convertir y mezclar",
303
  variant="primary",
304
  size="lg",
305
  )
306
 
307
  with gr.Column(scale=1):
308
  convert_status = gr.Textbox(
309
+ label="Estado",
310
  interactive=False,
311
  lines=3,
312
  )
313
+ gr.Markdown("**Vista previa de las pistas:**")
314
  preview_vocals = gr.Audio(
315
+ label="Voz original (separada)",
316
  interactive=False,
317
  )
318
  preview_converted = gr.Audio(
319
+ label="Voz convertida",
320
  interactive=False,
321
  )
322
+ gr.Markdown("**Resultado final:**")
323
  final_output = gr.Audio(
324
+ label="Canción final (voz + instrumentos)",
325
  interactive=False,
326
  )
327
 
 
344
  outputs=[convert_status, preview_vocals, preview_converted, final_output],
345
  )
346
 
347
+ # Pestaña 3: Modelos
348
+ with gr.TabItem("Mis modelos"):
349
+ gr.Markdown("### Gestionar tus perfiles de voz")
350
 
351
  models_table = gr.HTML(
352
  value=refresh_models(),
353
+ label="Modelos guardados",
354
  )
355
 
356
  with gr.Row():
357
+ models_refresh_btn = gr.Button("Actualizar", size="sm")
358
  models_delete_name = gr.Dropdown(
359
  choices=get_model_choices(),
360
+ label="Modelo a eliminar",
361
  interactive=True,
362
  )
363
+ models_delete_btn = gr.Button("Eliminar", variant="stop", size="sm")
364
 
365
+ models_delete_status = gr.Textbox(label="Estado", interactive=False)
366
 
367
  models_refresh_btn.click(
368
  fn=refresh_models,
 
379
  outputs=[models_delete_status, models_table],
380
  )
381
 
382
+ # Pestaña 4: Debug (temporal)
383
+ with gr.TabItem("Depuración GPU"):
384
+ gr.Markdown("### Logs del Trabajador GPU (para diagnóstico)")
385
  debug_output = gr.Textbox(
386
+ label="Últimos logs de GPU",
387
  interactive=False,
388
  lines=20,
389
  )
390
+ debug_btn = gr.Button("Leer los logs", size="sm")
391
 
392
  def read_debug_log():
393
  log_path = "/home/user/app/debug_gpu.log"
394
  if os.path.exists(log_path):
395
  with open(log_path, "r") as f:
396
  return f.read()
397
+ return "Ningún log disponible. Ejecuta una conversión primero."
398
 
399
  debug_btn.click(fn=read_debug_log, outputs=[debug_output])
400
 
401
 
402
  if __name__ == "__main__":
403
+ app.launch(server_name="0.0.0.0")