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.
- lumen_app/__init__.py +7 -0
- lumen_app/core/__init__.py +0 -0
- lumen_app/core/config.py +661 -0
- lumen_app/core/installer.py +274 -0
- lumen_app/core/loader.py +45 -0
- lumen_app/core/router.py +87 -0
- lumen_app/core/server.py +389 -0
- lumen_app/core/service.py +49 -0
- lumen_app/core/tests/__init__.py +1 -0
- lumen_app/core/tests/test_core_integration.py +561 -0
- lumen_app/core/tests/test_env_checker.py +487 -0
- lumen_app/proto/README.md +12 -0
- lumen_app/proto/ml_service.proto +88 -0
- lumen_app/proto/ml_service_pb2.py +66 -0
- lumen_app/proto/ml_service_pb2.pyi +136 -0
- lumen_app/proto/ml_service_pb2_grpc.py +251 -0
- lumen_app/server.py +362 -0
- lumen_app/utils/env_checker.py +752 -0
- lumen_app/utils/installation/__init__.py +25 -0
- lumen_app/utils/installation/env_manager.py +152 -0
- lumen_app/utils/installation/micromamba_installer.py +459 -0
- lumen_app/utils/installation/package_installer.py +149 -0
- lumen_app/utils/installation/verifier.py +95 -0
- lumen_app/utils/logger.py +181 -0
- lumen_app/utils/mamba/cuda.yaml +12 -0
- lumen_app/utils/mamba/default.yaml +6 -0
- lumen_app/utils/mamba/openvino.yaml +7 -0
- lumen_app/utils/mamba/tensorrt.yaml +13 -0
- lumen_app/utils/package_resolver.py +309 -0
- lumen_app/utils/preset_registry.py +219 -0
- lumen_app/web/__init__.py +3 -0
- lumen_app/web/api/__init__.py +1 -0
- lumen_app/web/api/config.py +229 -0
- lumen_app/web/api/hardware.py +201 -0
- lumen_app/web/api/install.py +608 -0
- lumen_app/web/api/server.py +253 -0
- lumen_app/web/core/__init__.py +1 -0
- lumen_app/web/core/server_manager.py +348 -0
- lumen_app/web/core/state.py +264 -0
- lumen_app/web/main.py +145 -0
- lumen_app/web/models/__init__.py +28 -0
- lumen_app/web/models/config.py +63 -0
- lumen_app/web/models/hardware.py +64 -0
- lumen_app/web/models/install.py +134 -0
- lumen_app/web/models/server.py +95 -0
- lumen_app/web/static/assets/index-CGuhGHC9.css +1 -0
- lumen_app/web/static/assets/index-DN6HmxWS.js +56 -0
- lumen_app/web/static/index.html +14 -0
- lumen_app/web/static/vite.svg +1 -0
- lumen_app/web/websockets/__init__.py +1 -0
- lumen_app/web/websockets/logs.py +159 -0
- lumen_app-0.4.2.dist-info/METADATA +23 -0
- lumen_app-0.4.2.dist-info/RECORD +56 -0
- lumen_app-0.4.2.dist-info/WHEEL +5 -0
- lumen_app-0.4.2.dist-info/entry_points.txt +3 -0
- 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
|
+
}
|