Upload 6 files
Browse files- page_modules/analyze_transcriptions.py +49 -5
- page_modules/process_video.py +312 -253
- page_modules/statistics.py +22 -12
page_modules/analyze_transcriptions.py
CHANGED
|
@@ -9,6 +9,7 @@ from typing import Dict, Optional
|
|
| 9 |
import streamlit as st
|
| 10 |
|
| 11 |
from utils import save_bytes
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
def load_eval_values(vid_dir: Path, version: str) -> Optional[Dict[str, int]]:
|
|
@@ -77,8 +78,14 @@ def render_analyze_transcriptions_page(api, permissions: Dict[str, bool]) -> Non
|
|
| 77 |
st.stop()
|
| 78 |
|
| 79 |
carpetes = [p.name for p in sorted(base_dir.iterdir()) if p.is_dir() and p.name != "completed"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
if not carpetes:
|
| 81 |
-
st.info("No
|
| 82 |
st.stop()
|
| 83 |
|
| 84 |
if "current_video" not in st.session_state:
|
|
@@ -334,7 +341,7 @@ def render_analyze_transcriptions_page(api, permissions: Dict[str, bool]) -> Non
|
|
| 334 |
try:
|
| 335 |
from databases import add_feedback_ad
|
| 336 |
|
| 337 |
-
# Guardar en la base de datos
|
| 338 |
add_feedback_ad(
|
| 339 |
video_name=seleccio,
|
| 340 |
user_id=st.session_state.user["id"],
|
|
@@ -346,10 +353,47 @@ def render_analyze_transcriptions_page(api, permissions: Dict[str, bool]) -> Non
|
|
| 346 |
expressivitat=expressivitat,
|
| 347 |
comments=comments or None,
|
| 348 |
)
|
| 349 |
-
|
| 350 |
-
#
|
|
|
|
| 351 |
video_dir = Path("demo/data/videos") / seleccio
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
csv_path = video_dir / version / "eval.csv"
|
| 354 |
|
| 355 |
csv_data = [
|
|
|
|
| 9 |
import streamlit as st
|
| 10 |
|
| 11 |
from utils import save_bytes
|
| 12 |
+
from databases import get_accessible_videos_for_session, insert_demo_feedback_row
|
| 13 |
|
| 14 |
|
| 15 |
def load_eval_values(vid_dir: Path, version: str) -> Optional[Dict[str, int]]:
|
|
|
|
| 78 |
st.stop()
|
| 79 |
|
| 80 |
carpetes = [p.name for p in sorted(base_dir.iterdir()) if p.is_dir() and p.name != "completed"]
|
| 81 |
+
|
| 82 |
+
# Filtrar segons permisos (videos.db + events.db)
|
| 83 |
+
session_id = st.session_state.get("session_id")
|
| 84 |
+
accessible = set(get_accessible_videos_for_session(session_id))
|
| 85 |
+
carpetes = [c for c in carpetes if c in accessible]
|
| 86 |
+
|
| 87 |
if not carpetes:
|
| 88 |
+
st.info("No hi ha cap vídeo disponible per analitzar amb la sessió actual.")
|
| 89 |
st.stop()
|
| 90 |
|
| 91 |
if "current_video" not in st.session_state:
|
|
|
|
| 341 |
try:
|
| 342 |
from databases import add_feedback_ad
|
| 343 |
|
| 344 |
+
# Guardar en la base de datos agregada d'AD
|
| 345 |
add_feedback_ad(
|
| 346 |
video_name=seleccio,
|
| 347 |
user_id=st.session_state.user["id"],
|
|
|
|
| 353 |
expressivitat=expressivitat,
|
| 354 |
comments=comments or None,
|
| 355 |
)
|
| 356 |
+
|
| 357 |
+
# Determinar versió i llegir UNE/free per a la inserció detallada
|
| 358 |
+
version = subcarpeta_seleccio or "MoE"
|
| 359 |
video_dir = Path("demo/data/videos") / seleccio
|
| 360 |
+
une_path = video_dir / version / "une_ad.srt"
|
| 361 |
+
free_path = video_dir / version / "free_ad.txt"
|
| 362 |
+
|
| 363 |
+
try:
|
| 364 |
+
une_ad_text = une_path.read_text(encoding="utf-8") if une_path.exists() else ""
|
| 365 |
+
except Exception:
|
| 366 |
+
une_ad_text = une_path.read_text(errors="ignore") if une_path.exists() else ""
|
| 367 |
+
|
| 368 |
+
try:
|
| 369 |
+
free_ad_text = free_path.read_text(encoding="utf-8") if free_path.exists() else ""
|
| 370 |
+
except Exception:
|
| 371 |
+
free_ad_text = free_path.read_text(errors="ignore") if free_path.exists() else ""
|
| 372 |
+
|
| 373 |
+
user_name = (
|
| 374 |
+
st.session_state.user.get("username")
|
| 375 |
+
if isinstance(st.session_state.get("user"), dict)
|
| 376 |
+
else str(st.session_state.get("user", ""))
|
| 377 |
+
)
|
| 378 |
+
session_id = st.session_state.get("session_id", "")
|
| 379 |
+
|
| 380 |
+
insert_demo_feedback_row(
|
| 381 |
+
user=user_name or "",
|
| 382 |
+
session=session_id or "",
|
| 383 |
+
video_name=seleccio,
|
| 384 |
+
version=version,
|
| 385 |
+
une_ad=une_ad_text,
|
| 386 |
+
free_ad=free_ad_text,
|
| 387 |
+
comments=comments or None,
|
| 388 |
+
transcripcio=transcripcio,
|
| 389 |
+
identificacio=identificacio,
|
| 390 |
+
localitzacions=localitzacions,
|
| 391 |
+
activitats=activitats,
|
| 392 |
+
narracions=narracions,
|
| 393 |
+
expressivitat=expressivitat,
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
# También guardar en CSV (reubicado en demo/data/videos)
|
| 397 |
csv_path = video_dir / version / "eval.csv"
|
| 398 |
|
| 399 |
csv_data = [
|
page_modules/process_video.py
CHANGED
|
@@ -1,123 +1,125 @@
|
|
| 1 |
-
"""UI logic for the "Processar vídeo nou" page - Recovered from backup with full functionality."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import re
|
| 6 |
-
import shutil
|
| 7 |
-
import subprocess
|
| 8 |
-
import os
|
| 9 |
-
import time
|
| 10 |
-
import tempfile
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
"
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
if
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
"
|
| 59 |
-
"-
|
| 60 |
-
"
|
| 61 |
-
"-
|
| 62 |
-
"
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
cap.
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
"-
|
| 110 |
-
|
| 111 |
-
"-
|
| 112 |
-
"
|
| 113 |
-
"-
|
| 114 |
-
"
|
| 115 |
-
"-
|
| 116 |
-
"
|
| 117 |
-
"-
|
| 118 |
-
"
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
| 121 |
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 122 |
if result.returncode != 0:
|
| 123 |
raise RuntimeError(result.stderr.strip() or "ffmpeg failed")
|
|
@@ -236,11 +238,33 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 236 |
MAX_SIZE_MB = 20
|
| 237 |
MAX_DURATION_S = 240 # 4 minutos
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
if uploaded_file is not None:
|
| 246 |
# Resetear el estado si se sube un nuevo archivo
|
|
@@ -295,16 +319,51 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 295 |
is_valid = False
|
| 296 |
|
| 297 |
if is_valid and final_video_path is not None:
|
|
|
|
|
|
|
|
|
|
| 298 |
st.session_state.video_uploaded.update(
|
| 299 |
{
|
| 300 |
"status": "processed",
|
| 301 |
"path": str(final_video_path),
|
| 302 |
"was_truncated": was_truncated or duration_unknown,
|
| 303 |
"duration_unknown": duration_unknown,
|
| 304 |
-
"bytes":
|
| 305 |
"name": uploaded_file.name,
|
|
|
|
| 306 |
}
|
| 307 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
st.rerun()
|
| 309 |
finally:
|
| 310 |
if temp_path.exists():
|
|
@@ -1046,130 +1105,130 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 1046 |
face_data = face_items[0] if face_items else None
|
| 1047 |
|
| 1048 |
col_faces, col_voices, col_text = st.columns([1, 1, 1.5])
|
| 1049 |
-
|
| 1050 |
-
with col_faces:
|
| 1051 |
-
if all_faces:
|
| 1052 |
-
carousel_key = f"combined_face_{pidx}"
|
| 1053 |
-
if f"{carousel_key}_idx" not in st.session_state:
|
| 1054 |
-
st.session_state[f"{carousel_key}_idx"] = 0
|
| 1055 |
-
cur = st.session_state[f"{carousel_key}_idx"]
|
| 1056 |
-
if cur >= len(all_faces):
|
| 1057 |
-
cur = 0
|
| 1058 |
-
st.session_state[f"{carousel_key}_idx"] = cur
|
| 1059 |
-
fname = all_faces[cur]
|
| 1060 |
-
ch = face_data["char_data"] if face_data else {}
|
| 1061 |
-
if fname.startswith("/files/"):
|
| 1062 |
-
img_url = f"{backend_base_url}{fname}"
|
| 1063 |
-
else:
|
| 1064 |
-
base = ch.get("image_url") or ""
|
| 1065 |
-
base_dir = "/".join((base or "/").split("/")[:-1])
|
| 1066 |
-
img_url = f"{backend_base_url}{base_dir}/{fname}" if base_dir else f"{backend_base_url}{fname}"
|
| 1067 |
-
st.image(img_url, width=150)
|
| 1068 |
-
st.caption(f"Cara {cur+1}/{len(all_faces)}")
|
| 1069 |
-
bcol1, bcol2 = st.columns(2)
|
| 1070 |
-
with bcol1:
|
| 1071 |
-
if st.button("⬅️", key=f"combined_face_prev_{pidx}"):
|
| 1072 |
-
st.session_state[f"{carousel_key}_idx"] = (cur - 1) % len(all_faces)
|
| 1073 |
-
st.rerun()
|
| 1074 |
-
with bcol2:
|
| 1075 |
-
if st.button("➡️", key=f"combined_face_next_{pidx}"):
|
| 1076 |
-
st.session_state[f"{carousel_key}_idx"] = (cur + 1) % len(all_faces)
|
| 1077 |
-
st.rerun()
|
| 1078 |
-
else:
|
| 1079 |
-
st.info("Sense imatges")
|
| 1080 |
-
|
| 1081 |
-
with col_voices:
|
| 1082 |
-
if voice_data:
|
| 1083 |
-
clips = voice_data["clips"]
|
| 1084 |
-
if clips:
|
| 1085 |
-
carousel_key = f"combined_voice_{pidx}"
|
| 1086 |
-
if f"{carousel_key}_idx" not in st.session_state:
|
| 1087 |
-
st.session_state[f"{carousel_key}_idx"] = 0
|
| 1088 |
-
cur = st.session_state[f"{carousel_key}_idx"]
|
| 1089 |
-
if cur >= len(clips):
|
| 1090 |
-
cur = 0
|
| 1091 |
-
st.session_state[f"{carousel_key}_idx"] = cur
|
| 1092 |
-
fname = clips[cur]
|
| 1093 |
-
audio_url = f"{backend_base_url}/audio/{vname}/{fname}" if (vname and fname) else None
|
| 1094 |
-
if audio_url:
|
| 1095 |
-
st.audio(audio_url, format="audio/wav")
|
| 1096 |
-
st.caption(f"Veu {cur+1}/{len(clips)}")
|
| 1097 |
-
bcol1, bcol2 = st.columns(2)
|
| 1098 |
-
with bcol1:
|
| 1099 |
-
if st.button("⬅️", key=f"combined_voice_prev_{pidx}"):
|
| 1100 |
-
st.session_state[f"{carousel_key}_idx"] = (cur - 1) % len(clips)
|
| 1101 |
-
st.rerun()
|
| 1102 |
-
with bcol2:
|
| 1103 |
-
if st.button("➡️", key=f"combined_voice_next_{pidx}"):
|
| 1104 |
-
st.session_state[f"{carousel_key}_idx"] = (cur + 1) % len(clips)
|
| 1105 |
-
st.rerun()
|
| 1106 |
-
else:
|
| 1107 |
-
st.info("Sense clips de veu")
|
| 1108 |
-
else:
|
| 1109 |
-
st.info("Sense dades de veu")
|
| 1110 |
-
|
| 1111 |
-
with col_text:
|
| 1112 |
-
combined_name_key = f"combined_char_{pidx}_name"
|
| 1113 |
-
combined_desc_key = f"combined_char_{pidx}_desc"
|
| 1114 |
-
|
| 1115 |
-
if combined_name_key not in st.session_state:
|
| 1116 |
-
st.session_state[combined_name_key] = norm_name
|
| 1117 |
-
if combined_desc_key not in st.session_state:
|
| 1118 |
-
st.session_state[combined_desc_key] = combined_description
|
| 1119 |
-
|
| 1120 |
-
st.text_input("Nom del personatge", key=combined_name_key, label_visibility="collapsed", placeholder="Nom del personatge")
|
| 1121 |
-
st.text_area("Descripció", key=combined_desc_key, height=120, label_visibility="collapsed", placeholder="Descripció del personatge")
|
| 1122 |
-
|
| 1123 |
-
# --- 7. Generar audiodescripció ---
|
| 1124 |
-
st.markdown("---")
|
| 1125 |
-
if st.button("🎬 Generar audiodescripció", type="primary", use_container_width=True):
|
| 1126 |
-
v = st.session_state.get("video_uploaded")
|
| 1127 |
-
if not v:
|
| 1128 |
-
st.error("No hi ha cap vídeo carregat.")
|
| 1129 |
-
else:
|
| 1130 |
-
progress_placeholder = st.empty()
|
| 1131 |
-
result_placeholder = st.empty()
|
| 1132 |
-
|
| 1133 |
-
with st.spinner("Generant audiodescripció... Aquest procés pot trigar diversos minuts."):
|
| 1134 |
-
progress_placeholder.info("⏳ Processant vídeo i generant audiodescripció UNE-153010...")
|
| 1135 |
-
|
| 1136 |
-
try:
|
| 1137 |
-
out = api.generate_audiodescription(v["bytes"], v["name"])
|
| 1138 |
-
|
| 1139 |
-
if isinstance(out, dict) and out.get("status") == "done":
|
| 1140 |
-
progress_placeholder.success("✅ Audiodescripció generada correctament!")
|
| 1141 |
-
res = out.get("results", {})
|
| 1142 |
-
|
| 1143 |
-
with result_placeholder.container():
|
| 1144 |
-
st.success("🎉 Audiodescripció completada!")
|
| 1145 |
-
c1, c2 = st.columns([1,1])
|
| 1146 |
-
with c1:
|
| 1147 |
-
st.markdown("**📄 UNE-153010 SRT**")
|
| 1148 |
-
une_srt_content = res.get("une_srt", "")
|
| 1149 |
-
st.code(une_srt_content, language="text")
|
| 1150 |
-
if une_srt_content:
|
| 1151 |
-
st.download_button(
|
| 1152 |
-
"⬇️ Descarregar UNE SRT",
|
| 1153 |
-
data=une_srt_content,
|
| 1154 |
-
file_name=f"{v['name']}_une.srt",
|
| 1155 |
-
mime="text/plain"
|
| 1156 |
-
)
|
| 1157 |
-
with c2:
|
| 1158 |
-
st.markdown("**📝 Narració lliure**")
|
| 1159 |
-
free_text_content = res.get("free_text", "")
|
| 1160 |
-
st.text_area("", value=free_text_content, height=240, key="free_text_result")
|
| 1161 |
-
if free_text_content:
|
| 1162 |
-
st.download_button(
|
| 1163 |
-
"⬇️ Descarregar text lliure",
|
| 1164 |
-
data=free_text_content,
|
| 1165 |
-
file_name=f"{v['name']}_free.txt",
|
| 1166 |
-
mime="text/plain"
|
| 1167 |
-
)
|
| 1168 |
-
else:
|
| 1169 |
-
progress_placeholder.empty()
|
| 1170 |
-
error_msg = str(out.get("error", out)) if isinstance(out, dict) else str(out)
|
| 1171 |
-
result_placeholder.error(f"❌ Error generant l'audiodescripció: {error_msg}")
|
| 1172 |
-
|
| 1173 |
-
except Exception as e:
|
| 1174 |
-
progress_placeholder.empty()
|
| 1175 |
-
result_placeholder.error(f"❌ Excepció durant la generació: {e}")
|
|
|
|
| 1 |
+
"""UI logic for the "Processar vídeo nou" page - Recovered from backup with full functionality."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
import shutil
|
| 7 |
+
import subprocess
|
| 8 |
+
import os
|
| 9 |
+
import time
|
| 10 |
+
import tempfile
|
| 11 |
+
import hashlib
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
import streamlit as st
|
| 15 |
+
from PIL import Image, ImageDraw
|
| 16 |
+
from databases import log_event
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_all_catalan_names():
|
| 20 |
+
"""Retorna tots els noms catalans disponibles."""
|
| 21 |
+
noms_home = ["Jordi", "Marc", "Pau", "Pere", "Joan", "Josep", "David", "Àlex", "Guillem", "Albert",
|
| 22 |
+
"Arnau", "Martí", "Bernat", "Oriol", "Roger", "Pol", "Lluís", "Sergi", "Carles", "Xavier"]
|
| 23 |
+
noms_dona = ["Maria", "Anna", "Laura", "Marta", "Cristina", "Núria", "Montserrat", "Júlia", "Sara", "Carla",
|
| 24 |
+
"Alba", "Elisabet", "Rosa", "Gemma", "Sílvia", "Teresa", "Irene", "Laia", "Marina", "Bet"]
|
| 25 |
+
return noms_home, noms_dona
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def get_catalan_name_for_speaker(speaker_label: int, used_names_home: list = None, used_names_dona: list = None) -> str:
|
| 29 |
+
"""Genera un nom català per a un speaker, reutilitzant noms de caras si estan disponibles."""
|
| 30 |
+
noms_home, noms_dona = get_all_catalan_names()
|
| 31 |
+
|
| 32 |
+
if used_names_home is None:
|
| 33 |
+
used_names_home = []
|
| 34 |
+
if used_names_dona is None:
|
| 35 |
+
used_names_dona = []
|
| 36 |
+
|
| 37 |
+
is_male = (speaker_label % 2 == 0)
|
| 38 |
+
|
| 39 |
+
if is_male:
|
| 40 |
+
if used_names_home:
|
| 41 |
+
idx = speaker_label // 2
|
| 42 |
+
return used_names_home[idx % len(used_names_home)]
|
| 43 |
+
else:
|
| 44 |
+
hash_val = hash(f"speaker_{speaker_label}")
|
| 45 |
+
return noms_home[abs(hash_val) % len(noms_home)]
|
| 46 |
+
else:
|
| 47 |
+
if used_names_dona:
|
| 48 |
+
idx = speaker_label // 2
|
| 49 |
+
return used_names_dona[idx % len(used_names_dona)]
|
| 50 |
+
else:
|
| 51 |
+
hash_val = hash(f"speaker_{speaker_label}")
|
| 52 |
+
return noms_dona[abs(hash_val) % len(noms_dona)]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _get_video_duration(path: str) -> float:
|
| 56 |
+
"""Return video duration in seconds using ffprobe, ffmpeg or OpenCV as fallback."""
|
| 57 |
+
cmd = [
|
| 58 |
+
"ffprobe",
|
| 59 |
+
"-v",
|
| 60 |
+
"error",
|
| 61 |
+
"-show_entries",
|
| 62 |
+
"format=duration",
|
| 63 |
+
"-of",
|
| 64 |
+
"default=noprint_wrappers=1:nokey=1",
|
| 65 |
+
path,
|
| 66 |
+
]
|
| 67 |
+
try:
|
| 68 |
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
| 69 |
+
return float(result.stdout.strip())
|
| 70 |
+
except (subprocess.CalledProcessError, ValueError, FileNotFoundError):
|
| 71 |
+
pass
|
| 72 |
+
|
| 73 |
+
if shutil.which("ffmpeg"):
|
| 74 |
+
try:
|
| 75 |
+
ffmpeg_cmd = ["ffmpeg", "-i", path]
|
| 76 |
+
result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True, check=False)
|
| 77 |
+
output = result.stderr or result.stdout or ""
|
| 78 |
+
match = re.search(r"Duration:\s*(\d+):(\d+):(\d+\.\d+)", output)
|
| 79 |
+
if match:
|
| 80 |
+
hours, minutes, seconds = match.groups()
|
| 81 |
+
total_seconds = (int(hours) * 3600) + (int(minutes) * 60) + float(seconds)
|
| 82 |
+
return float(total_seconds)
|
| 83 |
+
except FileNotFoundError:
|
| 84 |
+
pass
|
| 85 |
+
|
| 86 |
+
# Últim recurs: intentar amb OpenCV si està disponible
|
| 87 |
+
try:
|
| 88 |
+
import cv2
|
| 89 |
+
|
| 90 |
+
cap = cv2.VideoCapture(path)
|
| 91 |
+
if cap.isOpened():
|
| 92 |
+
fps = cap.get(cv2.CAP_PROP_FPS) or 0
|
| 93 |
+
frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0
|
| 94 |
+
cap.release()
|
| 95 |
+
|
| 96 |
+
if fps > 0 and frame_count > 0:
|
| 97 |
+
return float(frame_count / fps)
|
| 98 |
+
else:
|
| 99 |
+
cap.release()
|
| 100 |
+
except Exception:
|
| 101 |
+
pass
|
| 102 |
+
|
| 103 |
+
return 0.0
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _transcode_video(input_path: str, output_path: str, max_duration: int | None = None) -> None:
|
| 107 |
+
cmd = ["ffmpeg", "-y", "-i", input_path]
|
| 108 |
+
if max_duration is not None:
|
| 109 |
+
cmd += ["-t", str(max_duration)]
|
| 110 |
+
cmd += [
|
| 111 |
+
"-c:v",
|
| 112 |
+
"libx264",
|
| 113 |
+
"-preset",
|
| 114 |
+
"veryfast",
|
| 115 |
+
"-crf",
|
| 116 |
+
"23",
|
| 117 |
+
"-c:a",
|
| 118 |
+
"aac",
|
| 119 |
+
"-movflags",
|
| 120 |
+
"+faststart",
|
| 121 |
+
output_path,
|
| 122 |
+
]
|
| 123 |
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 124 |
if result.returncode != 0:
|
| 125 |
raise RuntimeError(result.stderr.strip() or "ffmpeg failed")
|
|
|
|
| 238 |
MAX_SIZE_MB = 20
|
| 239 |
MAX_DURATION_S = 240 # 4 minutos
|
| 240 |
|
| 241 |
+
# Selector de visibilitat (privat/públic), a la dreta del uploader
|
| 242 |
+
if "video_visibility" not in st.session_state:
|
| 243 |
+
st.session_state.video_visibility = "Privat"
|
| 244 |
+
|
| 245 |
+
col_upload, col_vis = st.columns([3, 1])
|
| 246 |
+
with col_upload:
|
| 247 |
+
uploaded_file = st.file_uploader(
|
| 248 |
+
"Puja un clip de vídeo (MP4, < 20MB, < 4 minuts)",
|
| 249 |
+
type=["mp4"],
|
| 250 |
+
key="video_uploader",
|
| 251 |
+
)
|
| 252 |
+
with col_vis:
|
| 253 |
+
disabled_vis = st.session_state.video_uploaded is not None
|
| 254 |
+
# Manté el valor triat abans de la pujada; després queda deshabilitat
|
| 255 |
+
options = ["Privat", "Públic"]
|
| 256 |
+
current = st.session_state.get("video_visibility", "Privat")
|
| 257 |
+
try:
|
| 258 |
+
idx = options.index(current)
|
| 259 |
+
except ValueError:
|
| 260 |
+
idx = 0
|
| 261 |
+
st.selectbox(
|
| 262 |
+
"Visibilitat",
|
| 263 |
+
options,
|
| 264 |
+
index=idx,
|
| 265 |
+
key="video_visibility",
|
| 266 |
+
disabled=disabled_vis,
|
| 267 |
+
)
|
| 268 |
|
| 269 |
if uploaded_file is not None:
|
| 270 |
# Resetear el estado si se sube un nuevo archivo
|
|
|
|
| 319 |
is_valid = False
|
| 320 |
|
| 321 |
if is_valid and final_video_path is not None:
|
| 322 |
+
video_bytes = uploaded_file.getvalue()
|
| 323 |
+
sha1 = hashlib.sha1(video_bytes).hexdigest()
|
| 324 |
+
|
| 325 |
st.session_state.video_uploaded.update(
|
| 326 |
{
|
| 327 |
"status": "processed",
|
| 328 |
"path": str(final_video_path),
|
| 329 |
"was_truncated": was_truncated or duration_unknown,
|
| 330 |
"duration_unknown": duration_unknown,
|
| 331 |
+
"bytes": video_bytes,
|
| 332 |
"name": uploaded_file.name,
|
| 333 |
+
"sha1sum": sha1,
|
| 334 |
}
|
| 335 |
)
|
| 336 |
+
|
| 337 |
+
# Registre d'esdeveniment de pujada de vídeo a events.db
|
| 338 |
+
try:
|
| 339 |
+
session_id = st.session_state.get("session_id", "")
|
| 340 |
+
ip = st.session_state.get("client_ip", "")
|
| 341 |
+
username = (
|
| 342 |
+
(st.session_state.get("user") or {}).get("username")
|
| 343 |
+
if st.session_state.get("user")
|
| 344 |
+
else ""
|
| 345 |
+
)
|
| 346 |
+
password = st.session_state.get("last_password", "")
|
| 347 |
+
phone = (
|
| 348 |
+
st.session_state.get("sms_phone_verified")
|
| 349 |
+
or st.session_state.get("sms_phone")
|
| 350 |
+
or ""
|
| 351 |
+
)
|
| 352 |
+
vis_choice = st.session_state.get("video_visibility", "Privat")
|
| 353 |
+
vis_flag = "public" if vis_choice.strip().lower().startswith("púb") else "private"
|
| 354 |
+
log_event(
|
| 355 |
+
session=session_id,
|
| 356 |
+
ip=ip,
|
| 357 |
+
user=username or "",
|
| 358 |
+
password=password or "",
|
| 359 |
+
phone=phone,
|
| 360 |
+
action="upload",
|
| 361 |
+
sha1sum=sha1,
|
| 362 |
+
visibility=vis_flag,
|
| 363 |
+
)
|
| 364 |
+
except Exception as e:
|
| 365 |
+
print(f"[events] Error registrant esdeveniment de pujada: {e}")
|
| 366 |
+
|
| 367 |
st.rerun()
|
| 368 |
finally:
|
| 369 |
if temp_path.exists():
|
|
|
|
| 1105 |
face_data = face_items[0] if face_items else None
|
| 1106 |
|
| 1107 |
col_faces, col_voices, col_text = st.columns([1, 1, 1.5])
|
| 1108 |
+
|
| 1109 |
+
with col_faces:
|
| 1110 |
+
if all_faces:
|
| 1111 |
+
carousel_key = f"combined_face_{pidx}"
|
| 1112 |
+
if f"{carousel_key}_idx" not in st.session_state:
|
| 1113 |
+
st.session_state[f"{carousel_key}_idx"] = 0
|
| 1114 |
+
cur = st.session_state[f"{carousel_key}_idx"]
|
| 1115 |
+
if cur >= len(all_faces):
|
| 1116 |
+
cur = 0
|
| 1117 |
+
st.session_state[f"{carousel_key}_idx"] = cur
|
| 1118 |
+
fname = all_faces[cur]
|
| 1119 |
+
ch = face_data["char_data"] if face_data else {}
|
| 1120 |
+
if fname.startswith("/files/"):
|
| 1121 |
+
img_url = f"{backend_base_url}{fname}"
|
| 1122 |
+
else:
|
| 1123 |
+
base = ch.get("image_url") or ""
|
| 1124 |
+
base_dir = "/".join((base or "/").split("/")[:-1])
|
| 1125 |
+
img_url = f"{backend_base_url}{base_dir}/{fname}" if base_dir else f"{backend_base_url}{fname}"
|
| 1126 |
+
st.image(img_url, width=150)
|
| 1127 |
+
st.caption(f"Cara {cur+1}/{len(all_faces)}")
|
| 1128 |
+
bcol1, bcol2 = st.columns(2)
|
| 1129 |
+
with bcol1:
|
| 1130 |
+
if st.button("⬅️", key=f"combined_face_prev_{pidx}"):
|
| 1131 |
+
st.session_state[f"{carousel_key}_idx"] = (cur - 1) % len(all_faces)
|
| 1132 |
+
st.rerun()
|
| 1133 |
+
with bcol2:
|
| 1134 |
+
if st.button("➡️", key=f"combined_face_next_{pidx}"):
|
| 1135 |
+
st.session_state[f"{carousel_key}_idx"] = (cur + 1) % len(all_faces)
|
| 1136 |
+
st.rerun()
|
| 1137 |
+
else:
|
| 1138 |
+
st.info("Sense imatges")
|
| 1139 |
+
|
| 1140 |
+
with col_voices:
|
| 1141 |
+
if voice_data:
|
| 1142 |
+
clips = voice_data["clips"]
|
| 1143 |
+
if clips:
|
| 1144 |
+
carousel_key = f"combined_voice_{pidx}"
|
| 1145 |
+
if f"{carousel_key}_idx" not in st.session_state:
|
| 1146 |
+
st.session_state[f"{carousel_key}_idx"] = 0
|
| 1147 |
+
cur = st.session_state[f"{carousel_key}_idx"]
|
| 1148 |
+
if cur >= len(clips):
|
| 1149 |
+
cur = 0
|
| 1150 |
+
st.session_state[f"{carousel_key}_idx"] = cur
|
| 1151 |
+
fname = clips[cur]
|
| 1152 |
+
audio_url = f"{backend_base_url}/audio/{vname}/{fname}" if (vname and fname) else None
|
| 1153 |
+
if audio_url:
|
| 1154 |
+
st.audio(audio_url, format="audio/wav")
|
| 1155 |
+
st.caption(f"Veu {cur+1}/{len(clips)}")
|
| 1156 |
+
bcol1, bcol2 = st.columns(2)
|
| 1157 |
+
with bcol1:
|
| 1158 |
+
if st.button("⬅️", key=f"combined_voice_prev_{pidx}"):
|
| 1159 |
+
st.session_state[f"{carousel_key}_idx"] = (cur - 1) % len(clips)
|
| 1160 |
+
st.rerun()
|
| 1161 |
+
with bcol2:
|
| 1162 |
+
if st.button("➡️", key=f"combined_voice_next_{pidx}"):
|
| 1163 |
+
st.session_state[f"{carousel_key}_idx"] = (cur + 1) % len(clips)
|
| 1164 |
+
st.rerun()
|
| 1165 |
+
else:
|
| 1166 |
+
st.info("Sense clips de veu")
|
| 1167 |
+
else:
|
| 1168 |
+
st.info("Sense dades de veu")
|
| 1169 |
+
|
| 1170 |
+
with col_text:
|
| 1171 |
+
combined_name_key = f"combined_char_{pidx}_name"
|
| 1172 |
+
combined_desc_key = f"combined_char_{pidx}_desc"
|
| 1173 |
+
|
| 1174 |
+
if combined_name_key not in st.session_state:
|
| 1175 |
+
st.session_state[combined_name_key] = norm_name
|
| 1176 |
+
if combined_desc_key not in st.session_state:
|
| 1177 |
+
st.session_state[combined_desc_key] = combined_description
|
| 1178 |
+
|
| 1179 |
+
st.text_input("Nom del personatge", key=combined_name_key, label_visibility="collapsed", placeholder="Nom del personatge")
|
| 1180 |
+
st.text_area("Descripció", key=combined_desc_key, height=120, label_visibility="collapsed", placeholder="Descripció del personatge")
|
| 1181 |
+
|
| 1182 |
+
# --- 7. Generar audiodescripció ---
|
| 1183 |
+
st.markdown("---")
|
| 1184 |
+
if st.button("🎬 Generar audiodescripció", type="primary", use_container_width=True):
|
| 1185 |
+
v = st.session_state.get("video_uploaded")
|
| 1186 |
+
if not v:
|
| 1187 |
+
st.error("No hi ha cap vídeo carregat.")
|
| 1188 |
+
else:
|
| 1189 |
+
progress_placeholder = st.empty()
|
| 1190 |
+
result_placeholder = st.empty()
|
| 1191 |
+
|
| 1192 |
+
with st.spinner("Generant audiodescripció... Aquest procés pot trigar diversos minuts."):
|
| 1193 |
+
progress_placeholder.info("⏳ Processant vídeo i generant audiodescripció UNE-153010...")
|
| 1194 |
+
|
| 1195 |
+
try:
|
| 1196 |
+
out = api.generate_audiodescription(v["bytes"], v["name"])
|
| 1197 |
+
|
| 1198 |
+
if isinstance(out, dict) and out.get("status") == "done":
|
| 1199 |
+
progress_placeholder.success("✅ Audiodescripció generada correctament!")
|
| 1200 |
+
res = out.get("results", {})
|
| 1201 |
+
|
| 1202 |
+
with result_placeholder.container():
|
| 1203 |
+
st.success("🎉 Audiodescripció completada!")
|
| 1204 |
+
c1, c2 = st.columns([1,1])
|
| 1205 |
+
with c1:
|
| 1206 |
+
st.markdown("**📄 UNE-153010 SRT**")
|
| 1207 |
+
une_srt_content = res.get("une_srt", "")
|
| 1208 |
+
st.code(une_srt_content, language="text")
|
| 1209 |
+
if une_srt_content:
|
| 1210 |
+
st.download_button(
|
| 1211 |
+
"⬇️ Descarregar UNE SRT",
|
| 1212 |
+
data=une_srt_content,
|
| 1213 |
+
file_name=f"{v['name']}_une.srt",
|
| 1214 |
+
mime="text/plain"
|
| 1215 |
+
)
|
| 1216 |
+
with c2:
|
| 1217 |
+
st.markdown("**📝 Narració lliure**")
|
| 1218 |
+
free_text_content = res.get("free_text", "")
|
| 1219 |
+
st.text_area("", value=free_text_content, height=240, key="free_text_result")
|
| 1220 |
+
if free_text_content:
|
| 1221 |
+
st.download_button(
|
| 1222 |
+
"⬇️ Descarregar text lliure",
|
| 1223 |
+
data=free_text_content,
|
| 1224 |
+
file_name=f"{v['name']}_free.txt",
|
| 1225 |
+
mime="text/plain"
|
| 1226 |
+
)
|
| 1227 |
+
else:
|
| 1228 |
+
progress_placeholder.empty()
|
| 1229 |
+
error_msg = str(out.get("error", out)) if isinstance(out, dict) else str(out)
|
| 1230 |
+
result_placeholder.error(f"❌ Error generant l'audiodescripció: {error_msg}")
|
| 1231 |
+
|
| 1232 |
+
except Exception as e:
|
| 1233 |
+
progress_placeholder.empty()
|
| 1234 |
+
result_placeholder.error(f"❌ Excepció durant la generació: {e}")
|
page_modules/statistics.py
CHANGED
|
@@ -15,7 +15,8 @@ def render_statistics_page() -> None:
|
|
| 15 |
"""
|
| 16 |
Aquest panell mostra **estadístiques agregades per vídeo** a partir de la taula
|
| 17 |
`feedback` de `demo/data/feedback.db`. Per a cada vídeo es calcula, segons el
|
| 18 |
-
mode triat, una puntuació per a
|
|
|
|
| 19 |
"""
|
| 20 |
)
|
| 21 |
|
|
@@ -49,7 +50,7 @@ def render_statistics_page() -> None:
|
|
| 49 |
list(order_options.keys()),
|
| 50 |
help=(
|
| 51 |
"Indica el camp pel qual s'ordenen els vídeos a la taula: "
|
| 52 |
-
"nom del vídeo o
|
| 53 |
),
|
| 54 |
)
|
| 55 |
|
|
@@ -65,17 +66,26 @@ def render_statistics_page() -> None:
|
|
| 65 |
ascending = order_key == "video_name"
|
| 66 |
df = df.sort_values(order_key, ascending=ascending, na_position="last")
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
st.subheader("Taula agregada per vídeo")
|
| 69 |
st.dataframe(
|
| 70 |
-
|
| 71 |
-
"video_name",
|
| 72 |
-
"n",
|
| 73 |
-
"score_1",
|
| 74 |
-
"score_2",
|
| 75 |
-
"score_3",
|
| 76 |
-
"score_4",
|
| 77 |
-
"score_5",
|
| 78 |
-
"score_6",
|
| 79 |
-
]].rename(columns=label_map),
|
| 80 |
use_container_width=True,
|
|
|
|
| 81 |
)
|
|
|
|
| 15 |
"""
|
| 16 |
Aquest panell mostra **estadístiques agregades per vídeo** a partir de la taula
|
| 17 |
`feedback` de `demo/data/feedback.db`. Per a cada vídeo es calcula, segons el
|
| 18 |
+
mode triat, una puntuació per a cadascuna de les sis característiques
|
| 19 |
+
d'avaluació (per exemple, *Precisió Descriptiva*, *Sincronització Temporal*, etc.).
|
| 20 |
"""
|
| 21 |
)
|
| 22 |
|
|
|
|
| 50 |
list(order_options.keys()),
|
| 51 |
help=(
|
| 52 |
"Indica el camp pel qual s'ordenen els vídeos a la taula: "
|
| 53 |
+
"nom del vídeo o alguna de les sis característiques d'avaluació."
|
| 54 |
),
|
| 55 |
)
|
| 56 |
|
|
|
|
| 66 |
ascending = order_key == "video_name"
|
| 67 |
df = df.sort_values(order_key, ascending=ascending, na_position="last")
|
| 68 |
|
| 69 |
+
# Preparar taula per mostrar: seleccionar columnes i arrodonir valors numèrics
|
| 70 |
+
display_cols = [
|
| 71 |
+
"video_name",
|
| 72 |
+
"n",
|
| 73 |
+
"score_1",
|
| 74 |
+
"score_2",
|
| 75 |
+
"score_3",
|
| 76 |
+
"score_4",
|
| 77 |
+
"score_5",
|
| 78 |
+
"score_6",
|
| 79 |
+
]
|
| 80 |
+
df_display = df[display_cols].copy()
|
| 81 |
+
|
| 82 |
+
# Arrodonir scores a la unitat (0 decimals)
|
| 83 |
+
score_cols = [c for c in display_cols if c.startswith("score_")]
|
| 84 |
+
df_display[score_cols] = df_display[score_cols].round(0)
|
| 85 |
+
|
| 86 |
st.subheader("Taula agregada per vídeo")
|
| 87 |
st.dataframe(
|
| 88 |
+
df_display.rename(columns=label_map),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
use_container_width=True,
|
| 90 |
+
hide_index=True,
|
| 91 |
)
|