|
import streamlit as st |
|
import subprocess |
|
import random |
|
import os |
|
import tempfile |
|
import time |
|
import base64 |
|
import uuid |
|
import shutil |
|
import datetime |
|
import yt_dlp |
|
|
|
st.set_page_config(page_title="🎬 Cortar e Embaralhar Vídeo", layout="centered") |
|
st.title("🎬 Cortar e Embaralhar Vídeo") |
|
|
|
st.markdown("**1️⃣ Envie um vídeo curto (máx 3 minutos)**") |
|
video = st.file_uploader("Selecione um vídeo", type=["mp4"]) |
|
|
|
st.markdown("**🔗 Ou insira o link do TikTok**") |
|
url = st.text_input("URL do vídeo TikTok") |
|
|
|
|
|
if "processando" not in st.session_state: |
|
st.session_state.processando = False |
|
if "video_path" not in st.session_state: |
|
st.session_state.video_path = None |
|
if "tutorial_path" not in st.session_state: |
|
st.session_state.tutorial_path = None |
|
if "video_final" not in st.session_state: |
|
st.session_state.video_final = None |
|
|
|
if url and st.button("🔄 Puxar vídeo do TikTok"): |
|
with st.spinner("Baixando vídeo do TikTok..."): |
|
unique_id = str(uuid.uuid4()) |
|
saida_video = os.path.join(tempfile.gettempdir(), f'tiktok_video_{unique_id}.mp4') |
|
ydl_opts = { |
|
'outtmpl': saida_video, |
|
'format': 'mp4', |
|
'noplaylist': True |
|
} |
|
try: |
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl: |
|
ydl.download([url]) |
|
if os.path.exists(saida_video): |
|
st.session_state.video_path = saida_video |
|
st.success("✅ Vídeo do TikTok baixado com sucesso!") |
|
except Exception as e: |
|
st.error(f"Erro ao baixar vídeo: {e}") |
|
|
|
|
|
if video and not st.session_state.video_path: |
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp: |
|
tmp.write(video.read()) |
|
st.session_state.video_path = tmp.name |
|
|
|
video_path = st.session_state.video_path |
|
|
|
if video_path and not st.session_state.processando: |
|
st.subheader("🎥 Pré-visualização do vídeo original") |
|
st.video(video_path) |
|
|
|
st.markdown("**2️⃣ Configure a exclusão e os cortes**") |
|
excluir_inicio = st.number_input("Excluir a partir do segundo", min_value=0, value=8, key="excluir_inicio") |
|
excluir_fim = st.number_input("Excluir até o segundo", min_value=0, value=12, key="excluir_fim") |
|
|
|
st.checkbox("✂️ Ativar cortes e embaralhamento aleatório", value=False, key="embaralhar_cortes") |
|
|
|
if st.session_state.embaralhar_cortes: |
|
corte_min = st.number_input("Tamanho mínimo do corte (segundos)", min_value=1, value=2, key="corte_min") |
|
corte_max = st.number_input("Tamanho máximo do corte (segundos)", min_value=3, value=5, key="corte_max") |
|
|
|
tutorial = st.file_uploader("Adicionar tutorial (opcional)", type=["mp4"]) |
|
if tutorial: |
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp: |
|
tmp.write(tutorial.read()) |
|
st.session_state.tutorial_path = tmp.name |
|
|
|
with st.expander("⚙️ Opções avançadas (zoom, velocidade, música, espelhamento)"): |
|
zoom = st.slider("Zoom (%)", 100, 300, 110, key="zoom") |
|
velocidade_cortes = st.slider("Velocidade dos cortes", 0.8, 2.0, 1.0, key="velocidade_cortes") |
|
velocidade_final = st.slider("Velocidade final do vídeo", 0.8, 2.0, 1.0, key="velocidade_final") |
|
espelhar = st.checkbox("Espelhar vídeo final", value=False, key="espelhar") |
|
|
|
musica = st.file_uploader("Adicionar música (opcional)", type=["mp3"]) |
|
if musica: |
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmpmus: |
|
tmpmus.write(musica.read()) |
|
st.session_state.musica_path = tmpmus.name |
|
else: |
|
st.session_state.musica_path = None |
|
|
|
processar = st.button("Gerar novo vídeo") |
|
|
|
if processar: |
|
st.session_state.processando = True |
|
st.session_state.video_final = None |
|
if st.session_state.processando: |
|
|
|
loader = st.empty() |
|
barra = st.progress(0) |
|
|
|
loader.markdown( |
|
""" |
|
<div style='display:flex; align-items:center; gap:10px; font-size:20px;'> |
|
<span class="loader"></span> <strong>Processando...</strong> |
|
</div> |
|
<style> |
|
.loader { |
|
border: 4px solid #f3f3f3; |
|
border-top: 4px solid #3498db; |
|
border-radius: 50%; |
|
width: 20px; |
|
height: 20px; |
|
animation: spin 1s linear infinite; |
|
} |
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
</style> |
|
""", unsafe_allow_html=True |
|
) |
|
|
|
status = st.empty() |
|
status.markdown("🔎 Analisando duração e resolução do vídeo...") |
|
barra.progress(5) |
|
|
|
|
|
duracao_cmd = [ |
|
"ffprobe", "-v", "error", "-show_entries", |
|
"format=duration", "-of", "default=noprint_wrappers=1:nokey=1", video_path |
|
] |
|
resultado = subprocess.run(duracao_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
dur = float(resultado.stdout.decode().strip()) |
|
dur = int(dur) |
|
|
|
resolucao_cmd = [ |
|
"ffprobe", "-v", "error", "-select_streams", "v:0", |
|
"-show_entries", "stream=width,height", |
|
"-of", "csv=s=x:p=0", video_path |
|
] |
|
resultado = subprocess.run(resolucao_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
largura, altura = resultado.stdout.decode().strip().split("x") |
|
largura = int(largura) |
|
altura = int(altura) |
|
|
|
excluir_inicio = st.session_state.excluir_inicio |
|
excluir_fim = st.session_state.excluir_fim |
|
velocidade_cortes = st.session_state.velocidade_cortes |
|
|
|
barra.progress(10) |
|
status.markdown("🧹 Removendo parte indesejada...") |
|
|
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
parte1 = os.path.join(tmpdir, "parte1.mp4") |
|
parte2 = os.path.join(tmpdir, "parte2.mp4") |
|
video_base = os.path.join(tmpdir, "video_base.mp4") |
|
|
|
if excluir_inicio > 0: |
|
subprocess.call([ |
|
"ffmpeg", "-y", "-i", video_path, |
|
"-t", str(excluir_inicio), |
|
"-c", "copy", |
|
parte1 |
|
]) |
|
if excluir_fim < dur: |
|
subprocess.call([ |
|
"ffmpeg", "-y", "-i", video_path, |
|
"-ss", str(excluir_fim), |
|
"-c", "copy", |
|
parte2 |
|
]) |
|
|
|
with open(os.path.join(tmpdir, "lista_remocao.txt"), "w") as f: |
|
if os.path.exists(parte1): |
|
f.write(f"file '{parte1}'\n") |
|
if os.path.exists(parte2): |
|
f.write(f"file '{parte2}'\n") |
|
|
|
subprocess.call([ |
|
"ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", |
|
os.path.join(tmpdir, "lista_remocao.txt"), |
|
"-c", "copy", |
|
video_base |
|
]) |
|
|
|
barra.progress(15) |
|
status.markdown("✂️ Preparando cortes...") |
|
|
|
cortes = [] |
|
usados = set() |
|
|
|
if st.session_state.embaralhar_cortes: |
|
corte_min = st.session_state.corte_min |
|
corte_max = st.session_state.corte_max |
|
|
|
|
|
resultado = subprocess.run([ |
|
"ffprobe", "-v", "error", "-show_entries", |
|
"format=duration", "-of", |
|
"default=noprint_wrappers=1:nokey=1", video_base |
|
], stdout=subprocess.PIPE) |
|
dur_base = float(resultado.stdout.decode().strip()) |
|
|
|
pos = 0 |
|
while pos < dur_base: |
|
tam_possivel = min(corte_max, dur_base - pos) |
|
if tam_possivel < corte_min: |
|
break |
|
tamanho = random.randint(corte_min, int(tam_possivel)) |
|
fim_corte = pos + tamanho |
|
|
|
if (pos, fim_corte) not in usados: |
|
cortes.append((int(pos), int(fim_corte))) |
|
usados.add((pos, fim_corte)) |
|
pos = fim_corte |
|
|
|
random.shuffle(cortes) |
|
else: |
|
|
|
cortes = [(0, dur)] |
|
barra.progress(25) |
|
|
|
arquivos_cortes = [] |
|
total_cortes = len(cortes) |
|
|
|
for idx, (start, end) in enumerate(cortes): |
|
saida = os.path.join(tmpdir, f"clip_{idx}.mp4") |
|
|
|
subprocess.call([ |
|
"ffmpeg", "-y", "-i", video_base, |
|
"-ss", str(start), "-to", str(end), |
|
"-filter:v", f"setpts=PTS/{velocidade_cortes}", |
|
"-an", |
|
"-c:v", "libx264", "-preset", "veryfast", |
|
saida |
|
]) |
|
|
|
if os.path.exists(saida) and os.path.getsize(saida) > 1000: |
|
arquivos_cortes.append(saida) |
|
|
|
barra.progress(25 + int(((idx + 1) / total_cortes) * 30)) |
|
|
|
if not arquivos_cortes: |
|
barra.empty() |
|
loader.empty() |
|
status.markdown("❌ Nenhum corte foi criado.") |
|
st.stop() |
|
|
|
barra.progress(60) |
|
status.markdown("🔗 Unindo cortes...") |
|
|
|
lista_txt = os.path.join(tmpdir, "lista_cortes.txt") |
|
with open(lista_txt, "w") as f: |
|
for arquivo in arquivos_cortes: |
|
f.write(f"file '{arquivo}'\n") |
|
|
|
video_unido = os.path.join(tmpdir, "video_unido.mp4") |
|
subprocess.call([ |
|
"ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", lista_txt, |
|
"-c:v", "libx264", "-preset", "veryfast", |
|
video_unido |
|
]) |
|
|
|
barra.progress(70) |
|
status.markdown("🔍 Aplicando zoom e espelhamento no vídeo unido...") |
|
|
|
video_processado = os.path.join(tmpdir, "video_processado.mp4") |
|
zoom_factor = st.session_state.zoom / 100 |
|
filtro_zoom = f"scale=iw*{zoom_factor}:ih*{zoom_factor},crop={largura}:{altura}" |
|
|
|
if st.session_state.espelhar: |
|
filtro_zoom += ",hflip" |
|
|
|
subprocess.call([ |
|
"ffmpeg", "-y", "-i", video_unido, |
|
"-vf", filtro_zoom, |
|
"-c:v", "libx264", "-preset", "veryfast", |
|
video_processado |
|
]) |
|
barra.progress(75) |
|
status.markdown("📌 Inserindo tutorial (se houver)...") |
|
|
|
if st.session_state.tutorial_path: |
|
|
|
duracao_cmd = [ |
|
"ffprobe", "-v", "error", "-show_entries", |
|
"format=duration", "-of", |
|
"default=noprint_wrappers=1:nokey=1", video_processado |
|
] |
|
resultado = subprocess.run(duracao_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
duracao_video = float(resultado.stdout.decode().strip()) |
|
|
|
|
|
pos_tutorial = random.uniform(3, duracao_video - 1) |
|
|
|
parte1 = os.path.join(tmpdir, "parte1.mp4") |
|
parte2 = os.path.join(tmpdir, "parte2.mp4") |
|
|
|
subprocess.call([ |
|
"ffmpeg", "-y", "-i", video_processado, |
|
"-t", str(pos_tutorial), |
|
"-c", "copy", |
|
parte1 |
|
]) |
|
|
|
subprocess.call([ |
|
"ffmpeg", "-y", "-i", video_processado, |
|
"-ss", str(pos_tutorial), |
|
"-c", "copy", |
|
parte2 |
|
]) |
|
|
|
tutorial_pad = os.path.join(tmpdir, "tutorial_pad.mp4") |
|
subprocess.call([ |
|
"ffmpeg", "-y", "-i", st.session_state.tutorial_path, |
|
"-vf", f"scale={largura}:{altura}", |
|
"-c:v", "libx264", "-preset", "veryfast", |
|
"-c:a", "aac", |
|
tutorial_pad |
|
]) |
|
|
|
lista_final_txt = os.path.join(tmpdir, "lista_final.txt") |
|
with open(lista_final_txt, "w") as f: |
|
f.write(f"file '{parte1}'\n") |
|
f.write(f"file '{tutorial_pad}'\n") |
|
f.write(f"file '{parte2}'\n") |
|
|
|
video_com_tutorial = os.path.join(tmpdir, "video_com_tutorial.mp4") |
|
subprocess.call([ |
|
"ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", lista_final_txt, |
|
"-c:v", "libx264", "-preset", "veryfast", |
|
video_com_tutorial |
|
]) |
|
else: |
|
video_com_tutorial = video_processado |
|
|
|
barra.progress(80) |
|
status.markdown("🚀 Aplicando velocidade final...") |
|
|
|
video_final_temp = os.path.join(tmpdir, "video_final_temp.mp4") |
|
subprocess.call([ |
|
"ffmpeg", "-y", "-i", video_com_tutorial, |
|
"-filter:v", f"setpts=PTS/{st.session_state.velocidade_final}", |
|
"-c:v", "libx264", "-preset", "veryfast", |
|
video_final_temp |
|
]) |
|
|
|
video_final = os.path.join(tmpdir, "video_final.mp4") |
|
if st.session_state.musica_path: |
|
barra.progress(90) |
|
status.markdown("🎵 Adicionando música...") |
|
subprocess.call([ |
|
"ffmpeg", "-y", "-i", video_final_temp, "-i", st.session_state.musica_path, |
|
"-shortest", |
|
"-c:v", "copy", |
|
"-c:a", "aac", |
|
video_final |
|
]) |
|
else: |
|
os.rename(video_final_temp, video_final) |
|
barra.progress(95) |
|
status.markdown("📋 Adicionando metadata...") |
|
|
|
marcas = { |
|
"samsung": ["SM-S918B", "SM-A546E", "SM-M536B"], |
|
"xiaomi": ["Redmi Note 12", "Poco X5", "Mi 11 Lite"], |
|
"motorola": ["Moto G84", "Moto Edge 40", "Moto E13"], |
|
"apple": ["iPhone 13", "iPhone 14 Pro", "iPhone SE"], |
|
"google": ["Pixel 6", "Pixel 7a"] |
|
} |
|
|
|
marca = random.choice(list(marcas.keys())) |
|
modelo = random.choice(marcas[marca]) |
|
software = f"{marca.capitalize()} Video Editor {random.randint(1,5)}.{random.randint(0,9)}" |
|
latitude = round(random.uniform(-33.0, -2.0), 4) |
|
longitude = round(random.uniform(-60.0, -35.0), 4) |
|
location = f"{latitude:+.4f}{longitude:+.4f}/" |
|
creation_time = (datetime.datetime.now() - datetime.timedelta(days=random.randint(0,5))).strftime("%Y-%m-%dT%H:%M:%SZ") |
|
|
|
video_final_meta = os.path.join(tmpdir, "video_final_meta.mp4") |
|
metadata_cmd = [ |
|
"ffmpeg", "-y", "-i", video_final, |
|
"-metadata", "title=Video Shorts", |
|
"-metadata", "comment=Captured with mobile device", |
|
"-metadata", f"make={marca}", |
|
"-metadata", f"model={modelo}", |
|
"-metadata", f"software={software}", |
|
"-metadata", f"creation_time={creation_time}", |
|
"-metadata", f"location={location}", |
|
"-c", "copy", |
|
video_final_meta |
|
] |
|
subprocess.call(metadata_cmd) |
|
|
|
barra.empty() |
|
loader.empty() |
|
status.empty() |
|
|
|
barra.progress(100) |
|
|
|
video_final_permanente = os.path.join(tempfile.gettempdir(), "video_final_final.mp4") |
|
shutil.copy(video_final_meta, video_final_permanente) |
|
st.session_state.video_final = video_final_permanente |
|
|
|
if st.session_state.video_final: |
|
|
|
st.success("✅ Vídeo criado com sucesso!") |
|
|
|
load_players = st.empty() |
|
load_players.markdown( |
|
""" |
|
<div style='display:flex; align-items:center; gap:10px; font-size:20px; margin-top:20px;'> |
|
<span class="loader"></span> <strong>Aguarde, preparando os players...</strong> |
|
</div> |
|
<style> |
|
.loader { |
|
border: 4px solid #f3f3f3; |
|
border-top: 4px solid #3498db; |
|
border-radius: 50%; |
|
width: 20px; |
|
height: 20px; |
|
animation: spin 1s linear infinite; |
|
} |
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
</style> |
|
""", unsafe_allow_html=True |
|
) |
|
|
|
time.sleep(1) |
|
|
|
st.subheader("🔎 Comparação lado a lado") |
|
st.markdown(f""" |
|
<div style='display: flex; gap: 10px; justify-content: center; align-items: flex-start; flex-wrap: nowrap; overflow-x: auto;'> |
|
<div style='flex: 1; min-width: 45%;'> |
|
<strong>🎬 Original</strong><br> |
|
<video controls style='width: 100%; height: auto;'> |
|
<source src="data:video/mp4;base64,{base64.b64encode(open(video_path, "rb").read()).decode()}" type="video/mp4"> |
|
</video> |
|
</div> |
|
<div style='flex: 1; min-width: 45%;'> |
|
<strong>🎬 Novo</strong><br> |
|
<video controls style='width: 100%; height: auto;'> |
|
<source src="data:video/mp4;base64,{base64.b64encode(open(st.session_state.video_final, "rb").read()).decode()}" type="video/mp4"> |
|
</video> |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
load_players.empty() |
|
|
|
with open(st.session_state.video_final, "rb") as f: |
|
st.download_button("⬇️ Baixar vídeo", f, file_name="video_novo.mp4") |
|
|
|
if st.button("🔄 Criar novo"): |
|
for key in list(st.session_state.keys()): |
|
del st.session_state[key] |
|
st.experimental_rerun() |
|
|
|
if st.button("🔄 Gerar novamente com os mesmos ajustes"): |
|
st.session_state.processando = True |
|
st.session_state.video_final = None |
|
|