Spaces:
Sleeping
Sleeping
import gradio as gr | |
import subprocess | |
import tempfile | |
import os | |
from pathlib import Path | |
import json | |
import shutil | |
def detect_silence_ffmpeg(video_path, silence_thresh=-40, min_silence_len=1): | |
"""Detecta silêncio usando FFmpeg diretamente, muito mais rápido que pydub""" | |
command = [ | |
'ffmpeg', '-i', video_path, | |
'-af', f'silencedetect=noise={silence_thresh}dB:d={min_silence_len}', | |
'-f', 'null', '-' | |
] | |
# Executa FFmpeg e captura a saída de erro (onde está a informação do silêncio) | |
result = subprocess.run(command, stderr=subprocess.PIPE, text=True) | |
# Processa a saída para encontrar timestamps | |
silence_data = [] | |
start_times = [] | |
end_times = [] | |
for line in result.stderr.split('\n'): | |
if 'silence_start' in line: | |
start_time = float(line.split('silence_start: ')[1].split()[0]) | |
start_times.append(start_time) | |
elif 'silence_end' in line: | |
end_time = float(line.split('silence_end: ')[1].split()[0]) | |
end_times.append(end_time) | |
# Cria lista de intervalos não silenciosos | |
if not start_times: | |
return [] | |
nonsilent_ranges = [] | |
video_duration = float(get_video_duration(video_path)) | |
# Adiciona segmento do início até o primeiro silêncio | |
if start_times[0] > 0: | |
nonsilent_ranges.append((0, start_times[0])) | |
# Adiciona segmentos entre silêncios | |
for i in range(len(end_times)): | |
if i < len(start_times): | |
nonsilent_ranges.append((end_times[i], start_times[i])) | |
# Adiciona segmento final se necessário | |
if end_times and end_times[-1] < video_duration: | |
nonsilent_ranges.append((end_times[-1], video_duration)) | |
return nonsilent_ranges | |
def get_video_duration(video_path): | |
"""Obtém a duração do vídeo usando FFmpeg""" | |
command = [ | |
'ffprobe', '-v', 'error', | |
'-show_entries', 'format=duration', | |
'-of', 'json', | |
video_path | |
] | |
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
data = json.loads(result.stdout) | |
return float(data['format']['duration']) | |
def create_filter_complex(ranges): | |
"""Cria o filtro complexo para FFmpeg baseado nos intervalos não silenciosos""" | |
parts = [] | |
for i, (start, end) in enumerate(ranges): | |
parts.append(f"[0:v]trim=start={start}:end={end},setpts=PTS-STARTPTS[v{i}]; " | |
f"[0:a]atrim=start={start}:end={end},asetpts=PTS-STARTPTS[a{i}]") | |
# Concatena os vídeos | |
v_list = ''.join(f'[v{i}]' for i in range(len(ranges))) | |
a_list = ''.join(f'[a{i}]' for i in range(len(ranges))) | |
concat = f"; {v_list}concat=n={len(ranges)}:v=1:a=0[vout]; " | |
concat += f"{a_list}concat=n={len(ranges)}:v=0:a=1[aout]" | |
return ''.join(parts) + concat | |
def process_video_fast(video_path, silence_thresh=-40, min_silence_len=1): | |
"""Processa o vídeo removendo silêncio usando FFmpeg diretamente""" | |
# Detecta intervalos não silenciosos | |
nonsilent_ranges = detect_silence_ffmpeg(video_path, silence_thresh, min_silence_len) | |
if not nonsilent_ranges: | |
return video_path | |
# Cria arquivo de saída | |
output_path = str(Path(video_path).parent / f"processed_{Path(video_path).name}") | |
# Cria filtro complexo | |
filter_complex = create_filter_complex(nonsilent_ranges) | |
# Processa o vídeo em uma única passagem | |
command = [ | |
'ffmpeg', '-i', video_path, | |
'-filter_complex', filter_complex, | |
'-map', '[vout]', | |
'-map', '[aout]', | |
'-c:v', 'libx264', | |
'-preset', 'ultrafast', # Mais rápido encoding | |
'-c:a', 'aac', | |
'-y', | |
output_path | |
] | |
subprocess.run(command, stderr=subprocess.PIPE) | |
return output_path | |
def remove_silence(video_input, silence_duration, silence_threshold): | |
"""Função para remoção normal de silêncio""" | |
try: | |
if video_input is None: | |
raise ValueError("Por favor, faça upload de um vídeo") | |
return process_video_fast( | |
video_input, | |
silence_thresh=silence_threshold, | |
min_silence_len=silence_duration | |
) | |
except Exception as e: | |
gr.Error(str(e)) | |
return None | |
def remove_max_silence(video_input): | |
"""Função para remoção máxima de silêncio""" | |
try: | |
if video_input is None: | |
raise ValueError("Por favor, faça upload de um vídeo") | |
return process_video_fast( | |
video_input, | |
silence_thresh=-30, | |
min_silence_len=0.1 | |
) | |
except Exception as e: | |
gr.Error(str(e)) | |
return None | |
# Interface Gradio | |
with gr.Blocks(title="Removedor de Silêncio de Vídeos") as app: | |
gr.Markdown("# Removedor de Silêncio de Vídeos") | |
with gr.Row(): | |
with gr.Column(): | |
video_input = gr.Video( | |
label="Selecione ou Arraste o Vídeo" | |
) | |
with gr.Row(): | |
remove_max_btn = gr.Button("🔇 Remover 100% do Silêncio", variant="primary") | |
remove_custom_btn = gr.Button("Remover Silêncio Personalizado") | |
with gr.Group(): | |
gr.Markdown("### Configurações Personalizadas") | |
silence_duration = gr.Slider( | |
minimum=0.1, | |
maximum=5.0, | |
value=1.0, | |
step=0.1, | |
label="Duração Mínima do Silêncio (segundos)" | |
) | |
silence_threshold = gr.Slider( | |
minimum=-60, | |
maximum=-20, | |
value=-40, | |
step=1, | |
label="Limite de Silêncio (dB)" | |
) | |
with gr.Row(): | |
video_output = gr.Video(label="Vídeo Processado") | |
# Event handlers | |
remove_max_btn.click( | |
fn=remove_max_silence, | |
inputs=[video_input], | |
outputs=[video_output] | |
) | |
remove_custom_btn.click( | |
fn=remove_silence, | |
inputs=[ | |
video_input, | |
silence_duration, | |
silence_threshold | |
], | |
outputs=[video_output] | |
) | |
if __name__ == "__main__": | |
app.launch(show_error=True) |