Raí Santos commited on
Commit ·
9a8fc49
1
Parent(s): 0078030
feat: Complete optimization with 3 bugs fixed + backend-only
Browse files- backend/main.py +5 -0
- backend/processor.py +73 -11
- google_colab/colab_app.py +146 -111
backend/main.py
CHANGED
|
@@ -78,6 +78,11 @@ async def process_media(
|
|
| 78 |
|
| 79 |
# TRANSCRICÃO COM SEGURANÇA TOTAL
|
| 80 |
words = processor.transcribe(audio_path, language="pt")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
words = processor.correct_orthography(words)
|
| 82 |
|
| 83 |
# GERAÇÃO DO JSON
|
|
|
|
| 78 |
|
| 79 |
# TRANSCRICÃO COM SEGURANÇA TOTAL
|
| 80 |
words = processor.transcribe(audio_path, language="pt")
|
| 81 |
+
|
| 82 |
+
# CORREÇÃO INTELIGENTE (Script Based)
|
| 83 |
+
if script_text:
|
| 84 |
+
words = processor.align_with_script(words, script_text)
|
| 85 |
+
|
| 86 |
words = processor.correct_orthography(words)
|
| 87 |
|
| 88 |
# GERAÇÃO DO JSON
|
backend/processor.py
CHANGED
|
@@ -3,6 +3,7 @@ import whisperx
|
|
| 3 |
import torch
|
| 4 |
import re
|
| 5 |
import json
|
|
|
|
| 6 |
from docx import Document
|
| 7 |
import gc
|
| 8 |
|
|
@@ -44,14 +45,14 @@ class TranscriptionProcessor:
|
|
| 44 |
if text:
|
| 45 |
# Limpeza de caracteres não-imprimíveis (comum em DOCX vindo do Windows)
|
| 46 |
text = "".join(char for char in text if char.isprintable() or char == "\n")
|
| 47 |
-
#
|
| 48 |
text = re.sub(r'[—–-]\s+', ', ', text)
|
| 49 |
text = re.sub(r'\s+,\s+', ', ', text)
|
| 50 |
full_text.append(text)
|
| 51 |
return " ".join(full_text)
|
| 52 |
except Exception as e:
|
| 53 |
-
print(f"[DOCX ERROR]
|
| 54 |
-
return
|
| 55 |
|
| 56 |
def transcribe(self, audio_path, language="pt"):
|
| 57 |
"""Transcrição com sistema de Fallback Blindado"""
|
|
@@ -60,11 +61,11 @@ class TranscriptionProcessor:
|
|
| 60 |
audio = whisperx.load_audio(audio_path)
|
| 61 |
|
| 62 |
# Passo 1: Transcrição (Parâmetros mínimos para compatibilidade total)
|
| 63 |
-
print("[WHISPER]
|
| 64 |
result = self.model.transcribe(audio, batch_size=8, language=language)
|
| 65 |
|
| 66 |
# Passo 2: Alinhamento (Com Try-Except interno para evitar erro 500)
|
| 67 |
-
print("[WHISPER]
|
| 68 |
try:
|
| 69 |
if language not in self.align_model_cache:
|
| 70 |
self.align_model_cache[language] = whisperx.load_align_model(
|
|
@@ -72,7 +73,7 @@ class TranscriptionProcessor:
|
|
| 72 |
)
|
| 73 |
model_a, metadata = self.align_model_cache[language]
|
| 74 |
result = whisperx.align(
|
| 75 |
-
result["segments"], model_a, metadata, audio, self.device
|
| 76 |
)
|
| 77 |
except Exception as align_err:
|
| 78 |
print(f"[WHISPER WARNING] Falha no alinhamento: {align_err}. Seguindo com transcrição base.")
|
|
@@ -91,18 +92,79 @@ class TranscriptionProcessor:
|
|
| 91 |
"word": w.get("word", w.get("text", "")).strip()
|
| 92 |
})
|
| 93 |
|
| 94 |
-
print(f"[WHISPER] Sucesso. {len(words)} palavras processadas.")
|
| 95 |
return words
|
| 96 |
|
| 97 |
except Exception as e:
|
| 98 |
-
print(f"[WHISPER ERROR]
|
| 99 |
-
import traceback
|
| 100 |
-
print(traceback.format_exc())
|
| 101 |
raise e
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
def correct_orthography(self, words):
|
| 104 |
-
"""
|
| 105 |
for w in words:
|
|
|
|
| 106 |
w["word"] = w["word"].replace(" ,", ",").replace(",,", ",")
|
| 107 |
return words
|
| 108 |
|
|
|
|
| 3 |
import torch
|
| 4 |
import re
|
| 5 |
import json
|
| 6 |
+
import difflib
|
| 7 |
from docx import Document
|
| 8 |
import gc
|
| 9 |
|
|
|
|
| 45 |
if text:
|
| 46 |
# Limpeza de caracteres não-imprimíveis (comum em DOCX vindo do Windows)
|
| 47 |
text = "".join(char for char in text if char.isprintable() or char == "\n")
|
| 48 |
+
# Normalização de hífens para vírgulas conforme solicitado pelo USER
|
| 49 |
text = re.sub(r'[—–-]\s+', ', ', text)
|
| 50 |
text = re.sub(r'\s+,\s+', ', ', text)
|
| 51 |
full_text.append(text)
|
| 52 |
return " ".join(full_text)
|
| 53 |
except Exception as e:
|
| 54 |
+
print(f"[DOCX ERROR] {e}")
|
| 55 |
+
return ""
|
| 56 |
|
| 57 |
def transcribe(self, audio_path, language="pt"):
|
| 58 |
"""Transcrição com sistema de Fallback Blindado"""
|
|
|
|
| 61 |
audio = whisperx.load_audio(audio_path)
|
| 62 |
|
| 63 |
# Passo 1: Transcrição (Parâmetros mínimos para compatibilidade total)
|
| 64 |
+
print("[WHISPER] Transcrevendo...")
|
| 65 |
result = self.model.transcribe(audio, batch_size=8, language=language)
|
| 66 |
|
| 67 |
# Passo 2: Alinhamento (Com Try-Except interno para evitar erro 500)
|
| 68 |
+
print("[WHISPER] Alinhando...")
|
| 69 |
try:
|
| 70 |
if language not in self.align_model_cache:
|
| 71 |
self.align_model_cache[language] = whisperx.load_align_model(
|
|
|
|
| 73 |
)
|
| 74 |
model_a, metadata = self.align_model_cache[language]
|
| 75 |
result = whisperx.align(
|
| 76 |
+
result["segments"], model_a, metadata, audio, self.device
|
| 77 |
)
|
| 78 |
except Exception as align_err:
|
| 79 |
print(f"[WHISPER WARNING] Falha no alinhamento: {align_err}. Seguindo com transcrição base.")
|
|
|
|
| 92 |
"word": w.get("word", w.get("text", "")).strip()
|
| 93 |
})
|
| 94 |
|
|
|
|
| 95 |
return words
|
| 96 |
|
| 97 |
except Exception as e:
|
| 98 |
+
print(f"[WHISPER ERROR] {str(e)}")
|
|
|
|
|
|
|
| 99 |
raise e
|
| 100 |
|
| 101 |
+
def align_with_script(self, audio_words, script_text):
|
| 102 |
+
"""
|
| 103 |
+
CORREÇÃO INTELIGENTE (VSL BLINDADA):
|
| 104 |
+
Compara a transcrição com o roteiro e corrige ortografia/termos técnicos
|
| 105 |
+
preservando o tempo do áudio.
|
| 106 |
+
"""
|
| 107 |
+
if not script_text:
|
| 108 |
+
return audio_words
|
| 109 |
+
|
| 110 |
+
print("[REFINE] Iniciando correção inteligente baseada no roteiro...")
|
| 111 |
+
|
| 112 |
+
# 1. Preparação das listas (Original e Limpa para matching)
|
| 113 |
+
script_raw = script_text.split()
|
| 114 |
+
script_clean = [re.sub(r'[^\w]', '', w).lower() for w in script_raw]
|
| 115 |
+
audio_clean = [re.sub(r'[^\w]', '', w['word']).lower() for w in audio_words]
|
| 116 |
+
|
| 117 |
+
# 2. Matching de Sequência
|
| 118 |
+
matcher = difflib.SequenceMatcher(None, audio_clean, script_clean)
|
| 119 |
+
opcodes = matcher.get_opcodes()
|
| 120 |
+
|
| 121 |
+
refined_words = []
|
| 122 |
+
|
| 123 |
+
for tag, i1, i2, j1, j2 in opcodes:
|
| 124 |
+
if tag == 'equal':
|
| 125 |
+
# Palavras batem: usamos a grafia exata do roteiro (casing/pontuação)
|
| 126 |
+
for k in range(i2 - i1):
|
| 127 |
+
word_obj = audio_words[i1 + k].copy()
|
| 128 |
+
word_obj['word'] = script_raw[j1 + k]
|
| 129 |
+
refined_words.append(word_obj)
|
| 130 |
+
|
| 131 |
+
elif tag == 'replace':
|
| 132 |
+
# Caso crítico: setox -> Cetox ou setox31 -> Cetox 31
|
| 133 |
+
# Se o número de palavras for diferente, tentamos fundir para manter o tempo
|
| 134 |
+
if (i2 - i1) == (j2 - j1):
|
| 135 |
+
# 1 para 1
|
| 136 |
+
for k in range(i2 - i1):
|
| 137 |
+
word_obj = audio_words[i1 + k].copy()
|
| 138 |
+
word_obj['word'] = script_raw[j1 + k]
|
| 139 |
+
refined_words.append(word_obj)
|
| 140 |
+
else:
|
| 141 |
+
# M:N (Fusão inteligente)
|
| 142 |
+
# Pegamos o tempo do primeiro ao último do bloco e aplicamos o texto do roteiro
|
| 143 |
+
new_word_text = " ".join(script_raw[j1:j2])
|
| 144 |
+
word_obj = {
|
| 145 |
+
"start": audio_words[i1]["start"],
|
| 146 |
+
"end": audio_words[i2-1]["end"],
|
| 147 |
+
"word": new_word_text
|
| 148 |
+
}
|
| 149 |
+
refined_words.append(word_obj)
|
| 150 |
+
|
| 151 |
+
elif tag == 'delete':
|
| 152 |
+
# Palavra no áudio mas não no roteiro (ad-lib ou erro): mantemos o áudio
|
| 153 |
+
for k in range(i1, i2):
|
| 154 |
+
refined_words.append(audio_words[k])
|
| 155 |
+
|
| 156 |
+
elif tag == 'insert':
|
| 157 |
+
# Palavra no roteiro mas não detectada pelo Whisper: ignoramos para não quebrar o tempo
|
| 158 |
+
# (Ou poderíamos interpolar, mas para Slides VSL é melhor ignorar)
|
| 159 |
+
pass
|
| 160 |
+
|
| 161 |
+
print(f"[REFINE] Concluído. {len(refined_words)} palavras na saída final.")
|
| 162 |
+
return refined_words
|
| 163 |
+
|
| 164 |
def correct_orthography(self, words):
|
| 165 |
+
"""Correções rápidas pós-processamento"""
|
| 166 |
for w in words:
|
| 167 |
+
# Limpeza básica de detritos
|
| 168 |
w["word"] = w["word"].replace(" ,", ",").replace(",,", ",")
|
| 169 |
return words
|
| 170 |
|
google_colab/colab_app.py
CHANGED
|
@@ -1,156 +1,191 @@
|
|
| 1 |
-
#
|
| 2 |
-
#
|
| 3 |
-
|
| 4 |
-
# 1. INSTALAÇÃO DIRETA (Resolve ModuleNotFoundError)
|
| 5 |
-
!pip install --quiet --upgrade torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
|
| 6 |
-
!pip install --quiet transformers accelerate nest_asyncio
|
| 7 |
-
!pip install --quiet git+https://github.com/m-bain/whisperX.git gradio python-docx onnxruntime-gpu
|
| 8 |
-
!apt-get install -y -qq ffmpeg
|
| 9 |
|
| 10 |
import os
|
| 11 |
-
import
|
| 12 |
-
import
|
| 13 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
import json
|
| 15 |
import re
|
| 16 |
import uuid
|
| 17 |
import gc
|
| 18 |
-
import asyncio
|
| 19 |
import nest_asyncio
|
|
|
|
| 20 |
from docx import Document
|
| 21 |
|
| 22 |
-
# Permite que o Gradio rode sem travar o loop de eventos do Colab
|
| 23 |
nest_asyncio.apply()
|
| 24 |
|
| 25 |
-
|
| 26 |
-
import omegaconf
|
| 27 |
-
try:
|
| 28 |
-
from torch.serialization import add_safe_globals
|
| 29 |
-
original_torch_load = torch.load
|
| 30 |
-
def patched_torch_load(*args, **kwargs):
|
| 31 |
-
kwargs['weights_only'] = False
|
| 32 |
-
return original_torch_load(*args, **kwargs)
|
| 33 |
-
torch.load = patched_torch_load
|
| 34 |
-
except:
|
| 35 |
-
pass
|
| 36 |
-
|
| 37 |
-
class ColabProcessor:
|
| 38 |
def __init__(self):
|
| 39 |
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 40 |
self.gpu_name = torch.cuda.get_device_name(0) if self.device == "cuda" else "CPU"
|
|
|
|
| 41 |
|
| 42 |
-
# OTIMIZAÇÃO CIRÚRGICA PARA 12.7GB RAM
|
| 43 |
if self.device == "cuda":
|
| 44 |
-
|
| 45 |
-
self.
|
|
|
|
| 46 |
else:
|
| 47 |
self.compute_type = "int8"
|
| 48 |
self.batch_size = 4
|
| 49 |
-
|
| 50 |
-
print(f"
|
| 51 |
-
self.model = None
|
| 52 |
-
self.align_model_cache = {}
|
| 53 |
|
| 54 |
def load_model(self):
|
| 55 |
if self.model is None:
|
| 56 |
gc.collect()
|
| 57 |
-
torch.cuda.empty_cache()
|
| 58 |
-
print(
|
| 59 |
-
self.model = whisperx.load_model("large-v3", self.device, compute_type=self.compute_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
try:
|
| 64 |
-
doc = Document(
|
| 65 |
-
|
| 66 |
-
for
|
| 67 |
-
|
| 68 |
-
if
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
except Exception as e:
|
| 74 |
-
return f"Erro no DOCX: {str(e)}"
|
| 75 |
|
| 76 |
def run(self, audio_path, docx_file):
|
| 77 |
-
if not audio_path: return "Erro:
|
| 78 |
-
|
| 79 |
|
| 80 |
try:
|
| 81 |
self.load_model()
|
| 82 |
|
| 83 |
-
# 1.
|
| 84 |
-
print(f"🎙️ [
|
| 85 |
audio = whisperx.load_audio(audio_path)
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
# 2. ALINHAMENTO MILIMÉTRICO
|
| 89 |
-
print("📏 Sincronizando timestamps...")
|
| 90 |
-
if "pt" not in self.align_model_cache:
|
| 91 |
-
self.align_model_cache["pt"] = whisperx.load_align_model(language_code="pt", device=self.device)
|
| 92 |
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
})
|
| 106 |
-
|
| 107 |
-
transcribed_text = " ".join([w["word"] for w in words])
|
| 108 |
-
|
| 109 |
-
# 3. DOCX
|
| 110 |
-
script_text = ""
|
| 111 |
-
if docx_file:
|
| 112 |
-
script_text = self.process_docx(docx_file.name)
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
| 116 |
with open(json_path, "w", encoding="utf-8") as f:
|
| 117 |
-
json.dump({"words":
|
| 118 |
|
| 119 |
-
#
|
| 120 |
-
del audio
|
| 121 |
gc.collect()
|
| 122 |
-
torch.cuda.empty_cache()
|
| 123 |
|
| 124 |
-
|
| 125 |
-
return transcribed_text, script_text, json_path
|
| 126 |
|
| 127 |
except Exception as e:
|
| 128 |
import traceback
|
| 129 |
-
|
| 130 |
-
return f"Erro fatal: {str(e)}", "Verifique os logs detalhados acima", None
|
| 131 |
|
| 132 |
-
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
gr.Markdown(f"# 🚀 WHISPER VSL ULTRA (GPU {processor.gpu_name})")
|
| 137 |
-
gr.Markdown("Transcrição Cirúrgica Otimizada para áudios longos (31 min+)")
|
| 138 |
-
|
| 139 |
with gr.Row():
|
| 140 |
-
with gr.Column(
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
btn = gr.Button("🔥
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
# Launch para Colab Python 3.12
|
| 156 |
-
demo.launch(debug=True, share=True, show_error=True)
|
|
|
|
| 1 |
+
# 🚀 WHISPER VSL ULTRA - FINAL CURE EDITION
|
| 2 |
+
# Resolve conflitos de Torch 2.8.0 e implementa correção inteligente (Fuzzy Match)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
import os
|
| 5 |
+
import sys
|
| 6 |
+
import subprocess
|
| 7 |
+
import difflib
|
| 8 |
+
|
| 9 |
+
def install_safe_stack():
|
| 10 |
+
print("🛠️ LIMPANDO E CURANDO AMBIENTE (Aguarde 3 min)...")
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
# 1. Limpeza Radical para evitar conflitos de versões "sequestradas"
|
| 14 |
+
print("🧹 Removendo versões instáveis...")
|
| 15 |
+
subprocess.check_call([sys.executable, "-m", "pip", "uninstall", "-y", "torch", "torchaudio", "torchvision", "whisperx", "pandas"])
|
| 16 |
+
|
| 17 |
+
# 2. Instalação Sincronizada (A "Santíssima Trindade" estável para T4)
|
| 18 |
+
print("📦 Instalando PyTorch Stack Estável (2.5.1)...")
|
| 19 |
+
subprocess.check_call([
|
| 20 |
+
sys.executable, "-m", "pip", "install",
|
| 21 |
+
"torch==2.5.1+cu121", "torchvision==0.20.1+cu121", "torchaudio==2.5.1+cu121",
|
| 22 |
+
"pandas==2.2.2", # Versão que o Colab exige
|
| 23 |
+
"--index-url", "https://download.pytorch.org/whl/cu121"
|
| 24 |
+
])
|
| 25 |
+
|
| 26 |
+
# 3. WhisperX v3.1.1 (A versão mais estável já feita)
|
| 27 |
+
print("📦 Instalando WhisperX v3.1.1...")
|
| 28 |
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "git+https://github.com/m-bain/whisperX.git@v3.1.1"])
|
| 29 |
+
|
| 30 |
+
# 4. Dependências cruciais
|
| 31 |
+
print("📦 Finalizando componentes...")
|
| 32 |
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "pyannote.audio==3.3.1", "gradio", "python-docx", "transformers", "accelerate", "nest_asyncio"])
|
| 33 |
+
subprocess.check_call(["apt-get", "install", "-y", "-qq", "ffmpeg", "libsndfile1"])
|
| 34 |
+
|
| 35 |
+
print("\n✅ AMBIENTE CURADO COM SUCESSO!")
|
| 36 |
+
print("⚠️ AÇÃO NECESSÁRIA: Vá em 'Ambiente de Execução' > 'Reiniciar sessão' e rode esta célula de novo.")
|
| 37 |
+
os.kill(os.getpid(), 9)
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f"❌ Erro na cura: {e}")
|
| 40 |
+
sys.exit(1)
|
| 41 |
+
|
| 42 |
+
# Check de Saúde
|
| 43 |
+
try:
|
| 44 |
+
import torch
|
| 45 |
+
import whisperx
|
| 46 |
+
if "2.5.1" not in torch.__version__: raise ImportError()
|
| 47 |
+
print(f"✅ Ambiente Saudável: Torch {torch.__version__} | WhisperX {whisperx.__version__}")
|
| 48 |
+
except:
|
| 49 |
+
install_safe_stack()
|
| 50 |
+
|
| 51 |
import json
|
| 52 |
import re
|
| 53 |
import uuid
|
| 54 |
import gc
|
|
|
|
| 55 |
import nest_asyncio
|
| 56 |
+
import gradio as gr
|
| 57 |
from docx import Document
|
| 58 |
|
|
|
|
| 59 |
nest_asyncio.apply()
|
| 60 |
|
| 61 |
+
class VSLEngine:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
def __init__(self):
|
| 63 |
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 64 |
self.gpu_name = torch.cuda.get_device_name(0) if self.device == "cuda" else "CPU"
|
| 65 |
+
self.model = None
|
| 66 |
|
|
|
|
| 67 |
if self.device == "cuda":
|
| 68 |
+
caps = torch.cuda.get_device_capability()
|
| 69 |
+
self.compute_type = "int8_float16" if caps[0] < 8 else "bfloat16"
|
| 70 |
+
self.batch_size = 8 if caps[0] < 8 else 16
|
| 71 |
else:
|
| 72 |
self.compute_type = "int8"
|
| 73 |
self.batch_size = 4
|
| 74 |
+
|
| 75 |
+
print(f"🔥 ENGINE PRONTA: {self.gpu_name} | MODO: {self.compute_type}")
|
|
|
|
|
|
|
| 76 |
|
| 77 |
def load_model(self):
|
| 78 |
if self.model is None:
|
| 79 |
gc.collect()
|
| 80 |
+
if self.device == "cuda": torch.cuda.empty_cache()
|
| 81 |
+
print("🧠 Carregando Modelo VSL (Large-v3)...")
|
| 82 |
+
self.model = whisperx.load_model("large-v3", self.device, compute_type=self.compute_type, download_root="/content/models")
|
| 83 |
+
|
| 84 |
+
def align_with_script(self, audio_words, script_text):
|
| 85 |
+
"""CORREÇÃO FUZZY: Faz 'setox' virar 'Cetox 31' comparando com o roteiro"""
|
| 86 |
+
if not script_text: return audio_words
|
| 87 |
+
|
| 88 |
+
print("[REFINE] Aplicando inteligência de roteiro...")
|
| 89 |
+
s_raw = script_text.split()
|
| 90 |
+
s_clean = [re.sub(r'[^\w]', '', w).lower() for w in s_raw]
|
| 91 |
+
a_clean = [re.sub(r'[^\w]', '', w['word']).lower() for w in audio_words]
|
| 92 |
|
| 93 |
+
matcher = difflib.SequenceMatcher(None, a_clean, s_clean)
|
| 94 |
+
refined = []
|
| 95 |
+
|
| 96 |
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
| 97 |
+
if tag == 'equal':
|
| 98 |
+
for k in range(i2-i1):
|
| 99 |
+
word_obj = audio_words[i1+k].copy()
|
| 100 |
+
word_obj['word'] = s_raw[j1+k]
|
| 101 |
+
refined.append(word_obj)
|
| 102 |
+
elif tag == 'replace':
|
| 103 |
+
if (i2-i1) == (j2-j1):
|
| 104 |
+
for k in range(i2-i1):
|
| 105 |
+
word_obj = audio_words[i1+k].copy()
|
| 106 |
+
word_obj['word'] = s_raw[j1+k]
|
| 107 |
+
refined.append(word_obj)
|
| 108 |
+
else:
|
| 109 |
+
new_text = " ".join(s_raw[j1:j2])
|
| 110 |
+
refined.append({"start": audio_words[i1]["start"], "end": audio_words[i2-1]["end"], "word": new_text})
|
| 111 |
+
elif tag == 'delete':
|
| 112 |
+
for k in range(i1, i2): refined.append(audio_words[k])
|
| 113 |
+
return refined
|
| 114 |
+
|
| 115 |
+
def process_docx(self, path):
|
| 116 |
+
if not path: return ""
|
| 117 |
try:
|
| 118 |
+
doc = Document(path)
|
| 119 |
+
full = []
|
| 120 |
+
for p in doc.paragraphs:
|
| 121 |
+
t = "".join(c for c in p.text if c.isprintable()).strip()
|
| 122 |
+
if t:
|
| 123 |
+
t = re.sub(r'[—–-]\s+', ', ', t)
|
| 124 |
+
full.append(t)
|
| 125 |
+
return " ".join(full)
|
| 126 |
+
except: return ""
|
|
|
|
|
|
|
| 127 |
|
| 128 |
def run(self, audio_path, docx_file):
|
| 129 |
+
if not audio_path: return "Erro: Áudio falta", "", None
|
| 130 |
+
sid = uuid.uuid4().hex[:6]
|
| 131 |
|
| 132 |
try:
|
| 133 |
self.load_model()
|
| 134 |
|
| 135 |
+
# 1. Transcrição
|
| 136 |
+
print(f"🎙️ [{sid}] Transcrevendo...")
|
| 137 |
audio = whisperx.load_audio(audio_path)
|
| 138 |
+
res = self.model.transcribe(audio, batch_size=self.batch_size, language="pt")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
+
# 2. Alinhamento
|
| 141 |
+
print(f"📐 [{sid}] Alinhando...")
|
| 142 |
+
m_a, meta = whisperx.load_align_model(language_code="pt", device=self.device)
|
| 143 |
+
res = whisperx.align(res["segments"], m_a, meta, audio, self.device, return_char_alignments=False)
|
| 144 |
|
| 145 |
+
# 3. Extração e Refinamento
|
| 146 |
+
raw_words = []
|
| 147 |
+
for s in res["segments"]:
|
| 148 |
+
for w in s.get("words", s.get("word_segments", [])):
|
| 149 |
+
if "start" in w and "end" in w:
|
| 150 |
+
raw_words.append({"start": round(w["start"], 3), "end": round(w["end"], 3), "word": w.get("word", "").strip()})
|
| 151 |
+
|
| 152 |
+
script_text = self.process_docx(docx_file.name) if docx_file else ""
|
| 153 |
+
final_words = self.align_with_script(raw_words, script_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
+
transcription = " ".join([w["word"] for w in final_words])
|
| 156 |
+
|
| 157 |
+
# 4. JSON
|
| 158 |
+
json_path = f"vsl_output_{sid}.json"
|
| 159 |
with open(json_path, "w", encoding="utf-8") as f:
|
| 160 |
+
json.dump({"words": final_words}, f, ensure_ascii=False, indent=2)
|
| 161 |
|
| 162 |
+
# Memo Cleanup
|
| 163 |
+
del audio, m_a, res
|
| 164 |
gc.collect()
|
| 165 |
+
if self.device == "cuda": torch.cuda.empty_cache()
|
| 166 |
|
| 167 |
+
return transcription, script_text, json_path
|
|
|
|
| 168 |
|
| 169 |
except Exception as e:
|
| 170 |
import traceback
|
| 171 |
+
return f"Erro: {str(e)}\n{traceback.format_exc()}", "", None
|
|
|
|
| 172 |
|
| 173 |
+
engine = VSLEngine()
|
| 174 |
|
| 175 |
+
with gr.Blocks(theme=gr.themes.Monochrome(), title="VSL ULTRA") as demo:
|
| 176 |
+
gr.Markdown("# 🎯 WHISPER VSL ULTRA - FINAL EDITION")
|
|
|
|
|
|
|
|
|
|
| 177 |
with gr.Row():
|
| 178 |
+
with gr.Column():
|
| 179 |
+
a_in = gr.Audio(type="filepath", label="Áudio da VSL")
|
| 180 |
+
d_in = gr.File(label="Roteiro DOCX (Para correção inteligente)")
|
| 181 |
+
btn = gr.Button("🔥 GERAR VSL DATA", variant="primary")
|
| 182 |
+
with gr.Column():
|
| 183 |
+
f_out = gr.File(label="JSON Final")
|
| 184 |
+
t_out = gr.Textbox(label="Transcrição Corrigida", lines=8)
|
| 185 |
+
s_out = gr.Textbox(label="Roteiro Extraído", lines=8)
|
| 186 |
+
|
| 187 |
+
btn.click(engine.run, inputs=[a_in, d_in], outputs=[t_out, s_out, f_out])
|
| 188 |
+
|
| 189 |
+
print("✅ Sistema Pronto.")
|
| 190 |
+
demo.launch(share=True, debug=True)
|
| 191 |
+
|
|
|
|
|
|
|
|
|