lumen-app 0.4.2__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.
Files changed (56) hide show
  1. lumen_app/__init__.py +7 -0
  2. lumen_app/core/__init__.py +0 -0
  3. lumen_app/core/config.py +661 -0
  4. lumen_app/core/installer.py +274 -0
  5. lumen_app/core/loader.py +45 -0
  6. lumen_app/core/router.py +87 -0
  7. lumen_app/core/server.py +389 -0
  8. lumen_app/core/service.py +49 -0
  9. lumen_app/core/tests/__init__.py +1 -0
  10. lumen_app/core/tests/test_core_integration.py +561 -0
  11. lumen_app/core/tests/test_env_checker.py +487 -0
  12. lumen_app/proto/README.md +12 -0
  13. lumen_app/proto/ml_service.proto +88 -0
  14. lumen_app/proto/ml_service_pb2.py +66 -0
  15. lumen_app/proto/ml_service_pb2.pyi +136 -0
  16. lumen_app/proto/ml_service_pb2_grpc.py +251 -0
  17. lumen_app/server.py +362 -0
  18. lumen_app/utils/env_checker.py +752 -0
  19. lumen_app/utils/installation/__init__.py +25 -0
  20. lumen_app/utils/installation/env_manager.py +152 -0
  21. lumen_app/utils/installation/micromamba_installer.py +459 -0
  22. lumen_app/utils/installation/package_installer.py +149 -0
  23. lumen_app/utils/installation/verifier.py +95 -0
  24. lumen_app/utils/logger.py +181 -0
  25. lumen_app/utils/mamba/cuda.yaml +12 -0
  26. lumen_app/utils/mamba/default.yaml +6 -0
  27. lumen_app/utils/mamba/openvino.yaml +7 -0
  28. lumen_app/utils/mamba/tensorrt.yaml +13 -0
  29. lumen_app/utils/package_resolver.py +309 -0
  30. lumen_app/utils/preset_registry.py +219 -0
  31. lumen_app/web/__init__.py +3 -0
  32. lumen_app/web/api/__init__.py +1 -0
  33. lumen_app/web/api/config.py +229 -0
  34. lumen_app/web/api/hardware.py +201 -0
  35. lumen_app/web/api/install.py +608 -0
  36. lumen_app/web/api/server.py +253 -0
  37. lumen_app/web/core/__init__.py +1 -0
  38. lumen_app/web/core/server_manager.py +348 -0
  39. lumen_app/web/core/state.py +264 -0
  40. lumen_app/web/main.py +145 -0
  41. lumen_app/web/models/__init__.py +28 -0
  42. lumen_app/web/models/config.py +63 -0
  43. lumen_app/web/models/hardware.py +64 -0
  44. lumen_app/web/models/install.py +134 -0
  45. lumen_app/web/models/server.py +95 -0
  46. lumen_app/web/static/assets/index-CGuhGHC9.css +1 -0
  47. lumen_app/web/static/assets/index-DN6HmxWS.js +56 -0
  48. lumen_app/web/static/index.html +14 -0
  49. lumen_app/web/static/vite.svg +1 -0
  50. lumen_app/web/websockets/__init__.py +1 -0
  51. lumen_app/web/websockets/logs.py +159 -0
  52. lumen_app-0.4.2.dist-info/METADATA +23 -0
  53. lumen_app-0.4.2.dist-info/RECORD +56 -0
  54. lumen_app-0.4.2.dist-info/WHEEL +5 -0
  55. lumen_app-0.4.2.dist-info/entry_points.txt +3 -0
  56. lumen_app-0.4.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,229 @@
1
+ """Configuration API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+ import yaml
9
+ from fastapi import APIRouter, HTTPException
10
+
11
+ from lumen_app.core.config import Config
12
+ from lumen_app.core.installer import CoreInstaller
13
+ from lumen_app.utils.logger import get_logger
14
+ from lumen_app.utils.preset_registry import PresetRegistry
15
+ from lumen_app.web.core.state import app_state
16
+ from lumen_app.web.models.config import (
17
+ ConfigRequest,
18
+ ConfigResponse,
19
+ )
20
+
21
+ logger = get_logger("lumen.web.api.config")
22
+ router = APIRouter()
23
+
24
+
25
+ @router.post("/generate", response_model=ConfigResponse)
26
+ async def generate_config(request: ConfigRequest):
27
+ """Generate a Lumen configuration from preset and options."""
28
+ logger.info(f"Generating config for preset: {request.preset}")
29
+
30
+ # Validate preset
31
+ if not PresetRegistry.preset_exists(request.preset):
32
+ return ConfigResponse(
33
+ success=False,
34
+ preset=request.preset,
35
+ message=f"Unknown preset: {request.preset}",
36
+ )
37
+
38
+ try:
39
+ # Create device config from preset
40
+ device_config = PresetRegistry.create_config(request.preset)
41
+
42
+ # Create Config instance
43
+ config = Config(
44
+ cache_dir=request.cache_dir,
45
+ device_config=device_config,
46
+ region=request.region,
47
+ service_name=request.service_name,
48
+ port=request.port,
49
+ )
50
+
51
+ # Generate config based on config_type
52
+ if request.config_type == "minimal":
53
+ lumen_config = config.minimal()
54
+ elif request.config_type == "light_weight":
55
+ # Ensure clip_model is one of the valid types for light_weight
56
+ light_clip_model: Literal["MobileCLIP2-S2", "CN-CLIP_ViT-B-16"] = (
57
+ request.clip_model # type: ignore
58
+ if request.clip_model in ["MobileCLIP2-S2", "CN-CLIP_ViT-B-16"]
59
+ else "MobileCLIP2-S2"
60
+ )
61
+ lumen_config = config.light_weight(clip_model=light_clip_model)
62
+ elif request.config_type == "basic":
63
+ # Ensure clip_model is one of the valid types for basic
64
+ basic_clip_model: Literal["MobileCLIP2-S4", "CN-CLIP_ViT-L-14"] = (
65
+ request.clip_model # type: ignore
66
+ if request.clip_model in ["MobileCLIP2-S4", "CN-CLIP_ViT-L-14"]
67
+ else "MobileCLIP2-S4"
68
+ )
69
+ lumen_config = config.basic(clip_model=basic_clip_model)
70
+ elif request.config_type == "brave":
71
+ lumen_config = config.brave()
72
+ else:
73
+ raise ValueError(f"Unknown config_type: {request.config_type}")
74
+
75
+ # Save config to file
76
+ config_dict = lumen_config.model_dump(mode="json")
77
+ installer = CoreInstaller(cache_dir=request.cache_dir)
78
+ success, message = installer.save_config(lumen_config)
79
+
80
+ if not success:
81
+ raise ValueError(message)
82
+
83
+ config_path = str(Path(request.cache_dir).expanduser() / "lumen-config.yaml")
84
+
85
+ # Update app state
86
+ app_state.set_config(config, device_config)
87
+
88
+ return ConfigResponse(
89
+ success=True,
90
+ preset=request.preset,
91
+ config_path=str(config_path),
92
+ config_content=config_dict,
93
+ message=f"Configuration generated successfully at {config_path}",
94
+ )
95
+
96
+ except Exception as e:
97
+ logger.error(f"Failed to generate config: {e}", exc_info=True)
98
+ return ConfigResponse(
99
+ success=False,
100
+ preset=request.preset,
101
+ message=f"Failed to generate configuration: {str(e)}",
102
+ )
103
+
104
+
105
+ @router.get("/current")
106
+ async def get_current_config():
107
+ """Get the currently loaded configuration."""
108
+ config, device_config = app_state.get_config()
109
+
110
+ if not config or not device_config:
111
+ return {"loaded": False, "message": "No configuration loaded"}
112
+
113
+ return {
114
+ "loaded": True,
115
+ "cache_dir": config.cache_dir,
116
+ "region": config.region,
117
+ "port": config.port,
118
+ "service_name": config.service_name,
119
+ "device": {
120
+ "runtime": device_config.runtime.value,
121
+ "batch_size": device_config.batch_size,
122
+ "precision": device_config.precision,
123
+ "rknn_device": device_config.rknn_device,
124
+ "onnx_providers": [
125
+ p if isinstance(p, str) else p[0]
126
+ for p in (device_config.onnx_providers or [])
127
+ ],
128
+ },
129
+ }
130
+
131
+
132
+ @router.post("/validate")
133
+ async def validate_config(config: dict):
134
+ """Validate a configuration."""
135
+ # TODO: Implement validation logic
136
+ return {"valid": True, "errors": [], "warnings": []}
137
+
138
+
139
+ @router.post("/validate-path")
140
+ async def validate_path(request: dict):
141
+ """Validate installation path for permissions and disk space."""
142
+ path_str = request.get("path", "")
143
+
144
+ if not path_str:
145
+ return {
146
+ "valid": False,
147
+ "exists": False,
148
+ "writable": False,
149
+ "error": "路径不能为空",
150
+ }
151
+
152
+ try:
153
+ import os
154
+ import shutil
155
+
156
+ # Expand user home directory
157
+ path = Path(path_str).expanduser()
158
+
159
+ # Check if path exists
160
+ exists = path.exists()
161
+
162
+ # Check if writable
163
+ writable = False
164
+ error = None
165
+
166
+ if exists:
167
+ # Check if we can write to existing directory
168
+ writable = os.access(path, os.W_OK)
169
+ if not writable:
170
+ error = f"没有写入权限: {path}"
171
+ else:
172
+ # Check if we can create the directory
173
+ parent = path.parent
174
+ if parent.exists():
175
+ writable = os.access(parent, os.W_OK)
176
+ if not writable:
177
+ error = f"没有创建目录的权限: {parent}"
178
+ else:
179
+ error = f"父目录不存在: {parent}"
180
+
181
+ # Check disk space (only if writable)
182
+ free_space_gb = 0
183
+ if writable:
184
+ stat = shutil.disk_usage(path if exists else path.parent)
185
+ free_space_gb = stat.free / (1024**3) # Convert to GB
186
+
187
+ return {
188
+ "valid": writable and free_space_gb >= 10,
189
+ "exists": exists,
190
+ "writable": writable,
191
+ "free_space_gb": round(free_space_gb, 2),
192
+ "error": error,
193
+ "warning": "磁盘空间不足 (建议至少 10GB)"
194
+ if writable and free_space_gb < 10
195
+ else None,
196
+ }
197
+
198
+ except Exception as e:
199
+ logger.error(f"Failed to validate path: {e}")
200
+ return {
201
+ "valid": False,
202
+ "exists": False,
203
+ "writable": False,
204
+ "error": f"路径验证失败: {str(e)}",
205
+ }
206
+
207
+
208
+ @router.post("/load")
209
+ async def load_config(config_path: str):
210
+ """Load a configuration from file."""
211
+ try:
212
+ path = Path(config_path).expanduser()
213
+ if not path.exists():
214
+ raise HTTPException(
215
+ status_code=404, detail=f"Config file not found: {config_path}"
216
+ )
217
+
218
+ with open(path) as f:
219
+ config_content = yaml.safe_load(f)
220
+
221
+ # TODO: Parse and validate the config, then set app_state
222
+
223
+ return {"loaded": True, "config_path": str(path), "config": config_content}
224
+
225
+ except HTTPException:
226
+ raise
227
+ except Exception as e:
228
+ logger.error(f"Failed to load config: {e}")
229
+ raise HTTPException(status_code=500, detail=f"Failed to load config: {str(e)}")
@@ -0,0 +1,201 @@
1
+ """Hardware detection and driver API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ from typing import Literal
7
+
8
+ from fastapi import APIRouter, HTTPException
9
+
10
+ from lumen_app.utils.env_checker import EnvironmentChecker
11
+ from lumen_app.utils.logger import get_logger
12
+ from lumen_app.utils.preset_registry import PresetRegistry
13
+ from lumen_app.web.models.hardware import (
14
+ DriverCheckResponse,
15
+ HardwareInfoResponse,
16
+ HardwarePresetResponse,
17
+ )
18
+
19
+ logger = get_logger("lumen.web.api.hardware")
20
+ router = APIRouter()
21
+
22
+
23
+ def _map_driver_status(status) -> Literal["available", "missing", "incompatible"]:
24
+ """Map internal DriverStatus enum to API status string."""
25
+ from lumen_app.utils.env_checker import DriverStatus as InternalDriverStatus
26
+
27
+ if status == InternalDriverStatus.AVAILABLE:
28
+ return "available"
29
+ elif status == InternalDriverStatus.INCOMPATIBLE:
30
+ return "incompatible"
31
+ else:
32
+ return "missing"
33
+
34
+
35
+ @router.get("/info", response_model=HardwareInfoResponse)
36
+ async def get_hardware_info():
37
+ """Get comprehensive hardware information."""
38
+ logger.info("Getting hardware info")
39
+
40
+ # Get system info
41
+ info = HardwareInfoResponse(
42
+ platform=platform.system(),
43
+ machine=platform.machine(),
44
+ processor=platform.processor(),
45
+ python_version=platform.python_version(),
46
+ )
47
+
48
+ # Get all presets
49
+ presets = []
50
+ for name in PresetRegistry.get_preset_names():
51
+ preset_info = PresetRegistry.get_preset(name)
52
+ if preset_info:
53
+ try:
54
+ device_config = preset_info.factory_method()
55
+ preset = HardwarePresetResponse(
56
+ name=name,
57
+ description=preset_info.description,
58
+ runtime=device_config.runtime.value,
59
+ providers=[
60
+ p if isinstance(p, str) else p[0]
61
+ for p in (device_config.onnx_providers or [])
62
+ ],
63
+ requires_drivers=preset_info.requires_drivers,
64
+ )
65
+ presets.append(preset)
66
+ except Exception as e:
67
+ logger.warning(f"Failed to process preset {name}: {e}")
68
+
69
+ info.presets = presets
70
+
71
+ # Detect recommended preset using priority-based detection order
72
+ detection_order = PresetRegistry.get_detection_order()
73
+
74
+ detected_drivers = []
75
+ for preset_name in detection_order:
76
+ if PresetRegistry.preset_exists(preset_name):
77
+ try:
78
+ report = EnvironmentChecker.check_preset(preset_name)
79
+ if report.ready:
80
+ info.recommended_preset = preset_name
81
+ info.drivers = [
82
+ DriverCheckResponse(
83
+ name=d.name,
84
+ status=_map_driver_status(d.status),
85
+ details=d.details,
86
+ installable_via_mamba=d.installable_via_mamba,
87
+ mamba_config_path=d.mamba_config_path,
88
+ )
89
+ for d in report.drivers
90
+ ]
91
+ info.all_drivers_available = True
92
+ break
93
+ elif detected_drivers:
94
+ # Collect driver info from first preset with available info
95
+ continue
96
+ except Exception as e:
97
+ logger.warning(f"Failed to check preset {preset_name}: {e}")
98
+
99
+ # If no recommended preset found, default to CPU
100
+ if not info.recommended_preset:
101
+ info.recommended_preset = "cpu"
102
+ info.all_drivers_available = True # CPU doesn't need drivers
103
+
104
+ return info
105
+
106
+
107
+ @router.get("/presets", response_model=list[HardwarePresetResponse])
108
+ async def list_hardware_presets():
109
+ """List all available hardware presets."""
110
+ logger.info("Listing hardware presets")
111
+
112
+ presets = []
113
+ for name in PresetRegistry.get_preset_names():
114
+ preset_info = PresetRegistry.get_preset(name)
115
+ if preset_info:
116
+ try:
117
+ device_config = preset_info.factory_method()
118
+ preset = HardwarePresetResponse(
119
+ name=name,
120
+ description=preset_info.description,
121
+ runtime=device_config.runtime.value,
122
+ providers=[
123
+ p if isinstance(p, str) else p[0]
124
+ for p in (device_config.onnx_providers or [])
125
+ ],
126
+ requires_drivers=preset_info.requires_drivers,
127
+ )
128
+ presets.append(preset)
129
+ except Exception as e:
130
+ logger.warning(f"Failed to process preset {name}: {e}")
131
+
132
+ return presets
133
+
134
+
135
+ @router.get("/presets/{preset_name}/check", response_model=list[DriverCheckResponse])
136
+ async def check_preset_drivers(preset_name: str):
137
+ """Check driver status for a specific preset."""
138
+ logger.info(f"Checking drivers for preset: {preset_name}")
139
+
140
+ if not PresetRegistry.preset_exists(preset_name):
141
+ raise HTTPException(status_code=404, detail=f"Preset '{preset_name}' not found")
142
+
143
+ try:
144
+ report = EnvironmentChecker.check_preset(preset_name)
145
+ return [
146
+ DriverCheckResponse(
147
+ name=d.name,
148
+ status=_map_driver_status(d.status),
149
+ details=d.details,
150
+ installable_via_mamba=d.installable_via_mamba,
151
+ mamba_config_path=d.mamba_config_path,
152
+ )
153
+ for d in report.drivers
154
+ ]
155
+ except Exception as e:
156
+ logger.error(f"Failed to check preset {preset_name}: {e}")
157
+ raise HTTPException(
158
+ status_code=500, detail=f"Failed to check drivers: {str(e)}"
159
+ )
160
+
161
+
162
+ @router.post("/detect", response_model=dict)
163
+ async def detect_hardware():
164
+ """Detect available hardware and recommended preset."""
165
+ logger.info("Detecting hardware")
166
+
167
+ # Check each preset in priority order
168
+ detection_order = PresetRegistry.get_detection_order()
169
+
170
+ detected = []
171
+ recommended = None
172
+
173
+ for preset_name in detection_order:
174
+ if not PresetRegistry.preset_exists(preset_name):
175
+ continue
176
+
177
+ try:
178
+ report = EnvironmentChecker.check_preset(preset_name)
179
+ status = {
180
+ "preset": preset_name,
181
+ "ready": report.ready,
182
+ "drivers": [
183
+ {
184
+ "name": d.name,
185
+ "status": _map_driver_status(d.status),
186
+ "installable": d.installable_via_mamba,
187
+ }
188
+ for d in report.drivers
189
+ ],
190
+ }
191
+ detected.append(status)
192
+
193
+ if report.ready and not recommended:
194
+ recommended = preset_name
195
+ except Exception as e:
196
+ logger.warning(f"Failed to check {preset_name}: {e}")
197
+
198
+ return {
199
+ "recommended_preset": recommended or "cpu",
200
+ "detailed_status": detected,
201
+ }