leicam commited on
Commit
f9e0f33
·
verified ·
1 Parent(s): 99d559e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -51
app.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import os
3
  import re
4
  import xml.etree.ElementTree as ET
@@ -7,7 +6,7 @@ from typing import List
7
  import gradio as gr
8
 
9
  # Optional LLM (Gemini)
10
- USE_LLM_DEFAULT = True # default checked
11
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "").strip()
12
 
13
  LLM_AVAILABLE = False
@@ -59,7 +58,7 @@ def frames_to_timecode(frames: int, fps: int = FPS) -> str:
59
  def parse_transcript(txt: str) -> List[Segment]:
60
  lines = [l.strip() for l in txt.splitlines() if l.strip()]
61
  results: List[Segment] = []
62
- 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+(.*)$")
63
  pat_point = re.compile(r"^(\d{2}:\d{2}:\d{2}[:;]\d{2})\s+(.*)$")
64
  for l in lines:
65
  m = pat_range.match(l)
@@ -78,14 +77,15 @@ def parse_transcript(txt: str) -> List[Segment]:
78
  s, text = m.groups()
79
  try:
80
  s_f = parse_timecode_to_frames(s)
81
- e_f = s_f + 4*FPS # default window
82
  e = frames_to_timecode(e_f)
83
  results.append(Segment(s, e, s_f, e_f, text, 0.0))
84
  except Exception:
85
  continue
86
  return results
87
 
88
- def keyword_score(text: str) -> float:
 
89
  t = text.lower()
90
  kw_emotion = ["medo", "coragem", "raiva", "chorei", "feliz", "triste", "emocion", "culpa", "vergonha", "orgulho"]
91
  kw_break = ["nunca", "de repente", "contraintuitivo", "ninguém te conta", "parei", "decidi", "quebrei", "virada"]
@@ -93,48 +93,71 @@ def keyword_score(text: str) -> float:
93
  kw_viral = ["segredo", "verdade", "por trás", "3 passos", "passo a passo", "como eu", "ninguém fala"]
94
 
95
  score = 0.0
96
- for kw in kw_emotion: score += 2.0 if kw in t else 0.0
97
- for kw in kw_break: score += 1.5 if kw in t else 0.0
98
- for kw in kw_learn: score += 1.2 if kw in t else 0.0
99
- for kw in kw_viral: score += 1.0 if kw in t else 0.0
 
 
 
 
 
 
 
100
  score += 0.2 * text.count("!")
101
  score += 0.0005 * len(text)
102
  return score
103
 
104
- def llm_rank_segments(candidates: List[Segment]) -> List[Segment]:
105
- """Ask the LLM to pick 2–5 best segments by narrative strength."""
106
  if not LLM_AVAILABLE:
107
- return candidates
 
108
  sample = "\n".join([f"{i}. [{c.start_tc}-{c.end_tc}] {c.text[:300]}" for i, c in enumerate(candidates)])
109
- prompt = (
110
- "Você é um editor. Selecione de 2 a 5 trechos mais fortes pela emoção, quebra de expectativa e aprendizado, "
111
- "mantendo uma mini-narrativa coerente. Responda apenas com índices (0-based) separados por vírgula.\n\n"
112
- f"{sample}"
113
  )
 
 
 
 
 
 
114
  try:
115
- r = LLM.generate_content(prompt, generation_config={"temperature": 0.2})
116
  txt = (r.text or "").strip()
117
  idxs = [int(x) for x in re.findall(r"\d+", txt)]
118
  idxs = [i for i in idxs if 0 <= i < len(candidates)]
119
- if 2 <= len(idxs) <= 5:
 
 
120
  return [candidates[i] for i in idxs]
121
- except Exception:
122
- pass
123
- return candidates[:min(5, max(2, len(candidates)))]
124
-
125
- def select_segments(transcript_txt: str, use_llm: bool) -> List[Segment]:
 
 
 
 
126
  segs = parse_transcript(transcript_txt)
127
  if not segs:
128
  raise ValueError("Nenhum trecho válido encontrado na transcrição.")
 
129
  for s in segs:
130
- s.score = keyword_score(s.text)
131
- # Heuristic pre-filter
132
  segs.sort(key=lambda x: x.score, reverse=True)
133
- top = segs[:12]
 
134
  if use_llm and LLM_AVAILABLE:
135
- ranked = llm_rank_segments(top)
136
- return ranked[:min(5, max(2, len(ranked)))]
137
- return top[:min(5, max(2, len(top)))]
 
138
 
139
  # ---- XML editing ----
140
  def get_sequence(root: ET.Element) -> ET.Element:
@@ -242,13 +265,18 @@ def edit_sequence_with_segments(tree: ET.ElementTree, segs: List[Segment]) -> ET
242
  return tree
243
 
244
  # ---- Gradio app ----
245
- def process_xml_and_transcript(premiere_xml_file, transcript_txt_file, use_llm):
 
 
246
  if premiere_xml_file is None or transcript_txt_file is None:
247
  return "Envie o XML do Premiere e a transcrição em .txt.", None, f"LLM disponível: {LLM_AVAILABLE}"
 
248
  with open(transcript_txt_file.name, "r", encoding="utf-8") as f:
249
  transcript = f.read()
250
 
251
- segs = select_segments(transcript, use_llm and LLM_AVAILABLE)
 
 
252
 
253
  tree = ET.parse(premiere_xml_file.name)
254
  tree = edit_sequence_with_segments(tree, segs)
@@ -257,30 +285,86 @@ def process_xml_and_transcript(premiere_xml_file, transcript_txt_file, use_llm):
257
  out_path = os.path.join(OUTPUT_DIR, f"{base}_EDITADO.xml")
258
  tree.write(out_path, encoding="utf-8", xml_declaration=True)
259
 
260
- resumo = "Cortes aplicados (24 fps):\n"
261
  for i, s in enumerate(segs, 1):
262
- resumo += f"{i}. {s.start_tc} -> {s.end_tc} | {s.end_f - s.start_f} frames | {s.text[:120]}\n"
 
 
263
 
264
- status = f"LLM disponível: {LLM_AVAILABLE} | LLM ligado na UI: {use_llm}"
265
  return resumo, out_path, status
266
 
267
- with gr.Blocks() as demo:
268
- gr.Markdown("# Agente de Edição XML para Premiere (24 fps) — Modo LLM")
269
- gr.Markdown(" transcrição com timecodes e edita **a mesma sequência** no XML do Premiere. "
270
- "Mantenha V/A sincronizados, sem mídias externas, sem nova `<sequence>`.")
271
-
272
  with gr.Row():
273
- xml_in = gr.File(label="XML da sequência do Premiere (FCP XML)", file_types=[".xml"])
274
- txt_in = gr.File(label="Transcrição (.txt) hh:mm:ss:ff", file_types=[".txt"])
275
-
276
- use_llm = gr.Checkbox(label="Usar LLM (Gemini) para seleção semântica", value=USE_LLM_DEFAULT and LLM_AVAILABLE)
277
- run_btn = gr.Button("Processar")
278
- resumo_out = gr.Textbox(label="Resumo dos cortes aplicados")
279
- file_out = gr.File(label="Download do XML Editado")
280
- status_out = gr.Textbox(label="Status do LLM", interactive=False)
281
-
282
- run_btn.click(process_xml_and_transcript, inputs=[xml_in, txt_in, use_llm],
283
- outputs=[resumo_out, file_out, status_out])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
  if __name__ == "__main__":
286
- demo.launch()
 
 
1
  import os
2
  import re
3
  import xml.etree.ElementTree as ET
 
6
  import gradio as gr
7
 
8
  # Optional LLM (Gemini)
9
+ USE_LLM_DEFAULT = True
10
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "").strip()
11
 
12
  LLM_AVAILABLE = False
 
58
  def parse_transcript(txt: str) -> List[Segment]:
59
  lines = [l.strip() for l in txt.splitlines() if l.strip()]
60
  results: List[Segment] = []
61
+ 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+(.*)$")
62
  pat_point = re.compile(r"^(\d{2}:\d{2}:\d{2}[:;]\d{2})\s+(.*)$")
63
  for l in lines:
64
  m = pat_range.match(l)
 
77
  s, text = m.groups()
78
  try:
79
  s_f = parse_timecode_to_frames(s)
80
+ e_f = s_f + 4*FPS
81
  e = frames_to_timecode(e_f)
82
  results.append(Segment(s, e, s_f, e_f, text, 0.0))
83
  except Exception:
84
  continue
85
  return results
86
 
87
+ def keyword_score(text: str, custom_keywords: str = "", weight_emotion: float = 2.0,
88
+ weight_break: float = 1.5, weight_learn: float = 1.2, weight_viral: float = 1.0) -> float:
89
  t = text.lower()
90
  kw_emotion = ["medo", "coragem", "raiva", "chorei", "feliz", "triste", "emocion", "culpa", "vergonha", "orgulho"]
91
  kw_break = ["nunca", "de repente", "contraintuitivo", "ninguém te conta", "parei", "decidi", "quebrei", "virada"]
 
93
  kw_viral = ["segredo", "verdade", "por trás", "3 passos", "passo a passo", "como eu", "ninguém fala"]
94
 
95
  score = 0.0
96
+ for kw in kw_emotion: score += weight_emotion if kw in t else 0.0
97
+ for kw in kw_break: score += weight_break if kw in t else 0.0
98
+ for kw in kw_learn: score += weight_learn if kw in t else 0.0
99
+ for kw in kw_viral: score += weight_viral if kw in t else 0.0
100
+
101
+ # Custom keywords
102
+ if custom_keywords.strip():
103
+ custom_kw_list = [kw.strip().lower() for kw in custom_keywords.split(",") if kw.strip()]
104
+ for kw in custom_kw_list:
105
+ score += 3.0 if kw in t else 0.0
106
+
107
  score += 0.2 * text.count("!")
108
  score += 0.0005 * len(text)
109
  return score
110
 
111
+ def llm_rank_segments(candidates: List[Segment], num_segments: int, custom_instructions: str = "") -> List[Segment]:
112
+ """Ask the LLM to pick segments based on criteria."""
113
  if not LLM_AVAILABLE:
114
+ return candidates[:num_segments]
115
+
116
  sample = "\n".join([f"{i}. [{c.start_tc}-{c.end_tc}] {c.text[:300]}" for i, c in enumerate(candidates)])
117
+
118
+ base_prompt = (
119
+ f"Você é um editor profissional. Selecione exatamente {num_segments} trechos mais fortes "
120
+ "pela emoção, quebra de expectativa e aprendizado, mantendo uma mini-narrativa coerente.\n\n"
121
  )
122
+
123
+ if custom_instructions.strip():
124
+ base_prompt += f"INSTRUÇÕES ADICIONAIS: {custom_instructions}\n\n"
125
+
126
+ base_prompt += "Responda apenas com índices (0-based) separados por vírgula.\n\n" + sample
127
+
128
  try:
129
+ r = LLM.generate_content(base_prompt, generation_config={"temperature": 0.2})
130
  txt = (r.text or "").strip()
131
  idxs = [int(x) for x in re.findall(r"\d+", txt)]
132
  idxs = [i for i in idxs if 0 <= i < len(candidates)]
133
+ if len(idxs) >= num_segments:
134
+ return [candidates[i] for i in idxs[:num_segments]]
135
+ elif len(idxs) > 0:
136
  return [candidates[i] for i in idxs]
137
+ except Exception as e:
138
+ print(f"Erro no LLM: {e}")
139
+
140
+ return candidates[:num_segments]
141
+
142
+ def select_segments(transcript_txt: str, use_llm: bool, num_segments: int,
143
+ custom_keywords: str, custom_instructions: str,
144
+ weight_emotion: float, weight_break: float,
145
+ weight_learn: float, weight_viral: float) -> List[Segment]:
146
  segs = parse_transcript(transcript_txt)
147
  if not segs:
148
  raise ValueError("Nenhum trecho válido encontrado na transcrição.")
149
+
150
  for s in segs:
151
+ s.score = keyword_score(s.text, custom_keywords, weight_emotion, weight_break, weight_learn, weight_viral)
152
+
153
  segs.sort(key=lambda x: x.score, reverse=True)
154
+ top = segs[:min(20, len(segs))]
155
+
156
  if use_llm and LLM_AVAILABLE:
157
+ ranked = llm_rank_segments(top, num_segments, custom_instructions)
158
+ return ranked
159
+
160
+ return top[:num_segments]
161
 
162
  # ---- XML editing ----
163
  def get_sequence(root: ET.Element) -> ET.Element:
 
265
  return tree
266
 
267
  # ---- Gradio app ----
268
+ def process_xml_and_transcript(premiere_xml_file, transcript_txt_file, use_llm,
269
+ num_segments, custom_keywords, custom_instructions,
270
+ weight_emotion, weight_break, weight_learn, weight_viral):
271
  if premiere_xml_file is None or transcript_txt_file is None:
272
  return "Envie o XML do Premiere e a transcrição em .txt.", None, f"LLM disponível: {LLM_AVAILABLE}"
273
+
274
  with open(transcript_txt_file.name, "r", encoding="utf-8") as f:
275
  transcript = f.read()
276
 
277
+ segs = select_segments(transcript, use_llm and LLM_AVAILABLE, num_segments,
278
+ custom_keywords, custom_instructions,
279
+ weight_emotion, weight_break, weight_learn, weight_viral)
280
 
281
  tree = ET.parse(premiere_xml_file.name)
282
  tree = edit_sequence_with_segments(tree, segs)
 
285
  out_path = os.path.join(OUTPUT_DIR, f"{base}_EDITADO.xml")
286
  tree.write(out_path, encoding="utf-8", xml_declaration=True)
287
 
288
+ resumo = f"✂️ {len(segs)} cortes aplicados (24 fps):\n\n"
289
  for i, s in enumerate(segs, 1):
290
+ dur_sec = (s.end_f - s.start_f) / FPS
291
+ resumo += f"{i}. {s.start_tc} → {s.end_tc} ({dur_sec:.1f}s)\n"
292
+ resumo += f" Score: {s.score:.1f} | {s.text[:150]}\n\n"
293
 
294
+ status = f"LLM disponível: {LLM_AVAILABLE} | LLM usado: {use_llm and LLM_AVAILABLE}"
295
  return resumo, out_path, status
296
 
297
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
298
+ gr.Markdown("# 🎬 Agente de Edição XML para Premiere (Controles Avançados)")
299
+ gr.Markdown("Edite sua sequência do Premiere com controle total sobre a seleção de trechos.")
300
+
 
301
  with gr.Row():
302
+ with gr.Column():
303
+ xml_in = gr.File(label="📁 XML da sequência (FCP XML)", file_types=[".xml"])
304
+ txt_in = gr.File(label="📄 Transcrição (.txt) com timecodes", file_types=[".txt"])
305
+
306
+ with gr.Column():
307
+ gr.Markdown("### ⚙️ Configurações")
308
+ use_llm = gr.Checkbox(
309
+ label="🤖 Usar LLM (Gemini) para seleção semântica",
310
+ value=USE_LLM_DEFAULT and LLM_AVAILABLE,
311
+ info="Usa IA para escolher os melhores trechos narrativamente"
312
+ )
313
+ num_segments = gr.Slider(
314
+ minimum=2, maximum=10, step=1, value=5,
315
+ label="📊 Número de segmentos a selecionar",
316
+ info="Quantos trechos incluir no vídeo final"
317
+ )
318
+
319
+ with gr.Accordion("🎯 Palavras-chave Personalizadas", open=False):
320
+ custom_keywords = gr.Textbox(
321
+ label="Adicione palavras-chave importantes (separadas por vírgula)",
322
+ placeholder="Exemplo: transformação, resultado, método, estratégia",
323
+ info="Trechos com essas palavras terão prioridade máxima (peso 3.0)"
324
+ )
325
+
326
+ with gr.Accordion("📝 Instruções em Texto Livre para o LLM", open=False):
327
+ custom_instructions = gr.Textbox(
328
+ label="Instruções adicionais para o LLM",
329
+ placeholder="Exemplo: Prefira trechos que mostrem resultados concretos e evite introduções longas",
330
+ lines=3,
331
+ info="Só funciona se o LLM estiver ativado"
332
+ )
333
+
334
+ with gr.Accordion("⚖️ Ajuste Fino dos Pesos de Pontuação", open=False):
335
+ gr.Markdown("Ajuste a importância de cada categoria na pontuação heurística:")
336
+ with gr.Row():
337
+ weight_emotion = gr.Slider(0, 5, value=2.0, step=0.1, label="Emoção")
338
+ weight_break = gr.Slider(0, 5, value=1.5, step=0.1, label="Quebra de expectativa")
339
+ with gr.Row():
340
+ weight_learn = gr.Slider(0, 5, value=1.2, step=0.1, label="Aprendizado")
341
+ weight_viral = gr.Slider(0, 5, value=1.0, step=0.1, label="Viralização")
342
+
343
+ run_btn = gr.Button("🚀 Processar e Gerar XML Editado", variant="primary", size="lg")
344
+
345
+ gr.Markdown("---")
346
+
347
+ with gr.Row():
348
+ with gr.Column(scale=2):
349
+ resumo_out = gr.Textbox(label="📋 Resumo dos cortes aplicados", lines=15)
350
+ with gr.Column(scale=1):
351
+ status_out = gr.Textbox(label="ℹ️ Status", interactive=False)
352
+ file_out = gr.File(label="💾 Download do XML Editado")
353
+
354
+ run_btn.click(
355
+ process_xml_and_transcript,
356
+ inputs=[xml_in, txt_in, use_llm, num_segments, custom_keywords,
357
+ custom_instructions, weight_emotion, weight_break, weight_learn, weight_viral],
358
+ outputs=[resumo_out, file_out, status_out]
359
+ )
360
+
361
+ gr.Markdown("""
362
+ ### 💡 Dicas de uso:
363
+ - **Modo Heurístico**: Desative o LLM e ajuste os pesos para controle total baseado em palavras-chave
364
+ - **Modo LLM**: Ative o LLM e use as instruções em texto livre para guiar a seleção semanticamente
365
+ - **Híbrido**: Combine palavras-chave personalizadas + instruções LLM para máximo controle
366
+ - **Palavras-chave**: Adicione termos específicos do seu nicho que devem ter alta prioridade
367
+ """)
368
 
369
  if __name__ == "__main__":
370
+ demo.launch()