leicam commited on
Commit
b561d7a
·
verified ·
1 Parent(s): 8769bc9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +506 -241
app.py CHANGED
@@ -50,12 +50,7 @@ class Segment:
50
  # Funções de Timecode
51
  # =========================
52
  def _tc_to_hmsf(tc: str, fps: int = FPS) -> Tuple[int, int, int, int]:
53
- """
54
- Converte timecode para (hh, mm, ss, ff). Aceita:
55
- - HH:MM:SS:FF ou HH:MM:SS;FF
56
- - HH:MM:SS[.,]mmm (milissegundos)
57
- - H:MM:SS (sem frames)
58
- """
59
  s = tc.strip()
60
 
61
  # HH:MM:SS:FF ou HH:MM:SS;FF
@@ -102,17 +97,7 @@ def frames_to_timecode(frames: int, fps: int = FPS) -> str:
102
  # Parser de Transcrição
103
  # =========================
104
  def parse_transcript(txt: str) -> List[Segment]:
105
- """
106
- Aceita múltiplos formatos:
107
- A) Uma linha: 00:00:00:00 - 00:00:10:00 Texto...
108
- B) Duas linhas: 00:00:00:00 - 00:00:10:00 \n Texto...
109
- C) SRT/VTT com setas:
110
- 1
111
- 00:00:05,120 --> 00:00:08,300
112
- Texto linha 1
113
- Texto linha 2
114
- [linha em branco]
115
- """
116
  if not txt or not txt.strip():
117
  return []
118
 
@@ -135,7 +120,7 @@ def parse_transcript(txt: str) -> List[Segment]:
135
  i += 1
136
  continue
137
 
138
- # Casos A e B (com traço)
139
  m = line_range.match(raw)
140
  if m:
141
  start_tc, end_tc, trailing_text = m.groups()
@@ -144,7 +129,6 @@ def parse_transcript(txt: str) -> List[Segment]:
144
  if trailing_text.strip():
145
  text_parts.append(trailing_text.strip())
146
  else:
147
- # Texto nas linhas seguintes até linha em branco ou novo bloco
148
  j = i + 1
149
  while j < len(lines):
150
  nxt = lines[j].strip()
@@ -152,9 +136,9 @@ def parse_transcript(txt: str) -> List[Segment]:
152
  break
153
  if line_range.match(nxt):
154
  break
155
- if re.match(r'^\d+\s*$', nxt): # índice SRT
156
  break
157
- if arrow.search(nxt): # linha SRT com -->
158
  break
159
  text_parts.append(nxt)
160
  j += 1
@@ -178,9 +162,8 @@ def parse_transcript(txt: str) -> List[Segment]:
178
  i += 1
179
  continue
180
 
181
- # Caso C (SRT/VTT com -->)
182
  if arrow.search(raw) or (i + 1 < len(lines) and arrow.search(lines[i + 1])):
183
- # Se a linha atual não tem arrow, tente a próxima (muitos SRTs têm um índice numérico antes)
184
  line_with_tc = raw if arrow.search(raw) else lines[i + 1]
185
  mm = arrow.search(line_with_tc)
186
  if mm:
@@ -191,7 +174,6 @@ def parse_transcript(txt: str) -> List[Segment]:
191
  nxt = lines[j].strip()
192
  if not nxt:
193
  break
194
- # próximo bloco: índice seguido de timecode
195
  if re.match(r'^\d+\s*$', nxt) and (j + 1 < len(lines) and arrow.search(lines[j + 1])):
196
  break
197
  if arrow.search(nxt):
@@ -215,7 +197,6 @@ def parse_transcript(txt: str) -> List[Segment]:
215
  except Exception:
216
  pass
217
 
218
- # Avança o ponteiro para depois do bloco
219
  i = j + 1
220
  continue
221
 
@@ -242,241 +223,410 @@ def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
242
 
243
 
244
  # =========================
245
- # Interpretação do Comando (NLP simples)
246
  # =========================
247
  @dataclass
248
  class CommandSpec:
249
- total_segments: int # quantidade de cortes
250
- per_segment_seconds: Optional[int] # duração por corte (segundos), se especificada
251
- total_minutes: Optional[float] # duração total (minutos), alternativa ao per_segment_seconds
252
- start_timecode: Optional[str] # início explícito
253
- keywords: List[str] # termos para achar o começo
254
- use_best_moments: bool # flag para "melhores momentos"
 
 
255
 
256
 
257
  def parse_natural_command(text: str) -> CommandSpec:
258
- """
259
- Extrai:
260
- - quantidade de cortes: "3 cortes", "crie 2"
261
- - duração por corte: "cortes de 30s", "clipes de 1min", "1 minuto"
262
- - duração total: "corte de 10 minutos", "15min", "faça 5 minutos"
263
- - timecode de início: "começando em 00:02:10:00" ou "a partir de 00:02:10,500"
264
- - palavras-chave: "sobre X", "da parte do X", "tema X", "palavra X"
265
- - melhores momentos: presença de "melhores momentos"
266
- Regras:
267
- - se per_segment_seconds e total_minutes vierem juntos, prioriza per_segment_seconds (mais específico)
268
- - caso apenas total_minutes: cria 1 corte dessa duração (ou divide pelos 'total_segments' se quantidade também vier)
269
- """
270
  s = text.strip().lower()
271
-
272
- # quantidade de cortes
273
  count = 1
274
- m = re.search(r'(\d+)\s*(?:cortes?|clipes?)\b', s)
275
- if m:
276
- count = max(1, int(m.group(1)))
277
- else:
278
- m = re.search(r'\bcrie\s+(\d+)\b', s)
 
 
 
279
  if m:
280
  count = max(1, int(m.group(1)))
 
281
 
282
- # duração por corte (segundos)
283
  per_seg_sec = None
284
- m = re.search(r'(\d+)\s*(?:segundos?|s)\b', s)
285
- if m:
286
- per_seg_sec = int(m.group(1))
287
- else:
288
- # "de 30s", "30 s", etc.
289
- m = re.search(r'de\s+(\d+)\s*s\b', s)
 
 
290
  if m:
291
  per_seg_sec = int(m.group(1))
 
292
 
293
- # duração por corte em minutos -> segundos
294
  if per_seg_sec is None:
295
- m = re.search(r'(\d+)\s*(?:minutos?|min)\b', s)
296
- if m:
297
- per_seg_sec = int(m.group(1)) * 60
298
- else:
299
- # "de 1min"
300
- m = re.search(r'de\s+(\d+)\s*min\b', s)
 
 
301
  if m:
302
- per_seg_sec = int(m.group(1)) * 60
 
303
 
304
- # duração total (minutos)
305
  total_min = None
306
- # expressões como "corte de 10 minutos", "faça 5 minutos", "crie 15min"
307
- m = re.search(r'\b(?:corte|faça|faca|crie|criar|gerar|make|montar)\b.*?(\d+)\s*(?:minutos?|min)\b', s)
308
- if m:
309
- total_min = float(m.group(1))
310
- else:
311
- m = re.search(r'\b(\d+)\s*(?:minutos?|min)\b', s)
 
 
312
  if m:
313
  total_min = float(m.group(1))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
 
315
- # timecode de início explícito
316
- m = re.search(r'(?:começando|comecando|a partir de|starting at|start at)\s*(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)', s)
317
- start_tc = m.group(1) if m else None
318
-
319
- # palavras-chave depois de "sobre", "da parte do", "tema", "assunto"
320
  kw = []
321
- kw_match = re.search(r'(?:sobre|da parte do|tema|assunto)\s+(.+)', s)
322
- if kw_match:
323
- # pega o resto da frase e quebra por vírgula
324
- tail = kw_match.group(1)
325
- kw = [t.strip() for t in re.split(r'[,\.;/]', tail) if t.strip()]
326
-
327
- # flag de "melhores momentos"
328
- best = bool(re.search(r'melhores momentos', s))
329
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  return CommandSpec(
331
  total_segments=count,
332
  per_segment_seconds=per_seg_sec,
333
  total_minutes=total_min,
334
  start_timecode=start_tc,
 
335
  keywords=kw,
336
- use_best_moments=best
 
337
  )
338
 
339
 
340
  # =========================
341
- # Utilidades de seleção
342
  # =========================
343
- def find_keyword_in_segments(segs: List[Segment], keywords: List[str]) -> int:
 
344
  if not segs or not keywords:
345
- return 0
346
- best_idx, best_score = 0, -1
 
 
 
347
  for idx, seg in enumerate(segs):
348
  text_lower = seg.text.lower()
349
- score = sum(1 for kw in keywords if kw.lower() in text_lower)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  if score > best_score:
351
  best_idx, best_score = idx, score
352
- return best_idx
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
 
355
  def create_continuous_segment_from(start_frame: int, duration_frames: int, segs_preview: List[Segment]) -> Segment:
356
  end_frame = max(start_frame + duration_frames, start_frame + 1)
357
- # preview opcional do texto
358
  text_parts = []
359
- for seg in segs_preview[:10]:
360
- if seg.text:
361
- text_parts.append(seg.text[:80])
362
- combined = " ".join(text_parts)[:300]
 
 
363
  return Segment(
364
  start_tc=frames_to_timecode(start_frame),
365
  end_tc=frames_to_timecode(end_frame),
366
  start_f=start_frame,
367
  end_f=end_frame,
368
- text=("Corte contínuo: " + combined) if combined else "Corte contínuo",
369
  score=100.0
370
  )
371
 
372
 
373
- def process_with_command(
374
- segs: List[Segment],
375
- command: str,
376
- use_llm: bool
377
- ) -> List[Segment]:
378
- """
379
- Processa instruções naturais. Funciona com ou sem transcrição:
380
- - sem transcrição: cria cortes contínuos a partir do timecode (ou 00:00)
381
- - com transcrição: usa keywords/LLM para achar início e criar cortes
382
- Regras de duração:
383
- - se per_segment_seconds for fornecido -> aplica em cada corte
384
- - do contrário, se total_minutes e total_segments > 1 -> divide igualmente
385
- - se apenas total_minutes -> 1 corte com essa duração
386
- - default se nada especificado -> 1 corte de 60s
387
- """
388
  spec = parse_natural_command(command)
389
 
390
- # Determinar duração por corte (segundos)
391
  if spec.per_segment_seconds:
392
  per_seg_seconds = spec.per_segment_seconds
393
  total_segments = max(1, spec.total_segments)
394
- elif spec.total_minutes and spec.total_segments and spec.total_segments > 1:
395
- total_seconds = int(spec.total_minutes * 60)
396
- total_segments = spec.total_segments
397
- per_seg_seconds = max(1, total_seconds // total_segments)
398
  elif spec.total_minutes:
399
- per_seg_seconds = int(spec.total_minutes * 60)
400
- total_segments = 1
 
 
 
 
 
401
  else:
402
  per_seg_seconds = 60
403
  total_segments = max(1, spec.total_segments)
404
 
405
- # Determinar ponto de início (frame)
406
  start_frame = 0
 
 
 
 
407
  if spec.start_timecode:
408
  try:
409
  start_frame = parse_timecode_to_frames(spec.start_timecode)
 
410
  except Exception:
411
- start_frame = 0
412
-
413
- # Se houver transcrição, tentar achar índice inicial por palavra-chave/LLM
414
- start_idx = None
415
- if segs:
416
- if spec.keywords:
417
- start_idx = find_keyword_in_segments(segs, spec.keywords)
418
-
419
- if use_llm and LLM_AVAILABLE and segs:
420
- try:
421
- # prepara um preview leve de 80 segmentos (índice|tc|texto)
422
- preview = []
423
- for i, s in enumerate(segs[:80]):
424
- preview.append(f"{i}|{s.start_tc}|{(s.text or '')[:60]}")
425
- preview_text = "\n".join(preview)
426
-
427
- prompt = f"""Encontre o índice inicial do assunto solicitado, retornando apenas o número (ex: 42).
428
-
429
- BUSCAR: {' '.join(spec.keywords[:5]) or '(sem keywords)'}
430
-
431
- SEGMENTOS (índice|timecode|texto):
432
- {preview_text}
433
- """
434
- response = LLM.generate_content(
435
- prompt,
436
- generation_config={"temperature": 0.1, "max_output_tokens": 20}
437
- )
438
- text = (response.text or "").strip()
439
- m = re.search(r'\b(\d+)\b', text)
440
- if m:
441
- idx = int(m.group(1))
442
- if 0 <= idx < len(segs):
443
- start_idx = idx
444
- except Exception:
445
- pass
446
-
447
- # Construir cortes
448
  segments_out: List[Segment] = []
449
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  if not segs:
451
- # Sem transcrição: cortes contínuos a partir do timecode (ou zero)
452
- base_frame = start_frame
453
  for _ in range(total_segments):
454
  duration_frames = int(per_seg_seconds * FPS)
455
  seg = create_continuous_segment_from(base_frame, duration_frames, [])
456
  segments_out.append(seg)
457
  base_frame = seg.end_f
458
  return segments_out
459
-
460
  # Com transcrição
461
- # Determina start_frame baseado em start_idx ou em timecode explícito
462
- if start_idx is not None and 0 <= start_idx < len(segs):
463
- start_frame = segs[start_idx].start_f
464
- # Se já havia start_timecode, preserva; se não, usa 0 como fallback
465
- base_frame = max(0, start_frame)
466
-
467
- for _ in range(total_segments):
468
  duration_frames = int(per_seg_seconds * FPS)
469
- # usa preview de texto para descrição
470
- seg_preview = segs[start_idx:start_idx + 10] if (start_idx is not None) else segs[:10]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  seg = create_continuous_segment_from(base_frame, duration_frames, seg_preview)
472
  segments_out.append(seg)
473
  base_frame = seg.end_f
474
-
475
  return segments_out
476
 
477
 
478
  # =========================
479
- # Modo Automático (score simples)
480
  # =========================
481
  def auto_score_segments(
482
  segs: List[Segment],
@@ -487,32 +637,57 @@ def auto_score_segments(
487
  weight_learn: float,
488
  weight_viral: float
489
  ) -> List[Segment]:
 
 
 
 
 
 
 
 
 
 
490
  for s in segs:
491
  score = 0.0
492
  text = (s.text or "").lower()
493
-
494
- if "medo" in text or "coragem" in text:
495
- score += weight_emotion
496
- if "nunca" in text or "de repente" in text:
497
- score += weight_break
498
- if "aprendi" in text or "descobri" in text:
499
- score += weight_learn
500
- if "segredo" in text or "verdade" in text:
501
- score += weight_viral
502
-
 
 
 
 
 
 
 
503
  if custom_keywords:
504
  for kw in custom_keywords.split(","):
505
- if kw.strip().lower() in text:
506
- score += 3.0
507
-
 
 
 
 
 
 
 
 
508
  s.score = score
509
-
510
  segs.sort(key=lambda x: x.score, reverse=True)
511
  return segs[:num_segments]
512
 
513
 
514
  # =========================
515
- # Edição de XML (Premiere)
516
  # =========================
517
  def deep_copy_element(elem: ET.Element) -> ET.Element:
518
  new = ET.Element(elem.tag, attrib=dict(elem.attrib))
@@ -537,13 +712,11 @@ def edit_xml(tree: ET.ElementTree, segs: List[Segment]) -> ET.ElementTree:
537
  v_template = v_track.find("./clipitem")
538
  a_template = a_track.find("./clipitem")
539
 
540
- # Limpa clips existentes
541
  for clip in list(v_track.findall("./clipitem")):
542
  v_track.remove(clip)
543
  for clip in list(a_track.findall("./clipitem")):
544
  a_track.remove(clip)
545
 
546
- # Adiciona novos clips
547
  timeline_pos = 0
548
  for i, seg in enumerate(segs, 1):
549
  duration = seg.end_f - seg.start_f
@@ -623,10 +796,10 @@ def select_segments(
623
  pass
624
  return result
625
 
626
- # 2) Parser de transcrição (se houver)
627
  segs = parse_transcript(transcript_txt) if transcript_txt else []
628
 
629
- # 3) Linguagem natural (sempre permitido; funciona com ou sem transcrição)
630
  if natural_instructions.strip():
631
  return process_with_command(segs, natural_instructions, use_llm and LLM_AVAILABLE)
632
 
@@ -648,18 +821,19 @@ def process_files(
648
  weight_emotion, weight_break, weight_learn, weight_viral
649
  ):
650
  if not xml_file:
651
- return "Envie o XML", None, f"LLM: {LLM_AVAILABLE}"
652
 
653
  try:
654
- # transcrição apenas se necessário
 
655
  transcript = ""
656
  manual = parse_manual_timecodes(manual_timecodes)
657
 
658
  if not manual and txt_file:
659
  with open(txt_file.name, "r", encoding="utf-8-sig") as f:
660
  transcript = f.read()
 
661
 
662
- # Seleciona segmentos
663
  segments = select_segments(
664
  transcript, use_llm and LLM_AVAILABLE, num_segments,
665
  custom_keywords, manual_timecodes, natural_instructions,
@@ -667,93 +841,171 @@ def process_files(
667
  )
668
 
669
  if not segments:
670
- return "Nenhum segmento selecionado", None, f"LLM: {LLM_AVAILABLE}"
 
 
 
 
 
 
 
 
 
 
 
671
 
672
- # Edita XML
673
  tree = ET.parse(xml_file.name)
674
  tree = edit_xml(tree, segments)
675
 
676
- # Salva
677
  basename = os.path.splitext(os.path.basename(xml_file.name))[0]
678
  output = os.path.join(OUTPUT_DIR, f"{basename}_EDITADO.xml")
679
  tree.write(output, encoding="utf-8", xml_declaration=True)
680
 
681
- # Resumo
682
  total_sec = sum((s.end_f - s.start_f) / FPS for s in segments)
683
  total_min = total_sec / 60.0
684
- mode = "MANUAL" if manual else ("IA/NATURAL" if natural_instructions.strip() else "AUTOMÁTICO")
685
-
686
- summary_lines = [f"{len(segments)} corte(s) | {total_min:.1f} min total | Modo: {mode}"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
  for i, seg in enumerate(segments, 1):
688
  dur_sec = (seg.end_f - seg.start_f) / FPS
689
- line = f"{i}. {seg.start_tc} → {seg.end_tc} ({dur_sec/60:.1f} min)"
690
- if seg.text and len(seg.text) > 50:
691
- line += f"\n {seg.text[:120]}..."
 
 
 
 
 
 
 
 
 
 
 
692
  summary_lines.append(line)
 
 
 
 
 
 
 
693
  summary = "\n".join(summary_lines)
694
-
695
- status = f"Sucesso | {mode} | {total_min:.1f} min | LLM: {LLM_AVAILABLE}"
696
  return summary, output, status
697
 
698
  except Exception as e:
699
  import traceback
700
- traceback.print_exc()
701
- return f"Erro: {str(e)}", None, f"LLM: {LLM_AVAILABLE}"
 
 
 
702
 
703
 
704
  # =========================
705
- # Interface (Gradio)
706
  # =========================
707
  with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere") as demo:
708
- gr.Markdown("# Editor XML Premiere - IA")
709
- gr.Markdown("Cortes com transcrição, minutagens ou comando em linguagem natural.")
 
 
 
710
 
711
  with gr.Row():
712
- xml_in = gr.File(label="XML do Premiere", file_types=[".xml"])
713
- txt_in = gr.File(label="Transcrição (.txt) - opcional", file_types=[".txt"])
714
 
715
  with gr.Row():
716
- use_llm = gr.Checkbox(label="Usar IA (Gemini) quando útil", value=USE_LLM_DEFAULT and LLM_AVAILABLE)
717
- num_segments = gr.Slider(2, 20, 5, 1, label="Segmentos (modo automático)")
 
 
 
 
718
 
719
- with gr.Accordion("Comando em linguagem natural", open=True):
720
  gr.Markdown("""
721
- Exemplos:
722
- - "Crie 1 corte de 10 minutos começando da parte do tenista"
723
- - "Quero 3 cortes de 30s sobre Maria e José"
724
- - "Faça 2 cortes de 45s começando em 00:02:10:00"
725
- Se não fornecer transcrição, os cortes serão contínuos a partir do timecode indicado (ou 00:00:00:00).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
726
  """)
727
  natural_instructions = gr.Textbox(
728
  label="Seu comando",
729
- placeholder='Ex: "Crie 2 cortes de 45s sobre coragem e disciplina, começando em 00:01:00:00"',
730
- lines=2
731
  )
732
 
733
- with gr.Accordion("Minutagens manuais", open=False):
 
734
  manual_timecodes = gr.Textbox(
735
- label="Timecodes (um por linha)",
736
- placeholder="00:21:18:09 - 00:31:18:09",
737
- lines=3
738
  )
739
 
740
- with gr.Accordion("Modo automático (com transcrição)", open=False):
741
- custom_keywords = gr.Textbox(label="Palavras-chave (separadas por vírgula)")
 
 
 
 
742
  with gr.Row():
743
- weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="Peso: emoção")
744
- weight_break = gr.Slider(0, 5, 1.5, 0.1, label="Peso: quebra")
745
  with gr.Row():
746
- weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="Peso: aprendizado")
747
- weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="Peso: viral")
748
 
749
- btn = gr.Button("Processar", variant="primary", size="lg")
750
 
751
  with gr.Row():
752
  with gr.Column(scale=2):
753
- summary_out = gr.Textbox(label="Resumo", lines=12)
754
  with gr.Column(scale=1):
755
- status_out = gr.Textbox(label="Status")
756
- file_out = gr.File(label="Download")
757
 
758
  btn.click(
759
  process_files,
@@ -762,6 +1014,19 @@ Se não fornecer transcrição, os cortes serão contínuos a partir do timecode
762
  weight_emotion, weight_break, weight_learn, weight_viral],
763
  [summary_out, file_out, status_out]
764
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
765
 
766
  if __name__ == "__main__":
767
- demo.launch()
 
50
  # Funções de Timecode
51
  # =========================
52
  def _tc_to_hmsf(tc: str, fps: int = FPS) -> Tuple[int, int, int, int]:
53
+ """Converte timecode para (hh, mm, ss, ff)."""
 
 
 
 
 
54
  s = tc.strip()
55
 
56
  # HH:MM:SS:FF ou HH:MM:SS;FF
 
97
  # Parser de Transcrição
98
  # =========================
99
  def parse_transcript(txt: str) -> List[Segment]:
100
+ """Parser robusto para múltiplos formatos de transcrição."""
 
 
 
 
 
 
 
 
 
 
101
  if not txt or not txt.strip():
102
  return []
103
 
 
120
  i += 1
121
  continue
122
 
123
+ # Formato com traço
124
  m = line_range.match(raw)
125
  if m:
126
  start_tc, end_tc, trailing_text = m.groups()
 
129
  if trailing_text.strip():
130
  text_parts.append(trailing_text.strip())
131
  else:
 
132
  j = i + 1
133
  while j < len(lines):
134
  nxt = lines[j].strip()
 
136
  break
137
  if line_range.match(nxt):
138
  break
139
+ if re.match(r'^\d+\s*$', nxt):
140
  break
141
+ if arrow.search(nxt):
142
  break
143
  text_parts.append(nxt)
144
  j += 1
 
162
  i += 1
163
  continue
164
 
165
+ # Formato SRT/VTT
166
  if arrow.search(raw) or (i + 1 < len(lines) and arrow.search(lines[i + 1])):
 
167
  line_with_tc = raw if arrow.search(raw) else lines[i + 1]
168
  mm = arrow.search(line_with_tc)
169
  if mm:
 
174
  nxt = lines[j].strip()
175
  if not nxt:
176
  break
 
177
  if re.match(r'^\d+\s*$', nxt) and (j + 1 < len(lines) and arrow.search(lines[j + 1])):
178
  break
179
  if arrow.search(nxt):
 
197
  except Exception:
198
  pass
199
 
 
200
  i = j + 1
201
  continue
202
 
 
223
 
224
 
225
  # =========================
226
+ # Interpretação do Comando (NLP otimizado)
227
  # =========================
228
  @dataclass
229
  class CommandSpec:
230
+ total_segments: int
231
+ per_segment_seconds: Optional[int]
232
+ total_minutes: Optional[float]
233
+ start_timecode: Optional[str]
234
+ end_timecode: Optional[str]
235
+ keywords: List[str]
236
+ use_best_moments: bool
237
+ search_mode: str
238
 
239
 
240
  def parse_natural_command(text: str) -> CommandSpec:
241
+ """Parser NLP robusto com múltiplos padrões."""
 
 
 
 
 
 
 
 
 
 
 
242
  s = text.strip().lower()
243
+
244
+ # Quantidade
245
  count = 1
246
+ patterns = [
247
+ r'(\d+)\s*(?:cortes?|clipes?|segmentos?|trechos?|partes?)',
248
+ r'(?:crie?|faça?|faca|gere?|monte?|extraia?)\s+(\d+)',
249
+ r'quero\s+(\d+)',
250
+ r'preciso\s+(?:de\s+)?(\d+)'
251
+ ]
252
+ for pattern in patterns:
253
+ m = re.search(pattern, s)
254
  if m:
255
  count = max(1, int(m.group(1)))
256
+ break
257
 
258
+ # Duração em segundos
259
  per_seg_sec = None
260
+ patterns_sec = [
261
+ r'(?:cortes?|clipes?|trechos?)\s+de\s+(\d+)\s*(?:segundos?|s\b)',
262
+ r'(\d+)\s*(?:segundos?|s\b)\s+(?:cada|por)',
263
+ r'(?:duração|duracao)\s+(?:de\s+)?(\d+)\s*s\b',
264
+ r'com\s+(\d+)\s*segundos?'
265
+ ]
266
+ for pattern in patterns_sec:
267
+ m = re.search(pattern, s)
268
  if m:
269
  per_seg_sec = int(m.group(1))
270
+ break
271
 
272
+ # Duração em minutos
273
  if per_seg_sec is None:
274
+ patterns_min = [
275
+ r'(?:cortes?|clipes?|trechos?)\s+de\s+(\d+(?:\.\d+)?)\s*(?:minutos?|min\b)',
276
+ r'(\d+(?:\.\d+)?)\s*(?:minutos?|min\b)\s+(?:cada|por)',
277
+ r'(?:duração|duracao)\s+(?:de\s+)?(\d+(?:\.\d+)?)\s*min',
278
+ r'com\s+(\d+(?:\.\d+)?)\s*minutos?'
279
+ ]
280
+ for pattern in patterns_min:
281
+ m = re.search(pattern, s)
282
  if m:
283
+ per_seg_sec = int(float(m.group(1)) * 60)
284
+ break
285
 
286
+ # Duração total
287
  total_min = None
288
+ patterns_total = [
289
+ r'(?:corte|video|vídeo)\s+(?:de|com)\s+(\d+(?:\.\d+)?)\s*(?:minutos?|min\b)',
290
+ r'(?:totalizando|total\s+de)\s+(\d+(?:\.\d+)?)\s*min',
291
+ r'(?:faça|faca|crie)\s+(\d+(?:\.\d+)?)\s*minutos?',
292
+ r'(\d+(?:\.\d+)?)\s*minutos?\s+no\s+total'
293
+ ]
294
+ for pattern in patterns_total:
295
+ m = re.search(pattern, s)
296
  if m:
297
  total_min = float(m.group(1))
298
+ break
299
+
300
+ # Timecode início
301
+ start_tc = None
302
+ patterns_start = [
303
+ r'(?:começando|comecando|iniciando|a partir de|desde|starting at|from)\s+(?:em\s+|às\s+|as\s+)?(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)',
304
+ r'(?:do|no)\s+(?:tempo|timecode|tc)\s+(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)'
305
+ ]
306
+ for pattern in patterns_start:
307
+ m = re.search(pattern, s)
308
+ if m:
309
+ start_tc = m.group(1)
310
+ break
311
+
312
+ # Timecode fim
313
+ end_tc = None
314
+ patterns_end = [
315
+ r'(?:até|ate|terminando em|até o|finalizando em)\s+(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)',
316
+ r'(?:ao|no)\s+(?:tempo|timecode|tc)\s+(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)'
317
+ ]
318
+ for pattern in patterns_end:
319
+ m = re.search(pattern, s)
320
+ if m:
321
+ end_tc = m.group(1)
322
+ break
323
 
324
+ # Keywords
 
 
 
 
325
  kw = []
326
+ patterns_kw = [
327
+ r'(?:sobre|falando sobre|abordando|tratando de|relacionado a)\s+([^,\.]+)',
328
+ r'(?:da parte|trecho|momento|cena)\s+(?:do|da|dos|das)\s+([^,\.]+)',
329
+ r'(?:tema|assunto|tópico|topico|conteúdo|conteudo)\s+([^,\.]+)',
330
+ r'(?:com|contendo|que menciona?|que fala sobre)\s+([^,\.]+)',
331
+ r'(?:onde|quando|que)\s+(?:fala|menciona|cita|aparece)\s+([^,\.]+)'
332
+ ]
333
+ for pattern in patterns_kw:
334
+ m = re.search(pattern, s)
335
+ if m:
336
+ keywords_text = m.group(1)
337
+ keywords_text = re.sub(r'\s+(?:e|ou|,)\s+', ',', keywords_text)
338
+ kw = [k.strip() for k in keywords_text.split(',') if k.strip()]
339
+ stopwords = {'o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'em', 'no', 'na'}
340
+ kw = [k for k in kw if k.lower() not in stopwords]
341
+ break
342
+
343
+ if not kw:
344
+ for word in ['sobre', 'do', 'da', 'dos', 'das']:
345
+ if word in s:
346
+ idx = s.index(word)
347
+ tail = s[idx + len(word):].strip()
348
+ end_words = ['começando', 'comecando', 'iniciando', 'de', 'com', 'em']
349
+ for ew in end_words:
350
+ if ew in tail:
351
+ tail = tail[:tail.index(ew)]
352
+ if tail:
353
+ kw = [w.strip() for w in tail.split() if len(w.strip()) > 2][:5]
354
+ break
355
+
356
+ # Melhores momentos
357
+ best = bool(re.search(r'melhor(?:es)?\s+momento|mais\s+interessante|destaque|highlight', s))
358
+
359
+ # Modo de busca
360
+ search_mode = 'continuous'
361
+ if best:
362
+ search_mode = 'best_moments'
363
+ elif kw:
364
+ search_mode = 'keyword'
365
+
366
  return CommandSpec(
367
  total_segments=count,
368
  per_segment_seconds=per_seg_sec,
369
  total_minutes=total_min,
370
  start_timecode=start_tc,
371
+ end_timecode=end_tc,
372
  keywords=kw,
373
+ use_best_moments=best,
374
+ search_mode=search_mode
375
  )
376
 
377
 
378
  # =========================
379
+ # Utilidades (melhoradas)
380
  # =========================
381
+ def find_keyword_in_segments(segs: List[Segment], keywords: List[str]) -> Tuple[int, float]:
382
+ """Retorna (índice, score) do melhor match."""
383
  if not segs or not keywords:
384
+ return 0, 0.0
385
+
386
+ best_idx, best_score = 0, 0.0
387
+ kw_lower = [kw.lower() for kw in keywords]
388
+
389
  for idx, seg in enumerate(segs):
390
  text_lower = seg.text.lower()
391
+ score = 0.0
392
+
393
+ for kw in kw_lower:
394
+ if kw in text_lower:
395
+ score += len(kw.split()) * 5.0
396
+
397
+ words = text_lower.split()
398
+ for kw in kw_lower:
399
+ kw_words = kw.split()
400
+ for kw_word in kw_words:
401
+ if len(kw_word) > 2:
402
+ for word in words:
403
+ if kw_word in word or word in kw_word:
404
+ score += 1.0
405
+
406
  if score > best_score:
407
  best_idx, best_score = idx, score
408
+
409
+ return best_idx, best_score
410
+
411
+
412
+ def find_llm_segment(segs: List[Segment], keywords: List[str], command: str) -> Tuple[Optional[int], float]:
413
+ """Usa LLM para encontrar segmento. Retorna (índice, confiança)."""
414
+ if not LLM_AVAILABLE or not segs:
415
+ return None, 0.0
416
+
417
+ try:
418
+ preview_lines = []
419
+ for i, s in enumerate(segs[:100]):
420
+ text_preview = (s.text or '')[:120]
421
+ duration_sec = (s.end_f - s.start_f) / FPS
422
+ preview_lines.append(f"{i}|{s.start_tc}|{duration_sec:.1f}s|{text_preview}")
423
+
424
+ preview_text = "\n".join(preview_lines)
425
+ keywords_str = ", ".join(keywords[:10]) if keywords else "não especificado"
426
+
427
+ prompt = f"""Analise os segmentos e retorne APENAS o número do índice onde o conteúdo solicitado começa.
428
+
429
+ IMPORTANTE: Responda SOMENTE com o número do índice (ex: 42). Não explique.
430
+
431
+ COMANDO DO USUÁRIO: {command}
432
+
433
+ PALAVRAS-CHAVE: {keywords_str}
434
+
435
+ SEGMENTOS (formato: índice|timecode|duração|texto):
436
+ {preview_text}
437
+
438
+ Qual índice melhor corresponde ao início do conteúdo solicitado?
439
+ Responda apenas o número:"""
440
+
441
+ response = LLM.generate_content(
442
+ prompt,
443
+ generation_config={
444
+ "temperature": 0.1,
445
+ "max_output_tokens": 30,
446
+ "top_p": 0.8
447
+ }
448
+ )
449
+
450
+ text = (response.text or "").strip()
451
+
452
+ patterns = [
453
+ r'^\s*(\d+)\s*$',
454
+ r'(?:índice|index|segmento)\s*(\d+)',
455
+ r'(?:número|numero|#)\s*(\d+)',
456
+ r'\b(\d+)\b'
457
+ ]
458
+
459
+ for pattern in patterns:
460
+ m = re.search(pattern, text, re.IGNORECASE)
461
+ if m:
462
+ idx = int(m.group(1))
463
+ if 0 <= idx < len(segs):
464
+ confidence = 0.9 if pattern == patterns[0] else 0.7
465
+ return idx, confidence
466
+
467
+ return None, 0.0
468
+
469
+ except Exception as e:
470
+ print(f"Erro no LLM: {e}")
471
+ return None, 0.0
472
 
473
 
474
  def create_continuous_segment_from(start_frame: int, duration_frames: int, segs_preview: List[Segment]) -> Segment:
475
  end_frame = max(start_frame + duration_frames, start_frame + 1)
476
+
477
  text_parts = []
478
+ for seg in segs_preview[:15]:
479
+ if seg.text and len(seg.text.strip()) > 5:
480
+ text_parts.append(seg.text[:100])
481
+
482
+ combined = " [...] ".join(text_parts)[:400] if text_parts else ""
483
+
484
  return Segment(
485
  start_tc=frames_to_timecode(start_frame),
486
  end_tc=frames_to_timecode(end_frame),
487
  start_f=start_frame,
488
  end_f=end_frame,
489
+ text=combined if combined else f"Corte contínuo de {duration_frames/FPS:.1f}s",
490
  score=100.0
491
  )
492
 
493
 
494
+ def process_with_command(segs: List[Segment], command: str, use_llm: bool) -> List[Segment]:
495
+ """Processa instruções naturais com sistema multi-camadas."""
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  spec = parse_natural_command(command)
497
 
498
+ # Calcula duração
499
  if spec.per_segment_seconds:
500
  per_seg_seconds = spec.per_segment_seconds
501
  total_segments = max(1, spec.total_segments)
 
 
 
 
502
  elif spec.total_minutes:
503
+ total_seconds = int(spec.total_minutes * 60)
504
+ if spec.total_segments > 1:
505
+ per_seg_seconds = max(5, total_seconds // spec.total_segments)
506
+ total_segments = spec.total_segments
507
+ else:
508
+ per_seg_seconds = total_seconds
509
+ total_segments = 1
510
  else:
511
  per_seg_seconds = 60
512
  total_segments = max(1, spec.total_segments)
513
 
514
+ # Determina início com fallback
515
  start_frame = 0
516
+ start_idx = None
517
+ search_confidence = 0.0
518
+
519
+ # Timecode explícito
520
  if spec.start_timecode:
521
  try:
522
  start_frame = parse_timecode_to_frames(spec.start_timecode)
523
+ search_confidence = 1.0
524
  except Exception:
525
+ pass
526
+
527
+ # LLM
528
+ if search_confidence < 0.8 and use_llm and segs and (spec.keywords or spec.search_mode == 'llm'):
529
+ llm_idx, llm_conf = find_llm_segment(segs, spec.keywords, command)
530
+ if llm_idx is not None and llm_conf > search_confidence:
531
+ start_idx = llm_idx
532
+ start_frame = segs[start_idx].start_f
533
+ search_confidence = llm_conf
534
+
535
+ # Keywords
536
+ if search_confidence < 0.6 and segs and spec.keywords:
537
+ kw_idx, kw_score = find_keyword_in_segments(segs, spec.keywords)
538
+ kw_conf = min(0.9, kw_score / 10.0)
539
+ if kw_conf > search_confidence:
540
+ start_idx = kw_idx
541
+ start_frame = segs[start_idx].start_f
542
+ search_confidence = kw_conf
543
+
544
+ # Melhores momentos
545
+ if spec.use_best_moments and segs:
546
+ scored = [(i, s) for i, s in enumerate(segs) if s.score > 0]
547
+ if scored:
548
+ scored.sort(key=lambda x: x[1].score, reverse=True)
549
+ start_idx = scored[0][0]
550
+ start_frame = segs[start_idx].start_f
551
+ search_confidence = 0.8
552
+
553
+ # Determina fim
554
+ end_frame = None
555
+ if spec.end_timecode:
556
+ try:
557
+ end_frame = parse_timecode_to_frames(spec.end_timecode)
558
+ except Exception:
559
+ pass
560
+
561
+ # Construção dos cortes
562
  segments_out: List[Segment] = []
563
+
564
+ # Intervalo específico
565
+ if end_frame and end_frame > start_frame:
566
+ duration_frames = end_frame - start_frame
567
+ if total_segments == 1:
568
+ seg_preview = []
569
+ if segs and start_idx is not None:
570
+ seg_preview = segs[start_idx:start_idx + 20]
571
+ seg = create_continuous_segment_from(start_frame, duration_frames, seg_preview)
572
+ segments_out.append(seg)
573
+ else:
574
+ frames_per_seg = duration_frames // total_segments
575
+ base = start_frame
576
+ for i in range(total_segments):
577
+ seg_preview = []
578
+ if segs and start_idx is not None:
579
+ seg_preview = segs[start_idx + i:start_idx + i + 10]
580
+ seg = create_continuous_segment_from(base, frames_per_seg, seg_preview)
581
+ segments_out.append(seg)
582
+ base = seg.end_f
583
+ return segments_out
584
+
585
+ # Cortes sequenciais
586
+ base_frame = start_frame
587
+
588
  if not segs:
 
 
589
  for _ in range(total_segments):
590
  duration_frames = int(per_seg_seconds * FPS)
591
  seg = create_continuous_segment_from(base_frame, duration_frames, [])
592
  segments_out.append(seg)
593
  base_frame = seg.end_f
594
  return segments_out
595
+
596
  # Com transcrição
597
+ for i in range(total_segments):
 
 
 
 
 
 
598
  duration_frames = int(per_seg_seconds * FPS)
599
+
600
+ seg_start_idx = None
601
+ if start_idx is not None:
602
+ for idx in range(start_idx, len(segs)):
603
+ if segs[idx].start_f >= base_frame:
604
+ seg_start_idx = idx
605
+ break
606
+ else:
607
+ for idx, s in enumerate(segs):
608
+ if s.start_f >= base_frame:
609
+ seg_start_idx = idx
610
+ break
611
+
612
+ seg_preview = []
613
+ if seg_start_idx is not None:
614
+ end_of_cut = base_frame + duration_frames
615
+ for s in segs[seg_start_idx:]:
616
+ if s.start_f < end_of_cut:
617
+ seg_preview.append(s)
618
+ else:
619
+ break
620
+
621
  seg = create_continuous_segment_from(base_frame, duration_frames, seg_preview)
622
  segments_out.append(seg)
623
  base_frame = seg.end_f
624
+
625
  return segments_out
626
 
627
 
628
  # =========================
629
+ # Modo Automático
630
  # =========================
631
  def auto_score_segments(
632
  segs: List[Segment],
 
637
  weight_learn: float,
638
  weight_viral: float
639
  ) -> List[Segment]:
640
+ """Sistema de pontuação expandido."""
641
+ emotion_words = ['medo', 'coragem', 'amor', 'ódio', 'paixão', 'alegria', 'tristeza',
642
+ 'ansiedade', 'felicidade', 'emoção', 'sentimento', 'coração']
643
+ break_words = ['nunca', 'de repente', 'surpreendente', 'inesperado', 'incrível',
644
+ 'chocante', 'virada', 'mudança', 'momento', 'aconteceu']
645
+ learn_words = ['aprendi', 'descobri', 'entendi', 'percebi', 'compreendi', 'lição',
646
+ 'ensinamento', 'experiência', 'conhecimento', 'insight']
647
+ viral_words = ['segredo', 'verdade', 'ninguém sabe', 'revelação', 'exclusivo',
648
+ 'primeira vez', 'confissão', 'polêmica', 'controverso']
649
+
650
  for s in segs:
651
  score = 0.0
652
  text = (s.text or "").lower()
653
+
654
+ for word in emotion_words:
655
+ if word in text:
656
+ score += weight_emotion
657
+
658
+ for word in break_words:
659
+ if word in text:
660
+ score += weight_break
661
+
662
+ for word in learn_words:
663
+ if word in text:
664
+ score += weight_learn
665
+
666
+ for word in viral_words:
667
+ if word in text:
668
+ score += weight_viral
669
+
670
  if custom_keywords:
671
  for kw in custom_keywords.split(","):
672
+ kw_clean = kw.strip().lower()
673
+ if kw_clean and kw_clean in text:
674
+ score += 3.0 * len(kw_clean.split())
675
+
676
+ duration_sec = (s.end_f - s.start_f) / FPS
677
+ if 10 <= duration_sec <= 120:
678
+ score += 0.5
679
+
680
+ if len(text) > 100:
681
+ score += 0.3
682
+
683
  s.score = score
684
+
685
  segs.sort(key=lambda x: x.score, reverse=True)
686
  return segs[:num_segments]
687
 
688
 
689
  # =========================
690
+ # Edição de XML
691
  # =========================
692
  def deep_copy_element(elem: ET.Element) -> ET.Element:
693
  new = ET.Element(elem.tag, attrib=dict(elem.attrib))
 
712
  v_template = v_track.find("./clipitem")
713
  a_template = a_track.find("./clipitem")
714
 
 
715
  for clip in list(v_track.findall("./clipitem")):
716
  v_track.remove(clip)
717
  for clip in list(a_track.findall("./clipitem")):
718
  a_track.remove(clip)
719
 
 
720
  timeline_pos = 0
721
  for i, seg in enumerate(segs, 1):
722
  duration = seg.end_f - seg.start_f
 
796
  pass
797
  return result
798
 
799
+ # 2) Parser de transcrição
800
  segs = parse_transcript(transcript_txt) if transcript_txt else []
801
 
802
+ # 3) Linguagem natural
803
  if natural_instructions.strip():
804
  return process_with_command(segs, natural_instructions, use_llm and LLM_AVAILABLE)
805
 
 
821
  weight_emotion, weight_break, weight_learn, weight_viral
822
  ):
823
  if not xml_file:
824
+ return "⚠️ Envie o XML do Premiere", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
825
 
826
  try:
827
+ debug_info = []
828
+
829
  transcript = ""
830
  manual = parse_manual_timecodes(manual_timecodes)
831
 
832
  if not manual and txt_file:
833
  with open(txt_file.name, "r", encoding="utf-8-sig") as f:
834
  transcript = f.read()
835
+ debug_info.append(f"📄 Transcrição carregada: {len(transcript)} caracteres")
836
 
 
837
  segments = select_segments(
838
  transcript, use_llm and LLM_AVAILABLE, num_segments,
839
  custom_keywords, manual_timecodes, natural_instructions,
 
841
  )
842
 
843
  if not segments:
844
+ return "⚠️ Nenhum segmento selecionado. Verifique os parâmetros.", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
845
+
846
+ valid_segments = []
847
+ for seg in segments:
848
+ if seg.end_f > seg.start_f and seg.end_f - seg.start_f >= FPS:
849
+ valid_segments.append(seg)
850
+
851
+ if not valid_segments:
852
+ return "⚠️ Segmentos inválidos (duração muito curta). Ajuste os parâmetros.", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
853
+
854
+ segments = valid_segments
855
+ debug_info.append(f"✓ {len(segments)} segmento(s) válido(s)")
856
 
 
857
  tree = ET.parse(xml_file.name)
858
  tree = edit_xml(tree, segments)
859
 
 
860
  basename = os.path.splitext(os.path.basename(xml_file.name))[0]
861
  output = os.path.join(OUTPUT_DIR, f"{basename}_EDITADO.xml")
862
  tree.write(output, encoding="utf-8", xml_declaration=True)
863
 
 
864
  total_sec = sum((s.end_f - s.start_f) / FPS for s in segments)
865
  total_min = total_sec / 60.0
866
+
867
+ if manual:
868
+ mode = "🎯 MANUAL"
869
+ elif natural_instructions.strip():
870
+ spec = parse_natural_command(natural_instructions)
871
+ if spec.keywords:
872
+ mode = f"🤖 IA + BUSCA ({', '.join(spec.keywords[:3])})"
873
+ else:
874
+ mode = "📐 IA + CONTÍNUO"
875
+ else:
876
+ mode = "⚙️ AUTOMÁTICO"
877
+
878
+ summary_lines = [
879
+ "═" * 60,
880
+ f"✨ RESULTADO: {len(segments)} corte(s) | {total_min:.1f} min total",
881
+ f"📊 Modo: {mode}",
882
+ "═" * 60,
883
+ ""
884
+ ]
885
+
886
  for i, seg in enumerate(segments, 1):
887
  dur_sec = (seg.end_f - seg.start_f) / FPS
888
+ dur_min = dur_sec / 60.0
889
+
890
+ line = f"🎬 Corte {i}:"
891
+ line += f"\n ⏱️ {seg.start_tc} → {seg.end_tc} ({dur_min:.2f} min)"
892
+
893
+ if seg.text and len(seg.text.strip()) > 10:
894
+ text_preview = seg.text[:150].strip()
895
+ if len(seg.text) > 150:
896
+ text_preview += "..."
897
+ line += f"\n 💬 {text_preview}"
898
+
899
+ if seg.score > 0:
900
+ line += f"\n ⭐ Score: {seg.score:.1f}"
901
+
902
  summary_lines.append(line)
903
+ summary_lines.append("")
904
+
905
+ if debug_info:
906
+ summary_lines.append("═" * 60)
907
+ summary_lines.append("🔍 Debug:")
908
+ summary_lines.extend(f" {info}" for info in debug_info)
909
+
910
  summary = "\n".join(summary_lines)
911
+ status = f"✅ Sucesso | {mode} | {total_min:.1f} min | LLM: {'✓' if LLM_AVAILABLE else '✗'}"
912
+
913
  return summary, output, status
914
 
915
  except Exception as e:
916
  import traceback
917
+ error_trace = traceback.format_exc()
918
+ print(error_trace)
919
+
920
+ error_msg = f"❌ Erro: {str(e)}\n\n🔍 Detalhes técnicos:\n{error_trace[:500]}"
921
+ return error_msg, None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
922
 
923
 
924
  # =========================
925
+ # Interface Gradio
926
  # =========================
927
  with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere") as demo:
928
+ gr.Markdown("# 🎬 Editor XML Premiere - IA Avançada")
929
+ gr.Markdown("Sistema inteligente de cortes com IA (Gemini), busca por keywords e timecodes manuais.")
930
+
931
+ status_inicial = f"{'🟢 IA Disponível (Gemini 2.0)' if LLM_AVAILABLE else '🟡 Modo básico (IA desabilitada - configure GEMINI_API_KEY)'}"
932
+ gr.Markdown(f"**Status:** {status_inicial}")
933
 
934
  with gr.Row():
935
+ xml_in = gr.File(label="📄 XML do Premiere", file_types=[".xml"])
936
+ txt_in = gr.File(label="📝 Transcrição (.txt) - opcional", file_types=[".txt"])
937
 
938
  with gr.Row():
939
+ use_llm = gr.Checkbox(
940
+ label="🤖 Usar IA (Gemini) para busca inteligente",
941
+ value=USE_LLM_DEFAULT and LLM_AVAILABLE,
942
+ interactive=LLM_AVAILABLE
943
+ )
944
+ num_segments = gr.Slider(2, 20, 5, 1, label="📊 Segmentos (modo automático)")
945
 
946
+ with gr.Accordion("💬 Comando em linguagem natural (RECOMENDADO)", open=True):
947
  gr.Markdown("""
948
+ **Exemplos de comandos suportados:**
949
+
950
+ 📌 **Duração e quantidade:**
951
+ - "Crie 3 cortes de 30 segundos"
952
+ - "Faça 1 corte de 10 minutos"
953
+ - "Quero 5 clipes de 45s cada"
954
+
955
+ 📍 **Com timecode:**
956
+ - "2 cortes de 1min começando em 00:02:10:00"
957
+ - "Corte de 5 minutos a partir de 00:05:00:00"
958
+
959
+ 🔍 **Com busca de conteúdo (requer transcrição + IA):**
960
+ - "3 cortes de 30s sobre Maria e José"
961
+ - "1 corte de 10 minutos da parte do tenista"
962
+ - "2 clipes de 45s falando sobre coragem"
963
+ - "Corte sobre disciplina começando em 00:02:00"
964
+
965
+ 🎯 **Intervalo específico:**
966
+ - "Corte de 00:10:00:00 até 00:15:00:00"
967
+ - "3 segmentos começando em 00:02:00 até 00:05:00"
968
+
969
+ 💡 **Dicas:**
970
+ - Com transcrição + IA: busca automática do conteúdo
971
+ - Sem transcrição: cortes contínuos a partir do timecode
972
+ - Seja específico nas durações e palavras-chave
973
  """)
974
  natural_instructions = gr.Textbox(
975
  label="Seu comando",
976
+ placeholder='Ex: "Crie 2 cortes de 45s sobre disciplina, começando em 00:01:00:00"',
977
+ lines=3
978
  )
979
 
980
+ with gr.Accordion("🎯 Minutagens manuais (alta precisão)", open=False):
981
+ gr.Markdown("Use este modo quando souber exatamente os timecodes. Um por linha ou separados por vírgula.")
982
  manual_timecodes = gr.Textbox(
983
+ label="Timecodes (formato: HH:MM:SS:FF - HH:MM:SS:FF)",
984
+ placeholder="00:21:18:09 - 00:31:18:09\n00:45:20:15 - 00:50:10:22",
985
+ lines=4
986
  )
987
 
988
+ with gr.Accordion("⚙️ Modo automático (com transcrição)", open=False):
989
+ gr.Markdown("Sistema de pontuação automática baseado em palavras-chave e pesos.")
990
+ custom_keywords = gr.Textbox(
991
+ label="Palavras-chave personalizadas (separadas por vírgula)",
992
+ placeholder="coragem, superação, vitória"
993
+ )
994
  with gr.Row():
995
+ weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="Peso: emoção")
996
+ weight_break = gr.Slider(0, 5, 1.5, 0.1, label="💥 Peso: quebra")
997
  with gr.Row():
998
+ weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="🎓 Peso: aprendizado")
999
+ weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="🔥 Peso: viral")
1000
 
1001
+ btn = gr.Button("🚀 Processar e Gerar XML", variant="primary", size="lg")
1002
 
1003
  with gr.Row():
1004
  with gr.Column(scale=2):
1005
+ summary_out = gr.Textbox(label="📋 Resumo dos Cortes", lines=15, max_lines=25)
1006
  with gr.Column(scale=1):
1007
+ status_out = gr.Textbox(label="📊 Status", lines=3)
1008
+ file_out = gr.File(label="⬇️ Download XML Editado")
1009
 
1010
  btn.click(
1011
  process_files,
 
1014
  weight_emotion, weight_break, weight_learn, weight_viral],
1015
  [summary_out, file_out, status_out]
1016
  )
1017
+
1018
+ gr.Markdown("""
1019
+ ---
1020
+ ### 📚 Como usar:
1021
+ 1. **Envie o XML** exportado do Premiere (File > Export > Final Cut Pro XML)
1022
+ 2. **Opcional:** Envie transcrição para buscas inteligentes
1023
+ 3. **Escolha um modo:**
1024
+ - 💬 Linguagem natural (mais fácil)
1025
+ - 🎯 Minutagens manuais (mais preciso)
1026
+ - ⚙️ Automático (experimental)
1027
+ 4. Clique em **Processar** e faça download do XML editado
1028
+ 5. Importe de volta no Premiere (File > Import)
1029
+ """)
1030
 
1031
  if __name__ == "__main__":
1032
+ demo.launch()