import os import re import subprocess import tempfile import shutil import gradio as gr # ===================== CONFIGURAÇÕES ===================== DEFAULT_PADDING_START = 0.12 DEFAULT_PADDING_END = 0.18 CRF = "19" PRESET = "fast" # mude para "veryfast" se quiser mais velocidade class ModoZoom: SEM_ZOOM = "Sem Zoom" ZOOM_FIXO = "Zoom Fixo" ZOOM_ALTERNADO = "Zoom Alternado (Jump-Cut)" def run_ffmpeg(cmd, description=""): """Executa FFmpeg com log de erro visível""" print(f"[FFmpeg] {description}") result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if result.returncode != 0: print(f"[ERRO] {description}") print(result.stderr[-800:]) # mostra os últimos erros raise RuntimeError(f"FFmpeg falhou em: {description}\n{result.stderr[-500:]}") return result def detectar_silencios(video_path: str, noise_threshold_db: int = -30, min_duration: float = 0.5): cmd = [ 'ffmpeg', '-i', video_path, '-vn', '-af', f'silencedetect=noise={noise_threshold_db}dB:d={min_duration}', '-f', 'null', '-' ] result = subprocess.run(cmd, stderr=subprocess.PIPE, text=True) starts = re.findall(r'silence_start: (\d+\.?\d*)', result.stderr) ends = re.findall(r'silence_end: (\d+\.?\d*)', result.stderr) return list(zip(map(float, starts), map(float, ends))) def obter_segmentos_fala(silencios, total_duration, padding_start=0.12, padding_end=0.18): if not silencios: return [(0.0, total_duration)] silencios = sorted(silencios) segmentos = [] cursor = 0.0 for s_start, s_end in silencios: end_fala = max(cursor, s_start - padding_end) if end_fala - cursor > 0.05: # evita segmentos muito curtos segmentos.append((cursor, end_fala)) cursor = max(cursor, s_end + padding_start) if total_duration - cursor > 0.05: segmentos.append((cursor, total_duration)) return segmentos def processar_corte_com_zoom(video_path, silencios, modo_zoom, zoom_intensity, padding_start, padding_end, progress=gr.Progress()): meta = obter_metadados_video(video_path) segmentos = obter_segmentos_fala(silencios, meta['duration'], padding_start, padding_end) temp_dir = tempfile.mkdtemp() clips = [] try: for i, (start, end) in enumerate(segmentos): progress(0.15 + 0.7 * (i / len(segmentos)), f"Processando segmento {i+1}/{len(segmentos)}") zoom = 1.0 if modo_zoom == ModoZoom.ZOOM_FIXO: zoom = zoom_intensity elif modo_zoom == ModoZoom.ZOOM_ALTERNADO: zoom = zoom_intensity if i % 2 == 0 else 1.0 output_clip = os.path.join(temp_dir, f"clip_{i:04d}.mp4") # CORREÇÃO: Utilizando crop e scale estático para evitar corrupção de cabeçalho (moov atom) if zoom > 1.0: vf = f"crop=iw/{zoom}:ih/{zoom},scale=iw:ih,setsar=1" else: vf = "null" cmd = [ 'ffmpeg', '-y', '-ss', f"{start:.4f}", '-to', f"{end:.4f}", '-i', video_path, '-vf', vf, '-c:v', 'libx264', '-crf', CRF, '-preset', PRESET, '-c:a', 'aac', '-b:a', '192k', '-avoid_negative_ts', 'make_zero', output_clip ] run_ffmpeg(cmd, f"Criando clip {i+1}") clips.append(output_clip) # ===================== CONCATENAÇÃO FINAL ===================== progress(0.9, "Concatenando vídeo final...") concat_txt = os.path.join(temp_dir, "concat.txt") with open(concat_txt, "w", encoding="utf-8") as f: for clip in clips: # CORREÇÃO: Tratamento de barras invertidas do Windows (.replace) para evitar falhas de leitura no parser do FFmpeg abs_path = os.path.abspath(clip).replace('\\', '/') f.write(f"file '{abs_path}'\n") output_final = "video_final_editado.mp4" cmd_concat = [ 'ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', concat_txt, '-c', 'copy', output_final ] run_ffmpeg(cmd_concat, "Concatenação final") return output_final finally: shutil.rmtree(temp_dir, ignore_errors=True) # ===================== FUNÇÕES AUXILIARES ===================== def obter_metadados_video(video_path): cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration:stream=r_frame_rate,width,height', '-of', 'json', video_path] result = subprocess.run(cmd, stdout=subprocess.PIPE, text=True, check=True) data = __import__('json').loads(result.stdout) duration = float(data['format']['duration']) stream = next(s for s in data['streams'] if 'width' in s) # CORREÇÃO: Substituído o eval() por uma divisão segura de strings para obter o FPS try: num, den = map(int, stream['r_frame_rate'].split('/')) fps = num / den if den != 0 else 30.0 except Exception: fps = 30.0 return {'duration': duration, 'fps': fps} # ===================== GRADIO ===================== def processar_video(video, threshold, min_duration, zoom_mode, zoom_factor, pad_start, pad_end, progress=gr.Progress()): if not video: return None, "Envie um vídeo primeiro", "Erro" try: progress(0.05, "Detectando silêncios...") silencios = detectar_silencios(video, threshold, min_duration) progress(0.1, "Iniciando corte e zoom...") video_final = processar_corte_com_zoom( video, silencios, zoom_mode, zoom_factor, pad_start, pad_end, progress ) dur_in = obter_metadados_video(video)['duration'] dur_out = obter_metadados_video(video_final)['duration'] relatorio = f""" **✅ Sucesso!** - Duração original: **{dur_in:.1f} segundos** - Duração final: **{dur_out:.1f} segundos** - Silêncios removidos: **{len(silencios)}** - Modo de Zoom: **{zoom_mode}** """ return video_final, relatorio, "Processamento concluído com sucesso!" except Exception as e: return None, f"❌ Erro: {str(e)}", str(e) # ===================== SCRIPT INJETADO (WAKE LOCK & BIPES) ===================== js_code = """ function() { // 1. CONTROLAR TELA ACESA (Wake Lock API) let wakeLock = null; async function solicitarWakeLock() { try { if ('wakeLock' in navigator) { wakeLock = await navigator.wakeLock.request('screen'); console.log('Foco ativo: impedindo a tela de apagar.'); } } catch (err) { console.log('Aviso WakeLock:', err.message); } } solicitarWakeLock(); // Reativa a trava se o usuário alternar abas e retornar ao app document.addEventListener('visibilitychange', () => { if (wakeLock !== null && document.visibilityState === 'visible') { solicitarWakeLock(); } }); // 2. EMISSÃO DE SONS DE AVISO (Web Audio API) window.dispararAlertaSonoro = function() { try { let AudioContext = window.AudioContext || window.webkitAudioContext; if (!AudioContext) return; let ctx = new AudioContext(); // Toca 3 bipes limpos consecutivos com intervalo de 250ms [0, 250, 500].forEach(delay => { setTimeout(() => { let osc = ctx.createOscillator(); let gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(950, ctx.currentTime); // Tom claro de notificação gain.gain.setValueAtTime(0.4, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15); // Suaviza o final do bipe osc.connect(gain); gain.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 0.15); }, delay); }); } catch (e) { console.log('Incapaz de reproduzir alerta sonoro:', e); } }; } """ # Interface Gráfica vinculando o Javascript global ao bloco with gr.Blocks(title="AutoCut Engine - Versão Estável", js=js_code) as demo: gr.Markdown("# 🎬 AutoCut Engine **Versão Corrigida**") with gr.Row(): with gr.Column(scale=5): video_input = gr.Video(label="Vídeo Original") threshold = gr.Slider(-60, -10, -30, step=1, label="Sensibilidade do Silêncio (dB)") min_dur = gr.Slider(0.1, 3.0, 0.5, step=0.1, label="Duração Mínima do Silêncio") zoom_mode = gr.Dropdown([ModoZoom.SEM_ZOOM, ModoZoom.ZOOM_FIXO, ModoZoom.ZOOM_ALTERNADO], value=ModoZoom.ZOOM_ALTERNADO, label="Modo de Zoom") zoom_factor = gr.Slider(1.01, 1.25, 1.08, step=0.01, label="Força do Zoom") with gr.Accordion("Padding", open=False): pad_start = gr.Slider(0.0, 0.5, 0.12, step=0.01, label="Padding Início") pad_end = gr.Slider(0.0, 0.5, 0.18, step=0.01, label="Padding Fim") btn = gr.Button("✂️ Processar Vídeo", variant="primary") with gr.Column(scale=5): video_output = gr.Video(label="Vídeo Final") relatorio_md = gr.Markdown() log = gr.Textbox(label="Log de Erros", interactive=False) # Dispara o processador no backend e, imediatamente na sequência (.then), executa o som do frontend btn.click( processar_video, inputs=[video_input, threshold, min_dur, zoom_mode, zoom_factor, pad_start, pad_end], outputs=[video_output, relatorio_md, log] ).then( fn=None, js="() => { window.dispararAlertaSonoro(); }" ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)