TTS-Rapido / utils.py
RafaG's picture
Upload 4 files
b7f3430 verified
# utils.py
import os
import subprocess
from pathlib import Path
from pydub import AudioSegment
from pydub.silence import split_on_silence
import pysrt
from tqdm import tqdm
import asyncio
def remove_silence(input_file, output_file):
"""Lê um arquivo MP3, remove o silêncio e salva como MP3 com alta qualidade, mantendo pequenas pausas."""
audio = AudioSegment.from_mp3(input_file)
segments = split_on_silence(
audio,
min_silence_len=500,
silence_thresh=-40,
keep_silence=250
)
non_silent_audio = AudioSegment.silent(duration=0)
for segment in segments:
non_silent_audio += segment
non_silent_audio.export(output_file, format="mp3", bitrate="192k")
def timetoms(time_obj):
"""Converte um objeto de tempo do Pysrt para milissegundos."""
return time_obj.hours * 3600000 + time_obj.minutes * 60000 + time_obj.seconds * 1000 + time_obj.milliseconds
# --- VERSÃO COMPLETAMENTE NOVA E ROBUSTA ---
async def adjust_audio_speed(input_file, output_file, target_duration_ms):
"""Ajusta a velocidade do áudio usando o filtro 'atempo' do FFmpeg para máxima qualidade."""
# Usa ffprobe para obter a duração exata, é mais confiável que pydub
try:
probe_cmd = [
"ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", input_file
]
result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
original_duration_ms = float(result.stdout.strip()) * 1000
except (subprocess.CalledProcessError, FileNotFoundError):
# Fallback para pydub se ffprobe não estiver disponível ou falhar
original_duration_ms = len(AudioSegment.from_mp3(input_file))
if original_duration_ms == 0 or target_duration_ms <= 0:
silent_audio = AudioSegment.silent(duration=target_duration_ms)
silent_audio.export(output_file, format="mp3", bitrate="192k")
return silent_audio
speed_factor = original_duration_ms / target_duration_ms
# Se a velocidade já for quase perfeita, apenas renomeia para evitar re-compressão
if 0.99 < speed_factor < 1.01:
Path(input_file).rename(output_file)
return AudioSegment.from_mp3(output_file)
# Constrói a cadeia de filtros 'atempo'
atempo_filters = []
current_factor = speed_factor
# Para aceleração > 2.0x
while current_factor > 2.0:
atempo_filters.append("atempo=2.0")
current_factor /= 2.0
# Para desaceleração < 0.5x
while current_factor < 0.5:
atempo_filters.append("atempo=0.5")
current_factor /= 0.5
# Adiciona o fator final (que agora está entre 0.5 e 2.0)
if current_factor != 1.0:
atempo_filters.append(f"atempo={current_factor:.5f}")
filter_string = ",".join(atempo_filters)
# Executa o comando FFmpeg
ffmpeg_cmd = [
"ffmpeg", "-y", "-i", input_file, "-filter:a", filter_string,
"-b:a", "192k", "-ar", "44100", # Define bitrate e sample rate de alta qualidade
"-hide_banner", "-loglevel", "error", output_file
]
try:
# Roda o subprocesso bloqueante em uma thread separada para não congelar a UI
proc = await asyncio.create_subprocess_exec(
*ffmpeg_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
print(f"Erro no FFmpeg ao ajustar a velocidade: {stderr.decode()}")
# Em caso de erro, cria silêncio para não quebrar o processo
silent = AudioSegment.silent(duration=target_duration_ms)
silent.export(output_file, format="mp3")
except FileNotFoundError:
print("ERRO: FFmpeg não encontrado. Verifique se ele está instalado e no PATH do sistema.")
raise
return AudioSegment.from_mp3(output_file)
async def merge_audio_files(output_folder, srt_file_path):
"""Mescla segmentos de áudio baseados nos tempos de um arquivo SRT com sincronização correta."""
subs = pysrt.open(srt_file_path)
final_audio = AudioSegment.silent(duration=0)
base_name = Path(srt_file_path).stem
with tqdm(total=len(subs), desc=f"Mesclando áudios para {base_name}", unit="segmento") as pbar:
for sub in subs:
start_time_ms = timetoms(sub.start)
end_time_ms = timetoms(sub.end)
audio_file = Path(output_folder) / f"{sub.index:02d}.mp3"
silence_duration = start_time_ms - len(final_audio)
if silence_duration > 5: # Adiciona uma pequena margem para evitar micro-silêncios
final_audio += AudioSegment.silent(duration=silence_duration)
if audio_file.exists() and audio_file.stat().st_size > 0:
audio_segment = AudioSegment.from_mp3(str(audio_file))
final_audio += audio_segment
else:
segment_duration = end_time_ms - start_time_ms
final_audio += AudioSegment.silent(duration=max(0, segment_duration))
pbar.update(1)
srt_output_dir = Path("output/srt_output")
srt_output_dir.mkdir(parents=True, exist_ok=True)
output_file_path = srt_output_dir / f"{base_name}_final.mp3"
final_audio.export(str(output_file_path), format="mp3", bitrate="192k")
print(f"\nÁudio final salvo em: {output_file_path}\n")
return str(output_file_path)
def listar_audios():
"""Lista os arquivos de áudio na pasta de saída do SRT."""
try:
srt_output_dir = "output/srt_output"
if not os.path.exists(srt_output_dir):
os.makedirs(srt_output_dir, exist_ok=True)
return ["Nenhum áudio gerado ainda"]
arquivos = [f for f in os.listdir(srt_output_dir) if f.endswith(('.mp3', '.wav'))]
return arquivos if arquivos else ["Nenhum áudio gerado ainda"]
except Exception as e:
print(f"Erro ao listar áudios: {e}")
return ["Erro ao listar arquivos"]
def tocar_audio(arquivo):
"""Retorna o caminho completo para um arquivo de áudio selecionado para tocar."""
if arquivo and arquivo != "Nenhum áudio gerado ainda":
return f"output/srt_output/{arquivo}"
return None