Upload 15 files
Browse files- auth.py +4 -12
- compliance_client.py +47 -0
- databases.py +343 -40
- page_modules/__pycache__/new_video_processing.cpython-313.pyc +0 -0
- page_modules/analyze_audiodescriptions.py +174 -91
- page_modules/new_video_processing.py +212 -33
- page_modules/validation.py +199 -88
- persistent_data_gate.py +158 -4
- scripts/add_status_column_videos.py +36 -0
- scripts/create_actions_db.py +38 -0
- scripts/drop_feedback_ad_table.py +24 -0
- scripts/show_db_columns.py +57 -0
auth.py
CHANGED
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
| 8 |
|
| 9 |
import streamlit as st
|
| 10 |
|
| 11 |
-
from databases import get_user, create_user, update_user_password, get_all_users,
|
| 12 |
from mobile_verification import (
|
| 13 |
initialize_sms_state,
|
| 14 |
render_mobile_verification_screen,
|
|
@@ -115,21 +115,17 @@ def render_login_form():
|
|
| 115 |
# Registre d'esdeveniment de login a events.db
|
| 116 |
try:
|
| 117 |
session_id = st.session_state.get("session_id", "")
|
| 118 |
-
ip = st.session_state.get("client_ip", "")
|
| 119 |
phone = (
|
| 120 |
st.session_state.get("sms_phone_verified")
|
| 121 |
or st.session_state.get("sms_phone")
|
| 122 |
or ""
|
| 123 |
)
|
| 124 |
-
|
| 125 |
session=session_id,
|
| 126 |
-
ip=ip,
|
| 127 |
user=username or "",
|
| 128 |
-
password=password or "",
|
| 129 |
phone=phone,
|
| 130 |
action="login",
|
| 131 |
sha1sum="",
|
| 132 |
-
visibility="",
|
| 133 |
)
|
| 134 |
except Exception as e:
|
| 135 |
log(f"Error registrant esdeveniment de login: {e}")
|
|
@@ -243,22 +239,18 @@ def render_sidebar():
|
|
| 243 |
try:
|
| 244 |
current_user = st.session_state.user or {}
|
| 245 |
session_id = st.session_state.get("session_id", "")
|
| 246 |
-
ip = st.session_state.get("client_ip", "")
|
| 247 |
phone = (
|
| 248 |
st.session_state.get("sms_phone_verified")
|
| 249 |
or st.session_state.get("sms_phone")
|
| 250 |
or ""
|
| 251 |
)
|
| 252 |
last_password = st.session_state.get("last_password", "")
|
| 253 |
-
|
| 254 |
session=session_id,
|
| 255 |
-
ip=ip,
|
| 256 |
user=current_user.get("username", ""),
|
| 257 |
-
password=last_password,
|
| 258 |
phone=phone,
|
| 259 |
action="logout",
|
| 260 |
sha1sum="",
|
| 261 |
-
visibility="",
|
| 262 |
)
|
| 263 |
except Exception as e:
|
| 264 |
log(f"Error registrant esdeveniment de logout: {e}")
|
|
@@ -269,7 +261,7 @@ def render_sidebar():
|
|
| 269 |
log(
|
| 270 |
"Logout completat: "
|
| 271 |
f"session={session_id or '-'} "
|
| 272 |
-
f"events_digest={
|
| 273 |
f"events_count={events_count if events_count is not None else '-'} "
|
| 274 |
f"polygon_published={'sí' if blockchain_published else 'no'} "
|
| 275 |
f"polygon_url={polygonscan_url or '-'}"
|
|
|
|
| 8 |
|
| 9 |
import streamlit as st
|
| 10 |
|
| 11 |
+
from databases import get_user, create_user, update_user_password, get_all_users, log_action
|
| 12 |
from mobile_verification import (
|
| 13 |
initialize_sms_state,
|
| 14 |
render_mobile_verification_screen,
|
|
|
|
| 115 |
# Registre d'esdeveniment de login a events.db
|
| 116 |
try:
|
| 117 |
session_id = st.session_state.get("session_id", "")
|
|
|
|
| 118 |
phone = (
|
| 119 |
st.session_state.get("sms_phone_verified")
|
| 120 |
or st.session_state.get("sms_phone")
|
| 121 |
or ""
|
| 122 |
)
|
| 123 |
+
log_action(
|
| 124 |
session=session_id,
|
|
|
|
| 125 |
user=username or "",
|
|
|
|
| 126 |
phone=phone,
|
| 127 |
action="login",
|
| 128 |
sha1sum="",
|
|
|
|
| 129 |
)
|
| 130 |
except Exception as e:
|
| 131 |
log(f"Error registrant esdeveniment de login: {e}")
|
|
|
|
| 239 |
try:
|
| 240 |
current_user = st.session_state.user or {}
|
| 241 |
session_id = st.session_state.get("session_id", "")
|
|
|
|
| 242 |
phone = (
|
| 243 |
st.session_state.get("sms_phone_verified")
|
| 244 |
or st.session_state.get("sms_phone")
|
| 245 |
or ""
|
| 246 |
)
|
| 247 |
last_password = st.session_state.get("last_password", "")
|
| 248 |
+
log_action(
|
| 249 |
session=session_id,
|
|
|
|
| 250 |
user=current_user.get("username", ""),
|
|
|
|
| 251 |
phone=phone,
|
| 252 |
action="logout",
|
| 253 |
sha1sum="",
|
|
|
|
| 254 |
)
|
| 255 |
except Exception as e:
|
| 256 |
log(f"Error registrant esdeveniment de logout: {e}")
|
|
|
|
| 261 |
log(
|
| 262 |
"Logout completat: "
|
| 263 |
f"session={session_id or '-'} "
|
| 264 |
+
f"events_digest={digest_hash or '-'} "
|
| 265 |
f"events_count={events_count if events_count is not None else '-'} "
|
| 266 |
f"polygon_published={'sí' if blockchain_published else 'no'} "
|
| 267 |
f"polygon_url={polygonscan_url or '-'}"
|
compliance_client.py
CHANGED
|
@@ -458,6 +458,32 @@ class ComplianceClient:
|
|
| 458 |
)
|
| 459 |
return bool(response and response.get("success"))
|
| 460 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
def send_login_sms(self, phone: str, code: str) -> bool:
|
| 462 |
"""Envia un SMS de verificació de login a través del servei de compliance.
|
| 463 |
|
|
@@ -489,6 +515,27 @@ class ComplianceClient:
|
|
| 489 |
return response
|
| 490 |
|
| 491 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
| 493 |
def send_validation_request(self, validation_request: Dict[str, Any]) -> bool:
|
| 494 |
"""Envía solicitud de validación a validadores"""
|
|
|
|
| 458 |
)
|
| 459 |
return bool(response and response.get("success"))
|
| 460 |
|
| 461 |
+
def notify_user_video_approved(self, phone: str, message: str, sha1sum: str) -> bool:
|
| 462 |
+
"""Envia un SMS a l'usuari indicant que el seu vídeo ha estat aprovat.
|
| 463 |
+
|
| 464 |
+
El backend de compliance decidirà si utilitza Twilio o Zapier segons
|
| 465 |
+
la configuració (twilio_enabled / zapier_enabled).
|
| 466 |
+
"""
|
| 467 |
+
|
| 468 |
+
payload = {"phone": phone, "message": message, "sha1sum": sha1sum}
|
| 469 |
+
response = self._make_request(
|
| 470 |
+
"POST", "/api/notifications/user-video-approved-sms", payload
|
| 471 |
+
)
|
| 472 |
+
return bool(response and response.get("success"))
|
| 473 |
+
|
| 474 |
+
def notify_une_validator_new_ads(self, phone: str, message: str) -> bool:
|
| 475 |
+
"""Envia un SMS al validador UNE indicant que hi ha noves AD per validar.
|
| 476 |
+
|
| 477 |
+
El backend de compliance s'encarrega de triar Twilio o Zapier segons
|
| 478 |
+
la configuració (twilio_enabled / zapier_enabled).
|
| 479 |
+
"""
|
| 480 |
+
|
| 481 |
+
payload = {"phone": phone, "message": message}
|
| 482 |
+
response = self._make_request(
|
| 483 |
+
"POST", "/api/notifications/une-validation-sms", payload
|
| 484 |
+
)
|
| 485 |
+
return bool(response and response.get("success"))
|
| 486 |
+
|
| 487 |
def send_login_sms(self, phone: str, code: str) -> bool:
|
| 488 |
"""Envia un SMS de verificació de login a través del servei de compliance.
|
| 489 |
|
|
|
|
| 515 |
return response
|
| 516 |
|
| 517 |
return None
|
| 518 |
+
|
| 519 |
+
def publish_actions_qldb(self, session_id: str, actions: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
| 520 |
+
"""Envia el registre de canvis d'actions.db a una taula QLDB.
|
| 521 |
+
|
| 522 |
+
El backend és responsable d'escriure aquest payload a AWS QLDB quan
|
| 523 |
+
la funcionalitat de blockchain privada estigui activada.
|
| 524 |
+
"""
|
| 525 |
+
|
| 526 |
+
payload = {
|
| 527 |
+
"session_id": session_id,
|
| 528 |
+
"actions": actions,
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
response = self._make_request(
|
| 532 |
+
"POST", "/api/blockchain/publish-actions-qldb", payload
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
if response:
|
| 536 |
+
return response
|
| 537 |
+
|
| 538 |
+
return None
|
| 539 |
|
| 540 |
def send_validation_request(self, validation_request: Dict[str, Any]) -> bool:
|
| 541 |
"""Envía solicitud de validación a validadores"""
|
databases.py
CHANGED
|
@@ -19,15 +19,19 @@ FEEDBACK_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "feedback.d
|
|
| 19 |
# Ruta a la base de dades de captions per als scores
|
| 20 |
CAPTIONS_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "captions.db"
|
| 21 |
|
| 22 |
-
# Ruta a la base de dades d'esdeveniments (events.db) a demo/temp/db
|
| 23 |
-
EVENTS_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "events.db"
|
| 24 |
-
|
| 25 |
# Ruta a la base de dades de vídeos (videos.db) a demo/temp/db
|
| 26 |
VIDEOS_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "videos.db"
|
| 27 |
|
| 28 |
# Ruta a la base de dades d'audiodescripcions (audiodescriptions.db) a demo/temp/db
|
| 29 |
AUDIODESCRIPTIONS_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "audiodescriptions.db"
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
def set_db_path(db_path: str):
|
| 33 |
global DEFAULT_DB_PATH
|
|
@@ -220,11 +224,11 @@ def get_accessible_videos_for_session(session_id: str | None) -> List[str]:
|
|
| 220 |
if not session_id:
|
| 221 |
return sorted(public_videos)
|
| 222 |
|
| 223 |
-
# 2) Telèfons associats a la sessió actual
|
| 224 |
phones: set[str] = set()
|
| 225 |
-
with
|
| 226 |
-
for row in
|
| 227 |
-
"SELECT DISTINCT phone FROM
|
| 228 |
(session_id,),
|
| 229 |
):
|
| 230 |
phones.add(row["phone"])
|
|
@@ -310,6 +314,255 @@ def _connect_feedback_db() -> sqlite3.Connection:
|
|
| 310 |
return conn
|
| 311 |
|
| 312 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
def _connect_audiodescriptions_db() -> sqlite3.Connection:
|
| 314 |
"""Connexió directa a demo/temp/audiodescriptions.db.
|
| 315 |
|
|
@@ -702,50 +955,100 @@ def _connect_videos_db() -> sqlite3.Connection:
|
|
| 702 |
return conn
|
| 703 |
|
| 704 |
|
| 705 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
*,
|
| 707 |
session: str,
|
| 708 |
-
ip: str,
|
| 709 |
user: str,
|
| 710 |
-
password: str,
|
| 711 |
phone: str,
|
| 712 |
action: str,
|
| 713 |
sha1sum: str,
|
| 714 |
-
visibility: str | None = None,
|
| 715 |
timestamp: Optional[str] = None,
|
| 716 |
) -> None:
|
| 717 |
-
"""Insereix un registre a demo/temp/
|
| 718 |
-
|
| 719 |
-
- timestamp: si no s'especifica, es fa servir UTC "YYYY-MM-DD HH:MM:SS".
|
| 720 |
-
- session, ip, user, password, phone, sha1sum es guarden com a TEXT.
|
| 721 |
-
"""
|
| 722 |
|
| 723 |
ts = timestamp or datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
| 724 |
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
session or "",
|
| 734 |
-
ip or "",
|
| 735 |
-
user or "",
|
| 736 |
-
password or "",
|
| 737 |
-
phone or "",
|
| 738 |
-
action,
|
| 739 |
-
sha1sum or "",
|
| 740 |
-
visibility or "",
|
| 741 |
-
),
|
| 742 |
-
)
|
| 743 |
|
| 744 |
|
| 745 |
-
def
|
| 746 |
-
"""Comprova si existeix
|
| 747 |
|
| 748 |
-
Busca a demo/temp/
|
| 749 |
especificat.
|
| 750 |
"""
|
| 751 |
|
|
@@ -753,10 +1056,10 @@ def has_video_approval_event(sha1sum: str) -> bool:
|
|
| 753 |
return False
|
| 754 |
|
| 755 |
try:
|
| 756 |
-
with
|
| 757 |
cur = conn.execute(
|
| 758 |
-
"SELECT 1 FROM
|
| 759 |
-
("
|
| 760 |
)
|
| 761 |
return cur.fetchone() is not None
|
| 762 |
except sqlite3.OperationalError:
|
|
|
|
| 19 |
# Ruta a la base de dades de captions per als scores
|
| 20 |
CAPTIONS_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "captions.db"
|
| 21 |
|
|
|
|
|
|
|
|
|
|
| 22 |
# Ruta a la base de dades de vídeos (videos.db) a demo/temp/db
|
| 23 |
VIDEOS_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "videos.db"
|
| 24 |
|
| 25 |
# Ruta a la base de dades d'audiodescripcions (audiodescriptions.db) a demo/temp/db
|
| 26 |
AUDIODESCRIPTIONS_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "audiodescriptions.db"
|
| 27 |
|
| 28 |
+
# Ruta a la base de dades d'accions (actions.db) a demo/temp/db
|
| 29 |
+
ACTIONS_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "actions.db"
|
| 30 |
+
|
| 31 |
+
# Ruta a les bases de dades de càsting i escenaris a demo/temp/db
|
| 32 |
+
CASTING_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "casting.db"
|
| 33 |
+
SCENARIOS_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "scenarios.db"
|
| 34 |
+
|
| 35 |
|
| 36 |
def set_db_path(db_path: str):
|
| 37 |
global DEFAULT_DB_PATH
|
|
|
|
| 224 |
if not session_id:
|
| 225 |
return sorted(public_videos)
|
| 226 |
|
| 227 |
+
# 2) Telèfons associats a la sessió actual (a partir d'actions.db)
|
| 228 |
phones: set[str] = set()
|
| 229 |
+
with _connect_actions_db() as aconn:
|
| 230 |
+
for row in aconn.execute(
|
| 231 |
+
"SELECT DISTINCT phone FROM actions WHERE session = ? AND phone IS NOT NULL AND phone != ''",
|
| 232 |
(session_id,),
|
| 233 |
):
|
| 234 |
phones.add(row["phone"])
|
|
|
|
| 314 |
return conn
|
| 315 |
|
| 316 |
|
| 317 |
+
def _connect_actions_db() -> sqlite3.Connection:
|
| 318 |
+
ACTIONS_DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
| 319 |
+
conn = sqlite3.connect(str(ACTIONS_DB_PATH))
|
| 320 |
+
conn.row_factory = sqlite3.Row
|
| 321 |
+
return conn
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def get_latest_user_phone_for_session(session_id: str) -> Tuple[str, str]:
|
| 325 |
+
if not session_id:
|
| 326 |
+
return "", ""
|
| 327 |
+
|
| 328 |
+
try:
|
| 329 |
+
with _connect_actions_db() as conn:
|
| 330 |
+
cur = conn.execute(
|
| 331 |
+
"SELECT user, phone FROM actions "
|
| 332 |
+
"WHERE session = ? AND (user IS NOT NULL OR phone IS NOT NULL) "
|
| 333 |
+
"ORDER BY id DESC LIMIT 1",
|
| 334 |
+
(session_id,),
|
| 335 |
+
)
|
| 336 |
+
row = cur.fetchone()
|
| 337 |
+
if not row:
|
| 338 |
+
return "", ""
|
| 339 |
+
u = row["user"] if row["user"] is not None else ""
|
| 340 |
+
p = row["phone"] if row["phone"] is not None else ""
|
| 341 |
+
return str(u), str(p)
|
| 342 |
+
except sqlite3.OperationalError:
|
| 343 |
+
return "", ""
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def insert_action(
|
| 347 |
+
*,
|
| 348 |
+
session: str,
|
| 349 |
+
user: str,
|
| 350 |
+
phone: str,
|
| 351 |
+
action: str,
|
| 352 |
+
sha1sum: str,
|
| 353 |
+
timestamp: Optional[str] = None,
|
| 354 |
+
) -> None:
|
| 355 |
+
ts = timestamp or datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
| 356 |
+
|
| 357 |
+
try:
|
| 358 |
+
with _connect_actions_db() as conn:
|
| 359 |
+
cur = conn.cursor()
|
| 360 |
+
cur.execute(
|
| 361 |
+
"""
|
| 362 |
+
CREATE TABLE IF NOT EXISTS actions (
|
| 363 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 364 |
+
timestamp TEXT NOT NULL,
|
| 365 |
+
action TEXT NOT NULL,
|
| 366 |
+
session TEXT,
|
| 367 |
+
user TEXT,
|
| 368 |
+
phone TEXT,
|
| 369 |
+
sha1sum TEXT
|
| 370 |
+
);
|
| 371 |
+
"""
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
cur.execute(
|
| 375 |
+
"""INSERT INTO actions
|
| 376 |
+
(timestamp, action, session, user, phone, sha1sum)
|
| 377 |
+
VALUES (?,?,?,?,?,?)""",
|
| 378 |
+
(ts, action, session or "", user or "", phone or "", sha1sum or ""),
|
| 379 |
+
)
|
| 380 |
+
except sqlite3.OperationalError:
|
| 381 |
+
return
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def get_video_owner_by_sha1(sha1sum: str) -> str:
|
| 385 |
+
"""Retorna el telèfon (owner) associat a un sha1sum a videos.db, o "".
|
| 386 |
+
|
| 387 |
+
Cerca a demo/temp/db/videos.db una fila amb aquest sha1sum i retorna el
|
| 388 |
+
camp owner si existeix.
|
| 389 |
+
"""
|
| 390 |
+
|
| 391 |
+
if not sha1sum:
|
| 392 |
+
return ""
|
| 393 |
+
|
| 394 |
+
try:
|
| 395 |
+
with _connect_videos_db() as conn:
|
| 396 |
+
cur = conn.execute(
|
| 397 |
+
"SELECT owner FROM videos WHERE sha1sum = ? LIMIT 1",
|
| 398 |
+
(sha1sum,),
|
| 399 |
+
)
|
| 400 |
+
row = cur.fetchone()
|
| 401 |
+
if not row:
|
| 402 |
+
return ""
|
| 403 |
+
owner = row["owner"] if "owner" in row.keys() else None
|
| 404 |
+
return str(owner or "")
|
| 405 |
+
except sqlite3.OperationalError:
|
| 406 |
+
return ""
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def is_video_input_ok(sha1sum: str) -> bool:
|
| 410 |
+
"""Retorna True si el vídeo té status='input-OK' a videos.db per a aquest sha1sum."""
|
| 411 |
+
|
| 412 |
+
if not sha1sum:
|
| 413 |
+
return False
|
| 414 |
+
|
| 415 |
+
try:
|
| 416 |
+
with _connect_videos_db() as conn:
|
| 417 |
+
cur = conn.execute(
|
| 418 |
+
"SELECT status FROM videos WHERE sha1sum = ? LIMIT 1",
|
| 419 |
+
(sha1sum,),
|
| 420 |
+
)
|
| 421 |
+
row = cur.fetchone()
|
| 422 |
+
if not row:
|
| 423 |
+
return False
|
| 424 |
+
status = row["status"] if "status" in row.keys() else None
|
| 425 |
+
return str(status or "").strip().lower() == "input-ok"
|
| 426 |
+
except sqlite3.OperationalError:
|
| 427 |
+
return False
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
def ensure_video_row_for_upload(
|
| 431 |
+
*,
|
| 432 |
+
sha1sum: str,
|
| 433 |
+
video_name: str,
|
| 434 |
+
owner_phone: str,
|
| 435 |
+
status: str = "input-pending",
|
| 436 |
+
visibility: str | None = None,
|
| 437 |
+
) -> None:
|
| 438 |
+
if not sha1sum:
|
| 439 |
+
return
|
| 440 |
+
|
| 441 |
+
try:
|
| 442 |
+
with _connect_videos_db() as conn:
|
| 443 |
+
cur = conn.cursor()
|
| 444 |
+
|
| 445 |
+
try:
|
| 446 |
+
cur.execute("PRAGMA table_info(videos)")
|
| 447 |
+
cols = {row[1] for row in cur.fetchall()}
|
| 448 |
+
except sqlite3.OperationalError:
|
| 449 |
+
return
|
| 450 |
+
|
| 451 |
+
alter_stmts = []
|
| 452 |
+
if "owner" not in cols:
|
| 453 |
+
alter_stmts.append("ALTER TABLE videos ADD COLUMN owner TEXT")
|
| 454 |
+
if "status" not in cols:
|
| 455 |
+
alter_stmts.append("ALTER TABLE videos ADD COLUMN status TEXT")
|
| 456 |
+
if "sha1sum" not in cols:
|
| 457 |
+
alter_stmts.append("ALTER TABLE videos ADD COLUMN sha1sum TEXT")
|
| 458 |
+
if "visibility" not in cols:
|
| 459 |
+
alter_stmts.append("ALTER TABLE videos ADD COLUMN visibility TEXT")
|
| 460 |
+
|
| 461 |
+
for stmt in alter_stmts:
|
| 462 |
+
try:
|
| 463 |
+
cur.execute(stmt)
|
| 464 |
+
except sqlite3.OperationalError:
|
| 465 |
+
continue
|
| 466 |
+
|
| 467 |
+
row = cur.execute(
|
| 468 |
+
"SELECT id FROM videos WHERE sha1sum = ? LIMIT 1",
|
| 469 |
+
(sha1sum,),
|
| 470 |
+
).fetchone()
|
| 471 |
+
if row is not None:
|
| 472 |
+
return
|
| 473 |
+
|
| 474 |
+
vis = visibility or "private"
|
| 475 |
+
cur.execute(
|
| 476 |
+
"INSERT INTO videos (video_name, owner, visibility, sha1sum, status) "
|
| 477 |
+
"VALUES (?,?,?,?,?)",
|
| 478 |
+
(video_name or sha1sum, owner_phone or "", vis, sha1sum, status),
|
| 479 |
+
)
|
| 480 |
+
except sqlite3.OperationalError:
|
| 481 |
+
return
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
def update_video_status(sha1sum: str, status: str) -> None:
|
| 485 |
+
"""Actualitza el camp status d'un vídeo existent a videos.db per sha1sum.
|
| 486 |
+
|
| 487 |
+
Si la taula o el registre no existeixen, no fa res.
|
| 488 |
+
"""
|
| 489 |
+
|
| 490 |
+
if not sha1sum:
|
| 491 |
+
return
|
| 492 |
+
|
| 493 |
+
try:
|
| 494 |
+
with _connect_videos_db() as conn:
|
| 495 |
+
cur = conn.cursor()
|
| 496 |
+
|
| 497 |
+
try:
|
| 498 |
+
cur.execute("PRAGMA table_info(videos)")
|
| 499 |
+
cols = {row[1] for row in cur.fetchall()}
|
| 500 |
+
except sqlite3.OperationalError:
|
| 501 |
+
return
|
| 502 |
+
|
| 503 |
+
# Assegurar columnes bàsiques
|
| 504 |
+
alter_stmts: list[str] = []
|
| 505 |
+
if "status" not in cols:
|
| 506 |
+
alter_stmts.append("ALTER TABLE videos ADD COLUMN status TEXT")
|
| 507 |
+
if "sha1sum" not in cols:
|
| 508 |
+
alter_stmts.append("ALTER TABLE videos ADD COLUMN sha1sum TEXT")
|
| 509 |
+
|
| 510 |
+
for stmt in alter_stmts:
|
| 511 |
+
try:
|
| 512 |
+
cur.execute(stmt)
|
| 513 |
+
except sqlite3.OperationalError:
|
| 514 |
+
continue
|
| 515 |
+
|
| 516 |
+
cur.execute(
|
| 517 |
+
"UPDATE videos SET status = ? WHERE sha1sum = ?",
|
| 518 |
+
(status, sha1sum),
|
| 519 |
+
)
|
| 520 |
+
except sqlite3.OperationalError:
|
| 521 |
+
return
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
def get_videos_by_status(status: str) -> List[Dict[str, Any]]:
|
| 525 |
+
"""Retorna llista de vídeos a videos.db amb un status concret.
|
| 526 |
+
|
| 527 |
+
Cada element és un dict amb com a mínim: sha1sum, video_name.
|
| 528 |
+
Si la taula o les columnes no existeixen, retorna [].
|
| 529 |
+
"""
|
| 530 |
+
|
| 531 |
+
if not status:
|
| 532 |
+
return []
|
| 533 |
+
|
| 534 |
+
try:
|
| 535 |
+
with _connect_videos_db() as conn:
|
| 536 |
+
cur = conn.cursor()
|
| 537 |
+
try:
|
| 538 |
+
cur.execute("PRAGMA table_info(videos)")
|
| 539 |
+
cols = {row[1] for row in cur.fetchall()}
|
| 540 |
+
except sqlite3.OperationalError:
|
| 541 |
+
return []
|
| 542 |
+
|
| 543 |
+
if "status" not in cols or "sha1sum" not in cols:
|
| 544 |
+
return []
|
| 545 |
+
|
| 546 |
+
# video_name pot no existir en esquemes antics; fem SELECT defensiu
|
| 547 |
+
has_video_name = "video_name" in cols
|
| 548 |
+
select_sql = (
|
| 549 |
+
"SELECT sha1sum, video_name FROM videos WHERE status = ?"
|
| 550 |
+
if has_video_name
|
| 551 |
+
else "SELECT sha1sum, sha1sum AS video_name FROM videos WHERE status = ?"
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
results: List[Dict[str, Any]] = []
|
| 555 |
+
for row in cur.execute(select_sql, (status,)):
|
| 556 |
+
sha1 = str(row[0]) if row[0] is not None else ""
|
| 557 |
+
vname = str(row[1]) if row[1] is not None else sha1
|
| 558 |
+
if not sha1:
|
| 559 |
+
continue
|
| 560 |
+
results.append({"sha1sum": sha1, "video_name": vname})
|
| 561 |
+
return results
|
| 562 |
+
except sqlite3.OperationalError:
|
| 563 |
+
return []
|
| 564 |
+
|
| 565 |
+
|
| 566 |
def _connect_audiodescriptions_db() -> sqlite3.Connection:
|
| 567 |
"""Connexió directa a demo/temp/audiodescriptions.db.
|
| 568 |
|
|
|
|
| 955 |
return conn
|
| 956 |
|
| 957 |
|
| 958 |
+
def _connect_simple_mapping_db(db_path: Path, table_name: str) -> sqlite3.Connection:
|
| 959 |
+
"""Connexió a una BD simple (sha1sum, name, description) a demo/temp/db.
|
| 960 |
+
|
| 961 |
+
Es fa servir per a casting.db i scenarios.db.
|
| 962 |
+
"""
|
| 963 |
+
|
| 964 |
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
| 965 |
+
conn = sqlite3.connect(str(db_path))
|
| 966 |
+
conn.row_factory = sqlite3.Row
|
| 967 |
+
cur = conn.cursor()
|
| 968 |
+
cur.execute(
|
| 969 |
+
f"""
|
| 970 |
+
CREATE TABLE IF NOT EXISTS {table_name} (
|
| 971 |
+
sha1sum TEXT NOT NULL,
|
| 972 |
+
name TEXT NOT NULL,
|
| 973 |
+
description TEXT
|
| 974 |
+
);
|
| 975 |
+
"""
|
| 976 |
+
)
|
| 977 |
+
conn.commit()
|
| 978 |
+
return conn
|
| 979 |
+
|
| 980 |
+
|
| 981 |
+
def _connect_casting_db() -> sqlite3.Connection:
|
| 982 |
+
"""Connexió directa a demo/temp/db/casting.db (taula casting)."""
|
| 983 |
+
|
| 984 |
+
return _connect_simple_mapping_db(CASTING_DB_PATH, "casting")
|
| 985 |
+
|
| 986 |
+
|
| 987 |
+
def _connect_scenarios_db() -> sqlite3.Connection:
|
| 988 |
+
"""Connexió directa a demo/temp/db/scenarios.db (taula scenarios)."""
|
| 989 |
+
|
| 990 |
+
return _connect_simple_mapping_db(SCENARIOS_DB_PATH, "scenarios")
|
| 991 |
+
|
| 992 |
+
|
| 993 |
+
def insert_casting_row(sha1sum: str, name: str, description: str | None = None) -> None:
|
| 994 |
+
"""Insereix un personatge a casting.db per a un sha1sum donat."""
|
| 995 |
+
|
| 996 |
+
if not sha1sum or not name:
|
| 997 |
+
return
|
| 998 |
+
|
| 999 |
+
try:
|
| 1000 |
+
with _connect_casting_db() as conn:
|
| 1001 |
+
conn.execute(
|
| 1002 |
+
"INSERT INTO casting (sha1sum, name, description) VALUES (?,?,?)",
|
| 1003 |
+
(sha1sum, name, description or ""),
|
| 1004 |
+
)
|
| 1005 |
+
except sqlite3.OperationalError:
|
| 1006 |
+
return
|
| 1007 |
+
|
| 1008 |
+
|
| 1009 |
+
def insert_scenario_row(sha1sum: str, name: str, description: str | None = None) -> None:
|
| 1010 |
+
"""Insereix un escenari a scenarios.db per a un sha1sum donat."""
|
| 1011 |
+
|
| 1012 |
+
if not sha1sum or not name:
|
| 1013 |
+
return
|
| 1014 |
+
|
| 1015 |
+
try:
|
| 1016 |
+
with _connect_scenarios_db() as conn:
|
| 1017 |
+
conn.execute(
|
| 1018 |
+
"INSERT INTO scenarios (sha1sum, name, description) VALUES (?,?,?)",
|
| 1019 |
+
(sha1sum, name, description or ""),
|
| 1020 |
+
)
|
| 1021 |
+
except sqlite3.OperationalError:
|
| 1022 |
+
return
|
| 1023 |
+
|
| 1024 |
+
|
| 1025 |
+
def log_action(
|
| 1026 |
*,
|
| 1027 |
session: str,
|
|
|
|
| 1028 |
user: str,
|
|
|
|
| 1029 |
phone: str,
|
| 1030 |
action: str,
|
| 1031 |
sha1sum: str,
|
|
|
|
| 1032 |
timestamp: Optional[str] = None,
|
| 1033 |
) -> None:
|
| 1034 |
+
"""Insereix un registre a demo/temp/actions.db (taula actions)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1035 |
|
| 1036 |
ts = timestamp or datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
| 1037 |
|
| 1038 |
+
insert_action(
|
| 1039 |
+
session=session or "",
|
| 1040 |
+
user=user or "",
|
| 1041 |
+
phone=phone or "",
|
| 1042 |
+
action=action,
|
| 1043 |
+
sha1sum=sha1sum or "",
|
| 1044 |
+
timestamp=ts,
|
| 1045 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1046 |
|
| 1047 |
|
| 1048 |
+
def has_video_approval_action(sha1sum: str) -> bool:
|
| 1049 |
+
"""Comprova si existeix una acció d'acceptació d'input per a un sha1sum.
|
| 1050 |
|
| 1051 |
+
Busca a demo/temp/actions.db una fila amb action='input-OK' i el sha1sum
|
| 1052 |
especificat.
|
| 1053 |
"""
|
| 1054 |
|
|
|
|
| 1056 |
return False
|
| 1057 |
|
| 1058 |
try:
|
| 1059 |
+
with _connect_actions_db() as conn:
|
| 1060 |
cur = conn.execute(
|
| 1061 |
+
"SELECT 1 FROM actions WHERE action = ? AND sha1sum = ? LIMIT 1",
|
| 1062 |
+
("input-OK", sha1sum),
|
| 1063 |
)
|
| 1064 |
return cur.fetchone() is not None
|
| 1065 |
except sqlite3.OperationalError:
|
page_modules/__pycache__/new_video_processing.cpython-313.pyc
ADDED
|
Binary file (99.4 kB). View file
|
|
|
page_modules/analyze_audiodescriptions.py
CHANGED
|
@@ -16,11 +16,16 @@ from utils import save_bytes
|
|
| 16 |
from persistent_data_gate import ensure_media_for_video
|
| 17 |
from databases import (
|
| 18 |
get_videos_from_audiodescriptions,
|
| 19 |
-
insert_demo_feedback_row,
|
| 20 |
get_audiodescription,
|
| 21 |
get_audiodescription_history,
|
| 22 |
update_audiodescription_text,
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
)
|
| 25 |
|
| 26 |
|
|
@@ -67,6 +72,29 @@ def _find_best_file_for_version(vid_dir: Path, version: str, filename: str) -> O
|
|
| 67 |
return None
|
| 68 |
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
def load_eval_values(vid_dir: Path, version: str, eval_content: Optional[str] = None) -> Optional[Dict[str, int]]:
|
| 71 |
"""Carga los valores de evaluación desde eval (DB o CSV) si existe.
|
| 72 |
|
|
@@ -229,38 +257,16 @@ def render_analyze_audiodescriptions_page(api, permissions: Dict[str, bool]) ->
|
|
| 229 |
elif "eval_values" in st.session_state:
|
| 230 |
del st.session_state["eval_values"]
|
| 231 |
|
| 232 |
-
video_ad_path = (
|
| 233 |
-
_find_best_file_for_version(vid_dir, subcarpeta_seleccio, "une_ad.mp4")
|
| 234 |
-
if subcarpeta_seleccio
|
| 235 |
-
else None
|
| 236 |
-
)
|
| 237 |
-
is_ad_video_available = video_ad_path is not None and video_ad_path.exists()
|
| 238 |
-
|
| 239 |
-
add_ad_video = st.checkbox(
|
| 240 |
-
"Afegir audiodescripció",
|
| 241 |
-
disabled=not is_ad_video_available,
|
| 242 |
-
key="add_ad_checkbox",
|
| 243 |
-
)
|
| 244 |
-
|
| 245 |
-
video_to_show = None
|
| 246 |
-
if add_ad_video and is_ad_video_available:
|
| 247 |
-
video_to_show = video_ad_path
|
| 248 |
-
elif mp4s:
|
| 249 |
-
video_to_show = mp4s[0]
|
| 250 |
-
|
| 251 |
-
if video_to_show:
|
| 252 |
-
st.video(str(video_to_show))
|
| 253 |
-
else:
|
| 254 |
-
st.warning("No s'ha trobat cap fitxer **.mp4** a la carpeta seleccionada.")
|
| 255 |
-
|
| 256 |
st.markdown("---")
|
| 257 |
st.markdown("#### Accions")
|
| 258 |
-
c1, c2 = st.columns(
|
| 259 |
with c1:
|
| 260 |
if st.button("Reconstruir àudio amb narració lliure", use_container_width=True, key="rebuild_free_ad"):
|
| 261 |
# Genera nuevo MP3 desde el texto editado en el editor
|
| 262 |
if subcarpeta_seleccio:
|
| 263 |
-
|
|
|
|
|
|
|
| 264 |
if free_ad_txt_path is not None and free_ad_txt_path.exists():
|
| 265 |
with st.spinner("Generant àudio de la narració lliure..."):
|
| 266 |
# Leer el texto actual del archivo (puede haber sido editado)
|
|
@@ -273,44 +279,45 @@ def render_analyze_audiodescriptions_page(api, permissions: Dict[str, bool]) ->
|
|
| 273 |
response = api.tts_matxa(text=text_content, voice=voice)
|
| 274 |
|
| 275 |
if "mp3_bytes" in response:
|
| 276 |
-
subtype_for_files = "HITL
|
| 277 |
output_path = vid_dir / subcarpeta_seleccio / subtype_for_files / "free_ad.mp3"
|
| 278 |
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 279 |
mp3_bytes = response["mp3_bytes"]
|
| 280 |
save_bytes(output_path, mp3_bytes)
|
| 281 |
|
| 282 |
-
#
|
| 283 |
try:
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
ip = st.session_state.get("client_ip", "")
|
| 292 |
-
username = user_obj.get("username") if isinstance(user_obj, dict) else str(user_obj or "")
|
| 293 |
-
password = st.session_state.get("last_password", "")
|
| 294 |
-
phone = (
|
| 295 |
-
st.session_state.get("sms_phone_verified")
|
| 296 |
-
or st.session_state.get("sms_phone")
|
| 297 |
-
or ""
|
| 298 |
-
)
|
| 299 |
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
except Exception:
|
| 311 |
-
# No interrompre la UX si falla el logging
|
| 312 |
pass
|
| 313 |
|
|
|
|
|
|
|
|
|
|
| 314 |
st.success(f"✅ Àudio generat i desat a: {output_path}")
|
| 315 |
st.rerun()
|
| 316 |
else:
|
|
@@ -322,7 +329,9 @@ def render_analyze_audiodescriptions_page(api, permissions: Dict[str, bool]) ->
|
|
| 322 |
if st.button("Reconstruir vídeo amb audiodescripció", use_container_width=True, key="rebuild_video_ad"):
|
| 323 |
# Genera video con AD usando el SRT y el video original
|
| 324 |
if subcarpeta_seleccio and mp4s:
|
| 325 |
-
|
|
|
|
|
|
|
| 326 |
video_original_path = mp4s[0] # El único MP4 en videos/<video-seleccionado>
|
| 327 |
|
| 328 |
if une_srt_path is not None and une_srt_path.exists():
|
|
@@ -339,44 +348,52 @@ def render_analyze_audiodescriptions_page(api, permissions: Dict[str, bool]) ->
|
|
| 339 |
)
|
| 340 |
|
| 341 |
if "video_bytes" in response:
|
| 342 |
-
subtype_for_files = "HITL
|
| 343 |
output_path = vid_dir / subcarpeta_seleccio / subtype_for_files / "une_ad.mp4"
|
| 344 |
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 345 |
video_bytes = response["video_bytes"]
|
| 346 |
save_bytes(output_path, video_bytes)
|
| 347 |
|
| 348 |
-
#
|
| 349 |
try:
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
st.session_state.get("sms_phone_verified")
|
| 362 |
-
or st.session_state.get("sms_phone")
|
| 363 |
-
or ""
|
| 364 |
)
|
|
|
|
|
|
|
| 365 |
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
except Exception:
|
| 377 |
-
# No interrompre la UX si falla el logging
|
| 378 |
pass
|
| 379 |
|
|
|
|
|
|
|
|
|
|
| 380 |
st.success(f"✅ Vídeo amb AD generat i desat a: {output_path}")
|
| 381 |
st.info(
|
| 382 |
"Pots visualitzar-lo activant la casella 'Afegir audiodescripció' i seleccionant el nou fitxer si cal."
|
|
@@ -387,6 +404,51 @@ def render_analyze_audiodescriptions_page(api, permissions: Dict[str, bool]) ->
|
|
| 387 |
else:
|
| 388 |
st.warning("⚠️ No s'ha trobat el fitxer 'une_ad.srt' en aquesta versió.")
|
| 389 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
with col_txt:
|
| 391 |
# Selector de versió temporal de l'audiodescripció (històric)
|
| 392 |
hist_options = ["Original", "HITL OK", "HITL Test"]
|
|
@@ -401,6 +463,30 @@ def render_analyze_audiodescriptions_page(api, permissions: Dict[str, bool]) ->
|
|
| 401 |
horizontal=True,
|
| 402 |
)
|
| 403 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
tipus_ad_options = ["narració lliure", "UNE-153010"]
|
| 405 |
tipus_ad_seleccio = st.selectbox("Fitxer d'audiodescripció a editar:", tipus_ad_options)
|
| 406 |
|
|
@@ -457,9 +543,9 @@ def render_analyze_audiodescriptions_page(api, permissions: Dict[str, bool]) ->
|
|
| 457 |
disabled=not subcarpeta_seleccio,
|
| 458 |
key="play_button_editor",
|
| 459 |
):
|
| 460 |
-
#
|
| 461 |
if subcarpeta_seleccio:
|
| 462 |
-
mp3_path =
|
| 463 |
if mp3_path.exists():
|
| 464 |
try:
|
| 465 |
print(f"🎵 Reproduciendo MP3: {mp3_path}")
|
|
@@ -673,15 +759,12 @@ def render_analyze_audiodescriptions_page(api, permissions: Dict[str, bool]) ->
|
|
| 673 |
or ""
|
| 674 |
)
|
| 675 |
|
| 676 |
-
|
| 677 |
session=session_id or "",
|
| 678 |
-
ip=ip or "",
|
| 679 |
user=(user_obj.get("username") if isinstance(user_obj, dict) else str(user_obj or "")),
|
| 680 |
-
|
| 681 |
-
phone=phone or "",
|
| 682 |
action="Feedback for AD",
|
| 683 |
sha1sum=feedback_hash,
|
| 684 |
-
visibility=None,
|
| 685 |
)
|
| 686 |
except Exception:
|
| 687 |
# No interrompre la UX si falla el logging de feedback
|
|
|
|
| 16 |
from persistent_data_gate import ensure_media_for_video
|
| 17 |
from databases import (
|
| 18 |
get_videos_from_audiodescriptions,
|
|
|
|
| 19 |
get_audiodescription,
|
| 20 |
get_audiodescription_history,
|
| 21 |
update_audiodescription_text,
|
| 22 |
+
update_audiodescription_info_ad,
|
| 23 |
+
insert_demo_feedback_row,
|
| 24 |
+
get_feedback_score_labels,
|
| 25 |
+
log_action,
|
| 26 |
+
insert_action,
|
| 27 |
+
get_latest_user_phone_for_session,
|
| 28 |
+
get_video_owner_by_sha1,
|
| 29 |
)
|
| 30 |
|
| 31 |
|
|
|
|
| 72 |
return None
|
| 73 |
|
| 74 |
|
| 75 |
+
def _file_for_hist_choice(vid_dir: Path, version: str, filename: str, hist_choice: str) -> Optional[Path]:
|
| 76 |
+
"""Retorna el fitxer per al subtype seleccionat (Original/HITL OK/HITL Test).
|
| 77 |
+
|
| 78 |
+
Si no existeix al subtype triat, fa servir el comportament per defecte de
|
| 79 |
+
_find_best_file_for_version.
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
# Map hist_choice -> subcarpeta física
|
| 83 |
+
subtype_map = {
|
| 84 |
+
"Original": "Original",
|
| 85 |
+
"HITL OK": "HITL OK",
|
| 86 |
+
"HITL Test": "HITL Test",
|
| 87 |
+
}
|
| 88 |
+
subtype = subtype_map.get(hist_choice)
|
| 89 |
+
|
| 90 |
+
if subtype:
|
| 91 |
+
candidate = vid_dir / version / subtype / filename
|
| 92 |
+
if candidate.exists():
|
| 93 |
+
return candidate
|
| 94 |
+
|
| 95 |
+
return _find_best_file_for_version(vid_dir, version, filename)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
def load_eval_values(vid_dir: Path, version: str, eval_content: Optional[str] = None) -> Optional[Dict[str, int]]:
|
| 99 |
"""Carga los valores de evaluación desde eval (DB o CSV) si existe.
|
| 100 |
|
|
|
|
| 257 |
elif "eval_values" in st.session_state:
|
| 258 |
del st.session_state["eval_values"]
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
st.markdown("---")
|
| 261 |
st.markdown("#### Accions")
|
| 262 |
+
c1, c2, c3 = st.columns(3)
|
| 263 |
with c1:
|
| 264 |
if st.button("Reconstruir àudio amb narració lliure", use_container_width=True, key="rebuild_free_ad"):
|
| 265 |
# Genera nuevo MP3 desde el texto editado en el editor
|
| 266 |
if subcarpeta_seleccio:
|
| 267 |
+
# Fer servir sempre el subtype triat a l'optionbox d'historial
|
| 268 |
+
hist_choice = st.session_state.get("ad_hist_choice_" + hist_key_suffix, "HITL OK")
|
| 269 |
+
free_ad_txt_path = _file_for_hist_choice(vid_dir, subcarpeta_seleccio, "free_ad.txt", hist_choice)
|
| 270 |
if free_ad_txt_path is not None and free_ad_txt_path.exists():
|
| 271 |
with st.spinner("Generant àudio de la narració lliure..."):
|
| 272 |
# Leer el texto actual del archivo (puede haber sido editado)
|
|
|
|
| 279 |
response = api.tts_matxa(text=text_content, voice=voice)
|
| 280 |
|
| 281 |
if "mp3_bytes" in response:
|
| 282 |
+
subtype_for_files = "HITL Test"
|
| 283 |
output_path = vid_dir / subcarpeta_seleccio / subtype_for_files / "free_ad.mp3"
|
| 284 |
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 285 |
mp3_bytes = response["mp3_bytes"]
|
| 286 |
save_bytes(output_path, mp3_bytes)
|
| 287 |
|
| 288 |
+
# Actualitzar test_free_ad a audiodescriptions.db amb el text utilitzat
|
| 289 |
try:
|
| 290 |
+
update_audiodescription_text(
|
| 291 |
+
selected_sha1,
|
| 292 |
+
subcarpeta_seleccio,
|
| 293 |
+
test_free_ad=text_content,
|
| 294 |
+
)
|
| 295 |
+
except Exception:
|
| 296 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
|
| 298 |
+
# Registrar acció "New correction" a actions.db
|
| 299 |
+
try:
|
| 300 |
+
session_id = st.session_state.get("session_id", "")
|
| 301 |
+
user_obj = st.session_state.get("user") or {}
|
| 302 |
+
username = user_obj.get("username") if isinstance(user_obj, dict) else str(user_obj or "")
|
| 303 |
+
phone = (
|
| 304 |
+
st.session_state.get("sms_phone_verified")
|
| 305 |
+
or st.session_state.get("sms_phone")
|
| 306 |
+
or ""
|
| 307 |
+
)
|
| 308 |
+
insert_action(
|
| 309 |
+
session=session_id or "",
|
| 310 |
+
user=username or "",
|
| 311 |
+
phone=phone,
|
| 312 |
+
action="New correction",
|
| 313 |
+
sha1sum=selected_sha1,
|
| 314 |
+
)
|
| 315 |
except Exception:
|
|
|
|
| 316 |
pass
|
| 317 |
|
| 318 |
+
# Posar l'optionbox d'historial automàticament a "HITL Test"
|
| 319 |
+
st.session_state["ad_hist_choice_" + hist_key_suffix] = "HITL Test"
|
| 320 |
+
|
| 321 |
st.success(f"✅ Àudio generat i desat a: {output_path}")
|
| 322 |
st.rerun()
|
| 323 |
else:
|
|
|
|
| 329 |
if st.button("Reconstruir vídeo amb audiodescripció", use_container_width=True, key="rebuild_video_ad"):
|
| 330 |
# Genera video con AD usando el SRT y el video original
|
| 331 |
if subcarpeta_seleccio and mp4s:
|
| 332 |
+
# Fer servir sempre el subtype triat a l'optionbox d'historial
|
| 333 |
+
hist_choice = st.session_state.get("ad_hist_choice_" + hist_key_suffix, "HITL OK")
|
| 334 |
+
une_srt_path = _file_for_hist_choice(vid_dir, subcarpeta_seleccio, "une_ad.srt", hist_choice)
|
| 335 |
video_original_path = mp4s[0] # El único MP4 en videos/<video-seleccionado>
|
| 336 |
|
| 337 |
if une_srt_path is not None and une_srt_path.exists():
|
|
|
|
| 348 |
)
|
| 349 |
|
| 350 |
if "video_bytes" in response:
|
| 351 |
+
subtype_for_files = "HITL Test"
|
| 352 |
output_path = vid_dir / subcarpeta_seleccio / subtype_for_files / "une_ad.mp4"
|
| 353 |
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 354 |
video_bytes = response["video_bytes"]
|
| 355 |
save_bytes(output_path, video_bytes)
|
| 356 |
|
| 357 |
+
# Actualitzar test_une_ad a audiodescriptions.db amb el contingut del SRT
|
| 358 |
try:
|
| 359 |
+
une_text_for_db = ""
|
| 360 |
+
try:
|
| 361 |
+
une_text_for_db = une_srt_path.read_text(encoding="utf-8") if une_srt_path is not None else ""
|
| 362 |
+
except Exception:
|
| 363 |
+
if une_srt_path is not None:
|
| 364 |
+
une_text_for_db = une_srt_path.read_text(errors="ignore")
|
| 365 |
+
if une_text_for_db:
|
| 366 |
+
update_audiodescription_text(
|
| 367 |
+
selected_sha1,
|
| 368 |
+
subcarpeta_seleccio,
|
| 369 |
+
test_une_ad=une_text_for_db,
|
|
|
|
|
|
|
|
|
|
| 370 |
)
|
| 371 |
+
except Exception:
|
| 372 |
+
pass
|
| 373 |
|
| 374 |
+
# Registrar acció "New correction" a actions.db
|
| 375 |
+
try:
|
| 376 |
+
session_id = st.session_state.get("session_id", "")
|
| 377 |
+
user_obj = st.session_state.get("user") or {}
|
| 378 |
+
username = user_obj.get("username") if isinstance(user_obj, dict) else str(user_obj or "")
|
| 379 |
+
phone = (
|
| 380 |
+
st.session_state.get("sms_phone_verified")
|
| 381 |
+
or st.session_state.get("sms_phone")
|
| 382 |
+
or ""
|
| 383 |
+
)
|
| 384 |
+
insert_action(
|
| 385 |
+
session=session_id or "",
|
| 386 |
+
user=username or "",
|
| 387 |
+
phone=phone,
|
| 388 |
+
action="New correction",
|
| 389 |
+
sha1sum=selected_sha1,
|
| 390 |
+
)
|
| 391 |
except Exception:
|
|
|
|
| 392 |
pass
|
| 393 |
|
| 394 |
+
# Posar l'optionbox d'historial automàticament a "HITL Test"
|
| 395 |
+
st.session_state["ad_hist_choice_" + hist_key_suffix] = "HITL Test"
|
| 396 |
+
|
| 397 |
st.success(f"✅ Vídeo amb AD generat i desat a: {output_path}")
|
| 398 |
st.info(
|
| 399 |
"Pots visualitzar-lo activant la casella 'Afegir audiodescripció' i seleccionant el nou fitxer si cal."
|
|
|
|
| 404 |
else:
|
| 405 |
st.warning("⚠️ No s'ha trobat el fitxer 'une_ad.srt' en aquesta versió.")
|
| 406 |
|
| 407 |
+
# Botó per revocar permisos d'ús del vídeo actual
|
| 408 |
+
with c3:
|
| 409 |
+
if st.button("Revocar permisos d'ús del vídeo", use_container_width=True, key="revoke_video_permits"):
|
| 410 |
+
session_id = st.session_state.get("session_id", "")
|
| 411 |
+
if not session_id:
|
| 412 |
+
st.error("No s'ha pogut determinar la sessió actual.")
|
| 413 |
+
else:
|
| 414 |
+
try:
|
| 415 |
+
# Telèfon associat al login actual (actions.db)
|
| 416 |
+
user_for_session, phone_for_session = get_latest_user_phone_for_session(session_id)
|
| 417 |
+
except Exception:
|
| 418 |
+
user_for_session, phone_for_session = "", ""
|
| 419 |
+
|
| 420 |
+
try:
|
| 421 |
+
owner_phone = get_video_owner_by_sha1(selected_sha1)
|
| 422 |
+
except Exception:
|
| 423 |
+
owner_phone = ""
|
| 424 |
+
|
| 425 |
+
if phone_for_session and owner_phone and str(phone_for_session) == str(owner_phone):
|
| 426 |
+
# Registrar acció de revocació i informar a l'usuari
|
| 427 |
+
try:
|
| 428 |
+
username = (
|
| 429 |
+
st.session_state.get("user", {}).get("username")
|
| 430 |
+
if isinstance(st.session_state.get("user"), dict)
|
| 431 |
+
else str(st.session_state.get("user", ""))
|
| 432 |
+
)
|
| 433 |
+
insert_action(
|
| 434 |
+
session=session_id,
|
| 435 |
+
user=username or "",
|
| 436 |
+
phone=str(phone_for_session),
|
| 437 |
+
action="Revocation of permits",
|
| 438 |
+
sha1sum=selected_sha1,
|
| 439 |
+
)
|
| 440 |
+
except Exception:
|
| 441 |
+
pass
|
| 442 |
+
|
| 443 |
+
st.success(
|
| 444 |
+
"Els permisos per utilitzar el vídeo han estat revocats. "
|
| 445 |
+
"Has de desar els canvis i en breu rebràs un SMS de confirmació."
|
| 446 |
+
)
|
| 447 |
+
else:
|
| 448 |
+
st.warning(
|
| 449 |
+
"No es poden revocar els permisos: el teu telèfon no coincideix amb el propietari del vídeo."
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
with col_txt:
|
| 453 |
# Selector de versió temporal de l'audiodescripció (històric)
|
| 454 |
hist_options = ["Original", "HITL OK", "HITL Test"]
|
|
|
|
| 463 |
horizontal=True,
|
| 464 |
)
|
| 465 |
|
| 466 |
+
# Seleccionar el vídeo amb AD segons el subtype triat (si existeix)
|
| 467 |
+
video_ad_path = None
|
| 468 |
+
if subcarpeta_seleccio:
|
| 469 |
+
video_ad_path = _file_for_hist_choice(vid_dir, subcarpeta_seleccio, "une_ad.mp4", hist_choice)
|
| 470 |
+
|
| 471 |
+
is_ad_video_available = video_ad_path is not None and video_ad_path.exists()
|
| 472 |
+
|
| 473 |
+
add_ad_video = st.checkbox(
|
| 474 |
+
"Afegir audiodescripció",
|
| 475 |
+
disabled=not is_ad_video_available,
|
| 476 |
+
key="add_ad_checkbox",
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
video_to_show = None
|
| 480 |
+
if add_ad_video and is_ad_video_available:
|
| 481 |
+
video_to_show = video_ad_path
|
| 482 |
+
elif mp4s:
|
| 483 |
+
video_to_show = mp4s[0]
|
| 484 |
+
|
| 485 |
+
if video_to_show:
|
| 486 |
+
st.video(str(video_to_show))
|
| 487 |
+
else:
|
| 488 |
+
st.warning("No s'ha trobat cap fitxer **.mp4** a la carpeta seleccionada.")
|
| 489 |
+
|
| 490 |
tipus_ad_options = ["narració lliure", "UNE-153010"]
|
| 491 |
tipus_ad_seleccio = st.selectbox("Fitxer d'audiodescripció a editar:", tipus_ad_options)
|
| 492 |
|
|
|
|
| 543 |
disabled=not subcarpeta_seleccio,
|
| 544 |
key="play_button_editor",
|
| 545 |
):
|
| 546 |
+
# Reproduir el MP3 existent segons el subtype d'historial triat
|
| 547 |
if subcarpeta_seleccio:
|
| 548 |
+
mp3_path = _file_for_hist_choice(vid_dir, subcarpeta_seleccio, "free_ad.mp3", hist_choice)
|
| 549 |
if mp3_path.exists():
|
| 550 |
try:
|
| 551 |
print(f"🎵 Reproduciendo MP3: {mp3_path}")
|
|
|
|
| 759 |
or ""
|
| 760 |
)
|
| 761 |
|
| 762 |
+
log_action(
|
| 763 |
session=session_id or "",
|
|
|
|
| 764 |
user=(user_obj.get("username") if isinstance(user_obj, dict) else str(user_obj or "")),
|
| 765 |
+
phone=phone,
|
|
|
|
| 766 |
action="Feedback for AD",
|
| 767 |
sha1sum=feedback_hash,
|
|
|
|
| 768 |
)
|
| 769 |
except Exception:
|
| 770 |
# No interrompre la UX si falla el logging de feedback
|
page_modules/new_video_processing.py
CHANGED
|
@@ -15,12 +15,25 @@ from datetime import datetime
|
|
| 15 |
import yaml
|
| 16 |
import sqlite3
|
| 17 |
import json
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
import streamlit as st
|
| 20 |
from PIL import Image, ImageDraw
|
| 21 |
-
from databases import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
from compliance_client import compliance_client
|
| 23 |
-
from persistent_data_gate import ensure_temp_databases, _load_data_origin
|
| 24 |
|
| 25 |
|
| 26 |
def get_all_catalan_names():
|
|
@@ -148,6 +161,8 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 148 |
manual_validation_enabled = True
|
| 149 |
max_size_mb = 20
|
| 150 |
max_duration_s = 30
|
|
|
|
|
|
|
| 151 |
try:
|
| 152 |
if config_path.exists():
|
| 153 |
with config_path.open("r", encoding="utf-8") as f:
|
|
@@ -159,6 +174,10 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 159 |
# Límits configurables de mida i durada
|
| 160 |
max_size_mb = int(media_cfg.get("max_size_mb", max_size_mb))
|
| 161 |
max_duration_s = int(media_cfg.get("max_duration_s", max_duration_s))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
except Exception:
|
| 163 |
manual_validation_enabled = True
|
| 164 |
|
|
@@ -373,7 +392,22 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 373 |
}
|
| 374 |
)
|
| 375 |
|
| 376 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
try:
|
| 378 |
session_id = st.session_state.get("session_id", "")
|
| 379 |
ip = st.session_state.get("client_ip", "")
|
|
@@ -390,31 +424,57 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 390 |
)
|
| 391 |
vis_choice = st.session_state.get("video_visibility", "Privat")
|
| 392 |
vis_flag = "public" if vis_choice.strip().lower().startswith("púb") else "private"
|
| 393 |
-
|
|
|
|
|
|
|
| 394 |
session=session_id,
|
| 395 |
-
ip=ip,
|
| 396 |
user=username or "",
|
| 397 |
-
password=password or "",
|
| 398 |
phone=phone,
|
| 399 |
action="upload",
|
| 400 |
sha1sum=sha1,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
visibility=vis_flag,
|
| 402 |
)
|
| 403 |
except Exception as e:
|
| 404 |
-
print(f"[events] Error registrant
|
| 405 |
|
| 406 |
-
#
|
|
|
|
| 407 |
try:
|
| 408 |
base_dir = Path(__file__).parent.parent
|
| 409 |
data_origin = _load_data_origin(base_dir)
|
| 410 |
-
if data_origin == "external":
|
| 411 |
-
pending_root = base_dir / "temp" / "pending_videos" / sha1
|
| 412 |
-
pending_root.mkdir(parents=True, exist_ok=True)
|
| 413 |
-
local_pending_path = pending_root / "video.mp4"
|
| 414 |
-
# Guardar còpia local del vídeo pendent
|
| 415 |
-
with local_pending_path.open("wb") as f_pending:
|
| 416 |
-
f_pending.write(video_bytes)
|
| 417 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
# Enviar el vídeo al backend engine perquè aparegui a la llista de pendents
|
| 419 |
try:
|
| 420 |
resp_pending = api.upload_pending_video(video_bytes, uploaded_file.name)
|
|
@@ -425,18 +485,21 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 425 |
_log(f"[pending_videos] Error bloc exterior upload_pending_video: {e_ext}")
|
| 426 |
|
| 427 |
# Marcar estat de validació segons la configuració de seguretat
|
| 428 |
-
if manual_validation_enabled:
|
| 429 |
st.session_state.video_requires_validation = True
|
| 430 |
st.session_state.video_validation_approved = False
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
| 438 |
else:
|
| 439 |
-
# Sense validació manual: es considera validat automàticament
|
| 440 |
st.session_state.video_requires_validation = False
|
| 441 |
st.session_state.video_validation_approved = True
|
| 442 |
|
|
@@ -457,7 +520,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 457 |
if st.session_state.get("video_uploaded"):
|
| 458 |
current_sha1 = st.session_state.video_uploaded.get("sha1sum")
|
| 459 |
if current_sha1 and st.session_state.get("video_requires_validation") and not st.session_state.get("video_validation_approved"):
|
| 460 |
-
if
|
| 461 |
st.session_state.video_validation_approved = True
|
| 462 |
|
| 463 |
# Només podem continuar amb el càsting si el vídeo no requereix validació
|
|
@@ -637,7 +700,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 637 |
pass
|
| 638 |
|
| 639 |
if current_sha1:
|
| 640 |
-
if
|
| 641 |
st.session_state.video_validation_approved = True
|
| 642 |
st.success("✅ Vídeo validat. Pots continuar amb el càsting.")
|
| 643 |
else:
|
|
@@ -1748,11 +1811,16 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 1748 |
if any_success and refined_any and sha1:
|
| 1749 |
sms_channels_enabled = bool(twilio_enabled_cfg or zapier_enabled_cfg)
|
| 1750 |
if sms_channels_enabled and une_validator_sms_enabled and une_phone_validator:
|
| 1751 |
-
|
| 1752 |
-
|
| 1753 |
-
|
| 1754 |
-
|
| 1755 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1756 |
# Registrar estat d'espera de validació UNE a events.db
|
| 1757 |
try:
|
| 1758 |
log_event(
|
|
@@ -1770,9 +1838,120 @@ def render_process_video_page(api, backend_base_url: str) -> None:
|
|
| 1770 |
except Exception as e_sms:
|
| 1771 |
_log(f"[UNE SMS] Error en flux d'SMS/espera validació: {e_sms}")
|
| 1772 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1773 |
if any_success:
|
| 1774 |
-
progress_placeholder.success("✅ Audiodescripció generada i desada
|
| 1775 |
-
result_placeholder.
|
| 1776 |
else:
|
| 1777 |
progress_placeholder.empty()
|
| 1778 |
result_placeholder.error("❌ No s'ha pogut generar cap versió d'audiodescripció.")
|
|
|
|
| 15 |
import yaml
|
| 16 |
import sqlite3
|
| 17 |
import json
|
| 18 |
+
import zipfile
|
| 19 |
+
import io
|
| 20 |
+
import requests
|
| 21 |
|
| 22 |
import streamlit as st
|
| 23 |
from PIL import Image, ImageDraw
|
| 24 |
+
from databases import (
|
| 25 |
+
log_action,
|
| 26 |
+
has_video_approval_action,
|
| 27 |
+
upsert_audiodescription_text,
|
| 28 |
+
get_latest_user_phone_for_session,
|
| 29 |
+
insert_action,
|
| 30 |
+
ensure_video_row_for_upload,
|
| 31 |
+
is_video_input_ok,
|
| 32 |
+
update_video_status,
|
| 33 |
+
get_audiodescription,
|
| 34 |
+
)
|
| 35 |
from compliance_client import compliance_client
|
| 36 |
+
from persistent_data_gate import ensure_temp_databases, _load_data_origin, ensure_media_for_video
|
| 37 |
|
| 38 |
|
| 39 |
def get_all_catalan_names():
|
|
|
|
| 161 |
manual_validation_enabled = True
|
| 162 |
max_size_mb = 20
|
| 163 |
max_duration_s = 30
|
| 164 |
+
video_validator_sms_enabled = False
|
| 165 |
+
skip_manual_validation_for_this_video = False
|
| 166 |
try:
|
| 167 |
if config_path.exists():
|
| 168 |
with config_path.open("r", encoding="utf-8") as f:
|
|
|
|
| 174 |
# Límits configurables de mida i durada
|
| 175 |
max_size_mb = int(media_cfg.get("max_size_mb", max_size_mb))
|
| 176 |
max_duration_s = int(media_cfg.get("max_duration_s", max_duration_s))
|
| 177 |
+
|
| 178 |
+
# Flags de validació / SMS de validador de vídeo
|
| 179 |
+
validation_cfg = cfg.get("validation", {}) or {}
|
| 180 |
+
video_validator_sms_enabled = bool(validation_cfg.get("video_validator_sms_enabled", False))
|
| 181 |
except Exception:
|
| 182 |
manual_validation_enabled = True
|
| 183 |
|
|
|
|
| 392 |
}
|
| 393 |
)
|
| 394 |
|
| 395 |
+
# Si el vídeo ja està marcat com input-OK a videos.db, saltar validació
|
| 396 |
+
try:
|
| 397 |
+
if is_video_input_ok(sha1):
|
| 398 |
+
skip_manual_validation_for_this_video = True
|
| 399 |
+
|
| 400 |
+
# Assegurar que disposem de temp/media/<sha1>/video.mp4
|
| 401 |
+
base_dir = Path(__file__).parent.parent
|
| 402 |
+
api_client = st.session_state.get("api_client")
|
| 403 |
+
try:
|
| 404 |
+
ensure_media_for_video(base_dir, api_client, sha1)
|
| 405 |
+
except Exception as e_media:
|
| 406 |
+
_log(f"[MEDIA] Error assegurant media per a {sha1}: {e_media}")
|
| 407 |
+
except Exception as e_chk:
|
| 408 |
+
_log(f"[VIDEOS] Error comprovant status input-OK per a {sha1}: {e_chk}")
|
| 409 |
+
|
| 410 |
+
# Registre d'esdeveniment de pujada de vídeo a events.db i accions a actions.db/videos.db
|
| 411 |
try:
|
| 412 |
session_id = st.session_state.get("session_id", "")
|
| 413 |
ip = st.session_state.get("client_ip", "")
|
|
|
|
| 424 |
)
|
| 425 |
vis_choice = st.session_state.get("video_visibility", "Privat")
|
| 426 |
vis_flag = "public" if vis_choice.strip().lower().startswith("púb") else "private"
|
| 427 |
+
|
| 428 |
+
# 1) Registre a actions.db (acció bàsica)
|
| 429 |
+
log_action(
|
| 430 |
session=session_id,
|
|
|
|
| 431 |
user=username or "",
|
|
|
|
| 432 |
phone=phone,
|
| 433 |
action="upload",
|
| 434 |
sha1sum=sha1,
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
# 2) Determinar user/phone per a actions.db
|
| 438 |
+
actions_user, actions_phone = get_latest_user_phone_for_session(session_id)
|
| 439 |
+
if not actions_user:
|
| 440 |
+
actions_user = username or ""
|
| 441 |
+
if not actions_phone:
|
| 442 |
+
actions_phone = phone or ""
|
| 443 |
+
|
| 444 |
+
# 3) Inserir acció "Uploaded video" a actions.db (demo/temp/db/actions.db)
|
| 445 |
+
insert_action(
|
| 446 |
+
session=session_id,
|
| 447 |
+
user=actions_user,
|
| 448 |
+
phone=actions_phone,
|
| 449 |
+
action="Uploaded video",
|
| 450 |
+
sha1sum=sha1,
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
# 4) Assegurar fila a videos.db (demo/temp/db/videos.db) amb owner i status="input-pending"
|
| 454 |
+
ensure_video_row_for_upload(
|
| 455 |
+
sha1sum=sha1,
|
| 456 |
+
video_name=uploaded_file.name,
|
| 457 |
+
owner_phone=actions_phone,
|
| 458 |
+
status="input-pending",
|
| 459 |
visibility=vis_flag,
|
| 460 |
)
|
| 461 |
except Exception as e:
|
| 462 |
+
print(f"[events/actions] Error registrant pujada de vídeo: {e}")
|
| 463 |
|
| 464 |
+
# Guardar sempre el vídeo a demo/temp/pending_videos/<sha1>/video.mp4
|
| 465 |
+
# i, en mode external, enviar-lo també a pending_videos de l'engine
|
| 466 |
try:
|
| 467 |
base_dir = Path(__file__).parent.parent
|
| 468 |
data_origin = _load_data_origin(base_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
|
| 470 |
+
pending_root = base_dir / "temp" / "pending_videos" / sha1
|
| 471 |
+
pending_root.mkdir(parents=True, exist_ok=True)
|
| 472 |
+
local_pending_path = pending_root / "video.mp4"
|
| 473 |
+
# Guardar còpia local del vídeo pendent
|
| 474 |
+
with local_pending_path.open("wb") as f_pending:
|
| 475 |
+
f_pending.write(video_bytes)
|
| 476 |
+
|
| 477 |
+
if data_origin == "external":
|
| 478 |
# Enviar el vídeo al backend engine perquè aparegui a la llista de pendents
|
| 479 |
try:
|
| 480 |
resp_pending = api.upload_pending_video(video_bytes, uploaded_file.name)
|
|
|
|
| 485 |
_log(f"[pending_videos] Error bloc exterior upload_pending_video: {e_ext}")
|
| 486 |
|
| 487 |
# Marcar estat de validació segons la configuració de seguretat
|
| 488 |
+
if manual_validation_enabled and not skip_manual_validation_for_this_video:
|
| 489 |
st.session_state.video_requires_validation = True
|
| 490 |
st.session_state.video_validation_approved = False
|
| 491 |
+
|
| 492 |
+
# Notificar al validador per SMS només si està habilitat a config.yaml
|
| 493 |
+
if video_validator_sms_enabled:
|
| 494 |
+
try:
|
| 495 |
+
compliance_client.notify_video_upload(
|
| 496 |
+
video_name=uploaded_file.name,
|
| 497 |
+
sha1sum=sha1,
|
| 498 |
+
)
|
| 499 |
+
except Exception as sms_exc:
|
| 500 |
+
print(f"[VIDEO SMS] Error enviant notificació al validor: {sms_exc}")
|
| 501 |
else:
|
| 502 |
+
# Sense validació manual (o ja input-OK): es considera validat automàticament
|
| 503 |
st.session_state.video_requires_validation = False
|
| 504 |
st.session_state.video_validation_approved = True
|
| 505 |
|
|
|
|
| 520 |
if st.session_state.get("video_uploaded"):
|
| 521 |
current_sha1 = st.session_state.video_uploaded.get("sha1sum")
|
| 522 |
if current_sha1 and st.session_state.get("video_requires_validation") and not st.session_state.get("video_validation_approved"):
|
| 523 |
+
if has_video_approval_action(current_sha1):
|
| 524 |
st.session_state.video_validation_approved = True
|
| 525 |
|
| 526 |
# Només podem continuar amb el càsting si el vídeo no requereix validació
|
|
|
|
| 700 |
pass
|
| 701 |
|
| 702 |
if current_sha1:
|
| 703 |
+
if has_video_approval_action(current_sha1):
|
| 704 |
st.session_state.video_validation_approved = True
|
| 705 |
st.success("✅ Vídeo validat. Pots continuar amb el càsting.")
|
| 706 |
else:
|
|
|
|
| 1811 |
if any_success and refined_any and sha1:
|
| 1812 |
sms_channels_enabled = bool(twilio_enabled_cfg or zapier_enabled_cfg)
|
| 1813 |
if sms_channels_enabled and une_validator_sms_enabled and une_phone_validator:
|
| 1814 |
+
try:
|
| 1815 |
+
# Text de l'SMS en català, tal com has indicat
|
| 1816 |
+
sms_msg = "Noves audiodescripcions a validar segons la norma UNE-153020"
|
| 1817 |
+
compliance_client.notify_une_validator_new_ads(
|
| 1818 |
+
phone=une_phone_validator,
|
| 1819 |
+
message=sms_msg,
|
| 1820 |
+
)
|
| 1821 |
+
except Exception as e_sms_call:
|
| 1822 |
+
_log(f"[UNE SMS] Error cridant compliance per UNE: {e_sms_call}")
|
| 1823 |
+
|
| 1824 |
# Registrar estat d'espera de validació UNE a events.db
|
| 1825 |
try:
|
| 1826 |
log_event(
|
|
|
|
| 1838 |
except Exception as e_sms:
|
| 1839 |
_log(f"[UNE SMS] Error en flux d'SMS/espera validació: {e_sms}")
|
| 1840 |
|
| 1841 |
+
# 8) Actualitzar status del vídeo a 'UNE-pending' a videos.db
|
| 1842 |
+
try:
|
| 1843 |
+
if any_success and sha1:
|
| 1844 |
+
update_video_status(sha1, "UNE-pending")
|
| 1845 |
+
except Exception as e_upd_status:
|
| 1846 |
+
_log(f"[videos] Error actualitzant status a 'UNE-pending': {e_upd_status}")
|
| 1847 |
+
|
| 1848 |
+
# 9) Invocar Space TTS per generar free_ad.mp3 i une_ad.mp4 a temp/media/<sha1>/Original
|
| 1849 |
+
try:
|
| 1850 |
+
if any_success and sha1:
|
| 1851 |
+
# Obtenir el text UNE més recent des d'audiodescriptions.db (prioritzem Salamandra)
|
| 1852 |
+
une_text = ""
|
| 1853 |
+
row_s = get_audiodescription(sha1, "Salamandra")
|
| 1854 |
+
if row_s is not None:
|
| 1855 |
+
try:
|
| 1856 |
+
une_text = (row_s["une_ad"] or "").strip()
|
| 1857 |
+
except Exception:
|
| 1858 |
+
une_text = ""
|
| 1859 |
+
if not une_text:
|
| 1860 |
+
row_m = get_audiodescription(sha1, "MoE")
|
| 1861 |
+
if row_m is not None:
|
| 1862 |
+
try:
|
| 1863 |
+
une_text = (row_m["une_ad"] or "").strip()
|
| 1864 |
+
except Exception:
|
| 1865 |
+
une_text = ""
|
| 1866 |
+
|
| 1867 |
+
if une_text:
|
| 1868 |
+
base_media_dir = Path(__file__).parent.parent / "temp" / "media" / sha1
|
| 1869 |
+
video_path = base_media_dir / "video.mp4"
|
| 1870 |
+
if not video_path.exists():
|
| 1871 |
+
# Assegurar que tenim la media localment
|
| 1872 |
+
try:
|
| 1873 |
+
ensure_media_for_video(Path(__file__).parent.parent, api, sha1)
|
| 1874 |
+
except Exception as e_em:
|
| 1875 |
+
_log(f"[TTS] Error assegurant media per al vídeo: {e_em}")
|
| 1876 |
+
|
| 1877 |
+
if video_path.exists():
|
| 1878 |
+
# Preparar carpeta de sortida Original
|
| 1879 |
+
original_dir = base_media_dir / "Original"
|
| 1880 |
+
original_dir.mkdir(parents=True, exist_ok=True)
|
| 1881 |
+
|
| 1882 |
+
# Escriure SRT temporal i cridar Space TTS (/tts/srt)
|
| 1883 |
+
tts_url = os.getenv("API_TTS_URL", "").strip()
|
| 1884 |
+
if tts_url:
|
| 1885 |
+
try:
|
| 1886 |
+
with tempfile.TemporaryDirectory(prefix="tts_srt_") as td:
|
| 1887 |
+
td_path = Path(td)
|
| 1888 |
+
srt_tmp = td_path / "ad_input.srt"
|
| 1889 |
+
srt_tmp.write_text(une_text, encoding="utf-8")
|
| 1890 |
+
|
| 1891 |
+
files = {
|
| 1892 |
+
"srt": ("ad_input.srt", srt_tmp.open("rb"), "text/plain"),
|
| 1893 |
+
"video": ("video.mp4", video_path.open("rb"), "video/mp4"),
|
| 1894 |
+
}
|
| 1895 |
+
data = {
|
| 1896 |
+
"voice": "central/grau",
|
| 1897 |
+
"ad_format": "mp3",
|
| 1898 |
+
"include_final_mp4": "1",
|
| 1899 |
+
}
|
| 1900 |
+
|
| 1901 |
+
resp = requests.post(
|
| 1902 |
+
f"{tts_url.rstrip('/')}/tts/srt",
|
| 1903 |
+
files=files,
|
| 1904 |
+
data=data,
|
| 1905 |
+
timeout=300,
|
| 1906 |
+
)
|
| 1907 |
+
resp.raise_for_status()
|
| 1908 |
+
|
| 1909 |
+
# La resposta és un ZIP amb ad_master.(mp3|wav), mix i opcionalment video_con_ad.mp4
|
| 1910 |
+
zip_bytes = resp.content
|
| 1911 |
+
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
| 1912 |
+
for member in zf.infolist():
|
| 1913 |
+
name = member.filename
|
| 1914 |
+
lower = name.lower()
|
| 1915 |
+
if lower.endswith("ad_master.mp3"):
|
| 1916 |
+
target = original_dir / "free_ad.mp3"
|
| 1917 |
+
with zf.open(member) as src, target.open("wb") as dst:
|
| 1918 |
+
shutil.copyfileobj(src, dst)
|
| 1919 |
+
elif lower.endswith("video_con_ad.mp4"):
|
| 1920 |
+
target = original_dir / "une_ad.mp4"
|
| 1921 |
+
with zf.open(member) as src, target.open("wb") as dst:
|
| 1922 |
+
shutil.copyfileobj(src, dst)
|
| 1923 |
+
except Exception as e_tts:
|
| 1924 |
+
_log(f"[TTS] Error generant assets TTS (free_ad.mp3/une_ad.mp4): {e_tts}")
|
| 1925 |
+
else:
|
| 1926 |
+
_log("[TTS] API_TTS_URL no configurada; s'omet la generació de free_ad.mp3/une_ad.mp4")
|
| 1927 |
+
else:
|
| 1928 |
+
_log("[TTS] No s'ha trobat text UNE per al vídeo; s'omet la generació TTS")
|
| 1929 |
+
except Exception as e_tts_global:
|
| 1930 |
+
_log(f"[TTS] Error global al flux TTS: {e_tts_global}")
|
| 1931 |
+
|
| 1932 |
+
# 10) Registrar acció "AD generated" a actions.db per a aquest vídeo
|
| 1933 |
+
try:
|
| 1934 |
+
if any_success and sha1:
|
| 1935 |
+
session_id_actions = session_id
|
| 1936 |
+
actions_user, actions_phone = get_latest_user_phone_for_session(session_id_actions)
|
| 1937 |
+
if not actions_user:
|
| 1938 |
+
actions_user = username or ""
|
| 1939 |
+
if not actions_phone:
|
| 1940 |
+
actions_phone = phone or ""
|
| 1941 |
+
|
| 1942 |
+
insert_action(
|
| 1943 |
+
session=session_id_actions,
|
| 1944 |
+
user=actions_user,
|
| 1945 |
+
phone=actions_phone,
|
| 1946 |
+
action="AD generated",
|
| 1947 |
+
sha1sum=sha1,
|
| 1948 |
+
)
|
| 1949 |
+
except Exception as e_act:
|
| 1950 |
+
_log(f"[actions] Error registrant acció 'AD generated': {e_act}")
|
| 1951 |
+
|
| 1952 |
if any_success:
|
| 1953 |
+
progress_placeholder.success("✅ Audiodescripció generada i desada. Ara està pendent de validació UNE.")
|
| 1954 |
+
result_placeholder.info("La teva audiodescripció s'està generant i queda pendent de validació. Pots sortir de la sessió guardant els canvis i tornar més endavant per revisar el resultat.")
|
| 1955 |
else:
|
| 1956 |
progress_placeholder.empty()
|
| 1957 |
result_placeholder.error("❌ No s'ha pogut generar cap versió d'audiodescripció.")
|
page_modules/validation.py
CHANGED
|
@@ -8,13 +8,24 @@ from typing import Dict
|
|
| 8 |
import sys
|
| 9 |
|
| 10 |
import shutil
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
import streamlit as st
|
| 12 |
|
| 13 |
from databases import (
|
| 14 |
get_accessible_videos_with_sha1,
|
| 15 |
-
|
| 16 |
get_audiodescription_history,
|
| 17 |
update_audiodescription_text,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
)
|
| 19 |
from persistent_data_gate import _load_data_origin
|
| 20 |
|
|
@@ -43,6 +54,20 @@ def render_validation_page(
|
|
| 43 |
base_dir = Path(__file__).resolve().parent.parent
|
| 44 |
data_origin = _load_data_origin(base_dir)
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
# Llista de vídeos accessibles (mode internal) o pendents al backend (mode external)
|
| 47 |
session_id = st.session_state.get("session_id")
|
| 48 |
accessible_rows = get_accessible_videos_with_sha1(session_id) if data_origin == "internal" else []
|
|
@@ -201,46 +226,65 @@ def render_validation_page(
|
|
| 201 |
comments=f"Vídeo validat per {username}",
|
| 202 |
)
|
| 203 |
|
| 204 |
-
# 2) Registrar
|
| 205 |
session_id = st.session_state.get("session_id") or ""
|
| 206 |
-
client_ip = st.session_state.get("client_ip") or ""
|
| 207 |
phone = st.session_state.get("phone_number") or ""
|
| 208 |
-
password = st.session_state.get("password") or ""
|
| 209 |
|
| 210 |
try:
|
| 211 |
-
|
| 212 |
session=session_id,
|
| 213 |
-
ip=client_ip,
|
| 214 |
user=username or "",
|
| 215 |
-
password=password,
|
| 216 |
phone=phone,
|
| 217 |
-
action="
|
| 218 |
-
sha1sum=video_seleccionat["sha1sum"],
|
| 219 |
-
visibility=None,
|
| 220 |
)
|
| 221 |
-
except Exception
|
| 222 |
-
|
| 223 |
|
| 224 |
if success:
|
| 225 |
-
st.success("✅ Vídeo acceptat, registrat al servei de compliance i marcat com
|
| 226 |
else:
|
| 227 |
st.error("❌ Error registrant el veredicte al servei de compliance")
|
| 228 |
|
| 229 |
-
# 3)
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
try:
|
| 235 |
-
|
| 236 |
-
src = local_pending_dir / "video.mp4"
|
| 237 |
-
if src.exists():
|
| 238 |
-
dst = local_media_dir / "video.mp4"
|
| 239 |
-
shutil.copy2(src, dst)
|
| 240 |
-
if local_pending_dir.exists():
|
| 241 |
-
shutil.rmtree(local_pending_dir)
|
| 242 |
except Exception:
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
with col_btn2:
|
| 246 |
if st.button("❌ Rebutjar", type="secondary", key=f"reject_video_{video_seleccionat['video_name']}"):
|
|
@@ -257,48 +301,16 @@ def render_validation_page(
|
|
| 257 |
|
| 258 |
with tab_ads:
|
| 259 |
st.subheader("🎬 Validar Audiodescripcions")
|
| 260 |
-
|
| 261 |
-
#
|
| 262 |
-
|
| 263 |
-
# Construir mapa sha1 -> video_name a partir de vídeos accessibles
|
| 264 |
-
sha1_to_name = {row["sha1sum"]: (row["video_name"] or row["sha1sum"]) for row in accessible_rows}
|
| 265 |
-
|
| 266 |
-
# Llegir events.db directament per obtenir l'última acció per cada sha1
|
| 267 |
-
from databases import _connect_events_db # tipus intern però útil aquí
|
| 268 |
-
pending_videos = []
|
| 269 |
-
try:
|
| 270 |
-
with _connect_events_db() as conn:
|
| 271 |
-
cur = conn.execute(
|
| 272 |
-
"""
|
| 273 |
-
SELECT sha1sum, MAX(timestamp) AS latest_ts
|
| 274 |
-
FROM events
|
| 275 |
-
GROUP BY sha1sum
|
| 276 |
-
"""
|
| 277 |
-
)
|
| 278 |
-
latest_by_sha1 = {row["sha1sum"]: row["latest_ts"] for row in cur.fetchall()}
|
| 279 |
-
|
| 280 |
-
# Filtrar aquells on l'última acció és "Waiting for UNE validation"
|
| 281 |
-
for sha1, latest_ts in latest_by_sha1.items():
|
| 282 |
-
if not sha1:
|
| 283 |
-
continue
|
| 284 |
-
row = conn.execute(
|
| 285 |
-
"SELECT action FROM events WHERE sha1sum=? AND timestamp=?",
|
| 286 |
-
(sha1, latest_ts),
|
| 287 |
-
).fetchone()
|
| 288 |
-
if row and row["action"] == "Waiting for UNE validation":
|
| 289 |
-
pending_videos.append({
|
| 290 |
-
"sha1sum": sha1,
|
| 291 |
-
"video_name": sha1_to_name.get(sha1, sha1),
|
| 292 |
-
})
|
| 293 |
-
except Exception as e_ev:
|
| 294 |
-
_log(f"[UNE validation] Error llegint events.db: {e_ev}")
|
| 295 |
|
| 296 |
if not pending_videos:
|
| 297 |
st.info("📝 No hi ha audiodescripcions pendents de validació UNE.")
|
| 298 |
else:
|
| 299 |
options = [f"{v['video_name']} ({v['sha1sum']})" for v in pending_videos]
|
| 300 |
seleccion_ad = st.selectbox(
|
| 301 |
-
"Selecciona
|
| 302 |
options,
|
| 303 |
index=0 if options else None,
|
| 304 |
)
|
|
@@ -309,20 +321,25 @@ def render_validation_page(
|
|
| 309 |
sha1 = sel["sha1sum"]
|
| 310 |
video_name = sel["video_name"]
|
| 311 |
|
| 312 |
-
#
|
| 313 |
-
|
| 314 |
-
for
|
| 315 |
-
rows = get_audiodescription_history(sha1,
|
| 316 |
if rows:
|
| 317 |
-
|
| 318 |
-
ad_rows = rows
|
| 319 |
-
break
|
| 320 |
|
| 321 |
-
if not
|
| 322 |
st.error("No s'ha trobat cap audiodescripció per a aquest vídeo a la base de dades.")
|
| 323 |
else:
|
| 324 |
-
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
current_une = row_ad.get("une_ad") or ""
|
| 327 |
current_free = row_ad.get("free_ad") or ""
|
| 328 |
|
|
@@ -367,7 +384,7 @@ def render_validation_page(
|
|
| 367 |
|
| 368 |
if st.button("✅ Acceptar", type="primary", key=f"accept_ad_{sha1}_{selected_version}"):
|
| 369 |
try:
|
| 370 |
-
# 1) Registrar decisió al servei de compliance (opcional
|
| 371 |
try:
|
| 372 |
success = compliance_client.record_validator_decision(
|
| 373 |
document_id=f"ad_{video_name}",
|
|
@@ -380,34 +397,128 @@ def render_validation_page(
|
|
| 380 |
except Exception as e_comp:
|
| 381 |
_log(f"[UNE validation] Error amb compliance: {e_comp}")
|
| 382 |
|
| 383 |
-
# 2) Actualitzar camps OK
|
| 384 |
update_audiodescription_text(
|
| 385 |
sha1sum=sha1,
|
| 386 |
version=selected_version,
|
| 387 |
ok_une_ad=new_une,
|
| 388 |
-
test_une_ad=new_une,
|
| 389 |
ok_free_ad=new_free,
|
| 390 |
-
test_free_ad=new_free,
|
| 391 |
)
|
| 392 |
|
| 393 |
-
# 3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
try:
|
| 395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
session=session_id or "",
|
| 397 |
-
ip=st.session_state.get("client_ip", "") or "",
|
| 398 |
user=username or "",
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
or st.session_state.get("sms_phone")
|
| 402 |
-
or "",
|
| 403 |
-
action="Validated AD UNE-153020",
|
| 404 |
sha1sum=sha1,
|
| 405 |
-
visibility=None,
|
| 406 |
)
|
| 407 |
-
except Exception
|
| 408 |
-
|
| 409 |
|
| 410 |
-
st.success("✅ Audiodescripció UNE-153010 validada i desada (OK
|
| 411 |
st.rerun()
|
| 412 |
except Exception as e_val:
|
| 413 |
st.error(f"❌ Error durant la validació de l'audiodescripció: {e_val}")
|
|
|
|
| 8 |
import sys
|
| 9 |
|
| 10 |
import shutil
|
| 11 |
+
import os
|
| 12 |
+
import tempfile
|
| 13 |
+
import zipfile
|
| 14 |
+
import io
|
| 15 |
+
import requests
|
| 16 |
import streamlit as st
|
| 17 |
|
| 18 |
from databases import (
|
| 19 |
get_accessible_videos_with_sha1,
|
| 20 |
+
get_audiodescription,
|
| 21 |
get_audiodescription_history,
|
| 22 |
update_audiodescription_text,
|
| 23 |
+
update_audiodescription_info_ad,
|
| 24 |
+
log_action,
|
| 25 |
+
update_video_status,
|
| 26 |
+
get_video_owner_by_sha1,
|
| 27 |
+
get_videos_by_status,
|
| 28 |
+
insert_action,
|
| 29 |
)
|
| 30 |
from persistent_data_gate import _load_data_origin
|
| 31 |
|
|
|
|
| 54 |
base_dir = Path(__file__).resolve().parent.parent
|
| 55 |
data_origin = _load_data_origin(base_dir)
|
| 56 |
|
| 57 |
+
# Llegir config.yaml per saber si cal enviar SMS a l'usuari quan el vídeo és aprovat
|
| 58 |
+
config_path = base_dir / "config.yaml"
|
| 59 |
+
user_sms_enabled = False
|
| 60 |
+
try:
|
| 61 |
+
if config_path.exists():
|
| 62 |
+
import yaml # import local per no afegir-lo al top del fitxer
|
| 63 |
+
|
| 64 |
+
with config_path.open("r", encoding="utf-8") as f:
|
| 65 |
+
cfg = yaml.safe_load(f) or {}
|
| 66 |
+
validation_cfg = cfg.get("validation", {}) or {}
|
| 67 |
+
user_sms_enabled = bool(validation_cfg.get("user_sms_enabled", False))
|
| 68 |
+
except Exception:
|
| 69 |
+
user_sms_enabled = False
|
| 70 |
+
|
| 71 |
# Llista de vídeos accessibles (mode internal) o pendents al backend (mode external)
|
| 72 |
session_id = st.session_state.get("session_id")
|
| 73 |
accessible_rows = get_accessible_videos_with_sha1(session_id) if data_origin == "internal" else []
|
|
|
|
| 226 |
comments=f"Vídeo validat per {username}",
|
| 227 |
)
|
| 228 |
|
| 229 |
+
# 2) Registrar acció d'acceptació d'input (input-OK) a actions.db
|
| 230 |
session_id = st.session_state.get("session_id") or ""
|
|
|
|
| 231 |
phone = st.session_state.get("phone_number") or ""
|
|
|
|
| 232 |
|
| 233 |
try:
|
| 234 |
+
log_action(
|
| 235 |
session=session_id,
|
|
|
|
| 236 |
user=username or "",
|
|
|
|
| 237 |
phone=phone,
|
| 238 |
+
action="input-OK",
|
| 239 |
+
sha1sum=video_seleccionat["sha1sum"] or "",
|
|
|
|
| 240 |
)
|
| 241 |
+
except Exception:
|
| 242 |
+
pass
|
| 243 |
|
| 244 |
if success:
|
| 245 |
+
st.success("✅ Vídeo acceptat, registrat al servei de compliance i marcat com input-OK")
|
| 246 |
else:
|
| 247 |
st.error("❌ Error registrant el veredicte al servei de compliance")
|
| 248 |
|
| 249 |
+
# 3) Moure el vídeo de temp/pending_videos a temp/media (tant en mode internal com external)
|
| 250 |
+
sha1 = video_seleccionat["sha1sum"]
|
| 251 |
+
local_pending_dir = pending_root / sha1
|
| 252 |
+
local_media_dir = base_media_dir / sha1
|
| 253 |
+
try:
|
| 254 |
+
local_media_dir.mkdir(parents=True, exist_ok=True)
|
| 255 |
+
src = local_pending_dir / "video.mp4"
|
| 256 |
+
if src.exists():
|
| 257 |
+
dst = local_media_dir / "video.mp4"
|
| 258 |
+
shutil.copy2(src, dst)
|
| 259 |
+
if local_pending_dir.exists():
|
| 260 |
+
shutil.rmtree(local_pending_dir)
|
| 261 |
+
except Exception:
|
| 262 |
+
pass
|
| 263 |
+
|
| 264 |
+
# 4) Actualitzar status="input-OK" a videos.db per aquest sha1
|
| 265 |
+
try:
|
| 266 |
+
update_video_status(sha1, "input-OK")
|
| 267 |
+
except Exception:
|
| 268 |
+
pass
|
| 269 |
+
|
| 270 |
+
# 5) Si està habilitat user_sms_enabled, enviar SMS a l'usuari (owner)
|
| 271 |
+
if user_sms_enabled:
|
| 272 |
try:
|
| 273 |
+
owner_phone = get_video_owner_by_sha1(sha1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
except Exception:
|
| 275 |
+
owner_phone = ""
|
| 276 |
+
|
| 277 |
+
if owner_phone:
|
| 278 |
+
try:
|
| 279 |
+
# Text proporcionat (en castellà, però segons els requisits de l'SMS)
|
| 280 |
+
msg = "Su vídeo ha sido aprobado. Puede entrar en la aplicación y subirlo de nuevo para generar la audiodescripción"
|
| 281 |
+
compliance_client.notify_user_video_approved(
|
| 282 |
+
phone=owner_phone,
|
| 283 |
+
message=msg,
|
| 284 |
+
sha1sum=sha1,
|
| 285 |
+
)
|
| 286 |
+
except Exception as e_sms:
|
| 287 |
+
_log(f"[VIDEO USER SMS] Error enviant SMS a l'usuari: {e_sms}")
|
| 288 |
|
| 289 |
with col_btn2:
|
| 290 |
if st.button("❌ Rebutjar", type="secondary", key=f"reject_video_{video_seleccionat['video_name']}"):
|
|
|
|
| 301 |
|
| 302 |
with tab_ads:
|
| 303 |
st.subheader("🎬 Validar Audiodescripcions")
|
| 304 |
+
|
| 305 |
+
# Llistar vídeos amb status="UNE-pending" a videos.db
|
| 306 |
+
pending_videos = get_videos_by_status("UNE-pending")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
if not pending_videos:
|
| 309 |
st.info("📝 No hi ha audiodescripcions pendents de validació UNE.")
|
| 310 |
else:
|
| 311 |
options = [f"{v['video_name']} ({v['sha1sum']})" for v in pending_videos]
|
| 312 |
seleccion_ad = st.selectbox(
|
| 313 |
+
"Selecciona un vídeo per validar la seva audiodescripció:",
|
| 314 |
options,
|
| 315 |
index=0 if options else None,
|
| 316 |
)
|
|
|
|
| 321 |
sha1 = sel["sha1sum"]
|
| 322 |
video_name = sel["video_name"]
|
| 323 |
|
| 324 |
+
# Permetre escollir versió (Salamandra / MoE) segons el que existeixi a audiodescriptions.db
|
| 325 |
+
available_versions = []
|
| 326 |
+
for v_name in ("Salamandra", "MoE"):
|
| 327 |
+
rows = get_audiodescription_history(sha1, v_name)
|
| 328 |
if rows:
|
| 329 |
+
available_versions.append(v_name)
|
|
|
|
|
|
|
| 330 |
|
| 331 |
+
if not available_versions:
|
| 332 |
st.error("No s'ha trobat cap audiodescripció per a aquest vídeo a la base de dades.")
|
| 333 |
else:
|
| 334 |
+
selected_version = st.selectbox(
|
| 335 |
+
"Selecciona la versió a validar:",
|
| 336 |
+
available_versions,
|
| 337 |
+
index=0,
|
| 338 |
+
key=f"ad_version_{sha1}",
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
rows = get_audiodescription_history(sha1, selected_version) or []
|
| 342 |
+
row_ad = rows[-1]
|
| 343 |
current_une = row_ad.get("une_ad") or ""
|
| 344 |
current_free = row_ad.get("free_ad") or ""
|
| 345 |
|
|
|
|
| 384 |
|
| 385 |
if st.button("✅ Acceptar", type="primary", key=f"accept_ad_{sha1}_{selected_version}"):
|
| 386 |
try:
|
| 387 |
+
# 1) Registrar decisió al servei de compliance (opcional)
|
| 388 |
try:
|
| 389 |
success = compliance_client.record_validator_decision(
|
| 390 |
document_id=f"ad_{video_name}",
|
|
|
|
| 397 |
except Exception as e_comp:
|
| 398 |
_log(f"[UNE validation] Error amb compliance: {e_comp}")
|
| 399 |
|
| 400 |
+
# 2) Actualitzar camps OK per a aquest vídeo/versió (sense tocar TEST)
|
| 401 |
update_audiodescription_text(
|
| 402 |
sha1sum=sha1,
|
| 403 |
version=selected_version,
|
| 404 |
ok_une_ad=new_une,
|
|
|
|
| 405 |
ok_free_ad=new_free,
|
|
|
|
| 406 |
)
|
| 407 |
|
| 408 |
+
# 3) Actualitzar status a 'UNE-OK' a videos.db
|
| 409 |
+
try:
|
| 410 |
+
update_video_status(sha1, "UNE-OK")
|
| 411 |
+
except Exception as e_stat:
|
| 412 |
+
_log(f"[UNE validation] Error actualitzant status a UNE-OK: {e_stat}")
|
| 413 |
+
|
| 414 |
+
# 4) Registrar acció 'UNE-OK' a actions.db
|
| 415 |
try:
|
| 416 |
+
session_id_actions = session_id or ""
|
| 417 |
+
user_for_action = username or ""
|
| 418 |
+
phone_for_action = st.session_state.get("phone_number") or ""
|
| 419 |
+
insert_action(
|
| 420 |
+
session=session_id_actions,
|
| 421 |
+
user=user_for_action,
|
| 422 |
+
phone=phone_for_action,
|
| 423 |
+
action="UNE-OK",
|
| 424 |
+
sha1sum=sha1,
|
| 425 |
+
)
|
| 426 |
+
except Exception as e_act:
|
| 427 |
+
_log(f"[UNE validation] Error registrant acció UNE-OK: {e_act}")
|
| 428 |
+
|
| 429 |
+
# 5) Enviar SMS a l'usuari que va pujar el vídeo, si user_sms_enabled
|
| 430 |
+
if user_sms_enabled:
|
| 431 |
+
try:
|
| 432 |
+
owner_phone = get_video_owner_by_sha1(sha1)
|
| 433 |
+
except Exception:
|
| 434 |
+
owner_phone = ""
|
| 435 |
+
|
| 436 |
+
if owner_phone:
|
| 437 |
+
try:
|
| 438 |
+
msg = (
|
| 439 |
+
"La seva audiodescripció ha estat validada segons la norma UNE-153020. "
|
| 440 |
+
"Pots tornar a l'aplicació per revisar-la i descarregar-la."
|
| 441 |
+
)
|
| 442 |
+
compliance_client.notify_user_video_approved(
|
| 443 |
+
phone=owner_phone,
|
| 444 |
+
message=msg,
|
| 445 |
+
sha1sum=sha1,
|
| 446 |
+
)
|
| 447 |
+
except Exception as e_sms:
|
| 448 |
+
_log(f"[UNE USER SMS] Error enviant SMS a l'usuari: {e_sms}")
|
| 449 |
+
|
| 450 |
+
# 6) Generar assets TTS definitius a temp/media/<sha1>/HITL OK
|
| 451 |
+
try:
|
| 452 |
+
if new_une.strip():
|
| 453 |
+
base_media_dir = base_dir / "temp" / "media"
|
| 454 |
+
video_dir = base_media_dir / sha1
|
| 455 |
+
video_path = None
|
| 456 |
+
if video_dir.exists():
|
| 457 |
+
for cand in [video_dir / "video.mp4", video_dir / "video.avi", video_dir / "video.mov"]:
|
| 458 |
+
if cand.exists():
|
| 459 |
+
video_path = cand
|
| 460 |
+
break
|
| 461 |
+
|
| 462 |
+
if video_path is not None:
|
| 463 |
+
hitl_ok_dir = video_dir / "HITL OK"
|
| 464 |
+
hitl_ok_dir.mkdir(parents=True, exist_ok=True)
|
| 465 |
+
|
| 466 |
+
tts_url = os.getenv("API_TTS_URL", "").strip()
|
| 467 |
+
if tts_url:
|
| 468 |
+
with tempfile.TemporaryDirectory(prefix="tts_hitl_ok_") as td:
|
| 469 |
+
td_path = Path(td)
|
| 470 |
+
srt_tmp = td_path / "ad_ok.srt"
|
| 471 |
+
srt_tmp.write_text(new_une, encoding="utf-8")
|
| 472 |
+
|
| 473 |
+
files = {
|
| 474 |
+
"srt": ("ad_ok.srt", srt_tmp.open("rb"), "text/plain"),
|
| 475 |
+
"video": (video_path.name, video_path.open("rb"), "video/mp4"),
|
| 476 |
+
}
|
| 477 |
+
data = {
|
| 478 |
+
"voice": "central/grau",
|
| 479 |
+
"ad_format": "mp3",
|
| 480 |
+
"include_final_mp4": "1",
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
resp = requests.post(
|
| 484 |
+
f"{tts_url.rstrip('/')}/tts/srt",
|
| 485 |
+
files=files,
|
| 486 |
+
data=data,
|
| 487 |
+
timeout=300,
|
| 488 |
+
)
|
| 489 |
+
resp.raise_for_status()
|
| 490 |
+
|
| 491 |
+
zip_bytes = resp.content
|
| 492 |
+
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
| 493 |
+
for member in zf.infolist():
|
| 494 |
+
name = member.filename
|
| 495 |
+
lower = name.lower()
|
| 496 |
+
if lower.endswith("ad_master.mp3"):
|
| 497 |
+
target = hitl_ok_dir / "free_ad.mp3"
|
| 498 |
+
with zf.open(member) as src, target.open("wb") as dst:
|
| 499 |
+
shutil.copyfileobj(src, dst)
|
| 500 |
+
elif lower.endswith("video_con_ad.mp4"):
|
| 501 |
+
target = hitl_ok_dir / "une_ad.mp4"
|
| 502 |
+
with zf.open(member) as src, target.open("wb") as dst:
|
| 503 |
+
shutil.copyfileobj(src, dst)
|
| 504 |
+
else:
|
| 505 |
+
_log("[UNE TTS] API_TTS_URL no configurada; s'omet la generació de free_ad.mp3/une_ad.mp4 (HITL OK)")
|
| 506 |
+
except Exception as e_tts:
|
| 507 |
+
_log(f"[UNE TTS] Error generant assets HITL OK: {e_tts}")
|
| 508 |
+
|
| 509 |
+
# 7) Registrar acció de validació a events.db (per traçabilitat)
|
| 510 |
+
try:
|
| 511 |
+
log_action(
|
| 512 |
session=session_id or "",
|
|
|
|
| 513 |
user=username or "",
|
| 514 |
+
phone=st.session_state.get("phone_number") or "",
|
| 515 |
+
action="validate_ad_une_153020",
|
|
|
|
|
|
|
|
|
|
| 516 |
sha1sum=sha1,
|
|
|
|
| 517 |
)
|
| 518 |
+
except Exception:
|
| 519 |
+
pass
|
| 520 |
|
| 521 |
+
st.success("✅ Audiodescripció UNE-153010 validada i desada (HITL OK).")
|
| 522 |
st.rerun()
|
| 523 |
except Exception as e_val:
|
| 524 |
st.error(f"❌ Error durant la validació de l'audiodescripció: {e_val}")
|
persistent_data_gate.py
CHANGED
|
@@ -75,8 +75,6 @@ def ensure_temp_databases(base_dir: Path, api_client) -> None:
|
|
| 75 |
"""
|
| 76 |
|
| 77 |
data_origin = _load_data_origin(base_dir)
|
| 78 |
-
compliance_flags = _load_compliance_flags(base_dir)
|
| 79 |
-
public_blockchain_enabled = bool(compliance_flags.get("public_blockchain_enabled", False))
|
| 80 |
temp_root = base_dir / "temp"
|
| 81 |
db_temp_dir = temp_root / "db"
|
| 82 |
db_temp_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -414,7 +412,162 @@ def confirm_changes_and_logout(base_dir: Path, api_client, session_id: str) -> N
|
|
| 414 |
# No aturar el procés si hi ha errors en el càlcul del digest
|
| 415 |
events_digest_info = None
|
| 416 |
|
| 417 |
-
# ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
videos_db = db_temp_dir / "videos.db"
|
| 419 |
new_sha1s: set[str] = set()
|
| 420 |
|
|
@@ -430,7 +583,8 @@ def confirm_changes_and_logout(base_dir: Path, api_client, session_id: str) -> N
|
|
| 430 |
col_names = [c[1] for c in cols]
|
| 431 |
if "session" in col_names and "sha1sum" in col_names:
|
| 432 |
cur.execute(
|
| 433 |
-
"SELECT DISTINCT sha1sum FROM videos WHERE session = ?",
|
|
|
|
| 434 |
)
|
| 435 |
for r in cur.fetchall():
|
| 436 |
if r["sha1sum"]:
|
|
|
|
| 75 |
"""
|
| 76 |
|
| 77 |
data_origin = _load_data_origin(base_dir)
|
|
|
|
|
|
|
| 78 |
temp_root = base_dir / "temp"
|
| 79 |
db_temp_dir = temp_root / "db"
|
| 80 |
db_temp_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 412 |
# No aturar el procés si hi ha errors en el càlcul del digest
|
| 413 |
events_digest_info = None
|
| 414 |
|
| 415 |
+
# --- 2b) Registre opcional de canvis d'actions a QLDB (private blockchain) ---
|
| 416 |
+
if private_blockchain_enabled:
|
| 417 |
+
actions_db_path = db_temp_dir / "actions.db"
|
| 418 |
+
try:
|
| 419 |
+
import sqlite3
|
| 420 |
+
import hashlib
|
| 421 |
+
import json
|
| 422 |
+
|
| 423 |
+
if actions_db_path.exists():
|
| 424 |
+
with sqlite3.connect(str(actions_db_path)) as aconn:
|
| 425 |
+
aconn.row_factory = sqlite3.Row
|
| 426 |
+
cur = aconn.cursor()
|
| 427 |
+
cur.execute(
|
| 428 |
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='actions'"
|
| 429 |
+
)
|
| 430 |
+
if cur.fetchone():
|
| 431 |
+
cur.execute(
|
| 432 |
+
"SELECT * FROM actions WHERE session = ?",
|
| 433 |
+
(session_id,),
|
| 434 |
+
)
|
| 435 |
+
rows = cur.fetchall()
|
| 436 |
+
if rows:
|
| 437 |
+
actions_payload = []
|
| 438 |
+
for r in rows:
|
| 439 |
+
row_dict = {k: r[k] for k in r.keys()}
|
| 440 |
+
phone_val = row_dict.get("phone")
|
| 441 |
+
if phone_val:
|
| 442 |
+
phone_hash = hashlib.sha256(
|
| 443 |
+
str(phone_val).encode("utf-8")
|
| 444 |
+
).hexdigest()
|
| 445 |
+
row_dict["phone_hash"] = phone_hash
|
| 446 |
+
row_dict["phone"] = None
|
| 447 |
+
actions_payload.append(row_dict)
|
| 448 |
+
|
| 449 |
+
try:
|
| 450 |
+
_ = compliance_client.publish_actions_qldb(
|
| 451 |
+
session_id=session_id,
|
| 452 |
+
actions=actions_payload,
|
| 453 |
+
)
|
| 454 |
+
except Exception as qexc:
|
| 455 |
+
print(f"[QLDB PUBLISH] error guardant actions a QLDB: {qexc}")
|
| 456 |
+
except Exception:
|
| 457 |
+
pass
|
| 458 |
+
|
| 459 |
+
# --- 3) Tractar revocacions de permisos abans de sincronitzar media ---
|
| 460 |
+
# Vídeos per als quals, en aquesta sessió, s'ha registrat l'acció
|
| 461 |
+
# "Revocation of permits" a actions.db. Aquests vídeos s'han d'eliminar
|
| 462 |
+
# de temp/media, temp/pending_videos i de les BDs locals, i s'ha d'enviar
|
| 463 |
+
# un SMS de confirmació a l'usuari.
|
| 464 |
+
|
| 465 |
+
revoked_sha1s: dict[str, str] = {}
|
| 466 |
+
actions_db = db_temp_dir / "actions.db"
|
| 467 |
+
try:
|
| 468 |
+
import sqlite3
|
| 469 |
+
|
| 470 |
+
with sqlite3.connect(str(actions_db)) as aconn:
|
| 471 |
+
aconn.row_factory = sqlite3.Row
|
| 472 |
+
cur = aconn.cursor()
|
| 473 |
+
# Comprovar que existeixen les columnes necessàries
|
| 474 |
+
cur.execute("PRAGMA table_info(actions)")
|
| 475 |
+
cols = {row[1] for row in cur.fetchall()}
|
| 476 |
+
if {"session", "action", "sha1sum", "phone"}.issubset(cols):
|
| 477 |
+
cur.execute(
|
| 478 |
+
"""
|
| 479 |
+
SELECT DISTINCT sha1sum, phone
|
| 480 |
+
FROM actions
|
| 481 |
+
WHERE session = ? AND action = 'Revocation of permits' AND sha1sum IS NOT NULL
|
| 482 |
+
""",
|
| 483 |
+
(session_id,),
|
| 484 |
+
)
|
| 485 |
+
for row in cur.fetchall():
|
| 486 |
+
sha1 = str(row["sha1sum"] or "")
|
| 487 |
+
phone = str(row["phone"] or "")
|
| 488 |
+
if sha1:
|
| 489 |
+
revoked_sha1s[sha1] = phone
|
| 490 |
+
except Exception:
|
| 491 |
+
revoked_sha1s = {}
|
| 492 |
+
|
| 493 |
+
# Eliminar entrades als BDs i carpetes de media/pending per a aquests vídeos
|
| 494 |
+
if revoked_sha1s:
|
| 495 |
+
videos_db = db_temp_dir / "videos.db"
|
| 496 |
+
ad_db = db_temp_dir / "audiodescriptions.db"
|
| 497 |
+
|
| 498 |
+
try:
|
| 499 |
+
import sqlite3
|
| 500 |
+
|
| 501 |
+
# Esborrar de videos.db
|
| 502 |
+
if videos_db.exists():
|
| 503 |
+
with sqlite3.connect(str(videos_db)) as vconn:
|
| 504 |
+
cur_v = vconn.cursor()
|
| 505 |
+
cur_v.execute("PRAGMA table_info(videos)")
|
| 506 |
+
vcols = {row[1] for row in cur_v.fetchall()}
|
| 507 |
+
if "sha1sum" in vcols:
|
| 508 |
+
for sha1 in revoked_sha1s.keys():
|
| 509 |
+
cur_v.execute("DELETE FROM videos WHERE sha1sum = ?", (sha1,))
|
| 510 |
+
vconn.commit()
|
| 511 |
+
|
| 512 |
+
# Esborrar de audiodescriptions.db
|
| 513 |
+
if ad_db.exists():
|
| 514 |
+
with sqlite3.connect(str(ad_db)) as adconn:
|
| 515 |
+
cur_ad = adconn.cursor()
|
| 516 |
+
cur_ad.execute("PRAGMA table_info(audiodescriptions)")
|
| 517 |
+
acols = {row[1] for row in cur_ad.fetchall()}
|
| 518 |
+
if {"sha1sum"}.issubset(acols):
|
| 519 |
+
for sha1 in revoked_sha1s.keys():
|
| 520 |
+
cur_ad.execute("DELETE FROM audiodescriptions WHERE sha1sum = ?", (sha1,))
|
| 521 |
+
adconn.commit()
|
| 522 |
+
except Exception:
|
| 523 |
+
pass
|
| 524 |
+
|
| 525 |
+
# Esborrar carpetes de media i pending
|
| 526 |
+
temp_media_root = temp_root / "media"
|
| 527 |
+
temp_pending_root = temp_root / "pending_videos"
|
| 528 |
+
for sha1 in revoked_sha1s.keys():
|
| 529 |
+
try:
|
| 530 |
+
media_dir = temp_media_root / sha1
|
| 531 |
+
pending_dir = temp_pending_root / sha1
|
| 532 |
+
if media_dir.exists():
|
| 533 |
+
shutil.rmtree(media_dir, ignore_errors=True)
|
| 534 |
+
if pending_dir.exists():
|
| 535 |
+
shutil.rmtree(pending_dir, ignore_errors=True)
|
| 536 |
+
except Exception:
|
| 537 |
+
pass
|
| 538 |
+
|
| 539 |
+
# Enviar SMS de confirmació per a cada vídeo revocat
|
| 540 |
+
try:
|
| 541 |
+
import yaml
|
| 542 |
+
|
| 543 |
+
config_path = base_dir / "config.yaml"
|
| 544 |
+
user_sms_enabled = False
|
| 545 |
+
if config_path.exists():
|
| 546 |
+
with config_path.open("r", encoding="utf-8") as f:
|
| 547 |
+
cfg = yaml.safe_load(f) or {}
|
| 548 |
+
validation_cfg = cfg.get("validation", {}) or {}
|
| 549 |
+
user_sms_enabled = bool(validation_cfg.get("user_sms_enabled", False))
|
| 550 |
+
|
| 551 |
+
if user_sms_enabled:
|
| 552 |
+
for sha1, phone in revoked_sha1s.items():
|
| 553 |
+
if not phone:
|
| 554 |
+
continue
|
| 555 |
+
try:
|
| 556 |
+
msg = (
|
| 557 |
+
"Els permisos per utilitzar el vostre vídeo han estat revocats. "
|
| 558 |
+
"Les dades associades han estat eliminades del sistema."
|
| 559 |
+
)
|
| 560 |
+
compliance_client.notify_user_video_approved(
|
| 561 |
+
phone=phone,
|
| 562 |
+
message=msg,
|
| 563 |
+
sha1sum=sha1,
|
| 564 |
+
)
|
| 565 |
+
except Exception:
|
| 566 |
+
continue
|
| 567 |
+
except Exception:
|
| 568 |
+
pass
|
| 569 |
+
|
| 570 |
+
# --- 4) Nous vídeos a videos.db associats a la sessió (excloent revocats) ---
|
| 571 |
videos_db = db_temp_dir / "videos.db"
|
| 572 |
new_sha1s: set[str] = set()
|
| 573 |
|
|
|
|
| 583 |
col_names = [c[1] for c in cols]
|
| 584 |
if "session" in col_names and "sha1sum" in col_names:
|
| 585 |
cur.execute(
|
| 586 |
+
"SELECT DISTINCT sha1sum FROM videos WHERE session = ?",
|
| 587 |
+
(session_id,),
|
| 588 |
)
|
| 589 |
for r in cur.fetchall():
|
| 590 |
if r["sha1sum"]:
|
scripts/add_status_column_videos.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
DB_PATH = Path("demo/data/db/videos.db")
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def column_exists(cursor: sqlite3.Cursor, table: str, column: str) -> bool:
|
| 9 |
+
cursor.execute(f"PRAGMA table_info({table})")
|
| 10 |
+
cols = cursor.fetchall()
|
| 11 |
+
return any(col[1] == column for col in cols)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main() -> None:
|
| 15 |
+
if not DB_PATH.exists():
|
| 16 |
+
raise FileNotFoundError(f"Database file not found: {DB_PATH}")
|
| 17 |
+
|
| 18 |
+
conn = sqlite3.connect(DB_PATH)
|
| 19 |
+
cur = conn.cursor()
|
| 20 |
+
|
| 21 |
+
table_name = "videos"
|
| 22 |
+
column_name = "status"
|
| 23 |
+
|
| 24 |
+
# Añadir la columna si no existe
|
| 25 |
+
if not column_exists(cur, table_name, column_name):
|
| 26 |
+
cur.execute(f"ALTER TABLE {table_name} ADD COLUMN {column_name} TEXT")
|
| 27 |
+
|
| 28 |
+
# Establecer el valor 'UNE-OK' para todos los registros
|
| 29 |
+
cur.execute(f"UPDATE {table_name} SET {column_name} = ?", ("UNE-OK",))
|
| 30 |
+
|
| 31 |
+
conn.commit()
|
| 32 |
+
conn.close()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
if __name__ == "__main__":
|
| 36 |
+
main()
|
scripts/create_actions_db.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
DB_DIR = Path("demo/data/db")
|
| 6 |
+
DB_PATH = DB_DIR / "actions.db"
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main() -> None:
|
| 10 |
+
# Asegurarse de que existe el directorio
|
| 11 |
+
DB_DIR.mkdir(parents=True, exist_ok=True)
|
| 12 |
+
|
| 13 |
+
conn = sqlite3.connect(DB_PATH)
|
| 14 |
+
cur = conn.cursor()
|
| 15 |
+
|
| 16 |
+
# Crear tabla actions si no existe
|
| 17 |
+
cur.execute(
|
| 18 |
+
"""
|
| 19 |
+
CREATE TABLE IF NOT EXISTS actions (
|
| 20 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 21 |
+
timestamp TEXT NOT NULL,
|
| 22 |
+
action TEXT NOT NULL,
|
| 23 |
+
session TEXT,
|
| 24 |
+
user TEXT,
|
| 25 |
+
phone TEXT,
|
| 26 |
+
sha1sum TEXT
|
| 27 |
+
);
|
| 28 |
+
"""
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
conn.commit()
|
| 32 |
+
conn.close()
|
| 33 |
+
print(f"Base de datos creada/actualizada: {DB_PATH}")
|
| 34 |
+
print("Tabla 'actions' lista (vacía si no había registros previos).")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
if __name__ == "__main__":
|
| 38 |
+
main()
|
scripts/drop_feedback_ad_table.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
DB_PATH = Path("demo/data/db/users.db")
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def main() -> None:
|
| 9 |
+
if not DB_PATH.exists():
|
| 10 |
+
raise FileNotFoundError(f"Database file not found: {DB_PATH}")
|
| 11 |
+
|
| 12 |
+
conn = sqlite3.connect(DB_PATH)
|
| 13 |
+
cur = conn.cursor()
|
| 14 |
+
|
| 15 |
+
# Borrar la tabla feedback_ad si existe
|
| 16 |
+
cur.execute("DROP TABLE IF EXISTS feedback_ad")
|
| 17 |
+
|
| 18 |
+
conn.commit()
|
| 19 |
+
conn.close()
|
| 20 |
+
print("Tabla 'feedback_ad' eliminada (si existía).")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
if __name__ == "__main__":
|
| 24 |
+
main()
|
scripts/show_db_columns.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
DB_DIR = Path("demo/data/db")
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def list_db_columns(db_path: Path) -> None:
|
| 9 |
+
print("=" * 80)
|
| 10 |
+
print(f"Database: {db_path}")
|
| 11 |
+
print("=" * 80)
|
| 12 |
+
|
| 13 |
+
conn = sqlite3.connect(db_path)
|
| 14 |
+
cur = conn.cursor()
|
| 15 |
+
|
| 16 |
+
# Listar tablas de usuario (evitar tablas internas de SQLite)
|
| 17 |
+
cur.execute(
|
| 18 |
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
| 19 |
+
)
|
| 20 |
+
tables = [row[0] for row in cur.fetchall()]
|
| 21 |
+
|
| 22 |
+
if not tables:
|
| 23 |
+
print(" (No hay tablas en esta base de datos)")
|
| 24 |
+
else:
|
| 25 |
+
for table in tables:
|
| 26 |
+
print(f"\n Tabla: {table}")
|
| 27 |
+
cur.execute(f"PRAGMA table_info({table})")
|
| 28 |
+
columns = cur.fetchall()
|
| 29 |
+
if not columns:
|
| 30 |
+
print(" (Sin columnas o tabla vacía)")
|
| 31 |
+
else:
|
| 32 |
+
print(" Columnas:")
|
| 33 |
+
for cid, name, col_type, notnull, dflt_value, pk in columns:
|
| 34 |
+
pk_flag = " PK" if pk else ""
|
| 35 |
+
nn_flag = " NOT NULL" if notnull else ""
|
| 36 |
+
print(f" - {name} ({col_type or 'TYPE?'}{nn_flag}{pk_flag})")
|
| 37 |
+
|
| 38 |
+
conn.close()
|
| 39 |
+
print()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def main() -> None:
|
| 43 |
+
if not DB_DIR.exists():
|
| 44 |
+
raise FileNotFoundError(f"Directorio de bases de datos no encontrado: {DB_DIR}")
|
| 45 |
+
|
| 46 |
+
db_files = sorted(DB_DIR.glob("*.db"))
|
| 47 |
+
|
| 48 |
+
if not db_files:
|
| 49 |
+
print(f"No se encontraron archivos .db en {DB_DIR}")
|
| 50 |
+
return
|
| 51 |
+
|
| 52 |
+
for db_path in db_files:
|
| 53 |
+
list_db_columns(db_path)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
if __name__ == "__main__":
|
| 57 |
+
main()
|