Локальный переводчик
Языковая модель:
https://huggingface.co/t-tech/T-lite-it-2.1-GGUF
llama-cpp:
yay -S llama.cpp
python -m venv venv
source venv/bin/activate
pip install requests nltk PySide6
translator.py:
import sys
import os
import re
import time
import subprocess
import requests
import nltk
from nltk.tokenize import sent_tokenize
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget,
QVBoxLayout, QHBoxLayout,
QPushButton, QTextEdit, QProgressBar,
QLabel
)
from PySide6.QtCore import QThread, Signal, Qt
# Configuration
LLAMA_SERVER_URL = "http://127.0.0.1:8080/completion"
SERVER_EXECUTABLE_PATH = '/usr/bin/llama-server'
MODEL_PATH = 'T-lite-it-2.1-Q4_K_M.gguf'
MODEL_PARAMS = {
"repeat_penalty": 1.0,
"temperature": 0.6,
"top_k": 20,
"top_p": 0.95,
}
SERVER_ARGS = [
"-m", MODEL_PATH,
"-c", "4096",
"-t", "4",
"--port", "8080",
"--host", "127.0.0.1"
]
SERVER_STARTUP_TIMEOUT = 300
class TranslationWorker(QThread):
progress = Signal(int)
status_msg = Signal(str)
chunk_done = Signal(str, bool)
finished = Signal(str)
error = Signal(str)
def __init__(self, text):
super().__init__()
self.raw_text = text
self.server_process = None
def run(self):
try:
self.status_msg.emit("Initializing NLTK...")
try:
nltk.data.find('tokenizers/punkt')
except LookupError:
nltk.download('punkt', quiet=True)
nltk.download('punkt_tab', quiet=True)
if not self.is_server_ready():
self.status_msg.emit("Launching llama-server...")
self.server_process = self.start_server()
if not self.server_process:
self.error.emit(f"Failed to launch server at {SERVER_EXECUTABLE_PATH}")
return
paragraphs = [p.strip() for p in re.split(r'\n\s*\n', self.raw_text) if p.strip()]
batches = self.create_batches(paragraphs)
self.status_msg.emit(f"Translating {len(batches)} batches...")
last_p_idx = -1
for i, (p_idx, batch_text) in enumerate(batches):
is_new_paragraph = (p_idx != last_p_idx)
if len(batch_text.split()) < 2:
translated = batch_text
else:
translated = self.translate_batch_api(batch_text)
self.chunk_done.emit(translated + " ", is_new_paragraph)
last_p_idx = p_idx
self.progress.emit(int(((i + 1) / len(batches)) * 100))
self.finished.emit("Complete")
except Exception as e:
self.error.emit(f"Worker Exception: {str(e)}")
finally:
self.cleanup_server()
def is_server_ready(self):
try:
r = requests.get("http://127.0.0.1:8080/health", timeout=2)
return r.status_code == 200
except:
return False
def start_server(self):
if not os.path.exists(MODEL_PATH):
return None
proc = subprocess.Popen(
[SERVER_EXECUTABLE_PATH] + SERVER_ARGS,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
for _ in range(SERVER_STARTUP_TIMEOUT):
if self.is_server_ready():
return proc
time.sleep(1)
return None
def create_batches(self, paragraphs):
batches = []
for p_idx, paragraph in enumerate(paragraphs):
sentences = sent_tokenize(paragraph)
for i in range(0, len(sentences), 3):
batch = " ".join(sentences[i:i+3])
batches.append((p_idx, batch))
return batches
def translate_batch_api(self, batch_text):
prompt_text = (
f"<|im_start|>user\nТы - переводчик. Верни только перевод. Переведи на русский: {batch_text}\n<|im_end|>\n"
f"<|im_start|>assistant\n"
)
payload = {
"prompt": prompt_text,
"repeat_penalty": MODEL_PARAMS['repeat_penalty'],
"temperature": MODEL_PARAMS['temperature'],
"top_k": MODEL_PARAMS['top_k'],
"top_p": MODEL_PARAMS['top_p'],
"stop": ["<|im_end|>", "<|file_separator|>"],
"stream": False
}
try:
res = requests.post(LLAMA_SERVER_URL, json=payload, timeout=120)
res.raise_for_status()
content = res.json().get('content', '').strip()
content = re.sub(r'<think>', '', content, flags=re.DOTALL)
content = re.sub(r'</think>', '', content, flags=re.DOTALL)
return content.strip()
except Exception as e:
return f"[Error: {str(e)[:20]}]"
def cleanup_server(self):
if self.server_process:
self.server_process.terminate()
class TranslatorApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Local Translator")
self.resize(1000, 655)
container = QWidget()
self.main_layout = QVBoxLayout(container)
self.editor_layout = QHBoxLayout()
self.editor_layout.setSpacing(0)
input_container = QWidget()
input_layout = QVBoxLayout(input_container)
input_layout.addWidget(QLabel("Input:"))
self.input_area = QTextEdit()
self.input_area.setAcceptRichText(False)
input_layout.addWidget(self.input_area)
output_container = QWidget()
output_layout = QVBoxLayout(output_container)
output_layout.addWidget(QLabel("Output:"))
self.output_area = QTextEdit()
self.output_area.setReadOnly(True)
output_layout.addWidget(self.output_area)
self.editor_layout.addWidget(input_container)
self.editor_layout.addWidget(output_container)
self.progress_bar = QProgressBar()
self.status_label = QLabel("Ready")
self.btn = QPushButton("Translate")
self.btn.clicked.connect(self.start)
self.main_layout.addLayout(self.editor_layout)
self.main_layout.addWidget(self.progress_bar)
self.main_layout.addWidget(self.status_label)
self.main_layout.addWidget(self.btn)
self.setCentralWidget(container)
def start(self):
text = self.input_area.toPlainText().strip()
if not text:
return
self.btn.setEnabled(False)
self.output_area.clear()
self.progress_bar.setValue(0)
self.status_label.setText("Starting...")
self.worker = TranslationWorker(text)
self.worker.status_msg.connect(self.status_label.setText)
self.worker.progress.connect(self.progress_bar.setValue)
self.worker.chunk_done.connect(self.on_chunk_done)
self.worker.error.connect(self.on_error)
self.worker.finished.connect(self.on_finish)
self.worker.start()
def on_chunk_done(self, text, is_new_paragraph):
if is_new_paragraph and self.output_area.toPlainText().strip():
self.output_area.insertPlainText("\n\n")
self.output_area.insertPlainText(text)
self.output_area.verticalScrollBar().setValue(
self.output_area.verticalScrollBar().maximum()
)
def on_error(self, msg):
self.status_label.setText(msg)
self.btn.setEnabled(True)
def on_finish(self):
self.status_label.setText("Success")
self.btn.setEnabled(True)
def closeEvent(self, event):
if hasattr(self, 'worker') and self.worker.isRunning():
self.worker.cleanup_server()
self.worker.terminate()
self.worker.wait()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyleSheet("""
QTextEdit {
border: 1px solid #c0c0c0;
border-radius: 4px;
padding: 8px;
}
QPushButton {
padding: 10px;
border: 1px solid #c0c0c0;
border-radius: 4px;
}
""")
from PySide6.QtGui import QFont
app.setFont(QFont("Adwaita Sans", 13))
window = TranslatorApp()
window.show()
sys.exit(app.exec())