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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +2089 -65
app.py CHANGED
@@ -1,4 +1,46 @@
1
- import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import re
3
  import json
4
  import xml.etree.ElementTree as ET
@@ -9,7 +51,7 @@ import gradio as gr
9
  # =========================
10
  # Configurações Gerais
11
  # =========================
12
- FPS = 24
13
  OUTPUT_DIR = "./Output"
14
  os.makedirs(OUTPUT_DIR, exist_ok=True)
15
 
@@ -50,7 +92,7 @@ class Segment:
50
  # =========================
51
  # Funções de Timecode
52
  # =========================
53
- def _tc_to_hmsf(tc: str, fps: int = FPS) -> Tuple[int, int, int, int]:
54
  """Converte timecode para (hh, mm, ss, ff)."""
55
  s = tc.strip()
56
 
@@ -76,12 +118,12 @@ def _tc_to_hmsf(tc: str, fps: int = FPS) -> Tuple[int, int, int, int]:
76
  raise ValueError(f"Timecode inválido: {tc}")
77
 
78
 
79
- def parse_timecode_to_frames(tc: str, fps: int = FPS) -> int:
80
  hh, mm, ss, ff = _tc_to_hmsf(tc, fps)
81
  return hh * 3600 * fps + mm * 60 * fps + ss * fps + ff
82
 
83
 
84
- def frames_to_timecode(frames: int, fps: int = FPS) -> str:
85
  hh = frames // (3600 * fps)
86
  rem = frames % (3600 * fps)
87
  mm = rem // (60 * fps)
@@ -94,7 +136,7 @@ def frames_to_timecode(frames: int, fps: int = FPS) -> str:
94
  # =========================
95
  # Parser de Transcrição
96
  # =========================
97
- def parse_transcript(txt: str) -> List[Segment]:
98
  """Parser robusto para múltiplos formatos."""
99
  if not txt or not txt.strip():
100
  return []
@@ -104,67 +146,2049 @@ def parse_transcript(txt: str) -> List[Segment]:
104
 
105
  line_range = re.compile(
106
  r'^\s*\[?\s*(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*[-—–]\s*'
107
- r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*\]?\s*(.*)$'
108
- )
109
- arrow = re.compile(
110
- r'(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3}|[:;]\d{2})?)\s*-->\s*'
111
- r'(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3}|[:;]\d{2})?)'
112
- )
113
 
114
- i = 0
115
- while i < len(lines):
116
- raw = lines[i].strip()
117
- if not raw or raw.lower() == "desconhecido":
118
- i += 1
119
- continue
120
 
121
- m = line_range.match(raw)
 
 
 
 
 
 
 
 
 
 
 
122
  if m:
123
- start_tc, end_tc, trailing_text = m.groups()
124
- text_parts = []
125
 
126
- if trailing_text.strip():
127
- text_parts.append(trailing_text.strip())
128
- else:
129
- j = i + 1
130
- while j < len(lines):
131
- nxt = lines[j].strip()
132
- if not nxt or line_range.match(nxt) or re.match(r'^\d+\s*$', nxt) or arrow.search(nxt):
133
- break
134
- text_parts.append(nxt)
135
- j += 1
136
- i = j - 1
137
 
138
- text = " ".join(text_parts).strip()
139
- try:
140
- sf = parse_timecode_to_frames(start_tc)
141
- ef = parse_timecode_to_frames(end_tc)
142
- if ef > sf:
143
- results.append(Segment(
144
- start_tc=frames_to_timecode(sf),
145
- end_tc=frames_to_timecode(ef),
146
- start_f=sf,
147
- end_f=ef,
148
- text=text if text else f"{start_tc} - {end_tc}",
149
- score=0.0
150
- ))
151
- except Exception:
152
- pass
153
- i += 1
154
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
- if arrow.search(raw) or (i + 1 < len(lines) and arrow.search(lines[i + 1])):
157
- line_with_tc = raw if arrow.search(raw) else lines[i + 1]
158
- mm = arrow.search(line_with_tc)
159
- if mm:
160
- start_tc, end_tc = mm.groups()
161
- j = i + 1 if line_with_tc == raw else i + 2
162
- text_parts = []
163
- while j < len(lines):
164
- nxt = lines[j].strip()
165
- if not nxt:
166
- break
167
- if re.match(r'^\d+\s*$', nxt) and (j + 1 < len(lines) and arrow.search(lines[j + 1])):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  break
169
  if arrow.search(nxt):
170
  break
@@ -173,12 +2197,12 @@ def parse_transcript(txt: str) -> List[Segment]:
173
 
174
  text = " ".join(text_parts).strip()
175
  try:
176
- sf = parse_timecode_to_frames(start_tc)
177
- ef = parse_timecode_to_frames(end_tc)
178
  if ef > sf:
179
  results.append(Segment(
180
- start_tc=frames_to_timecode(sf),
181
- end_tc=frames_to_timecode(ef),
182
  start_f=sf,
183
  end_f=ef,
184
  text=text,
 
1
+ def select_segments(
2
+ transcript_txt: str,
3
+ use_llm: bool,
4
+ num_segments: int,
5
+ custom_keywords: str,
6
+ manual_timecodes: str,
7
+ natural_instructions: str,
8
+ weight_emotion: float,
9
+ weight_break: float,
10
+ weight_learn: float,
11
+ weight_viral: float,
12
+ fps: int,
13
+ progress_callback=None
14
+ ) -> List[Segment]:
15
+
16
+ # 1) Manual
17
+ manual = parse_manual_timecodes(manual_timecodes)
18
+ if manual:
19
+ result = []
20
+ for start_tc, end_tc in manual:
21
+ try:
22
+ result.append(Segment(
23
+ start_tc=frames_to_timecode(parse_timecode_to_frames(start_tc, fps), fps),
24
+ end_tc=frames_to_timecode(parse_timecode_to_frames(end_tc, fps), fps),
25
+ start_f=parse_timecode_to_frames(start_tc, fps),
26
+ end_f=parse_timecode_to_frames(end_tc, fps),
27
+ text=f"Manual: {start_tc} - {end_tc}",
28
+ score=100.0
29
+ ))
30
+ except Exception:
31
+ pass
32
+ return result
33
+
34
+ # 2) Parser de transcrição
35
+ segs = parse_transcript(transcript_txt, fps) if transcript_txt else []
36
+
37
+ # 3) Linguagem natural COM IA
38
+ if natural_instructions.strip():
39
+ if use_llm and LLM_AVAILABLE and segs:
40
+ # USA IA PARA ANÁLISE COMPLETA
41
+ return ai_analyze_and_select(segs, natural_instructions, fps, progress_callback)
42
+ elif segs:
43
+ # Fallback semimport os
44
  import re
45
  import json
46
  import xml.etree.ElementTree as ET
 
51
  # =========================
52
  # Configurações Gerais
53
  # =========================
54
+ DEFAULT_FPS = 24 # FPS padrão, mas será configurável na interface
55
  OUTPUT_DIR = "./Output"
56
  os.makedirs(OUTPUT_DIR, exist_ok=True)
57
 
 
92
  # =========================
93
  # Funções de Timecode
94
  # =========================
95
+ def _tc_to_hmsf(tc: str, fps: int) -> Tuple[int, int, int, int]:
96
  """Converte timecode para (hh, mm, ss, ff)."""
97
  s = tc.strip()
98
 
 
118
  raise ValueError(f"Timecode inválido: {tc}")
119
 
120
 
121
+ def parse_timecode_to_frames(tc: str, fps: int) -> int:
122
  hh, mm, ss, ff = _tc_to_hmsf(tc, fps)
123
  return hh * 3600 * fps + mm * 60 * fps + ss * fps + ff
124
 
125
 
126
+ def frames_to_timecode(frames: int, fps: int) -> str:
127
  hh = frames // (3600 * fps)
128
  rem = frames % (3600 * fps)
129
  mm = rem // (60 * fps)
 
136
  # =========================
137
  # Parser de Transcrição
138
  # =========================
139
+ def parse_transcript(txt: str, fps: int) -> List[Segment]:
140
  """Parser robusto para múltiplos formatos."""
141
  if not txt or not txt.strip():
142
  return []
 
146
 
147
  line_range = re.compile(
148
  r'^\s*\[?\s*(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*[-—–]\s*'
149
+ r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*\]?\s*(.*)
 
 
 
 
 
150
 
 
 
 
 
 
 
151
 
152
+ # =========================
153
+ # Minutagens Manuais
154
+ # =========================
155
+ def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
156
+ if not manual_input or not manual_input.strip():
157
+ return []
158
+
159
+ manual_ranges = []
160
+ lines = manual_input.replace(",", "\n").splitlines()
161
+ pattern = re.compile(r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*[-–—]\s*(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)')
162
+ for line in lines:
163
+ m = pattern.search(line.strip())
164
  if m:
165
+ manual_ranges.append((m.group(1), m.group(2)))
166
+ return manual_ranges
167
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ # =========================
170
+ # IA: Análise Inteligente com Gemini
171
+ # =========================
172
+ def ai_analyze_and_select(segments: List[Segment], command: str, fps: int, progress_callback=None) -> List[Segment]:
173
+ """
174
+ Usa Gemini para analisar a transcrição completa e identificar os melhores trechos.
175
+ Processo em 2 etapas para máxima precisão.
176
+ """
177
+ if not LLM_AVAILABLE or not segments:
178
+ raise ValueError("IA não disponível ou sem segmentos para analisar")
179
+
180
+ if progress_callback:
181
+ progress_callback("🤖 Etapa 1/3: Preparando dados para análise...")
182
+
183
+ # Prepara a transcrição completa com índices
184
+ transcript_data = []
185
+ for i, seg in enumerate(segments):
186
+ duration_sec = (seg.end_f - seg.start_f) / fps
187
+ transcript_data.append({
188
+ "index": i,
189
+ "timecode": seg.start_tc,
190
+ "duration_sec": round(duration_sec, 1),
191
+ "text": seg.text[:200] # Limita texto para não estourar tokens
192
+ })
193
+
194
+ # Converte para JSON para análise estruturada
195
+ transcript_json = json.dumps(transcript_data, ensure_ascii=False, indent=2)
196
+
197
+ if progress_callback:
198
+ progress_callback(f"🤖 Etapa 2/3: Analisando {len(segments)} segmentos com IA (pode levar 30-60s)...")
199
+
200
+ # Prompt detalhado para análise completa
201
+ prompt = f"""Você é um especialista em edição de vídeo. Analise a transcrição e identifique os MELHORES trechos baseado no comando do usuário.
202
 
203
+ COMANDO DO USUÁRIO:
204
+ {command}
205
+
206
+ TRANSCRIÇÃO COMPLETA (formato JSON com index, timecode, duração e texto):
207
+ {transcript_json}
208
+
209
+ INSTRUÇÕES:
210
+ 1. Leia o comando com atenção e identifique:
211
+ - Quantidade de cortes desejada
212
+ - Duração de cada corte (em segundos)
213
+ - Tema/assunto/palavras-chave mencionados
214
+ - Timecode de início (se mencionado)
215
+
216
+ 2. Analise TODA a transcrição e identifique os segmentos que melhor correspondem ao comando
217
+
218
+ 3. Para cada corte, retorne no formato JSON:
219
+ {{
220
+ "cuts": [
221
+ {{
222
+ "start_index": <índice do segmento inicial>,
223
+ "duration_seconds": <duração desejada em segundos>,
224
+ "reason": "<breve explicação de por que escolheu este trecho>"
225
+ }}
226
+ ]
227
+ }}
228
+
229
+ IMPORTANTE:
230
+ - Seja PRECISO na identificação dos trechos
231
+ - Considere o contexto completo ao redor das palavras-chave
232
+ - Se o comando pedir "sobre X", encontre onde X é realmente discutido
233
+ - Se houver timecode, priorize começar próximo a ele
234
+ - Retorne APENAS o JSON, sem texto adicional
235
+
236
+ Responda com o JSON:"""
237
+
238
+ try:
239
+ response = LLM.generate_content(
240
+ prompt,
241
+ generation_config={
242
+ "temperature": 0.2,
243
+ "max_output_tokens": 2000,
244
+ }
245
+ )
246
+
247
+ response_text = response.text.strip()
248
+
249
+ if progress_callback:
250
+ progress_callback("🤖 Etapa 3/3: Processando resposta da IA...")
251
+
252
+ # Extrai JSON da resposta
253
+ json_match = re.search(r'\{[\s\S]*"cuts"[\s\S]*\}', response_text)
254
+ if not json_match:
255
+ raise ValueError("IA não retornou JSON válido")
256
+
257
+ result = json.loads(json_match.group(0))
258
+ cuts_data = result.get("cuts", [])
259
+
260
+ if not cuts_data:
261
+ raise ValueError("IA não encontrou cortes adequados")
262
+
263
+ # Cria os segmentos baseado na análise da IA
264
+ selected_segments = []
265
+
266
+ for cut_info in cuts_data:
267
+ start_idx = cut_info.get("start_index", 0)
268
+ duration_sec = cut_info.get("duration_seconds", 60)
269
+ reason = cut_info.get("reason", "")
270
+
271
+ if start_idx < 0 or start_idx >= len(segments):
272
+ continue
273
+
274
+ start_seg = segments[start_idx]
275
+ start_frame = start_seg.start_f
276
+ duration_frames = int(duration_sec * fps)
277
+ end_frame = start_frame + duration_frames
278
+
279
+ # Coleta texto dos segmentos envolvidos
280
+ text_parts = [f"[IA: {reason}]"] if reason else []
281
+ for seg in segments[start_idx:]:
282
+ if seg.start_f < end_frame:
283
+ if seg.text:
284
+ text_parts.append(seg.text[:150])
285
+ else:
286
+ break
287
+
288
+ combined_text = " [...] ".join(text_parts)[:500]
289
+
290
+ selected_segments.append(Segment(
291
+ start_tc=frames_to_timecode(start_frame, fps),
292
+ end_tc=frames_to_timecode(end_frame, fps),
293
+ start_f=start_frame,
294
+ end_f=end_frame,
295
+ text=combined_text,
296
+ score=100.0
297
+ ))
298
+
299
+ return selected_segments
300
+
301
+ except json.JSONDecodeError as e:
302
+ raise ValueError(f"Erro ao processar resposta da IA (JSON inválido): {str(e)}\nResposta: {response_text[:300]}")
303
+ except Exception as e:
304
+ raise ValueError(f"Erro na análise da IA: {str(e)}")
305
+
306
+
307
+ # =========================
308
+ # Processamento com Comando Manual (sem IA)
309
+ # =========================
310
+ def manual_command_processing(segments: List[Segment], command: str, fps: int) -> List[Segment]:
311
+ """Fallback: processamento básico sem IA para comandos simples."""
312
+ s = command.lower()
313
+
314
+ count = 1
315
+ m = re.search(r'(\d+)\s*(?:cortes?|clipes?|segmentos?)', s)
316
+ if m:
317
+ count = int(m.group(1))
318
+
319
+ duration_sec = 60
320
+ m = re.search(r'(\d+)\s*(?:segundos?|s\b)', s)
321
+ if m:
322
+ duration_sec = int(m.group(1))
323
+ else:
324
+ m = re.search(r'(\d+)\s*(?:minutos?|min\b)', s)
325
+ if m:
326
+ duration_sec = int(m.group(1)) * 60
327
+
328
+ start_frame = 0
329
+ m = re.search(r'(?:começando|a partir de)\s+(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)', s)
330
+ if m:
331
+ try:
332
+ start_frame = parse_timecode_to_frames(m.group(1), fps)
333
+ except:
334
+ pass
335
+
336
+ results = []
337
+ base_frame = start_frame
338
+
339
+ for i in range(count):
340
+ duration_frames = duration_sec * fps
341
+ end_frame = base_frame + duration_frames
342
+
343
+ text_parts = []
344
+ for seg in segments:
345
+ if seg.start_f >= base_frame and seg.start_f < end_frame:
346
+ if seg.text:
347
+ text_parts.append(seg.text[:100])
348
+
349
+ combined_text = " [...] ".join(text_parts[:10])[:400]
350
+
351
+ results.append(Segment(
352
+ start_tc=frames_to_timecode(base_frame, fps),
353
+ end_tc=frames_to_timecode(end_frame, fps),
354
+ start_f=base_frame,
355
+ end_f=end_frame,
356
+ text=combined_text if combined_text else f"Corte {i+1}",
357
+ score=50.0
358
+ ))
359
+
360
+ base_frame = end_frame
361
+
362
+ return results
363
+
364
+
365
+ # =========================
366
+ # Modo Automático
367
+ # =========================
368
+ def auto_score_segments(
369
+ segs: List[Segment],
370
+ num_segments: int,
371
+ custom_keywords: str,
372
+ weight_emotion: float,
373
+ weight_break: float,
374
+ weight_learn: float,
375
+ weight_viral: float
376
+ ) -> List[Segment]:
377
+ """Sistema de pontuação automática."""
378
+ emotion_words = ['medo', 'coragem', 'amor', 'ódio', 'paixão', 'alegria', 'tristeza']
379
+ break_words = ['nunca', 'de repente', 'surpreendente', 'inesperado', 'incrível']
380
+ learn_words = ['aprendi', 'descobri', 'entendi', 'percebi', 'lição']
381
+ viral_words = ['segredo', 'verdade', 'revelação', 'exclusivo', 'confissão']
382
+
383
+ for s in segs:
384
+ score = 0.0
385
+ text = (s.text or "").lower()
386
+
387
+ for word in emotion_words:
388
+ if word in text:
389
+ score += weight_emotion
390
+
391
+ for word in break_words:
392
+ if word in text:
393
+ score += weight_break
394
+
395
+ for word in learn_words:
396
+ if word in text:
397
+ score += weight_learn
398
+
399
+ for word in viral_words:
400
+ if word in text:
401
+ score += weight_viral
402
+
403
+ if custom_keywords:
404
+ for kw in custom_keywords.split(","):
405
+ kw_clean = kw.strip().lower()
406
+ if kw_clean and kw_clean in text:
407
+ score += 5.0
408
+
409
+ s.score = score
410
+
411
+ segs.sort(key=lambda x: x.score, reverse=True)
412
+ return segs[:num_segments]
413
+
414
+
415
+ # =========================
416
+ # Edição de XML
417
+ # =========================
418
+ def deep_copy_element(elem: ET.Element) -> ET.Element:
419
+ new = ET.Element(elem.tag, attrib=dict(elem.attrib))
420
+ new.text = elem.text
421
+ new.tail = elem.tail
422
+ for child in elem:
423
+ new.append(deep_copy_element(child))
424
+ return new
425
+
426
+
427
+ def edit_xml(tree: ET.ElementTree, segs: List[Segment]) -> ET.ElementTree:
428
+ root = tree.getroot()
429
+ seq = root.find(".//sequence")
430
+ if seq is None:
431
+ raise ValueError("Sequence não encontrada no XML")
432
+
433
+ v_track = seq.find(".//media/video/track")
434
+ a_track = seq.find(".//media/audio/track")
435
+ if not v_track or not a_track:
436
+ raise ValueError("Trilhas de vídeo/áudio não encontradas")
437
+
438
+ v_template = v_track.find("./clipitem")
439
+ a_template = a_track.find("./clipitem")
440
+
441
+ for clip in list(v_track.findall("./clipitem")):
442
+ v_track.remove(clip)
443
+ for clip in list(a_track.findall("./clipitem")):
444
+ a_track.remove(clip)
445
+
446
+ timeline_pos = 0
447
+ for i, seg in enumerate(segs, 1):
448
+ duration = seg.end_f - seg.start_f
449
+ if duration <= 0:
450
+ continue
451
+
452
+ v_clip = ET.Element("clipitem", {"id": f"clip-v{i}"})
453
+ ET.SubElement(v_clip, "name").text = f"Clip {i}"
454
+ ET.SubElement(v_clip, "start").text = str(timeline_pos)
455
+ ET.SubElement(v_clip, "end").text = str(timeline_pos + duration)
456
+ ET.SubElement(v_clip, "in").text = str(seg.start_f)
457
+ ET.SubElement(v_clip, "out").text = str(seg.end_f)
458
+
459
+ if v_template is not None:
460
+ rate = v_template.find("rate")
461
+ if rate is not None:
462
+ v_clip.append(deep_copy_element(rate))
463
+ file_elem = v_template.find("file")
464
+ if file_elem is not None:
465
+ v_clip.append(deep_copy_element(file_elem))
466
+
467
+ a_clip = ET.Element("clipitem", {"id": f"clip-a{i}"})
468
+ ET.SubElement(a_clip, "name").text = f"Clip {i}"
469
+ ET.SubElement(a_clip, "start").text = str(timeline_pos)
470
+ ET.SubElement(a_clip, "end").text = str(timeline_pos + duration)
471
+ ET.SubElement(a_clip, "in").text = str(seg.start_f)
472
+ ET.SubElement(a_clip, "out").text = str(seg.end_f)
473
+
474
+ if a_template is not None:
475
+ rate = a_template.find("rate")
476
+ if rate is not None:
477
+ a_clip.append(deep_copy_element(rate))
478
+ file_elem = a_template.find("file")
479
+ if file_elem is not None:
480
+ a_clip.append(deep_copy_element(file_elem))
481
+
482
+ v_track.append(v_clip)
483
+ a_track.append(a_clip)
484
+ timeline_pos += duration
485
+
486
+ return tree
487
+
488
+
489
+ # =========================
490
+ # Seleção (orquestração)
491
+ # =========================
492
+ def select_segments(
493
+ transcript_txt: str,
494
+ use_llm: bool,
495
+ num_segments: int,
496
+ custom_keywords: str,
497
+ manual_timecodes: str,
498
+ natural_instructions: str,
499
+ weight_emotion: float,
500
+ weight_break: float,
501
+ weight_learn: float,
502
+ weight_viral: float,
503
+ progress_callback=None
504
+ ) -> List[Segment]:
505
+
506
+ # 1) Manual
507
+ manual = parse_manual_timecodes(manual_timecodes)
508
+ if manual:
509
+ result = []
510
+ for start_tc, end_tc in manual:
511
+ try:
512
+ result.append(Segment(
513
+ start_tc=frames_to_timecode(parse_timecode_to_frames(start_tc)),
514
+ end_tc=frames_to_timecode(parse_timecode_to_frames(end_tc)),
515
+ start_f=parse_timecode_to_frames(start_tc),
516
+ end_f=parse_timecode_to_frames(end_tc),
517
+ text=f"Manual: {start_tc} - {end_tc}",
518
+ score=100.0
519
+ ))
520
+ except Exception:
521
+ pass
522
+ return result
523
+
524
+ # 2) Parser de transcrição
525
+ segs = parse_transcript(transcript_txt) if transcript_txt else []
526
+
527
+ # 3) Linguagem natural COM IA
528
+ if natural_instructions.strip():
529
+ if use_llm and LLM_AVAILABLE and segs:
530
+ # USA IA PARA ANÁLISE COMPLETA
531
+ return ai_analyze_and_select(segs, natural_instructions, progress_callback)
532
+ elif segs:
533
+ # Fallback sem IA
534
+ return manual_command_processing(segs, natural_instructions)
535
+ else:
536
+ raise ValueError("Para usar comandos em linguagem natural, forneça uma transcrição ou ative as minutagens manuais.")
537
+
538
+ # 4) Automático
539
+ if not segs:
540
+ raise ValueError("Nenhum segmento encontrado. Forneça uma transcrição, minutagens ou um comando em linguagem natural.")
541
+ return auto_score_segments(
542
+ segs, num_segments, custom_keywords,
543
+ weight_emotion, weight_break, weight_learn, weight_viral
544
+ )
545
+
546
+
547
+ # =========================
548
+ # Pipeline principal
549
+ # =========================
550
+ def process_files(
551
+ xml_file, txt_file, use_llm, num_segments,
552
+ custom_keywords, manual_timecodes, natural_instructions,
553
+ weight_emotion, weight_break, weight_learn, weight_viral,
554
+ progress=gr.Progress()
555
+ ):
556
+ if not xml_file:
557
+ return "⚠️ Envie o XML do Premiere", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
558
+
559
+ try:
560
+ debug_info = []
561
+
562
+ def progress_callback(msg):
563
+ progress(0.5, desc=msg)
564
+ debug_info.append(msg)
565
+
566
+ progress(0.1, desc="📂 Carregando arquivos...")
567
+
568
+ transcript = ""
569
+ manual = parse_manual_timecodes(manual_timecodes)
570
+
571
+ if not manual and txt_file:
572
+ with open(txt_file.name, "r", encoding="utf-8-sig") as f:
573
+ transcript = f.read()
574
+ debug_info.append(f"📄 Transcrição: {len(transcript)} caracteres")
575
+
576
+ progress(0.2, desc="🔍 Selecionando segmentos...")
577
+
578
+ segments = select_segments(
579
+ transcript, use_llm and LLM_AVAILABLE, num_segments,
580
+ custom_keywords, manual_timecodes, natural_instructions,
581
+ weight_emotion, weight_break, weight_learn, weight_viral,
582
+ progress_callback
583
+ )
584
+
585
+ if not segments:
586
+ return "⚠️ Nenhum segmento selecionado", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
587
+
588
+ valid_segments = []
589
+ for seg in segments:
590
+ if seg.end_f > seg.start_f and seg.end_f - seg.start_f >= FPS:
591
+ valid_segments.append(seg)
592
+
593
+ if not valid_segments:
594
+ return "⚠️ Segmentos inválidos (duração muito curta)", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
595
+
596
+ segments = valid_segments
597
+ debug_info.append(f"✓ {len(segments)} segmento(s) válido(s)")
598
+
599
+ progress(0.7, desc="✂️ Editando XML...")
600
+
601
+ tree = ET.parse(xml_file.name)
602
+ tree = edit_xml(tree, segments)
603
+
604
+ basename = os.path.splitext(os.path.basename(xml_file.name))[0]
605
+ output = os.path.join(OUTPUT_DIR, f"{basename}_EDITADO.xml")
606
+ tree.write(output, encoding="utf-8", xml_declaration=True)
607
+
608
+ progress(0.9, desc="📊 Gerando resumo...")
609
+
610
+ total_sec = sum((s.end_f - s.start_f) / FPS for s in segments)
611
+ total_min = total_sec / 60.0
612
+
613
+ if manual:
614
+ mode = "🎯 MANUAL"
615
+ elif natural_instructions.strip() and use_llm and LLM_AVAILABLE:
616
+ mode = "🤖 IA COMPLETA (Gemini)"
617
+ elif natural_instructions.strip():
618
+ mode = "📐 BÁSICO (sem IA)"
619
+ else:
620
+ mode = "⚙️ AUTOMÁTICO"
621
+
622
+ summary_lines = [
623
+ "═" * 70,
624
+ f"✨ RESULTADO: {len(segments)} corte(s) | {total_min:.1f} min total",
625
+ f"📊 Modo: {mode}",
626
+ "═" * 70,
627
+ ""
628
+ ]
629
+
630
+ for i, seg in enumerate(segments, 1):
631
+ dur_sec = (seg.end_f - seg.start_f) / FPS
632
+ dur_min = dur_sec / 60.0
633
+
634
+ line = f"🎬 Corte {i}:"
635
+ line += f"\n ⏱️ {seg.start_tc} → {seg.end_tc} ({dur_min:.2f} min / {dur_sec:.0f}s)"
636
+
637
+ if seg.text and len(seg.text.strip()) > 10:
638
+ text_preview = seg.text[:200].strip()
639
+ if len(seg.text) > 200:
640
+ text_preview += "..."
641
+ line += f"\n 💬 {text_preview}"
642
+
643
+ summary_lines.append(line)
644
+ summary_lines.append("")
645
+
646
+ if debug_info:
647
+ summary_lines.append("═" * 70)
648
+ summary_lines.append("🔍 Log do Processamento:")
649
+ summary_lines.extend(f" {info}" for info in debug_info)
650
+
651
+ summary = "\n".join(summary_lines)
652
+ status = f"✅ Sucesso | {mode} | {total_min:.1f} min | LLM: {'✓' if LLM_AVAILABLE else '✗'}"
653
+
654
+ progress(1.0, desc="✅ Concluído!")
655
+ return summary, output, status
656
+
657
+ except Exception as e:
658
+ import traceback
659
+ error_trace = traceback.format_exc()
660
+ print(error_trace)
661
+
662
+ error_msg = f"❌ Erro: {str(e)}\n\n🔍 Detalhes:\n{error_trace[:800]}"
663
+ return error_msg, None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
664
+
665
+
666
+ # =========================
667
+ # Interface Gradio
668
+ # =========================
669
+ with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere - IA") as demo:
670
+ gr.Markdown("# 🎬 Editor XML Premiere - IA Completa (Gemini)")
671
+ gr.Markdown("Sistema que **REALMENTE ENTENDE** seu comando usando análise completa com IA.")
672
+
673
+ status_inicial = f"{'🟢 IA Gemini Ativa - Análise Completa Habilitada' if LLM_AVAILABLE else '🔴 IA Desabilitada - Configure GEMINI_API_KEY para análise inteligente'}"
674
+ gr.Markdown(f"**Status:** {status_inicial}")
675
+
676
+ if LLM_AVAILABLE:
677
+ gr.Markdown("""
678
+ ### 🚀 Como funciona a IA:
679
+ 1. **Você descreve** o que quer em linguagem natural
680
+ 2. **IA analisa** toda a transcrição (pode levar 30-60s)
681
+ 3. **IA identifica** os trechos exatos que correspondem ao seu pedido
682
+ 4. **Sistema cria** os cortes precisos automaticamente
683
+
684
+ ⚡ **Mais lento, mas MUITO mais preciso!**
685
+ """)
686
+ else:
687
+ gr.Markdown("""
688
+ ### ⚠️ IA Desabilitada
689
+ Configure a variável de ambiente `GEMINI_API_KEY` para ativar análise inteligente.
690
+ No modo básico, apenas comandos simples e timecodes manuais funcionam bem.
691
+ """)
692
+
693
+ with gr.Row():
694
+ xml_in = gr.File(label="📄 XML do Premiere", file_types=[".xml"])
695
+ txt_in = gr.File(label="📝 Transcrição (.txt) - OBRIGATÓRIA para IA", file_types=[".txt"])
696
+
697
+ with gr.Row():
698
+ use_llm = gr.Checkbox(
699
+ label="🤖 Usar IA Gemini (análise completa - RECOMENDADO)",
700
+ value=USE_LLM_DEFAULT and LLM_AVAILABLE,
701
+ interactive=LLM_AVAILABLE,
702
+ info="Quando ativo, a IA lê TODA a transcrição e encontra os melhores trechos"
703
+ )
704
+ num_segments = gr.Slider(2, 20, 5, 1, label="📊 Segmentos (apenas modo automático)")
705
+
706
+ with gr.Accordion("💬 Comando em Linguagem Natural (MODO PRINCIPAL)", open=True):
707
+ gr.Markdown("""
708
+ ### ✨ Exemplos de comandos que a IA entende:
709
+
710
+ **📌 Simples:**
711
+ - "Crie 3 cortes de 30 segundos sobre futebol"
712
+ - "Quero 2 clipes de 1 minuto falando sobre Maria"
713
+ - "Faça 5 cortes de 45s sobre o tema educação"
714
+
715
+ **🎯 Específicos:**
716
+ - "1 corte de 10 minutos da parte onde ele fala sobre a infância"
717
+ - "3 cortes de 30s sobre os momentos engraçados"
718
+ - "2 clipes de 1min sobre superação e disciplina"
719
+
720
+ **📍 Com timecode:**
721
+ - "Corte de 5 minutos começando em 00:02:00:00 sobre tecnologia"
722
+ - "3 cortes de 45s a partir de 00:10:00 falando sobre amor"
723
+
724
+ **🔍 Busca temática:**
725
+ - "Os melhores momentos sobre família, cada um com 40s"
726
+ - "Trechos emocionantes de 1 minuto cada"
727
+ - "Partes onde menciona desafios e conquistas"
728
+
729
+ ### 💡 Dicas para melhores resultados:
730
+ - ✅ Seja específico sobre o tema/assunto
731
+ - ✅ Especifique duração e quantidade
732
+ - ✅ Use a transcrição completa
733
+ - ✅ Deixe a IA trabalhar (30-60s de análise)
734
+ - ❌ Evite comandos vagos como "faça algo legal"
735
+ """)
736
+ natural_instructions = gr.Textbox(
737
+ label="Digite seu comando aqui",
738
+ placeholder='Ex: "Crie 3 cortes de 45 segundos sobre os momentos onde ele fala de disciplina e superação"',
739
+ lines=4
740
+ )
741
+
742
+ with gr.Accordion("🎯 Minutagens Manuais (precisão total)", open=False):
743
+ gr.Markdown("Use quando souber exatamente os timecodes. Ignora IA e outros modos.")
744
+ manual_timecodes = gr.Textbox(
745
+ label="Timecodes (um por linha)",
746
+ placeholder="00:21:18:09 - 00:31:18:09\n00:45:20:15 - 00:50:10:22",
747
+ lines=4
748
+ )
749
+
750
+ with gr.Accordion("⚙️ Modo Automático (sem comando)", open=False):
751
+ gr.Markdown("Sistema de pontuação simples. **Não recomendado** - use comandos em linguagem natural.")
752
+ custom_keywords = gr.Textbox(
753
+ label="Palavras-chave (separadas por vírgula)",
754
+ placeholder="coragem, superação, vitória"
755
+ )
756
+ with gr.Row():
757
+ weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="⚡ Peso: emoção")
758
+ weight_break = gr.Slider(0, 5, 1.5, 0.1, label="💥 Peso: quebra")
759
+ with gr.Row():
760
+ weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="🎓 Peso: aprendizado")
761
+ weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="🔥 Peso: viral")
762
+
763
+ btn = gr.Button("🚀 Processar com IA (pode levar 30-60s)", variant="primary", size="lg")
764
+
765
+ with gr.Row():
766
+ with gr.Column(scale=2):
767
+ summary_out = gr.Textbox(label="📋 Resumo dos Cortes", lines=20, max_lines=30)
768
+ with gr.Column(scale=1):
769
+ status_out = gr.Textbox(label="📊 Status", lines=3)
770
+ file_out = gr.File(label="⬇️ Download XML Editado")
771
+
772
+ btn.click(
773
+ process_files,
774
+ [xml_in, txt_in, use_llm, num_segments, custom_keywords,
775
+ manual_timecodes, natural_instructions,
776
+ weight_emotion, weight_break, weight_learn, weight_viral],
777
+ [summary_out, file_out, status_out]
778
+ )
779
+
780
+ gr.Markdown("""
781
+ ---
782
+ ### 📚 Guia Rápido:
783
+
784
+ **🎯 Para melhores resultados:**
785
+ 1. ✅ Envie XML + Transcrição completa
786
+ 2. ✅ Ative a IA (checkbox)
787
+ 3. ✅ Escreva comando claro e específico
788
+ 4. ✅ Aguarde 30-60s para análise completa
789
+ 5. ✅ Baixe e importe no Premiere
790
+
791
+ **⚡ Ordem de prioridade:**
792
+ 1. **Minutagens Manuais** (ignora tudo, máxima precisão)
793
+ 2. **Comando + IA** (análise completa, muito preciso)
794
+ 3. **Comando sem IA** (básico, menos preciso)
795
+ 4. **Modo Automático** (não recomendado)
796
+
797
+ **🔧 Troubleshooting:**
798
+ - Erro "IA não disponível": Configure `GEMINI_API_KEY`
799
+ - Cortes errados: Seja mais específico no comando
800
+ - Demora muito: Normal para IA completa (30-60s)
801
+ - Sem transcrição: Use minutagens manuais
802
+ """)
803
+
804
+ if __name__ == "__main__":
805
+ demo.launch()
806
+ )
807
+ arrow = re.compile(
808
+ r'(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3}|[:;]\d{2})?)\s*-->\s*'
809
+ r'(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3}|[:;]\d{2})?)'
810
+ )
811
+
812
+ i = 0
813
+ while i < len(lines):
814
+ raw = lines[i].strip()
815
+ if not raw or raw.lower() == "desconhecido":
816
+ i += 1
817
+ continue
818
+
819
+ m = line_range.match(raw)
820
+ if m:
821
+ start_tc, end_tc, trailing_text = m.groups()
822
+ text_parts = []
823
+
824
+ if trailing_text.strip():
825
+ text_parts.append(trailing_text.strip())
826
+ else:
827
+ j = i + 1
828
+ while j < len(lines):
829
+ nxt = lines[j].strip()
830
+ if not nxt or line_range.match(nxt) or re.match(r'^\d+\s*
831
+
832
+
833
+ # =========================
834
+ # Minutagens Manuais
835
+ # =========================
836
+ def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
837
+ if not manual_input or not manual_input.strip():
838
+ return []
839
+
840
+ manual_ranges = []
841
+ lines = manual_input.replace(",", "\n").splitlines()
842
+ pattern = re.compile(r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*[-–—]\s*(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)')
843
+ for line in lines:
844
+ m = pattern.search(line.strip())
845
+ if m:
846
+ manual_ranges.append((m.group(1), m.group(2)))
847
+ return manual_ranges
848
+
849
+
850
+ # =========================
851
+ # IA: Análise Inteligente com Gemini
852
+ # =========================
853
+ def ai_analyze_and_select(segments: List[Segment], command: str, progress_callback=None) -> List[Segment]:
854
+ """
855
+ Usa Gemini para analisar a transcrição completa e identificar os melhores trechos.
856
+ Processo em 2 etapas para máxima precisão.
857
+ """
858
+ if not LLM_AVAILABLE or not segments:
859
+ raise ValueError("IA não disponível ou sem segmentos para analisar")
860
+
861
+ if progress_callback:
862
+ progress_callback("🤖 Etapa 1/3: Preparando dados para análise...")
863
+
864
+ # Prepara a transcrição completa com índices
865
+ transcript_data = []
866
+ for i, seg in enumerate(segments):
867
+ duration_sec = (seg.end_f - seg.start_f) / FPS
868
+ transcript_data.append({
869
+ "index": i,
870
+ "timecode": seg.start_tc,
871
+ "duration_sec": round(duration_sec, 1),
872
+ "text": seg.text[:200] # Limita texto para não estourar tokens
873
+ })
874
+
875
+ # Converte para JSON para análise estruturada
876
+ transcript_json = json.dumps(transcript_data, ensure_ascii=False, indent=2)
877
+
878
+ if progress_callback:
879
+ progress_callback(f"🤖 Etapa 2/3: Analisando {len(segments)} segmentos com IA (pode levar 30-60s)...")
880
+
881
+ # Prompt detalhado para análise completa
882
+ prompt = f"""Você é um especialista em edição de vídeo. Analise a transcrição e identifique os MELHORES trechos baseado no comando do usuário.
883
+
884
+ COMANDO DO USUÁRIO:
885
+ {command}
886
+
887
+ TRANSCRIÇÃO COMPLETA (formato JSON com index, timecode, duração e texto):
888
+ {transcript_json}
889
+
890
+ INSTRUÇÕES:
891
+ 1. Leia o comando com atenção e identifique:
892
+ - Quantidade de cortes desejada
893
+ - Duração de cada corte (em segundos)
894
+ - Tema/assunto/palavras-chave mencionados
895
+ - Timecode de início (se mencionado)
896
+
897
+ 2. Analise TODA a transcrição e identifique os segmentos que melhor correspondem ao comando
898
+
899
+ 3. Para cada corte, retorne no formato JSON:
900
+ {{
901
+ "cuts": [
902
+ {{
903
+ "start_index": <índice do segmento inicial>,
904
+ "duration_seconds": <duração desejada em segundos>,
905
+ "reason": "<breve explicação de por que escolheu este trecho>"
906
+ }}
907
+ ]
908
+ }}
909
+
910
+ IMPORTANTE:
911
+ - Seja PRECISO na identificação dos trechos
912
+ - Considere o contexto completo ao redor das palavras-chave
913
+ - Se o comando pedir "sobre X", encontre onde X é realmente discutido
914
+ - Se houver timecode, priorize começar próximo a ele
915
+ - Retorne APENAS o JSON, sem texto adicional
916
+
917
+ Responda com o JSON:"""
918
+
919
+ try:
920
+ response = LLM.generate_content(
921
+ prompt,
922
+ generation_config={
923
+ "temperature": 0.2,
924
+ "max_output_tokens": 2000,
925
+ }
926
+ )
927
+
928
+ response_text = response.text.strip()
929
+
930
+ if progress_callback:
931
+ progress_callback("🤖 Etapa 3/3: Processando resposta da IA...")
932
+
933
+ # Extrai JSON da resposta
934
+ json_match = re.search(r'\{[\s\S]*"cuts"[\s\S]*\}', response_text)
935
+ if not json_match:
936
+ raise ValueError("IA não retornou JSON válido")
937
+
938
+ result = json.loads(json_match.group(0))
939
+ cuts_data = result.get("cuts", [])
940
+
941
+ if not cuts_data:
942
+ raise ValueError("IA não encontrou cortes adequados")
943
+
944
+ # Cria os segmentos baseado na análise da IA
945
+ selected_segments = []
946
+
947
+ for cut_info in cuts_data:
948
+ start_idx = cut_info.get("start_index", 0)
949
+ duration_sec = cut_info.get("duration_seconds", 60)
950
+ reason = cut_info.get("reason", "")
951
+
952
+ if start_idx < 0 or start_idx >= len(segments):
953
+ continue
954
+
955
+ start_seg = segments[start_idx]
956
+ start_frame = start_seg.start_f
957
+ duration_frames = int(duration_sec * FPS)
958
+ end_frame = start_frame + duration_frames
959
+
960
+ # Coleta texto dos segmentos envolvidos
961
+ text_parts = [f"[IA: {reason}]"] if reason else []
962
+ for seg in segments[start_idx:]:
963
+ if seg.start_f < end_frame:
964
+ if seg.text:
965
+ text_parts.append(seg.text[:150])
966
+ else:
967
+ break
968
+
969
+ combined_text = " [...] ".join(text_parts)[:500]
970
+
971
+ selected_segments.append(Segment(
972
+ start_tc=frames_to_timecode(start_frame),
973
+ end_tc=frames_to_timecode(end_frame),
974
+ start_f=start_frame,
975
+ end_f=end_frame,
976
+ text=combined_text,
977
+ score=100.0
978
+ ))
979
+
980
+ return selected_segments
981
+
982
+ except json.JSONDecodeError as e:
983
+ raise ValueError(f"Erro ao processar resposta da IA (JSON inválido): {str(e)}\nResposta: {response_text[:300]}")
984
+ except Exception as e:
985
+ raise ValueError(f"Erro na análise da IA: {str(e)}")
986
+
987
+
988
+ # =========================
989
+ # Processamento com Comando Manual (sem IA)
990
+ # =========================
991
+ def manual_command_processing(segments: List[Segment], command: str) -> List[Segment]:
992
+ """
993
+ Fallback: processamento básico sem IA para comandos simples.
994
+ """
995
+ s = command.lower()
996
+
997
+ # Extrai quantidade
998
+ count = 1
999
+ m = re.search(r'(\d+)\s*(?:cortes?|clipes?|segmentos?)', s)
1000
+ if m:
1001
+ count = int(m.group(1))
1002
+
1003
+ # Extrai duração
1004
+ duration_sec = 60
1005
+ m = re.search(r'(\d+)\s*(?:segundos?|s\b)', s)
1006
+ if m:
1007
+ duration_sec = int(m.group(1))
1008
+ else:
1009
+ m = re.search(r'(\d+)\s*(?:minutos?|min\b)', s)
1010
+ if m:
1011
+ duration_sec = int(m.group(1)) * 60
1012
+
1013
+ # Extrai timecode inicial
1014
+ start_frame = 0
1015
+ m = re.search(r'(?:começando|a partir de)\s+(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)', s)
1016
+ if m:
1017
+ try:
1018
+ start_frame = parse_timecode_to_frames(m.group(1))
1019
+ except:
1020
+ pass
1021
+
1022
+ # Cria cortes contínuos
1023
+ results = []
1024
+ base_frame = start_frame
1025
+
1026
+ for i in range(count):
1027
+ duration_frames = duration_sec * FPS
1028
+ end_frame = base_frame + duration_frames
1029
+
1030
+ # Coleta texto
1031
+ text_parts = []
1032
+ for seg in segments:
1033
+ if seg.start_f >= base_frame and seg.start_f < end_frame:
1034
+ if seg.text:
1035
+ text_parts.append(seg.text[:100])
1036
+
1037
+ combined_text = " [...] ".join(text_parts[:10])[:400]
1038
+
1039
+ results.append(Segment(
1040
+ start_tc=frames_to_timecode(base_frame),
1041
+ end_tc=frames_to_timecode(end_frame),
1042
+ start_f=base_frame,
1043
+ end_f=end_frame,
1044
+ text=combined_text if combined_text else f"Corte {i+1}",
1045
+ score=50.0
1046
+ ))
1047
+
1048
+ base_frame = end_frame
1049
+
1050
+ return results
1051
+
1052
+
1053
+ # =========================
1054
+ # Modo Automático
1055
+ # =========================
1056
+ def auto_score_segments(
1057
+ segs: List[Segment],
1058
+ num_segments: int,
1059
+ custom_keywords: str,
1060
+ weight_emotion: float,
1061
+ weight_break: float,
1062
+ weight_learn: float,
1063
+ weight_viral: float
1064
+ ) -> List[Segment]:
1065
+ """Sistema de pontuação automática."""
1066
+ emotion_words = ['medo', 'coragem', 'amor', 'ódio', 'paixão', 'alegria', 'tristeza']
1067
+ break_words = ['nunca', 'de repente', 'surpreendente', 'inesperado', 'incrível']
1068
+ learn_words = ['aprendi', 'descobri', 'entendi', 'percebi', 'lição']
1069
+ viral_words = ['segredo', 'verdade', 'revelação', 'exclusivo', 'confissão']
1070
+
1071
+ for s in segs:
1072
+ score = 0.0
1073
+ text = (s.text or "").lower()
1074
+
1075
+ for word in emotion_words:
1076
+ if word in text:
1077
+ score += weight_emotion
1078
+
1079
+ for word in break_words:
1080
+ if word in text:
1081
+ score += weight_break
1082
+
1083
+ for word in learn_words:
1084
+ if word in text:
1085
+ score += weight_learn
1086
+
1087
+ for word in viral_words:
1088
+ if word in text:
1089
+ score += weight_viral
1090
+
1091
+ if custom_keywords:
1092
+ for kw in custom_keywords.split(","):
1093
+ kw_clean = kw.strip().lower()
1094
+ if kw_clean and kw_clean in text:
1095
+ score += 5.0
1096
+
1097
+ s.score = score
1098
+
1099
+ segs.sort(key=lambda x: x.score, reverse=True)
1100
+ return segs[:num_segments]
1101
+
1102
+
1103
+ # =========================
1104
+ # Edição de XML
1105
+ # =========================
1106
+ def deep_copy_element(elem: ET.Element) -> ET.Element:
1107
+ new = ET.Element(elem.tag, attrib=dict(elem.attrib))
1108
+ new.text = elem.text
1109
+ new.tail = elem.tail
1110
+ for child in elem:
1111
+ new.append(deep_copy_element(child))
1112
+ return new
1113
+
1114
+
1115
+ def edit_xml(tree: ET.ElementTree, segs: List[Segment]) -> ET.ElementTree:
1116
+ root = tree.getroot()
1117
+ seq = root.find(".//sequence")
1118
+ if seq is None:
1119
+ raise ValueError("Sequence não encontrada no XML")
1120
+
1121
+ v_track = seq.find(".//media/video/track")
1122
+ a_track = seq.find(".//media/audio/track")
1123
+ if not v_track or not a_track:
1124
+ raise ValueError("Trilhas de vídeo/áudio não encontradas")
1125
+
1126
+ v_template = v_track.find("./clipitem")
1127
+ a_template = a_track.find("./clipitem")
1128
+
1129
+ for clip in list(v_track.findall("./clipitem")):
1130
+ v_track.remove(clip)
1131
+ for clip in list(a_track.findall("./clipitem")):
1132
+ a_track.remove(clip)
1133
+
1134
+ timeline_pos = 0
1135
+ for i, seg in enumerate(segs, 1):
1136
+ duration = seg.end_f - seg.start_f
1137
+ if duration <= 0:
1138
+ continue
1139
+
1140
+ v_clip = ET.Element("clipitem", {"id": f"clip-v{i}"})
1141
+ ET.SubElement(v_clip, "name").text = f"Clip {i}"
1142
+ ET.SubElement(v_clip, "start").text = str(timeline_pos)
1143
+ ET.SubElement(v_clip, "end").text = str(timeline_pos + duration)
1144
+ ET.SubElement(v_clip, "in").text = str(seg.start_f)
1145
+ ET.SubElement(v_clip, "out").text = str(seg.end_f)
1146
+
1147
+ if v_template is not None:
1148
+ rate = v_template.find("rate")
1149
+ if rate is not None:
1150
+ v_clip.append(deep_copy_element(rate))
1151
+ file_elem = v_template.find("file")
1152
+ if file_elem is not None:
1153
+ v_clip.append(deep_copy_element(file_elem))
1154
+
1155
+ a_clip = ET.Element("clipitem", {"id": f"clip-a{i}"})
1156
+ ET.SubElement(a_clip, "name").text = f"Clip {i}"
1157
+ ET.SubElement(a_clip, "start").text = str(timeline_pos)
1158
+ ET.SubElement(a_clip, "end").text = str(timeline_pos + duration)
1159
+ ET.SubElement(a_clip, "in").text = str(seg.start_f)
1160
+ ET.SubElement(a_clip, "out").text = str(seg.end_f)
1161
+
1162
+ if a_template is not None:
1163
+ rate = a_template.find("rate")
1164
+ if rate is not None:
1165
+ a_clip.append(deep_copy_element(rate))
1166
+ file_elem = a_template.find("file")
1167
+ if file_elem is not None:
1168
+ a_clip.append(deep_copy_element(file_elem))
1169
+
1170
+ v_track.append(v_clip)
1171
+ a_track.append(a_clip)
1172
+ timeline_pos += duration
1173
+
1174
+ return tree
1175
+
1176
+
1177
+ # =========================
1178
+ # Seleção (orquestração)
1179
+ # =========================
1180
+ def select_segments(
1181
+ transcript_txt: str,
1182
+ use_llm: bool,
1183
+ num_segments: int,
1184
+ custom_keywords: str,
1185
+ manual_timecodes: str,
1186
+ natural_instructions: str,
1187
+ weight_emotion: float,
1188
+ weight_break: float,
1189
+ weight_learn: float,
1190
+ weight_viral: float,
1191
+ progress_callback=None
1192
+ ) -> List[Segment]:
1193
+
1194
+ # 1) Manual
1195
+ manual = parse_manual_timecodes(manual_timecodes)
1196
+ if manual:
1197
+ result = []
1198
+ for start_tc, end_tc in manual:
1199
+ try:
1200
+ result.append(Segment(
1201
+ start_tc=frames_to_timecode(parse_timecode_to_frames(start_tc)),
1202
+ end_tc=frames_to_timecode(parse_timecode_to_frames(end_tc)),
1203
+ start_f=parse_timecode_to_frames(start_tc),
1204
+ end_f=parse_timecode_to_frames(end_tc),
1205
+ text=f"Manual: {start_tc} - {end_tc}",
1206
+ score=100.0
1207
+ ))
1208
+ except Exception:
1209
+ pass
1210
+ return result
1211
+
1212
+ # 2) Parser de transcrição
1213
+ segs = parse_transcript(transcript_txt) if transcript_txt else []
1214
+
1215
+ # 3) Linguagem natural COM IA
1216
+ if natural_instructions.strip():
1217
+ if use_llm and LLM_AVAILABLE and segs:
1218
+ # USA IA PARA ANÁLISE COMPLETA
1219
+ return ai_analyze_and_select(segs, natural_instructions, progress_callback)
1220
+ elif segs:
1221
+ # Fallback sem IA
1222
+ return manual_command_processing(segs, natural_instructions)
1223
+ else:
1224
+ raise ValueError("Para usar comandos em linguagem natural, forneça uma transcrição ou ative as minutagens manuais.")
1225
+
1226
+ # 4) Automático
1227
+ if not segs:
1228
+ raise ValueError("Nenhum segmento encontrado. Forneça uma transcrição, minutagens ou um comando em linguagem natural.")
1229
+ return auto_score_segments(
1230
+ segs, num_segments, custom_keywords,
1231
+ weight_emotion, weight_break, weight_learn, weight_viral
1232
+ )
1233
+
1234
+
1235
+ # =========================
1236
+ # Pipeline principal
1237
+ # =========================
1238
+ def process_files(
1239
+ xml_file, txt_file, use_llm, num_segments,
1240
+ custom_keywords, manual_timecodes, natural_instructions,
1241
+ weight_emotion, weight_break, weight_learn, weight_viral,
1242
+ progress=gr.Progress()
1243
+ ):
1244
+ if not xml_file:
1245
+ return "⚠️ Envie o XML do Premiere", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1246
+
1247
+ try:
1248
+ debug_info = []
1249
+
1250
+ def progress_callback(msg):
1251
+ progress(0.5, desc=msg)
1252
+ debug_info.append(msg)
1253
+
1254
+ progress(0.1, desc="📂 Carregando arquivos...")
1255
+
1256
+ transcript = ""
1257
+ manual = parse_manual_timecodes(manual_timecodes)
1258
+
1259
+ if not manual and txt_file:
1260
+ with open(txt_file.name, "r", encoding="utf-8-sig") as f:
1261
+ transcript = f.read()
1262
+ debug_info.append(f"📄 Transcrição: {len(transcript)} caracteres")
1263
+
1264
+ progress(0.2, desc="🔍 Selecionando segmentos...")
1265
+
1266
+ segments = select_segments(
1267
+ transcript, use_llm and LLM_AVAILABLE, num_segments,
1268
+ custom_keywords, manual_timecodes, natural_instructions,
1269
+ weight_emotion, weight_break, weight_learn, weight_viral,
1270
+ progress_callback
1271
+ )
1272
+
1273
+ if not segments:
1274
+ return "⚠️ Nenhum segmento selecionado", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1275
+
1276
+ valid_segments = []
1277
+ for seg in segments:
1278
+ if seg.end_f > seg.start_f and seg.end_f - seg.start_f >= FPS:
1279
+ valid_segments.append(seg)
1280
+
1281
+ if not valid_segments:
1282
+ return "⚠️ Segmentos inválidos (duração muito curta)", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1283
+
1284
+ segments = valid_segments
1285
+ debug_info.append(f"✓ {len(segments)} segmento(s) válido(s)")
1286
+
1287
+ progress(0.7, desc="✂️ Editando XML...")
1288
+
1289
+ tree = ET.parse(xml_file.name)
1290
+ tree = edit_xml(tree, segments)
1291
+
1292
+ basename = os.path.splitext(os.path.basename(xml_file.name))[0]
1293
+ output = os.path.join(OUTPUT_DIR, f"{basename}_EDITADO.xml")
1294
+ tree.write(output, encoding="utf-8", xml_declaration=True)
1295
+
1296
+ progress(0.9, desc="📊 Gerando resumo...")
1297
+
1298
+ total_sec = sum((s.end_f - s.start_f) / FPS for s in segments)
1299
+ total_min = total_sec / 60.0
1300
+
1301
+ if manual:
1302
+ mode = "🎯 MANUAL"
1303
+ elif natural_instructions.strip() and use_llm and LLM_AVAILABLE:
1304
+ mode = "🤖 IA COMPLETA (Gemini)"
1305
+ elif natural_instructions.strip():
1306
+ mode = "📐 BÁSICO (sem IA)"
1307
+ else:
1308
+ mode = "⚙️ AUTOMÁTICO"
1309
+
1310
+ summary_lines = [
1311
+ "═" * 70,
1312
+ f"✨ RESULTADO: {len(segments)} corte(s) | {total_min:.1f} min total",
1313
+ f"📊 Modo: {mode}",
1314
+ "═" * 70,
1315
+ ""
1316
+ ]
1317
+
1318
+ for i, seg in enumerate(segments, 1):
1319
+ dur_sec = (seg.end_f - seg.start_f) / FPS
1320
+ dur_min = dur_sec / 60.0
1321
+
1322
+ line = f"🎬 Corte {i}:"
1323
+ line += f"\n ⏱️ {seg.start_tc} → {seg.end_tc} ({dur_min:.2f} min / {dur_sec:.0f}s)"
1324
+
1325
+ if seg.text and len(seg.text.strip()) > 10:
1326
+ text_preview = seg.text[:200].strip()
1327
+ if len(seg.text) > 200:
1328
+ text_preview += "..."
1329
+ line += f"\n 💬 {text_preview}"
1330
+
1331
+ summary_lines.append(line)
1332
+ summary_lines.append("")
1333
+
1334
+ if debug_info:
1335
+ summary_lines.append("═" * 70)
1336
+ summary_lines.append("🔍 Log do Processamento:")
1337
+ summary_lines.extend(f" {info}" for info in debug_info)
1338
+
1339
+ summary = "\n".join(summary_lines)
1340
+ status = f"✅ Sucesso | {mode} | {total_min:.1f} min | LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1341
+
1342
+ progress(1.0, desc="✅ Concluído!")
1343
+ return summary, output, status
1344
+
1345
+ except Exception as e:
1346
+ import traceback
1347
+ error_trace = traceback.format_exc()
1348
+ print(error_trace)
1349
+
1350
+ error_msg = f"❌ Erro: {str(e)}\n\n🔍 Detalhes:\n{error_trace[:800]}"
1351
+ return error_msg, None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1352
+
1353
+
1354
+ # =========================
1355
+ # Interface Gradio
1356
+ # =========================
1357
+ with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere - IA") as demo:
1358
+ gr.Markdown("# 🎬 Editor XML Premiere - IA Completa (Gemini)")
1359
+ gr.Markdown("Sistema que **REALMENTE ENTENDE** seu comando usando análise completa com IA.")
1360
+
1361
+ status_inicial = f"{'🟢 IA Gemini Ativa - Análise Completa Habilitada' if LLM_AVAILABLE else '🔴 IA Desabilitada - Configure GEMINI_API_KEY para análise inteligente'}"
1362
+ gr.Markdown(f"**Status:** {status_inicial}")
1363
+
1364
+ if LLM_AVAILABLE:
1365
+ gr.Markdown("""
1366
+ ### 🚀 Como funciona a IA:
1367
+ 1. **Você descreve** o que quer em linguagem natural
1368
+ 2. **IA analisa** toda a transcrição (pode levar 30-60s)
1369
+ 3. **IA identifica** os trechos exatos que correspondem ao seu pedido
1370
+ 4. **Sistema cria** os cortes precisos automaticamente
1371
+
1372
+ ⚡ **Mais lento, mas MUITO mais preciso!**
1373
+ """)
1374
+ else:
1375
+ gr.Markdown("""
1376
+ ### ⚠️ IA Desabilitada
1377
+ Configure a variável de ambiente `GEMINI_API_KEY` para ativar análise inteligente.
1378
+ No modo básico, apenas comandos simples e timecodes manuais funcionam bem.
1379
+ """)
1380
+
1381
+ with gr.Row():
1382
+ xml_in = gr.File(label="📄 XML do Premiere", file_types=[".xml"])
1383
+ txt_in = gr.File(label="📝 Transcrição (.txt) - OBRIGATÓRIA para IA", file_types=[".txt"])
1384
+
1385
+ with gr.Row():
1386
+ use_llm = gr.Checkbox(
1387
+ label="🤖 Usar IA Gemini (análise completa - RECOMENDADO)",
1388
+ value=USE_LLM_DEFAULT and LLM_AVAILABLE,
1389
+ interactive=LLM_AVAILABLE,
1390
+ info="Quando ativo, a IA lê TODA a transcrição e encontra os melhores trechos"
1391
+ )
1392
+ num_segments = gr.Slider(2, 20, 5, 1, label="📊 Segmentos (apenas modo automático)")
1393
+
1394
+ with gr.Accordion("💬 Comando em Linguagem Natural (MODO PRINCIPAL)", open=True):
1395
+ gr.Markdown("""
1396
+ ### ✨ Exemplos de comandos que a IA entende:
1397
+
1398
+ **📌 Simples:**
1399
+ - "Crie 3 cortes de 30 segundos sobre futebol"
1400
+ - "Quero 2 clipes de 1 minuto falando sobre Maria"
1401
+ - "Faça 5 cortes de 45s sobre o tema educação"
1402
+
1403
+ **🎯 Específicos:**
1404
+ - "1 corte de 10 minutos da parte onde ele fala sobre a infância"
1405
+ - "3 cortes de 30s sobre os momentos engraçados"
1406
+ - "2 clipes de 1min sobre superação e disciplina"
1407
+
1408
+ **📍 Com timecode:**
1409
+ - "Corte de 5 minutos começando em 00:02:00:00 sobre tecnologia"
1410
+ - "3 cortes de 45s a partir de 00:10:00 falando sobre amor"
1411
+
1412
+ **🔍 Busca temática:**
1413
+ - "Os melhores momentos sobre família, cada um com 40s"
1414
+ - "Trechos emocionantes de 1 minuto cada"
1415
+ - "Partes onde menciona desafios e conquistas"
1416
+
1417
+ ### 💡 Dicas para melhores resultados:
1418
+ - ✅ Seja específico sobre o tema/assunto
1419
+ - ✅ Especifique duração e quantidade
1420
+ - ✅ Use a transcrição completa
1421
+ - ✅ Deixe a IA trabalhar (30-60s de análise)
1422
+ - ❌ Evite comandos vagos como "faça algo legal"
1423
+ """)
1424
+ natural_instructions = gr.Textbox(
1425
+ label="Digite seu comando aqui",
1426
+ placeholder='Ex: "Crie 3 cortes de 45 segundos sobre os momentos onde ele fala de disciplina e superação"',
1427
+ lines=4
1428
+ )
1429
+
1430
+ with gr.Accordion("🎯 Minutagens Manuais (precisão total)", open=False):
1431
+ gr.Markdown("Use quando souber exatamente os timecodes. Ignora IA e outros modos.")
1432
+ manual_timecodes = gr.Textbox(
1433
+ label="Timecodes (um por linha)",
1434
+ placeholder="00:21:18:09 - 00:31:18:09\n00:45:20:15 - 00:50:10:22",
1435
+ lines=4
1436
+ )
1437
+
1438
+ with gr.Accordion("⚙️ Modo Automático (sem comando)", open=False):
1439
+ gr.Markdown("Sistema de pontuação simples. **Não recomendado** - use comandos em linguagem natural.")
1440
+ custom_keywords = gr.Textbox(
1441
+ label="Palavras-chave (separadas por vírgula)",
1442
+ placeholder="coragem, superação, vitória"
1443
+ )
1444
+ with gr.Row():
1445
+ weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="⚡ Peso: emoção")
1446
+ weight_break = gr.Slider(0, 5, 1.5, 0.1, label="💥 Peso: quebra")
1447
+ with gr.Row():
1448
+ weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="🎓 Peso: aprendizado")
1449
+ weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="🔥 Peso: viral")
1450
+
1451
+ btn = gr.Button("🚀 Processar com IA (pode levar 30-60s)", variant="primary", size="lg")
1452
+
1453
+ with gr.Row():
1454
+ with gr.Column(scale=2):
1455
+ summary_out = gr.Textbox(label="📋 Resumo dos Cortes", lines=20, max_lines=30)
1456
+ with gr.Column(scale=1):
1457
+ status_out = gr.Textbox(label="📊 Status", lines=3)
1458
+ file_out = gr.File(label="⬇️ Download XML Editado")
1459
+
1460
+ btn.click(
1461
+ process_files,
1462
+ [xml_in, txt_in, use_llm, num_segments, custom_keywords,
1463
+ manual_timecodes, natural_instructions,
1464
+ weight_emotion, weight_break, weight_learn, weight_viral],
1465
+ [summary_out, file_out, status_out]
1466
+ )
1467
+
1468
+ gr.Markdown("""
1469
+ ---
1470
+ ### 📚 Guia Rápido:
1471
+
1472
+ **🎯 Para melhores resultados:**
1473
+ 1. ✅ Envie XML + Transcrição completa
1474
+ 2. ✅ Ative a IA (checkbox)
1475
+ 3. ✅ Escreva comando claro e específico
1476
+ 4. ✅ Aguarde 30-60s para análise completa
1477
+ 5. ✅ Baixe e importe no Premiere
1478
+
1479
+ **⚡ Ordem de prioridade:**
1480
+ 1. **Minutagens Manuais** (ignora tudo, máxima precisão)
1481
+ 2. **Comando + IA** (análise completa, muito preciso)
1482
+ 3. **Comando sem IA** (básico, menos preciso)
1483
+ 4. **Modo Automático** (não recomendado)
1484
+
1485
+ **🔧 Troubleshooting:**
1486
+ - Erro "IA não disponível": Configure `GEMINI_API_KEY`
1487
+ - Cortes errados: Seja mais específico no comando
1488
+ - Demora muito: Normal para IA completa (30-60s)
1489
+ - Sem transcrição: Use minutagens manuais
1490
+ """)
1491
+
1492
+ if __name__ == "__main__":
1493
+ demo.launch(), nxt) or arrow.search(nxt):
1494
+ break
1495
+ text_parts.append(nxt)
1496
+ j += 1
1497
+ i = j - 1
1498
+
1499
+ text = " ".join(text_parts).strip()
1500
+ try:
1501
+ sf = parse_timecode_to_frames(start_tc, fps)
1502
+ ef = parse_timecode_to_frames(end_tc, fps)
1503
+ if ef > sf:
1504
+ results.append(Segment(
1505
+ start_tc=frames_to_timecode(sf, fps),
1506
+ end_tc=frames_to_timecode(ef, fps),
1507
+ start_f=sf,
1508
+ end_f=ef,
1509
+ text=text if text else f"{start_tc} - {end_tc}",
1510
+ score=0.0
1511
+ ))
1512
+ except Exception:
1513
+ pass
1514
+ i += 1
1515
+ continue
1516
+
1517
+ if arrow.search(raw) or (i + 1 < len(lines) and arrow.search(lines[i + 1])):
1518
+ line_with_tc = raw if arrow.search(raw) else lines[i + 1]
1519
+ mm = arrow.search(line_with_tc)
1520
+ if mm:
1521
+ start_tc, end_tc = mm.groups()
1522
+ j = i + 1 if line_with_tc == raw else i + 2
1523
+ text_parts = []
1524
+ while j < len(lines):
1525
+ nxt = lines[j].strip()
1526
+ if not nxt:
1527
+ break
1528
+ if re.match(r'^\d+\s*
1529
+
1530
+
1531
+ # =========================
1532
+ # Minutagens Manuais
1533
+ # =========================
1534
+ def parse_manual_timecodes(manual_input: str) -> List[Tuple[str, str]]:
1535
+ if not manual_input or not manual_input.strip():
1536
+ return []
1537
+
1538
+ manual_ranges = []
1539
+ lines = manual_input.replace(",", "\n").splitlines()
1540
+ pattern = re.compile(r'(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)\s*[-–—]\s*(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)')
1541
+ for line in lines:
1542
+ m = pattern.search(line.strip())
1543
+ if m:
1544
+ manual_ranges.append((m.group(1), m.group(2)))
1545
+ return manual_ranges
1546
+
1547
+
1548
+ # =========================
1549
+ # IA: Análise Inteligente com Gemini
1550
+ # =========================
1551
+ def ai_analyze_and_select(segments: List[Segment], command: str, progress_callback=None) -> List[Segment]:
1552
+ """
1553
+ Usa Gemini para analisar a transcrição completa e identificar os melhores trechos.
1554
+ Processo em 2 etapas para máxima precisão.
1555
+ """
1556
+ if not LLM_AVAILABLE or not segments:
1557
+ raise ValueError("IA não disponível ou sem segmentos para analisar")
1558
+
1559
+ if progress_callback:
1560
+ progress_callback("🤖 Etapa 1/3: Preparando dados para análise...")
1561
+
1562
+ # Prepara a transcrição completa com índices
1563
+ transcript_data = []
1564
+ for i, seg in enumerate(segments):
1565
+ duration_sec = (seg.end_f - seg.start_f) / FPS
1566
+ transcript_data.append({
1567
+ "index": i,
1568
+ "timecode": seg.start_tc,
1569
+ "duration_sec": round(duration_sec, 1),
1570
+ "text": seg.text[:200] # Limita texto para não estourar tokens
1571
+ })
1572
+
1573
+ # Converte para JSON para análise estruturada
1574
+ transcript_json = json.dumps(transcript_data, ensure_ascii=False, indent=2)
1575
+
1576
+ if progress_callback:
1577
+ progress_callback(f"🤖 Etapa 2/3: Analisando {len(segments)} segmentos com IA (pode levar 30-60s)...")
1578
+
1579
+ # Prompt detalhado para análise completa
1580
+ prompt = f"""Você é um especialista em edição de vídeo. Analise a transcrição e identifique os MELHORES trechos baseado no comando do usuário.
1581
+
1582
+ COMANDO DO USUÁRIO:
1583
+ {command}
1584
+
1585
+ TRANSCRIÇÃO COMPLETA (formato JSON com index, timecode, duração e texto):
1586
+ {transcript_json}
1587
+
1588
+ INSTRUÇÕES:
1589
+ 1. Leia o comando com atenção e identifique:
1590
+ - Quantidade de cortes desejada
1591
+ - Duração de cada corte (em segundos)
1592
+ - Tema/assunto/palavras-chave mencionados
1593
+ - Timecode de início (se mencionado)
1594
+
1595
+ 2. Analise TODA a transcrição e identifique os segmentos que melhor correspondem ao comando
1596
+
1597
+ 3. Para cada corte, retorne no formato JSON:
1598
+ {{
1599
+ "cuts": [
1600
+ {{
1601
+ "start_index": <índice do segmento inicial>,
1602
+ "duration_seconds": <duração desejada em segundos>,
1603
+ "reason": "<breve explicação de por que escolheu este trecho>"
1604
+ }}
1605
+ ]
1606
+ }}
1607
+
1608
+ IMPORTANTE:
1609
+ - Seja PRECISO na identificação dos trechos
1610
+ - Considere o contexto completo ao redor das palavras-chave
1611
+ - Se o comando pedir "sobre X", encontre onde X é realmente discutido
1612
+ - Se houver timecode, priorize começar próximo a ele
1613
+ - Retorne APENAS o JSON, sem texto adicional
1614
+
1615
+ Responda com o JSON:"""
1616
+
1617
+ try:
1618
+ response = LLM.generate_content(
1619
+ prompt,
1620
+ generation_config={
1621
+ "temperature": 0.2,
1622
+ "max_output_tokens": 2000,
1623
+ }
1624
+ )
1625
+
1626
+ response_text = response.text.strip()
1627
+
1628
+ if progress_callback:
1629
+ progress_callback("🤖 Etapa 3/3: Processando resposta da IA...")
1630
+
1631
+ # Extrai JSON da resposta
1632
+ json_match = re.search(r'\{[\s\S]*"cuts"[\s\S]*\}', response_text)
1633
+ if not json_match:
1634
+ raise ValueError("IA não retornou JSON válido")
1635
+
1636
+ result = json.loads(json_match.group(0))
1637
+ cuts_data = result.get("cuts", [])
1638
+
1639
+ if not cuts_data:
1640
+ raise ValueError("IA não encontrou cortes adequados")
1641
+
1642
+ # Cria os segmentos baseado na análise da IA
1643
+ selected_segments = []
1644
+
1645
+ for cut_info in cuts_data:
1646
+ start_idx = cut_info.get("start_index", 0)
1647
+ duration_sec = cut_info.get("duration_seconds", 60)
1648
+ reason = cut_info.get("reason", "")
1649
+
1650
+ if start_idx < 0 or start_idx >= len(segments):
1651
+ continue
1652
+
1653
+ start_seg = segments[start_idx]
1654
+ start_frame = start_seg.start_f
1655
+ duration_frames = int(duration_sec * FPS)
1656
+ end_frame = start_frame + duration_frames
1657
+
1658
+ # Coleta texto dos segmentos envolvidos
1659
+ text_parts = [f"[IA: {reason}]"] if reason else []
1660
+ for seg in segments[start_idx:]:
1661
+ if seg.start_f < end_frame:
1662
+ if seg.text:
1663
+ text_parts.append(seg.text[:150])
1664
+ else:
1665
+ break
1666
+
1667
+ combined_text = " [...] ".join(text_parts)[:500]
1668
+
1669
+ selected_segments.append(Segment(
1670
+ start_tc=frames_to_timecode(start_frame),
1671
+ end_tc=frames_to_timecode(end_frame),
1672
+ start_f=start_frame,
1673
+ end_f=end_frame,
1674
+ text=combined_text,
1675
+ score=100.0
1676
+ ))
1677
+
1678
+ return selected_segments
1679
+
1680
+ except json.JSONDecodeError as e:
1681
+ raise ValueError(f"Erro ao processar resposta da IA (JSON inválido): {str(e)}\nResposta: {response_text[:300]}")
1682
+ except Exception as e:
1683
+ raise ValueError(f"Erro na análise da IA: {str(e)}")
1684
+
1685
+
1686
+ # =========================
1687
+ # Processamento com Comando Manual (sem IA)
1688
+ # =========================
1689
+ def manual_command_processing(segments: List[Segment], command: str) -> List[Segment]:
1690
+ """
1691
+ Fallback: processamento básico sem IA para comandos simples.
1692
+ """
1693
+ s = command.lower()
1694
+
1695
+ # Extrai quantidade
1696
+ count = 1
1697
+ m = re.search(r'(\d+)\s*(?:cortes?|clipes?|segmentos?)', s)
1698
+ if m:
1699
+ count = int(m.group(1))
1700
+
1701
+ # Extrai duração
1702
+ duration_sec = 60
1703
+ m = re.search(r'(\d+)\s*(?:segundos?|s\b)', s)
1704
+ if m:
1705
+ duration_sec = int(m.group(1))
1706
+ else:
1707
+ m = re.search(r'(\d+)\s*(?:minutos?|min\b)', s)
1708
+ if m:
1709
+ duration_sec = int(m.group(1)) * 60
1710
+
1711
+ # Extrai timecode inicial
1712
+ start_frame = 0
1713
+ m = re.search(r'(?:começando|a partir de)\s+(\d{1,2}:\d{2}:\d{2}(?:[:;]\d{2}|[.,]\d{1,3})?)', s)
1714
+ if m:
1715
+ try:
1716
+ start_frame = parse_timecode_to_frames(m.group(1))
1717
+ except:
1718
+ pass
1719
+
1720
+ # Cria cortes contínuos
1721
+ results = []
1722
+ base_frame = start_frame
1723
+
1724
+ for i in range(count):
1725
+ duration_frames = duration_sec * FPS
1726
+ end_frame = base_frame + duration_frames
1727
+
1728
+ # Coleta texto
1729
+ text_parts = []
1730
+ for seg in segments:
1731
+ if seg.start_f >= base_frame and seg.start_f < end_frame:
1732
+ if seg.text:
1733
+ text_parts.append(seg.text[:100])
1734
+
1735
+ combined_text = " [...] ".join(text_parts[:10])[:400]
1736
+
1737
+ results.append(Segment(
1738
+ start_tc=frames_to_timecode(base_frame),
1739
+ end_tc=frames_to_timecode(end_frame),
1740
+ start_f=base_frame,
1741
+ end_f=end_frame,
1742
+ text=combined_text if combined_text else f"Corte {i+1}",
1743
+ score=50.0
1744
+ ))
1745
+
1746
+ base_frame = end_frame
1747
+
1748
+ return results
1749
+
1750
+
1751
+ # =========================
1752
+ # Modo Automático
1753
+ # =========================
1754
+ def auto_score_segments(
1755
+ segs: List[Segment],
1756
+ num_segments: int,
1757
+ custom_keywords: str,
1758
+ weight_emotion: float,
1759
+ weight_break: float,
1760
+ weight_learn: float,
1761
+ weight_viral: float
1762
+ ) -> List[Segment]:
1763
+ """Sistema de pontuação automática."""
1764
+ emotion_words = ['medo', 'coragem', 'amor', 'ódio', 'paixão', 'alegria', 'tristeza']
1765
+ break_words = ['nunca', 'de repente', 'surpreendente', 'inesperado', 'incrível']
1766
+ learn_words = ['aprendi', 'descobri', 'entendi', 'percebi', 'lição']
1767
+ viral_words = ['segredo', 'verdade', 'revelação', 'exclusivo', 'confissão']
1768
+
1769
+ for s in segs:
1770
+ score = 0.0
1771
+ text = (s.text or "").lower()
1772
+
1773
+ for word in emotion_words:
1774
+ if word in text:
1775
+ score += weight_emotion
1776
+
1777
+ for word in break_words:
1778
+ if word in text:
1779
+ score += weight_break
1780
+
1781
+ for word in learn_words:
1782
+ if word in text:
1783
+ score += weight_learn
1784
+
1785
+ for word in viral_words:
1786
+ if word in text:
1787
+ score += weight_viral
1788
+
1789
+ if custom_keywords:
1790
+ for kw in custom_keywords.split(","):
1791
+ kw_clean = kw.strip().lower()
1792
+ if kw_clean and kw_clean in text:
1793
+ score += 5.0
1794
+
1795
+ s.score = score
1796
+
1797
+ segs.sort(key=lambda x: x.score, reverse=True)
1798
+ return segs[:num_segments]
1799
+
1800
+
1801
+ # =========================
1802
+ # Edição de XML
1803
+ # =========================
1804
+ def deep_copy_element(elem: ET.Element) -> ET.Element:
1805
+ new = ET.Element(elem.tag, attrib=dict(elem.attrib))
1806
+ new.text = elem.text
1807
+ new.tail = elem.tail
1808
+ for child in elem:
1809
+ new.append(deep_copy_element(child))
1810
+ return new
1811
+
1812
+
1813
+ def edit_xml(tree: ET.ElementTree, segs: List[Segment]) -> ET.ElementTree:
1814
+ root = tree.getroot()
1815
+ seq = root.find(".//sequence")
1816
+ if seq is None:
1817
+ raise ValueError("Sequence não encontrada no XML")
1818
+
1819
+ v_track = seq.find(".//media/video/track")
1820
+ a_track = seq.find(".//media/audio/track")
1821
+ if not v_track or not a_track:
1822
+ raise ValueError("Trilhas de vídeo/áudio não encontradas")
1823
+
1824
+ v_template = v_track.find("./clipitem")
1825
+ a_template = a_track.find("./clipitem")
1826
+
1827
+ for clip in list(v_track.findall("./clipitem")):
1828
+ v_track.remove(clip)
1829
+ for clip in list(a_track.findall("./clipitem")):
1830
+ a_track.remove(clip)
1831
+
1832
+ timeline_pos = 0
1833
+ for i, seg in enumerate(segs, 1):
1834
+ duration = seg.end_f - seg.start_f
1835
+ if duration <= 0:
1836
+ continue
1837
+
1838
+ v_clip = ET.Element("clipitem", {"id": f"clip-v{i}"})
1839
+ ET.SubElement(v_clip, "name").text = f"Clip {i}"
1840
+ ET.SubElement(v_clip, "start").text = str(timeline_pos)
1841
+ ET.SubElement(v_clip, "end").text = str(timeline_pos + duration)
1842
+ ET.SubElement(v_clip, "in").text = str(seg.start_f)
1843
+ ET.SubElement(v_clip, "out").text = str(seg.end_f)
1844
+
1845
+ if v_template is not None:
1846
+ rate = v_template.find("rate")
1847
+ if rate is not None:
1848
+ v_clip.append(deep_copy_element(rate))
1849
+ file_elem = v_template.find("file")
1850
+ if file_elem is not None:
1851
+ v_clip.append(deep_copy_element(file_elem))
1852
+
1853
+ a_clip = ET.Element("clipitem", {"id": f"clip-a{i}"})
1854
+ ET.SubElement(a_clip, "name").text = f"Clip {i}"
1855
+ ET.SubElement(a_clip, "start").text = str(timeline_pos)
1856
+ ET.SubElement(a_clip, "end").text = str(timeline_pos + duration)
1857
+ ET.SubElement(a_clip, "in").text = str(seg.start_f)
1858
+ ET.SubElement(a_clip, "out").text = str(seg.end_f)
1859
+
1860
+ if a_template is not None:
1861
+ rate = a_template.find("rate")
1862
+ if rate is not None:
1863
+ a_clip.append(deep_copy_element(rate))
1864
+ file_elem = a_template.find("file")
1865
+ if file_elem is not None:
1866
+ a_clip.append(deep_copy_element(file_elem))
1867
+
1868
+ v_track.append(v_clip)
1869
+ a_track.append(a_clip)
1870
+ timeline_pos += duration
1871
+
1872
+ return tree
1873
+
1874
+
1875
+ # =========================
1876
+ # Seleção (orquestração)
1877
+ # =========================
1878
+ def select_segments(
1879
+ transcript_txt: str,
1880
+ use_llm: bool,
1881
+ num_segments: int,
1882
+ custom_keywords: str,
1883
+ manual_timecodes: str,
1884
+ natural_instructions: str,
1885
+ weight_emotion: float,
1886
+ weight_break: float,
1887
+ weight_learn: float,
1888
+ weight_viral: float,
1889
+ progress_callback=None
1890
+ ) -> List[Segment]:
1891
+
1892
+ # 1) Manual
1893
+ manual = parse_manual_timecodes(manual_timecodes)
1894
+ if manual:
1895
+ result = []
1896
+ for start_tc, end_tc in manual:
1897
+ try:
1898
+ result.append(Segment(
1899
+ start_tc=frames_to_timecode(parse_timecode_to_frames(start_tc)),
1900
+ end_tc=frames_to_timecode(parse_timecode_to_frames(end_tc)),
1901
+ start_f=parse_timecode_to_frames(start_tc),
1902
+ end_f=parse_timecode_to_frames(end_tc),
1903
+ text=f"Manual: {start_tc} - {end_tc}",
1904
+ score=100.0
1905
+ ))
1906
+ except Exception:
1907
+ pass
1908
+ return result
1909
+
1910
+ # 2) Parser de transcrição
1911
+ segs = parse_transcript(transcript_txt) if transcript_txt else []
1912
+
1913
+ # 3) Linguagem natural COM IA
1914
+ if natural_instructions.strip():
1915
+ if use_llm and LLM_AVAILABLE and segs:
1916
+ # USA IA PARA ANÁLISE COMPLETA
1917
+ return ai_analyze_and_select(segs, natural_instructions, progress_callback)
1918
+ elif segs:
1919
+ # Fallback sem IA
1920
+ return manual_command_processing(segs, natural_instructions)
1921
+ else:
1922
+ raise ValueError("Para usar comandos em linguagem natural, forneça uma transcrição ou ative as minutagens manuais.")
1923
+
1924
+ # 4) Automático
1925
+ if not segs:
1926
+ raise ValueError("Nenhum segmento encontrado. Forneça uma transcrição, minutagens ou um comando em linguagem natural.")
1927
+ return auto_score_segments(
1928
+ segs, num_segments, custom_keywords,
1929
+ weight_emotion, weight_break, weight_learn, weight_viral
1930
+ )
1931
+
1932
+
1933
+ # =========================
1934
+ # Pipeline principal
1935
+ # =========================
1936
+ def process_files(
1937
+ xml_file, txt_file, use_llm, num_segments,
1938
+ custom_keywords, manual_timecodes, natural_instructions,
1939
+ weight_emotion, weight_break, weight_learn, weight_viral,
1940
+ progress=gr.Progress()
1941
+ ):
1942
+ if not xml_file:
1943
+ return "⚠️ Envie o XML do Premiere", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1944
+
1945
+ try:
1946
+ debug_info = []
1947
+
1948
+ def progress_callback(msg):
1949
+ progress(0.5, desc=msg)
1950
+ debug_info.append(msg)
1951
+
1952
+ progress(0.1, desc="📂 Carregando arquivos...")
1953
+
1954
+ transcript = ""
1955
+ manual = parse_manual_timecodes(manual_timecodes)
1956
+
1957
+ if not manual and txt_file:
1958
+ with open(txt_file.name, "r", encoding="utf-8-sig") as f:
1959
+ transcript = f.read()
1960
+ debug_info.append(f"📄 Transcrição: {len(transcript)} caracteres")
1961
+
1962
+ progress(0.2, desc="🔍 Selecionando segmentos...")
1963
+
1964
+ segments = select_segments(
1965
+ transcript, use_llm and LLM_AVAILABLE, num_segments,
1966
+ custom_keywords, manual_timecodes, natural_instructions,
1967
+ weight_emotion, weight_break, weight_learn, weight_viral,
1968
+ progress_callback
1969
+ )
1970
+
1971
+ if not segments:
1972
+ return "⚠️ Nenhum segmento selecionado", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1973
+
1974
+ valid_segments = []
1975
+ for seg in segments:
1976
+ if seg.end_f > seg.start_f and seg.end_f - seg.start_f >= FPS:
1977
+ valid_segments.append(seg)
1978
+
1979
+ if not valid_segments:
1980
+ return "⚠️ Segmentos inválidos (duração muito curta)", None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
1981
+
1982
+ segments = valid_segments
1983
+ debug_info.append(f"✓ {len(segments)} segmento(s) válido(s)")
1984
+
1985
+ progress(0.7, desc="✂️ Editando XML...")
1986
+
1987
+ tree = ET.parse(xml_file.name)
1988
+ tree = edit_xml(tree, segments)
1989
+
1990
+ basename = os.path.splitext(os.path.basename(xml_file.name))[0]
1991
+ output = os.path.join(OUTPUT_DIR, f"{basename}_EDITADO.xml")
1992
+ tree.write(output, encoding="utf-8", xml_declaration=True)
1993
+
1994
+ progress(0.9, desc="📊 Gerando resumo...")
1995
+
1996
+ total_sec = sum((s.end_f - s.start_f) / FPS for s in segments)
1997
+ total_min = total_sec / 60.0
1998
+
1999
+ if manual:
2000
+ mode = "🎯 MANUAL"
2001
+ elif natural_instructions.strip() and use_llm and LLM_AVAILABLE:
2002
+ mode = "🤖 IA COMPLETA (Gemini)"
2003
+ elif natural_instructions.strip():
2004
+ mode = "📐 BÁSICO (sem IA)"
2005
+ else:
2006
+ mode = "⚙️ AUTOMÁTICO"
2007
+
2008
+ summary_lines = [
2009
+ "═" * 70,
2010
+ f"✨ RESULTADO: {len(segments)} corte(s) | {total_min:.1f} min total",
2011
+ f"📊 Modo: {mode}",
2012
+ "═" * 70,
2013
+ ""
2014
+ ]
2015
+
2016
+ for i, seg in enumerate(segments, 1):
2017
+ dur_sec = (seg.end_f - seg.start_f) / FPS
2018
+ dur_min = dur_sec / 60.0
2019
+
2020
+ line = f"🎬 Corte {i}:"
2021
+ line += f"\n ⏱️ {seg.start_tc} → {seg.end_tc} ({dur_min:.2f} min / {dur_sec:.0f}s)"
2022
+
2023
+ if seg.text and len(seg.text.strip()) > 10:
2024
+ text_preview = seg.text[:200].strip()
2025
+ if len(seg.text) > 200:
2026
+ text_preview += "..."
2027
+ line += f"\n 💬 {text_preview}"
2028
+
2029
+ summary_lines.append(line)
2030
+ summary_lines.append("")
2031
+
2032
+ if debug_info:
2033
+ summary_lines.append("═" * 70)
2034
+ summary_lines.append("🔍 Log do Processamento:")
2035
+ summary_lines.extend(f" {info}" for info in debug_info)
2036
+
2037
+ summary = "\n".join(summary_lines)
2038
+ status = f"✅ Sucesso | {mode} | {total_min:.1f} min | LLM: {'✓' if LLM_AVAILABLE else '✗'}"
2039
+
2040
+ progress(1.0, desc="✅ Concluído!")
2041
+ return summary, output, status
2042
+
2043
+ except Exception as e:
2044
+ import traceback
2045
+ error_trace = traceback.format_exc()
2046
+ print(error_trace)
2047
+
2048
+ error_msg = f"❌ Erro: {str(e)}\n\n🔍 Detalhes:\n{error_trace[:800]}"
2049
+ return error_msg, None, f"LLM: {'✓' if LLM_AVAILABLE else '✗'}"
2050
+
2051
+
2052
+ # =========================
2053
+ # Interface Gradio
2054
+ # =========================
2055
+ with gr.Blocks(theme=gr.themes.Soft(), title="Editor XML Premiere - IA") as demo:
2056
+ gr.Markdown("# 🎬 Editor XML Premiere - IA Completa (Gemini)")
2057
+ gr.Markdown("Sistema que **REALMENTE ENTENDE** seu comando usando análise completa com IA.")
2058
+
2059
+ status_inicial = f"{'🟢 IA Gemini Ativa - Análise Completa Habilitada' if LLM_AVAILABLE else '🔴 IA Desabilitada - Configure GEMINI_API_KEY para análise inteligente'}"
2060
+ gr.Markdown(f"**Status:** {status_inicial}")
2061
+
2062
+ if LLM_AVAILABLE:
2063
+ gr.Markdown("""
2064
+ ### 🚀 Como funciona a IA:
2065
+ 1. **Você descreve** o que quer em linguagem natural
2066
+ 2. **IA analisa** toda a transcrição (pode levar 30-60s)
2067
+ 3. **IA identifica** os trechos exatos que correspondem ao seu pedido
2068
+ 4. **Sistema cria** os cortes precisos automaticamente
2069
+
2070
+ ⚡ **Mais lento, mas MUITO mais preciso!**
2071
+ """)
2072
+ else:
2073
+ gr.Markdown("""
2074
+ ### ⚠️ IA Desabilitada
2075
+ Configure a variável de ambiente `GEMINI_API_KEY` para ativar análise inteligente.
2076
+ No modo básico, apenas comandos simples e timecodes manuais funcionam bem.
2077
+ """)
2078
+
2079
+ with gr.Row():
2080
+ xml_in = gr.File(label="📄 XML do Premiere", file_types=[".xml"])
2081
+ txt_in = gr.File(label="📝 Transcrição (.txt) - OBRIGATÓRIA para IA", file_types=[".txt"])
2082
+
2083
+ with gr.Row():
2084
+ use_llm = gr.Checkbox(
2085
+ label="🤖 Usar IA Gemini (análise completa - RECOMENDADO)",
2086
+ value=USE_LLM_DEFAULT and LLM_AVAILABLE,
2087
+ interactive=LLM_AVAILABLE,
2088
+ info="Quando ativo, a IA lê TODA a transcrição e encontra os melhores trechos"
2089
+ )
2090
+ num_segments = gr.Slider(2, 20, 5, 1, label="📊 Segmentos (apenas modo automático)")
2091
+
2092
+ with gr.Accordion("💬 Comando em Linguagem Natural (MODO PRINCIPAL)", open=True):
2093
+ gr.Markdown("""
2094
+ ### ✨ Exemplos de comandos que a IA entende:
2095
+
2096
+ **📌 Simples:**
2097
+ - "Crie 3 cortes de 30 segundos sobre futebol"
2098
+ - "Quero 2 clipes de 1 minuto falando sobre Maria"
2099
+ - "Faça 5 cortes de 45s sobre o tema educação"
2100
+
2101
+ **🎯 Específicos:**
2102
+ - "1 corte de 10 minutos da parte onde ele fala sobre a infância"
2103
+ - "3 cortes de 30s sobre os momentos engraçados"
2104
+ - "2 clipes de 1min sobre superação e disciplina"
2105
+
2106
+ **📍 Com timecode:**
2107
+ - "Corte de 5 minutos começando em 00:02:00:00 sobre tecnologia"
2108
+ - "3 cortes de 45s a partir de 00:10:00 falando sobre amor"
2109
+
2110
+ **🔍 Busca temática:**
2111
+ - "Os melhores momentos sobre família, cada um com 40s"
2112
+ - "Trechos emocionantes de 1 minuto cada"
2113
+ - "Partes onde menciona desafios e conquistas"
2114
+
2115
+ ### 💡 Dicas para melhores resultados:
2116
+ - ✅ Seja específico sobre o tema/assunto
2117
+ - ✅ Especifique duração e quantidade
2118
+ - ✅ Use a transcrição completa
2119
+ - ✅ Deixe a IA trabalhar (30-60s de análise)
2120
+ - ❌ Evite comandos vagos como "faça algo legal"
2121
+ """)
2122
+ natural_instructions = gr.Textbox(
2123
+ label="Digite seu comando aqui",
2124
+ placeholder='Ex: "Crie 3 cortes de 45 segundos sobre os momentos onde ele fala de disciplina e superação"',
2125
+ lines=4
2126
+ )
2127
+
2128
+ with gr.Accordion("🎯 Minutagens Manuais (precisão total)", open=False):
2129
+ gr.Markdown("Use quando souber exatamente os timecodes. Ignora IA e outros modos.")
2130
+ manual_timecodes = gr.Textbox(
2131
+ label="Timecodes (um por linha)",
2132
+ placeholder="00:21:18:09 - 00:31:18:09\n00:45:20:15 - 00:50:10:22",
2133
+ lines=4
2134
+ )
2135
+
2136
+ with gr.Accordion("⚙️ Modo Automático (sem comando)", open=False):
2137
+ gr.Markdown("Sistema de pontuação simples. **Não recomendado** - use comandos em linguagem natural.")
2138
+ custom_keywords = gr.Textbox(
2139
+ label="Palavras-chave (separadas por vírgula)",
2140
+ placeholder="coragem, superação, vitória"
2141
+ )
2142
+ with gr.Row():
2143
+ weight_emotion = gr.Slider(0, 5, 2.0, 0.1, label="⚡ Peso: emoção")
2144
+ weight_break = gr.Slider(0, 5, 1.5, 0.1, label="💥 Peso: quebra")
2145
+ with gr.Row():
2146
+ weight_learn = gr.Slider(0, 5, 1.2, 0.1, label="🎓 Peso: aprendizado")
2147
+ weight_viral = gr.Slider(0, 5, 1.0, 0.1, label="🔥 Peso: viral")
2148
+
2149
+ btn = gr.Button("🚀 Processar com IA (pode levar 30-60s)", variant="primary", size="lg")
2150
+
2151
+ with gr.Row():
2152
+ with gr.Column(scale=2):
2153
+ summary_out = gr.Textbox(label="📋 Resumo dos Cortes", lines=20, max_lines=30)
2154
+ with gr.Column(scale=1):
2155
+ status_out = gr.Textbox(label="📊 Status", lines=3)
2156
+ file_out = gr.File(label="⬇️ Download XML Editado")
2157
+
2158
+ btn.click(
2159
+ process_files,
2160
+ [xml_in, txt_in, use_llm, num_segments, custom_keywords,
2161
+ manual_timecodes, natural_instructions,
2162
+ weight_emotion, weight_break, weight_learn, weight_viral],
2163
+ [summary_out, file_out, status_out]
2164
+ )
2165
+
2166
+ gr.Markdown("""
2167
+ ---
2168
+ ### 📚 Guia Rápido:
2169
+
2170
+ **🎯 Para melhores resultados:**
2171
+ 1. ✅ Envie XML + Transcrição completa
2172
+ 2. ✅ Ative a IA (checkbox)
2173
+ 3. ✅ Escreva comando claro e específico
2174
+ 4. ✅ Aguarde 30-60s para análise completa
2175
+ 5. ✅ Baixe e importe no Premiere
2176
+
2177
+ **⚡ Ordem de prioridade:**
2178
+ 1. **Minutagens Manuais** (ignora tudo, máxima precisão)
2179
+ 2. **Comando + IA** (análise completa, muito preciso)
2180
+ 3. **Comando sem IA** (básico, menos preciso)
2181
+ 4. **Modo Automático** (não recomendado)
2182
+
2183
+ **🔧 Troubleshooting:**
2184
+ - Erro "IA não disponível": Configure `GEMINI_API_KEY`
2185
+ - Cortes errados: Seja mais específico no comando
2186
+ - Demora muito: Normal para IA completa (30-60s)
2187
+ - Sem transcrição: Use minutagens manuais
2188
+ """)
2189
+
2190
+ if __name__ == "__main__":
2191
+ demo.launch(), nxt) and (j + 1 < len(lines) and arrow.search(lines[j + 1])):
2192
  break
2193
  if arrow.search(nxt):
2194
  break
 
2197
 
2198
  text = " ".join(text_parts).strip()
2199
  try:
2200
+ sf = parse_timecode_to_frames(start_tc, fps)
2201
+ ef = parse_timecode_to_frames(end_tc, fps)
2202
  if ef > sf:
2203
  results.append(Segment(
2204
+ start_tc=frames_to_timecode(sf, fps),
2205
+ end_tc=frames_to_timecode(ef, fps),
2206
  start_f=sf,
2207
  end_f=ef,
2208
  text=text,