VeuReu commited on
Commit
696c7e3
·
verified ·
1 Parent(s): 07c4f0e

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -934
app.py DELETED
@@ -1,934 +0,0 @@
1
- import os
2
- import io
3
- import json
4
- import yaml
5
- import shutil
6
- import sqlite3
7
- import sys
8
- from pathlib import Path
9
- try:
10
- import tomllib
11
- except ModuleNotFoundError: # Py<3.11
12
- import tomli as tomllib
13
- import streamlit as st
14
- # from moviepy.editor import VideoFileClip
15
-
16
- from database import set_db_path, init_schema, get_user, create_user, update_user_password, create_video, update_video_status, list_videos, get_video, get_all_users, upsert_result, get_results, add_feedback, get_feedback_for_video, get_feedback_stats
17
- from api_client import APIClient
18
- from utils import ensure_dirs, save_bytes, save_text, human_size, get_project_root
19
-
20
- from scripts.client_generate_av import generate_free_ad_mp3, generate_une_ad_video
21
-
22
- # --- Rutas y Configuración Inicial ---
23
- PROJECT_ROOT = get_project_root()
24
-
25
- # Copia de seguridad de la base de datos y vídeos a un directorio escribible en HF Spaces
26
- if os.getenv("SPACE_ID") is not None:
27
- os.environ["STREAMLIT_DATA_DIRECTORY"] = "/tmp/.streamlit"
28
- Path("/tmp/.streamlit").mkdir(parents=True, exist_ok=True)
29
-
30
- # Limpiar archivos temporales antiguos para evitar llenar el disco
31
- import glob
32
- import time
33
- temp_files = glob.glob("/tmp/*")
34
- current_time = time.time()
35
- for f in temp_files:
36
- try:
37
- if os.path.isfile(f) and current_time - os.path.getmtime(f) > 3600: # Archivos > 1 hora
38
- os.remove(f)
39
- print(f"Archivo temporal eliminado: {f}")
40
- except Exception as e:
41
- pass # Ignorar errores de permisos
42
-
43
-
44
- # static_videos = Path(__file__).parent / "videos"
45
- # runtime_videos = PROJECT_ROOT / "videos"
46
- # if not runtime_videos.exists():
47
- # shutil.copytree(static_videos, runtime_videos, dirs_exist_ok=True)
48
-
49
- # --- Config ---
50
- def _load_yaml(path="config.yaml") -> dict:
51
- with open(path, "r", encoding="utf-8") as f:
52
- cfg = yaml.safe_load(f) or {}
53
- # interpolación sencilla de ${VARS} si las usas en el YAML
54
- def _subst(s: str) -> str:
55
- return os.path.expandvars(s) if isinstance(s, str) else s
56
-
57
- # aplica sustitución en los campos que te interesan
58
- if "api" in cfg:
59
- cfg["api"]["base_url"] = _subst(cfg["api"].get("base_url", ""))
60
- cfg["api"]["token"] = _subst(cfg["api"].get("token", ""))
61
-
62
- if "storage" in cfg and "root_dir" in cfg["storage"]:
63
- cfg["storage"]["root_dir"] = _subst(cfg["storage"]["root_dir"])
64
-
65
- if "sqlite" in cfg and "path" in cfg["sqlite"]:
66
- cfg["sqlite"]["path"] = _subst(cfg["sqlite"]["path"])
67
-
68
- return cfg
69
-
70
- CFG = _load_yaml("config.yaml")
71
-
72
- # Ajuste de variables según tu esquema YAML
73
- DATA_DIR = CFG.get("storage", {}).get("root_dir", "data")
74
- BACKEND_BASE_URL = CFG.get("api", {}).get("base_url", "http://localhost:8000")
75
- USE_MOCK = bool(CFG.get("app", {}).get("use_mock", False)) # si no la tienes en el yaml, queda False
76
- API_TOKEN = CFG.get("api", {}).get("token") or os.getenv("API_SHARED_TOKEN")
77
-
78
- os.makedirs(DATA_DIR, exist_ok=True)
79
- ensure_dirs(DATA_DIR)
80
- DB_PATH = os.path.join(DATA_DIR, "app.db")
81
- set_db_path(DB_PATH)
82
-
83
-
84
- init_schema()
85
-
86
- # --- Helper de logging ---
87
- def log(msg):
88
- """Helper para escribir logs que aparezcan en el container de HF Spaces"""
89
- sys.stderr.write(f"{msg}\n")
90
- sys.stderr.flush()
91
-
92
- def create_default_users_if_needed():
93
- """Asegura que existan los usuarios por defecto y sus contraseñas esperadas (texto plano)."""
94
- log("Sincronizando usuarios por defecto...")
95
- users_to_create = [
96
- ("verd", "verd123", "verd"),
97
- ("groc", "groc123", "groc"),
98
- ("taronja", "taronja123", "taronja"),
99
- ("blau", "blau123", "blau"),
100
- ]
101
- for username, password, role in users_to_create:
102
- try:
103
- row = get_user(username)
104
- if row:
105
- update_user_password(username, password)
106
- log(f"Usuario '{username}' actualizado (password reset).")
107
- else:
108
- create_user(username, password, role)
109
- log(f"Usuario '{username}' creado.")
110
- except Exception as e:
111
- log(f"Error sincronizando usuario {username}: {e}")
112
-
113
- create_default_users_if_needed()
114
-
115
- # --- LOG DE DIAGNÓSTICO: COMPROBAR ESTADO DE LA BD ---
116
-
117
- log("\n--- DIAGNÓSTICO DE BASE DE DATOS ---")
118
- log(f"Ruta de la BD en uso: {DB_PATH}")
119
- try:
120
- all_users = get_all_users()
121
- if all_users:
122
- log("Usuarios encontrados en la BD al arrancar:")
123
- # Convertimos las filas a diccionarios para una mejor visualización
124
- users_list = [dict(user) for user in all_users]
125
- log(str(users_list))
126
- else:
127
- log("La tabla de usuarios está vacía.")
128
- except Exception as e:
129
- log(f"Error al intentar leer los usuarios de la BD: {e}")
130
- log("--- FIN DIAGNÓSTICO ---\n")
131
- # --- FIN LOG DE DIAGNÓSTICO ---
132
-
133
- api = APIClient(BACKEND_BASE_URL, use_mock=USE_MOCK, data_dir=DATA_DIR, token=API_TOKEN)
134
-
135
- # Diagnóstico de configuración de API
136
- log(f"\n--- CONFIGURACIÓN DE API ---")
137
- log(f"BACKEND_BASE_URL: {BACKEND_BASE_URL}")
138
- log(f"API_TOKEN configurado: {'Sí' if API_TOKEN else 'No'}")
139
- log(f"USE_MOCK: {USE_MOCK}")
140
- log(f"--- FIN CONFIGURACIÓN ---\n")
141
-
142
- st.set_page_config(page_title="Veureu — Audiodescripció", page_icon="🎬", layout="wide")
143
-
144
- # Configurar Streamlit para aceptar archivos más grandes
145
- try:
146
- import streamlit.web.server.server as server
147
- # Aumentar el límite de tamaño de archivo
148
- server.UPLOAD_FILE_SIZE_LIMIT = 50 * 1024 * 1024 # 50MB
149
- log("Límite de subida configurado a 50MB en server")
150
- except Exception as e:
151
- log(f"No se pudo configurar límite de subida en server: {e}")
152
-
153
- # Verificar configuración actual
154
- try:
155
- import streamlit.config as st_config
156
- max_upload = st_config.get_option("server.maxUploadSize")
157
- log(f"Configuración actual maxUploadSize: {max_upload}MB")
158
- except Exception as e:
159
- log(f"No se pudo leer configuración: {e}")
160
-
161
- # --- Session: auth ---
162
- # print("Usuarios disponibles:", get_all_users()) # Descomentar para depurar
163
- if "user" not in st.session_state:
164
- st.session_state.user = None # dict with {username, role, id(optional)}
165
-
166
- def require_login():
167
- if not st.session_state.user:
168
- st.info("Por favor, inicia sesión para continuar.")
169
- login_form()
170
- st.stop()
171
-
172
- def verify_password(password: str, stored_password: str) -> bool:
173
- """Verifica la contraseña como texto plano."""
174
- return password == stored_password
175
-
176
- # --- Sidebar (only after login) ---
177
- role = st.session_state.user["role"] if st.session_state.user else None
178
- with st.sidebar:
179
- st.title("Veureu")
180
- if st.session_state.user:
181
- st.write(f"Usuari: **{st.session_state.user['username']}** (rol: {st.session_state.user['role']})")
182
- if st.button("Tancar sessió"):
183
- st.session_state.user = None
184
- st.rerun()
185
- if st.session_state.user:
186
- page = st.radio("Navegació", ["Analitzar audio-descripcions","Processar vídeo nou","Estadístiques"], index=0)
187
- log(f"Página seleccionada: {page}")
188
- else:
189
- page = None
190
-
191
- # --- Pre-login screen ---
192
- if not st.session_state.user:
193
- st.title("Veureu — Audiodescripció")
194
- def login_form():
195
- st.subheader("Inici de sessió")
196
- username = st.text_input("Usuari")
197
- password = st.text_input("Contrasenya", type="password")
198
- if st.button("Entrar", type="primary"):
199
- row = get_user(username)
200
-
201
- # --- LOGS DE DEPURACIÓN ---
202
- log("\n--- INTENTO DE LOGIN ---")
203
- log(f"Usuario introducido: '{username}'")
204
- # No mostramos la contraseña por seguridad, pero confirmamos que no está vacía
205
- log(f"Contraseña introducida: {'Sí' if password else 'No'}")
206
-
207
- if row:
208
- log(f"Usuario encontrado en BD: '{row['username']}'")
209
- stored_pw = (row["password_hash"] or "")
210
- log(f"Password almacenado (longitud): {len(stored_pw)}")
211
- is_valid = verify_password(password, stored_pw)
212
- log(f"Resultado de verify_password: {is_valid}")
213
- else:
214
- log("Usuario no encontrado en la BD.")
215
- is_valid = False
216
-
217
- log("--- FIN INTENTO DE LOGIN ---\n")
218
- # --- FIN LOGS DE DEPURACIÓN ---
219
-
220
- if is_valid:
221
- st.session_state.user = {"id": row["id"], "username": row["username"], "role": row["role"]}
222
- st.success(f"Benvingut/da, {row['username']}")
223
- st.rerun()
224
- else:
225
- st.error("Credencials invàlides")
226
- login_form()
227
- st.stop()
228
-
229
- # --- Pages ---
230
- if page == "Processar vídeo nou":
231
- log("\n=== ACCESO A PÁGINA 'Processar vídeo nou' ===")
232
- require_login()
233
- if role != "verd":
234
- log("ERROR: Usuario sin permisos para procesar vídeos")
235
- st.error("No tens permisos per processar nous vídeos. Canvia d'usuari o sol·licita permisos.")
236
- st.stop()
237
-
238
- log("Usuario autorizado, mostrando interfaz de subida")
239
- st.header("Processar un nou clip de vídeo")
240
-
241
- # Inicializar el estado de la página si no existe
242
- if 'video_uploaded' not in st.session_state:
243
- st.session_state.video_uploaded = None
244
- log("Estado 'video_uploaded' inicializado")
245
- if 'characters_detected' not in st.session_state:
246
- st.session_state.characters_detected = None
247
- log("Estado 'characters_detected' inicializado")
248
- if 'characters_saved' not in st.session_state:
249
- st.session_state.characters_saved = False
250
- log("Estado 'characters_saved' inicializado")
251
-
252
- # --- 1. Subida del vídeo ---
253
- MAX_SIZE_MB = 10 # Reducido a 10MB para evitar llenar el disco en HF Spaces
254
- MAX_DURATION_S = 120 # Reducido a 2 minutos
255
-
256
- log("Mostrando widget de subida de archivo...")
257
-
258
- # Verificar configuración de Streamlit
259
- import streamlit.config as st_config
260
- try:
261
- max_size = st_config.get_option("server.maxUploadSize")
262
- log(f"Configuración maxUploadSize: {max_size}MB")
263
- except Exception as e:
264
- log(f"No se pudo leer maxUploadSize: {e}")
265
-
266
- uploaded_file = st.file_uploader(
267
- "Puja un clip de vídeo (MP4, < 10MB, < 2 minuts)",
268
- type=["mp4"],
269
- key="video_uploader",
270
- help="Selecciona un archivo MP4 de tu dispositivo. Máximo 10MB."
271
- )
272
- log(f"Widget renderizado. Archivo subido: {uploaded_file is not None}")
273
-
274
- # Debug: Mostrar información del archivo si existe
275
- if uploaded_file is not None:
276
- log(f"¡ARCHIVO DETECTADO! Nombre: {uploaded_file.name}, Tamaño: {uploaded_file.size}")
277
- else:
278
- log("No hay archivo subido todavía")
279
- # Mostrar mensaje de ayuda en la interfaz
280
- st.info("ℹ️ **Instrucciones:**\n1. Haz clic en 'Browse files'\n2. Selecciona un archivo MP4 (< 10MB)\n3. Espera a que se cargue (puede tardar unos segundos)")
281
- st.warning("⚠️ **Nota**: Si el archivo no se detecta después de seleccionarlo, puede haber un problema con la configuración de Hugging Face Spaces.")
282
-
283
- # Mostrar estado de depuración solo si no hay archivo o si hay un problema
284
- if uploaded_file is None and st.session_state.video_uploaded is None:
285
- with st.expander("🔍 Debug Info", expanded=False):
286
- st.write(f"**Archivo subido:** {uploaded_file is not None}")
287
- st.info("ℹ️ No se ha seleccionado ningún archivo todavía")
288
- st.write(f"**Estado video_uploaded:** {st.session_state.video_uploaded}")
289
-
290
- if uploaded_file is not None:
291
- log(f"\n--- SUBIDA DE VÍDEO INICIADA (archivo detectado) ---")
292
- log(f"Nombre del archivo: {uploaded_file.name}")
293
- log(f"Tamaño del archivo: {uploaded_file.size} bytes ({uploaded_file.size / (1024*1024):.2f} MB)")
294
- log(f"Tipo MIME: {uploaded_file.type}")
295
-
296
- # Resetear el estado si se sube un nuevo archivo
297
- if st.session_state.video_uploaded is None or uploaded_file.name != st.session_state.video_uploaded.get('original_name'):
298
- log(f"Nuevo archivo detectado, reseteando estado...")
299
- st.session_state.video_uploaded = {'original_name': uploaded_file.name, 'status': 'validating'}
300
- st.session_state.characters_detected = None
301
- st.session_state.characters_saved = False
302
-
303
- # --- Validación y Procesamiento ---
304
- if st.session_state.video_uploaded['status'] == 'validating':
305
- log(f"Validando archivo...")
306
- is_valid = True
307
- error_messages = []
308
-
309
- # 1. Validar tamaño
310
- if uploaded_file.size > MAX_SIZE_MB * 1024 * 1024:
311
- error_msg = f"El vídeo supera el límit de {MAX_SIZE_MB}MB. Tamaño actual: {uploaded_file.size / (1024*1024):.2f}MB"
312
- log(f"ERROR: {error_msg}")
313
- st.error(error_msg)
314
- st.warning("💡 **Consejo**: Reduce el tamaño del vídeo o usa un clip más corto.")
315
- error_messages.append(error_msg)
316
- is_valid = False
317
- else:
318
- log(f"✓ Tamaño válido: {uploaded_file.size / (1024*1024):.2f} MB")
319
-
320
- # 2. Validar que el archivo no esté vacío
321
- if uploaded_file.size == 0:
322
- error_msg = "El archivo está vacío."
323
- log(f"ERROR: {error_msg}")
324
- st.error(error_msg)
325
- error_messages.append(error_msg)
326
- is_valid = False
327
-
328
- if is_valid:
329
- try:
330
- with st.spinner("Processant el vídeo..."):
331
- log("Leyendo bytes del archivo...")
332
- # Guardar el buffer en memoria para enviarlo al engine
333
- video_bytes = uploaded_file.getbuffer().tobytes()
334
- log(f"✓ Bytes leídos correctamente: {len(video_bytes)} bytes")
335
-
336
- video_name = Path(uploaded_file.name).stem
337
- log(f"Nombre del vídeo (sin extensión): {video_name}")
338
-
339
- # Actualizar estado con los bytes del video
340
- st.session_state.video_uploaded.update({
341
- 'status': 'processed',
342
- 'video_bytes': video_bytes,
343
- 'video_name': f"{video_name}.mp4",
344
- 'was_truncated': False
345
- })
346
- log(f"✓ Estado actualizado correctamente")
347
- log(f"--- FIN SUBIDA DE VÍDEO (ÉXITO) ---\n")
348
- st.rerun()
349
- except Exception as e:
350
- error_msg = f"Error al procesar el vídeo: {str(e)}"
351
- log(f"ERROR CRÍTICO: {error_msg}")
352
- log(f"Tipo de error: {type(e).__name__}")
353
- import traceback
354
- log(f"Traceback completo:\n{traceback.format_exc()}")
355
- log(f"--- FIN SUBIDA DE VÍDEO (ERROR) ---\n")
356
- st.error(error_msg)
357
- st.session_state.video_uploaded = None
358
- else:
359
- log(f"Validación fallida. Errores: {error_messages}")
360
- log(f"--- FIN SUBIDA DE VÍDEO (VALIDACIÓN FALLIDA) ---\n")
361
- st.session_state.video_uploaded = None
362
-
363
- # --- Mensajes de estado ---
364
- if st.session_state.video_uploaded and st.session_state.video_uploaded['status'] == 'processed':
365
- st.success(f"✅ Vídeo '{st.session_state.video_uploaded['original_name']}' pujat i processat correctament.")
366
-
367
- # Mostrar info del vídeo procesado
368
- with st.expander("📊 Informació del vídeo", expanded=False):
369
- st.write(f"**Nombre:** {st.session_state.video_uploaded['original_name']}")
370
- st.write(f"**Tamaño:** {len(st.session_state.video_uploaded.get('video_bytes', [])) / (1024*1024):.2f} MB")
371
- st.write(f"**Estado:** Processat i llest per detectar personatges")
372
-
373
- if st.session_state.video_uploaded['was_truncated']:
374
- st.warning(f"El vídeo s'ha truncat a {MAX_DURATION_S // 60} minuts.")
375
-
376
- # --- 2. Detección de personajes ---
377
- st.markdown("---")
378
- col1, col2 = st.columns([1, 3])
379
- # Sliders a la derecha del botón
380
- with col2:
381
- epsilon = st.slider("sensibitivity (épsilon)", 0.0, 2.0, 0.5, 0.1, key="epsilon_slider")
382
- min_cluster_size = st.slider("mínimum cluster size", 1, 5, 2, 1, key="min_cluster_slider")
383
- # Botón a la izquierda
384
- with col1:
385
- detect_button_disabled = st.session_state.video_uploaded is None
386
- if st.button("Detectar Personatges", disabled=detect_button_disabled):
387
- log(f"\n--- DETECCIÓN DE PERSONAJES INICIADA ---")
388
-
389
- with st.spinner("Detectant personatges..."):
390
- # Llamar al endpoint del engine para crear el casting inicial
391
- try:
392
- video_bytes = st.session_state.video_uploaded.get('video_bytes') if st.session_state.video_uploaded else None
393
- video_name = st.session_state.video_uploaded.get('video_name') if st.session_state.video_uploaded else None
394
-
395
- # NO logear los bytes del vídeo (son binarios y ensucian el log)
396
- log(f"Video bytes disponibles: {len(video_bytes) if video_bytes else 0} bytes")
397
- log(f"Nombre del vídeo: {video_name}")
398
-
399
- if not video_bytes:
400
- error_msg = "No s'ha trobat el vídeo pujat en memòria."
401
- log(f"ERROR: {error_msg}")
402
- st.error(error_msg)
403
- else:
404
- # Verificar configuración antes de llamar
405
- log(f"BACKEND_BASE_URL: {BACKEND_BASE_URL}")
406
- log(f"API_TOKEN configurado: {'Sí' if API_TOKEN else 'No'}")
407
-
408
- if BACKEND_BASE_URL == "http://localhost:8000" or not BACKEND_BASE_URL:
409
- error_msg = "⚠️ **Error de configuració**: La URL del servei 'engine' no està configurada correctament."
410
- log(f"ERROR: {error_msg}")
411
- st.error(error_msg)
412
- st.info(f"URL actual: `{BACKEND_BASE_URL}`\n\nConfigura la variable d'entorn `API_BASE_URL` amb la URL pública del Space 'engine'.")
413
- else:
414
- log(f"Llamando a create_initial_casting...")
415
- log(f"Parámetros: epsilon={st.session_state.get('epsilon_slider', epsilon)}, min_cluster_size={int(st.session_state.get('min_cluster_slider', min_cluster_size))}")
416
-
417
- resp = api.create_initial_casting(
418
- video_bytes=video_bytes,
419
- video_name=video_name,
420
- epsilon=st.session_state.get("epsilon_slider", epsilon),
421
- min_cluster_size=int(st.session_state.get("min_cluster_slider", min_cluster_size)),
422
- )
423
-
424
- log(f"Respuesta recibida: {resp}")
425
-
426
- if isinstance(resp, dict) and resp.get("error"):
427
- error_msg = resp['error']
428
- log(f"ERROR en la respuesta: {error_msg}")
429
- st.error(f"❌ **Error en 'create_initial_casting'**: {error_msg}")
430
- if "403" in error_msg or "Forbidden" in error_msg:
431
- st.warning("**Possible causes:**\n- El Space 'engine' no està accessible públicament\n- El token d'API no és correcte\n- CORS bloquejat")
432
- elif "Connection" in error_msg or "timeout" in error_msg:
433
- st.warning(f"**No s'ha pogut connectar** amb el servei engine a: `{BACKEND_BASE_URL}`")
434
- elif isinstance(resp, dict) and resp.get("job_id"):
435
- # El engine devolvió un job_id - hacer polling
436
- job_id = resp["job_id"]
437
- log(f"Job creado con ID: {job_id}")
438
- log(f"Iniciando polling del estado...")
439
-
440
- # Placeholder para mensajes
441
- message_placeholder = st.empty()
442
-
443
- # Mostrar spinner durante el procesamiento
444
- import time
445
- max_attempts = 60 # 5 minutos máximo (5 segundos * 60)
446
- attempt = 0
447
-
448
- with message_placeholder:
449
- with st.spinner("⏳ Detectant personatges... Això pot trigar uns minuts."):
450
- while attempt < max_attempts:
451
- job_status = api.get_job(job_id)
452
- status = job_status.get("status", "unknown")
453
-
454
- log(f"Polling attempt {attempt + 1}: status = {status}")
455
-
456
- if status == "done":
457
- log(f"✓ Job completado exitosamente")
458
-
459
- # DEBUG: Ver estructura completa de la respuesta
460
- log(f"DEBUG - job_status completo: {job_status}")
461
-
462
- # Guardar los resultados si los hay
463
- if "results" in job_status:
464
- log(f"DEBUG - results: {job_status['results']}")
465
- characters = job_status["results"].get("characters", [])
466
- log(f"DEBUG - characters extraídos: {characters}")
467
-
468
- if characters:
469
- st.session_state.characters_detected = characters
470
- num_chars = len(st.session_state.characters_detected)
471
- log(f"Personajes detectados: {num_chars}")
472
-
473
- # Mensaje en catalán
474
- if num_chars == 1:
475
- st.success(f"✅ S'ha detectat {num_chars} personatge possible.")
476
- else:
477
- st.success(f"✅ S'han detectat {num_chars} personatges possibles.")
478
- else:
479
- log(f"WARNING - No se encontraron personajes en results")
480
- st.warning("⚠️ No s'han detectat personatges al vídeo.")
481
- else:
482
- log(f"WARNING - No hay 'results' en job_status")
483
- st.warning("⚠️ No s'han rebut resultats del servidor.")
484
- break
485
- elif status == "failed":
486
- error_msg = job_status.get("error", "Unknown error")
487
- log(f"✗ Job falló: {error_msg}")
488
- st.error(f"❌ Error en el processament: {error_msg}")
489
- break
490
- elif status in ["queued", "processing"]:
491
- # Solo esperar, el spinner ya muestra que está procesando
492
- time.sleep(5) # Esperar 5 segundos antes del siguiente polling
493
- attempt += 1
494
- else:
495
- log(f"Estado desconocido: {status}")
496
- time.sleep(5)
497
- attempt += 1
498
-
499
- if attempt >= max_attempts:
500
- log(f"✗ Timeout: el job no se completó en el tiempo esperado")
501
- st.warning("⚠️ El processament està trigant més del previst. El job continua executant-se al servidor.")
502
- else:
503
- # Respuesta sin job_id ni error - asumimos éxito inmediato (modo antiguo)
504
- log(f"✓ Respuesta recibida sin job_id (modo síncrono)")
505
- st.success("✅ Casting inicial creat. S'han generat subcarpetes a 'temp/<uploaded-video>/*'.")
506
- except Exception as e:
507
- error_msg = f"❌ Error inesperat: {e}"
508
- log(f"ERROR CRÍTICO: {error_msg}")
509
- log(f"Tipo de error: {type(e).__name__}")
510
- import traceback
511
- log(f"Traceback completo:\n{traceback.format_exc()}")
512
- st.error(error_msg)
513
- finally:
514
- log(f"--- FIN DETECCIÓN DE PERSONAJES ---\n")
515
-
516
- # Datos de ejemplo para mostrar UI (mientras no tengamos retorno estructurado)
517
- # NOTA: Comentado temporalmente porque placeholder.png no existe
518
- # st.session_state.characters_detected = [
519
- # {"id": "char1", "image_path": "init_data/placeholder.png", "description": "Dona amb cabell ros i ulleres"},
520
- # {"id": "char2", "image_path": "init_data/placeholder.png", "description": "Home amb barba i barret"},
521
- # ]
522
- # st.session_state.characters_saved = False
523
-
524
- # --- 3. Formularios de personajes (apilados) ---
525
- if st.session_state.characters_detected:
526
- st.markdown("---")
527
- st.subheader(f"📋 Personatges detectats: {len(st.session_state.characters_detected)}")
528
- st.info("Edita cada personatge i confirma el fine-tuning manual al final. Els personatges amb el mateix nom es fusionaran.")
529
-
530
- # Inicializar datos de personajes si no existe
531
- if 'character_data' not in st.session_state:
532
- st.session_state.character_data = {}
533
- # Inicializar con datos por defecto
534
- for char in st.session_state.characters_detected:
535
- char_id = char['id']
536
- st.session_state.character_data[char_id] = {
537
- 'name': char.get('name', ''),
538
- 'description': '',
539
- 'selected_faces': list(range(char.get('num_faces', 0))), # Todas seleccionadas por defecto
540
- 'selected_voices': [], # Por ahora vacío
541
- 'current_face_idx': 0,
542
- 'current_voice_idx': 0
543
- }
544
-
545
- # Mostrar formulario para cada personaje
546
- for idx, char in enumerate(st.session_state.characters_detected):
547
- char_id = char['id']
548
- char_data = st.session_state.character_data[char_id]
549
-
550
- # Contenedor con borde para cada personaje
551
- with st.container():
552
- st.markdown(f"### Personatge {idx + 1}: {char_data['name'] or char_id}")
553
-
554
- col1, col2 = st.columns([1, 1])
555
-
556
- # --- Columna 1: Visualizadores ---
557
- with col1:
558
- # Visualizador de caras
559
- st.markdown("**🖼️ Mostres de cara:**")
560
-
561
- num_faces = char.get('num_faces', 0)
562
- if num_faces > 0 and char_data['selected_faces']:
563
- current_face_idx = char_data['current_face_idx']
564
- selected_faces = char_data['selected_faces']
565
-
566
- # Navegación de caras
567
- col_nav1, col_nav2, col_nav3, col_nav4 = st.columns([1, 2, 1, 1])
568
-
569
- with col_nav1:
570
- if st.button("◀", key=f"face_prev_{char_id}", disabled=(current_face_idx == 0)):
571
- st.session_state.character_data[char_id]['current_face_idx'] = max(0, current_face_idx - 1)
572
- st.rerun()
573
-
574
- with col_nav2:
575
- st.caption(f"Cara {current_face_idx + 1} de {len(selected_faces)}")
576
-
577
- with col_nav3:
578
- if st.button("▶", key=f"face_next_{char_id}", disabled=(current_face_idx >= len(selected_faces) - 1)):
579
- st.session_state.character_data[char_id]['current_face_idx'] = min(len(selected_faces) - 1, current_face_idx + 1)
580
- st.rerun()
581
-
582
- with col_nav4:
583
- if st.button("❌", key=f"face_delete_{char_id}", disabled=(len(selected_faces) <= 1)):
584
- # Eliminar cara actual
585
- face_to_remove = selected_faces[current_face_idx]
586
- st.session_state.character_data[char_id]['selected_faces'].remove(face_to_remove)
587
- st.session_state.character_data[char_id]['current_face_idx'] = min(current_face_idx, len(selected_faces) - 2)
588
- st.rerun()
589
-
590
- # Mostrar imagen de la cara actual
591
- if 'folder' in char:
592
- try:
593
- # Construir URL de la cara
594
- face_filename = f"face_{selected_faces[current_face_idx]:03d}.jpg"
595
- face_url = f"{BACKEND_BASE_URL}/files/{st.session_state.video_name}/{char_id}/{face_filename}"
596
- st.image(face_url, width=250)
597
- except Exception as e:
598
- st.info(f"Imatge no disponible: {e}")
599
- else:
600
- st.info("No hi ha mostres de cara")
601
-
602
- st.markdown("---")
603
-
604
- # Visualizador de voces
605
- st.markdown("**🎤 Mostres de veu:**")
606
- st.info("🚧 Funcionalitat de veu en desenvolupament")
607
-
608
- # TODO: Implementar visualizador de voces similar al de caras
609
-
610
- # --- Columna 2: Datos del personaje ---
611
- with col2:
612
- st.markdown("**📝 Informació del personatge:**")
613
-
614
- # Nombre del personaje
615
- char_name = st.text_input(
616
- "Nom del personatge:",
617
- value=char_data['name'],
618
- key=f"name_input_{char_id}",
619
- placeholder="Ex: Maria, Joan, etc.",
620
- help="Personatges amb el mateix nom es fusionaran"
621
- )
622
-
623
- # Actualizar nombre en tiempo real
624
- if char_name != char_data['name']:
625
- st.session_state.character_data[char_id]['name'] = char_name
626
-
627
- # Descripción
628
- char_description = st.text_area(
629
- "Descripció (text lliure):",
630
- value=char_data['description'],
631
- key=f"desc_input_{char_id}",
632
- placeholder="Ex: Dona d'uns 30 anys, cabell ros, ulleres...",
633
- height=150
634
- )
635
-
636
- # Actualizar descripción en tiempo real
637
- if char_description != char_data['description']:
638
- st.session_state.character_data[char_id]['description'] = char_description
639
-
640
- # Información adicional
641
- st.caption(f"**ID original:** {char_id}")
642
- st.caption(f"**Caras seleccionades:** {len(char_data['selected_faces'])} de {num_faces}")
643
-
644
- st.markdown("---")
645
-
646
- # --- 4. Botón de confirmación de fine-tuning ---
647
- st.markdown("### 🎯 Confirmació del fine-tuning manual")
648
-
649
- if st.button("✅ Confirmar fine-tuning i fusionar personatges", type="primary", use_container_width=True):
650
- # Agrupar personajes por nombre
651
- merged_characters = {}
652
-
653
- for char in st.session_state.characters_detected:
654
- char_id = char['id']
655
- char_data = st.session_state.character_data[char_id]
656
- char_name = char_data['name'].strip()
657
-
658
- if not char_name:
659
- char_name = f"Personatge sense nom {char_id}"
660
-
661
- if char_name not in merged_characters:
662
- merged_characters[char_name] = {
663
- 'id': f"merged_{len(merged_characters) + 1}",
664
- 'name': char_name,
665
- 'description': char_data['description'],
666
- 'selected_faces': [],
667
- 'selected_voices': [],
668
- 'original_ids': []
669
- }
670
-
671
- # Fusionar datos
672
- merged_characters[char_name]['selected_faces'].extend(char_data['selected_faces'])
673
- merged_characters[char_name]['selected_voices'].extend(char_data['selected_voices'])
674
- merged_characters[char_name]['original_ids'].append(char_id)
675
-
676
- # Fusionar descripciones (concatenar si hay múltiples)
677
- if char_data['description'] and char_data['description'] not in merged_characters[char_name]['description']:
678
- if merged_characters[char_name]['description']:
679
- merged_characters[char_name]['description'] += " | " + char_data['description']
680
- else:
681
- merged_characters[char_name]['description'] = char_data['description']
682
-
683
- # Actualizar personajes con los fusionados
684
- st.session_state.characters_detected = list(merged_characters.values())
685
-
686
- # Reinicializar character_data con los nuevos personajes
687
- st.session_state.character_data = {}
688
- for char in st.session_state.characters_detected:
689
- char_id = char['id']
690
- st.session_state.character_data[char_id] = {
691
- 'name': char['name'],
692
- 'description': char['description'],
693
- 'selected_faces': char['selected_faces'],
694
- 'selected_voices': char['selected_voices'],
695
- 'current_face_idx': 0,
696
- 'current_voice_idx': 0
697
- }
698
-
699
- # Marcar como guardados
700
- st.session_state.characters_saved = True
701
- st.success(f"✅ Fine-tuning confirmat! {len(merged_characters)} personatges finals.")
702
- st.balloons()
703
- st.rerun()
704
-
705
- # --- 5. Botón para generar audiodescripción (solo si están guardados) ---
706
- if st.session_state.characters_saved:
707
- st.markdown("---")
708
- st.markdown("### 🎬 Generar audiodescripció")
709
- if st.button("🎬 Generar Audiodescripció", type="primary", use_container_width=True):
710
- st.info("🚧 Funcionalitat en desenvolupament...")
711
- # Aquí iría la lógica para generar la audiodescripción
712
-
713
- elif page == "Analitzar audio-descripcions":
714
- require_login()
715
- st.header("Analitzar audio-descripcions")
716
-
717
- # En HF Spaces, los videos están en el repo (read-only), no en /tmp/data
718
- if os.getenv("SPACE_ID") is not None:
719
- base_dir = Path(__file__).resolve().parent / "videos"
720
- else:
721
- base_dir = PROJECT_ROOT / "videos"
722
-
723
- if not base_dir.exists():
724
- st.info("No s'ha trobat la carpeta **videos**. Crea-la i afegeix-hi subcarpetes amb els teus vídeos.")
725
- st.stop()
726
-
727
- carpetes = [p.name for p in sorted(base_dir.iterdir()) if p.is_dir() and p.name != 'completed']
728
- if not carpetes:
729
- st.info("No s'ha trobat la carpeta **videos**. Crea-la i afegeix-hi subcarpetes amb els teus vídeos.")
730
- st.stop()
731
-
732
- # --- Lógica de Estado y Selección ---
733
-
734
- # Detectar si el vídeo principal ha cambiado para resetear el estado secundario
735
- if 'current_video' not in st.session_state:
736
- st.session_state.current_video = None
737
-
738
- # Widget de selección de vídeo
739
- seleccio = st.selectbox("Selecciona un vídeo (carpeta):", carpetes, index=None, placeholder="Tria una carpeta…")
740
-
741
- if seleccio != st.session_state.current_video:
742
- st.session_state.current_video = seleccio
743
- # Forzar reseteo de los widgets dependientes
744
- # No establecemos a None, dejamos que el widget se inicialice
745
- st.session_state.add_ad_checkbox = False
746
- if 'version_selector' in st.session_state:
747
- del st.session_state['version_selector'] # Forzar reinicio completo del widget
748
- st.rerun()
749
-
750
- if not seleccio:
751
- st.stop()
752
-
753
- vid_dir = base_dir / seleccio
754
- mp4s = sorted(vid_dir.glob("*.mp4"))
755
-
756
- # --- Dibujado de la Interfaz ---
757
- col_video, col_txt = st.columns([2, 1], gap="large")
758
-
759
- with col_video:
760
- # Selección de versión
761
- subcarpetas_ad = [p.name for p in sorted(vid_dir.iterdir()) if p.is_dir()]
762
- default_index_sub = subcarpetas_ad.index("Salamandra") if "Salamandra" in subcarpetas_ad else 0
763
- subcarpeta_seleccio = st.selectbox(
764
- "Selecciona una versió d'audiodescripció:", subcarpetas_ad,
765
- index=default_index_sub if subcarpetas_ad and 'version_selector' not in st.session_state else 0,
766
- placeholder="Tria una versió…" if subcarpetas_ad else "No hi ha versions",
767
- key="version_selector"
768
- )
769
-
770
- # Lógica de vídeo AD
771
- video_ad_path = vid_dir / subcarpeta_seleccio / "une_ad.mp4" if subcarpeta_seleccio else None
772
- is_ad_video_available = video_ad_path is not None and video_ad_path.exists()
773
-
774
- # Checkbox
775
- add_ad_video = st.checkbox("Afegir audiodescripció", disabled=not is_ad_video_available, key="add_ad_checkbox")
776
-
777
- # Decidir qué vídeo mostrar
778
- video_to_show = None
779
- if add_ad_video and is_ad_video_available:
780
- video_to_show = video_ad_path
781
- elif mp4s:
782
- video_to_show = mp4s[0]
783
-
784
- if video_to_show:
785
- st.video(str(video_to_show))
786
- else:
787
- st.warning("No s'ha trobat cap fitxer **.mp4** a la carpeta seleccionada.")
788
-
789
- st.markdown("---")
790
-
791
- # Sección de ACCIONES
792
- st.markdown("#### Accions")
793
- c1, c2 = st.columns(2)
794
- with c1:
795
- if st.button("Reconstruir àudio amb narració lliure", use_container_width=True, key="rebuild_free_ad"):
796
- if seleccio and subcarpeta_seleccio:
797
- with st.spinner("Generant àudio de la narració lliure..."):
798
- result = generate_free_ad_mp3(seleccio, subcarpeta_seleccio, api, PROJECT_ROOT)
799
- if result.get("status") == "success":
800
- st.success(f"Àudio generat amb èxit: {result.get('path')}")
801
- else:
802
- st.error(f"Error: {result.get('reason', 'Desconegut')}")
803
- else:
804
- st.warning("Selecciona un vídeo i una versió.")
805
-
806
- with c2:
807
- if st.button("Reconstruir vídeo amb audiodescripció", use_container_width=True, key="rebuild_video_ad"):
808
- if seleccio and subcarpeta_seleccio:
809
- with st.spinner("Reconstruint el vídeo... Aquesta operació pot trigar."):
810
- result = generate_une_ad_video(seleccio, subcarpeta_seleccio, api, PROJECT_ROOT)
811
- if result.get("status") == "success":
812
- st.success(f"Vídeo generat amb èxit: {result.get('path')}")
813
- st.info("Pots visualitzar-lo activant la casella 'Afegir audiodescripció'.")
814
- else:
815
- st.error(f"Error: {result.get('reason', 'Desconegut')}")
816
- else:
817
- st.warning("Selecciona un vídeo i una versió.")
818
-
819
-
820
- # --- Columna Derecha (Editor de texto y guardado) ---
821
- with col_txt:
822
- tipus_ad_options = ["narració lliure", "UNE-153010"]
823
- tipus_ad_seleccio = st.selectbox("Fitxer d'audiodescripció a editar:", tipus_ad_options)
824
-
825
- ad_filename = "free_ad.txt" if tipus_ad_seleccio == "narració lliure" else "une_ad.srt"
826
-
827
- # Cargar el contenido del fichero seleccionado
828
- text_content = ""
829
- ad_path = None
830
- if subcarpeta_seleccio:
831
- ad_path = vid_dir / subcarpeta_seleccio / ad_filename
832
- if ad_path.exists():
833
- try:
834
- text_content = ad_path.read_text(encoding="utf-8")
835
- except Exception:
836
- text_content = ad_path.read_text(errors="ignore")
837
- else:
838
- st.info(f"No s'ha trobat el fitxer **{ad_filename}**.")
839
- else:
840
- # Eliminada la nota de advertencia
841
- pass
842
-
843
- # Área de texto para edición
844
- new_text = st.text_area(f"Contingut de {tipus_ad_seleccio}", value=text_content, height=500, key=f"editor_{seleccio}_{subcarpeta_seleccio}_{ad_filename}")
845
-
846
- # Botón de guardado
847
- if st.button("Desar canvis", use_container_width=True, type="primary"):
848
- if ad_path:
849
- try:
850
- save_text(ad_path, new_text)
851
- st.success(f"Fitxer **{ad_filename}** desat correctament.")
852
- # El st.rerun() ya se encarga de recargar el estado desde el fichero guardado.
853
- # La línea que modificaba el session_state directamente ha sido eliminada para evitar el error.
854
- st.rerun()
855
- except Exception as e:
856
- st.error(f"No s'ha pogut desar el fitxer: {e}")
857
- else:
858
- st.error("No s'ha seleccionat una ruta de fitxer vàlida per desar.")
859
-
860
- # Controles de reproducción de narración
861
- free_ad_mp3_path = vid_dir / subcarpeta_seleccio / "free_ad.mp3" if seleccio and subcarpeta_seleccio else None
862
- can_play_free_ad = free_ad_mp3_path is not None and free_ad_mp3_path.exists()
863
-
864
- if st.button("▶️ Reproduir narració lliure", use_container_width=True, disabled=not can_play_free_ad, key="play_button_editor"):
865
- if can_play_free_ad:
866
- st.audio(str(free_ad_mp3_path), format="audio/mp3")
867
- else:
868
- st.warning("No s'ha trobat el fitxer 'free_ad.mp3'. Reconstrueix l'àudio primer.")
869
-
870
-
871
- st.markdown("---")
872
- st.subheader("Avaluació de la qualitat de l'audiodescripció")
873
-
874
- c1, c2, c3 = st.columns(3)
875
- with c1:
876
- transcripcio = st.slider("Transcripció", 1, 10, 7)
877
- identificacio = st.slider("Identificació de personatges", 1, 10, 7)
878
- with c2:
879
- localitzacions = st.slider("Localitzacions", 1, 10, 7)
880
- activitats = st.slider("Activitats", 1, 10, 7)
881
- with c3:
882
- narracions = st.slider("Narracions", 1, 10, 7)
883
- expressivitat = st.slider("Expressivitat", 1, 10, 7)
884
-
885
- comments = st.text_area("Comentaris (opcional)", placeholder="Escriu els teus comentaris lliures…", height=120)
886
-
887
- role = st.session_state.user["role"]
888
- can_rate = role in ("verd", "groc", "blau")
889
-
890
- if not can_rate:
891
- st.info("El teu rol no permet enviar valoracions.")
892
- else:
893
- if st.button("Enviar valoració", type="primary", use_container_width=True):
894
- try:
895
- add_feedback_ad(
896
- video_name=seleccio,
897
- user_id=st.session_state.user["id"],
898
- transcripcio=transcripcio,
899
- identificacio=identificacio,
900
- localitzacions=localitzacions,
901
- activitats=activitats,
902
- narracions=narracions,
903
- expressivitat=expressivitat,
904
- comments=comments or None
905
- )
906
- st.success("Gràcies! La teva valoració s'ha desat correctament.")
907
- except Exception as e:
908
- st.error(f"S'ha produït un error en desar la valoració: {e}")
909
-
910
-
911
- elif page == "Estadístiques":
912
- require_login()
913
- st.header("Estadístiques")
914
-
915
- from database import get_feedback_ad_stats
916
- stats = get_feedback_ad_stats() # medias por vídeo + avg_global
917
- if not stats:
918
- st.caption("Encara no hi ha valoracions.")
919
- st.stop()
920
-
921
- import pandas as pd
922
- df = pd.DataFrame(stats, columns=stats[0].keys())
923
- ordre = st.radio("Ordre de rànquing", ["Descendent (millors primer)", "Ascendent (pitjors primer)"], horizontal=True)
924
- if ordre.startswith("Asc"):
925
- df = df.sort_values("avg_global", ascending=True)
926
- else:
927
- df = df.sort_values("avg_global", ascending=False)
928
-
929
- st.subheader("Rànquing de vídeos")
930
- st.dataframe(
931
- df[["video_name","n","avg_global","avg_transcripcio","avg_identificacio","avg_localitzacions","avg_activitats","avg_narracions", "avg_expressivitat"]],
932
- use_container_width=True
933
- )
934
-