VeuReu commited on
Commit
9df87bd
·
verified ·
1 Parent(s): 5a0b124

Create salamandra_router.py

Browse files
Files changed (1) hide show
  1. main_process/salamandra_router.py +635 -0
main_process/salamandra_router.py ADDED
@@ -0,0 +1,635 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import re
4
+ import ast
5
+ import json
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import List, Dict, Counter
9
+
10
+ # --- Third-Party Libraries ---
11
+ import cv2
12
+ import torch
13
+ from fastapi import APIRouter, UploadFile, File, Query, HTTPException
14
+ from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
15
+ from transformers import AutoModelForCausalLM, AutoTokenizer
16
+ from openai import OpenAI
17
+
18
+ # --- Internal Modules / Project Imports ---
19
+ from svision_client import (
20
+ extract_scenes,
21
+ add_ocr_and_faces,
22
+ keyframes_every_second_extraction,
23
+ extract_descripcion_escena
24
+ )
25
+
26
+ from asr_client import (
27
+ extract_audio_from_video,
28
+ diarize_audio,
29
+ transcribe_long_audio,
30
+ transcribe_short_audio,
31
+ identificar_veu
32
+ )
33
+
34
+ from storage.common import validate_token
35
+ from storage.files.file_manager import FileManager
36
+ from storage.embeddings_routers import get_embeddings_json
37
+
38
+ from main_process.main_router import (
39
+ get_initial_info_path,
40
+ get_initial_srt_path
41
+ )
42
+
43
+ EMBEDDINGS_ROOT = Path("/data/embeddings")
44
+ MEDIA_ROOT = Path("/data/media")
45
+ os.environ["CUDA_VISIBLE_DEVICES"] = "1"
46
+ router = APIRouter(prefix="/salamandra", tags=["Salamandra Process"])
47
+ HF_TOKEN = os.getenv("HF_TOKEN")
48
+ OPEN_AI_KEY = os.getenv("OPEN_AI_KEY")
49
+
50
+ class DataHub:
51
+ def __init__(self, video_analysis_json: str):
52
+ print("DataHub inicializando con JSON:", video_analysis_json)
53
+ self.video = json.loads(Path(video_analysis_json).read_text(encoding='utf-8'))
54
+
55
+ class NState(dict):
56
+ pass
57
+
58
+ # ---------------- LLM utilizado para el free_narration ----------------
59
+ class SalamandraClient:
60
+ def __init__(self, model_id="BSC-LT/salamandra-7b-instruct"):
61
+ self.tokenizer = AutoTokenizer.from_pretrained(model_id)
62
+ self.model = AutoModelForCausalLM.from_pretrained(
63
+ model_id,
64
+ device_map="auto",
65
+ torch_dtype=torch.bfloat16
66
+ )
67
+
68
+ def chat(self, prompt) -> str:
69
+ encodings = self.tokenizer(
70
+ prompt,
71
+ return_tensors="pt",
72
+ padding=True,
73
+ )
74
+
75
+ inputs = encodings["input_ids"].to(self.model.device)
76
+ attention_mask = encodings["attention_mask"].to(self.model.device)
77
+
78
+ outputs = self.model.generate(
79
+ input_ids=inputs,
80
+ attention_mask=attention_mask,
81
+ pad_token_id=self.tokenizer.pad_token_id,
82
+ max_new_tokens=300, # más grande si el texto es largo
83
+ temperature=0.01, # control de creatividad
84
+ top_k=50, # tokens más probables
85
+ top_p=0.9
86
+ )
87
+ print(self.tokenizer.decode(outputs[0], skip_special_tokens=True))
88
+ print("Separación")
89
+ # Cortar la parte del prompt
90
+ generated_tokens = outputs[0][inputs.shape[1]:]
91
+ return self.tokenizer.decode(generated_tokens, skip_special_tokens=True)
92
+
93
+ # Esto aquí sólo se utiliza para la valoración:
94
+ class GPT5Client:
95
+ def __init__(self, api_key: str):
96
+ key = api_key
97
+ if not key:
98
+ raise RuntimeError(f"Missing {api_key_env} in environment for GPT-5 client")
99
+ self.cli = OpenAI(api_key=key)
100
+ print("GPT5Client inicializado con clave env:", api_key_env)
101
+
102
+ def chat(self, messages: list, model: str = 'gpt-4o-mini') -> str:
103
+ print("GPT5Client.chat llamado con", len(messages), "mensajes")
104
+ r = self.cli.chat.completions.create(model=model, messages=messages,temperature=0)
105
+ content = r.choices[0].message.content.strip()
106
+ return content
107
+
108
+
109
+ def get_video_duration(video_path: str) -> float:
110
+ """
111
+ Devuelve la duración total del vídeo en segundos.
112
+ """
113
+ cap = cv2.VideoCapture(video_path)
114
+ if not cap.isOpened():
115
+ raise RuntimeError(f"No s'ha pogut obrir el vídeo: {video_path}")
116
+
117
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
118
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 0
119
+ cap.release()
120
+
121
+ duration_sec = total_frames / fps if total_frames > 0 else 0.0
122
+ return duration_sec
123
+
124
+ def generate_srt_con_silencios(path_srt_original, path_srt_silences, video_path):
125
+ # Obtenir duració total del vídeo
126
+ duracio_total = get_video_duration(video_path)
127
+
128
+ with open(path_srt_original, "r", encoding="utf-8-sig") as f:
129
+ srt_text = f.read()
130
+
131
+ blocks = srt_text.strip().split("\n\n")
132
+ prev = 0
133
+ srt_entries = []
134
+ idx = 1
135
+
136
+ for block in blocks:
137
+ lines = block.split("\n")
138
+ time_range = lines[1]
139
+ print(time_range)
140
+ content = " ".join(line.strip() for line in lines[2:])
141
+
142
+ start_str, end_str = time_range.split(" --> ")
143
+ start_sec = srt_time_to_seconds(start_str)
144
+ end_sec = srt_time_to_seconds(end_str)
145
+
146
+ # Afegir silenci si hi ha espai
147
+ if prev < start_sec:
148
+ srt_entries.append(
149
+ f"{idx}\n{seconds_to_srt_time(prev)} --> {seconds_to_srt_time(start_sec)}\n[silenci]\n"
150
+ )
151
+ idx += 1
152
+
153
+ # Afegir clip amb text
154
+ srt_entries.append(
155
+ f"{idx}\n{seconds_to_srt_time(start_sec)} --> {seconds_to_srt_time(end_sec)}\n{content}\n"
156
+ )
157
+ idx += 1
158
+ prev = end_sec
159
+
160
+ # Afegir últim bloc de silenci si la duració del vídeo és més llarga que l'últim clip
161
+ if prev < duracio_total:
162
+ srt_entries.append(
163
+ f"{idx}\n{seconds_to_srt_time(prev)} --> {seconds_to_srt_time(duracio_total)}\n[silenci]\n"
164
+ )
165
+
166
+ # Guardar a l'arxiu final
167
+ with open(path_srt_silences, "w", encoding="utf-8") as f:
168
+ f.write("\n".join(srt_entries))
169
+
170
+ def srt_time_to_seconds(s):
171
+ h, m, rest = s.split(":")
172
+ s, ms = rest.split(",")
173
+ return int(h)*3600 + int(m)*60 + float(s) + int(ms)/1000
174
+
175
+ def seconds_to_srt_time(seconds):
176
+ h = int(seconds // 3600)
177
+ m = int((seconds % 3600) // 60)
178
+ s = int(seconds % 60)
179
+ ms = int((seconds - int(seconds)) * 1000)
180
+ return f"{h:02}:{m:02}:{s:02},{ms:03}"
181
+
182
+ class Add_AD:
183
+ def __init__(self, data: DataHub):
184
+ self.data = data
185
+
186
+ def __call__(self, state: NState, srt_modified_silence, srt_modified_silence_con_ad) -> NState:
187
+ print("Add_Ad.__call__ iniciado")
188
+
189
+ # Leer SRT original
190
+ with open(srt_modified_silence, "r", encoding="utf-8") as f:
191
+ srt_text = f.read()
192
+
193
+ # Frames del video
194
+ frames = self.data.video.get('info_escenas', {})
195
+
196
+ # Parsear SRT a bloques
197
+ srt_blocks = []
198
+ srt_blocks_modified=[]
199
+ pattern = re.compile(
200
+ r"(\d+)\s+(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\s+(.*?)(?=\n\d+\n|\Z)",
201
+ re.S
202
+ )
203
+
204
+ for match in pattern.finditer(srt_text):
205
+ index = int(match.group(1))
206
+ start = srt_time_to_seconds(match.group(2))
207
+ end = srt_time_to_seconds(match.group(3))
208
+ text = match.group(4).strip()
209
+ srt_blocks.append({
210
+ "index": index,
211
+ "start": start,
212
+ "end": end,
213
+ "text": text
214
+ })
215
+
216
+ index=1
217
+ # Procesar cada bloque
218
+ for block in srt_blocks:
219
+ if "[silenci]" in block["text"]:
220
+ start_block = block["start"]
221
+ end_block = block["end"]
222
+
223
+ for frame in frames:
224
+ if frame.get("start")<=start_block and frame.get("end")>=end_block:
225
+ srt_blocks_modified.append({
226
+ "index":index,
227
+ "start": start_block,
228
+ "end": end_block,
229
+ "text": f"(AD): {frame.get('descripcion', '')}"
230
+ })
231
+ index+=1
232
+
233
+ elif start_block<frame.get("end")<end_block:
234
+ srt_blocks_modified.append({
235
+ "index":index,
236
+ "start": start_block,
237
+ "end": frame.get("end"),
238
+ "text": f"(AD): {frame.get('descripcion', '')}"
239
+ })
240
+ start_block=frame.get("end")
241
+ index+=1
242
+
243
+ elif start_block==frame.get("start") and start_block<end_block and frame.get("end")>=end_block:
244
+ srt_blocks_modified.append({
245
+ "index":index,
246
+ "start": start_block,
247
+ "end": end_block,
248
+ "text": f"(AD): {frame.get('descripcion', '')}"
249
+ })
250
+ start_block=end_block
251
+ index+=1
252
+
253
+ else:
254
+ srt_blocks_modified.append({
255
+ "index": index,
256
+ "start": block["start"],
257
+ "end": block["end"],
258
+ "text": block["text"]
259
+ })
260
+ index+=1
261
+
262
+ # Reconstruir el SRT final
263
+ srt_final = ""
264
+
265
+ for block in srt_blocks_modified:
266
+ start_tc = seconds_to_srt_time(block["start"])
267
+ end_tc = seconds_to_srt_time(block["end"])
268
+ srt_final += f"{block['index']}\n{start_tc} --> {end_tc}\n{block['text']}\n\n"
269
+
270
+ # Guardar en un nuevo archivo
271
+ with open(srt_modified_silence_con_ad, "w", encoding="utf-8") as f:
272
+ f.write(srt_final)
273
+
274
+ # Actualizar estado
275
+ state['srt_con_audiodescripcion'] = srt_final
276
+ return state
277
+
278
+ class Free_Narration:
279
+ def __init__(self, data: DataHub):
280
+ self.data = data
281
+
282
+ def __call__(self, state: NState, srt_original_silence_con_ad, story_path) -> NState:
283
+ print("Free_Narration.__call__ iniciado")
284
+
285
+ descriptions=[]
286
+ frames = self.data.video.get('info_escenas', [])
287
+ for frame in frames:
288
+ descriptions.append(frame["descripcion"])
289
+
290
+ full_transcription = self.data.video.get('full_transcription', [])
291
+
292
+ with open(srt_original_silence_con_ad, "r", encoding="utf-8-sig") as f:
293
+ diarization_text = f.read()
294
+
295
+ prompt = f"""
296
+ La teva tasca és elaborar una descripció lliure d'un vídeo d'unes 100 paraules a partir de la informació següent:
297
+ 1.) A partir del vídeo s'han extret captures de pantalla en els moments en què es canviava d'escena i tens una descripció de cadascuna d'elles a: {descriptions}
298
+ 2.) La transcripció completa del vídeo és: {full_transcription}
299
+ Per tant, a partir de tota aquesta informació, genera'm la història completa, intentant incloure els personatges identificats i la trama general de la història.
300
+ """
301
+ out = state['llm_Salamandra'](prompt)
302
+ print(out)
303
+
304
+ with open(story_path, "w", encoding="utf-8-sig") as f:
305
+ f.write(out)
306
+
307
+ state['free_narration'] = out
308
+
309
+ return state
310
+
311
+ class Valoracion_Final:
312
+ def __call__(self, state, srt_final, csv_evaluacion):
313
+ print("Valoracion_Final.__call__ iniciat")
314
+
315
+ # Llegeix el contingut del fitxer SRT
316
+ with open(srt_final, "r", encoding="utf-8-sig") as f:
317
+ srt_text = f.read().strip()
318
+
319
+ # Defineix el prompt principal
320
+ prompt = f"""
321
+ Ets un avaluador expert en accessibilitat audiovisual segons la NORMA UNE 153020.
322
+
323
+ Analitza el següent fitxer SRT i avalua'l segons les característiques indicades.
324
+ Per a cada característica, assigna una puntuació del 0 al 7 i una justificació breu i específica,
325
+ seguint el format establert.
326
+
327
+ SRT a analitzar:
328
+ {srt_text}
329
+
330
+ Format de sortida:
331
+ Caracteristica,Valoracio (0-7),Justificacio
332
+
333
+ Les característiques a avaluar són:
334
+ - Precisió Descriptiva: Avalua si la descripció visual dels plans, accions i context és exacta i coherent amb el contingut esperat.
335
+ - Sincronització Temporal: Avalua si el text apareix i desapareix al moment adequat segons el contingut visual o sonor.
336
+ - Claredat i Concisió: Analitza si el llenguatge és clar, natural i sense redundàncies.
337
+ - Inclusió de Diàleg/So: Determina si es recullen correctament els diàlegs, sons i elements musicals rellevants.
338
+ - Contextualització: Avalua si el context (ambient, espai, personatges, situacions) està ben representat.
339
+ - Flux i Ritme de la Narració: Avalua la fluïdesa de la lectura i la coherència temporal entre segments.
340
+
341
+ Respon només amb la taula CSV, sense cap text addicional.
342
+ """
343
+
344
+ # Missatges estructurats per al model (rols system + user)
345
+ messages = [
346
+ {"role": "system", "content": "Ets un assistent expert en accessibilitat audiovisual i normativa UNE 153020."},
347
+ {"role": "user", "content": prompt}
348
+ ]
349
+
350
+ # Crida al model (s’assumeix que state['llm_GPT'] és una funció que processa missatges)
351
+ out = state['llm_GPT'](messages)
352
+
353
+ out_text = str(out).strip()
354
+
355
+ # Escriu el resultat CSV
356
+ with open(csv_evaluacion, "w", encoding="utf-8-sig") as f:
357
+ f.write(out_text)
358
+
359
+ return state
360
+
361
+ @router.post("/generate_salamadra_result", tags=["Salamandra Process"])
362
+ async def generate_salamadra_result(
363
+ sha1: str,
364
+ token: str = Query(..., description="Token required for authorization")
365
+ ):
366
+ )
367
+ ):
368
+ """
369
+ Generate all Salamandra output files (final SRT, free narration, and evaluation CSV)
370
+ for a processed video identified by its SHA1 hash.
371
+
372
+ This endpoint orchestrates the full Salamandra processing pipeline:
373
+ - Validates the access token.
374
+ - Locates the processed video and its associated metadata.
375
+ - Generates an intermediate SRT file enriched with silence markers.
376
+ - Runs the Salamandra logic to produce:
377
+ * A finalized SRT subtitle file (`result.srt`)
378
+ * A free-narration text file (`free_narration.txt`)
379
+ * An evaluation CSV (`evaluation.csv`)
380
+ - Ensures the expected directory structure exists, creating folders if necessary.
381
+ - Uses both GPT-based and Salamandra-based LLMs to generate narrative and evaluation content.
382
+
383
+ Args:
384
+ sha1 (str): The SHA1 hash that identifies the media processing workspace.
385
+ token (str): Authorization token required to execute Salamandra operations.
386
+
387
+ Raises:
388
+ HTTPException:
389
+ - 404 if the SHA1 folder does not exist.
390
+ - 404 if the `clip` folder is missing.
391
+ - 404 if no MP4 file is found inside the clip folder.
392
+
393
+ Processing Steps:
394
+ 1. Validates that all required folders exist (`sha1`, `clip`, `result/Salamandra`).
395
+ 2. Retrieves the input video and initial metadata (original SRT, info JSON).
396
+ 3. Creates temporary enriched SRT with silence detection.
397
+ 4. Runs Add_AD, Free_Narration, and Valoracion_Final modules.
398
+ 5. Generates the final Salamandra output files:
399
+ - result.srt
400
+ - free_narration.txt
401
+ - evaluation.csv
402
+
403
+ Returns:
404
+ dict: A JSON response indicating successful generation:
405
+ {
406
+ "status": "ok",
407
+ "message": "Salamandra SRT, free_narration and CSV evaluation generated"
408
+ }
409
+ """
410
+ validate_token(token)
411
+
412
+ # Resolve directories
413
+ file_manager = FileManager(MEDIA_ROOT)
414
+ sha1_folder = MEDIA_ROOT / sha1
415
+ clip_folder = sha1_folder / "clip"
416
+
417
+ if not sha1_folder.exists() or not sha1_folder.is_dir():
418
+ raise HTTPException(status_code=404, detail="SHA1 folder not found")
419
+
420
+ if not clip_folder.exists() or not clip_folder.is_dir():
421
+ raise HTTPException(status_code=404, detail="Clip folder not found")
422
+
423
+ # Locate video file
424
+ mp4_files = list(clip_folder.glob("*.mp4"))
425
+ if not mp4_files:
426
+ raise HTTPException(status_code=404, detail="No MP4 files found")
427
+ video_path = clip_folder / mp4_files[0]
428
+
429
+ # Get initial srt
430
+ srt_original = get_initial_srt_path(sha1)
431
+
432
+ # Get initial info json
433
+ informacion_json = get_initial_info_path(sha1)
434
+
435
+ # Generate srt final path
436
+ file_manager = FileManager(MEDIA_ROOT)
437
+ sha1_folder = MEDIA_ROOT / sha1
438
+ result_folder = sha1_folder / "result"
439
+ result_folder.mkdir(parents=True, exist_ok=True)
440
+ salamdra_folder = result_folder / "Salamandra"
441
+ salamdra_folder.mkdir(parents=True, exist_ok=True)
442
+ srt_final = salamdra_folder / "result.srt"
443
+
444
+ # Generate free_narration_salamandra final path
445
+ file_manager = FileManager(MEDIA_ROOT)
446
+ sha1_folder = MEDIA_ROOT / sha1
447
+ result_folder = sha1_folder / "result"
448
+ result_folder.mkdir(parents=True, exist_ok=True)
449
+ salamdra_folder = result_folder / "Salamandra"
450
+ salamdra_folder.mkdir(parents=True, exist_ok=True)
451
+ free_narration_salamandra = salamdra_folder / "free_narration.txt"
452
+
453
+ # Generate evaluation csv path
454
+ file_manager = FileManager(MEDIA_ROOT)
455
+ sha1_folder = MEDIA_ROOT / sha1
456
+ result_folder = sha1_folder / "result"
457
+ result_folder.mkdir(parents=True, exist_ok=True)
458
+ salamdra_folder = result_folder / "Salamandra"
459
+ salamdra_folder.mkdir(parents=True, exist_ok=True)
460
+ csv_evaluacion = salamdra_folder / "evaluation.csv"
461
+
462
+ # Temp srt name
463
+ srt_name = sha1 + "_srt"
464
+ tmp = tempfile.NamedTemporaryFile(mode="w+", suffix=".srt", prefix=srt_name + "_", delete=False)
465
+
466
+ generate_srt_con_silencios(srt_original, tmp.name, video_path)
467
+
468
+ datahub=DataHub(informacion_json)
469
+ add_ad = Add_AD(datahub)
470
+ free_narration = Free_Narration(datahub)
471
+ valoracion_final = Valoracion_Final()
472
+
473
+ GPTclient = GPT5Client(api_key=OPEN_AI_KEY)
474
+ salamandraclient = SalamandraClient()
475
+
476
+ state = {
477
+ "llm_GPT": GPTclient.chat,
478
+ "llm_Salamandra": salamandraclient.chat
479
+ }
480
+
481
+ state = add_ad(state, tmp.name, srt_final)
482
+ state= free_narration(state, srt_final, free_narration_salamandra)
483
+ state = valoracion_final(state, srt_final, csv_evaluacion)
484
+ tmp.close()
485
+
486
+ return {"status": "ok", "message": "Salamandra SRT, free_narration and CSV evaluation generated"}
487
+
488
+ @router.get("/download_salamadra_srt", tags=["Salamandra Process"])
489
+ def download_salamadra_srt(
490
+ sha1: str,
491
+ token: str = Query(..., description="Token required for authorization")
492
+ ):
493
+ """
494
+ Download the final SRT subtitle file generated by the Salamandra processing pipeline.
495
+
496
+ This endpoint retrieves the file `result.srt` associated with a specific SHA1 hash.
497
+ It validates the authorization token, checks the expected folder structure, and
498
+ returns the subtitle file if it exists.
499
+
500
+ Args:
501
+ sha1 (str): The SHA1 identifier corresponding to the processed media folder.
502
+ token (str): Authorization token required to access the resource.
503
+
504
+ Raises:
505
+ HTTPException:
506
+ - 404 if any of the required directories (SHA1 folder, result folder, Salamandra folder)
507
+ are missing.
508
+ - 404 if the `result.srt` file is not found.
509
+
510
+ Returns:
511
+ FileResponse: The SRT file (`result.srt`) with media type `text/srt`.
512
+ """
513
+ validate_token(token)
514
+
515
+ file_manager = FileManager(MEDIA_ROOT)
516
+ sha1_folder = MEDIA_ROOT / sha1
517
+ result_folder = sha1_folder / "result"
518
+ result_folder.mkdir(parents=True, exist_ok=True)
519
+ salamandra_folder = result_folder / "Salamandra"
520
+ salamandra_folder.mkdir(parents=True, exist_ok=True)
521
+ srt_final = salamandra_folder / "result.srt"
522
+
523
+ if not sha1_folder.exists() or not sha1_folder.is_dir():
524
+ raise HTTPException(status_code=404, detail="SHA1 folder not found")
525
+ if not result_folder.exists() or not result_folder.is_dir():
526
+ raise HTTPException(status_code=404, detail="result folder not found")
527
+ if not salamandra_folder.exists() or not salamandra_folder.is_dir():
528
+ raise HTTPException(status_code=404, detail="Salamandra folder not found")
529
+ if not srt_final.exists() or not srt_final.is_file():
530
+ raise HTTPException(status_code=404, detail="result.srt SRT not found")
531
+
532
+ return FileResponse(
533
+ path=srt_final,
534
+ media_type="text/srt",
535
+ filename="result.srt"
536
+ )
537
+
538
+ @router.get("/download_salamadra_free_narration", tags=["Salamandra Process"])
539
+ def download_salamadra_free_narration(
540
+ sha1: str,
541
+ token: str = Query(..., description="Token required for authorization")
542
+ ):
543
+ """
544
+ Download the free narration text file generated by the Salamandra process.
545
+
546
+ This endpoint retrieves `free_narration.txt` from the Salamandra result directory
547
+ associated with a specific SHA1 hash. The token is validated before accessing the
548
+ file system. If the file or required folders do not exist, appropriate HTTP
549
+ errors are returned.
550
+
551
+ Args:
552
+ sha1 (str): The SHA1 identifier for the processed media folder.
553
+ token (str): Authorization token required to access the file.
554
+
555
+ Raises:
556
+ HTTPException:
557
+ - 404 if the SHA1 folder, result folder, or Salamandra folder is missing.
558
+ - 404 if `free_narration.txt` is not found.
559
+
560
+ Returns:
561
+ FileResponse: The free narration text file with media type `text/srt`.
562
+ """
563
+ validate_token(token)
564
+
565
+ file_manager = FileManager(MEDIA_ROOT)
566
+ sha1_folder = MEDIA_ROOT / sha1
567
+ result_folder = sha1_folder / "result"
568
+ result_folder.mkdir(parents=True, exist_ok=True)
569
+ salamandra_folder = result_folder / "Salamandra"
570
+ salamandra_folder.mkdir(parents=True, exist_ok=True)
571
+ free_narration_salamandra = salamandra_folder / "free_narration.txt"
572
+
573
+ if not sha1_folder.exists() or not sha1_folder.is_dir():
574
+ raise HTTPException(status_code=404, detail="SHA1 folder not found")
575
+ if not result_folder.exists() or not result_folder.is_dir():
576
+ raise HTTPException(status_code=404, detail="result folder not found")
577
+ if not salamandra_folder.exists() or not salamandra_folder.is_dir():
578
+ raise HTTPException(status_code=404, detail="Salamandra folder not found")
579
+ if not free_narration_salamandra.exists() or not free_narration_salamandra.is_file():
580
+ raise HTTPException(status_code=404, detail="free_narration.txt not found")
581
+
582
+ return FileResponse(
583
+ path=free_narration_salamandra,
584
+ media_type="text/srt",
585
+ filename="result.srt"
586
+ )
587
+
588
+ @router.get("/download_salamadra_csv_evaluation", tags=["Salamandra Process"])
589
+ def download_salamadra_csv_evaluation(
590
+ sha1: str,
591
+ token: str = Query(..., description="Token required for authorization")
592
+ ):
593
+ """
594
+ Download the evaluation CSV generated by the Salamandra processing workflow.
595
+
596
+ This endpoint returns the `evaluation.csv` file corresponding to the given SHA1 hash.
597
+ It performs token validation and ensures that the folder structure and file exist.
598
+ If any element is missing, a 404 HTTP error is raised.
599
+
600
+ Args:
601
+ sha1 (str): The SHA1 identifier representing the processed media directory.
602
+ token (str): Authorization token required for file retrieval.
603
+
604
+ Raises:
605
+ HTTPException:
606
+ - 404 if the SHA1 folder, result folder, or Salamandra folder does not exist.
607
+ - 404 if the `evaluation.csv` file is missing.
608
+
609
+ Returns:
610
+ FileResponse: The evaluation CSV file with media type `text/srt`.
611
+ """
612
+ validate_token(token)
613
+
614
+ file_manager = FileManager(MEDIA_ROOT)
615
+ sha1_folder = MEDIA_ROOT / sha1
616
+ result_folder = sha1_folder / "result"
617
+ result_folder.mkdir(parents=True, exist_ok=True)
618
+ salamandra_folder = result_folder / "Salamandra"
619
+ salamandra_folder.mkdir(parents=True, exist_ok=True)
620
+ csv_evaluacion = salamandra_folder / "evaluation.csv"
621
+
622
+ if not sha1_folder.exists() or not sha1_folder.is_dir():
623
+ raise HTTPException(status_code=404, detail="SHA1 folder not found")
624
+ if not result_folder.exists() or not result_folder.is_dir():
625
+ raise HTTPException(status_code=404, detail="result folder not found")
626
+ if not salamandra_folder.exists() or not salamandra_folder.is_dir():
627
+ raise HTTPException(status_code=404, detail="Salamandra folder not found")
628
+ if not csv_evaluacion.exists() or not csv_evaluacion.is_file():
629
+ raise HTTPException(status_code=404, detail="evaluation.csv CSV not found")
630
+
631
+ return FileResponse(
632
+ path=csv_evaluacion,
633
+ media_type="text/srt",
634
+ filename="result.srt"
635
+ )