From d23aadcd0e8389a9f905d0fe7040f57fc712b9e6 Mon Sep 17 00:00:00 2001 From: godax84 Date: Tue, 26 Nov 2024 07:31:50 -0800 Subject: [PATCH] Modificar main.py Usar Wishper --- main.py | 783 ++++++++++++++++++++++---------------------------------- 1 file changed, 301 insertions(+), 482 deletions(-) diff --git a/main.py b/main.py index 51f36c4..7ce61c5 100644 --- a/main.py +++ b/main.py @@ -1,483 +1,302 @@ -import tkinter as tk -from tkinter import ttk, filedialog, scrolledtext -from tkinter import messagebox -import torch -from transformers import AutoProcessor, WhisperForConditionalGeneration -import cv2 -from datetime import timedelta -import os -import threading -import subprocess -import time -import re -import numpy as np - -class VideoSubtitleApp: - def __init__(self, root): - self.root = root - self.root.title("Extrator de Legendas") - self.root.geometry("900x700") - - # Variáveis - self.video_path = tk.StringVar() - self.video_info = tk.StringVar() - self.selected_language = tk.StringVar(value='pt-BR') - self.subtitles_list = [] - - # Inicializar modelo Whisper e processador - self.initialize_whisper() - - # Dicionário de línguas disponíveis - self.languages = { - 'Português (Brasil)': 'pt', - 'Português (Portugal)': 'pt', - 'English': 'en', - 'Español': 'es', - 'Français': 'fr', - 'Deutsch': 'de', - 'Italiano': 'it' - } - - # Criar interface - self.create_widgets() - - # Variável para armazenar o vídeo - self.video = None - - def create_widgets(self): - # Frame principal - main_frame = ttk.Frame(self.root, padding="10") - main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) - - # Configurar expansão da grade - self.root.grid_rowconfigure(0, weight=1) - self.root.grid_columnconfigure(0, weight=1) - main_frame.grid_columnconfigure(1, weight=1) - - # Frame para seleção de arquivo e idioma - file_frame = ttk.Frame(main_frame) - file_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5) - - # Botão para selecionar arquivo - ttk.Button(file_frame, text="Selecionar Vídeo", command=self.select_file).pack(side=tk.LEFT, padx=5) - - # Seleção de idioma - ttk.Label(file_frame, text="Idioma:").pack(side=tk.LEFT, padx=5) - language_combo = ttk.Combobox(file_frame, - values=list(self.languages.keys()), - textvariable=self.selected_language, - state='readonly', - width=20) - language_combo.pack(side=tk.LEFT, padx=5) - language_combo.set('Português (Brasil)') - - # Label para mostrar caminho do arquivo - ttk.Label(main_frame, textvariable=self.video_path, wraplength=500).grid(row=1, column=0, columnspan=2, pady=5) - - # Frame para informações do vídeo - info_frame = ttk.LabelFrame(main_frame, text="Informações do Vídeo", padding="5") - info_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5) - - ttk.Label(info_frame, textvariable=self.video_info).grid(row=0, column=0, sticky=tk.W) - - # Frame para botões de ação - button_frame = ttk.Frame(main_frame) - button_frame.grid(row=3, column=0, columnspan=2, pady=5) - - ttk.Button(button_frame, text="Gerar Legendas", command=self.generate_subtitles).pack(side=tk.LEFT, padx=5) - ttk.Button(button_frame, text="Salvar Alterações", command=self.save_subtitles).pack(side=tk.LEFT, padx=5) - - # Progress bar - self.progress = ttk.Progressbar(main_frame, mode='indeterminate') - self.progress.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5) - - # Frame para edição de legendas - subtitle_frame = ttk.LabelFrame(main_frame, text="Editor de Legendas", padding="5") - subtitle_frame.grid(row=5, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) - subtitle_frame.grid_rowconfigure(0, weight=1) - subtitle_frame.grid_columnconfigure(0, weight=1) - - # Área de texto editável para legendas - self.subtitle_text = scrolledtext.ScrolledText(subtitle_frame, height=20, width=80, wrap=tk.WORD) - self.subtitle_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=5, pady=5) - - # Instruções de uso - instructions = """Instruções: - 1. Selecione o idioma do áudio do vídeo - 2. Clique em 'Selecionar Vídeo' e escolha o arquivo - 3. Aguarde o processamento do modelo Whisper - 4. Edite as legendas se necessário - 5. Clique em 'Salvar Alterações' para gerar o arquivo .srt""" - - ttk.Label(main_frame, text=instructions, justify=tk.LEFT, wraplength=600).grid( - row=6, column=0, columnspan=2, pady=5, sticky=tk.W) - - def select_file(self): - filetypes = ( - ('Arquivos de vídeo', '*.mp4 *.avi *.mkv'), - ('Todos os arquivos', '*.*') - ) - - filename = filedialog.askopenfilename( - title='Selecione um vídeo', - filetypes=filetypes - ) - - if filename: - self.video_path.set(filename) - self.load_video_info(filename) - - def load_video_info(self, filename): - try: - self.video = cv2.VideoCapture(filename) - - # Obter informações do vídeo - fps = self.video.get(cv2.CAP_PROP_FPS) - frame_count = int(self.video.get(cv2.CAP_PROP_FRAME_COUNT)) - duration = frame_count / fps - width = int(self.video.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(self.video.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - info = f""" - Duração: {str(timedelta(seconds=int(duration)))} - Resolução: {width}x{height} - FPS: {fps:.2f} - Formato: {os.path.splitext(filename)[1]} - """ - self.video_info.set(info) - - except Exception as e: - messagebox.showerror("Erro", f"Erro ao carregar o vídeo: {str(e)}") - - def generate_subtitles(self): - if not self.video_path.get(): - messagebox.showwarning("Aviso", "Por favor, selecione um vídeo primeiro.") - return - - # Iniciar processamento em thread separada - self.progress.start() - thread = threading.Thread(target=self.process_video) - thread.start() - - def initialize_whisper(self): - """Inicializa o modelo Whisper e o processador com configurações otimizadas""" - try: - # Usar o modelo maior para melhor qualidade - model_name = "openai/whisper-large-v3" - self.processor = AutoProcessor.from_pretrained(model_name) - self.model = WhisperForConditionalGeneration.from_pretrained( - model_name, - device_map="auto", # Usar a melhor dispositivo disponível - torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32, - low_cpu_mem_usage=True - ) - - if torch.cuda.is_available(): - print("Usando GPU para processamento") - else: - print("Usando CPU para processamento") - - except Exception as e: - messagebox.showerror("Erro", f"Erro ao carregar modelo Whisper: {str(e)}") - - def extract_audio(self, video_path, audio_path): - """Extrai o áudio do vídeo com configurações otimizadas""" - try: - print(f"Extraindo áudio de {video_path}") - - # Primeiro comando - qualidade máxima - command = [ - 'ffmpeg', - '-i', video_path, - '-vn', # Não processar vídeo - '-acodec', 'pcm_s16le', # Codec PCM 16-bit - '-ac', '1', # Mono - '-ar', '16000', # Taxa de amostragem para Whisper - '-af', 'volume=2.0,highpass=f=200,lowpass=f=3000,areverse,silenceremove=start_periods=1:start_duration=1:start_threshold=-60dB,areverse', # Filtros de áudio - '-y', # Sobrescrever arquivo - audio_path - ] - - print("Tentando primeira extração de áudio...") - process = subprocess.run( - command, - capture_output=True, - text=True, - encoding='utf-8' - ) - - if process.returncode != 0: - print("Primeira tentativa falhou, tentando método alternativo...") - # Comando alternativo - mais simples - alt_command = [ - 'ffmpeg', - '-i', video_path, - '-vn', - '-acodec', 'pcm_s16le', - '-ac', '1', - '-ar', '16000', - '-y', - audio_path - ] - process = subprocess.run( - alt_command, - capture_output=True, - text=True, - encoding='utf-8' - ) - - if os.path.exists(audio_path) and os.path.getsize(audio_path) > 0: - print(f"Áudio extraído com sucesso: {os.path.getsize(audio_path)} bytes") - return True - else: - raise Exception("Arquivo de áudio não foi criado ou está vazio") - - except Exception as e: - print(f"Erro detalhado na extração de áudio: {str(e)}") - if process and process.stderr: - print(f"Erro FFmpeg: {process.stderr}") - return False - - def process_audio_with_whisper(self, audio_path, language_code): - try: - import soundfile as sf - print(f"Processando áudio em {language_code}...") - - # Carregar áudio - audio, sample_rate = sf.read(audio_path) - print(f"Áudio carregado: {len(audio)} amostras, taxa de amostragem: {sample_rate}Hz") - - # Normalizar áudio - if audio.dtype == np.int16: - audio = audio.astype(np.float32) / 32768.0 - elif audio.dtype == np.int32: - audio = audio.astype(np.float32) / 2147483648.0 - - # Garantir que o áudio esteja entre -1 e 1 - max_abs = np.max(np.abs(audio)) - if max_abs > 1.0: - audio = audio / max_abs - - # Preparar input features com configurações explícitas - inputs = self.processor( - audio, - sampling_rate=sample_rate, - return_tensors="pt", - padding=True, - do_normalize=True, - return_attention_mask=True - ) - - print("Features de entrada processadas") - - # Mover para GPU se disponível - if torch.cuda.is_available(): - inputs = {k: v.to("cuda") for k, v in inputs.items()} - print("Dados movidos para GPU") - - # Configurar parâmetros de geração corrigidos - generate_kwargs = { - "temperature": 0.0, # Determinístico - "no_speech_threshold": 0.6, - "logprob_threshold": -1.0, - "compression_ratio_threshold": 2.4, - "condition_on_previous_text": True, - "max_initial_timestamp": 1.0, - "return_timestamps": True - } - - if language_code: - generate_kwargs["language"] = language_code - - print("Iniciando geração da transcrição...") - - # Gerar transcrição com timestamps - with torch.no_grad(): - outputs = self.model.generate( - inputs.input_features, - **generate_kwargs - ) - - print("Transcrição gerada, decodificando...") - - # Decodificar saída com timestamp_begin=True - transcription = self.processor.batch_decode( - outputs, - skip_special_tokens=True, - output_offsets=True - )[0] - - print(f"Transcrição decodificada: {len(transcription.text)} caracteres") - - if not transcription.text.strip(): - raise Exception("Transcrição vazia retornada pelo modelo") - - # Formatar segmentos com timestamps - segments = [] - for i, segment in enumerate(transcription.offsets, start=1): - start_time = self.format_timestamp(segment['timestamp'][0]) - end_time = self.format_timestamp(segment['timestamp'][1]) - text = segment['text'].strip() - - if text: # Só adicionar se houver texto - segment_str = f"{i}\n{start_time} --> {end_time}\n{text}\n\n" - segments.append(segment_str) - - print(f"Segmentos formatados: {len(segments)}") - return segments - - except Exception as e: - print(f"Erro detalhado no processamento do áudio: {str(e)}") - raise Exception(f"Erro no processamento do áudio: {str(e)}") - - def format_timestamp(self, seconds): - """Converte segundos em formato de timestamp SRT (HH:MM:SS,mmm)""" - hours = int(seconds // 3600) - minutes = int((seconds % 3600) // 60) - seconds = seconds % 60 - milliseconds = int((seconds % 1) * 1000) - seconds = int(seconds) - - return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}" - - - def format_whisper_output(self, transcription): - """Formata a saída do Whisper em formato SRT""" - segments = [] - pattern = r"\[(\d+:\d+\.\d+) --> (\d+:\d+\.\d+)\](.*?)(?=\[|$)" - - matches = re.finditer(pattern, transcription, re.DOTALL) - - for idx, match in enumerate(matches, 1): - start_time = match.group(1) - end_time = match.group(2) - text = match.group(3).strip() - - # Converter para formato SRT - start_time = self.convert_timestamp_to_srt(start_time) - end_time = self.convert_timestamp_to_srt(end_time) - - segment = f"{idx}\n{start_time} --> {end_time}\n{text}\n\n" - segments.append(segment) - - return segments - - def convert_timestamp_to_srt(self, timestamp): - """Converte timestamp do Whisper para formato SRT""" - # Converter MM:SS.ms para HH:MM:SS,mmm - minutes, seconds = timestamp.split(":") - seconds, milliseconds = seconds.split(".") - - hours = int(minutes) // 60 - minutes = int(minutes) % 60 - - return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}" - - def process_video(self): - try: - # Extrair áudio - audio_path = "temp_audio.wav" - print("Iniciando extração de áudio...") - - if not self.extract_audio(self.video_path.get(), audio_path): - raise Exception("Falha na extração do áudio") - - print("Áudio extraído com sucesso") - - # Obter código do idioma - selected_name = self.selected_language.get() - language_code = self.languages.get(selected_name, 'en') - print(f"Idioma selecionado: {selected_name} ({language_code})") - - # Processar áudio com Whisper - print("Iniciando reconhecimento de fala...") - self.subtitles_list = self.process_audio_with_whisper(audio_path, language_code) - - if not self.subtitles_list: - raise Exception("Nenhum texto foi reconhecido") - - print(f"Texto reconhecido com sucesso: {len(self.subtitles_list)} segmentos") - - # Mostrar legendas na interface - self.root.after(0, self.update_subtitle_text, ''.join(self.subtitles_list)) - - except Exception as e: - print(f"Erro no processamento: {str(e)}") - self.root.after(0, messagebox.showerror, "Erro", f"Erro ao gerar legendas: {str(e)}") - - finally: - # Limpar - self.root.after(0, self.progress.stop) - if self.video is not None: - self.video.release() - try: - if os.path.exists(audio_path): - print(f"Removendo arquivo temporário: {audio_path}") - os.remove(audio_path) - except Exception as e: - print(f"Erro ao remover arquivo temporário: {str(e)}") - - def update_subtitle_text(self, text): - self.subtitle_text.delete(1.0, tk.END) - self.subtitle_text.insert(tk.END, text) - - def save_subtitles(self): - try: - # Pegar texto atual - current_text = self.subtitle_text.get(1.0, tk.END).strip() - - # Validar formato básico das legendas - if not self.validate_subtitle_format(current_text): - raise ValueError("Formato de legendas inválido. Mantenha o formato: número + tempo + texto") - - # Salvar em arquivo - output_path = os.path.splitext(self.video_path.get())[0] + ".srt" - with open(output_path, 'w', encoding='utf-8') as f: - f.write(current_text) - - messagebox.showinfo("Sucesso", f"Legendas salvas com sucesso em:\n{output_path}") - - except Exception as e: - messagebox.showerror("Erro", f"Erro ao salvar legendas: {str(e)}") - - def validate_subtitle_format(self, text): - """Validação melhorada do formato das legendas""" - if not text.strip(): - return False - - lines = text.split('\n') - i = 0 - - while i < len(lines): - if not lines[i].strip(): - i += 1 - continue - - # Validar número da legenda - if not lines[i].strip().isdigit(): - return False - - # Validar formato do tempo - i += 1 - if i >= len(lines): - return False - - time_line = lines[i].strip() - if not (' --> ' in time_line and - time_line.count(':') == 4 and - len(time_line.split(' --> ')) == 2): - return False - - # Validar texto da legenda - i += 1 - if i >= len(lines) or not lines[i].strip(): - return False - - i += 1 - - return True - -if __name__ == "__main__": - root = tk.Tk() - app = VideoSubtitleApp(root) +import tkinter as tk +from tkinter import ttk, scrolledtext, filedialog, messagebox +import threading +import whisper +import cv2 +from datetime import timedelta +import os +import subprocess +import numpy as np + +class VideoSubtitleApp: + def __init__(self, root): + self.root = root + self.root.title("Extrator de Legendas") + self.root.geometry("900x700") + + # Variáveis + self.video_path = tk.StringVar() + self.video_info = tk.StringVar() + self.selected_language = tk.StringVar(value='English') + self.status_var = tk.StringVar(value="Pronto") + self.subtitles_list = [] + + # Dicionário de línguas + self.languages = { + 'Português (Brasil)': 'pt', + 'Português (Portugal)': 'pt', + 'English': 'en', + 'Español': 'es', + 'Français': 'fr', + 'Deutsch': 'de', + 'Italiano': 'it' + } + + # Criar interface + self.create_widgets() + + # Inicializar modelo em thread separada + self.model_ready = False + threading.Thread(target=self.initialize_whisper, daemon=True).start() + + def initialize_whisper(self): + """Inicializa o modelo Whisper""" + try: + self.status_var.set("Carregando modelo...") + # Usar modelo base para melhor equilíbrio + self.model = whisper.load_model("base") + self.model_ready = True + self.status_var.set("Modelo carregado com sucesso") + self.generate_button.config(state='normal') + except Exception as e: + self.status_var.set("Erro ao carregar modelo") + messagebox.showerror("Erro", f"Erro ao carregar modelo Whisper:\n{str(e)}") + + def create_widgets(self): + """Cria a interface gráfica""" + # Frame principal + main_frame = ttk.Frame(self.root, padding=(10, 10, 10, 10)) + main_frame.grid(row=0, column=0, sticky="nsew") + + # Configurar grid + self.root.grid_rowconfigure(0, weight=1) + self.root.grid_columnconfigure(0, weight=1) + main_frame.grid_columnconfigure(0, weight=1) + + # Frame superior + top_frame = ttk.Frame(main_frame) + top_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10)) + + # Botões e controles + ttk.Button(top_frame, text="📂 Selecionar Vídeo", + command=self.select_file).pack(side=tk.LEFT, padx=5) + + ttk.Label(top_frame, text="🌐 Idioma:").pack(side=tk.LEFT, padx=5) + + language_combo = ttk.Combobox(top_frame, + values=list(self.languages.keys()), + textvariable=self.selected_language, + state='readonly', + width=20) + language_combo.pack(side=tk.LEFT, padx=5) + + # Caminho do arquivo + path_frame = ttk.LabelFrame(main_frame, text="Arquivo Selecionado") + path_frame.grid(row=1, column=0, sticky="ew", pady=(0, 10)) + ttk.Label(path_frame, textvariable=self.video_path, + wraplength=800).grid(row=0, column=0, padx=5, pady=5) + + # Informações do vídeo + info_frame = ttk.LabelFrame(main_frame, text="Informações do Vídeo") + info_frame.grid(row=2, column=0, sticky="ew", pady=(0, 10)) + ttk.Label(info_frame, textvariable=self.video_info).grid(row=0, column=0, padx=5, pady=5) + + # Botões de ação + button_frame = ttk.Frame(main_frame) + button_frame.grid(row=3, column=0, pady=(0, 10)) + + self.generate_button = ttk.Button(button_frame, text="🎬 Gerar Legendas", + command=self.generate_subtitles, + state='disabled') + self.generate_button.pack(side=tk.LEFT, padx=5) + + self.save_button = ttk.Button(button_frame, text="💾 Salvar", + command=self.save_subtitles, + state='disabled') + self.save_button.pack(side=tk.LEFT, padx=5) + + # Barra de progresso + self.progress = ttk.Progressbar(main_frame, mode='indeterminate') + self.progress.grid(row=4, column=0, sticky="ew", pady=(0, 10)) + + # Editor de legendas + editor_frame = ttk.LabelFrame(main_frame, text="Editor de Legendas") + editor_frame.grid(row=5, column=0, sticky="nsew", pady=(0, 10)) + editor_frame.grid_columnconfigure(0, weight=1) + editor_frame.grid_rowconfigure(0, weight=1) + + self.subtitle_text = scrolledtext.ScrolledText( + editor_frame, + wrap=tk.WORD, + font=('Consolas', 10) + ) + self.subtitle_text.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) + + # Barra de status + status_frame = ttk.Frame(main_frame) + status_frame.grid(row=6, column=0, sticky="ew") + + ttk.Label(status_frame, textvariable=self.status_var, relief=tk.SUNKEN).grid( + row=0, column=0, sticky="ew") + status_frame.grid_columnconfigure(0, weight=1) + + def select_file(self): + """Seleciona arquivo de vídeo""" + filename = filedialog.askopenfilename( + title="Selecionar Vídeo", + filetypes=[ + ("Arquivos de Vídeo", "*.mp4 *.mkv *.avi"), + ("Todos os Arquivos", "*.*") + ] + ) + + if filename: + self.video_path.set(filename) + self.load_video_info(filename) + + def load_video_info(self, filename): + """Carrega informações do vídeo""" + try: + cap = cv2.VideoCapture(filename) + fps = cap.get(cv2.CAP_PROP_FPS) + frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + duration = frame_count / fps + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + info = f"Duração: {str(timedelta(seconds=int(duration)))}\n" + info += f"Resolução: {width}x{height}\n" + info += f"FPS: {fps:.2f}\n" + info += f"Formato: {os.path.splitext(filename)[1]}" + + self.video_info.set(info) + cap.release() + + except Exception as e: + messagebox.showerror("Erro", f"Erro ao carregar vídeo: {str(e)}") + + def extract_audio(self, video_path, audio_path): + """Extrai o áudio do vídeo""" + try: + command = [ + 'ffmpeg', + '-i', video_path, + '-vn', + '-acodec', 'pcm_s16le', + '-ar', '16000', + '-ac', '1', + '-y', + audio_path + ] + + process = subprocess.run( + command, + capture_output=True, + text=True + ) + + return os.path.exists(audio_path) and os.path.getsize(audio_path) > 0 + + except Exception as e: + print(f"Erro na extração de áudio: {str(e)}") + return False + + def process_audio_with_whisper(self, audio_path, language_code): + """Processa o áudio usando Whisper""" + try: + # Configurar opções do Whisper + options = { + "language": language_code, + "task": "transcribe", + "verbose": False + } + + # Realizar transcrição + result = self.model.transcribe(audio_path, **options) + + # Processar segmentos + segments = [] + for i, segment in enumerate(result["segments"], 1): + start_time = segment["start"] + end_time = segment["end"] + text = segment["text"].strip() + + if text: + segment_str = f"{i}\n" + segment_str += f"{self.format_timestamp(start_time)} --> {self.format_timestamp(end_time)}\n" + segment_str += f"{text}\n\n" + segments.append(segment_str) + + return segments + + except Exception as e: + print(f"Erro detalhado no processamento do áudio: {str(e)}") + raise + + def format_timestamp(self, seconds): + """Converte segundos para formato SRT""" + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = int(seconds % 60) + millisecs = int((seconds * 1000) % 1000) + + return f"{hours:02d}:{minutes:02d}:{secs:02d},{millisecs:03d}" + + def generate_subtitles(self): + """Inicia processo de geração de legendas""" + if not self.video_path.get(): + messagebox.showwarning("Aviso", "Selecione um vídeo primeiro.") + return + + if not self.model_ready: + messagebox.showwarning("Aviso", "Aguarde o modelo ser carregado.") + return + + self.progress.start() + self.generate_button.config(state='disabled') + self.save_button.config(state='disabled') + threading.Thread(target=self.process_video, daemon=True).start() + + def process_video(self): + """Processa o vídeo e gera legendas""" + audio_path = "temp_audio.wav" + try: + self.status_var.set("Extraindo áudio...") + + if not self.extract_audio(self.video_path.get(), audio_path): + raise Exception("Falha na extração do áudio") + + self.status_var.set("Processando áudio...") + language = self.languages[self.selected_language.get()] + + self.subtitles_list = self.process_audio_with_whisper(audio_path, language) + + if not self.subtitles_list: + raise Exception("Nenhuma legenda gerada") + + self.status_var.set("Legendas geradas com sucesso!") + self.root.after(0, self.update_subtitle_text) + + except Exception as e: + self.status_var.set("Erro no processamento") + messagebox.showerror("Erro", str(e)) + + finally: + self.progress.stop() + self.generate_button.config(state='normal') + try: + if os.path.exists(audio_path): + os.remove(audio_path) + except: + pass + + def update_subtitle_text(self): + """Atualiza o texto das legendas na interface""" + self.subtitle_text.delete('1.0', tk.END) + self.subtitle_text.insert('1.0', ''.join(self.subtitles_list)) + self.save_button.config(state='normal') + + def save_subtitles(self): + """Salva as legendas em arquivo""" + try: + output_path = os.path.splitext(self.video_path.get())[0] + ".srt" + with open(output_path, 'w', encoding='utf-8') as f: + f.write(self.subtitle_text.get('1.0', tk.END)) + messagebox.showinfo("Sucesso", f"Legendas salvas em:\n{output_path}") + except Exception as e: + messagebox.showerror("Erro", f"Erro ao salvar legendas: {str(e)}") + +if __name__ == "__main__": + root = tk.Tk() + app = VideoSubtitleApp(root) root.mainloop() \ No newline at end of file