vt / backend /processor.py
Raí Santos
feat: Complete optimization with 3 bugs fixed + backend-only
ac24a29
import os
import whisperx
import torch
import re
import json
import difflib
from docx import Document
import gc
class TranscriptionProcessor:
def __init__(self, device="cpu", model_name="large-v3", compute_type="int8"):
self.device = device
self.model_name = model_name
self.compute_type = compute_type
self.model = None
self.align_model_cache = {}
# Garante diretório de modelos (Caminho absoluto para container)
self.base_dir = os.path.dirname(os.path.abspath(__file__))
self.model_dir = os.path.join(self.base_dir, "models")
os.makedirs(self.model_dir, exist_ok=True)
def load_models(self):
"""Carregamento seguro com gerenciamento de memória"""
if self.model is None:
print(f"[PROCESSOR] Carregando modelo Whisper: {self.model_name} em {self.device}")
# Limpeza prévia
gc.collect()
if self.device == "cuda": torch.cuda.empty_cache()
try:
self.model = whisperx.load_model(
self.model_name,
self.device,
compute_type=self.compute_type,
download_root=self.model_dir
)
print("[PROCESSOR] Modelo Whisper carregado com sucesso.")
except Exception as e:
print(f"[PROCESSOR ERROR] Falha ao carregar modelo: {e}")
raise e
def process_docx(self, file_path):
"""Processamento robusto de DOCX com limpeza de caracteres de controle"""
if not file_path: return ""
print(f"[DOCX] Processando roteiro: {file_path}")
try:
doc = Document(file_path)
full_text = []
for para in doc.paragraphs:
text = para.text.strip()
if text:
# Limpeza de caracteres não-imprimíveis (comum em DOCX vindo do Windows)
text = "".join(char for char in text if char.isprintable() or char == "\n")
# Normalização de hífens para vírgulas conforme solicitado pelo USER
text = re.sub(r'[—–-]\s+', ', ', text)
text = re.sub(r'\s+,\s+', ', ', text)
full_text.append(text)
return " ".join(full_text)
except Exception as e:
print(f"[DOCX ERROR] {e}")
return ""
def transcribe(self, audio_path, language="pt"):
"""Transcrição com sistema de Fallback Blindado"""
try:
self.load_models()
audio = whisperx.load_audio(audio_path)
# Passo 1: Transcrição (Parâmetros mínimos para compatibilidade total)
print("[WHISPER] Transcrevendo...")
result = self.model.transcribe(audio, batch_size=8, language=language)
# Passo 2: Alinhamento (Com Try-Except interno para evitar erro 500)
print("[WHISPER] Alinhando...")
try:
if language not in self.align_model_cache:
self.align_model_cache[language] = whisperx.load_align_model(
language_code=language, device=self.device
)
model_a, metadata = self.align_model_cache[language]
result = whisperx.align(
result["segments"], model_a, metadata, audio, self.device
)
except Exception as align_err:
print(f"[WHISPER WARNING] Falha no alinhamento: {align_err}. Seguindo com transcrição base.")
# Se o alinhamento falhar, 'result' ainda contém os segmentos da transcrição base
# Passo 3: Processamento de palavras com segurança contra chaves ausentes
words = []
for segment in result["segments"]:
# Caso o WhisperX mude o nome da chave entre 'words' e 'word_segments'
word_list = segment.get("words", segment.get("word_segments", []))
for w in word_list:
if "start" in w and "end" in w:
words.append({
"start": round(w["start"], 3),
"end": round(w["end"], 3),
"word": w.get("word", w.get("text", "")).strip()
})
return words
except Exception as e:
print(f"[WHISPER ERROR] {str(e)}")
raise e
def align_with_script(self, audio_words, script_text):
"""
CORREÇÃO INTELIGENTE (VSL BLINDADA):
Compara a transcrição com o roteiro e corrige ortografia/termos técnicos
preservando o tempo do áudio.
"""
if not script_text:
return audio_words
print("[REFINE] Iniciando correção inteligente baseada no roteiro...")
# 1. Preparação das listas (Original e Limpa para matching)
script_raw = script_text.split()
script_clean = [re.sub(r'[^\w]', '', w).lower() for w in script_raw]
audio_clean = [re.sub(r'[^\w]', '', w['word']).lower() for w in audio_words]
# 2. Matching de Sequência
matcher = difflib.SequenceMatcher(None, audio_clean, script_clean)
opcodes = matcher.get_opcodes()
refined_words = []
for tag, i1, i2, j1, j2 in opcodes:
if tag == 'equal':
# Palavras batem: usamos a grafia exata do roteiro (casing/pontuação)
for k in range(i2 - i1):
word_obj = audio_words[i1 + k].copy()
word_obj['word'] = script_raw[j1 + k]
refined_words.append(word_obj)
elif tag == 'replace':
# Caso crítico: setox -> Cetox ou setox31 -> Cetox 31
# Se o número de palavras for diferente, tentamos fundir para manter o tempo
if (i2 - i1) == (j2 - j1):
# 1 para 1
for k in range(i2 - i1):
word_obj = audio_words[i1 + k].copy()
word_obj['word'] = script_raw[j1 + k]
refined_words.append(word_obj)
else:
# M:N (Fusão inteligente)
# Pegamos o tempo do primeiro ao último do bloco e aplicamos o texto do roteiro
new_word_text = " ".join(script_raw[j1:j2])
word_obj = {
"start": audio_words[i1]["start"],
"end": audio_words[i2-1]["end"],
"word": new_word_text
}
refined_words.append(word_obj)
elif tag == 'delete':
# Palavra no áudio mas não no roteiro (ad-lib ou erro): mantemos o áudio
for k in range(i1, i2):
refined_words.append(audio_words[k])
elif tag == 'insert':
# Palavra no roteiro mas não detectada pelo Whisper: ignoramos para não quebrar o tempo
# (Ou poderíamos interpolar, mas para Slides VSL é melhor ignorar)
pass
print(f"[REFINE] Concluído. {len(refined_words)} palavras na saída final.")
return refined_words
def correct_orthography(self, words):
"""Correções rápidas pós-processamento"""
for w in words:
# Limpeza básica de detritos
w["word"] = w["word"].replace(" ,", ",").replace(",,", ",")
return words
def generate_json(self, words):
"""Garante formato JSON estável para o WebApp"""
return {"words": words}