leicam commited on
Commit
b4d350d
·
verified ·
1 Parent(s): db61476

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +326 -326
app.py CHANGED
@@ -1,346 +1,346 @@
1
- """
2
- Módulo de rastreamento facial para crop inteligente de vídeos.
3
- Usa OpenCV e detecção de rostos para manter pessoas centralizadas ao redimensionar.
4
- """
 
5
 
6
- import cv2
7
- import numpy as np
8
- from typing import Tuple, Optional, List
9
- from dataclasses import dataclass
10
 
11
- @dataclass
12
- class FaceBox:
13
- """Representa uma detecção de rosto."""
14
- x: int
15
- y: int
16
- w: int
17
- h: int
18
- center_x: int
19
- center_y: int
20
- confidence: float = 1.0
21
 
22
- class FaceTracker:
23
- """Rastreador de rostos para crop inteligente de vídeos."""
24
-
25
- def __init__(self):
26
- """Inicializa o detector de rostos usando Haar Cascades do OpenCV."""
27
- # Tenta carregar diferentes cascades (frontal e perfil)
28
- cascade_paths = [
29
- cv2.data.haarcascades + 'haarcascade_frontalface_default.xml',
30
- cv2.data.haarcascades + 'haarcascade_frontalface_alt.xml',
31
- ]
32
-
33
- self.face_cascade = None
34
- for path in cascade_paths:
35
- try:
36
- self.face_cascade = cv2.CascadeClassifier(path)
37
- if not self.face_cascade.empty():
38
- break
39
- except:
40
- continue
41
-
42
- if self.face_cascade is None or self.face_cascade.empty():
43
- print("⚠️ Aviso: Não foi possível carregar detector de rostos. Crop será centralizado.")
44
- self.enabled = False
45
- else:
46
- self.enabled = True
47
- print("✓ Detector de rostos carregado com sucesso")
48
-
49
- def detect_faces(self, frame: np.ndarray) -> List[FaceBox]:
50
- """
51
- Detecta rostos em um frame.
52
-
53
- Args:
54
- frame: Frame do vídeo (BGR ou RGB)
55
-
56
- Returns:
57
- Lista de FaceBox com rostos detectados
58
- """
59
- if not self.enabled:
60
- return []
61
-
62
- # Converte para escala de cinza para detecção
63
- gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
64
-
65
- # Detecta rostos
66
- faces = self.face_cascade.detectMultiScale(
67
- gray,
68
- scaleFactor=1.1,
69
- minNeighbors=5,
70
- minSize=(30, 30),
71
- flags=cv2.CASCADE_SCALE_IMAGE
72
- )
73
-
74
- # Converte para FaceBox
75
- face_boxes = []
76
- for (x, y, w, h) in faces:
77
- center_x = x + w // 2
78
- center_y = y + h // 2
79
- face_boxes.append(FaceBox(x, y, w, h, center_x, center_y))
80
-
81
- return face_boxes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- def get_primary_face(self, faces: List[FaceBox], frame_width: int, frame_height: int) -> Optional[FaceBox]:
84
- """
85
- Seleciona o rosto principal (mais central e maior).
86
-
87
- Args:
88
- faces: Lista de rostos detectados
89
- frame_width: Largura do frame
90
- frame_height: Altura do frame
91
 
92
- Returns:
93
- FaceBox do rosto principal ou None
94
- """
95
- if not faces:
96
- return None
97
-
98
- # Se só há um rosto, retorna ele
99
- if len(faces) == 1:
100
- return faces[0]
101
-
102
- # Calcula score para cada rosto (baseado em tamanho e centralização)
103
- frame_center_x = frame_width / 2
104
- frame_center_y = frame_height / 2
105
-
106
- scored_faces = []
107
- for face in faces:
108
- # Score por tamanho (normalizado)
109
- size_score = (face.w * face.h) / (frame_width * frame_height)
110
 
111
- # Score por distância ao centro (normalizado e invertido)
112
- dx = abs(face.center_x - frame_center_x) / frame_width
113
- dy = abs(face.center_y - frame_center_y) / frame_height
114
- center_score = 1 - (dx + dy) / 2
 
 
 
 
 
 
 
 
115
 
116
- # Score final (peso maior para centralização)
117
- total_score = (size_score * 0.3) + (center_score * 0.7)
118
- scored_faces.append((total_score, face))
119
-
120
- # Retorna o rosto com maior score
121
- scored_faces.sort(reverse=True, key=lambda x: x[0])
122
- return scored_faces[0][1]
123
-
124
- def calculate_smart_crop(
125
- self,
126
- frame: np.ndarray,
127
- target_width: int,
128
- target_height: int
129
- ) -> Tuple[int, int, int, int]:
130
- """
131
- Calcula coordenadas de crop inteligente baseado em detecção facial.
132
 
133
- Args:
134
- frame: Frame do vídeo
135
- target_width: Largura desejada
136
- target_height: Altura desejada
 
 
137
 
138
- Returns:
139
- Tupla (x, y, w, h) das coordenadas de crop
140
- """
141
- frame_h, frame_w = frame.shape[:2]
142
-
143
- # Detecta rostos
144
- faces = self.detect_faces(frame)
145
- primary_face = self.get_primary_face(faces, frame_w, frame_h)
146
-
147
- # Calcula aspect ratio alvo
148
- target_ar = target_width / target_height
149
- frame_ar = frame_w / frame_h
150
-
151
- if primary_face:
152
- # Crop baseado no rosto detectado
153
- face_center_x = primary_face.center_x
154
- face_center_y = primary_face.center_y
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
- # Ajusta centro baseado no rosto com margens de segurança
157
- if target_ar < frame_ar: # Crop vertical (9:16, 1:1, 4:5)
158
- crop_w = int(frame_h * target_ar)
159
- crop_h = frame_h
160
 
161
- # Centraliza horizontalmente no rosto
162
- crop_x = max(0, min(face_center_x - crop_w // 2, frame_w - crop_w))
163
- crop_y = 0
164
- else: # Crop horizontal ou quadrado
165
- crop_w = frame_w
166
- crop_h = int(frame_w / target_ar)
167
 
168
- # Centraliza verticalmente no rosto (com leve offset para cima)
169
- offset = int(crop_h * 0.1) # 10% offset para dar espaço acima da cabeça
170
- crop_x = 0
171
- crop_y = max(0, min(face_center_y - crop_h // 2 - offset, frame_h - crop_h))
172
- else:
173
- # Fallback: crop centralizado tradicional
174
- if target_ar < frame_ar: # Mais alto que largo
175
- crop_w = int(frame_h * target_ar)
176
- crop_h = frame_h
177
- crop_x = (frame_w - crop_w) // 2
178
- crop_y = 0
179
- else: # Mais largo que alto
180
- crop_w = frame_w
181
- crop_h = int(frame_w / target_ar)
182
- crop_x = 0
183
- crop_y = (frame_h - crop_h) // 2
184
-
185
- return (crop_x, crop_y, crop_w, crop_h)
 
 
 
 
 
 
 
 
 
 
186
 
 
187
 
188
- def apply_smart_crop_to_video(
189
- input_path: str,
190
- output_path: str,
191
- target_width: int,
192
- target_height: int,
193
- sample_frames: int = 10
194
- ) -> bool:
195
- """
196
- Aplica crop inteligente com rastreamento facial a um vídeo.
197
-
198
- Args:
199
- input_path: Caminho do vídeo de entrada
200
- output_path: Caminho do vídeo de saída
201
- target_width: Largura desejada
202
- target_height: Altura desejada
203
- sample_frames: Número de frames para amostragem (para calcular posição média)
204
-
205
- Returns:
206
- True se sucesso, False caso contrário
207
- """
208
- tracker = FaceTracker()
209
-
210
- # Abre vídeo de entrada
211
- cap = cv2.VideoCapture(input_path)
212
- if not cap.isOpened():
213
- print(f"❌ Erro ao abrir vídeo: {input_path}")
214
- return False
215
-
216
- # Propriedades do vídeo
217
- fps = int(cap.get(cv2.CAP_PROP_FPS))
218
- frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
219
-
220
- # Amostra alguns frames para determinar melhor posição de crop
221
- sample_positions = []
222
- frame_indices = np.linspace(0, frame_count - 1, min(sample_frames, frame_count), dtype=int)
223
-
224
- for idx in frame_indices:
225
- cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
226
- ret, frame = cap.read()
227
- if ret:
228
- crop_coords = tracker.calculate_smart_crop(frame, target_width, target_height)
229
- sample_positions.append(crop_coords)
230
-
231
- # Calcula posição média de crop (suaviza movimento)
232
- if sample_positions:
233
- avg_x = int(np.median([p[0] for p in sample_positions]))
234
- avg_y = int(np.median([p[1] for p in sample_positions]))
235
- crop_w = sample_positions[0][2]
236
- crop_h = sample_positions[0][3]
237
- final_crop = (avg_x, avg_y, crop_w, crop_h)
238
- else:
239
- # Fallback
240
- frame_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
241
- frame_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
242
- target_ar = target_width / target_height
243
- frame_ar = frame_w / frame_h
244
-
245
- if target_ar < frame_ar:
246
- crop_w = int(frame_h * target_ar)
247
- crop_h = frame_h
248
- final_crop = ((frame_w - crop_w) // 2, 0, crop_w, crop_h)
249
- else:
250
- crop_w = frame_w
251
- crop_h = int(frame_w / target_ar)
252
- final_crop = (0, (frame_h - crop_h) // 2, crop_w, crop_h)
253
-
254
- # Reseta para início do vídeo
255
- cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
256
-
257
- # Configura writer de saída
258
- fourcc = cv2.VideoWriter_fourcc(*'mp4v')
259
- out = cv2.VideoWriter(output_path, fourcc, fps, (target_width, target_height))
260
-
261
- if not out.isOpened():
262
- print(f"❌ Erro ao criar vídeo de saída: {output_path}")
263
- cap.release()
264
- return False
265
-
266
- # Processa cada frame
267
- print(f"🎬 Processando vídeo com crop inteligente: {final_crop}")
268
- frame_num = 0
269
- while True:
270
- ret, frame = cap.read()
271
- if not ret:
272
- break
273
-
274
- # Aplica crop
275
- x, y, w, h = final_crop
276
- cropped = frame[y:y+h, x:x+w]
277
-
278
- # Redimensiona para tamanho final
279
- resized = cv2.resize(cropped, (target_width, target_height), interpolation=cv2.INTER_LANCZOS4)
280
-
281
- # Escreve frame
282
- out.write(resized)
283
- frame_num += 1
284
-
285
- # Progress
286
- if frame_num % 30 == 0:
287
- progress = (frame_num / frame_count) * 100
288
- print(f" Progresso: {progress:.1f}% ({frame_num}/{frame_count} frames)")
289
-
290
- # Finaliza
291
- cap.release()
292
- out.release()
293
-
294
- print(f"✓ Vídeo processado com sucesso: {output_path}")
295
- return True
296
 
 
 
 
 
 
297
 
298
- def get_aspect_ratio_dimensions(ar_mode: str, base_height: int = 1080) -> Tuple[int, int]:
299
- """
300
- Retorna dimensões (width, height) baseado no modo de aspect ratio.
301
-
302
- Args:
303
- ar_mode: Modo do aspect ratio ("Original", "Vertical 9:16", "Quadrado 1:1", "Retrato 4:5")
304
- base_height: Altura base para cálculos (padrão: 1080p)
305
-
306
- Returns:
307
- Tupla (width, height)
308
- """
309
- ar_map = {
310
- "Original": None, # Mantém original
311
- "Vertical 9:16": (9, 16),
312
- "Quadrado 1:1": (1, 1),
313
- "Retrato 4:5": (4, 5),
314
- }
315
-
316
- if ar_mode not in ar_map or ar_map[ar_mode] is None:
317
- return None
318
-
319
- w_ratio, h_ratio = ar_map[ar_mode]
320
 
321
- # Calcula width baseado na altura
322
- width = int((base_height / h_ratio) * w_ratio)
 
 
 
 
 
 
 
 
 
323
 
324
- return (width, base_height)
 
 
 
 
 
 
 
 
 
 
325
 
326
-
327
- # Exemplo de uso:
328
  if __name__ == "__main__":
329
- # Teste básico
330
- tracker = FaceTracker()
331
-
332
- # Simula um frame de teste
333
- test_frame = np.zeros((1080, 1920, 3), dtype=np.uint8)
334
-
335
- # Detecta rostos
336
- faces = tracker.detect_faces(test_frame)
337
- print(f"Rostos detectados: {len(faces)}")
338
-
339
- # Calcula crop para 9:16
340
- crop_coords = tracker.calculate_smart_crop(test_frame, 1080, 1920)
341
- print(f"Coordenadas de crop (9:16): {crop_coords}")
342
-
343
- # Testa diferentes aspect ratios
344
- for ar_mode in ["Vertical 9:16", "Quadrado 1:1", "Retrato 4:5"]:
345
- dims = get_aspect_ratio_dimensions(ar_mode)
346
- print(f"{ar_mode}: {dims}")
 
1
+ import gradio as gr
2
+ from pathlib import Path
3
+ import shutil
4
+ import os
5
+ from core import transcribe, generate_linear_cuts, generate_creative_cuts, Segment
6
 
7
+ SPACE_OUT = Path("outputs"); SPACE_OUT.mkdir(exist_ok=True, parents=True)
 
 
 
8
 
9
+ def do_transcribe(video_file, model_size):
10
+ if video_file is None:
11
+ return [], "Selecione um vídeo."
12
+ segs = transcribe(video_file, model_size=model_size)
13
+ # show a small preview of transcript
14
+ preview = "\n".join([f"[{s.start:.1f}–{s.end:.1f}] {s.text}" for s in segs[:12]])
15
+ return segs, f"Transcrição ok. Segmentos: {len(segs)}\n\nPrévia:\n{preview}"
 
 
 
16
 
17
+ def run_linear(segs, video_file, out_subdir, min_len, max_len, ideal_len, k, gap, pad, ar_mode, face_tracking):
18
+ if not segs:
19
+ return [], "Transcreva antes de cortar."
20
+ workdir = SPACE_OUT / (out_subdir or "cortes")
21
+ outs = generate_linear_cuts(video_file, segs, str(workdir),
22
+ min_len=min_len, max_len=max_len, ideal_len=ideal_len,
23
+ k=k, gap_threshold=gap, pad=pad, ar_mode=ar_mode,
24
+ face_tracking=face_tracking)
25
+ links = [str(Path(p)) for p in outs]
26
+ return links, f"Gerados: {len(links)} arquivo(s)."
27
+
28
+ def run_creative(segs, video_file, out_subdir, min_len, max_len, ideal_len, minb, maxb, k, gap, pad, ar_mode, face_tracking):
29
+ if not segs:
30
+ return [], "Transcreva antes de cortar."
31
+ workdir = SPACE_OUT / (out_subdir or "cortes")
32
+ outs = generate_creative_cuts(video_file, segs, str(workdir),
33
+ min_len=min_len, max_len=max_len, ideal_len=ideal_len,
34
+ min_blocks=minb, max_blocks=maxb,
35
+ k=k, gap_threshold=gap, pad=pad, ar_mode=ar_mode,
36
+ face_tracking=face_tracking)
37
+ links = [str(Path(p)) for p in outs]
38
+ return links, f"Gerados: {len(links)} arquivo(s)."
39
+
40
+ css = """
41
+ /* Design Tokens */
42
+ :root {
43
+ --neon: #39FF14;
44
+ --txt: #0a0a0a;
45
+ --muted: #6b7280;
46
+ --line: #e5e7eb;
47
+ --bg: #ffffff;
48
+ }
49
+
50
+ /* Global Styles */
51
+ .gradio-container {
52
+ font-family: 'Manrope', system-ui, -apple-system, sans-serif !important;
53
+ background: linear-gradient(135deg, rgba(57,255,20,0.03) 0%, rgba(255,255,255,1) 100%);
54
+ background-attachment: fixed;
55
+ }
56
+
57
+ /* Headers */
58
+ .gradio-container h1, .gradio-container h2, .gradio-container h3 {
59
+ font-weight: 800 !important;
60
+ letter-spacing: -0.3px !important;
61
+ color: var(--txt) !important;
62
+ }
63
+
64
+ .gradio-container h1 {
65
+ font-size: clamp(28px, 5vw, 46px) !important;
66
+ margin-bottom: 8px !important;
67
+ }
68
+
69
+ .gradio-container .gr-prose p {
70
+ color: var(--muted) !important;
71
+ line-height: 1.65 !important;
72
+ font-size: 16px !important;
73
+ }
74
+
75
+ /* Buttons */
76
+ .gradio-container button.primary {
77
+ background: var(--neon) !important;
78
+ color: #000 !important;
79
+ border: none !important;
80
+ border-radius: 10px !important;
81
+ font-weight: 800 !important;
82
+ padding: 12px 20px !important;
83
+ box-shadow: 0 2px 0 rgba(0,0,0,0.12), 0 10px 30px rgba(57,255,20,0.18) !important;
84
+ transition: all 0.2s ease !important;
85
+ }
86
+
87
+ .gradio-container button.primary:hover {
88
+ transform: translateY(-1px) !important;
89
+ filter: saturate(1.03) !important;
90
+ }
91
+
92
+ .gradio-container button:not(.primary) {
93
+ background: #fff !important;
94
+ border: 1px solid var(--line) !important;
95
+ border-radius: 10px !important;
96
+ color: var(--txt) !important;
97
+ font-weight: 600 !important;
98
+ }
99
+
100
+ /* Inputs, Textareas, Dropdowns */
101
+ .gradio-container input, .gradio-container textarea, .gradio-container select, .gradio-container .wrap {
102
+ border: 1px solid var(--line) !important;
103
+ border-radius: 12px !important;
104
+ background: #fff !important;
105
+ transition: all 0.2s ease !important;
106
+ }
107
+
108
+ .gradio-container input:focus, .gradio-container textarea:focus, .gradio-container select:focus {
109
+ border-color: #cbd5e1 !important;
110
+ box-shadow: 0 0 0 3px rgba(57,255,20,0.16) !important;
111
+ }
112
+
113
+ /* Cards/Panels */
114
+ .gradio-container .block {
115
+ border: 1px solid var(--line) !important;
116
+ border-radius: 16px !important;
117
+ background: #fff !important;
118
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06) !important;
119
+ transition: all 0.2s ease !important;
120
+ }
121
+
122
+ .gradio-container .block:hover {
123
+ box-shadow: 0 6px 16px rgba(0,0,0,0.08) !important;
124
+ }
125
+
126
+ /* Tabs */
127
+ .gradio-container .tabs {
128
+ border-radius: 12px !important;
129
+ }
130
+
131
+ .gradio-container .tab-nav button {
132
+ border-radius: 8px !important;
133
+ font-weight: 600 !important;
134
+ }
135
+
136
+ .gradio-container .tab-nav button.selected {
137
+ background: var(--neon) !important;
138
+ color: #000 !important;
139
+ }
140
+
141
+ /* Checkboxes */
142
+ .gradio-container input[type="checkbox"]:checked {
143
+ background: var(--neon) !important;
144
+ border-color: var(--neon) !important;
145
+ }
146
+
147
+ /* Video player */
148
+ .gradio-container video {
149
+ border-radius: 12px !important;
150
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important;
151
+ }
152
+
153
+ /* File upload areas */
154
+ .gradio-container .upload-container {
155
+ border: 2px dashed var(--line) !important;
156
+ border-radius: 12px !important;
157
+ background: #fafafa !important;
158
+ }
159
+
160
+ /* Number inputs */
161
+ .gradio-container input[type="number"] {
162
+ font-weight: 600 !important;
163
+ }
164
+
165
+ /* Labels */
166
+ .gradio-container label {
167
+ font-weight: 600 !important;
168
+ color: var(--txt) !important;
169
+ }
170
+
171
+ /* Container spacing */
172
+ .gradio-container .contain {
173
+ max-width: 1200px !important;
174
+ margin: 0 auto !important;
175
+ }
176
+ """
177
+
178
+ with gr.Blocks(title="Editor de cortes automático", css=css) as demo:
179
+ gr.HTML("""
180
+ <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
181
+ <div style="text-align: center; padding: 24px 0 16px;">
182
+ <div style="display: inline-flex; align-items: center; gap: 8px; margin-bottom: 12px;">
183
+ <div style="width: 12px; height: 12px; border-radius: 50%; background: #39FF14; box-shadow: 0 0 20px rgba(57,255,20,0.4);"></div>
184
+ <h1 style="margin: 0; font-weight: 800; letter-spacing: -0.4px;">Editor de Cortes Automático</h1>
185
+ </div>
186
+ <p style="color: #6b7280; max-width: 720px; margin: 0 auto; line-height: 1.65;">
187
+ Gere cortes criativos ou trechos a partir de qualquer vídeo com <strong>rastreamento facial inteligente</strong>.
188
+ </p>
189
+ </div>
190
+ """)
191
 
192
+ with gr.Row():
193
+ with gr.Column(scale=1):
194
+ gr.HTML("""<div style="background: linear-gradient(135deg, #f9fafb 0%, #fff 100%);
195
+ padding: 16px; border-radius: 16px; border: 1px solid #e5e7eb; margin-bottom: 16px;">
196
+ <div style="font-weight: 700; color: #0a0a0a; margin-bottom: 8px;">🎬 Entrada</div>
197
+ <p style="color: #6b7280; font-size: 14px; margin: 0;">Envie seu vídeo e configure as opções</p>
198
+ </div>""")
 
199
 
200
+ video = gr.Video(label="Vídeo de entrada", interactive=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
+ with gr.Row():
203
+ model_size = gr.Dropdown(
204
+ choices=["tiny","base","small","medium"],
205
+ value="small",
206
+ label="Modelo Whisper",
207
+ info="Quanto maior, mais preciso mas mais lento"
208
+ )
209
+ out_subdir = gr.Textbox(
210
+ label="Subpasta de saída",
211
+ value="editor_de_cortes_automatico",
212
+ info="Nome da pasta onde os cortes serão salvos"
213
+ )
214
 
215
+ transcribe_btn = gr.Button("🎙️ 1) Transcrever Vídeo", variant="primary", size="lg")
216
+ transcript_preview = gr.Textbox(label="Status / Prévia da Transcrição", lines=10)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
+ with gr.Column(scale=1):
219
+ gr.HTML("""<div style="background: linear-gradient(135deg, rgba(57,255,20,0.08) 0%, rgba(57,255,20,0.02) 100%);
220
+ padding: 16px; border-radius: 16px; border: 1px solid #e5e7eb; margin-bottom: 16px;">
221
+ <div style="font-weight: 700; color: #0a0a0a; margin-bottom: 8px;">⚙️ Configurações de Corte</div>
222
+ <p style="color: #6b7280; font-size: 14px; margin: 0;">Escolha entre cortes simples ou criativos</p>
223
+ </div>""")
224
 
225
+ with gr.Tab("✂️ Cortes Simples"):
226
+ gr.HTML("""<p style="color: #6b7280; font-size: 14px; margin-bottom: 16px;">
227
+ Cortes lineares e contínuos do vídeo original</p>""")
228
+
229
+ with gr.Row():
230
+ min_len = gr.Number(value=600, label="⏱️ Duração mínima (s)", info="Mínimo de segundos por corte")
231
+ max_len = gr.Number(value=900, label="⏱️ Duração máxima (s)", info="Máximo de segundos por corte")
232
+
233
+ with gr.Row():
234
+ ideal_len = gr.Number(value=900, label="🎯 Duração ideal (s)", info="Tamanho preferencial")
235
+ k = gr.Number(value=2, label="📊 Quantidade de cortes", info="Quantos vídeos gerar")
236
+
237
+ with gr.Row():
238
+ gap = gr.Number(value=0.60, label="Gap (s)", info="Intervalo entre frases")
239
+ pad = gr.Number(value=0.08, label="Pad (s)", info="Margem extra")
240
+
241
+ ar_mode = gr.Dropdown(
242
+ choices=["Original","Vertical 9:16","Quadrado 1:1","Retrato 4:5"],
243
+ value="Original",
244
+ label="📐 Formato de vídeo"
245
+ )
246
+
247
+ face_tracking = gr.Checkbox(
248
+ label="👤 Ativar rastreamento facial no crop",
249
+ value=True,
250
+ info="Detecta e centraliza rostos automaticamente ao redimensionar"
251
+ )
252
+
253
+ gr.HTML("""<div style="background: #ecfdf5; padding: 12px; border-radius: 10px; border: 1px solid #a7f3d0; margin: 12px 0;">
254
+ <strong style="color: #065f46;">💡 Dica:</strong>
255
+ <p style="color: #047857; font-size: 13px; margin: 6px 0 0;">
256
+ O rastreamento facial mantém a pessoa sempre centralizada ao cortar para 9:16 ou 1:1
257
+ </p>
258
+ </div>""")
259
+
260
+ go_linear = gr.Button("🚀 2) Gerar Cortes Simples", variant="primary")
261
+ out_linear = gr.Files(label="📦 Arquivos gerados (simples)")
262
+ status_linear = gr.Textbox(label="Status", lines=2)
263
 
264
+ with gr.Tab("🎨 Cortes Criativos"):
265
+ gr.HTML("""<p style="color: #6b7280; font-size: 14px; margin-bottom: 16px;">
266
+ Montagens com múltiplos blocos e transições dinâmicas</p>""")
 
267
 
268
+ with gr.Row():
269
+ minb = gr.Number(value=3, label="🧩 Blocos mínimos", info="Mínimo de segmentos por vídeo")
270
+ maxb = gr.Number(value=8, label="🧩 Blocos máximos", info="Máximo de segmentos por vídeo")
 
 
 
271
 
272
+ with gr.Row():
273
+ k2 = gr.Number(value=2, label="📊 Quantidade de cortes")
274
+ gap2 = gr.Number(value=0.60, label="Gap (s)")
275
+
276
+ with gr.Row():
277
+ pad2 = gr.Number(value=0.08, label="Pad (s)")
278
+ ar_mode2 = gr.Dropdown(
279
+ choices=["Original","Vertical 9:16","Quadrado 1:1","Retrato 4:5"],
280
+ value="Original",
281
+ label="📐 Formato"
282
+ )
283
+
284
+ face_tracking2 = gr.Checkbox(
285
+ label="👤 Ativar rastreamento facial no crop",
286
+ value=True,
287
+ info="Detecta e centraliza rostos automaticamente"
288
+ )
289
+
290
+ gr.HTML("""<div style="background: #fef3c7; padding: 12px; border-radius: 10px; border: 1px solid #fcd34d; margin: 12px 0;">
291
+ <strong style="color: #92400e;">⚡ Cortes Criativos:</strong>
292
+ <p style="color: #78350f; font-size: 13px; margin: 6px 0 0;">
293
+ Combina diferentes momentos do vídeo em uma montagem dinâmica
294
+ </p>
295
+ </div>""")
296
+
297
+ go_creative = gr.Button("🎬 3) Gerar Cortes Criativos", variant="primary")
298
+ out_creative = gr.Files(label="📦 Arquivos gerados (criativos)")
299
+ status_creative = gr.Textbox(label="Status", lines=2)
300
 
301
+ segs_state = gr.State([])
302
 
303
+ transcribe_btn.click(
304
+ do_transcribe,
305
+ inputs=[video, model_size],
306
+ outputs=[segs_state, transcript_preview],
307
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
 
309
+ go_linear.click(
310
+ run_linear,
311
+ inputs=[segs_state, video, out_subdir, min_len, max_len, ideal_len, k, gap, pad, ar_mode, face_tracking],
312
+ outputs=[out_linear, status_linear],
313
+ )
314
 
315
+ go_creative.click(
316
+ run_creative,
317
+ inputs=[segs_state, video, out_subdir, min_len, max_len, ideal_len, minb, maxb, k2, gap2, pad2, ar_mode2, face_tracking2],
318
+ outputs=[out_creative, status_creative],
319
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
+ gr.HTML("""
322
+ <div style="margin-top: 32px; padding: 20px; background: #f9fafb; border-radius: 16px; border: 1px solid #e5e7eb;">
323
+ <h3 style="margin: 0 0 12px; font-weight: 700; color: #0a0a0a;">💡 Como funciona o rastreamento facial</h3>
324
+ <ul style="color: #6b7280; line-height: 1.65; padding-left: 20px; margin: 0;">
325
+ <li><strong>Detecção automática:</strong> O sistema identifica rostos em cada frame do vídeo</li>
326
+ <li><strong>Crop inteligente:</strong> Ao redimensionar para 9:16 ou 1:1, mantém o rosto centralizado</li>
327
+ <li><strong>Múltiplos rostos:</strong> Se houver várias pessoas, prioriza o rosto mais central/próximo</li>
328
+ <li><strong>Fallback:</strong> Se nenhum rosto for detectado, usa crop centralizado tradicional</li>
329
+ </ul>
330
+ </div>
331
+ """)
332
 
333
+ gr.HTML("""
334
+ <footer style="margin-top: 40px; padding: 24px 0; border-top: 1px solid #e5e7eb; text-align: center;">
335
+ <div style="display: inline-flex; align-items: center; gap: 8px; margin-bottom: 8px;">
336
+ <div style="width: 10px; height: 10px; border-radius: 50%; background: #39FF14;"></div>
337
+ <span style="font-weight: 700; color: #0a0a0a;">Leicam · Tech</span>
338
+ </div>
339
+ <p style="color: #6b7280; font-size: 13px; margin: 0;">
340
+ Ferramentas práticas para produção de conteúdo
341
+ </p>
342
+ </footer>
343
+ """)
344
 
 
 
345
  if __name__ == "__main__":
346
+ demo.launch()