VeuReu commited on
Commit
d390d05
·
verified ·
1 Parent(s): b102cea

Update main_process/salamandra_router.py

Browse files
Files changed (1) hide show
  1. main_process/salamandra_router.py +1085 -108
main_process/salamandra_router.py CHANGED
@@ -55,42 +55,32 @@ class DataHub:
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
@@ -104,24 +94,7 @@ class GPT5Client:
104
  content = r.choices[0].message.content.strip()
105
  return content
106
 
107
-
108
- def get_video_duration(video_path: str) -> float:
109
- """
110
- Devuelve la duración total del vídeo en segundos.
111
- """
112
- cap = cv2.VideoCapture(video_path)
113
- if not cap.isOpened():
114
- raise RuntimeError(f"No s'ha pogut obrir el vídeo: {video_path}")
115
-
116
- fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
117
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 0
118
- cap.release()
119
-
120
- duration_sec = total_frames / fps if total_frames > 0 else 0.0
121
- return duration_sec
122
-
123
  def generate_srt_con_silencios(path_srt_original, path_srt_silences, video_path):
124
- # Obtenir duració total del vídeo
125
  duracio_total = get_video_duration(video_path)
126
 
127
  with open(path_srt_original, "r", encoding="utf-8-sig") as f:
@@ -135,37 +108,48 @@ def generate_srt_con_silencios(path_srt_original, path_srt_silences, video_path)
135
  for block in blocks:
136
  lines = block.split("\n")
137
  time_range = lines[1]
138
- print(time_range)
139
  content = " ".join(line.strip() for line in lines[2:])
140
 
141
  start_str, end_str = time_range.split(" --> ")
142
  start_sec = srt_time_to_seconds(start_str)
143
  end_sec = srt_time_to_seconds(end_str)
144
 
145
- # Afegir silenci si hi ha espai
146
  if prev < start_sec:
147
  srt_entries.append(
148
  f"{idx}\n{seconds_to_srt_time(prev)} --> {seconds_to_srt_time(start_sec)}\n[silenci]\n"
149
  )
150
  idx += 1
151
 
152
- # Afegir clip amb text
153
  srt_entries.append(
154
  f"{idx}\n{seconds_to_srt_time(start_sec)} --> {seconds_to_srt_time(end_sec)}\n{content}\n"
155
  )
156
  idx += 1
157
  prev = end_sec
158
 
159
- # Afegir últim bloc de silenci si la duració del vídeo és més llarga que l'últim clip
160
  if prev < duracio_total:
161
  srt_entries.append(
162
  f"{idx}\n{seconds_to_srt_time(prev)} --> {seconds_to_srt_time(duracio_total)}\n[silenci]\n"
163
  )
164
 
165
- # Guardar a l'arxiu final
166
  with open(path_srt_silences, "w", encoding="utf-8") as f:
167
  f.write("\n".join(srt_entries))
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  def srt_time_to_seconds(s):
170
  h, m, rest = s.split(":")
171
  s, ms = rest.split(",")
@@ -182,17 +166,14 @@ class Add_AD:
182
  def __init__(self, data: DataHub):
183
  self.data = data
184
 
185
- def __call__(self, state: NState, srt_modified_silence, srt_modified_silence_con_ad) -> NState:
186
  print("Add_Ad.__call__ iniciado")
187
 
188
- # Leer SRT original
189
- with open(srt_modified_silence, "r", encoding="utf-8") as f:
190
  srt_text = f.read()
191
 
192
- # Frames del video
193
  frames = self.data.video.get('info_escenas', {})
194
 
195
- # Parsear SRT a bloques
196
  srt_blocks = []
197
  srt_blocks_modified=[]
198
  pattern = re.compile(
@@ -213,7 +194,11 @@ class Add_AD:
213
  })
214
 
215
  index=1
216
- # Procesar cada bloque
 
 
 
 
217
  for block in srt_blocks:
218
  if "[silenci]" in block["text"]:
219
  start_block = block["start"]
@@ -225,30 +210,75 @@ class Add_AD:
225
  "index":index,
226
  "start": start_block,
227
  "end": end_block,
228
- "text": f"(AD): {frame.get('descripcion', '')}"
229
  })
230
  index+=1
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  elif start_block<frame.get("end")<end_block:
233
  srt_blocks_modified.append({
234
  "index":index,
235
  "start": start_block,
236
  "end": frame.get("end"),
237
- "text": f"(AD): {frame.get('descripcion', '')}"
238
  })
239
  start_block=frame.get("end")
240
  index+=1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
  elif start_block==frame.get("start") and start_block<end_block and frame.get("end")>=end_block:
243
  srt_blocks_modified.append({
244
  "index":index,
245
  "start": start_block,
246
  "end": end_block,
247
- "text": f"(AD): {frame.get('descripcion', '')}"
248
  })
249
  start_block=end_block
250
  index+=1
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  else:
253
  srt_blocks_modified.append({
254
  "index": index,
@@ -258,7 +288,12 @@ class Add_AD:
258
  })
259
  index+=1
260
 
261
- # Reconstruir el SRT final
 
 
 
 
 
262
  srt_final = ""
263
 
264
  for block in srt_blocks_modified:
@@ -266,53 +301,658 @@ class Add_AD:
266
  end_tc = seconds_to_srt_time(block["end"])
267
  srt_final += f"{block['index']}\n{start_tc} --> {end_tc}\n{block['text']}\n\n"
268
 
269
- # Guardar en un nuevo archivo
270
- with open(srt_modified_silence_con_ad, "w", encoding="utf-8") as f:
271
  f.write(srt_final)
272
 
273
- # Actualizar estado
274
- state['srt_con_audiodescripcion'] = srt_final
 
 
 
275
  return state
276
 
277
- class Free_Narration:
278
- def __init__(self, data: DataHub):
279
- self.data = data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
- def __call__(self, state: NState, srt_original_silence_con_ad, story_path) -> NState:
282
- print("Free_Narration.__call__ iniciado")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
- descriptions=[]
285
- frames = self.data.video.get('info_escenas', [])
286
- for frame in frames:
287
- descriptions.append(frame["descripcion"])
288
 
289
- full_transcription = self.data.video.get('full_transcription', [])
 
 
290
 
291
- with open(srt_original_silence_con_ad, "r", encoding="utf-8-sig") as f:
292
- diarization_text = f.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  prompt = f"""
295
- La teva tasca és elaborar una descripció lliure d'un vídeo d'unes 100 paraules a partir de la informació següent:
296
- 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}
297
- 2.) La transcripció completa del vídeo és: {full_transcription}
298
- 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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  """
300
- out = state['llm_Salamandra'](prompt)
301
- print(out)
302
 
303
- with open(story_path, "w", encoding="utf-8-sig") as f:
304
- f.write(out)
 
 
305
 
306
- state['free_narration'] = out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
  return state
309
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  class Valoracion_Final:
311
- def __call__(self, state, srt_final, csv_evaluacion):
312
  print("Valoracion_Final.__call__ iniciat")
313
 
314
  # Llegeix el contingut del fitxer SRT
315
- with open(srt_final, "r", encoding="utf-8-sig") as f:
316
  srt_text = f.read().strip()
317
 
318
  # Defineix el prompt principal
@@ -357,15 +997,48 @@ class Valoracion_Final:
357
 
358
  return state
359
 
360
- @router.post("/generate_salamadra_result", tags=["Salamandra Process"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  async def generate_salamadra_result(
362
  sha1: str,
363
  token: str = Query(..., description="Token required for authorization")
364
  ):
365
  """
366
- Generate all Salamandra output files (final SRT, free narration, and evaluation CSV)
367
  for a processed video identified by its SHA1 hash.
368
-
369
  This endpoint orchestrates the full Salamandra processing pipeline:
370
  - Validates the access token.
371
  - Locates the processed video and its associated metadata.
@@ -376,17 +1049,14 @@ async def generate_salamadra_result(
376
  * An evaluation CSV (`evaluation.csv`)
377
  - Ensures the expected directory structure exists, creating folders if necessary.
378
  - Uses both GPT-based and Salamandra-based LLMs to generate narrative and evaluation content.
379
-
380
  Args:
381
  sha1 (str): The SHA1 hash that identifies the media processing workspace.
382
  token (str): Authorization token required to execute Salamandra operations.
383
-
384
  Raises:
385
  HTTPException:
386
  - 404 if the SHA1 folder does not exist.
387
  - 404 if the `clip` folder is missing.
388
  - 404 if no MP4 file is found inside the clip folder.
389
-
390
  Processing Steps:
391
  1. Validates that all required folders exist (`sha1`, `clip`, `result/Salamandra`).
392
  2. Retrieves the input video and initial metadata (original SRT, info JSON).
@@ -396,7 +1066,6 @@ async def generate_salamadra_result(
396
  - result.srt
397
  - free_narration.txt
398
  - evaluation.csv
399
-
400
  Returns:
401
  dict: A JSON response indicating successful generation:
402
  {
@@ -455,31 +1124,339 @@ async def generate_salamadra_result(
455
  salamdra_folder = result_folder / "Salamandra"
456
  salamdra_folder.mkdir(parents=True, exist_ok=True)
457
  csv_evaluacion = salamdra_folder / "evaluation.csv"
 
 
458
 
459
- # Temp srt name
460
- srt_name = sha1 + "_srt"
461
- tmp = tempfile.NamedTemporaryFile(mode="w+", suffix=".srt", prefix=srt_name + "_", delete=False)
462
-
463
- generate_srt_con_silencios(srt_original, tmp.name, video_path)
464
-
465
- datahub=DataHub(informacion_json)
466
  add_ad = Add_AD(datahub)
467
- free_narration = Free_Narration(datahub)
 
 
 
 
 
 
468
  valoracion_final = Valoracion_Final()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
 
470
  GPTclient = GPT5Client(api_key=OPEN_AI_KEY)
471
- salamandraclient = SalamandraClient()
472
 
 
473
  state = {
474
  "llm_GPT": GPTclient.chat,
475
  "llm_Salamandra": salamandraclient.chat
476
  }
477
 
478
- state = add_ad(state, tmp.name, srt_final)
479
- state= free_narration(state, srt_final, free_narration_salamandra)
480
- state = valoracion_final(state, srt_final, csv_evaluacion)
481
- tmp.close()
482
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  return {"status": "ok", "message": "Salamandra SRT, free_narration and CSV evaluation generated"}
484
 
485
  @router.get("/download_salamadra_srt", tags=["Salamandra Process"])
 
55
  class NState(dict):
56
  pass
57
 
 
58
  class SalamandraClient:
59
+ def __init__(self, model_id="BSC-LT/salamandra-7b-instruct-tools"):
60
+ self.tokenizer = AutoTokenizer.from_pretrained(model_id, use_auth_token=HF_TOKEN)
61
  self.model = AutoModelForCausalLM.from_pretrained(
62
  model_id,
63
  device_map="auto",
64
+ torch_dtype=torch.bfloat16,
65
+ use_auth_token= HF_TOKEN
66
  )
67
 
68
+ def chat(self, message) -> str:
69
+ date_string = datetime.today().strftime('%Y-%m-%d')
 
 
 
 
70
 
71
+ prompt = self.tokenizer.apply_chat_template(
72
+ message,
73
+ tokenize=False,
74
+ add_generation_prompt=True,
75
+ date_string=date_string,
76
+ tools=tools
 
 
 
 
 
77
  )
 
 
 
 
 
78
 
79
+ inputs = self.tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
80
+ outputs = self.model.generate(input_ids=inputs.to(self.model.device), max_new_tokens=200)
81
+
82
+ return self.tokenizer.decode(outputs[0], skip_special_tokens=True)
83
+
84
  class GPT5Client:
85
  def __init__(self, api_key: str):
86
  key = api_key
 
94
  content = r.choices[0].message.content.strip()
95
  return content
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  def generate_srt_con_silencios(path_srt_original, path_srt_silences, video_path):
 
98
  duracio_total = get_video_duration(video_path)
99
 
100
  with open(path_srt_original, "r", encoding="utf-8-sig") as f:
 
108
  for block in blocks:
109
  lines = block.split("\n")
110
  time_range = lines[1]
 
111
  content = " ".join(line.strip() for line in lines[2:])
112
 
113
  start_str, end_str = time_range.split(" --> ")
114
  start_sec = srt_time_to_seconds(start_str)
115
  end_sec = srt_time_to_seconds(end_str)
116
 
 
117
  if prev < start_sec:
118
  srt_entries.append(
119
  f"{idx}\n{seconds_to_srt_time(prev)} --> {seconds_to_srt_time(start_sec)}\n[silenci]\n"
120
  )
121
  idx += 1
122
 
 
123
  srt_entries.append(
124
  f"{idx}\n{seconds_to_srt_time(start_sec)} --> {seconds_to_srt_time(end_sec)}\n{content}\n"
125
  )
126
  idx += 1
127
  prev = end_sec
128
 
 
129
  if prev < duracio_total:
130
  srt_entries.append(
131
  f"{idx}\n{seconds_to_srt_time(prev)} --> {seconds_to_srt_time(duracio_total)}\n[silenci]\n"
132
  )
133
 
 
134
  with open(path_srt_silences, "w", encoding="utf-8") as f:
135
  f.write("\n".join(srt_entries))
136
 
137
+
138
+ def get_video_duration(video_path: str) -> float:
139
+ """
140
+ Devuelve la duración total del vídeo en segundos.
141
+ """
142
+ cap = cv2.VideoCapture(video_path)
143
+ if not cap.isOpened():
144
+ raise RuntimeError(f"No s'ha pogut obrir el vídeo: {video_path}")
145
+
146
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
147
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 0
148
+ cap.release()
149
+
150
+ duration_sec = total_frames / fps if total_frames > 0 else 0.0
151
+ return duration_sec
152
+
153
  def srt_time_to_seconds(s):
154
  h, m, rest = s.split(":")
155
  s, ms = rest.split(",")
 
166
  def __init__(self, data: DataHub):
167
  self.data = data
168
 
169
+ def __call__(self, state: NState, srt_original_silence, srt_original_silence_con_ad) -> NState:
170
  print("Add_Ad.__call__ iniciado")
171
 
172
+ with open(srt_original_silence, "r", encoding="utf-8") as f:
 
173
  srt_text = f.read()
174
 
 
175
  frames = self.data.video.get('info_escenas', {})
176
 
 
177
  srt_blocks = []
178
  srt_blocks_modified=[]
179
  pattern = re.compile(
 
194
  })
195
 
196
  index=1
197
+ persona_keyframe = []
198
+ personas_per_second = []
199
+ ocr_text = []
200
+ descripcion_text =[]
201
+
202
  for block in srt_blocks:
203
  if "[silenci]" in block["text"]:
204
  start_block = block["start"]
 
210
  "index":index,
211
  "start": start_block,
212
  "end": end_block,
213
+ "text": f"(AD): OCR: {frame.get('ocr')}\nDescripción: {frame.get('descripcion', '')}"
214
  })
215
  index+=1
216
 
217
+ personas=frame.get("faces")
218
+ if personas==[]:
219
+ persona_keyframe.append([])
220
+ else:
221
+ person=[]
222
+ for p in personas:
223
+ person.append(p)
224
+ persona_keyframe.append(person)
225
+
226
+ persona=frame.get("counts",{})
227
+ personas_per_second.append(persona)
228
+
229
+ ocr_text.append(frame.get('ocr'))
230
+ descripcion_text.append(frame.get('descripcion', ''))
231
+
232
  elif start_block<frame.get("end")<end_block:
233
  srt_blocks_modified.append({
234
  "index":index,
235
  "start": start_block,
236
  "end": frame.get("end"),
237
+ "text": f"(AD): OCR: {frame.get('ocr')}\n Descripción: {frame.get('descripcion', '')}"
238
  })
239
  start_block=frame.get("end")
240
  index+=1
241
+
242
+ personas=frame.get("faces")
243
+ if personas==[]:
244
+ persona_keyframe.append([])
245
+ else:
246
+ person=[]
247
+ for p in personas:
248
+ person.append(p)
249
+ persona_keyframe.append(person)
250
+
251
+ persona=frame.get("counts",{})
252
+ personas_per_second.append(persona)
253
+
254
+ ocr_text.append(frame.get('ocr'))
255
+ descripcion_text.append(frame.get('descripcion', ''))
256
 
257
  elif start_block==frame.get("start") and start_block<end_block and frame.get("end")>=end_block:
258
  srt_blocks_modified.append({
259
  "index":index,
260
  "start": start_block,
261
  "end": end_block,
262
+ "text": f"(AD): OCR: {frame.get('ocr')}\n Descripción: {frame.get('descripcion', '')}"
263
  })
264
  start_block=end_block
265
  index+=1
266
 
267
+ personas=frame.get("faces")
268
+ if personas==[]:
269
+ persona_keyframe.append([])
270
+ else:
271
+ person=[]
272
+ for p in personas:
273
+ person.append(p)
274
+ persona_keyframe.append(person)
275
+
276
+ persona=frame.get("counts",{})
277
+ personas_per_second.append(persona)
278
+
279
+ ocr_text.append(frame.get('ocr'))
280
+ descripcion_text.append(frame.get('descripcion', ''))
281
+
282
  else:
283
  srt_blocks_modified.append({
284
  "index": index,
 
288
  })
289
  index+=1
290
 
291
+ persona_keyframe.append("")
292
+ personas_per_second.append({})
293
+
294
+ ocr_text.append("")
295
+ descripcion_text.append("")
296
+
297
  srt_final = ""
298
 
299
  for block in srt_blocks_modified:
 
301
  end_tc = seconds_to_srt_time(block["end"])
302
  srt_final += f"{block['index']}\n{start_tc} --> {end_tc}\n{block['text']}\n\n"
303
 
304
+ with open(srt_original_silence_con_ad, "w", encoding="utf-8") as f:
 
305
  f.write(srt_final)
306
 
307
+ state['personas_keyframes'] = persona_keyframe
308
+ state['personas_per_second'] = personas_per_second
309
+ state['ocr'] = ocr_text
310
+ state['descripcion'] = descripcion_text
311
+
312
  return state
313
 
314
+ class Add_Silence_AD:
315
+ def __call__(self, state: NState, srt_original_silence_con_ad, srt_original_silence_con_ad_silence) -> NState:
316
+ print("Add_Silence_AD.__call__ iniciado")
317
+
318
+ with open(srt_original_silence_con_ad, "r", encoding="utf-8") as f:
319
+ srt_text = f.read()
320
+
321
+ srt_blocks = []
322
+ srt_blocks_modified=[]
323
+ pattern = re.compile(
324
+ r"(\d+)\s+(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\s+(.*?)(?=\n\d+\n|\Z)",
325
+ re.S
326
+ )
327
+
328
+ for match in pattern.finditer(srt_text):
329
+ index = int(match.group(1))
330
+ start = srt_time_to_seconds(match.group(2))
331
+ end = srt_time_to_seconds(match.group(3))
332
+ text = match.group(4).strip()
333
+ srt_blocks.append({
334
+ "index": index,
335
+ "start": start,
336
+ "end": end,
337
+ "text": text
338
+ })
339
+
340
+ index=1
341
+
342
+ for block in srt_blocks:
343
+ if "(AD):" in block["text"]:
344
+ start_block = block["start"]
345
+ end_block = block["end"]
346
+
347
+ if end_block - start_block < 2.0:
348
+ srt_blocks_modified.append({
349
+ "index":index,
350
+ "start": start_block,
351
+ "end": end_block,
352
+ "text": f"(AD): "
353
+ })
354
+ index+=1
355
+
356
+ else:
357
+ srt_blocks_modified.append({
358
+ "index":index,
359
+ "start": start_block,
360
+ "end": end_block,
361
+ "text": block['text']
362
+ })
363
+ index+=1
364
+
365
+ else:
366
+ srt_blocks_modified.append({
367
+ "index": index,
368
+ "start": block["start"],
369
+ "end": block["end"],
370
+ "text": block["text"]
371
+ })
372
+ index+=1
373
+
374
+ srt_final = ""
375
+
376
+ for block in srt_blocks_modified:
377
+ start_tc = seconds_to_srt_time(block["start"])
378
+ end_tc = seconds_to_srt_time(block["end"])
379
+ srt_final += f"{block['index']}\n{start_tc} --> {end_tc}\n{block['text']}\n\n"
380
+
381
+ with open(srt_original_silence_con_ad_silence, "w", encoding="utf-8") as f:
382
+ f.write(srt_final)
383
+
384
+ return state
385
+
386
+ def es_silencio(texto):
387
+ if "(AD):" in texto:
388
+ if "OCR:" in texto:
389
+ return False
390
+
391
+ elif "[" in texto:
392
+ return False
393
+
394
+ else:
395
+ return True
396
+
397
+ else:
398
+ return False
399
+
400
+ class Unir_AD_Silence:
401
+ def __call__(self, state: NState, srt_original_silence_con_ad_silence, srt_original_silence_con_ad_silence_unidos) -> NState:
402
+ print("Unir_AD_Silence.__call__ iniciado")
403
+
404
+ with open(srt_original_silence_con_ad_silence, "r", encoding="utf-8") as f:
405
+ srt_text = f.read()
406
+
407
+ srt_blocks = []
408
+ pattern = re.compile(
409
+ r"(\d+)\s+(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\s+(.*?)(?=\n\d+\n|\Z)",
410
+ re.S
411
+ )
412
+
413
+ for match in pattern.finditer(srt_text):
414
+ index = int(match.group(1))
415
+ start = srt_time_to_seconds(match.group(2))
416
+ end = srt_time_to_seconds(match.group(3))
417
+ text = match.group(4).strip()
418
+ srt_blocks.append({
419
+ "index": index,
420
+ "start": start,
421
+ "end": end,
422
+ "text": text
423
+ })
424
+
425
+ index_unidos = 1
426
+ i=0
427
+ srt_blocks_unidos = []
428
+ bloques_unidos = []
429
+ ocr_text = state["ocr"]
430
+
431
+ descripcion = state["descripcion"]
432
+
433
+ while i < len(srt_blocks):
434
+ actual = srt_blocks[i]
435
+
436
+ if es_silencio(actual["text"]) and "(AD):" in actual["text"]:
437
+ origenes = [i]
438
+ start_time = actual["start"]
439
+ end_time = actual["end"]
440
+ j = i+1
441
+ texto_ocr = ocr_text[i]
442
+ texto_descripcion = descripcion[i]
443
+
444
+ while j < len(srt_blocks) and es_silencio(srt_blocks[j]["text"]) and "(AD):" in srt_blocks[j]["text"]:
445
+ end_time = srt_blocks[j]["end"]
446
+ origenes.append(j)
447
+ texto_ocr += "" + ocr_text[j]
448
+ if descripcion[j] is None:
449
+ descripcion[j] = ""
450
+ texto_descripcion += "" + descripcion[j]
451
+
452
+ j+=1
453
+
454
+ srt_blocks_unidos.append({
455
+ "index": index_unidos,
456
+ "start": start_time,
457
+ "end": end_time,
458
+ "text": f"(AD): OCR: {texto_ocr}\n Descripción: {texto_descripcion}"
459
+ })
460
+
461
+ i = j
462
+ index_unidos +=1
463
+
464
+ else:
465
+ origenes=[i]
466
+ srt_blocks_unidos.append({
467
+ "index": index_unidos,
468
+ "start": srt_blocks[i]["start"],
469
+ "end": srt_blocks[i]["end"],
470
+ "text": srt_blocks[i]["text"]
471
+ })
472
+ i +=1
473
+ index_unidos +=1
474
+
475
+ bloques_unidos.append(origenes)
476
+
477
+ srt_final = ""
478
+
479
+ for block in srt_blocks_unidos:
480
+ start_tc = seconds_to_srt_time(block["start"])
481
+ end_tc = seconds_to_srt_time(block["end"])
482
+ srt_final += f"{block['index']}\n{start_tc} --> {end_tc}\n{block['text']}\n\n"
483
+
484
+ with open(srt_original_silence_con_ad_silence_unidos, "w", encoding="utf-8") as f:
485
+ f.write(srt_final)
486
+
487
+ state["bloques_unidos"] = bloques_unidos
488
+
489
+ return state
490
+
491
+ class Unir_AD_Silences_a_ADs:
492
+ def __call__(self, state: NState, srt_original_silence_con_ad_silence_unidos_silence, srt_original_silence_con_ad_silence_unidos_silence_general) -> NState:
493
+ print("Unir_AD_Silences_a_ADs.__call__ iniciado")
494
+
495
+ with open(srt_original_silence_con_ad_silence_unidos_silence, "r", encoding="utf-8") as f:
496
+ srt_text = f.read()
497
+
498
+ srt_blocks = []
499
+ pattern = re.compile(
500
+ r"(\d+)\s+(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\s+(.*?)(?=\n\d+\n|\Z)",
501
+ re.S
502
+ )
503
+
504
+ for match in pattern.finditer(srt_text):
505
+ index = int(match.group(1))
506
+ start = srt_time_to_seconds(match.group(2))
507
+ end = srt_time_to_seconds(match.group(3))
508
+ text = match.group(4).strip()
509
+ srt_blocks.append({
510
+ "index": index,
511
+ "start": start,
512
+ "end": end,
513
+ "text": text
514
+ })
515
+
516
+ index = 1
517
+ srt_blocks_unidos = []
518
+
519
+ bloques_unidos = state["bloques_unidos"]
520
+ nuevos_bloques_unidos = []
521
+
522
+ for i, block in enumerate(srt_blocks):
523
+ antes = False
524
+ despues = False
525
+
526
+ if "(AD):" in block["text"]:
527
+
528
+ if es_silencio(block["text"]):
529
+ if i!=0 and ("(AD): OCR:" in srt_blocks[i-1]["text"]):
530
+ continue
531
+
532
+ elif i!=len(srt_blocks)-1 and ("(AD): OCR:" in srt_blocks[i+1]["text"]):
533
+ continue
534
+
535
+ else:
536
+ nuevos_bloques_unidos.append(bloques_unidos[i])
537
+ srt_blocks_unidos.append({
538
+ "index": index,
539
+ "start": block["start"],
540
+ "end": block["end"],
541
+ "text": block["text"]
542
+ })
543
+ index += 1
544
+
545
+ elif "(AD): OCR:" in block["text"]:
546
+
547
+ if i!=0 and es_silencio(srt_blocks[i-1]["text"]):
548
+ start_time = srt_blocks[i-1]["start"]
549
+ antes = True
550
 
551
+ if i!= len(srt_blocks)-1 and es_silencio(srt_blocks[i+1]["text"]):
552
+ end_time = srt_blocks[i+1]["end"]
553
+ despues = True
554
+
555
+ if antes == True and despues == True:
556
+ start = start_time
557
+ end = end_time
558
+
559
+ elif antes == True and despues == False:
560
+ start = start_time
561
+ end = block["end"]
562
+
563
+ elif antes == False and despues == True:
564
+ start = block["start"]
565
+ end = end_time
566
+
567
+ else:
568
+ start = block["start"]
569
+ end = block["end"]
570
+ nuevos_bloques_unidos.append(bloques_unidos[i])
571
+ srt_blocks_unidos.append({
572
+ "index": index,
573
+ "start": start,
574
+ "end": end,
575
+ "text": block["text"]
576
+ })
577
+
578
+ index += 1
579
+
580
+ else:
581
+ nuevos_bloques_unidos.append(bloques_unidos[i])
582
+ srt_blocks_unidos.append({
583
+ "index": index,
584
+ "start": block["start"],
585
+ "end": block["end"],
586
+ "text": block["text"]
587
+ })
588
+ index +=1
589
+
590
+ else:
591
+ nuevos_bloques_unidos.append(bloques_unidos[i])
592
+ srt_blocks_unidos.append({
593
+ "index": index,
594
+ "start": block["start"],
595
+ "end": block["end"],
596
+ "text": block["text"]
597
+ })
598
+ index +=1
599
+ srt_final = ""
600
+
601
+ for block in srt_blocks_unidos:
602
+ start_tc = seconds_to_srt_time(block["start"])
603
+ end_tc = seconds_to_srt_time(block["end"])
604
+ srt_final += f"{block['index']}\n{start_tc} --> {end_tc}\n{block['text']}\n\n"
605
+
606
+ with open(srt_original_silence_con_ad_silence_unidos_silence_general, "w", encoding="utf-8") as f:
607
+ f.write(srt_final)
608
+
609
+ state["bloques_unidos"] = nuevos_bloques_unidos
610
+
611
+ return state
612
+
613
+ def words_silence_srt(srt_silence_path):
614
+ with open(srt_silence_path, "r", encoding="utf-8-sig") as f:
615
+ srt_text=f.read()
616
+
617
+ silence_dict = {}
618
+
619
+ blocks = srt_text.strip().split("\n\n")
620
+ for block in blocks:
621
+ lines = block.split("\n")
622
+ idx = int(lines[0])
623
+ time_range = lines[1]
624
+ content = "\n".join(lines[2:]).strip()
625
+
626
+ start_str, end_str = time_range.split(" --> ")
627
+ start_sec = srt_time_to_seconds(start_str)
628
+ end_sec = srt_time_to_seconds(end_str)
629
+
630
+ if content.startswith("(AD"):
631
+ duration = end_sec - start_sec
632
+ words = max(1, round(duration * 2))
633
+ silence_dict[idx] = words
634
+
635
+ else:
636
+ silence_dict[idx] = 0
637
+ return silence_dict
638
+
639
+ class Introduccion_OCR:
640
+ def __call__(self, state: NState, srt_original_silence_con_ad_silence_unidos_silence_general, srt_original_silence_con_ad_silence_unidos_silence_general_ocr):
641
+ print("Introduccion_OCR.__call__ iniciat")
642
+
643
+ words_silence = words_silence_srt(srt_original_silence_con_ad_silence_unidos_silence_general)
644
+
645
+ with open(srt_original_silence_con_ad_silence_unidos_silence_general, "r", encoding="utf-8-sig") as f:
646
+ srt_text = f.read()
647
+
648
+ blocks = srt_text.strip().split("\n\n")
649
+ srt_text_modified = ""
650
+
651
+ bloques_unidos = state["bloques_unidos"]
652
+ nuevos_bloques_unidos = []
653
+
654
+ for i, block in enumerate(blocks):
655
+ lines = block.split("\n")
656
+ idx = int(lines[0])
657
+ time_range = lines[1]
658
+ content = "\n".join(lines[2:]).strip()
659
+
660
+ start_str, end_str = time_range.split(" --> ")
661
+ start_sec = srt_time_to_seconds(start_str)
662
+ end_sec = srt_time_to_seconds(end_str)
663
+
664
+ if content.startswith("(AD): OCR"):
665
+ lines = content.split("\n")
666
+ ocr_text = lines[0].split("OCR: ")[1].strip()
667
+ descripcion_text = lines[1].split("Descripción: ")[1].strip()
668
+
669
+ if ocr_text is None or ocr_text == "":
670
+ nuevos_bloques_unidos.append(bloques_unidos[i])
671
+ srt_text_modified += f"{idx}\n{time_range}\n(AD_Descripción): {descripcion_text}\n\n"
672
+
673
+ else:
674
+ count_palabras = len(ocr_text.split())
675
+ palabras_limite = words_silence[i+1]
676
+ if count_palabras <= palabras_limite:
677
+
678
+ prompt = f"""
679
+ Tens davant teu el text extret per OCR d'un frame d'un vídeo. El text està en català.
680
+ Només has de decidir si aquest text és català i té sentit com a frase o paraula en català, sense jutjar-ne la llargada ni si és molt simple.
681
+ Si és català i té sentit, respon només 'yes'.
682
+ Si no és català o no té sentit, respon només 'no'.
683
+
684
+ OCR: {ocr_text}
685
+ """
686
+ messages = [{'role': 'system', 'content': prompt}]
687
+
688
+ out = state['llm_GPT'](messages).strip()
689
+
690
+ if out =="yes":
691
+ end_sec_1 = start_sec + count_palabras / 2
692
+ end_str_1 = seconds_to_srt_time(end_sec_1)
693
+ time_range = f"{start_str} --> {end_str_1}"
694
+ nuevos_bloques_unidos.append(bloques_unidos[i])
695
+ srt_text_modified += f"{idx}\n{time_range}\n(AD_OCR): {ocr_text}\n\n"
696
+
697
+ start_str = end_str_1
698
+ time_range = f"{start_str} --> {end_str}"
699
+ nuevos_bloques_unidos.append(bloques_unidos[i])
700
+ srt_text_modified += f"{idx}\n{time_range}\n(AD_Descripción): {descripcion_text}\n\n"
701
+
702
+ else:
703
+ srt_text_modified += f"{idx}\n{time_range}\n(AD_Descripción): {descripcion_text}\n\n"
704
+ nuevos_bloques_unidos.append(bloques_unidos[i])
705
+
706
+ else:
707
+ nuevos_bloques_unidos.append(bloques_unidos[i])
708
+ srt_text_modified += f"{idx}\n{time_range}\n(AD_Descripción): {descripcion_text}\n\n"
709
+
710
+ else:
711
+ nuevos_bloques_unidos.append(bloques_unidos[i])
712
+ srt_text_modified += f"{idx}\n{time_range}\n{content}\n\n"
713
+
714
+ with open(srt_original_silence_con_ad_silence_unidos_silence_general_ocr, "w", encoding="utf-8-sig") as f:
715
+ f.write(srt_text_modified)
716
+
717
+ state["bloques_unidos"] = nuevos_bloques_unidos
718
 
719
+ return state
 
 
 
720
 
721
+ class Identity_Manager:
722
+ def __call__(self, state: NState, srt_original_silence_con_ad_ocr, srt_original_silence_con_ad_ocr_identity):
723
+ print("Identity_Manager.__call__ iniciat")
724
 
725
+ with open(srt_original_silence_con_ad_ocr, "r", encoding="utf-8-sig") as f:
726
+ srt_text = f.read()
727
+
728
+ blocks = srt_text.strip().split("\n\n")
729
+ srt_text_modified = ""
730
+
731
+ bloques_unidos = state["bloques_unidos"]
732
+
733
+ content_anterior = ""
734
+
735
+ for i, block in enumerate(blocks):
736
+ persona = state['personas_keyframes'][bloques_unidos[i][0]]
737
+ personas_per_second = state["personas_per_second"][bloques_unidos[i][0]]
738
+
739
+ lines = block.split("\n")
740
+ idx = int(lines[0])
741
+ time_range = lines[1]
742
+ content = lines[2].strip()
743
+
744
+ if content.startswith("(AD_Descripción):"):
745
+ if content == content_anterior:
746
 
747
+ prompt = (
748
+ f"Sobre la escena '{content}' (persona principal: {persona}) ya se ha escrito '{content_escena}'. "
749
+ f"Las personas detectadas en la escena actual son: {personas_per_second}. "
750
+ f"¿Hay algo nuevo y no repetitivo que añadir cumpliendo la norma UNE para ciegos? "
751
+ f"Si no hay nada nuevo, deja la respuesta vacía: ' (AD):'' '"
752
+ )
753
+ messages = [{'role': 'system', 'content': prompt}]
754
+ out = state['llm_GPT'](messages).strip()
755
+
756
+ salida = out or "" # manejar vacío
757
+ srt_text_modified += f"{idx}\n{time_range}\n{salida}\n\n"
758
+
759
+ content_escena += " " + salida
760
+
761
+ else:
762
+ # Aquí entra cuando hay una escena nueva
763
+ content_escena = ""
764
+
765
+ prompt = f"""
766
+ Sabent que aquesta és la frase que cal corregir: {content}, on apareix la persona identificada com a {persona}.
767
+ Si la descripció apareix de manera genèrica, per exemple "Una dona", substitueix-la pel nom concret de la persona identificada.
768
+
769
+ Informació addicional que pot ser útil i d’on també pots identificar els personatges segons el context:
770
+ 1.) Bloc concret {i} que s’està modificant de la diarització completa: {srt_text}
771
+ 2.) Personatges identificats en la escena completa con el número de veces que han aparecido: {personas_per_second}
772
+
773
+ Recorda:
774
+ - Torna només el text corregit en el format (AD_Descripción): "text"
775
+ """
776
+ messages = [{'role': 'system', 'content': prompt}]
777
+ out = state['llm_GPT'](messages).strip()
778
+
779
+ srt_text_modified += f"{idx}\n{time_range}\n{out}\n\n"
780
+
781
+ content_anterior = content
782
+ content_escena += out
783
+ salida = out
784
+
785
+ else:
786
+ srt_text_modified += f"{idx}\n{time_range}\n{content}\n\n"
787
+
788
+ # Guardem el SRT final amb identitats aplicades
789
+ with open(srt_original_silence_con_ad_ocr_identity, "w", encoding="utf-8-sig") as f:
790
+ f.write(srt_text_modified)
791
+
792
+ # Actualitzem l'estat
793
+ state['audiodescripcion_ad_identity'] = srt_text_modified
794
+
795
+ return state
796
+
797
+ class UNE_Actor_prev:
798
+ def __call__(self, state: NState, srt_original_silence_con_ad_ocr_identity, srt_original_silence_con_ad_ocr_identity_une_1):
799
+ print("UNE_Actor_prev.__call__ iniciado")
800
+
801
+ with open(srt_original_silence_con_ad_ocr_identity, "r", encoding="utf-8-sig") as f:
802
+ srt_text = f.read()
803
+
804
  prompt = f"""
805
+ PROMPT PER A LA GENERACIÓ D’AUDIODESCRIPCIÓ (AD) NORMA UNE 153020
806
+
807
+ Rol i Objectiu:
808
+ Ets un guionista d’audiodescripció expert en la norma UNE 153020 (Descripció del contingut visual per a persones cegues o amb baixa visió).
809
+ La teva tasca és revisar i generar (o corregir si ja existeixen) les audiodescripcions de l’arxiu SRT proporcionat.
810
+ Has de retornar l’arxiu SRT complet, mantenint la numeració i els temps originals, sense afegir cap text explicatiu fora del format SRT.
811
+
812
+ INSTRUCCIONS DETALLADES:
813
+
814
+ 1. **Format de sortida**
815
+ - Retorna l’arxiu SRT complet i corregit.
816
+ - No incloguis comentaris, explicacions ni encapçalaments fora del format de l’arxiu.
817
+ - Respecta la numeració, els temps i la resta del text original.
818
+
819
+ 2. **Etiquetes a modificar**
820
+ - Només modifica el contingut que estigui entre les etiquetes `(AD_Descripción):` o `(AD):`.
821
+ - Si una línia amb `(AD):` està buida, no la omplis (s’assumeix que hi ha so rellevant o que no hi ha espai per a la descripció).
822
+ - Substitueix o completa únicament aquestes línies, sense alterar la resta del subtítol.
823
+
824
+ 3. **Criteris d’Audiodescripció (segons UNE 153020)**
825
+ - Descriu **només la informació visual rellevant** que no aparegui a l’àudio.
826
+ - Fes servir un estil **objectiu, clar i concís**, sense interpretacions ni judicis subjectius.
827
+ - Descriu només allò necessari perquè una persona cega pugui comprendre l’escena.
828
+ - No descriguis durant diàlegs, música o efectes rellevants.
829
+ - Si el silenci és expressiu (suspens, comèdia, tensió), deixa la descripció en blanc.
830
+
831
+ 4. **Contingut que has d’incloure (Què descriure?)**
832
+ - **QUAN i ON:** lloc, moment del dia o època.
833
+ - **QUI:** identificació, roba, atributs físics rellevants.
834
+ - **QUÈ i COM:** llenguatge corporal, moviments, gestos, accions, expressions facials.
835
+ - **Altres:** text en pantalla, logotips, títols o rètols visibles.
836
+
837
+ 5. **Llenguatge i estil**
838
+ - Fes servir **temps present** (“Camina”, no “Va caminar”).
839
+ - Utilitza **veu activa**, evita la passiva.
840
+ - Lèxic clar, variat però concís.
841
+ - Sense metàfores, suposicions ni valoracions subjectives.
842
+ - Evita els verbs “veure” i “aparèixer”.
843
+ - Indica salts o transicions de temps (p. ex. “Tres anys després…”).
844
+
845
+ 6. **Errors que has d’evitar absolutament**
846
+ - No interpretis emocions ni intencions (“sembla trist”, “com si recordés”).
847
+ - No expliquis, no valoris (“una imatge preciosa”, “una escena intensa”).
848
+ - No afegeixis informació no visible o no verificable.
849
+
850
+ TASCA:
851
+ Revisa el següent arxiu SRT i substitueix, completa o corregeix les parts que continguin `(AD_Descripción)` o `(AD):` d’acord amb totes les regles anteriors.
852
+ Retorna’m **només l’arxiu SRT corregit**, sense cap comentari addicional.
853
+
854
+ ARXIU SRT A PROCESSAR: {srt_text}
855
  """
 
 
856
 
857
+ messages = [{'role': 'system', 'content': prompt}]
858
+
859
+ out = state['llm_GPT'](messages).strip()
860
+ out = out.replace('```', '')
861
 
862
+ blocks = re.split(r'\n\s*\n', out)
863
+
864
+ # Comprobar si el primer bloque empieza con un número
865
+ first_block = blocks[0].strip().split('\n')[0]
866
+
867
+ if first_block.isdigit():
868
+ # El primer bloque ya tiene número, no hacemos nada
869
+ fixed_content = out
870
+ else:
871
+ # Reindexamos todos los bloques
872
+ output_lines = []
873
+ for i, block in enumerate(blocks, start=1):
874
+ block = re.sub(r'^\d+\s*\n', '', block)
875
+ block = f"{i}\n{block.strip()}"
876
+ output_lines.append(block)
877
+
878
+ fixed_content = "\n\n".join(output_lines)
879
+
880
+ with open(srt_original_silence_con_ad_ocr_identity_une_1, "w", encoding="utf-8-sig") as f:
881
+ f.write(fixed_content)
882
 
883
  return state
884
+
885
+ class UNE_Actor:
886
+ def __call__(self, state: NState, srt_original_silence_con_ad_ocr_identity_une_1, srt_original_silence_con_ad_ocr_identity_une_2):
887
+ print("UNE_Actor.__call__ iniciado")
888
+
889
+ silence_dict = words_silence_srt(srt_original_silence_con_ad_ocr_identity_une_1)
890
+
891
+ with open(srt_original_silence_con_ad_ocr_identity_une_1, "r", encoding="utf-8-sig") as f:
892
+ srt_text = f.read()
893
+
894
+ srt_text_modified = ""
895
+
896
+ blocks = srt_text.strip().split("\n\n")
897
+ for block in blocks:
898
+ lines = block.split("\n")
899
+ idx = int(lines[0])
900
+ time_range = lines[1]
901
+ content = lines[2].strip()
902
+
903
+ start_str, end_str = time_range.split(" --> ")
904
+
905
+ if content.startswith("(AD_Descripción):"):
906
+ if silence_dict[idx] < 2:
907
+ out = '(AD): ""'
908
+
909
+ else:
910
+ # Construimos el prompt aquí, con los datos ya disponibles
911
+ sys_prompt = f"""
912
+ En primer lloc, has de generar un contingut amb un nombre determinat de paraules ({silence_dict[idx]})
913
+ que representi el mateix significat que aquest fragment: {content}.
914
+
915
+ D’altra banda, s’està modificant el fitxer SRT complet {srt_text}, concretament el fragment número {idx},
916
+ per si et pot servir de context. Aquí tens el contingut actualitzat de l’SRT fins ara: {srt_text_modified}
917
+
918
+ Has de complir amb la norma UNE: llenguatge clar, descriptiu i narratiu, sense repeticions i mostrant
919
+ les accions i emocions de manera natural.
920
+
921
+ Important:
922
+ - Revisa el contingut anterior de l’SRT i evita repetir frases o expressions ja utilitzades.
923
+ - Si hi ha informació semblant, expressa-la d’una manera diferent, mantenint la coherència i la claredat.
924
+ - El resultat ha de ser narratiu, natural i fluid.
925
+ - Regla estricta: si el nombre de paraules requerit és 1 o 2 i no és possible expressar el contingut de manera coherent amb tan poques paraules,
926
+ has de retornar exactament: (AD): "" (cometes buides), sense afegir res més.
927
+
928
+ La resposta s’ha de donar en el format següent:
929
+
930
+ (AD): "text amb exactament {silence_dict[idx]} paraules, que representi fidelment el text proporcionat ({content}),
931
+ sense repetir fórmules ja utilitzades a l’SRT i complint amb la norma UNE"
932
+ """
933
+
934
+ messages = [{'role': 'system', 'content': sys_prompt}]
935
+ out = state['llm_GPT'](messages)
936
+
937
+ srt_text_modified += f"{idx}\n{start_str} --> {end_str}\n{out}\n\n"
938
+ else:
939
+ srt_text_modified += f"{idx}\n{start_str} --> {end_str}\n{content}\n\n"
940
+
941
+ # Guardamos el resultado
942
+ with open(srt_original_silence_con_ad_ocr_identity_une_2, "w", encoding="utf-8-sig") as f:
943
+ f.write(srt_text_modified)
944
+
945
+ # Guardamos también en el estado
946
+ state['audiodescripcion_une'] = srt_text_modified
947
+
948
+ return state
949
+
950
  class Valoracion_Final:
951
+ def __call__(self, state, srt_original_silence_con_ad_ocr_identity_une_2, csv_evaluacion):
952
  print("Valoracion_Final.__call__ iniciat")
953
 
954
  # Llegeix el contingut del fitxer SRT
955
+ with open(srt_original_silence_con_ad_ocr_identity_une_2, "r", encoding="utf-8-sig") as f:
956
  srt_text = f.read().strip()
957
 
958
  # Defineix el prompt principal
 
997
 
998
  return state
999
 
1000
+ class Free_Narration:
1001
+ def __call__(self, state: NState, audio_descripcion_path_sin_une, story_path) -> NState:
1002
+
1003
+ with open(audio_descripcion_path_sin_une, "r", encoding="utf-8-sig") as f:
1004
+ audio_descripcion = f.read()
1005
+
1006
+ sys_prompt = f"""
1007
+ Ets un relator objectiu. Tens la següent informació:
1008
+
1009
+ 1. Audiodescripció del vídeo (incloent diàlegs i descripcions visuals): {audio_descripcion}
1010
+
1011
+ Objectiu:
1012
+ - Resumeix de forma precisa i cronològica tot el que passa al vídeo.
1013
+ - Inclou només els esdeveniments essencials i les accions principals dels personatges.
1014
+ - Elimina qualsevol detall visual, emocional o descriptiu que no sigui necessari per entendre què passa.
1015
+ - No afegeixis cap informació que no aparegui explícitament a la font.
1016
+ - No reprodueixis diàlegs, només explica el que succeeix.
1017
+ - Mantén el relat neutre, breu i clar.
1018
+ - Usa els noms correctes dels personatges segons apareguin a la història.
1019
+
1020
+ Sortida:
1021
+ - Un únic text narratiu continu.
1022
+ """
1023
+
1024
+ messages = [{'role':'system','content': sys_prompt}]
1025
+ out = state['llm_GPT'](messages)
1026
+
1027
+ with open(story_path, "w", encoding="utf-8-sig") as f:
1028
+ f.write(out)
1029
+
1030
+ state['free_narration'] = out
1031
+
1032
+ return state
1033
+
1034
+ @router.post("/generate_salamandra_result", tags=["Salamandra Process"])
1035
  async def generate_salamadra_result(
1036
  sha1: str,
1037
  token: str = Query(..., description="Token required for authorization")
1038
  ):
1039
  """
1040
+ Generate all MoE output files (final SRT, free narration, and evaluation CSV)
1041
  for a processed video identified by its SHA1 hash.
 
1042
  This endpoint orchestrates the full Salamandra processing pipeline:
1043
  - Validates the access token.
1044
  - Locates the processed video and its associated metadata.
 
1049
  * An evaluation CSV (`evaluation.csv`)
1050
  - Ensures the expected directory structure exists, creating folders if necessary.
1051
  - Uses both GPT-based and Salamandra-based LLMs to generate narrative and evaluation content.
 
1052
  Args:
1053
  sha1 (str): The SHA1 hash that identifies the media processing workspace.
1054
  token (str): Authorization token required to execute Salamandra operations.
 
1055
  Raises:
1056
  HTTPException:
1057
  - 404 if the SHA1 folder does not exist.
1058
  - 404 if the `clip` folder is missing.
1059
  - 404 if no MP4 file is found inside the clip folder.
 
1060
  Processing Steps:
1061
  1. Validates that all required folders exist (`sha1`, `clip`, `result/Salamandra`).
1062
  2. Retrieves the input video and initial metadata (original SRT, info JSON).
 
1066
  - result.srt
1067
  - free_narration.txt
1068
  - evaluation.csv
 
1069
  Returns:
1070
  dict: A JSON response indicating successful generation:
1071
  {
 
1124
  salamdra_folder = result_folder / "Salamandra"
1125
  salamdra_folder.mkdir(parents=True, exist_ok=True)
1126
  csv_evaluacion = salamdra_folder / "evaluation.csv"
1127
+
1128
+ datahub=DataHub(informacion_json)
1129
 
1130
+ # Instancia de la herramienta como clase
 
 
 
 
 
 
1131
  add_ad = Add_AD(datahub)
1132
+ add_silence_ad = Add_Silence_AD()
1133
+ unir_ad_silence = Unir_AD_Silence()
1134
+ unir_ad_silences_a_ads = Unir_AD_Silences_a_ADs()
1135
+ introduccion_ocr = Introduccion_OCR()
1136
+ identity_manager = Identity_Manager()
1137
+ une_actor_prev = UNE_Actor_prev()
1138
+ une_actor = UNE_Actor()
1139
  valoracion_final = Valoracion_Final()
1140
+ free_narration = Free_Narration()
1141
+
1142
+ tools = [
1143
+ {
1144
+ "type": "function",
1145
+ "name": "Add_AD",
1146
+ "description": "Agregame las descripciones de lo que esta ocurriendo por pantalla",
1147
+ "parameters": {
1148
+ "type": "object",
1149
+ "properties": {
1150
+ "state": {
1151
+ "type": "object",
1152
+ "description": "Estado actual de procesamiento"
1153
+ }
1154
+ },
1155
+ "required": ["state", "srt_original_silence", "srt_original_silence_con_ad"],
1156
+ "additionalProperties": False
1157
+ },
1158
+ "function": add_ad
1159
+ },
1160
+ {
1161
+ "type": "function",
1162
+ "name": "Add_Silence_AD",
1163
+ "description": "Introduceme bloques de silencio en la audiodescripción",
1164
+ "parameters": {
1165
+ "type": "object",
1166
+ "properties": {
1167
+ "state": {
1168
+ "type": "object",
1169
+ "description": "Estado actual de procesamiento"
1170
+ }
1171
+ },
1172
+ "required": ["state", "srt_original_silence_con_ad", "srt_original_silence_con_ad_silence"],
1173
+ "additionalProperties": False
1174
+ },
1175
+ "function": add_silence_ad
1176
+ },
1177
+ {
1178
+ "type": "function",
1179
+ "name": "Unir_AD_Silence",
1180
+ "description": "Unificame bloques de silencio que son consecutivos en la audiodescripción",
1181
+ "parameters": {
1182
+ "type": "object",
1183
+ "properties": {
1184
+ "state": {
1185
+ "type": "object",
1186
+ "description": "Estado actual de procesamiento"
1187
+ }
1188
+ },
1189
+ "required": ["state", "srt_original_silence_con_ad_silence", "srt_original_silence_con_ad_silence_unidos"],
1190
+ "additionalProperties": False
1191
+ },
1192
+ "function": unir_ad_silence
1193
+ },
1194
+ {
1195
+ "type": "function",
1196
+ "name": "Unir_AD_Silences_a_ADs",
1197
+ "description": "Unificame los bloques de silencio a la audiodescripción en caso de que haya de manera consecutiva para aprovechar mejor los tiempos",
1198
+ "parameters": {
1199
+ "type": "object",
1200
+ "properties": {
1201
+ "state": {
1202
+ "type": "object",
1203
+ "description": "Estado actual de procesamiento"
1204
+ }
1205
+ },
1206
+ "required": ["state", "srt_original_silence_con_ad_silence_unidos", "srt_original_silence_con_ad_silence_unidos_general"],
1207
+ "additionalProperties": False
1208
+ },
1209
+ "function": unir_ad_silences_a_ads
1210
+ },
1211
+ {
1212
+ "type": "function",
1213
+ "name": "Introduccion_OCR",
1214
+ "description": "Introducción del texto OCR en la audiodescripción",
1215
+ "parameters": {
1216
+ "type": "object",
1217
+ "properties": {
1218
+ "state": {
1219
+ "type": "object",
1220
+ "description": "Estado actual de procesamiento"
1221
+ }
1222
+ },
1223
+ "required": ["state", "srt_original_silence_con_ad_silence_unidos_silence_general", "srt_original_silence_con_ad_silence_unidos_silence_general_ocr"],
1224
+ "additionalProperties": False
1225
+ },
1226
+ "function": introduccion_ocr
1227
+ },
1228
+ {
1229
+ "type": "function",
1230
+ "name": "Identity_Manager",
1231
+ "description": "Incluye en los fragmentos de audiodescripción las identidades de los actores presentes en la escena",
1232
+ "parameters": {
1233
+ "type": "object",
1234
+ "properties": {
1235
+ "state": {
1236
+ "type": "object",
1237
+ "description": "Estado actual de procesamiento"
1238
+ }
1239
+ },
1240
+ "required": ["state", "srt_original_silence_con_ad", "srt_original_silence_con_ad_ocr_identity"],
1241
+ "additionalProperties": False
1242
+ },
1243
+ "function": identity_manager
1244
+ },
1245
+ {
1246
+ "type": "function",
1247
+ "name": "UNE_Actor_prev",
1248
+ "description": "Verifica en la audiodescripción general quese verifica la norma UNE 153020",
1249
+ "parameters": {
1250
+ "type": "object",
1251
+ "properties": {
1252
+ "state": {
1253
+ "type": "object",
1254
+ "description": "Estado actual de procesamiento"
1255
+ }
1256
+ },
1257
+ "required": ["state", "srt_original_silence_con_ad_ocr_identity", "srt_original_silence_con_ad_ocr_identity_une_1"],
1258
+ "additionalProperties": False
1259
+ },
1260
+ "function": une_actor_prev
1261
+ },
1262
+ {
1263
+ "type": "function",
1264
+ "name": "UNE_Actor",
1265
+ "description": "Modifica la audiodescripción para que cumpla con el número de palabras según la norma UNE 153020",
1266
+ "parameters": {
1267
+ "type": "object",
1268
+ "properties": {
1269
+ "state": {
1270
+ "type": "object",
1271
+ "description": "Estado actual de procesamiento"
1272
+ }
1273
+ },
1274
+ "required": ["state", "srt_original_silence_con_ad_ocr_identity_une_1", "srt_original_silence_con_ad_ocr_identity_une_2"],
1275
+ "additionalProperties": False
1276
+ },
1277
+ "function": une_actor
1278
+ },
1279
+ {
1280
+ "type": "function",
1281
+ "name": "Valoracion_Final",
1282
+ "description": "Genera una valoración final de la audiodescripción según la norma UNE 153020",
1283
+ "parameters": {
1284
+ "type": "object",
1285
+ "properties": {
1286
+ "state": {
1287
+ "type": "object",
1288
+ "description": "Estado actual de procesamiento"
1289
+ }
1290
+ },
1291
+ "required": ["state", "srt_original_silence_con_ad_ocr_identity_une_2", "csv_evaluacion"],
1292
+ "additionalProperties": False
1293
+ },
1294
+ "function": valoracion_final
1295
+ },
1296
+ {
1297
+ "type": "function",
1298
+ "name": "Free_Narration",
1299
+ "description": "Genera una narración libre basada en la audiodescripción",
1300
+ "parameters": {
1301
+ "type": "object",
1302
+ "properties": {
1303
+ "state": {
1304
+ "type": "object",
1305
+ "description": "Estado actual de procesamiento"
1306
+ }
1307
+ },
1308
+ "required": ["state", "srt_final", "free_narration"],
1309
+ "additionalProperties": False
1310
+ },
1311
+ "function": free_narration
1312
+ }
1313
+ ]
1314
+
1315
+ # Aqui van las rutas temporales de los SRT intermedios hasta llegar al final
1316
+ srt_names = [
1317
+ "transcription_initial_silence",
1318
+ "transcription_initial_silence_con_ad",
1319
+ "transcription_initial_silence_con_ad_silence",
1320
+ "transcription_initial_silence_con_ad_silence_unidos",
1321
+ "transcription_initial_silence_con_ad_silence_unidos_silence",
1322
+ "transcription_initial_silence_con_ad_silence_unidos_silence_general",
1323
+ "transcription_initial_silence_con_ad_silence_unidos_silence_general_ocr",
1324
+ "transcription_initial_silence_con_ad_silence_unidos_silence_general_ocr_identity",
1325
+ "transcription_initial_silence_con_ad_ocr_identity_une_1"
1326
+ ]
1327
+
1328
+ # Crear archivos temporales
1329
+ temp_srt_files = []
1330
+
1331
+ for name in srt_names:
1332
+ tmp = tempfile.NamedTemporaryFile(mode="w+", suffix=".srt", prefix=name + "_", delete=False)
1333
+ temp_srt_files.append(tmp)
1334
+ print(tmp.name) # Aquí obtienes la ruta temporal del archivo
1335
+
1336
+ generate_srt_con_silencios(srt_original, temp_srt_files[0].name, video_path)
1337
 
1338
  GPTclient = GPT5Client(api_key=OPEN_AI_KEY)
 
1339
 
1340
+ salamandraclient = SalamandraClient()
1341
  state = {
1342
  "llm_GPT": GPTclient.chat,
1343
  "llm_Salamandra": salamandraclient.chat
1344
  }
1345
 
1346
+ def run_salamandra_agent(salamandra_client, state, tools, user_prompt, messages, count):
1347
+ messages = [{"role": "system", "content": "Eres un agente que puede ejecutar herramientas Python usando las herramientas disponibles."}]
1348
+
1349
+ messages.append({"role": "user", "content": user_prompt})
1350
+
1351
+ messages_registro.append({"role": "user", "content": user_prompt})
1352
+
1353
+ response = salamandra_client.chat(messages)
1354
+ print(f"[Salamandra] {response}")
1355
+
1356
+ # Extraer lo que viene después de 'assistant'
1357
+ match_assistant = re.search(r"assistant\s*(.*)", response, re.DOTALL)
1358
+ assistant_text = match_assistant.group(1).strip() if match_assistant else ""
1359
+
1360
+ # Extraer <tool_call>
1361
+ match_tool = re.search(r"<tool_call>(.*?)</tool_call>", assistant_text, re.DOTALL)
1362
+ if match_tool:
1363
+ resp_json = json.loads(match_tool.group(1).strip())
1364
+ tool_name = resp_json["name"]
1365
+ tool_params = resp_json["arguments"]
1366
+
1367
+ tool = next((t['function'] for t in tools if t['name'] == tool_name), None)
1368
+ if tool:
1369
+
1370
+ if isinstance(tool, Add_AD):
1371
+ state = tool(state, temp_srt_files[0].name, temp_srt_files[1].name)
1372
+
1373
+ elif isinstance(tool, Add_Silence_AD) and count ==1:
1374
+ state = tool(state, temp_srt_files[1].name, temp_srt_files[2].name)
1375
+
1376
+ elif isinstance(tool, Unir_AD_Silence):
1377
+ state = tool(state, temp_srt_files[2].name, temp_srt_files[3].name)
1378
+
1379
+ elif isinstance(tool, Add_Silence_AD) and count ==2:
1380
+ state = tool(state, temp_srt_files[3].name, temp_srt_files[4].name)
1381
+
1382
+ elif isinstance(tool, Unir_AD_Silences_a_ADs):
1383
+ state = tool(state, temp_srt_files[4].name, temp_srt_files[5].name)
1384
+
1385
+ elif isinstance(tool, Introduccion_OCR):
1386
+ state = tool(state, temp_srt_files[5].name, temp_srt_files[6].name)
1387
+
1388
+ elif isinstance(tool, Identity_Manager):
1389
+ state = tool(state, temp_srt_files[6].name, temp_srt_files[7].name)
1390
+
1391
+ elif isinstance(tool, UNE_Actor_prev):
1392
+ state = tool(state, temp_srt_files[7].name, temp_srt_files[8].name)
1393
+
1394
+ elif isinstance(tool, UNE_Actor):
1395
+ state = tool(state, temp_srt_files[8].name, srt_final)
1396
+
1397
+ elif isinstance(tool, Valoracion_Final):
1398
+ state = tool(state, srt_final, csv_evaluacion)
1399
+
1400
+ elif isinstance(tool, Free_Narration):
1401
+ state = tool(state, srt_final, free_narration_moe)
1402
+
1403
+ messages_registro.append({"role": "assistant", "content": f"Ejecuté {tool_name} correctamente."})
1404
+ else:
1405
+ print("No se detectó ejecución de herramienta")
1406
+
1407
+ return state, messages_registro
1408
+
1409
+ messages_registro = [{"role": "system", "content": "Eres un agente que puede ejecutar herramientas Python usando las herramientas disponibles."}]
1410
+
1411
+ count = 1
1412
+
1413
+ user_prompt = "Ejecuta la función Add_AD"
1414
+ final_state, messages_registro = run_salamandra_agent(salamandraclient, state, tools, user_prompt, messages_registro, count)
1415
+ print("Transcripción con AD guardada")
1416
+
1417
+ user_prompt = "Ejecuta la función Add_Silence_AD"
1418
+ final_state, messages_registro = run_salamandra_agent(salamandraclient, final_state, tools, user_prompt, messages_registro, count)
1419
+ print("Transcripción con AD y Add_Silence_AD guardada")
1420
+
1421
+ user_prompt = "Ejecuta la función Unir_AD_Silence"
1422
+ final_state, messages_registro = run_salamandra_agent(salamandraclient, final_state, tools, user_prompt, messages_registro, count)
1423
+ print("Transcripción con AD y Unir_AD_Silence guardada")
1424
+
1425
+ count = 2
1426
+
1427
+ user_prompt = "Ejecuta la función Add_Silence_AD"
1428
+ final_state, messages_registro = run_salamandra_agent(salamandraclient, final_state, tools, user_prompt, messages_registro, count)
1429
+ print("Transcripción con AD y Add_Silence_AD guardada")
1430
+
1431
+ user_prompt = "Ejecuta la función Unir_AD_Silences_a_ADs"
1432
+ final_state, messages_registro = run_salamandra_agent(salamandraclient, final_state, tools, user_prompt, messages_registro, count)
1433
+ print("Transcripción con AD y Unir_AD_Silences_a_ADs guardada")
1434
+
1435
+ user_prompt = "Ejecuta la función Introduccion_OCR"
1436
+ final_state, messages_registro = run_salamandra_agent(salamandraclient, final_state, tools, user_prompt, messages_registro, count)
1437
+ print("Transcripción con AD, Add_Silence_AD e Introduccion_OCR guardada")
1438
+
1439
+
1440
+ user_prompt = "Ejecuta la función Identity_Manager"
1441
+ final_state, messages_registro = run_salamandra_agent(salamandraclient, final_state, tools, user_prompt, messages_registro, count)
1442
+ print("Transcripción con AD, Add_Silence_AD, Introduccion_OCR e Identity_Manager guardada")
1443
+
1444
+ user_prompt = "Ejecuta la función UNE_Actor_prev"
1445
+ final_state, messages_registro = run_salamandra_agent(salamandraclient, final_state, tools, user_prompt, messages_registro, count)
1446
+ print("Transcripción con AD, Add_Silence_AD, Introduccion_OCR, Identity_Manager y norma UNE guardada")
1447
+
1448
+ user_prompt = "Ejecuta la función UNE_Actor"
1449
+ final_state, messages_registro = run_salamandra_agent(salamandraclient, final_state, tools, user_prompt, messages_registro, count)
1450
+ print("Transcripción con AD, Add_Silence_AD, Introduccion_OCR, Identity_Manager y norma UNE guardada")
1451
+
1452
+ user_prompt = "Ejecuta la función Valoracion_Final"
1453
+ final_state, messages_registro = run_salamandra_agent(salamandraclient, final_state, tools, user_prompt, messages_registro, count)
1454
+ print("Valoración guardada")
1455
+
1456
+ user_prompt = "Ejecuta la función Free_Narration"
1457
+ final_state, messages_registro = run_salamandra_agent(salamandraclient, final_state, tools, user_prompt, messages_registro, count)
1458
+ print("Free Narration guardada")
1459
+
1460
  return {"status": "ok", "message": "Salamandra SRT, free_narration and CSV evaluation generated"}
1461
 
1462
  @router.get("/download_salamadra_srt", tags=["Salamandra Process"])