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.
Files changed (63) hide show
  1. visionservex/__init__.py +29 -0
  2. visionservex/__main__.py +11 -0
  3. visionservex/api/__init__.py +24 -0
  4. visionservex/api/errors.py +87 -0
  5. visionservex/api/schemas.py +136 -0
  6. visionservex/cli/__init__.py +5 -0
  7. visionservex/cli/main.py +957 -0
  8. visionservex/cli/tunnel.py +119 -0
  9. visionservex/config/__init__.py +31 -0
  10. visionservex/config/settings.py +236 -0
  11. visionservex/core/__init__.py +37 -0
  12. visionservex/core/model.py +245 -0
  13. visionservex/core/results.py +380 -0
  14. visionservex/engines/__init__.py +30 -0
  15. visionservex/engines/_stub.py +118 -0
  16. visionservex/engines/base.py +89 -0
  17. visionservex/engines/dfine.py +21 -0
  18. visionservex/engines/grounded_sam.py +181 -0
  19. visionservex/engines/grounding_dino.py +207 -0
  20. visionservex/engines/huggingface.py +21 -0
  21. visionservex/engines/mock.py +220 -0
  22. visionservex/engines/onnx.py +21 -0
  23. visionservex/engines/openmmlab.py +26 -0
  24. visionservex/engines/pytorch.py +27 -0
  25. visionservex/engines/registry.py +36 -0
  26. visionservex/engines/rfdetr.py +280 -0
  27. visionservex/engines/sam2.py +25 -0
  28. visionservex/engines/sam_hf.py +205 -0
  29. visionservex/engines/swinv2.py +160 -0
  30. visionservex/models/__init__.py +6 -0
  31. visionservex/registry/__init__.py +25 -0
  32. visionservex/registry/models.yaml +1545 -0
  33. visionservex/registry/registry.py +289 -0
  34. visionservex/runtime/__init__.py +30 -0
  35. visionservex/runtime/cache.py +129 -0
  36. visionservex/runtime/device.py +203 -0
  37. visionservex/runtime/downloads.py +634 -0
  38. visionservex/runtime/jobs.py +173 -0
  39. visionservex/runtime/monitor.py +73 -0
  40. visionservex/runtime/recommendations.py +172 -0
  41. visionservex/runtime/scheduler.py +169 -0
  42. visionservex/security/__init__.py +20 -0
  43. visionservex/security/auth.py +74 -0
  44. visionservex/security/middleware.py +128 -0
  45. visionservex/security/ssrf.py +84 -0
  46. visionservex/security/validators.py +54 -0
  47. visionservex/server/__init__.py +5 -0
  48. visionservex/server/app.py +730 -0
  49. visionservex/tunnel/__init__.py +15 -0
  50. visionservex/tunnel/cloudflare.py +151 -0
  51. visionservex/utils/__init__.py +1 -0
  52. visionservex/utils/hashing.py +37 -0
  53. visionservex/utils/ids.py +13 -0
  54. visionservex/utils/images.py +99 -0
  55. visionservex/utils/logging.py +77 -0
  56. visionservex/utils/paths.py +60 -0
  57. visionservex/utils/system.py +222 -0
  58. visionservex-0.3.0.dist-info/METADATA +435 -0
  59. visionservex-0.3.0.dist-info/RECORD +63 -0
  60. visionservex-0.3.0.dist-info/WHEEL +4 -0
  61. visionservex-0.3.0.dist-info/entry_points.txt +2 -0
  62. visionservex-0.3.0.dist-info/licenses/LICENSE +29 -0
  63. visionservex-0.3.0.dist-info/licenses/NOTICE +19 -0
@@ -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
+ ]
@@ -0,0 +1,11 @@
1
+ """Entry point so ``python -m visionservex`` works."""
2
+
3
+ from visionservex.cli.main import app
4
+
5
+
6
+ def main() -> None:
7
+ app()
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -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
+ ]
@@ -0,0 +1,5 @@
1
+ """Command-line interface."""
2
+
3
+ from visionservex.cli.main import app
4
+
5
+ __all__ = ["app"]