pcdoido2 commited on
Commit
a91577e
·
verified ·
1 Parent(s): 07458aa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +97 -165
app.py CHANGED
@@ -6,10 +6,10 @@ import tempfile
6
  import shutil
7
  import time
8
 
9
- st.set_page_config(page_title="TikTok Video Generator - PRO + Anti-Flop", layout="centered")
10
- st.title("🎥 TikTok Video Generator - PRO + Anti-Flop")
11
 
12
- st.markdown("Envie seus vídeos e gere conteúdo com efeitos, zoom, texto, música, filtros e antiflop!")
13
 
14
  # Uploads dos vídeos de cortes (principal)
15
  st.markdown("<div style='background-color:#F0F0FF;padding:10px;border-radius:8px'>", unsafe_allow_html=True)
@@ -36,6 +36,7 @@ blur_strength = st.slider("Blur no fundo", 1, 50, 10)
36
  velocidade_cortes = st.slider("Velocidade dos cortes", 0.5, 2.0, 1.0, 0.1)
37
  velocidade_final = st.slider("Velocidade final", 0.5, 2.0, 1.0, 0.1)
38
  crf_value = st.slider("Qualidade CRF", 18, 30, 23)
 
39
  # Texto com emojis
40
  st.write("### Texto no Vídeo")
41
  ativar_texto = st.checkbox("Ativar texto", value=False)
@@ -58,6 +59,12 @@ ativar_granulado = st.checkbox("Granulado", False)
58
  ativar_pb = st.checkbox("Preto e branco", False)
59
  ativar_vignette = st.checkbox("Vignette", False)
60
 
 
 
 
 
 
 
61
  # Efeitos extras
62
  st.write("### Outros efeitos")
63
  ativar_espelhar = st.checkbox("Espelhar vídeo", True)
@@ -70,55 +77,7 @@ ativar_borda_personalizada = st.checkbox("Borda personalizada", False)
70
  if ativar_borda_personalizada:
71
  cor_borda = st.color_picker("Cor da borda", "#FF0000")
72
  animacao_borda = st.selectbox("Animação da borda", ["Nenhuma", "Borda Pulsante", "Cor Animada", "Neon", "Ondulada"])
73
-
74
- # 🔒 Anti-Flop Completo
75
- st.write("### 🔒 Anti-Flop (Evitar Detecção)")
76
- ativar_antiflop = st.checkbox("Ativar Anti-Flop", value=False)
77
- if ativar_antiflop:
78
- aplicar_random = st.button("🎲 RANDOMIZAR Anti-Flop")
79
- if 'antiflop_settings' not in st.session_state or aplicar_random:
80
- st.session_state.antiflop_settings = {
81
- 'zoom': random.uniform(1.00, 1.05),
82
- 'brilho': random.uniform(-0.05, 0.05),
83
- 'contraste': random.uniform(0.95, 1.05),
84
- 'saturacao': random.uniform(0.95, 1.05),
85
- 'ruido': random.randint(3, 15),
86
- 'rotacao': random.uniform(0.2, 1.5),
87
- 'pitch': random.uniform(1.00, 1.08),
88
- 'velocidade': random.uniform(1.00, 1.03),
89
- 'bitrate': random.randint(900, 1500),
90
- 'fps': random.choice([30, 29.97, 25])
91
- }
92
- antiflop = st.session_state.antiflop_settings
93
- zoom_af = st.slider("Zoom Anti-Flop (%)", 100, 105, int(antiflop['zoom']*100)) / 100
94
- brilho_af = st.slider("Brilho Anti-Flop", -0.1, 0.1, antiflop['brilho'])
95
- contraste_af = st.slider("Contraste Anti-Flop", 0.9, 1.1, antiflop['contraste'])
96
- saturacao_af = st.slider("Saturação Anti-Flop", 0.9, 1.1, antiflop['saturacao'])
97
- ruido_af = st.slider("Ruído Visual Anti-Flop (0-20)", 0, 20, antiflop['ruido'])
98
- rotacao_af = st.slider("Rotação Anti-Flop (graus)", 0.0, 2.0, antiflop['rotacao'])
99
- pitch_af = st.slider("Pitch do Áudio Anti-Flop (%)", 100, 110, int(antiflop['pitch']*100)) / 100
100
- velocidade_af = st.slider("Velocidade do Vídeo Anti-Flop (%)", 100, 103, int(antiflop['velocidade']*100)) / 100
101
- bitrate_af = st.slider("Bitrate Anti-Flop (kbps)", 800, 2000, antiflop['bitrate'])
102
- fps_af = st.selectbox("FPS Final Anti-Flop", [30, 29.97, 25], index=[30, 29.97, 25].index(antiflop['fps']))
103
- st.subheader("🧩 Extras Anti-Detecção")
104
- usar_marca = st.checkbox("Marca d’água Fantasma com perfil", value=True)
105
- usar_glitch = st.checkbox("Inserir Frame Preto Aleatório", value=True)
106
- res_random = st.checkbox("Resolução Aleatória", value=True)
107
- cortar = st.checkbox("Cortar trecho aleatório do vídeo", value=True)
108
- embaralhar = st.checkbox("Embaralhar trechos do vídeo", value=True)
109
- st.subheader("🎨 Avançado Anti-Flop")
110
- lote_efeitos = st.checkbox("Aplicar efeitos por lote (aleatórios)", value=True)
111
- transicoes = st.checkbox("Transições visuais aleatórias", value=True)
112
- st.subheader("🕵️‍♂️ Perfil Fingerprint")
113
- fingerprint = st.selectbox("Escolha o Perfil Anti-Flop", ["Android", "iPhone", "Câmera", "CapCut", "Samsung", "Xiaomi", "LG"])
114
-
115
- # 💾 Download Automático
116
- st.write("### 💾 Download Automático")
117
- download_automatico = st.checkbox("Ativar Download Automático", value=False)
118
- # Lista para armazenar os vídeos gerados
119
- videos_gerados = []
120
-
121
- # Botão principal
122
  if st.button("Gerar Vídeo(s)"):
123
  if not cortes:
124
  st.error("❌ Envie os vídeos de cortes.")
@@ -130,17 +89,19 @@ if st.button("Gerar Vídeo(s)"):
130
  try:
131
  # Salvar ou gerar fundo
132
  fundo_path = os.path.join(temp_dir, "fundo.mp4")
 
133
  if video_fundo:
134
  with open(fundo_path, "wb") as f:
135
  f.write(video_fundo.read())
136
  else:
 
137
  subprocess.run([
138
  "ffmpeg", "-f", "lavfi", "-i", "color=black:s=720x1280:d=600",
139
  "-c:v", "libx264", "-t", str(duracao_final),
140
  "-pix_fmt", "yuv420p", "-y", fundo_path
141
  ], check=True, stderr=subprocess.PIPE)
142
 
143
- # Tutorial (opcional)
144
  if video_tutorial:
145
  tutorial_path = os.path.join(temp_dir, "tutorial_raw.mp4")
146
  with open(tutorial_path, "wb") as f:
@@ -152,7 +113,7 @@ if st.button("Gerar Vídeo(s)"):
152
  "-y", tutorial_mp4
153
  ], check=True, stderr=subprocess.PIPE)
154
 
155
- # Padronizar cortes 1280x720
156
  cortes_names = []
157
  for idx, corte in enumerate(cortes):
158
  path_in = os.path.join(temp_dir, f"corte_in_{idx}.mp4")
@@ -161,11 +122,11 @@ if st.button("Gerar Vídeo(s)"):
161
  f.write(corte.read())
162
  subprocess.run([
163
  "ffmpeg", "-i", path_in,
164
- "-vf", "scale=1280:720:force_original_aspect_ratio=decrease",
165
  "-c:v", "libx264", "-preset", "ultrafast", "-crf", "30", path_out
166
  ], check=True, stderr=subprocess.PIPE)
167
  cortes_names.append(path_out)
168
- # Cortar fundo para a duração final
169
  fundo_cortado = os.path.join(temp_dir, "fundo_cortado.mp4")
170
  subprocess.run([
171
  "ffmpeg", "-i", fundo_path, "-t", str(duracao_final),
@@ -190,10 +151,32 @@ if st.button("Gerar Vídeo(s)"):
190
  if d > duracao_corte:
191
  ini = random.uniform(0, d - duracao_corte)
192
  out = os.path.join(temp_dir, f"cut_{random.randint(1000,9999)}.mp4")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  subprocess.run([
194
  "ffmpeg", "-ss", str(ini), "-i", c, "-t", str(duracao_corte),
 
195
  "-an", "-c:v", "libx264", "-preset", "ultrafast", "-crf", "30", out
196
  ], check=True, stderr=subprocess.PIPE)
 
197
  cortes_prontos.append(out)
198
  tempo_total += duracao_corte / velocidade_cortes
199
  if tempo_total >= duracao_final:
@@ -201,7 +184,7 @@ if st.button("Gerar Vídeo(s)"):
201
  except:
202
  continue
203
 
204
- # Concatenação dos cortes
205
  lista = os.path.join(temp_dir, f"lista_{n}.txt")
206
  with open(lista, "w") as f:
207
  for c in cortes_prontos:
@@ -210,37 +193,45 @@ if st.button("Gerar Vídeo(s)"):
210
  video_raw = os.path.join(temp_dir, f"video_raw_{n}.mp4")
211
  subprocess.run([
212
  "ffmpeg", "-f", "concat", "-safe", "0", "-i", lista,
 
213
  "-c:v", "libx264", "-preset", "ultrafast", "-crf", "30", video_raw
214
  ], check=True, stderr=subprocess.PIPE)
215
  progresso.progress(35 + n * 5)
216
-
217
- # Filtros no vídeo principal (zoom, espelho, borda, etc.)
218
  filtros_main = ["scale=720:1280:force_original_aspect_ratio=decrease"]
 
219
  if zoom != 1.0:
220
  filtros_main.append(f"scale=iw*{zoom}:ih*{zoom}")
 
221
  filtros_main.append(f"setpts=PTS/{velocidade_cortes}")
 
222
  if ativar_espelhar:
223
  filtros_main.append("hflip")
 
224
  if remover_borda and tamanho_borda > 0:
225
  filtros_main.append(f"crop=in_w-{tamanho_borda*2}:in_h-{tamanho_borda*2}")
 
226
  if ativar_filtro_cor:
227
  filtros_main.append("eq=contrast=1.1:saturation=1.2")
228
- if ativar_borda_personalizada:
229
- cor_ffmpeg = f"0x{cor_borda.lstrip('#')}FF"
230
- anim_map = {
231
- "Nenhuma": "",
232
- "Borda Pulsante": "enable='lt(mod(t,1),0.5)'",
233
- "Cor Animada": "enable='lt(mod(t,1),0.5)'",
234
- "Neon": "enable='lt(mod(t,0.5),0.25)'",
235
- "Ondulada": "enable='gt(sin(t*3.14),0)'"
236
- }
237
- drawbox = f"drawbox=x=0:y=0:w=iw:h=ih:color={cor_ffmpeg}:t=5"
238
- if anim_map[animacao_borda]:
239
- drawbox += f":{anim_map[animacao_borda]}"
240
- filtros_main.append(drawbox)
241
-
242
- # Fundo com filtros
243
- filtro_complex = f"[0:v]scale=720:1280:force_original_aspect_ratio=increase,crop=720:1280"
 
 
 
244
  if ativar_blur_fundo:
245
  filtro_complex += f",boxblur={blur_strength}:1"
246
  if ativar_sepia:
@@ -251,26 +242,36 @@ if st.button("Gerar Vídeo(s)"):
251
  filtro_complex += ",hue=s=0.3"
252
  if ativar_vignette:
253
  filtro_complex += ",vignette"
254
- filtro_complex += "[blur];[1:v]" + ",".join(filtros_main) + "[zoomed];[blur][zoomed]overlay=(W-w)/2:(H-h)/2[base]"
255
 
256
- # Inserir texto se ativado
 
 
257
  if ativar_texto and texto_personalizado.strip():
258
  y_pos = "100" if posicao_texto == "Topo" else "(h-text_h)/2" if posicao_texto == "Centro" else "h-text_h-100"
259
  enable = f":enable='lt(t\\,{segundos_texto})'" if duracao_texto == "Apenas primeiros segundos" else ""
260
  texto_clean = texto_personalizado.replace(":", "\\:").replace("'", "\\'")
261
- filtro_complex += f";[base]drawtext=text='{texto_clean}':fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf:fontcolor={cor_texto}:fontsize={tamanho_texto}:shadowcolor={cor_sombra}:shadowx=3:shadowy=3:x=(w-text_w)/2:y={y_pos}{enable}[final]"
 
 
 
 
 
 
262
  else:
263
  filtro_complex += ";[base]null[final]"
264
 
 
265
  video_editado = os.path.join(temp_dir, f"video_editado_{n}.mp4")
266
  subprocess.run([
267
  "ffmpeg", "-i", fundo_cortado, "-i", video_raw,
268
  "-filter_complex", filtro_complex,
269
- "-map", "[final]", "-c:v", "libx264", "-preset", "ultrafast", "-crf", str(crf_value),
 
270
  video_editado
271
  ], check=True, stderr=subprocess.PIPE)
272
 
273
- # Acelerar vídeo final (sem áudio)
274
  video_acelerado = os.path.join(temp_dir, f"video_acelerado_{n}.mp4")
275
  subprocess.run([
276
  "ffmpeg", "-y", "-i", video_editado, "-an",
@@ -279,9 +280,9 @@ if st.button("Gerar Vídeo(s)"):
279
  video_acelerado
280
  ], check=True, stderr=subprocess.PIPE)
281
 
282
- progresso.progress(70)
283
- # Inserir tutorial no meio (se enviado)
284
- video_final_raw = video_acelerado
285
  if video_tutorial:
286
  dur_proc = subprocess.run([
287
  "ffprobe", "-v", "error", "-show_entries", "format=duration",
@@ -289,8 +290,10 @@ if st.button("Gerar Vídeo(s)"):
289
  ], stdout=subprocess.PIPE)
290
  dur_f = float(dur_proc.stdout.decode().strip())
291
  pt = dur_f / 2 if dur_f < 10 else random.uniform(5, dur_f - 5)
 
292
  part1 = os.path.join(temp_dir, f"part1_{n}.mp4")
293
  part2 = os.path.join(temp_dir, f"part2_{n}.mp4")
 
294
  subprocess.run([
295
  "ffmpeg", "-i", video_acelerado, "-ss", "0", "-t", str(pt),
296
  "-c:v", "libx264", "-preset", "ultrafast", part1
@@ -299,9 +302,11 @@ if st.button("Gerar Vídeo(s)"):
299
  "ffmpeg", "-i", video_acelerado, "-ss", str(pt),
300
  "-c:v", "libx264", "-preset", "ultrafast", part2
301
  ], check=True, stderr=subprocess.PIPE)
 
302
  final_txt = os.path.join(temp_dir, f"final_{n}.txt")
303
  with open(final_txt, "w") as f:
304
  f.write(f"file '{part1}'\nfile '{tutorial_mp4}'\nfile '{part2}'\n")
 
305
  video_final_raw = os.path.join(temp_dir, f"video_final_raw_{n}.mp4")
306
  subprocess.run([
307
  "ffmpeg", "-f", "concat", "-safe", "0", "-i", final_txt,
@@ -309,22 +314,25 @@ if st.button("Gerar Vídeo(s)"):
309
  video_final_raw
310
  ], check=True, stderr=subprocess.PIPE)
311
 
312
- # 🎵 Inserir música no final
313
  dur_proc = subprocess.run([
314
  "ffprobe", "-v", "error", "-show_entries", "format=duration",
315
  "-of", "default=noprint_wrappers=1:nokey=1", video_final_raw
316
  ], stdout=subprocess.PIPE)
317
  dur_video_real = float(dur_proc.stdout.decode().strip())
318
 
 
319
  if musica:
320
  musica_path = os.path.join(temp_dir, "musica_original.mp3")
321
  with open(musica_path, "wb") as f:
322
  f.write(musica.read())
 
323
  musica_cortada = os.path.join(temp_dir, f"musica_cortada_{n}.aac")
324
  subprocess.run([
325
  "ffmpeg", "-i", musica_path, "-ss", "0", "-t", str(dur_video_real),
326
  "-vn", "-acodec", "aac", "-y", musica_cortada
327
  ], check=True, stderr=subprocess.PIPE)
 
328
  final_name = f"video_final_{n}_{int(time.time())}.mp4"
329
  subprocess.run([
330
  "ffmpeg", "-i", video_final_raw, "-i", musica_cortada,
@@ -332,67 +340,16 @@ if st.button("Gerar Vídeo(s)"):
332
  "-c:v", "copy", "-c:a", "aac",
333
  "-shortest", final_name
334
  ], check=True, stderr=subprocess.PIPE)
 
335
  else:
 
336
  final_name = f"video_final_{n}_{int(time.time())}.mp4"
337
  shutil.copy(video_final_raw, final_name)
338
- # 🔒 APLICAR ANTI-FLOP NO FINAL
339
- if ativar_antiflop:
340
- st.info(f"🔒 Aplicando Anti-Flop no vídeo {n+1}...")
341
-
342
- antiflop_out = f"antiflop_{n}_{int(time.time())}.mp4"
343
- encoder_name = f"{fingerprint}Cam_{random.randint(1000,9999)}"
344
-
345
- vf_af = (
346
- f"scale=iw*{zoom_af}:ih*{zoom_af},"
347
- f"crop=iw/{zoom_af}:ih/{zoom_af},"
348
- f"eq=brightness={brilho_af}:contrast={contraste_af}:saturation={saturacao_af},"
349
- f"noise=alls={ruido_af}:allf=t,"
350
- f"rotate={rotacao_af}*PI/180:[email protected],"
351
- "scale=trunc(iw/2)*2:trunc(ih/2)*2"
352
- )
353
- if usar_marca:
354
- vf_af += f",drawtext=text='{encoder_name}':[email protected]:x=10:y=10:fontsize=24"
355
- if usar_glitch:
356
- vf_af += ",blackframe=1:0"
357
- if transicoes:
358
- effects = [
359
- "fade=t=in:st=0:d=0.5",
360
- "fade=t=out:st=3:d=0.5",
361
- "zoompan=z='min(pzoom+0.0015,1.5)':d=1",
362
- "drawbox=x=0:y=0:w=iw:h=ih:[email protected]:t=fill"
363
- ]
364
- vf_af += "," + random.choice(effects)
365
-
366
- af = f"asetrate=44100*{pitch_af},aresample=44100"
367
- profile = "baseline" if fingerprint in ["Android", "Xiaomi"] else "main"
368
- if fingerprint == "iPhone":
369
- profile = "high"
370
- level = "4.0" if fingerprint == "Samsung" else "3.1"
371
- ar = "44100" if fingerprint in ["iPhone", "Xiaomi"] else "48000"
372
 
373
- subprocess.run([
374
- 'ffmpeg', '-i', final_name,
375
- '-vf', vf_af,
376
- '-af', af,
377
- '-r', str(fps_af),
378
- '-c:v', 'libx264', '-profile:v', profile, '-level', level,
379
- '-b:v', f"{bitrate_af}k", '-ar', ar,
380
- '-preset', 'fast', '-tune', 'zerolatency',
381
- '-movflags', '+faststart',
382
- antiflop_out
383
- ], check=True)
384
-
385
- videos_gerados.append(antiflop_out)
386
- st.video(antiflop_out)
387
- with open(antiflop_out, "rb") as f:
388
- st.download_button(f"📥 Baixar vídeo {n+1} Anti-Flop", f, file_name=antiflop_out)
389
- else:
390
- videos_gerados.append(final_name)
391
- st.video(final_name)
392
- with open(final_name, "rb") as f:
393
- st.download_button(f"📥 Baixar vídeo {n+1}", f, file_name=final_name)
394
 
395
- # Fim do for n in range(num_videos_finais)
396
  progresso.progress(100)
397
  st.success("✅ Todos os vídeos foram gerados com sucesso!")
398
 
@@ -401,28 +358,3 @@ if st.button("Gerar Vídeo(s)"):
401
 
402
  finally:
403
  shutil.rmtree(temp_dir)
404
- # Fora do try-finally: Botão "Baixar Todos os Vídeos"
405
- if videos_gerados:
406
- st.markdown("### 📥 Baixar Todos os Vídeos")
407
-
408
- # Criar links ocultos para todos os vídeos
409
- download_links = ""
410
- for v in videos_gerados:
411
- download_links += f"<a href='{v}' download='{v}'></a>\n"
412
-
413
- # Botão com JavaScript para clicar em todos os links
414
- st.markdown(
415
- f"""
416
- <button onclick="baixarTodos()">📥 Baixar Todos</button>
417
- <script>
418
- function baixarTodos() {{
419
- const links = Array.from(document.querySelectorAll('a[download]'));
420
- for (let l of links) {{
421
- l.click();
422
- }}
423
- }}
424
- </script>
425
- {download_links}
426
- """,
427
- unsafe_allow_html=True
428
- )
 
6
  import shutil
7
  import time
8
 
9
+ st.set_page_config(page_title="TikTok Video Generator", layout="centered")
10
+ st.title("🎥 TikTok Video Generator - PRO")
11
 
12
+ st.markdown("Envie seus vídeos e gere conteúdo com efeitos, zoom, texto, música e filtros!")
13
 
14
  # Uploads dos vídeos de cortes (principal)
15
  st.markdown("<div style='background-color:#F0F0FF;padding:10px;border-radius:8px'>", unsafe_allow_html=True)
 
36
  velocidade_cortes = st.slider("Velocidade dos cortes", 0.5, 2.0, 1.0, 0.1)
37
  velocidade_final = st.slider("Velocidade final", 0.5, 2.0, 1.0, 0.1)
38
  crf_value = st.slider("Qualidade CRF", 18, 30, 23)
39
+
40
  # Texto com emojis
41
  st.write("### Texto no Vídeo")
42
  ativar_texto = st.checkbox("Ativar texto", value=False)
 
59
  ativar_pb = st.checkbox("Preto e branco", False)
60
  ativar_vignette = st.checkbox("Vignette", False)
61
 
62
+ # Novos efeitos PRO
63
+ st.write("### Efeitos PRO")
64
+ ativar_zoom_dinamico = st.checkbox("Ativar Zoom/Pan Dinâmico (Movimento de câmera)", value=False)
65
+ ativar_color_grading = st.checkbox("Aplicar Color Grading Aleatório", value=False)
66
+ ativar_transicoes = st.checkbox("Adicionar Transições Cinemáticas", value=False)
67
+
68
  # Efeitos extras
69
  st.write("### Outros efeitos")
70
  ativar_espelhar = st.checkbox("Espelhar vídeo", True)
 
77
  if ativar_borda_personalizada:
78
  cor_borda = st.color_picker("Cor da borda", "#FF0000")
79
  animacao_borda = st.selectbox("Animação da borda", ["Nenhuma", "Borda Pulsante", "Cor Animada", "Neon", "Ondulada"])
80
+ # BOTÃO PRINCIPAL
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  if st.button("Gerar Vídeo(s)"):
82
  if not cortes:
83
  st.error("❌ Envie os vídeos de cortes.")
 
89
  try:
90
  # Salvar ou gerar fundo
91
  fundo_path = os.path.join(temp_dir, "fundo.mp4")
92
+
93
  if video_fundo:
94
  with open(fundo_path, "wb") as f:
95
  f.write(video_fundo.read())
96
  else:
97
+ # Gera fundo preto 720x1280 com duração longa
98
  subprocess.run([
99
  "ffmpeg", "-f", "lavfi", "-i", "color=black:s=720x1280:d=600",
100
  "-c:v", "libx264", "-t", str(duracao_final),
101
  "-pix_fmt", "yuv420p", "-y", fundo_path
102
  ], check=True, stderr=subprocess.PIPE)
103
 
104
+ # Salvar e converter tutorial (se enviado)
105
  if video_tutorial:
106
  tutorial_path = os.path.join(temp_dir, "tutorial_raw.mp4")
107
  with open(tutorial_path, "wb") as f:
 
113
  "-y", tutorial_mp4
114
  ], check=True, stderr=subprocess.PIPE)
115
 
116
+ # Padronizar vídeos de cortes para 1280x720 com múltiplos de 2
117
  cortes_names = []
118
  for idx, corte in enumerate(cortes):
119
  path_in = os.path.join(temp_dir, f"corte_in_{idx}.mp4")
 
122
  f.write(corte.read())
123
  subprocess.run([
124
  "ffmpeg", "-i", path_in,
125
+ "-vf", "scale=1280:720:force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2",
126
  "-c:v", "libx264", "-preset", "ultrafast", "-crf", "30", path_out
127
  ], check=True, stderr=subprocess.PIPE)
128
  cortes_names.append(path_out)
129
+ # Cortar o vídeo de fundo para a duração final
130
  fundo_cortado = os.path.join(temp_dir, "fundo_cortado.mp4")
131
  subprocess.run([
132
  "ffmpeg", "-i", fundo_path, "-t", str(duracao_final),
 
151
  if d > duracao_corte:
152
  ini = random.uniform(0, d - duracao_corte)
153
  out = os.path.join(temp_dir, f"cut_{random.randint(1000,9999)}.mp4")
154
+
155
+ filtros_corte = []
156
+ # Aplica zoom dinâmico se ativado
157
+ if ativar_zoom_dinamico:
158
+ # Movimento aleatório para cima/baixo/esquerda/direita
159
+ movimentos = [
160
+ "crop=in_w-20:in_h-20:0:0,zoompan=z='zoom+0.001':d=125",
161
+ "crop=in_w-20:in_h-20:20:0,zoompan=z='zoom+0.0015':d=125",
162
+ "crop=in_w-20:in_h-20:0:20,zoompan=z='zoom+0.0015':d=125"
163
+ ]
164
+ filtros_corte.append(random.choice(movimentos))
165
+ else:
166
+ filtros_corte.append("scale=trunc(iw/2)*2:trunc(ih/2)*2")
167
+
168
+ # Transição: Adiciona fade in/out aleatório
169
+ if ativar_transicoes:
170
+ filtros_corte.append("fade=t=in:st=0:d=0.5,fade=t=out:st=4.5:d=0.5")
171
+
172
+ filtro_final = ",".join(filtros_corte)
173
+
174
  subprocess.run([
175
  "ffmpeg", "-ss", str(ini), "-i", c, "-t", str(duracao_corte),
176
+ "-vf", filtro_final,
177
  "-an", "-c:v", "libx264", "-preset", "ultrafast", "-crf", "30", out
178
  ], check=True, stderr=subprocess.PIPE)
179
+
180
  cortes_prontos.append(out)
181
  tempo_total += duracao_corte / velocidade_cortes
182
  if tempo_total >= duracao_final:
 
184
  except:
185
  continue
186
 
187
+ # Concatenação dos cortes com ajuste para múltiplos de 2
188
  lista = os.path.join(temp_dir, f"lista_{n}.txt")
189
  with open(lista, "w") as f:
190
  for c in cortes_prontos:
 
193
  video_raw = os.path.join(temp_dir, f"video_raw_{n}.mp4")
194
  subprocess.run([
195
  "ffmpeg", "-f", "concat", "-safe", "0", "-i", lista,
196
+ "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2",
197
  "-c:v", "libx264", "-preset", "ultrafast", "-crf", "30", video_raw
198
  ], check=True, stderr=subprocess.PIPE)
199
  progresso.progress(35 + n * 5)
200
+ # Filtros aplicados ao vídeo principal
 
201
  filtros_main = ["scale=720:1280:force_original_aspect_ratio=decrease"]
202
+
203
  if zoom != 1.0:
204
  filtros_main.append(f"scale=iw*{zoom}:ih*{zoom}")
205
+
206
  filtros_main.append(f"setpts=PTS/{velocidade_cortes}")
207
+
208
  if ativar_espelhar:
209
  filtros_main.append("hflip")
210
+
211
  if remover_borda and tamanho_borda > 0:
212
  filtros_main.append(f"crop=in_w-{tamanho_borda*2}:in_h-{tamanho_borda*2}")
213
+
214
  if ativar_filtro_cor:
215
  filtros_main.append("eq=contrast=1.1:saturation=1.2")
216
+
217
+ # Aplica Color Grading aleatório se ativado
218
+ if ativar_color_grading:
219
+ opcoes_color = [
220
+ "curves=preset=vintage",
221
+ "curves=preset=strong_contrast",
222
+ "curves=preset=color_negative",
223
+ "hue=s=0.5",
224
+ "eq=brightness=0.05:saturation=1.5:contrast=1.2"
225
+ ]
226
+ filtros_main.append(random.choice(opcoes_color))
227
+
228
+ # Garantir múltiplos de 2 no final (sempre)
229
+ filtros_main.append("scale=trunc(iw/2)*2:trunc(ih/2)*2")
230
+ # Montar filter_complex
231
+ filtro_complex = (
232
+ f"[0:v]scale=720:1280:force_original_aspect_ratio=increase,"
233
+ f"crop=720:1280"
234
+ )
235
  if ativar_blur_fundo:
236
  filtro_complex += f",boxblur={blur_strength}:1"
237
  if ativar_sepia:
 
242
  filtro_complex += ",hue=s=0.3"
243
  if ativar_vignette:
244
  filtro_complex += ",vignette"
245
+ filtro_complex += "[blur];"
246
 
247
+ filtro_complex += f"[1:v]{','.join(filtros_main)}[zoomed];"
248
+ filtro_complex += "[blur][zoomed]overlay=(W-w)/2:(H-h)/2[base]"
249
+
250
  if ativar_texto and texto_personalizado.strip():
251
  y_pos = "100" if posicao_texto == "Topo" else "(h-text_h)/2" if posicao_texto == "Centro" else "h-text_h-100"
252
  enable = f":enable='lt(t\\,{segundos_texto})'" if duracao_texto == "Apenas primeiros segundos" else ""
253
  texto_clean = texto_personalizado.replace(":", "\\:").replace("'", "\\'")
254
+ filtro_complex += f";[base]drawtext=text='{texto_clean}':"
255
+ filtro_complex += (
256
+ f"fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf:"
257
+ f"fontcolor={cor_texto}:fontsize={tamanho_texto}:"
258
+ f"shadowcolor={cor_sombra}:shadowx=3:shadowy=3:"
259
+ f"x=(w-text_w)/2:y={y_pos}{enable}[final]"
260
+ )
261
  else:
262
  filtro_complex += ";[base]null[final]"
263
 
264
+ # Gerar vídeo com filtros
265
  video_editado = os.path.join(temp_dir, f"video_editado_{n}.mp4")
266
  subprocess.run([
267
  "ffmpeg", "-i", fundo_cortado, "-i", video_raw,
268
  "-filter_complex", filtro_complex,
269
+ "-map", "[final]",
270
+ "-c:v", "libx264", "-preset", "ultrafast", "-crf", str(crf_value),
271
  video_editado
272
  ], check=True, stderr=subprocess.PIPE)
273
 
274
+ # Acelerar vídeo (sem áudio ainda)
275
  video_acelerado = os.path.join(temp_dir, f"video_acelerado_{n}.mp4")
276
  subprocess.run([
277
  "ffmpeg", "-y", "-i", video_editado, "-an",
 
280
  video_acelerado
281
  ], check=True, stderr=subprocess.PIPE)
282
 
283
+ progresso.progress(90)
284
+ # Tutorial no meio, se fornecido
285
+ video_final_raw = video_acelerado # Começa com o vídeo acelerado
286
  if video_tutorial:
287
  dur_proc = subprocess.run([
288
  "ffprobe", "-v", "error", "-show_entries", "format=duration",
 
290
  ], stdout=subprocess.PIPE)
291
  dur_f = float(dur_proc.stdout.decode().strip())
292
  pt = dur_f / 2 if dur_f < 10 else random.uniform(5, dur_f - 5)
293
+
294
  part1 = os.path.join(temp_dir, f"part1_{n}.mp4")
295
  part2 = os.path.join(temp_dir, f"part2_{n}.mp4")
296
+
297
  subprocess.run([
298
  "ffmpeg", "-i", video_acelerado, "-ss", "0", "-t", str(pt),
299
  "-c:v", "libx264", "-preset", "ultrafast", part1
 
302
  "ffmpeg", "-i", video_acelerado, "-ss", str(pt),
303
  "-c:v", "libx264", "-preset", "ultrafast", part2
304
  ], check=True, stderr=subprocess.PIPE)
305
+
306
  final_txt = os.path.join(temp_dir, f"final_{n}.txt")
307
  with open(final_txt, "w") as f:
308
  f.write(f"file '{part1}'\nfile '{tutorial_mp4}'\nfile '{part2}'\n")
309
+
310
  video_final_raw = os.path.join(temp_dir, f"video_final_raw_{n}.mp4")
311
  subprocess.run([
312
  "ffmpeg", "-f", "concat", "-safe", "0", "-i", final_txt,
 
314
  video_final_raw
315
  ], check=True, stderr=subprocess.PIPE)
316
 
317
+ # 🧠 Obter duração real do vídeo final (após tudo)
318
  dur_proc = subprocess.run([
319
  "ffprobe", "-v", "error", "-show_entries", "format=duration",
320
  "-of", "default=noprint_wrappers=1:nokey=1", video_final_raw
321
  ], stdout=subprocess.PIPE)
322
  dur_video_real = float(dur_proc.stdout.decode().strip())
323
 
324
+ # 🎵 Cortar música para a duração real
325
  if musica:
326
  musica_path = os.path.join(temp_dir, "musica_original.mp3")
327
  with open(musica_path, "wb") as f:
328
  f.write(musica.read())
329
+
330
  musica_cortada = os.path.join(temp_dir, f"musica_cortada_{n}.aac")
331
  subprocess.run([
332
  "ffmpeg", "-i", musica_path, "-ss", "0", "-t", str(dur_video_real),
333
  "-vn", "-acodec", "aac", "-y", musica_cortada
334
  ], check=True, stderr=subprocess.PIPE)
335
+
336
  final_name = f"video_final_{n}_{int(time.time())}.mp4"
337
  subprocess.run([
338
  "ffmpeg", "-i", video_final_raw, "-i", musica_cortada,
 
340
  "-c:v", "copy", "-c:a", "aac",
341
  "-shortest", final_name
342
  ], check=True, stderr=subprocess.PIPE)
343
+
344
  else:
345
+ # Se não tiver música, só copia o vídeo final
346
  final_name = f"video_final_{n}_{int(time.time())}.mp4"
347
  shutil.copy(video_final_raw, final_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
 
349
+ st.video(final_name)
350
+ with open(final_name, "rb") as f:
351
+ st.download_button(f"📥 Baixar vídeo {n+1}", f, file_name=final_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
 
353
  progresso.progress(100)
354
  st.success("✅ Todos os vídeos foram gerados com sucesso!")
355
 
 
358
 
359
  finally:
360
  shutil.rmtree(temp_dir)