visynx 0.1.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.
visynx/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """visynx — run your VisionForge models from Python.
2
+
3
+ from visynx import Visynx
4
+
5
+ vf = Visynx(api_key="vf_...")
6
+ model = vf.model("shelf-scanner/3")
7
+
8
+ result = model.predict("shelf.jpg", confidence=0.4)
9
+ print(result.json())
10
+ result.save("annotated.jpg")
11
+ """
12
+ from .client import (
13
+ AuthenticationError,
14
+ InsufficientCreditsError,
15
+ MaintenanceError,
16
+ ModelNotFoundError,
17
+ PayloadTooLargeError,
18
+ PermissionDeniedError,
19
+ RateLimitError,
20
+ Visynx,
21
+ VisynxError,
22
+ )
23
+ from .model import Model
24
+ from .results import Detection, Result
25
+
26
+ __version__ = "0.1.0"
27
+
28
+ __all__ = [
29
+ "AuthenticationError",
30
+ "Detection",
31
+ "InsufficientCreditsError",
32
+ "MaintenanceError",
33
+ "Model",
34
+ "ModelNotFoundError",
35
+ "PayloadTooLargeError",
36
+ "PermissionDeniedError",
37
+ "RateLimitError",
38
+ "Result",
39
+ "Visynx",
40
+ "VisynxError",
41
+ "__version__",
42
+ ]
visynx/client.py ADDED
@@ -0,0 +1,213 @@
1
+ """HTTP client, auth, and error mapping for the VisionForge API.
2
+
3
+ One ``requests.Session`` carries the bearer token for every call. The retry and
4
+ error semantics here are not invented — they mirror what the VisionForge API
5
+ already documents (see the platform's "Rate Limits & Error Handling" guide):
6
+ 429 is retried honouring ``Retry-After``; 402/401/403 are terminal and mean
7
+ distinct, actionable things; every error quotes the ``X-Request-ID`` header the
8
+ server stamps on every response, because that is what support needs to find the
9
+ matching server log.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import time
15
+ import uuid
16
+ from typing import Any
17
+
18
+ import requests
19
+
20
+ DEFAULT_API_URL = "https://visionforge.evo-ss.com/api"
21
+
22
+ #: Server-side cap on an inference upload (``MAX_INFERENCE_UPLOAD_BYTES``).
23
+ #: Checked client-side so a doomed 50 MB upload fails instantly instead of
24
+ #: after the transfer.
25
+ MAX_UPLOAD_BYTES = 50 * 1024 * 1024
26
+
27
+ #: Training backends a model can be served by. Used to tell a raw
28
+ #: ``provider/job_id`` reference from a friendly ``project/version`` slug.
29
+ PROVIDERS = ("yolo", "detectron2", "rfdetr", "smp")
30
+
31
+ MAX_ATTEMPTS = 4
32
+
33
+
34
+ class VisynxError(Exception):
35
+ """Base class for every error raised by visynx."""
36
+
37
+
38
+ class AuthenticationError(VisynxError):
39
+ """401 — the API key is missing, invalid, or revoked."""
40
+
41
+
42
+ class PermissionDeniedError(VisynxError):
43
+ """403 — email unverified, account deactivated, or the key's IP allowlist rejected you."""
44
+
45
+
46
+ class ModelNotFoundError(VisynxError):
47
+ """404 — no such model, or it isn't yours."""
48
+
49
+
50
+ class InsufficientCreditsError(VisynxError):
51
+ """402 — out of Vision Credits. Not a transient error; top up."""
52
+
53
+
54
+ class PayloadTooLargeError(VisynxError):
55
+ """413 — the image exceeds the inference upload limit."""
56
+
57
+
58
+ class RateLimitError(VisynxError):
59
+ """429 — rate limited, and the retries were exhausted."""
60
+
61
+
62
+ class MaintenanceError(VisynxError):
63
+ """503 — the platform is in maintenance mode."""
64
+
65
+
66
+ _STATUS_ERRORS = {
67
+ 401: (AuthenticationError, "API key is invalid or has been revoked"),
68
+ 402: (InsufficientCreditsError, "out of Vision Credits — top up to continue"),
69
+ 403: (
70
+ PermissionDeniedError,
71
+ "forbidden — verify your email, or check this key's IP allowlist",
72
+ ),
73
+ 404: (ModelNotFoundError, "not found"),
74
+ 413: (PayloadTooLargeError, "upload exceeds the size limit"),
75
+ 429: (RateLimitError, "rate limited"),
76
+ 503: (MaintenanceError, "platform is in maintenance mode"),
77
+ }
78
+
79
+
80
+ def _normalise(slug: str | None) -> str:
81
+ """Match slugs forgivingly — a user types whatever separator they saw last."""
82
+ return (slug or "").strip().strip("/").lower().replace("_", "-")
83
+
84
+
85
+ def _retry_after(response) -> float:
86
+ """Seconds to wait per the server's ``Retry-After``, clamped to something sane."""
87
+ try:
88
+ return max(0.0, min(60.0, float(response.headers.get("Retry-After", 5))))
89
+ except (TypeError, ValueError):
90
+ return 5.0
91
+
92
+
93
+ def _raise_for(response) -> None:
94
+ """Translate a failed response into the matching typed VisynxError."""
95
+ try:
96
+ detail = (response.json() or {}).get("detail")
97
+ except ValueError:
98
+ detail = None
99
+ detail = str(detail) if detail else (response.text or "").strip()[:300]
100
+
101
+ error_cls, summary = _STATUS_ERRORS.get(
102
+ response.status_code, (VisynxError, f"HTTP {response.status_code}")
103
+ )
104
+ message = f"{summary}: {detail}" if detail else summary
105
+ request_id = response.headers.get("X-Request-ID")
106
+ if request_id:
107
+ message = f"{message} (request id: {request_id})"
108
+ raise error_cls(message)
109
+
110
+
111
+ class Visynx:
112
+ """Entry point to the VisionForge inference API.
113
+
114
+ >>> vf = Visynx(api_key="vf_...") # or set VISYNX_API_KEY
115
+ >>> model = vf.model("shelf-scanner/3")
116
+ >>> result = model.predict("shelf.jpg", confidence=0.4)
117
+ """
118
+
119
+ def __init__(
120
+ self,
121
+ api_key: str | None = None,
122
+ api_url: str | None = None,
123
+ timeout: float = 60.0,
124
+ ) -> None:
125
+ api_key = api_key or os.environ.get("VISYNX_API_KEY")
126
+ if not api_key:
127
+ raise AuthenticationError(
128
+ "No API key. Pass Visynx(api_key='vf_...') or set the "
129
+ "VISYNX_API_KEY environment variable. Create a key in "
130
+ "VisionForge under Settings -> API Keys."
131
+ )
132
+ self.api_url = (api_url or os.environ.get("VISYNX_API_URL") or DEFAULT_API_URL).rstrip("/")
133
+ self.timeout = timeout
134
+ self._session = requests.Session()
135
+ self._session.headers["Authorization"] = f"Bearer {api_key}"
136
+ self._models_cache: list | None = None
137
+
138
+ def __repr__(self) -> str:
139
+ return f"Visynx(api_url={self.api_url!r})"
140
+
141
+ # -- transport ----------------------------------------------------------
142
+
143
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
144
+ """Call the API and return the decoded JSON body, or raise a VisynxError.
145
+
146
+ Retries only 429, and only because every caller passes replayable bytes
147
+ rather than a file handle — a consumed stream could not be re-sent.
148
+ """
149
+ url = f"{self.api_url}{path}"
150
+ # One id for the whole call, reused across retries: the server treats a
151
+ # repeated (request id, image) pair as the same billable inference, so a
152
+ # retry can never be charged twice.
153
+ headers = {"X-Request-ID": uuid.uuid4().hex, **kwargs.pop("headers", {})}
154
+ for attempt in range(MAX_ATTEMPTS):
155
+ response = self._session.request(
156
+ method, url, timeout=self.timeout, headers=headers, **kwargs
157
+ )
158
+ if response.status_code == 429 and attempt < MAX_ATTEMPTS - 1:
159
+ time.sleep(_retry_after(response))
160
+ continue
161
+ if not response.ok:
162
+ _raise_for(response)
163
+ return response.json()
164
+ raise RateLimitError(f"rate limited after {MAX_ATTEMPTS} attempts")
165
+
166
+ # -- models -------------------------------------------------------------
167
+
168
+ def models(self, refresh: bool = False) -> list:
169
+ """Every trained model on this account that is ready to run.
170
+
171
+ Cached on the instance; pass ``refresh=True`` after training something new.
172
+ """
173
+ from .model import Model
174
+
175
+ if self._models_cache is None or refresh:
176
+ jobs = self._request("GET", "/train/jobs").get("jobs") or []
177
+ self._models_cache = [
178
+ Model._from_job(self, job)
179
+ for job in jobs
180
+ if job.get("status") == "completed" and job.get("artifact_available")
181
+ ]
182
+ return list(self._models_cache)
183
+
184
+ def model(self, ref: str):
185
+ """Resolve a model by ``"project/version"`` slug or raw ``"provider/job_id"``.
186
+
187
+ The slug is what the platform shows you and what you should write in code;
188
+ the raw form is accepted so scripts holding a job id keep working (and it
189
+ skips the model-list round trip entirely).
190
+ """
191
+ from .model import Model
192
+
193
+ if isinstance(ref, Model):
194
+ return ref
195
+ ref = str(ref).strip().strip("/")
196
+ provider, _, tail = ref.partition("/")
197
+ # "yolo/<uuid>" is a raw reference; "yolo/3" is a slug for a project that
198
+ # happens to be named "yolo" — a version is always a bare integer.
199
+ if provider in PROVIDERS and tail and not tail.isdigit():
200
+ return Model(client=self, provider=provider, job_id=tail)
201
+
202
+ wanted = _normalise(ref)
203
+ matches = [m for m in self.models() if _normalise(m.slug) == wanted]
204
+ if len(matches) == 1:
205
+ return matches[0]
206
+ if not matches:
207
+ known = ", ".join(sorted(m.slug for m in self.models() if m.slug)) or "none"
208
+ raise ModelNotFoundError(f"No model {ref!r}. Available: {known}")
209
+ candidates = ", ".join(f"{m.provider}/{m.job_id}" for m in matches)
210
+ raise ModelNotFoundError(
211
+ f"{ref!r} is ambiguous — several projects share that name. "
212
+ f"Use one of: {candidates}"
213
+ )
visynx/model.py ADDED
@@ -0,0 +1,148 @@
1
+ """A trained model you can run images through."""
2
+ from __future__ import annotations
3
+
4
+ import io
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .client import MAX_UPLOAD_BYTES, PayloadTooLargeError, VisynxError
10
+ from .results import Result
11
+
12
+ IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"}
13
+
14
+
15
+ def _encode(image) -> bytes:
16
+ """PIL image -> JPEG bytes."""
17
+ buffer = io.BytesIO()
18
+ image.convert("RGB").save(buffer, format="JPEG", quality=95)
19
+ return buffer.getvalue()
20
+
21
+
22
+ def _to_bytes(source: Any) -> tuple:
23
+ """Normalise anything image-shaped to ``(filename, bytes)``.
24
+
25
+ Accepts a path, raw bytes, an open file, a PIL image, or any object exposing
26
+ the array interface (a numpy array, without visynx ever importing numpy).
27
+ """
28
+ if isinstance(source, (bytes, bytearray)):
29
+ return "image.jpg", bytes(source)
30
+ if isinstance(source, (str, Path)):
31
+ path = Path(source)
32
+ if not path.is_file():
33
+ raise VisynxError(f"No such image: {path}")
34
+ return path.name, path.read_bytes()
35
+ if hasattr(source, "read"):
36
+ data = source.read()
37
+ if isinstance(data, str):
38
+ raise VisynxError("File opened in text mode — open it with 'rb'")
39
+ return getattr(source, "name", "image.jpg"), data
40
+ if hasattr(source, "save") and hasattr(source, "mode"): # PIL.Image
41
+ return "image.jpg", _encode(source)
42
+ if hasattr(source, "__array_interface__"): # numpy array, duck-typed
43
+ from PIL import Image
44
+
45
+ return "image.jpg", _encode(Image.fromarray(source))
46
+ raise VisynxError(
47
+ f"Cannot read an image from {type(source).__name__}. Pass a path, bytes, "
48
+ "a file opened in binary mode, a PIL image, or a numpy array."
49
+ )
50
+
51
+
52
+ @dataclass
53
+ class Model:
54
+ """A trained model. Get one from :meth:`Visynx.model`, not by constructing it."""
55
+
56
+ provider: str
57
+ job_id: str
58
+ slug: str | None = None
59
+ display_name: str | None = None
60
+ version: int | None = None
61
+ dataset_id: int | None = None
62
+ created_at: str | None = None
63
+ client: Any = field(default=None, repr=False, compare=False)
64
+ _labels: list | None = field(default=None, repr=False, compare=False)
65
+
66
+ @classmethod
67
+ def _from_job(cls, client, job: dict) -> Model:
68
+ return cls(
69
+ provider=job.get("provider"),
70
+ job_id=job.get("job_id"),
71
+ slug=job.get("slug"),
72
+ display_name=job.get("display_name"),
73
+ version=job.get("training_version"),
74
+ dataset_id=job.get("dataset_id"),
75
+ created_at=job.get("created_at"),
76
+ client=client,
77
+ )
78
+
79
+ def __repr__(self) -> str:
80
+ return f"Model({self.slug or f'{self.provider}/{self.job_id}'!r}, provider={self.provider!r})"
81
+
82
+ @property
83
+ def labels(self) -> list:
84
+ """Class names the model predicts, in class-id order.
85
+
86
+ Read from the weights themselves, so they stay correct even if the source
87
+ dataset's classes were renamed after training.
88
+ """
89
+ if self._labels is None:
90
+ payload = self.client._request("GET", f"/train/model-labels/{self.job_id}")
91
+ self._labels = payload.get("labels") or []
92
+ return self._labels
93
+
94
+ def predict(
95
+ self,
96
+ source: Any,
97
+ *,
98
+ confidence: float = 0.0,
99
+ segment: bool = False,
100
+ depth: bool = False,
101
+ ) -> Result | list:
102
+ """Run the model on an image.
103
+
104
+ Args:
105
+ source: image path, directory of images, bytes, open binary file,
106
+ PIL image, or numpy array.
107
+ confidence: drop detections scoring below this (0-1).
108
+ segment: refine each detected box into an instance polygon with SAM2.
109
+ depth: also return a monocular depth map for the image.
110
+
111
+ Returns:
112
+ A :class:`Result`, or a list of them when ``source`` is a directory.
113
+
114
+ A plain call spends 1 Vision Credit. ``segment`` and ``depth`` each run a
115
+ second model on the GPU and cost extra. You are only charged once the
116
+ inference succeeds, and a retried call is billed once.
117
+ """
118
+ if isinstance(source, (str, Path)) and Path(source).is_dir():
119
+ images = sorted(
120
+ p for p in Path(source).iterdir() if p.suffix.lower() in IMAGE_SUFFIXES
121
+ )
122
+ return [
123
+ self.predict(p, confidence=confidence, segment=segment, depth=depth)
124
+ for p in images
125
+ ]
126
+
127
+ filename, data = _to_bytes(source)
128
+ if not data:
129
+ raise VisynxError("Image is empty")
130
+ if len(data) > MAX_UPLOAD_BYTES:
131
+ raise PayloadTooLargeError(
132
+ f"{filename} is {len(data) / 1e6:.1f} MB; the inference limit is "
133
+ f"{MAX_UPLOAD_BYTES // 1024 // 1024} MB. Resize it before sending."
134
+ )
135
+
136
+ params = {}
137
+ if segment:
138
+ params["segment"] = "true"
139
+ if depth:
140
+ params["depth"] = "true"
141
+
142
+ payload = self.client._request(
143
+ "POST",
144
+ f"/train/jobs/{self.provider}/{self.job_id}/infer",
145
+ files={"file": (filename, data)},
146
+ params=params,
147
+ )
148
+ return Result(payload, image_bytes=data, confidence=confidence)
visynx/py.typed ADDED
File without changes
visynx/results.py ADDED
@@ -0,0 +1,182 @@
1
+ """Prediction results, and drawing them on the image.
2
+
3
+ Field names deliberately mirror the VisionForge API one-for-one — matching the
4
+ wire format is the feature, so anything you learn from ``result.json()`` applies
5
+ to the typed objects and vice versa.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import io
11
+ from dataclasses import dataclass, field
12
+
13
+ #: Deterministic per-class colours, so the same class is the same colour across
14
+ #: every image in a run.
15
+ PALETTE = (
16
+ "#ef4444", "#3b82f6", "#22c55e", "#f59e0b", "#a855f7", "#06b6d4",
17
+ "#ec4899", "#84cc16", "#f97316", "#6366f1", "#14b8a6", "#eab308",
18
+ )
19
+
20
+
21
+ def _colour(class_id: int | None) -> str:
22
+ return PALETTE[(class_id or 0) % len(PALETTE)]
23
+
24
+
25
+ @dataclass
26
+ class Detection:
27
+ """One predicted object. ``box`` is ``[x1, y1, x2, y2]`` in absolute pixels.
28
+
29
+ Classification models return detections with no ``box``; segmentation models
30
+ add ``polygons`` (flat ``[x1, y1, x2, y2, ...]`` per instance); pose models
31
+ add ``keypoints`` (``[[x, y, visibility], ...]``).
32
+ """
33
+
34
+ class_id: int | None = None
35
+ class_name: str | None = None
36
+ confidence: float = 0.0
37
+ box: list | None = None
38
+ polygons: list = field(default_factory=list)
39
+ keypoints: list = field(default_factory=list)
40
+ #: Relative depth (0-1, higher is closer). Only set when ``depth=True``.
41
+ depth_median: float | None = None
42
+
43
+ @classmethod
44
+ def _from_json(cls, data: dict) -> Detection:
45
+ return cls(
46
+ class_id=data.get("class_id"),
47
+ class_name=data.get("class_name"),
48
+ confidence=float(data.get("confidence") or 0.0),
49
+ box=data.get("box"),
50
+ polygons=data.get("polygons") or [],
51
+ keypoints=data.get("keypoints") or [],
52
+ depth_median=data.get("depth_median"),
53
+ )
54
+
55
+ def __repr__(self) -> str:
56
+ return f"Detection({self.class_name!r}, {self.confidence:.2f})"
57
+
58
+
59
+ def _decode_png(b64: str):
60
+ from PIL import Image
61
+
62
+ return Image.open(io.BytesIO(base64.b64decode(b64)))
63
+
64
+
65
+ class Result:
66
+ """What one image came back as.
67
+
68
+ Iterate it for detections, call :meth:`json` for the raw payload, or
69
+ :meth:`plot` / :meth:`save` to see it.
70
+ """
71
+
72
+ def __init__(self, payload: dict, image_bytes: bytes | None = None, confidence: float = 0.0):
73
+ self._payload = payload
74
+ self._image_bytes = image_bytes
75
+ self.job_id = payload.get("job_id")
76
+ self.detections = [
77
+ Detection._from_json(d)
78
+ for d in (payload.get("detections") or [])
79
+ if float(d.get("confidence") or 0.0) >= confidence
80
+ ]
81
+ #: Dense per-pixel overlay from a semantic-segmentation (smp) model.
82
+ #: ``{width, height, overlay_png_b64, legend}``; ``detections`` is empty.
83
+ self.semantic: dict | None = payload.get("semantic")
84
+ #: Monocular depth map, when ``depth=True`` was passed.
85
+ self.depth: dict | None = payload.get("depth")
86
+
87
+ def __len__(self) -> int:
88
+ return len(self.detections)
89
+
90
+ def __iter__(self):
91
+ return iter(self.detections)
92
+
93
+ def __repr__(self) -> str:
94
+ if self.semantic:
95
+ classes = len(self.semantic.get("legend") or [])
96
+ return f"Result(semantic, {classes} classes)"
97
+ return f"Result({len(self.detections)} detections)"
98
+
99
+ def json(self) -> dict:
100
+ """The raw API response, unmodified."""
101
+ return self._payload
102
+
103
+ @property
104
+ def image(self):
105
+ """The image that was sent, as a PIL image."""
106
+ if not self._image_bytes:
107
+ raise ValueError("This result carries no source image")
108
+ from PIL import Image
109
+
110
+ return Image.open(io.BytesIO(self._image_bytes))
111
+
112
+ @property
113
+ def depth_image(self):
114
+ """The depth map as a colourised PIL image, or None if depth wasn't requested."""
115
+ if not self.depth or not self.depth.get("overlay_png_b64"):
116
+ return None
117
+ return _decode_png(self.depth["overlay_png_b64"]).convert("RGB")
118
+
119
+ def plot(self):
120
+ """Draw the predictions onto the source image and return a PIL image."""
121
+ from PIL import Image, ImageDraw
122
+
123
+ canvas = self.image.convert("RGBA")
124
+
125
+ # Semantic models return a transparent RGBA mask to blend, not boxes.
126
+ if self.semantic and self.semantic.get("overlay_png_b64"):
127
+ overlay = _decode_png(self.semantic["overlay_png_b64"]).convert("RGBA")
128
+ if overlay.size != canvas.size:
129
+ overlay = overlay.resize(canvas.size, Image.NEAREST)
130
+ canvas = Image.alpha_composite(canvas, overlay)
131
+
132
+ draw = ImageDraw.Draw(canvas)
133
+ width = max(2, canvas.width // 400)
134
+ font = _font(canvas.width)
135
+
136
+ # Classification models have no geometry — stack the scores in a corner.
137
+ boxless = [d for d in self.detections if not d.box and not d.polygons]
138
+ for index, det in enumerate(boxless):
139
+ _label(draw, (width * 2, width * 2 + index * (width * 9)),
140
+ f"{det.class_name} {det.confidence:.2f}", _colour(det.class_id), font)
141
+
142
+ for det in self.detections:
143
+ colour = _colour(det.class_id)
144
+ for polygon in det.polygons:
145
+ if len(polygon) >= 6:
146
+ points = list(zip(polygon[0::2], polygon[1::2]))
147
+ draw.line(points + points[:1], fill=colour, width=width)
148
+ if det.box and len(det.box) == 4:
149
+ x1, y1, x2, y2 = (float(v) for v in det.box)
150
+ draw.rectangle((x1, y1, x2, y2), outline=colour, width=width)
151
+ _label(draw, (x1, y1), f"{det.class_name} {det.confidence:.2f}", colour, font)
152
+ for point in det.keypoints:
153
+ if len(point) >= 2:
154
+ x, y = float(point[0]), float(point[1])
155
+ draw.ellipse((x - width * 2, y - width * 2, x + width * 2, y + width * 2), fill=colour)
156
+
157
+ return canvas.convert("RGB")
158
+
159
+ def save(self, path) -> str:
160
+ """Write :meth:`plot`'s annotated image to ``path``. Returns the path."""
161
+ self.plot().save(path)
162
+ return str(path)
163
+
164
+
165
+ def _font(image_width: int):
166
+ from PIL import ImageFont
167
+
168
+ size = max(12, image_width // 60)
169
+ try: # Pillow >= 10.1 can scale the built-in font
170
+ return ImageFont.load_default(size=size)
171
+ except TypeError:
172
+ return ImageFont.load_default()
173
+
174
+
175
+ def _label(draw, anchor, text: str, colour: str, font) -> None:
176
+ """A filled chip with the class name, kept inside the image."""
177
+ x, y = anchor
178
+ left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
179
+ w, h = right - left, bottom - top
180
+ y = max(0, y - h - 4)
181
+ draw.rectangle((x, y, x + w + 6, y + h + 6), fill=colour)
182
+ draw.text((x + 3, y + 2), text, fill="white", font=font)
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: visynx
3
+ Version: 0.1.0
4
+ Summary: Run your VisionForge computer-vision models from Python.
5
+ Project-URL: Homepage, https://visionforge.evo-ss.com
6
+ Project-URL: Documentation, https://visionforge.evo-ss.com/docs/python-quickstart
7
+ Project-URL: Source, https://github.com/manasjohri/Labelling_Package
8
+ Project-URL: Issues, https://github.com/manasjohri/Labelling_Package/issues
9
+ Author: VisionForge
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 VisionForge
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: computer-vision,detectron2,inference,machine-learning,object-detection,rf-detr,segmentation,visionforge,yolo
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: Intended Audience :: Science/Research
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.9
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
43
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
44
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
45
+ Classifier: Typing :: Typed
46
+ Requires-Python: >=3.9
47
+ Requires-Dist: pillow>=9.0
48
+ Requires-Dist: requests>=2.25
49
+ Provides-Extra: dev
50
+ Requires-Dist: pytest>=7; extra == 'dev'
51
+ Requires-Dist: ruff>=0.5; extra == 'dev'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # visynx
55
+
56
+ Run your [VisionForge](https://visionforge.evo-ss.com) computer-vision models from Python.
57
+
58
+ Train a model in VisionForge, grab an API key, and you are three lines from predictions —
59
+ detection, segmentation, pose, classification, and semantic segmentation, all through one call.
60
+
61
+ ```bash
62
+ pip install visynx
63
+ ```
64
+
65
+ ## Quickstart
66
+
67
+ ```python
68
+ from visynx import Visynx
69
+
70
+ vf = Visynx(api_key="vf_...") # or set VISYNX_API_KEY
71
+ model = vf.model("shelf-scanner/3") # your project name and version
72
+
73
+ result = model.predict("shelf.jpg", confidence=0.4)
74
+
75
+ for d in result:
76
+ print(d.class_name, round(d.confidence, 2), d.box)
77
+
78
+ result.save("annotated.jpg")
79
+ ```
80
+
81
+ ```
82
+ bottle 0.94 [412.0, 118.5, 501.2, 388.0]
83
+ bottle 0.91 [502.6, 121.0, 590.4, 390.7]
84
+ can 0.78 [604.1, 210.3, 664.9, 372.2]
85
+ ```
86
+
87
+ `visynx` pulls in only `requests` and `Pillow`. No OpenCV, no numpy, no torch —
88
+ the models run on VisionForge's GPUs, not yours.
89
+
90
+ ## Find your models
91
+
92
+ ```python
93
+ for m in vf.models():
94
+ print(m.slug, m.provider, m.display_name)
95
+ # shelf-scanner/3 yolo Shelf Scanner v3
96
+ # defect-seg/1 rfdetr Defect Seg v1
97
+ ```
98
+
99
+ `"shelf-scanner/3"` is the model's slug — your project name and its training
100
+ version, the same name you see in the app. A raw `"yolo/<job_id>"` also works
101
+ and skips the lookup.
102
+
103
+ ## What you get back
104
+
105
+ `predict()` returns a `Result`: iterate it for detections, or reach for the raw
106
+ API payload with `.json()`.
107
+
108
+ ```python
109
+ result = model.predict("shelf.jpg")
110
+
111
+ len(result) # 3
112
+ result.detections[0] # Detection('bottle', 0.94)
113
+ result.json() # the raw API response, untouched
114
+
115
+ result.plot() # a PIL image with boxes drawn on
116
+ result.save("out.jpg") # ...written to disk
117
+ ```
118
+
119
+ Each `Detection` carries `class_id`, `class_name`, `confidence`, and `box`
120
+ (`[x1, y1, x2, y2]` in absolute pixels). Segmentation models add `polygons`;
121
+ pose models add `keypoints`.
122
+
123
+ ## Anything image-shaped works
124
+
125
+ ```python
126
+ model.predict("shelf.jpg") # a path
127
+ model.predict("frames/") # a directory -> list of Results
128
+ model.predict(open("shelf.jpg", "rb")) # an open file
129
+ model.predict(pil_image) # a PIL image
130
+ model.predict(numpy_array) # a numpy array (from OpenCV, say)
131
+ ```
132
+
133
+ ## Beyond boxes
134
+
135
+ Two flags the hosted GPUs do for you, with nothing extra to install. Each runs a
136
+ second model, so each costs extra credits:
137
+
138
+ ```python
139
+ # Turn every detected box into a precise instance polygon with SAM 2
140
+ result = model.predict("shelf.jpg", segment=True)
141
+ result.detections[0].polygons
142
+
143
+ # Add a monocular depth map, and a relative distance per detection
144
+ result = model.predict("shelf.jpg", depth=True)
145
+ result.detections[0].depth_median # 0-1, higher is closer
146
+ result.depth_image.save("depth.png")
147
+ ```
148
+
149
+ Semantic-segmentation models return a dense mask instead of boxes:
150
+
151
+ ```python
152
+ result = model.predict("road.jpg")
153
+ result.semantic["legend"] # [{'class_id': 1, 'class_name': 'road', ...}]
154
+ result.save("mask.jpg") # the mask, blended over your image
155
+ ```
156
+
157
+ ## Get an API key
158
+
159
+ In VisionForge, go to **Settings → API Keys**, create a key, and copy the secret —
160
+ it is shown once. Keys start with `vf_`, never expire, and can be locked to
161
+ specific IPs or CIDR ranges. Keep it out of source control:
162
+
163
+ ```python
164
+ vf = Visynx() # reads VISYNX_API_KEY
165
+ ```
166
+
167
+ A plain `predict()` call spends 1 Vision Credit; `segment=True` and `depth=True`
168
+ each add a surcharge for the extra model they run. You are charged only after the
169
+ inference succeeds — a failed call is free — and a call retried after a `429` is
170
+ billed once, not twice.
171
+
172
+ Some endpoints are deliberately not reachable with an API key: batch prediction
173
+ over a whole dataset, live-camera inference, workflow runs, and the AI-labelling
174
+ tools (SAM/DINO/box-prompt). Those run from the VisionForge app with a signed-in
175
+ session. Calling them with a key returns `PermissionDeniedError`.
176
+
177
+ ## Errors
178
+
179
+ Every failure is a typed exception, and it quotes the response's `X-Request-ID`
180
+ when the server sent one — include it if you contact support. (Authentication
181
+ failures are rejected before that header is assigned, so `401`s carry none.)
182
+
183
+ | Exception | Means | Do |
184
+ |---|---|---|
185
+ | `AuthenticationError` | key invalid or revoked | create a new key |
186
+ | `PermissionDeniedError` | email unverified, or the key's IP allowlist rejected you | verify your email / check the allowlist |
187
+ | `InsufficientCreditsError` | out of Vision Credits | top up |
188
+ | `ModelNotFoundError` | no such model, or not yours | check `vf.models()` |
189
+ | `PayloadTooLargeError` | image over 50 MB | resize before sending |
190
+ | `RateLimitError` | rate limited, retries exhausted | back off |
191
+ | `MaintenanceError` | platform in maintenance | retry later |
192
+
193
+ `429`s are retried automatically, honouring the server's `Retry-After`.
194
+ All of these subclass `VisynxError`, so one `except VisynxError` catches everything.
195
+
196
+ ## Self-hosting
197
+
198
+ Point the client at your own deployment:
199
+
200
+ ```python
201
+ vf = Visynx(api_key="vf_...", api_url="https://visionforge.internal/api")
202
+ ```
203
+
204
+ or set `VISYNX_API_URL`.
205
+
206
+ ## License
207
+
208
+ MIT
@@ -0,0 +1,9 @@
1
+ visynx/__init__.py,sha256=OekP2u7XLrzwVBCjODZrrttZGvgCRoEYos8_uN3MlwA,893
2
+ visynx/client.py,sha256=5wgcltVaSu57k0uR-HsFH4uYwC330Q0Gsk1_16LB95Y,8122
3
+ visynx/model.py,sha256=cWDXZU-X5jH8WBMQYJ1MpAVM4BFu-G27EnbKxWJsgck,5335
4
+ visynx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ visynx/results.py,sha256=pi3Ukv-p5NWaFIucCUnY0rtsbdraNpxRXLELs3N-4Mc,6803
6
+ visynx-0.1.0.dist-info/METADATA,sha256=GkE8C37wxJ6zoZpWPCPEYJmlatDLBnJjKoxjY4yEIII,7823
7
+ visynx-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
+ visynx-0.1.0.dist-info/licenses/LICENSE,sha256=Vs6853kI20vijlKlB1Jo17u6X_mNPjwdc3xPtvB2zxI,1068
9
+ visynx-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VisionForge
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.