Upload 10 files
Browse files- app.py +1 -1
- config.yaml +1 -1
- databases.py +8 -8
- page_modules/process_video.py +305 -268
- 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: "
|
| 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 |
-
"""
|
| 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 |
-
"""
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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
|
| 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.
|
| 276 |
MAX_SIZE_MB = max_size_mb
|
| 277 |
MAX_DURATION_S = max_duration_s
|
| 278 |
|
| 279 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 415 |
with local_pending_path.open("wb") as f_pending:
|
| 416 |
f_pending.write(video_bytes)
|
| 417 |
|
| 418 |
-
#
|
| 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 |
-
#
|
| 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
|
| 438 |
else:
|
| 439 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 464 |
-
#
|
| 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.
|
| 474 |
-
#
|
| 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
|
| 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
|
| 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
|
| 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 |
-
#
|
| 615 |
-
#
|
| 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-
|
| 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.
|
| 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.
|
| 1035 |
if st.session_state.get("detect_done"):
|
| 1036 |
st.markdown("---")
|
| 1037 |
-
st.
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1173 |
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 1183 |
cur = 0
|
| 1184 |
st.session_state[f"{carousel_key}_idx"] = cur
|
| 1185 |
-
fname =
|
| 1186 |
-
|
| 1187 |
-
if
|
| 1188 |
-
|
| 1189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1190 |
bcol1, bcol2 = st.columns(2)
|
| 1191 |
with bcol1:
|
| 1192 |
-
if st.button("⬅️", key=f"
|
| 1193 |
-
st.session_state[f"{carousel_key}_idx"] = (cur - 1) % len(
|
| 1194 |
st.rerun()
|
| 1195 |
with bcol2:
|
| 1196 |
-
if st.button("➡️", key=f"
|
| 1197 |
-
st.session_state[f"{carousel_key}_idx"] = (cur + 1) % len(
|
| 1198 |
st.rerun()
|
| 1199 |
else:
|
| 1200 |
-
st.info("Sense
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
|
| 1209 |
-
|
| 1210 |
-
|
| 1211 |
-
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
|
| 1222 |
-
|
| 1223 |
-
|
| 1224 |
-
|
| 1225 |
-
|
| 1226 |
-
|
| 1227 |
-
|
| 1228 |
-
|
| 1229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 78 |
-
|
|
|
|
| 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 =
|
| 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 =
|
| 97 |
-
missing = [name for name in ("events.db", "feedback.db", "users.db", "videos.db") if not (
|
| 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,
|
| 118 |
-
print(f"[ensure_temp_databases] Extracted DBs to {
|
| 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 =
|
| 132 |
backup_dir.mkdir(parents=True, exist_ok=True)
|
| 133 |
|
| 134 |
-
for db_path in
|
| 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 |
-
|
|
|
|
| 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
|
| 240 |
-
target_db =
|
| 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 =
|
| 280 |
if backup_dir.exists() and api_client is not None:
|
| 281 |
-
for db_path in
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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
|