| import os |
| import io |
| import json |
| import yaml |
| import shutil |
| import sqlite3 |
| import sys |
| from pathlib import Path |
| try: |
| import tomllib |
| except ModuleNotFoundError: |
| import tomli as tomllib |
| import streamlit as st |
| |
|
|
| 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 |
|
|
| |
| PROJECT_ROOT = get_project_root() |
|
|
| |
| if os.getenv("SPACE_ID") is not None: |
| os.environ["STREAMLIT_DATA_DIRECTORY"] = "/tmp/.streamlit" |
| Path("/tmp/.streamlit").mkdir(parents=True, exist_ok=True) |
| |
| |
| 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: |
| os.remove(f) |
| print(f"Archivo temporal eliminado: {f}") |
| except Exception as e: |
| pass |
|
|
|
|
| |
| |
| |
| |
|
|
| |
| def _load_yaml(path="config.yaml") -> dict: |
| with open(path, "r", encoding="utf-8") as f: |
| cfg = yaml.safe_load(f) or {} |
| |
| def _subst(s: str) -> str: |
| return os.path.expandvars(s) if isinstance(s, str) else s |
|
|
| |
| 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") |
|
|
| |
| 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)) |
| 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() |
|
|
| |
| 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("\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:") |
| |
| 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") |
| |
|
|
| api = APIClient(BACKEND_BASE_URL, use_mock=USE_MOCK, data_dir=DATA_DIR, token=API_TOKEN) |
|
|
| |
| 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") |
|
|
| |
| try: |
| import streamlit.web.server.server as server |
| |
| server.UPLOAD_FILE_SIZE_LIMIT = 50 * 1024 * 1024 |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| |
| if "user" not in st.session_state: |
| st.session_state.user = None |
|
|
| 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 |
| |
| |
| 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 |
|
|
| |
| 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) |
|
|
| |
| log("\n--- INTENTO DE LOGIN ---") |
| log(f"Usuario introducido: '{username}'") |
| |
| 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") |
| |
|
|
| 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() |
|
|
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| MAX_SIZE_MB = 10 |
| MAX_DURATION_S = 120 |
|
|
| log("Mostrando widget de subida de archivo...") |
| |
| |
| 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}") |
| |
| |
| 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") |
| |
| 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.") |
|
|
| |
| 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}") |
| |
| |
| 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 |
|
|
| |
| if st.session_state.video_uploaded['status'] == 'validating': |
| log(f"Validando archivo...") |
| is_valid = True |
| error_messages = [] |
| |
| |
| 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") |
| |
| |
| 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...") |
| |
| 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}") |
| |
| |
| 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 |
|
|
| |
| 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.") |
| |
| |
| 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.") |
|
|
| |
| st.markdown("---") |
| col1, col2 = st.columns([1, 3]) |
| |
| 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") |
| |
| 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..."): |
| |
| 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 |
| |
| |
| 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: |
| |
| 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/<uploaded-video>/*'.") |
| 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") |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| 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("---_**") |
|
|
| |
| col1, col2, col3 = st.columns([1,1,2]) |
| with col1: |
| if st.button("Desar", type="primary"): |
| |
| 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") |
| |
| |
| 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() |
|
|
| |
|
|
| |
| if 'current_video' not in st.session_state: |
| st.session_state.current_video = None |
|
|
| |
| 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 |
| |
| |
| st.session_state.add_ad_checkbox = False |
| if 'version_selector' in st.session_state: |
| del st.session_state['version_selector'] |
| st.rerun() |
|
|
| if not seleccio: |
| st.stop() |
|
|
| vid_dir = base_dir / seleccio |
| mp4s = sorted(vid_dir.glob("*.mp4")) |
|
|
| |
| col_video, col_txt = st.columns([2, 1], gap="large") |
|
|
| with col_video: |
| |
| 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" |
| ) |
|
|
| |
| 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() |
|
|
| |
| add_ad_video = st.checkbox("Afegir audiodescripció", disabled=not is_ad_video_available, key="add_ad_checkbox") |
|
|
| |
| 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("---") |
|
|
| |
| 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ó.") |
|
|
|
|
| |
| 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" |
| |
| |
| 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: |
| |
| pass |
|
|
| |
| new_text = st.text_area(f"Contingut de {tipus_ad_seleccio}", value=text_content, height=500, key=f"editor_{seleccio}_{subcarpeta_seleccio}_{ad_filename}") |
|
|
| |
| 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.") |
| |
| |
| 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.") |
|
|
| |
| 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() |
| 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 |
| ) |
|
|
|
|