VeuReu commited on
Commit
4208190
·
verified ·
1 Parent(s): 5d81375

Upload 15 files

Browse files
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, log_event
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
- log_event(
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
- log_event(
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={'sí' if digest_hash else 'no'} "
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 _connect_events_db() as econn:
226
- for row in econn.execute(
227
- "SELECT DISTINCT phone FROM events WHERE session = ? AND phone IS NOT NULL AND phone != ''",
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 log_event(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/events.db.
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
- # Mode únic: registrar en demo/temp/events.db
726
- with _connect_events_db() as conn:
727
- conn.execute(
728
- """INSERT INTO events
729
- (timestamp, session, ip, user, password, phone, action, sha1sum, visibility)
730
- VALUES (?,?,?,?,?,?,?,?,?)""",
731
- (
732
- ts,
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 has_video_approval_event(sha1sum: str) -> bool:
746
- """Comprova si existeix un esdeveniment d'aprovació de vídeo per a un sha1sum.
747
 
748
- Busca a demo/temp/events.db una fila amb action='video approval' i el sha1sum
749
  especificat.
750
  """
751
 
@@ -753,10 +1056,10 @@ def has_video_approval_event(sha1sum: str) -> bool:
753
  return False
754
 
755
  try:
756
- with _connect_events_db() as conn:
757
  cur = conn.execute(
758
- "SELECT 1 FROM events WHERE action = ? AND sha1sum = ? LIMIT 1",
759
- ("video approval", sha1sum),
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
- log_event,
 
 
 
 
 
 
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(2)
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
- free_ad_txt_path = _find_best_file_for_version(vid_dir, subcarpeta_seleccio, "free_ad.txt")
 
 
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 OK"
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
- # Registrar esdeveniment a events.db amb hash del contingut
283
  try:
284
- user_obj = st.session_state.get("user") or {}
285
- role = user_obj.get("role") if isinstance(user_obj, dict) else None
286
-
287
- # Només registrar per a usuaris verd o groc
288
- if role in ("verd", "groc"):
289
- file_hash = hashlib.sha1(mp3_bytes).hexdigest()
290
- session_id = st.session_state.get("session_id", "")
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
- log_event(
301
- session=session_id or "",
302
- ip=ip or "",
303
- user=username or "",
304
- password=password or "",
305
- phone=phone or "",
306
- action="Free AD generated",
307
- sha1sum=file_hash,
308
- visibility=None,
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
- une_srt_path = _find_best_file_for_version(vid_dir, subcarpeta_seleccio, "une_ad.srt")
 
 
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 OK"
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
- # Registrar esdeveniment a events.db amb hash del contingut
349
  try:
350
- user_obj = st.session_state.get("user") or {}
351
- role = user_obj.get("role") if isinstance(user_obj, dict) else None
352
-
353
- # Només registrar per a usuaris verd o groc
354
- if role in ("verd", "groc"):
355
- file_hash = hashlib.sha1(video_bytes).hexdigest()
356
- session_id = st.session_state.get("session_id", "")
357
- ip = st.session_state.get("client_ip", "")
358
- username = user_obj.get("username") if isinstance(user_obj, dict) else str(user_obj or "")
359
- password = st.session_state.get("last_password", "")
360
- phone = (
361
- st.session_state.get("sms_phone_verified")
362
- or st.session_state.get("sms_phone")
363
- or ""
364
  )
 
 
365
 
366
- log_event(
367
- session=session_id or "",
368
- ip=ip or "",
369
- user=username or "",
370
- password=password or "",
371
- phone=phone or "",
372
- action="UNE AD generated",
373
- sha1sum=file_hash,
374
- visibility=None,
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
- # Reproducir el MP3 existente en videos/<video-seleccionat>/<versió>/free_ad.mp3
461
  if subcarpeta_seleccio:
462
- mp3_path = _find_best_file_for_version(vid_dir, subcarpeta_seleccio, "free_ad.mp3")
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
- log_event(
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
- password=password or "",
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 log_event, has_video_approval_event, upsert_audiodescription_text
 
 
 
 
 
 
 
 
 
 
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
- # Registre d'esdeveniment de pujada de vídeo a events.db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- log_event(
 
 
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 esdeveniment de pujada: {e}")
405
 
406
- # Si treballem en mode external, enviar el vídeo a pending_videos de l'engine
 
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
- try:
432
- compliance_client.notify_video_upload(
433
- video_name=uploaded_file.name,
434
- sha1sum=sha1,
435
- )
436
- except Exception as sms_exc:
437
- print(f"[VIDEO SMS] Error enviant notificació al validor: {sms_exc}")
 
 
 
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 has_video_approval_event(current_sha1):
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 has_video_approval_event(current_sha1):
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
- # Simular enviament d'SMS (la integració real es farà via servei extern)
1752
- _log(
1753
- f"[UNE SMS] Enviant SMS de validació UNE a {une_phone_validator} "
1754
- f"per al vídeo {sha1}"
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 correctament (Salamandra/MoE).")
1775
- result_placeholder.success("🎉 Procés completat. Pots revisar les versions a la pantalla d'anàlisi d'audiodescripcions.")
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
- log_event,
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 esdeveniment "video approval" a events.db
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
- log_event(
212
  session=session_id,
213
- ip=client_ip,
214
  user=username or "",
215
- password=password,
216
  phone=phone,
217
- action="video approval",
218
- sha1sum=video_seleccionat["sha1sum"],
219
- visibility=None,
220
  )
221
- except Exception as e:
222
- st.warning(f"⚠️ No s'ha pogut registrar l'esdeveniment d'aprovació: {e}")
223
 
224
  if success:
225
- st.success("✅ Vídeo acceptat, registrat al servei de compliance i marcat com aprovat a events.db")
226
  else:
227
  st.error("❌ Error registrant el veredicte al servei de compliance")
228
 
229
- # 3) En mode external, moure el vídeo de temp/pending_videos a temp/media
230
- if data_origin == "external":
231
- sha1 = video_seleccionat["sha1sum"]
232
- local_pending_dir = pending_root / sha1
233
- local_media_dir = base_media_dir / sha1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  try:
235
- local_media_dir.mkdir(parents=True, exist_ok=True)
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
- pass
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Llistar vídeos la darrera acció dels quals a events.db sigui "Waiting for UNE validation"
261
- # i permetre validar l'audiodescripció (UNE + narració lliure).
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 una audiodescripció per validar:",
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
- # Preferim la versió MoE si existeix; si no, Salamandra
313
- selected_version = None
314
- for v in ("MoE", "Salamandra"):
315
- rows = get_audiodescription_history(sha1, v)
316
  if rows:
317
- selected_version = v
318
- ad_rows = rows
319
- break
320
 
321
- if not selected_version:
322
  st.error("No s'ha trobat cap audiodescripció per a aquest vídeo a la base de dades.")
323
  else:
324
- # Agafem la darrera fila per a aquesta versió
325
- row_ad = ad_rows[-1]
 
 
 
 
 
 
 
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, com abans)
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/TEST per a aquest vídeo/versió
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) Registrar event 'Validated AD UNE-153020'
 
 
 
 
 
 
394
  try:
395
- log_event(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  session=session_id or "",
397
- ip=st.session_state.get("client_ip", "") or "",
398
  user=username or "",
399
- password=st.session_state.get("last_password", "") or "",
400
- phone=st.session_state.get("sms_phone_verified")
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 as e_evt:
408
- _log(f"[UNE validation] Error registrant event Validated AD UNE-153020: {e_evt}")
409
 
410
- st.success("✅ Audiodescripció UNE-153010 validada i desada (OK/TEST).")
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
- # --- 3) Nous vídeos a videos.db associats a la sessió ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = ?", (session_id,)
 
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()