VeuReu commited on
Commit
3df3b6c
·
verified ·
1 Parent(s): c93c30a

Upload 10 files

Browse files
Files changed (5) hide show
  1. app.py +1 -1
  2. config.yaml +1 -1
  3. databases.py +8 -8
  4. page_modules/process_video.py +305 -268
  5. persistent_data_gate.py +35 -23
app.py CHANGED
@@ -115,7 +115,7 @@ ensure_dirs(DATA_DIR)
115
  base_dir = Path(__file__).parent
116
  ensure_temp_databases(base_dir, api)
117
 
118
- DB_PATH = os.path.join(DATA_DIR, "users.db")
119
  set_db_path(DB_PATH)
120
 
121
  # Configurar si els esdeveniments s'han de registrar a SQLite o a AWS QLDB
 
115
  base_dir = Path(__file__).parent
116
  ensure_temp_databases(base_dir, api)
117
 
118
+ DB_PATH = os.path.join(DATA_DIR, "db", "users.db")
119
  set_db_path(DB_PATH)
120
 
121
  # Configurar si els esdeveniments s'han de registrar a SQLite o a AWS QLDB
config.yaml CHANGED
@@ -2,7 +2,7 @@
2
  # Title and basic app behaviour.
3
  app:
4
  title: "Veureu AD"
5
- data_origin: "external" # Where data comes from: "internal" or "external".
6
  manual_validation_enabled: false # If true, require manual validation steps in the UI.
7
 
8
  ## Engine API connection
 
2
  # Title and basic app behaviour.
3
  app:
4
  title: "Veureu AD"
5
+ data_origin: "internal" # Where data comes from: "internal" or "external".
6
  manual_validation_enabled: false # If true, require manual validation steps in the UI.
7
 
8
  ## Engine API connection
databases.py CHANGED
@@ -14,19 +14,19 @@ DEFAULT_DB_PATH = None # set by set_db_path at runtime
14
  USE_BLOCKCHAIN_FOR_EVENTS = False
15
 
16
  # Ruta a la base de dades de feedback agregat (separa de users.db)
17
- FEEDBACK_DB_PATH = Path(__file__).resolve().parent / "temp" / "feedback.db"
18
 
19
  # Ruta a la base de dades de captions per als scores
20
- CAPTIONS_DB_PATH = Path(__file__).resolve().parent / "temp" / "captions.db"
21
 
22
- # Ruta a la base de dades d'esdeveniments (events.db) a demo/temp
23
- EVENTS_DB_PATH = Path(__file__).resolve().parent / "temp" / "events.db"
24
 
25
- # Ruta a la base de dades de vídeos (videos.db) a demo/temp
26
- VIDEOS_DB_PATH = Path(__file__).resolve().parent / "temp" / "videos.db"
27
 
28
- # Ruta a la base de dades d'audiodescripcions (audiodescriptions.db) a demo/temp
29
- AUDIODESCRIPTIONS_DB_PATH = Path(__file__).resolve().parent / "temp" / "audiodescriptions.db"
30
 
31
 
32
  def set_db_path(db_path: str):
 
14
  USE_BLOCKCHAIN_FOR_EVENTS = False
15
 
16
  # Ruta a la base de dades de feedback agregat (separa de users.db)
17
+ FEEDBACK_DB_PATH = Path(__file__).resolve().parent / "temp" / "db" / "feedback.db"
18
 
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):
page_modules/process_video.py CHANGED
@@ -24,7 +24,7 @@ from persistent_data_gate import ensure_temp_databases, _load_data_origin
24
 
25
 
26
  def get_all_catalan_names():
27
- """Return the predefined lists of common Catalan names (male and female)."""
28
  noms_home = ["Jordi", "Marc", "Pau", "Pere", "Joan", "Josep", "David", "Àlex", "Guillem", "Albert",
29
  "Arnau", "Martí", "Bernat", "Oriol", "Roger", "Pol", "Lluís", "Sergi", "Carles", "Xavier"]
30
  noms_dona = ["Maria", "Anna", "Laura", "Marta", "Cristina", "Núria", "Montserrat", "Júlia", "Sara", "Carla",
@@ -33,7 +33,7 @@ def get_all_catalan_names():
33
 
34
 
35
  def _log(msg: str) -> None:
36
- """Logging helper to stderr with timestamp (kept consistent with auth.py)."""
37
  ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
38
  sys.stderr.write(f"[{ts}] {msg}\n")
39
  sys.stderr.flush()
@@ -97,7 +97,7 @@ def _get_video_duration(path: str) -> float:
97
  except FileNotFoundError:
98
  pass
99
 
100
- # Last resort: try with OpenCV if it is available
101
  try:
102
  import cv2
103
 
@@ -142,7 +142,7 @@ def _transcode_video(input_path: str, output_path: str, max_duration: int | None
142
  def render_process_video_page(api, backend_base_url: str) -> None:
143
  st.header("Processar un nou clip de vídeo")
144
 
145
- # Read config.yaml (app flags and media limits)
146
  base_dir = Path(__file__).parent.parent
147
  config_path = base_dir / "config.yaml"
148
  manual_validation_enabled = True
@@ -156,13 +156,13 @@ def render_process_video_page(api, backend_base_url: str) -> None:
156
  manual_validation_enabled = bool(app_cfg.get("manual_validation_enabled", True))
157
 
158
  media_cfg = cfg.get("media", {}) or {}
159
- # Configurable limits for file size and video duration
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
 
165
- # CSS to stabilize carousels and avoid layout vibration
166
  st.markdown("""
167
  <style>
168
  /* Contenedor de imagen con aspect ratio fijo para evitar saltos */
@@ -272,11 +272,11 @@ def render_process_video_page(api, backend_base_url: str) -> None:
272
  if "video_validation_approved" not in st.session_state:
273
  st.session_state.video_validation_approved = False
274
 
275
- # --- 1. Video upload ---
276
  MAX_SIZE_MB = max_size_mb
277
  MAX_DURATION_S = max_duration_s
278
 
279
- # Visibility selector (private/public) shown to the right of the uploader
280
  if "video_visibility" not in st.session_state:
281
  st.session_state.video_visibility = "Privat"
282
 
@@ -305,7 +305,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
305
  )
306
 
307
  if uploaded_file is not None:
308
- # Reset session state if a new file is uploaded
309
  if st.session_state.video_uploaded is None or uploaded_file.name != st.session_state.video_uploaded.get(
310
  "original_name"
311
  ):
@@ -373,7 +373,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
373
  }
374
  )
375
 
376
- # Register video upload event into events.db
377
  try:
378
  session_id = st.session_state.get("session_id", "")
379
  ip = st.session_state.get("client_ip", "")
@@ -403,7 +403,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
403
  except Exception as e:
404
  print(f"[events] Error registrant esdeveniment de pujada: {e}")
405
 
406
- # If working in "external" mode, send the video to engine pending_videos
407
  try:
408
  base_dir = Path(__file__).parent.parent
409
  data_origin = _load_data_origin(base_dir)
@@ -411,11 +411,11 @@ def render_process_video_page(api, backend_base_url: str) -> None:
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
- # Save local copy of the pending video
415
  with local_pending_path.open("wb") as f_pending:
416
  f_pending.write(video_bytes)
417
 
418
- # Send the video to the backend engine so it appears in the pending list
419
  try:
420
  resp_pending = api.upload_pending_video(video_bytes, uploaded_file.name)
421
  _log(f"[pending_videos] upload_pending_video resp: {resp_pending}")
@@ -424,7 +424,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
424
  except Exception as e_ext:
425
  _log(f"[pending_videos] Error bloc exterior upload_pending_video: {e_ext}")
426
 
427
- # Mark validation state according to security/validation configuration
428
  if manual_validation_enabled:
429
  st.session_state.video_requires_validation = True
430
  st.session_state.video_validation_approved = False
@@ -434,9 +434,9 @@ def render_process_video_page(api, backend_base_url: str) -> None:
434
  sha1sum=sha1,
435
  )
436
  except Exception as sms_exc:
437
- print(f"[VIDEO SMS] Error sending notification to validator: {sms_exc}")
438
  else:
439
- # Without manual validation: consider it automatically approved
440
  st.session_state.video_requires_validation = False
441
  st.session_state.video_validation_approved = True
442
 
@@ -452,7 +452,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
452
  if manual_validation_enabled and st.session_state.get("video_requires_validation") and not st.session_state.get("video_validation_approved"):
453
  st.info("Per favor, espera a la revisió humana del vídeo.")
454
 
455
- # Check if there is a video approval event in events.db for the current sha1sum
456
  current_sha1 = None
457
  if st.session_state.get("video_uploaded"):
458
  current_sha1 = st.session_state.video_uploaded.get("sha1sum")
@@ -460,8 +460,8 @@ def render_process_video_page(api, backend_base_url: str) -> None:
460
  if has_video_approval_event(current_sha1):
461
  st.session_state.video_validation_approved = True
462
 
463
- # We can only continue with casting if the video does not require validation
464
- # or if it has already been marked as approved.
465
  can_proceed_casting = (
466
  st.session_state.get("video_uploaded") is not None
467
  and (
@@ -470,8 +470,8 @@ def render_process_video_page(api, backend_base_url: str) -> None:
470
  )
471
  )
472
 
473
- # --- 2. Detection form with sliders ---
474
- # Only shown once there is an uploaded video and it is validated (if validation is required).
475
  if can_proceed_casting:
476
  st.markdown("---")
477
 
@@ -512,7 +512,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
512
  msg_ad.empty()
513
  try:
514
  v = st.session_state.video_uploaded
515
- # Reset detection-related state before starting
516
  st.session_state.scene_clusters = None
517
  st.session_state.scene_detection_done = False
518
  st.session_state.detect_done = False
@@ -554,7 +554,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
554
  msg_detect.error("El processament ha fallat al servidor.")
555
  break
556
 
557
- # Success: collect results from engine job
558
  res = stt.get("results", {})
559
  chars = res.get("characters", [])
560
  fl = res.get("face_labels", [])
@@ -582,7 +582,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
582
  else:
583
  msg_detect.info("No s'han detectat cares en aquest vídeo.")
584
 
585
- # Detect scenes based on configured parameters
586
  try:
587
  scene_out = api.detect_scenes(
588
  video_bytes=v["bytes"],
@@ -611,8 +611,8 @@ def render_process_video_page(api, backend_base_url: str) -> None:
611
  except Exception as e:
612
  msg_detect.error(f"Error inesperat: {e}")
613
 
614
- # Button to manually refresh the validation status of the video
615
- # Only shown while the video is waiting for human validation
616
  if (
617
  st.session_state.get("video_uploaded")
618
  and st.session_state.get("video_requires_validation")
@@ -623,7 +623,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
623
  st.caption("⏳ Vídeo pendent de validació humana.")
624
  with col_refresh:
625
  if st.button("🔄 Actualitzar estat de validació", key="refresh_video_validation"):
626
- # Re-synchronise temporary DBs (including events.db) from the origin
627
  try:
628
  base_dir = Path(__file__).parent.parent
629
  api_client = st.session_state.get("api_client")
@@ -638,7 +638,7 @@ def render_process_video_page(api, backend_base_url: str) -> None:
638
  else:
639
  st.info("Encara no s'ha registrat cap aprovació per a aquest vídeo.")
640
 
641
- # --- 3. Face carousels ---
642
  if st.session_state.get("characters_detected") is not None:
643
  st.markdown("---")
644
  n_face_clusters = len(st.session_state.get("characters_detected") or [])
@@ -1031,202 +1031,298 @@ def render_process_video_page(api, backend_base_url: str) -> None:
1031
  import traceback
1032
  traceback.print_exc()
1033
 
1034
- # --- 6. Combined characters (faces + voices) ---
1035
  if st.session_state.get("detect_done"):
1036
  st.markdown("---")
1037
- st.subheader("👥 Personatges")
1038
-
1039
- def normalize_name(name: str) -> str:
1040
- import unicodedata
1041
- name_upper = name.upper()
1042
- name_normalized = ''.join(
1043
- c for c in unicodedata.normalize('NFD', name_upper)
1044
- if unicodedata.category(c) != 'Mn'
1045
- )
1046
- return name_normalized
1047
-
1048
- characters_detected = st.session_state.get("characters_detected") or []
1049
-
1050
- chars_payload: list[dict[str, Any]] = []
1051
- for idx, ch in enumerate(characters_detected):
1052
- try:
1053
- folder_name = Path(ch.get("folder") or "").name
1054
- except Exception:
1055
- folder_name = ""
1056
- char_id = ch.get("id") or folder_name or f"char{idx+1}"
1057
-
1058
- def _safe_key(value: str) -> str:
1059
- key = re.sub(r"[^0-9a-zA-Z_]+", "_", value or "")
1060
- return key or f"cluster_{idx+1}"
1061
-
1062
- key_prefix = _safe_key(f"char_{idx+1}_{char_id}")
1063
- name = st.session_state.get(f"{key_prefix}_name") or ch.get("name") or f"Cluster {idx+1}"
1064
- name_normalized = normalize_name(name)
1065
- desc = st.session_state.get(f"{key_prefix}_desc", "").strip()
1066
- chars_payload.append({
1067
- "id": char_id,
1068
- "name": name,
1069
- "name_normalized": name_normalized,
1070
- "face_key_prefix": key_prefix,
1071
- "face_files": ch.get("face_files") or [],
1072
- "char_data": ch,
1073
- "description": desc,
1074
- "folder": ch.get("folder"),
1075
- })
1076
-
1077
- used_names_home = []
1078
- used_names_dona = []
1079
- noms_home_all, noms_dona_all = get_all_catalan_names()
1080
- for cp in chars_payload:
1081
- face_name = cp.get("name", "")
1082
- if face_name in noms_home_all:
1083
- used_names_home.append(face_name)
1084
- elif face_name in noms_dona_all:
1085
- used_names_dona.append(face_name)
1086
-
1087
- segs = st.session_state.get("audio_segments") or []
1088
- vlabels = st.session_state.get("voice_labels") or []
1089
- vname = st.session_state.get("video_name_from_engine") or ""
1090
- voice_clusters_by_name: dict[str, dict[str, Any]] = {}
1091
-
1092
- for i, seg in enumerate(segs):
1093
- lbl = vlabels[i] if i < len(vlabels) else -1
1094
- if not (isinstance(lbl, int) and lbl >= 0):
1095
- continue
1096
-
1097
- vpref = f"voice_{int(lbl):02d}"
1098
- default_voice_name = get_catalan_name_for_speaker(int(lbl), used_names_home, used_names_dona)
1099
- voice_name = st.session_state.get(f"{vpref}_name") or default_voice_name
1100
- voice_desc = st.session_state.get(f"{vpref}_desc", "").strip()
1101
- voice_norm = normalize_name(voice_name)
1102
-
1103
- clip_local = seg.get("clip_path")
1104
- fname = os.path.basename(clip_local) if clip_local else None
1105
- if not fname:
1106
- continue
1107
-
1108
- voice_clusters_by_name.setdefault(voice_norm, {
1109
- "voice_key_prefix": vpref,
1110
- "clips": [],
1111
- "label": int(lbl),
1112
- "original_name": voice_name,
1113
- "description": voice_desc,
1114
- })
1115
- voice_clusters_by_name[voice_norm]["clips"].append(fname)
1116
-
1117
- all_normalized_names = set([c["name_normalized"] for c in chars_payload] + list(voice_clusters_by_name.keys()))
1118
-
1119
- for pidx, norm_name in enumerate(sorted(all_normalized_names)):
1120
- face_items = [c for c in chars_payload if c["name_normalized"] == norm_name]
1121
- voice_data = voice_clusters_by_name.get(norm_name)
1122
-
1123
- display_name = face_items[0]["name"] if face_items else (voice_data["original_name"] if voice_data else norm_name)
1124
-
1125
- descriptions: list[str] = []
1126
- for face_item in face_items:
1127
- if face_item["description"]:
1128
- descriptions.append(face_item["description"])
1129
- if voice_data and voice_data.get("description"):
1130
- descriptions.append(voice_data["description"])
1131
- combined_description = "\n".join(descriptions) if descriptions else ""
1132
-
1133
- st.markdown(f"**{pidx+1}. {display_name}**")
1134
-
1135
- all_faces = []
1136
- for face_item in face_items:
1137
- all_faces.extend(face_item["face_files"])
1138
-
1139
- face_data = face_items[0] if face_items else None
1140
-
1141
- col_faces, col_voices, col_text = st.columns([1, 1, 1.5])
1142
-
1143
- with col_faces:
1144
- if all_faces:
1145
- carousel_key = f"combined_face_{pidx}"
1146
- if f"{carousel_key}_idx" not in st.session_state:
1147
- st.session_state[f"{carousel_key}_idx"] = 0
1148
- cur = st.session_state[f"{carousel_key}_idx"]
1149
- if cur >= len(all_faces):
1150
- cur = 0
1151
- st.session_state[f"{carousel_key}_idx"] = cur
1152
- fname = all_faces[cur]
1153
- ch = face_data["char_data"] if face_data else {}
1154
- if fname.startswith("/files/"):
1155
- img_url = f"{backend_base_url}{fname}"
1156
- else:
1157
- base = ch.get("image_url") or ""
1158
- base_dir = "/".join((base or "/").split("/")[:-1])
1159
- img_url = f"{backend_base_url}{base_dir}/{fname}" if base_dir else f"{backend_base_url}{fname}"
1160
- st.image(img_url, width=150)
1161
- st.caption(f"Cara {cur+1}/{len(all_faces)}")
1162
- bcol1, bcol2 = st.columns(2)
1163
- with bcol1:
1164
- if st.button("⬅️", key=f"combined_face_prev_{pidx}"):
1165
- st.session_state[f"{carousel_key}_idx"] = (cur - 1) % len(all_faces)
1166
- st.rerun()
1167
- with bcol2:
1168
- if st.button("➡️", key=f"combined_face_next_{pidx}"):
1169
- st.session_state[f"{carousel_key}_idx"] = (cur + 1) % len(all_faces)
1170
- st.rerun()
1171
  else:
1172
- st.info("Sense imatges")
 
 
 
 
1173
 
1174
- with col_voices:
1175
- if voice_data:
1176
- clips = voice_data["clips"]
1177
- if clips:
1178
- carousel_key = f"combined_voice_{pidx}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1179
  if f"{carousel_key}_idx" not in st.session_state:
1180
  st.session_state[f"{carousel_key}_idx"] = 0
1181
  cur = st.session_state[f"{carousel_key}_idx"]
1182
- if cur >= len(clips):
1183
  cur = 0
1184
  st.session_state[f"{carousel_key}_idx"] = cur
1185
- fname = clips[cur]
1186
- audio_url = f"{backend_base_url}/audio/{vname}/{fname}" if (vname and fname) else None
1187
- if audio_url:
1188
- st.audio(audio_url, format="audio/wav")
1189
- st.caption(f"Veu {cur+1}/{len(clips)}")
 
 
 
 
 
1190
  bcol1, bcol2 = st.columns(2)
1191
  with bcol1:
1192
- if st.button("⬅️", key=f"combined_voice_prev_{pidx}"):
1193
- st.session_state[f"{carousel_key}_idx"] = (cur - 1) % len(clips)
1194
  st.rerun()
1195
  with bcol2:
1196
- if st.button("➡️", key=f"combined_voice_next_{pidx}"):
1197
- st.session_state[f"{carousel_key}_idx"] = (cur + 1) % len(clips)
1198
  st.rerun()
1199
  else:
1200
- st.info("Sense clips de veu")
1201
- else:
1202
- st.info("Sense dades de veu")
1203
-
1204
- with col_text:
1205
- combined_name_key = f"combined_char_{pidx}_name"
1206
- combined_desc_key = f"combined_char_{pidx}_desc"
1207
-
1208
- if combined_name_key not in st.session_state:
1209
- st.session_state[combined_name_key] = display_name
1210
- if combined_desc_key not in st.session_state:
1211
- st.session_state[combined_desc_key] = combined_description
1212
-
1213
- st.text_input(
1214
- "Nom del personatge",
1215
- key=combined_name_key,
1216
- label_visibility="collapsed",
1217
- placeholder="Nom del personatge",
1218
- )
1219
- st.text_area(
1220
- "Descripció",
1221
- key=combined_desc_key,
1222
- height=120,
1223
- label_visibility="collapsed",
1224
- placeholder="Descripció del personatge",
1225
- )
1226
-
1227
- # --- 7. Generar audiodescripció ---
1228
- st.markdown("---")
1229
- if st.button("🎬 Generar audiodescripció", type="primary", use_container_width=True):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1230
  v = st.session_state.get("video_uploaded")
1231
  if not v:
1232
  st.error("No hi ha cap vídeo carregat.")
@@ -1246,65 +1342,6 @@ def render_process_video_page(api, backend_base_url: str) -> None:
1246
  base_media_dir = Path(__file__).parent.parent / "temp" / "media" / sha1
1247
  base_media_dir.mkdir(parents=True, exist_ok=True)
1248
 
1249
- # 0) Finalitzar càsting automàticament abans de generar AD
1250
- progress_placeholder.info("⏳ Consolidant personatges i veus...")
1251
- try:
1252
- video_name = v.get("original_filename", "").replace(".mp4", "") or sha1
1253
- characters = st.session_state.get("characters", [])
1254
- voice_labels = st.session_state.get("voice_labels", [])
1255
- audio_segments = st.session_state.get("audio_segments", [])
1256
-
1257
- # Construir payload per finalize_casting
1258
- char_payload = []
1259
- for ch in characters:
1260
- char_id = ch.get("id", "")
1261
- name_key = f"char_name_{char_id}"
1262
- desc_key = f"char_desc_{char_id}"
1263
- kept_key = f"char_kept_{char_id}"
1264
- char_payload.append({
1265
- "id": char_id,
1266
- "name": st.session_state.get(name_key, ch.get("name", f"Cluster {char_id}")),
1267
- "description": st.session_state.get(desc_key, ch.get("description", "")),
1268
- "folder": ch.get("folder", ""),
1269
- "kept_files": st.session_state.get(kept_key, ch.get("face_files", [])),
1270
- })
1271
-
1272
- # Construir voice_clusters
1273
- voice_clusters = []
1274
- unique_speakers = sorted(set(voice_labels)) if voice_labels else []
1275
- for spk in unique_speakers:
1276
- vname_key = f"voice_name_{spk}"
1277
- vdesc_key = f"voice_desc_{spk}"
1278
- clips = [seg for i, seg in enumerate(audio_segments) if i < len(voice_labels) and voice_labels[i] == spk]
1279
- voice_clusters.append({
1280
- "label": spk,
1281
- "name": st.session_state.get(vname_key, spk),
1282
- "description": st.session_state.get(vdesc_key, ""),
1283
- "clips": [c.get("clip_path", "") for c in clips],
1284
- })
1285
-
1286
- payload = {
1287
- "video_name": video_name,
1288
- "characters": char_payload,
1289
- "voice_clusters": voice_clusters,
1290
- }
1291
-
1292
- fin_resp = api.finalize_casting(payload)
1293
- _log(f"[finalize] finalize_casting resp: {fin_resp}")
1294
-
1295
- # Carregar índexs Chroma
1296
- load_resp = api.load_casting(
1297
- faces_dir="identities/faces",
1298
- voices_dir="identities/voices",
1299
- db_dir="chroma_db",
1300
- drop_collections=False
1301
- )
1302
- _log(f"[load] load_casting resp: {load_resp}")
1303
-
1304
- except Exception as e_fin:
1305
- _log(f"[finalize] Error en finalize_casting: {e_fin}")
1306
- # Continuem encara que falli
1307
-
1308
  # 1) Carregar i enviar el casting_json com a embeddings al engine
1309
  casting_json = None
1310
  try:
 
24
 
25
 
26
  def get_all_catalan_names():
27
+ """Retorna tots els noms catalans disponibles."""
28
  noms_home = ["Jordi", "Marc", "Pau", "Pere", "Joan", "Josep", "David", "Àlex", "Guillem", "Albert",
29
  "Arnau", "Martí", "Bernat", "Oriol", "Roger", "Pol", "Lluís", "Sergi", "Carles", "Xavier"]
30
  noms_dona = ["Maria", "Anna", "Laura", "Marta", "Cristina", "Núria", "Montserrat", "Júlia", "Sara", "Carla",
 
33
 
34
 
35
  def _log(msg: str) -> None:
36
+ """Helper de logging a stderr amb timestamp (coherent amb auth.py)."""
37
  ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
38
  sys.stderr.write(f"[{ts}] {msg}\n")
39
  sys.stderr.flush()
 
97
  except FileNotFoundError:
98
  pass
99
 
100
+ # Últim recurs: intentar amb OpenCV si està disponible
101
  try:
102
  import cv2
103
 
 
142
  def render_process_video_page(api, backend_base_url: str) -> None:
143
  st.header("Processar un nou clip de vídeo")
144
 
145
+ # Llegir config.yaml (flags d'app i límits de media)
146
  base_dir = Path(__file__).parent.parent
147
  config_path = base_dir / "config.yaml"
148
  manual_validation_enabled = True
 
156
  manual_validation_enabled = bool(app_cfg.get("manual_validation_enabled", True))
157
 
158
  media_cfg = cfg.get("media", {}) or {}
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
 
165
+ # CSS para estabilizar carruseles y evitar vibración del layout
166
  st.markdown("""
167
  <style>
168
  /* Contenedor de imagen con aspect ratio fijo para evitar saltos */
 
272
  if "video_validation_approved" not in st.session_state:
273
  st.session_state.video_validation_approved = False
274
 
275
+ # --- 1. Subida del vídeo ---
276
  MAX_SIZE_MB = max_size_mb
277
  MAX_DURATION_S = max_duration_s
278
 
279
+ # Selector de visibilitat (privat/públic), a la dreta del uploader
280
  if "video_visibility" not in st.session_state:
281
  st.session_state.video_visibility = "Privat"
282
 
 
305
  )
306
 
307
  if uploaded_file is not None:
308
+ # Resetear el estado si se sube un nuevo archivo
309
  if st.session_state.video_uploaded is None or uploaded_file.name != st.session_state.video_uploaded.get(
310
  "original_name"
311
  ):
 
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", "")
 
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)
 
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)
421
  _log(f"[pending_videos] upload_pending_video resp: {resp_pending}")
 
424
  except Exception as e_ext:
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
 
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
 
 
452
  if manual_validation_enabled and st.session_state.get("video_requires_validation") and not st.session_state.get("video_validation_approved"):
453
  st.info("Per favor, espera a la revisió humana del vídeo.")
454
 
455
+ # Comprovar si hi ha aprovació de vídeo a events.db per al sha1sum actual
456
  current_sha1 = None
457
  if st.session_state.get("video_uploaded"):
458
  current_sha1 = st.session_state.video_uploaded.get("sha1sum")
 
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ó
464
+ # o si ja ha estat marcat com a validat.
465
  can_proceed_casting = (
466
  st.session_state.get("video_uploaded") is not None
467
  and (
 
470
  )
471
  )
472
 
473
+ # --- 2. Form de detecció amb sliders ---
474
+ # Només es mostra quan ja hi ha un vídeo pujat **i** està validat (si cal validació).
475
  if can_proceed_casting:
476
  st.markdown("---")
477
 
 
512
  msg_ad.empty()
513
  try:
514
  v = st.session_state.video_uploaded
515
+ # Reset estat abans de començar
516
  st.session_state.scene_clusters = None
517
  st.session_state.scene_detection_done = False
518
  st.session_state.detect_done = False
 
554
  msg_detect.error("El processament ha fallat al servidor.")
555
  break
556
 
557
+ # Success
558
  res = stt.get("results", {})
559
  chars = res.get("characters", [])
560
  fl = res.get("face_labels", [])
 
582
  else:
583
  msg_detect.info("No s'han detectat cares en aquest vídeo.")
584
 
585
+ # Detect scenes
586
  try:
587
  scene_out = api.detect_scenes(
588
  video_bytes=v["bytes"],
 
611
  except Exception as e:
612
  msg_detect.error(f"Error inesperat: {e}")
613
 
614
+ # Botó per actualitzar manualment l'estat de validació del vídeo
615
+ # Només es mostra mentre el vídeo està pendent de validació humana
616
  if (
617
  st.session_state.get("video_uploaded")
618
  and st.session_state.get("video_requires_validation")
 
623
  st.caption("⏳ Vídeo pendent de validació humana.")
624
  with col_refresh:
625
  if st.button("🔄 Actualitzar estat de validació", key="refresh_video_validation"):
626
+ # Re-sincronitzar BDs temp (inclosa events.db) des de l'origen
627
  try:
628
  base_dir = Path(__file__).parent.parent
629
  api_client = st.session_state.get("api_client")
 
638
  else:
639
  st.info("Encara no s'ha registrat cap aprovació per a aquest vídeo.")
640
 
641
+ # --- 3. Carruseles de cares ---
642
  if st.session_state.get("characters_detected") is not None:
643
  st.markdown("---")
644
  n_face_clusters = len(st.session_state.get("characters_detected") or [])
 
1031
  import traceback
1032
  traceback.print_exc()
1033
 
1034
+ # --- 6. Confirmación de casting y personajes combinados ---
1035
  if st.session_state.get("detect_done"):
1036
  st.markdown("---")
1037
+ colc1, colc2 = st.columns([1,1])
1038
+ with colc1:
1039
+ if st.button("Confirmar càsting definitiu", type="primary"):
1040
+ chars_payload = []
1041
+ for idx, ch in enumerate(st.session_state.characters_detected or []):
1042
+ try:
1043
+ folder_name = Path(ch.get("folder") or "").name
1044
+ except Exception:
1045
+ folder_name = ""
1046
+ char_id = ch.get("id") or folder_name or f"char{idx+1}"
1047
+ def _safe_key(s: str) -> str:
1048
+ k = re.sub(r"[^0-9a-zA-Z_]+", "_", s or "")
1049
+ return k or f"cluster_{idx+1}"
1050
+ key_prefix = _safe_key(f"char_{idx+1}_{char_id}")
1051
+ name = st.session_state.get(f"{key_prefix}_name") or ch.get("name") or f"Personatge {idx+1}"
1052
+ desc = st.session_state.get(f"{key_prefix}_desc", "")
1053
+ faces_all = ch.get("face_files") or []
1054
+ discard = st.session_state.get(f"{key_prefix}_discard", set())
1055
+ kept = [f for f in faces_all if f and f not in discard]
1056
+ chars_payload.append({
1057
+ "id": char_id,
1058
+ "name": name,
1059
+ "description": desc,
1060
+ "folder": ch.get("folder"),
1061
+ "kept_files": kept,
1062
+ })
1063
+
1064
+ used_names_home_fin = []
1065
+ used_names_dona_fin = []
1066
+ noms_home_all, noms_dona_all = get_all_catalan_names()
1067
+ for cp in chars_payload:
1068
+ face_name = cp.get("name", "")
1069
+ if face_name in noms_home_all:
1070
+ used_names_home_fin.append(face_name)
1071
+ elif face_name in noms_dona_all:
1072
+ used_names_dona_fin.append(face_name)
1073
+
1074
+ segs = st.session_state.audio_segments or []
1075
+ vlabels = st.session_state.voice_labels or []
1076
+ vname = st.session_state.video_name_from_engine
1077
+ voice_clusters = {}
1078
+ for i, seg in enumerate(segs):
1079
+ lbl = vlabels[i] if i < len(vlabels) else -1
1080
+ # Només considerem clústers de veu amb etiqueta vàlida (enter >= 0)
1081
+ if not (isinstance(lbl, int) and lbl >= 0):
1082
+ continue
1083
+ clip_local = seg.get("clip_path")
1084
+ fname = os.path.basename(clip_local) if clip_local else None
1085
+ if fname:
1086
+ default_voice_name = get_catalan_name_for_speaker(int(lbl), used_names_home_fin, used_names_dona_fin)
1087
+ voice_clusters.setdefault(lbl, {"label": lbl, "name": default_voice_name, "description": "", "clips": []})
1088
+ vpref = f"voice_{int(lbl):02d}"
1089
+ vname_custom = st.session_state.get(f"{vpref}_name")
1090
+ vdesc_custom = st.session_state.get(f"{vpref}_desc")
1091
+ if vname_custom:
1092
+ voice_clusters[lbl]["name"] = vname_custom
1093
+ if vdesc_custom is not None:
1094
+ voice_clusters[lbl]["description"] = vdesc_custom
1095
+ voice_clusters[lbl]["clips"].append(fname)
1096
+
1097
+ payload = {
1098
+ "video_name": vname,
1099
+ "base_dir": st.session_state.get("engine_base_dir"),
1100
+ "characters": chars_payload,
1101
+ "voice_clusters": list(voice_clusters.values()),
1102
+ }
1103
+
1104
+ if not payload["video_name"] or not payload["base_dir"]:
1105
+ st.error("Falten dades del vídeo per confirmar el càsting (video_name/base_dir). Torna a processar el vídeo.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1106
  else:
1107
+ with st.spinner("Consolidant càsting al servidor…"):
1108
+ res_fc = api.finalize_casting(payload)
1109
+ if isinstance(res_fc, dict) and res_fc.get("ok"):
1110
+ st.success(f"Càsting consolidat. Identities: {len(res_fc.get('face_identities', []))} cares, {len(res_fc.get('voice_identities', []))} veus.")
1111
+ st.session_state.casting_finalized = True
1112
 
1113
+ # Guardar casting_json localment per a futurs processos (p.ex. audiodescripció)
1114
+ try:
1115
+ casting_json = res_fc.get("casting_json") or {}
1116
+ v = st.session_state.get("video_uploaded") or {}
1117
+ sha1 = v.get("sha1sum")
1118
+ if casting_json and sha1:
1119
+ base_dir = Path(__file__).parent.parent / "temp" / "media" / sha1
1120
+ base_dir.mkdir(parents=True, exist_ok=True)
1121
+ casting_path = base_dir / "casting.json"
1122
+ with casting_path.open("w", encoding="utf-8") as f:
1123
+ json.dump(casting_json, f, ensure_ascii=False, indent=2)
1124
+ except Exception as e:
1125
+ _log(f"[casting_json] Error guardant casting.json: {e}")
1126
+
1127
+ f_id = res_fc.get('face_identities', []) or []
1128
+ v_id = res_fc.get('voice_identities', []) or []
1129
+ c3, c4 = st.columns(2)
1130
+ with c3:
1131
+ st.markdown("**Identitats de cara**")
1132
+ for n in f_id:
1133
+ st.write(f"- {n}")
1134
+ with c4:
1135
+ st.markdown("**Identitats de veu**")
1136
+ for n in v_id:
1137
+ st.write(f"- {n}")
1138
+
1139
+ faces_dir = res_fc.get('faces_dir')
1140
+ voices_dir = res_fc.get('voices_dir')
1141
+ db_dir = res_fc.get('db_dir')
1142
+ with st.spinner("Carregant índexs al cercador (Chroma)…"):
1143
+ load_res = api.load_casting(faces_dir=faces_dir, voices_dir=voices_dir, db_dir=db_dir, drop_collections=True)
1144
+ if isinstance(load_res, dict) and load_res.get('ok'):
1145
+ st.success(f"Índexs carregats: {load_res.get('faces', 0)} cares, {load_res.get('voices', 0)} veus.")
1146
+ else:
1147
+ st.error(f"Error carregant índexs: {load_res}")
1148
+ else:
1149
+ st.error(f"No s'ha pogut consolidar el càsting: {res_fc}")
1150
+
1151
+ # --- Personatges combinats (cares + veus) ---
1152
+ if st.session_state.get("casting_finalized"):
1153
+ st.markdown("---")
1154
+ st.subheader("👥 Personatges")
1155
+
1156
+ def normalize_name(name: str) -> str:
1157
+ import unicodedata
1158
+ name_upper = name.upper()
1159
+ name_normalized = ''.join(
1160
+ c for c in unicodedata.normalize('NFD', name_upper)
1161
+ if unicodedata.category(c) != 'Mn'
1162
+ )
1163
+ return name_normalized
1164
+
1165
+ chars_payload = []
1166
+ for idx, ch in enumerate(st.session_state.characters_detected or []):
1167
+ try:
1168
+ folder_name = Path(ch.get("folder") or "").name
1169
+ except Exception:
1170
+ folder_name = ""
1171
+ char_id = ch.get("id") or folder_name or f"char{idx+1}"
1172
+ def _safe_key(s: str) -> str:
1173
+ k = re.sub(r"[^0-9a-zA-Z_]+", "_", s or "")
1174
+ return k or f"cluster_{idx+1}"
1175
+ key_prefix = _safe_key(f"char_{idx+1}_{char_id}")
1176
+ name = st.session_state.get(f"{key_prefix}_name") or ch.get("name") or f"Personatge {idx+1}"
1177
+ name_normalized = normalize_name(name)
1178
+ desc = st.session_state.get(f"{key_prefix}_desc", "").strip()
1179
+ chars_payload.append({
1180
+ "name": name,
1181
+ "name_normalized": name_normalized,
1182
+ "face_key_prefix": key_prefix,
1183
+ "face_files": ch.get("face_files") or [],
1184
+ "char_data": ch,
1185
+ "description": desc,
1186
+ })
1187
+
1188
+ used_names_home_pers = []
1189
+ used_names_dona_pers = []
1190
+ noms_home_all, noms_dona_all = get_all_catalan_names()
1191
+ for cp in chars_payload:
1192
+ face_name = cp.get("name", "")
1193
+ if face_name in noms_home_all:
1194
+ used_names_home_pers.append(face_name)
1195
+ elif face_name in noms_dona_all:
1196
+ used_names_dona_pers.append(face_name)
1197
+
1198
+ segs = st.session_state.audio_segments or []
1199
+ vlabels = st.session_state.voice_labels or []
1200
+ vname = st.session_state.video_name_from_engine
1201
+ voice_clusters_by_name = {}
1202
+ for i, seg in enumerate(segs):
1203
+ lbl = vlabels[i] if i < len(vlabels) else -1
1204
+ if not (isinstance(lbl, int) and lbl >= 0):
1205
+ continue
1206
+ vpref = f"voice_{int(lbl):02d}"
1207
+ default_voice_name = get_catalan_name_for_speaker(int(lbl), used_names_home_pers, used_names_dona_pers) if isinstance(lbl, int) and lbl >= 0 else f"SPEAKER_{int(lbl):02d}"
1208
+ vname_custom = st.session_state.get(f"{vpref}_name") or default_voice_name
1209
+ vname_normalized = normalize_name(vname_custom)
1210
+ vdesc = st.session_state.get(f"{vpref}_desc", "").strip()
1211
+ clip_local = seg.get("clip_path")
1212
+ fname = os.path.basename(clip_local) if clip_local else None
1213
+ if fname:
1214
+ voice_clusters_by_name.setdefault(vname_normalized, {
1215
+ "voice_key_prefix": vpref,
1216
+ "clips": [],
1217
+ "label": lbl,
1218
+ "original_name": vname_custom,
1219
+ "description": vdesc,
1220
+ })
1221
+ voice_clusters_by_name[vname_normalized]["clips"].append(fname)
1222
+
1223
+ all_normalized_names = set([c["name_normalized"] for c in chars_payload] + list(voice_clusters_by_name.keys()))
1224
+
1225
+ for pidx, norm_name in enumerate(sorted(all_normalized_names)):
1226
+ face_items = [c for c in chars_payload if c["name_normalized"] == norm_name]
1227
+ voice_data = voice_clusters_by_name.get(norm_name)
1228
+
1229
+ display_name = face_items[0]["name"] if face_items else (voice_data["original_name"] if voice_data else norm_name)
1230
+
1231
+ descriptions = []
1232
+ for face_item in face_items:
1233
+ if face_item["description"]:
1234
+ descriptions.append(face_item["description"])
1235
+ if voice_data and voice_data.get("description"):
1236
+ descriptions.append(voice_data["description"])
1237
+
1238
+ combined_description = "\n".join(descriptions) if descriptions else ""
1239
+
1240
+ st.markdown(f"**{pidx+1}. {display_name}**")
1241
+
1242
+ all_faces = []
1243
+ for face_item in face_items:
1244
+ all_faces.extend(face_item["face_files"])
1245
+
1246
+ face_data = face_items[0] if face_items else None
1247
+
1248
+ col_faces, col_voices, col_text = st.columns([1, 1, 1.5])
1249
+
1250
+ with col_faces:
1251
+ if all_faces:
1252
+ carousel_key = f"combined_face_{pidx}"
1253
  if f"{carousel_key}_idx" not in st.session_state:
1254
  st.session_state[f"{carousel_key}_idx"] = 0
1255
  cur = st.session_state[f"{carousel_key}_idx"]
1256
+ if cur >= len(all_faces):
1257
  cur = 0
1258
  st.session_state[f"{carousel_key}_idx"] = cur
1259
+ fname = all_faces[cur]
1260
+ ch = face_data["char_data"] if face_data else {}
1261
+ if fname.startswith("/files/"):
1262
+ img_url = f"{backend_base_url}{fname}"
1263
+ else:
1264
+ base = ch.get("image_url") or ""
1265
+ base_dir = "/".join((base or "/").split("/")[:-1])
1266
+ img_url = f"{backend_base_url}{base_dir}/{fname}" if base_dir else f"{backend_base_url}{fname}"
1267
+ st.image(img_url, width=150)
1268
+ st.caption(f"Cara {cur+1}/{len(all_faces)}")
1269
  bcol1, bcol2 = st.columns(2)
1270
  with bcol1:
1271
+ if st.button("⬅️", key=f"combined_face_prev_{pidx}"):
1272
+ st.session_state[f"{carousel_key}_idx"] = (cur - 1) % len(all_faces)
1273
  st.rerun()
1274
  with bcol2:
1275
+ if st.button("➡️", key=f"combined_face_next_{pidx}"):
1276
+ st.session_state[f"{carousel_key}_idx"] = (cur + 1) % len(all_faces)
1277
  st.rerun()
1278
  else:
1279
+ st.info("Sense imatges")
1280
+
1281
+ with col_voices:
1282
+ if voice_data:
1283
+ clips = voice_data["clips"]
1284
+ if clips:
1285
+ carousel_key = f"combined_voice_{pidx}"
1286
+ if f"{carousel_key}_idx" not in st.session_state:
1287
+ st.session_state[f"{carousel_key}_idx"] = 0
1288
+ cur = st.session_state[f"{carousel_key}_idx"]
1289
+ if cur >= len(clips):
1290
+ cur = 0
1291
+ st.session_state[f"{carousel_key}_idx"] = cur
1292
+ fname = clips[cur]
1293
+ audio_url = f"{backend_base_url}/audio/{vname}/{fname}" if (vname and fname) else None
1294
+ if audio_url:
1295
+ st.audio(audio_url, format="audio/wav")
1296
+ st.caption(f"Veu {cur+1}/{len(clips)}")
1297
+ bcol1, bcol2 = st.columns(2)
1298
+ with bcol1:
1299
+ if st.button("⬅️", key=f"combined_voice_prev_{pidx}"):
1300
+ st.session_state[f"{carousel_key}_idx"] = (cur - 1) % len(clips)
1301
+ st.rerun()
1302
+ with bcol2:
1303
+ if st.button("➡️", key=f"combined_voice_next_{pidx}"):
1304
+ st.session_state[f"{carousel_key}_idx"] = (cur + 1) % len(clips)
1305
+ st.rerun()
1306
+ else:
1307
+ st.info("Sense clips de veu")
1308
+ else:
1309
+ st.info("Sense dades de veu")
1310
+
1311
+ with col_text:
1312
+ combined_name_key = f"combined_char_{pidx}_name"
1313
+ combined_desc_key = f"combined_char_{pidx}_desc"
1314
+
1315
+ if combined_name_key not in st.session_state:
1316
+ st.session_state[combined_name_key] = norm_name
1317
+ if combined_desc_key not in st.session_state:
1318
+ st.session_state[combined_desc_key] = combined_description
1319
+
1320
+ st.text_input("Nom del personatge", key=combined_name_key, label_visibility="collapsed", placeholder="Nom del personatge")
1321
+ st.text_area("Descripció", key=combined_desc_key, height=120, label_visibility="collapsed", placeholder="Descripció del personatge")
1322
+
1323
+ # --- 7. Generar audiodescripció ---
1324
+ st.markdown("---")
1325
+ if st.button("🎬 Generar audiodescripció", type="primary", use_container_width=True):
1326
  v = st.session_state.get("video_uploaded")
1327
  if not v:
1328
  st.error("No hi ha cap vídeo carregat.")
 
1342
  base_media_dir = Path(__file__).parent.parent / "temp" / "media" / sha1
1343
  base_media_dir.mkdir(parents=True, exist_ok=True)
1344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1345
  # 1) Carregar i enviar el casting_json com a embeddings al engine
1346
  casting_json = None
1347
  try:
persistent_data_gate.py CHANGED
@@ -65,36 +65,37 @@ def _load_compliance_flags(base_dir: Path) -> dict:
65
 
66
 
67
  def ensure_temp_databases(base_dir: Path, api_client) -> None:
68
- """Garantiza que las BDs *.db estén presentes en demo/temp antes del login.
69
 
70
- - data_origin == "internal": copia demo/data/*.db -> demo/temp/*.db
71
- - data_origin == "external": llama al endpoint remoto import_databases.
72
  """
73
 
74
  data_origin = _load_data_origin(base_dir)
75
  compliance_flags = _load_compliance_flags(base_dir)
76
  public_blockchain_enabled = bool(compliance_flags.get("public_blockchain_enabled", False))
77
- temp_dir = base_dir / "temp"
78
- temp_dir.mkdir(parents=True, exist_ok=True)
 
79
 
80
  print(f"[ensure_temp_databases] data_origin={data_origin}")
81
  if data_origin == "internal":
82
- source_dir = base_dir / "data"
83
  print(f"[ensure_temp_databases] data_origin=internal, source_dir={source_dir}")
84
  print(f"[ensure_temp_databases] source_dir.exists()={source_dir.exists()}")
85
  if source_dir.exists():
86
  db_files = list(source_dir.glob("*.db"))
87
  print(f"[ensure_temp_databases] Found {len(db_files)} .db files in {source_dir}")
88
  for entry in db_files:
89
- dest = temp_dir / entry.name
90
  print(f"[ensure_temp_databases] Copying {entry} -> {dest}")
91
  shutil.copy2(entry, dest)
92
  else:
93
  print(f"[ensure_temp_databases] WARNING: source_dir does not exist!")
94
  else:
95
  # Mode external: descarregar BDs del backend una sola vegada per sessió del servidor
96
- marker_file = temp_dir / ".external_db_imported"
97
- missing = [name for name in ("events.db", "feedback.db", "users.db", "videos.db") if not (temp_dir / name).exists()]
98
  needs_import = not marker_file.exists() or missing
99
 
100
  if not needs_import:
@@ -114,8 +115,8 @@ def ensure_temp_databases(base_dir: Path, api_client) -> None:
114
  resp = api_client.import_databases()
115
  zip_bytes = resp.get("zip_bytes") if isinstance(resp, dict) else None
116
  if zip_bytes:
117
- _extract_zip_bytes(zip_bytes, temp_dir)
118
- print(f"[ensure_temp_databases] Extracted DBs to {temp_dir}")
119
  try:
120
  marker_file.write_text("imported", encoding="utf-8")
121
  except Exception:
@@ -127,11 +128,11 @@ def ensure_temp_databases(base_dir: Path, api_client) -> None:
127
  print(f"[ensure_temp_databases] Exception: {e}")
128
  return
129
 
130
- # Un cop les BDs estan a temp/, crear una còpia de seguretat a temp/backup
131
- backup_dir = temp_dir / "backup"
132
  backup_dir.mkdir(parents=True, exist_ok=True)
133
 
134
- for db_path in temp_dir.glob("*.db"):
135
  dest_backup = backup_dir / db_path.name
136
  try:
137
  shutil.copy2(db_path, dest_backup)
@@ -139,6 +140,15 @@ def ensure_temp_databases(base_dir: Path, api_client) -> None:
139
  # No interrompre el flux per un error puntual de còpia
140
  continue
141
 
 
 
 
 
 
 
 
 
 
142
 
143
  def _extract_zip_bytes(zip_bytes: bytes, target_dir: Path) -> None:
144
  target_dir.mkdir(parents=True, exist_ok=True)
@@ -223,8 +233,10 @@ def confirm_changes_and_logout(base_dir: Path, api_client, session_id: str) -> N
223
  return
224
 
225
  data_origin = _load_data_origin(base_dir)
226
- temp_dir = base_dir / "temp"
 
227
  data_dir = base_dir / "data"
 
228
 
229
  # --- 1) Sincronitzar taules ---
230
  # - internal: mantenim el comportament antic basat en el camp 'session'.
@@ -236,8 +248,8 @@ def confirm_changes_and_logout(base_dir: Path, api_client, session_id: str) -> N
236
  if data_origin == "internal":
237
  sql_statements: list[str] = []
238
 
239
- for db_path in temp_dir.glob("*.db"):
240
- target_db = data_dir / db_path.name
241
 
242
  with sqlite3.connect(str(db_path)) as src_conn:
243
  src_conn.row_factory = sqlite3.Row
@@ -275,10 +287,10 @@ def confirm_changes_and_logout(base_dir: Path, api_client, session_id: str) -> N
275
  dst_conn.execute(insert_sql, values)
276
  dst_conn.commit()
277
  else:
278
- # Mode external: diferències entre temp/*.db i temp/backup/*.db, enviant INSERTs un a un
279
- backup_dir = temp_dir / "backup"
280
  if backup_dir.exists() and api_client is not None:
281
- for db_path in temp_dir.glob("*.db"):
282
  backup_db = backup_dir / db_path.name
283
  if not backup_db.exists():
284
  continue
@@ -354,7 +366,7 @@ def confirm_changes_and_logout(base_dir: Path, api_client, session_id: str) -> N
354
  # --- 2) Digest d'esdeveniments per a la sessió (public blockchain) ---
355
  events_digest_info = None
356
  if public_blockchain_enabled:
357
- events_db = temp_dir / "events.db"
358
  try:
359
  import sqlite3
360
  import hashlib
@@ -415,7 +427,7 @@ def confirm_changes_and_logout(base_dir: Path, api_client, session_id: str) -> N
415
  events_digest_info = None
416
 
417
  # --- 3) Nous vídeos a videos.db associats a la sessió ---
418
- videos_db = temp_dir / "videos.db"
419
  new_sha1s: set[str] = set()
420
 
421
  try:
@@ -441,7 +453,7 @@ def confirm_changes_and_logout(base_dir: Path, api_client, session_id: str) -> N
441
  if not new_sha1s:
442
  return events_digest_info
443
 
444
- temp_media_root = temp_dir / "media"
445
 
446
  if data_origin == "internal":
447
  # Copiar carpetes de media noves a demo/data/media
 
65
 
66
 
67
  def ensure_temp_databases(base_dir: Path, api_client) -> None:
68
+ """Garantiza que las BDs *.db estén presentes en demo/temp/db antes del login.
69
 
70
+ - data_origin == "internal": copia demo/data/db/*.db -> demo/temp/db/*.db
71
+ - data_origin == "external": llama al endpoint remoto import_databases (ZIP) y lo extrae en demo/temp/db.
72
  """
73
 
74
  data_origin = _load_data_origin(base_dir)
75
  compliance_flags = _load_compliance_flags(base_dir)
76
  public_blockchain_enabled = bool(compliance_flags.get("public_blockchain_enabled", False))
77
+ temp_root = base_dir / "temp"
78
+ db_temp_dir = temp_root / "db"
79
+ db_temp_dir.mkdir(parents=True, exist_ok=True)
80
 
81
  print(f"[ensure_temp_databases] data_origin={data_origin}")
82
  if data_origin == "internal":
83
+ source_dir = base_dir / "data" / "db"
84
  print(f"[ensure_temp_databases] data_origin=internal, source_dir={source_dir}")
85
  print(f"[ensure_temp_databases] source_dir.exists()={source_dir.exists()}")
86
  if source_dir.exists():
87
  db_files = list(source_dir.glob("*.db"))
88
  print(f"[ensure_temp_databases] Found {len(db_files)} .db files in {source_dir}")
89
  for entry in db_files:
90
+ dest = db_temp_dir / entry.name
91
  print(f"[ensure_temp_databases] Copying {entry} -> {dest}")
92
  shutil.copy2(entry, dest)
93
  else:
94
  print(f"[ensure_temp_databases] WARNING: source_dir does not exist!")
95
  else:
96
  # Mode external: descarregar BDs del backend una sola vegada per sessió del servidor
97
+ marker_file = db_temp_dir / ".external_db_imported"
98
+ missing = [name for name in ("events.db", "feedback.db", "users.db", "videos.db") if not (db_temp_dir / name).exists()]
99
  needs_import = not marker_file.exists() or missing
100
 
101
  if not needs_import:
 
115
  resp = api_client.import_databases()
116
  zip_bytes = resp.get("zip_bytes") if isinstance(resp, dict) else None
117
  if zip_bytes:
118
+ _extract_zip_bytes(zip_bytes, db_temp_dir)
119
+ print(f"[ensure_temp_databases] Extracted DBs to {db_temp_dir}")
120
  try:
121
  marker_file.write_text("imported", encoding="utf-8")
122
  except Exception:
 
128
  print(f"[ensure_temp_databases] Exception: {e}")
129
  return
130
 
131
+ # Un cop les BDs estan a temp/db, crear una còpia de seguretat a temp/db/backup
132
+ backup_dir = db_temp_dir / "backup"
133
  backup_dir.mkdir(parents=True, exist_ok=True)
134
 
135
+ for db_path in db_temp_dir.glob("*.db"):
136
  dest_backup = backup_dir / db_path.name
137
  try:
138
  shutil.copy2(db_path, dest_backup)
 
140
  # No interrompre el flux per un error puntual de còpia
141
  continue
142
 
143
+ # Verificació opcional: llistar estat de demo/data/db i demo/temp/db al log
144
+ try:
145
+ from scripts.verify_temp_dbs import run_verification as _run_db_verification
146
+
147
+ print("[ensure_temp_databases] Executant verificador de BDs (demo/scripts/verify_temp_dbs.py)...")
148
+ _run_db_verification()
149
+ except Exception as _e_ver:
150
+ print(f"[ensure_temp_databases] Error executant verificador de BDs: {_e_ver}")
151
+
152
 
153
  def _extract_zip_bytes(zip_bytes: bytes, target_dir: Path) -> None:
154
  target_dir.mkdir(parents=True, exist_ok=True)
 
233
  return
234
 
235
  data_origin = _load_data_origin(base_dir)
236
+ temp_root = base_dir / "temp"
237
+ db_temp_dir = temp_root / "db"
238
  data_dir = base_dir / "data"
239
+ data_db_dir = data_dir / "db"
240
 
241
  # --- 1) Sincronitzar taules ---
242
  # - internal: mantenim el comportament antic basat en el camp 'session'.
 
248
  if data_origin == "internal":
249
  sql_statements: list[str] = []
250
 
251
+ for db_path in db_temp_dir.glob("*.db"):
252
+ target_db = data_db_dir / db_path.name
253
 
254
  with sqlite3.connect(str(db_path)) as src_conn:
255
  src_conn.row_factory = sqlite3.Row
 
287
  dst_conn.execute(insert_sql, values)
288
  dst_conn.commit()
289
  else:
290
+ # Mode external: diferències entre temp/db/*.db i temp/db/backup/*.db, enviant INSERTs un a un
291
+ backup_dir = db_temp_dir / "backup"
292
  if backup_dir.exists() and api_client is not None:
293
+ for db_path in db_temp_dir.glob("*.db"):
294
  backup_db = backup_dir / db_path.name
295
  if not backup_db.exists():
296
  continue
 
366
  # --- 2) Digest d'esdeveniments per a la sessió (public blockchain) ---
367
  events_digest_info = None
368
  if public_blockchain_enabled:
369
+ events_db = db_temp_dir / "events.db"
370
  try:
371
  import sqlite3
372
  import hashlib
 
427
  events_digest_info = None
428
 
429
  # --- 3) Nous vídeos a videos.db associats a la sessió ---
430
+ videos_db = db_temp_dir / "videos.db"
431
  new_sha1s: set[str] = set()
432
 
433
  try:
 
453
  if not new_sha1s:
454
  return events_digest_info
455
 
456
+ temp_media_root = temp_root / "media"
457
 
458
  if data_origin == "internal":
459
  # Copiar carpetes de media noves a demo/data/media