Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,134 +1,128 @@
|
|
1 |
import gradio as gr
|
2 |
-
import
|
3 |
-
from pydub import AudioSegment
|
4 |
-
from pydub.silence import detect_nonsilent
|
5 |
import tempfile
|
6 |
import os
|
7 |
-
import subprocess
|
8 |
-
from concurrent.futures import ThreadPoolExecutor
|
9 |
-
import shutil
|
10 |
from pathlib import Path
|
|
|
|
|
11 |
|
12 |
-
def
|
13 |
-
"""
|
|
|
14 |
command = [
|
15 |
'ffmpeg', '-i', video_path,
|
16 |
-
'-
|
17 |
-
'-
|
18 |
-
'-ar', '44100', # Sample rate
|
19 |
-
'-ac', '2', # Canais
|
20 |
-
'-y', # Sobrescreve arquivo se existir
|
21 |
-
output_path
|
22 |
]
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
|
25 |
-
def
|
26 |
-
"""
|
27 |
command = [
|
28 |
-
'
|
29 |
-
'-
|
30 |
-
'-
|
31 |
-
|
32 |
-
'-c:a', 'aac', # Codec de áudio
|
33 |
-
'-strict', 'experimental',
|
34 |
-
'-y',
|
35 |
-
output_path
|
36 |
]
|
37 |
-
subprocess.run(command, stderr=subprocess.PIPE)
|
|
|
|
|
38 |
|
39 |
-
def
|
40 |
-
"""
|
41 |
-
|
42 |
-
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
|
45 |
-
def
|
46 |
-
"""
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
|
|
52 |
|
|
|
|
|
|
|
|
|
53 |
command = [
|
54 |
-
'ffmpeg', '-
|
55 |
-
'-
|
56 |
-
'-
|
57 |
-
'-
|
58 |
-
'-c:
|
59 |
-
'-
|
|
|
60 |
'-y',
|
61 |
output_path
|
62 |
]
|
63 |
-
subprocess.run(command, stderr=subprocess.PIPE)
|
64 |
-
os.unlink(list_file.name)
|
65 |
-
|
66 |
-
def process_video(video_path, min_silence_len=1000, silence_thresh=-40, max_workers=4):
|
67 |
-
"""Remove segmentos silenciosos do vídeo com processamento otimizado."""
|
68 |
-
if not os.path.exists(video_path):
|
69 |
-
raise ValueError("Arquivo de vídeo não encontrado")
|
70 |
-
|
71 |
-
temp_dir = tempfile.mkdtemp()
|
72 |
|
73 |
-
|
74 |
-
|
75 |
-
temp_audio = os.path.join(temp_dir, "temp_audio.wav")
|
76 |
-
extract_audio_ffmpeg(video_path, temp_audio)
|
77 |
-
|
78 |
-
# Analisar áudio para detectar silêncio
|
79 |
-
audio = AudioSegment.from_wav(temp_audio)
|
80 |
-
nonsilent_ranges = detect_nonsilent(
|
81 |
-
audio,
|
82 |
-
min_silence_len=min_silence_len,
|
83 |
-
silence_thresh=silence_thresh
|
84 |
-
)
|
85 |
-
|
86 |
-
if not nonsilent_ranges:
|
87 |
-
return video_path
|
88 |
-
|
89 |
-
# Converter para segundos
|
90 |
-
nonsilent_ranges_sec = [(start/1000.0, end/1000.0) for start, end in nonsilent_ranges]
|
91 |
-
|
92 |
-
# Preparar chunks de vídeo
|
93 |
-
chunk_args = []
|
94 |
-
chunk_outputs = []
|
95 |
-
for i, (start, end) in enumerate(nonsilent_ranges_sec):
|
96 |
-
output_chunk = os.path.join(temp_dir, f"chunk_{i}.mp4")
|
97 |
-
chunk_args.append((video_path, output_chunk, start, end))
|
98 |
-
chunk_outputs.append(output_chunk)
|
99 |
-
|
100 |
-
# Processar chunks em paralelo
|
101 |
-
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
102 |
-
list(executor.map(process_video_chunk, chunk_args))
|
103 |
-
|
104 |
-
# Concatenar chunks
|
105 |
-
output_path = os.path.join(temp_dir, "processed_video.mp4")
|
106 |
-
concatenate_videos_ffmpeg(chunk_outputs, output_path)
|
107 |
-
|
108 |
-
# Copiar resultado final
|
109 |
-
final_output = str(Path(video_path).parent / f"processed_{Path(video_path).name}")
|
110 |
-
shutil.copy2(output_path, final_output)
|
111 |
-
|
112 |
-
return final_output
|
113 |
-
|
114 |
-
except Exception as e:
|
115 |
-
raise Exception(f"Erro ao processar vídeo: {str(e)}")
|
116 |
-
finally:
|
117 |
-
shutil.rmtree(temp_dir)
|
118 |
|
119 |
def remove_silence(video_input, silence_duration, silence_threshold):
|
120 |
"""Função para remoção normal de silêncio"""
|
121 |
try:
|
122 |
if video_input is None:
|
123 |
raise ValueError("Por favor, faça upload de um vídeo")
|
124 |
-
|
125 |
-
|
126 |
video_input,
|
127 |
-
|
128 |
-
|
129 |
)
|
130 |
-
|
131 |
-
return processed_video
|
132 |
except Exception as e:
|
133 |
gr.Error(str(e))
|
134 |
return None
|
@@ -138,15 +132,12 @@ def remove_max_silence(video_input):
|
|
138 |
try:
|
139 |
if video_input is None:
|
140 |
raise ValueError("Por favor, faça upload de um vídeo")
|
141 |
-
|
142 |
-
|
143 |
-
processed_video = process_video(
|
144 |
video_input,
|
145 |
-
|
146 |
-
|
147 |
)
|
148 |
-
|
149 |
-
return processed_video
|
150 |
except Exception as e:
|
151 |
gr.Error(str(e))
|
152 |
return None
|
@@ -162,10 +153,7 @@ with gr.Blocks(title="Removedor de Silêncio de Vídeos") as app:
|
|
162 |
)
|
163 |
|
164 |
with gr.Row():
|
165 |
-
# Botão para remoção máxima de silêncio
|
166 |
remove_max_btn = gr.Button("🔇 Remover 100% do Silêncio", variant="primary")
|
167 |
-
|
168 |
-
# Botão para remoção personalizada
|
169 |
remove_custom_btn = gr.Button("Remover Silêncio Personalizado")
|
170 |
|
171 |
with gr.Group():
|
|
|
1 |
import gradio as gr
|
2 |
+
import subprocess
|
|
|
|
|
3 |
import tempfile
|
4 |
import os
|
|
|
|
|
|
|
5 |
from pathlib import Path
|
6 |
+
import json
|
7 |
+
import shutil
|
8 |
|
9 |
+
def detect_silence_ffmpeg(video_path, silence_thresh=-40, min_silence_len=1):
|
10 |
+
"""Detecta silêncio usando FFmpeg diretamente, muito mais rápido que pydub"""
|
11 |
+
|
12 |
command = [
|
13 |
'ffmpeg', '-i', video_path,
|
14 |
+
'-af', f'silencedetect=noise={silence_thresh}dB:d={min_silence_len}',
|
15 |
+
'-f', 'null', '-'
|
|
|
|
|
|
|
|
|
16 |
]
|
17 |
+
|
18 |
+
# Executa FFmpeg e captura a saída de erro (onde está a informação do silêncio)
|
19 |
+
result = subprocess.run(command, stderr=subprocess.PIPE, text=True)
|
20 |
+
|
21 |
+
# Processa a saída para encontrar timestamps
|
22 |
+
silence_data = []
|
23 |
+
start_times = []
|
24 |
+
end_times = []
|
25 |
+
|
26 |
+
for line in result.stderr.split('\n'):
|
27 |
+
if 'silence_start' in line:
|
28 |
+
start_time = float(line.split('silence_start: ')[1].split()[0])
|
29 |
+
start_times.append(start_time)
|
30 |
+
elif 'silence_end' in line:
|
31 |
+
end_time = float(line.split('silence_end: ')[1].split()[0])
|
32 |
+
end_times.append(end_time)
|
33 |
+
|
34 |
+
# Cria lista de intervalos não silenciosos
|
35 |
+
if not start_times:
|
36 |
+
return []
|
37 |
+
|
38 |
+
nonsilent_ranges = []
|
39 |
+
video_duration = float(get_video_duration(video_path))
|
40 |
+
|
41 |
+
# Adiciona segmento do início até o primeiro silêncio
|
42 |
+
if start_times[0] > 0:
|
43 |
+
nonsilent_ranges.append((0, start_times[0]))
|
44 |
+
|
45 |
+
# Adiciona segmentos entre silêncios
|
46 |
+
for i in range(len(end_times)):
|
47 |
+
if i < len(start_times):
|
48 |
+
nonsilent_ranges.append((end_times[i], start_times[i]))
|
49 |
+
|
50 |
+
# Adiciona segmento final se necessário
|
51 |
+
if end_times and end_times[-1] < video_duration:
|
52 |
+
nonsilent_ranges.append((end_times[-1], video_duration))
|
53 |
+
|
54 |
+
return nonsilent_ranges
|
55 |
|
56 |
+
def get_video_duration(video_path):
|
57 |
+
"""Obtém a duração do vídeo usando FFmpeg"""
|
58 |
command = [
|
59 |
+
'ffprobe', '-v', 'error',
|
60 |
+
'-show_entries', 'format=duration',
|
61 |
+
'-of', 'json',
|
62 |
+
video_path
|
|
|
|
|
|
|
|
|
63 |
]
|
64 |
+
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
65 |
+
data = json.loads(result.stdout)
|
66 |
+
return float(data['format']['duration'])
|
67 |
|
68 |
+
def create_filter_complex(ranges):
|
69 |
+
"""Cria o filtro complexo para FFmpeg baseado nos intervalos não silenciosos"""
|
70 |
+
parts = []
|
71 |
+
for i, (start, end) in enumerate(ranges):
|
72 |
+
parts.append(f"[0:v]trim=start={start}:end={end},setpts=PTS-STARTPTS[v{i}]; "
|
73 |
+
f"[0:a]atrim=start={start}:end={end},asetpts=PTS-STARTPTS[a{i}]")
|
74 |
+
|
75 |
+
# Concatena os vídeos
|
76 |
+
v_list = ''.join(f'[v{i}]' for i in range(len(ranges)))
|
77 |
+
a_list = ''.join(f'[a{i}]' for i in range(len(ranges)))
|
78 |
+
|
79 |
+
concat = f"; {v_list}concat=n={len(ranges)}:v=1:a=0[vout]; "
|
80 |
+
concat += f"{a_list}concat=n={len(ranges)}:v=0:a=1[aout]"
|
81 |
+
|
82 |
+
return ''.join(parts) + concat
|
83 |
|
84 |
+
def process_video_fast(video_path, silence_thresh=-40, min_silence_len=1):
|
85 |
+
"""Processa o vídeo removendo silêncio usando FFmpeg diretamente"""
|
86 |
+
|
87 |
+
# Detecta intervalos não silenciosos
|
88 |
+
nonsilent_ranges = detect_silence_ffmpeg(video_path, silence_thresh, min_silence_len)
|
89 |
+
|
90 |
+
if not nonsilent_ranges:
|
91 |
+
return video_path
|
92 |
+
|
93 |
+
# Cria arquivo de saída
|
94 |
+
output_path = str(Path(video_path).parent / f"processed_{Path(video_path).name}")
|
95 |
|
96 |
+
# Cria filtro complexo
|
97 |
+
filter_complex = create_filter_complex(nonsilent_ranges)
|
98 |
+
|
99 |
+
# Processa o vídeo em uma única passagem
|
100 |
command = [
|
101 |
+
'ffmpeg', '-i', video_path,
|
102 |
+
'-filter_complex', filter_complex,
|
103 |
+
'-map', '[vout]',
|
104 |
+
'-map', '[aout]',
|
105 |
+
'-c:v', 'libx264',
|
106 |
+
'-preset', 'ultrafast', # Mais rápido encoding
|
107 |
+
'-c:a', 'aac',
|
108 |
'-y',
|
109 |
output_path
|
110 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
111 |
|
112 |
+
subprocess.run(command, stderr=subprocess.PIPE)
|
113 |
+
return output_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
|
115 |
def remove_silence(video_input, silence_duration, silence_threshold):
|
116 |
"""Função para remoção normal de silêncio"""
|
117 |
try:
|
118 |
if video_input is None:
|
119 |
raise ValueError("Por favor, faça upload de um vídeo")
|
120 |
+
|
121 |
+
return process_video_fast(
|
122 |
video_input,
|
123 |
+
silence_thresh=silence_threshold,
|
124 |
+
min_silence_len=silence_duration
|
125 |
)
|
|
|
|
|
126 |
except Exception as e:
|
127 |
gr.Error(str(e))
|
128 |
return None
|
|
|
132 |
try:
|
133 |
if video_input is None:
|
134 |
raise ValueError("Por favor, faça upload de um vídeo")
|
135 |
+
|
136 |
+
return process_video_fast(
|
|
|
137 |
video_input,
|
138 |
+
silence_thresh=-30,
|
139 |
+
min_silence_len=0.1
|
140 |
)
|
|
|
|
|
141 |
except Exception as e:
|
142 |
gr.Error(str(e))
|
143 |
return None
|
|
|
153 |
)
|
154 |
|
155 |
with gr.Row():
|
|
|
156 |
remove_max_btn = gr.Button("🔇 Remover 100% do Silêncio", variant="primary")
|
|
|
|
|
157 |
remove_custom_btn = gr.Button("Remover Silêncio Personalizado")
|
158 |
|
159 |
with gr.Group():
|