amv-vision 2.0.0__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.
- amv_vision/__init__.py +75 -0
- amv_vision/__main__.py +7 -0
- amv_vision/adapter.py +201 -0
- amv_vision/bridge/__init__.py +13 -0
- amv_vision/bridge/amv_train_bridge.py +87 -0
- amv_vision/cli/__init__.py +5 -0
- amv_vision/cli/doctor.py +122 -0
- amv_vision/cli/main.py +548 -0
- amv_vision/cluster.py +219 -0
- amv_vision/control/__init__.py +5 -0
- amv_vision/control/component.py +391 -0
- amv_vision/control/status.py +15 -0
- amv_vision/csrc/__init__.py +24 -0
- amv_vision/csrc/backend.py +488 -0
- amv_vision/cv/__init__.py +24 -0
- amv_vision/cv/contours.py +79 -0
- amv_vision/cv/crop.py +24 -0
- amv_vision/cv/filters.py +165 -0
- amv_vision/cv/integral.py +62 -0
- amv_vision/cv/morphology.py +76 -0
- amv_vision/detect/__init__.py +16 -0
- amv_vision/detect/haar.py +194 -0
- amv_vision/detect/neural.py +129 -0
- amv_vision/detect/nms.py +85 -0
- amv_vision/detect/types.py +51 -0
- amv_vision/downloader.py +38 -0
- amv_vision/errors.py +306 -0
- amv_vision/exceptions.py +273 -0
- amv_vision/hardware.py +295 -0
- amv_vision/installer.py +573 -0
- amv_vision/io/__init__.py +14 -0
- amv_vision/io/camera.py +39 -0
- amv_vision/io/limits.py +35 -0
- amv_vision/io/loader.py +116 -0
- amv_vision/io/safetensors.py +99 -0
- amv_vision/models/__init__.py +17 -0
- amv_vision/models/conv_block.py +89 -0
- amv_vision/models/embedding.py +44 -0
- amv_vision/models/mobilenet.py +78 -0
- amv_vision/models/vit_patch.py +62 -0
- amv_vision/models/yolo_detector.py +89 -0
- amv_vision/models/yolo_post.py +83 -0
- amv_vision/transforms/__init__.py +46 -0
- amv_vision/transforms/compose.py +59 -0
- amv_vision/transforms/functional.py +115 -0
- amv_vision/transforms/scale.py +101 -0
- amv_vision/vlm/__init__.py +38 -0
- amv_vision/vlm/adapters/__init__.py +19 -0
- amv_vision/vlm/adapters/base.py +19 -0
- amv_vision/vlm/adapters/qwen2vl.py +10 -0
- amv_vision/vlm/adapters/smolvlm.py +10 -0
- amv_vision/vlm/api.py +290 -0
- amv_vision/vlm/cache.py +553 -0
- amv_vision/vlm/engine.py +243 -0
- amv_vision/vlm/manifest.py +89 -0
- amv_vision/vlm/memory.py +115 -0
- amv_vision/vlm/model_hub.py +150 -0
- amv_vision/vlm/pipeline.py +76 -0
- amv_vision/vlm/protocol.py +122 -0
- amv_vision/vlm/result.py +35 -0
- amv_vision/vlm/runtime/resolver.py +166 -0
- amv_vision/vlm/runtime/subprocess.py +263 -0
- amv_vision/vlm/supervisor.py +192 -0
- amv_vision-2.0.0.dist-info/METADATA +359 -0
- amv_vision-2.0.0.dist-info/RECORD +71 -0
- amv_vision-2.0.0.dist-info/WHEEL +5 -0
- amv_vision-2.0.0.dist-info/entry_points.txt +5 -0
- amv_vision-2.0.0.dist-info/licenses/LICENSE +176 -0
- amv_vision-2.0.0.dist-info/licenses/NOTICE +17 -0
- amv_vision-2.0.0.dist-info/top_level.txt +2 -0
- termux_vision/__init__.py +55 -0
amv_vision/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
"""
|
|
4
|
+
amv-vision: Lightweight On-Device Computer Vision & VLM Framework for Android Termux.
|
|
5
|
+
Open-Source under Apache License 2.0.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "2.0.0"
|
|
9
|
+
__author__ = "amv-vision contributors"
|
|
10
|
+
__license__ = "Apache-2.0"
|
|
11
|
+
|
|
12
|
+
from . import errors
|
|
13
|
+
from . import io
|
|
14
|
+
from . import transforms
|
|
15
|
+
from . import cv
|
|
16
|
+
from . import detect
|
|
17
|
+
from . import models
|
|
18
|
+
from . import bridge
|
|
19
|
+
from . import csrc
|
|
20
|
+
from . import vlm
|
|
21
|
+
from . import cli
|
|
22
|
+
|
|
23
|
+
from .detect.types import BoundingBox, Detection
|
|
24
|
+
from .detect.haar import detect_faces
|
|
25
|
+
from .models.embedding import Embedding, compute_similarity
|
|
26
|
+
from .vlm.api import load
|
|
27
|
+
from .hardware import HardwareProfile, detect_hardware, resolve_device
|
|
28
|
+
from .downloader import list_models, download_model, resolve_model_path
|
|
29
|
+
from . import cluster
|
|
30
|
+
from .exceptions import (
|
|
31
|
+
AmevaTermuxError,
|
|
32
|
+
TermuxVisionError,
|
|
33
|
+
ClusterConnectionError,
|
|
34
|
+
ClusterConfigurationError,
|
|
35
|
+
)
|
|
36
|
+
from .cluster import (
|
|
37
|
+
parse_cluster_rpc_spec,
|
|
38
|
+
verify_rpc_cluster_nodes,
|
|
39
|
+
verify_rpc_cluster_health,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"io",
|
|
44
|
+
"transforms",
|
|
45
|
+
"cv",
|
|
46
|
+
"detect",
|
|
47
|
+
"models",
|
|
48
|
+
"bridge",
|
|
49
|
+
"csrc",
|
|
50
|
+
"vlm",
|
|
51
|
+
"cluster",
|
|
52
|
+
"errors",
|
|
53
|
+
"cli",
|
|
54
|
+
"BoundingBox",
|
|
55
|
+
"Detection",
|
|
56
|
+
"detect_faces",
|
|
57
|
+
"Embedding",
|
|
58
|
+
"compute_similarity",
|
|
59
|
+
"load",
|
|
60
|
+
"HardwareProfile",
|
|
61
|
+
"detect_hardware",
|
|
62
|
+
"resolve_device",
|
|
63
|
+
"list_models",
|
|
64
|
+
"download_model",
|
|
65
|
+
"resolve_model_path",
|
|
66
|
+
"AmevaTermuxError",
|
|
67
|
+
"TermuxVisionError",
|
|
68
|
+
"ClusterConnectionError",
|
|
69
|
+
"ClusterConfigurationError",
|
|
70
|
+
"parse_cluster_rpc_spec",
|
|
71
|
+
"verify_rpc_cluster_nodes",
|
|
72
|
+
"verify_rpc_cluster_health",
|
|
73
|
+
"__version__"
|
|
74
|
+
]
|
|
75
|
+
|
amv_vision/__main__.py
ADDED
amv_vision/adapter.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
"""
|
|
4
|
+
amv_vision.adapter
|
|
5
|
+
======================
|
|
6
|
+
AMEVA Component Protocol v1 — Orchestrator Adapter (v0.8.1 호환)
|
|
7
|
+
|
|
8
|
+
P0-2: infer() fallback yield _not_supported → raise OperationNotSupported
|
|
9
|
+
P0-4: except Exception → retryable 분류, 원본 오류 코드 보존
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, AsyncIterator
|
|
14
|
+
|
|
15
|
+
from ameva_component.adapter_base import BaseOrchestratorAdapter
|
|
16
|
+
from ameva_component.exceptions import (
|
|
17
|
+
ComponentError,
|
|
18
|
+
OperationNotSupported,
|
|
19
|
+
redact_details,
|
|
20
|
+
redact_text,
|
|
21
|
+
)
|
|
22
|
+
from amv_vision.control.component import VisionControl
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VisionOrchestratorAdapter(BaseOrchestratorAdapter):
|
|
26
|
+
"""Vision Orchestrator Adapter.
|
|
27
|
+
|
|
28
|
+
이미지 분석 / VLM captioning은 파일 기반으로 수행됩니다.
|
|
29
|
+
infer(): image_path를 받아 분석 결과를 반환합니다.
|
|
30
|
+
Native backend 실패 시 fallback_used=True를 명시합니다.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
COMPONENT_ID = "amv-vision"
|
|
34
|
+
|
|
35
|
+
_RETRYABLE_CODES: frozenset[str] = frozenset({
|
|
36
|
+
"REMOTE_TIMEOUT",
|
|
37
|
+
"MODEL_BUSY",
|
|
38
|
+
"BACKEND_OVERLOADED",
|
|
39
|
+
})
|
|
40
|
+
_NON_RETRYABLE_CODES: frozenset[str] = frozenset({
|
|
41
|
+
"IMAGE_PATH_FORBIDDEN",
|
|
42
|
+
"IMAGE_FORMAT_UNSUPPORTED",
|
|
43
|
+
"IMAGE_CORRUPT",
|
|
44
|
+
"MODEL_NOT_FOUND",
|
|
45
|
+
"HASH_MISMATCH",
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
def __init__(self, control: VisionControl | None = None) -> None:
|
|
49
|
+
self._control = control or VisionControl()
|
|
50
|
+
|
|
51
|
+
async def infer(self, request: dict[str, Any]) -> AsyncIterator[dict[str, Any]]:
|
|
52
|
+
"""Vision inference: image_path → analysis result.
|
|
53
|
+
|
|
54
|
+
request 키:
|
|
55
|
+
image_path (str): 분석할 이미지 파일 경로 (필수)
|
|
56
|
+
task (str): "caption" | "vlm" | "detect" (선택, 기본: "caption")
|
|
57
|
+
prompt (str): VLM 프롬프트 (task="vlm"일 때)
|
|
58
|
+
model_id (str): 모델 ID (선택)
|
|
59
|
+
|
|
60
|
+
반환 프레임:
|
|
61
|
+
{"type": "vision_result", "task": str, "result": dict, ...}
|
|
62
|
+
{"type": "error", "ok": False, "error": {...}}
|
|
63
|
+
"""
|
|
64
|
+
image_path = request.get("image_path", "").strip()
|
|
65
|
+
if not image_path:
|
|
66
|
+
yield {
|
|
67
|
+
"type": "error",
|
|
68
|
+
"ok": False,
|
|
69
|
+
"error": {
|
|
70
|
+
"code": "IMAGE_PATH_MISSING",
|
|
71
|
+
"message": "image_path is required for Vision infer",
|
|
72
|
+
"operation": "infer",
|
|
73
|
+
"component_id": self.COMPONENT_ID,
|
|
74
|
+
"retryable": False,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if not hasattr(self._control, "analyze"):
|
|
80
|
+
# P0-2: fallback yield → raise
|
|
81
|
+
raise OperationNotSupported(operation="infer.analyze", component_id=self.COMPONENT_ID)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
result = await self._control.analyze(request)
|
|
85
|
+
|
|
86
|
+
if not isinstance(result, dict):
|
|
87
|
+
raise ValueError(f"analyze() must return dict, got {type(result).__name__}")
|
|
88
|
+
|
|
89
|
+
if result.get("ok") is not True:
|
|
90
|
+
err_payload = result.get("error") if isinstance(result.get("error"), dict) else {}
|
|
91
|
+
yield {
|
|
92
|
+
"type": "error",
|
|
93
|
+
"ok": False,
|
|
94
|
+
"error": {
|
|
95
|
+
"code": err_payload.get("code", "ADAPTER_RESULT_NOT_SUCCESS"),
|
|
96
|
+
"message": err_payload.get("message", "analyze() did not return ok=True"),
|
|
97
|
+
"operation": "infer",
|
|
98
|
+
"component_id": self.COMPONENT_ID,
|
|
99
|
+
"retryable": False,
|
|
100
|
+
"details": {"result_keys": sorted(result.keys())},
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if "result" not in result:
|
|
106
|
+
raise ValueError("analyze() result missing required field 'result'")
|
|
107
|
+
|
|
108
|
+
yield {
|
|
109
|
+
"type": "vision_result",
|
|
110
|
+
"task": request.get("task", "caption"),
|
|
111
|
+
"result": result["result"],
|
|
112
|
+
"fallback_used": result.get("fallback_used", False),
|
|
113
|
+
"requested_backend": result.get("requested_backend"),
|
|
114
|
+
"executed_backend": result.get("executed_backend"),
|
|
115
|
+
"ok": True,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
except ComponentError as component_err:
|
|
119
|
+
# HIGH 1: ComponentError public_message와 redact_details로 외부 노출 보안 격리
|
|
120
|
+
import logging
|
|
121
|
+
logging.getLogger(__name__).exception(
|
|
122
|
+
"Vision component operation failed",
|
|
123
|
+
extra={
|
|
124
|
+
"component_id": self.COMPONENT_ID,
|
|
125
|
+
"operation": "infer",
|
|
126
|
+
"code": getattr(component_err, "code", "COMPONENT_ERROR"),
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
if hasattr(component_err, "to_public_dict"):
|
|
130
|
+
err_dict = component_err.to_public_dict()
|
|
131
|
+
else:
|
|
132
|
+
err_dict = {
|
|
133
|
+
"code": getattr(component_err, "code", "COMPONENT_ERROR"),
|
|
134
|
+
"message": redact_text(getattr(component_err, "public_message", "Component operation failed")),
|
|
135
|
+
"retryable": getattr(component_err, "retryable", False),
|
|
136
|
+
"details": redact_details(getattr(component_err, "details", {})),
|
|
137
|
+
}
|
|
138
|
+
yield {
|
|
139
|
+
"type": "error",
|
|
140
|
+
"ok": False,
|
|
141
|
+
"error": {
|
|
142
|
+
**err_dict,
|
|
143
|
+
"operation": "infer",
|
|
144
|
+
"component_id": self.COMPONENT_ID,
|
|
145
|
+
"retryable": self._classify_retryable(
|
|
146
|
+
err_dict.get("code", ""), default=err_dict.get("retryable", False)
|
|
147
|
+
),
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
except (ValueError, TypeError) as contract_err:
|
|
152
|
+
# P0-3/4 & HIGH 1: 계약 위반 (str(contract_err) 직접 노출 금지, 내부 로그 격리)
|
|
153
|
+
import logging
|
|
154
|
+
logging.getLogger(__name__).exception("Vision contract validation error during infer: %s", contract_err)
|
|
155
|
+
yield {
|
|
156
|
+
"type": "error",
|
|
157
|
+
"ok": False,
|
|
158
|
+
"error": {
|
|
159
|
+
"code": "ADAPTER_CONTRACT_ERROR",
|
|
160
|
+
"message": "Adapter contract validation failed",
|
|
161
|
+
"operation": "infer",
|
|
162
|
+
"component_id": self.COMPONENT_ID,
|
|
163
|
+
"retryable": False,
|
|
164
|
+
"details": {
|
|
165
|
+
"cause_type": type(contract_err).__name__,
|
|
166
|
+
"operation": "infer",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
except Exception as unexpected_err:
|
|
172
|
+
import logging
|
|
173
|
+
logging.getLogger(__name__).exception("Vision adapter unexpected error during infer: %s", unexpected_err)
|
|
174
|
+
code = getattr(unexpected_err, "code", "ADAPTER_INTERNAL_ERROR")
|
|
175
|
+
yield {
|
|
176
|
+
"type": "error",
|
|
177
|
+
"ok": False,
|
|
178
|
+
"error": {
|
|
179
|
+
"code": code if isinstance(code, str) else "ADAPTER_INTERNAL_ERROR",
|
|
180
|
+
"message": "Unexpected adapter failure",
|
|
181
|
+
"operation": "infer",
|
|
182
|
+
"component_id": self.COMPONENT_ID,
|
|
183
|
+
"retryable": False,
|
|
184
|
+
"details": {
|
|
185
|
+
"cause_type": type(unexpected_err).__name__,
|
|
186
|
+
"operation": "infer",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
def _classify_retryable(self, code: str, *, default: bool = False) -> bool:
|
|
192
|
+
if code in self._RETRYABLE_CODES:
|
|
193
|
+
return True
|
|
194
|
+
if code in self._NON_RETRYABLE_CODES:
|
|
195
|
+
return False
|
|
196
|
+
return default
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def create_adapter() -> VisionOrchestratorAdapter:
|
|
200
|
+
"""Entry Point Factory."""
|
|
201
|
+
return VisionOrchestratorAdapter()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
from .amv_train_bridge import (
|
|
4
|
+
to_termux_tensor,
|
|
5
|
+
from_termux_tensor,
|
|
6
|
+
VisionPatchDataset
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"to_termux_tensor",
|
|
11
|
+
"from_termux_tensor",
|
|
12
|
+
"VisionPatchDataset"
|
|
13
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
def to_termux_tensor(data, requires_grad: bool = False):
|
|
6
|
+
"""
|
|
7
|
+
Convert a NumPy image array or patch matrix to amv_train.Tensor.
|
|
8
|
+
Gracefully imports amv_train or creates equivalent autograd structure.
|
|
9
|
+
"""
|
|
10
|
+
try:
|
|
11
|
+
import amv_train as tt
|
|
12
|
+
if isinstance(data, np.ndarray):
|
|
13
|
+
# Direct flat C-buffer pass without .tolist() to eliminate 8x memory expansion and LMK kills
|
|
14
|
+
return tt.Tensor(data, dtype="float32", requires_grad=requires_grad)
|
|
15
|
+
elif isinstance(data, (list, tuple)):
|
|
16
|
+
return tt.Tensor(data, dtype="float32", requires_grad=requires_grad)
|
|
17
|
+
elif hasattr(data, "to_numpy"):
|
|
18
|
+
return tt.Tensor(data.to_numpy(), dtype="float32", requires_grad=requires_grad)
|
|
19
|
+
else:
|
|
20
|
+
return tt.Tensor(data, dtype="float32", requires_grad=requires_grad)
|
|
21
|
+
except ImportError:
|
|
22
|
+
# Fallback if amv-train is not installed in current environment
|
|
23
|
+
class StandaloneVisionTensor:
|
|
24
|
+
def __init__(self, arr, requires_grad=False):
|
|
25
|
+
self.data = np.array(arr, dtype=np.float32)
|
|
26
|
+
self.shape = self.data.shape
|
|
27
|
+
self.requires_grad = requires_grad
|
|
28
|
+
self.grad = None
|
|
29
|
+
def to_numpy(self):
|
|
30
|
+
return self.data
|
|
31
|
+
def __repr__(self):
|
|
32
|
+
return f"StandaloneVisionTensor(shape={self.shape}, dtype={self.data.dtype})"
|
|
33
|
+
return StandaloneVisionTensor(data, requires_grad=requires_grad)
|
|
34
|
+
|
|
35
|
+
def from_termux_tensor(tensor) -> np.ndarray:
|
|
36
|
+
"""
|
|
37
|
+
Convert a amv_train.Tensor or StandaloneVisionTensor back to NumPy array.
|
|
38
|
+
"""
|
|
39
|
+
if hasattr(tensor, "to_numpy"):
|
|
40
|
+
return np.asarray(tensor.to_numpy(), dtype=np.float32)
|
|
41
|
+
elif hasattr(tensor, "numpy"):
|
|
42
|
+
return np.asarray(tensor.numpy(), dtype=np.float32)
|
|
43
|
+
elif hasattr(tensor, "tolist"):
|
|
44
|
+
return np.asarray(tensor.tolist(), dtype=np.float32)
|
|
45
|
+
elif hasattr(tensor, "_data"):
|
|
46
|
+
if hasattr(tensor._data, "to_list"):
|
|
47
|
+
return np.asarray(tensor._data.to_list(), dtype=np.float32)
|
|
48
|
+
return np.asarray(tensor._data, dtype=np.float32)
|
|
49
|
+
elif hasattr(tensor, "data"):
|
|
50
|
+
if isinstance(tensor.data, np.ndarray):
|
|
51
|
+
return np.asarray(tensor.data, dtype=np.float32)
|
|
52
|
+
return np.asarray(tensor.data, dtype=np.float32)
|
|
53
|
+
elif isinstance(tensor, np.ndarray):
|
|
54
|
+
return np.asarray(tensor, dtype=np.float32)
|
|
55
|
+
else:
|
|
56
|
+
return np.asarray(tensor, dtype=np.float32)
|
|
57
|
+
|
|
58
|
+
class VisionPatchDataset:
|
|
59
|
+
"""
|
|
60
|
+
On-Device Dataset for batching image patches for ViT or classifier training.
|
|
61
|
+
"""
|
|
62
|
+
def __init__(self, patches: np.ndarray, labels: np.ndarray = None, batch_size: int = 32, shuffle: bool = True):
|
|
63
|
+
self.patches = np.asarray(patches, dtype=np.float32)
|
|
64
|
+
self.labels = np.asarray(labels, dtype=np.float32) if labels is not None else None
|
|
65
|
+
self.batch_size = batch_size
|
|
66
|
+
self.shuffle = shuffle
|
|
67
|
+
self.num_samples = len(self.patches)
|
|
68
|
+
|
|
69
|
+
def __len__(self):
|
|
70
|
+
return (self.num_samples + self.batch_size - 1) // self.batch_size
|
|
71
|
+
|
|
72
|
+
def __iter__(self):
|
|
73
|
+
indices = np.arange(self.num_samples)
|
|
74
|
+
if self.shuffle:
|
|
75
|
+
np.random.shuffle(indices)
|
|
76
|
+
|
|
77
|
+
for i in range(0, self.num_samples, self.batch_size):
|
|
78
|
+
batch_idx = indices[i:i + self.batch_size]
|
|
79
|
+
batch_x = self.patches[batch_idx]
|
|
80
|
+
batch_tensor_x = to_termux_tensor(batch_x)
|
|
81
|
+
|
|
82
|
+
if self.labels is not None:
|
|
83
|
+
batch_y = self.labels[batch_idx]
|
|
84
|
+
batch_tensor_y = to_termux_tensor(batch_y)
|
|
85
|
+
yield batch_tensor_x, batch_tensor_y
|
|
86
|
+
else:
|
|
87
|
+
yield batch_tensor_x
|
amv_vision/cli/doctor.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import shutil
|
|
6
|
+
import platform
|
|
7
|
+
import subprocess
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from typing import Dict, Any, Optional
|
|
11
|
+
from .. import __version__
|
|
12
|
+
from .. import csrc
|
|
13
|
+
from ..vlm.cache import ModelCacheManager
|
|
14
|
+
from amv_vision.exceptions import RuntimeRequiredError
|
|
15
|
+
|
|
16
|
+
def run_doctor(probe_vulkan: bool = False, full_check: bool = False) -> Dict[str, Any]:
|
|
17
|
+
"""
|
|
18
|
+
Runs truthful diagnostic inspection of the Android Termux runtime environment.
|
|
19
|
+
- Default: Read-only environment inspection.
|
|
20
|
+
- probe_vulkan: Checks driver presence and reports execution capability (Zero-Hype / Ground Truth).
|
|
21
|
+
- full_check: Runs actual SHA-256 and byte-level integrity verification on installed models.
|
|
22
|
+
"""
|
|
23
|
+
cache_mgr = ModelCacheManager()
|
|
24
|
+
|
|
25
|
+
report = {
|
|
26
|
+
"schema_version": 1,
|
|
27
|
+
"client_version": __version__,
|
|
28
|
+
"runtime_version": __version__,
|
|
29
|
+
"platform": {
|
|
30
|
+
"system": platform.system(),
|
|
31
|
+
"machine": platform.machine(),
|
|
32
|
+
"python_version": platform.python_version(),
|
|
33
|
+
"is_android": os.path.exists("/system/build.prop") or "ANDROID_ROOT" in os.environ
|
|
34
|
+
},
|
|
35
|
+
"hardware": {
|
|
36
|
+
"cpu_cores": os.cpu_count() or 1,
|
|
37
|
+
"total_ram_mb": None,
|
|
38
|
+
"available_ram_mb": None
|
|
39
|
+
},
|
|
40
|
+
"vulkan": {
|
|
41
|
+
"driver_dir": None,
|
|
42
|
+
"vulkan_loader_installed": shutil.which("vulkaninfo") is not None,
|
|
43
|
+
"loader_detected": shutil.which("vulkaninfo") is not None,
|
|
44
|
+
"driver_file_detected": False,
|
|
45
|
+
"compute_probe_executed": probe_vulkan,
|
|
46
|
+
"safe_for_vlm": None,
|
|
47
|
+
"status": "unverified"
|
|
48
|
+
},
|
|
49
|
+
"vlm_runtime": {
|
|
50
|
+
"llama_cli_available": shutil.which("llama-cli") is not None,
|
|
51
|
+
"multimodal_capable": False,
|
|
52
|
+
"multimodal_status": "unverified",
|
|
53
|
+
"installed_models_count": 0,
|
|
54
|
+
"cache_dir": cache_mgr.cache_root
|
|
55
|
+
},
|
|
56
|
+
"native_backends": {
|
|
57
|
+
"has_c_backend": csrc.has_c_backend(),
|
|
58
|
+
"has_vulkan_backend": csrc.has_vulkan_backend(),
|
|
59
|
+
"vulkan_device_name": csrc.get_vulkan_device_name(),
|
|
60
|
+
"c_backend_errors": csrc.get_c_backend_load_errors(),
|
|
61
|
+
"cpp_backend_errors": csrc.get_cpp_backend_load_errors()
|
|
62
|
+
},
|
|
63
|
+
"recommended_preset": "Tier M (smolvlm-500m / 4-Threads CPU Reference)",
|
|
64
|
+
"warnings": []
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Verify multimodal capability via installer gatekeeper
|
|
68
|
+
try:
|
|
69
|
+
from ..installer import verify_multimodal_support
|
|
70
|
+
mm_ok, mm_msg = verify_multimodal_support()
|
|
71
|
+
report["vlm_runtime"]["multimodal_capable"] = mm_ok
|
|
72
|
+
report["vlm_runtime"]["multimodal_status"] = mm_msg
|
|
73
|
+
if not mm_ok and report["vlm_runtime"]["llama_cli_available"]:
|
|
74
|
+
report["warnings"].append(f"llama-cli detected but lacks multimodal support: {mm_msg}")
|
|
75
|
+
except RuntimeRequiredError:
|
|
76
|
+
raise
|
|
77
|
+
except Exception as _mm_err:
|
|
78
|
+
report["vlm_runtime"]["multimodal_status"] = f"Check failed: {_mm_err}"
|
|
79
|
+
|
|
80
|
+
# RAM Inspection
|
|
81
|
+
try:
|
|
82
|
+
with open("/proc/meminfo", "r") as f:
|
|
83
|
+
for line in f:
|
|
84
|
+
if line.startswith("MemTotal:"):
|
|
85
|
+
report["hardware"]["total_ram_mb"] = int(line.split()[1]) // 1024
|
|
86
|
+
elif line.startswith("MemAvailable:"):
|
|
87
|
+
report["hardware"]["available_ram_mb"] = int(line.split()[1]) // 1024
|
|
88
|
+
except (OSError, ValueError, IndexError) as _mem_err:
|
|
89
|
+
report["warnings"].append(f"RAM inspection failed: {_mem_err}")
|
|
90
|
+
|
|
91
|
+
# Truthful Vulkan status via ameva-runtime integration
|
|
92
|
+
if probe_vulkan:
|
|
93
|
+
try:
|
|
94
|
+
from ameva_runtime.vulkan.adapters import find_system_vulkan_driver_dir
|
|
95
|
+
from ameva_runtime import vulkan as avr
|
|
96
|
+
driver_dir = find_system_vulkan_driver_dir()
|
|
97
|
+
is_avail = bool(driver_dir is not None)
|
|
98
|
+
report["vulkan"]["driver_dir"] = driver_dir
|
|
99
|
+
report["vulkan"]["driver_file_detected"] = is_avail
|
|
100
|
+
report["vulkan"]["loader_detected"] = bool(is_avail or report["vulkan"]["vulkan_loader_installed"])
|
|
101
|
+
report["vulkan"]["ameva_runtime_detected"] = True
|
|
102
|
+
report["vulkan"]["status"] = "driver_detected_experimental" if is_avail else "disabled"
|
|
103
|
+
dev_name = avr.get_device_name() if hasattr(avr, "get_device_name") and callable(avr.get_device_name) else "Vulkan GPU"
|
|
104
|
+
report["vulkan"]["device_name"] = dev_name
|
|
105
|
+
report["vulkan"]["note"] = "Hardware driver dynamically discovered via ameva-runtime."
|
|
106
|
+
except ImportError:
|
|
107
|
+
report["vulkan"]["ameva_runtime_detected"] = False
|
|
108
|
+
report["vulkan"]["status"] = "disabled"
|
|
109
|
+
report["vulkan"]["safe_for_vlm"] = False
|
|
110
|
+
report["vulkan"]["note"] = "Install ameva-runtime for optimized GPU compute shaders."
|
|
111
|
+
|
|
112
|
+
# Model count and Actual Full SHA-256 check
|
|
113
|
+
installed = cache_mgr.list_installed()
|
|
114
|
+
report["vlm_runtime"]["installed_models_count"] = len(installed)
|
|
115
|
+
|
|
116
|
+
if full_check:
|
|
117
|
+
report["vlm_runtime"]["models_integrity"] = {}
|
|
118
|
+
for m in installed:
|
|
119
|
+
mid = m["model_id"]
|
|
120
|
+
report["vlm_runtime"]["models_integrity"][mid] = cache_mgr.verify_integrity(mid)
|
|
121
|
+
|
|
122
|
+
return report
|