278 lines
10 KiB
Python
Executable File
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())
|