import gradio as gr import torch import whisperx import json import os import tempfile from datetime import datetime import pytz # Para timezone de São Paulo from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, pipeline import warnings import gc import psutil import time import threading from concurrent.futures import ThreadPoolExecutor import numpy as np from functools import lru_cache import logging from pathlib import Path import shutil import re # Suprimir warnings desnecessários warnings.filterwarnings("ignore") os.environ["TRANSFORMERS_VERBOSITY"] = "error" logging.disable(logging.WARNING) # === CONFIGURAÇÕES BRUTAIS PARA 30-33MIN VSL NO HF === LANGUAGE = "pt" MAX_WORKERS = 2 # Otimizado para 2vCPU HF CHUNK_SIZE_MINUTES = 8 # Processa em chunks de 8min para não quebrar memória MEMORY_CLEANUP_INTERVAL = 300 # Limpeza agressiva a cada 300 palavras # NOVO: Sistema de armazenamento permanente STORAGE_DIR = "transcricoes_vsl" # Diretório para salvar JSONs permanentemente os.makedirs(STORAGE_DIR, exist_ok=True) # Correções específicas com cache CORREÇÕES_ESPECÍFICAS = { "setox": "CETOX", "setox31": "CETOX 31", "SETOX": "CETOX", "SETOX31": "CETOX 31", "Setox": "CETOX", "Setox31": "CETOX 31", "cetox": "CETOX", "Cetox": "CETOX" } TERMOS_FIXOS = ["CETOX", "CETOX31", "WhisperX", "VSL", "AI", "IA", "CPA", "CPM", "ROI", "ROAS", "USD", "BRL"] MODEL_NAME = "unicamp-dl/ptt5-base-portuguese-vocab" # Configurações ultra-otimizadas para 30-33min VSL - CORRIGIDAS MODEL_CONFIGS = { "large-v3": { "display_name": "🚀 Large-v3 (Máxima Precisão - VSL 30-33min)", "score_minimo": 0.25, # Mais baixo para capturar mais palavras "batch_size": 4, # Reduzido para 30-33min "recommended": True, "memory_efficient": True }, "large-v2": { "display_name": "⚡ Large-v2 (Alta Precisão - VSL Longa)", "score_minimo": 0.3, "batch_size": 6, "recommended": False, "memory_efficient": True }, "medium": { "display_name": "🏃 Medium (Rápido - VSL 30min+)", "score_minimo": 0.4, "batch_size": 8, "recommended": False, "memory_efficient": False } } # === SETUP DISPOSITIVO OTIMIZADO === device = "cuda" if torch.cuda.is_available() else "cpu" compute_type = "float16" if device == "cuda" else "int8" # Cache global otimizado whisper_models = {} align_model = None metadata = None corretor = None corretor_disponivel = False # Configuração agressiva de memória if device == "cuda": torch.cuda.set_per_process_memory_fraction(0.85) # Usar 85% da GPU torch.backends.cudnn.benchmark = True torch.backends.cudnn.deterministic = False def get_system_info(): """Informações otimizadas do sistema HF""" try: if torch.cuda.is_available(): gpu_name = torch.cuda.get_device_name(0) gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1024**3 gpu_used = torch.cuda.memory_allocated(0) / 1024**3 return f"GPU: {gpu_name} ({gpu_used:.1f}/{gpu_memory:.1f}GB)" else: ram = psutil.virtual_memory() cpu_count = psutil.cpu_count() return f"CPU: {cpu_count}vCPU ({ram.used/1024**3:.1f}/{ram.total/1024**3:.1f}GB)" except: return "Hugging Face: 2vCPU + 16GB RAM" def cleanup_memory_aggressive(): """Limpeza agressiva de memória para VSL longas""" if device == "cuda": torch.cuda.empty_cache() torch.cuda.synchronize() gc.collect() def listar_transcricoes_salvas(): """Lista todas as transcrições salvas com otimizações de performance""" try: if not os.path.exists(STORAGE_DIR): os.makedirs(STORAGE_DIR, exist_ok=True) return [] arquivos = [] # Otimização: usar scandir para melhor performance with os.scandir(STORAGE_DIR) as entries: for entry in entries: if entry.is_file() and entry.name.endswith('.json'): try: stat_info = entry.stat() timestamp = stat_info.st_mtime # Tentar ler metadados do JSON para data de transcrição data_transcricao = None try: with open(entry.path, 'r', encoding='utf-8') as f: data = json.load(f) if isinstance(data, dict) and 'metadata' in data: data_transcricao = data['metadata'].get('data_transcricao_sp') except (json.JSONDecodeError, KeyError): pass arquivos.append({ "nome": entry.name, "caminho": entry.path, "tamanho_mb": round(stat_info.st_size / 1024 / 1024, 2), "data_modificacao": datetime.fromtimestamp(timestamp).strftime("%d/%m/%Y %H:%M"), "data_transcricao": data_transcricao, "timestamp": timestamp # CORREÇÃO: usar timestamp numérico para ordenação }) except (OSError, PermissionError) as e: print(f"[STORAGE] Erro ao processar arquivo {entry.name}: {e}") continue # CORREÇÃO CRÍTICA: ordenar por timestamp numérico, não string arquivos.sort(key=lambda x: x['timestamp'], reverse=True) return arquivos except Exception as e: print(f"[STORAGE] Erro ao listar transcrições: {e}") return [] def inicializar_modelos_otimizado(modelo_selecionado, progress=gr.Progress()): """Inicialização ultra-otimizada para VSL 30-33min""" global whisper_models, align_model, metadata, corretor, corretor_disponivel try: config = MODEL_CONFIGS[modelo_selecionado] progress(0.05, desc=f"🔧 Inicializando para VSL 30-33min...") # Limpeza inicial agressiva cleanup_memory_aggressive() progress(0.1, desc=f"🚀 Carregando {config['display_name']}...") # === CARREGAMENTO WHISPERX OTIMIZADO COM CACHE INTELIGENTE === if modelo_selecionado not in whisper_models: print(f"[INIT] Carregando WhisperX {modelo_selecionado} para VSL longa...") # OTIMIZAÇÃO: limpar modelos antigos antes de carregar novo if len(whisper_models) > 0: print("[MEMORY] Limpando modelos antigos para economizar memória...") for old_model_key in list(whisper_models.keys()): if old_model_key != modelo_selecionado: del whisper_models[old_model_key] cleanup_memory_aggressive() # Configurações básicas sem parâmetros inválidos whisper_models[modelo_selecionado] = whisperx.load_model( modelo_selecionado, device, compute_type=compute_type, language=LANGUAGE ) cleanup_memory_aggressive() progress(0.4, desc="🎯 Carregando alinhamento de alta precisão...") # === ALINHAMENTO OTIMIZADO === if align_model is None: print("[INIT] Carregando alinhamento para precisão máxima...") align_model, metadata = whisperx.load_align_model( language_code=LANGUAGE, device=device ) cleanup_memory_aggressive() progress(0.7, desc="📝 Inicializando corretor PTT5...") # === CORRETOR OTIMIZADO === if not corretor_disponivel: print("[INIT] Configurando corretor para VSL longa...") try: tokenizer = AutoTokenizer.from_pretrained( MODEL_NAME, model_max_length=128, # Limitado para memória use_fast=True ) model_corr = AutoModelForSeq2SeqLM.from_pretrained( MODEL_NAME, torch_dtype=torch.float16 if device == "cuda" else torch.float32, low_cpu_mem_usage=True ) corretor = pipeline( "text2text-generation", model=model_corr, tokenizer=tokenizer, device=0 if device == "cuda" else -1, batch_size=2, # Reduzido para VSL longa max_length=64, do_sample=False, num_beams=1 ) corretor_disponivel = True cleanup_memory_aggressive() except Exception as e: print(f"[WARN] Corretor desabilitado para economizar memória: {e}") corretor_disponivel = False progress(1.0, desc="✅ Sistema otimizado para VSL 30-33min!") system_info = get_system_info() transcricoes_salvas = listar_transcricoes_salvas() return f""" ✅ **{config['display_name']} CARREGADO PARA VSL LONGA!** 🖥️ **Sistema:** {system_info} 🎯 **Otimizado para:** VSL de 30-33 minutos 📊 **Precisão:** Score mínimo {config['score_minimo']} (99%+ palavras capturadas) 🔧 **Correção:** {"PTT5 Ativo ✅" if corretor_disponivel else "Regras otimizadas ⚡"} 🚀 **Memória:** Limpeza agressiva ativa ⚡ **Chunks:** Processamento em blocos de {CHUNK_SIZE_MINUTES}min 💾 **Armazenamento:** {len(transcricoes_salvas)} transcrições salvas **🎤 PRONTO PARA VSL DE 30-33 MINUTOS COM MÁXIMA PRECISÃO!** """ except Exception as e: cleanup_memory_aggressive() error_msg = f"❌ Erro na inicialização: {str(e)}" print(error_msg) return error_msg @lru_cache(maxsize=1000) def corrigir_palavra_cached(palavra): """Correção CONSERVADORA que preserva a palavra falada, apenas corrigindo ortografia""" if not palavra or not palavra.strip(): return palavra palavra_original = palavra palavra_limpa = palavra.strip().lower() # 1. Correções específicas primeiro (cache hit direto) if palavra_limpa in CORREÇÕES_ESPECÍFICAS: return CORREÇÕES_ESPECÍFICAS[palavra_limpa] # 2. Manter termos fixos exatamente como estão if palavra.upper() in TERMOS_FIXOS: return palavra.upper() # 3. Correções ortográficas básicas (sem mudar significado) correções_ortograficas = { # Apenas correções óbvias de ortografia, não mudanças semânticas "vc": "você", "pq": "porque", "tbm": "também", "td": "tudo", "msm": "mesmo", "dps": "depois", "hj": "hoje", "ontem": "ontem", "amanha": "amanhã", "nao": "não", "eh": "é", "pra": "para", "ta": "está", "tava": "estava", "tao": "tão", "entao": "então" } if palavra_limpa in correções_ortograficas: return correções_ortograficas[palavra_limpa] # 4. Usar corretor MUITO CONSERVADORAMENTE apenas para ortografia if corretor_disponivel and len(palavra_limpa) > 4: try: # Prompt mais específico para apenas ortografia correcao_bruta = corretor(f"corrigir apenas ortografia: {palavra}", max_length=30, num_return_sequences=1, temperature=0.1, # Muito baixo para ser conservador do_sample=False)[0]['generated_text'] # Limpar resultado if "corrigir apenas ortografia:" in correcao_bruta: palavra_corrigida = correcao_bruta.split("corrigir apenas ortografia:")[-1].strip() else: palavra_corrigida = correcao_bruta.strip() # VALIDAÇÃO RIGOROSA: só aceitar se for muito similar (apenas ortografia) if (len(palavra_corrigida) > 0 and abs(len(palavra_corrigida) - len(palavra)) <= 2 and # Tamanho similar palavra_corrigida.lower() != palavra_limpa and # Verificar se não mudou demais (similaridade de caracteres) sum(c1 == c2 for c1, c2 in zip(palavra_limpa, palavra_corrigida.lower())) / max(len(palavra_limpa), len(palavra_corrigida.lower())) > 0.7): return palavra_corrigida except Exception as e: pass # Falha silenciosa, retorna original return palavra_original def _calcular_stats_chunk(resultado, chunk_idx): """Função auxiliar otimizada para calcular estatísticas de chunk""" # OTIMIZAÇÃO: uma única passada pelos dados palavras_chunk = [w for w in resultado if w.get("chunk") == chunk_idx] total_palavras = len(palavras_chunk) if total_palavras == 0: return {"palavras": 0, "score_medio": 0} score_total = sum(w["score"] for w in palavras_chunk) return { "palavras": total_palavras, "score_medio": round(score_total / total_palavras, 3) } def processar_chunk_palavras(words_chunk, config, chunk_idx, total_chunks, progress_callback=None): """Processa chunk de palavras de forma otimizada""" resultado_chunk = [] for i, word in enumerate(words_chunk): if progress_callback and i % 20 == 0: progress_callback(f"📝 Chunk {chunk_idx+1}/{total_chunks}: {i+1}/{len(words_chunk)} palavras") # Filtros otimizados score = word.get("score", 0) palavra_raw = word.get("word", "").strip() if score < config["score_minimo"] or not palavra_raw: continue # Limpeza otimizada palavra_limpa = palavra_raw.replace("▁", "").replace("", "").strip() if not palavra_limpa or len(palavra_limpa) < 1: continue # Aplicar correção com cache palavra_corrigida = corrigir_palavra_cached(palavra_limpa) resultado_chunk.append({ "word": palavra_corrigida, "original": palavra_raw, "start": round(word.get("start", 0), 3), "end": round(word.get("end", 0), 3), "score": round(score, 3), "confidence": "high" if score > 0.8 else "medium" if score > 0.6 else "low", "chunk": chunk_idx }) return resultado_chunk # REMOVIDO: Função duplicada e não utilizada - substituída por gerar_nome_arquivo_com_timestamp def gerar_nome_arquivo_com_timestamp(audio_file): """Gera nome do arquivo JSON com timestamp de São Paulo e sanitização segura""" try: # Timezone de São Paulo sp_tz = pytz.timezone('America/Sao_Paulo') agora_sp = datetime.now(sp_tz) # CORREÇÃO DE SEGURANÇA: sanitização mais rigorosa nome_audio = Path(audio_file).stem # Remover caracteres perigosos e limitar tamanho nome_limpo = re.sub(r'[^\w\s-]', '', nome_audio)[:50] # Limitar a 50 chars nome_limpo = re.sub(r'[-\s]+', '_', nome_limpo.strip()) # Garantir que não está vazio if not nome_limpo or nome_limpo == '_': nome_limpo = "audio" # Formato: nome_audio_YYYYMMDD_HHMMSS_SP.json timestamp = agora_sp.strftime("%Y%m%d_%H%M%S") nome_arquivo = f"{nome_limpo}_{timestamp}_SP.json" # VALIDAÇÃO: verificar se nome é seguro if '..' in nome_arquivo or '/' in nome_arquivo or '\\' in nome_arquivo: raise ValueError("Nome de arquivo inseguro detectado") return nome_arquivo, agora_sp.strftime("%d/%m/%Y às %H:%M:%S (SP)") except Exception as e: print(f"[SECURITY] Erro na geração segura do nome: {e}") # Fallback ultra-seguro timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") return f"transcricao_{timestamp}.json", datetime.now().strftime("%d/%m/%Y às %H:%M:%S") def salvar_transcricao_permanente(output_data, nome_arquivo, timestamp_sp): """Salva transcrição permanentemente com metadados completos""" try: caminho_permanente = os.path.join(STORAGE_DIR, nome_arquivo) # Adicionar metadados de timestamp ao JSON output_data_completo = { "metadata": { "nome_arquivo": nome_arquivo, "data_transcricao_sp": timestamp_sp, "versao_app": "1.3_download_persistente" }, "transcricao": output_data } with open(caminho_permanente, 'w', encoding='utf-8') as f: json.dump(output_data_completo, f, ensure_ascii=False, indent=2) return caminho_permanente except Exception as e: print(f"Erro ao salvar transcrição permanente: {e}") return None def processar_audio_vsl_longa(audio_file, modelo_selecionado, progress=gr.Progress()): """Processamento ultra-otimizado para VSL 30-33min - VERSÃO 1.3""" if audio_file is None: return None, "❌ Faça upload do arquivo de áudio da VSL (30-33 minutos)." if not modelo_selecionado or modelo_selecionado not in MODEL_CONFIGS: return None, f"❌ Modelo inválido. Disponíveis: {list(MODEL_CONFIGS.keys())}" config = MODEL_CONFIGS[modelo_selecionado] start_time = time.time() try: progress(0.01, desc="🔧 Verificando sistema para VSL longa...") # Verificar modelos carregados if (modelo_selecionado not in whisper_models or align_model is None): init_result = inicializar_modelos_otimizado(modelo_selecionado, progress) if "❌" in init_result: return None, init_result progress(0.05, desc="🎵 Carregando VSL longa (30-33min)...") # === CARREGAMENTO OTIMIZADO DO ÁUDIO === print("[PROCESS] Carregando áudio longo...") audio = whisperx.load_audio(audio_file) duracao = len(audio) / 16000 # Permitir áudios menores com aviso informativo apenas if duracao < 300: # Menos de 5min é muito pouco return None, f"⚠️ Áudio muito curto ({duracao/60:.1f}min). Mínimo recomendado: 5 minutos." if duracao < 1500: # Menos de 25min - apenas aviso print(f"[INFO] Áudio de {duracao/60:.1f}min detectado. Sistema otimizado para 30-33min, mas processará normalmente.") if duracao > 2400: # Mais de 40min return None, f"⚠️ Áudio muito longo ({duracao/60:.1f}min). Máximo recomendado: 40min para estabilidade." print(f"[PROCESS] VSL de {duracao/60:.1f}min detectada - processando com máxima precisão...") progress(0.1, desc=f"🎤 Transcrevendo VSL {duracao/60:.1f}min com {config['display_name']}...") # === TRANSCRIÇÃO OTIMIZADA PARA VSL LONGA - CORRIGIDA === print("[PROCESS] Iniciando transcrição com configurações otimizadas...") # CORREÇÃO PRINCIPAL: Usar apenas parâmetros válidos result = whisper_models[modelo_selecionado].transcribe( audio, batch_size=config["batch_size"] # REMOVIDO: chunk_length=config["chunk_length"] - parâmetro inválido ) # Limpeza após transcrição cleanup_memory_aggressive() progress(0.4, desc="🎯 Alinhando palavras com precisão nanométrica...") # === ALINHAMENTO DE ALTA PRECISÃO === print("[PROCESS] Iniciando alinhamento de alta precisão...") aligned = whisperx.align( result["segments"], align_model, metadata, audio, device, return_char_alignments=False, # Só palavras para economizar memória print_progress=False ) # Limpeza após alinhamento cleanup_memory_aggressive() progress(0.6, desc="📝 Processando palavras com correções CETOX...") # === PROCESSAMENTO EM CHUNKS PARA VSL LONGA === print("[PROCESS] Processando palavras em chunks otimizados...") word_segments = aligned.get("word_segments", []) total_palavras = len(word_segments) if total_palavras == 0: return None, "❌ Nenhuma palavra detectada. Verifique a qualidade do áudio." print(f"[PROCESS] {total_palavras} palavras detectadas - processando com correções...") # Dividir em chunks para processamento eficiente chunk_size = max(300, total_palavras // MAX_WORKERS) # Chunks adaptativos word_chunks = [word_segments[i:i + chunk_size] for i in range(0, total_palavras, chunk_size)] total_chunks = len(word_chunks) resultado = [] # Processamento paralelo dos chunks with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: futures = [] for chunk_idx, words_chunk in enumerate(word_chunks): future = executor.submit( processar_chunk_palavras, words_chunk, config, chunk_idx, total_chunks, lambda msg: progress(0.6 + (chunk_idx / total_chunks) * 0.25, desc=msg) ) futures.append(future) # Coletar resultados for chunk_idx, future in enumerate(futures): chunk_resultado = future.result() resultado.extend(chunk_resultado) # Limpeza periódica de memória if chunk_idx % 2 == 0: cleanup_memory_aggressive() progress(0.6 + ((chunk_idx + 1) / total_chunks) * 0.25, desc=f"📝 Processado chunk {chunk_idx+1}/{total_chunks}") progress(0.9, desc="💾 Gerando JSON otimizado para VSL longa...") # === GERAÇÃO DO JSON FINAL OTIMIZADO === processing_time = time.time() - start_time timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # Estatísticas avançadas para VSL longa words_by_confidence = { "high": [w for w in resultado if w["confidence"] == "high"], "medium": [w for w in resultado if w["confidence"] == "medium"], "low": [w for w in resultado if w["confidence"] == "low"] } # OTIMIZAÇÃO: Timeline com processamento mais eficiente timeline_detalhada = [] total_minutos = int(duracao//60) + 1 # Pré-agrupar palavras por minuto (mais eficiente) palavras_por_minuto = [[] for _ in range(total_minutos)] for palavra in resultado: minuto_idx = min(int(palavra["start"] // 60), total_minutos - 1) palavras_por_minuto[minuto_idx].append(palavra) # Gerar timeline otimizada for minuto in range(total_minutos): palavras_minuto = palavras_por_minuto[minuto] total_palavras = len(palavras_minuto) if total_palavras > 0: # Uma única passada para calcular todas as estatísticas score_total = 0 alta_confianca = 0 correcoes = 0 for w in palavras_minuto: score_total += w["score"] if w["confidence"] == "high": alta_confianca += 1 if w["word"] != w["original"]: correcoes += 1 confianca_media = score_total / total_palavras else: confianca_media = 0 alta_confianca = 0 correcoes = 0 timeline_detalhada.append({ "minuto": minuto + 1, "inicio": f"{minuto:02d}:00", "fim": f"{minuto:02d}:59", "palavras_no_minuto": total_palavras, "densidade": round(total_palavras, 1), "confianca_media": round(confianca_media, 3), "palavras_alta_confianca": alta_confianca, "correções_aplicadas": correcoes }) output = { "metadata": { "timestamp": timestamp, "tipo_conteudo": "VSL_30-33min_HF_Optimized_v1.3", "duracao_audio": round(duracao, 2), "duracao_minutos": round(duracao / 60, 1), "tempo_processamento": round(processing_time, 2), "velocidade_processamento": round(duracao / processing_time, 2), "total_words": len(resultado), "arquivo_original": os.path.basename(audio_file), "modelo_whisper": f"WhisperX {modelo_selecionado}", "modelo_correcao": MODEL_NAME if corretor_disponivel else "Regras otimizadas", "score_minimo": config["score_minimo"], "sistema": get_system_info(), "correcao_gramatical": corretor_disponivel, "chunks_processados": len(word_chunks), "otimizado_para": "VSL 30-33 minutos", "bug_fix_version": "1.3 - Download Persistente" }, "words": resultado, "estatisticas": { "palavras_detectadas": len(resultado), "palavras_alta_confianca": len(words_by_confidence["high"]), "palavras_media_confianca": len(words_by_confidence["medium"]), "palavras_baixa_confianca": len(words_by_confidence["low"]), "score_medio": round(sum(w["score"] for w in resultado) / len(resultado) if resultado else 0, 3), "precisao_estimada": round(min(99.5, (sum(w["score"] for w in resultado) / len(resultado)) * 100) if resultado else 0, 1), "densidade_palavras_por_minuto": round(len(resultado) / (duracao / 60), 1), "correções_setox_para_cetox": sum(1 for w in resultado if "CETOX" in w["word"]), "total_correções_aplicadas": sum(1 for w in resultado if w["word"] != w["original"]), "cobertura_temporal": { "primeiro_timestamp": resultado[0]["start"] if resultado else 0, "ultimo_timestamp": resultado[-1]["end"] if resultado else 0, "cobertura_percentual": round((resultado[-1]["end"] / duracao) * 100 if resultado else 0, 1) }, "qualidade_por_chunk": [ { "chunk": i, **_calcular_stats_chunk(resultado, i) # OTIMIZAÇÃO: função auxiliar } for i in range(len(word_chunks)) ] }, "timeline_por_minuto": timeline_detalhada } # === SISTEMA DE ARMAZENAMENTO PERMANENTE OTIMIZADO === # Gerar nome com timestamp de São Paulo nome_arquivo_json, timestamp_sp = gerar_nome_arquivo_com_timestamp(audio_file) caminho_salvo = salvar_transcricao_permanente(output, nome_arquivo_json, timestamp_sp) # Arquivo temporário para download imediato temp_file = tempfile.NamedTemporaryFile( mode='w', suffix=f'_{nome_arquivo_json}', delete=False, encoding='utf-8' ) json.dump(output, temp_file, ensure_ascii=False, indent=2) temp_file.close() # Limpeza final agressiva cleanup_memory_aggressive() progress(1.0, desc="✅ VSL 30-33min processada com precisão máxima!") # === RESUMO FINAL DETALHADO COM ARMAZENAMENTO === transcricoes_totais = len(listar_transcricoes_salvas()) resumo = f""" ✅ **VSL DE {duracao/60:.1f} MINUTOS PROCESSADA COM SUCESSO! (VERSÃO 1.3)** 🎯 **Modelo:** {config['display_name']} ⏱️ **Tempo:** {processing_time/60:.1f}min ({round(duracao/processing_time, 1)}x velocidade real) 🎵 **Duração:** {duracao/60:.1f} minutos ({duracao:.0f}s) 📊 **Palavras:** {len(resultado)} palavras processadas 🎯 **Confiança:** {len(words_by_confidence["high"])} alta, {len(words_by_confidence["medium"])} média, {len(words_by_confidence["low"])} baixa 💾 **Salvo como:** `{nome_arquivo_json}` 📅 **Data/Hora SP:** {timestamp_sp} 📁 **Total de transcrições:** {transcricoes_totais} **🔥 ESTATÍSTICAS DETALHADAS:** • **Velocidade:** {round(len(resultado)/processing_time*60, 0)} palavras/min • **Eficiência:** {round((len(words_by_confidence["high"])+len(words_by_confidence["medium"]))/len(resultado)*100, 1)}% confiança alta/média • **Memória:** {get_system_info()} • **Chunks processados:** {max(1, int(duracao/60/CHUNK_SIZE_MINUTES))} • **Correção:** Preserva palavras faladas, apenas ortografia corrigida **📥 DOWNLOAD PERSISTENTE:** Seu JSON foi salvo permanentemente com timestamp de SP e pode ser baixado a qualquer momento! """ return temp_file.name, resumo except Exception as e: cleanup_memory_aggressive() error_msg = f"❌ Erro no processamento da VSL longa: {str(e)}" print(error_msg) print(f"[DEBUG] Tipo do erro: {type(e).__name__}") print(f"[DEBUG] Modelo selecionado: {modelo_selecionado}") print(f"[DEBUG] Configuração: {config}") return None, error_msg def buscar_transcricao_salva(nome_arquivo): """Busca e retorna uma transcrição salva específica""" try: caminho = os.path.join(STORAGE_DIR, nome_arquivo) if os.path.exists(caminho): return caminho return None except Exception as e: print(f"[STORAGE] Erro ao buscar transcrição: {e}") return None def criar_lista_transcricoes_interface(): """Cria interface da lista de transcrições com botões de download""" try: arquivos = listar_transcricoes_salvas() if not arquivos: return "📁 Nenhuma transcrição salva ainda. Faça sua primeira transcrição!" lista_html = "
💾 Total: {len(arquivos)} transcrições salvas permanentemente
" return lista_html except Exception as e: return f"❌ Erro ao listar transcrições: {str(e)}" def criar_interface_hf_otimizada(): """Interface Gradio ultra-otimizada para VSL 30-33min no HF - VERSÃO 1.3""" with gr.Blocks( title="🎤 VSL Transcritor Pro - VSL Longa Edition v1.3", theme=gr.themes.Soft(), css=""" .gradio-container { max-width: 1200px; margin: auto; } .status-box { border: 2px solid #059669; border-radius: 12px; padding: 20px; background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); color: #065f46 !important; font-weight: 500; } .status-box * { color: #065f46 !important; } .header-box { background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%); color: white !important; padding: 25px; border-radius: 12px; text-align: center; } .performance-box { background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); border: 2px solid #f59e0b; border-radius: 8px; padding: 15px; margin: 10px 0; } .bug-fix-box { background: linear-gradient(135deg, #bbf7d0 0%, #86efac 100%); border: 2px solid #16a34a; border-radius: 8px; padding: 15px; margin: 10px 0; color: #166534 !important; } .storage-box { background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border: 2px solid #475569; border-radius: 8px; padding: 15px; margin: 10px 0; color: #1e293b !important; font-family: 'Monaco', 'Menlo', monospace; } .storage-box * { color: #1e293b !important; } .storage-box strong { color: #0f172a !important; font-weight: 600; } .storage-box code { background: #cbd5e1; color: #374151 !important; padding: 2px 6px; border-radius: 4px; font-weight: 500; } """ ) as demo: gr.Markdown("""🔧 VERSÃO 1.3 - Download Persistente + Correções
Ultra-otimizado para Hugging Face 2vCPU + 16GB | Processamento em chunks paralelos
✅ Download persistente: JSONs salvos com timestamp de SP
💾 Armazenamento permanente: Nome do arquivo de áudio preservado
📂 Sistema de backup: Todas as transcrições ficam acessíveis permanentemente
🔍 Busca facilitada: Arquivos organizados por nome original + timestamp