VeuReu commited on
Commit
a8cfd8c
·
verified ·
1 Parent(s): f07c3b1

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -462
app.py DELETED
@@ -1,462 +0,0 @@
1
- import os
2
- import io
3
- import json
4
- import yaml
5
- import shutil
6
- from pathlib import Path
7
- from passlib.hash import bcrypt
8
- try:
9
- import tomllib
10
- except ModuleNotFoundError: # Py<3.11
11
- import tomli as tomllib
12
- import streamlit as st
13
- from moviepy.editor import VideoFileClip
14
-
15
- from database import set_db_path, init_schema, get_user, create_video, update_video_status, list_videos, get_video, get_all_users, upsert_result, get_results, add_feedback, get_feedback_for_video, get_feedback_stats
16
- from api_client import APIClient
17
- from utils import ensure_dirs, save_bytes, save_text, human_size
18
-
19
-
20
- # -- Move DB ---
21
- os.environ["STREAMLIT_DATA_DIRECTORY"] = "/tmp/.streamlit"
22
- Path("/tmp/.streamlit").mkdir(parents=True, exist_ok=True)
23
- Path("/tmp/data").mkdir(parents=True, exist_ok=True)
24
- source_db = "init_data/veureu.db"
25
- target_db = "/tmp/data/app.db"
26
- if not os.path.exists(target_db) and os.path.exists(source_db):
27
- shutil.copy(source_db, target_db)
28
-
29
- static_videos = Path(__file__).parent / "videos"
30
- runtime_videos = Path("/tmp/data/videos")
31
- if not runtime_videos.exists():
32
- shutil.copytree(static_videos, runtime_videos, dirs_exist_ok=True)
33
-
34
-
35
- # --- Config ---
36
- def _load_yaml(path="config.yaml") -> dict:
37
- with open(path, "r", encoding="utf-8") as f:
38
- cfg = yaml.safe_load(f) or {}
39
- # interpolación sencilla de ${VARS} si las usas en el YAML
40
- def _subst(s: str) -> str:
41
- return os.path.expandvars(s) if isinstance(s, str) else s
42
-
43
- # aplica sustitución en los campos que te interesan
44
- if "api" in cfg:
45
- cfg["api"]["base_url"] = _subst(cfg["api"].get("base_url", ""))
46
- cfg["api"]["token"] = _subst(cfg["api"].get("token", ""))
47
-
48
- if "storage" in cfg and "root_dir" in cfg["storage"]:
49
- cfg["storage"]["root_dir"] = _subst(cfg["storage"]["root_dir"])
50
-
51
- if "sqlite" in cfg and "path" in cfg["sqlite"]:
52
- cfg["sqlite"]["path"] = _subst(cfg["sqlite"]["path"])
53
-
54
- return cfg
55
-
56
- CFG = _load_yaml("config.yaml")
57
-
58
- # Ajuste de variables según tu esquema YAML
59
- DATA_DIR = CFG.get("storage", {}).get("root_dir", "data")
60
- BACKEND_BASE_URL = CFG.get("api", {}).get("base_url", "http://localhost:8000")
61
- USE_MOCK = bool(CFG.get("app", {}).get("use_mock", False)) # si no la tienes en el yaml, queda False
62
- API_TOKEN = CFG.get("api", {}).get("token") or os.getenv("API_SHARED_TOKEN")
63
-
64
- os.makedirs(DATA_DIR, exist_ok=True)
65
- ensure_dirs(DATA_DIR)
66
- DB_PATH = os.path.join(DATA_DIR, "app.db")
67
- set_db_path(DB_PATH)
68
- init_schema()
69
-
70
- api = APIClient(BACKEND_BASE_URL, use_mock=USE_MOCK, data_dir=DATA_DIR, token=API_TOKEN)
71
-
72
- st.set_page_config(page_title="Veureu — Audiodescripció", page_icon="🎬", layout="wide")
73
-
74
- # --- Session: auth ---
75
- # print("Usuarios disponibles:", get_all_users()) # Descomentar para depurar
76
- if "user" not in st.session_state:
77
- st.session_state.user = None # dict with {username, role, id(optional)}
78
-
79
- def require_login():
80
- if not st.session_state.user:
81
- st.info("Por favor, inicia sesión para continuar.")
82
- login_form()
83
- st.stop()
84
-
85
- def verify_password(password: str, pw_hash: str) -> bool:
86
- try:
87
- return bcrypt.verify(password, pw_hash)
88
- except Exception:
89
- return False
90
-
91
- # --- Sidebar (only after login) ---
92
- role = st.session_state.user["role"] if st.session_state.user else None
93
- with st.sidebar:
94
- st.title("Veureu")
95
- if st.session_state.user:
96
- st.write(f"Usuari: **{st.session_state.user['username']}** (rol: {st.session_state.user['role']})")
97
- if st.button("Tancar sessió"):
98
- st.session_state.user = None
99
- st.rerun()
100
- if st.session_state.user:
101
- page = st.radio("Navegació", ["Analitzar video-transcripcions","Processar vídeo nou","Estadístiques"], index=0)
102
- else:
103
- page = None
104
-
105
- # --- Pre-login screen ---
106
- if not st.session_state.user:
107
- st.title("Veureu — Audiodescripció")
108
- def login_form():
109
- st.subheader("Inici de sessió")
110
- username = st.text_input("Usuari")
111
- password = st.text_input("Contrasenya", type="password")
112
- if st.button("Entrar", type="primary"):
113
- row = get_user(username)
114
- if row and verify_password(password, row["pw_hash"]):
115
- st.session_state.user = {"id": row["id"], "username": row["username"], "role": row["role"]}
116
- st.success(f"Benvingut/da, {row['username']}")
117
- st.rerun()
118
- else:
119
- st.error("Credencials invàlides")
120
- login_form()
121
- st.stop()
122
-
123
- # --- Pages ---
124
- if page == "Processar vídeo nou":
125
- require_login()
126
- if role != "verd":
127
- st.error("No tens permisos per processar nous vídeos. Canvia d'usuari o sol·licita permisos.")
128
- st.stop()
129
-
130
- st.header("Processar un nou clip de vídeo")
131
-
132
- # Inicializar el estado de la página si no existe
133
- if 'video_uploaded' not in st.session_state:
134
- st.session_state.video_uploaded = None
135
- if 'characters_detected' not in st.session_state:
136
- st.session_state.characters_detected = None
137
- if 'characters_saved' not in st.session_state:
138
- st.session_state.characters_saved = False
139
-
140
- # --- 1. Subida del vídeo ---
141
- MAX_SIZE_MB = 20
142
- MAX_DURATION_S = 240 # 4 minutos
143
-
144
- uploaded_file = st.file_uploader("Puja un clip de vídeo (MP4, < 20MB, < 4 minuts)", type=["mp4"], key="video_uploader")
145
-
146
- if uploaded_file is not None:
147
- # Resetear el estado si se sube un nuevo archivo
148
- if st.session_state.video_uploaded is None or uploaded_file.name != st.session_state.video_uploaded.get('original_name'):
149
- st.session_state.video_uploaded = {'original_name': uploaded_file.name, 'status': 'validating'}
150
- st.session_state.characters_detected = None
151
- st.session_state.characters_saved = False
152
-
153
- # --- Validación y Procesamiento ---
154
- if st.session_state.video_uploaded['status'] == 'validating':
155
- is_valid = True
156
- # 1. Validar tamaño
157
- if uploaded_file.size > MAX_SIZE_MB * 1024 * 1024:
158
- st.error(f"El vídeo supera el límit de {MAX_SIZE_MB}MB.")
159
- is_valid = False
160
-
161
- if is_valid:
162
- with st.spinner("Processant el vídeo..."):
163
- # Guardar temporalmente para analizarlo
164
- with open("temp_video.mp4", "wb") as f:
165
- f.write(uploaded_file.getbuffer())
166
-
167
- clip = VideoFileClip("temp_video.mp4")
168
- duration = clip.duration
169
-
170
- # 2. Validar y truncar duración
171
- was_truncated = False
172
- if duration > MAX_DURATION_S:
173
- clip = clip.subclip(0, MAX_DURATION_S)
174
- was_truncated = True
175
-
176
- # Crear carpeta y guardar el vídeo final
177
- video_name = Path(uploaded_file.name).stem
178
- video_dir = Path("/tmp/data/videos") / video_name
179
- video_dir.mkdir(parents=True, exist_ok=True)
180
- final_video_path = video_dir / f"{video_name}.mp4"
181
- clip.write_videofile(str(final_video_path), codec="libx264", audio_codec="aac")
182
-
183
- clip.close()
184
- os.remove("temp_video.mp4")
185
-
186
- # Actualizar estado
187
- st.session_state.video_uploaded.update({
188
- 'status': 'processed',
189
- 'path': str(final_video_path),
190
- 'was_truncated': was_truncated
191
- })
192
- st.rerun()
193
-
194
- # --- Mensajes de estado ---
195
- if st.session_state.video_uploaded and st.session_state.video_uploaded['status'] == 'processed':
196
- st.success(f"Vídeo '{st.session_state.video_uploaded['original_name']}' pujat i processat correctament.")
197
- if st.session_state.video_uploaded['was_truncated']:
198
- st.warning(f"El vídeo s'ha truncat a {MAX_DURATION_S // 60} minuts.")
199
-
200
- # --- 2. Detección de personajes ---
201
- st.markdown("---")
202
- col1, col2 = st.columns([1, 3])
203
- with col1:
204
- detect_button_disabled = st.session_state.video_uploaded is None
205
- if st.button("Detectar Personatges", disabled=detect_button_disabled):
206
- with st.spinner("Detectant personatges..."):
207
- # Aquí iría la llamada a la API para detectar personajes
208
- # Por ahora, usamos datos de ejemplo
209
- st.session_state.characters_detected = [
210
- {"id": "char1", "image_path": "init_data/placeholder.png", "description": "Dona amb cabell ros i ulleres"},
211
- {"id": "char2", "image_path": "init_data/placeholder.png", "description": "Home amb barba i barret"},
212
- ]
213
- st.session_state.characters_saved = False # Resetear el estado de guardado
214
-
215
- # --- 3. Formularios de personajes ---
216
- if st.session_state.characters_detected:
217
- st.subheader("Personatges detectats")
218
- for char in st.session_state.characters_detected:
219
- with st.form(key=f"form_{char['id']}"):
220
- col1, col2 = st.columns(2)
221
- with col1:
222
- st.image(char['image_path'], width=150)
223
-
224
- with col2:
225
- st.caption(char['description'])
226
- st.text_input("Nom del personatge", key=f"name_{char['id']}")
227
- st.form_submit_button("Cercar")
228
-
229
- st.markdown("---_**")
230
-
231
- # --- 4. Guardar y Generar ---
232
- col1, col2, col3 = st.columns([1,1,2])
233
- with col1:
234
- if st.button("Desar", type="primary"):
235
- # Aquí iría la lógica para guardar los nombres de los personajes
236
- st.session_state.characters_saved = True
237
- st.success("Personatges desats correctament.")
238
-
239
- with col2:
240
- if st.session_state.characters_saved:
241
- st.button("Generar Audiodescripció")
242
-
243
- elif page == "Analitzar video-transcripcions":
244
- require_login()
245
- st.header("Analitzar video-transcripcions")
246
- base_dir = Path("/tmp/data/videos")
247
-
248
- if not base_dir.exists():
249
- st.info("No s'ha trobat la carpeta **videos**. Crea-la i afegeix-hi subcarpetes amb els teus vídeos.")
250
- st.stop()
251
-
252
- carpetes = [p.name for p in sorted(base_dir.iterdir()) if p.is_dir() and p.name != 'completed']
253
- if not carpetes:
254
- st.info("No s'ha trobat la carpeta **videos**. Crea-la i afegeix-hi subcarpetes amb els teus vídeos.")
255
- st.stop()
256
-
257
- # --- Lógica de Estado y Selección ---
258
-
259
- # Detectar si el vídeo principal ha cambiado para resetear el estado secundario
260
- if 'current_video' not in st.session_state:
261
- st.session_state.current_video = None
262
-
263
- # Widget de selección de vídeo
264
- seleccio = st.selectbox("Selecciona un vídeo (carpeta):", carpetes, index=None, placeholder="Tria una carpeta…")
265
-
266
- if seleccio != st.session_state.current_video:
267
- st.session_state.current_video = seleccio
268
- # Forzar reseteo de los widgets dependientes
269
- st.session_state.version_selector = None
270
- st.session_state.add_ad_checkbox = False
271
- st.rerun()
272
-
273
- if not seleccio:
274
- st.stop()
275
-
276
- vid_dir = base_dir / seleccio
277
- mp4s = sorted(vid_dir.glob("*.mp4"))
278
-
279
- # --- Dibujado de la Interfaz ---
280
- col_video, col_txt = st.columns([2, 1], gap="large")
281
-
282
- with col_video:
283
- # Selección de versión
284
- subcarpetas_ad = [p.name for p in sorted(vid_dir.iterdir()) if p.is_dir()]
285
- default_index_sub = subcarpetas_ad.index("Salamandra") if "Salamandra" in subcarpetas_ad else 0
286
- subcarpeta_seleccio = st.selectbox(
287
- "Selecciona una versió d'audiodescripció:", subcarpetas_ad,
288
- index=default_index_sub if subcarpetas_ad else None,
289
- placeholder="Tria una versió…" if subcarpetas_ad else "No hi ha versions",
290
- key="version_selector"
291
- )
292
-
293
- # Lógica de vídeo AD
294
- video_ad_path = vid_dir / subcarpeta_seleccio / "une_ad.mp4" if subcarpeta_seleccio else None
295
- is_ad_video_available = video_ad_path is not None and video_ad_path.exists()
296
-
297
- # Checkbox
298
- add_ad_video = st.checkbox("Afegir audiodescripció", disabled=not is_ad_video_available, key="add_ad_checkbox")
299
-
300
- # Decidir qué vídeo mostrar
301
- video_to_show = None
302
- if add_ad_video and is_ad_video_available:
303
- video_to_show = video_ad_path
304
- elif mp4s:
305
- video_to_show = mp4s[0]
306
-
307
- if video_to_show:
308
- st.video(str(video_to_show))
309
- else:
310
- st.warning("No s'ha trobat cap fitxer **.mp4** a la carpeta seleccionada.")
311
-
312
- st.markdown("---")
313
-
314
- # Sección de ACCIONES
315
- st.markdown("#### Accions")
316
- c1, c2 = st.columns(2)
317
- with c1:
318
- if st.button("Reconstruir àudio amb narració lliure", use_container_width=True, key="rebuild_free_ad"):
319
- if subcarpeta_seleccio:
320
- free_ad_path = vid_dir / subcarpeta_seleccio / "free_ad.txt"
321
- if free_ad_path.exists():
322
- with st.spinner("Generant àudio de la narració lliure..."):
323
- text_content = free_ad_path.read_text(encoding="utf-8")
324
- voice = "central/grau" # Voz fijada
325
- response = api.tts_matxa(text=text_content, voice=voice)
326
- if "mp3_bytes" in response:
327
- output_path = vid_dir / subcarpeta_seleccio / "free_ad.mp3"
328
- save_bytes(output_path, response["mp3_bytes"])
329
- st.success(f"Àudio generat i desat a: {output_path}")
330
- else:
331
- st.error(f"Error en la generació de l'àudio: {response.get('error', 'Desconegut')}")
332
- else:
333
- st.warning("No s'ha trobat el fitxer 'free_ad.txt' en aquesta versió.")
334
-
335
- with c2:
336
- if st.button("Reconstruir vídeo amb audiodescripció", use_container_width=True, key="rebuild_video_ad"):
337
- if subcarpeta_seleccio and mp4s:
338
- une_srt_path = vid_dir / subcarpeta_seleccio / "une_ad.srt"
339
- video_original_path = mp4s[0]
340
- if une_srt_path.exists():
341
- with st.spinner("Reconstruint el vídeo amb l'audiodescripció... Aquesta operació pot trigar una estona."):
342
- response = api.rebuild_video_with_ad(video_path=str(video_original_path), srt_path=str(une_srt_path))
343
- if "video_bytes" in response:
344
- output_path = vid_dir / subcarpeta_seleccio / "video_ad_rebuilt.mp4"
345
- save_bytes(output_path, response["video_bytes"])
346
- st.success(f"Vídeo reconstruït i desat a: {output_path}")
347
- st.info("Pots visualitzar-lo activant la casella 'Afegir audiodescripció' i seleccionant el nou fitxer si cal.")
348
- else:
349
- st.error(f"Error en la reconstrucció del vídeo: {response.get('error', 'Desconegut')}")
350
- else:
351
- st.warning("No s'ha trobat el fitxer 'une_ad.srt' en aquesta versió.")
352
-
353
-
354
- # --- Columna Derecha (Editor de texto y guardado) ---
355
- with col_txt:
356
- tipus_ad_options = ["narració lliure", "UNE-153010"]
357
- tipus_ad_seleccio = st.selectbox("Fitxer d'audiodescripció a editar:", tipus_ad_options)
358
-
359
- ad_filename = "free_ad.txt" if tipus_ad_seleccio == "narració lliure" else "une_ad.srt"
360
-
361
- # Cargar el contenido del fichero seleccionado
362
- text_content = ""
363
- ad_path = None
364
- if subcarpeta_seleccio:
365
- ad_path = vid_dir / subcarpeta_seleccio / ad_filename
366
- if ad_path.exists():
367
- try:
368
- text_content = ad_path.read_text(encoding="utf-8")
369
- except Exception:
370
- text_content = ad_path.read_text(errors="ignore")
371
- else:
372
- st.info(f"No s'ha trobat el fitxer **{ad_filename}**.")
373
- else:
374
- st.warning("Selecciona una versió per veure els fitxers.")
375
-
376
- # Área de texto para edición
377
- new_text = st.text_area(f"Contingut de {tipus_ad_seleccio}", value=text_content, height=500, key=f"editor_{seleccio}_{subcarpeta_seleccio}_{ad_filename}")
378
-
379
- # Controles de reproducción de narración (selector de voz eliminado)
380
- if st.button("▶️ Reproduir narració", use_container_width=True, disabled=not new_text.strip(), key="play_button_editor"):
381
- with st.spinner("Generant àudio..."):
382
- # Lógica de TTS con el texto del área
383
- pass # Implementación de la llamada a la API TTS
384
-
385
- # Botón de guardado
386
- if st.button("Desar canvis", use_container_width=True, type="primary"):
387
- if ad_path:
388
- try:
389
- ad_path.write_text(new_text, encoding="utf-8")
390
- st.success(f"Fitxer **{ad_filename}** desat correctament.")
391
- st.rerun()
392
- except Exception as e:
393
- st.error(f"No s'ha pogut desar el fitxer: {e}")
394
- else:
395
- st.error("No s'ha seleccionat una ruta de fitxer vàlida per desar.")
396
-
397
-
398
- st.markdown("---")
399
- st.subheader("Avaluació de la qualitat de l'audiodescripció")
400
-
401
- c1, c2, c3 = st.columns(3)
402
- with c1:
403
- transcripcio = st.slider("Transcripció", 1, 10, 7)
404
- identificacio = st.slider("Identificació de personatges", 1, 10, 7)
405
- with c2:
406
- localitzacions = st.slider("Localitzacions", 1, 10, 7)
407
- activitats = st.slider("Activitats", 1, 10, 7)
408
- with c3:
409
- narracions = st.slider("Narracions", 1, 10, 7)
410
- expressivitat = st.slider("Expressivitat", 1, 10, 7)
411
-
412
- comments = st.text_area("Comentaris (opcional)", placeholder="Escriu els teus comentaris lliures…", height=120)
413
-
414
- role = st.session_state.user["role"]
415
- can_rate = role in ("verd", "groc", "blau")
416
-
417
- if not can_rate:
418
- st.info("El teu rol no permet enviar valoracions.")
419
- else:
420
- if st.button("Enviar valoració", type="primary", use_container_width=True):
421
- try:
422
- from database import add_feedback_ad
423
- add_feedback_ad(
424
- video_name=seleccio,
425
- user_id=st.session_state.user["id"],
426
- transcripcio=transcripcio,
427
- identificacio=identificacio,
428
- localitzacions=localitzacions,
429
- activitats=activitats,
430
- narracions=narracions,
431
- expressivitat=expressivitat,
432
- comments=comments or None
433
- )
434
- st.success("Gràcies! La teva valoració s'ha desat correctament.")
435
- except Exception as e:
436
- st.error(f"S'ha produït un error en desar la valoració: {e}")
437
-
438
-
439
- elif page == "Estadístiques":
440
- require_login()
441
- st.header("Estadístiques")
442
-
443
- from database import get_feedback_ad_stats
444
- stats = get_feedback_ad_stats() # medias por vídeo + avg_global
445
- if not stats:
446
- st.caption("Encara no hi ha valoracions.")
447
- st.stop()
448
-
449
- import pandas as pd
450
- df = pd.DataFrame(stats, columns=stats[0].keys())
451
- ordre = st.radio("Ordre de rànquing", ["Descendent (millors primer)", "Ascendent (pitjors primer)"], horizontal=True)
452
- if ordre.startswith("Asc"):
453
- df = df.sort_values("avg_global", ascending=True)
454
- else:
455
- df = df.sort_values("avg_global", ascending=False)
456
-
457
- st.subheader("Rànquing de vídeos")
458
- st.dataframe(
459
- df[["video_name","n","avg_global","avg_transcripcio","avg_identificacio","avg_localitzacions","avg_activitats","avg_narracions", "avg_expressivitat"]],
460
- use_container_width=True
461
- )
462
-