abstractvision 0.1.0__py3-none-any.whl → 0.2.1__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.
- abstractvision/__init__.py +18 -3
- abstractvision/__main__.py +8 -0
- abstractvision/artifacts.py +320 -0
- abstractvision/assets/vision_model_capabilities.json +406 -0
- abstractvision/backends/__init__.py +43 -0
- abstractvision/backends/base_backend.py +63 -0
- abstractvision/backends/huggingface_diffusers.py +1503 -0
- abstractvision/backends/openai_compatible.py +325 -0
- abstractvision/backends/stable_diffusion_cpp.py +751 -0
- abstractvision/cli.py +778 -0
- abstractvision/errors.py +19 -0
- abstractvision/integrations/__init__.py +5 -0
- abstractvision/integrations/abstractcore.py +263 -0
- abstractvision/integrations/abstractcore_plugin.py +193 -0
- abstractvision/model_capabilities.py +255 -0
- abstractvision/types.py +95 -0
- abstractvision/vision_manager.py +115 -0
- abstractvision-0.2.1.dist-info/METADATA +243 -0
- abstractvision-0.2.1.dist-info/RECORD +23 -0
- {abstractvision-0.1.0.dist-info → abstractvision-0.2.1.dist-info}/WHEEL +1 -1
- abstractvision-0.2.1.dist-info/entry_points.txt +5 -0
- abstractvision-0.1.0.dist-info/METADATA +0 -65
- abstractvision-0.1.0.dist-info/RECORD +0 -6
- {abstractvision-0.1.0.dist-info → abstractvision-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {abstractvision-0.1.0.dist-info → abstractvision-0.2.1.dist-info}/top_level.txt +0 -0
abstractvision/__init__.py
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
"""abstractvision: Generative vision capabilities for abstractcore.ai.
|
|
1
|
+
"""abstractvision: Generative vision capabilities for abstractcore.ai.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The default install is batteries-included (Diffusers + stable-diffusion.cpp python bindings),
|
|
4
|
+
so users generally only need to download model weights.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .artifacts import LocalAssetStore, RuntimeArtifactStoreAdapter, is_artifact_ref
|
|
8
|
+
from .model_capabilities import VisionModelCapabilitiesRegistry
|
|
9
|
+
from .vision_manager import VisionManager
|
|
10
|
+
|
|
11
|
+
__version__ = "0.2.1"
|
|
4
12
|
__author__ = "Laurent-Philippe Albou"
|
|
5
13
|
__email__ = "contact@abstractcore.ai"
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
__all__ = [
|
|
16
|
+
"VisionManager",
|
|
17
|
+
"VisionModelCapabilitiesRegistry",
|
|
18
|
+
"LocalAssetStore",
|
|
19
|
+
"RuntimeArtifactStoreAdapter",
|
|
20
|
+
"is_artifact_ref",
|
|
21
|
+
"__version__",
|
|
22
|
+
]
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import mimetypes
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import asdict, is_dataclass
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, Optional, Protocol, Union
|
|
11
|
+
|
|
12
|
+
from .errors import AbstractVisionError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_ARTIFACT_ID_RE = re.compile(r"^[a-f0-9]{32}$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _utc_now_iso() -> str:
|
|
19
|
+
return datetime.now(timezone.utc).isoformat()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def sha256_hex(content: bytes) -> str:
|
|
23
|
+
return hashlib.sha256(content).hexdigest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def compute_artifact_id(content: bytes) -> str:
|
|
27
|
+
"""Compute a stable content-addressed artifact id (sha256 truncated)."""
|
|
28
|
+
return sha256_hex(content)[:32]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_artifact_ref(value: Any) -> bool:
|
|
32
|
+
"""Check if a value matches the framework artifact-ref shape."""
|
|
33
|
+
return isinstance(value, dict) and isinstance(value.get("$artifact"), str) and bool(value.get("$artifact"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_artifact_id(ref: Dict[str, Any]) -> str:
|
|
37
|
+
"""Extract artifact id from a ref dict."""
|
|
38
|
+
return str(ref["$artifact"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def make_media_ref(
|
|
42
|
+
artifact_id: str,
|
|
43
|
+
*,
|
|
44
|
+
content_type: Optional[str] = None,
|
|
45
|
+
filename: Optional[str] = None,
|
|
46
|
+
sha256: Optional[str] = None,
|
|
47
|
+
size_bytes: Optional[int] = None,
|
|
48
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
49
|
+
) -> Dict[str, Any]:
|
|
50
|
+
out: Dict[str, Any] = {"$artifact": str(artifact_id)}
|
|
51
|
+
if content_type:
|
|
52
|
+
out["content_type"] = str(content_type)
|
|
53
|
+
if filename:
|
|
54
|
+
out["filename"] = str(filename)
|
|
55
|
+
if sha256:
|
|
56
|
+
out["sha256"] = str(sha256)
|
|
57
|
+
if size_bytes is not None:
|
|
58
|
+
out["size_bytes"] = int(size_bytes)
|
|
59
|
+
if isinstance(metadata, dict) and metadata:
|
|
60
|
+
out["metadata"] = metadata
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MediaStore(Protocol):
|
|
65
|
+
"""Minimal storage interface for generated media outputs (artifact-ref first)."""
|
|
66
|
+
|
|
67
|
+
def store_bytes(
|
|
68
|
+
self,
|
|
69
|
+
content: bytes,
|
|
70
|
+
*,
|
|
71
|
+
content_type: str,
|
|
72
|
+
filename: Optional[str] = None,
|
|
73
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
74
|
+
tags: Optional[Dict[str, str]] = None,
|
|
75
|
+
run_id: Optional[str] = None,
|
|
76
|
+
artifact_id: Optional[str] = None,
|
|
77
|
+
) -> Dict[str, Any]: ...
|
|
78
|
+
|
|
79
|
+
def load_bytes(self, artifact_id: str) -> bytes: ...
|
|
80
|
+
|
|
81
|
+
def get_metadata(self, artifact_id: str) -> Optional[Dict[str, Any]]: ...
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class LocalAssetStore:
|
|
85
|
+
"""Local filesystem store for generated assets (standalone mode).
|
|
86
|
+
|
|
87
|
+
Writes:
|
|
88
|
+
- content: <base_dir>/<artifact_id><ext>
|
|
89
|
+
- metadata: <base_dir>/<artifact_id>.meta.json
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, base_dir: Optional[Union[str, Path]] = None):
|
|
93
|
+
if base_dir is None:
|
|
94
|
+
base_dir = Path.home() / ".abstractvision" / "assets"
|
|
95
|
+
self._base_dir = Path(base_dir).expanduser().resolve()
|
|
96
|
+
self._base_dir.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def base_dir(self) -> Path:
|
|
100
|
+
return self._base_dir
|
|
101
|
+
|
|
102
|
+
def _validate_artifact_id(self, artifact_id: str) -> None:
|
|
103
|
+
if not _ARTIFACT_ID_RE.match(str(artifact_id or "")):
|
|
104
|
+
raise ValueError(f"Invalid artifact id: {artifact_id!r}")
|
|
105
|
+
|
|
106
|
+
def _meta_path(self, artifact_id: str) -> Path:
|
|
107
|
+
return self._base_dir / f"{artifact_id}.meta.json"
|
|
108
|
+
|
|
109
|
+
def _guess_ext(self, content_type: str) -> str:
|
|
110
|
+
ct = str(content_type or "").strip().lower()
|
|
111
|
+
if not ct:
|
|
112
|
+
return ".bin"
|
|
113
|
+
ext = mimetypes.guess_extension(ct) or ""
|
|
114
|
+
# Avoid ambiguous/empty extensions.
|
|
115
|
+
return ext if ext else ".bin"
|
|
116
|
+
|
|
117
|
+
def store_bytes(
|
|
118
|
+
self,
|
|
119
|
+
content: bytes,
|
|
120
|
+
*,
|
|
121
|
+
content_type: str,
|
|
122
|
+
filename: Optional[str] = None,
|
|
123
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
124
|
+
tags: Optional[Dict[str, str]] = None,
|
|
125
|
+
run_id: Optional[str] = None,
|
|
126
|
+
artifact_id: Optional[str] = None,
|
|
127
|
+
) -> Dict[str, Any]:
|
|
128
|
+
if not isinstance(content, (bytes, bytearray)):
|
|
129
|
+
raise TypeError("content must be bytes")
|
|
130
|
+
content_b = bytes(content)
|
|
131
|
+
content_type = str(content_type or "application/octet-stream")
|
|
132
|
+
|
|
133
|
+
sha = sha256_hex(content_b)
|
|
134
|
+
artifact_id = str(artifact_id or compute_artifact_id(content_b))
|
|
135
|
+
self._validate_artifact_id(artifact_id)
|
|
136
|
+
|
|
137
|
+
ext = self._guess_ext(content_type)
|
|
138
|
+
content_path = self._base_dir / f"{artifact_id}{ext}"
|
|
139
|
+
meta_path = self._meta_path(artifact_id)
|
|
140
|
+
|
|
141
|
+
# Best-effort idempotency: don't rewrite existing blobs.
|
|
142
|
+
if not content_path.exists():
|
|
143
|
+
content_path.write_bytes(content_b)
|
|
144
|
+
|
|
145
|
+
meta: Dict[str, Any] = {
|
|
146
|
+
"schema": "abstractvision.asset.v1",
|
|
147
|
+
"artifact_id": artifact_id,
|
|
148
|
+
"content_type": content_type,
|
|
149
|
+
"size_bytes": len(content_b),
|
|
150
|
+
"sha256": sha,
|
|
151
|
+
"created_at": _utc_now_iso(),
|
|
152
|
+
"content_file": content_path.name,
|
|
153
|
+
}
|
|
154
|
+
if filename:
|
|
155
|
+
meta["filename"] = str(filename)
|
|
156
|
+
if run_id:
|
|
157
|
+
meta["run_id"] = str(run_id)
|
|
158
|
+
if isinstance(tags, dict) and tags:
|
|
159
|
+
meta["tags"] = {str(k): str(v) for k, v in tags.items()}
|
|
160
|
+
if isinstance(metadata, dict) and metadata:
|
|
161
|
+
meta["metadata"] = metadata
|
|
162
|
+
|
|
163
|
+
# Always refresh meta so UX fields can be updated without rewriting blobs.
|
|
164
|
+
meta_path.write_text(json.dumps(meta, indent=2, sort_keys=True), encoding="utf-8")
|
|
165
|
+
|
|
166
|
+
return make_media_ref(
|
|
167
|
+
artifact_id,
|
|
168
|
+
content_type=content_type,
|
|
169
|
+
filename=str(filename) if filename else None,
|
|
170
|
+
sha256=sha,
|
|
171
|
+
size_bytes=len(content_b),
|
|
172
|
+
metadata=metadata if isinstance(metadata, dict) else None,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def get_metadata(self, artifact_id: str) -> Optional[Dict[str, Any]]:
|
|
176
|
+
artifact_id = str(artifact_id or "")
|
|
177
|
+
self._validate_artifact_id(artifact_id)
|
|
178
|
+
p = self._meta_path(artifact_id)
|
|
179
|
+
if not p.exists():
|
|
180
|
+
return None
|
|
181
|
+
try:
|
|
182
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
183
|
+
except Exception:
|
|
184
|
+
return None
|
|
185
|
+
return data if isinstance(data, dict) else None
|
|
186
|
+
|
|
187
|
+
def get_content_path(self, artifact_id: str) -> Optional[Path]:
|
|
188
|
+
"""Best-effort: return the path to the stored blob for an artifact id (local store only)."""
|
|
189
|
+
artifact_id = str(artifact_id or "")
|
|
190
|
+
self._validate_artifact_id(artifact_id)
|
|
191
|
+
meta = self.get_metadata(artifact_id)
|
|
192
|
+
if isinstance(meta, dict):
|
|
193
|
+
content_file = meta.get("content_file")
|
|
194
|
+
if isinstance(content_file, str) and content_file:
|
|
195
|
+
p = self._base_dir / content_file
|
|
196
|
+
if p.exists():
|
|
197
|
+
return p
|
|
198
|
+
|
|
199
|
+
matches = sorted(self._base_dir.glob(f"{artifact_id}.*"))
|
|
200
|
+
for p in matches:
|
|
201
|
+
if p.name.endswith(".meta.json"):
|
|
202
|
+
continue
|
|
203
|
+
if p.is_file():
|
|
204
|
+
return p
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
def load_bytes(self, artifact_id: str) -> bytes:
|
|
208
|
+
artifact_id = str(artifact_id or "")
|
|
209
|
+
self._validate_artifact_id(artifact_id)
|
|
210
|
+
p = self.get_content_path(artifact_id)
|
|
211
|
+
if p is not None:
|
|
212
|
+
return p.read_bytes()
|
|
213
|
+
|
|
214
|
+
# Fallback: locate blob by prefix.
|
|
215
|
+
matches = sorted(self._base_dir.glob(f"{artifact_id}.*"))
|
|
216
|
+
for p in matches:
|
|
217
|
+
if p.name.endswith(".meta.json"):
|
|
218
|
+
continue
|
|
219
|
+
if p.is_file():
|
|
220
|
+
return p.read_bytes()
|
|
221
|
+
raise FileNotFoundError(f"Asset not found: {artifact_id}")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class RuntimeArtifactStoreAdapter:
|
|
225
|
+
"""Duck-typed adapter for AbstractRuntime's ArtifactStore (no hard dependency)."""
|
|
226
|
+
|
|
227
|
+
def __init__(self, artifact_store: Any):
|
|
228
|
+
self._store = artifact_store
|
|
229
|
+
|
|
230
|
+
def store_bytes(
|
|
231
|
+
self,
|
|
232
|
+
content: bytes,
|
|
233
|
+
*,
|
|
234
|
+
content_type: str,
|
|
235
|
+
filename: Optional[str] = None,
|
|
236
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
237
|
+
tags: Optional[Dict[str, str]] = None,
|
|
238
|
+
run_id: Optional[str] = None,
|
|
239
|
+
artifact_id: Optional[str] = None,
|
|
240
|
+
) -> Dict[str, Any]:
|
|
241
|
+
store_fn = getattr(self._store, "store", None)
|
|
242
|
+
if not callable(store_fn):
|
|
243
|
+
raise AbstractVisionError("Provided artifact_store does not have a callable .store(...)")
|
|
244
|
+
|
|
245
|
+
content_b = bytes(content)
|
|
246
|
+
content_type = str(content_type or "application/octet-stream")
|
|
247
|
+
sha = sha256_hex(content_b)
|
|
248
|
+
|
|
249
|
+
merged_tags: Dict[str, str] = {}
|
|
250
|
+
if isinstance(tags, dict):
|
|
251
|
+
merged_tags.update({str(k): str(v) for k, v in tags.items()})
|
|
252
|
+
if filename and "filename" not in merged_tags:
|
|
253
|
+
merged_tags["filename"] = str(filename)
|
|
254
|
+
if sha and "sha256" not in merged_tags:
|
|
255
|
+
merged_tags["sha256"] = sha
|
|
256
|
+
|
|
257
|
+
# Try the full AbstractRuntime signature first; fall back if needed.
|
|
258
|
+
try:
|
|
259
|
+
meta = store_fn(
|
|
260
|
+
content_b,
|
|
261
|
+
content_type=content_type,
|
|
262
|
+
run_id=str(run_id) if run_id else None,
|
|
263
|
+
tags=merged_tags or None,
|
|
264
|
+
artifact_id=str(artifact_id) if artifact_id else None,
|
|
265
|
+
)
|
|
266
|
+
except TypeError:
|
|
267
|
+
meta = store_fn(
|
|
268
|
+
content_b,
|
|
269
|
+
content_type=content_type,
|
|
270
|
+
run_id=str(run_id) if run_id else None,
|
|
271
|
+
tags=merged_tags or None,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
artifact_id_out = None
|
|
275
|
+
if isinstance(meta, dict):
|
|
276
|
+
artifact_id_out = meta.get("artifact_id")
|
|
277
|
+
elif hasattr(meta, "artifact_id"):
|
|
278
|
+
artifact_id_out = getattr(meta, "artifact_id", None)
|
|
279
|
+
if not isinstance(artifact_id_out, str) or not artifact_id_out.strip():
|
|
280
|
+
raise AbstractVisionError("artifact_store.store(...) did not return a usable artifact_id")
|
|
281
|
+
|
|
282
|
+
return make_media_ref(
|
|
283
|
+
str(artifact_id_out),
|
|
284
|
+
content_type=content_type,
|
|
285
|
+
filename=str(filename) if filename else None,
|
|
286
|
+
sha256=sha,
|
|
287
|
+
size_bytes=len(content_b),
|
|
288
|
+
metadata=metadata if isinstance(metadata, dict) else None,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def load_bytes(self, artifact_id: str) -> bytes:
|
|
292
|
+
load_fn = getattr(self._store, "load", None)
|
|
293
|
+
if not callable(load_fn):
|
|
294
|
+
raise AbstractVisionError("Provided artifact_store does not have a callable .load(...)")
|
|
295
|
+
artifact = load_fn(str(artifact_id))
|
|
296
|
+
if artifact is None:
|
|
297
|
+
raise FileNotFoundError(f"Artifact not found: {artifact_id}")
|
|
298
|
+
if isinstance(artifact, (bytes, bytearray)):
|
|
299
|
+
return bytes(artifact)
|
|
300
|
+
if hasattr(artifact, "content"):
|
|
301
|
+
return bytes(getattr(artifact, "content"))
|
|
302
|
+
raise AbstractVisionError("artifact_store.load(...) returned an unsupported value")
|
|
303
|
+
|
|
304
|
+
def get_metadata(self, artifact_id: str) -> Optional[Dict[str, Any]]:
|
|
305
|
+
meta_fn = getattr(self._store, "get_metadata", None)
|
|
306
|
+
if not callable(meta_fn):
|
|
307
|
+
return None
|
|
308
|
+
meta = meta_fn(str(artifact_id))
|
|
309
|
+
if meta is None:
|
|
310
|
+
return None
|
|
311
|
+
if isinstance(meta, dict):
|
|
312
|
+
return meta
|
|
313
|
+
to_dict = getattr(meta, "to_dict", None)
|
|
314
|
+
if callable(to_dict):
|
|
315
|
+
out = to_dict()
|
|
316
|
+
return out if isinstance(out, dict) else None
|
|
317
|
+
if is_dataclass(meta):
|
|
318
|
+
out = asdict(meta)
|
|
319
|
+
return out if isinstance(out, dict) else None
|
|
320
|
+
return None
|