| 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.") |