visionai-sdk-python 2.0.0__tar.gz → 2.1.0__tar.gz

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 (47) hide show
  1. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/PKG-INFO +39 -1
  2. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/README.md +38 -0
  3. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/vlm/__init__.py +14 -2
  4. visionai_sdk_python-2.1.0/src/visionai_sdk_python/vlm/preprocess.py +186 -0
  5. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python.egg-info/PKG-INFO +39 -1
  6. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python.egg-info/SOURCES.txt +3 -1
  7. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python.egg-info/scm_file_list.json +26 -24
  8. visionai_sdk_python-2.1.0/src/visionai_sdk_python.egg-info/scm_version.json +8 -0
  9. visionai_sdk_python-2.1.0/tests/test_vlm_preprocess.py +213 -0
  10. visionai_sdk_python-2.0.0/src/visionai_sdk_python.egg-info/scm_version.json +0 -8
  11. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/.github/dependabot.yml +0 -0
  12. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/.github/workflows/ci.yml +0 -0
  13. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/.github/workflows/publish.yml +0 -0
  14. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/.gitignore +0 -0
  15. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/pyproject.toml +0 -0
  16. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/setup.cfg +0 -0
  17. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/__init__.py +0 -0
  18. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/_base.py +0 -0
  19. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/_jwt_verifier.py +0 -0
  20. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/_request_attribution.py +0 -0
  21. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/_source_header.py +0 -0
  22. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/async_client.py +0 -0
  23. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/auth/__init__.py +0 -0
  24. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/auth/_mixin.py +0 -0
  25. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/auth/async_resource.py +0 -0
  26. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/auth/models.py +0 -0
  27. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/auth/resource.py +0 -0
  28. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/client.py +0 -0
  29. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/constants.py +0 -0
  30. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/endpoints.py +0 -0
  31. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/exceptions.py +0 -0
  32. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/instrumentation.py +0 -0
  33. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/py.typed +0 -0
  34. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/vlm/_mixin.py +0 -0
  35. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/vlm/async_resource.py +0 -0
  36. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/vlm/models.py +0 -0
  37. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python/vlm/resource.py +0 -0
  38. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python.egg-info/dependency_links.txt +0 -0
  39. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python.egg-info/requires.txt +0 -0
  40. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/src/visionai_sdk_python.egg-info/top_level.txt +0 -0
  41. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/tests/__init__.py +0 -0
  42. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/tests/constants.py +0 -0
  43. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/tests/test__base.py +0 -0
  44. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/tests/test_async_client.py +0 -0
  45. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/tests/test_client.py +0 -0
  46. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/tests/test_instrumentation.py +0 -0
  47. {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visionai-sdk-python
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: VisionAI SDK for Python
5
5
  Author-email: Tony Yang <tonyyang@linkervision.com>
6
6
  Requires-Python: >=3.11
@@ -27,6 +27,7 @@ Python client library for VisionAI authentication and Vision Language Model (VLM
27
27
  - **Auto Token Management**: Automatic token refresh before expiration
28
28
  - **JWT Validation**: Built-in token signature and expiration verification
29
29
  - **VLM Inference**: Submit and poll vision-language model tasks
30
+ - **Resize Planning**: Compute VLM input dimensions (smart / square resize) without any image dependency — the caller does the actual resize
30
31
  - **Async Support**: Full async/await support with `AsyncClient`
31
32
  - **Type Safe**: Full type hints with Pydantic validation
32
33
  - **Service Source Attribution**: Auto-attach a service-source header to every outbound `requests`/`httpx`/`aiohttp` call with a single startup call
@@ -187,6 +188,43 @@ elif result.status in ("failed", "timeout"):
187
188
  - `failed`: Error, check `error`
188
189
  - `timeout`: Request timeout
189
190
 
191
+ ### Resize Planning
192
+
193
+ Compute the target dimensions for a VLM input image. Pure math, no image
194
+ dependencies — resize with whatever library your service already uses,
195
+ applying `plan.interpolation`. `name`, `factor`, and `interpolation` come
196
+ from the model server's resize spec; `pixels` is the UI resize option:
197
+
198
+ ```python
199
+ from visionai_sdk_python.vlm import compute_resize
200
+
201
+ plan = compute_resize(
202
+ width=1920, height=1080, name="smart_resize", factor=32, pixels=768,
203
+ interpolation="bicubic",
204
+ )
205
+ # ResizePlan(width=1024, height=576, interpolation='bicubic')
206
+ # smart_resize: dimensions divisible by factor, area capped at pixels * pixels
207
+
208
+ compute_resize(width=w, height=h, name="square_resize", pixels=384, interpolation="lanczos")
209
+ # square_resize: exact pixels x pixels; factor is ignored, so a whole resize
210
+ # spec can be forwarded as-is
211
+
212
+ # No resize spec from the model server? Fall back to the default
213
+ # (smart_resize, factor 32, bicubic):
214
+ from visionai_sdk_python.vlm import DEFAULT_RESIZE_SPEC
215
+ plan = compute_resize(width=w, height=h, pixels=768, **DEFAULT_RESIZE_SPEC)
216
+
217
+ # Apply with your own imaging library (PIL shown; see module docstring for cv2)
218
+ interpolation = {"bicubic": Image.Resampling.BICUBIC,
219
+ "lanczos": Image.Resampling.LANCZOS}[plan.interpolation]
220
+ img = img.resize((plan.width, plan.height), interpolation)
221
+ ```
222
+
223
+ Invalid input (non-positive dimensions, a non-integer or non-positive
224
+ `pixels`/`factor`, unknown `name` or `interpolation`, missing `factor` for
225
+ smart resize, `min_pixels` on a square resize or over the pixel budget, an
226
+ unsatisfiable budget) raises `ValueError`.
227
+
190
228
  ## Token Validation
191
229
 
192
230
  ```python
@@ -9,6 +9,7 @@ Python client library for VisionAI authentication and Vision Language Model (VLM
9
9
  - **Auto Token Management**: Automatic token refresh before expiration
10
10
  - **JWT Validation**: Built-in token signature and expiration verification
11
11
  - **VLM Inference**: Submit and poll vision-language model tasks
12
+ - **Resize Planning**: Compute VLM input dimensions (smart / square resize) without any image dependency — the caller does the actual resize
12
13
  - **Async Support**: Full async/await support with `AsyncClient`
13
14
  - **Type Safe**: Full type hints with Pydantic validation
14
15
  - **Service Source Attribution**: Auto-attach a service-source header to every outbound `requests`/`httpx`/`aiohttp` call with a single startup call
@@ -169,6 +170,43 @@ elif result.status in ("failed", "timeout"):
169
170
  - `failed`: Error, check `error`
170
171
  - `timeout`: Request timeout
171
172
 
173
+ ### Resize Planning
174
+
175
+ Compute the target dimensions for a VLM input image. Pure math, no image
176
+ dependencies — resize with whatever library your service already uses,
177
+ applying `plan.interpolation`. `name`, `factor`, and `interpolation` come
178
+ from the model server's resize spec; `pixels` is the UI resize option:
179
+
180
+ ```python
181
+ from visionai_sdk_python.vlm import compute_resize
182
+
183
+ plan = compute_resize(
184
+ width=1920, height=1080, name="smart_resize", factor=32, pixels=768,
185
+ interpolation="bicubic",
186
+ )
187
+ # ResizePlan(width=1024, height=576, interpolation='bicubic')
188
+ # smart_resize: dimensions divisible by factor, area capped at pixels * pixels
189
+
190
+ compute_resize(width=w, height=h, name="square_resize", pixels=384, interpolation="lanczos")
191
+ # square_resize: exact pixels x pixels; factor is ignored, so a whole resize
192
+ # spec can be forwarded as-is
193
+
194
+ # No resize spec from the model server? Fall back to the default
195
+ # (smart_resize, factor 32, bicubic):
196
+ from visionai_sdk_python.vlm import DEFAULT_RESIZE_SPEC
197
+ plan = compute_resize(width=w, height=h, pixels=768, **DEFAULT_RESIZE_SPEC)
198
+
199
+ # Apply with your own imaging library (PIL shown; see module docstring for cv2)
200
+ interpolation = {"bicubic": Image.Resampling.BICUBIC,
201
+ "lanczos": Image.Resampling.LANCZOS}[plan.interpolation]
202
+ img = img.resize((plan.width, plan.height), interpolation)
203
+ ```
204
+
205
+ Invalid input (non-positive dimensions, a non-integer or non-positive
206
+ `pixels`/`factor`, unknown `name` or `interpolation`, missing `factor` for
207
+ smart resize, `min_pixels` on a square resize or over the pixel budget, an
208
+ unsatisfiable budget) raises `ValueError`.
209
+
172
210
  ## Token Validation
173
211
 
174
212
  ```python
@@ -2,12 +2,24 @@
2
2
 
3
3
  from .async_resource import AsyncVLMResource
4
4
  from .models import NIMRequestModel, ResponseErrorModel, ResponseNormalModel
5
+ from .preprocess import (
6
+ DEFAULT_RESIZE_SPEC,
7
+ Interpolation,
8
+ ResizeMode,
9
+ ResizePlan,
10
+ compute_resize,
11
+ )
5
12
  from .resource import VLMResource
6
13
 
7
14
  __all__ = [
15
+ "DEFAULT_RESIZE_SPEC",
8
16
  "AsyncVLMResource",
9
- "VLMResource",
17
+ "Interpolation",
10
18
  "NIMRequestModel",
11
- "ResponseNormalModel",
19
+ "ResizeMode",
20
+ "ResizePlan",
12
21
  "ResponseErrorModel",
22
+ "ResponseNormalModel",
23
+ "VLMResource",
24
+ "compute_resize",
13
25
  ]
@@ -0,0 +1,186 @@
1
+ """Image-resize planning helpers for VLM inference.
2
+
3
+ This module only computes target dimensions (pure math, no image
4
+ dependencies). The mode (``name``), ``factor``, and ``interpolation`` come
5
+ from the model server's resize spec; ``pixels`` comes from the UI resize
6
+ option. The actual resize is done by the caller with whatever imaging
7
+ library its pipeline already uses, applying ``plan.interpolation``:
8
+
9
+ plan = compute_resize(
10
+ width=w, height=h, name="smart_resize", factor=32, pixels=768,
11
+ interpolation="bicubic",
12
+ )
13
+
14
+ # PIL client
15
+ PIL_INTERPOLATION = {
16
+ "bicubic": Image.Resampling.BICUBIC,
17
+ "lanczos": Image.Resampling.LANCZOS,
18
+ }
19
+ out = img.resize((plan.width, plan.height), PIL_INTERPOLATION[plan.interpolation])
20
+
21
+ # OpenCV client
22
+ CV2_INTERPOLATION = {
23
+ "bicubic": cv2.INTER_CUBIC,
24
+ "lanczos": cv2.INTER_LANCZOS4,
25
+ }
26
+ out = cv2.resize(
27
+ arr, (plan.width, plan.height), interpolation=CV2_INTERPOLATION[plan.interpolation]
28
+ )
29
+ """
30
+
31
+ import math
32
+ from collections.abc import Mapping
33
+ from types import MappingProxyType
34
+ from typing import Any, Literal, NamedTuple, get_args
35
+
36
+ _MAX_ASPECT_RATIO = 200
37
+
38
+ ResizeMode = Literal["smart_resize", "square_resize"]
39
+ Interpolation = Literal["bicubic", "lanczos"]
40
+
41
+ # Fallback resize spec for when the model server does not provide one.
42
+ # Read-only; splat it into compute_resize alongside the UI pixels value:
43
+ # compute_resize(width=w, height=h, pixels=768, **DEFAULT_RESIZE_SPEC)
44
+ DEFAULT_RESIZE_SPEC: Mapping[str, Any] = MappingProxyType(
45
+ {"name": "smart_resize", "factor": 32, "interpolation": "bicubic"}
46
+ )
47
+
48
+
49
+ class ResizePlan(NamedTuple):
50
+ """Target dimensions and interpolation method for a client-side resize."""
51
+
52
+ width: int
53
+ height: int
54
+ interpolation: Interpolation
55
+
56
+
57
+ def _smart_resize(
58
+ height: int,
59
+ width: int,
60
+ factor: int,
61
+ min_pixels: int,
62
+ max_pixels: int,
63
+ ) -> tuple[int, int]:
64
+ """Compute target (height, width) — note height-first, unlike ResizePlan.
65
+
66
+ 1. Both dimensions are divisible by ``factor``.
67
+ 2. The total number of pixels is within [``min_pixels``, ``max_pixels``].
68
+ 3. The aspect ratio is maintained as closely as possible.
69
+
70
+ Raises ValueError when the aspect ratio exceeds 200 or the constraints
71
+ cannot all be satisfied. Argument validation is compute_resize's job.
72
+ """
73
+ if max(height, width) / min(height, width) > _MAX_ASPECT_RATIO:
74
+ raise ValueError(
75
+ "absolute aspect ratio must be smaller than "
76
+ f"{_MAX_ASPECT_RATIO}, got {max(height, width) / min(height, width)}"
77
+ )
78
+ h_bar = max(factor, round(height / factor) * factor)
79
+ w_bar = max(factor, round(width / factor) * factor)
80
+ if h_bar * w_bar > max_pixels:
81
+ beta = math.sqrt((height * width) / max_pixels)
82
+ h_bar = max(factor, math.floor(height / beta / factor) * factor)
83
+ w_bar = max(factor, math.floor(width / beta / factor) * factor)
84
+ elif h_bar * w_bar < min_pixels:
85
+ beta = math.sqrt(min_pixels / (height * width))
86
+ h_bar = math.ceil(height * beta / factor) * factor
87
+ w_bar = math.ceil(width * beta / factor) * factor
88
+ if not min_pixels <= h_bar * w_bar <= max_pixels:
89
+ raise ValueError(
90
+ f"cannot satisfy min_pixels={min_pixels}, max_pixels={max_pixels} for a "
91
+ f"{width}x{height} image with factor={factor}"
92
+ )
93
+ return h_bar, w_bar
94
+
95
+
96
+ def compute_resize(
97
+ *,
98
+ width: int,
99
+ height: int,
100
+ name: ResizeMode,
101
+ pixels: int,
102
+ interpolation: Interpolation,
103
+ factor: int | None = None,
104
+ min_pixels: int | None = None,
105
+ ) -> ResizePlan:
106
+ """Plan a VLM-input resize from the model server's resize spec.
107
+
108
+ All arguments are keyword-only — a transposed width/height cannot happen
109
+ silently. ``name``, ``factor``, and ``interpolation`` mirror the model
110
+ server's resize spec fields; ``pixels`` is the UI resize option value.
111
+
112
+ - ``name="smart_resize"`` — dimensions divisible by ``factor``, total
113
+ pixel count within [``min_pixels``, ``pixels * pixels``], aspect ratio
114
+ kept. ``factor`` is required (the model's patch size, e.g. 32 or 48);
115
+ ``min_pixels`` defaults to ``4 * factor * factor``.
116
+ - ``name="square_resize"`` — exact ``pixels`` x ``pixels``; aspect ratio
117
+ not preserved. ``factor`` has no meaning and is ignored, so a whole
118
+ server resize spec can be forwarded as-is.
119
+
120
+ Args:
121
+ width: Source image width in pixels.
122
+ height: Source image height in pixels.
123
+ name: Resize mode from the model server: "smart_resize" or
124
+ "square_resize".
125
+ pixels: UI pixel option — one number, but its meaning depends on
126
+ ``name``:
127
+
128
+ - ``"smart_resize"``: an area budget, not a side length. The
129
+ output area is capped at ``pixels * pixels`` while keeping the
130
+ source aspect ratio, so neither output side is generally equal
131
+ to ``pixels`` — e.g. ``pixels=768`` on a 1920x1080 frame gives
132
+ 1024x576 (= 589,824 px, the same area as 768x768).
133
+ - ``"square_resize"``: the exact side length. The output is
134
+ always ``pixels`` x ``pixels`` — e.g. ``pixels=384`` gives
135
+ 384x384 regardless of the source shape.
136
+ interpolation: Interpolation the caller should resize with,
137
+ from the model server: "bicubic" or "lanczos". Passed through
138
+ into the returned plan.
139
+ factor: Smart resize only: round dimensions to multiples of this.
140
+ min_pixels: Smart resize only: lower bound on output pixel count.
141
+
142
+ Returns:
143
+ ResizePlan(width, height, interpolation) — note width-first,
144
+ matching the (width, height) order PIL and OpenCV resize calls expect.
145
+
146
+ Raises:
147
+ ValueError: If height/width are not positive, ``pixels`` or ``factor``
148
+ is not a positive integer, ``name`` or ``interpolation`` is not a
149
+ supported value, ``factor`` is missing (smart), ``min_pixels`` is
150
+ given for square resize or exceeds the pixel budget, or the
151
+ constraints cannot be satisfied (extreme aspect ratio).
152
+ """
153
+ if height <= 0 or width <= 0:
154
+ raise ValueError(f"height and width must be positive, got {width}x{height}")
155
+ if name not in get_args(ResizeMode):
156
+ raise ValueError(f"name must be one of {get_args(ResizeMode)}, got {name!r}")
157
+ if interpolation not in get_args(Interpolation):
158
+ raise ValueError(
159
+ f"interpolation must be one of {get_args(Interpolation)}, "
160
+ f"got {interpolation!r}"
161
+ )
162
+ if isinstance(pixels, bool) or not isinstance(pixels, int) or pixels <= 0:
163
+ raise ValueError(f"pixels must be a positive integer, got {pixels!r}")
164
+
165
+ if name == "smart_resize":
166
+ if factor is None:
167
+ raise ValueError("smart_resize requires factor")
168
+ if isinstance(factor, bool) or not isinstance(factor, int) or factor <= 0:
169
+ raise ValueError(f"factor must be a positive integer, got {factor!r}")
170
+ max_pixels = pixels * pixels
171
+ if min_pixels is None:
172
+ min_pixels = 4 * factor * factor
173
+ if min_pixels <= 0 or min_pixels > max_pixels:
174
+ raise ValueError(
175
+ f"min_pixels must be in (0, pixels * pixels], got "
176
+ f"min_pixels={min_pixels}, pixels={pixels}"
177
+ )
178
+ h_bar, w_bar = _smart_resize(height, width, factor, min_pixels, max_pixels)
179
+ else:
180
+ # factor is part of the server resize spec, so a caller forwarding the
181
+ # whole spec may pass it; it has no meaning here, so ignore it.
182
+ if min_pixels is not None:
183
+ raise ValueError("min_pixels only applies to smart_resize")
184
+ w_bar = h_bar = pixels
185
+
186
+ return ResizePlan(width=w_bar, height=h_bar, interpolation=interpolation)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visionai-sdk-python
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: VisionAI SDK for Python
5
5
  Author-email: Tony Yang <tonyyang@linkervision.com>
6
6
  Requires-Python: >=3.11
@@ -27,6 +27,7 @@ Python client library for VisionAI authentication and Vision Language Model (VLM
27
27
  - **Auto Token Management**: Automatic token refresh before expiration
28
28
  - **JWT Validation**: Built-in token signature and expiration verification
29
29
  - **VLM Inference**: Submit and poll vision-language model tasks
30
+ - **Resize Planning**: Compute VLM input dimensions (smart / square resize) without any image dependency — the caller does the actual resize
30
31
  - **Async Support**: Full async/await support with `AsyncClient`
31
32
  - **Type Safe**: Full type hints with Pydantic validation
32
33
  - **Service Source Attribution**: Auto-attach a service-source header to every outbound `requests`/`httpx`/`aiohttp` call with a single startup call
@@ -187,6 +188,43 @@ elif result.status in ("failed", "timeout"):
187
188
  - `failed`: Error, check `error`
188
189
  - `timeout`: Request timeout
189
190
 
191
+ ### Resize Planning
192
+
193
+ Compute the target dimensions for a VLM input image. Pure math, no image
194
+ dependencies — resize with whatever library your service already uses,
195
+ applying `plan.interpolation`. `name`, `factor`, and `interpolation` come
196
+ from the model server's resize spec; `pixels` is the UI resize option:
197
+
198
+ ```python
199
+ from visionai_sdk_python.vlm import compute_resize
200
+
201
+ plan = compute_resize(
202
+ width=1920, height=1080, name="smart_resize", factor=32, pixels=768,
203
+ interpolation="bicubic",
204
+ )
205
+ # ResizePlan(width=1024, height=576, interpolation='bicubic')
206
+ # smart_resize: dimensions divisible by factor, area capped at pixels * pixels
207
+
208
+ compute_resize(width=w, height=h, name="square_resize", pixels=384, interpolation="lanczos")
209
+ # square_resize: exact pixels x pixels; factor is ignored, so a whole resize
210
+ # spec can be forwarded as-is
211
+
212
+ # No resize spec from the model server? Fall back to the default
213
+ # (smart_resize, factor 32, bicubic):
214
+ from visionai_sdk_python.vlm import DEFAULT_RESIZE_SPEC
215
+ plan = compute_resize(width=w, height=h, pixels=768, **DEFAULT_RESIZE_SPEC)
216
+
217
+ # Apply with your own imaging library (PIL shown; see module docstring for cv2)
218
+ interpolation = {"bicubic": Image.Resampling.BICUBIC,
219
+ "lanczos": Image.Resampling.LANCZOS}[plan.interpolation]
220
+ img = img.resize((plan.width, plan.height), interpolation)
221
+ ```
222
+
223
+ Invalid input (non-positive dimensions, a non-integer or non-positive
224
+ `pixels`/`factor`, unknown `name` or `interpolation`, missing `factor` for
225
+ smart resize, `min_pixels` on a square resize or over the pixel budget, an
226
+ unsatisfiable budget) raises `ValueError`.
227
+
190
228
  ## Token Validation
191
229
 
192
230
  ```python
@@ -33,10 +33,12 @@ src/visionai_sdk_python/vlm/__init__.py
33
33
  src/visionai_sdk_python/vlm/_mixin.py
34
34
  src/visionai_sdk_python/vlm/async_resource.py
35
35
  src/visionai_sdk_python/vlm/models.py
36
+ src/visionai_sdk_python/vlm/preprocess.py
36
37
  src/visionai_sdk_python/vlm/resource.py
37
38
  tests/__init__.py
38
39
  tests/constants.py
39
40
  tests/test__base.py
40
41
  tests/test_async_client.py
41
42
  tests/test_client.py
42
- tests/test_instrumentation.py
43
+ tests/test_instrumentation.py
44
+ tests/test_vlm_preprocess.py
@@ -1,39 +1,41 @@
1
1
  {
2
2
  "files": [
3
- "pyproject.toml",
4
- "README.md",
5
- "uv.lock",
6
- ".gitignore",
7
3
  ".github/dependabot.yml",
8
4
  ".github/workflows/ci.yml",
9
5
  ".github/workflows/publish.yml",
10
- "tests/test_async_client.py",
11
- "tests/test_instrumentation.py",
12
- "tests/__init__.py",
13
- "tests/test_client.py",
14
- "tests/test__base.py",
15
- "tests/constants.py",
16
- "src/visionai_sdk_python/_base.py",
17
- "src/visionai_sdk_python/async_client.py",
18
- "src/visionai_sdk_python/endpoints.py",
19
- "src/visionai_sdk_python/_source_header.py",
20
- "src/visionai_sdk_python/_request_attribution.py",
21
- "src/visionai_sdk_python/py.typed",
6
+ ".gitignore",
7
+ "README.md",
8
+ "pyproject.toml",
22
9
  "src/visionai_sdk_python/__init__.py",
10
+ "src/visionai_sdk_python/_base.py",
23
11
  "src/visionai_sdk_python/_jwt_verifier.py",
24
- "src/visionai_sdk_python/client.py",
25
- "src/visionai_sdk_python/exceptions.py",
26
- "src/visionai_sdk_python/instrumentation.py",
27
- "src/visionai_sdk_python/constants.py",
12
+ "src/visionai_sdk_python/_request_attribution.py",
13
+ "src/visionai_sdk_python/_source_header.py",
14
+ "src/visionai_sdk_python/async_client.py",
28
15
  "src/visionai_sdk_python/auth/__init__.py",
16
+ "src/visionai_sdk_python/auth/_mixin.py",
29
17
  "src/visionai_sdk_python/auth/async_resource.py",
30
- "src/visionai_sdk_python/auth/resource.py",
31
18
  "src/visionai_sdk_python/auth/models.py",
32
- "src/visionai_sdk_python/auth/_mixin.py",
19
+ "src/visionai_sdk_python/auth/resource.py",
20
+ "src/visionai_sdk_python/client.py",
21
+ "src/visionai_sdk_python/constants.py",
22
+ "src/visionai_sdk_python/endpoints.py",
23
+ "src/visionai_sdk_python/exceptions.py",
24
+ "src/visionai_sdk_python/instrumentation.py",
25
+ "src/visionai_sdk_python/py.typed",
33
26
  "src/visionai_sdk_python/vlm/__init__.py",
27
+ "src/visionai_sdk_python/vlm/_mixin.py",
34
28
  "src/visionai_sdk_python/vlm/async_resource.py",
35
- "src/visionai_sdk_python/vlm/resource.py",
36
29
  "src/visionai_sdk_python/vlm/models.py",
37
- "src/visionai_sdk_python/vlm/_mixin.py"
30
+ "src/visionai_sdk_python/vlm/preprocess.py",
31
+ "src/visionai_sdk_python/vlm/resource.py",
32
+ "tests/__init__.py",
33
+ "tests/constants.py",
34
+ "tests/test__base.py",
35
+ "tests/test_async_client.py",
36
+ "tests/test_client.py",
37
+ "tests/test_instrumentation.py",
38
+ "tests/test_vlm_preprocess.py",
39
+ "uv.lock"
38
40
  ]
39
41
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.1.0",
3
+ "distance": 0,
4
+ "node": "g72bde18e5ad7c772f12e7d313f7396f24342ef92",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-14"
8
+ }
@@ -0,0 +1,213 @@
1
+ """Tests for VLM image-resize planning helpers."""
2
+
3
+ import pytest
4
+
5
+ from visionai_sdk_python.vlm import compute_resize
6
+
7
+
8
+ def smart(width, height, **overrides):
9
+ kwargs = {
10
+ "width": width,
11
+ "height": height,
12
+ "name": "smart_resize",
13
+ "factor": 32,
14
+ "pixels": 768,
15
+ "interpolation": "bicubic",
16
+ }
17
+ kwargs.update(overrides)
18
+ return compute_resize(**kwargs)
19
+
20
+
21
+ @pytest.mark.parametrize("factor", [32, 48])
22
+ @pytest.mark.parametrize(
23
+ "width,height",
24
+ [
25
+ (1920, 1080), # downscale (over the pixel budget)
26
+ (32, 32), # tiny, upscale to min_pixels
27
+ (375, 500), # within bounds, snap to factor
28
+ (4000, 3000), # large downscale
29
+ ],
30
+ )
31
+ def test_smart_constraints(width, height, factor):
32
+ plan = smart(width, height, factor=factor)
33
+ assert plan.width % factor == 0
34
+ assert plan.height % factor == 0
35
+ assert 4 * factor * factor <= plan.width * plan.height <= 768 * 768
36
+
37
+
38
+ @pytest.mark.parametrize(
39
+ "width,height,expected",
40
+ [
41
+ (1920, 1080, (1024, 576)), # README example
42
+ (1280, 8, (1280, 32)), # thin strip: short side snaps up, long side kept
43
+ (1000, 17, (992, 32)), # ...and does not jump at the factor/2 boundary
44
+ (1000, 16, (992, 32)),
45
+ (224, 2, (224, 32)), # already inside the budget — no upscale
46
+ ],
47
+ )
48
+ def test_smart_golden_dimensions(width, height, expected):
49
+ # Pins parity with the model server's smart_resize; the constraint-only
50
+ # tests above pass for wrong-but-legal plans.
51
+ plan = smart(width, height)
52
+ assert (plan.width, plan.height) == expected
53
+
54
+
55
+ def test_smart_keeps_aspect_ratio():
56
+ plan = smart(1920, 1080)
57
+ assert plan.width / plan.height == pytest.approx(1920 / 1080, rel=0.1)
58
+
59
+
60
+ def test_smart_orientation_not_transposed():
61
+ # Landscape in, landscape out — catches any width/height swap regression.
62
+ plan = smart(1920, 1080)
63
+ assert plan.width > plan.height
64
+
65
+
66
+ def test_smart_rejects_extreme_aspect_ratio():
67
+ with pytest.raises(ValueError, match="aspect ratio"):
68
+ smart(2010, 10)
69
+
70
+
71
+ def test_smart_rejects_unsatisfiable_max_pixels():
72
+ # Elongated strip: aspect ratio passes the <=200 check, but the factor
73
+ # floor cannot fit the pixel budget. Must raise, not silently overshoot.
74
+ with pytest.raises(ValueError, match="cannot satisfy"):
75
+ smart(40000, 200, factor=48, pixels=384)
76
+
77
+
78
+ def test_smart_rejects_unsatisfiable_min_pixels():
79
+ # Tight budget + extreme shape floors below min_pixels.
80
+ with pytest.raises(ValueError, match="cannot satisfy"):
81
+ smart(20000, 10000, pixels=64)
82
+
83
+
84
+ def test_smart_min_pixels_defaults_to_four_patches():
85
+ plan = smart(32, 32)
86
+ assert plan.width * plan.height >= 4 * 32 * 32
87
+ assert plan == smart(32, 32, min_pixels=4 * 32 * 32)
88
+
89
+
90
+ def test_smart_requires_factor():
91
+ with pytest.raises(ValueError, match="requires factor"):
92
+ compute_resize(
93
+ width=1920,
94
+ height=1080,
95
+ name="smart_resize",
96
+ pixels=768,
97
+ interpolation="bicubic",
98
+ )
99
+
100
+
101
+ def test_smart_rejects_min_pixels_over_budget():
102
+ with pytest.raises(ValueError, match="min_pixels"):
103
+ smart(1920, 1080, min_pixels=100000, pixels=64)
104
+ # Implicit default min_pixels (4 * 48 * 48 = 9216) exceeds 64 * 64.
105
+ with pytest.raises(ValueError, match="min_pixels"):
106
+ smart(1920, 1080, factor=48, pixels=64)
107
+
108
+
109
+ @pytest.mark.parametrize(
110
+ "kwargs",
111
+ [{"factor": 0}, {"factor": -32}, {"factor": 32.5}, {"min_pixels": 0}],
112
+ )
113
+ def test_smart_rejects_bad_params(kwargs):
114
+ with pytest.raises(ValueError):
115
+ smart(1920, 1080, **kwargs)
116
+
117
+
118
+ def test_square():
119
+ plan = compute_resize(
120
+ width=1920,
121
+ height=1080,
122
+ name="square_resize",
123
+ pixels=384,
124
+ interpolation="lanczos",
125
+ )
126
+ assert (plan.width, plan.height) == (384, 384)
127
+ assert plan.interpolation == "lanczos"
128
+
129
+
130
+ def test_square_ignores_factor():
131
+ # factor comes from the server resize spec, so forwarding the whole spec
132
+ # must not blow up on a square model.
133
+ plan = compute_resize(
134
+ width=1920,
135
+ height=1080,
136
+ name="square_resize",
137
+ pixels=384,
138
+ factor=32,
139
+ interpolation="bicubic",
140
+ )
141
+ assert (plan.width, plan.height) == (384, 384)
142
+
143
+
144
+ def test_square_rejects_min_pixels():
145
+ with pytest.raises(ValueError, match="only applies to smart_resize"):
146
+ compute_resize(
147
+ width=1920,
148
+ height=1080,
149
+ name="square_resize",
150
+ pixels=384,
151
+ min_pixels=100,
152
+ interpolation="bicubic",
153
+ )
154
+
155
+
156
+ def test_positional_arguments_rejected():
157
+ with pytest.raises(TypeError):
158
+ compute_resize(
159
+ 1920, 1080, name="square_resize", pixels=384, interpolation="lanczos"
160
+ ) # type: ignore[misc]
161
+
162
+
163
+ @pytest.mark.parametrize("width,height", [(0, 1080), (1920, 0), (0, 0), (-1, 100)])
164
+ def test_rejects_non_positive_dimensions(width, height):
165
+ with pytest.raises(ValueError, match="height and width"):
166
+ smart(width, height)
167
+
168
+
169
+ @pytest.mark.parametrize("pixels", [0, -384, 384.0, True, "384"])
170
+ def test_rejects_bad_pixels(pixels):
171
+ # 384.0 is what a JSON-decoded UI option looks like; no imaging library
172
+ # accepts float dimensions, so reject at the boundary, not downstream.
173
+ with pytest.raises(ValueError, match="pixels must be a positive integer"):
174
+ compute_resize(
175
+ width=1920,
176
+ height=1080,
177
+ name="square_resize",
178
+ pixels=pixels,
179
+ interpolation="bicubic",
180
+ )
181
+
182
+
183
+ def test_rejects_unknown_name():
184
+ with pytest.raises(ValueError, match="name must be one of"):
185
+ compute_resize(
186
+ width=1920,
187
+ height=1080,
188
+ name="longest_edge",
189
+ pixels=768,
190
+ interpolation="bicubic",
191
+ )
192
+
193
+
194
+ def test_rejects_unknown_interpolation():
195
+ # bilinear is no longer a supported value.
196
+ with pytest.raises(ValueError, match="interpolation must be one of"):
197
+ smart(1920, 1080, interpolation="bilinear")
198
+
199
+
200
+ def test_default_resize_spec():
201
+ from visionai_sdk_python.vlm import DEFAULT_RESIZE_SPEC
202
+
203
+ plan = compute_resize(width=1920, height=1080, pixels=768, **DEFAULT_RESIZE_SPEC)
204
+ assert plan == compute_resize(
205
+ width=1920,
206
+ height=1080,
207
+ pixels=768,
208
+ name="smart_resize",
209
+ factor=32,
210
+ interpolation="bicubic",
211
+ )
212
+ with pytest.raises(TypeError):
213
+ DEFAULT_RESIZE_SPEC["factor"] = 48 # type: ignore[index]
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.0.0",
3
- "distance": 0,
4
- "node": "ge6183ee40ad92d5fc80df4dd82c92fa4843d782a",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-07"
8
- }