| import os |
| import re |
| import subprocess |
| import tempfile |
| import shutil |
| import gradio as gr |
|
|
| |
| DEFAULT_PADDING_START = 0.12 |
| DEFAULT_PADDING_END = 0.18 |
| CRF = "19" |
| PRESET = "fast" |
|
|
| 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:]) |
| 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: |
| 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") |
|
|
| |
| 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) |
|
|
| |
| 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: |
| |
| 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) |
|
|
|
|
| |
| 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) |
| |
| |
| 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} |
|
|
|
|
| |
| 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) |
|
|
|
|
| |
| 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); |
| } |
| }; |
| } |
| """ |
|
|
| |
| 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) |
|
|
| |
| 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) |