autocut / app.py
Dan-22's picture
Update app.py
4784592 verified
Raw
History Blame Contribute Delete
10.2 kB
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)