Files
ollama-translate-model/translator_gui.py
T
dinlo 4655401fd3 Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:45:38 +08:00

484 lines
17 KiB
Python

import customtkinter as ctk
import tkinter as tk
from tkinter import filedialog, messagebox
import subprocess
import re
import threading
from pathlib import Path
import os
import tempfile
class TranslatorApp(ctk.CTk):
def __init__(self):
super().__init__()
# Window configuration
self.title("Ollama Translator - Русский переводчик")
self.geometry("1000x700")
# Set theme
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
# Variables
self.is_translating = False
self.current_file = None
# Create UI
self.create_widgets()
# Enable drag and drop (using tkinterdnd2 if available)
try:
self.setup_drag_drop()
except:
pass # Drag and drop not available, skip it
def create_widgets(self):
# Main container
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
# Header
header_frame = ctk.CTkFrame(self, fg_color="transparent")
header_frame.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="ew")
title_label = ctk.CTkLabel(
header_frame,
text="🌐 Ollama Translator",
font=ctk.CTkFont(size=24, weight="bold")
)
title_label.pack(side="left")
subtitle_label = ctk.CTkLabel(
header_frame,
text="Uncensored перевод на русский язык",
font=ctk.CTkFont(size=12),
text_color="gray"
)
subtitle_label.pack(side="left", padx=10)
# Theme switch
self.theme_switch = ctk.CTkSwitch(
header_frame,
text="🌙 Темная тема",
command=self.toggle_theme,
onvalue="dark",
offvalue="light"
)
self.theme_switch.pack(side="right")
self.theme_switch.select()
# Main content area
content_frame = ctk.CTkFrame(self)
content_frame.grid(row=1, column=0, padx=20, pady=10, sticky="nsew")
content_frame.grid_columnconfigure(0, weight=1)
content_frame.grid_columnconfigure(1, weight=1)
content_frame.grid_rowconfigure(1, weight=1)
# Source text area
source_label = ctk.CTkLabel(
content_frame,
text="📝 Исходный текст",
font=ctk.CTkFont(size=14, weight="bold")
)
source_label.grid(row=0, column=0, padx=10, pady=(10, 5), sticky="w")
self.source_text = ctk.CTkTextbox(
content_frame,
font=ctk.CTkFont(size=13),
wrap="word"
)
self.source_text.grid(row=1, column=0, padx=(10, 5), pady=(0, 10), sticky="nsew")
# Character counter for source
self.source_counter = ctk.CTkLabel(
content_frame,
text="Символов: 0",
font=ctk.CTkFont(size=10),
text_color="gray"
)
self.source_counter.grid(row=2, column=0, padx=10, pady=(0, 5), sticky="w")
# Bind text change event
self.source_text.bind("<KeyRelease>", self.update_char_count)
# Translation text area
translation_label = ctk.CTkLabel(
content_frame,
text="🇷🇺 Перевод на русский",
font=ctk.CTkFont(size=14, weight="bold")
)
translation_label.grid(row=0, column=1, padx=10, pady=(10, 5), sticky="w")
self.translation_text = ctk.CTkTextbox(
content_frame,
font=ctk.CTkFont(size=13),
wrap="word"
)
self.translation_text.grid(row=1, column=1, padx=(5, 10), pady=(0, 10), sticky="nsew")
# Character counter for translation
self.translation_counter = ctk.CTkLabel(
content_frame,
text="Символов: 0",
font=ctk.CTkFont(size=10),
text_color="gray"
)
self.translation_counter.grid(row=2, column=1, padx=10, pady=(0, 5), sticky="w")
# Control buttons
button_frame = ctk.CTkFrame(self, fg_color="transparent")
button_frame.grid(row=2, column=0, padx=20, pady=10, sticky="ew")
# Left side buttons
left_buttons = ctk.CTkFrame(button_frame, fg_color="transparent")
left_buttons.pack(side="left", fill="x", expand=True)
self.open_btn = ctk.CTkButton(
left_buttons,
text="📁 Открыть файл",
command=self.open_file,
width=140,
height=35
)
self.open_btn.pack(side="left", padx=5)
self.paste_btn = ctk.CTkButton(
left_buttons,
text="📋 Вставить",
command=self.paste_text,
width=120,
height=35
)
self.paste_btn.pack(side="left", padx=5)
self.clear_btn = ctk.CTkButton(
left_buttons,
text="🗑️ Очистить",
command=self.clear_all,
width=120,
height=35,
fg_color="gray40",
hover_color="gray30"
)
self.clear_btn.pack(side="left", padx=5)
# Center button (Translate)
self.translate_btn = ctk.CTkButton(
button_frame,
text="🔄 Перевести",
command=self.translate_text,
width=180,
height=40,
font=ctk.CTkFont(size=14, weight="bold"),
fg_color="#1f6aa5",
hover_color="#144870"
)
self.translate_btn.pack(side="left", padx=20)
# Right side buttons
right_buttons = ctk.CTkFrame(button_frame, fg_color="transparent")
right_buttons.pack(side="right", fill="x", expand=True)
self.copy_btn = ctk.CTkButton(
right_buttons,
text="📄 Копировать",
command=self.copy_translation,
width=140,
height=35
)
self.copy_btn.pack(side="right", padx=5)
self.save_btn = ctk.CTkButton(
right_buttons,
text="💾 Сохранить",
command=self.save_translation,
width=140,
height=35
)
self.save_btn.pack(side="right", padx=5)
# Status bar
self.status_frame = ctk.CTkFrame(self, height=30)
self.status_frame.grid(row=3, column=0, padx=20, pady=(0, 20), sticky="ew")
self.status_label = ctk.CTkLabel(
self.status_frame,
text="✅ Готов к работе",
font=ctk.CTkFont(size=11)
)
self.status_label.pack(side="left", padx=10, pady=5)
self.progress_bar = ctk.CTkProgressBar(self.status_frame, width=200)
self.progress_bar.pack(side="right", padx=10, pady=5)
self.progress_bar.set(0)
# Keyboard shortcuts
self.bind("<Control-o>", lambda e: self.open_file())
self.bind("<Control-v>", lambda e: self.paste_text())
self.bind("<Control-s>", lambda e: self.save_translation())
self.bind("<Control-Return>", lambda e: self.translate_text())
self.bind("<F5>", lambda e: self.translate_text())
def setup_drag_drop(self):
"""Setup drag and drop for files (requires tkinterdnd2)"""
try:
from tkinterdnd2 import DND_FILES, TkinterDnD
# This would require tkinterdnd2 package
# For now, we'll skip this feature
pass
except ImportError:
# tkinterdnd2 not available, drag-drop disabled
pass
def on_drop(self, event):
"""Handle file drop"""
files = self.tk.splitlist(event.data)
if files:
self.load_file(files[0])
def toggle_theme(self):
"""Toggle between dark and light theme"""
if self.theme_switch.get() == "dark":
ctk.set_appearance_mode("dark")
else:
ctk.set_appearance_mode("light")
def update_char_count(self, event=None):
"""Update character counter"""
text = self.source_text.get("1.0", "end-1c")
char_count = len(text)
word_count = len(text.split())
self.source_counter.configure(text=f"Символов: {char_count} | Слов: {word_count}")
def update_translation_count(self):
"""Update translation character counter"""
text = self.translation_text.get("1.0", "end-1c")
char_count = len(text)
word_count = len(text.split())
self.translation_counter.configure(text=f"Символов: {char_count} | Слов: {word_count}")
def open_file(self):
"""Open file dialog and load file"""
file_path = filedialog.askopenfilename(
title="Выберите файл для перевода",
filetypes=[
("Текстовые файлы", "*.txt"),
("Markdown файлы", "*.md"),
("Все файлы", "*.*")
]
)
if file_path:
self.load_file(file_path)
def load_file(self, file_path):
"""Load file content"""
try:
# Try different encodings
encodings = ['utf-8', 'cp1251', 'latin-1']
content = None
for encoding in encodings:
try:
with open(file_path, 'r', encoding=encoding) as f:
content = f.read()
break
except UnicodeDecodeError:
continue
if content is None:
messagebox.showerror("Ошибка", "Не удалось прочитать файл")
return
self.source_text.delete("1.0", "end")
self.source_text.insert("1.0", content)
self.current_file = file_path
self.update_char_count()
self.update_status(f"📁 Загружен: {Path(file_path).name}")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось открыть файл:\n{str(e)}")
def paste_text(self):
"""Paste text from clipboard"""
try:
clipboard_text = self.clipboard_get()
self.source_text.delete("1.0", "end")
self.source_text.insert("1.0", clipboard_text)
self.update_char_count()
self.update_status("📋 Текст вставлен из буфера обмена")
except:
messagebox.showwarning("Предупреждение", "Буфер обмена пуст")
def clear_all(self):
"""Clear all text fields"""
self.source_text.delete("1.0", "end")
self.translation_text.delete("1.0", "end")
self.current_file = None
self.update_char_count()
self.update_translation_count()
self.update_status("🗑️ Поля очищены")
def translate_text(self):
"""Translate text using ollama model"""
if self.is_translating:
return
text = self.source_text.get("1.0", "end-1c").strip()
if not text:
messagebox.showwarning("Предупреждение", "Введите текст для перевода")
return
# Run translation in separate thread
thread = threading.Thread(target=self._translate_worker, args=(text,))
thread.daemon = True
thread.start()
def _translate_worker(self, text):
"""Worker thread for translation"""
self.is_translating = True
self.update_status("⏳ Перевод...")
self.translate_btn.configure(state="disabled", text="⏳ Перевожу...")
try:
# Split text into chunks if it's too long
chunk_size = 800 # Characters per chunk
chunks = self._split_text_into_chunks(text, chunk_size)
total_chunks = len(chunks)
translated_parts = []
for i, chunk in enumerate(chunks):
# Update progress
progress = (i + 1) / total_chunks
self.progress_bar.set(progress)
if total_chunks > 1:
self.after(0, lambda p=i+1, t=total_chunks:
self.update_status(f"⏳ Перевод части {p}/{t}..."))
# Translate chunk
result = subprocess.run(
['ollama', 'run', 'translator', chunk],
capture_output=True,
text=True,
encoding='utf-8',
timeout=120
)
# Filter output
output = result.stdout
output = re.sub(r'<think>.*?</think>', '', output, flags=re.DOTALL)
output = '\n'.join(line.strip() for line in output.split('\n') if line.strip())
translated_parts.append(output)
# Combine all parts
final_translation = '\n\n'.join(translated_parts)
# Update UI in main thread
self.after(0, self._update_translation, final_translation)
except subprocess.TimeoutExpired:
self.after(0, lambda: messagebox.showerror("Ошибка", "Превышено время ожидания перевода"))
self.after(0, lambda: self.update_status("❌ Ошибка: таймаут"))
except Exception as e:
self.after(0, lambda: messagebox.showerror("Ошибка", f"Ошибка перевода:\n{str(e)}"))
self.after(0, lambda: self.update_status("❌ Ошибка перевода"))
finally:
self.is_translating = False
self.progress_bar.set(0)
self.translate_btn.configure(state="normal", text="🔄 Перевести")
def _split_text_into_chunks(self, text, chunk_size=800):
"""Split text into chunks, trying to break at sentence boundaries"""
if len(text) <= chunk_size:
return [text]
chunks = []
current_chunk = ""
# Split by sentences (simple approach)
sentences = re.split(r'([.!?]\s+|\n\n)', text)
for i in range(0, len(sentences), 2):
sentence = sentences[i]
separator = sentences[i + 1] if i + 1 < len(sentences) else ""
# If adding this sentence exceeds chunk size, start new chunk
if len(current_chunk) + len(sentence) + len(separator) > chunk_size and current_chunk:
chunks.append(current_chunk.strip())
current_chunk = sentence + separator
else:
current_chunk += sentence + separator
# Add remaining text
if current_chunk.strip():
chunks.append(current_chunk.strip())
# If no sentence boundaries found, split by character count
if not chunks:
chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
return chunks
def _update_translation(self, translation):
"""Update translation text in UI"""
self.translation_text.delete("1.0", "end")
self.translation_text.insert("1.0", translation)
self.update_translation_count()
self.progress_bar.set(1.0)
self.update_status("✅ Перевод завершен")
def copy_translation(self):
"""Copy translation to clipboard"""
translation = self.translation_text.get("1.0", "end-1c")
if translation.strip():
self.clipboard_clear()
self.clipboard_append(translation)
self.update_status("📄 Перевод скопирован в буфер обмена")
else:
messagebox.showwarning("Предупреждение", "Нет текста для копирования")
def save_translation(self):
"""Save translation to file"""
translation = self.translation_text.get("1.0", "end-1c")
if not translation.strip():
messagebox.showwarning("Предупреждение", "Нет текста для сохранения")
return
# Suggest filename
if self.current_file:
default_name = Path(self.current_file).stem + "_ru.txt"
else:
default_name = "translation_ru.txt"
file_path = filedialog.asksaveasfilename(
title="Сохранить перевод",
defaultextension=".txt",
initialfile=default_name,
filetypes=[
("Текстовые файлы", "*.txt"),
("Markdown файлы", "*.md"),
("Все файлы", "*.*")
]
)
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(translation)
self.update_status(f"💾 Сохранено: {Path(file_path).name}")
messagebox.showinfo("Успех", f"Перевод сохранен:\n{file_path}")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось сохранить файл:\n{str(e)}")
def update_status(self, message):
"""Update status bar message"""
self.status_label.configure(text=message)
if __name__ == "__main__":
app = TranslatorApp()
app.mainloop()