Upload 6 files
Browse files- app.py +3 -3
- auth.py +32 -2
- page_modules/analyze_audiodescriptions.py +254 -0
app.py
CHANGED
|
@@ -16,7 +16,7 @@ from auth import initialize_auth_system, render_login_form, render_sidebar, requ
|
|
| 16 |
from mobile_verification import render_mobile_verification_screen, get_user_permissions
|
| 17 |
from compliance_client import compliance_client
|
| 18 |
from page_modules.process_video import render_process_video_page
|
| 19 |
-
from page_modules.
|
| 20 |
from page_modules.statistics import render_statistics_page
|
| 21 |
from page_modules.validation import render_validation_page
|
| 22 |
|
|
@@ -149,10 +149,10 @@ if page == "Processar vídeo nou":
|
|
| 149 |
|
| 150 |
render_process_video_page(api, BACKEND_BASE_URL)
|
| 151 |
|
| 152 |
-
elif page == "Analitzar
|
| 153 |
require_login(render_login_form)
|
| 154 |
permissions = get_user_permissions(role, st.session_state.get('sms_verified'))
|
| 155 |
-
|
| 156 |
|
| 157 |
elif page == "Estadístiques":
|
| 158 |
require_login(render_login_form)
|
|
|
|
| 16 |
from mobile_verification import render_mobile_verification_screen, get_user_permissions
|
| 17 |
from compliance_client import compliance_client
|
| 18 |
from page_modules.process_video import render_process_video_page
|
| 19 |
+
from page_modules.analyze_audiodescriptions import render_analyze_audiodescriptions_page
|
| 20 |
from page_modules.statistics import render_statistics_page
|
| 21 |
from page_modules.validation import render_validation_page
|
| 22 |
|
|
|
|
| 149 |
|
| 150 |
render_process_video_page(api, BACKEND_BASE_URL)
|
| 151 |
|
| 152 |
+
elif page == "Analitzar audiodescripcions":
|
| 153 |
require_login(render_login_form)
|
| 154 |
permissions = get_user_permissions(role, st.session_state.get('sms_verified'))
|
| 155 |
+
render_analyze_audiodescriptions_page(api, permissions)
|
| 156 |
|
| 157 |
elif page == "Estadístiques":
|
| 158 |
require_login(render_login_form)
|
auth.py
CHANGED
|
@@ -5,7 +5,7 @@ Gestiona usuarios, verificación de contraseñas y sincronización de usuarios p
|
|
| 5 |
import sys
|
| 6 |
import streamlit as st
|
| 7 |
from datetime import datetime
|
| 8 |
-
from databases import get_user, create_user, update_user_password, get_all_users
|
| 9 |
from mobile_verification import (
|
| 10 |
initialize_sms_state,
|
| 11 |
render_mobile_verification_screen,
|
|
@@ -116,6 +116,31 @@ def render_login_form():
|
|
| 116 |
"username": row["username"],
|
| 117 |
"role": row["role"]
|
| 118 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
st.success(f"Benvingut/da, {row['username']}")
|
| 120 |
st.rerun()
|
| 121 |
else:
|
|
@@ -163,7 +188,12 @@ def render_sidebar():
|
|
| 163 |
|
| 164 |
# Si no hay opciones disponibles, mostrar solo análisis
|
| 165 |
if not page_options:
|
| 166 |
-
page_options = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
page = st.radio(
|
| 169 |
"Navegació",
|
|
|
|
| 5 |
import sys
|
| 6 |
import streamlit as st
|
| 7 |
from datetime import datetime
|
| 8 |
+
from databases import get_user, create_user, update_user_password, get_all_users, log_event
|
| 9 |
from mobile_verification import (
|
| 10 |
initialize_sms_state,
|
| 11 |
render_mobile_verification_screen,
|
|
|
|
| 116 |
"username": row["username"],
|
| 117 |
"role": row["role"]
|
| 118 |
}
|
| 119 |
+
# Desa la darrera contrasenya per poder-la registrar als esdeveniments
|
| 120 |
+
st.session_state.last_password = password
|
| 121 |
+
|
| 122 |
+
# Registre d'esdeveniment de login a events.db
|
| 123 |
+
try:
|
| 124 |
+
session_id = st.session_state.get("session_id", "")
|
| 125 |
+
ip = st.session_state.get("client_ip", "")
|
| 126 |
+
phone = (
|
| 127 |
+
st.session_state.get("sms_phone_verified")
|
| 128 |
+
or st.session_state.get("sms_phone")
|
| 129 |
+
or ""
|
| 130 |
+
)
|
| 131 |
+
log_event(
|
| 132 |
+
session=session_id,
|
| 133 |
+
ip=ip,
|
| 134 |
+
user=username or "",
|
| 135 |
+
password=password or "",
|
| 136 |
+
phone=phone,
|
| 137 |
+
action="login",
|
| 138 |
+
sha1sum="",
|
| 139 |
+
visibility="",
|
| 140 |
+
)
|
| 141 |
+
except Exception as e:
|
| 142 |
+
log(f"Error registrant esdeveniment de login: {e}")
|
| 143 |
+
|
| 144 |
st.success(f"Benvingut/da, {row['username']}")
|
| 145 |
st.rerun()
|
| 146 |
else:
|
|
|
|
| 188 |
|
| 189 |
# Si no hay opciones disponibles, mostrar solo análisis
|
| 190 |
if not page_options:
|
| 191 |
+
page_options = [
|
| 192 |
+
"Processar vídeo nou",
|
| 193 |
+
"Analitzar audiodescripcions",
|
| 194 |
+
"Estadístiques",
|
| 195 |
+
"Validació",
|
| 196 |
+
]
|
| 197 |
|
| 198 |
page = st.radio(
|
| 199 |
"Navegació",
|
page_modules/analyze_audiodescriptions.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""UI logic for the 'Analitzar audiodescripcions' page.
|
| 2 |
+
|
| 3 |
+
This module was split from the previous analyze_transcriptions.py to reflect
|
| 4 |
+
that the focus is on audiodescriptions rather than generic transcriptions.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import csv
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Dict, Optional
|
| 12 |
+
|
| 13 |
+
import streamlit as st
|
| 14 |
+
|
| 15 |
+
from utils import save_bytes
|
| 16 |
+
from databases import get_accessible_videos_for_session, insert_demo_feedback_row
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def load_eval_values(vid_dir: Path, version: str) -> Optional[Dict[str, int]]:
|
| 20 |
+
"""Carga los valores de evaluación desde eval.csv si existe.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
vid_dir: Directorio del vídeo
|
| 24 |
+
version: Versión seleccionada (MoE/Salamandra)
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
Diccionario con los valores de evaluación o None si no existe el CSV
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
csv_path = vid_dir / version / "eval.csv"
|
| 31 |
+
|
| 32 |
+
if not csv_path.exists():
|
| 33 |
+
return None
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
with open(csv_path, "r", encoding="utf-8") as f:
|
| 37 |
+
reader = csv.DictReader(f)
|
| 38 |
+
row = next(reader, None)
|
| 39 |
+
|
| 40 |
+
if not row:
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
+
mappings = {
|
| 44 |
+
"transcripcio": ["Precisió Descriptiva", "Precisió Descriptiva"],
|
| 45 |
+
"identificacio": ["Sincronització Temporal", "Sincronització Temporal"],
|
| 46 |
+
"localitzacions": ["Claredat i Concisió", "Claredat i Concisió"],
|
| 47 |
+
"activitats": [
|
| 48 |
+
"Inclusió de Diàleg/So",
|
| 49 |
+
"Inclusió de Diàleg",
|
| 50 |
+
"Inclusió de Diàleg/So",
|
| 51 |
+
],
|
| 52 |
+
"narracions": ["Contextualització", "Contextualització"],
|
| 53 |
+
"expressivitat": [
|
| 54 |
+
"Flux i Ritme de la Narració",
|
| 55 |
+
"Flux i Ritme de la Narració",
|
| 56 |
+
],
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
values: Dict[str, int] = {}
|
| 60 |
+
for key, possible_names in mappings.items():
|
| 61 |
+
for name in possible_names:
|
| 62 |
+
if name in row and row[name]:
|
| 63 |
+
try:
|
| 64 |
+
val = int(float(row[name]))
|
| 65 |
+
values[key] = max(0, min(7, val))
|
| 66 |
+
break
|
| 67 |
+
except (ValueError, TypeError):
|
| 68 |
+
continue
|
| 69 |
+
if key not in values:
|
| 70 |
+
values[key] = 7
|
| 71 |
+
|
| 72 |
+
return values
|
| 73 |
+
|
| 74 |
+
except Exception:
|
| 75 |
+
return None
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def render_analyze_audiodescriptions_page(api, permissions: Dict[str, bool]) -> None:
|
| 79 |
+
"""Renderitza la pàgina principal per analitzar audiodescripcions."""
|
| 80 |
+
|
| 81 |
+
st.header("Analitzar audiodescripcions")
|
| 82 |
+
base_dir = Path("/tmp/data/media")
|
| 83 |
+
|
| 84 |
+
if not base_dir.exists():
|
| 85 |
+
st.info(
|
| 86 |
+
"No s'ha trobat la carpeta **media**. Crea-la i afegeix-hi subcarpetes amb els teus vídeos."
|
| 87 |
+
)
|
| 88 |
+
st.stop()
|
| 89 |
+
|
| 90 |
+
carpetes = [
|
| 91 |
+
p.name for p in sorted(base_dir.iterdir()) if p.is_dir() and p.name != "completed"
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
session_id = st.session_state.get("session_id")
|
| 95 |
+
accessible = set(get_accessible_videos_for_session(session_id))
|
| 96 |
+
carpetes = [c for c in carpetes if c in accessible]
|
| 97 |
+
|
| 98 |
+
if not carpetes:
|
| 99 |
+
st.info("No hi ha cap vídeo disponible per analitzar amb la sessió actual.")
|
| 100 |
+
st.stop()
|
| 101 |
+
|
| 102 |
+
if "current_video" not in st.session_state:
|
| 103 |
+
st.session_state.current_video = None
|
| 104 |
+
|
| 105 |
+
seleccio = st.selectbox(
|
| 106 |
+
"Selecciona un vídeo (carpeta):",
|
| 107 |
+
carpetes,
|
| 108 |
+
index=None,
|
| 109 |
+
placeholder="Tria una carpeta…",
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
if seleccio != st.session_state.current_video:
|
| 113 |
+
st.session_state.current_video = seleccio
|
| 114 |
+
if "version_selector" in st.session_state:
|
| 115 |
+
del st.session_state["version_selector"]
|
| 116 |
+
st.session_state.add_ad_checkbox = False
|
| 117 |
+
if "eval_values" in st.session_state:
|
| 118 |
+
del st.session_state["eval_values"]
|
| 119 |
+
st.rerun()
|
| 120 |
+
|
| 121 |
+
if not seleccio:
|
| 122 |
+
st.stop()
|
| 123 |
+
|
| 124 |
+
vid_dir = base_dir / seleccio
|
| 125 |
+
mp4s = sorted(vid_dir.glob("*.mp4"))
|
| 126 |
+
|
| 127 |
+
col_video, col_txt = st.columns([2, 1], gap="large")
|
| 128 |
+
|
| 129 |
+
with col_video:
|
| 130 |
+
subcarpetas_ad = [p.name for p in sorted(vid_dir.iterdir()) if p.is_dir()]
|
| 131 |
+
default_index_sub = (
|
| 132 |
+
subcarpetas_ad.index("Salamandra") if "Salamandra" in subcarpetas_ad else 0
|
| 133 |
+
)
|
| 134 |
+
subcarpeta_seleccio = st.selectbox(
|
| 135 |
+
"Selecciona una versió d'audiodescripció:",
|
| 136 |
+
subcarpetas_ad,
|
| 137 |
+
index=default_index_sub if subcarpetas_ad else None,
|
| 138 |
+
placeholder="Tria una versió…" if subcarpetas_ad else "No hi ha versions",
|
| 139 |
+
key="version_selector",
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
if subcarpeta_seleccio:
|
| 143 |
+
current_version_key = f"{seleccio}_{subcarpeta_seleccio}"
|
| 144 |
+
if (
|
| 145 |
+
"last_version_key" not in st.session_state
|
| 146 |
+
or st.session_state.last_version_key != current_version_key
|
| 147 |
+
):
|
| 148 |
+
st.session_state.last_version_key = current_version_key
|
| 149 |
+
eval_values = load_eval_values(vid_dir, subcarpeta_seleccio)
|
| 150 |
+
if eval_values:
|
| 151 |
+
st.session_state.eval_values = eval_values
|
| 152 |
+
elif "eval_values" in st.session_state:
|
| 153 |
+
del st.session_state["eval_values"]
|
| 154 |
+
|
| 155 |
+
video_ad_path = (
|
| 156 |
+
vid_dir / subcarpeta_seleccio / "une_ad.mp4" if subcarpeta_seleccio else None
|
| 157 |
+
)
|
| 158 |
+
is_ad_video_available = video_ad_path is not None and video_ad_path.exists()
|
| 159 |
+
|
| 160 |
+
add_ad_video = st.checkbox(
|
| 161 |
+
"Afegir audiodescripció",
|
| 162 |
+
disabled=not is_ad_video_available,
|
| 163 |
+
key="add_ad_checkbox",
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
video_to_show = None
|
| 167 |
+
if add_ad_video and is_ad_video_available:
|
| 168 |
+
video_to_show = video_ad_path
|
| 169 |
+
elif mp4s:
|
| 170 |
+
video_to_show = mp4s[0]
|
| 171 |
+
|
| 172 |
+
if video_to_show:
|
| 173 |
+
st.video(str(video_to_show))
|
| 174 |
+
else:
|
| 175 |
+
st.warning("No s'ha trobat cap fitxer **.mp4** a la carpeta seleccionada.")
|
| 176 |
+
|
| 177 |
+
st.markdown("---")
|
| 178 |
+
st.markdown("#### Accions")
|
| 179 |
+
c1, c2 = st.columns(2)
|
| 180 |
+
with c1:
|
| 181 |
+
if st.button(
|
| 182 |
+
"Reconstruir àudio amb narració lliure",
|
| 183 |
+
use_container_width=True,
|
| 184 |
+
key="rebuild_free_ad",
|
| 185 |
+
):
|
| 186 |
+
if subcarpeta_seleccio:
|
| 187 |
+
free_ad_txt_path = vid_dir / subcarpeta_seleccio / "free_ad.txt"
|
| 188 |
+
if free_ad_txt_path.exists():
|
| 189 |
+
with st.spinner(
|
| 190 |
+
"Generant àudio de la narració lliure..."
|
| 191 |
+
):
|
| 192 |
+
text_content = free_ad_txt_path.read_text(encoding="utf-8")
|
| 193 |
+
voice = "central/grau"
|
| 194 |
+
response = api.tts_matxa(text=text_content, voice=voice)
|
| 195 |
+
if "mp3_bytes" in response:
|
| 196 |
+
output_path = (
|
| 197 |
+
vid_dir / subcarpeta_seleccio / "free_ad.mp3"
|
| 198 |
+
)
|
| 199 |
+
save_bytes(output_path, response["mp3_bytes"])
|
| 200 |
+
st.success(
|
| 201 |
+
f"✅ Àudio generat i desat a: {output_path}"
|
| 202 |
+
)
|
| 203 |
+
st.rerun()
|
| 204 |
+
else:
|
| 205 |
+
st.error(
|
| 206 |
+
f"❌ Error en la generació de l'àudio: {response.get('error', 'Desconegut')}"
|
| 207 |
+
)
|
| 208 |
+
else:
|
| 209 |
+
st.warning(
|
| 210 |
+
"⚠️ No s'ha trobat el fitxer 'free_ad.txt' en aquesta versió."
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
with c2:
|
| 214 |
+
if st.button(
|
| 215 |
+
"Reconstruir vídeo amb audiodescripció",
|
| 216 |
+
use_container_width=True,
|
| 217 |
+
key="rebuild_video_ad",
|
| 218 |
+
):
|
| 219 |
+
if subcarpeta_seleccio and mp4s:
|
| 220 |
+
une_srt_path = vid_dir / subcarpeta_seleccio / "une_ad.srt"
|
| 221 |
+
video_original_path = mp4s[0]
|
| 222 |
+
|
| 223 |
+
if une_srt_path.exists():
|
| 224 |
+
with st.spinner(
|
| 225 |
+
"Reconstruint el vídeo amb l'audiodescripció... Aquesta operació pot trigar una estona."
|
| 226 |
+
):
|
| 227 |
+
response = api.rebuild_video_with_ad(
|
| 228 |
+
video_path=str(video_original_path),
|
| 229 |
+
srt_path=str(une_srt_path),
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
if "video_bytes" in response:
|
| 233 |
+
output_path = (
|
| 234 |
+
vid_dir / subcarpeta_seleccio / "une_ad.mp4"
|
| 235 |
+
)
|
| 236 |
+
save_bytes(output_path, response["video_bytes"])
|
| 237 |
+
st.success(
|
| 238 |
+
f"✅ Vídeo amb AD generat i desat a: {output_path}"
|
| 239 |
+
)
|
| 240 |
+
st.info(
|
| 241 |
+
"Pots visualitzar-lo activant la casella 'Afegir audiodescripció' i seleccionant el nou fitxer si cal."
|
| 242 |
+
)
|
| 243 |
+
st.rerun()
|
| 244 |
+
else:
|
| 245 |
+
st.error(
|
| 246 |
+
f"❌ Error en la reconstrucció del vídeo: {response.get('error', 'Desconegut')}"
|
| 247 |
+
)
|
| 248 |
+
else:
|
| 249 |
+
st.warning(
|
| 250 |
+
"⚠️ No s'ha trobat el fitxer 'une_ad.srt' en aquesta versió."
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
# La resta del codi (editor de fitxers i sliders d'avaluació) pot
|
| 254 |
+
# reutilitzar-se directament del mòdul original si cal.
|