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,309 @@
|
|
|
1
|
+
"""Package resolver for Lumen installations.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for resolving package URLs from GitHub Releases,
|
|
4
|
+
selecting appropriate mirrors based on region, and building pip installation commands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import urllib.request
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
from lumen_resources.lumen_config import LumenConfig, Region
|
|
13
|
+
|
|
14
|
+
from ..core.config import DeviceConfig
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MirrorSelector:
|
|
20
|
+
"""Selects mirror URLs based on region."""
|
|
21
|
+
|
|
22
|
+
GITHUB_MIRROR_CN = "https://gh-proxy.org/https://github.com"
|
|
23
|
+
PYPI_MIRROR_CN = "https://mirrors.aliyun.com/pypi/simple/"
|
|
24
|
+
|
|
25
|
+
def get_github_urls(self, base_url: str, region: Region) -> list[str]:
|
|
26
|
+
"""Get GitHub URLs with mirror fallback.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
base_url: Original GitHub URL (e.g., https://github.com/...)
|
|
30
|
+
region: Region.cn or Region.other
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of URLs to try (mirror first if cn, then original)
|
|
34
|
+
"""
|
|
35
|
+
urls = []
|
|
36
|
+
if region == Region.cn:
|
|
37
|
+
# Apply ghproxy mirror
|
|
38
|
+
mirror_url = base_url.replace("https://github.com", self.GITHUB_MIRROR_CN)
|
|
39
|
+
urls.append(mirror_url)
|
|
40
|
+
urls.append(base_url)
|
|
41
|
+
return urls
|
|
42
|
+
|
|
43
|
+
def get_pypi_indexes(self, region: Region) -> list[str]:
|
|
44
|
+
"""Get PyPI index URLs with mirror fallback.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
region: Region.cn or Region.other
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List of index URLs (mirror first if cn, then original)
|
|
51
|
+
"""
|
|
52
|
+
indexes = []
|
|
53
|
+
if region == Region.cn:
|
|
54
|
+
indexes.append(self.PYPI_MIRROR_CN)
|
|
55
|
+
# Always include PyPI official as fallback
|
|
56
|
+
indexes.append("https://pypi.org/simple/")
|
|
57
|
+
return indexes
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class GitHubPackageResolver:
|
|
61
|
+
"""Resolves package download URLs from GitHub Releases."""
|
|
62
|
+
|
|
63
|
+
REPO_OWNER = "EdwinZhanCN"
|
|
64
|
+
REPO_NAME = "Lumen"
|
|
65
|
+
API_BASE = "https://api.github.com"
|
|
66
|
+
|
|
67
|
+
def __init__(self, region: Region):
|
|
68
|
+
"""Initialize resolver.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
region: Region for mirror selection
|
|
72
|
+
"""
|
|
73
|
+
self.region = region
|
|
74
|
+
self.mirror_selector = MirrorSelector()
|
|
75
|
+
|
|
76
|
+
def get_latest_release(self) -> str:
|
|
77
|
+
"""Get latest release tag from GitHub API.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Release tag (e.g., "v0.1.0")
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
Exception: If API call fails
|
|
84
|
+
"""
|
|
85
|
+
url = (
|
|
86
|
+
f"{self.API_BASE}/repos/{self.REPO_OWNER}/{self.REPO_NAME}/releases/latest"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
logger.debug(f"[GitHubPackageResolver] Fetching latest release from {url}")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
with urllib.request.urlopen(url, timeout=30) as response:
|
|
93
|
+
import json
|
|
94
|
+
|
|
95
|
+
data = json.loads(response.read().decode())
|
|
96
|
+
tag = data.get("tag_name")
|
|
97
|
+
if not tag:
|
|
98
|
+
raise Exception("No tag_name found in release")
|
|
99
|
+
logger.info(f"[GitHubPackageResolver] Latest release: {tag}")
|
|
100
|
+
return tag
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"[GitHubPackageResolver] Failed to fetch release: {e}")
|
|
103
|
+
raise Exception(f"Failed to fetch latest release: {e}")
|
|
104
|
+
|
|
105
|
+
def resolve_package_url(self, package_name: str) -> tuple[str, str]:
|
|
106
|
+
"""Resolve package wheel download URL.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
package_name: Package name (e.g., "lumen_ocr")
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Tuple of (download_url, version)
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
Exception: If wheel file not found
|
|
116
|
+
"""
|
|
117
|
+
# Get latest release tag
|
|
118
|
+
tag = self.get_latest_release()
|
|
119
|
+
|
|
120
|
+
# We need to get the actual wheel filename from the release assets
|
|
121
|
+
api_url = f"{self.API_BASE}/repos/{self.REPO_OWNER}/{self.REPO_NAME}/releases/tags/{tag}"
|
|
122
|
+
|
|
123
|
+
logger.debug(f"[GitHubPackageResolver] Fetching release assets from {api_url}")
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
with urllib.request.urlopen(api_url, timeout=30) as response:
|
|
127
|
+
import json
|
|
128
|
+
|
|
129
|
+
data = json.loads(response.read().decode())
|
|
130
|
+
assets = data.get("assets", [])
|
|
131
|
+
|
|
132
|
+
# Find matching wheel
|
|
133
|
+
for asset in assets:
|
|
134
|
+
name = asset.get("name", "")
|
|
135
|
+
if name.startswith(package_name) and name.endswith(
|
|
136
|
+
"-py3-none-any.whl"
|
|
137
|
+
):
|
|
138
|
+
download_url = asset.get("browser_download_url")
|
|
139
|
+
logger.info(f"[GitHubPackageResolver] Found wheel: {name}")
|
|
140
|
+
return download_url, tag
|
|
141
|
+
|
|
142
|
+
raise Exception(f"Wheel file not found for {package_name}")
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(f"[GitHubPackageResolver] Failed to resolve package URL: {e}")
|
|
146
|
+
raise Exception(f"Failed to resolve package URL for {package_name}: {e}")
|
|
147
|
+
|
|
148
|
+
def download_wheel(
|
|
149
|
+
self,
|
|
150
|
+
url: str,
|
|
151
|
+
dest_dir: Path,
|
|
152
|
+
log_callback: Callable[[str], None] | None = None,
|
|
153
|
+
) -> Path:
|
|
154
|
+
"""Download wheel file to destination directory.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
url: Download URL
|
|
158
|
+
dest_dir: Destination directory
|
|
159
|
+
log_callback: Optional callback for log messages
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Path to downloaded wheel file
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
Exception: If download fails
|
|
166
|
+
"""
|
|
167
|
+
dest_dir = Path(dest_dir)
|
|
168
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
|
|
170
|
+
# Extract filename from URL
|
|
171
|
+
filename = url.split("/")[-1]
|
|
172
|
+
dest_path = dest_dir / filename
|
|
173
|
+
|
|
174
|
+
# Get URLs with mirror fallback
|
|
175
|
+
urls = self.mirror_selector.get_github_urls(url, self.region)
|
|
176
|
+
|
|
177
|
+
# Try each URL
|
|
178
|
+
for download_url in urls:
|
|
179
|
+
try:
|
|
180
|
+
if log_callback:
|
|
181
|
+
log_callback(f"Downloading {filename}...")
|
|
182
|
+
|
|
183
|
+
logger.debug(f"[GitHubPackageResolver] Downloading from {download_url}")
|
|
184
|
+
|
|
185
|
+
def download_progress(block_num, block_size, total_size):
|
|
186
|
+
if log_callback and total_size > 0:
|
|
187
|
+
progress = (block_num * block_size / total_size) * 100
|
|
188
|
+
if progress % 10 < 5: # Log every ~10%
|
|
189
|
+
log_callback(f"Downloading {filename}: {progress:.0f}%")
|
|
190
|
+
|
|
191
|
+
urllib.request.urlretrieve(download_url, dest_path, download_progress)
|
|
192
|
+
|
|
193
|
+
logger.info(f"[GitHubPackageResolver] Downloaded: {dest_path}")
|
|
194
|
+
if log_callback:
|
|
195
|
+
log_callback(f"Downloaded {filename}")
|
|
196
|
+
|
|
197
|
+
return dest_path
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.warning(
|
|
201
|
+
f"[GitHubPackageResolver] Failed to download from {download_url}: {e}"
|
|
202
|
+
)
|
|
203
|
+
if log_callback:
|
|
204
|
+
log_callback(f"Failed: {e}, trying next source...")
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
raise Exception(f"Failed to download wheel from all sources: {filename}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class LumenPackageResolver:
|
|
211
|
+
"""Resolves Lumen package names and installation commands."""
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def resolve_packages(lumen_config: LumenConfig) -> list[str]:
|
|
215
|
+
"""Extract package names from LumenConfig.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
lumen_config: Lumen configuration
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
List of package names (e.g., ["lumen_ocr", "lumen_clip"])
|
|
222
|
+
"""
|
|
223
|
+
packages = []
|
|
224
|
+
|
|
225
|
+
# Get deployment services
|
|
226
|
+
deployment = lumen_config.deployment
|
|
227
|
+
|
|
228
|
+
if hasattr(deployment, "services") and deployment.services:
|
|
229
|
+
for service in deployment.services:
|
|
230
|
+
if hasattr(service, "root"):
|
|
231
|
+
root = service.root
|
|
232
|
+
package_name = f"lumen_{root}"
|
|
233
|
+
packages.append(package_name)
|
|
234
|
+
|
|
235
|
+
logger.info(f"[LumenPackageResolver] Resolved packages: {packages}")
|
|
236
|
+
return list(set(packages)) # Remove duplicates
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def build_pip_install_args(
|
|
240
|
+
packages: list[str],
|
|
241
|
+
device_config: DeviceConfig,
|
|
242
|
+
region: Region,
|
|
243
|
+
wheel_paths: dict[str, Path] | None = None,
|
|
244
|
+
) -> list[str]:
|
|
245
|
+
"""Build pip installation command arguments.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
packages: List of package names (e.g., ["lumen_ocr", "lumen_clip"])
|
|
249
|
+
device_config: Device configuration with dependency metadata
|
|
250
|
+
region: Region for mirror selection
|
|
251
|
+
wheel_paths: Dictionary mapping package names to wheel file paths
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
List of pip command arguments
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
["install", "--index-url", "...", "--extra-index-url", "...",
|
|
258
|
+
"/path/to/lumen_ocr-0.4.1-py3-none-any.whl[apple]", "--no-cache-dir"]
|
|
259
|
+
"""
|
|
260
|
+
args = ["install"]
|
|
261
|
+
|
|
262
|
+
# Add index URLs
|
|
263
|
+
mirror_selector = MirrorSelector()
|
|
264
|
+
indexes = mirror_selector.get_pypi_indexes(region)
|
|
265
|
+
|
|
266
|
+
# Add --index-url (first mirror)
|
|
267
|
+
if indexes:
|
|
268
|
+
args.extend(["--index-url", indexes[0]])
|
|
269
|
+
|
|
270
|
+
# Add custom extra index URLs (for CUDA, etc.)
|
|
271
|
+
# NOTE: Don't add PyPI as extra-index-url automatically to preserve mirror speed
|
|
272
|
+
if device_config.dependency_metadata:
|
|
273
|
+
meta = device_config.dependency_metadata
|
|
274
|
+
|
|
275
|
+
# Add custom extra index URLs
|
|
276
|
+
if meta.extra_index_url:
|
|
277
|
+
for url in meta.extra_index_url:
|
|
278
|
+
args.extend(["--extra-index-url", url])
|
|
279
|
+
|
|
280
|
+
# Add wheel file paths with extras
|
|
281
|
+
if wheel_paths:
|
|
282
|
+
for pkg in packages:
|
|
283
|
+
if pkg in wheel_paths:
|
|
284
|
+
wheel_path = str(wheel_paths[pkg])
|
|
285
|
+
|
|
286
|
+
# Add extras if specified
|
|
287
|
+
if (
|
|
288
|
+
device_config.dependency_metadata
|
|
289
|
+
and device_config.dependency_metadata.extra_deps
|
|
290
|
+
):
|
|
291
|
+
extra_deps = ",".join(
|
|
292
|
+
device_config.dependency_metadata.extra_deps
|
|
293
|
+
)
|
|
294
|
+
wheel_path = f"{wheel_path}[{extra_deps}]"
|
|
295
|
+
|
|
296
|
+
args.append(wheel_path)
|
|
297
|
+
|
|
298
|
+
# Add additional install args
|
|
299
|
+
if (
|
|
300
|
+
device_config.dependency_metadata
|
|
301
|
+
and device_config.dependency_metadata.install_args
|
|
302
|
+
):
|
|
303
|
+
args.extend(device_config.dependency_metadata.install_args)
|
|
304
|
+
|
|
305
|
+
# Add --no-cache-dir to avoid cache issues
|
|
306
|
+
args.append("--no-cache-dir")
|
|
307
|
+
|
|
308
|
+
logger.debug(f"[LumenPackageResolver] Pip args: {args}")
|
|
309
|
+
return args
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Preset registry for Lumen device configurations.
|
|
3
|
+
|
|
4
|
+
Provides dynamic discovery of available presets from DeviceConfig,
|
|
5
|
+
eliminating hardcoded preset identifiers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Callable
|
|
13
|
+
|
|
14
|
+
from lumen_app.core.config import DeviceConfig
|
|
15
|
+
from lumen_app.utils.logger import get_logger
|
|
16
|
+
|
|
17
|
+
logger = get_logger("lumen.preset_registry")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PresetInfo:
|
|
22
|
+
"""Information about a device preset."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
description: str
|
|
26
|
+
factory_method: Callable[[], DeviceConfig]
|
|
27
|
+
requires_drivers: bool = True
|
|
28
|
+
priority: int = 50 # Lower number = higher priority (0-100)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PresetRegistry:
|
|
32
|
+
"""
|
|
33
|
+
Dynamic registry of DeviceConfig presets.
|
|
34
|
+
|
|
35
|
+
Automatically discovers all factory methods (classmethods) from DeviceConfig
|
|
36
|
+
that return DeviceConfig instances, eliminating the need for hardcoded preset names.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
_presets: dict[str, PresetInfo] | None = None
|
|
40
|
+
|
|
41
|
+
# Preset priority mapping (lower = higher priority)
|
|
42
|
+
_PRESET_PRIORITIES = {
|
|
43
|
+
"nvidia_gpu_high": 5,
|
|
44
|
+
"nvidia_gpu": 10,
|
|
45
|
+
"nvidia_jetson_high": 12,
|
|
46
|
+
"nvidia_jetson": 15,
|
|
47
|
+
"apple_silicon": 20,
|
|
48
|
+
"rockchip": 25,
|
|
49
|
+
"intel_gpu": 30,
|
|
50
|
+
"amd_gpu_win": 35,
|
|
51
|
+
"amd_npu": 40,
|
|
52
|
+
"cpu": 100, # Lowest priority (fallback)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def _discover_presets(cls) -> dict[str, PresetInfo]:
|
|
57
|
+
"""Discover all preset factory methods from DeviceConfig."""
|
|
58
|
+
if cls._presets is not None:
|
|
59
|
+
return cls._presets
|
|
60
|
+
|
|
61
|
+
presets = {}
|
|
62
|
+
|
|
63
|
+
# Scan all members of DeviceConfig (including bound methods)
|
|
64
|
+
for name in dir(DeviceConfig):
|
|
65
|
+
# Skip private attributes
|
|
66
|
+
if name.startswith("_"):
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
# Get the member
|
|
70
|
+
try:
|
|
71
|
+
member = getattr(DeviceConfig, name)
|
|
72
|
+
except AttributeError:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# Skip non-methods and dataclass fields
|
|
76
|
+
if not callable(member):
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# Check if it's a classmethod descriptor
|
|
80
|
+
is_classmethod = False
|
|
81
|
+
try:
|
|
82
|
+
raw_attr = inspect.getattr_static(DeviceConfig, name)
|
|
83
|
+
is_classmethod = isinstance(raw_attr, classmethod)
|
|
84
|
+
except AttributeError:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
if not is_classmethod:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
# Try to call it and see if it returns a DeviceConfig
|
|
91
|
+
try:
|
|
92
|
+
result = member()
|
|
93
|
+
if isinstance(result, DeviceConfig):
|
|
94
|
+
# Get description from DeviceConfig.description or docstring
|
|
95
|
+
if hasattr(result, "description") and result.description:
|
|
96
|
+
description = result.description
|
|
97
|
+
else:
|
|
98
|
+
docstring = inspect.getdoc(member) or ""
|
|
99
|
+
description = docstring.split("\n")[0] if docstring else name
|
|
100
|
+
|
|
101
|
+
# Get priority from mapping, default to 50
|
|
102
|
+
priority = cls._PRESET_PRIORITIES.get(name, 50)
|
|
103
|
+
|
|
104
|
+
presets[name] = PresetInfo(
|
|
105
|
+
name=name,
|
|
106
|
+
description=description,
|
|
107
|
+
factory_method=member,
|
|
108
|
+
requires_drivers=name
|
|
109
|
+
!= "cpu", # CPU preset requires no special drivers
|
|
110
|
+
priority=priority,
|
|
111
|
+
)
|
|
112
|
+
logger.debug(f"Discovered preset: {name} (priority: {priority})")
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.debug(f"Skipping {name}: {e}")
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
cls._presets = presets
|
|
118
|
+
logger.info(f"Discovered {len(presets)} device presets")
|
|
119
|
+
return presets
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def get_all_presets(cls) -> dict[str, PresetInfo]:
|
|
123
|
+
"""Get all available presets."""
|
|
124
|
+
return cls._discover_presets().copy()
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def get_preset(cls, name: str) -> PresetInfo | None:
|
|
128
|
+
"""Get a specific preset by name."""
|
|
129
|
+
presets = cls._discover_presets()
|
|
130
|
+
return presets.get(name)
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def preset_exists(cls, name: str) -> bool:
|
|
134
|
+
"""Check if a preset exists."""
|
|
135
|
+
return name in cls._discover_presets()
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def get_preset_names(cls) -> list[str]:
|
|
139
|
+
"""Get list of all preset names."""
|
|
140
|
+
return list(cls._discover_presets().keys())
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def get_detection_order(cls) -> list[str]:
|
|
144
|
+
"""
|
|
145
|
+
Get preset names in detection order (highest to lowest priority).
|
|
146
|
+
|
|
147
|
+
Returns presets sorted by priority, with lower numbers checked first.
|
|
148
|
+
This ensures the best available hardware is detected first.
|
|
149
|
+
"""
|
|
150
|
+
presets = cls._discover_presets()
|
|
151
|
+
# Sort by priority (lower number = higher priority)
|
|
152
|
+
sorted_presets = sorted(presets.values(), key=lambda p: p.priority)
|
|
153
|
+
return [p.name for p in sorted_presets]
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def create_config(cls, preset_name: str) -> DeviceConfig:
|
|
157
|
+
"""
|
|
158
|
+
Create a DeviceConfig from preset name.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
preset_name: Name of the preset (e.g., "nvidia_gpu", "apple_silicon")
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
DeviceConfig instance
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If preset doesn't exist
|
|
168
|
+
"""
|
|
169
|
+
preset = cls.get_preset(preset_name)
|
|
170
|
+
if preset is None:
|
|
171
|
+
available = ", ".join(cls.get_preset_names())
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"Unknown preset '{preset_name}'. Available presets: {available}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return preset.factory_method()
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def get_driver_requirements(cls, preset_name: str) -> list[str]:
|
|
180
|
+
"""
|
|
181
|
+
Get required driver checks for a preset.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
preset_name: Name of the preset
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of driver names to check
|
|
188
|
+
"""
|
|
189
|
+
preset = cls.get_preset(preset_name)
|
|
190
|
+
if preset is None or not preset.requires_drivers:
|
|
191
|
+
return []
|
|
192
|
+
|
|
193
|
+
config = preset.factory_method()
|
|
194
|
+
|
|
195
|
+
# Map runtime/providers to required drivers
|
|
196
|
+
requirements = []
|
|
197
|
+
providers = config.onnx_providers or []
|
|
198
|
+
|
|
199
|
+
for provider in providers:
|
|
200
|
+
provider_name = (
|
|
201
|
+
provider
|
|
202
|
+
if isinstance(provider, str)
|
|
203
|
+
else (provider[0] if provider else "")
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if "CUDA" in provider_name or "TensorRT" in provider_name:
|
|
207
|
+
requirements.append("cuda")
|
|
208
|
+
elif "CoreML" in provider_name:
|
|
209
|
+
requirements.append("coreml")
|
|
210
|
+
elif "OpenVINO" in provider_name:
|
|
211
|
+
requirements.append("openvino")
|
|
212
|
+
elif "DML" in provider_name:
|
|
213
|
+
requirements.append("directml")
|
|
214
|
+
|
|
215
|
+
# RKNN runtime
|
|
216
|
+
if config.runtime.value == "rknn":
|
|
217
|
+
requirements.append("rknn")
|
|
218
|
+
|
|
219
|
+
return list(set(requirements)) # Remove duplicates
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API routers for Lumen Web."""
|