extra-desktop 0.1.1__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.
- extra/__init__.py +5 -0
- extra/cli.py +242 -0
- extra/core/__init__.py +113 -0
- extra/core/capture.py +187 -0
- extra/core/focus.py +226 -0
- extra/core/geometry.py +380 -0
- extra/core/input_engine.py +422 -0
- extra/core/stall_breaker.py +185 -0
- extra/core/uia_plane.py +402 -0
- extra/fastpath/__init__.py +26 -0
- extra/fastpath/browser.py +255 -0
- extra/fastpath/shell.py +260 -0
- extra/mcp/__init__.py +13 -0
- extra/mcp/server.py +446 -0
- extra/test_core_engine.py +159 -0
- extra_desktop-0.1.1.dist-info/METADATA +237 -0
- extra_desktop-0.1.1.dist-info/RECORD +22 -0
- extra_desktop-0.1.1.dist-info/WHEEL +5 -0
- extra_desktop-0.1.1.dist-info/entry_points.txt +2 -0
- extra_desktop-0.1.1.dist-info/licenses/LICENSE +21 -0
- extra_desktop-0.1.1.dist-info/licenses/NOTICE +10 -0
- extra_desktop-0.1.1.dist-info/top_level.txt +1 -0
extra/__init__.py
ADDED
extra/cli.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Extra — Command-Line Interface (CLI) & System Diagnostic Runner
|
|
3
|
+
Provides `extra doctor`, `extra test`, `extra run`, `extra inspect`, and `extra snap`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# Add project root to sys.path
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from extra import __version__
|
|
20
|
+
except Exception:
|
|
21
|
+
__version__ = "0.1.1"
|
|
22
|
+
|
|
23
|
+
from extra.core.capture import capture_screen
|
|
24
|
+
from extra.core.focus import (
|
|
25
|
+
find_window_by_title,
|
|
26
|
+
get_foreground_window,
|
|
27
|
+
list_windows,
|
|
28
|
+
)
|
|
29
|
+
from extra.core.geometry import (
|
|
30
|
+
attach_input_desktop,
|
|
31
|
+
ensure_dpi_aware,
|
|
32
|
+
get_cursor_position,
|
|
33
|
+
get_monitors_info,
|
|
34
|
+
)
|
|
35
|
+
from extra.core.uia_plane import SetOfMarkAnnotator, UIAutomationPlane
|
|
36
|
+
from extra.fastpath.shell import resolve_executable
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_doctor() -> int:
|
|
40
|
+
"""Runs a complete system health and capability diagnostic."""
|
|
41
|
+
ensure_dpi_aware()
|
|
42
|
+
attach_input_desktop()
|
|
43
|
+
|
|
44
|
+
print("=" * 65)
|
|
45
|
+
print(" EXTRA SYSTEM HEALTH & HARDWARE DIAGNOSTIC (DOCTOR)")
|
|
46
|
+
print("=" * 65)
|
|
47
|
+
|
|
48
|
+
# 1. OS & Architecture
|
|
49
|
+
print("\n[Operating System & Architecture]")
|
|
50
|
+
os_name = platform.system()
|
|
51
|
+
os_release = platform.release()
|
|
52
|
+
os_version = platform.version()
|
|
53
|
+
machine = platform.machine()
|
|
54
|
+
py_ver = sys.version.split()[0]
|
|
55
|
+
print(f" Extra Version: {__version__}")
|
|
56
|
+
print(f" OS: {os_name} {os_release} (Build {os_version})")
|
|
57
|
+
print(f" Architecture: {machine}")
|
|
58
|
+
print(f" Python: {py_ver} ({'64-bit' if sys.maxsize > 2**32 else '32-bit'})")
|
|
59
|
+
|
|
60
|
+
if os_name != "Windows":
|
|
61
|
+
print(" [FAIL] Extra requires Windows 10 or Windows 11.")
|
|
62
|
+
return 1
|
|
63
|
+
else:
|
|
64
|
+
print(" [OK] Supported Windows host.")
|
|
65
|
+
|
|
66
|
+
# 2. DPI & Security Desktop Attachment
|
|
67
|
+
print("\n[DPI Geometry & Thread Desktop]")
|
|
68
|
+
dpi_ok = ensure_dpi_aware()
|
|
69
|
+
desk_ok = attach_input_desktop()
|
|
70
|
+
print(f" DPI Awareness: {'PerMonitorV2 (Active)' if dpi_ok else 'Fallback'}")
|
|
71
|
+
print(f" Input Desktop: {'Attached (Default)' if desk_ok else 'Standard'}")
|
|
72
|
+
|
|
73
|
+
cursor = get_cursor_position()
|
|
74
|
+
print(f" Mouse Cursor: Physical pixel {cursor}")
|
|
75
|
+
|
|
76
|
+
# 3. Displays & Hardware Metrics
|
|
77
|
+
print("\n[Displays & Multi-Monitor Metrics]")
|
|
78
|
+
monitors = get_monitors_info()
|
|
79
|
+
print(f" Monitors: {len(monitors)} connected display(s)")
|
|
80
|
+
for m in monitors:
|
|
81
|
+
print(f" - Display {m.index}: {m.width}x{m.height} @ ({m.left}, {m.top}) | DPI: {m.dpi_x}x{m.dpi_y} ({m.scale_factor}x scale) | Primary={m.is_primary}")
|
|
82
|
+
|
|
83
|
+
# 4. Perception Engine Latency Benchmark
|
|
84
|
+
print("\n[Screen Capture Benchmark]")
|
|
85
|
+
latencies = []
|
|
86
|
+
for _ in range(3):
|
|
87
|
+
cap = capture_screen(0)
|
|
88
|
+
latencies.append(cap.duration_ms)
|
|
89
|
+
avg_latency = sum(latencies) / len(latencies)
|
|
90
|
+
print(f" Capture Runs: {latencies[0]:.1f}ms, {latencies[1]:.1f}ms, {latencies[2]:.1f}ms (Average: {avg_latency:.1f}ms)")
|
|
91
|
+
if avg_latency < 100:
|
|
92
|
+
print(f" [OK] High-speed screen capture verified ({avg_latency:.1f}ms).")
|
|
93
|
+
else:
|
|
94
|
+
print(" [WARN] Screen capture took longer than expected.")
|
|
95
|
+
|
|
96
|
+
# 5. Semantic UI Automation v3 Plane
|
|
97
|
+
print("\n[Microsoft UI Automation Core COM Layer]")
|
|
98
|
+
try:
|
|
99
|
+
uia = UIAutomationPlane()
|
|
100
|
+
t0 = time.perf_counter()
|
|
101
|
+
elems = uia.inspect_window(interactive_only=True, max_elements=20)
|
|
102
|
+
t_uia = (time.perf_counter() - t0) * 1000.0
|
|
103
|
+
print(f" UIA Status: Active & Operational ({len(elems)} elements inspected in {t_uia:.1f}ms)")
|
|
104
|
+
print(" [OK] UIAutomationCore.dll COM interface functional.")
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print(f" [FAIL] UIA Plane Error: {e}")
|
|
107
|
+
|
|
108
|
+
# 6. Microsoft Edge & Playwright Fast-Path
|
|
109
|
+
print("\n[Browser Fast-Path (Microsoft Edge / Playwright)]")
|
|
110
|
+
edge_path = resolve_executable("edge")
|
|
111
|
+
if edge_path and os.path.exists(edge_path):
|
|
112
|
+
print(f" Edge Binary: {edge_path}")
|
|
113
|
+
print(" [OK] Microsoft Edge available for zero-download web fast-path.")
|
|
114
|
+
else:
|
|
115
|
+
print(" [WARN] Microsoft Edge not found at standard path.")
|
|
116
|
+
|
|
117
|
+
# 7. Active Windows
|
|
118
|
+
print("\n[Active Desktop Windows]")
|
|
119
|
+
fg = get_foreground_window()
|
|
120
|
+
print(f" Foreground: {repr(fg.title) if fg else 'None'} ({fg.process_name if fg else ''})")
|
|
121
|
+
wins = list_windows(visible_only=True)
|
|
122
|
+
print(f" Total Windows: {len(wins)} visible top-level windows")
|
|
123
|
+
|
|
124
|
+
print("\n" + "=" * 65)
|
|
125
|
+
print(" DOCTOR DIAGNOSTIC COMPLETE: SYSTEM IS READY FOR EXTRA")
|
|
126
|
+
print("=" * 65)
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def cmd_test() -> int:
|
|
131
|
+
"""Runs the full integration test suite."""
|
|
132
|
+
from extra.test_core_engine import run_tests
|
|
133
|
+
try:
|
|
134
|
+
run_tests()
|
|
135
|
+
return 0
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print(f"\n[FAIL] Test suite failed: {e}")
|
|
138
|
+
return 1
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
142
|
+
"""Starts the Model Context Protocol (MCP) server."""
|
|
143
|
+
from extra.mcp.server import main
|
|
144
|
+
main()
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def cmd_inspect(args: argparse.Namespace) -> int:
|
|
149
|
+
"""Quickly inspects UI Automation controls on the desktop."""
|
|
150
|
+
ensure_dpi_aware()
|
|
151
|
+
attach_input_desktop()
|
|
152
|
+
|
|
153
|
+
uia = UIAutomationPlane()
|
|
154
|
+
print(f"Inspecting UI elements (window='{args.window}', max={args.limit})...")
|
|
155
|
+
hwnd = None
|
|
156
|
+
if args.window:
|
|
157
|
+
win = find_window_by_title(args.window)
|
|
158
|
+
if win:
|
|
159
|
+
hwnd = win.hwnd
|
|
160
|
+
print(f"Targeting window [{hwnd}] '{win.title}'")
|
|
161
|
+
|
|
162
|
+
elems = uia.inspect_window(hwnd=hwnd, interactive_only=not args.all, max_elements=args.limit)
|
|
163
|
+
print(f"\nDiscovered {len(elems)} elements:")
|
|
164
|
+
for el in elems:
|
|
165
|
+
safe_name = el.name.encode("ascii", "replace").decode("ascii")
|
|
166
|
+
print(f" [{el.element_id:2d}] {el.control_type:<12} center={el.center} bbox={el.bounding_box} '{safe_name}'")
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def cmd_snap(args: argparse.Namespace) -> int:
|
|
171
|
+
"""Captures a screenshot and optionally overlays Set-of-Mark badges."""
|
|
172
|
+
ensure_dpi_aware()
|
|
173
|
+
attach_input_desktop()
|
|
174
|
+
|
|
175
|
+
cap = capture_screen(monitor_index=args.monitor)
|
|
176
|
+
output_path = Path(args.output)
|
|
177
|
+
|
|
178
|
+
if args.som:
|
|
179
|
+
uia = UIAutomationPlane()
|
|
180
|
+
elems = uia.inspect_window(interactive_only=True, max_elements=50)
|
|
181
|
+
annotator = SetOfMarkAnnotator()
|
|
182
|
+
ann_img, mark_map = annotator.annotate(cap.image, elems)
|
|
183
|
+
ann_img.save(output_path)
|
|
184
|
+
print(f"Saved Set-of-Mark annotated screenshot ({len(mark_map)} badges) to {output_path.resolve()}")
|
|
185
|
+
else:
|
|
186
|
+
cap.image.save(output_path)
|
|
187
|
+
print(f"Saved screenshot ({cap.width}x{cap.height}, {cap.duration_ms}ms) to {output_path.resolve()}")
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def main() -> None:
|
|
192
|
+
parser = argparse.ArgumentParser(
|
|
193
|
+
prog="extra",
|
|
194
|
+
description="Extra — Flashless Windows 10/11 Computer-Use Engine & MCP Server",
|
|
195
|
+
)
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
"--version", "-v", action="version", version=f"extra {__version__}"
|
|
198
|
+
)
|
|
199
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
200
|
+
|
|
201
|
+
# doctor
|
|
202
|
+
subparsers.add_parser("doctor", help="Run system health and capability diagnostics")
|
|
203
|
+
|
|
204
|
+
# test
|
|
205
|
+
subparsers.add_parser("test", help="Run integration test suite")
|
|
206
|
+
|
|
207
|
+
# run
|
|
208
|
+
run_parser = subparsers.add_parser("run", help="Start the MCP Server on stdio")
|
|
209
|
+
run_parser.add_argument("--transport", default="stdio", choices=["stdio"], help="Transport mode")
|
|
210
|
+
|
|
211
|
+
# inspect
|
|
212
|
+
inspect_parser = subparsers.add_parser("inspect", help="Inspect Windows UI Automation tree")
|
|
213
|
+
inspect_parser.add_argument("--window", "-w", default=None, help="Filter by window title")
|
|
214
|
+
inspect_parser.add_argument("--limit", "-n", type=int, default=30, help="Max elements to display")
|
|
215
|
+
inspect_parser.add_argument("--all", "-a", action="store_true", help="Include non-interactive containers")
|
|
216
|
+
|
|
217
|
+
# snap
|
|
218
|
+
snap_parser = subparsers.add_parser("snap", help="Capture a desktop screenshot")
|
|
219
|
+
snap_parser.add_argument("--output", "-o", default="screenshot.png", help="Output file path")
|
|
220
|
+
snap_parser.add_argument("--monitor", "-m", type=int, default=0, help="Monitor index")
|
|
221
|
+
snap_parser.add_argument("--som", action="store_true", help="Overlay Set-of-Mark numbered badges")
|
|
222
|
+
|
|
223
|
+
args = parser.parse_args()
|
|
224
|
+
|
|
225
|
+
if not args.command:
|
|
226
|
+
parser.print_help()
|
|
227
|
+
sys.exit(0)
|
|
228
|
+
|
|
229
|
+
if args.command == "doctor":
|
|
230
|
+
sys.exit(cmd_doctor())
|
|
231
|
+
elif args.command == "test":
|
|
232
|
+
sys.exit(cmd_test())
|
|
233
|
+
elif args.command == "run":
|
|
234
|
+
sys.exit(cmd_run(args))
|
|
235
|
+
elif args.command == "inspect":
|
|
236
|
+
sys.exit(cmd_inspect(args))
|
|
237
|
+
elif args.command == "snap":
|
|
238
|
+
sys.exit(cmd_snap(args))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
main()
|
extra/core/__init__.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Extra — Core Engine Package
|
|
3
|
+
High-performance Windows 10/11 desktop automation, dual-plane perception,
|
|
4
|
+
zero-latency SendInput injection, and closed-loop stall breaker.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from extra.core.capture import (
|
|
8
|
+
CaptureResult,
|
|
9
|
+
ScreenCaptureEngine,
|
|
10
|
+
capture_roi,
|
|
11
|
+
capture_screen,
|
|
12
|
+
get_capture_engine,
|
|
13
|
+
image_to_base64,
|
|
14
|
+
)
|
|
15
|
+
from extra.core.focus import (
|
|
16
|
+
WindowInfo,
|
|
17
|
+
find_window_by_title,
|
|
18
|
+
find_windows_by_process,
|
|
19
|
+
force_activate_window,
|
|
20
|
+
get_foreground_window,
|
|
21
|
+
get_window_info,
|
|
22
|
+
list_windows,
|
|
23
|
+
)
|
|
24
|
+
from extra.core.geometry import (
|
|
25
|
+
MonitorInfo,
|
|
26
|
+
attach_input_desktop,
|
|
27
|
+
clamp_coordinates,
|
|
28
|
+
denormalize_bbox,
|
|
29
|
+
denormalize_coordinates,
|
|
30
|
+
ensure_dpi_aware,
|
|
31
|
+
get_bbox_center,
|
|
32
|
+
get_cursor_position,
|
|
33
|
+
get_monitors_info,
|
|
34
|
+
get_primary_monitor,
|
|
35
|
+
get_virtual_screen_bounds,
|
|
36
|
+
normalize_bbox,
|
|
37
|
+
normalize_coordinates,
|
|
38
|
+
)
|
|
39
|
+
from extra.core.input_engine import (
|
|
40
|
+
atomic_clipboard_paste,
|
|
41
|
+
instant_type,
|
|
42
|
+
mouse_click,
|
|
43
|
+
mouse_double_click,
|
|
44
|
+
mouse_down,
|
|
45
|
+
mouse_drag,
|
|
46
|
+
mouse_move,
|
|
47
|
+
mouse_scroll,
|
|
48
|
+
mouse_up,
|
|
49
|
+
send_hotkey,
|
|
50
|
+
)
|
|
51
|
+
from extra.core.stall_breaker import (
|
|
52
|
+
ActionOutcome,
|
|
53
|
+
EmergencyAbortError,
|
|
54
|
+
StallBreaker,
|
|
55
|
+
StallStatus,
|
|
56
|
+
)
|
|
57
|
+
from extra.core.uia_plane import (
|
|
58
|
+
SetOfMarkAnnotator,
|
|
59
|
+
UIAutomationPlane,
|
|
60
|
+
UIElement,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
# Geometry
|
|
65
|
+
"ensure_dpi_aware",
|
|
66
|
+
"attach_input_desktop",
|
|
67
|
+
"get_monitors_info",
|
|
68
|
+
"get_primary_monitor",
|
|
69
|
+
"get_virtual_screen_bounds",
|
|
70
|
+
"normalize_coordinates",
|
|
71
|
+
"denormalize_coordinates",
|
|
72
|
+
"normalize_bbox",
|
|
73
|
+
"denormalize_bbox",
|
|
74
|
+
"get_bbox_center",
|
|
75
|
+
"clamp_coordinates",
|
|
76
|
+
"get_cursor_position",
|
|
77
|
+
"MonitorInfo",
|
|
78
|
+
# Capture
|
|
79
|
+
"ScreenCaptureEngine",
|
|
80
|
+
"get_capture_engine",
|
|
81
|
+
"capture_screen",
|
|
82
|
+
"capture_roi",
|
|
83
|
+
"image_to_base64",
|
|
84
|
+
"CaptureResult",
|
|
85
|
+
# Input
|
|
86
|
+
"instant_type",
|
|
87
|
+
"atomic_clipboard_paste",
|
|
88
|
+
"mouse_move",
|
|
89
|
+
"mouse_down",
|
|
90
|
+
"mouse_up",
|
|
91
|
+
"mouse_click",
|
|
92
|
+
"mouse_double_click",
|
|
93
|
+
"mouse_drag",
|
|
94
|
+
"mouse_scroll",
|
|
95
|
+
"send_hotkey",
|
|
96
|
+
# Focus
|
|
97
|
+
"WindowInfo",
|
|
98
|
+
"get_window_info",
|
|
99
|
+
"get_foreground_window",
|
|
100
|
+
"list_windows",
|
|
101
|
+
"find_window_by_title",
|
|
102
|
+
"find_windows_by_process",
|
|
103
|
+
"force_activate_window",
|
|
104
|
+
# UIA Plane
|
|
105
|
+
"UIAutomationPlane",
|
|
106
|
+
"SetOfMarkAnnotator",
|
|
107
|
+
"UIElement",
|
|
108
|
+
# Stall Breaker
|
|
109
|
+
"StallBreaker",
|
|
110
|
+
"StallStatus",
|
|
111
|
+
"ActionOutcome",
|
|
112
|
+
"EmergencyAbortError",
|
|
113
|
+
]
|
extra/core/capture.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Extra — Screen Capture Engine
|
|
3
|
+
High-speed screen buffer capture via hardware-accelerated and GDI pipelines,
|
|
4
|
+
supporting sub-30ms frame grabs, multi-monitor selection, ROI cropping,
|
|
5
|
+
and base64 encoding.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import concurrent.futures
|
|
12
|
+
import io
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Optional, Tuple
|
|
16
|
+
|
|
17
|
+
import mss
|
|
18
|
+
from PIL import Image
|
|
19
|
+
|
|
20
|
+
from extra.core.geometry import (
|
|
21
|
+
attach_input_desktop,
|
|
22
|
+
clamp_coordinates,
|
|
23
|
+
ensure_dpi_aware,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CaptureResult:
|
|
29
|
+
"""Encapsulates captured screen image with performance metadata."""
|
|
30
|
+
image: Image.Image
|
|
31
|
+
duration_ms: float
|
|
32
|
+
monitor_index: int
|
|
33
|
+
width: int
|
|
34
|
+
height: int
|
|
35
|
+
crop_box: Optional[Tuple[int, int, int, int]] = None
|
|
36
|
+
|
|
37
|
+
def to_base64(self, format: str = "JPEG", quality: int = 85) -> str:
|
|
38
|
+
"""Encodes captured PIL Image into base64 string."""
|
|
39
|
+
return image_to_base64(self.image, format=format, quality=quality)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ScreenCaptureEngine:
|
|
43
|
+
"""
|
|
44
|
+
High-performance Windows screen capture engine.
|
|
45
|
+
Uses an isolated background capture worker thread to avoid Win32 desktop
|
|
46
|
+
security context contamination from COM apartments or UI message loops.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
ensure_dpi_aware()
|
|
51
|
+
self._executor = concurrent.futures.ThreadPoolExecutor(
|
|
52
|
+
max_workers=1, thread_name_prefix="ExtraCaptureWorker"
|
|
53
|
+
)
|
|
54
|
+
self._sct: Optional[mss.MSS] = None
|
|
55
|
+
|
|
56
|
+
def _worker_grab(
|
|
57
|
+
self,
|
|
58
|
+
monitor_index: int,
|
|
59
|
+
crop_box: Optional[Tuple[int, int, int, int]],
|
|
60
|
+
) -> Tuple[bytes, Tuple[int, int], str]:
|
|
61
|
+
# Always attach the clean worker thread to the active input desktop
|
|
62
|
+
attach_input_desktop()
|
|
63
|
+
|
|
64
|
+
if self._sct is None:
|
|
65
|
+
self._sct = mss.MSS()
|
|
66
|
+
|
|
67
|
+
# mss.monitors[0] is all monitors combined, mss.monitors[1] is monitor 0
|
|
68
|
+
mss_idx = monitor_index + 1
|
|
69
|
+
if mss_idx < 1 or mss_idx >= len(self._sct.monitors):
|
|
70
|
+
mss_idx = 1 # Fallback to primary monitor
|
|
71
|
+
|
|
72
|
+
mon_info = self._sct.monitors[mss_idx]
|
|
73
|
+
|
|
74
|
+
if crop_box:
|
|
75
|
+
left, top, right, bottom = crop_box
|
|
76
|
+
left, top = clamp_coordinates(left, top, monitor_index)
|
|
77
|
+
right, bottom = clamp_coordinates(right, bottom, monitor_index)
|
|
78
|
+
|
|
79
|
+
if right <= left or bottom <= top:
|
|
80
|
+
region = mon_info
|
|
81
|
+
else:
|
|
82
|
+
region = {
|
|
83
|
+
"left": left,
|
|
84
|
+
"top": top,
|
|
85
|
+
"width": right - left,
|
|
86
|
+
"height": bottom - top,
|
|
87
|
+
}
|
|
88
|
+
else:
|
|
89
|
+
region = mon_info
|
|
90
|
+
|
|
91
|
+
raw_frame = self._sct.grab(region)
|
|
92
|
+
return (bytes(raw_frame.bgra), raw_frame.size, "BGRX")
|
|
93
|
+
|
|
94
|
+
def capture(
|
|
95
|
+
self,
|
|
96
|
+
monitor_index: int = 0,
|
|
97
|
+
crop_box: Optional[Tuple[int, int, int, int]] = None,
|
|
98
|
+
) -> CaptureResult:
|
|
99
|
+
"""
|
|
100
|
+
Captures a screen frame from the specified monitor.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
monitor_index: 0-based index of monitor (0 = primary).
|
|
104
|
+
crop_box: Optional (left, top, right, bottom) physical pixel bounding box.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
CaptureResult with PIL Image and latency stats.
|
|
108
|
+
"""
|
|
109
|
+
ensure_dpi_aware()
|
|
110
|
+
t_start = time.perf_counter()
|
|
111
|
+
|
|
112
|
+
future = self._executor.submit(self._worker_grab, monitor_index, crop_box)
|
|
113
|
+
bgra_bytes, size, raw_mode = future.result()
|
|
114
|
+
|
|
115
|
+
img = Image.frombytes("RGB", size, bgra_bytes, "raw", raw_mode)
|
|
116
|
+
duration_ms = (time.perf_counter() - t_start) * 1000.0
|
|
117
|
+
|
|
118
|
+
return CaptureResult(
|
|
119
|
+
image=img,
|
|
120
|
+
duration_ms=round(duration_ms, 2),
|
|
121
|
+
monitor_index=monitor_index,
|
|
122
|
+
width=img.width,
|
|
123
|
+
height=img.height,
|
|
124
|
+
crop_box=crop_box,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def close(self) -> None:
|
|
128
|
+
"""Releases capture worker thread and resources."""
|
|
129
|
+
def _cleanup():
|
|
130
|
+
if self._sct is not None:
|
|
131
|
+
try:
|
|
132
|
+
self._sct.close()
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
self._sct = None
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
self._executor.submit(_cleanup).result(timeout=1.0)
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
self._executor.shutdown(wait=False)
|
|
142
|
+
|
|
143
|
+
def __enter__(self) -> "ScreenCaptureEngine":
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
147
|
+
self.close()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_default_engine: Optional[ScreenCaptureEngine] = None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_capture_engine() -> ScreenCaptureEngine:
|
|
154
|
+
"""Returns or initializes the shared ScreenCaptureEngine."""
|
|
155
|
+
global _default_engine
|
|
156
|
+
if _default_engine is None:
|
|
157
|
+
_default_engine = ScreenCaptureEngine()
|
|
158
|
+
return _default_engine
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def capture_screen(
|
|
162
|
+
monitor_index: int = 0,
|
|
163
|
+
crop_box: Optional[Tuple[int, int, int, int]] = None,
|
|
164
|
+
) -> CaptureResult:
|
|
165
|
+
"""Convenience function to capture screen via default engine."""
|
|
166
|
+
return get_capture_engine().capture(monitor_index=monitor_index, crop_box=crop_box)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def capture_roi(
|
|
170
|
+
left: int, top: int, right: int, bottom: int, monitor_index: int = 0
|
|
171
|
+
) -> CaptureResult:
|
|
172
|
+
"""Convenience function to capture a specific region of interest."""
|
|
173
|
+
return capture_screen(monitor_index=monitor_index, crop_box=(left, top, right, bottom))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def image_to_base64(image: Image.Image, format: str = "JPEG", quality: int = 85) -> str:
|
|
177
|
+
"""Converts a PIL Image to a base64 encoded string."""
|
|
178
|
+
buffer = io.BytesIO()
|
|
179
|
+
if format.upper() == "JPEG":
|
|
180
|
+
if image.mode != "RGB":
|
|
181
|
+
image = image.convert("RGB")
|
|
182
|
+
image.save(buffer, format="JPEG", quality=quality, optimize=True)
|
|
183
|
+
else:
|
|
184
|
+
image.save(buffer, format=format)
|
|
185
|
+
|
|
186
|
+
encoded = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
187
|
+
return encoded
|