leicam commited on
Commit
3f6d341
·
verified ·
1 Parent(s): b561d7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +303 -477
app.py CHANGED
@@ -1,5 +1,6 @@
1
  import os
2
  import re
 
3
  import xml.etree.ElementTree as ET
4
  from dataclasses import dataclass
5
  from typing import List, Tuple, Optional
@@ -13,7 +14,7 @@ OUTPUT_DIR = "./Output"
13
  os.makedirs(OUTPUT_DIR, exist_ok=True)
14
 
15
  # =========================
16
- # LLM (opcional - Gemini)
17
  # =========================
18
  USE_LLM_DEFAULT = True
19
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "").strip()
@@ -53,13 +54,11 @@ 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
57
  m = re.match(r'^(\d{1,2}):(\d{2}):(\d{2})[:;](\d{2})$', s)
58
  if m:
59
  hh, mm, ss, ff = map(int, m.groups())
60
  return hh, mm, ss, ff
61
 
62
- # HH:MM:SS[.,]mmm
63
  m = re.match(r'^(\d{1,2}):(\d{2}):(\d{2})[.,](\d{1,3})$', s)
64
  if m:
65
  hh, mm, ss, ms = map(int, m.groups())
@@ -69,7 +68,6 @@ def _tc_to_hmsf(tc: str, fps: int = FPS) -> Tuple[int, int, int, int]:
69
  ff = 0
70
  return hh, mm, ss, ff
71
 
72
- # H:MM:SS
73
  m = re.match(r'^(\d{1,2}):(\d{2}):(\d{2})$', s)
74
  if m:
75
  hh, mm, ss = map(int, m.groups())
@@ -97,7 +95,7 @@ def frames_to_timecode(frames: int, fps: int = FPS) -> str:
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,7 +118,6 @@ def parse_transcript(txt: str) -> List[Segment]:
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()
@@ -132,13 +129,7 @@ def parse_transcript(txt: str) -> List[Segment]:
132
  j = i + 1
133
  while j < len(lines):
134
  nxt = lines[j].strip()
135
- if not nxt:
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,7 +153,6 @@ def parse_transcript(txt: str) -> List[Segment]:
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)
@@ -223,406 +213,206 @@ def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
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
  # =========================
@@ -637,15 +427,11 @@ def auto_score_segments(
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
@@ -671,14 +457,7 @@ def auto_score_segments(
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
 
@@ -723,7 +502,6 @@ def edit_xml(tree: ET.ElementTree, segs: List[Segment]) -> ET.ElementTree:
723
  if duration <= 0:
724
  continue
725
 
726
- # Vídeo
727
  v_clip = ET.Element("clipitem", {"id": f"clip-v{i}"})
728
  ET.SubElement(v_clip, "name").text = f"Clip {i}"
729
  ET.SubElement(v_clip, "start").text = str(timeline_pos)
@@ -739,7 +517,6 @@ def edit_xml(tree: ET.ElementTree, segs: List[Segment]) -> ET.ElementTree:
739
  if file_elem is not None:
740
  v_clip.append(deep_copy_element(file_elem))
741
 
742
- # Áudio
743
  a_clip = ET.Element("clipitem", {"id": f"clip-a{i}"})
744
  ET.SubElement(a_clip, "name").text = f"Clip {i}"
745
  ET.SubElement(a_clip, "start").text = str(timeline_pos)
@@ -775,7 +552,8 @@ def select_segments(
775
  weight_emotion: float,
776
  weight_break: float,
777
  weight_learn: float,
778
- weight_viral: float
 
779
  ) -> List[Segment]:
780
 
781
  # 1) Manual
@@ -799,9 +577,16 @@ def select_segments(
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
 
806
  # 4) Automático
807
  if not segs:
@@ -818,7 +603,8 @@ def select_segments(
818
  def process_files(
819
  xml_file, txt_file, use_llm, num_segments,
820
  custom_keywords, manual_timecodes, natural_instructions,
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 '✗'}"
@@ -826,22 +612,31 @@ def process_files(
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,
840
- weight_emotion, weight_break, weight_learn, weight_viral
 
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:
@@ -849,11 +644,13 @@ def process_files(
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
 
@@ -861,25 +658,25 @@ def process_files(
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
 
@@ -888,28 +685,26 @@ def process_files(
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:
@@ -917,78 +712,98 @@ def process_files(
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():
@@ -998,11 +813,11 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere") as demo:
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")
@@ -1017,15 +832,26 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere") as demo:
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__":
 
1
  import os
2
  import re
3
+ import json
4
  import xml.etree.ElementTree as ET
5
  from dataclasses import dataclass
6
  from typing import List, Tuple, Optional
 
14
  os.makedirs(OUTPUT_DIR, exist_ok=True)
15
 
16
  # =========================
17
+ # LLM (Gemini)
18
  # =========================
19
  USE_LLM_DEFAULT = True
20
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "").strip()
 
54
  """Converte timecode para (hh, mm, ss, ff)."""
55
  s = tc.strip()
56
 
 
57
  m = re.match(r'^(\d{1,2}):(\d{2}):(\d{2})[:;](\d{2})$', s)
58
  if m:
59
  hh, mm, ss, ff = map(int, m.groups())
60
  return hh, mm, ss, ff
61
 
 
62
  m = re.match(r'^(\d{1,2}):(\d{2}):(\d{2})[.,](\d{1,3})$', s)
63
  if m:
64
  hh, mm, ss, ms = map(int, m.groups())
 
68
  ff = 0
69
  return hh, mm, ss, ff
70
 
 
71
  m = re.match(r'^(\d{1,2}):(\d{2}):(\d{2})$', s)
72
  if m:
73
  hh, mm, ss = map(int, m.groups())
 
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
 
 
118
  i += 1
119
  continue
120
 
 
121
  m = line_range.match(raw)
122
  if m:
123
  start_tc, end_tc, trailing_text = m.groups()
 
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
 
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)
 
213
 
214
 
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:
250
+ {command}
251
+
252
+ TRANSCRIÇÃO COMPLETA (formato JSON com index, timecode, duração e texto):
253
+ {transcript_json}
254
+
255
+ INSTRUÇÕES:
256
+ 1. Leia o comando com atenção e identifique:
257
+ - Quantidade de cortes desejada
258
+ - Duração de cada corte (em segundos)
259
+ - Tema/assunto/palavras-chave mencionados
260
+ - Timecode de início (se mencionado)
261
+
262
+ 2. Analise TODA a transcrição e identifique os segmentos que melhor correspondem ao comando
263
+
264
+ 3. Para cada corte, retorne no formato JSON:
265
+ {{
266
+ "cuts": [
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(
286
  prompt,
287
  generation_config={
288
+ "temperature": 0.2,
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
 
352
 
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:
372
+ duration_sec = int(m.group(1))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  else:
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
 
418
  # =========================
 
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
 
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
 
 
502
  if duration <= 0:
503
  continue
504
 
 
505
  v_clip = ET.Element("clipitem", {"id": f"clip-v{i}"})
506
  ET.SubElement(v_clip, "name").text = f"Clip {i}"
507
  ET.SubElement(v_clip, "start").text = str(timeline_pos)
 
517
  if file_elem is not None:
518
  v_clip.append(deep_copy_element(file_elem))
519
 
 
520
  a_clip = ET.Element("clipitem", {"id": f"clip-a{i}"})
521
  ET.SubElement(a_clip, "name").text = f"Clip {i}"
522
  ET.SubElement(a_clip, "start").text = str(timeline_pos)
 
552
  weight_emotion: float,
553
  weight_break: float,
554
  weight_learn: float,
555
+ weight_viral: float,
556
+ progress_callback=None
557
  ) -> List[Segment]:
558
 
559
  # 1) Manual
 
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:
 
603
  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 '✗'}"
 
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:
 
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
 
 
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
 
 
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:
 
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():
 
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")
 
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__":