leicam commited on
Commit
adfb425
·
verified ·
1 Parent(s): fb81b2f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +204 -159
app.py CHANGED
@@ -54,39 +54,44 @@ 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
  # ============ 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
- # Aceita vários formatos: com ou sem colchetes, - ou —
64
- 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*(.*)$")
65
 
66
  for l in lines:
67
- # Pula linhas com apenas "Desconhecido"
68
- if l.strip() == "Desconhecido":
69
- continue
70
-
71
  m = pat_range.match(l)
72
  if m:
73
  s, e, text = m.groups()
74
- text = text.strip()
75
-
76
- # Pula se não tiver texto
77
- if not text or text == "Desconhecido":
78
- continue
79
-
80
  try:
81
  s_f = parse_timecode_to_frames(s)
82
  e_f = parse_timecode_to_frames(e)
83
  if e_f > s_f:
84
  results.append(Segment(s, e, s_f, e_f, text, 0.0))
85
- except Exception as ex:
86
- print(f"Erro ao processar linha: {l[:50]}... -> {ex}")
 
 
 
 
 
 
 
 
 
 
 
87
  continue
88
 
89
- print(f"✓ {len(results)} segmentos encontrados na transcrição")
90
  return results
91
 
92
  # ============ MANUAL TIMECODES ============
@@ -105,12 +110,88 @@ def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
105
 
106
  return manual_ranges
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  # ============ AI PROCESSING ============
109
  def extract_duration_from_instructions(instructions: str) -> float:
110
- """Extrai duração em minutos das instruções"""
 
111
  patterns = [
112
  r"(\d+)\s*minutos?",
113
  r"(\d+)\s*min\b",
 
114
  r"(\d+)m\b"
115
  ]
116
 
@@ -121,125 +202,53 @@ def extract_duration_from_instructions(instructions: str) -> float:
121
 
122
  return None
123
 
124
- def find_start_point_in_transcript(segs: List[Segment], instructions: str) -> int:
125
- """Encontra o ponto de início baseado nas instruções"""
126
- if not LLM_AVAILABLE:
127
- return 0
128
-
129
- # Cria resumo dos segmentos
130
- segments_text = "\n".join([
131
- f"{i}. [{s.start_tc}-{s.end_tc}] {s.text[:150]}"
132
- for i, s in enumerate(segs[:100]) # Primeiros 100 para não sobrecarregar
133
- ])
134
-
135
- prompt = f"""Analise as instruções e encontre o índice do segmento onde deve COMEÇAR o corte.
136
-
137
- INSTRUÇÕES: {instructions}
138
-
139
- SEGMENTOS:
140
- {segments_text}
141
-
142
- RESPONDA APENAS com o NÚMERO do índice onde deve começar (exemplo: 45)
143
- Não adicione explicações."""
144
-
145
- try:
146
- response = LLM.generate_content(prompt, generation_config={"temperature": 0.2})
147
- txt = (response.text or "").strip()
148
-
149
- # Extrai o primeiro número
150
- match = re.search(r"\d+", txt)
151
- if match:
152
- idx = int(match.group())
153
- if 0 <= idx < len(segs):
154
- return idx
155
- except Exception as e:
156
- print(f"Erro ao buscar ponto inicial: {e}")
157
-
158
- return 0
159
-
160
- def create_continuous_cut(segs: List[Segment], start_idx: int, target_minutes: float) -> List[Segment]:
161
- """Cria um corte contínuo de duração específica"""
162
- if start_idx >= len(segs):
163
- start_idx = 0
164
-
165
- target_seconds = target_minutes * 60
166
- target_frames = int(target_seconds * FPS)
167
-
168
- start_segment = segs[start_idx]
169
- start_frame = start_segment.start_f
170
- end_frame = start_frame + target_frames
171
-
172
- # Cria um único segmento contínuo
173
- end_tc = frames_to_timecode(end_frame)
174
-
175
- combined_text = " ".join([s.text for s in segs[start_idx:min(start_idx + 50, len(segs))]])[:500]
176
-
177
- result = Segment(
178
- start_tc=start_segment.start_tc,
179
- end_tc=end_tc,
180
- start_f=start_frame,
181
- end_f=end_frame,
182
- text=f"Corte contínuo: {combined_text}...",
183
- score=100.0
184
- )
185
-
186
- return [result]
187
-
188
  def ai_select_segments(segs: List[Segment], instructions: str) -> List[Segment]:
189
- """Usa IA para processar instruções em linguagem natural"""
190
  if not LLM_AVAILABLE:
191
  raise ValueError("IA não disponível. Configure GEMINI_API_KEY")
192
 
193
- # Detecta se pede duração específica
194
  target_duration = extract_duration_from_instructions(instructions)
195
 
196
- if target_duration:
197
- # Modo: corte contínuo de X minutos
198
- print(f"Modo: Corte contínuo de {target_duration} minutos")
199
-
200
- # Encontra ponto de início
201
- start_idx = find_start_point_in_transcript(segs, instructions)
202
- print(f"Iniciando do segmento {start_idx}: {segs[start_idx].start_tc}")
203
-
204
- # Cria corte contínuo
205
- result = create_continuous_cut(segs, start_idx, target_duration)
206
-
207
- duration_min = (result[0].end_f - result[0].start_f) / FPS / 60
208
- print(f"✓ Corte criado: {result[0].start_tc} → {result[0].end_tc} ({duration_min:.1f} min)")
209
-
210
- return result
211
-
212
- else:
213
- # Modo: seleção de múltiplos trechos
214
- print("Modo: Seleção de múltiplos trechos")
215
- return ai_select_multiple_segments(segs, instructions, num_segments=5)
216
-
217
- def ai_select_multiple_segments(segs: List[Segment], instructions: str, num_segments: int = 5) -> List[Segment]:
218
- """Seleciona múltiplos segmentos baseado em critérios"""
219
- segments_summary = []
220
  for i in range(0, len(segs), 5):
221
  group = segs[i:i+5]
222
  start_tc = group[0].start_tc
223
  end_tc = group[-1].end_tc
 
224
  combined_text = " ".join([s.text[:100] for s in group])
225
- segments_summary.append(f"Grupo {i//5}: [{start_tc}-{end_tc}] {combined_text[:200]}")
226
 
227
- prompt = f"""Você é um editor profissional.
 
 
 
228
 
229
- INSTRUÇÕES: {instructions}
 
 
230
 
231
- SEGMENTOS (agrupados):
232
- {chr(10).join(segments_summary[:40])}
233
 
234
- Selecione 10-15 GRUPOS que atendem às instruções.
235
- RESPONDA APENAS com números separados por vírgula (ex: 0,3,7,12,18,25)"""
 
 
 
 
 
 
236
 
237
  try:
238
- response = LLM.generate_content(prompt, generation_config={"temperature": 0.4})
239
  txt = (response.text or "").strip()
240
 
 
241
  group_indices = [int(x) for x in re.findall(r"\d+", txt)]
242
 
 
243
  selected_segs = []
244
  for group_idx in group_indices:
245
  start_idx = group_idx * 5
@@ -248,19 +257,36 @@ RESPONDA APENAS com números separados por vírgula (ex: 0,3,7,12,18,25)"""
248
  selected_segs.extend(segs[start_idx:end_idx])
249
 
250
  if not selected_segs:
 
251
  step = max(1, len(segs) // 30)
252
- selected_segs = segs[::step][:num_segments]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
- selected_segs.sort(key=lambda x: x.start_f)
 
255
 
256
- duration = sum((s.end_f - s.start_f) / FPS for s in selected_segs)
257
- print(f"✓ {len(selected_segs)} trechos selecionados, duração: {duration/60:.1f} min")
258
 
259
- return selected_segs[:num_segments * 3] # Retorna mais segmentos
260
 
261
  except Exception as e:
262
- print(f"Erro: {e}")
263
- return segs[:num_segments]
264
 
265
  # ============ KEYWORD SCORING ============
266
  def keyword_score(text: str, custom_keywords: str = "", weights: dict = None) -> float:
@@ -268,10 +294,10 @@ def keyword_score(text: str, custom_keywords: str = "", weights: dict = None) ->
268
  weights = {"emotion": 2.0, "break": 1.5, "learn": 1.2, "viral": 1.0}
269
 
270
  t = text.lower()
271
- kw_emotion = ["medo", "coragem", "raiva", "chorei", "feliz", "triste"]
272
  kw_break = ["nunca", "de repente", "contraintuitivo", "virada"]
273
- kw_learn = ["aprendi", "descobri", "lição", "entendi"]
274
- kw_viral = ["segredo", "verdade", "3 passos"]
275
 
276
  score = 0.0
277
  for kw in kw_emotion: score += weights["emotion"] if kw in t else 0.0
@@ -284,6 +310,7 @@ def keyword_score(text: str, custom_keywords: str = "", weights: dict = None) ->
284
  if kw.strip().lower() in t:
285
  score += 3.0
286
 
 
287
  return score
288
 
289
  # ============ MAIN SELECTION LOGIC ============
@@ -292,7 +319,7 @@ def select_segments(transcript_txt: str, use_llm: bool, num_segments: int,
292
  weight_emotion: float, weight_break: float,
293
  weight_learn: float, weight_viral: float) -> List[Segment]:
294
 
295
- # Priority 1: Manual
296
  manual_ranges = parse_manual_timecodes(manual_timecodes)
297
  if manual_ranges:
298
  result_segs = []
@@ -311,17 +338,21 @@ def select_segments(transcript_txt: str, use_llm: bool, num_segments: int,
311
  print(f"Erro: {e}")
312
  return result_segs if result_segs else []
313
 
314
- # Parse transcript
315
- segs = parse_transcript_full(transcript_txt)
316
  if not segs:
317
- raise ValueError("Nenhum trecho encontrado")
318
 
319
- # Priority 2: AI with natural instructions
320
  if natural_instructions.strip() and use_llm and LLM_AVAILABLE:
321
  return ai_select_segments(segs, natural_instructions)
322
 
323
  # Priority 3: Automatic scoring
324
- weights = {"emotion": weight_emotion, "break": weight_break, "learn": weight_learn, "viral": weight_viral}
 
 
 
 
 
325
 
326
  for s in segs:
327
  s.score = keyword_score(s.text, custom_keywords, weights)
@@ -422,20 +453,21 @@ def process_xml_and_transcript(xml_file, txt_file, use_llm, num_segments,
422
  custom_keywords, manual_timecodes, natural_instructions,
423
  weight_emotion, weight_break, weight_learn, weight_viral):
424
  if not xml_file:
425
- return "❌ Envie o XML", None, f"LLM: {LLM_AVAILABLE}"
426
 
427
  manual_ranges = parse_manual_timecodes(manual_timecodes)
428
  has_instructions = natural_instructions.strip() != ""
429
 
 
430
  if manual_ranges:
431
  mode = "MANUAL"
432
  transcript = ""
433
  elif has_instructions:
434
  mode = "IA (Linguagem Natural)"
435
  if not txt_file:
436
- return "❌ Envie a transcrição", None, f"LLM: {LLM_AVAILABLE}"
437
  if not LLM_AVAILABLE:
438
- return "❌ Configure GEMINI_API_KEY", None, f"LLM: False"
439
  with open(txt_file.name, "r", encoding="utf-8") as f:
440
  transcript = f.read()
441
  else:
@@ -460,7 +492,7 @@ def process_xml_and_transcript(xml_file, txt_file, use_llm, num_segments,
460
  out_path = os.path.join(OUTPUT_DIR, f"{base}_EDITADO.xml")
461
  tree.write(out_path, encoding="utf-8", xml_declaration=True)
462
 
463
- total_duration = sum((s.end_f - s.start_f) / FPS for s in segs)
464
 
465
  resumo = f"✂️ {len(segs)} cortes | Duração: {total_duration/60:.1f} min | Modo: {mode}\n\n"
466
  for i, s in enumerate(segs, 1):
@@ -470,23 +502,36 @@ def process_xml_and_transcript(xml_file, txt_file, use_llm, num_segments,
470
  resumo += f" {s.text[:120]}...\n"
471
  resumo += "\n"
472
 
473
- status = f"✓ {mode} | Duração: {total_duration/60:.1f} min | LLM: {LLM_AVAILABLE}"
474
  return resumo, out_path, status
475
 
476
  except Exception as e:
477
  return f"❌ Erro: {str(e)}", None, f"LLM: {LLM_AVAILABLE}"
478
 
479
- # ============ CSS & GRADIO APP ============
480
  css = """
481
- :root { --primary: #39FF14; --text: #1a1a1a; }
482
- .gradio-container { font-family: system-ui, sans-serif !important; }
483
- .gradio-container h1, .gradio-container label { color: var(--text) !important; }
484
- .gradio-container button.primary { background: var(--primary) !important; color: #000 !important; font-weight: 700 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
485
  """
486
 
 
487
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
488
- gr.Markdown("# Editor XML Premiere - IA")
489
- gr.Markdown("Cortes inteligentes com linguagem natural")
490
 
491
  with gr.Row():
492
  xml_in = gr.File(label="XML do Premiere", file_types=[".xml"])
@@ -496,34 +541,34 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
496
  use_llm = gr.Checkbox(label="Usar IA", value=USE_LLM_DEFAULT and LLM_AVAILABLE)
497
  num_segments = gr.Slider(2, 20, 5, step=1, label="Segmentos (modo automático)")
498
 
499
- with gr.Accordion("IA - Linguagem Natural", open=True):
500
  gr.Markdown("""
501
- **Exemplos:**
502
- - "Extraia um corte de 10 minutos começando da parte do tenista"
503
- - "Crie um corte de 15 minutos com os melhores momentos"
504
- - "Faça um corte de 5 minutos sobre superação"
505
  """)
506
  natural_instructions = gr.Textbox(
507
- label="Instruções",
508
- placeholder='Ex: "Extraia um corte de 10 minutos começando da parte do tenista"',
509
  lines=3
510
  )
511
 
512
  with gr.Accordion("Minutagens Manuais", open=False):
513
  manual_timecodes = gr.Textbox(
514
- label="Timecodes exatos",
515
- placeholder="00:01:23:15 - 00:02:45:10",
516
  lines=4
517
  )
518
 
519
- with gr.Accordion("Modo Automático", open=False):
520
- custom_keywords = gr.Textbox(label="Palavras-chave")
521
  with gr.Row():
522
- weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="Emoção")
523
- weight_break = gr.Slider(0, 5, 1.5, 0.1, label="Quebra")
524
  with gr.Row():
525
- weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="Aprendizado")
526
- weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="Viral")
527
 
528
  run_btn = gr.Button("Processar XML", variant="primary", size="lg")
529
 
 
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)
73
  if m:
74
  s, e, text = m.groups()
 
 
 
 
 
 
75
  try:
76
  s_f = parse_timecode_to_frames(s)
77
  e_f = parse_timecode_to_frames(e)
78
  if e_f > s_f:
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
 
97
  # ============ MANUAL TIMECODES ============
 
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
 
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
  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
  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
  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
  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
  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
  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
  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
  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
  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