engine / app.py
VeuReu's picture
Upload 3 files
04a4cfc verified
raw
history blame
32.5 kB
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/<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")
# 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
)