Upload 6 files
Browse files- api_client.py +43 -0
- page_modules/process_video.py +21 -1
- page_modules/validation.py +115 -24
api_client.py
CHANGED
|
@@ -204,6 +204,49 @@ class APIClient:
|
|
| 204 |
return {"error": str(e)}
|
| 205 |
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
def update_databases(self, payload: dict) -> dict:
|
| 208 |
"""Envia les sentències SQL generades a l'endpoint /update_databases."""
|
| 209 |
|
|
|
|
| 204 |
return {"error": str(e)}
|
| 205 |
|
| 206 |
|
| 207 |
+
# ---- Pending videos (peding_videos) ----
|
| 208 |
+
|
| 209 |
+
def upload_pending_video(self, video_bytes: bytes, filename: str) -> dict:
|
| 210 |
+
"""Sube un vídeo pendiente al engine (carpeta /data/peding_videos).
|
| 211 |
+
|
| 212 |
+
Usa el endpoint POST /peding_videos/upload_peding_video.
|
| 213 |
+
"""
|
| 214 |
+
|
| 215 |
+
url = f"{self.base_url}/peding_videos/upload_peding_video"
|
| 216 |
+
files = {"video": (filename, io.BytesIO(video_bytes), "video/mp4")}
|
| 217 |
+
try:
|
| 218 |
+
r = self.session.post(url, files=files, timeout=self.timeout * 5)
|
| 219 |
+
r.raise_for_status()
|
| 220 |
+
return r.json() if r.headers.get("content-type", "").startswith("application/json") else {"status": "ok"}
|
| 221 |
+
except requests.exceptions.RequestException as e:
|
| 222 |
+
return {"error": str(e)}
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def list_pending_videos(self) -> dict:
|
| 226 |
+
"""Llista els vídeos pendents al backend (endpoint GET /peding_videos/list_peding_videos)."""
|
| 227 |
+
|
| 228 |
+
url = f"{self.base_url}/peding_videos/list_peding_videos"
|
| 229 |
+
try:
|
| 230 |
+
r = self.session.get(url, timeout=self.timeout * 5)
|
| 231 |
+
r.raise_for_status()
|
| 232 |
+
return r.json()
|
| 233 |
+
except requests.exceptions.RequestException as e:
|
| 234 |
+
return {"error": str(e)}
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def download_pending_video(self, sha1sum: str) -> dict:
|
| 238 |
+
"""Descarrega un vídeo pendent per sha1 (GET /peding_videos/download_peding_video)."""
|
| 239 |
+
|
| 240 |
+
url = f"{self.base_url}/peding_videos/download_peding_video"
|
| 241 |
+
params = {"sha1": sha1sum}
|
| 242 |
+
try:
|
| 243 |
+
r = self.session.get(url, params=params, timeout=self.timeout * 5)
|
| 244 |
+
r.raise_for_status()
|
| 245 |
+
return {"video_bytes": r.content}
|
| 246 |
+
except requests.exceptions.RequestException as e:
|
| 247 |
+
return {"error": str(e)}
|
| 248 |
+
|
| 249 |
+
|
| 250 |
def update_databases(self, payload: dict) -> dict:
|
| 251 |
"""Envia les sentències SQL generades a l'endpoint /update_databases."""
|
| 252 |
|
page_modules/process_video.py
CHANGED
|
@@ -15,7 +15,7 @@ import streamlit as st
|
|
| 15 |
from PIL import Image, ImageDraw
|
| 16 |
from databases import log_event, has_video_approval_event
|
| 17 |
from compliance_client import compliance_client
|
| 18 |
-
from persistent_data_gate import ensure_temp_databases
|
| 19 |
|
| 20 |
|
| 21 |
def get_all_catalan_names():
|
|
@@ -371,6 +371,26 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 371 |
except Exception as e:
|
| 372 |
print(f"[events] Error registrant esdeveniment de pujada: {e}")
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
# Marcar vídeo com pendent de validació i enviar SMS al validor
|
| 375 |
st.session_state.video_requires_validation = True
|
| 376 |
st.session_state.video_validation_approved = False
|
|
|
|
| 15 |
from PIL import Image, ImageDraw
|
| 16 |
from databases import log_event, has_video_approval_event
|
| 17 |
from compliance_client import compliance_client
|
| 18 |
+
from persistent_data_gate import ensure_temp_databases, _load_data_origin
|
| 19 |
|
| 20 |
|
| 21 |
def get_all_catalan_names():
|
|
|
|
| 371 |
except Exception as e:
|
| 372 |
print(f"[events] Error registrant esdeveniment de pujada: {e}")
|
| 373 |
|
| 374 |
+
# Si treballem en mode external, enviar el vídeo a pending_videos de l'engine
|
| 375 |
+
try:
|
| 376 |
+
base_dir = Path(__file__).parent.parent
|
| 377 |
+
data_origin = _load_data_origin(base_dir)
|
| 378 |
+
if data_origin == "external":
|
| 379 |
+
pending_root = base_dir / "temp" / "pending_videos" / sha1
|
| 380 |
+
pending_root.mkdir(parents=True, exist_ok=True)
|
| 381 |
+
local_pending_path = pending_root / "video.mp4"
|
| 382 |
+
# Guardar còpia local del vídeo pendent
|
| 383 |
+
with local_pending_path.open("wb") as f_pending:
|
| 384 |
+
f_pending.write(video_bytes)
|
| 385 |
+
|
| 386 |
+
# Enviar el vídeo al backend engine perquè aparegui a la llista de pendents
|
| 387 |
+
try:
|
| 388 |
+
api.upload_pending_video(video_bytes, uploaded_file.name)
|
| 389 |
+
except Exception:
|
| 390 |
+
pass
|
| 391 |
+
except Exception:
|
| 392 |
+
pass
|
| 393 |
+
|
| 394 |
# Marcar vídeo com pendent de validació i enviar SMS al validor
|
| 395 |
st.session_state.video_requires_validation = True
|
| 396 |
st.session_state.video_validation_approved = False
|
page_modules/validation.py
CHANGED
|
@@ -6,9 +6,11 @@ from datetime import datetime
|
|
| 6 |
from pathlib import Path
|
| 7 |
from typing import Dict
|
| 8 |
|
|
|
|
| 9 |
import streamlit as st
|
| 10 |
|
| 11 |
from databases import get_accessible_videos_with_sha1, log_event
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
def render_validation_page(
|
|
@@ -25,37 +27,88 @@ def render_validation_page(
|
|
| 25 |
|
| 26 |
tab_videos, tab_ads = st.tabs(["📹 Validar Vídeos", "🎬 Validar Audiodescripcions"])
|
| 27 |
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
| 29 |
session_id = st.session_state.get("session_id")
|
| 30 |
-
accessible_rows = get_accessible_videos_with_sha1(session_id)
|
| 31 |
|
| 32 |
-
#
|
| 33 |
-
base_media_dir =
|
|
|
|
| 34 |
|
| 35 |
with tab_videos:
|
| 36 |
st.subheader("📹 Validar Vídeos Pujats")
|
| 37 |
|
| 38 |
video_folders = []
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
if not video_folders:
|
| 61 |
st.info("📝 No hi ha vídeos pujats pendents de validació.")
|
|
@@ -79,6 +132,28 @@ def render_validation_page(
|
|
| 79 |
st.markdown(f"**Data:** {video_seleccionat['created_at']}")
|
| 80 |
st.markdown(f"**Arxius:** {len(video_seleccionat['video_files'])} vídeos trobats")
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
if video_seleccionat["video_files"]:
|
| 83 |
video_path = str(video_seleccionat["video_files"][0])
|
| 84 |
st.markdown("**Vídeo principal:**")
|
|
@@ -126,6 +201,22 @@ def render_validation_page(
|
|
| 126 |
else:
|
| 127 |
st.error("❌ Error registrant el veredicte al servei de compliance")
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
with col_btn2:
|
| 130 |
if st.button("❌ Rebutjar", type="secondary", key=f"reject_video_{video_seleccionat['video_name']}"):
|
| 131 |
success = compliance_client.record_validator_decision(
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
from typing import Dict
|
| 8 |
|
| 9 |
+
import shutil
|
| 10 |
import streamlit as st
|
| 11 |
|
| 12 |
from databases import get_accessible_videos_with_sha1, log_event
|
| 13 |
+
from persistent_data_gate import _load_data_origin
|
| 14 |
|
| 15 |
|
| 16 |
def render_validation_page(
|
|
|
|
| 27 |
|
| 28 |
tab_videos, tab_ads = st.tabs(["📹 Validar Vídeos", "🎬 Validar Audiodescripcions"])
|
| 29 |
|
| 30 |
+
base_dir = Path(__file__).resolve().parent.parent
|
| 31 |
+
data_origin = _load_data_origin(base_dir)
|
| 32 |
+
|
| 33 |
+
# Llista de vídeos accessibles (mode internal) o pendents al backend (mode external)
|
| 34 |
session_id = st.session_state.get("session_id")
|
| 35 |
+
accessible_rows = get_accessible_videos_with_sha1(session_id) if data_origin == "internal" else []
|
| 36 |
|
| 37 |
+
# Rutes base per a media i vídeos pendents
|
| 38 |
+
base_media_dir = base_dir / "temp" / "media"
|
| 39 |
+
pending_root = base_dir / "temp" / "pending_videos"
|
| 40 |
|
| 41 |
with tab_videos:
|
| 42 |
st.subheader("📹 Validar Vídeos Pujats")
|
| 43 |
|
| 44 |
video_folders = []
|
| 45 |
+
|
| 46 |
+
if data_origin == "internal":
|
| 47 |
+
# Mode intern: llistar vídeos accessibles des de temp/media
|
| 48 |
+
for row in accessible_rows:
|
| 49 |
+
sha1 = row["sha1sum"]
|
| 50 |
+
video_name = row["video_name"] or row["sha1sum"]
|
| 51 |
+
folder = base_media_dir / sha1
|
| 52 |
+
if not folder.exists() or not folder.is_dir():
|
| 53 |
+
continue
|
| 54 |
+
video_files = list(folder.glob("*.mp4")) + list(folder.glob("*.avi")) + list(folder.glob("*.mov"))
|
| 55 |
+
if not video_files:
|
| 56 |
+
continue
|
| 57 |
+
mod_time = folder.stat().st_mtime
|
| 58 |
+
fecha = datetime.fromtimestamp(mod_time).strftime("%Y-%m-%d %H:%M")
|
| 59 |
+
video_folders.append(
|
| 60 |
+
{
|
| 61 |
+
"sha1sum": sha1,
|
| 62 |
+
"video_name": video_name,
|
| 63 |
+
"path": str(folder),
|
| 64 |
+
"created_at": fecha,
|
| 65 |
+
"video_files": video_files,
|
| 66 |
+
}
|
| 67 |
+
)
|
| 68 |
+
else:
|
| 69 |
+
# Mode external: llistar vídeos pendents des de l'engine
|
| 70 |
+
api_client = st.session_state.get("api_client")
|
| 71 |
+
if api_client is not None:
|
| 72 |
+
try:
|
| 73 |
+
resp = api_client.list_pending_videos()
|
| 74 |
+
except Exception:
|
| 75 |
+
resp = {"error": "exception"}
|
| 76 |
+
|
| 77 |
+
pending_list = []
|
| 78 |
+
if isinstance(resp, dict) and not resp.get("error"):
|
| 79 |
+
# Pot ser un dict amb clau "videos" o directament una llista
|
| 80 |
+
if isinstance(resp.get("videos"), list):
|
| 81 |
+
pending_list = resp["videos"]
|
| 82 |
+
elif isinstance(resp.get("items"), list):
|
| 83 |
+
pending_list = resp["items"]
|
| 84 |
+
elif isinstance(resp.get("results"), list):
|
| 85 |
+
pending_list = resp["results"]
|
| 86 |
+
elif isinstance(resp, list):
|
| 87 |
+
pending_list = resp
|
| 88 |
+
elif isinstance(resp, list):
|
| 89 |
+
pending_list = resp
|
| 90 |
+
|
| 91 |
+
for item in pending_list:
|
| 92 |
+
sha1 = item.get("sha1") or item.get("video_hash") or item.get("id")
|
| 93 |
+
if not sha1:
|
| 94 |
+
continue
|
| 95 |
+
video_name = item.get("latest_video") or sha1
|
| 96 |
+
# Carpeta local on descarregarem el vídeo pendent si cal
|
| 97 |
+
folder = pending_root / sha1
|
| 98 |
+
if folder.exists():
|
| 99 |
+
video_files = list(folder.glob("*.mp4"))
|
| 100 |
+
else:
|
| 101 |
+
video_files = []
|
| 102 |
+
created_at = item.get("created_at") or datetime.utcnow().strftime("%Y-%m-%d %H:%M")
|
| 103 |
+
video_folders.append(
|
| 104 |
+
{
|
| 105 |
+
"sha1sum": sha1,
|
| 106 |
+
"video_name": video_name,
|
| 107 |
+
"path": str(folder),
|
| 108 |
+
"created_at": created_at,
|
| 109 |
+
"video_files": video_files,
|
| 110 |
+
}
|
| 111 |
+
)
|
| 112 |
|
| 113 |
if not video_folders:
|
| 114 |
st.info("📝 No hi ha vídeos pujats pendents de validació.")
|
|
|
|
| 132 |
st.markdown(f"**Data:** {video_seleccionat['created_at']}")
|
| 133 |
st.markdown(f"**Arxius:** {len(video_seleccionat['video_files'])} vídeos trobats")
|
| 134 |
|
| 135 |
+
# Assegurar que disposem del fitxer local en mode external
|
| 136 |
+
if data_origin == "external" and not video_seleccionat["video_files"]:
|
| 137 |
+
api_client = st.session_state.get("api_client")
|
| 138 |
+
if api_client is not None:
|
| 139 |
+
try:
|
| 140 |
+
resp = api_client.download_pending_video(video_seleccionat["sha1sum"])
|
| 141 |
+
except Exception:
|
| 142 |
+
resp = {"error": "exception"}
|
| 143 |
+
|
| 144 |
+
video_bytes = (
|
| 145 |
+
resp.get("video_bytes")
|
| 146 |
+
if isinstance(resp, dict)
|
| 147 |
+
else None
|
| 148 |
+
)
|
| 149 |
+
if video_bytes:
|
| 150 |
+
local_folder = pending_root / video_seleccionat["sha1sum"]
|
| 151 |
+
local_folder.mkdir(parents=True, exist_ok=True)
|
| 152 |
+
local_path = local_folder / "video.mp4"
|
| 153 |
+
with local_path.open("wb") as f:
|
| 154 |
+
f.write(video_bytes)
|
| 155 |
+
video_seleccionat["video_files"] = [local_path]
|
| 156 |
+
|
| 157 |
if video_seleccionat["video_files"]:
|
| 158 |
video_path = str(video_seleccionat["video_files"][0])
|
| 159 |
st.markdown("**Vídeo principal:**")
|
|
|
|
| 201 |
else:
|
| 202 |
st.error("❌ Error registrant el veredicte al servei de compliance")
|
| 203 |
|
| 204 |
+
# 3) En mode external, moure el vídeo de temp/pending_videos a temp/media
|
| 205 |
+
if data_origin == "external":
|
| 206 |
+
sha1 = video_seleccionat["sha1sum"]
|
| 207 |
+
local_pending_dir = pending_root / sha1
|
| 208 |
+
local_media_dir = base_media_dir / sha1
|
| 209 |
+
try:
|
| 210 |
+
local_media_dir.mkdir(parents=True, exist_ok=True)
|
| 211 |
+
src = local_pending_dir / "video.mp4"
|
| 212 |
+
if src.exists():
|
| 213 |
+
dst = local_media_dir / "video.mp4"
|
| 214 |
+
shutil.copy2(src, dst)
|
| 215 |
+
if local_pending_dir.exists():
|
| 216 |
+
shutil.rmtree(local_pending_dir)
|
| 217 |
+
except Exception:
|
| 218 |
+
pass
|
| 219 |
+
|
| 220 |
with col_btn2:
|
| 221 |
if st.button("❌ Rebutjar", type="secondary", key=f"reject_video_{video_seleccionat['video_name']}"):
|
| 222 |
success = compliance_client.record_validator_decision(
|