leicam commited on
Commit
c7bfbda
·
verified ·
1 Parent(s): 080dc62

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +65 -2089
app.py CHANGED
@@ -1,46 +1,4 @@
1
- def select_segments(
2
- transcript_txt: str,
3
- use_llm: bool,
4
- num_segments: int,
5
- custom_keywords: str,
6
- manual_timecodes: str,
7
- natural_instructions: str,
8
- weight_emotion: float,
9
- weight_break: float,
10
- weight_learn: float,
11
- weight_viral: float,
12
- fps: int,
13
- progress_callback=None
14
- ) -> List[Segment]:
15
-
16
- # 1) Manual
17
- manual = parse_manual_timecodes(manual_timecodes)
18
- if manual:
19
- result = []
20
- for start_tc, end_tc in manual:
21
- try:
22
- result.append(Segment(
23
- start_tc=frames_to_timecode(parse_timecode_to_frames(start_tc, fps), fps),
24
- end_tc=frames_to_timecode(parse_timecode_to_frames(end_tc, fps), fps),
25
- start_f=parse_timecode_to_frames(start_tc, fps),
26
- end_f=parse_timecode_to_frames(end_tc, fps),
27
- text=f"Manual: {start_tc} - {end_tc}",
28
- score=100.0
29
- ))
30
- except Exception:
31
- pass
32
- return result
33
-
34
- # 2) Parser de transcrição
35
- segs = parse_transcript(transcript_txt, fps) if transcript_txt else []
36
-
37
- # 3) Linguagem natural COM IA
38
- if natural_instructions.strip():
39
- if use_llm and LLM_AVAILABLE and segs:
40
- # USA IA PARA ANÁLISE COMPLETA
41
- return ai_analyze_and_select(segs, natural_instructions, fps, progress_callback)
42
- elif segs:
43
- # Fallback semimport os
44
  import re
45
  import json
46
  import xml.etree.ElementTree as ET
@@ -51,7 +9,7 @@ import gradio as gr
51
  # =========================
52
  # Configurações Gerais
53
  # =========================
54
- DEFAULT_FPS = 24 # FPS padrão, mas será configurável na interface
55
  OUTPUT_DIR = "./Output"
56
  os.makedirs(OUTPUT_DIR, exist_ok=True)
57
 
@@ -92,7 +50,7 @@ class Segment:
92
  # =========================
93
  # Funções de Timecode
94
  # =========================
95
- def _tc_to_hmsf(tc: str, fps: int) -> Tuple[int, int, int, int]:
96
  """Converte timecode para (hh, mm, ss, ff)."""
97
  s = tc.strip()
98
 
@@ -118,12 +76,12 @@ def _tc_to_hmsf(tc: str, fps: int) -> Tuple[int, int, int, int]:
118
  raise ValueError(f"Timecode inválido: {tc}")
119
 
120
 
121
- def parse_timecode_to_frames(tc: str, fps: int) -> int:
122
  hh, mm, ss, ff = _tc_to_hmsf(tc, fps)
123
  return hh * 3600 * fps + mm * 60 * fps + ss * fps + ff
124
 
125
 
126
- def frames_to_timecode(frames: int, fps: int) -> str:
127
  hh = frames // (3600 * fps)
128
  rem = frames % (3600 * fps)
129
  mm = rem // (60 * fps)
@@ -136,7 +94,7 @@ def frames_to_timecode(frames: int, fps: int) -> str:
136
  # =========================
137
  # Parser de Transcrição
138
  # =========================
139
- def parse_transcript(txt: str, fps: int) -> List[Segment]:
140
  """Parser robusto para múltiplos formatos."""
141
  if not txt or not txt.strip():
142
  return []
@@ -146,2049 +104,67 @@ def parse_transcript(txt: str, fps: int) -> List[Segment]:
146
 
147
  line_range = re.compile(
148
  r'^\s*\[?\s*(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*[-—–]\s*'
149
- r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*\]?\s*(.*)
150
-
 
 
 
 
151
 
152
- # =========================
153
- # Minutagens Manuais
154
- # =========================
155
- def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
156
- if not manual_input or not manual_input.strip():
157
- return []
158
 
159
- manual_ranges = []
160
- lines = manual_input.replace(",", "\n").splitlines()
161
- pattern = re.compile(r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*[-–—]\s*(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)')
162
- for line in lines:
163
- m = pattern.search(line.strip())
164
  if m:
165
- manual_ranges.append((m.group(1), m.group(2)))
166
- return manual_ranges
167
-
168
-
169
- # =========================
170
- # IA: Análise Inteligente com Gemini
171
- # =========================
172
- def ai_analyze_and_select(segments: List[Segment], command: str, fps: int, progress_callback=None) -> List[Segment]:
173
- """
174
- Usa Gemini para analisar a transcrição completa e identificar os melhores trechos.
175
- Processo em 2 etapas para máxima precisão.
176
- """
177
- if not LLM_AVAILABLE or not segments:
178
- raise ValueError("IA não disponível ou sem segmentos para analisar")
179
-
180
- if progress_callback:
181
- progress_callback("🤖 Etapa 1/3: Preparando dados para análise...")
182
-
183
- # Prepara a transcrição completa com índices
184
- transcript_data = []
185
- for i, seg in enumerate(segments):
186
- duration_sec = (seg.end_f - seg.start_f) / fps
187
- transcript_data.append({
188
- "index": i,
189
- "timecode": seg.start_tc,
190
- "duration_sec": round(duration_sec, 1),
191
- "text": seg.text[:200] # Limita texto para não estourar tokens
192
- })
193
-
194
- # Converte para JSON para análise estruturada
195
- transcript_json = json.dumps(transcript_data, ensure_ascii=False, indent=2)
196
-
197
- if progress_callback:
198
- progress_callback(f"🤖 Etapa 2/3: Analisando {len(segments)} segmentos com IA (pode levar 30-60s)...")
199
-
200
- # Prompt detalhado para análise completa
201
- prompt = f"""Você é um especialista em edição de vídeo. Analise a transcrição e identifique os MELHORES trechos baseado no comando do usuário.
202
-
203
- COMANDO DO USUÁRIO:
204
- {command}
205
-
206
- TRANSCRIÇÃO COMPLETA (formato JSON com index, timecode, duração e texto):
207
- {transcript_json}
208
-
209
- INSTRUÇÕES:
210
- 1. Leia o comando com atenção e identifique:
211
- - Quantidade de cortes desejada
212
- - Duração de cada corte (em segundos)
213
- - Tema/assunto/palavras-chave mencionados
214
- - Timecode de início (se mencionado)
215
-
216
- 2. Analise TODA a transcrição e identifique os segmentos que melhor correspondem ao comando
217
-
218
- 3. Para cada corte, retorne no formato JSON:
219
- {{
220
- "cuts": [
221
- {{
222
- "start_index": <índice do segmento inicial>,
223
- "duration_seconds": <duração desejada em segundos>,
224
- "reason": "<breve explicação de por que escolheu este trecho>"
225
- }}
226
- ]
227
- }}
228
-
229
- IMPORTANTE:
230
- - Seja PRECISO na identificação dos trechos
231
- - Considere o contexto completo ao redor das palavras-chave
232
- - Se o comando pedir "sobre X", encontre onde X é realmente discutido
233
- - Se houver timecode, priorize começar próximo a ele
234
- - Retorne APENAS o JSON, sem texto adicional
235
-
236
- Responda com o JSON:"""
237
 
238
- try:
239
- response = LLM.generate_content(
240
- prompt,
241
- generation_config={
242
- "temperature": 0.2,
243
- "max_output_tokens": 2000,
244
- }
245
- )
246
-
247
- response_text = response.text.strip()
248
-
249
- if progress_callback:
250
- progress_callback("🤖 Etapa 3/3: Processando resposta da IA...")
251
-
252
- # Extrai JSON da resposta
253
- json_match = re.search(r'\{[\s\S]*"cuts"[\s\S]*\}', response_text)
254
- if not json_match:
255
- raise ValueError("IA não retornou JSON válido")
256
-
257
- result = json.loads(json_match.group(0))
258
- cuts_data = result.get("cuts", [])
259
-
260
- if not cuts_data:
261
- raise ValueError("IA não encontrou cortes adequados")
262
-
263
- # Cria os segmentos baseado na análise da IA
264
- selected_segments = []
265
-
266
- for cut_info in cuts_data:
267
- start_idx = cut_info.get("start_index", 0)
268
- duration_sec = cut_info.get("duration_seconds", 60)
269
- reason = cut_info.get("reason", "")
270
-
271
- if start_idx < 0 or start_idx >= len(segments):
272
- continue
273
-
274
- start_seg = segments[start_idx]
275
- start_frame = start_seg.start_f
276
- duration_frames = int(duration_sec * fps)
277
- end_frame = start_frame + duration_frames
278
-
279
- # Coleta texto dos segmentos envolvidos
280
- text_parts = [f"[IA: {reason}]"] if reason else []
281
- for seg in segments[start_idx:]:
282
- if seg.start_f < end_frame:
283
- if seg.text:
284
- text_parts.append(seg.text[:150])
285
- else:
286
- break
287
-
288
- combined_text = " [...] ".join(text_parts)[:500]
289
-
290
- selected_segments.append(Segment(
291
- start_tc=frames_to_timecode(start_frame, fps),
292
- end_tc=frames_to_timecode(end_frame, fps),
293
- start_f=start_frame,
294
- end_f=end_frame,
295
- text=combined_text,
296
- score=100.0
297
- ))
298
-
299
- return selected_segments
300
-
301
- except json.JSONDecodeError as e:
302
- raise ValueError(f"Erro ao processar resposta da IA (JSON inválido): {str(e)}\nResposta: {response_text[:300]}")
303
- except Exception as e:
304
- raise ValueError(f"Erro na análise da IA: {str(e)}")
305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
- # =========================
308
- # Processamento com Comando Manual (sem IA)
309
- # =========================
310
- def manual_command_processing(segments: List[Segment], command: str, fps: int) -> List[Segment]:
311
- """Fallback: processamento básico sem IA para comandos simples."""
312
- s = command.lower()
313
-
314
- count = 1
315
- m = re.search(r'(\d+)\s*(?:cortes?|clipes?|segmentos?)', s)
316
- if m:
317
- count = int(m.group(1))
318
-
319
- duration_sec = 60
320
- m = re.search(r'(\d+)\s*(?:segundos?|s\b)', s)
321
- if m:
322
- duration_sec = int(m.group(1))
323
- else:
324
- m = re.search(r'(\d+)\s*(?:minutos?|min\b)', s)
325
- if m:
326
- duration_sec = int(m.group(1)) * 60
327
-
328
- start_frame = 0
329
- m = re.search(r'(?:começando|a partir de)\s+(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)', s)
330
- if m:
331
- try:
332
- start_frame = parse_timecode_to_frames(m.group(1), fps)
333
- except:
334
- pass
335
-
336
- results = []
337
- base_frame = start_frame
338
-
339
- for i in range(count):
340
- duration_frames = duration_sec * fps
341
- end_frame = base_frame + duration_frames
342
-
343
- text_parts = []
344
- for seg in segments:
345
- if seg.start_f >= base_frame and seg.start_f < end_frame:
346
- if seg.text:
347
- text_parts.append(seg.text[:100])
348
-
349
- combined_text = " [...] ".join(text_parts[:10])[:400]
350
-
351
- results.append(Segment(
352
- start_tc=frames_to_timecode(base_frame, fps),
353
- end_tc=frames_to_timecode(end_frame, fps),
354
- start_f=base_frame,
355
- end_f=end_frame,
356
- text=combined_text if combined_text else f"Corte {i+1}",
357
- score=50.0
358
- ))
359
-
360
- base_frame = end_frame
361
-
362
- return results
363
-
364
-
365
- # =========================
366
- # Modo Automático
367
- # =========================
368
- def auto_score_segments(
369
- segs: List[Segment],
370
- num_segments: int,
371
- custom_keywords: str,
372
- weight_emotion: float,
373
- weight_break: float,
374
- weight_learn: float,
375
- weight_viral: float
376
- ) -> List[Segment]:
377
- """Sistema de pontuação automática."""
378
- emotion_words = ['medo', 'coragem', 'amor', 'ódio', 'paixão', 'alegria', 'tristeza']
379
- break_words = ['nunca', 'de repente', 'surpreendente', 'inesperado', 'incrível']
380
- learn_words = ['aprendi', 'descobri', 'entendi', 'percebi', 'lição']
381
- viral_words = ['segredo', 'verdade', 'revelação', 'exclusivo', 'confissão']
382
-
383
- for s in segs:
384
- score = 0.0
385
- text = (s.text or "").lower()
386
-
387
- for word in emotion_words:
388
- if word in text:
389
- score += weight_emotion
390
-
391
- for word in break_words:
392
- if word in text:
393
- score += weight_break
394
-
395
- for word in learn_words:
396
- if word in text:
397
- score += weight_learn
398
-
399
- for word in viral_words:
400
- if word in text:
401
- score += weight_viral
402
-
403
- if custom_keywords:
404
- for kw in custom_keywords.split(","):
405
- kw_clean = kw.strip().lower()
406
- if kw_clean and kw_clean in text:
407
- score += 5.0
408
-
409
- s.score = score
410
-
411
- segs.sort(key=lambda x: x.score, reverse=True)
412
- return segs[:num_segments]
413
-
414
-
415
- # =========================
416
- # Edição de XML
417
- # =========================
418
- def deep_copy_element(elem: ET.Element) -> ET.Element:
419
- new = ET.Element(elem.tag, attrib=dict(elem.attrib))
420
- new.text = elem.text
421
- new.tail = elem.tail
422
- for child in elem:
423
- new.append(deep_copy_element(child))
424
- return new
425
-
426
-
427
- def edit_xml(tree: ET.ElementTree, segs: List[Segment]) -> ET.ElementTree:
428
- root = tree.getroot()
429
- seq = root.find(".//sequence")
430
- if seq is None:
431
- raise ValueError("Sequence não encontrada no XML")
432
-
433
- v_track = seq.find(".//media/video/track")
434
- a_track = seq.find(".//media/audio/track")
435
- if not v_track or not a_track:
436
- raise ValueError("Trilhas de vídeo/áudio não encontradas")
437
-
438
- v_template = v_track.find("./clipitem")
439
- a_template = a_track.find("./clipitem")
440
-
441
- for clip in list(v_track.findall("./clipitem")):
442
- v_track.remove(clip)
443
- for clip in list(a_track.findall("./clipitem")):
444
- a_track.remove(clip)
445
-
446
- timeline_pos = 0
447
- for i, seg in enumerate(segs, 1):
448
- duration = seg.end_f - seg.start_f
449
- if duration <= 0:
450
- continue
451
-
452
- v_clip = ET.Element("clipitem", {"id": f"clip-v{i}"})
453
- ET.SubElement(v_clip, "name").text = f"Clip {i}"
454
- ET.SubElement(v_clip, "start").text = str(timeline_pos)
455
- ET.SubElement(v_clip, "end").text = str(timeline_pos + duration)
456
- ET.SubElement(v_clip, "in").text = str(seg.start_f)
457
- ET.SubElement(v_clip, "out").text = str(seg.end_f)
458
-
459
- if v_template is not None:
460
- rate = v_template.find("rate")
461
- if rate is not None:
462
- v_clip.append(deep_copy_element(rate))
463
- file_elem = v_template.find("file")
464
- if file_elem is not None:
465
- v_clip.append(deep_copy_element(file_elem))
466
-
467
- a_clip = ET.Element("clipitem", {"id": f"clip-a{i}"})
468
- ET.SubElement(a_clip, "name").text = f"Clip {i}"
469
- ET.SubElement(a_clip, "start").text = str(timeline_pos)
470
- ET.SubElement(a_clip, "end").text = str(timeline_pos + duration)
471
- ET.SubElement(a_clip, "in").text = str(seg.start_f)
472
- ET.SubElement(a_clip, "out").text = str(seg.end_f)
473
-
474
- if a_template is not None:
475
- rate = a_template.find("rate")
476
- if rate is not None:
477
- a_clip.append(deep_copy_element(rate))
478
- file_elem = a_template.find("file")
479
- if file_elem is not None:
480
- a_clip.append(deep_copy_element(file_elem))
481
-
482
- v_track.append(v_clip)
483
- a_track.append(a_clip)
484
- timeline_pos += duration
485
-
486
- return tree
487
-
488
-
489
- # =========================
490
- # Seleção (orquestração)
491
- # =========================
492
- def select_segments(
493
- transcript_txt: str,
494
- use_llm: bool,
495
- num_segments: int,
496
- custom_keywords: str,
497
- manual_timecodes: str,
498
- natural_instructions: str,
499
- weight_emotion: float,
500
- weight_break: float,
501
- weight_learn: float,
502
- weight_viral: float,
503
- progress_callback=None
504
- ) -> List[Segment]:
505
-
506
- # 1) Manual
507
- manual = parse_manual_timecodes(manual_timecodes)
508
- if manual:
509
- result = []
510
- for start_tc, end_tc in manual:
511
- try:
512
- result.append(Segment(
513
- start_tc=frames_to_timecode(parse_timecode_to_frames(start_tc)),
514
- end_tc=frames_to_timecode(parse_timecode_to_frames(end_tc)),
515
- start_f=parse_timecode_to_frames(start_tc),
516
- end_f=parse_timecode_to_frames(end_tc),
517
- text=f"Manual: {start_tc} - {end_tc}",
518
- score=100.0
519
- ))
520
- except Exception:
521
- pass
522
- return result
523
-
524
- # 2) Parser de transcrição
525
- segs = parse_transcript(transcript_txt) if transcript_txt else []
526
-
527
- # 3) Linguagem natural COM IA
528
- if natural_instructions.strip():
529
- if use_llm and LLM_AVAILABLE and segs:
530
- # USA IA PARA ANÁLISE COMPLETA
531
- return ai_analyze_and_select(segs, natural_instructions, progress_callback)
532
- elif segs:
533
- # Fallback sem IA
534
- return manual_command_processing(segs, natural_instructions)
535
- else:
536
- raise ValueError("Para usar comandos em linguagem natural, forneça uma transcrição ou ative as minutagens manuais.")
537
-
538
- # 4) Automático
539
- if not segs:
540
- raise ValueError("Nenhum segmento encontrado. Forneça uma transcrição, minutagens ou um comando em linguagem natural.")
541
- return auto_score_segments(
542
- segs, num_segments, custom_keywords,
543
- weight_emotion, weight_break, weight_learn, weight_viral
544
- )
545
-
546
-
547
- # =========================
548
- # Pipeline principal
549
- # =========================
550
- def process_files(
551
- xml_file, txt_file, use_llm, num_segments,
552
- custom_keywords, manual_timecodes, natural_instructions,
553
- weight_emotion, weight_break, weight_learn, weight_viral,
554
- progress=gr.Progress()
555
- ):
556
- if not xml_file:
557
- return "⚠️ Envie o XML do Premiere", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
558
-
559
- try:
560
- debug_info = []
561
-
562
- def progress_callback(msg):
563
- progress(0.5, desc=msg)
564
- debug_info.append(msg)
565
-
566
- progress(0.1, desc="📂 Carregando arquivos...")
567
-
568
- transcript = ""
569
- manual = parse_manual_timecodes(manual_timecodes)
570
-
571
- if not manual and txt_file:
572
- with open(txt_file.name, "r", encoding="utf-8-sig") as f:
573
- transcript = f.read()
574
- debug_info.append(f"📄 Transcrição: {len(transcript)} caracteres")
575
-
576
- progress(0.2, desc="🔍 Selecionando segmentos...")
577
-
578
- segments = select_segments(
579
- transcript, use_llm and LLM_AVAILABLE, num_segments,
580
- custom_keywords, manual_timecodes, natural_instructions,
581
- weight_emotion, weight_break, weight_learn, weight_viral,
582
- progress_callback
583
- )
584
-
585
- if not segments:
586
- return "⚠️ Nenhum segmento selecionado", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
587
-
588
- valid_segments = []
589
- for seg in segments:
590
- if seg.end_f > seg.start_f and seg.end_f - seg.start_f >= FPS:
591
- valid_segments.append(seg)
592
-
593
- if not valid_segments:
594
- return "⚠️ Segmentos inválidos (duração muito curta)", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
595
-
596
- segments = valid_segments
597
- debug_info.append(f"✓ {len(segments)} segmento(s) válido(s)")
598
-
599
- progress(0.7, desc="✂️ Editando XML...")
600
-
601
- tree = ET.parse(xml_file.name)
602
- tree = edit_xml(tree, segments)
603
-
604
- basename = os.path.splitext(os.path.basename(xml_file.name))[0]
605
- output = os.path.join(OUTPUT_DIR, f"{basename}_EDITADO.xml")
606
- tree.write(output, encoding="utf-8", xml_declaration=True)
607
-
608
- progress(0.9, desc="📊 Gerando resumo...")
609
-
610
- total_sec = sum((s.end_f - s.start_f) / FPS for s in segments)
611
- total_min = total_sec / 60.0
612
-
613
- if manual:
614
- mode = "🎯 MANUAL"
615
- elif natural_instructions.strip() and use_llm and LLM_AVAILABLE:
616
- mode = "🤖 IA COMPLETA (Gemini)"
617
- elif natural_instructions.strip():
618
- mode = "📐 BÁSICO (sem IA)"
619
- else:
620
- mode = "⚙️ AUTOMÁTICO"
621
-
622
- summary_lines = [
623
- "═" * 70,
624
- f"✨ RESULTADO: {len(segments)} corte(s) | {total_min:.1f} min total",
625
- f"📊 Modo: {mode}",
626
- "═" * 70,
627
- ""
628
- ]
629
-
630
- for i, seg in enumerate(segments, 1):
631
- dur_sec = (seg.end_f - seg.start_f) / FPS
632
- dur_min = dur_sec / 60.0
633
-
634
- line = f"🎬 Corte {i}:"
635
- line += f"\n ⏱️ {seg.start_tc} → {seg.end_tc} ({dur_min:.2f} min / {dur_sec:.0f}s)"
636
-
637
- if seg.text and len(seg.text.strip()) > 10:
638
- text_preview = seg.text[:200].strip()
639
- if len(seg.text) > 200:
640
- text_preview += "..."
641
- line += f"\n 💬 {text_preview}"
642
-
643
- summary_lines.append(line)
644
- summary_lines.append("")
645
-
646
- if debug_info:
647
- summary_lines.append("═" * 70)
648
- summary_lines.append("🔍 Log do Processamento:")
649
- summary_lines.extend(f" {info}" for info in debug_info)
650
-
651
- summary = "\n".join(summary_lines)
652
- status = f"✅ Sucesso | {mode} | {total_min:.1f} min | LLM: {'✓' if LLM_AVAILABLE else '✗'}"
653
-
654
- progress(1.0, desc="✅ Concluído!")
655
- return summary, output, status
656
-
657
- except Exception as e:
658
- import traceback
659
- error_trace = traceback.format_exc()
660
- print(error_trace)
661
-
662
- error_msg = f"❌ Erro: {str(e)}\n\n🔍 Detalhes:\n{error_trace[:800]}"
663
- return error_msg, None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
664
-
665
-
666
- # =========================
667
- # Interface Gradio
668
- # =========================
669
- with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere - IA") as demo:
670
- gr.Markdown("# 🎬 Editor XML Premiere - IA Completa (Gemini)")
671
- gr.Markdown("Sistema que **REALMENTE ENTENDE** seu comando usando análise completa com IA.")
672
-
673
- status_inicial = f"{'🟢 IA Gemini Ativa - Análise Completa Habilitada' if LLM_AVAILABLE else '🔴 IA Desabilitada - Configure GEMINI_API_KEY para análise inteligente'}"
674
- gr.Markdown(f"**Status:** {status_inicial}")
675
-
676
- if LLM_AVAILABLE:
677
- gr.Markdown("""
678
- ### 🚀 Como funciona a IA:
679
- 1. **Você descreve** o que quer em linguagem natural
680
- 2. **IA analisa** toda a transcrição (pode levar 30-60s)
681
- 3. **IA identifica** os trechos exatos que correspondem ao seu pedido
682
- 4. **Sistema cria** os cortes precisos automaticamente
683
-
684
- ⚡ **Mais lento, mas MUITO mais preciso!**
685
- """)
686
- else:
687
- gr.Markdown("""
688
- ### ⚠️ IA Desabilitada
689
- Configure a variável de ambiente `GEMINI_API_KEY` para ativar análise inteligente.
690
- No modo básico, apenas comandos simples e timecodes manuais funcionam bem.
691
- """)
692
-
693
- with gr.Row():
694
- xml_in = gr.File(label="📄 XML do Premiere", file_types=[".xml"])
695
- txt_in = gr.File(label="📝 Transcrição (.txt) - OBRIGATÓRIA para IA", file_types=[".txt"])
696
-
697
- with gr.Row():
698
- use_llm = gr.Checkbox(
699
- label="🤖 Usar IA Gemini (análise completa - RECOMENDADO)",
700
- value=USE_LLM_DEFAULT and LLM_AVAILABLE,
701
- interactive=LLM_AVAILABLE,
702
- info="Quando ativo, a IA lê TODA a transcrição e encontra os melhores trechos"
703
- )
704
- num_segments = gr.Slider(2, 20, 5, 1, label="📊 Segmentos (apenas modo automático)")
705
-
706
- with gr.Accordion("💬 Comando em Linguagem Natural (MODO PRINCIPAL)", open=True):
707
- gr.Markdown("""
708
- ### ✨ Exemplos de comandos que a IA entende:
709
-
710
- **📌 Simples:**
711
- - "Crie 3 cortes de 30 segundos sobre futebol"
712
- - "Quero 2 clipes de 1 minuto falando sobre Maria"
713
- - "Faça 5 cortes de 45s sobre o tema educação"
714
-
715
- **🎯 Específicos:**
716
- - "1 corte de 10 minutos da parte onde ele fala sobre a infância"
717
- - "3 cortes de 30s sobre os momentos engraçados"
718
- - "2 clipes de 1min sobre superação e disciplina"
719
-
720
- **📍 Com timecode:**
721
- - "Corte de 5 minutos começando em 00:02:00:00 sobre tecnologia"
722
- - "3 cortes de 45s a partir de 00:10:00 falando sobre amor"
723
-
724
- **🔍 Busca temática:**
725
- - "Os melhores momentos sobre família, cada um com 40s"
726
- - "Trechos emocionantes de 1 minuto cada"
727
- - "Partes onde menciona desafios e conquistas"
728
-
729
- ### 💡 Dicas para melhores resultados:
730
- - ✅ Seja específico sobre o tema/assunto
731
- - ✅ Especifique duração e quantidade
732
- - ✅ Use a transcrição completa
733
- - ✅ Deixe a IA trabalhar (30-60s de análise)
734
- - ❌ Evite comandos vagos como "faça algo legal"
735
- """)
736
- natural_instructions = gr.Textbox(
737
- label="Digite seu comando aqui",
738
- placeholder='Ex: "Crie 3 cortes de 45 segundos sobre os momentos onde ele fala de disciplina e superação"',
739
- lines=4
740
- )
741
-
742
- with gr.Accordion("🎯 Minutagens Manuais (precisão total)", open=False):
743
- gr.Markdown("Use quando souber exatamente os timecodes. Ignora IA e outros modos.")
744
- manual_timecodes = gr.Textbox(
745
- label="Timecodes (um por linha)",
746
- placeholder="00:21:18:09 - 00:31:18:09\n00:45:20:15 - 00:50:10:22",
747
- lines=4
748
- )
749
-
750
- with gr.Accordion("⚙️ Modo Automático (sem comando)", open=False):
751
- gr.Markdown("Sistema de pontuação simples. **Não recomendado** - use comandos em linguagem natural.")
752
- custom_keywords = gr.Textbox(
753
- label="Palavras-chave (separadas por vírgula)",
754
- placeholder="coragem, superação, vitória"
755
- )
756
- with gr.Row():
757
- weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="⚡ Peso: emoção")
758
- weight_break = gr.Slider(0, 5, 1.5, 0.1, label="💥 Peso: quebra")
759
- with gr.Row():
760
- weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="🎓 Peso: aprendizado")
761
- weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="🔥 Peso: viral")
762
-
763
- btn = gr.Button("🚀 Processar com IA (pode levar 30-60s)", variant="primary", size="lg")
764
-
765
- with gr.Row():
766
- with gr.Column(scale=2):
767
- summary_out = gr.Textbox(label="📋 Resumo dos Cortes", lines=20, max_lines=30)
768
- with gr.Column(scale=1):
769
- status_out = gr.Textbox(label="📊 Status", lines=3)
770
- file_out = gr.File(label="⬇️ Download XML Editado")
771
-
772
- btn.click(
773
- process_files,
774
- [xml_in, txt_in, use_llm, num_segments, custom_keywords,
775
- manual_timecodes, natural_instructions,
776
- weight_emotion, weight_break, weight_learn, weight_viral],
777
- [summary_out, file_out, status_out]
778
- )
779
-
780
- gr.Markdown("""
781
- ---
782
- ### 📚 Guia Rápido:
783
-
784
- **🎯 Para melhores resultados:**
785
- 1. ✅ Envie XML + Transcrição completa
786
- 2. ✅ Ative a IA (checkbox)
787
- 3. ✅ Escreva comando claro e específico
788
- 4. ✅ Aguarde 30-60s para análise completa
789
- 5. ✅ Baixe e importe no Premiere
790
-
791
- **⚡ Ordem de prioridade:**
792
- 1. **Minutagens Manuais** (ignora tudo, máxima precisão)
793
- 2. **Comando + IA** (análise completa, muito preciso)
794
- 3. **Comando sem IA** (básico, menos preciso)
795
- 4. **Modo Automático** (não recomendado)
796
-
797
- **🔧 Troubleshooting:**
798
- - Erro "IA não disponível": Configure `GEMINI_API_KEY`
799
- - Cortes errados: Seja mais específico no comando
800
- - Demora muito: Normal para IA completa (30-60s)
801
- - Sem transcrição: Use minutagens manuais
802
- """)
803
-
804
- if __name__ == "__main__":
805
- demo.launch()
806
- )
807
- arrow = re.compile(
808
- r'(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3}|[:;]\d{2})?)\s*-->\s*'
809
- r'(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3}|[:;]\d{2})?)'
810
- )
811
-
812
- i = 0
813
- while i < len(lines):
814
- raw = lines[i].strip()
815
- if not raw or raw.lower() == "desconhecido":
816
- i += 1
817
- continue
818
-
819
- m = line_range.match(raw)
820
- if m:
821
- start_tc, end_tc, trailing_text = m.groups()
822
- text_parts = []
823
-
824
- if trailing_text.strip():
825
- text_parts.append(trailing_text.strip())
826
- else:
827
- j = i + 1
828
- while j < len(lines):
829
- nxt = lines[j].strip()
830
- if not nxt or line_range.match(nxt) or re.match(r'^\d+\s*
831
-
832
-
833
- # =========================
834
- # Minutagens Manuais
835
- # =========================
836
- def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
837
- if not manual_input or not manual_input.strip():
838
- return []
839
-
840
- manual_ranges = []
841
- lines = manual_input.replace(",", "\n").splitlines()
842
- pattern = re.compile(r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*[-–—]\s*(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)')
843
- for line in lines:
844
- m = pattern.search(line.strip())
845
- if m:
846
- manual_ranges.append((m.group(1), m.group(2)))
847
- return manual_ranges
848
-
849
-
850
- # =========================
851
- # IA: Análise Inteligente com Gemini
852
- # =========================
853
- def ai_analyze_and_select(segments: List[Segment], command: str, progress_callback=None) -> List[Segment]:
854
- """
855
- Usa Gemini para analisar a transcrição completa e identificar os melhores trechos.
856
- Processo em 2 etapas para máxima precisão.
857
- """
858
- if not LLM_AVAILABLE or not segments:
859
- raise ValueError("IA não disponível ou sem segmentos para analisar")
860
-
861
- if progress_callback:
862
- progress_callback("🤖 Etapa 1/3: Preparando dados para análise...")
863
-
864
- # Prepara a transcrição completa com índices
865
- transcript_data = []
866
- for i, seg in enumerate(segments):
867
- duration_sec = (seg.end_f - seg.start_f) / FPS
868
- transcript_data.append({
869
- "index": i,
870
- "timecode": seg.start_tc,
871
- "duration_sec": round(duration_sec, 1),
872
- "text": seg.text[:200] # Limita texto para não estourar tokens
873
- })
874
-
875
- # Converte para JSON para análise estruturada
876
- transcript_json = json.dumps(transcript_data, ensure_ascii=False, indent=2)
877
-
878
- if progress_callback:
879
- progress_callback(f"🤖 Etapa 2/3: Analisando {len(segments)} segmentos com IA (pode levar 30-60s)...")
880
-
881
- # Prompt detalhado para análise completa
882
- prompt = f"""Você é um especialista em edição de vídeo. Analise a transcrição e identifique os MELHORES trechos baseado no comando do usuário.
883
-
884
- COMANDO DO USUÁRIO:
885
- {command}
886
-
887
- TRANSCRIÇÃO COMPLETA (formato JSON com index, timecode, duração e texto):
888
- {transcript_json}
889
-
890
- INSTRUÇÕES:
891
- 1. Leia o comando com atenção e identifique:
892
- - Quantidade de cortes desejada
893
- - Duração de cada corte (em segundos)
894
- - Tema/assunto/palavras-chave mencionados
895
- - Timecode de início (se mencionado)
896
-
897
- 2. Analise TODA a transcrição e identifique os segmentos que melhor correspondem ao comando
898
-
899
- 3. Para cada corte, retorne no formato JSON:
900
- {{
901
- "cuts": [
902
- {{
903
- "start_index": <índice do segmento inicial>,
904
- "duration_seconds": <duração desejada em segundos>,
905
- "reason": "<breve explicação de por que escolheu este trecho>"
906
- }}
907
- ]
908
- }}
909
-
910
- IMPORTANTE:
911
- - Seja PRECISO na identificação dos trechos
912
- - Considere o contexto completo ao redor das palavras-chave
913
- - Se o comando pedir "sobre X", encontre onde X é realmente discutido
914
- - Se houver timecode, priorize começar próximo a ele
915
- - Retorne APENAS o JSON, sem texto adicional
916
-
917
- Responda com o JSON:"""
918
-
919
- try:
920
- response = LLM.generate_content(
921
- prompt,
922
- generation_config={
923
- "temperature": 0.2,
924
- "max_output_tokens": 2000,
925
- }
926
- )
927
-
928
- response_text = response.text.strip()
929
-
930
- if progress_callback:
931
- progress_callback("🤖 Etapa 3/3: Processando resposta da IA...")
932
-
933
- # Extrai JSON da resposta
934
- json_match = re.search(r'\{[\s\S]*"cuts"[\s\S]*\}', response_text)
935
- if not json_match:
936
- raise ValueError("IA não retornou JSON válido")
937
-
938
- result = json.loads(json_match.group(0))
939
- cuts_data = result.get("cuts", [])
940
-
941
- if not cuts_data:
942
- raise ValueError("IA não encontrou cortes adequados")
943
-
944
- # Cria os segmentos baseado na análise da IA
945
- selected_segments = []
946
-
947
- for cut_info in cuts_data:
948
- start_idx = cut_info.get("start_index", 0)
949
- duration_sec = cut_info.get("duration_seconds", 60)
950
- reason = cut_info.get("reason", "")
951
-
952
- if start_idx < 0 or start_idx >= len(segments):
953
- continue
954
-
955
- start_seg = segments[start_idx]
956
- start_frame = start_seg.start_f
957
- duration_frames = int(duration_sec * FPS)
958
- end_frame = start_frame + duration_frames
959
-
960
- # Coleta texto dos segmentos envolvidos
961
- text_parts = [f"[IA: {reason}]"] if reason else []
962
- for seg in segments[start_idx:]:
963
- if seg.start_f < end_frame:
964
- if seg.text:
965
- text_parts.append(seg.text[:150])
966
- else:
967
- break
968
-
969
- combined_text = " [...] ".join(text_parts)[:500]
970
-
971
- selected_segments.append(Segment(
972
- start_tc=frames_to_timecode(start_frame),
973
- end_tc=frames_to_timecode(end_frame),
974
- start_f=start_frame,
975
- end_f=end_frame,
976
- text=combined_text,
977
- score=100.0
978
- ))
979
-
980
- return selected_segments
981
-
982
- except json.JSONDecodeError as e:
983
- raise ValueError(f"Erro ao processar resposta da IA (JSON inválido): {str(e)}\nResposta: {response_text[:300]}")
984
- except Exception as e:
985
- raise ValueError(f"Erro na análise da IA: {str(e)}")
986
-
987
-
988
- # =========================
989
- # Processamento com Comando Manual (sem IA)
990
- # =========================
991
- def manual_command_processing(segments: List[Segment], command: str) -> List[Segment]:
992
- """
993
- Fallback: processamento básico sem IA para comandos simples.
994
- """
995
- s = command.lower()
996
-
997
- # Extrai quantidade
998
- count = 1
999
- m = re.search(r'(\d+)\s*(?:cortes?|clipes?|segmentos?)', s)
1000
- if m:
1001
- count = int(m.group(1))
1002
-
1003
- # Extrai duração
1004
- duration_sec = 60
1005
- m = re.search(r'(\d+)\s*(?:segundos?|s\b)', s)
1006
- if m:
1007
- duration_sec = int(m.group(1))
1008
- else:
1009
- m = re.search(r'(\d+)\s*(?:minutos?|min\b)', s)
1010
- if m:
1011
- duration_sec = int(m.group(1)) * 60
1012
-
1013
- # Extrai timecode inicial
1014
- start_frame = 0
1015
- m = re.search(r'(?:começando|a partir de)\s+(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)', s)
1016
- if m:
1017
- try:
1018
- start_frame = parse_timecode_to_frames(m.group(1))
1019
- except:
1020
- pass
1021
-
1022
- # Cria cortes contínuos
1023
- results = []
1024
- base_frame = start_frame
1025
-
1026
- for i in range(count):
1027
- duration_frames = duration_sec * FPS
1028
- end_frame = base_frame + duration_frames
1029
-
1030
- # Coleta texto
1031
- text_parts = []
1032
- for seg in segments:
1033
- if seg.start_f >= base_frame and seg.start_f < end_frame:
1034
- if seg.text:
1035
- text_parts.append(seg.text[:100])
1036
-
1037
- combined_text = " [...] ".join(text_parts[:10])[:400]
1038
-
1039
- results.append(Segment(
1040
- start_tc=frames_to_timecode(base_frame),
1041
- end_tc=frames_to_timecode(end_frame),
1042
- start_f=base_frame,
1043
- end_f=end_frame,
1044
- text=combined_text if combined_text else f"Corte {i+1}",
1045
- score=50.0
1046
- ))
1047
-
1048
- base_frame = end_frame
1049
-
1050
- return results
1051
-
1052
-
1053
- # =========================
1054
- # Modo Automático
1055
- # =========================
1056
- def auto_score_segments(
1057
- segs: List[Segment],
1058
- num_segments: int,
1059
- custom_keywords: str,
1060
- weight_emotion: float,
1061
- weight_break: float,
1062
- weight_learn: float,
1063
- weight_viral: float
1064
- ) -> List[Segment]:
1065
- """Sistema de pontuação automática."""
1066
- emotion_words = ['medo', 'coragem', 'amor', 'ódio', 'paixão', 'alegria', 'tristeza']
1067
- break_words = ['nunca', 'de repente', 'surpreendente', 'inesperado', 'incrível']
1068
- learn_words = ['aprendi', 'descobri', 'entendi', 'percebi', 'lição']
1069
- viral_words = ['segredo', 'verdade', 'revelação', 'exclusivo', 'confissão']
1070
-
1071
- for s in segs:
1072
- score = 0.0
1073
- text = (s.text or "").lower()
1074
-
1075
- for word in emotion_words:
1076
- if word in text:
1077
- score += weight_emotion
1078
-
1079
- for word in break_words:
1080
- if word in text:
1081
- score += weight_break
1082
-
1083
- for word in learn_words:
1084
- if word in text:
1085
- score += weight_learn
1086
-
1087
- for word in viral_words:
1088
- if word in text:
1089
- score += weight_viral
1090
-
1091
- if custom_keywords:
1092
- for kw in custom_keywords.split(","):
1093
- kw_clean = kw.strip().lower()
1094
- if kw_clean and kw_clean in text:
1095
- score += 5.0
1096
-
1097
- s.score = score
1098
-
1099
- segs.sort(key=lambda x: x.score, reverse=True)
1100
- return segs[:num_segments]
1101
-
1102
-
1103
- # =========================
1104
- # Edição de XML
1105
- # =========================
1106
- def deep_copy_element(elem: ET.Element) -> ET.Element:
1107
- new = ET.Element(elem.tag, attrib=dict(elem.attrib))
1108
- new.text = elem.text
1109
- new.tail = elem.tail
1110
- for child in elem:
1111
- new.append(deep_copy_element(child))
1112
- return new
1113
-
1114
-
1115
- def edit_xml(tree: ET.ElementTree, segs: List[Segment]) -> ET.ElementTree:
1116
- root = tree.getroot()
1117
- seq = root.find(".//sequence")
1118
- if seq is None:
1119
- raise ValueError("Sequence não encontrada no XML")
1120
-
1121
- v_track = seq.find(".//media/video/track")
1122
- a_track = seq.find(".//media/audio/track")
1123
- if not v_track or not a_track:
1124
- raise ValueError("Trilhas de vídeo/áudio não encontradas")
1125
-
1126
- v_template = v_track.find("./clipitem")
1127
- a_template = a_track.find("./clipitem")
1128
-
1129
- for clip in list(v_track.findall("./clipitem")):
1130
- v_track.remove(clip)
1131
- for clip in list(a_track.findall("./clipitem")):
1132
- a_track.remove(clip)
1133
-
1134
- timeline_pos = 0
1135
- for i, seg in enumerate(segs, 1):
1136
- duration = seg.end_f - seg.start_f
1137
- if duration <= 0:
1138
- continue
1139
-
1140
- v_clip = ET.Element("clipitem", {"id": f"clip-v{i}"})
1141
- ET.SubElement(v_clip, "name").text = f"Clip {i}"
1142
- ET.SubElement(v_clip, "start").text = str(timeline_pos)
1143
- ET.SubElement(v_clip, "end").text = str(timeline_pos + duration)
1144
- ET.SubElement(v_clip, "in").text = str(seg.start_f)
1145
- ET.SubElement(v_clip, "out").text = str(seg.end_f)
1146
-
1147
- if v_template is not None:
1148
- rate = v_template.find("rate")
1149
- if rate is not None:
1150
- v_clip.append(deep_copy_element(rate))
1151
- file_elem = v_template.find("file")
1152
- if file_elem is not None:
1153
- v_clip.append(deep_copy_element(file_elem))
1154
-
1155
- a_clip = ET.Element("clipitem", {"id": f"clip-a{i}"})
1156
- ET.SubElement(a_clip, "name").text = f"Clip {i}"
1157
- ET.SubElement(a_clip, "start").text = str(timeline_pos)
1158
- ET.SubElement(a_clip, "end").text = str(timeline_pos + duration)
1159
- ET.SubElement(a_clip, "in").text = str(seg.start_f)
1160
- ET.SubElement(a_clip, "out").text = str(seg.end_f)
1161
-
1162
- if a_template is not None:
1163
- rate = a_template.find("rate")
1164
- if rate is not None:
1165
- a_clip.append(deep_copy_element(rate))
1166
- file_elem = a_template.find("file")
1167
- if file_elem is not None:
1168
- a_clip.append(deep_copy_element(file_elem))
1169
-
1170
- v_track.append(v_clip)
1171
- a_track.append(a_clip)
1172
- timeline_pos += duration
1173
-
1174
- return tree
1175
-
1176
-
1177
- # =========================
1178
- # Seleção (orquestração)
1179
- # =========================
1180
- def select_segments(
1181
- transcript_txt: str,
1182
- use_llm: bool,
1183
- num_segments: int,
1184
- custom_keywords: str,
1185
- manual_timecodes: str,
1186
- natural_instructions: str,
1187
- weight_emotion: float,
1188
- weight_break: float,
1189
- weight_learn: float,
1190
- weight_viral: float,
1191
- progress_callback=None
1192
- ) -> List[Segment]:
1193
-
1194
- # 1) Manual
1195
- manual = parse_manual_timecodes(manual_timecodes)
1196
- if manual:
1197
- result = []
1198
- for start_tc, end_tc in manual:
1199
- try:
1200
- result.append(Segment(
1201
- start_tc=frames_to_timecode(parse_timecode_to_frames(start_tc)),
1202
- end_tc=frames_to_timecode(parse_timecode_to_frames(end_tc)),
1203
- start_f=parse_timecode_to_frames(start_tc),
1204
- end_f=parse_timecode_to_frames(end_tc),
1205
- text=f"Manual: {start_tc} - {end_tc}",
1206
- score=100.0
1207
- ))
1208
- except Exception:
1209
- pass
1210
- return result
1211
-
1212
- # 2) Parser de transcrição
1213
- segs = parse_transcript(transcript_txt) if transcript_txt else []
1214
-
1215
- # 3) Linguagem natural COM IA
1216
- if natural_instructions.strip():
1217
- if use_llm and LLM_AVAILABLE and segs:
1218
- # USA IA PARA ANÁLISE COMPLETA
1219
- return ai_analyze_and_select(segs, natural_instructions, progress_callback)
1220
- elif segs:
1221
- # Fallback sem IA
1222
- return manual_command_processing(segs, natural_instructions)
1223
- else:
1224
- raise ValueError("Para usar comandos em linguagem natural, forneça uma transcrição ou ative as minutagens manuais.")
1225
-
1226
- # 4) Automático
1227
- if not segs:
1228
- raise ValueError("Nenhum segmento encontrado. Forneça uma transcrição, minutagens ou um comando em linguagem natural.")
1229
- return auto_score_segments(
1230
- segs, num_segments, custom_keywords,
1231
- weight_emotion, weight_break, weight_learn, weight_viral
1232
- )
1233
-
1234
-
1235
- # =========================
1236
- # Pipeline principal
1237
- # =========================
1238
- def process_files(
1239
- xml_file, txt_file, use_llm, num_segments,
1240
- custom_keywords, manual_timecodes, natural_instructions,
1241
- weight_emotion, weight_break, weight_learn, weight_viral,
1242
- progress=gr.Progress()
1243
- ):
1244
- if not xml_file:
1245
- return "⚠️ Envie o XML do Premiere", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1246
-
1247
- try:
1248
- debug_info = []
1249
-
1250
- def progress_callback(msg):
1251
- progress(0.5, desc=msg)
1252
- debug_info.append(msg)
1253
-
1254
- progress(0.1, desc="📂 Carregando arquivos...")
1255
-
1256
- transcript = ""
1257
- manual = parse_manual_timecodes(manual_timecodes)
1258
-
1259
- if not manual and txt_file:
1260
- with open(txt_file.name, "r", encoding="utf-8-sig") as f:
1261
- transcript = f.read()
1262
- debug_info.append(f"📄 Transcrição: {len(transcript)} caracteres")
1263
-
1264
- progress(0.2, desc="🔍 Selecionando segmentos...")
1265
-
1266
- segments = select_segments(
1267
- transcript, use_llm and LLM_AVAILABLE, num_segments,
1268
- custom_keywords, manual_timecodes, natural_instructions,
1269
- weight_emotion, weight_break, weight_learn, weight_viral,
1270
- progress_callback
1271
- )
1272
-
1273
- if not segments:
1274
- return "⚠️ Nenhum segmento selecionado", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1275
-
1276
- valid_segments = []
1277
- for seg in segments:
1278
- if seg.end_f > seg.start_f and seg.end_f - seg.start_f >= FPS:
1279
- valid_segments.append(seg)
1280
-
1281
- if not valid_segments:
1282
- return "⚠️ Segmentos inválidos (duração muito curta)", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1283
-
1284
- segments = valid_segments
1285
- debug_info.append(f"✓ {len(segments)} segmento(s) válido(s)")
1286
-
1287
- progress(0.7, desc="✂️ Editando XML...")
1288
-
1289
- tree = ET.parse(xml_file.name)
1290
- tree = edit_xml(tree, segments)
1291
-
1292
- basename = os.path.splitext(os.path.basename(xml_file.name))[0]
1293
- output = os.path.join(OUTPUT_DIR, f"{basename}_EDITADO.xml")
1294
- tree.write(output, encoding="utf-8", xml_declaration=True)
1295
-
1296
- progress(0.9, desc="📊 Gerando resumo...")
1297
-
1298
- total_sec = sum((s.end_f - s.start_f) / FPS for s in segments)
1299
- total_min = total_sec / 60.0
1300
-
1301
- if manual:
1302
- mode = "🎯 MANUAL"
1303
- elif natural_instructions.strip() and use_llm and LLM_AVAILABLE:
1304
- mode = "🤖 IA COMPLETA (Gemini)"
1305
- elif natural_instructions.strip():
1306
- mode = "📐 BÁSICO (sem IA)"
1307
- else:
1308
- mode = "⚙️ AUTOMÁTICO"
1309
-
1310
- summary_lines = [
1311
- "═" * 70,
1312
- f"✨ RESULTADO: {len(segments)} corte(s) | {total_min:.1f} min total",
1313
- f"📊 Modo: {mode}",
1314
- "═" * 70,
1315
- ""
1316
- ]
1317
-
1318
- for i, seg in enumerate(segments, 1):
1319
- dur_sec = (seg.end_f - seg.start_f) / FPS
1320
- dur_min = dur_sec / 60.0
1321
-
1322
- line = f"🎬 Corte {i}:"
1323
- line += f"\n ⏱️ {seg.start_tc} → {seg.end_tc} ({dur_min:.2f} min / {dur_sec:.0f}s)"
1324
-
1325
- if seg.text and len(seg.text.strip()) > 10:
1326
- text_preview = seg.text[:200].strip()
1327
- if len(seg.text) > 200:
1328
- text_preview += "..."
1329
- line += f"\n 💬 {text_preview}"
1330
-
1331
- summary_lines.append(line)
1332
- summary_lines.append("")
1333
-
1334
- if debug_info:
1335
- summary_lines.append("═" * 70)
1336
- summary_lines.append("🔍 Log do Processamento:")
1337
- summary_lines.extend(f" {info}" for info in debug_info)
1338
-
1339
- summary = "\n".join(summary_lines)
1340
- status = f"✅ Sucesso | {mode} | {total_min:.1f} min | LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1341
-
1342
- progress(1.0, desc="✅ Concluído!")
1343
- return summary, output, status
1344
-
1345
- except Exception as e:
1346
- import traceback
1347
- error_trace = traceback.format_exc()
1348
- print(error_trace)
1349
-
1350
- error_msg = f"❌ Erro: {str(e)}\n\n🔍 Detalhes:\n{error_trace[:800]}"
1351
- return error_msg, None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1352
-
1353
-
1354
- # =========================
1355
- # Interface Gradio
1356
- # =========================
1357
- with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere - IA") as demo:
1358
- gr.Markdown("# 🎬 Editor XML Premiere - IA Completa (Gemini)")
1359
- gr.Markdown("Sistema que **REALMENTE ENTENDE** seu comando usando análise completa com IA.")
1360
-
1361
- status_inicial = f"{'🟢 IA Gemini Ativa - Análise Completa Habilitada' if LLM_AVAILABLE else '🔴 IA Desabilitada - Configure GEMINI_API_KEY para análise inteligente'}"
1362
- gr.Markdown(f"**Status:** {status_inicial}")
1363
-
1364
- if LLM_AVAILABLE:
1365
- gr.Markdown("""
1366
- ### 🚀 Como funciona a IA:
1367
- 1. **Você descreve** o que quer em linguagem natural
1368
- 2. **IA analisa** toda a transcrição (pode levar 30-60s)
1369
- 3. **IA identifica** os trechos exatos que correspondem ao seu pedido
1370
- 4. **Sistema cria** os cortes precisos automaticamente
1371
-
1372
- ⚡ **Mais lento, mas MUITO mais preciso!**
1373
- """)
1374
- else:
1375
- gr.Markdown("""
1376
- ### ⚠️ IA Desabilitada
1377
- Configure a variável de ambiente `GEMINI_API_KEY` para ativar análise inteligente.
1378
- No modo básico, apenas comandos simples e timecodes manuais funcionam bem.
1379
- """)
1380
-
1381
- with gr.Row():
1382
- xml_in = gr.File(label="📄 XML do Premiere", file_types=[".xml"])
1383
- txt_in = gr.File(label="📝 Transcrição (.txt) - OBRIGATÓRIA para IA", file_types=[".txt"])
1384
-
1385
- with gr.Row():
1386
- use_llm = gr.Checkbox(
1387
- label="🤖 Usar IA Gemini (análise completa - RECOMENDADO)",
1388
- value=USE_LLM_DEFAULT and LLM_AVAILABLE,
1389
- interactive=LLM_AVAILABLE,
1390
- info="Quando ativo, a IA lê TODA a transcrição e encontra os melhores trechos"
1391
- )
1392
- num_segments = gr.Slider(2, 20, 5, 1, label="📊 Segmentos (apenas modo automático)")
1393
-
1394
- with gr.Accordion("💬 Comando em Linguagem Natural (MODO PRINCIPAL)", open=True):
1395
- gr.Markdown("""
1396
- ### ✨ Exemplos de comandos que a IA entende:
1397
-
1398
- **📌 Simples:**
1399
- - "Crie 3 cortes de 30 segundos sobre futebol"
1400
- - "Quero 2 clipes de 1 minuto falando sobre Maria"
1401
- - "Faça 5 cortes de 45s sobre o tema educação"
1402
-
1403
- **🎯 Específicos:**
1404
- - "1 corte de 10 minutos da parte onde ele fala sobre a infância"
1405
- - "3 cortes de 30s sobre os momentos engraçados"
1406
- - "2 clipes de 1min sobre superação e disciplina"
1407
-
1408
- **📍 Com timecode:**
1409
- - "Corte de 5 minutos começando em 00:02:00:00 sobre tecnologia"
1410
- - "3 cortes de 45s a partir de 00:10:00 falando sobre amor"
1411
-
1412
- **🔍 Busca temática:**
1413
- - "Os melhores momentos sobre família, cada um com 40s"
1414
- - "Trechos emocionantes de 1 minuto cada"
1415
- - "Partes onde menciona desafios e conquistas"
1416
-
1417
- ### 💡 Dicas para melhores resultados:
1418
- - ✅ Seja específico sobre o tema/assunto
1419
- - ✅ Especifique duração e quantidade
1420
- - ✅ Use a transcrição completa
1421
- - ✅ Deixe a IA trabalhar (30-60s de análise)
1422
- - ❌ Evite comandos vagos como "faça algo legal"
1423
- """)
1424
- natural_instructions = gr.Textbox(
1425
- label="Digite seu comando aqui",
1426
- placeholder='Ex: "Crie 3 cortes de 45 segundos sobre os momentos onde ele fala de disciplina e superação"',
1427
- lines=4
1428
- )
1429
-
1430
- with gr.Accordion("🎯 Minutagens Manuais (precisão total)", open=False):
1431
- gr.Markdown("Use quando souber exatamente os timecodes. Ignora IA e outros modos.")
1432
- manual_timecodes = gr.Textbox(
1433
- label="Timecodes (um por linha)",
1434
- placeholder="00:21:18:09 - 00:31:18:09\n00:45:20:15 - 00:50:10:22",
1435
- lines=4
1436
- )
1437
-
1438
- with gr.Accordion("⚙️ Modo Automático (sem comando)", open=False):
1439
- gr.Markdown("Sistema de pontuação simples. **Não recomendado** - use comandos em linguagem natural.")
1440
- custom_keywords = gr.Textbox(
1441
- label="Palavras-chave (separadas por vírgula)",
1442
- placeholder="coragem, superação, vitória"
1443
- )
1444
- with gr.Row():
1445
- weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="⚡ Peso: emoção")
1446
- weight_break = gr.Slider(0, 5, 1.5, 0.1, label="💥 Peso: quebra")
1447
- with gr.Row():
1448
- weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="🎓 Peso: aprendizado")
1449
- weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="🔥 Peso: viral")
1450
-
1451
- btn = gr.Button("🚀 Processar com IA (pode levar 30-60s)", variant="primary", size="lg")
1452
-
1453
- with gr.Row():
1454
- with gr.Column(scale=2):
1455
- summary_out = gr.Textbox(label="📋 Resumo dos Cortes", lines=20, max_lines=30)
1456
- with gr.Column(scale=1):
1457
- status_out = gr.Textbox(label="📊 Status", lines=3)
1458
- file_out = gr.File(label="⬇️ Download XML Editado")
1459
-
1460
- btn.click(
1461
- process_files,
1462
- [xml_in, txt_in, use_llm, num_segments, custom_keywords,
1463
- manual_timecodes, natural_instructions,
1464
- weight_emotion, weight_break, weight_learn, weight_viral],
1465
- [summary_out, file_out, status_out]
1466
- )
1467
-
1468
- gr.Markdown("""
1469
- ---
1470
- ### 📚 Guia Rápido:
1471
-
1472
- **🎯 Para melhores resultados:**
1473
- 1. ✅ Envie XML + Transcrição completa
1474
- 2. ✅ Ative a IA (checkbox)
1475
- 3. ✅ Escreva comando claro e específico
1476
- 4. ✅ Aguarde 30-60s para análise completa
1477
- 5. ✅ Baixe e importe no Premiere
1478
-
1479
- **⚡ Ordem de prioridade:**
1480
- 1. **Minutagens Manuais** (ignora tudo, máxima precisão)
1481
- 2. **Comando + IA** (análise completa, muito preciso)
1482
- 3. **Comando sem IA** (básico, menos preciso)
1483
- 4. **Modo Automático** (não recomendado)
1484
-
1485
- **🔧 Troubleshooting:**
1486
- - Erro "IA não disponível": Configure `GEMINI_API_KEY`
1487
- - Cortes errados: Seja mais específico no comando
1488
- - Demora muito: Normal para IA completa (30-60s)
1489
- - Sem transcrição: Use minutagens manuais
1490
- """)
1491
-
1492
- if __name__ == "__main__":
1493
- demo.launch(), nxt) or arrow.search(nxt):
1494
- break
1495
- text_parts.append(nxt)
1496
- j += 1
1497
- i = j - 1
1498
-
1499
- text = " ".join(text_parts).strip()
1500
- try:
1501
- sf = parse_timecode_to_frames(start_tc, fps)
1502
- ef = parse_timecode_to_frames(end_tc, fps)
1503
- if ef > sf:
1504
- results.append(Segment(
1505
- start_tc=frames_to_timecode(sf, fps),
1506
- end_tc=frames_to_timecode(ef, fps),
1507
- start_f=sf,
1508
- end_f=ef,
1509
- text=text if text else f"{start_tc} - {end_tc}",
1510
- score=0.0
1511
- ))
1512
- except Exception:
1513
- pass
1514
- i += 1
1515
- continue
1516
-
1517
- if arrow.search(raw) or (i + 1 < len(lines) and arrow.search(lines[i + 1])):
1518
- line_with_tc = raw if arrow.search(raw) else lines[i + 1]
1519
- mm = arrow.search(line_with_tc)
1520
- if mm:
1521
- start_tc, end_tc = mm.groups()
1522
- j = i + 1 if line_with_tc == raw else i + 2
1523
- text_parts = []
1524
- while j < len(lines):
1525
- nxt = lines[j].strip()
1526
- if not nxt:
1527
- break
1528
- if re.match(r'^\d+\s*
1529
-
1530
-
1531
- # =========================
1532
- # Minutagens Manuais
1533
- # =========================
1534
- def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
1535
- if not manual_input or not manual_input.strip():
1536
- return []
1537
-
1538
- manual_ranges = []
1539
- lines = manual_input.replace(",", "\n").splitlines()
1540
- pattern = re.compile(r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*[-–—]\s*(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)')
1541
- for line in lines:
1542
- m = pattern.search(line.strip())
1543
- if m:
1544
- manual_ranges.append((m.group(1), m.group(2)))
1545
- return manual_ranges
1546
-
1547
-
1548
- # =========================
1549
- # IA: Análise Inteligente com Gemini
1550
- # =========================
1551
- def ai_analyze_and_select(segments: List[Segment], command: str, progress_callback=None) -> List[Segment]:
1552
- """
1553
- Usa Gemini para analisar a transcrição completa e identificar os melhores trechos.
1554
- Processo em 2 etapas para máxima precisão.
1555
- """
1556
- if not LLM_AVAILABLE or not segments:
1557
- raise ValueError("IA não disponível ou sem segmentos para analisar")
1558
-
1559
- if progress_callback:
1560
- progress_callback("🤖 Etapa 1/3: Preparando dados para análise...")
1561
-
1562
- # Prepara a transcrição completa com índices
1563
- transcript_data = []
1564
- for i, seg in enumerate(segments):
1565
- duration_sec = (seg.end_f - seg.start_f) / FPS
1566
- transcript_data.append({
1567
- "index": i,
1568
- "timecode": seg.start_tc,
1569
- "duration_sec": round(duration_sec, 1),
1570
- "text": seg.text[:200] # Limita texto para não estourar tokens
1571
- })
1572
-
1573
- # Converte para JSON para análise estruturada
1574
- transcript_json = json.dumps(transcript_data, ensure_ascii=False, indent=2)
1575
-
1576
- if progress_callback:
1577
- progress_callback(f"🤖 Etapa 2/3: Analisando {len(segments)} segmentos com IA (pode levar 30-60s)...")
1578
-
1579
- # Prompt detalhado para análise completa
1580
- prompt = f"""Você é um especialista em edição de vídeo. Analise a transcrição e identifique os MELHORES trechos baseado no comando do usuário.
1581
-
1582
- COMANDO DO USUÁRIO:
1583
- {command}
1584
-
1585
- TRANSCRIÇÃO COMPLETA (formato JSON com index, timecode, duração e texto):
1586
- {transcript_json}
1587
-
1588
- INSTRUÇÕES:
1589
- 1. Leia o comando com atenção e identifique:
1590
- - Quantidade de cortes desejada
1591
- - Duração de cada corte (em segundos)
1592
- - Tema/assunto/palavras-chave mencionados
1593
- - Timecode de início (se mencionado)
1594
-
1595
- 2. Analise TODA a transcrição e identifique os segmentos que melhor correspondem ao comando
1596
-
1597
- 3. Para cada corte, retorne no formato JSON:
1598
- {{
1599
- "cuts": [
1600
- {{
1601
- "start_index": <índice do segmento inicial>,
1602
- "duration_seconds": <duração desejada em segundos>,
1603
- "reason": "<breve explicação de por que escolheu este trecho>"
1604
- }}
1605
- ]
1606
- }}
1607
-
1608
- IMPORTANTE:
1609
- - Seja PRECISO na identificação dos trechos
1610
- - Considere o contexto completo ao redor das palavras-chave
1611
- - Se o comando pedir "sobre X", encontre onde X é realmente discutido
1612
- - Se houver timecode, priorize começar próximo a ele
1613
- - Retorne APENAS o JSON, sem texto adicional
1614
-
1615
- Responda com o JSON:"""
1616
-
1617
- try:
1618
- response = LLM.generate_content(
1619
- prompt,
1620
- generation_config={
1621
- "temperature": 0.2,
1622
- "max_output_tokens": 2000,
1623
- }
1624
- )
1625
-
1626
- response_text = response.text.strip()
1627
-
1628
- if progress_callback:
1629
- progress_callback("🤖 Etapa 3/3: Processando resposta da IA...")
1630
-
1631
- # Extrai JSON da resposta
1632
- json_match = re.search(r'\{[\s\S]*"cuts"[\s\S]*\}', response_text)
1633
- if not json_match:
1634
- raise ValueError("IA não retornou JSON válido")
1635
-
1636
- result = json.loads(json_match.group(0))
1637
- cuts_data = result.get("cuts", [])
1638
-
1639
- if not cuts_data:
1640
- raise ValueError("IA não encontrou cortes adequados")
1641
-
1642
- # Cria os segmentos baseado na análise da IA
1643
- selected_segments = []
1644
-
1645
- for cut_info in cuts_data:
1646
- start_idx = cut_info.get("start_index", 0)
1647
- duration_sec = cut_info.get("duration_seconds", 60)
1648
- reason = cut_info.get("reason", "")
1649
-
1650
- if start_idx < 0 or start_idx >= len(segments):
1651
- continue
1652
-
1653
- start_seg = segments[start_idx]
1654
- start_frame = start_seg.start_f
1655
- duration_frames = int(duration_sec * FPS)
1656
- end_frame = start_frame + duration_frames
1657
-
1658
- # Coleta texto dos segmentos envolvidos
1659
- text_parts = [f"[IA: {reason}]"] if reason else []
1660
- for seg in segments[start_idx:]:
1661
- if seg.start_f < end_frame:
1662
- if seg.text:
1663
- text_parts.append(seg.text[:150])
1664
- else:
1665
- break
1666
-
1667
- combined_text = " [...] ".join(text_parts)[:500]
1668
-
1669
- selected_segments.append(Segment(
1670
- start_tc=frames_to_timecode(start_frame),
1671
- end_tc=frames_to_timecode(end_frame),
1672
- start_f=start_frame,
1673
- end_f=end_frame,
1674
- text=combined_text,
1675
- score=100.0
1676
- ))
1677
-
1678
- return selected_segments
1679
-
1680
- except json.JSONDecodeError as e:
1681
- raise ValueError(f"Erro ao processar resposta da IA (JSON inválido): {str(e)}\nResposta: {response_text[:300]}")
1682
- except Exception as e:
1683
- raise ValueError(f"Erro na análise da IA: {str(e)}")
1684
-
1685
-
1686
- # =========================
1687
- # Processamento com Comando Manual (sem IA)
1688
- # =========================
1689
- def manual_command_processing(segments: List[Segment], command: str) -> List[Segment]:
1690
- """
1691
- Fallback: processamento básico sem IA para comandos simples.
1692
- """
1693
- s = command.lower()
1694
-
1695
- # Extrai quantidade
1696
- count = 1
1697
- m = re.search(r'(\d+)\s*(?:cortes?|clipes?|segmentos?)', s)
1698
- if m:
1699
- count = int(m.group(1))
1700
-
1701
- # Extrai duração
1702
- duration_sec = 60
1703
- m = re.search(r'(\d+)\s*(?:segundos?|s\b)', s)
1704
- if m:
1705
- duration_sec = int(m.group(1))
1706
- else:
1707
- m = re.search(r'(\d+)\s*(?:minutos?|min\b)', s)
1708
- if m:
1709
- duration_sec = int(m.group(1)) * 60
1710
-
1711
- # Extrai timecode inicial
1712
- start_frame = 0
1713
- m = re.search(r'(?:começando|a partir de)\s+(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)', s)
1714
- if m:
1715
- try:
1716
- start_frame = parse_timecode_to_frames(m.group(1))
1717
- except:
1718
- pass
1719
-
1720
- # Cria cortes contínuos
1721
- results = []
1722
- base_frame = start_frame
1723
-
1724
- for i in range(count):
1725
- duration_frames = duration_sec * FPS
1726
- end_frame = base_frame + duration_frames
1727
-
1728
- # Coleta texto
1729
- text_parts = []
1730
- for seg in segments:
1731
- if seg.start_f >= base_frame and seg.start_f < end_frame:
1732
- if seg.text:
1733
- text_parts.append(seg.text[:100])
1734
-
1735
- combined_text = " [...] ".join(text_parts[:10])[:400]
1736
-
1737
- results.append(Segment(
1738
- start_tc=frames_to_timecode(base_frame),
1739
- end_tc=frames_to_timecode(end_frame),
1740
- start_f=base_frame,
1741
- end_f=end_frame,
1742
- text=combined_text if combined_text else f"Corte {i+1}",
1743
- score=50.0
1744
- ))
1745
-
1746
- base_frame = end_frame
1747
-
1748
- return results
1749
-
1750
-
1751
- # =========================
1752
- # Modo Automático
1753
- # =========================
1754
- def auto_score_segments(
1755
- segs: List[Segment],
1756
- num_segments: int,
1757
- custom_keywords: str,
1758
- weight_emotion: float,
1759
- weight_break: float,
1760
- weight_learn: float,
1761
- weight_viral: float
1762
- ) -> List[Segment]:
1763
- """Sistema de pontuação automática."""
1764
- emotion_words = ['medo', 'coragem', 'amor', 'ódio', 'paixão', 'alegria', 'tristeza']
1765
- break_words = ['nunca', 'de repente', 'surpreendente', 'inesperado', 'incrível']
1766
- learn_words = ['aprendi', 'descobri', 'entendi', 'percebi', 'lição']
1767
- viral_words = ['segredo', 'verdade', 'revelação', 'exclusivo', 'confissão']
1768
-
1769
- for s in segs:
1770
- score = 0.0
1771
- text = (s.text or "").lower()
1772
-
1773
- for word in emotion_words:
1774
- if word in text:
1775
- score += weight_emotion
1776
-
1777
- for word in break_words:
1778
- if word in text:
1779
- score += weight_break
1780
-
1781
- for word in learn_words:
1782
- if word in text:
1783
- score += weight_learn
1784
-
1785
- for word in viral_words:
1786
- if word in text:
1787
- score += weight_viral
1788
-
1789
- if custom_keywords:
1790
- for kw in custom_keywords.split(","):
1791
- kw_clean = kw.strip().lower()
1792
- if kw_clean and kw_clean in text:
1793
- score += 5.0
1794
-
1795
- s.score = score
1796
-
1797
- segs.sort(key=lambda x: x.score, reverse=True)
1798
- return segs[:num_segments]
1799
-
1800
-
1801
- # =========================
1802
- # Edição de XML
1803
- # =========================
1804
- def deep_copy_element(elem: ET.Element) -> ET.Element:
1805
- new = ET.Element(elem.tag, attrib=dict(elem.attrib))
1806
- new.text = elem.text
1807
- new.tail = elem.tail
1808
- for child in elem:
1809
- new.append(deep_copy_element(child))
1810
- return new
1811
-
1812
-
1813
- def edit_xml(tree: ET.ElementTree, segs: List[Segment]) -> ET.ElementTree:
1814
- root = tree.getroot()
1815
- seq = root.find(".//sequence")
1816
- if seq is None:
1817
- raise ValueError("Sequence não encontrada no XML")
1818
-
1819
- v_track = seq.find(".//media/video/track")
1820
- a_track = seq.find(".//media/audio/track")
1821
- if not v_track or not a_track:
1822
- raise ValueError("Trilhas de vídeo/áudio não encontradas")
1823
-
1824
- v_template = v_track.find("./clipitem")
1825
- a_template = a_track.find("./clipitem")
1826
-
1827
- for clip in list(v_track.findall("./clipitem")):
1828
- v_track.remove(clip)
1829
- for clip in list(a_track.findall("./clipitem")):
1830
- a_track.remove(clip)
1831
-
1832
- timeline_pos = 0
1833
- for i, seg in enumerate(segs, 1):
1834
- duration = seg.end_f - seg.start_f
1835
- if duration <= 0:
1836
- continue
1837
-
1838
- v_clip = ET.Element("clipitem", {"id": f"clip-v{i}"})
1839
- ET.SubElement(v_clip, "name").text = f"Clip {i}"
1840
- ET.SubElement(v_clip, "start").text = str(timeline_pos)
1841
- ET.SubElement(v_clip, "end").text = str(timeline_pos + duration)
1842
- ET.SubElement(v_clip, "in").text = str(seg.start_f)
1843
- ET.SubElement(v_clip, "out").text = str(seg.end_f)
1844
-
1845
- if v_template is not None:
1846
- rate = v_template.find("rate")
1847
- if rate is not None:
1848
- v_clip.append(deep_copy_element(rate))
1849
- file_elem = v_template.find("file")
1850
- if file_elem is not None:
1851
- v_clip.append(deep_copy_element(file_elem))
1852
-
1853
- a_clip = ET.Element("clipitem", {"id": f"clip-a{i}"})
1854
- ET.SubElement(a_clip, "name").text = f"Clip {i}"
1855
- ET.SubElement(a_clip, "start").text = str(timeline_pos)
1856
- ET.SubElement(a_clip, "end").text = str(timeline_pos + duration)
1857
- ET.SubElement(a_clip, "in").text = str(seg.start_f)
1858
- ET.SubElement(a_clip, "out").text = str(seg.end_f)
1859
-
1860
- if a_template is not None:
1861
- rate = a_template.find("rate")
1862
- if rate is not None:
1863
- a_clip.append(deep_copy_element(rate))
1864
- file_elem = a_template.find("file")
1865
- if file_elem is not None:
1866
- a_clip.append(deep_copy_element(file_elem))
1867
-
1868
- v_track.append(v_clip)
1869
- a_track.append(a_clip)
1870
- timeline_pos += duration
1871
-
1872
- return tree
1873
-
1874
-
1875
- # =========================
1876
- # Seleção (orquestração)
1877
- # =========================
1878
- def select_segments(
1879
- transcript_txt: str,
1880
- use_llm: bool,
1881
- num_segments: int,
1882
- custom_keywords: str,
1883
- manual_timecodes: str,
1884
- natural_instructions: str,
1885
- weight_emotion: float,
1886
- weight_break: float,
1887
- weight_learn: float,
1888
- weight_viral: float,
1889
- progress_callback=None
1890
- ) -> List[Segment]:
1891
-
1892
- # 1) Manual
1893
- manual = parse_manual_timecodes(manual_timecodes)
1894
- if manual:
1895
- result = []
1896
- for start_tc, end_tc in manual:
1897
- try:
1898
- result.append(Segment(
1899
- start_tc=frames_to_timecode(parse_timecode_to_frames(start_tc)),
1900
- end_tc=frames_to_timecode(parse_timecode_to_frames(end_tc)),
1901
- start_f=parse_timecode_to_frames(start_tc),
1902
- end_f=parse_timecode_to_frames(end_tc),
1903
- text=f"Manual: {start_tc} - {end_tc}",
1904
- score=100.0
1905
- ))
1906
- except Exception:
1907
- pass
1908
- return result
1909
-
1910
- # 2) Parser de transcrição
1911
- segs = parse_transcript(transcript_txt) if transcript_txt else []
1912
-
1913
- # 3) Linguagem natural COM IA
1914
- if natural_instructions.strip():
1915
- if use_llm and LLM_AVAILABLE and segs:
1916
- # USA IA PARA ANÁLISE COMPLETA
1917
- return ai_analyze_and_select(segs, natural_instructions, progress_callback)
1918
- elif segs:
1919
- # Fallback sem IA
1920
- return manual_command_processing(segs, natural_instructions)
1921
- else:
1922
- raise ValueError("Para usar comandos em linguagem natural, forneça uma transcrição ou ative as minutagens manuais.")
1923
-
1924
- # 4) Automático
1925
- if not segs:
1926
- raise ValueError("Nenhum segmento encontrado. Forneça uma transcrição, minutagens ou um comando em linguagem natural.")
1927
- return auto_score_segments(
1928
- segs, num_segments, custom_keywords,
1929
- weight_emotion, weight_break, weight_learn, weight_viral
1930
- )
1931
-
1932
-
1933
- # =========================
1934
- # Pipeline principal
1935
- # =========================
1936
- def process_files(
1937
- xml_file, txt_file, use_llm, num_segments,
1938
- custom_keywords, manual_timecodes, natural_instructions,
1939
- weight_emotion, weight_break, weight_learn, weight_viral,
1940
- progress=gr.Progress()
1941
- ):
1942
- if not xml_file:
1943
- return "⚠️ Envie o XML do Premiere", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1944
-
1945
- try:
1946
- debug_info = []
1947
-
1948
- def progress_callback(msg):
1949
- progress(0.5, desc=msg)
1950
- debug_info.append(msg)
1951
-
1952
- progress(0.1, desc="📂 Carregando arquivos...")
1953
-
1954
- transcript = ""
1955
- manual = parse_manual_timecodes(manual_timecodes)
1956
-
1957
- if not manual and txt_file:
1958
- with open(txt_file.name, "r", encoding="utf-8-sig") as f:
1959
- transcript = f.read()
1960
- debug_info.append(f"📄 Transcrição: {len(transcript)} caracteres")
1961
-
1962
- progress(0.2, desc="🔍 Selecionando segmentos...")
1963
-
1964
- segments = select_segments(
1965
- transcript, use_llm and LLM_AVAILABLE, num_segments,
1966
- custom_keywords, manual_timecodes, natural_instructions,
1967
- weight_emotion, weight_break, weight_learn, weight_viral,
1968
- progress_callback
1969
- )
1970
-
1971
- if not segments:
1972
- return "⚠️ Nenhum segmento selecionado", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1973
-
1974
- valid_segments = []
1975
- for seg in segments:
1976
- if seg.end_f > seg.start_f and seg.end_f - seg.start_f >= FPS:
1977
- valid_segments.append(seg)
1978
-
1979
- if not valid_segments:
1980
- return "⚠️ Segmentos inválidos (duração muito curta)", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1981
-
1982
- segments = valid_segments
1983
- debug_info.append(f"✓ {len(segments)} segmento(s) válido(s)")
1984
-
1985
- progress(0.7, desc="✂️ Editando XML...")
1986
-
1987
- tree = ET.parse(xml_file.name)
1988
- tree = edit_xml(tree, segments)
1989
-
1990
- basename = os.path.splitext(os.path.basename(xml_file.name))[0]
1991
- output = os.path.join(OUTPUT_DIR, f"{basename}_EDITADO.xml")
1992
- tree.write(output, encoding="utf-8", xml_declaration=True)
1993
-
1994
- progress(0.9, desc="📊 Gerando resumo...")
1995
-
1996
- total_sec = sum((s.end_f - s.start_f) / FPS for s in segments)
1997
- total_min = total_sec / 60.0
1998
-
1999
- if manual:
2000
- mode = "🎯 MANUAL"
2001
- elif natural_instructions.strip() and use_llm and LLM_AVAILABLE:
2002
- mode = "🤖 IA COMPLETA (Gemini)"
2003
- elif natural_instructions.strip():
2004
- mode = "📐 BÁSICO (sem IA)"
2005
- else:
2006
- mode = "⚙️ AUTOMÁTICO"
2007
-
2008
- summary_lines = [
2009
- "═" * 70,
2010
- f"✨ RESULTADO: {len(segments)} corte(s) | {total_min:.1f} min total",
2011
- f"📊 Modo: {mode}",
2012
- "═" * 70,
2013
- ""
2014
- ]
2015
-
2016
- for i, seg in enumerate(segments, 1):
2017
- dur_sec = (seg.end_f - seg.start_f) / FPS
2018
- dur_min = dur_sec / 60.0
2019
-
2020
- line = f"🎬 Corte {i}:"
2021
- line += f"\n ⏱️ {seg.start_tc} → {seg.end_tc} ({dur_min:.2f} min / {dur_sec:.0f}s)"
2022
-
2023
- if seg.text and len(seg.text.strip()) > 10:
2024
- text_preview = seg.text[:200].strip()
2025
- if len(seg.text) > 200:
2026
- text_preview += "..."
2027
- line += f"\n 💬 {text_preview}"
2028
-
2029
- summary_lines.append(line)
2030
- summary_lines.append("")
2031
-
2032
- if debug_info:
2033
- summary_lines.append("═" * 70)
2034
- summary_lines.append("🔍 Log do Processamento:")
2035
- summary_lines.extend(f" {info}" for info in debug_info)
2036
-
2037
- summary = "\n".join(summary_lines)
2038
- status = f"✅ Sucesso | {mode} | {total_min:.1f} min | LLM: {'✓' if LLM_AVAILABLE else '✗'}"
2039
-
2040
- progress(1.0, desc="✅ Concluído!")
2041
- return summary, output, status
2042
-
2043
- except Exception as e:
2044
- import traceback
2045
- error_trace = traceback.format_exc()
2046
- print(error_trace)
2047
-
2048
- error_msg = f"❌ Erro: {str(e)}\n\n🔍 Detalhes:\n{error_trace[:800]}"
2049
- return error_msg, None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
2050
-
2051
-
2052
- # =========================
2053
- # Interface Gradio
2054
- # =========================
2055
- with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere - IA") as demo:
2056
- gr.Markdown("# 🎬 Editor XML Premiere - IA Completa (Gemini)")
2057
- gr.Markdown("Sistema que **REALMENTE ENTENDE** seu comando usando análise completa com IA.")
2058
-
2059
- status_inicial = f"{'🟢 IA Gemini Ativa - Análise Completa Habilitada' if LLM_AVAILABLE else '🔴 IA Desabilitada - Configure GEMINI_API_KEY para análise inteligente'}"
2060
- gr.Markdown(f"**Status:** {status_inicial}")
2061
-
2062
- if LLM_AVAILABLE:
2063
- gr.Markdown("""
2064
- ### 🚀 Como funciona a IA:
2065
- 1. **Você descreve** o que quer em linguagem natural
2066
- 2. **IA analisa** toda a transcrição (pode levar 30-60s)
2067
- 3. **IA identifica** os trechos exatos que correspondem ao seu pedido
2068
- 4. **Sistema cria** os cortes precisos automaticamente
2069
-
2070
- ⚡ **Mais lento, mas MUITO mais preciso!**
2071
- """)
2072
- else:
2073
- gr.Markdown("""
2074
- ### ⚠️ IA Desabilitada
2075
- Configure a variável de ambiente `GEMINI_API_KEY` para ativar análise inteligente.
2076
- No modo básico, apenas comandos simples e timecodes manuais funcionam bem.
2077
- """)
2078
-
2079
- with gr.Row():
2080
- xml_in = gr.File(label="📄 XML do Premiere", file_types=[".xml"])
2081
- txt_in = gr.File(label="📝 Transcrição (.txt) - OBRIGATÓRIA para IA", file_types=[".txt"])
2082
-
2083
- with gr.Row():
2084
- use_llm = gr.Checkbox(
2085
- label="🤖 Usar IA Gemini (análise completa - RECOMENDADO)",
2086
- value=USE_LLM_DEFAULT and LLM_AVAILABLE,
2087
- interactive=LLM_AVAILABLE,
2088
- info="Quando ativo, a IA lê TODA a transcrição e encontra os melhores trechos"
2089
- )
2090
- num_segments = gr.Slider(2, 20, 5, 1, label="📊 Segmentos (apenas modo automático)")
2091
-
2092
- with gr.Accordion("💬 Comando em Linguagem Natural (MODO PRINCIPAL)", open=True):
2093
- gr.Markdown("""
2094
- ### ✨ Exemplos de comandos que a IA entende:
2095
-
2096
- **📌 Simples:**
2097
- - "Crie 3 cortes de 30 segundos sobre futebol"
2098
- - "Quero 2 clipes de 1 minuto falando sobre Maria"
2099
- - "Faça 5 cortes de 45s sobre o tema educação"
2100
-
2101
- **🎯 Específicos:**
2102
- - "1 corte de 10 minutos da parte onde ele fala sobre a infância"
2103
- - "3 cortes de 30s sobre os momentos engraçados"
2104
- - "2 clipes de 1min sobre superação e disciplina"
2105
-
2106
- **📍 Com timecode:**
2107
- - "Corte de 5 minutos começando em 00:02:00:00 sobre tecnologia"
2108
- - "3 cortes de 45s a partir de 00:10:00 falando sobre amor"
2109
-
2110
- **🔍 Busca temática:**
2111
- - "Os melhores momentos sobre família, cada um com 40s"
2112
- - "Trechos emocionantes de 1 minuto cada"
2113
- - "Partes onde menciona desafios e conquistas"
2114
-
2115
- ### 💡 Dicas para melhores resultados:
2116
- - ✅ Seja específico sobre o tema/assunto
2117
- - ✅ Especifique duração e quantidade
2118
- - ✅ Use a transcrição completa
2119
- - ✅ Deixe a IA trabalhar (30-60s de análise)
2120
- - ❌ Evite comandos vagos como "faça algo legal"
2121
- """)
2122
- natural_instructions = gr.Textbox(
2123
- label="Digite seu comando aqui",
2124
- placeholder='Ex: "Crie 3 cortes de 45 segundos sobre os momentos onde ele fala de disciplina e superação"',
2125
- lines=4
2126
- )
2127
-
2128
- with gr.Accordion("🎯 Minutagens Manuais (precisão total)", open=False):
2129
- gr.Markdown("Use quando souber exatamente os timecodes. Ignora IA e outros modos.")
2130
- manual_timecodes = gr.Textbox(
2131
- label="Timecodes (um por linha)",
2132
- placeholder="00:21:18:09 - 00:31:18:09\n00:45:20:15 - 00:50:10:22",
2133
- lines=4
2134
- )
2135
-
2136
- with gr.Accordion("⚙️ Modo Automático (sem comando)", open=False):
2137
- gr.Markdown("Sistema de pontuação simples. **Não recomendado** - use comandos em linguagem natural.")
2138
- custom_keywords = gr.Textbox(
2139
- label="Palavras-chave (separadas por vírgula)",
2140
- placeholder="coragem, superação, vitória"
2141
- )
2142
- with gr.Row():
2143
- weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="⚡ Peso: emoção")
2144
- weight_break = gr.Slider(0, 5, 1.5, 0.1, label="💥 Peso: quebra")
2145
- with gr.Row():
2146
- weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="🎓 Peso: aprendizado")
2147
- weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="🔥 Peso: viral")
2148
-
2149
- btn = gr.Button("🚀 Processar com IA (pode levar 30-60s)", variant="primary", size="lg")
2150
-
2151
- with gr.Row():
2152
- with gr.Column(scale=2):
2153
- summary_out = gr.Textbox(label="📋 Resumo dos Cortes", lines=20, max_lines=30)
2154
- with gr.Column(scale=1):
2155
- status_out = gr.Textbox(label="📊 Status", lines=3)
2156
- file_out = gr.File(label="⬇️ Download XML Editado")
2157
-
2158
- btn.click(
2159
- process_files,
2160
- [xml_in, txt_in, use_llm, num_segments, custom_keywords,
2161
- manual_timecodes, natural_instructions,
2162
- weight_emotion, weight_break, weight_learn, weight_viral],
2163
- [summary_out, file_out, status_out]
2164
- )
2165
-
2166
- gr.Markdown("""
2167
- ---
2168
- ### 📚 Guia Rápido:
2169
-
2170
- **🎯 Para melhores resultados:**
2171
- 1. ✅ Envie XML + Transcrição completa
2172
- 2. ✅ Ative a IA (checkbox)
2173
- 3. ✅ Escreva comando claro e específico
2174
- 4. ✅ Aguarde 30-60s para análise completa
2175
- 5. ✅ Baixe e importe no Premiere
2176
-
2177
- **⚡ Ordem de prioridade:**
2178
- 1. **Minutagens Manuais** (ignora tudo, máxima precisão)
2179
- 2. **Comando + IA** (análise completa, muito preciso)
2180
- 3. **Comando sem IA** (básico, menos preciso)
2181
- 4. **Modo Automático** (não recomendado)
2182
-
2183
- **🔧 Troubleshooting:**
2184
- - Erro "IA não disponível": Configure `GEMINI_API_KEY`
2185
- - Cortes errados: Seja mais específico no comando
2186
- - Demora muito: Normal para IA completa (30-60s)
2187
- - Sem transcrição: Use minutagens manuais
2188
- """)
2189
-
2190
- if __name__ == "__main__":
2191
- demo.launch(), nxt) and (j + 1 < len(lines) and arrow.search(lines[j + 1])):
2192
  break
2193
  if arrow.search(nxt):
2194
  break
@@ -2197,12 +173,12 @@ if __name__ == "__main__":
2197
 
2198
  text = " ".join(text_parts).strip()
2199
  try:
2200
- sf = parse_timecode_to_frames(start_tc, fps)
2201
- ef = parse_timecode_to_frames(end_tc, fps)
2202
  if ef > sf:
2203
  results.append(Segment(
2204
- start_tc=frames_to_timecode(sf, fps),
2205
- end_tc=frames_to_timecode(ef, fps),
2206
  start_f=sf,
2207
  end_f=ef,
2208
  text=text,
 
1
+ import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import re
3
  import json
4
  import xml.etree.ElementTree as ET
 
9
  # =========================
10
  # Configurações Gerais
11
  # =========================
12
+ FPS = 24
13
  OUTPUT_DIR = "./Output"
14
  os.makedirs(OUTPUT_DIR, exist_ok=True)
15
 
 
50
  # =========================
51
  # Funções de Timecode
52
  # =========================
53
+ def _tc_to_hmsf(tc: str, fps: int = FPS) -> Tuple[int, int, int, int]:
54
  """Converte timecode para (hh, mm, ss, ff)."""
55
  s = tc.strip()
56
 
 
76
  raise ValueError(f"Timecode inválido: {tc}")
77
 
78
 
79
+ def parse_timecode_to_frames(tc: str, fps: int = FPS) -> int:
80
  hh, mm, ss, ff = _tc_to_hmsf(tc, fps)
81
  return hh * 3600 * fps + mm * 60 * fps + ss * fps + ff
82
 
83
 
84
+ def frames_to_timecode(frames: int, fps: int = FPS) -> str:
85
  hh = frames // (3600 * fps)
86
  rem = frames % (3600 * fps)
87
  mm = rem // (60 * fps)
 
94
  # =========================
95
  # Parser de Transcrição
96
  # =========================
97
+ def parse_transcript(txt: str) -> List[Segment]:
98
  """Parser robusto para múltiplos formatos."""
99
  if not txt or not txt.strip():
100
  return []
 
104
 
105
  line_range = re.compile(
106
  r'^\s*\[?\s*(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*[-—–]\s*'
107
+ r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*\]?\s*(.*)$'
108
+ )
109
+ arrow = re.compile(
110
+ r'(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3}|[:;]\d{2})?)\s*-->\s*'
111
+ r'(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3}|[:;]\d{2})?)'
112
+ )
113
 
114
+ i = 0
115
+ while i < len(lines):
116
+ raw = lines[i].strip()
117
+ if not raw or raw.lower() == "desconhecido":
118
+ i += 1
119
+ continue
120
 
121
+ m = line_range.match(raw)
 
 
 
 
122
  if m:
123
+ start_tc, end_tc, trailing_text = m.groups()
124
+ text_parts = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ if trailing_text.strip():
127
+ text_parts.append(trailing_text.strip())
128
+ else:
129
+ j = i + 1
130
+ while j < len(lines):
131
+ nxt = lines[j].strip()
132
+ if not nxt or line_range.match(nxt) or re.match(r'^\d+\s*$', nxt) or arrow.search(nxt):
133
+ break
134
+ text_parts.append(nxt)
135
+ j += 1
136
+ i = j - 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ text = " ".join(text_parts).strip()
139
+ try:
140
+ sf = parse_timecode_to_frames(start_tc)
141
+ ef = parse_timecode_to_frames(end_tc)
142
+ if ef > sf:
143
+ results.append(Segment(
144
+ start_tc=frames_to_timecode(sf),
145
+ end_tc=frames_to_timecode(ef),
146
+ start_f=sf,
147
+ end_f=ef,
148
+ text=text if text else f"{start_tc} - {end_tc}",
149
+ score=0.0
150
+ ))
151
+ except Exception:
152
+ pass
153
+ i += 1
154
+ continue
155
 
156
+ if arrow.search(raw) or (i + 1 < len(lines) and arrow.search(lines[i + 1])):
157
+ line_with_tc = raw if arrow.search(raw) else lines[i + 1]
158
+ mm = arrow.search(line_with_tc)
159
+ if mm:
160
+ start_tc, end_tc = mm.groups()
161
+ j = i + 1 if line_with_tc == raw else i + 2
162
+ text_parts = []
163
+ while j < len(lines):
164
+ nxt = lines[j].strip()
165
+ if not nxt:
166
+ break
167
+ if re.match(r'^\d+\s*$', nxt) and (j + 1 < len(lines) and arrow.search(lines[j + 1])):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  break
169
  if arrow.search(nxt):
170
  break
 
173
 
174
  text = " ".join(text_parts).strip()
175
  try:
176
+ sf = parse_timecode_to_frames(start_tc)
177
+ ef = parse_timecode_to_frames(end_tc)
178
  if ef > sf:
179
  results.append(Segment(
180
+ start_tc=frames_to_timecode(sf),
181
+ end_tc=frames_to_timecode(ef),
182
  start_f=sf,
183
  end_f=ef,
184
  text=text,