leicam commited on
Commit
21b6fcf
·
verified ·
1 Parent(s): 603b064

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +145 -203
app.py CHANGED
@@ -54,19 +54,13 @@ def frames_to_timecode(frames: int, fps: int = FPS) -> str:
54
  ff = rem % fps
55
  return f"{hh:02d}:{mm:02d}:{ss:02d}:{ff:02d}"
56
 
57
- def frames_to_seconds(frames: int, fps: int = FPS) -> float:
58
- return frames / fps
59
-
60
- def seconds_to_frames(seconds: float, fps: int = FPS) -> int:
61
- return int(seconds * fps)
62
-
63
  # ============ TRANSCRIPT PARSING ============
64
- def parse_transcript(txt: str) -> List[Segment]:
 
65
  lines = [l.strip() for l in txt.splitlines() if l.strip()]
66
  results: List[Segment] = []
67
 
68
- pat_range = re.compile(r"^\[?\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})\s*[-—]\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})\s*\]?\s+(.*)$")
69
- pat_point = re.compile(r"^(\d{2}:\d{2}:\d{2}[:;]\d{2})\s+(.*)$")
70
 
71
  for l in lines:
72
  m = pat_range.match(l)
@@ -79,18 +73,6 @@ def parse_transcript(txt: str) -> List[Segment]:
79
  results.append(Segment(s, e, s_f, e_f, text, 0.0))
80
  except Exception:
81
  continue
82
- continue
83
-
84
- m = pat_point.match(l)
85
- if m:
86
- s, text = m.groups()
87
- try:
88
- s_f = parse_timecode_to_frames(s)
89
- e_f = s_f + 4*FPS
90
- e = frames_to_timecode(e_f)
91
- results.append(Segment(s, e, s_f, e_f, text, 0.0))
92
- except Exception:
93
- continue
94
 
95
  return results
96
 
@@ -110,88 +92,12 @@ def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
110
 
111
  return manual_ranges
112
 
113
- # ============ SEGMENT PROCESSING ============
114
- def get_total_duration(segs: List[Segment]) -> float:
115
- """Retorna duração total em segundos"""
116
- return sum((s.end_f - s.start_f) / FPS for s in segs)
117
-
118
- def create_target_selection(segs: List[Segment], target_minutes: float, strategy: str = "distributed") -> List[Segment]:
119
- """
120
- Cria uma seleção de segmentos para atingir duração alvo.
121
- strategy: 'distributed' = espalhado pelo vídeo, 'sequential' = em sequência
122
- """
123
- target_seconds = target_minutes * 60
124
- total_available = get_total_duration(segs)
125
-
126
- if target_seconds > total_available:
127
- print(f"Aviso: Duração solicitada ({target_minutes:.1f}min) maior que disponível ({total_available/60:.1f}min)")
128
- return segs
129
-
130
- if strategy == "distributed":
131
- # Distribui seleção ao longo do vídeo
132
- ratio = target_seconds / total_available
133
- selected = []
134
- current_duration = 0
135
-
136
- # Seleciona proporcionalmente de cada parte
137
- for seg in segs:
138
- if current_duration >= target_seconds:
139
- break
140
- seg_duration = (seg.end_f - seg.start_f) / FPS
141
- if ratio >= 0.8 or (current_duration + seg_duration <= target_seconds * 1.1):
142
- selected.append(seg)
143
- current_duration += seg_duration
144
-
145
- return selected
146
-
147
- else: # sequential
148
- selected = []
149
- current_duration = 0
150
-
151
- for seg in segs:
152
- if current_duration >= target_seconds:
153
- break
154
- selected.append(seg)
155
- current_duration += (seg.end_f - seg.start_f) / FPS
156
-
157
- return selected
158
-
159
- def merge_close_segments(segs: List[Segment], max_gap_seconds: float = 3.0) -> List[Segment]:
160
- """Mescla segmentos que estão próximos um do outro"""
161
- if not segs:
162
- return []
163
-
164
- segs_sorted = sorted(segs, key=lambda x: x.start_f)
165
- merged = [segs_sorted[0]]
166
- max_gap_frames = int(max_gap_seconds * FPS)
167
-
168
- for current in segs_sorted[1:]:
169
- last = merged[-1]
170
- gap = current.start_f - last.end_f
171
-
172
- if gap <= max_gap_frames and gap >= 0:
173
- # Mescla os segmentos
174
- merged[-1] = Segment(
175
- start_tc=last.start_tc,
176
- end_tc=current.end_tc,
177
- start_f=last.start_f,
178
- end_f=current.end_f,
179
- text=last.text + " [...] " + current.text,
180
- score=(last.score + current.score) / 2
181
- )
182
- else:
183
- merged.append(current)
184
-
185
- return merged
186
-
187
  # ============ AI PROCESSING ============
188
  def extract_duration_from_instructions(instructions: str) -> float:
189
- """Extrai duração em minutos das instruções do usuário"""
190
- # Procura por padrões como "10 minutos", "5 min", "15 minutes"
191
  patterns = [
192
  r"(\d+)\s*minutos?",
193
  r"(\d+)\s*min\b",
194
- r"(\d+)\s*minutes?",
195
  r"(\d+)m\b"
196
  ]
197
 
@@ -202,53 +108,125 @@ def extract_duration_from_instructions(instructions: str) -> float:
202
 
203
  return None
204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  def ai_select_segments(segs: List[Segment], instructions: str) -> List[Segment]:
206
- """Usa IA para selecionar segmentos baseado em instruções"""
207
  if not LLM_AVAILABLE:
208
  raise ValueError("IA não disponível. Configure GEMINI_API_KEY")
209
 
210
- total_duration_min = get_total_duration(segs) / 60
211
  target_duration = extract_duration_from_instructions(instructions)
212
 
213
- # Cria resumo dos segmentos (agrupados para prompt menor)
214
- segment_summary = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  for i in range(0, len(segs), 5):
216
  group = segs[i:i+5]
217
  start_tc = group[0].start_tc
218
  end_tc = group[-1].end_tc
219
- duration = sum((s.end_f - s.start_f) / FPS for s in group)
220
  combined_text = " ".join([s.text[:100] for s in group])
221
- segment_summary.append(f"Grupo {i//5}: [{start_tc}-{end_tc}] ({duration:.0f}s) {combined_text[:200]}")
222
 
223
- prompt = f"""Você é um editor de vídeo profissional.
224
-
225
- INSTRUÇÕES DO USUÁRIO:
226
- {instructions}
227
 
228
- INFORMAÇÕES:
229
- - Total disponível: {total_duration_min:.1f} minutos ({len(segs)} segmentos)
230
- - Duração alvo detectada: {target_duration if target_duration else 'não especificada'} minutos
231
 
232
- SEGMENTOS (agrupados de 5 em 5):
233
- {chr(10).join(segment_summary[:50])}
234
 
235
- TAREFA:
236
- 1. Identifique quais GRUPOS de segmentos atendem às instruções
237
- 2. Se foi solicitada duração específica, selecione grupos suficientes para atingi-la
238
- 3. Distribua a seleção: pegue grupos do INÍCIO, MEIO e FIM do vídeo
239
- 4. Retorne os NÚMEROS dos grupos selecionados
240
-
241
- RESPONDA APENAS com números separados por vírgula (ex: 0,2,5,8,12,15,20,25,30)
242
- Selecione pelo menos 10-20 grupos para ter duração adequada."""
243
 
244
  try:
245
- response = LLM.generate_content(prompt, generation_config={"temperature": 0.4, "max_output_tokens": 500})
246
  txt = (response.text or "").strip()
247
 
248
- # Extrai números dos grupos
249
  group_indices = [int(x) for x in re.findall(r"\d+", txt)]
250
 
251
- # Converte grupos em segmentos individuais
252
  selected_segs = []
253
  for group_idx in group_indices:
254
  start_idx = group_idx * 5
@@ -257,36 +235,19 @@ Selecione pelo menos 10-20 grupos para ter duração adequada."""
257
  selected_segs.extend(segs[start_idx:end_idx])
258
 
259
  if not selected_segs:
260
- # Fallback: pega distribuído
261
  step = max(1, len(segs) // 30)
262
- selected_segs = segs[::step]
263
-
264
- # Remove duplicatas e ordena
265
- seen = set()
266
- unique_segs = []
267
- for seg in selected_segs:
268
- key = (seg.start_f, seg.end_f)
269
- if key not in seen:
270
- seen.add(key)
271
- unique_segs.append(seg)
272
-
273
- unique_segs.sort(key=lambda x: x.start_f)
274
-
275
- # Ajusta para duração alvo se especificada
276
- if target_duration:
277
- unique_segs = create_target_selection(unique_segs, target_duration, "distributed")
278
 
279
- # Mescla segmentos próximos
280
- final_segs = merge_close_segments(unique_segs, max_gap_seconds=3.0)
281
 
282
- final_duration = get_total_duration(final_segs)
283
- print(f"✓ Selecionados {len(final_segs)} trechos, duração total: {final_duration/60:.1f} min")
284
 
285
- return final_segs
286
 
287
  except Exception as e:
288
- print(f"Erro na IA: {e}")
289
- raise
290
 
291
  # ============ KEYWORD SCORING ============
292
  def keyword_score(text: str, custom_keywords: str = "", weights: dict = None) -> float:
@@ -294,10 +255,10 @@ def keyword_score(text: str, custom_keywords: str = "", weights: dict = None) ->
294
  weights = {"emotion": 2.0, "break": 1.5, "learn": 1.2, "viral": 1.0}
295
 
296
  t = text.lower()
297
- kw_emotion = ["medo", "coragem", "raiva", "chorei", "feliz", "triste", "emocion"]
298
  kw_break = ["nunca", "de repente", "contraintuitivo", "virada"]
299
- kw_learn = ["aprendi", "descobri", "lição", "entendi", "percebi"]
300
- kw_viral = ["segredo", "verdade", "3 passos", "como eu"]
301
 
302
  score = 0.0
303
  for kw in kw_emotion: score += weights["emotion"] if kw in t else 0.0
@@ -310,7 +271,6 @@ def keyword_score(text: str, custom_keywords: str = "", weights: dict = None) ->
310
  if kw.strip().lower() in t:
311
  score += 3.0
312
 
313
- score += 0.2 * text.count("!")
314
  return score
315
 
316
  # ============ MAIN SELECTION LOGIC ============
@@ -319,7 +279,7 @@ def select_segments(transcript_txt: str, use_llm: bool, num_segments: int,
319
  weight_emotion: float, weight_break: float,
320
  weight_learn: float, weight_viral: float) -> List[Segment]:
321
 
322
- # Priority 1: Manual timecodes
323
  manual_ranges = parse_manual_timecodes(manual_timecodes)
324
  if manual_ranges:
325
  result_segs = []
@@ -338,21 +298,17 @@ def select_segments(transcript_txt: str, use_llm: bool, num_segments: int,
338
  print(f"Erro: {e}")
339
  return result_segs if result_segs else []
340
 
341
- # Priority 2: AI with natural instructions
342
- segs = parse_transcript(transcript_txt)
343
  if not segs:
344
- raise ValueError("Nenhum trecho encontrado na transcrição")
345
 
 
346
  if natural_instructions.strip() and use_llm and LLM_AVAILABLE:
347
  return ai_select_segments(segs, natural_instructions)
348
 
349
  # Priority 3: Automatic scoring
350
- weights = {
351
- "emotion": weight_emotion,
352
- "break": weight_break,
353
- "learn": weight_learn,
354
- "viral": weight_viral
355
- }
356
 
357
  for s in segs:
358
  s.score = keyword_score(s.text, custom_keywords, weights)
@@ -453,21 +409,20 @@ def process_xml_and_transcript(xml_file, txt_file, use_llm, num_segments,
453
  custom_keywords, manual_timecodes, natural_instructions,
454
  weight_emotion, weight_break, weight_learn, weight_viral):
455
  if not xml_file:
456
- return "❌ Envie o XML do Premiere", None, f"LLM: {LLM_AVAILABLE}"
457
 
458
  manual_ranges = parse_manual_timecodes(manual_timecodes)
459
  has_instructions = natural_instructions.strip() != ""
460
 
461
- # Determine mode
462
  if manual_ranges:
463
  mode = "MANUAL"
464
  transcript = ""
465
  elif has_instructions:
466
  mode = "IA (Linguagem Natural)"
467
  if not txt_file:
468
- return "❌ Envie a transcrição para usar IA", None, f"LLM: {LLM_AVAILABLE}"
469
  if not LLM_AVAILABLE:
470
- return "❌ IA não disponível. Configure GEMINI_API_KEY", None, f"LLM: False"
471
  with open(txt_file.name, "r", encoding="utf-8") as f:
472
  transcript = f.read()
473
  else:
@@ -492,7 +447,7 @@ def process_xml_and_transcript(xml_file, txt_file, use_llm, num_segments,
492
  out_path = os.path.join(OUTPUT_DIR, f"{base}_EDITADO.xml")
493
  tree.write(out_path, encoding="utf-8", xml_declaration=True)
494
 
495
- total_duration = get_total_duration(segs)
496
 
497
  resumo = f"✂️ {len(segs)} cortes | Duração: {total_duration/60:.1f} min | Modo: {mode}\n\n"
498
  for i, s in enumerate(segs, 1):
@@ -502,36 +457,23 @@ def process_xml_and_transcript(xml_file, txt_file, use_llm, num_segments,
502
  resumo += f" {s.text[:120]}...\n"
503
  resumo += "\n"
504
 
505
- status = f"✓ {mode} | Duração total: {total_duration/60:.1f} min | LLM: {LLM_AVAILABLE}"
506
  return resumo, out_path, status
507
 
508
  except Exception as e:
509
  return f"❌ Erro: {str(e)}", None, f"LLM: {LLM_AVAILABLE}"
510
 
511
- # ============ CSS ============
512
  css = """
513
- :root {
514
- --primary: #39FF14;
515
- --text: #1a1a1a;
516
- --muted: #6b7280;
517
- }
518
- .gradio-container {
519
- font-family: system-ui, sans-serif !important;
520
- }
521
- .gradio-container h1, .gradio-container label {
522
- color: var(--text) !important;
523
- }
524
- .gradio-container button.primary {
525
- background: var(--primary) !important;
526
- color: #000 !important;
527
- font-weight: 700 !important;
528
- }
529
  """
530
 
531
- # ============ GRADIO APP ============
532
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
533
- gr.Markdown("# Agente de Edição XML - Premiere Pro")
534
- gr.Markdown("Edite sequências do Premiere com IA ou controle manual")
535
 
536
  with gr.Row():
537
  xml_in = gr.File(label="XML do Premiere", file_types=[".xml"])
@@ -541,34 +483,34 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
541
  use_llm = gr.Checkbox(label="Usar IA", value=USE_LLM_DEFAULT and LLM_AVAILABLE)
542
  num_segments = gr.Slider(2, 20, 5, step=1, label="Segmentos (modo automático)")
543
 
544
- with gr.Accordion("IA - Linguagem Natural (RECOMENDADO)", open=True):
545
  gr.Markdown("""
546
- **Use linguagem natural para dar instruções:**
547
- - "Crie um corte de 10 minutos com os melhores momentos"
548
- - "Extraia 15 minutos das partes mais engraçadas"
549
- - "Faça um resumo de 5 minutos sobre superação"
550
  """)
551
  natural_instructions = gr.Textbox(
552
- label="Instruções para a IA",
553
- placeholder='Ex: "Crie um corte de 10 minutos com os melhores momentos distribuídos pelo vídeo"',
554
  lines=3
555
  )
556
 
557
  with gr.Accordion("Minutagens Manuais", open=False):
558
  manual_timecodes = gr.Textbox(
559
- label="Timecodes exatos (um por linha)",
560
- placeholder="00:01:23:15 - 00:02:45:10\n00:05:30:00 - 00:07:15:22",
561
  lines=4
562
  )
563
 
564
- with gr.Accordion("Modo Automático (Palavras-chave)", open=False):
565
- custom_keywords = gr.Textbox(label="Palavras-chave personalizadas (separadas por vírgula)")
566
  with gr.Row():
567
- weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="Peso: Emoção")
568
- weight_break = gr.Slider(0, 5, 1.5, 0.1, label="Peso: Quebra")
569
  with gr.Row():
570
- weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="Peso: Aprendizado")
571
- weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="Peso: Viral")
572
 
573
  run_btn = gr.Button("Processar XML", variant="primary", size="lg")
574
 
 
54
  ff = rem % fps
55
  return f"{hh:02d}:{mm:02d}:{ss:02d}:{ff:02d}"
56
 
 
 
 
 
 
 
57
  # ============ TRANSCRIPT PARSING ============
58
+ def parse_transcript_full(txt: str) -> List[Segment]:
59
+ """Parse transcrição mantendo ranges originais"""
60
  lines = [l.strip() for l in txt.splitlines() if l.strip()]
61
  results: List[Segment] = []
62
 
63
+ pat_range = re.compile(r"^\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})\s*[-—]\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})\s+(.*)$")
 
64
 
65
  for l in lines:
66
  m = pat_range.match(l)
 
73
  results.append(Segment(s, e, s_f, e_f, text, 0.0))
74
  except Exception:
75
  continue
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  return results
78
 
 
92
 
93
  return manual_ranges
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  # ============ AI PROCESSING ============
96
  def extract_duration_from_instructions(instructions: str) -> float:
97
+ """Extrai duração em minutos das instruções"""
 
98
  patterns = [
99
  r"(\d+)\s*minutos?",
100
  r"(\d+)\s*min\b",
 
101
  r"(\d+)m\b"
102
  ]
103
 
 
108
 
109
  return None
110
 
111
+ def find_start_point_in_transcript(segs: List[Segment], instructions: str) -> int:
112
+ """Encontra o ponto de início baseado nas instruções"""
113
+ if not LLM_AVAILABLE:
114
+ return 0
115
+
116
+ # Cria resumo dos segmentos
117
+ segments_text = "\n".join([
118
+ f"{i}. [{s.start_tc}-{s.end_tc}] {s.text[:150]}"
119
+ for i, s in enumerate(segs[:100]) # Primeiros 100 para não sobrecarregar
120
+ ])
121
+
122
+ prompt = f"""Analise as instruções e encontre o índice do segmento onde deve COMEÇAR o corte.
123
+
124
+ INSTRUÇÕES: {instructions}
125
+
126
+ SEGMENTOS:
127
+ {segments_text}
128
+
129
+ RESPONDA APENAS com o NÚMERO do índice onde deve começar (exemplo: 45)
130
+ Não adicione explicações."""
131
+
132
+ try:
133
+ response = LLM.generate_content(prompt, generation_config={"temperature": 0.2})
134
+ txt = (response.text or "").strip()
135
+
136
+ # Extrai o primeiro número
137
+ match = re.search(r"\d+", txt)
138
+ if match:
139
+ idx = int(match.group())
140
+ if 0 <= idx < len(segs):
141
+ return idx
142
+ except Exception as e:
143
+ print(f"Erro ao buscar ponto inicial: {e}")
144
+
145
+ return 0
146
+
147
+ def create_continuous_cut(segs: List[Segment], start_idx: int, target_minutes: float) -> List[Segment]:
148
+ """Cria um corte contínuo de duração específica"""
149
+ if start_idx >= len(segs):
150
+ start_idx = 0
151
+
152
+ target_seconds = target_minutes * 60
153
+ target_frames = int(target_seconds * FPS)
154
+
155
+ start_segment = segs[start_idx]
156
+ start_frame = start_segment.start_f
157
+ end_frame = start_frame + target_frames
158
+
159
+ # Cria um único segmento contínuo
160
+ end_tc = frames_to_timecode(end_frame)
161
+
162
+ combined_text = " ".join([s.text for s in segs[start_idx:min(start_idx + 50, len(segs))]])[:500]
163
+
164
+ result = Segment(
165
+ start_tc=start_segment.start_tc,
166
+ end_tc=end_tc,
167
+ start_f=start_frame,
168
+ end_f=end_frame,
169
+ text=f"Corte contínuo: {combined_text}...",
170
+ score=100.0
171
+ )
172
+
173
+ return [result]
174
+
175
  def ai_select_segments(segs: List[Segment], instructions: str) -> List[Segment]:
176
+ """Usa IA para processar instruções em linguagem natural"""
177
  if not LLM_AVAILABLE:
178
  raise ValueError("IA não disponível. Configure GEMINI_API_KEY")
179
 
180
+ # Detecta se pede duração específica
181
  target_duration = extract_duration_from_instructions(instructions)
182
 
183
+ if target_duration:
184
+ # Modo: corte contínuo de X minutos
185
+ print(f"Modo: Corte contínuo de {target_duration} minutos")
186
+
187
+ # Encontra ponto de início
188
+ start_idx = find_start_point_in_transcript(segs, instructions)
189
+ print(f"Iniciando do segmento {start_idx}: {segs[start_idx].start_tc}")
190
+
191
+ # Cria corte contínuo
192
+ result = create_continuous_cut(segs, start_idx, target_duration)
193
+
194
+ duration_min = (result[0].end_f - result[0].start_f) / FPS / 60
195
+ print(f"✓ Corte criado: {result[0].start_tc} → {result[0].end_tc} ({duration_min:.1f} min)")
196
+
197
+ return result
198
+
199
+ else:
200
+ # Modo: seleção de múltiplos trechos
201
+ print("Modo: Seleção de múltiplos trechos")
202
+ return ai_select_multiple_segments(segs, instructions, num_segments=5)
203
+
204
+ def ai_select_multiple_segments(segs: List[Segment], instructions: str, num_segments: int = 5) -> List[Segment]:
205
+ """Seleciona múltiplos segmentos baseado em critérios"""
206
+ segments_summary = []
207
  for i in range(0, len(segs), 5):
208
  group = segs[i:i+5]
209
  start_tc = group[0].start_tc
210
  end_tc = group[-1].end_tc
 
211
  combined_text = " ".join([s.text[:100] for s in group])
212
+ segments_summary.append(f"Grupo {i//5}: [{start_tc}-{end_tc}] {combined_text[:200]}")
213
 
214
+ prompt = f"""Você é um editor profissional.
 
 
 
215
 
216
+ INSTRUÇÕES: {instructions}
 
 
217
 
218
+ SEGMENTOS (agrupados):
219
+ {chr(10).join(segments_summary[:40])}
220
 
221
+ Selecione 10-15 GRUPOS que atendem às instru��ões.
222
+ RESPONDA APENAS com números separados por vírgula (ex: 0,3,7,12,18,25)"""
 
 
 
 
 
 
223
 
224
  try:
225
+ response = LLM.generate_content(prompt, generation_config={"temperature": 0.4})
226
  txt = (response.text or "").strip()
227
 
 
228
  group_indices = [int(x) for x in re.findall(r"\d+", txt)]
229
 
 
230
  selected_segs = []
231
  for group_idx in group_indices:
232
  start_idx = group_idx * 5
 
235
  selected_segs.extend(segs[start_idx:end_idx])
236
 
237
  if not selected_segs:
 
238
  step = max(1, len(segs) // 30)
239
+ selected_segs = segs[::step][:num_segments]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
+ selected_segs.sort(key=lambda x: x.start_f)
 
242
 
243
+ duration = sum((s.end_f - s.start_f) / FPS for s in selected_segs)
244
+ print(f"✓ {len(selected_segs)} trechos selecionados, duração: {duration/60:.1f} min")
245
 
246
+ return selected_segs[:num_segments * 3] # Retorna mais segmentos
247
 
248
  except Exception as e:
249
+ print(f"Erro: {e}")
250
+ return segs[:num_segments]
251
 
252
  # ============ KEYWORD SCORING ============
253
  def keyword_score(text: str, custom_keywords: str = "", weights: dict = None) -> float:
 
255
  weights = {"emotion": 2.0, "break": 1.5, "learn": 1.2, "viral": 1.0}
256
 
257
  t = text.lower()
258
+ kw_emotion = ["medo", "coragem", "raiva", "chorei", "feliz", "triste"]
259
  kw_break = ["nunca", "de repente", "contraintuitivo", "virada"]
260
+ kw_learn = ["aprendi", "descobri", "lição", "entendi"]
261
+ kw_viral = ["segredo", "verdade", "3 passos"]
262
 
263
  score = 0.0
264
  for kw in kw_emotion: score += weights["emotion"] if kw in t else 0.0
 
271
  if kw.strip().lower() in t:
272
  score += 3.0
273
 
 
274
  return score
275
 
276
  # ============ MAIN SELECTION LOGIC ============
 
279
  weight_emotion: float, weight_break: float,
280
  weight_learn: float, weight_viral: float) -> List[Segment]:
281
 
282
+ # Priority 1: Manual
283
  manual_ranges = parse_manual_timecodes(manual_timecodes)
284
  if manual_ranges:
285
  result_segs = []
 
298
  print(f"Erro: {e}")
299
  return result_segs if result_segs else []
300
 
301
+ # Parse transcript
302
+ segs = parse_transcript_full(transcript_txt)
303
  if not segs:
304
+ raise ValueError("Nenhum trecho encontrado")
305
 
306
+ # Priority 2: AI with natural instructions
307
  if natural_instructions.strip() and use_llm and LLM_AVAILABLE:
308
  return ai_select_segments(segs, natural_instructions)
309
 
310
  # Priority 3: Automatic scoring
311
+ weights = {"emotion": weight_emotion, "break": weight_break, "learn": weight_learn, "viral": weight_viral}
 
 
 
 
 
312
 
313
  for s in segs:
314
  s.score = keyword_score(s.text, custom_keywords, weights)
 
409
  custom_keywords, manual_timecodes, natural_instructions,
410
  weight_emotion, weight_break, weight_learn, weight_viral):
411
  if not xml_file:
412
+ return "❌ Envie o XML", None, f"LLM: {LLM_AVAILABLE}"
413
 
414
  manual_ranges = parse_manual_timecodes(manual_timecodes)
415
  has_instructions = natural_instructions.strip() != ""
416
 
 
417
  if manual_ranges:
418
  mode = "MANUAL"
419
  transcript = ""
420
  elif has_instructions:
421
  mode = "IA (Linguagem Natural)"
422
  if not txt_file:
423
+ return "❌ Envie a transcrição", None, f"LLM: {LLM_AVAILABLE}"
424
  if not LLM_AVAILABLE:
425
+ return "❌ Configure GEMINI_API_KEY", None, f"LLM: False"
426
  with open(txt_file.name, "r", encoding="utf-8") as f:
427
  transcript = f.read()
428
  else:
 
447
  out_path = os.path.join(OUTPUT_DIR, f"{base}_EDITADO.xml")
448
  tree.write(out_path, encoding="utf-8", xml_declaration=True)
449
 
450
+ total_duration = sum((s.end_f - s.start_f) / FPS for s in segs)
451
 
452
  resumo = f"✂️ {len(segs)} cortes | Duração: {total_duration/60:.1f} min | Modo: {mode}\n\n"
453
  for i, s in enumerate(segs, 1):
 
457
  resumo += f" {s.text[:120]}...\n"
458
  resumo += "\n"
459
 
460
+ status = f"✓ {mode} | Duração: {total_duration/60:.1f} min | LLM: {LLM_AVAILABLE}"
461
  return resumo, out_path, status
462
 
463
  except Exception as e:
464
  return f"❌ Erro: {str(e)}", None, f"LLM: {LLM_AVAILABLE}"
465
 
466
+ # ============ CSS & GRADIO APP ============
467
  css = """
468
+ :root { --primary: #39FF14; --text: #1a1a1a; }
469
+ .gradio-container { font-family: system-ui, sans-serif !important; }
470
+ .gradio-container h1, .gradio-container label { color: var(--text) !important; }
471
+ .gradio-container button.primary { background: var(--primary) !important; color: #000 !important; font-weight: 700 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
472
  """
473
 
 
474
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
475
+ gr.Markdown("# Editor XML Premiere - IA")
476
+ gr.Markdown("Cortes inteligentes com linguagem natural")
477
 
478
  with gr.Row():
479
  xml_in = gr.File(label="XML do Premiere", file_types=[".xml"])
 
483
  use_llm = gr.Checkbox(label="Usar IA", value=USE_LLM_DEFAULT and LLM_AVAILABLE)
484
  num_segments = gr.Slider(2, 20, 5, step=1, label="Segmentos (modo automático)")
485
 
486
+ with gr.Accordion("IA - Linguagem Natural", open=True):
487
  gr.Markdown("""
488
+ **Exemplos:**
489
+ - "Extraia um corte de 10 minutos começando da parte do tenista"
490
+ - "Crie um corte de 15 minutos com os melhores momentos"
491
+ - "Faça um corte de 5 minutos sobre superação"
492
  """)
493
  natural_instructions = gr.Textbox(
494
+ label="Instruções",
495
+ placeholder='Ex: "Extraia um corte de 10 minutos começando da parte do tenista"',
496
  lines=3
497
  )
498
 
499
  with gr.Accordion("Minutagens Manuais", open=False):
500
  manual_timecodes = gr.Textbox(
501
+ label="Timecodes exatos",
502
+ placeholder="00:01:23:15 - 00:02:45:10",
503
  lines=4
504
  )
505
 
506
+ with gr.Accordion("Modo Automático", open=False):
507
+ custom_keywords = gr.Textbox(label="Palavras-chave")
508
  with gr.Row():
509
+ weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="Emoção")
510
+ weight_break = gr.Slider(0, 5, 1.5, 0.1, label="Quebra")
511
  with gr.Row():
512
+ weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="Aprendizado")
513
+ weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="Viral")
514
 
515
  run_btn = gr.Button("Processar XML", variant="primary", size="lg")
516