|
|
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 |
|
|
) |
|
|
|
|
|
|