302 lines
11 KiB
Python
302 lines
11 KiB
Python
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() |