import os import io import json import yaml import shutil import sqlite3 import sys from pathlib import Path try: import tomllib except ModuleNotFoundError: # Py<3.11 import tomli as tomllib import streamlit as st # from moviepy.editor import VideoFileClip 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 from api_client import APIClient from utils import ensure_dirs, save_bytes, save_text, human_size, get_project_root from scripts.client_generate_av import generate_free_ad_mp3, generate_une_ad_video # --- Rutas y Configuración Inicial --- PROJECT_ROOT = get_project_root() # Copia de seguridad de la base de datos y vídeos a un directorio escribible en HF Spaces if os.getenv("SPACE_ID") is not None: os.environ["STREAMLIT_DATA_DIRECTORY"] = "/tmp/.streamlit" Path("/tmp/.streamlit").mkdir(parents=True, exist_ok=True) # Limpiar archivos temporales antiguos para evitar llenar el disco import glob import time temp_files = glob.glob("/tmp/*") current_time = time.time() for f in temp_files: try: if os.path.isfile(f) and current_time - os.path.getmtime(f) > 3600: # Archivos > 1 hora os.remove(f) print(f"Archivo temporal eliminado: {f}") except Exception as e: pass # Ignorar errores de permisos # static_videos = Path(__file__).parent / "videos" # runtime_videos = PROJECT_ROOT / "videos" # if not runtime_videos.exists(): # shutil.copytree(static_videos, runtime_videos, dirs_exist_ok=True) # --- Config --- def _load_yaml(path="config.yaml") -> dict: with open(path, "r", encoding="utf-8") as f: cfg = yaml.safe_load(f) or {} # interpolación sencilla de ${VARS} si las usas en el YAML def _subst(s: str) -> str: return os.path.expandvars(s) if isinstance(s, str) else s # aplica sustitución en los campos que te interesan if "api" in cfg: cfg["api"]["base_url"] = _subst(cfg["api"].get("base_url", "")) cfg["api"]["token"] = _subst(cfg["api"].get("token", "")) if "storage" in cfg and "root_dir" in cfg["storage"]: cfg["storage"]["root_dir"] = _subst(cfg["storage"]["root_dir"]) if "sqlite" in cfg and "path" in cfg["sqlite"]: cfg["sqlite"]["path"] = _subst(cfg["sqlite"]["path"]) return cfg CFG = _load_yaml("config.yaml") # Ajuste de variables según tu esquema YAML DATA_DIR = CFG.get("storage", {}).get("root_dir", "data") BACKEND_BASE_URL = CFG.get("api", {}).get("base_url", "http://localhost:8000") USE_MOCK = bool(CFG.get("app", {}).get("use_mock", False)) # si no la tienes en el yaml, queda False API_TOKEN = CFG.get("api", {}).get("token") or os.getenv("API_SHARED_TOKEN") os.makedirs(DATA_DIR, exist_ok=True) ensure_dirs(DATA_DIR) DB_PATH = os.path.join(DATA_DIR, "app.db") set_db_path(DB_PATH) init_schema() # --- Helper de logging --- def log(msg): """Helper para escribir logs que aparezcan en el container de HF Spaces""" sys.stderr.write(f"{msg}\n") sys.stderr.flush() def create_default_users_if_needed(): """Asegura que existan los usuarios por defecto y sus contraseñas esperadas (texto plano).""" log("Sincronizando usuarios por defecto...") users_to_create = [ ("verd", "verd123", "verd"), ("groc", "groc123", "groc"), ("taronja", "taronja123", "taronja"), ("blau", "blau123", "blau"), ] for username, password, role in users_to_create: try: row = get_user(username) if row: update_user_password(username, password) log(f"Usuario '{username}' actualizado (password reset).") else: create_user(username, password, role) log(f"Usuario '{username}' creado.") except Exception as e: log(f"Error sincronizando usuario {username}: {e}") create_default_users_if_needed() # --- LOG DE DIAGNÓSTICO: COMPROBAR ESTADO DE LA BD --- log("\n--- DIAGNÓSTICO DE BASE DE DATOS ---") log(f"Ruta de la BD en uso: {DB_PATH}") try: all_users = get_all_users() if all_users: log("Usuarios encontrados en la BD al arrancar:") # Convertimos las filas a diccionarios para una mejor visualización users_list = [dict(user) for user in all_users] log(str(users_list)) else: log("La tabla de usuarios está vacía.") except Exception as e: log(f"Error al intentar leer los usuarios de la BD: {e}") log("--- FIN DIAGNÓSTICO ---\n") # --- FIN LOG DE DIAGNÓSTICO --- api = APIClient(BACKEND_BASE_URL, use_mock=USE_MOCK, data_dir=DATA_DIR, token=API_TOKEN) # Diagnóstico de configuración de API log(f"\n--- CONFIGURACIÓN DE API ---") log(f"BACKEND_BASE_URL: {BACKEND_BASE_URL}") log(f"API_TOKEN configurado: {'Sí' if API_TOKEN else 'No'}") log(f"USE_MOCK: {USE_MOCK}") log(f"--- FIN CONFIGURACIÓN ---\n") st.set_page_config(page_title="Veureu — Audiodescripció", page_icon="🎬", layout="wide") # Configurar Streamlit para aceptar archivos más grandes try: import streamlit.web.server.server as server # Aumentar el límite de tamaño de archivo server.UPLOAD_FILE_SIZE_LIMIT = 50 * 1024 * 1024 # 50MB log("Límite de subida configurado a 50MB en server") except Exception as e: log(f"No se pudo configurar límite de subida en server: {e}") # Verificar configuración actual try: import streamlit.config as st_config max_upload = st_config.get_option("server.maxUploadSize") log(f"Configuración actual maxUploadSize: {max_upload}MB") except Exception as e: log(f"No se pudo leer configuración: {e}") # --- Session: auth --- # print("Usuarios disponibles:", get_all_users()) # Descomentar para depurar if "user" not in st.session_state: st.session_state.user = None # dict with {username, role, id(optional)} def require_login(): if not st.session_state.user: st.info("Por favor, inicia sesión para continuar.") login_form() st.stop() def verify_password(password: str, stored_password: str) -> bool: """Verifica la contraseña como texto plano.""" return password == stored_password # --- Sidebar (only after login) --- role = st.session_state.user["role"] if st.session_state.user else None with st.sidebar: st.title("Veureu") if st.session_state.user: st.write(f"Usuari: **{st.session_state.user['username']}** (rol: {st.session_state.user['role']})") if st.button("Tancar sessió"): st.session_state.user = None st.rerun() if st.session_state.user: page = st.radio("Navegació", ["Analitzar audio-descripcions","Processar vídeo nou","Estadístiques"], index=0) log(f"Página seleccionada: {page}") else: page = None # --- Pre-login screen --- if not st.session_state.user: st.title("Veureu — Audiodescripció") def login_form(): st.subheader("Inici de sessió") username = st.text_input("Usuari") password = st.text_input("Contrasenya", type="password") if st.button("Entrar", type="primary"): row = get_user(username) # --- LOGS DE DEPURACIÓN --- log("\n--- INTENTO DE LOGIN ---") log(f"Usuario introducido: '{username}'") # No mostramos la contraseña por seguridad, pero confirmamos que no está vacía log(f"Contraseña introducida: {'Sí' if password else 'No'}") if row: log(f"Usuario encontrado en BD: '{row['username']}'") stored_pw = (row["password_hash"] or "") log(f"Password almacenado (longitud): {len(stored_pw)}") is_valid = verify_password(password, stored_pw) log(f"Resultado de verify_password: {is_valid}") else: log("Usuario no encontrado en la BD.") is_valid = False log("--- FIN INTENTO DE LOGIN ---\n") # --- FIN LOGS DE DEPURACIÓN --- if is_valid: st.session_state.user = {"id": row["id"], "username": row["username"], "role": row["role"]} st.success(f"Benvingut/da, {row['username']}") st.rerun() else: st.error("Credencials invàlides") login_form() st.stop() # --- Pages --- if page == "Processar vídeo nou": log("\n=== ACCESO A PÁGINA 'Processar vídeo nou' ===") require_login() if role != "verd": log("ERROR: Usuario sin permisos para procesar vídeos") st.error("No tens permisos per processar nous vídeos. Canvia d'usuari o sol·licita permisos.") st.stop() log("Usuario autorizado, mostrando interfaz de subida") st.header("Processar un nou clip de vídeo") # Inicializar el estado de la página si no existe if 'video_uploaded' not in st.session_state: st.session_state.video_uploaded = None log("Estado 'video_uploaded' inicializado") if 'characters_detected' not in st.session_state: st.session_state.characters_detected = None log("Estado 'characters_detected' inicializado") if 'characters_saved' not in st.session_state: st.session_state.characters_saved = False log("Estado 'characters_saved' inicializado") # --- 1. Subida del vídeo --- MAX_SIZE_MB = 10 # Reducido a 10MB para evitar llenar el disco en HF Spaces MAX_DURATION_S = 120 # Reducido a 2 minutos log("Mostrando widget de subida de archivo...") # Verificar configuración de Streamlit import streamlit.config as st_config try: max_size = st_config.get_option("server.maxUploadSize") log(f"Configuración maxUploadSize: {max_size}MB") except Exception as e: log(f"No se pudo leer maxUploadSize: {e}") uploaded_file = st.file_uploader( "Puja un clip de vídeo (MP4, < 10MB, < 2 minuts)", type=["mp4"], key="video_uploader", help="Selecciona un archivo MP4 de tu dispositivo. Máximo 10MB." ) log(f"Widget renderizado. Archivo subido: {uploaded_file is not None}") # Debug: Mostrar información del archivo si existe if uploaded_file is not None: log(f"¡ARCHIVO DETECTADO! Nombre: {uploaded_file.name}, Tamaño: {uploaded_file.size}") else: log("No hay archivo subido todavía") # Mostrar mensaje de ayuda en la interfaz 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)") 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.") # Mostrar estado de depuración solo si no hay archivo o si hay un problema if uploaded_file is None and st.session_state.video_uploaded is None: with st.expander("🔍 Debug Info", expanded=False): st.write(f"**Archivo subido:** {uploaded_file is not None}") st.info("ℹ️ No se ha seleccionado ningún archivo todavía") st.write(f"**Estado video_uploaded:** {st.session_state.video_uploaded}") if uploaded_file is not None: log(f"\n--- SUBIDA DE VÍDEO INICIADA (archivo detectado) ---") log(f"Nombre del archivo: {uploaded_file.name}") log(f"Tamaño del archivo: {uploaded_file.size} bytes ({uploaded_file.size / (1024*1024):.2f} MB)") log(f"Tipo MIME: {uploaded_file.type}") # Resetear el estado si se sube un nuevo archivo if st.session_state.video_uploaded is None or uploaded_file.name != st.session_state.video_uploaded.get('original_name'): log(f"Nuevo archivo detectado, reseteando estado...") st.session_state.video_uploaded = {'original_name': uploaded_file.name, 'status': 'validating'} st.session_state.characters_detected = None st.session_state.characters_saved = False # --- Validación y Procesamiento --- if st.session_state.video_uploaded['status'] == 'validating': log(f"Validando archivo...") is_valid = True error_messages = [] # 1. Validar tamaño if uploaded_file.size > MAX_SIZE_MB * 1024 * 1024: 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" log(f"ERROR: {error_msg}") st.error(error_msg) st.warning("💡 **Consejo**: Reduce el tamaño del vídeo o usa un clip más corto.") error_messages.append(error_msg) is_valid = False else: log(f"✓ Tamaño válido: {uploaded_file.size / (1024*1024):.2f} MB") # 2. Validar que el archivo no esté vacío if uploaded_file.size == 0: error_msg = "El archivo está vacío." log(f"ERROR: {error_msg}") st.error(error_msg) error_messages.append(error_msg) is_valid = False if is_valid: try: with st.spinner("Processant el vídeo..."): log("Leyendo bytes del archivo...") # Guardar el buffer en memoria para enviarlo al engine video_bytes = uploaded_file.getbuffer().tobytes() log(f"✓ Bytes leídos correctamente: {len(video_bytes)} bytes") video_name = Path(uploaded_file.name).stem log(f"Nombre del vídeo (sin extensión): {video_name}") # Actualizar estado con los bytes del video st.session_state.video_uploaded.update({ 'status': 'processed', 'video_bytes': video_bytes, 'video_name': f"{video_name}.mp4", 'was_truncated': False }) log(f"✓ Estado actualizado correctamente") log(f"--- FIN SUBIDA DE VÍDEO (ÉXITO) ---\n") st.rerun() except Exception as e: error_msg = f"Error al procesar el vídeo: {str(e)}" log(f"ERROR CRÍTICO: {error_msg}") log(f"Tipo de error: {type(e).__name__}") import traceback log(f"Traceback completo:\n{traceback.format_exc()}") log(f"--- FIN SUBIDA DE VÍDEO (ERROR) ---\n") st.error(error_msg) st.session_state.video_uploaded = None else: log(f"Validación fallida. Errores: {error_messages}") log(f"--- FIN SUBIDA DE VÍDEO (VALIDACIÓN FALLIDA) ---\n") st.session_state.video_uploaded = None # --- Mensajes de estado --- if st.session_state.video_uploaded and st.session_state.video_uploaded['status'] == 'processed': st.success(f"✅ Vídeo '{st.session_state.video_uploaded['original_name']}' pujat i processat correctament.") # Mostrar info del vídeo procesado with st.expander("📊 Informació del vídeo", expanded=False): st.write(f"**Nombre:** {st.session_state.video_uploaded['original_name']}") st.write(f"**Tamaño:** {len(st.session_state.video_uploaded.get('video_bytes', [])) / (1024*1024):.2f} MB") st.write(f"**Estado:** Processat i llest per detectar personatges") if st.session_state.video_uploaded['was_truncated']: st.warning(f"El vídeo s'ha truncat a {MAX_DURATION_S // 60} minuts.") # --- 2. Detección de personajes --- st.markdown("---") col1, col2 = st.columns([1, 3]) # Sliders a la derecha del botón with col2: epsilon = st.slider("sensibitivity (épsilon)", 0.0, 2.0, 0.5, 0.1, key="epsilon_slider") min_cluster_size = st.slider("mínimum cluster size", 1, 5, 2, 1, key="min_cluster_slider") # Botón a la izquierda with col1: detect_button_disabled = st.session_state.video_uploaded is None if st.button("Detectar Personatges", disabled=detect_button_disabled): log(f"\n--- DETECCIÓN DE PERSONAJES INICIADA ---") log(f"Estado del vídeo: {st.session_state.video_uploaded}") with st.spinner("Detectant personatges..."): # Llamar al endpoint del engine para crear el casting inicial try: video_bytes = st.session_state.video_uploaded.get('video_bytes') if st.session_state.video_uploaded else None video_name = st.session_state.video_uploaded.get('video_name') if st.session_state.video_uploaded else None # NO logear los bytes del vídeo (son binarios y ensucian el log) log(f"Video bytes disponibles: {len(video_bytes) if video_bytes else 0} bytes") log(f"Nombre del vídeo: {video_name}") if not video_bytes: error_msg = "No s'ha trobat el vídeo pujat en memòria." log(f"ERROR: {error_msg}") st.error(error_msg) else: # Verificar configuración antes de llamar log(f"BACKEND_BASE_URL: {BACKEND_BASE_URL}") log(f"API_TOKEN configurado: {'Sí' if API_TOKEN else 'No'}") if BACKEND_BASE_URL == "http://localhost:8000" or not BACKEND_BASE_URL: error_msg = "⚠️ **Error de configuració**: La URL del servei 'engine' no està configurada correctament." log(f"ERROR: {error_msg}") st.error(error_msg) 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'.") else: log(f"Llamando a create_initial_casting...") 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))}") resp = api.create_initial_casting( video_bytes=video_bytes, video_name=video_name, epsilon=st.session_state.get("epsilon_slider", epsilon), min_cluster_size=int(st.session_state.get("min_cluster_slider", min_cluster_size)), ) log(f"Respuesta recibida: {resp}") if isinstance(resp, dict) and resp.get("error"): error_msg = resp['error'] log(f"ERROR en la respuesta: {error_msg}") st.error(f"❌ **Error en 'create_initial_casting'**: {error_msg}") if "403" in error_msg or "Forbidden" in error_msg: st.warning("**Possible causes:**\n- El Space 'engine' no està accessible públicament\n- El token d'API no és correcte\n- CORS bloquejat") elif "Connection" in error_msg or "timeout" in error_msg: st.warning(f"**No s'ha pogut connectar** amb el servei engine a: `{BACKEND_BASE_URL}`") else: log(f"✓ Casting inicial creado con éxito") st.success("✅ Casting inicial creat. S'han generat subcarpetes a 'temp//*'.") except Exception as e: error_msg = f"❌ Error inesperat: {e}" log(f"ERROR CRÍTICO: {error_msg}") log(f"Tipo de error: {type(e).__name__}") import traceback log(f"Traceback completo:\n{traceback.format_exc()}") st.error(error_msg) finally: log(f"--- FIN DETECCIÓN DE PERSONAJES ---\n") # Datos de ejemplo para mostrar UI (mientras no tengamos retorno estructurado) # NOTA: Comentado temporalmente porque placeholder.png no existe # st.session_state.characters_detected = [ # {"id": "char1", "image_path": "init_data/placeholder.png", "description": "Dona amb cabell ros i ulleres"}, # {"id": "char2", "image_path": "init_data/placeholder.png", "description": "Home amb barba i barret"}, # ] # st.session_state.characters_saved = False # --- 3. Formularios de personajes --- if st.session_state.characters_detected: st.subheader("Personatges detectats") for char in st.session_state.characters_detected: with st.form(key=f"form_{char['id']}"): col1, col2 = st.columns(2) with col1: st.image(char['image_path'], width=150) with col2: st.caption(char['description']) st.text_input("Nom del personatge", key=f"name_{char['id']}") st.form_submit_button("Cercar") st.markdown("---_**") # --- 4. Guardar y Generar --- col1, col2, col3 = st.columns([1,1,2]) with col1: if st.button("Desar", type="primary"): # Aquí iría la lógica para guardar los nombres de los personajes st.session_state.characters_saved = True st.success("Personatges desats correctament.") with col2: if st.session_state.characters_saved: st.button("Generar Audiodescripció") elif page == "Analitzar audio-descripcions": require_login() st.header("Analitzar audio-descripcions") # En HF Spaces, los videos están en el repo (read-only), no en /tmp/data if os.getenv("SPACE_ID") is not None: base_dir = Path(__file__).resolve().parent / "videos" else: base_dir = PROJECT_ROOT / "videos" if not base_dir.exists(): st.info("No s'ha trobat la carpeta **videos**. Crea-la i afegeix-hi subcarpetes amb els teus vídeos.") st.stop() carpetes = [p.name for p in sorted(base_dir.iterdir()) if p.is_dir() and p.name != 'completed'] if not carpetes: st.info("No s'ha trobat la carpeta **videos**. Crea-la i afegeix-hi subcarpetes amb els teus vídeos.") st.stop() # --- Lógica de Estado y Selección --- # Detectar si el vídeo principal ha cambiado para resetear el estado secundario if 'current_video' not in st.session_state: st.session_state.current_video = None # Widget de selección de vídeo seleccio = st.selectbox("Selecciona un vídeo (carpeta):", carpetes, index=None, placeholder="Tria una carpeta…") if seleccio != st.session_state.current_video: st.session_state.current_video = seleccio # Forzar reseteo de los widgets dependientes # No establecemos a None, dejamos que el widget se inicialice st.session_state.add_ad_checkbox = False if 'version_selector' in st.session_state: del st.session_state['version_selector'] # Forzar reinicio completo del widget st.rerun() if not seleccio: st.stop() vid_dir = base_dir / seleccio mp4s = sorted(vid_dir.glob("*.mp4")) # --- Dibujado de la Interfaz --- col_video, col_txt = st.columns([2, 1], gap="large") with col_video: # Selección de versión subcarpetas_ad = [p.name for p in sorted(vid_dir.iterdir()) if p.is_dir()] default_index_sub = subcarpetas_ad.index("Salamandra") if "Salamandra" in subcarpetas_ad else 0 subcarpeta_seleccio = st.selectbox( "Selecciona una versió d'audiodescripció:", subcarpetas_ad, index=default_index_sub if subcarpetas_ad and 'version_selector' not in st.session_state else 0, placeholder="Tria una versió…" if subcarpetas_ad else "No hi ha versions", key="version_selector" ) # Lógica de vídeo AD video_ad_path = vid_dir / subcarpeta_seleccio / "une_ad.mp4" if subcarpeta_seleccio else None is_ad_video_available = video_ad_path is not None and video_ad_path.exists() # Checkbox add_ad_video = st.checkbox("Afegir audiodescripció", disabled=not is_ad_video_available, key="add_ad_checkbox") # Decidir qué vídeo mostrar video_to_show = None if add_ad_video and is_ad_video_available: video_to_show = video_ad_path elif mp4s: video_to_show = mp4s[0] if video_to_show: st.video(str(video_to_show)) else: st.warning("No s'ha trobat cap fitxer **.mp4** a la carpeta seleccionada.") st.markdown("---") # Sección de ACCIONES st.markdown("#### Accions") c1, c2 = st.columns(2) with c1: if st.button("Reconstruir àudio amb narració lliure", use_container_width=True, key="rebuild_free_ad"): if seleccio and subcarpeta_seleccio: with st.spinner("Generant àudio de la narració lliure..."): result = generate_free_ad_mp3(seleccio, subcarpeta_seleccio, api, PROJECT_ROOT) if result.get("status") == "success": st.success(f"Àudio generat amb èxit: {result.get('path')}") else: st.error(f"Error: {result.get('reason', 'Desconegut')}") else: st.warning("Selecciona un vídeo i una versió.") with c2: if st.button("Reconstruir vídeo amb audiodescripció", use_container_width=True, key="rebuild_video_ad"): if seleccio and subcarpeta_seleccio: with st.spinner("Reconstruint el vídeo... Aquesta operació pot trigar."): result = generate_une_ad_video(seleccio, subcarpeta_seleccio, api, PROJECT_ROOT) if result.get("status") == "success": st.success(f"Vídeo generat amb èxit: {result.get('path')}") st.info("Pots visualitzar-lo activant la casella 'Afegir audiodescripció'.") else: st.error(f"Error: {result.get('reason', 'Desconegut')}") else: st.warning("Selecciona un vídeo i una versió.") # --- Columna Derecha (Editor de texto y guardado) --- with col_txt: tipus_ad_options = ["narració lliure", "UNE-153010"] tipus_ad_seleccio = st.selectbox("Fitxer d'audiodescripció a editar:", tipus_ad_options) ad_filename = "free_ad.txt" if tipus_ad_seleccio == "narració lliure" else "une_ad.srt" # Cargar el contenido del fichero seleccionado text_content = "" ad_path = None if subcarpeta_seleccio: ad_path = vid_dir / subcarpeta_seleccio / ad_filename if ad_path.exists(): try: text_content = ad_path.read_text(encoding="utf-8") except Exception: text_content = ad_path.read_text(errors="ignore") else: st.info(f"No s'ha trobat el fitxer **{ad_filename}**.") else: # Eliminada la nota de advertencia pass # Área de texto para edición new_text = st.text_area(f"Contingut de {tipus_ad_seleccio}", value=text_content, height=500, key=f"editor_{seleccio}_{subcarpeta_seleccio}_{ad_filename}") # Botón de guardado if st.button("Desar canvis", use_container_width=True, type="primary"): if ad_path: try: save_text(ad_path, new_text) st.success(f"Fitxer **{ad_filename}** desat correctament.") # El st.rerun() ya se encarga de recargar el estado desde el fichero guardado. # La línea que modificaba el session_state directamente ha sido eliminada para evitar el error. st.rerun() except Exception as e: st.error(f"No s'ha pogut desar el fitxer: {e}") else: st.error("No s'ha seleccionat una ruta de fitxer vàlida per desar.") # Controles de reproducción de narración free_ad_mp3_path = vid_dir / subcarpeta_seleccio / "free_ad.mp3" if seleccio and subcarpeta_seleccio else None can_play_free_ad = free_ad_mp3_path is not None and free_ad_mp3_path.exists() if st.button("▶️ Reproduir narració lliure", use_container_width=True, disabled=not can_play_free_ad, key="play_button_editor"): if can_play_free_ad: st.audio(str(free_ad_mp3_path), format="audio/mp3") else: st.warning("No s'ha trobat el fitxer 'free_ad.mp3'. Reconstrueix l'àudio primer.") st.markdown("---") st.subheader("Avaluació de la qualitat de l'audiodescripció") c1, c2, c3 = st.columns(3) with c1: transcripcio = st.slider("Transcripció", 1, 10, 7) identificacio = st.slider("Identificació de personatges", 1, 10, 7) with c2: localitzacions = st.slider("Localitzacions", 1, 10, 7) activitats = st.slider("Activitats", 1, 10, 7) with c3: narracions = st.slider("Narracions", 1, 10, 7) expressivitat = st.slider("Expressivitat", 1, 10, 7) comments = st.text_area("Comentaris (opcional)", placeholder="Escriu els teus comentaris lliures…", height=120) role = st.session_state.user["role"] can_rate = role in ("verd", "groc", "blau") if not can_rate: st.info("El teu rol no permet enviar valoracions.") else: if st.button("Enviar valoració", type="primary", use_container_width=True): try: add_feedback_ad( video_name=seleccio, user_id=st.session_state.user["id"], transcripcio=transcripcio, identificacio=identificacio, localitzacions=localitzacions, activitats=activitats, narracions=narracions, expressivitat=expressivitat, comments=comments or None ) st.success("Gràcies! La teva valoració s'ha desat correctament.") except Exception as e: st.error(f"S'ha produït un error en desar la valoració: {e}") elif page == "Estadístiques": require_login() st.header("Estadístiques") from database import get_feedback_ad_stats stats = get_feedback_ad_stats() # medias por vídeo + avg_global if not stats: st.caption("Encara no hi ha valoracions.") st.stop() import pandas as pd df = pd.DataFrame(stats, columns=stats[0].keys()) ordre = st.radio("Ordre de rànquing", ["Descendent (millors primer)", "Ascendent (pitjors primer)"], horizontal=True) if ordre.startswith("Asc"): df = df.sort_values("avg_global", ascending=True) else: df = df.sort_values("avg_global", ascending=False) st.subheader("Rànquing de vídeos") st.dataframe( df[["video_name","n","avg_global","avg_transcripcio","avg_identificacio","avg_localitzacions","avg_activitats","avg_narracions", "avg_expressivitat"]], use_container_width=True )