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.
- marker_pdf_agent/__init__.py +4 -0
- marker_pdf_agent/assets/file-markdown.svg +13 -0
- marker_pdf_agent/tray.py +229 -0
- marker_pdf_agent/worker.py +931 -0
- marker_pdf_agent-0.1.0.dist-info/METADATA +196 -0
- marker_pdf_agent-0.1.0.dist-info/RECORD +10 -0
- marker_pdf_agent-0.1.0.dist-info/WHEEL +5 -0
- marker_pdf_agent-0.1.0.dist-info/entry_points.txt +2 -0
- marker_pdf_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- marker_pdf_agent-0.1.0.dist-info/top_level.txt +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>
|
marker_pdf_agent/tray.py
ADDED
|
@@ -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
|