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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +193 -261
app.py CHANGED
@@ -3,13 +3,12 @@ import re
3
  import json
4
  import xml.etree.ElementTree as ET
5
  from dataclasses import dataclass
6
- from typing import List, Tuple, Optional
7
  import gradio as gr
8
 
9
  # =========================
10
  # Configurações Gerais
11
  # =========================
12
- FPS = 24
13
  OUTPUT_DIR = "./Output"
14
  os.makedirs(OUTPUT_DIR, exist_ok=True)
15
 
@@ -50,7 +49,7 @@ class Segment:
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,12 +75,12 @@ def _tc_to_hmsf(tc: str, fps: int = FPS) -> Tuple[int, int, int, int]:
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,8 +93,8 @@ def frames_to_timecode(frames: int, fps: int = FPS) -> str:
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 []
101
 
@@ -137,12 +136,12 @@ def parse_transcript(txt: str) -> List[Segment]:
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}",
@@ -153,6 +152,7 @@ def parse_transcript(txt: str) -> List[Segment]:
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)
@@ -173,12 +173,12 @@ def parse_transcript(txt: str) -> List[Segment]:
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,
@@ -204,7 +204,10 @@ def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
204
 
205
  manual_ranges = []
206
  lines = manual_input.replace(",", "\n").splitlines()
207
- 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})?)')
 
 
 
208
  for line in lines:
209
  m = pattern.search(line.strip())
210
  if m:
@@ -215,35 +218,38 @@ def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
215
  # =========================
216
  # IA: Análise Inteligente com Gemini
217
  # =========================
218
- def ai_analyze_and_select(segments: List[Segment], command: str, progress_callback=None) -> List[Segment]:
 
 
 
 
 
219
  """
220
  Usa Gemini para analisar a transcrição completa e identificar os melhores trechos.
221
  Processo em 2 etapas para máxima precisão.
222
  """
223
  if not LLM_AVAILABLE or not segments:
224
  raise ValueError("IA não disponível ou sem segmentos para analisar")
225
-
226
  if progress_callback:
227
- progress_callback("🤖 Etapa 1/3: Preparando dados para análise...")
228
-
229
  # Prepara a transcrição completa com índices
230
  transcript_data = []
231
  for i, seg in enumerate(segments):
232
- duration_sec = (seg.end_f - seg.start_f) / FPS
233
  transcript_data.append({
234
  "index": i,
235
  "timecode": seg.start_tc,
236
  "duration_sec": round(duration_sec, 1),
237
- "text": seg.text[:200] # Limita texto para não estourar tokens
238
  })
239
-
240
- # Converte para JSON para análise estruturada
241
  transcript_json = json.dumps(transcript_data, ensure_ascii=False, indent=2)
242
-
243
  if progress_callback:
244
- progress_callback(f"🤖 Etapa 2/3: Analisando {len(segments)} segmentos com IA (pode levar 30-60s)...")
245
-
246
- # Prompt detalhado para análise completa
247
  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.
248
 
249
  COMANDO DO USUÁRIO:
@@ -267,19 +273,17 @@ INSTRUÇÕES:
267
  {{
268
  "start_index": <índice do segmento inicial>,
269
  "duration_seconds": <duração desejada em segundos>,
270
- "reason": "<breve explicação de por que escolheu este trecho>"
271
  }}
272
  ]
273
  }}
274
 
275
  IMPORTANTE:
276
- - Seja PRECISO na identificação dos trechos
277
- - Considere o contexto completo ao redor das palavras-chave
278
- - Se o comando pedir "sobre X", encontre onde X é realmente discutido
279
  - Se houver timecode, priorize começar próximo a ele
280
- - Retorne APENAS o JSON, sem texto adicional
281
-
282
- Responda com o JSON:"""
283
 
284
  try:
285
  response = LLM.generate_content(
@@ -289,63 +293,59 @@ Responda com o JSON:"""
289
  "max_output_tokens": 2000,
290
  }
291
  )
292
-
293
- response_text = response.text.strip()
294
-
295
  if progress_callback:
296
- progress_callback("🤖 Etapa 3/3: Processando resposta da IA...")
297
-
298
- # Extrai JSON da resposta
299
  json_match = re.search(r'\{[\s\S]*"cuts"[\s\S]*\}', response_text)
300
  if not json_match:
301
  raise ValueError("IA não retornou JSON válido")
302
-
303
  result = json.loads(json_match.group(0))
304
  cuts_data = result.get("cuts", [])
305
-
306
  if not cuts_data:
307
  raise ValueError("IA não encontrou cortes adequados")
308
-
309
- # Cria os segmentos baseado na análise da IA
310
- selected_segments = []
311
-
312
  for cut_info in cuts_data:
313
- start_idx = cut_info.get("start_index", 0)
314
- duration_sec = cut_info.get("duration_seconds", 60)
315
- reason = cut_info.get("reason", "")
316
-
317
  if start_idx < 0 or start_idx >= len(segments):
318
  continue
319
-
320
  start_seg = segments[start_idx]
321
  start_frame = start_seg.start_f
322
- duration_frames = int(duration_sec * FPS)
323
  end_frame = start_frame + duration_frames
324
-
325
  # Coleta texto dos segmentos envolvidos
326
- text_parts = [f"[IA: {reason}]"] if reason else []
327
  for seg in segments[start_idx:]:
328
  if seg.start_f < end_frame:
329
  if seg.text:
330
  text_parts.append(seg.text[:150])
331
  else:
332
  break
333
-
334
  combined_text = " [...] ".join(text_parts)[:500]
335
-
336
  selected_segments.append(Segment(
337
- start_tc=frames_to_timecode(start_frame),
338
- end_tc=frames_to_timecode(end_frame),
339
  start_f=start_frame,
340
  end_f=end_frame,
341
  text=combined_text,
342
  score=100.0
343
  ))
344
-
345
  return selected_segments
346
-
347
  except json.JSONDecodeError as e:
348
- raise ValueError(f"Erro ao processar resposta da IA (JSON inválido): {str(e)}\nResposta: {response_text[:300]}")
349
  except Exception as e:
350
  raise ValueError(f"Erro na análise da IA: {str(e)}")
351
 
@@ -353,19 +353,17 @@ Responda com o JSON:"""
353
  # =========================
354
  # Processamento com Comando Manual (sem IA)
355
  # =========================
356
- def manual_command_processing(segments: List[Segment], command: str) -> List[Segment]:
357
- """
358
- Fallback: processamento básico sem IA para comandos simples.
359
- """
360
- s = command.lower()
361
-
362
- # Extrai quantidade
363
  count = 1
364
  m = re.search(r'(\d+)\s*(?:cortes?|clipes?|segmentos?)', s)
365
  if m:
366
  count = int(m.group(1))
367
-
368
- # Extrai duração
369
  duration_sec = 60
370
  m = re.search(r'(\d+)\s*(?:segundos?|s\b)', s)
371
  if m:
@@ -374,44 +372,43 @@ def manual_command_processing(segments: List[Segment], command: str) -> List[Seg
374
  m = re.search(r'(\d+)\s*(?:minutos?|min\b)', s)
375
  if m:
376
  duration_sec = int(m.group(1)) * 60
377
-
378
- # Extrai timecode inicial
379
  start_frame = 0
380
  m = re.search(r'(?:começando|a partir de)\s+(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)', s)
381
  if m:
382
  try:
383
- start_frame = parse_timecode_to_frames(m.group(1))
384
- except:
385
  pass
386
-
387
- # Cria cortes contínuos
388
  results = []
389
  base_frame = start_frame
390
-
391
  for i in range(count):
392
- duration_frames = duration_sec * FPS
393
  end_frame = base_frame + duration_frames
394
-
395
- # Coleta texto
396
  text_parts = []
397
  for seg in segments:
398
  if seg.start_f >= base_frame and seg.start_f < end_frame:
399
  if seg.text:
400
  text_parts.append(seg.text[:100])
401
-
402
  combined_text = " [...] ".join(text_parts[:10])[:400]
403
-
404
  results.append(Segment(
405
- start_tc=frames_to_timecode(base_frame),
406
- end_tc=frames_to_timecode(end_frame),
407
  start_f=base_frame,
408
  end_f=end_frame,
409
  text=combined_text if combined_text else f"Corte {i+1}",
410
  score=50.0
411
  ))
412
-
413
  base_frame = end_frame
414
-
415
  return results
416
 
417
 
@@ -427,42 +424,42 @@ def auto_score_segments(
427
  weight_learn: float,
428
  weight_viral: float
429
  ) -> List[Segment]:
430
- """Sistema de pontuação automática."""
431
  emotion_words = ['medo', 'coragem', 'amor', 'ódio', 'paixão', 'alegria', 'tristeza']
432
  break_words = ['nunca', 'de repente', 'surpreendente', 'inesperado', 'incrível']
433
  learn_words = ['aprendi', 'descobri', 'entendi', 'percebi', 'lição']
434
  viral_words = ['segredo', 'verdade', 'revelação', 'exclusivo', 'confissão']
435
-
436
  for s in segs:
437
  score = 0.0
438
  text = (s.text or "").lower()
439
-
440
  for word in emotion_words:
441
  if word in text:
442
  score += weight_emotion
443
-
444
  for word in break_words:
445
  if word in text:
446
  score += weight_break
447
-
448
  for word in learn_words:
449
  if word in text:
450
  score += weight_learn
451
-
452
  for word in viral_words:
453
  if word in text:
454
  score += weight_viral
455
-
456
  if custom_keywords:
457
  for kw in custom_keywords.split(","):
458
  kw_clean = kw.strip().lower()
459
  if kw_clean and kw_clean in text:
460
  score += 5.0
461
-
462
  s.score = score
463
-
464
  segs.sort(key=lambda x: x.score, reverse=True)
465
- return segs[:num_segments]
466
 
467
 
468
  # =========================
@@ -553,7 +550,8 @@ def select_segments(
553
  weight_break: float,
554
  weight_learn: float,
555
  weight_viral: float,
556
- progress_callback=None
 
557
  ) -> List[Segment]:
558
 
559
  # 1) Manual
@@ -563,10 +561,10 @@ def select_segments(
563
  for start_tc, end_tc in manual:
564
  try:
565
  result.append(Segment(
566
- start_tc=frames_to_timecode(parse_timecode_to_frames(start_tc)),
567
- end_tc=frames_to_timecode(parse_timecode_to_frames(end_tc)),
568
- start_f=parse_timecode_to_frames(start_tc),
569
- end_f=parse_timecode_to_frames(end_tc),
570
  text=f"Manual: {start_tc} - {end_tc}",
571
  score=100.0
572
  ))
@@ -575,22 +573,23 @@ def select_segments(
575
  return result
576
 
577
  # 2) Parser de transcrição
578
- segs = parse_transcript(transcript_txt) if transcript_txt else []
579
 
580
- # 3) Linguagem natural COM IA
581
  if natural_instructions.strip():
582
  if use_llm and LLM_AVAILABLE and segs:
583
- # USA IA PARA ANÁLISE COMPLETA
584
- return ai_analyze_and_select(segs, natural_instructions, progress_callback)
585
  elif segs:
586
- # Fallback sem IA
587
- return manual_command_processing(segs, natural_instructions)
588
  else:
589
- raise ValueError("Para usar comandos em linguagem natural, forneça uma transcrição ou ative as minutagens manuais.")
 
 
 
590
 
591
  # 4) Automático
592
  if not segs:
593
- raise ValueError("Nenhum segmento encontrado. Forneça uma transcrição, minutagens ou um comando em linguagem natural.")
594
  return auto_score_segments(
595
  segs, num_segments, custom_keywords,
596
  weight_emotion, weight_break, weight_learn, weight_viral
@@ -604,255 +603,188 @@ def process_files(
604
  xml_file, txt_file, use_llm, num_segments,
605
  custom_keywords, manual_timecodes, natural_instructions,
606
  weight_emotion, weight_break, weight_learn, weight_viral,
 
607
  progress=gr.Progress()
608
  ):
609
  if not xml_file:
610
- return "⚠️ Envie o XML do Premiere", None, f"LLM: {'' if LLM_AVAILABLE else ''}"
611
 
612
  try:
613
  debug_info = []
614
-
615
  def progress_callback(msg):
616
  progress(0.5, desc=msg)
617
  debug_info.append(msg)
618
-
619
- progress(0.1, desc="📂 Carregando arquivos...")
620
-
621
  transcript = ""
622
  manual = parse_manual_timecodes(manual_timecodes)
623
 
624
  if not manual and txt_file:
625
  with open(txt_file.name, "r", encoding="utf-8-sig") as f:
626
  transcript = f.read()
627
- debug_info.append(f"📄 Transcrição: {len(transcript)} caracteres")
 
 
628
 
629
- progress(0.2, desc="🔍 Selecionando segmentos...")
630
-
631
  segments = select_segments(
632
- transcript, use_llm and LLM_AVAILABLE, num_segments,
633
  custom_keywords, manual_timecodes, natural_instructions,
634
- weight_emotion, weight_break, weight_learn, weight_viral,
 
635
  progress_callback
636
  )
637
 
638
  if not segments:
639
- return "⚠️ Nenhum segmento selecionado", None, f"LLM: {'' if LLM_AVAILABLE else ''}"
640
 
 
641
  valid_segments = []
642
  for seg in segments:
643
- if seg.end_f > seg.start_f and seg.end_f - seg.start_f >= FPS:
644
  valid_segments.append(seg)
645
-
646
  if not valid_segments:
647
- return "⚠️ Segmentos inválidos (duração muito curta)", None, f"LLM: {'' if LLM_AVAILABLE else ''}"
648
-
649
  segments = valid_segments
650
- debug_info.append(f"{len(segments)} segmento(s) válido(s)")
 
 
651
 
652
- progress(0.7, desc="✂️ Editando XML...")
653
-
654
  tree = ET.parse(xml_file.name)
655
  tree = edit_xml(tree, segments)
656
 
657
  basename = os.path.splitext(os.path.basename(xml_file.name))[0]
658
- output = os.path.join(OUTPUT_DIR, f"{basename}_EDITADO.xml")
659
- tree.write(output, encoding="utf-8", xml_declaration=True)
 
 
660
 
661
- progress(0.9, desc="📊 Gerando resumo...")
662
-
663
- total_sec = sum((s.end_f - s.start_f) / FPS for s in segments)
664
  total_min = total_sec / 60.0
665
-
666
  if manual:
667
- mode = "🎯 MANUAL"
668
  elif natural_instructions.strip() and use_llm and LLM_AVAILABLE:
669
- mode = "🤖 IA COMPLETA (Gemini)"
670
  elif natural_instructions.strip():
671
- mode = "📐 BÁSICO (sem IA)"
672
  else:
673
- mode = "⚙️ AUTOMÁTICO"
674
 
675
  summary_lines = [
676
- "" * 70,
677
- f" RESULTADO: {len(segments)} corte(s) | {total_min:.1f} min total",
678
- f"📊 Modo: {mode}",
679
- "═" * 70,
680
  ""
681
  ]
682
-
683
  for i, seg in enumerate(segments, 1):
684
- dur_sec = (seg.end_f - seg.start_f) / FPS
685
  dur_min = dur_sec / 60.0
686
-
687
- line = f"🎬 Corte {i}:"
688
- line += f"\n ⏱️ {seg.start_tc} → {seg.end_tc} ({dur_min:.2f} min / {dur_sec:.0f}s)"
689
-
690
  if seg.text and len(seg.text.strip()) > 10:
691
  text_preview = seg.text[:200].strip()
692
  if len(seg.text) > 200:
693
  text_preview += "..."
694
- line += f"\n 💬 {text_preview}"
695
-
696
  summary_lines.append(line)
697
  summary_lines.append("")
698
-
699
  if debug_info:
700
- summary_lines.append("═" * 70)
701
- summary_lines.append("🔍 Log do Processamento:")
702
- summary_lines.extend(f" {info}" for info in debug_info)
703
-
704
  summary = "\n".join(summary_lines)
705
- status = f"Sucesso | {mode} | {total_min:.1f} min | LLM: {'' if LLM_AVAILABLE else ''}"
706
-
707
- progress(1.0, desc="Concluído!")
708
- return summary, output, status
709
 
710
  except Exception as e:
711
  import traceback
712
  error_trace = traceback.format_exc()
713
  print(error_trace)
714
-
715
- error_msg = f"Erro: {str(e)}\n\n🔍 Detalhes:\n{error_trace[:800]}"
716
- return error_msg, None, f"LLM: {'' if LLM_AVAILABLE else ''}"
717
 
718
 
719
  # =========================
720
  # Interface Gradio
721
  # =========================
722
  with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere - IA") as demo:
723
- gr.Markdown("# 🎬 Editor XML Premiere - IA Completa (Gemini)")
724
- gr.Markdown("Sistema que **REALMENTE ENTENDE** seu comando usando análise completa com IA.")
725
-
726
- status_inicial = f"{'🟢 IA Gemini Ativa - Análise Completa Habilitada' if LLM_AVAILABLE else '🔴 IA Desabilitada - Configure GEMINI_API_KEY para análise inteligente'}"
727
- gr.Markdown(f"**Status:** {status_inicial}")
728
-
729
- if LLM_AVAILABLE:
730
- gr.Markdown("""
731
- ### 🚀 Como funciona a IA:
732
- 1. **Você descreve** o que quer em linguagem natural
733
- 2. **IA analisa** toda a transcrição (pode levar 30-60s)
734
- 3. **IA identifica** os trechos exatos que correspondem ao seu pedido
735
- 4. **Sistema cria** os cortes precisos automaticamente
736
-
737
- ⚡ **Mais lento, mas MUITO mais preciso!**
738
- """)
739
- else:
740
- gr.Markdown("""
741
- ### ⚠️ IA Desabilitada
742
- Configure a variável de ambiente `GEMINI_API_KEY` para ativar análise inteligente.
743
- No modo básico, apenas comandos simples e timecodes manuais funcionam bem.
744
- """)
745
 
746
  with gr.Row():
747
- xml_in = gr.File(label="📄 XML do Premiere", file_types=[".xml"])
748
- txt_in = gr.File(label="📝 Transcrição (.txt) - OBRIGATÓRIA para IA", file_types=[".txt"])
749
 
750
  with gr.Row():
751
  use_llm = gr.Checkbox(
752
- label="🤖 Usar IA Gemini (análise completa - RECOMENDADO)",
753
  value=USE_LLM_DEFAULT and LLM_AVAILABLE,
754
- interactive=LLM_AVAILABLE,
755
- info="Quando ativo, a IA lê TODA a transcrição e encontra os melhores trechos"
 
 
 
 
 
 
 
 
 
 
756
  )
757
- num_segments = gr.Slider(2, 20, 5, 1, label="📊 Segmentos (apenas modo automático)")
758
-
759
- with gr.Accordion("💬 Comando em Linguagem Natural (MODO PRINCIPAL)", open=True):
760
- gr.Markdown("""
761
- ### ✨ Exemplos de comandos que a IA entende:
762
-
763
- **📌 Simples:**
764
- - "Crie 3 cortes de 30 segundos sobre futebol"
765
- - "Quero 2 clipes de 1 minuto falando sobre Maria"
766
- - "Faça 5 cortes de 45s sobre o tema educação"
767
-
768
- **🎯 Específicos:**
769
- - "1 corte de 10 minutos da parte onde ele fala sobre a infância"
770
- - "3 cortes de 30s sobre os momentos engraçados"
771
- - "2 clipes de 1min sobre superação e disciplina"
772
-
773
- **📍 Com timecode:**
774
- - "Corte de 5 minutos começando em 00:02:00:00 sobre tecnologia"
775
- - "3 cortes de 45s a partir de 00:10:00 falando sobre amor"
776
-
777
- **🔍 Busca temática:**
778
- - "Os melhores momentos sobre família, cada um com 40s"
779
- - "Trechos emocionantes de 1 minuto cada"
780
- - "Partes onde menciona desafios e conquistas"
781
-
782
- ### 💡 Dicas para melhores resultados:
783
- - ✅ Seja específico sobre o tema/assunto
784
- - ✅ Especifique duração e quantidade
785
- - ✅ Use a transcrição completa
786
- - ✅ Deixe a IA trabalhar (30-60s de análise)
787
- - ❌ Evite comandos vagos como "faça algo legal"
788
- """)
789
  natural_instructions = gr.Textbox(
790
- label="Digite seu comando aqui",
791
- placeholder='Ex: "Crie 3 cortes de 45 segundos sobre os momentos onde ele fala de disciplina e superação"',
792
  lines=4
793
  )
794
 
795
- with gr.Accordion("🎯 Minutagens Manuais (precisão total)", open=False):
796
- gr.Markdown("Use quando souber exatamente os timecodes. Ignora IA e outros modos.")
797
  manual_timecodes = gr.Textbox(
798
  label="Timecodes (um por linha)",
799
  placeholder="00:21:18:09 - 00:31:18:09\n00:45:20:15 - 00:50:10:22",
800
  lines=4
801
  )
802
 
803
- with gr.Accordion("⚙️ Modo Automático (sem comando)", open=False):
804
- gr.Markdown("Sistema de pontuação simples. **Não recomendado** - use comandos em linguagem natural.")
805
  custom_keywords = gr.Textbox(
806
  label="Palavras-chave (separadas por vírgula)",
807
  placeholder="coragem, superação, vitória"
808
  )
809
  with gr.Row():
810
- weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="Peso: emoção")
811
- weight_break = gr.Slider(0, 5, 1.5, 0.1, label="💥 Peso: quebra")
812
  with gr.Row():
813
- weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="🎓 Peso: aprendizado")
814
- weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="🔥 Peso: viral")
815
 
816
- btn = gr.Button("🚀 Processar com IA (pode levar 30-60s)", variant="primary", size="lg")
817
 
818
  with gr.Row():
819
  with gr.Column(scale=2):
820
- summary_out = gr.Textbox(label="📋 Resumo dos Cortes", lines=20, max_lines=30)
821
  with gr.Column(scale=1):
822
- status_out = gr.Textbox(label="📊 Status", lines=3)
823
- file_out = gr.File(label="⬇️ Download XML Editado")
824
 
825
  btn.click(
826
  process_files,
827
  [xml_in, txt_in, use_llm, num_segments, custom_keywords,
828
  manual_timecodes, natural_instructions,
829
- weight_emotion, weight_break, weight_learn, weight_viral],
830
  [summary_out, file_out, status_out]
831
  )
832
-
833
- gr.Markdown("""
834
- ---
835
- ### 📚 Guia Rápido:
836
-
837
- **🎯 Para melhores resultados:**
838
- 1. ✅ Envie XML + Transcrição completa
839
- 2. ✅ Ative a IA (checkbox)
840
- 3. ✅ Escreva comando claro e específico
841
- 4. ✅ Aguarde 30-60s para análise completa
842
- 5. ✅ Baixe e importe no Premiere
843
-
844
- **⚡ Ordem de prioridade:**
845
- 1. **Minutagens Manuais** (ignora tudo, máxima precisão)
846
- 2. **Comando + IA** (análise completa, muito preciso)
847
- 3. **Comando sem IA** (básico, menos preciso)
848
- 4. **Modo Automático** (não recomendado)
849
-
850
- **🔧 Troubleshooting:**
851
- - Erro "IA não disponível": Configure `GEMINI_API_KEY`
852
- - Cortes errados: Seja mais específico no comando
853
- - Demora muito: Normal para IA completa (30-60s)
854
- - Sem transcrição: Use minutagens manuais
855
- """)
856
 
857
  if __name__ == "__main__":
858
- demo.launch()
 
3
  import json
4
  import xml.etree.ElementTree as ET
5
  from dataclasses import dataclass
6
+ from typing import List, Tuple, Optional, Callable
7
  import gradio as gr
8
 
9
  # =========================
10
  # Configurações Gerais
11
  # =========================
 
12
  OUTPUT_DIR = "./Output"
13
  os.makedirs(OUTPUT_DIR, exist_ok=True)
14
 
 
49
  # =========================
50
  # Funções de Timecode
51
  # =========================
52
+ def _tc_to_hmsf(tc: str, fps: int) -> Tuple[int, int, int, int]:
53
  """Converte timecode para (hh, mm, ss, ff)."""
54
  s = tc.strip()
55
 
 
75
  raise ValueError(f"Timecode inválido: {tc}")
76
 
77
 
78
+ def parse_timecode_to_frames(tc: str, fps: int) -> int:
79
  hh, mm, ss, ff = _tc_to_hmsf(tc, fps)
80
  return hh * 3600 * fps + mm * 60 * fps + ss * fps + ff
81
 
82
 
83
+ def frames_to_timecode(frames: int, fps: int) -> str:
84
  hh = frames // (3600 * fps)
85
  rem = frames % (3600 * fps)
86
  mm = rem // (60 * fps)
 
93
  # =========================
94
  # Parser de Transcrição
95
  # =========================
96
+ def parse_transcript(txt: str, fps: int) -> List[Segment]:
97
+ """Parser robusto para múltiplos formatos (intervalos e WEBVTT/SRT)."""
98
  if not txt or not txt.strip():
99
  return []
100
 
 
136
 
137
  text = " ".join(text_parts).strip()
138
  try:
139
+ sf = parse_timecode_to_frames(start_tc, fps)
140
+ ef = parse_timecode_to_frames(end_tc, fps)
141
  if ef > sf:
142
  results.append(Segment(
143
+ start_tc=frames_to_timecode(sf, fps),
144
+ end_tc=frames_to_timecode(ef, fps),
145
  start_f=sf,
146
  end_f=ef,
147
  text=text if text else f"{start_tc} - {end_tc}",
 
152
  i += 1
153
  continue
154
 
155
+ # Bloco estilo VTT/SRT: "00:00:01,000 --> 00:00:03,000"
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)
 
173
 
174
  text = " ".join(text_parts).strip()
175
  try:
176
+ sf = parse_timecode_to_frames(start_tc, fps)
177
+ ef = parse_timecode_to_frames(end_tc, fps)
178
  if ef > sf:
179
  results.append(Segment(
180
+ start_tc=frames_to_timecode(sf, fps),
181
+ end_tc=frames_to_timecode(ef, fps),
182
  start_f=sf,
183
  end_f=ef,
184
  text=text,
 
204
 
205
  manual_ranges = []
206
  lines = manual_input.replace(",", "\n").splitlines()
207
+ pattern = re.compile(
208
+ r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*[-–—]\s*'
209
+ r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)'
210
+ )
211
  for line in lines:
212
  m = pattern.search(line.strip())
213
  if m:
 
218
  # =========================
219
  # IA: Análise Inteligente com Gemini
220
  # =========================
221
+ def ai_analyze_and_select(
222
+ segments: List[Segment],
223
+ command: str,
224
+ fps: int,
225
+ progress_callback: Optional[Callable[[str], None]] = None
226
+ ) -> List[Segment]:
227
  """
228
  Usa Gemini para analisar a transcrição completa e identificar os melhores trechos.
229
  Processo em 2 etapas para máxima precisão.
230
  """
231
  if not LLM_AVAILABLE or not segments:
232
  raise ValueError("IA não disponível ou sem segmentos para analisar")
233
+
234
  if progress_callback:
235
+ progress_callback("Etapa 1/3: preparando dados para análise...")
236
+
237
  # Prepara a transcrição completa com índices
238
  transcript_data = []
239
  for i, seg in enumerate(segments):
240
+ duration_sec = max(0, (seg.end_f - seg.start_f) / fps)
241
  transcript_data.append({
242
  "index": i,
243
  "timecode": seg.start_tc,
244
  "duration_sec": round(duration_sec, 1),
245
+ "text": (seg.text or "")[:200] # Limita texto para não estourar tokens
246
  })
247
+
 
248
  transcript_json = json.dumps(transcript_data, ensure_ascii=False, indent=2)
249
+
250
  if progress_callback:
251
+ progress_callback(f"Etapa 2/3: analisando {len(segments)} segmentos com IA...")
252
+
 
253
  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.
254
 
255
  COMANDO DO USUÁRIO:
 
273
  {{
274
  "start_index": <índice do segmento inicial>,
275
  "duration_seconds": <duração desejada em segundos>,
276
+ "reason": "<breve explicação>"
277
  }}
278
  ]
279
  }}
280
 
281
  IMPORTANTE:
282
+ - Seja preciso na identificação dos trechos
283
+ - Considere o contexto completo
 
284
  - Se houver timecode, priorize começar próximo a ele
285
+ - Responda apenas com o JSON
286
+ """
 
287
 
288
  try:
289
  response = LLM.generate_content(
 
293
  "max_output_tokens": 2000,
294
  }
295
  )
296
+ response_text = (response.text or "").strip()
297
+
 
298
  if progress_callback:
299
+ progress_callback("Etapa 3/3: processando resposta da IA...")
300
+
 
301
  json_match = re.search(r'\{[\s\S]*"cuts"[\s\S]*\}', response_text)
302
  if not json_match:
303
  raise ValueError("IA não retornou JSON válido")
304
+
305
  result = json.loads(json_match.group(0))
306
  cuts_data = result.get("cuts", [])
 
307
  if not cuts_data:
308
  raise ValueError("IA não encontrou cortes adequados")
309
+
310
+ selected_segments: List[Segment] = []
311
+
 
312
  for cut_info in cuts_data:
313
+ start_idx = int(cut_info.get("start_index", 0))
314
+ duration_sec = int(cut_info.get("duration_seconds", 60))
315
+ reason = str(cut_info.get("reason", "")).strip()
316
+
317
  if start_idx < 0 or start_idx >= len(segments):
318
  continue
319
+
320
  start_seg = segments[start_idx]
321
  start_frame = start_seg.start_f
322
+ duration_frames = max(0, int(duration_sec * fps))
323
  end_frame = start_frame + duration_frames
324
+
325
  # Coleta texto dos segmentos envolvidos
326
+ text_parts = [f"[IA] {reason}"] if reason else []
327
  for seg in segments[start_idx:]:
328
  if seg.start_f < end_frame:
329
  if seg.text:
330
  text_parts.append(seg.text[:150])
331
  else:
332
  break
333
+
334
  combined_text = " [...] ".join(text_parts)[:500]
335
+
336
  selected_segments.append(Segment(
337
+ start_tc=frames_to_timecode(start_frame, fps),
338
+ end_tc=frames_to_timecode(end_frame, fps),
339
  start_f=start_frame,
340
  end_f=end_frame,
341
  text=combined_text,
342
  score=100.0
343
  ))
344
+
345
  return selected_segments
346
+
347
  except json.JSONDecodeError as e:
348
+ raise ValueError(f"Erro ao processar resposta da IA (JSON inválido): {str(e)}")
349
  except Exception as e:
350
  raise ValueError(f"Erro na análise da IA: {str(e)}")
351
 
 
353
  # =========================
354
  # Processamento com Comando Manual (sem IA)
355
  # =========================
356
+ def manual_command_processing(segments: List[Segment], command: str, fps: int) -> List[Segment]:
357
+ """Fallback: processamento básico sem IA para comandos simples."""
358
+ s = (command or "").lower()
359
+
360
+ # quantidade
 
 
361
  count = 1
362
  m = re.search(r'(\d+)\s*(?:cortes?|clipes?|segmentos?)', s)
363
  if m:
364
  count = int(m.group(1))
365
+
366
+ # duração
367
  duration_sec = 60
368
  m = re.search(r'(\d+)\s*(?:segundos?|s\b)', s)
369
  if m:
 
372
  m = re.search(r'(\d+)\s*(?:minutos?|min\b)', s)
373
  if m:
374
  duration_sec = int(m.group(1)) * 60
375
+
376
+ # timecode inicial
377
  start_frame = 0
378
  m = re.search(r'(?:começando|a partir de)\s+(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)', s)
379
  if m:
380
  try:
381
+ start_frame = parse_timecode_to_frames(m.group(1), fps)
382
+ except Exception:
383
  pass
384
+
385
+ # cortes contínuos
386
  results = []
387
  base_frame = start_frame
388
+
389
  for i in range(count):
390
+ duration_frames = duration_sec * fps
391
  end_frame = base_frame + duration_frames
392
+
 
393
  text_parts = []
394
  for seg in segments:
395
  if seg.start_f >= base_frame and seg.start_f < end_frame:
396
  if seg.text:
397
  text_parts.append(seg.text[:100])
398
+
399
  combined_text = " [...] ".join(text_parts[:10])[:400]
400
+
401
  results.append(Segment(
402
+ start_tc=frames_to_timecode(base_frame, fps),
403
+ end_tc=frames_to_timecode(end_frame, fps),
404
  start_f=base_frame,
405
  end_f=end_frame,
406
  text=combined_text if combined_text else f"Corte {i+1}",
407
  score=50.0
408
  ))
409
+
410
  base_frame = end_frame
411
+
412
  return results
413
 
414
 
 
424
  weight_learn: float,
425
  weight_viral: float
426
  ) -> List[Segment]:
427
+ """Sistema de pontuação automática simples por palavras-chave."""
428
  emotion_words = ['medo', 'coragem', 'amor', 'ódio', 'paixão', 'alegria', 'tristeza']
429
  break_words = ['nunca', 'de repente', 'surpreendente', 'inesperado', 'incrível']
430
  learn_words = ['aprendi', 'descobri', 'entendi', 'percebi', 'lição']
431
  viral_words = ['segredo', 'verdade', 'revelação', 'exclusivo', 'confissão']
432
+
433
  for s in segs:
434
  score = 0.0
435
  text = (s.text or "").lower()
436
+
437
  for word in emotion_words:
438
  if word in text:
439
  score += weight_emotion
440
+
441
  for word in break_words:
442
  if word in text:
443
  score += weight_break
444
+
445
  for word in learn_words:
446
  if word in text:
447
  score += weight_learn
448
+
449
  for word in viral_words:
450
  if word in text:
451
  score += weight_viral
452
+
453
  if custom_keywords:
454
  for kw in custom_keywords.split(","):
455
  kw_clean = kw.strip().lower()
456
  if kw_clean and kw_clean in text:
457
  score += 5.0
458
+
459
  s.score = score
460
+
461
  segs.sort(key=lambda x: x.score, reverse=True)
462
+ return segs[:max(1, num_segments)]
463
 
464
 
465
  # =========================
 
550
  weight_break: float,
551
  weight_learn: float,
552
  weight_viral: float,
553
+ fps: int,
554
+ progress_callback: Optional[Callable[[str], None]] = None
555
  ) -> List[Segment]:
556
 
557
  # 1) Manual
 
561
  for start_tc, end_tc in manual:
562
  try:
563
  result.append(Segment(
564
+ start_tc=frames_to_timecode(parse_timecode_to_frames(start_tc, fps), fps),
565
+ end_tc=frames_to_timecode(parse_timecode_to_frames(end_tc, fps), fps),
566
+ start_f=parse_timecode_to_frames(start_tc, fps),
567
+ end_f=parse_timecode_to_frames(end_tc, fps),
568
  text=f"Manual: {start_tc} - {end_tc}",
569
  score=100.0
570
  ))
 
573
  return result
574
 
575
  # 2) Parser de transcrição
576
+ segs = parse_transcript(transcript_txt, fps) if transcript_txt else []
577
 
578
+ # 3) Linguagem natural
579
  if natural_instructions.strip():
580
  if use_llm and LLM_AVAILABLE and segs:
581
+ return ai_analyze_and_select(segs, natural_instructions, fps, progress_callback)
 
582
  elif segs:
583
+ return manual_command_processing(segs, natural_instructions, fps)
 
584
  else:
585
+ raise ValueError(
586
+ "Para usar comandos em linguagem natural, forneça uma transcrição "
587
+ "ou use minutagens manuais."
588
+ )
589
 
590
  # 4) Automático
591
  if not segs:
592
+ raise ValueError("Nenhum segmento encontrado. Envie transcrição, minutagens ou um comando em linguagem natural.")
593
  return auto_score_segments(
594
  segs, num_segments, custom_keywords,
595
  weight_emotion, weight_break, weight_learn, weight_viral
 
603
  xml_file, txt_file, use_llm, num_segments,
604
  custom_keywords, manual_timecodes, natural_instructions,
605
  weight_emotion, weight_break, weight_learn, weight_viral,
606
+ fps,
607
  progress=gr.Progress()
608
  ):
609
  if not xml_file:
610
+ return "Envie o XML do Premiere", None, f"LLM: {'OK' if LLM_AVAILABLE else 'OFF'}"
611
 
612
  try:
613
  debug_info = []
614
+
615
  def progress_callback(msg):
616
  progress(0.5, desc=msg)
617
  debug_info.append(msg)
618
+
619
+ progress(0.1, desc="Carregando arquivos...")
620
+
621
  transcript = ""
622
  manual = parse_manual_timecodes(manual_timecodes)
623
 
624
  if not manual and txt_file:
625
  with open(txt_file.name, "r", encoding="utf-8-sig") as f:
626
  transcript = f.read()
627
+ debug_info.append(f"Transcrição: {len(transcript)} caracteres")
628
+
629
+ progress(0.2, desc="Selecionando segmentos...")
630
 
 
 
631
  segments = select_segments(
632
+ transcript, bool(use_llm) and LLM_AVAILABLE, int(num_segments),
633
  custom_keywords, manual_timecodes, natural_instructions,
634
+ float(weight_emotion), float(weight_break), float(weight_learn), float(weight_viral),
635
+ int(fps),
636
  progress_callback
637
  )
638
 
639
  if not segments:
640
+ return "Nenhum segmento selecionado", None, f"LLM: {'OK' if LLM_AVAILABLE else 'OFF'}"
641
 
642
+ # Validar duração mínima: pelo menos 1 segundo
643
  valid_segments = []
644
  for seg in segments:
645
+ if seg.end_f > seg.start_f and (seg.end_f - seg.start_f) >= max(1, int(fps)):
646
  valid_segments.append(seg)
647
+
648
  if not valid_segments:
649
+ return "Segmentos inválidos (duração muito curta)", None, f"LLM: {'OK' if LLM_AVAILABLE else 'OFF'}"
650
+
651
  segments = valid_segments
652
+ debug_info.append(f"{len(segments)} segmento(s) válidos")
653
+
654
+ progress(0.7, desc="Editando XML...")
655
 
 
 
656
  tree = ET.parse(xml_file.name)
657
  tree = edit_xml(tree, segments)
658
 
659
  basename = os.path.splitext(os.path.basename(xml_file.name))[0]
660
+ output_path = os.path.join(OUTPUT_DIR, f"{basename}_EDITADO.xml")
661
+ tree.write(output_path, encoding="utf-8", xml_declaration=True)
662
+
663
+ progress(0.9, desc="Gerando resumo...")
664
 
665
+ total_sec = sum((s.end_f - s.start_f) / fps for s in segments)
 
 
666
  total_min = total_sec / 60.0
667
+
668
  if manual:
669
+ mode = "Manual"
670
  elif natural_instructions.strip() and use_llm and LLM_AVAILABLE:
671
+ mode = "IA Completa (Gemini)"
672
  elif natural_instructions.strip():
673
+ mode = "Básico (sem IA)"
674
  else:
675
+ mode = "Automático"
676
 
677
  summary_lines = [
678
+ "RESULTADO",
679
+ f"- Cortes: {len(segments)}",
680
+ f"- Duração total: {total_min:.1f} min",
681
+ f"- Modo: {mode}",
682
  ""
683
  ]
684
+
685
  for i, seg in enumerate(segments, 1):
686
+ dur_sec = (seg.end_f - seg.start_f) / fps
687
  dur_min = dur_sec / 60.0
688
+ line = f"Corte {i}\n {seg.start_tc} -> {seg.end_tc} ({dur_min:.2f} min / {dur_sec:.0f}s)"
 
 
 
689
  if seg.text and len(seg.text.strip()) > 10:
690
  text_preview = seg.text[:200].strip()
691
  if len(seg.text) > 200:
692
  text_preview += "..."
693
+ line += f"\n {text_preview}"
 
694
  summary_lines.append(line)
695
  summary_lines.append("")
696
+
697
  if debug_info:
698
+ summary_lines.append("Log do processamento:")
699
+ summary_lines.extend(f"- {info}" for info in debug_info)
700
+
 
701
  summary = "\n".join(summary_lines)
702
+ status = f"Sucesso | {mode} | {total_min:.1f} min | LLM: {'OK' if LLM_AVAILABLE else 'OFF'}"
703
+
704
+ progress(1.0, desc="Concluído")
705
+ return summary, output_path, status
706
 
707
  except Exception as e:
708
  import traceback
709
  error_trace = traceback.format_exc()
710
  print(error_trace)
711
+
712
+ error_msg = f"Erro: {str(e)}\n\nDetalhes:\n{error_trace[:800]}"
713
+ return error_msg, None, f"LLM: {'OK' if LLM_AVAILABLE else 'OFF'}"
714
 
715
 
716
  # =========================
717
  # Interface Gradio
718
  # =========================
719
  with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere - IA") as demo:
720
+ gr.Markdown("# Editor XML Premiere - IA Completa (Gemini)")
721
+ status_inicial = f"{'IA Gemini ativa' if LLM_AVAILABLE else 'IA desabilitada: configure GEMINI_API_KEY'}"
722
+ gr.Markdown(f"Status: {status_inicial}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723
 
724
  with gr.Row():
725
+ xml_in = gr.File(label="XML do Premiere", file_types=[".xml"])
726
+ txt_in = gr.File(label="Transcrição (.txt) obrigatória para IA", file_types=[".txt"])
727
 
728
  with gr.Row():
729
  use_llm = gr.Checkbox(
730
+ label="Usar IA Gemini (análise completa recomendado)",
731
  value=USE_LLM_DEFAULT and LLM_AVAILABLE,
732
+ interactive=LLM_AVAILABLE
733
+ )
734
+ num_segments = gr.Slider(2, 20, 5, 1, label="Quantidade de segmentos (modo automático)")
735
+
736
+ fps_in = gr.Slider(12, 60, 24, 1, label="FPS")
737
+
738
+ with gr.Accordion("Comando em linguagem natural (modo principal)", open=True):
739
+ gr.Markdown(
740
+ "Exemplos: \n"
741
+ '- "Crie 3 cortes de 30 segundos sobre disciplina"\n'
742
+ '- "2 clipes de 1 minuto falando sobre Maria"\n'
743
+ '- "Corte de 5 minutos começando em 00:02:00:00 sobre tecnologia"'
744
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
745
  natural_instructions = gr.Textbox(
746
+ label="Digite seu comando",
747
+ placeholder='Ex: "Crie 3 cortes de 45 segundos sobre os momentos de disciplina e superação"',
748
  lines=4
749
  )
750
 
751
+ with gr.Accordion("Minutagens manuais (precisão total)", open=False):
752
+ gr.Markdown("Ignora IA e outros modos.")
753
  manual_timecodes = gr.Textbox(
754
  label="Timecodes (um por linha)",
755
  placeholder="00:21:18:09 - 00:31:18:09\n00:45:20:15 - 00:50:10:22",
756
  lines=4
757
  )
758
 
759
+ with gr.Accordion("Modo automático (sem comando)", open=False):
760
+ gr.Markdown("Sistema de pontuação simples por palavras-chave.")
761
  custom_keywords = gr.Textbox(
762
  label="Palavras-chave (separadas por vírgula)",
763
  placeholder="coragem, superação, vitória"
764
  )
765
  with gr.Row():
766
+ weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="Peso: emoção")
767
+ weight_break = gr.Slider(0, 5, 1.5, 0.1, label="Peso: quebra")
768
  with gr.Row():
769
+ weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="Peso: aprendizado")
770
+ weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="Peso: viral")
771
 
772
+ btn = gr.Button("Processar")
773
 
774
  with gr.Row():
775
  with gr.Column(scale=2):
776
+ summary_out = gr.Textbox(label="Resumo dos cortes", lines=20, max_lines=30)
777
  with gr.Column(scale=1):
778
+ status_out = gr.Textbox(label="Status", lines=3)
779
+ file_out = gr.File(label="Download XML editado")
780
 
781
  btn.click(
782
  process_files,
783
  [xml_in, txt_in, use_llm, num_segments, custom_keywords,
784
  manual_timecodes, natural_instructions,
785
+ weight_emotion, weight_break, weight_learn, weight_viral, fps_in],
786
  [summary_out, file_out, status_out]
787
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
788
 
789
  if __name__ == "__main__":
790
+ demo.launch()