ai-screenshooter 1.2.3__tar.gz → 1.3.0__tar.gz
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.
- {ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/PKG-INFO +1 -1
- {ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/ai_screenshooter.egg-info/PKG-INFO +1 -1
- ai_screenshooter-1.3.0/ai_screenshot.py +400 -0
- {ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/setup.py +1 -1
- ai_screenshooter-1.2.3/ai_screenshot.py +0 -161
- {ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/README.md +0 -0
- {ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/ai_screenshooter.egg-info/SOURCES.txt +0 -0
- {ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/ai_screenshooter.egg-info/dependency_links.txt +0 -0
- {ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/ai_screenshooter.egg-info/entry_points.txt +0 -0
- {ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/ai_screenshooter.egg-info/requires.txt +0 -0
- {ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/ai_screenshooter.egg-info/top_level.txt +0 -0
- {ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/setup.cfg +0 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import signal
|
|
5
|
+
import logging
|
|
6
|
+
import atexit
|
|
7
|
+
import time
|
|
8
|
+
import subprocess
|
|
9
|
+
import requests
|
|
10
|
+
import pygetwindow as gw
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from PIL import ImageGrab
|
|
13
|
+
from pynput import keyboard
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Constants
|
|
17
|
+
PID_FILE = Path.home() / ".ai-screenshooter.pid"
|
|
18
|
+
LOG_FILE = Path.home() / ".ai-screenshooter.log"
|
|
19
|
+
SCREENSHOT_DIR = Path.home() / ".ai-screenshooter" / "screenshots"
|
|
20
|
+
TIMEOUT_SECONDS = 5 * 60 * 60 # 5 hours
|
|
21
|
+
|
|
22
|
+
# Server URLs
|
|
23
|
+
PROD_URL = "https://service.tech4vision.net/ai-management-service/api/v1/sessions/code-challenge"
|
|
24
|
+
LOCAL_URL = "http://localhost:8082/api/v1/sessions/code-challenge"
|
|
25
|
+
|
|
26
|
+
# Global state
|
|
27
|
+
screenshot_list = []
|
|
28
|
+
API_TOKEN = None
|
|
29
|
+
API_URL = None
|
|
30
|
+
current_keys = set()
|
|
31
|
+
logger = logging.getLogger("ai-screenshooter")
|
|
32
|
+
|
|
33
|
+
if sys.platform == "win32":
|
|
34
|
+
import ctypes
|
|
35
|
+
from ctypes import Structure, c_long
|
|
36
|
+
windll = ctypes.windll
|
|
37
|
+
|
|
38
|
+
class RECT(Structure):
|
|
39
|
+
_fields_ = [("left", c_long), ("top", c_long), ("right", c_long), ("bottom", c_long)]
|
|
40
|
+
elif sys.platform == "darwin":
|
|
41
|
+
from AppKit import NSWorkspace
|
|
42
|
+
from Quartz import CGWindowListCopyWindowInfo, kCGWindowListOptionOnScreenOnly, kCGNullWindowID
|
|
43
|
+
elif sys.platform == "linux":
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ============ PID File Management ============
|
|
48
|
+
|
|
49
|
+
def get_pid_from_file():
|
|
50
|
+
"""Read PID from file, return None if invalid."""
|
|
51
|
+
if not PID_FILE.exists():
|
|
52
|
+
return None
|
|
53
|
+
try:
|
|
54
|
+
pid = int(PID_FILE.read_text().strip())
|
|
55
|
+
return pid if pid > 0 else None
|
|
56
|
+
except (ValueError, IOError):
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_process_running(pid):
|
|
61
|
+
"""Check if a process with the given PID is running."""
|
|
62
|
+
try:
|
|
63
|
+
os.kill(pid, 0) # Signal 0 doesn't kill, just checks
|
|
64
|
+
return True
|
|
65
|
+
except ProcessLookupError:
|
|
66
|
+
return False
|
|
67
|
+
except PermissionError:
|
|
68
|
+
# Process exists but we don't have permission
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def write_pid_file():
|
|
73
|
+
"""Write current PID to file."""
|
|
74
|
+
PID_FILE.write_text(str(os.getpid()))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cleanup_pid_file():
|
|
78
|
+
"""Remove PID file on exit."""
|
|
79
|
+
try:
|
|
80
|
+
if PID_FILE.exists():
|
|
81
|
+
PID_FILE.unlink()
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ============ Process Management ============
|
|
87
|
+
|
|
88
|
+
def kill_existing_process():
|
|
89
|
+
"""Kill any existing instance. Returns True if killed."""
|
|
90
|
+
pid = get_pid_from_file()
|
|
91
|
+
if not pid or not is_process_running(pid):
|
|
92
|
+
cleanup_pid_file()
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
os.kill(pid, signal.SIGTERM)
|
|
97
|
+
# Wait up to 3 seconds for graceful shutdown
|
|
98
|
+
for _ in range(30):
|
|
99
|
+
time.sleep(0.1)
|
|
100
|
+
if not is_process_running(pid):
|
|
101
|
+
break
|
|
102
|
+
else:
|
|
103
|
+
# Force kill if still running
|
|
104
|
+
os.kill(pid, signal.SIGKILL)
|
|
105
|
+
except ProcessLookupError:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
cleanup_pid_file()
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def start_background_process(token, local):
|
|
113
|
+
"""Start a new background process using subprocess (avoids fork issues)."""
|
|
114
|
+
# Build command to run this script with --daemon flag
|
|
115
|
+
cmd = [
|
|
116
|
+
sys.executable,
|
|
117
|
+
"-m", "ai_screenshot",
|
|
118
|
+
"start",
|
|
119
|
+
"--token", token,
|
|
120
|
+
"--daemon" # Internal flag for the actual daemon process
|
|
121
|
+
]
|
|
122
|
+
if local:
|
|
123
|
+
cmd.append("--local")
|
|
124
|
+
|
|
125
|
+
# Start the subprocess detached from terminal
|
|
126
|
+
with open(os.devnull, 'w') as devnull:
|
|
127
|
+
process = subprocess.Popen(
|
|
128
|
+
cmd,
|
|
129
|
+
stdout=devnull,
|
|
130
|
+
stderr=devnull,
|
|
131
|
+
stdin=devnull,
|
|
132
|
+
start_new_session=True, # Detach from terminal
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
print(f"Started background process (PID: {process.pid})")
|
|
136
|
+
print(f"PID file: {PID_FILE}")
|
|
137
|
+
print(f"Log file: {LOG_FILE}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ============ Signal Handlers ============
|
|
141
|
+
|
|
142
|
+
def handle_sigterm(signum, frame):
|
|
143
|
+
"""Handle SIGTERM for graceful shutdown."""
|
|
144
|
+
logger.info("Received SIGTERM, shutting down...")
|
|
145
|
+
cleanup_pid_file()
|
|
146
|
+
sys.exit(0)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def handle_sigalrm(signum, frame):
|
|
150
|
+
"""Handle SIGALRM for auto-termination after timeout."""
|
|
151
|
+
logger.info("5-hour timeout reached, auto-terminating...")
|
|
152
|
+
cleanup_pid_file()
|
|
153
|
+
sys.exit(0)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ============ Logging Setup ============
|
|
157
|
+
|
|
158
|
+
def setup_logging(daemon_mode):
|
|
159
|
+
"""Configure logging based on mode."""
|
|
160
|
+
logger.setLevel(logging.INFO)
|
|
161
|
+
logger.handlers.clear()
|
|
162
|
+
|
|
163
|
+
if daemon_mode:
|
|
164
|
+
# File logging for daemon mode
|
|
165
|
+
handler = logging.FileHandler(LOG_FILE)
|
|
166
|
+
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
|
167
|
+
else:
|
|
168
|
+
# Console logging for foreground mode
|
|
169
|
+
handler = logging.StreamHandler()
|
|
170
|
+
handler.setFormatter(logging.Formatter('%(message)s'))
|
|
171
|
+
|
|
172
|
+
logger.addHandler(handler)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ============ Screenshot Functions ============
|
|
176
|
+
|
|
177
|
+
def get_active_window_bounds():
|
|
178
|
+
"""Returns the active window's bounds (x, y, width, height) in a cross-platform way."""
|
|
179
|
+
time.sleep(0.1)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
active_window = gw.getActiveWindow()
|
|
183
|
+
|
|
184
|
+
if isinstance(active_window, str): # Ensure it's a window name
|
|
185
|
+
geometry = gw.getWindowGeometry(active_window)
|
|
186
|
+
if geometry:
|
|
187
|
+
x, y, width, height = geometry
|
|
188
|
+
logger.info(f"Active window detected: {active_window} at ({x}, {y}, {width}, {height})")
|
|
189
|
+
return x, y, width, height
|
|
190
|
+
|
|
191
|
+
elif active_window:
|
|
192
|
+
x, y = active_window.left, active_window.top
|
|
193
|
+
width, height = active_window.width, active_window.height
|
|
194
|
+
logger.info(f"Active window detected: {active_window.title} at ({x}, {y}, {width}, {height})")
|
|
195
|
+
return x, y, width, height
|
|
196
|
+
else:
|
|
197
|
+
logger.warning("No active window detected, defaulting to full screen.")
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.error(f"Error detecting active window: {e}")
|
|
201
|
+
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def capture_screenshot():
|
|
206
|
+
global screenshot_list
|
|
207
|
+
|
|
208
|
+
# Ensure screenshot directory exists
|
|
209
|
+
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
|
|
211
|
+
screenshot_path = SCREENSHOT_DIR / f"screenshot_{len(screenshot_list)}.jpg"
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
logger.info("Refreshing active window detection...")
|
|
215
|
+
window_bounds = get_active_window_bounds()
|
|
216
|
+
|
|
217
|
+
if window_bounds:
|
|
218
|
+
x, y, width, height = map(int, window_bounds)
|
|
219
|
+
logger.info(f"Capturing active window at ({x}, {y}, {width}, {height})")
|
|
220
|
+
screenshot = ImageGrab.grab(bbox=(x, y, x + width, y + height))
|
|
221
|
+
else:
|
|
222
|
+
logger.warning("No active window detected, capturing full screen.")
|
|
223
|
+
screenshot = ImageGrab.grab()
|
|
224
|
+
|
|
225
|
+
screenshot.convert("RGB").save(str(screenshot_path), "JPEG", quality=50)
|
|
226
|
+
|
|
227
|
+
if screenshot_path.exists():
|
|
228
|
+
screenshot_list.append(str(screenshot_path))
|
|
229
|
+
logger.info(f"Screenshot captured: {screenshot_path}")
|
|
230
|
+
else:
|
|
231
|
+
logger.error(f"Screenshot capture failed: {screenshot_path}")
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Error capturing screenshot: {e}")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def send_screenshots():
|
|
237
|
+
global screenshot_list
|
|
238
|
+
if not API_TOKEN:
|
|
239
|
+
logger.error("No API token provided!")
|
|
240
|
+
return
|
|
241
|
+
if not screenshot_list:
|
|
242
|
+
logger.warning("No screenshots to send.")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
files = []
|
|
246
|
+
for f in screenshot_list:
|
|
247
|
+
if os.path.exists(f):
|
|
248
|
+
files.append(("files", (os.path.basename(f), open(f, "rb"))))
|
|
249
|
+
else:
|
|
250
|
+
logger.warning(f"Screenshot file not found: {f}")
|
|
251
|
+
|
|
252
|
+
if not files:
|
|
253
|
+
logger.warning("No valid screenshots to send.")
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
response = requests.post(
|
|
258
|
+
API_URL,
|
|
259
|
+
headers={"Authorization": f"Bearer {API_TOKEN}"},
|
|
260
|
+
files=files,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if response.status_code == 200:
|
|
264
|
+
logger.info("Screenshots uploaded successfully.")
|
|
265
|
+
screenshot_list = []
|
|
266
|
+
else:
|
|
267
|
+
logger.error(f"Upload failed: {response.text}")
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.error(f"Error uploading screenshots: {e}")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ============ Keyboard Handlers ============
|
|
273
|
+
|
|
274
|
+
def on_press(key):
|
|
275
|
+
current_keys.add(key)
|
|
276
|
+
try:
|
|
277
|
+
if key == keyboard.Key.down and keyboard.Key.esc in current_keys:
|
|
278
|
+
logger.info("Capturing screenshot...")
|
|
279
|
+
capture_screenshot()
|
|
280
|
+
elif key == keyboard.Key.up and keyboard.Key.esc in current_keys:
|
|
281
|
+
logger.info("Sending all screenshots...")
|
|
282
|
+
send_screenshots()
|
|
283
|
+
except AttributeError:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def on_release(key):
|
|
288
|
+
try:
|
|
289
|
+
current_keys.remove(key)
|
|
290
|
+
except KeyError:
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ============ CLI Commands ============
|
|
295
|
+
|
|
296
|
+
def cmd_start(args):
|
|
297
|
+
"""Handle the start command."""
|
|
298
|
+
global API_TOKEN, API_URL
|
|
299
|
+
|
|
300
|
+
# If --background flag, spawn a new process and exit
|
|
301
|
+
if args.background:
|
|
302
|
+
print("Starting in background mode...")
|
|
303
|
+
killed = kill_existing_process()
|
|
304
|
+
if killed:
|
|
305
|
+
print("Killed existing instance.")
|
|
306
|
+
|
|
307
|
+
start_background_process(args.token, args.local)
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
# If --daemon flag (internal), this is the actual daemon process
|
|
311
|
+
is_daemon = getattr(args, 'daemon', False)
|
|
312
|
+
|
|
313
|
+
if is_daemon:
|
|
314
|
+
# Write PID file
|
|
315
|
+
write_pid_file()
|
|
316
|
+
atexit.register(cleanup_pid_file)
|
|
317
|
+
|
|
318
|
+
# Set up logging to file
|
|
319
|
+
setup_logging(daemon_mode=True)
|
|
320
|
+
|
|
321
|
+
# Set 5-hour auto-termination timer
|
|
322
|
+
signal.signal(signal.SIGALRM, handle_sigalrm)
|
|
323
|
+
signal.alarm(TIMEOUT_SECONDS)
|
|
324
|
+
else:
|
|
325
|
+
setup_logging(daemon_mode=False)
|
|
326
|
+
|
|
327
|
+
# Signal handler for graceful shutdown
|
|
328
|
+
signal.signal(signal.SIGTERM, handle_sigterm)
|
|
329
|
+
|
|
330
|
+
# Setup API config
|
|
331
|
+
API_TOKEN = args.token
|
|
332
|
+
API_URL = LOCAL_URL if args.local else PROD_URL
|
|
333
|
+
|
|
334
|
+
server_mode = "LOCAL" if args.local else "PRODUCTION"
|
|
335
|
+
logger.info("AI Screenshot CLI started.")
|
|
336
|
+
logger.info(f"Server: {server_mode} ({API_URL})")
|
|
337
|
+
logger.info("Press ESC + Down to capture a screenshot.")
|
|
338
|
+
logger.info("Press ESC + Up to send all stored screenshots.")
|
|
339
|
+
if not is_daemon:
|
|
340
|
+
logger.info("Running... (Press Ctrl + C to exit)")
|
|
341
|
+
|
|
342
|
+
# Listen for hotkeys using pynput
|
|
343
|
+
with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
|
|
344
|
+
listener.join()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def cmd_status(args):
|
|
348
|
+
"""Handle the status command."""
|
|
349
|
+
pid = get_pid_from_file()
|
|
350
|
+
if pid and is_process_running(pid):
|
|
351
|
+
print(f"ai-screenshooter is running (PID: {pid})")
|
|
352
|
+
return 0
|
|
353
|
+
else:
|
|
354
|
+
print("ai-screenshooter is not running")
|
|
355
|
+
if PID_FILE.exists():
|
|
356
|
+
print(f"(stale PID file exists at {PID_FILE})")
|
|
357
|
+
return 1
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def cmd_stop(args):
|
|
361
|
+
"""Handle the stop command."""
|
|
362
|
+
if kill_existing_process():
|
|
363
|
+
print("ai-screenshooter stopped")
|
|
364
|
+
return 0
|
|
365
|
+
else:
|
|
366
|
+
print("ai-screenshooter is not running")
|
|
367
|
+
return 1
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# ============ Main Entry Point ============
|
|
371
|
+
|
|
372
|
+
def main():
|
|
373
|
+
parser = argparse.ArgumentParser(description="AI Screenshot CLI")
|
|
374
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
375
|
+
|
|
376
|
+
# start command
|
|
377
|
+
start_parser = subparsers.add_parser("start", help="Start listening for hotkeys")
|
|
378
|
+
start_parser.add_argument("--token", required=True, help="API Token for authentication")
|
|
379
|
+
start_parser.add_argument("--local", action="store_true", help="Use localhost server instead of production")
|
|
380
|
+
start_parser.add_argument("--background", "-b", action="store_true", help="Run in background (daemon mode)")
|
|
381
|
+
start_parser.add_argument("--daemon", action="store_true", help=argparse.SUPPRESS) # Internal flag
|
|
382
|
+
|
|
383
|
+
# status command
|
|
384
|
+
subparsers.add_parser("status", help="Check if ai-screenshooter is running")
|
|
385
|
+
|
|
386
|
+
# stop command
|
|
387
|
+
subparsers.add_parser("stop", help="Stop the running ai-screenshooter instance")
|
|
388
|
+
|
|
389
|
+
args = parser.parse_args()
|
|
390
|
+
|
|
391
|
+
if args.command == "start":
|
|
392
|
+
cmd_start(args)
|
|
393
|
+
elif args.command == "status":
|
|
394
|
+
sys.exit(cmd_status(args))
|
|
395
|
+
elif args.command == "stop":
|
|
396
|
+
sys.exit(cmd_stop(args))
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
if __name__ == "__main__":
|
|
400
|
+
main()
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
|
-
import requests
|
|
5
|
-
import pygetwindow as gw
|
|
6
|
-
import time
|
|
7
|
-
from PIL import ImageGrab
|
|
8
|
-
from pynput import keyboard
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
screenshot_list = []
|
|
12
|
-
API_TOKEN = None
|
|
13
|
-
API_URL = None
|
|
14
|
-
|
|
15
|
-
# Server URLs
|
|
16
|
-
PROD_URL = "https://service.tech4vision.net/ai-management-service/api/v1/sessions/code-challenge"
|
|
17
|
-
LOCAL_URL = "http://localhost:8082/api/v1/sessions/code-challenge"
|
|
18
|
-
|
|
19
|
-
current_keys = set()
|
|
20
|
-
|
|
21
|
-
if sys.platform == "win32":
|
|
22
|
-
import ctypes
|
|
23
|
-
from ctypes import Structure, c_long
|
|
24
|
-
windll = ctypes.windll
|
|
25
|
-
|
|
26
|
-
class RECT(Structure):
|
|
27
|
-
_fields_ = [("left", c_long), ("top", c_long), ("right", c_long), ("bottom", c_long)]
|
|
28
|
-
elif sys.platform == "darwin":
|
|
29
|
-
from AppKit import NSWorkspace
|
|
30
|
-
from Quartz import CGWindowListCopyWindowInfo, kCGWindowListOptionOnScreenOnly, kCGNullWindowID
|
|
31
|
-
elif sys.platform == "linux":
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
def get_active_window_bounds():
|
|
35
|
-
"""Returns the active window's bounds (x, y, width, height) in a cross-platform way."""
|
|
36
|
-
time.sleep(0.1)
|
|
37
|
-
|
|
38
|
-
try:
|
|
39
|
-
active_window = gw.getActiveWindow()
|
|
40
|
-
|
|
41
|
-
if isinstance(active_window, str): # Ensure it's a window name
|
|
42
|
-
geometry = gw.getWindowGeometry(active_window)
|
|
43
|
-
if geometry:
|
|
44
|
-
x, y, width, height = geometry
|
|
45
|
-
print(f"🖥️ Active window detected: {active_window} at ({x}, {y}, {width}, {height})")
|
|
46
|
-
return x, y, width, height
|
|
47
|
-
|
|
48
|
-
elif active_window:
|
|
49
|
-
x, y = active_window.left, active_window.top
|
|
50
|
-
width, height = active_window.width, active_window.height
|
|
51
|
-
print(f"🖥️ Active window detected: {active_window.title} at ({x}, {y}, {width}, {height})")
|
|
52
|
-
return x, y, width, height
|
|
53
|
-
else:
|
|
54
|
-
print("⚠️ No active window detected, defaulting to full screen.")
|
|
55
|
-
|
|
56
|
-
except Exception as e:
|
|
57
|
-
print(f"❌ Error detecting active window: {e}")
|
|
58
|
-
|
|
59
|
-
return None
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def capture_screenshot():
|
|
63
|
-
global screenshot_list
|
|
64
|
-
screenshot_path = f"screenshot_{len(screenshot_list)}.jpg"
|
|
65
|
-
|
|
66
|
-
try:
|
|
67
|
-
print("🔄 Refreshing active window detection...")
|
|
68
|
-
window_bounds = get_active_window_bounds() # Ensure fresh window detection
|
|
69
|
-
|
|
70
|
-
if window_bounds:
|
|
71
|
-
x, y, width, height = map(int, window_bounds) # Convert all values to integers
|
|
72
|
-
print(f"📸 Capturing active window at ({x}, {y}, {width}, {height})")
|
|
73
|
-
screenshot = ImageGrab.grab(bbox=(x, y, x + width, y + height))
|
|
74
|
-
else:
|
|
75
|
-
print("⚠️ No active window detected, capturing full screen.")
|
|
76
|
-
screenshot = ImageGrab.grab()
|
|
77
|
-
|
|
78
|
-
screenshot.convert("RGB").save(screenshot_path, "JPEG", quality=50)
|
|
79
|
-
|
|
80
|
-
if os.path.exists(screenshot_path):
|
|
81
|
-
screenshot_list.append(screenshot_path)
|
|
82
|
-
print(f"✅ Screenshot captured: {screenshot_path}")
|
|
83
|
-
else:
|
|
84
|
-
print(f"❌ Screenshot capture failed: {screenshot_path}")
|
|
85
|
-
except Exception as e:
|
|
86
|
-
print(f"❌ Error capturing screenshot: {e}")
|
|
87
|
-
|
|
88
|
-
def send_screenshots():
|
|
89
|
-
global screenshot_list
|
|
90
|
-
if not API_TOKEN:
|
|
91
|
-
print("❌ No API token provided!")
|
|
92
|
-
return
|
|
93
|
-
if not screenshot_list:
|
|
94
|
-
print("⚠️ No screenshots to send.")
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
files = []
|
|
98
|
-
for f in screenshot_list:
|
|
99
|
-
if os.path.exists(f):
|
|
100
|
-
files.append(("files", (os.path.basename(f), open(f, "rb"))))
|
|
101
|
-
else:
|
|
102
|
-
print(f"⚠️ Warning: Screenshot file not found: {f}")
|
|
103
|
-
|
|
104
|
-
if not files:
|
|
105
|
-
print("⚠️ No valid screenshots to send.")
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
response = requests.post(
|
|
109
|
-
API_URL,
|
|
110
|
-
headers={"Authorization": f"Bearer {API_TOKEN}"},
|
|
111
|
-
files=files,
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
if response.status_code == 200:
|
|
115
|
-
print("✅ Screenshots uploaded successfully.")
|
|
116
|
-
screenshot_list = []
|
|
117
|
-
else:
|
|
118
|
-
print(f"❌ Upload failed: {response.text}")
|
|
119
|
-
|
|
120
|
-
def on_press(key):
|
|
121
|
-
current_keys.add(key)
|
|
122
|
-
try:
|
|
123
|
-
if key == keyboard.Key.down and keyboard.Key.esc in current_keys:
|
|
124
|
-
print("📸 Capturing screenshot...")
|
|
125
|
-
capture_screenshot()
|
|
126
|
-
elif key == keyboard.Key.up and keyboard.Key.esc in current_keys:
|
|
127
|
-
print("📤 Sending all screenshots...")
|
|
128
|
-
send_screenshots()
|
|
129
|
-
except AttributeError:
|
|
130
|
-
pass
|
|
131
|
-
|
|
132
|
-
def on_release(key):
|
|
133
|
-
try:
|
|
134
|
-
current_keys.remove(key)
|
|
135
|
-
except KeyError:
|
|
136
|
-
pass
|
|
137
|
-
|
|
138
|
-
def main():
|
|
139
|
-
parser = argparse.ArgumentParser(description="AI Screenshot CLI")
|
|
140
|
-
parser.add_argument("start", help="Start listening for hotkeys")
|
|
141
|
-
parser.add_argument("--token", required=True, help="API Token for authentication")
|
|
142
|
-
parser.add_argument("--local", action="store_true", help="Use localhost server instead of production")
|
|
143
|
-
|
|
144
|
-
args = parser.parse_args()
|
|
145
|
-
global API_TOKEN, API_URL
|
|
146
|
-
API_TOKEN = args.token
|
|
147
|
-
API_URL = LOCAL_URL if args.local else PROD_URL
|
|
148
|
-
|
|
149
|
-
server_mode = "LOCAL" if args.local else "PRODUCTION"
|
|
150
|
-
print("📸 AI Screenshot CLI started.")
|
|
151
|
-
print(f"🌐 Server: {server_mode} ({API_URL})")
|
|
152
|
-
print("✅ Press ESC + ↓ to capture a screenshot.")
|
|
153
|
-
print("✅ Press ESC + ↑ to send all stored screenshots.")
|
|
154
|
-
print("📌 Running... (Press Ctrl + C to exit)")
|
|
155
|
-
|
|
156
|
-
# Listen for hotkeys using pynput
|
|
157
|
-
with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
|
|
158
|
-
listener.join() # Keep script running
|
|
159
|
-
|
|
160
|
-
if __name__ == "__main__":
|
|
161
|
-
main()
|
|
File without changes
|
|
File without changes
|
{ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/ai_screenshooter.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ai_screenshooter-1.2.3 → ai_screenshooter-1.3.0}/ai_screenshooter.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|