File size: 7,806 Bytes
c35d4af
0078030
 
c35d4af
 
9a8fc49
c35d4af
0078030
c35d4af
 
0078030
c35d4af
 
0078030
c35d4af
7d4dcec
0078030
ac24a29
 
 
 
c35d4af
 
0078030
c35d4af
0078030
 
 
 
 
ac24a29
 
 
 
 
 
 
 
 
 
 
0078030
 
 
 
 
36d6c74
0078030
36d6c74
0078030
36d6c74
 
0078030
 
9a8fc49
36d6c74
 
 
0078030
36d6c74
9a8fc49
 
c35d4af
 
0078030
36d6c74
 
 
 
0078030
9a8fc49
0078030
36d6c74
0078030
9a8fc49
0078030
 
 
 
 
 
 
9a8fc49
0078030
 
 
 
 
 
36d6c74
 
0078030
 
 
 
 
 
 
 
 
36d6c74
 
0078030
36d6c74
9a8fc49
36d6c74
c35d4af
9a8fc49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c35d4af
9a8fc49
0078030
9a8fc49
0078030
 
c35d4af
 
0078030
c35d4af
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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}