| 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 |
|
|