Update app.py
Browse files
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
|
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
|
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 |
-
#
|
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 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
"
|
233 |
-
"
|
234 |
-
"
|
235 |
-
"
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
#
|
243 |
-
filtro_complex =
|
|
|
|
|
|
|
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];
|
255 |
|
256 |
-
|
|
|
|
|
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}':
|
|
|
|
|
|
|
|
|
|
|
|
|
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]",
|
|
|
270 |
video_editado
|
271 |
], check=True, stderr=subprocess.PIPE)
|
272 |
|
273 |
-
# Acelerar vídeo
|
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(
|
283 |
-
#
|
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 |
-
#
|
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 |
-
|
374 |
-
|
375 |
-
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|