leicam commited on
Commit
5ad5f13
·
verified ·
1 Parent(s): a304d68

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +106 -62
app.py CHANGED
@@ -10,10 +10,11 @@ import whisper
10
  import subprocess
11
  from pathlib import Path
12
  from dataclasses import dataclass
13
- from typing import List, Tuple, Optional
14
  import tempfile
15
  import os
16
  import shutil
 
17
 
18
  # ======================= DATACLASSES =======================
19
 
@@ -38,6 +39,40 @@ class FaceBox:
38
  center_y: int
39
  confidence: float = 1.0
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  # ======================= FACE TRACKING =======================
42
 
43
  class FaceTracker:
@@ -147,6 +182,7 @@ def extract_audio_wav(input_video: str, sr: int = 16000) -> str:
147
  """Extrai o áudio para WAV mono 16kHz para robustez da transcrição."""
148
  fd, tmp_path = tempfile.mkstemp(suffix=".wav")
149
  os.close(fd)
 
150
  cmd = [
151
  "ffmpeg", "-y", "-i", input_video,
152
  "-vn", "-ac", "1", "-ar", str(sr), "-f", "wav", tmp_path
@@ -155,31 +191,41 @@ def extract_audio_wav(input_video: str, sr: int = 16000) -> str:
155
  return tmp_path
156
 
157
  def transcribe(video_file: str, model_size: str = "small") -> List[Segment]:
158
- print(f"Carregando modelo Whisper: {model_size}")
159
- model = whisper.load_model(model_size)
160
-
161
- print(f"Extraindo áudio (WAV) de: {video_file}")
162
- audio_wav = extract_audio_wav(video_file, sr=16000)
163
-
164
- print("Transcrevendo WAV…")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  result = model.transcribe(
166
  audio_wav,
167
  language="pt",
168
  verbose=False,
169
  task="transcribe",
170
- temperature=0
 
 
171
  )
172
-
173
- segments = []
174
- for seg in result["segments"]:
175
- segments.append(Segment(
176
- start=seg["start"],
177
- end=seg["end"],
178
- text=seg["text"].strip()
179
- ))
180
-
181
- print(f"Transcrição completa: {len(segments)} segmentos")
182
- # limpa o wav temporário
183
  try:
184
  Path(audio_wav).unlink(missing_ok=True)
185
  except Exception:
@@ -189,21 +235,23 @@ def transcribe(video_file: str, model_size: str = "small") -> List[Segment]:
189
  # ======================= PROCESSAMENTO DE VÍDEO =======================
190
 
191
  def extract_video_segment(input_video: str, output_video: str, start_time: float, end_time: float) -> bool:
192
- duration = end_time - start_time
 
 
 
193
  cmd = [
194
  "ffmpeg", "-y", "-ss", str(start_time), "-i", input_video,
195
  "-t", str(duration),
196
  "-c:v", "libx264",
197
- "-c:a", "aac", # pode manter aac para compatibilidade ampla
198
- "-strict", "experimental",
199
  output_video
200
  ]
201
-
202
  try:
203
  subprocess.run(cmd, check=True, capture_output=True)
204
  return True
205
  except subprocess.CalledProcessError as e:
206
- print(f"Erro ao extrair: {e}")
207
  return False
208
 
209
  def apply_smart_crop_to_video(input_path: str, output_path: str, target_width: int,
@@ -211,19 +259,16 @@ def apply_smart_crop_to_video(input_path: str, output_path: str, target_width: i
211
  """Calcula o melhor crop com rastreamento facial e aplica o crop com FFmpeg preservando o áudio."""
212
  tracker = FaceTracker()
213
  cap = cv2.VideoCapture(input_path)
214
-
215
  if not cap.isOpened():
216
- print(f"Erro ao abrir: {input_path}")
217
  return False
218
 
219
  frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
220
  frame_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
221
  frame_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
222
 
223
- # Amostragem para suavização
224
  sample_positions = []
225
  frame_indices = np.linspace(0, frame_count - 1, min(sample_frames, max(1, frame_count)), dtype=int)
226
-
227
  for idx in frame_indices:
228
  cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
229
  ret, frame = cap.read()
@@ -232,7 +277,6 @@ def apply_smart_crop_to_video(input_path: str, output_path: str, target_width: i
232
  sample_positions.append(crop_coords)
233
  cap.release()
234
 
235
- # Posição média (suavizada)
236
  if sample_positions:
237
  avg_x = int(np.median([p[0] for p in sample_positions]))
238
  avg_y = int(np.median([p[1] for p in sample_positions]))
@@ -240,7 +284,6 @@ def apply_smart_crop_to_video(input_path: str, output_path: str, target_width: i
240
  crop_h = sample_positions[0][3]
241
  final_crop = (avg_x, avg_y, crop_w, crop_h)
242
  else:
243
- # Fallback central
244
  target_ar = target_width / target_height
245
  frame_ar = frame_w / frame_h
246
  if target_ar < frame_ar:
@@ -253,23 +296,23 @@ def apply_smart_crop_to_video(input_path: str, output_path: str, target_width: i
253
  final_crop = (0, (frame_h - crop_h) // 2, crop_w, crop_h)
254
 
255
  x, y, w, h = final_crop
256
- print(f"Crop final: x={x}, y={y}, w={w}, h={h} -> {target_width}x{target_height}")
257
 
258
- # Aplica o crop com FFmpeg preservando o áudio
259
  vf = f"crop={w}:{h}:{x}:{y},scale={target_width}:{target_height}:flags=lanczos"
260
  cmd = [
261
  "ffmpeg", "-y", "-i", input_path,
262
  "-vf", vf,
263
  "-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
264
- "-c:a", "copy", # mantém o áudio original
 
265
  output_path
266
  ]
267
  try:
268
  subprocess.run(cmd, check=True, capture_output=True)
269
- print(f"Concluído: {output_path}")
270
  return True
271
  except subprocess.CalledProcessError as e:
272
- print(f"Erro no ffmpeg (smart crop): {e}")
273
  return False
274
 
275
  def apply_aspect_ratio(input_video: str, output_video: str, ar_mode: str, face_tracking: bool = False) -> bool:
@@ -282,21 +325,19 @@ def apply_aspect_ratio(input_video: str, output_video: str, ar_mode: str, face_t
282
  "Quadrado 1:1": (1080, 1080),
283
  "Retrato 4:5": (1080, 1350),
284
  }
285
-
286
  if ar_mode not in ar_dims:
287
  return False
288
 
289
  width, height = ar_dims[ar_mode]
290
-
291
  if face_tracking:
292
  return apply_smart_crop_to_video(input_video, output_video, width, height)
293
  else:
294
- # Crop centralizado tradicional com áudio preservado
295
  cmd = [
296
  "ffmpeg", "-y", "-i", input_video,
297
  "-vf", f"scale={width}:{height}:force_original_aspect_ratio=increase,crop={width}:{height}",
298
- "-c:a", "copy",
299
  "-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
 
 
300
  output_video
301
  ]
302
  try:
@@ -315,16 +356,14 @@ def concatenate_videos(video_files: List[str], output_file: str) -> bool:
315
  f.write(f"file '{os.path.abspath(vf)}'\n")
316
 
317
  try:
318
- # Se der problema de "different stream parameters", troque -c copy por reencode controlado
319
- cmd = ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file, "-c", "copy", output_file]
320
  subprocess.run(cmd, check=True, capture_output=True)
321
  return True
322
  except subprocess.CalledProcessError:
323
- # fallback reencode
324
  try:
325
  cmd = ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file,
326
  "-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
327
- "-c:a", "aac", output_file]
328
  subprocess.run(cmd, check=True, capture_output=True)
329
  return True
330
  except subprocess.CalledProcessError:
@@ -343,12 +382,12 @@ def generate_linear_cuts(video_file: str, segments: List[Segment], output_dir: s
343
 
344
  Path(output_dir).mkdir(parents=True, exist_ok=True)
345
  total_duration = segments[-1].end - segments[0].start
346
- target_duration = min(max_len, max(min_len, total_duration / k))
347
 
348
  outputs = []
349
  current_start = segments[0].start
350
 
351
- for i in range(k):
352
  target_end = current_start + target_duration
353
  best_end = target_end
354
 
@@ -366,9 +405,10 @@ def generate_linear_cuts(video_file: str, segments: List[Segment], output_dir: s
366
  temp_file = Path(output_dir) / f"temp_linear_{i+1}.mp4"
367
  final_file = Path(output_dir) / f"cut_linear_{i+1}.mp4"
368
 
369
- print(f"Corte {i+1}/{k}: {start_with_pad:.1f}s - {end_with_pad:.1f}s")
370
 
371
- if extract_video_segment(video_file, str(temp_file), start_with_pad, end_with_pad):
 
372
  if ar_mode != "Original":
373
  if apply_aspect_ratio(str(temp_file), str(final_file), ar_mode, face_tracking):
374
  Path(temp_file).unlink(missing_ok=True)
@@ -395,7 +435,7 @@ def generate_creative_cuts(video_file: str, segments: List[Segment], output_dir:
395
  outputs = []
396
 
397
  import random
398
- for i in range(k):
399
  num_blocks = random.randint(min_blocks, min(max_blocks, len(segments)))
400
  step = max(1, len(segments) // num_blocks)
401
  selected_indices = [j * step for j in range(num_blocks)]
@@ -406,8 +446,8 @@ def generate_creative_cuts(video_file: str, segments: List[Segment], output_dir:
406
  block_file = Path(output_dir) / f"temp_creative_{i+1}_block_{j+1}.mp4"
407
  start = max(0, seg.start - pad)
408
  end = seg.end + pad
409
-
410
- if extract_video_segment(video_file, str(block_file), start, end):
411
  block_files.append(str(block_file))
412
 
413
  if not block_files:
@@ -436,9 +476,10 @@ SPACE_OUT = Path("outputs")
436
  SPACE_OUT.mkdir(exist_ok=True, parents=True)
437
 
438
  def do_transcribe(video_file, model_size):
439
- if video_file is None:
440
- return [], "Selecione um vídeo."
441
- segs = transcribe(video_file, model_size=model_size)
 
442
  preview = "\n".join([f"[{s.start:.1f}–{s.end:.1f}] {s.text}" for s in segs[:12]])
443
  return segs, f"Transcrição ok. Segmentos: {len(segs)}\n\nPrévia:\n{preview}"
444
 
@@ -446,19 +487,21 @@ def run_linear(segs, video_file, out_subdir, min_len, max_len, ideal_len, k, gap
446
  if not segs:
447
  return [], "Transcreva antes de cortar."
448
  workdir = SPACE_OUT / (out_subdir or "cortes")
449
- outs = generate_linear_cuts(video_file, segs, str(workdir), min_len=min_len, max_len=max_len,
450
- ideal_len=ideal_len, k=int(k), gap_threshold=gap, pad=pad,
451
- ar_mode=ar_mode, face_tracking=face_tracking)
 
452
  return [str(Path(p)) for p in outs], f"Gerados: {len(outs)} arquivo(s)."
453
 
454
  def run_creative(segs, video_file, out_subdir, min_len, max_len, ideal_len, minb, maxb, k, gap, pad, ar_mode, face_tracking):
455
  if not segs:
456
  return [], "Transcreva antes de cortar."
457
  workdir = SPACE_OUT / (out_subdir or "cortes")
458
- outs = generate_creative_cuts(video_file, segs, str(workdir), min_len=min_len, max_len=max_len,
459
- ideal_len=ideal_len, min_blocks=int(minb), max_blocks=int(maxb),
460
- k=int(k), gap_threshold=gap, pad=pad, ar_mode=ar_mode,
461
- face_tracking=face_tracking)
 
462
  return [str(Path(p)) for p in outs], f"Gerados: {len(outs)} arquivo(s)."
463
 
464
  css = """
@@ -538,4 +581,5 @@ with gr.Blocks(title="Editor de Cortes Automático", css=css) as demo:
538
  outputs=[out_creative, status_creative])
539
 
540
  if __name__ == "__main__":
541
- demo.launch()
 
 
10
  import subprocess
11
  from pathlib import Path
12
  from dataclasses import dataclass
13
+ from typing import List, Tuple, Optional, Union
14
  import tempfile
15
  import os
16
  import shutil
17
+ import json
18
 
19
  # ======================= DATACLASSES =======================
20
 
 
39
  center_y: int
40
  confidence: float = 1.0
41
 
42
+ # ======================= UTILS =======================
43
+
44
+ def resolve_video_path(v: Union[str, dict, None]) -> Optional[str]:
45
+ """Gradio às vezes entrega str (caminho) ou dict {'name':..., 'data':...}. Normaliza para caminho."""
46
+ if v is None:
47
+ return None
48
+ if isinstance(v, str):
49
+ return v
50
+ if isinstance(v, dict):
51
+ # Prioriza caminho local temporário
52
+ if "name" in v and isinstance(v["name"], str) and len(v["name"]) > 0 and os.path.exists(v["name"]):
53
+ return v["name"]
54
+ # Algumas versões usam 'path'
55
+ if "path" in v and isinstance(v["path"], str) and os.path.exists(v["path"]):
56
+ return v["path"]
57
+ # Fallback: alguns frontends mandam apenas nome base; não há como resolver sem arquivo
58
+ return v.get("name") or v.get("path")
59
+ return None
60
+
61
+ def probe_duration(path: str) -> Optional[float]:
62
+ """Retorna a duração (segundos) via ffprobe, ou None se falhar."""
63
+ try:
64
+ cmd = [
65
+ "ffprobe", "-v", "error", "-show_entries", "format=duration",
66
+ "-of", "json", path
67
+ ]
68
+ out = subprocess.run(cmd, check=True, capture_output=True)
69
+ data = json.loads(out.stdout.decode("utf-8", errors="ignore"))
70
+ dur = float(data.get("format", {}).get("duration", 0.0))
71
+ return dur if dur > 0 else None
72
+ except Exception as e:
73
+ print(f"[ffprobe] falhou: {e}")
74
+ return None
75
+
76
  # ======================= FACE TRACKING =======================
77
 
78
  class FaceTracker:
 
182
  """Extrai o áudio para WAV mono 16kHz para robustez da transcrição."""
183
  fd, tmp_path = tempfile.mkstemp(suffix=".wav")
184
  os.close(fd)
185
+ print(f"[ffmpeg] extraindo WAV -> {tmp_path}")
186
  cmd = [
187
  "ffmpeg", "-y", "-i", input_video,
188
  "-vn", "-ac", "1", "-ar", str(sr), "-f", "wav", tmp_path
 
191
  return tmp_path
192
 
193
  def transcribe(video_file: str, model_size: str = "small") -> List[Segment]:
194
+ true_path = resolve_video_path(video_file)
195
+ if not true_path or not os.path.exists(true_path):
196
+ print(f"[transcribe] caminho inválido: {video_file}")
197
+ return []
198
+
199
+ # Durações para diagnóstico
200
+ vid_dur = probe_duration(true_path)
201
+ print(f"[probe] duração do vídeo: {vid_dur:.2f}s" if vid_dur else "[probe] duração do vídeo: desconhecida")
202
+
203
+ print(f"[whisper] carregando modelo: {model_size}")
204
+ model = whisper.load_model(model_size) # device auto
205
+ print(f"[whisper] extraindo áudio WAV…")
206
+ audio_wav = extract_audio_wav(true_path, sr=16000)
207
+
208
+ wav_dur = probe_duration(audio_wav)
209
+ print(f"[probe] duração do WAV: {wav_dur:.2f}s" if wav_dur else "[probe] duração do WAV: desconhecida")
210
+ if vid_dur and wav_dur and wav_dur + 1 < vid_dur:
211
+ print("[aviso] WAV menor que o vídeo — verifique codecs/ffmpeg. Mesmo assim vou transcrever o que foi extraído.")
212
+
213
+ print("[whisper] transcrevendo…")
214
+ # Configs mais robustas para CPU/Spaces
215
  result = model.transcribe(
216
  audio_wav,
217
  language="pt",
218
  verbose=False,
219
  task="transcribe",
220
+ temperature=0,
221
+ condition_on_previous_text=False,
222
+ fp16=False
223
  )
224
+
225
+ segments = [Segment(start=s["start"], end=s["end"], text=s["text"].strip())
226
+ for s in result.get("segments", [])]
227
+
228
+ print(f"[whisper] segmentos: {len(segments)}")
 
 
 
 
 
 
229
  try:
230
  Path(audio_wav).unlink(missing_ok=True)
231
  except Exception:
 
235
  # ======================= PROCESSAMENTO DE VÍDEO =======================
236
 
237
  def extract_video_segment(input_video: str, output_video: str, start_time: float, end_time: float) -> bool:
238
+ duration = max(0.0, end_time - start_time)
239
+ if duration <= 0:
240
+ print(f"[extract] duração inválida: {duration}")
241
+ return False
242
  cmd = [
243
  "ffmpeg", "-y", "-ss", str(start_time), "-i", input_video,
244
  "-t", str(duration),
245
  "-c:v", "libx264",
246
+ "-c:a", "aac",
247
+ "-movflags", "+faststart",
248
  output_video
249
  ]
 
250
  try:
251
  subprocess.run(cmd, check=True, capture_output=True)
252
  return True
253
  except subprocess.CalledProcessError as e:
254
+ print(f"[extract] erro: {e}")
255
  return False
256
 
257
  def apply_smart_crop_to_video(input_path: str, output_path: str, target_width: int,
 
259
  """Calcula o melhor crop com rastreamento facial e aplica o crop com FFmpeg preservando o áudio."""
260
  tracker = FaceTracker()
261
  cap = cv2.VideoCapture(input_path)
 
262
  if not cap.isOpened():
263
+ print(f"[crop] erro ao abrir: {input_path}")
264
  return False
265
 
266
  frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
267
  frame_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
268
  frame_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
269
 
 
270
  sample_positions = []
271
  frame_indices = np.linspace(0, frame_count - 1, min(sample_frames, max(1, frame_count)), dtype=int)
 
272
  for idx in frame_indices:
273
  cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
274
  ret, frame = cap.read()
 
277
  sample_positions.append(crop_coords)
278
  cap.release()
279
 
 
280
  if sample_positions:
281
  avg_x = int(np.median([p[0] for p in sample_positions]))
282
  avg_y = int(np.median([p[1] for p in sample_positions]))
 
284
  crop_h = sample_positions[0][3]
285
  final_crop = (avg_x, avg_y, crop_w, crop_h)
286
  else:
 
287
  target_ar = target_width / target_height
288
  frame_ar = frame_w / frame_h
289
  if target_ar < frame_ar:
 
296
  final_crop = (0, (frame_h - crop_h) // 2, crop_w, crop_h)
297
 
298
  x, y, w, h = final_crop
299
+ print(f"[crop] final: x={x}, y={y}, w={w}, h={h} -> {target_width}x{target_height}")
300
 
 
301
  vf = f"crop={w}:{h}:{x}:{y},scale={target_width}:{target_height}:flags=lanczos"
302
  cmd = [
303
  "ffmpeg", "-y", "-i", input_path,
304
  "-vf", vf,
305
  "-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
306
+ "-c:a", "copy",
307
+ "-movflags", "+faststart",
308
  output_path
309
  ]
310
  try:
311
  subprocess.run(cmd, check=True, capture_output=True)
312
+ print(f"[crop] concluído: {output_path}")
313
  return True
314
  except subprocess.CalledProcessError as e:
315
+ print(f"[crop] erro ffmpeg: {e}")
316
  return False
317
 
318
  def apply_aspect_ratio(input_video: str, output_video: str, ar_mode: str, face_tracking: bool = False) -> bool:
 
325
  "Quadrado 1:1": (1080, 1080),
326
  "Retrato 4:5": (1080, 1350),
327
  }
 
328
  if ar_mode not in ar_dims:
329
  return False
330
 
331
  width, height = ar_dims[ar_mode]
 
332
  if face_tracking:
333
  return apply_smart_crop_to_video(input_video, output_video, width, height)
334
  else:
 
335
  cmd = [
336
  "ffmpeg", "-y", "-i", input_video,
337
  "-vf", f"scale={width}:{height}:force_original_aspect_ratio=increase,crop={width}:{height}",
 
338
  "-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
339
+ "-c:a", "copy",
340
+ "-movflags", "+faststart",
341
  output_video
342
  ]
343
  try:
 
356
  f.write(f"file '{os.path.abspath(vf)}'\n")
357
 
358
  try:
359
+ cmd = ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file, "-c", "copy", "-movflags", "+faststart", output_file]
 
360
  subprocess.run(cmd, check=True, capture_output=True)
361
  return True
362
  except subprocess.CalledProcessError:
 
363
  try:
364
  cmd = ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file,
365
  "-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
366
+ "-c:a", "aac", "-movflags", "+faststart", output_file]
367
  subprocess.run(cmd, check=True, capture_output=True)
368
  return True
369
  except subprocess.CalledProcessError:
 
382
 
383
  Path(output_dir).mkdir(parents=True, exist_ok=True)
384
  total_duration = segments[-1].end - segments[0].start
385
+ target_duration = min(max_len, max(min_len, total_duration / max(1, int(k))))
386
 
387
  outputs = []
388
  current_start = segments[0].start
389
 
390
+ for i in range(int(k)):
391
  target_end = current_start + target_duration
392
  best_end = target_end
393
 
 
405
  temp_file = Path(output_dir) / f"temp_linear_{i+1}.mp4"
406
  final_file = Path(output_dir) / f"cut_linear_{i+1}.mp4"
407
 
408
+ print(f"[linear] corte {i+1}/{k}: {start_with_pad:.1f}s - {end_with_pad:.1f}s")
409
 
410
+ src_path = resolve_video_path(video_file) or video_file
411
+ if extract_video_segment(src_path, str(temp_file), start_with_pad, end_with_pad):
412
  if ar_mode != "Original":
413
  if apply_aspect_ratio(str(temp_file), str(final_file), ar_mode, face_tracking):
414
  Path(temp_file).unlink(missing_ok=True)
 
435
  outputs = []
436
 
437
  import random
438
+ for i in range(int(k)):
439
  num_blocks = random.randint(min_blocks, min(max_blocks, len(segments)))
440
  step = max(1, len(segments) // num_blocks)
441
  selected_indices = [j * step for j in range(num_blocks)]
 
446
  block_file = Path(output_dir) / f"temp_creative_{i+1}_block_{j+1}.mp4"
447
  start = max(0, seg.start - pad)
448
  end = seg.end + pad
449
+ src_path = resolve_video_path(video_file) or video_file
450
+ if extract_video_segment(src_path, str(block_file), start, end):
451
  block_files.append(str(block_file))
452
 
453
  if not block_files:
 
476
  SPACE_OUT.mkdir(exist_ok=True, parents=True)
477
 
478
  def do_transcribe(video_file, model_size):
479
+ true_path = resolve_video_path(video_file)
480
+ if not true_path or not os.path.exists(true_path):
481
+ return [], "Selecione um vídeo válido."
482
+ segs = transcribe(true_path, model_size=model_size)
483
  preview = "\n".join([f"[{s.start:.1f}–{s.end:.1f}] {s.text}" for s in segs[:12]])
484
  return segs, f"Transcrição ok. Segmentos: {len(segs)}\n\nPrévia:\n{preview}"
485
 
 
487
  if not segs:
488
  return [], "Transcreva antes de cortar."
489
  workdir = SPACE_OUT / (out_subdir or "cortes")
490
+ outs = generate_linear_cuts(video_file, segs, str(workdir),
491
+ min_len=float(min_len), max_len=float(max_len), ideal_len=float(ideal_len),
492
+ k=int(k), gap_threshold=float(gap), pad=float(pad),
493
+ ar_mode=str(ar_mode), face_tracking=bool(face_tracking))
494
  return [str(Path(p)) for p in outs], f"Gerados: {len(outs)} arquivo(s)."
495
 
496
  def run_creative(segs, video_file, out_subdir, min_len, max_len, ideal_len, minb, maxb, k, gap, pad, ar_mode, face_tracking):
497
  if not segs:
498
  return [], "Transcreva antes de cortar."
499
  workdir = SPACE_OUT / (out_subdir or "cortes")
500
+ outs = generate_creative_cuts(video_file, segs, str(workdir),
501
+ min_len=float(min_len), max_len=float(max_len), ideal_len=float(ideal_len),
502
+ min_blocks=int(minb), max_blocks=int(maxb), k=int(k),
503
+ gap_threshold=float(gap), pad=float(pad),
504
+ ar_mode=str(ar_mode), face_tracking=bool(face_tracking))
505
  return [str(Path(p)) for p in outs], f"Gerados: {len(outs)} arquivo(s)."
506
 
507
  css = """
 
581
  outputs=[out_creative, status_creative])
582
 
583
  if __name__ == "__main__":
584
+ # Ativa fila para tarefas longas no Space
585
+ demo.queue(concurrency_count=1, max_size=20).launch()