#!/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) app.setStyleSheet("QWidget { font-size: 13pt; }") window = PotNativeApp(mode=mode) window.setWindowFlags(window.windowFlags() | Qt.WindowType.WindowStaysOnTopHint) window.show() sys.exit(app.exec())