VeuReu commited on
Commit
9167d9a
·
1 Parent(s): b6cdefa

Upload 93 files

Browse files
app.py CHANGED
@@ -12,10 +12,15 @@ except ModuleNotFoundError: # Py<3.11
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"
@@ -98,7 +103,7 @@ with st.sidebar:
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
 
@@ -245,9 +250,9 @@ if page == "Processar vídeo nou":
245
  if st.session_state.characters_saved:
246
  st.button("Generar Audiodescripció")
247
 
248
- elif page == "Analitzar video-transcripcions":
249
  require_login()
250
- st.header("Analitzar video-transcripcions")
251
  base_dir = Path("/tmp/data/videos")
252
 
253
  if not base_dir.exists():
@@ -321,39 +326,28 @@ elif page == "Analitzar video-transcripcions":
321
  c1, c2 = st.columns(2)
322
  with c1:
323
  if st.button("Reconstruir àudio amb narració lliure", use_container_width=True, key="rebuild_free_ad"):
324
- if subcarpeta_seleccio:
325
- free_ad_path = vid_dir / subcarpeta_seleccio / "free_ad.txt"
326
- if free_ad_path.exists():
327
- with st.spinner("Generant àudio de la narració lliure..."):
328
- text_content = free_ad_path.read_text(encoding="utf-8")
329
- voice = "central/grau" # Voz fijada
330
- response = api.tts_matxa(text=text_content, voice=voice)
331
- if "mp3_bytes" in response:
332
- output_path = vid_dir / subcarpeta_seleccio / "free_ad.mp3"
333
- save_bytes(output_path, response["mp3_bytes"])
334
- st.success(f"Àudio generat i desat a: {output_path}")
335
- else:
336
- st.error(f"Error en la generació de l'àudio: {response.get('error', 'Desconegut')}")
337
- else:
338
- st.warning("No s'ha trobat el fitxer 'free_ad.txt' en aquesta versió.")
339
 
340
  with c2:
341
  if st.button("Reconstruir vídeo amb audiodescripció", use_container_width=True, key="rebuild_video_ad"):
342
- if subcarpeta_seleccio and mp4s:
343
- une_srt_path = vid_dir / subcarpeta_seleccio / "une_ad.srt"
344
- video_original_path = mp4s[0]
345
- if une_srt_path.exists():
346
- with st.spinner("Reconstruint el vídeo amb l'audiodescripció... Aquesta operació pot trigar una estona."):
347
- response = api.rebuild_video_with_ad(video_path=str(video_original_path), srt_path=str(une_srt_path))
348
- if "video_bytes" in response:
349
- output_path = vid_dir / subcarpeta_seleccio / "video_ad_rebuilt.mp4"
350
- save_bytes(output_path, response["video_bytes"])
351
- st.success(f"Vídeo reconstruït i desat a: {output_path}")
352
- st.info("Pots visualitzar-lo activant la casella 'Afegir audiodescripció' i seleccionant el nou fitxer si cal.")
353
- else:
354
- st.error(f"Error en la reconstrucció del vídeo: {response.get('error', 'Desconegut')}")
355
- else:
356
- st.warning("No s'ha trobat el fitxer 'une_ad.srt' en aquesta versió.")
357
 
358
 
359
  # --- Columna Derecha (Editor de texto y guardado) ---
@@ -376,24 +370,31 @@ elif page == "Analitzar video-transcripcions":
376
  else:
377
  st.info(f"No s'ha trobat el fitxer **{ad_filename}**.")
378
  else:
379
- st.warning("Selecciona una versió per veure els fitxers.")
 
380
 
381
  # Área de texto para edición
382
  new_text = st.text_area(f"Contingut de {tipus_ad_seleccio}", value=text_content, height=500, key=f"editor_{seleccio}_{subcarpeta_seleccio}_{ad_filename}")
383
 
384
- # Controles de reproducción de narración (selector de voz eliminado)
385
- if st.button("▶️ Reproduir narració", use_container_width=True, disabled=not new_text.strip(), key="play_button_editor"):
386
- with st.spinner("Generant àudio..."):
387
- # Lógica de TTS con el texto del área
388
- pass # Implementación de la llamada a la API TTS
 
 
 
 
389
 
390
  # Botón de guardado
391
  if st.button("Desar canvis", use_container_width=True, type="primary"):
392
  if ad_path:
393
  try:
394
- ad_path.write_text(new_text, encoding="utf-8")
395
  st.success(f"Fitxer **{ad_filename}** desat correctament.")
396
- st.rerun()
 
 
397
  except Exception as e:
398
  st.error(f"No s'ha pogut desar el fitxer: {e}")
399
  else:
 
12
  import streamlit as st
13
  from moviepy.editor import VideoFileClip
14
 
15
+ import sys
16
  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
17
  from api_client import APIClient
18
  from utils import ensure_dirs, save_bytes, save_text, human_size
19
 
20
+ # Añadir la carpeta de scripts al path para poder importar el cliente
21
+ sys.path.append(str(Path(__file__).parent / "scripts"))
22
+ from client_generate_av import generate_free_ad_mp3, generate_une_ad_video
23
+
24
 
25
  # -- Move DB ---
26
  os.environ["STREAMLIT_DATA_DIRECTORY"] = "/tmp/.streamlit"
 
103
  st.session_state.user = None
104
  st.rerun()
105
  if st.session_state.user:
106
+ page = st.radio("Navegació", ["Analitzar audio-descripcions","Processar vídeo nou","Estadístiques"], index=0)
107
  else:
108
  page = None
109
 
 
250
  if st.session_state.characters_saved:
251
  st.button("Generar Audiodescripció")
252
 
253
+ elif page == "Analitzar audio-descripcions":
254
  require_login()
255
+ st.header("Analitzar audio-descripcions")
256
  base_dir = Path("/tmp/data/videos")
257
 
258
  if not base_dir.exists():
 
326
  c1, c2 = st.columns(2)
327
  with c1:
328
  if st.button("Reconstruir àudio amb narració lliure", use_container_width=True, key="rebuild_free_ad"):
329
+ if seleccio and subcarpeta_seleccio:
330
+ with st.spinner("Generant àudio de la narració lliure..."):
331
+ result = generate_free_ad_mp3(seleccio, subcarpeta_seleccio, api)
332
+ if result.get("status") == "success":
333
+ st.success(f"Àudio generat amb èxit: {result.get('path')}")
334
+ else:
335
+ st.error(f"Error: {result.get('reason', 'Desconegut')}")
336
+ else:
337
+ st.warning("Selecciona un vídeo i una versió.")
 
 
 
 
 
 
338
 
339
  with c2:
340
  if st.button("Reconstruir vídeo amb audiodescripció", use_container_width=True, key="rebuild_video_ad"):
341
+ if seleccio and subcarpeta_seleccio:
342
+ with st.spinner("Reconstruint el vídeo... Aquesta operació pot trigar."):
343
+ result = generate_une_ad_video(seleccio, subcarpeta_seleccio, api)
344
+ if result.get("status") == "success":
345
+ st.success(f"Vídeo generat amb èxit: {result.get('path')}")
346
+ st.info("Pots visualitzar-lo activant la casella 'Afegir audiodescripció'.")
347
+ else:
348
+ st.error(f"Error: {result.get('reason', 'Desconegut')}")
349
+ else:
350
+ st.warning("Selecciona un vídeo i una versió.")
 
 
 
 
 
351
 
352
 
353
  # --- Columna Derecha (Editor de texto y guardado) ---
 
370
  else:
371
  st.info(f"No s'ha trobat el fitxer **{ad_filename}**.")
372
  else:
373
+ # Eliminada la nota de advertencia
374
+ pass
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
380
+ free_ad_mp3_path = vid_dir / subcarpeta_seleccio / "free_ad.mp3" if seleccio and subcarpeta_seleccio else None
381
+ can_play_free_ad = free_ad_mp3_path is not None and free_ad_mp3_path.exists()
382
+
383
+ if st.button("▶️ Reproduir narració lliure", use_container_width=True, disabled=not can_play_free_ad, key="play_button_editor"):
384
+ if can_play_free_ad:
385
+ st.audio(str(free_ad_mp3_path), format="audio/mp3")
386
+ else:
387
+ st.warning("No s'ha trobat el fitxer 'free_ad.mp3'. Reconstrueix l'àudio primer.")
388
 
389
  # Botón de guardado
390
  if st.button("Desar canvis", use_container_width=True, type="primary"):
391
  if ad_path:
392
  try:
393
+ save_text(ad_path, new_text)
394
  st.success(f"Fitxer **{ad_filename}** desat correctament.")
395
+ # Forzar la recarga del contenido en el text_area
396
+ st.session_state[f"editor_{seleccio}_{subcarpeta_seleccio}_{ad_filename}"] = new_text
397
+ st.rerun() # Opcional, si quieres recargar toda la UI
398
  except Exception as e:
399
  st.error(f"No s'ha pogut desar el fitxer: {e}")
400
  else:
utils.py CHANGED
@@ -5,6 +5,11 @@ import subprocess
5
  from pathlib import Path
6
  from dataclasses import dataclass
7
  import shlex # Para manejar argumentos de línea de comandos de forma segura
 
 
 
 
 
8
 
9
 
10
  def incrustar_subtitulos_ffmpeg(
@@ -152,6 +157,142 @@ def recortar_video(input_path: str, output_path: str, duracion_segundos: int = 2
152
  subprocess.run(cmd, check=True)
153
 
154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  #----------------------------
156
 
157
  if __name__ == "__main__":
 
5
  from pathlib import Path
6
  from dataclasses import dataclass
7
  import shlex # Para manejar argumentos de línea de comandos de forma segura
8
+ from __future__ import annotations
9
+ from dataclasses import dataclass
10
+ from typing import List, Optional, Callable
11
+ import re
12
+ import xml.etree.ElementTree as ET
13
 
14
 
15
  def incrustar_subtitulos_ffmpeg(
 
157
  subprocess.run(cmd, check=True)
158
 
159
 
160
+ # ---- Núcleo: SRT -> ESF (XML string) ----
161
+
162
+ TIME_RE = re.compile(
163
+ r"(?P<start>\d{2}:\d{2}:\d{2}[,\.]\d{3})\s*-->\s*(?P<end>\d{2}:\d{2}:\d{2}[,\.]\d{3})"
164
+ )
165
+
166
+ @dataclass
167
+ class Cue:
168
+ index: int
169
+ start: str # "HH:MM:SS.mmm"
170
+ end: str # "HH:MM:SS.mmm"
171
+ text: str
172
+
173
+ def _norm_ts(ts: str) -> str:
174
+ """Convierte '01:02:03,456' -> '01:02:03.456'."""
175
+ return ts.replace(",", ".")
176
+
177
+ def _parse_srt(srt_text: str) -> List[Cue]:
178
+ """Parsea SRT a una lista de cues normalizados."""
179
+ srt_text = srt_text.replace("\r\n", "\n").replace("\r", "\n")
180
+ blocks = [b.strip() for b in re.split(r"\n\s*\n", srt_text) if b.strip()]
181
+ cues: List[Cue] = []
182
+
183
+ for block in blocks:
184
+ lines = block.split("\n")
185
+ # Detectar si la primera línea es índice
186
+ idx = None
187
+ if lines and lines[0].strip().isdigit():
188
+ idx = int(lines[0].strip())
189
+ time_candidates = lines[1:]
190
+ else:
191
+ idx = len(cues) + 1
192
+ time_candidates = lines
193
+
194
+ m = None
195
+ time_line_idx = None
196
+ for i, ln in enumerate(time_candidates[:3]): # robustez
197
+ mm = TIME_RE.search(ln)
198
+ if mm:
199
+ m = mm
200
+ time_line_idx = i
201
+ break
202
+ if not m:
203
+ raise ValueError(f"Bloque SRT sin tiempos válidos (índice {idx}):\n{block}")
204
+
205
+ start = _norm_ts(m.group("start"))
206
+ end = _norm_ts(m.group("end"))
207
+ text_lines = time_candidates[time_line_idx + 1 :]
208
+ text = "\n".join(text_lines).strip()
209
+
210
+ cues.append(Cue(index=idx, start=start, end=end, text=text))
211
+
212
+ # Re-indexar por si venía desordenado
213
+ for i, c in enumerate(cues, 1):
214
+ c.index = i
215
+ return cues
216
+
217
+ def _build_esf_tree(
218
+ cues: List[Cue],
219
+ language: str = "es",
220
+ voice_db: float = -6.0,
221
+ original_db: float = -3.0,
222
+ audio_lookup: Optional[Callable[[int], Optional[str]]] = None,
223
+ ) -> ET.ElementTree:
224
+ """
225
+ Construye el árbol XML ESF.
226
+ audio_lookup: función opcional index->filename (p. ej., lambda i: f\"{i:03d}.wav\" si existe).
227
+ """
228
+ root = ET.Element("esef", attrib={"version": "1.0"})
229
+ header = ET.SubElement(root, "header")
230
+ ET.SubElement(header, "language").text = language
231
+ mix = ET.SubElement(header, "mix")
232
+ ET.SubElement(mix, "voice", attrib={"level": f"{voice_db}dB"})
233
+ ET.SubElement(mix, "original", attrib={"level": f"{original_db}dB"})
234
+
235
+ ad = ET.SubElement(root, "ad")
236
+ for c in cues:
237
+ attrs = {"in": c.start, "out": c.end}
238
+ if audio_lookup:
239
+ fname = audio_lookup(c.index)
240
+ if fname:
241
+ attrs["file"] = fname
242
+ cue_el = ET.SubElement(ad, "cue", attrib=attrs)
243
+ cue_el.text = c.text
244
+ return ET.ElementTree(root)
245
+
246
+ def _xml_pretty_string(tree: ET.ElementTree) -> str:
247
+ """Devuelve XML con sangría (sin depender de minidom)."""
248
+ def _indent(elem, level=0):
249
+ i = "\n" + level * " "
250
+ if len(elem):
251
+ if not elem.text or not elem.text.strip():
252
+ elem.text = i + " "
253
+ for e in elem:
254
+ _indent(e, level + 1)
255
+ if not e.tail or not e.tail.strip():
256
+ e.tail = i
257
+ if level and (not elem.tail or not elem.tail.strip()):
258
+ elem.tail = i
259
+ root = tree.getroot()
260
+ _indent(root)
261
+ return ET.tostring(root, encoding="utf-8", xml_declaration=True).decode("utf-8")
262
+
263
+ def srt_to_esf(
264
+ srt_text: str,
265
+ *,
266
+ language: str = "es",
267
+ voice_db: float = -6.0,
268
+ original_db: float = -3.0,
269
+ audio_lookup: Optional[Callable[[int], Optional[str]]] = None,
270
+ ) -> str:
271
+ """
272
+ Convierte un SRT (texto) en un ESF (XML) y lo devuelve como string.
273
+
274
+ Parámetros:
275
+ srt_text: Contenido del .srt.
276
+ language: Código ISO del idioma (por defecto 'es').
277
+ voice_db: Nivel de la voz AD (dB).
278
+ original_db: Nivel del audio original (dB).
279
+ audio_lookup: Función opcional index->filename para asociar locuciones por cue.
280
+
281
+ Retorna:
282
+ Cadena XML ESF.
283
+ """
284
+ cues = _parse_srt(srt_text)
285
+ tree = _build_esf_tree(
286
+ cues,
287
+ language=language,
288
+ voice_db=voice_db,
289
+ original_db=original_db,
290
+ audio_lookup=audio_lookup,
291
+ )
292
+ return _xml_pretty_string(tree)
293
+
294
+
295
+
296
  #----------------------------
297
 
298
  if __name__ == "__main__":
videos/curtmetratge_4/MoE/une_ad.srt CHANGED
@@ -1,26 +1,26 @@
1
  1
2
- 00:00:00,000 --> 00:00:10,480
3
- (AD): "Neus es troba en un sofà, amb la llum apagada, concentrada en la pantalla del seu telèfon mòbil, reflexionant."
4
 
5
  2
6
- 00:00:10,480 --> 00:00:11,134
7
- (AD): ""
8
 
9
  3
 
 
 
 
10
  00:00:11,134 --> 00:00:12,044
11
  [Sento]: bon dia neus
12
 
13
- 4
14
  00:00:12,044 --> 00:00:12,653
15
  [Neus]: bon dia gràcies
16
 
17
- 5
18
- 00:00:12,653 --> 00:00:15,919
19
- (AD): "Neus, amb una samarreta, llegeix un diari."
20
-
21
  6
22
- 00:00:15,919 --> 00:00:16,568
23
- (AD): ""
24
 
25
  7
26
  00:00:16,568 --> 00:00:18,777
 
1
  1
2
+ 00:00:00,000 --> 00:00:01,020
3
+ (AD): "Títol: No puc."
4
 
5
  2
6
+ 00:00:01,020 --> 00:00:04,121
7
+ (AD): "Títol: No puc."
8
 
9
  3
10
+ 00:00:04,121 --> 00:00:11,134
11
+ (AD): "Neus es troba en un sofà, amb la llum apagada, concentrada en la pantalla del seu telèfon mòbil, reflexionant."
12
+
13
+ 4
14
  00:00:11,134 --> 00:00:12,044
15
  [Sento]: bon dia neus
16
 
17
+ 5
18
  00:00:12,044 --> 00:00:12,653
19
  [Neus]: bon dia gràcies
20
 
 
 
 
 
21
  6
22
+ 00:00:12,653 --> 00:00:16,568
23
+ (AD): "Neus, amb una samarreta, llegeix un diari."
24
 
25
  7
26
  00:00:16,568 --> 00:00:18,777
videos/curtmetratge_4/Salamandra/une_ad.srt CHANGED
@@ -1,10 +1,10 @@
1
  1
2
- 00:00:00,000 --> 00:00:10,480
3
- (AD): Una dona amb el llum apagat, asseguda en un sofà, mirant el seu telèfon.
4
 
5
  2
6
- 00:00:10,480 --> 00:00:11,134
7
- (AD): Una noia amb una samarreta de l'americana està llegint un diari en un restaurant.
8
 
9
  3
10
  00:00:11,134 --> 00:00:12,044
 
1
  1
2
+ 00:00:01,000 --> 00:00:03,000
3
+ (AD): "No puc."
4
 
5
  2
6
+ 00:00:03,000 --> 00:00:11,134
7
+ (AD): "Una dona amb el llum apagat, asseguda en un sofà, mirant el seu telèfon."
8
 
9
  3
10
  00:00:11,134 --> 00:00:12,044