visionservex 0.3.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.
- visionservex/__init__.py +29 -0
- visionservex/__main__.py +11 -0
- visionservex/api/__init__.py +24 -0
- visionservex/api/errors.py +87 -0
- visionservex/api/schemas.py +136 -0
- visionservex/cli/__init__.py +5 -0
- visionservex/cli/main.py +957 -0
- visionservex/cli/tunnel.py +119 -0
- visionservex/config/__init__.py +31 -0
- visionservex/config/settings.py +236 -0
- visionservex/core/__init__.py +37 -0
- visionservex/core/model.py +245 -0
- visionservex/core/results.py +380 -0
- visionservex/engines/__init__.py +30 -0
- visionservex/engines/_stub.py +118 -0
- visionservex/engines/base.py +89 -0
- visionservex/engines/dfine.py +21 -0
- visionservex/engines/grounded_sam.py +181 -0
- visionservex/engines/grounding_dino.py +207 -0
- visionservex/engines/huggingface.py +21 -0
- visionservex/engines/mock.py +220 -0
- visionservex/engines/onnx.py +21 -0
- visionservex/engines/openmmlab.py +26 -0
- visionservex/engines/pytorch.py +27 -0
- visionservex/engines/registry.py +36 -0
- visionservex/engines/rfdetr.py +280 -0
- visionservex/engines/sam2.py +25 -0
- visionservex/engines/sam_hf.py +205 -0
- visionservex/engines/swinv2.py +160 -0
- visionservex/models/__init__.py +6 -0
- visionservex/registry/__init__.py +25 -0
- visionservex/registry/models.yaml +1545 -0
- visionservex/registry/registry.py +289 -0
- visionservex/runtime/__init__.py +30 -0
- visionservex/runtime/cache.py +129 -0
- visionservex/runtime/device.py +203 -0
- visionservex/runtime/downloads.py +634 -0
- visionservex/runtime/jobs.py +173 -0
- visionservex/runtime/monitor.py +73 -0
- visionservex/runtime/recommendations.py +172 -0
- visionservex/runtime/scheduler.py +169 -0
- visionservex/security/__init__.py +20 -0
- visionservex/security/auth.py +74 -0
- visionservex/security/middleware.py +128 -0
- visionservex/security/ssrf.py +84 -0
- visionservex/security/validators.py +54 -0
- visionservex/server/__init__.py +5 -0
- visionservex/server/app.py +730 -0
- visionservex/tunnel/__init__.py +15 -0
- visionservex/tunnel/cloudflare.py +151 -0
- visionservex/utils/__init__.py +1 -0
- visionservex/utils/hashing.py +37 -0
- visionservex/utils/ids.py +13 -0
- visionservex/utils/images.py +99 -0
- visionservex/utils/logging.py +77 -0
- visionservex/utils/paths.py +60 -0
- visionservex/utils/system.py +222 -0
- visionservex-0.3.0.dist-info/METADATA +435 -0
- visionservex-0.3.0.dist-info/RECORD +63 -0
- visionservex-0.3.0.dist-info/WHEEL +4 -0
- visionservex-0.3.0.dist-info/entry_points.txt +2 -0
- visionservex-0.3.0.dist-info/licenses/LICENSE +29 -0
- visionservex-0.3.0.dist-info/licenses/NOTICE +19 -0
visionservex/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""VisionServeX: serve permissive-license computer vision models locally and over Cloudflare Tunnel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__version__ = "0.3.0"
|
|
6
|
+
__author__ = "Arash Sajjadi"
|
|
7
|
+
__email__ = "arash.sajjadi@usask.ca"
|
|
8
|
+
__license__ = "Apache-2.0"
|
|
9
|
+
|
|
10
|
+
from visionservex.core.model import VisionModel
|
|
11
|
+
from visionservex.core.results import (
|
|
12
|
+
ClassificationResult,
|
|
13
|
+
DetectionResult,
|
|
14
|
+
OpenVocabularyResult,
|
|
15
|
+
OrientedDetectionResult,
|
|
16
|
+
PoseResult,
|
|
17
|
+
SegmentationResult,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"VisionModel",
|
|
22
|
+
"DetectionResult",
|
|
23
|
+
"SegmentationResult",
|
|
24
|
+
"PoseResult",
|
|
25
|
+
"ClassificationResult",
|
|
26
|
+
"OrientedDetectionResult",
|
|
27
|
+
"OpenVocabularyResult",
|
|
28
|
+
"__version__",
|
|
29
|
+
]
|
visionservex/__main__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""HTTP API response/error schemas."""
|
|
2
|
+
|
|
3
|
+
from visionservex.api.errors import ApiError, ErrorBody
|
|
4
|
+
from visionservex.api.schemas import (
|
|
5
|
+
HealthResponse,
|
|
6
|
+
ModelListItem,
|
|
7
|
+
ModelListResponse,
|
|
8
|
+
PredictionResponse,
|
|
9
|
+
PromptRequest,
|
|
10
|
+
UrlInput,
|
|
11
|
+
VersionResponse,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ApiError",
|
|
16
|
+
"ErrorBody",
|
|
17
|
+
"HealthResponse",
|
|
18
|
+
"ModelListItem",
|
|
19
|
+
"ModelListResponse",
|
|
20
|
+
"PredictionResponse",
|
|
21
|
+
"PromptRequest",
|
|
22
|
+
"UrlInput",
|
|
23
|
+
"VersionResponse",
|
|
24
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Stable API error envelope."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import HTTPException, status
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ErrorBody(BaseModel):
|
|
12
|
+
code: str
|
|
13
|
+
message: str
|
|
14
|
+
hint: str = ""
|
|
15
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ApiError(HTTPException):
|
|
19
|
+
"""HTTPException subclass that produces a stable error envelope.
|
|
20
|
+
|
|
21
|
+
Endpoints raise this; the server's exception handler renders it as
|
|
22
|
+
``{"request_id": ..., "error": {...}}``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
status_code: int,
|
|
28
|
+
code: str,
|
|
29
|
+
message: str,
|
|
30
|
+
*,
|
|
31
|
+
hint: str = "",
|
|
32
|
+
details: dict[str, Any] | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
super().__init__(status_code=status_code, detail=message)
|
|
35
|
+
self.error_code = code
|
|
36
|
+
self.error_message = message
|
|
37
|
+
self.error_hint = hint
|
|
38
|
+
self.error_details = details or {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Common helpers
|
|
42
|
+
def not_found(code: str, message: str, *, hint: str = "") -> ApiError:
|
|
43
|
+
return ApiError(status.HTTP_404_NOT_FOUND, code, message, hint=hint)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def bad_request(code: str, message: str, *, hint: str = "") -> ApiError:
|
|
47
|
+
return ApiError(status.HTTP_400_BAD_REQUEST, code, message, hint=hint)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def unauthorized(message: str = "authentication required", *, hint: str = "") -> ApiError:
|
|
51
|
+
return ApiError(
|
|
52
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
53
|
+
"UNAUTHENTICATED",
|
|
54
|
+
message,
|
|
55
|
+
hint=hint or "send Authorization: Bearer <api-key>",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def forbidden(message: str, *, hint: str = "") -> ApiError:
|
|
60
|
+
return ApiError(status.HTTP_403_FORBIDDEN, "FORBIDDEN", message, hint=hint)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def too_large(message: str, *, hint: str = "") -> ApiError:
|
|
64
|
+
return ApiError(status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, "REQUEST_TOO_LARGE", message, hint=hint)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def unprocessable(code: str, message: str, *, hint: str = "") -> ApiError:
|
|
68
|
+
# 422; avoid touching the legacy ``HTTP_422_UNPROCESSABLE_ENTITY`` name
|
|
69
|
+
# unconditionally to suppress a Starlette deprecation warning.
|
|
70
|
+
return ApiError(422, code, message, hint=hint)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def busy(message: str, *, hint: str = "") -> ApiError:
|
|
74
|
+
return ApiError(status.HTTP_503_SERVICE_UNAVAILABLE, "BUSY", message, hint=hint)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"ErrorBody",
|
|
79
|
+
"ApiError",
|
|
80
|
+
"not_found",
|
|
81
|
+
"bad_request",
|
|
82
|
+
"unauthorized",
|
|
83
|
+
"forbidden",
|
|
84
|
+
"too_large",
|
|
85
|
+
"unprocessable",
|
|
86
|
+
"busy",
|
|
87
|
+
]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026 Arash Sajjadi
|
|
3
|
+
"""Pydantic schemas for HTTP request and response bodies."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HealthResponse(BaseModel):
|
|
13
|
+
status: str = "ok"
|
|
14
|
+
version: str
|
|
15
|
+
public_mode: bool
|
|
16
|
+
auth_enabled: bool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VersionResponse(BaseModel):
|
|
20
|
+
version: str
|
|
21
|
+
python: str
|
|
22
|
+
platform: str
|
|
23
|
+
author: str = "Arash Sajjadi"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ModelListItem(BaseModel):
|
|
27
|
+
id: str
|
|
28
|
+
display_name: str
|
|
29
|
+
task: str
|
|
30
|
+
family: str
|
|
31
|
+
license: str
|
|
32
|
+
status: str
|
|
33
|
+
implementation_status: str
|
|
34
|
+
engine: str
|
|
35
|
+
backend: str
|
|
36
|
+
difficulty: str
|
|
37
|
+
auto_download: bool
|
|
38
|
+
supported_devices: list[str]
|
|
39
|
+
minimum_vram_gb: float | None = None
|
|
40
|
+
recommended_vram_gb: float | None = None
|
|
41
|
+
warnings: list[str] = Field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ModelListResponse(BaseModel):
|
|
45
|
+
models: list[ModelListItem]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PromptRequest(BaseModel):
|
|
49
|
+
"""JSON body for endpoints that accept prompts and a base64 image."""
|
|
50
|
+
|
|
51
|
+
image_b64: str | None = Field(
|
|
52
|
+
default=None,
|
|
53
|
+
description="Base64-encoded image bytes. Mutually exclusive with `image_url`.",
|
|
54
|
+
)
|
|
55
|
+
image_url: str | None = Field(
|
|
56
|
+
default=None,
|
|
57
|
+
description="HTTPS URL of an image. Requires inputs.allow_url_inputs=true.",
|
|
58
|
+
)
|
|
59
|
+
prompts: list[str] = Field(default_factory=list)
|
|
60
|
+
model_id: str
|
|
61
|
+
options: dict[str, Any] = Field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class UrlInput(BaseModel):
|
|
65
|
+
image_url: str
|
|
66
|
+
model_id: str
|
|
67
|
+
options: dict[str, Any] = Field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PredictionResponse(BaseModel):
|
|
71
|
+
"""Stable wire format for every prediction endpoint."""
|
|
72
|
+
|
|
73
|
+
model_config = ConfigDict(extra="forbid", protected_namespaces=())
|
|
74
|
+
|
|
75
|
+
request_id: str
|
|
76
|
+
status: Literal["completed", "downloading", "queued", "failed"] = "completed"
|
|
77
|
+
model_id: str
|
|
78
|
+
task: str
|
|
79
|
+
backend: str = ""
|
|
80
|
+
device: str
|
|
81
|
+
precision: str = "fp32"
|
|
82
|
+
latency_ms: float
|
|
83
|
+
model_loaded_from: str | None = None
|
|
84
|
+
cache_path: str | None = None
|
|
85
|
+
fallback_reason: str | None = None
|
|
86
|
+
results: list[dict[str, Any]] = Field(default_factory=list)
|
|
87
|
+
warnings: list[str] = Field(default_factory=list)
|
|
88
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class JobProgress(BaseModel):
|
|
92
|
+
downloaded_bytes: int = 0
|
|
93
|
+
total_bytes: int | None = None
|
|
94
|
+
percent: float = 0.0
|
|
95
|
+
speed_bytes_per_sec: int = 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class JobResponse(BaseModel):
|
|
99
|
+
model_config = ConfigDict(protected_namespaces=())
|
|
100
|
+
request_id: str | None = None
|
|
101
|
+
job_id: str
|
|
102
|
+
kind: str
|
|
103
|
+
model_id: str
|
|
104
|
+
status: str
|
|
105
|
+
message: str = ""
|
|
106
|
+
progress: dict[str, Any] = Field(default_factory=dict)
|
|
107
|
+
result: Any | None = None
|
|
108
|
+
error: dict[str, Any] | None = None
|
|
109
|
+
created_at: float
|
|
110
|
+
updated_at: float
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class DownloadingResponse(BaseModel):
|
|
114
|
+
"""Returned when an auto-pull starts and the client opted out of waiting."""
|
|
115
|
+
|
|
116
|
+
model_config = ConfigDict(protected_namespaces=())
|
|
117
|
+
request_id: str
|
|
118
|
+
status: Literal["downloading"] = "downloading"
|
|
119
|
+
model_id: str
|
|
120
|
+
job_id: str
|
|
121
|
+
message: str
|
|
122
|
+
progress_url: str
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = [
|
|
126
|
+
"HealthResponse",
|
|
127
|
+
"VersionResponse",
|
|
128
|
+
"ModelListItem",
|
|
129
|
+
"ModelListResponse",
|
|
130
|
+
"PromptRequest",
|
|
131
|
+
"UrlInput",
|
|
132
|
+
"PredictionResponse",
|
|
133
|
+
"JobProgress",
|
|
134
|
+
"JobResponse",
|
|
135
|
+
"DownloadingResponse",
|
|
136
|
+
]
|