leicam commited on
Commit
ae976b8
·
verified ·
1 Parent(s): 0e92a90

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +295 -441
app.py CHANGED
@@ -17,14 +17,11 @@ try:
17
  genai.configure(api_key=GEMINI_API_KEY)
18
  LLM = genai.GenerativeModel(LLM_MODEL_NAME)
19
  LLM_AVAILABLE = True
20
- print("✓ IA Gemini configurada com sucesso")
21
  else:
22
  LLM = None
23
- print("⚠ GEMINI_API_KEY não encontrada")
24
- except Exception as e:
25
  LLM = None
26
  LLM_AVAILABLE = False
27
- print(f"⚠ Erro ao configurar IA: {e}")
28
 
29
  # Config
30
  FPS = 24
@@ -42,16 +39,14 @@ class Segment:
42
 
43
  # ============ TIMECODE FUNCTIONS ============
44
  def parse_timecode_to_frames(tc: str, fps: int = FPS) -> int:
45
- """Converte timecode para frames"""
46
  tc = tc.strip()
47
- m = re.match(r"^(\d{2}):(\d{2}):(\d{2})[:;](\d{2})$", tc)
48
  if not m:
49
  raise ValueError(f"Timecode inválido: {tc}")
50
  hh, mm, ss, ff = map(int, m.groups())
51
  return hh*3600*fps + mm*60*fps + ss*fps + ff
52
 
53
  def frames_to_timecode(frames: int, fps: int = FPS) -> str:
54
- """Converte frames para timecode"""
55
  hh = frames // (3600*fps)
56
  rem = frames % (3600*fps)
57
  mm = rem // (60*fps)
@@ -61,22 +56,25 @@ def frames_to_timecode(frames: int, fps: int = FPS) -> str:
61
  return f"{hh:02d}:{mm:02d}:{ss:02d}:{ff:02d}"
62
 
63
  # ============ TRANSCRIPT PARSING ============
64
- def parse_transcript_full(txt: str) -> List[Segment]:
65
- """Parse transcrição com diferentes formatos"""
66
  if not txt or not txt.strip():
 
67
  return []
68
 
69
- lines = txt.splitlines()
70
- results: List[Segment] = []
71
 
 
72
  pattern = re.compile(
73
- r'^\s*\[?\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})\s*[-—–]\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})\s*\]?\s*(.*)$'
 
74
  )
75
 
76
- for line in lines:
77
  line = line.strip()
78
 
79
- if not line or line == "Desconhecido":
80
  continue
81
 
82
  match = pattern.match(line)
@@ -85,7 +83,7 @@ def parse_transcript_full(txt: str) -> List[Segment]:
85
  start_tc, end_tc, text = match.groups()
86
  text = text.strip()
87
 
88
- if not text or text == "Desconhecido":
89
  continue
90
 
91
  try:
@@ -102,73 +100,50 @@ def parse_transcript_full(txt: str) -> List[Segment]:
102
  score=0.0
103
  ))
104
  except Exception as e:
105
- print(f" Erro ao processar linha: {e}")
106
  continue
107
 
108
- print(f" Encontrados {len(results)} segmentos na transcrição")
109
  return results
110
 
111
  # ============ MANUAL TIMECODES ============
112
  def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
113
- """Parse timecodes manuais"""
114
  if not manual_input or not manual_input.strip():
115
  return []
116
 
117
  manual_ranges = []
118
- normalized = manual_input.replace(",", "\n")
119
- lines = [l.strip() for l in normalized.splitlines() if l.strip()]
120
 
121
- pat = re.compile(r'(\d{2}:\d{2}:\d{2}[:;]\d{2})\s*[-–—]\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})')
122
 
123
  for line in lines:
124
- m = pat.search(line)
125
  if m:
126
- start_tc, end_tc = m.groups()
127
- manual_ranges.append((start_tc, end_tc))
128
 
129
  return manual_ranges
130
 
131
- # ============ AI HELPER FUNCTIONS ============
132
- def extract_duration_and_keywords(instructions: str) -> Tuple[Optional[float], List[str]]:
133
- """Extrai duração e palavras-chave das instruções"""
134
- instructions_lower = instructions.lower()
135
 
136
- # Extrai duração
137
- duration = None
138
- duration_patterns = [
139
  r'(\d+)\s*minutos?',
140
  r'(\d+)\s*min\b',
141
  r'(\d+)m\b',
142
  r'corte\s+de\s+(\d+)'
143
  ]
144
 
145
- for pattern in duration_patterns:
146
- match = re.search(pattern, instructions_lower)
147
  if match:
148
- duration = float(match.group(1))
149
- print(f"✓ Duração extraída: {duration} minutos")
150
- break
151
-
152
- # Extrai palavras-chave importantes
153
- keywords = []
154
-
155
- topic_keywords = {
156
- 'tenista': ['tenista', 'tênis', 'jogador', 'kinguios'],
157
- 'maria': ['maria', 'josé', 'casal', 'seguro', 'carro'],
158
- 'protocolo': ['protocolo', 'rodar', 'dependência'],
159
- 'emoção': ['medo', 'culpa', 'raiva', 'emoção'],
160
- 'negócio': ['empresa', 'negócio', 'faturamento', 'dinheiro'],
161
- }
162
-
163
- for key, terms in topic_keywords.items():
164
- if any(term in instructions_lower for term in terms):
165
- keywords.append(key)
166
 
167
- print(f"✓ Keywords encontradas: {keywords}")
168
- return duration, keywords
169
 
170
- def find_segment_by_content(segs: List[Segment], keywords: List[str]) -> int:
171
- """Encontra o índice do segmento que melhor corresponde às palavras-chave"""
172
  if not keywords:
173
  return 0
174
 
@@ -177,494 +152,373 @@ def find_segment_by_content(segs: List[Segment], keywords: List[str]) -> int:
177
 
178
  for idx, seg in enumerate(segs):
179
  text_lower = seg.text.lower()
180
- score = sum(1 for kw in keywords if kw in text_lower)
181
 
182
  if score > best_score:
183
  best_score = score
184
  best_idx = idx
185
 
186
- print(f"✓ Melhor match no segmento {best_idx} (score: {best_score})")
187
  return best_idx
188
 
189
- def ai_find_start_point(segs: List[Segment], instructions: str, keywords: List[str]) -> int:
190
- """Usa IA para encontrar ponto de início"""
191
- if not LLM_AVAILABLE:
192
- print("⚠ IA não disponível, usando busca por keywords")
193
- return find_segment_by_content(segs, keywords)
194
-
195
- # Cria resumo dos primeiros 150 segmentos
196
- segments_preview = []
197
- for i, s in enumerate(segs[:150]):
198
- duration = (s.end_f - s.start_f) / FPS
199
- segments_preview.append(
200
- f"{i}. [{s.start_tc}] ({duration:.1f}s) {s.text[:80]}"
201
- )
202
-
203
- prompt = f"""Você é um editor de vídeo. Encontre o índice do segmento onde deve COMEÇAR o corte.
204
-
205
- INSTRUÇÕES DO USUÁRIO:
206
- {instructions}
207
-
208
- SEGMENTOS DISPONÍVEIS:
209
- {chr(10).join(segments_preview)}
210
-
211
- IMPORTANTE:
212
- - Analise onde está o conteúdo solicitado
213
- - Retorne APENAS o número do índice (exemplo: 87)
214
- - Considere o contexto e o início da história relevante
215
-
216
- RESPONDA APENAS COM O NÚMERO:"""
217
-
218
- try:
219
- print("🤖 Consultando IA...")
220
- response = LLM.generate_content(prompt, generation_config={
221
- "temperature": 0.1,
222
- "max_output_tokens": 50
223
- })
224
-
225
- text = (response.text or "").strip()
226
- print(f"IA respondeu: {text}")
227
-
228
- match = re.search(r'\b(\d+)\b', text)
229
-
230
- if match:
231
- idx = int(match.group(1))
232
- if 0 <= idx < len(segs):
233
- print(f"✓ IA encontrou início no segmento {idx}: {segs[idx].start_tc}")
234
- return idx
235
-
236
- except Exception as e:
237
- print(f"⚠ Erro na IA: {e}")
238
-
239
- # Fallback
240
- fallback_idx = find_segment_by_content(segs, keywords)
241
- print(f"✓ Usando fallback no segmento {fallback_idx}")
242
- return fallback_idx
243
-
244
- def create_continuous_cut(segs: List[Segment], start_idx: int, duration_minutes: float) -> List[Segment]:
245
- """Cria um corte contínuo"""
246
  if start_idx >= len(segs):
247
  start_idx = 0
248
 
249
- target_frames = int(duration_minutes * 60 * FPS)
250
-
251
  start_seg = segs[start_idx]
252
  start_frame = start_seg.start_f
253
- end_frame = start_frame + target_frames
254
-
255
- # Garante que não ultrapassa o último segmento
256
- max_frame = segs[-1].end_f
257
- if end_frame > max_frame:
258
- end_frame = max_frame
259
- actual_duration = (end_frame - start_frame) / FPS / 60
260
- print(f"⚠ Ajustado para {actual_duration:.1f} min (limite da transcrição)")
261
-
262
- # Cria texto combinado
263
- involved_segs = []
264
- for seg in segs[start_idx:]:
265
- if seg.start_f < end_frame:
266
- involved_segs.append(seg)
267
- else:
268
- break
269
-
270
- combined_text = " ".join([s.text[:100] for s in involved_segs[:10]])
271
-
272
- result = Segment(
273
  start_tc=frames_to_timecode(start_frame),
274
  end_tc=frames_to_timecode(end_frame),
275
  start_f=start_frame,
276
  end_f=end_frame,
277
- text=f"Corte contínuo ({duration_minutes}min): {combined_text[:200]}...",
278
  score=100.0
279
  )
280
-
281
- print(f"✓ Corte criado: {result.start_tc} → {result.end_tc}")
282
- return [result]
283
 
284
- def ai_select_segments(segs: List[Segment], instructions: str) -> List[Segment]:
285
- """Processa instruções em linguagem natural"""
286
- if not segs:
287
- raise ValueError("Nenhum segmento disponível")
288
 
289
- print(f"📝 Processando instruções: {instructions[:100]}...")
 
290
 
291
- # Extrai duração e palavras-chave
292
- duration, keywords = extract_duration_and_keywords(instructions)
 
293
 
294
- if duration:
295
- # Modo: corte contínuo de X minutos
296
- print(f"Modo: CORTE CONTÍNUO de {duration} minutos")
297
- start_idx = ai_find_start_point(segs, instructions, keywords)
298
- result = create_continuous_cut(segs, start_idx, duration)
299
- return result
300
 
301
- else:
302
- # Modo: seleção múltipla (fallback)
303
- print("⚠ Duração não especificada, usando modo de seleção múltipla")
304
- start_idx = ai_find_start_point(segs, instructions, keywords)
305
- selected = segs[start_idx:start_idx + 10]
306
-
307
- if not selected:
308
- selected = segs[:10]
309
-
310
- return selected
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
- # ============ KEYWORD SCORING ============
313
- def keyword_score(text: str, custom_keywords: str = "", weights: dict = None) -> float:
314
- """Pontuação por palavras-chave"""
315
- if weights is None:
316
- weights = {"emotion": 2.0, "break": 1.5, "learn": 1.2, "viral": 1.0}
317
-
318
- t = text.lower()
319
- score = 0.0
320
-
321
- kw_emotion = ["medo", "coragem", "raiva", "chorei", "feliz", "triste"]
322
- kw_break = ["nunca", "de repente", "contraintuitivo", "virada"]
323
- kw_learn = ["aprendi", "descobri", "lição", "entendi"]
324
- kw_viral = ["segredo", "verdade", "3 passos"]
325
-
326
- for kw in kw_emotion:
327
- if kw in t:
328
- score += weights["emotion"]
329
- for kw in kw_break:
330
- if kw in t:
331
- score += weights["break"]
332
- for kw in kw_learn:
333
- if kw in t:
334
- score += weights["learn"]
335
- for kw in kw_viral:
336
- if kw in t:
337
- score += weights["viral"]
338
-
339
- if custom_keywords.strip():
340
- for kw in custom_keywords.split(","):
341
- kw = kw.strip().lower()
342
- if kw and kw in t:
343
- score += 3.0
344
-
345
- return score
346
 
347
- # ============ MAIN SELECTION LOGIC ============
348
- def select_segments(transcript_txt: str, use_llm: bool, num_segments: int,
349
  custom_keywords: str, manual_timecodes: str, natural_instructions: str,
350
- weight_emotion: float, weight_break: float,
351
  weight_learn: float, weight_viral: float) -> List[Segment]:
352
- """Função principal de seleção"""
353
-
354
- print("\n" + "="*60)
355
- print("INICIANDO SELEÇÃO DE SEGMENTOS")
356
- print("="*60)
357
-
358
- # Prioridade 1: Timecodes manuais
359
- manual_ranges = parse_manual_timecodes(manual_timecodes)
360
- if manual_ranges:
361
- print(f"✓ Modo: MANUAL - {len(manual_ranges)} ranges")
362
- result_segs = []
363
- for start_tc, end_tc in manual_ranges:
364
  try:
365
- start_f = parse_timecode_to_frames(start_tc)
366
- end_f = parse_timecode_to_frames(end_tc)
367
- if end_f > start_f:
368
- result_segs.append(Segment(
369
- start_tc=start_tc,
370
- end_tc=end_tc,
371
- start_f=start_f,
372
- end_f=end_f,
373
- text=f"Corte manual: {start_tc} - {end_tc}",
374
- score=100.0
375
- ))
376
- except Exception as e:
377
- print(f"⚠ Erro: {e}")
378
- return result_segs if result_segs else []
379
 
380
  # Parse transcrição
381
- segs = parse_transcript_full(transcript_txt)
382
 
383
  if not segs:
384
- raise ValueError("Nenhum segmento válido encontrado. Formato esperado: 00:00:00:00 - 00:00:10:00 Texto")
385
 
386
- # Prioridade 2: Instruções em linguagem natural
387
- if natural_instructions.strip():
388
- print(f" Modo: LINGUAGEM NATURAL")
389
- print(f" Instruções: {natural_instructions[:100]}...")
390
- print(f" IA disponível: {LLM_AVAILABLE}")
391
-
392
- # Funciona mesmo sem IA, usando keywords
393
- return ai_select_segments(segs, natural_instructions)
394
-
395
- # Prioridade 3: Modo automático com pontuação
396
- print(f"✓ Modo: AUTOMÁTICO por pontuação")
397
- weights = {
398
- "emotion": weight_emotion,
399
- "break": weight_break,
400
- "learn": weight_learn,
401
- "viral": weight_viral
402
- }
403
 
404
  for s in segs:
405
- s.score = keyword_score(s.text, custom_keywords, weights)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
 
407
  segs.sort(key=lambda x: x.score, reverse=True)
408
  return segs[:num_segments]
409
 
410
  # ============ XML EDITING ============
411
- def edit_sequence_with_segments(tree: ET.ElementTree, segs: List[Segment]) -> ET.ElementTree:
412
- """Edita a sequência do XML com os segmentos"""
 
 
 
 
 
 
 
413
  root = tree.getroot()
414
  seq = root.find(".//sequence")
415
 
416
  if seq is None:
417
- raise ValueError("Nenhuma <sequence> encontrada no XML")
418
 
419
- video_track = seq.find("./media/video/track")
420
- audio_track = seq.find("./media/audio/track")
421
 
422
- if not video_track or not audio_track:
423
- raise ValueError("Estrutura de trilhas não encontrada no XML")
424
 
425
- v_tpl = video_track.find("./clipitem")
426
- a_tpl = audio_track.find("./clipitem")
427
 
428
- if v_tpl is None or a_tpl is None:
429
- raise ValueError("Clipitem template não encontrado")
430
-
431
- def deep_copy(elem):
432
- new = ET.Element(elem.tag, attrib=elem.attrib)
433
- new.text = elem.text
434
- new.tail = elem.tail
435
- for child in list(elem):
436
- new.append(deep_copy(child))
437
- return new
438
-
439
- # Limpa trilhas
440
- for ci in list(video_track.findall("./clipitem")):
441
- video_track.remove(ci)
442
- for ci in list(audio_track.findall("./clipitem")):
443
- audio_track.remove(ci)
444
 
445
  # Adiciona novos clips
446
- cursor = 0
447
 
448
- for idx, seg in enumerate(segs, start=1):
449
  duration = seg.end_f - seg.start_f
450
- start = cursor
451
- end = cursor + duration
452
-
453
- v_id = f"clip-v-{idx}"
454
- a_id = f"clip-a-{idx}"
455
-
456
- # Video clip
457
- v_ci = ET.Element("clipitem", {"id": v_id})
458
- v_name = ET.SubElement(v_ci, "name")
459
- v_name.text = f"Clip {idx}"
460
-
461
- v_rate = deep_copy(v_tpl.find("rate"))
462
- v_ci.append(v_rate)
463
-
464
- ET.SubElement(v_ci, "start").text = str(start)
465
- ET.SubElement(v_ci, "end").text = str(end)
466
- ET.SubElement(v_ci, "in").text = str(seg.start_f)
467
- ET.SubElement(v_ci, "out").text = str(seg.end_f)
468
-
469
- v_file = deep_copy(v_tpl.find("./file"))
470
- if v_file is not None:
471
- v_ci.append(v_file)
472
-
473
- v_link = ET.SubElement(v_ci, "link")
474
- ET.SubElement(v_link, "linkclipref").text = a_id
475
-
476
- # Audio clip
477
- a_ci = ET.Element("clipitem", {"id": a_id})
478
- a_name = ET.SubElement(a_ci, "name")
479
- a_name.text = f"Clip {idx}"
480
 
481
- a_rate = deep_copy(a_tpl.find("rate"))
482
- a_ci.append(a_rate)
483
-
484
- ET.SubElement(a_ci, "start").text = str(start)
485
- ET.SubElement(a_ci, "end").text = str(end)
486
- ET.SubElement(a_ci, "in").text = str(seg.start_f)
487
- ET.SubElement(a_ci, "out").text = str(seg.end_f)
488
-
489
- a_file = deep_copy(a_tpl.find("./file"))
490
- if a_file is not None:
491
- a_ci.append(a_file)
492
-
493
- a_link = ET.SubElement(a_ci, "link")
494
- ET.SubElement(a_link, "linkclipref").text = v_id
495
-
496
- video_track.append(v_ci)
497
- audio_track.append(a_ci)
498
-
499
- cursor = end
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
 
501
  return tree
502
 
503
- # ============ GRADIO INTERFACE ============
504
- def process_xml_and_transcript(xml_file, txt_file, use_llm, num_segments,
505
- custom_keywords, manual_timecodes, natural_instructions,
506
- weight_emotion, weight_break, weight_learn, weight_viral):
507
- """Processa XML e transcrição"""
508
 
509
  if not xml_file:
510
- return "❌ Envie o arquivo XML do Premiere", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
511
-
512
- manual_ranges = parse_manual_timecodes(manual_timecodes)
513
- has_instructions = natural_instructions.strip() != ""
514
 
515
- # Determina modo
516
- if manual_ranges:
517
- mode = "MANUAL"
518
  transcript = ""
519
- elif has_instructions:
520
- mode = "IA (Linguagem Natural)" if (use_llm and LLM_AVAILABLE) else "Linguagem Natural (sem IA)"
521
- if not txt_file:
522
- return "❌ Para usar linguagem natural, envie a transcrição (.txt)", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
523
 
524
- with open(txt_file.name, "r", encoding="utf-8") as f:
525
- transcript = f.read()
526
- else:
527
- mode = "AUTOMÁTICO"
528
- if not txt_file:
529
- return "❌ Envie a transcrição (.txt)", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
530
 
531
- with open(txt_file.name, "r", encoding="utf-8") as f:
532
- transcript = f.read()
533
-
534
- try:
535
  # Seleciona segmentos
536
- segs = select_segments(
537
  transcript, use_llm and LLM_AVAILABLE, num_segments,
538
  custom_keywords, manual_timecodes, natural_instructions,
539
  weight_emotion, weight_break, weight_learn, weight_viral
540
  )
541
 
542
- if not segs:
543
- return "❌ Nenhum segmento foi selecionado", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
544
 
545
  # Edita XML
546
  tree = ET.parse(xml_file.name)
547
- tree = edit_sequence_with_segments(tree, segs)
548
-
549
- # Salva resultado
550
- base = os.path.splitext(os.path.basename(xml_file.name))[0]
551
- out_path = os.path.join(OUTPUT_DIR, f"{base}_EDITADO.xml")
552
- tree.write(out_path, encoding="utf-8", xml_declaration=True)
553
 
554
- # Gera resumo
555
- total_duration = sum((s.end_f - s.start_f) / FPS for s in segs)
 
 
556
 
557
- resumo = f"✅ {len(segs)} corte(s) criado(s) | Duração total: {total_duration/60:.1f} min | Modo: {mode}\n\n"
 
 
558
 
559
- for i, s in enumerate(segs, 1):
560
- dur = (s.end_f - s.start_f) / FPS
561
- resumo += f"{i}. {s.start_tc} → {s.end_tc} ({dur/60:.1f} min / {dur:.0f}s)\n"
562
- if s.text and not manual_ranges:
563
- resumo += f" {s.text[:150]}\n"
564
- resumo += "\n"
565
 
566
- status = f"✅ Sucesso! | Modo: {mode} | Duração: {total_duration/60:.1f} min | LLM: {'✓' if LLM_AVAILABLE else '✗'}"
567
 
568
- print(f"\n{status}\n")
 
 
 
 
 
569
 
570
- return resumo, out_path, status
571
 
 
 
572
  except Exception as e:
573
  import traceback
574
- error_detail = traceback.format_exc()
575
- print(f"\nERRO:\n{error_detail}\n")
576
- return f"❌ Erro: {str(e)}\n\nDetalhes no console", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
577
 
578
- # ============ CSS & GRADIO APP ============
579
- css = """
580
- :root { --primary: #39FF14; --text: #1a1a1a; --muted: #6b7280; }
581
- .gradio-container { font-family: 'Inter', system-ui, sans-serif !important; }
582
- .gradio-container h1, .gradio-container label { color: var(--text) !important; font-weight: 600 !important; }
583
- .gradio-container button.primary {
584
- background: var(--primary) !important;
585
- color: #000 !important;
586
- font-weight: 700 !important;
587
- border-radius: 8px !important;
588
- }
589
- .gradio-container .block { border-radius: 12px !important; }
590
- """
591
-
592
- with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Editor XML Premiere") as demo:
593
  gr.Markdown("# 🎬 Editor XML Premiere - IA")
594
- gr.Markdown("Cortes inteligentes com linguagem natural | Powered by Gemini AI")
595
 
596
  with gr.Row():
597
- with gr.Column():
598
- xml_in = gr.File(label="📁 XML do Premiere (FCP XML)", file_types=[".xml"])
599
- txt_in = gr.File(label="📄 Transcrição com timecodes (.txt)", file_types=[".txt"])
600
-
601
- with gr.Column():
602
- use_llm = gr.Checkbox(
603
- label="🤖 Usar IA (Gemini)",
604
- value=USE_LLM_DEFAULT and LLM_AVAILABLE,
605
- info="Requer GEMINI_API_KEY configurada" if not LLM_AVAILABLE else "IA configurada ✓"
606
- )
607
- num_segments = gr.Slider(
608
- 2, 20, 5, step=1,
609
- label="Número de segmentos (modo automático)"
610
- )
611
 
612
- with gr.Accordion("💬 IA - Linguagem Natural (RECOMENDADO)", open=True):
 
 
 
 
613
  gr.Markdown("""
614
- **Exemplos de comandos que funcionam:**
615
  - `Extraia um corte de 10 minutos começando da parte do tenista`
616
- - `Crie um corte de 15 minutos com os melhores momentos`
617
- - `Faça um corte de 5 minutos sobre Maria e José`
618
- - `Corte de 8 minutos a partir de onde fala sobre protocolo`
619
-
620
- **IMPORTANTE:** Sempre especifique a duração desejada (ex: "10 minutos")
621
  """)
622
  natural_instructions = gr.Textbox(
623
  label="Suas instruções",
624
- placeholder='Ex: "Extraia um corte de 10 minutos começando da parte do tenista"',
625
- lines=3
626
  )
627
 
628
  with gr.Accordion("⏱️ Minutagens Manuais", open=False):
629
- gr.Markdown("**Formato:** `00:01:23:15 - 00:02:45:10` (um por linha)")
630
  manual_timecodes = gr.Textbox(
631
- label="Timecodes exatos",
632
- placeholder="00:01:23:15 - 00:02:45:10\n00:05:30:00 - 00:07:15:22",
633
- lines=4
634
  )
635
 
636
- with gr.Accordion("⚙️ Modo Automático (Palavras-chave)", open=False):
637
- custom_keywords = gr.Textbox(
638
- label="Palavras-chave personalizadas (separadas por vírgula)",
639
- placeholder="transformação, resultado, superação"
640
- )
641
  with gr.Row():
642
- weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="Peso: Emoção")
643
- weight_break = gr.Slider(0, 5, 1.5, 0.1, label="Peso: Quebra")
644
  with gr.Row():
645
- weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="Peso: Aprendizado")
646
- weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="Peso: Viral")
647
 
648
- run_btn = gr.Button("🚀 Processar e Gerar XML Editado", variant="primary", size="lg")
649
-
650
- gr.Markdown("---")
651
 
652
  with gr.Row():
653
  with gr.Column(scale=2):
654
- resumo_out = gr.Textbox(label="📊 Resumo dos Cortes", lines=15, show_copy_button=True)
655
  with gr.Column(scale=1):
656
  status_out = gr.Textbox(label="Status")
657
- file_out = gr.File(label="⬇️ Download do XML Editado")
658
-
659
- run_btn.click(
660
- process_xml_and_transcript,
661
- inputs=[xml_in, txt_in, use_llm, num_segments, custom_keywords,
662
- manual_timecodes, natural_instructions,
663
- weight_emotion, weight_break, weight_learn, weight_viral],
664
- outputs=[resumo_out, file_out, status_out]
665
  )
666
-
667
- gr.Markdown("""
668
- ---
669
- **💡 Dicas:**
670
- - Formato da transcrição: `00:00:00:00 - 00
 
17
  genai.configure(api_key=GEMINI_API_KEY)
18
  LLM = genai.GenerativeModel(LLM_MODEL_NAME)
19
  LLM_AVAILABLE = True
 
20
  else:
21
  LLM = None
22
+ except Exception:
 
23
  LLM = None
24
  LLM_AVAILABLE = False
 
25
 
26
  # Config
27
  FPS = 24
 
39
 
40
  # ============ TIMECODE FUNCTIONS ============
41
  def parse_timecode_to_frames(tc: str, fps: int = FPS) -> int:
 
42
  tc = tc.strip()
43
+ m = re.match(r'^(\d{2}):(\d{2}):(\d{2})[:;](\d{2})$', tc)
44
  if not m:
45
  raise ValueError(f"Timecode inválido: {tc}")
46
  hh, mm, ss, ff = map(int, m.groups())
47
  return hh*3600*fps + mm*60*fps + ss*fps + ff
48
 
49
  def frames_to_timecode(frames: int, fps: int = FPS) -> str:
 
50
  hh = frames // (3600*fps)
51
  rem = frames % (3600*fps)
52
  mm = rem // (60*fps)
 
56
  return f"{hh:02d}:{mm:02d}:{ss:02d}:{ff:02d}"
57
 
58
  # ============ TRANSCRIPT PARSING ============
59
+ def parse_transcript(txt: str) -> List[Segment]:
60
+ """Parse transcrição - aceita vários formatos"""
61
  if not txt or not txt.strip():
62
+ print("⚠️ Transcrição vazia")
63
  return []
64
 
65
+ lines = txt.strip().splitlines()
66
+ results = []
67
 
68
+ # Regex flexível
69
  pattern = re.compile(
70
+ r'^\s*\[?\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})\s*[-—–]\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})\s*\]?\s*(.*)$',
71
+ re.IGNORECASE
72
  )
73
 
74
+ for idx, line in enumerate(lines):
75
  line = line.strip()
76
 
77
+ if not line or line.lower() == "desconhecido":
78
  continue
79
 
80
  match = pattern.match(line)
 
83
  start_tc, end_tc, text = match.groups()
84
  text = text.strip()
85
 
86
+ if not text or text.lower() == "desconhecido":
87
  continue
88
 
89
  try:
 
100
  score=0.0
101
  ))
102
  except Exception as e:
103
+ print(f"⚠️ Erro linha {idx}: {str(e)}")
104
  continue
105
 
106
+ print(f" {len(results)} segmentos encontrados")
107
  return results
108
 
109
  # ============ MANUAL TIMECODES ============
110
  def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
 
111
  if not manual_input or not manual_input.strip():
112
  return []
113
 
114
  manual_ranges = []
115
+ lines = manual_input.replace(",", "\n").splitlines()
 
116
 
117
+ pattern = re.compile(r'(\d{2}:\d{2}:\d{2}[:;]\d{2})\s*[-–—]\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})')
118
 
119
  for line in lines:
120
+ m = pattern.search(line.strip())
121
  if m:
122
+ manual_ranges.append((m.group(1), m.group(2)))
 
123
 
124
  return manual_ranges
125
 
126
+ # ============ AI HELPERS ============
127
+ def extract_duration_minutes(text: str) -> Optional[float]:
128
+ """Extrai duração em minutos"""
129
+ text_lower = text.lower()
130
 
131
+ patterns = [
 
 
132
  r'(\d+)\s*minutos?',
133
  r'(\d+)\s*min\b',
134
  r'(\d+)m\b',
135
  r'corte\s+de\s+(\d+)'
136
  ]
137
 
138
+ for pattern in patterns:
139
+ match = re.search(pattern, text_lower)
140
  if match:
141
+ return float(match.group(1))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
+ return None
 
144
 
145
+ def find_keyword_in_segments(segs: List[Segment], keywords: List[str]) -> int:
146
+ """Busca simples por palavras-chave"""
147
  if not keywords:
148
  return 0
149
 
 
152
 
153
  for idx, seg in enumerate(segs):
154
  text_lower = seg.text.lower()
155
+ score = sum(1 for kw in keywords if kw.lower() in text_lower)
156
 
157
  if score > best_score:
158
  best_score = score
159
  best_idx = idx
160
 
 
161
  return best_idx
162
 
163
+ def create_continuous_segment(segs: List[Segment], start_idx: int, duration_min: float) -> Segment:
164
+ """Cria um segmento contínuo"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  if start_idx >= len(segs):
166
  start_idx = 0
167
 
 
 
168
  start_seg = segs[start_idx]
169
  start_frame = start_seg.start_f
170
+ duration_frames = int(duration_min * 60 * FPS)
171
+ end_frame = start_frame + duration_frames
172
+
173
+ # Pega texto dos primeiros segmentos
174
+ text_parts = []
175
+ for seg in segs[start_idx:min(start_idx+10, len(segs))]:
176
+ text_parts.append(seg.text[:80])
177
+
178
+ combined_text = " ".join(text_parts)[:300]
179
+
180
+ return Segment(
 
 
 
 
 
 
 
 
 
181
  start_tc=frames_to_timecode(start_frame),
182
  end_tc=frames_to_timecode(end_frame),
183
  start_f=start_frame,
184
  end_f=end_frame,
185
+ text=f"Corte contínuo ({duration_min}min): {combined_text}",
186
  score=100.0
187
  )
 
 
 
188
 
189
+ def process_with_ai(segs: List[Segment], instructions: str) -> List[Segment]:
190
+ """Processa com IA"""
 
 
191
 
192
+ # Extrai duração
193
+ duration = extract_duration_minutes(instructions)
194
 
195
+ # Identifica palavras-chave importantes
196
+ keywords = []
197
+ text_lower = instructions.lower()
198
 
199
+ keyword_map = {
200
+ 'tenista': ['tenista', 'tênis', 'tenis', 'jogador', 'kinguios'],
201
+ 'maria': ['maria', 'josé', 'jose', 'casal', 'seguro'],
202
+ 'protocolo': ['protocolo', 'rodar', 'dependência', 'dependencia'],
203
+ }
 
204
 
205
+ for key, terms in keyword_map.items():
206
+ if any(term in text_lower for term in terms):
207
+ keywords.extend(terms)
208
+
209
+ print(f"📊 Duração: {duration}min | Keywords: {keywords[:3]}")
210
+
211
+ # Encontra ponto de início
212
+ start_idx = 0
213
+
214
+ if LLM_AVAILABLE and keywords:
215
+ try:
216
+ # Cria preview dos segmentos
217
+ preview = []
218
+ for i, s in enumerate(segs[:100]):
219
+ preview.append(f"{i}|{s.start_tc}|{s.text[:60]}")
220
+
221
+ preview_text = "\n".join(preview[:80])
222
+
223
+ prompt = f"""Encontre o índice onde começa o assunto solicitado.
224
+
225
+ BUSCAR: {' '.join(keywords[:3])}
226
+
227
+ SEGMENTOS (formato: índice|timecode|texto):
228
+ {preview_text}
229
 
230
+ Retorne APENAS o número do índice (exemplo: 42)"""
231
+
232
+ response = LLM.generate_content(
233
+ prompt,
234
+ generation_config={"temperature": 0.1, "max_output_tokens": 20}
235
+ )
236
+
237
+ text = (response.text or "").strip()
238
+ match = re.search(r'\b(\d+)\b', text)
239
+
240
+ if match:
241
+ idx = int(match.group(1))
242
+ if 0 <= idx < len(segs):
243
+ start_idx = idx
244
+ print(f"✅ IA encontrou: segmento {start_idx} ({segs[start_idx].start_tc})")
245
+
246
+ except Exception as e:
247
+ print(f"⚠️ IA falhou: {e}")
248
+
249
+ # Fallback: busca por keywords
250
+ if start_idx == 0 and keywords:
251
+ start_idx = find_keyword_in_segments(segs, keywords)
252
+ print(f"✅ Busca por keyword: segmento {start_idx} ({segs[start_idx].start_tc})")
253
+
254
+ # Cria corte
255
+ if duration:
256
+ result = create_continuous_segment(segs, start_idx, duration)
257
+ print(f"✅ Corte: {result.start_tc} → {result.end_tc} ({duration}min)")
258
+ return [result]
259
+ else:
260
+ # Sem duração: retorna múltiplos segmentos
261
+ return segs[start_idx:start_idx+10]
 
 
262
 
263
+ # ============ MAIN SELECTION ============
264
+ def select_segments(transcript_txt: str, use_llm: bool, num_segments: int,
265
  custom_keywords: str, manual_timecodes: str, natural_instructions: str,
266
+ weight_emotion: float, weight_break: float,
267
  weight_learn: float, weight_viral: float) -> List[Segment]:
268
+
269
+ # Prioridade 1: Manual
270
+ manual = parse_manual_timecodes(manual_timecodes)
271
+ if manual:
272
+ print(f"🔧 Modo MANUAL: {len(manual)} cortes")
273
+ result = []
274
+ for start_tc, end_tc in manual:
 
 
 
 
 
275
  try:
276
+ result.append(Segment(
277
+ start_tc=start_tc,
278
+ end_tc=end_tc,
279
+ start_f=parse_timecode_to_frames(start_tc),
280
+ end_f=parse_timecode_to_frames(end_tc),
281
+ text=f"Manual: {start_tc}-{end_tc}",
282
+ score=100.0
283
+ ))
284
+ except:
285
+ pass
286
+ return result
 
 
 
287
 
288
  # Parse transcrição
289
+ segs = parse_transcript(transcript_txt)
290
 
291
  if not segs:
292
+ raise ValueError("Nenhum segmento encontrado. Formato esperado: 00:00:00:00 - 00:00:10:00 Texto")
293
 
294
+ # Prioridade 2: IA com linguagem natural
295
+ if natural_instructions.strip() and use_llm:
296
+ print("🤖 Modo IA")
297
+ return process_with_ai(segs, natural_instructions)
298
+
299
+ # Prioridade 3: Automático por score
300
+ print("⚙️ Modo AUTOMÁTICO")
 
 
 
 
 
 
 
 
 
 
301
 
302
  for s in segs:
303
+ score = 0
304
+ text = s.text.lower()
305
+
306
+ if "medo" in text or "coragem" in text:
307
+ score += weight_emotion
308
+ if "nunca" in text or "de repente" in text:
309
+ score += weight_break
310
+ if "aprendi" in text or "descobri" in text:
311
+ score += weight_learn
312
+ if "segredo" in text or "verdade" in text:
313
+ score += weight_viral
314
+
315
+ if custom_keywords:
316
+ for kw in custom_keywords.split(","):
317
+ if kw.strip().lower() in text:
318
+ score += 3.0
319
+
320
+ s.score = score
321
 
322
  segs.sort(key=lambda x: x.score, reverse=True)
323
  return segs[:num_segments]
324
 
325
  # ============ XML EDITING ============
326
+ def deep_copy_element(elem: ET.Element) -> ET.Element:
327
+ new = ET.Element(elem.tag, attrib=dict(elem.attrib))
328
+ new.text = elem.text
329
+ new.tail = elem.tail
330
+ for child in elem:
331
+ new.append(deep_copy_element(child))
332
+ return new
333
+
334
+ def edit_xml(tree: ET.ElementTree, segs: List[Segment]) -> ET.ElementTree:
335
  root = tree.getroot()
336
  seq = root.find(".//sequence")
337
 
338
  if seq is None:
339
+ raise ValueError("Sequence não encontrada no XML")
340
 
341
+ v_track = seq.find(".//media/video/track")
342
+ a_track = seq.find(".//media/audio/track")
343
 
344
+ if not v_track or not a_track:
345
+ raise ValueError("Trilhas de vídeo/áudio não encontradas")
346
 
347
+ v_template = v_track.find("./clipitem")
348
+ a_template = a_track.find("./clipitem")
349
 
350
+ # Limpa clips existentes
351
+ for clip in list(v_track.findall("./clipitem")):
352
+ v_track.remove(clip)
353
+ for clip in list(a_track.findall("./clipitem")):
354
+ a_track.remove(clip)
 
 
 
 
 
 
 
 
 
 
 
355
 
356
  # Adiciona novos clips
357
+ timeline_pos = 0
358
 
359
+ for i, seg in enumerate(segs, 1):
360
  duration = seg.end_f - seg.start_f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
 
362
+ # Vídeo clip
363
+ v_clip = ET.Element("clipitem", {"id": f"clip-v{i}"})
364
+ ET.SubElement(v_clip, "name").text = f"Clip {i}"
365
+ ET.SubElement(v_clip, "start").text = str(timeline_pos)
366
+ ET.SubElement(v_clip, "end").text = str(timeline_pos + duration)
367
+ ET.SubElement(v_clip, "in").text = str(seg.start_f)
368
+ ET.SubElement(v_clip, "out").text = str(seg.end_f)
369
+
370
+ if v_template is not None:
371
+ rate = v_template.find("rate")
372
+ if rate is not None:
373
+ v_clip.append(deep_copy_element(rate))
374
+ file_elem = v_template.find("file")
375
+ if file_elem is not None:
376
+ v_clip.append(deep_copy_element(file_elem))
377
+
378
+ # Áudio clip
379
+ a_clip = ET.Element("clipitem", {"id": f"clip-a{i}"})
380
+ ET.SubElement(a_clip, "name").text = f"Clip {i}"
381
+ ET.SubElement(a_clip, "start").text = str(timeline_pos)
382
+ ET.SubElement(a_clip, "end").text = str(timeline_pos + duration)
383
+ ET.SubElement(a_clip, "in").text = str(seg.start_f)
384
+ ET.SubElement(a_clip, "out").text = str(seg.end_f)
385
+
386
+ if a_template is not None:
387
+ rate = a_template.find("rate")
388
+ if rate is not None:
389
+ a_clip.append(deep_copy_element(rate))
390
+ file_elem = a_template.find("file")
391
+ if file_elem is not None:
392
+ a_clip.append(deep_copy_element(file_elem))
393
+
394
+ v_track.append(v_clip)
395
+ a_track.append(a_clip)
396
+
397
+ timeline_pos += duration
398
 
399
  return tree
400
 
401
+ # ============ GRADIO ============
402
+ def process_files(xml_file, txt_file, use_llm, num_segments,
403
+ custom_keywords, manual_timecodes, natural_instructions,
404
+ weight_emotion, weight_break, weight_learn, weight_viral):
 
405
 
406
  if not xml_file:
407
+ return "❌ Envie o XML", None, f"LLM: {LLM_AVAILABLE}"
 
 
 
408
 
409
+ try:
410
+ # Lê transcrição se necessário
 
411
  transcript = ""
412
+ manual = parse_manual_timecodes(manual_timecodes)
 
 
 
413
 
414
+ if not manual:
415
+ if not txt_file:
416
+ return "❌ Envie a transcrição (.txt)", None, f"LLM: {LLM_AVAILABLE}"
417
+
418
+ with open(txt_file.name, "r", encoding="utf-8") as f:
419
+ transcript = f.read()
420
 
 
 
 
 
421
  # Seleciona segmentos
422
+ segments = select_segments(
423
  transcript, use_llm and LLM_AVAILABLE, num_segments,
424
  custom_keywords, manual_timecodes, natural_instructions,
425
  weight_emotion, weight_break, weight_learn, weight_viral
426
  )
427
 
428
+ if not segments:
429
+ return "❌ Nenhum segmento selecionado", None, f"LLM: {LLM_AVAILABLE}"
430
 
431
  # Edita XML
432
  tree = ET.parse(xml_file.name)
433
+ tree = edit_xml(tree, segments)
 
 
 
 
 
434
 
435
+ # Salva
436
+ basename = os.path.splitext(os.path.basename(xml_file.name))[0]
437
+ output = os.path.join(OUTPUT_DIR, f"{basename}_EDITADO.xml")
438
+ tree.write(output, encoding="utf-8", xml_declaration=True)
439
 
440
+ # Resumo
441
+ total_sec = sum((s.end_f - s.start_f) / FPS for s in segments)
442
+ total_min = total_sec / 60
443
 
444
+ mode = "MANUAL" if manual else ("IA" if natural_instructions.strip() else "AUTOMÁTICO")
 
 
 
 
 
445
 
446
+ summary = f"✅ {len(segments)} corte(s) | {total_min:.1f} min total | Modo: {mode}\n\n"
447
 
448
+ for i, seg in enumerate(segments, 1):
449
+ dur_sec = (seg.end_f - seg.start_f) / FPS
450
+ summary += f"{i}. {seg.start_tc} → {seg.end_tc} ({dur_sec/60:.1f}min)\n"
451
+ if seg.text and len(seg.text) > 50:
452
+ summary += f" {seg.text[:120]}...\n"
453
+ summary += "\n"
454
 
455
+ status = f"✅ Sucesso | {mode} | {total_min:.1f}min | LLM: {LLM_AVAILABLE}"
456
 
457
+ return summary, output, status
458
+
459
  except Exception as e:
460
  import traceback
461
+ traceback.print_exc()
462
+ return f"❌ Erro: {str(e)}", None, f"LLM: {LLM_AVAILABLE}"
 
463
 
464
+ # ============ UI ============
465
+ with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  gr.Markdown("# 🎬 Editor XML Premiere - IA")
467
+ gr.Markdown("Cortes inteligentes com linguagem natural")
468
 
469
  with gr.Row():
470
+ xml_in = gr.File(label="📁 XML do Premiere", file_types=[".xml"])
471
+ txt_in = gr.File(label="📄 Transcrição (.txt)", file_types=[".txt"])
 
 
 
 
 
 
 
 
 
 
 
 
472
 
473
+ with gr.Row():
474
+ use_llm = gr.Checkbox(label="🤖 Usar IA", value=USE_LLM_DEFAULT and LLM_AVAILABLE)
475
+ num_segments = gr.Slider(2, 20, 5, 1, label="Segmentos (automático)")
476
+
477
+ with gr.Accordion("💬 IA - Linguagem Natural", open=True):
478
  gr.Markdown("""
479
+ **Exemplos:**
480
  - `Extraia um corte de 10 minutos começando da parte do tenista`
481
+ - `Crie 15 minutos com os melhores momentos`
482
+ - `5 minutos sobre Maria e José`
 
 
 
483
  """)
484
  natural_instructions = gr.Textbox(
485
  label="Suas instruções",
486
+ placeholder='Ex: "10 minutos começando da parte do tenista"',
487
+ lines=2
488
  )
489
 
490
  with gr.Accordion("⏱️ Minutagens Manuais", open=False):
 
491
  manual_timecodes = gr.Textbox(
492
+ label="Timecodes (um por linha)",
493
+ placeholder="00:21:18:09 - 00:31:18:09",
494
+ lines=3
495
  )
496
 
497
+ with gr.Accordion("⚙️ Modo Automático", open=False):
498
+ custom_keywords = gr.Textbox(label="Palavras-chave")
 
 
 
499
  with gr.Row():
500
+ weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="Emoção")
501
+ weight_break = gr.Slider(0, 5, 1.5, 0.1, label="Quebra")
502
  with gr.Row():
503
+ weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="Aprendizado")
504
+ weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="Viral")
505
 
506
+ btn = gr.Button("🚀 Processar", variant="primary", size="lg")
 
 
507
 
508
  with gr.Row():
509
  with gr.Column(scale=2):
510
+ summary_out = gr.Textbox(label="📊 Resumo", lines=12)
511
  with gr.Column(scale=1):
512
  status_out = gr.Textbox(label="Status")
513
+ file_out = gr.File(label="⬇️ Download")
514
+
515
+ btn.click(
516
+ process_files,
517
+ [xml_in, txt_in, use_llm, num_segments, custom_keywords,
518
+ manual_timecodes, natural_instructions,
519
+ weight_emotion, weight_break, weight_learn, weight_viral],
520
+ [summary_out, file_out, status_out]
521
  )
522
+
523
+ if __name__ == "__main__":
524
+ demo.launch()