Raí Santos commited on
Commit
9a8fc49
·
1 Parent(s): 0078030

feat: Complete optimization with 3 bugs fixed + backend-only

Browse files
Files changed (3) hide show
  1. backend/main.py +5 -0
  2. backend/processor.py +73 -11
  3. 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
- # Regra de substituição cirúrgica do USER
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] Falha no processamento: {e}")
54
- return f"Erro ao ler roteiro: {str(e)}"
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] Iniciando Transcrição Base...")
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] Iniciando Alinhamento Cirúrgico...")
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, return_char_alignments=False
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] Falha Crítica: {str(e)}")
99
- import traceback
100
- print(traceback.format_exc())
101
  raise e
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  def correct_orthography(self, words):
104
- """Correção rápida de vírgulas duplicadas e espaços vazios"""
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
- # PROJETO WHISPER VSL ULTRA - VERSÃO COLAB DEFINITIVA
2
- # Recomendação: em 'Ambiente de Execução' -> 'Desconectar e excluir ambiente' antes de rodar.
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 torch
12
- import whisperx
13
- import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # FIX PARA PYTORCH 2.6+ (Chave Mestra contra UnpicklingError)
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
- self.compute_type = "int8_float16"
45
- self.batch_size = 16
 
46
  else:
47
  self.compute_type = "int8"
48
  self.batch_size = 4
49
-
50
- print(f"🚀 HARDWARE: {self.gpu_name} | MODO: {self.compute_type}")
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(f"🧠 Carregando Whisper Large-v3...")
59
- self.model = whisperx.load_model("large-v3", self.device, compute_type=self.compute_type)
 
 
 
 
 
 
 
 
 
60
 
61
- def process_docx(self, file_path):
62
- if not file_path: return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  try:
64
- doc = Document(file_path)
65
- full_text = []
66
- for para in doc.paragraphs:
67
- text = para.text.strip()
68
- if text:
69
- text = re.sub(r'[—–-]\s+', ', ', text)
70
- text = re.sub(r'\s+,\s+', ', ', text)
71
- full_text.append(text)
72
- return " ".join(full_text)
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: Envie um áudio!", "", None
78
- session_id = uuid.uuid4().hex[:8]
79
 
80
  try:
81
  self.load_model()
82
 
83
- # 1. TRANSCRIÇÃO
84
- print(f"🎙️ [SESSÃO {session_id}] Processando áudio...")
85
  audio = whisperx.load_audio(audio_path)
86
- result = self.model.transcribe(audio, batch_size=self.batch_size, language="pt")
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
- model_a, metadata = self.align_model_cache["pt"]
94
- result = whisperx.align(result["segments"], model_a, metadata, audio, self.device, return_char_alignments=False)
 
 
95
 
96
- words = []
97
- for segment in result["segments"]:
98
- if "words" in segment:
99
- for w in segment["words"]:
100
- if "start" in w and "end" in w:
101
- words.append({
102
- "start": round(w["start"], 3),
103
- "end": round(w["end"], 3),
104
- "word": w["word"].strip()
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
- # 4. JSON FINAL
115
- json_path = f"transcription_{session_id}.json"
 
 
116
  with open(json_path, "w", encoding="utf-8") as f:
117
- json.dump({"words": words}, f, ensure_ascii=False, indent=2)
118
 
119
- # CLEANUP
120
- del audio
121
  gc.collect()
122
- torch.cuda.empty_cache()
123
 
124
- print(f"✅ Concluído com sucesso!")
125
- return transcribed_text, script_text, json_path
126
 
127
  except Exception as e:
128
  import traceback
129
- print(traceback.format_exc())
130
- return f"Erro fatal: {str(e)}", "Verifique os logs detalhados acima", None
131
 
132
- processor = ColabProcessor()
133
 
134
- # INTERFACE GRADIO PREMIUM
135
- with gr.Blocks(theme=gr.themes.Monochrome()) as demo:
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(scale=1):
141
- audio_input = gr.Audio(type="filepath", label="Áudio da VSL")
142
- docx_input = gr.File(label="Roteiro (.docx)")
143
- btn = gr.Button("🔥 INICIAR PROCESSAMENTO", variant="primary")
144
-
145
- with gr.Column(scale=2):
146
- json_output = gr.File(label="📦 Baixar JSON para Slides")
147
- with gr.Tabs():
148
- with gr.TabItem("Transcrição Realizada"):
149
- text_out = gr.Textbox(label=None, lines=15, show_copy_button=True)
150
- with gr.TabItem("Roteiro Original Limpo"):
151
- script_out = gr.Textbox(label=None, lines=15)
152
-
153
- btn.click(processor.run, inputs=[audio_input, docx_input], outputs=[text_out, script_out, json_output])
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
+