| | import streamlit as st |
| | import os |
| | import subprocess |
| | import tempfile |
| | import shutil |
| | import base64 |
| | import random |
| | import string |
| | from datetime import datetime, timedelta |
| |
|
| | st.set_page_config(page_title="Turbo Spinner Shorts PRO", layout="centered") |
| | st.title("🎬 Turbo Spinner Shorts PRO (TikTok Safe)") |
| |
|
| | |
| | temp_dir = tempfile.mkdtemp() |
| |
|
| | uploaded_file = st.file_uploader("📤 Envie seu vídeo", type=["mp4", "mov", "avi", "mkv"]) |
| |
|
| | def run_ffmpeg(cmd, label, output_file): |
| | progress_bar = st.progress(0, text=f"⏳ {label} em andamento...") |
| | process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) |
| | percent = 0 |
| | while process.poll() is None: |
| | if percent < 100: |
| | percent += 2 |
| | progress_bar.progress(percent, text=f"⏳ {label} {percent}%") |
| | process.wait() |
| | progress_bar.progress(100, text=f"✅ {label} finalizado!") |
| | return os.path.exists(output_file) and os.path.getsize(output_file) > 0 |
| |
|
| | def make_download_button(file_path, label): |
| | with open(file_path, "rb") as f: |
| | b64 = base64.b64encode(f.read()).decode() |
| | href = f'<a href="data:file/mp4;base64,{b64}" download="{os.path.basename(file_path)}">⬇️ {label}</a>' |
| | st.markdown(href, unsafe_allow_html=True) |
| |
|
| | def random_string(n=6): |
| | return ''.join(random.choices(string.ascii_lowercase + string.digits, k=n)) |
| |
|
| | def gerar_nome_saida(): |
| | agora = datetime.now() |
| | data = agora.strftime("%Y%m%d") |
| | hora = agora.strftime("%H%M%S") |
| | return f"lv_0_{data}{hora}.mp4" |
| |
|
| | def get_duration(video_path): |
| | try: |
| | result = subprocess.run( |
| | ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', |
| | '-of', 'default=noprint_wrappers=1:nokey=1', video_path], |
| | stdout=subprocess.PIPE, |
| | stderr=subprocess.STDOUT |
| | ) |
| | return float(result.stdout) |
| | except Exception: |
| | return 15.0 |
| |
|
| | def gerar_microdrift(dur, base_speed=1.0): |
| | expr = f"{1/base_speed}*PTS" |
| | for _ in range(2): |
| | start = round(random.uniform(0, max(1, dur-2)), 2) |
| | end = round(start + random.uniform(0.5, 2), 2) |
| | if end > dur: |
| | end = dur |
| | factor = random.choice([0.98, 0.99, 1.01, 1.02]) |
| | expr = f"if(between(T,{start},{end}),{factor}*{expr},{expr})" |
| | return expr |
| |
|
| | if uploaded_file is not None: |
| | input_path = os.path.join(temp_dir, uploaded_file.name) |
| | with open(input_path, "wb") as f: |
| | f.write(uploaded_file.getbuffer()) |
| | |
| | st.success("✅ Vídeo carregado com sucesso!") |
| |
|
| | dur = get_duration(input_path) |
| |
|
| | st.subheader("🚀 Funções Avançadas") |
| |
|
| | opt_semantic_noise = st.checkbox("Injeção de Ruído Semântico") |
| | opt_dynamic_frame = st.checkbox("Molduras Dinâmicas") |
| |
|
| | st.subheader("✂️ Corte do Vídeo") |
| |
|
| | cut_start = st.slider( |
| | "Cortar do início (segundos)", |
| | 0.0, |
| | float(max(0.0, dur - 1)), |
| | 0.0, |
| | 0.1 |
| | ) |
| |
|
| | cut_end = st.slider( |
| | "Cortar do final (segundos)", |
| | 0.0, |
| | float(max(0.0, dur - cut_start - 1)), |
| | 0.0, |
| | 0.1 |
| | ) |
| |
|
| | st.subheader("🎵 Pitch Shift") |
| |
|
| | enable_pitch = st.checkbox("Ativar Pitch Shift") |
| | pitch_value = st.slider( |
| | "Tom do áudio", |
| | 0.8, |
| | 1.2, |
| | 1.0, |
| | 0.01 |
| | ) |
| |
|
| | res = st.radio("📱 Resolução final", ["720x1280", "1080x1920"], index=0) |
| | w, h = res.split("x") |
| |
|
| | preset = st.radio("⚡ Escolha um preset:", ["Low", "Médio", "High"], index=1) |
| |
|
| | preset_defaults = { |
| | "Low": { |
| | "speed_auto": False, "zoom_auto": False, "tilt": False, "eq": False, |
| | "bw": False, "sepia": False, "sharpen": False, "fade": False, "border": False |
| | }, |
| | "Médio": { |
| | "speed_auto": True, "zoom_auto": True, "tilt": False, "eq": True, |
| | "bw": False, "sepia": False, "sharpen": True, "fade": True, "border": False |
| | }, |
| | "High": { |
| | "speed_auto": True, "zoom_auto": True, "tilt": True, "eq": True, |
| | "bw": True, "sepia": True, "sharpen": True, "fade": True, "border": True |
| | } |
| | } |
| |
|
| | defaults = preset_defaults[preset] |
| |
|
| | st.subheader("🎨 Efeitos ativos") |
| |
|
| | speed_val = st.slider("⏩ Velocidade base", 0.5, 2.0, 1.0, 0.05) |
| | opt_speed_auto = st.checkbox("Velocidade automática", value=defaults["speed_auto"]) |
| |
|
| | zoom_val = st.slider("🔍 Zoom base", 1.0, 1.5, 1.0, 0.01) |
| | opt_zoom_auto = st.checkbox("Zoom automático", value=defaults["zoom_auto"]) |
| |
|
| | opt_tilt = st.checkbox("Tilt (ângulo leve)", value=defaults["tilt"]) |
| | opt_eq = st.checkbox("Brilho / Contraste / Saturação", value=defaults["eq"]) |
| | opt_bw = st.checkbox("Filtro de desaturação leve", value=defaults["bw"]) |
| | opt_sepia = st.checkbox("Filtro sépia suave", value=defaults["sepia"]) |
| | opt_sharpen = st.checkbox("Sharpen (nitidez extra)", value=defaults["sharpen"]) |
| | opt_fade = st.checkbox("Fade in/out (máx 1s)", value=defaults["fade"]) |
| | opt_border = st.checkbox("Borda preta", value=defaults["border"]) |
| |
|
| | hard_mode = st.checkbox("Ativar randomização HARD (reescrita de metadados)") |
| |
|
| | if st.button("🎲 Gerar Spin"): |
| |
|
| | out_name = gerar_nome_saida() |
| | out_path = os.path.join(temp_dir, out_name) |
| | first_pass = os.path.join(temp_dir, "first_pass.mp4") |
| |
|
| | effects = [] |
| |
|
| | expr = gerar_microdrift(dur, base_speed=speed_val) |
| | effects.append(f"setpts='{expr}'") |
| |
|
| | if opt_zoom_auto: |
| | zoom_choice = round(random.uniform(1.01, 1.25), 2) |
| | effects.append(f"scale=iw*{zoom_choice}:ih*{zoom_choice},crop={w}:{h}") |
| | elif zoom_val > 1.0: |
| | effects.append(f"scale=iw*{zoom_val}:ih*{zoom_val},crop={w}:{h}") |
| | else: |
| | effects.append(f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h}") |
| |
|
| | if opt_tilt: |
| | tilt = random.choice([1, 2, -1, -2]) |
| | effects.append(f"rotate={tilt}*PI/180:ow={w}:oh={h}:c=black") |
| |
|
| | if opt_eq: |
| | effects.append( |
| | f"eq=brightness={round(random.uniform(-0.1,0.1),2)}:" |
| | f"contrast={round(random.uniform(0.9,1.1),2)}:" |
| | f"saturation={round(random.uniform(0.9,1.1),2)}" |
| | ) |
| |
|
| | if opt_bw: |
| | effects.append("hue=s=0.7") |
| |
|
| | if opt_sepia: |
| | effects.append("colorchannelmixer=.85:.3:.15:0:.3:.85:.15:0:.2:.3:.7") |
| |
|
| | if opt_sharpen: |
| | effects.append("unsharp=5:5:1.0:5:5:0.0") |
| |
|
| | if opt_fade: |
| | fade_in = min(1, dur * 0.1) |
| | fade_out = min(1, dur * 0.1) |
| | effects.append( |
| | f"fade=t=in:st=0:d={fade_in}," |
| | f"fade=t=out:st={max(dur-fade_out,0)}:d={fade_out}" |
| | ) |
| |
|
| | if opt_border: |
| | effects.append(f"pad={w}:{h}:(ow-iw)/2:(oh-ih)/2:black") |
| |
|
| | effects.append("setsar=1") |
| |
|
| | |
| | if opt_semantic_noise: |
| | effects.append("noise=alls=3:allf=t") |
| | effects.append( |
| | f"eq=brightness={round(random.uniform(-0.02,0.02),3)}:" |
| | f"contrast={round(random.uniform(0.98,1.02),3)}:" |
| | f"saturation={round(random.uniform(0.98,1.02),3)}" |
| | ) |
| | crop_w = int(w) - random.randint(0, 3) |
| | crop_h = int(h) - random.randint(0, 3) |
| | effects.append(f"crop={crop_w}:{crop_h}") |
| |
|
| | |
| | if opt_dynamic_frame: |
| | emoji = random.choice(["🔥", "⚡", "✨", "👀", "🚀"]) |
| | x_pos = random.randint(20, int(w) - 80) |
| | y_pos = random.randint(20, int(h) - 80) |
| |
|
| | effects.append( |
| | f"drawtext=text='{emoji}':" |
| | f"x={x_pos}:y={y_pos}:" |
| | f"fontsize=40:" |
| | f"fontcolor=white@0.3" |
| | ) |
| |
|
| | effects.append(f"pad={w}:{h}:(ow-iw)/2:(oh-ih)/2:black@0.3") |
| |
|
| | vf = ",".join(effects) |
| |
|
| | final_duration = dur - cut_start - cut_end |
| | if final_duration <= 0: |
| | st.error("❌ Corte inválido.") |
| | st.stop() |
| |
|
| | if enable_pitch and pitch_value != 1.0: |
| | af_filter = f"asetrate=48000*{pitch_value},aresample=48000" |
| | else: |
| | af_filter = "aresample=48000" |
| |
|
| | cmd1 = [ |
| | "ffmpeg", |
| | "-y", |
| | "-ss", str(cut_start), |
| | "-i", input_path, |
| | "-t", str(final_duration), |
| | "-vf", vf, |
| | "-af", af_filter, |
| | "-c:v", "libx264", |
| | "-pix_fmt", "yuv420p", |
| | "-preset", "fast", |
| | "-crf", "23", |
| | "-c:a", "aac", |
| | "-b:a", "128k", |
| | "-shortest", |
| | "-vsync", "vfr", |
| | first_pass |
| | ] |
| |
|
| | success1 = run_ffmpeg(cmd1, "Efeitos", first_pass) |
| |
|
| | if not success1: |
| | st.error("❌ Erro no processamento inicial.") |
| | else: |
| | if not hard_mode: |
| | shutil.move(first_pass, out_path) |
| | st.success("🎉 Spin gerado com sucesso!") |
| | st.video(out_path) |
| | make_download_button(out_path, "Download Spin") |
| | else: |
| | crf = str(random.randint(18, 26)) |
| | encoder_name = random_string(12) |
| |
|
| | cmd2 = [ |
| | "ffmpeg", |
| | "-y", |
| | "-i", first_pass, |
| | "-map_metadata", "-1", |
| | "-c:v", "libx264", |
| | "-preset", "fast", |
| | "-crf", crf, |
| | "-c:a", "aac", |
| | "-movflags", "faststart", |
| | "-metadata", f"title={random_string(10)}", |
| | "-metadata", f"encoder={encoder_name}", |
| | out_path |
| | ] |
| |
|
| | success2 = run_ffmpeg(cmd2, "Finalizando Spin", out_path) |
| |
|
| | if success2: |
| | st.success("🎉 Spin gerado com randomização HARD!") |
| | st.video(out_path) |
| | make_download_button(out_path, "Download Spin") |
| | else: |
| | st.error("❌ Erro no passo final.") |
| |
|
| | if st.button("🗑️ Limpar temporários"): |
| | shutil.rmtree(temp_dir) |
| | st.warning("Arquivos temporários foram apagados.") |