Files
translate_ai/translate_ai.py

278 lines
10 KiB
Python
Executable File

#!/home/pali112/venv/bin/python3
import sys
import subprocess
import requests
import argparse
import json
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QTextBrowser, QPushButton, QLabel,
QProgressBar, QTextEdit)
from PyQt6.QtCore import QThread, pyqtSignal, Qt, QTimer
from PyQt6.QtGui import QFont
# Przejście na oficjalny i bezpieczny endpoint chat/completions dla llama.cpp
LLAMA_URL = "http://127.0.0.1:8081/v1/chat/completions"
class LlamaWorker(QThread):
"""Wątek w tle do strumieniowej komunikacji przez API Chat Completions"""
chunk_received = pyqtSignal(str)
finished_generating = pyqtSignal()
error_occurred = pyqtSignal(str)
def __init__(self, text, system_prompt):
super().__init__()
self.text = text
self.system_prompt = system_prompt
self._is_stopped = False
def stop(self):
self._is_stopped = True
def run(self):
payload = {
"messages": [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": self.text}
],
"temperature": 0.3,
"stream": True # Strumieniowanie tokenów na żywo
}
try:
with requests.post(LLAMA_URL, json=payload, timeout=10, stream=True) as response:
response.raise_for_status()
for line in response.iter_lines():
if self._is_stopped:
break
if line:
decoded = line.decode('utf-8').strip()
if decoded.startswith("data: "):
data_str = decoded[6:].strip()
if data_str == "[DONE]":
break
try:
data_json = json.loads(data_str)
choices = data_json.get("choices", [])
if choices:
delta = choices[0].get("delta", {})
chunk = delta.get("content", "")
if chunk:
self.chunk_received.emit(chunk)
except json.JSONDecodeError:
pass
self.finished_generating.emit()
except requests.exceptions.RequestException as e:
self.error_occurred.emit(f"\nBłąd komunikacji z serwerem llama.cpp:\n{e}")
class PotNativeApp(QMainWindow):
def __init__(self, mode="clip"):
super().__init__()
self.monitor_active = False
self.last_clipboard_text = ""
# Pobranie systemowego schowka Qt
self.clipboard = QApplication.clipboard()
self.init_ui()
# Podpięcie natywnego zdarzenia zmiany zawartości schowka
self.clipboard.dataChanged.connect(self.on_clipboard_changed)
if mode == "ocr":
QTimer.singleShot(200, self.trigger_ocr)
elif mode == "clip":
startup_text = self.clipboard.text().strip()
if startup_text:
self.last_clipboard_text = startup_text
self.process_text(startup_text)
def init_ui(self):
self.setWindowTitle("Pot-Native v2 (Llama.cpp)")
self.resize(650, 750)
main_widget = QWidget()
layout = QVBoxLayout()
main_widget.setLayout(layout)
self.setCentralWidget(main_widget)
# --- Panel Kontrolny ---
control_layout = QHBoxLayout()
self.btn_ocr = QPushButton("📷 Wykonaj OCR (Zaznacz ekran)")
self.btn_ocr.clicked.connect(self.trigger_ocr)
self.btn_monitor = QPushButton("👁️ Monitorowanie Schowka: OFF")
self.btn_monitor.setCheckable(True)
self.btn_monitor.clicked.connect(self.toggle_monitor)
control_layout.addWidget(self.btn_ocr)
control_layout.addWidget(self.btn_monitor)
layout.addLayout(control_layout)
# --- Prompt ---
self.label_prompt = QLabel("Prompt systemowy (AI):")
self.label_prompt.setFont(QFont("Arial", 9, QFont.Weight.Bold))
self.text_prompt = QTextEdit()
self.text_prompt.setMaximumHeight(65)
self.text_prompt.setPlainText("Jesteś zaawansowanym asystentem AI. Przetłumacz na język polski poniższy tekst, zachowując techniczny i specjalistyczny kontekst. Jeśli widzisz fragmenty kodu lub specyfikacje, nie psuj ich struktury.")
layout.addWidget(self.label_prompt)
layout.addWidget(self.text_prompt)
# --- Oryginał ---
self.label_source = QLabel("Tekst źródłowy:")
self.label_source.setFont(QFont("Arial", 9, QFont.Weight.Bold))
self.text_source = QTextBrowser()
self.text_source.setMaximumHeight(100)
layout.addWidget(self.label_source)
layout.addWidget(self.text_source)
# --- Wynik ---
self.label_result = QLabel("Wynik z LLM:")
self.label_result.setFont(QFont("Arial", 9, QFont.Weight.Bold))
self.text_result = QTextBrowser()
self.text_result.setPlaceholderText("Oczekiwanie na dane...")
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 0)
self.progress_bar.hide()
layout.addWidget(self.label_result)
layout.addWidget(self.text_result)
layout.addWidget(self.progress_bar)
# --- Przyciski dolne ---
btn_layout = QHBoxLayout()
self.btn_stop = QPushButton("🛑 STOP")
self.btn_stop.setDisabled(True)
self.btn_stop.setStyleSheet("background-color: #c62828; color: white; font-weight: bold;")
self.btn_stop.clicked.connect(self.stop_translation)
self.btn_copy = QPushButton("📋 Kopiuj wynik")
self.btn_copy.setDisabled(True)
self.btn_copy.clicked.connect(self.copy_to_clipboard)
self.btn_close = QPushButton("Zamknij")
self.btn_close.clicked.connect(self.close)
btn_layout.addWidget(self.btn_stop)
btn_layout.addWidget(self.btn_copy)
btn_layout.addWidget(self.btn_close)
layout.addLayout(btn_layout)
def toggle_monitor(self):
if self.btn_monitor.isChecked():
self.monitor_active = True
self.btn_monitor.setText("👁️ Monitorowanie Schowka: ON")
self.btn_monitor.setStyleSheet("background-color: #2e7d32; color: white; font-weight: bold;")
self.last_clipboard_text = self.clipboard.text().strip()
else:
self.monitor_active = False
self.btn_monitor.setText("👁️ Monitorowanie Schowka: OFF")
self.btn_monitor.setStyleSheet("")
def on_clipboard_changed(self):
"""Wywoływane automatycznie przez system, gdy schowek się zmienia"""
if not self.monitor_active:
return
text = self.clipboard.text().strip()
if text and text != self.last_clipboard_text:
self.last_clipboard_text = text
self.process_text(text)
def trigger_ocr(self):
self.hide()
QApplication.processEvents()
# Małe opóźnienie dające czas kompozytorowi okien na ukrycie aplikacji
QTimer.singleShot(250, self._execute_ocr_cli)
def _execute_ocr_cli(self):
try:
# Wywołanie maim oraz tesseract
subprocess.run(["maim", "-u", "-s", "/tmp/ocr_select.png"], check=True)
output = subprocess.check_output(["tesseract", "/tmp/ocr_select.png", "stdout", "-l", "pol+eng"], text=True)
text = output.strip()
self.show()
if text:
self.process_text(text)
else:
self.text_source.setText("OCR nie wykrył żadnego tekstu na zaznaczonym obszarze.")
except subprocess.CalledProcessError:
self.show() # Anulowano (np. klawiszem ESC)
except FileNotFoundError:
self.show()
self.text_source.setText("Błąd: Nadal brakuje pakietów systemowych 'maim' lub 'tesseract'. Zainstaluj je przez pacmana.")
def process_text(self, text):
if not text:
return
if hasattr(self, 'worker') and self.worker.isRunning():
self.stop_translation()
self.text_source.setText(text)
self.start_translation(text)
def start_translation(self, text):
self.progress_bar.show()
self.text_result.clear()
self.btn_copy.setDisabled(True)
self.btn_copy.setText("📋 Kopiuj wynik")
self.btn_stop.setDisabled(False)
current_prompt = self.text_prompt.toPlainText().strip()
self.worker = LlamaWorker(text, current_prompt)
self.worker.chunk_received.connect(self.on_chunk)
self.worker.finished_generating.connect(self.on_finished)
self.worker.error_occurred.connect(self.on_error)
self.worker.start()
def on_chunk(self, chunk):
cursor = self.text_result.textCursor()
cursor.movePosition(cursor.MoveOperation.End)
cursor.insertText(chunk)
self.text_result.setTextCursor(cursor)
def stop_translation(self):
if hasattr(self, 'worker') and self.worker.isRunning():
self.worker.stop()
self.text_result.append("\n\n[PRZERWANO GENEROWANIE]")
self.on_finished()
def on_finished(self):
self.progress_bar.hide()
self.btn_stop.setDisabled(True)
self.btn_copy.setDisabled(False)
def on_error(self, error_msg):
self.progress_bar.hide()
self.text_result.append(error_msg)
self.btn_stop.setDisabled(True)
def copy_to_clipboard(self):
text_to_copy = self.text_result.toPlainText()
# Aktualizacja filtra, aby aplikacja nie próbowała przetłumaczyć tego, co sama skopiowała
self.last_clipboard_text = text_to_copy
self.clipboard.setText(text_to_copy)
self.btn_copy.setText("✅ Skopiowano!")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--ocr", action="store_true")
args = parser.parse_args()
mode = "ocr" if args.ocr else "clip"
app = QApplication(sys.argv)
window = PotNativeApp(mode=mode)
window.setWindowFlags(window.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
window.show()
sys.exit(app.exec())