autoglm-gui 0.4.9__py3-none-any.whl → 0.4.11__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.
AutoGLM_GUI/api/media.py CHANGED
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  from fastapi import APIRouter, WebSocket, WebSocketDisconnect
8
8
 
9
9
  from AutoGLM_GUI.adb_plus import capture_screenshot
10
+ from AutoGLM_GUI.logger import logger
10
11
  from AutoGLM_GUI.schemas import ScreenshotRequest, ScreenshotResponse
11
12
  from AutoGLM_GUI.scrcpy_stream import ScrcpyStreamer
12
13
  from AutoGLM_GUI.state import scrcpy_locks, scrcpy_streamers
@@ -24,10 +25,10 @@ async def reset_video_stream(device_id: str | None = None) -> dict:
24
25
  if device_id in scrcpy_locks:
25
26
  async with scrcpy_locks[device_id]:
26
27
  if device_id in scrcpy_streamers:
27
- print(f"[video/reset] Stopping streamer for device {device_id}")
28
+ logger.info(f"Stopping streamer for device {device_id}")
28
29
  scrcpy_streamers[device_id].stop()
29
30
  del scrcpy_streamers[device_id]
30
- print(f"[video/reset] Streamer reset for device {device_id}")
31
+ logger.info(f"Streamer reset for device {device_id}")
31
32
  return {
32
33
  "success": True,
33
34
  "message": f"Video stream reset for device {device_id}",
@@ -45,7 +46,7 @@ async def reset_video_stream(device_id: str | None = None) -> dict:
45
46
  if dev_id in scrcpy_streamers:
46
47
  scrcpy_streamers[dev_id].stop()
47
48
  del scrcpy_streamers[dev_id]
48
- print("[video/reset] All streamers reset")
49
+ logger.info("All streamers reset")
49
50
  return {"success": True, "message": "All video streams reset"}
50
51
 
51
52
 
@@ -84,7 +85,7 @@ async def video_stream_ws(
84
85
  await websocket.send_json({"error": "device_id is required"})
85
86
  return
86
87
 
87
- print(f"[video/stream] WebSocket connection for device {device_id}")
88
+ logger.info(f"WebSocket connection for device {device_id}")
88
89
 
89
90
  # Debug: Save stream to file for analysis (controlled by DEBUG_SAVE_VIDEO_STREAM env var)
90
91
  debug_file = None
@@ -95,34 +96,34 @@ async def video_stream_ws(
95
96
  debug_dir / f"{device_id}_{int(__import__('time').time())}.h264"
96
97
  )
97
98
  debug_file = open(debug_file_path, "wb")
98
- print(f"[video/stream] DEBUG: Saving stream to {debug_file_path}")
99
+ logger.debug(f"DEBUG: Saving stream to {debug_file_path}")
99
100
 
100
101
  if device_id not in scrcpy_locks:
101
102
  scrcpy_locks[device_id] = asyncio.Lock()
102
103
 
103
104
  async with scrcpy_locks[device_id]:
104
105
  if device_id not in scrcpy_streamers:
105
- print(f"[video/stream] Creating streamer for device {device_id}")
106
+ logger.info(f"Creating streamer for device {device_id}")
106
107
  scrcpy_streamers[device_id] = ScrcpyStreamer(
107
108
  device_id=device_id, max_size=1280, bit_rate=4_000_000
108
109
  )
109
110
 
110
111
  try:
111
- print(f"[video/stream] Starting scrcpy server for device {device_id}")
112
+ logger.info(f"Starting scrcpy server for device {device_id}")
112
113
  await scrcpy_streamers[device_id].start()
113
- print(f"[video/stream] Scrcpy server started for device {device_id}")
114
+ logger.info(f"Scrcpy server started for device {device_id}")
114
115
 
115
116
  # Read NAL units until we have SPS, PPS, and IDR
116
117
  streamer = scrcpy_streamers[device_id]
117
118
 
118
- print("[video/stream] Reading NAL units for initialization...")
119
+ logger.debug("Reading NAL units for initialization...")
119
120
  for attempt in range(20): # Max 20 NAL units for initialization
120
121
  try:
121
122
  nal_unit = await streamer.read_nal_unit(auto_cache=True)
122
123
  nal_type = nal_unit[4] & 0x1F if len(nal_unit) > 4 else -1
123
124
  nal_type_names = {5: "IDR", 7: "SPS", 8: "PPS"}
124
- print(
125
- f"[video/stream] Read NAL unit: type={nal_type_names.get(nal_type, nal_type)}, size={len(nal_unit)} bytes"
125
+ logger.debug(
126
+ f"Read NAL unit: type={nal_type_names.get(nal_type, nal_type)}, size={len(nal_unit)} bytes"
126
127
  )
127
128
 
128
129
  # Check if we have all required parameter sets
@@ -131,12 +132,12 @@ async def video_stream_ws(
131
132
  and streamer.cached_pps
132
133
  and streamer.cached_idr
133
134
  ):
134
- print(
135
- f"[video/stream] ✓ Initialization complete: SPS={len(streamer.cached_sps)}B, PPS={len(streamer.cached_pps)}B, IDR={len(streamer.cached_idr)}B"
135
+ logger.debug(
136
+ f"✓ Initialization complete: SPS={len(streamer.cached_sps)}B, PPS={len(streamer.cached_pps)}B, IDR={len(streamer.cached_idr)}B"
136
137
  )
137
138
  break
138
139
  except Exception as e:
139
- print(f"[video/stream] Failed to read NAL unit: {e}")
140
+ logger.warning(f"Failed to read NAL unit: {e}")
140
141
  await asyncio.sleep(0.5)
141
142
  continue
142
143
 
@@ -149,8 +150,8 @@ async def video_stream_ws(
149
150
 
150
151
  # Send initialization data as ONE message (SPS+PPS+IDR combined)
151
152
  await websocket.send_bytes(init_data)
152
- print(
153
- f"[video/stream] ✓ Sent initialization data to first client: {len(init_data)} bytes total"
153
+ logger.debug(
154
+ f"✓ Sent initialization data to first client: {len(init_data)} bytes total"
154
155
  )
155
156
 
156
157
  # Debug: Save to file
@@ -159,10 +160,7 @@ async def video_stream_ws(
159
160
  debug_file.flush()
160
161
 
161
162
  except Exception as e:
162
- import traceback
163
-
164
- print(f"[video/stream] Failed to start streamer: {e}")
165
- print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
163
+ logger.exception(f"Failed to start streamer: {e}")
166
164
  scrcpy_streamers[device_id].stop()
167
165
  del scrcpy_streamers[device_id]
168
166
  try:
@@ -171,7 +169,7 @@ async def video_stream_ws(
171
169
  pass
172
170
  return
173
171
  else:
174
- print(f"[video/stream] Reusing streamer for device {device_id}")
172
+ logger.info(f"Reusing streamer for device {device_id}")
175
173
 
176
174
  streamer = scrcpy_streamers[device_id]
177
175
  # CRITICAL: Send complete initialization data (SPS+PPS+IDR)
@@ -183,29 +181,29 @@ async def video_stream_ws(
183
181
  init_data = streamer.get_initialization_data()
184
182
  if init_data:
185
183
  break
186
- print(
187
- f"[video/stream] Waiting for initialization data (attempt {attempt + 1}/10)..."
184
+ logger.debug(
185
+ f"Waiting for initialization data (attempt {attempt + 1}/10)..."
188
186
  )
189
187
  await asyncio.sleep(0.5)
190
188
 
191
189
  if init_data:
192
190
  # Log what we're sending
193
- print(
194
- f"[video/stream] ✓ Sending cached initialization data for device {device_id}:"
191
+ logger.debug(
192
+ f"✓ Sending cached initialization data for device {device_id}:"
195
193
  )
196
- print(
194
+ logger.debug(
197
195
  f" - SPS: {len(streamer.cached_sps) if streamer.cached_sps else 0}B"
198
196
  )
199
- print(
197
+ logger.debug(
200
198
  f" - PPS: {len(streamer.cached_pps) if streamer.cached_pps else 0}B"
201
199
  )
202
- print(
200
+ logger.debug(
203
201
  f" - IDR: {len(streamer.cached_idr) if streamer.cached_idr else 0}B"
204
202
  )
205
- print(f" - Total: {len(init_data)} bytes")
203
+ logger.debug(f" - Total: {len(init_data)} bytes")
206
204
 
207
205
  await websocket.send_bytes(init_data)
208
- print("[video/stream] ✓ Initialization data sent successfully")
206
+ logger.debug("✓ Initialization data sent successfully")
209
207
 
210
208
  # Debug: Save to file
211
209
  if debug_file:
@@ -213,7 +211,7 @@ async def video_stream_ws(
213
211
  debug_file.flush()
214
212
  else:
215
213
  error_msg = f"Initialization data not ready for device {device_id} after 5 seconds"
216
- print(f"[video/stream] ERROR: {error_msg}")
214
+ logger.error(f"ERROR: {error_msg}")
217
215
  try:
218
216
  await websocket.send_json({"error": error_msg})
219
217
  except Exception:
@@ -239,11 +237,9 @@ async def video_stream_ws(
239
237
 
240
238
  nal_count += 1
241
239
  if nal_count % 100 == 0:
242
- print(
243
- f"[video/stream] Device {device_id}: Sent {nal_count} NAL units"
244
- )
240
+ logger.debug(f"Device {device_id}: Sent {nal_count} NAL units")
245
241
  except ConnectionError as e:
246
- print(f"[video/stream] Device {device_id}: Connection error: {e}")
242
+ logger.warning(f"Device {device_id}: Connection error: {e}")
247
243
  stream_failed = True
248
244
  try:
249
245
  await websocket.send_json({"error": f"Stream error: {str(e)}"})
@@ -252,12 +248,9 @@ async def video_stream_ws(
252
248
  break
253
249
 
254
250
  except WebSocketDisconnect:
255
- print(f"[video/stream] Device {device_id}: Client disconnected")
251
+ logger.info(f"Device {device_id}: Client disconnected")
256
252
  except Exception as e:
257
- import traceback
258
-
259
- print(f"[video/stream] Device {device_id}: Error: {e}")
260
- print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
253
+ logger.exception(f"Device {device_id}: Error: {e}")
261
254
  stream_failed = True
262
255
  try:
263
256
  await websocket.send_json({"error": str(e)})
@@ -267,13 +260,13 @@ async def video_stream_ws(
267
260
  if stream_failed:
268
261
  async with scrcpy_locks[device_id]:
269
262
  if device_id in scrcpy_streamers:
270
- print(f"[video/stream] Resetting streamer for device {device_id}")
263
+ logger.info(f"Resetting streamer for device {device_id}")
271
264
  scrcpy_streamers[device_id].stop()
272
265
  del scrcpy_streamers[device_id]
273
266
 
274
267
  # Debug: Close file
275
268
  if debug_file:
276
269
  debug_file.close()
277
- print("[video/stream] DEBUG: Closed debug file")
270
+ logger.debug("DEBUG: Closed debug file")
278
271
 
279
- print(f"[video/stream] Device {device_id}: Stream ended")
272
+ logger.info(f"Device {device_id}: Stream ended")
@@ -0,0 +1,124 @@
1
+ """配置文件管理模块."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from AutoGLM_GUI.logger import logger
7
+
8
+ # 默认配置
9
+ DEFAULT_CONFIG = {"base_url": "", "model_name": "autoglm-phone-9b", "api_key": "EMPTY"}
10
+
11
+
12
+ def get_config_path() -> Path:
13
+ """获取配置文件路径.
14
+
15
+ Returns:
16
+ Path: 配置文件路径 (~/.config/autoglm/config.json)
17
+ """
18
+ config_dir = Path.home() / ".config" / "autoglm"
19
+ return config_dir / "config.json"
20
+
21
+
22
+ def load_config_file() -> dict | None:
23
+ """从文件加载配置.
24
+
25
+ Returns:
26
+ dict | None: 配置字典,如果文件不存在或加载失败则返回 None
27
+ """
28
+ config_path = get_config_path()
29
+
30
+ # 文件不存在(首次运行)
31
+ if not config_path.exists():
32
+ logger.debug(f"Config file not found at {config_path}")
33
+ return None
34
+
35
+ try:
36
+ with open(config_path, "r", encoding="utf-8") as f:
37
+ config = json.load(f)
38
+ logger.info(f"Loaded configuration from {config_path}")
39
+ return config
40
+ except json.JSONDecodeError as e:
41
+ logger.warning(f"Failed to parse config file {config_path}: {e}")
42
+ return None
43
+ except Exception as e:
44
+ logger.error(f"Failed to read config file {config_path}: {e}")
45
+ return None
46
+
47
+
48
+ def save_config_file(config: dict) -> bool:
49
+ """保存配置到文件(原子写入).
50
+
51
+ Args:
52
+ config: 配置字典
53
+
54
+ Returns:
55
+ bool: 成功返回 True,失败返回 False
56
+ """
57
+ config_path = get_config_path()
58
+
59
+ try:
60
+ # 确保目录存在
61
+ config_path.parent.mkdir(parents=True, exist_ok=True)
62
+
63
+ # 原子写入:先写入临时文件,然后重命名
64
+ temp_path = config_path.with_suffix(".tmp")
65
+ with open(temp_path, "w", encoding="utf-8") as f:
66
+ json.dump(config, f, indent=2, ensure_ascii=False)
67
+
68
+ # 重命名(原子操作)
69
+ temp_path.replace(config_path)
70
+
71
+ logger.info(f"Configuration saved to {config_path}")
72
+ return True
73
+ except Exception as e:
74
+ logger.error(f"Failed to save config file {config_path}: {e}")
75
+ return False
76
+
77
+
78
+ def delete_config_file() -> bool:
79
+ """删除配置文件.
80
+
81
+ Returns:
82
+ bool: 成功返回 True,失败返回 False
83
+ """
84
+ config_path = get_config_path()
85
+
86
+ if not config_path.exists():
87
+ logger.debug(f"Config file does not exist: {config_path}")
88
+ return True
89
+
90
+ try:
91
+ config_path.unlink()
92
+ logger.info(f"Configuration deleted: {config_path}")
93
+ return True
94
+ except Exception as e:
95
+ logger.error(f"Failed to delete config file {config_path}: {e}")
96
+ return False
97
+
98
+
99
+ def merge_configs(file_config: dict | None, cli_config: dict | None) -> dict:
100
+ """合并配置(优先级:CLI > 文件 > 默认值).
101
+
102
+ Args:
103
+ file_config: 从文件加载的配置(可为 None)
104
+ cli_config: CLI 参数配置(可为 None)
105
+
106
+ Returns:
107
+ dict: 合并后的配置字典
108
+ """
109
+ # 从默认配置开始
110
+ merged = DEFAULT_CONFIG.copy()
111
+
112
+ # 应用文件配置(如果存在)
113
+ if file_config:
114
+ for key in DEFAULT_CONFIG.keys():
115
+ if key in file_config:
116
+ merged[key] = file_config[key]
117
+
118
+ # 应用 CLI 配置(最高优先级)
119
+ if cli_config:
120
+ for key in DEFAULT_CONFIG.keys():
121
+ if key in cli_config:
122
+ merged[key] = cli_config[key]
123
+
124
+ return merged
AutoGLM_GUI/logger.py ADDED
@@ -0,0 +1,85 @@
1
+ """
2
+ Centralized logging configuration using loguru.
3
+ """
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from loguru import logger
8
+
9
+ # Remove default handler
10
+ logger.remove()
11
+
12
+ # Default configuration - will be overridden by configure_logger()
13
+ _configured = False
14
+
15
+
16
+ def configure_logger(
17
+ console_level: str = "INFO",
18
+ log_file: str | None = "logs/autoglm_{time:YYYY-MM-DD}.log",
19
+ log_level: str = "DEBUG",
20
+ rotation: str = "100 MB",
21
+ retention: str = "7 days",
22
+ compression: str = "zip",
23
+ ) -> None:
24
+ """
25
+ Configure the global logger with console and file handlers.
26
+
27
+ Args:
28
+ console_level: Console output level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
29
+ log_file: Log file path (None to disable file logging)
30
+ log_level: File logging level
31
+ rotation: Log rotation policy (e.g., "100 MB", "1 day")
32
+ retention: Log retention policy (e.g., "7 days", "1 week")
33
+ compression: Compression format for rotated logs (e.g., "zip", "gz")
34
+ """
35
+ global _configured
36
+
37
+ # Remove existing handlers if reconfiguring
38
+ if _configured:
39
+ logger.remove()
40
+
41
+ # Console handler with colors
42
+ logger.add(
43
+ sys.stderr,
44
+ format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
45
+ level=console_level,
46
+ colorize=True,
47
+ )
48
+
49
+ # File handler
50
+ if log_file:
51
+ # Create logs directory if it doesn't exist
52
+ log_path = Path(log_file)
53
+ log_path.parent.mkdir(parents=True, exist_ok=True)
54
+
55
+ logger.add(
56
+ log_file,
57
+ rotation=rotation,
58
+ retention=retention,
59
+ compression=compression,
60
+ level=log_level,
61
+ format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}",
62
+ encoding="utf-8",
63
+ )
64
+
65
+ # Separate error log file
66
+ error_file = str(log_path.parent / f"errors_{log_path.name.split('_', 1)[1]}")
67
+ logger.add(
68
+ error_file,
69
+ rotation="50 MB",
70
+ retention="30 days",
71
+ compression=compression,
72
+ level="ERROR",
73
+ format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}",
74
+ backtrace=True,
75
+ diagnose=True,
76
+ encoding="utf-8",
77
+ )
78
+
79
+ _configured = True
80
+
81
+
82
+ # Default initialization (can be reconfigured later)
83
+ configure_logger()
84
+
85
+ __all__ = ["logger", "configure_logger"]
@@ -12,18 +12,24 @@ def is_windows() -> bool:
12
12
 
13
13
 
14
14
  async def run_cmd_silently(cmd: Sequence[str]) -> subprocess.CompletedProcess:
15
- """Run a command, suppressing output; safe for async contexts on all platforms."""
15
+ """Run a command, suppressing output but preserving it in the result; safe for async contexts on all platforms."""
16
16
  if is_windows():
17
17
  # Avoid blocking the event loop with a blocking subprocess call on Windows.
18
18
  return await asyncio.to_thread(
19
- subprocess.run, cmd, capture_output=True, check=False
19
+ subprocess.run, cmd, capture_output=True, text=True, check=False
20
20
  )
21
21
 
22
+ # Use PIPE on macOS/Linux to capture output
22
23
  process = await asyncio.create_subprocess_exec(
23
- *cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
24
+ *cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
24
25
  )
25
- await process.wait()
26
- return subprocess.CompletedProcess(cmd, process.returncode, None, None)
26
+ stdout, stderr = await process.communicate()
27
+ # Decode bytes to string for API consistency across platforms
28
+ stdout_str = stdout.decode("utf-8") if stdout else ""
29
+ stderr_str = stderr.decode("utf-8") if stderr else ""
30
+ # Return CompletedProcess with stdout/stderr for API consistency across platforms
31
+ return_code = process.returncode if process.returncode is not None else -1
32
+ return subprocess.CompletedProcess(cmd, return_code, stdout_str, stderr_str)
27
33
 
28
34
 
29
35
  async def spawn_process(cmd: Sequence[str], *, capture_output: bool = False) -> Any: