marker-pdf-agent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ """Folder worker for marker-pdf document conversion."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 550 700">
3
+ <!-- Derived from SVG Repo file-markdown icon, MIT License: https://www.svgrepo.com/svg/332064/file-markdown -->
4
+ <!-- Generator: Adobe Illustrator 30.6.0, SVG Export Plug-In . SVG Version: 2.1.4 Build 109) -->
5
+ <defs>
6
+ <style>
7
+ .st0 {
8
+ fill: #fff;
9
+ }
10
+ </style>
11
+ </defs>
12
+ <path class="st0" d="M542.66,175.55c4.69,4.69,7.34,11.02,7.34,17.66v481.8c0,13.83-11.17,25-25,25H25c-13.83,0-25-11.17-25-25V25C0,11.17,11.17,0,25,0h331.8c6.64,0,13.05,2.66,17.73,7.34l168.13,168.2h0ZM492.34,204.69L345.31,57.66v147.03h147.03ZM207.91,419.48l46.18,103.88c2.01,4.51,6.48,7.42,11.42,7.42h18.8c4.94,0,9.42-2.91,11.43-7.43l46.17-104.18v123.02c0,6.9,5.6,12.5,12.5,12.5h21.37c6.9,0,12.5-5.6,12.5-12.5v-212.5c0-6.9-5.6-12.5-12.5-12.5h-27.15c-4.98,0-9.48,2.95-11.46,7.52l-62.09,142.64-62.09-142.65c-1.99-4.56-6.49-7.51-11.46-7.51h-27.3c-6.9,0-12.5,5.6-12.5,12.5v212.5c0,6.9,5.6,12.5,12.5,12.5h21.2c6.9,0,12.5-5.6,12.5-12.5v-122.71Z"/>
13
+ </svg>
@@ -0,0 +1,229 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import platform
6
+ import subprocess
7
+ import sys
8
+ import threading
9
+ from importlib import resources
10
+ from pathlib import Path
11
+
12
+ from marker_pdf_agent.worker import (
13
+ WorkerManager,
14
+ WorkerStatus,
15
+ build_config_for_root,
16
+ list_ollama_models,
17
+ save_agent_config,
18
+ )
19
+
20
+
21
+ def run_tray_app(manager: WorkerManager, args: argparse.Namespace, config_path: Path) -> None:
22
+ hide_macos_dock_icon()
23
+ try:
24
+ from PySide6.QtCore import QObject, Qt, Signal
25
+ from PySide6.QtGui import QIcon, QPainter, QPixmap
26
+ from PySide6.QtWidgets import (
27
+ QApplication,
28
+ QFileDialog,
29
+ QMenu,
30
+ QMessageBox,
31
+ QSystemTrayIcon,
32
+ )
33
+ except ImportError as exc:
34
+ raise RuntimeError('install GUI dependencies with: venv/bin/python -m pip install ".[gui]"') from exc
35
+
36
+ class StatusBridge(QObject):
37
+ status_changed = Signal(object)
38
+ models_changed = Signal(object)
39
+
40
+ def make_icon() -> QIcon:
41
+ icon_path = resources.files("marker_pdf_agent").joinpath("assets/file-markdown.svg")
42
+ with resources.as_file(icon_path) as path:
43
+ icon = QIcon(str(path))
44
+ if not icon.isNull():
45
+ return icon
46
+
47
+ pixmap = QPixmap(32, 32)
48
+ pixmap.fill(Qt.GlobalColor.transparent)
49
+ painter = QPainter(pixmap)
50
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
51
+ painter.setBrush(Qt.GlobalColor.black)
52
+ painter.setPen(Qt.PenStyle.NoPen)
53
+ painter.drawRoundedRect(5, 4, 20, 24, 3, 3)
54
+ painter.setBrush(Qt.GlobalColor.white)
55
+ painter.drawRect(9, 10, 12, 2)
56
+ painter.drawRect(9, 15, 12, 2)
57
+ painter.drawRect(9, 20, 8, 2)
58
+ painter.end()
59
+ return QIcon(pixmap)
60
+
61
+ def open_path(path: Path) -> None:
62
+ path.mkdir(parents=True, exist_ok=True)
63
+ system = platform.system()
64
+ if system == "Darwin":
65
+ subprocess.Popen(["open", str(path)])
66
+ elif system == "Windows":
67
+ os.startfile(path) # type: ignore[attr-defined]
68
+ else:
69
+ subprocess.Popen(["xdg-open", str(path)])
70
+
71
+ def quit_app() -> None:
72
+ manager.stop_event.set()
73
+ app.quit()
74
+
75
+ def add_folder() -> None:
76
+ folder = QFileDialog.getExistingDirectory(None, "Add monitored folder", str(Path.home()))
77
+ if not folder:
78
+ return
79
+ root = Path(folder).resolve()
80
+ config = build_config_for_root(args, root)
81
+ if manager.add_config(config):
82
+ save_current_config()
83
+ refresh_menu()
84
+ else:
85
+ QMessageBox.information(None, "Already monitored", f"{root} is already monitored.")
86
+
87
+ def remove_folder(root: Path) -> None:
88
+ if manager.remove_root(root):
89
+ save_current_config()
90
+ refresh_menu()
91
+ else:
92
+ QMessageBox.information(None, "Cannot remove folder", "At least one folder must stay monitored.")
93
+
94
+ available_ollama_models: list[str] = []
95
+ selected_ollama_model = args.ollama_model
96
+
97
+ def save_current_config() -> None:
98
+ save_agent_config(config_path, manager.roots(), selected_ollama_model)
99
+
100
+ def select_ollama_model(model: str | None) -> None:
101
+ nonlocal selected_ollama_model
102
+ selected_ollama_model = model
103
+ args.ollama_model = model
104
+ args.no_ollama = model is None
105
+ manager.set_ollama_model(model)
106
+ save_current_config()
107
+ refresh_menu()
108
+
109
+ def refresh_ollama_models() -> None:
110
+ def load_models() -> None:
111
+ bridge.models_changed.emit(list_ollama_models())
112
+
113
+ threading.Thread(target=load_models, name="marker-ollama-models", daemon=True).start()
114
+
115
+ def update_ollama_models(models: list[str]) -> None:
116
+ nonlocal available_ollama_models
117
+ available_ollama_models = models
118
+ if selected_ollama_model and selected_ollama_model not in available_ollama_models:
119
+ available_ollama_models = [selected_ollama_model, *available_ollama_models]
120
+ refresh_menu()
121
+
122
+ status_action = None
123
+ queue_action = None
124
+
125
+ def format_status(status: WorkerStatus) -> tuple[str, str]:
126
+ if status.stopping:
127
+ state = "Stopping"
128
+ elif status.current_document:
129
+ state = "Converting"
130
+ else:
131
+ state = "Idle"
132
+ return state, f"Queue: {status.queue_size}"
133
+
134
+ def update_status(status: WorkerStatus) -> None:
135
+ if status_action is None or queue_action is None:
136
+ return
137
+ status_text, queue_text = format_status(status)
138
+ status_action.setText(status_text)
139
+ queue_action.setText(queue_text)
140
+
141
+ def refresh_menu() -> None:
142
+ nonlocal status_action, queue_action
143
+ status = manager.status()
144
+ status_text, queue_text = format_status(status)
145
+
146
+ menu.clear()
147
+ status_action = menu.addAction(status_text)
148
+ status_action.setEnabled(False)
149
+ queue_action = menu.addAction(queue_text)
150
+ queue_action.setEnabled(False)
151
+ menu.addSeparator()
152
+
153
+ roots_menu = menu.addMenu("Monitored folders")
154
+ for root_path in status.roots:
155
+ root_menu = roots_menu.addMenu(str(root_path))
156
+ root_menu.addAction("Open incoming", lambda checked=False, path=root_path: open_path(path / args.incoming))
157
+ root_menu.addAction(
158
+ "Open converted", lambda checked=False, path=root_path: open_path(path / args.converted)
159
+ )
160
+ root_menu.addAction("Remove", lambda checked=False, path=root_path: remove_folder(path))
161
+ roots_menu.addSeparator()
162
+ roots_menu.addAction("Add folder", add_folder)
163
+
164
+ ollama_menu = menu.addMenu("Ollama routing")
165
+ disabled_action = ollama_menu.addAction("Disabled")
166
+ disabled_action.setCheckable(True)
167
+ disabled_action.setChecked(selected_ollama_model is None)
168
+ disabled_action.triggered.connect(lambda _checked=False: select_ollama_model(None))
169
+ if available_ollama_models:
170
+ ollama_menu.addSeparator()
171
+ for model in available_ollama_models:
172
+ model_action = ollama_menu.addAction(model)
173
+ model_action.setCheckable(True)
174
+ model_action.setChecked(model == selected_ollama_model)
175
+ model_action.triggered.connect(lambda _checked=False, name=model: select_ollama_model(name))
176
+ elif selected_ollama_model:
177
+ selected_action = ollama_menu.addAction(selected_ollama_model)
178
+ selected_action.setCheckable(True)
179
+ selected_action.setChecked(True)
180
+ selected_action.triggered.connect(lambda _checked=False: select_ollama_model(selected_ollama_model))
181
+ ollama_menu.addSeparator()
182
+ ollama_menu.addAction("Refresh models", refresh_ollama_models)
183
+ menu.addSeparator()
184
+ menu.addAction("Quit", quit_app)
185
+
186
+ app = QApplication.instance() or QApplication(sys.argv[:1])
187
+ app.setQuitOnLastWindowClosed(False)
188
+ hide_macos_dock_icon()
189
+ bridge = StatusBridge()
190
+ bridge.status_changed.connect(update_status, Qt.ConnectionType.QueuedConnection)
191
+ bridge.models_changed.connect(update_ollama_models, Qt.ConnectionType.QueuedConnection)
192
+
193
+ icon = make_icon()
194
+ tray = QSystemTrayIcon(icon)
195
+ tray.setToolTip("marker-pdf-agent")
196
+
197
+ menu = QMenu()
198
+ tray.setContextMenu(menu)
199
+ tray.activated.connect(
200
+ lambda reason: (
201
+ refresh_menu()
202
+ if reason in {QSystemTrayIcon.ActivationReason.Trigger, QSystemTrayIcon.ActivationReason.Context}
203
+ else None
204
+ )
205
+ )
206
+ refresh_menu()
207
+ tray.show()
208
+ manager.add_status_listener(lambda status: bridge.status_changed.emit(status))
209
+
210
+ worker_thread = threading.Thread(target=manager.run, name="marker-tray-worker", daemon=True)
211
+ worker_thread.start()
212
+
213
+ exit_code = app.exec()
214
+ manager.stop_event.set()
215
+ worker_thread.join(timeout=10)
216
+ if worker_thread.is_alive():
217
+ raise RuntimeError("worker did not stop within 10 seconds")
218
+ raise SystemExit(exit_code)
219
+
220
+
221
+ def hide_macos_dock_icon() -> None:
222
+ if platform.system() != "Darwin":
223
+ return
224
+ try:
225
+ from AppKit import NSApplication, NSApplicationActivationPolicyAccessory
226
+
227
+ NSApplication.sharedApplication().setActivationPolicy_(NSApplicationActivationPolicyAccessory)
228
+ except ImportError:
229
+ return