orient_express 2.3.1__tar.gz → 2.4.1__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.
- {orient_express-2.3.1 → orient_express-2.4.1}/PKG-INFO +211 -18
- {orient_express-2.3.1 → orient_express-2.4.1}/README.md +210 -17
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/__init__.py +1 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/predictor.py +1 -1
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/semantic_segmentation.py +15 -6
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/vector_index.py +37 -24
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/utils/image_processor.py +6 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/vertex.py +18 -11
- {orient_express-2.3.1 → orient_express-2.4.1}/pyproject.toml +4 -1
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/__init__.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/deployment.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/model_wrapper.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/classification.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/feature_extraction.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/instance_segmentation.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/multi_label_classification.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/object_detection.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/sklearn_pipeline.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/utils/colors.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/utils/gs.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/utils/paths.py +0 -0
- {orient_express-2.3.1 → orient_express-2.4.1}/orient_express/utils/retry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: orient_express
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: A library to simplify model deployment to Vertex AI
|
|
5
5
|
Author: Alexey Zankevich
|
|
6
6
|
Author-email: alex.zankevich@shiftsmart.com
|
|
@@ -46,11 +46,13 @@ pip install orient_express
|
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
For local development:
|
|
49
|
+
|
|
49
50
|
```bash
|
|
50
51
|
pip install -e .
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
Or with Poetry:
|
|
55
|
+
|
|
54
56
|
```bash
|
|
55
57
|
poetry install
|
|
56
58
|
```
|
|
@@ -102,8 +104,8 @@ vertex_model.deploy_to_endpoint(
|
|
|
102
104
|
|
|
103
105
|
# remote prediction API depends on the endpoint container deployed with the model
|
|
104
106
|
predictions = vertex_model.remote_predict(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
"my-classifier-endpoint",
|
|
108
|
+
[{"image": "https://storage.googleapis.com/ssm-media-uploads/example.jpg"}],
|
|
107
109
|
)
|
|
108
110
|
```
|
|
109
111
|
|
|
@@ -179,19 +181,20 @@ predictions = local_predictor.predict(X_test)
|
|
|
179
181
|
### Platform Support Matrix
|
|
180
182
|
|
|
181
183
|
| Platform | Architecture | ONNX Runtime Package | CUDA Available |
|
|
182
|
-
|
|
183
|
-
| Linux | x86_64 | onnxruntime-gpu
|
|
184
|
-
| Linux | aarch64 | onnxruntime
|
|
185
|
-
| Windows | x64 (AMD64) | onnxruntime-gpu
|
|
186
|
-
| Windows | ARM64 | onnxruntime
|
|
187
|
-
| macOS | x86_64 | onnxruntime
|
|
188
|
-
| macOS | arm64 | onnxruntime
|
|
184
|
+
| -------- | ------------ | -------------------- | -------------- |
|
|
185
|
+
| Linux | x86_64 | onnxruntime-gpu | Yes |
|
|
186
|
+
| Linux | aarch64 | onnxruntime | No |
|
|
187
|
+
| Windows | x64 (AMD64) | onnxruntime-gpu | Yes |
|
|
188
|
+
| Windows | ARM64 | onnxruntime | No |
|
|
189
|
+
| macOS | x86_64 | onnxruntime | No |
|
|
190
|
+
| macOS | arm64 | onnxruntime | No |
|
|
189
191
|
|
|
190
192
|
The appropriate package is installed automatically based on your platform.
|
|
191
193
|
|
|
192
194
|
### Selecting CPU vs CUDA Execution
|
|
193
195
|
|
|
194
196
|
When loading a predictor, use the `device` parameter to specify the execution provider:
|
|
197
|
+
|
|
195
198
|
```python
|
|
196
199
|
from orient_express.predictors import ObjectDetectionPredictor
|
|
197
200
|
|
|
@@ -203,6 +206,7 @@ predictor = ObjectDetectionPredictor("/path/to/model", classes, device="cuda")
|
|
|
203
206
|
```
|
|
204
207
|
|
|
205
208
|
When using a Vertex AI model:
|
|
209
|
+
|
|
206
210
|
```python
|
|
207
211
|
# CPU inference
|
|
208
212
|
predictor = model.get_local_predictor(device="cpu")
|
|
@@ -540,11 +544,8 @@ index = build_vector_index(
|
|
|
540
544
|
num_workers=8, # parallel image loading
|
|
541
545
|
)
|
|
542
546
|
|
|
543
|
-
# Aggregate to one centroid per label
|
|
544
|
-
centroid_index = index.aggregate()
|
|
545
|
-
|
|
546
547
|
# Save and load
|
|
547
|
-
|
|
548
|
+
index.dump("/path/to/artifact_dir")
|
|
548
549
|
|
|
549
550
|
from orient_express.predictors import get_predictor
|
|
550
551
|
loaded_index = get_predictor("/path/to/artifact_dir")
|
|
@@ -560,16 +561,23 @@ batch_results = loaded_index.search_batch(query_matrix, k=5)
|
|
|
560
561
|
|
|
561
562
|
#### Multi-label support
|
|
562
563
|
|
|
563
|
-
Vectors can have
|
|
564
|
+
Vectors can have composite labels (use tuples). This is useful when a single visual cluster maps to multiple things:
|
|
564
565
|
|
|
565
566
|
```python
|
|
566
567
|
index = VectorIndex(
|
|
567
568
|
vectors=feature_matrix,
|
|
568
|
-
labels=[
|
|
569
|
+
labels=[("sku_101", "sku_102"), ("sku_103",)],
|
|
569
570
|
)
|
|
571
|
+
```
|
|
570
572
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
+
#### Per-label aggregation
|
|
574
|
+
|
|
575
|
+
Vector indices in which labels are not unique can be aggregated so that each label has a single centroid.
|
|
576
|
+
If `per_label=True` and the labels are composite (tuples), then the labels will be unpacked and aggregated separately.
|
|
577
|
+
|
|
578
|
+
```python
|
|
579
|
+
aggregated = index.aggregate(per_label=True) # 3 vectors, one per label element ["sku_101", "sku_102", "sku_103"]
|
|
580
|
+
aggregated = index.aggregate(per_label=False) # 2 vectors, one per composite label [("sku_101", "sku_102"), ("sku_103")]
|
|
573
581
|
```
|
|
574
582
|
|
|
575
583
|
#### Output Structure
|
|
@@ -585,6 +593,191 @@ class SearchResult:
|
|
|
585
593
|
|
|
586
594
|
---
|
|
587
595
|
|
|
596
|
+
## Deployed Endpoint APIs
|
|
597
|
+
|
|
598
|
+
When you upload a model with orient-express and deploy it to a Vertex AI endpoint, the actual HTTP API exposed by the endpoint is determined by the serving container image — not by your Python predictor code. Orient-express ships two such images:
|
|
599
|
+
|
|
600
|
+
- `image-onnx` — serves the built-in ONNX predictor types (classification, detection, segmentation).
|
|
601
|
+
- `xgboost-scikit-learn` — serves any joblib-loadable model (sklearn pipelines, xgboost, etc.).
|
|
602
|
+
|
|
603
|
+
This section documents the request/response shape each image's endpoint exposes once deployed.
|
|
604
|
+
|
|
605
|
+
### How the docker images connect to GCP endpoints
|
|
606
|
+
|
|
607
|
+
The deployment flow is:
|
|
608
|
+
|
|
609
|
+
1. **Train + export.** You build a predictor locally (e.g. `ClassificationPredictor("model.onnx", classes)`) or train a sklearn/xgboost model.
|
|
610
|
+
2. **Upload.** `upload_model` / `upload_model_joblib` pushes the artifacts to GCS under `gs://<bucket>/models/<model_name>/<version>/` and registers a Vertex AI Model with `serving_container_image_uri` pointing at one of orient-express's images.
|
|
611
|
+
3. **Deploy.** `vertex_model.deploy_to_endpoint(...)` (or the Vertex console) attaches the registered model to a Vertex AI Endpoint. Vertex starts the container with `AIP_STORAGE_URI` set to the GCS path from step 2, plus `MODEL_NAME` set to the model's display name.
|
|
612
|
+
4. **Serve.** The container downloads the artifacts on startup, instantiates the right predictor via metadata, and listens on `/v1/models/<MODEL_NAME>:predict`.
|
|
613
|
+
5. **Call.** Clients POST to `https://<region>-aiplatform.googleapis.com/v1/projects/<project>/locations/<region>/endpoints/<endpoint_id>:predict` with a Bearer token. Vertex routes the request into the container and returns the JSON response.
|
|
614
|
+
|
|
615
|
+
Every endpoint accepts the same envelope:
|
|
616
|
+
|
|
617
|
+
```json
|
|
618
|
+
{
|
|
619
|
+
"instances": [...],
|
|
620
|
+
"parameters": {...}
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
What goes in `instances` / `parameters`, and what comes back in `predictions`, is per-image and per-predictor — covered below.
|
|
625
|
+
|
|
626
|
+
### ONNX Image Endpoint
|
|
627
|
+
|
|
628
|
+
Image: `us-west1-docker.pkg.dev/shiftsmart-api/orient-express/image-onnx:<tag>`
|
|
629
|
+
|
|
630
|
+
Common request shape across all ONNX predictor types:
|
|
631
|
+
|
|
632
|
+
```json
|
|
633
|
+
{
|
|
634
|
+
"instances": [
|
|
635
|
+
{"image": "<http(s) URL | gs:// URI | base64 | data URI>"}
|
|
636
|
+
],
|
|
637
|
+
"parameters": {
|
|
638
|
+
"confidence": 0.5
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Common response envelope:
|
|
644
|
+
|
|
645
|
+
```json
|
|
646
|
+
{
|
|
647
|
+
"predictions": [
|
|
648
|
+
{"status": "success", ...predictor-specific fields...}
|
|
649
|
+
]
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
`status` values: `"success"`, `"failed to download image"`, or `"failed to get debug image"`. Malformed payloads return top-level `{"error": "Failed to decode input"}` instead of `predictions`.
|
|
654
|
+
|
|
655
|
+
The predictor-specific fields differ by model type — covered in the subsections below.
|
|
656
|
+
|
|
657
|
+
#### Classification
|
|
658
|
+
|
|
659
|
+
For models uploaded as `ClassificationPredictor`. `parameters.confidence` is **not** honored (the predictor always returns the top class).
|
|
660
|
+
|
|
661
|
+
Per-image response:
|
|
662
|
+
|
|
663
|
+
```json
|
|
664
|
+
{
|
|
665
|
+
"status": "success",
|
|
666
|
+
"class": "cat",
|
|
667
|
+
"score": 0.95,
|
|
668
|
+
"class_scores": {"cat": 0.95, "dog": 0.03, "bird": 0.02}
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
No `debug_image` for classification (nothing meaningful to draw).
|
|
673
|
+
|
|
674
|
+
#### Multi-label classification
|
|
675
|
+
|
|
676
|
+
For models uploaded as `MultiLabelClassificationPredictor`. `parameters.confidence` is the per-class threshold for inclusion in `classes` (default `0.5`).
|
|
677
|
+
|
|
678
|
+
Per-image response:
|
|
679
|
+
|
|
680
|
+
```json
|
|
681
|
+
{
|
|
682
|
+
"status": "success",
|
|
683
|
+
"predictions": {
|
|
684
|
+
"classes": ["contains_cat", "contains_bird"],
|
|
685
|
+
"class_scores": {"contains_cat": 0.95, "contains_dog": 0.03, "contains_bird": 0.82}
|
|
686
|
+
},
|
|
687
|
+
"debug_image": null
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
`debug_image` is always `null` for multi-label.
|
|
692
|
+
|
|
693
|
+
#### Object detection
|
|
694
|
+
|
|
695
|
+
For models uploaded as `BoundingBoxPredictor`. `parameters.confidence` filters detections below the threshold (default `0.5`).
|
|
696
|
+
|
|
697
|
+
Per-image response:
|
|
698
|
+
|
|
699
|
+
```json
|
|
700
|
+
{
|
|
701
|
+
"status": "success",
|
|
702
|
+
"predictions": [
|
|
703
|
+
{"class": "person", "score": 0.92, "bbox": {"x1": 100.5, "y1": 50.2, "x2": 300.8, "y2": 400.1}}
|
|
704
|
+
],
|
|
705
|
+
"debug_image": "<base64 JPEG with boxes overlaid>"
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
`bbox` coordinates are in pixels of the original (EXIF-corrected) image. `predictions` is an empty list when nothing clears the confidence threshold.
|
|
710
|
+
|
|
711
|
+
#### Instance segmentation
|
|
712
|
+
|
|
713
|
+
For models uploaded as `InstanceSegmentationPredictor`. `parameters.confidence` filters detections (default `0.5`).
|
|
714
|
+
|
|
715
|
+
Per-image response:
|
|
716
|
+
|
|
717
|
+
```json
|
|
718
|
+
{
|
|
719
|
+
"status": "success",
|
|
720
|
+
"predictions": [
|
|
721
|
+
{"class": "person", "score": 0.89, "bbox": {"x1": 100.5, "y1": 50.2, "x2": 300.8, "y2": 400.1}}
|
|
722
|
+
],
|
|
723
|
+
"debug_image": "<base64 JPEG with masks overlaid>"
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
Per-instance mask arrays are **not** included in the response by default (too large). The annotated mask overlay is baked into `debug_image`.
|
|
728
|
+
|
|
729
|
+
#### Semantic segmentation
|
|
730
|
+
|
|
731
|
+
For models uploaded as `SemanticSegmentationPredictor`. `parameters.confidence` is the per-pixel threshold above which a class is considered "valid" (default `0.5`).
|
|
732
|
+
|
|
733
|
+
Per-image response:
|
|
734
|
+
|
|
735
|
+
```json
|
|
736
|
+
{
|
|
737
|
+
"status": "success",
|
|
738
|
+
"predictions": {
|
|
739
|
+
"class_mask": "<base64 PNG, uint8, per-pixel class id>",
|
|
740
|
+
"valid_mask": "<base64 PNG, uint8, 0=below threshold, 1=above>"
|
|
741
|
+
},
|
|
742
|
+
"debug_image": "<base64 JPEG with color-coded overlay>"
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
`class_mask` always paints every pixel with the argmax winner. `valid_mask` tells you which pixels actually cleared the confidence threshold — AND them together client-side to get the "real" segmentation.
|
|
747
|
+
|
|
748
|
+
### XGBoost / scikit-learn Endpoint
|
|
749
|
+
|
|
750
|
+
Image: `us-west1-docker.pkg.dev/shiftsmart-api/orient-express/xgboost-scikit-learn:<tag>`
|
|
751
|
+
|
|
752
|
+
For models uploaded via `upload_model_joblib` — sklearn pipelines, xgboost models, or anything `joblib.load`-able with a `.predict(DataFrame)` method.
|
|
753
|
+
|
|
754
|
+
Request shape — `instances` is a list of dicts, one row per input:
|
|
755
|
+
|
|
756
|
+
```json
|
|
757
|
+
{
|
|
758
|
+
"instances": [
|
|
759
|
+
{"pclass": 1, "sex": "female", "age": 29, "fare": 100.0, "embarked": "S"},
|
|
760
|
+
{"pclass": 3, "sex": "male", "age": 35, "fare": 8.05, "embarked": "S"}
|
|
761
|
+
]
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
The server constructs `pd.DataFrame(instances)` and calls `model.predict(df)` on it. The columns your pipeline expects must be present in each instance dict.
|
|
766
|
+
|
|
767
|
+
Response shape — one prediction per input row:
|
|
768
|
+
|
|
769
|
+
```json
|
|
770
|
+
{
|
|
771
|
+
"predictions": [0, 1]
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
Each element is whatever your `.predict()` returns — a class label for classifiers, a numeric value for regressors, an array for multi-output models.
|
|
776
|
+
|
|
777
|
+
`parameters` is ignored — there's no per-request configuration for this image.
|
|
778
|
+
|
|
779
|
+
---
|
|
780
|
+
|
|
588
781
|
## Color Schemes
|
|
589
782
|
|
|
590
783
|
For predictors that support annotation (`BoundingBoxPredictor`, `InstanceSegmentationPredictor`, `SemanticSegmentationPredictor`), you can set a custom color scheme:
|
|
@@ -19,11 +19,13 @@ pip install orient_express
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
For local development:
|
|
22
|
+
|
|
22
23
|
```bash
|
|
23
24
|
pip install -e .
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
Or with Poetry:
|
|
28
|
+
|
|
27
29
|
```bash
|
|
28
30
|
poetry install
|
|
29
31
|
```
|
|
@@ -75,8 +77,8 @@ vertex_model.deploy_to_endpoint(
|
|
|
75
77
|
|
|
76
78
|
# remote prediction API depends on the endpoint container deployed with the model
|
|
77
79
|
predictions = vertex_model.remote_predict(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
"my-classifier-endpoint",
|
|
81
|
+
[{"image": "https://storage.googleapis.com/ssm-media-uploads/example.jpg"}],
|
|
80
82
|
)
|
|
81
83
|
```
|
|
82
84
|
|
|
@@ -152,19 +154,20 @@ predictions = local_predictor.predict(X_test)
|
|
|
152
154
|
### Platform Support Matrix
|
|
153
155
|
|
|
154
156
|
| Platform | Architecture | ONNX Runtime Package | CUDA Available |
|
|
155
|
-
|
|
156
|
-
| Linux | x86_64 | onnxruntime-gpu
|
|
157
|
-
| Linux | aarch64 | onnxruntime
|
|
158
|
-
| Windows | x64 (AMD64) | onnxruntime-gpu
|
|
159
|
-
| Windows | ARM64 | onnxruntime
|
|
160
|
-
| macOS | x86_64 | onnxruntime
|
|
161
|
-
| macOS | arm64 | onnxruntime
|
|
157
|
+
| -------- | ------------ | -------------------- | -------------- |
|
|
158
|
+
| Linux | x86_64 | onnxruntime-gpu | Yes |
|
|
159
|
+
| Linux | aarch64 | onnxruntime | No |
|
|
160
|
+
| Windows | x64 (AMD64) | onnxruntime-gpu | Yes |
|
|
161
|
+
| Windows | ARM64 | onnxruntime | No |
|
|
162
|
+
| macOS | x86_64 | onnxruntime | No |
|
|
163
|
+
| macOS | arm64 | onnxruntime | No |
|
|
162
164
|
|
|
163
165
|
The appropriate package is installed automatically based on your platform.
|
|
164
166
|
|
|
165
167
|
### Selecting CPU vs CUDA Execution
|
|
166
168
|
|
|
167
169
|
When loading a predictor, use the `device` parameter to specify the execution provider:
|
|
170
|
+
|
|
168
171
|
```python
|
|
169
172
|
from orient_express.predictors import ObjectDetectionPredictor
|
|
170
173
|
|
|
@@ -176,6 +179,7 @@ predictor = ObjectDetectionPredictor("/path/to/model", classes, device="cuda")
|
|
|
176
179
|
```
|
|
177
180
|
|
|
178
181
|
When using a Vertex AI model:
|
|
182
|
+
|
|
179
183
|
```python
|
|
180
184
|
# CPU inference
|
|
181
185
|
predictor = model.get_local_predictor(device="cpu")
|
|
@@ -513,11 +517,8 @@ index = build_vector_index(
|
|
|
513
517
|
num_workers=8, # parallel image loading
|
|
514
518
|
)
|
|
515
519
|
|
|
516
|
-
# Aggregate to one centroid per label
|
|
517
|
-
centroid_index = index.aggregate()
|
|
518
|
-
|
|
519
520
|
# Save and load
|
|
520
|
-
|
|
521
|
+
index.dump("/path/to/artifact_dir")
|
|
521
522
|
|
|
522
523
|
from orient_express.predictors import get_predictor
|
|
523
524
|
loaded_index = get_predictor("/path/to/artifact_dir")
|
|
@@ -533,16 +534,23 @@ batch_results = loaded_index.search_batch(query_matrix, k=5)
|
|
|
533
534
|
|
|
534
535
|
#### Multi-label support
|
|
535
536
|
|
|
536
|
-
Vectors can have
|
|
537
|
+
Vectors can have composite labels (use tuples). This is useful when a single visual cluster maps to multiple things:
|
|
537
538
|
|
|
538
539
|
```python
|
|
539
540
|
index = VectorIndex(
|
|
540
541
|
vectors=feature_matrix,
|
|
541
|
-
labels=[
|
|
542
|
+
labels=[("sku_101", "sku_102"), ("sku_103",)],
|
|
542
543
|
)
|
|
544
|
+
```
|
|
543
545
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
+
#### Per-label aggregation
|
|
547
|
+
|
|
548
|
+
Vector indices in which labels are not unique can be aggregated so that each label has a single centroid.
|
|
549
|
+
If `per_label=True` and the labels are composite (tuples), then the labels will be unpacked and aggregated separately.
|
|
550
|
+
|
|
551
|
+
```python
|
|
552
|
+
aggregated = index.aggregate(per_label=True) # 3 vectors, one per label element ["sku_101", "sku_102", "sku_103"]
|
|
553
|
+
aggregated = index.aggregate(per_label=False) # 2 vectors, one per composite label [("sku_101", "sku_102"), ("sku_103")]
|
|
546
554
|
```
|
|
547
555
|
|
|
548
556
|
#### Output Structure
|
|
@@ -558,6 +566,191 @@ class SearchResult:
|
|
|
558
566
|
|
|
559
567
|
---
|
|
560
568
|
|
|
569
|
+
## Deployed Endpoint APIs
|
|
570
|
+
|
|
571
|
+
When you upload a model with orient-express and deploy it to a Vertex AI endpoint, the actual HTTP API exposed by the endpoint is determined by the serving container image — not by your Python predictor code. Orient-express ships two such images:
|
|
572
|
+
|
|
573
|
+
- `image-onnx` — serves the built-in ONNX predictor types (classification, detection, segmentation).
|
|
574
|
+
- `xgboost-scikit-learn` — serves any joblib-loadable model (sklearn pipelines, xgboost, etc.).
|
|
575
|
+
|
|
576
|
+
This section documents the request/response shape each image's endpoint exposes once deployed.
|
|
577
|
+
|
|
578
|
+
### How the docker images connect to GCP endpoints
|
|
579
|
+
|
|
580
|
+
The deployment flow is:
|
|
581
|
+
|
|
582
|
+
1. **Train + export.** You build a predictor locally (e.g. `ClassificationPredictor("model.onnx", classes)`) or train a sklearn/xgboost model.
|
|
583
|
+
2. **Upload.** `upload_model` / `upload_model_joblib` pushes the artifacts to GCS under `gs://<bucket>/models/<model_name>/<version>/` and registers a Vertex AI Model with `serving_container_image_uri` pointing at one of orient-express's images.
|
|
584
|
+
3. **Deploy.** `vertex_model.deploy_to_endpoint(...)` (or the Vertex console) attaches the registered model to a Vertex AI Endpoint. Vertex starts the container with `AIP_STORAGE_URI` set to the GCS path from step 2, plus `MODEL_NAME` set to the model's display name.
|
|
585
|
+
4. **Serve.** The container downloads the artifacts on startup, instantiates the right predictor via metadata, and listens on `/v1/models/<MODEL_NAME>:predict`.
|
|
586
|
+
5. **Call.** Clients POST to `https://<region>-aiplatform.googleapis.com/v1/projects/<project>/locations/<region>/endpoints/<endpoint_id>:predict` with a Bearer token. Vertex routes the request into the container and returns the JSON response.
|
|
587
|
+
|
|
588
|
+
Every endpoint accepts the same envelope:
|
|
589
|
+
|
|
590
|
+
```json
|
|
591
|
+
{
|
|
592
|
+
"instances": [...],
|
|
593
|
+
"parameters": {...}
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
What goes in `instances` / `parameters`, and what comes back in `predictions`, is per-image and per-predictor — covered below.
|
|
598
|
+
|
|
599
|
+
### ONNX Image Endpoint
|
|
600
|
+
|
|
601
|
+
Image: `us-west1-docker.pkg.dev/shiftsmart-api/orient-express/image-onnx:<tag>`
|
|
602
|
+
|
|
603
|
+
Common request shape across all ONNX predictor types:
|
|
604
|
+
|
|
605
|
+
```json
|
|
606
|
+
{
|
|
607
|
+
"instances": [
|
|
608
|
+
{"image": "<http(s) URL | gs:// URI | base64 | data URI>"}
|
|
609
|
+
],
|
|
610
|
+
"parameters": {
|
|
611
|
+
"confidence": 0.5
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Common response envelope:
|
|
617
|
+
|
|
618
|
+
```json
|
|
619
|
+
{
|
|
620
|
+
"predictions": [
|
|
621
|
+
{"status": "success", ...predictor-specific fields...}
|
|
622
|
+
]
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
`status` values: `"success"`, `"failed to download image"`, or `"failed to get debug image"`. Malformed payloads return top-level `{"error": "Failed to decode input"}` instead of `predictions`.
|
|
627
|
+
|
|
628
|
+
The predictor-specific fields differ by model type — covered in the subsections below.
|
|
629
|
+
|
|
630
|
+
#### Classification
|
|
631
|
+
|
|
632
|
+
For models uploaded as `ClassificationPredictor`. `parameters.confidence` is **not** honored (the predictor always returns the top class).
|
|
633
|
+
|
|
634
|
+
Per-image response:
|
|
635
|
+
|
|
636
|
+
```json
|
|
637
|
+
{
|
|
638
|
+
"status": "success",
|
|
639
|
+
"class": "cat",
|
|
640
|
+
"score": 0.95,
|
|
641
|
+
"class_scores": {"cat": 0.95, "dog": 0.03, "bird": 0.02}
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
No `debug_image` for classification (nothing meaningful to draw).
|
|
646
|
+
|
|
647
|
+
#### Multi-label classification
|
|
648
|
+
|
|
649
|
+
For models uploaded as `MultiLabelClassificationPredictor`. `parameters.confidence` is the per-class threshold for inclusion in `classes` (default `0.5`).
|
|
650
|
+
|
|
651
|
+
Per-image response:
|
|
652
|
+
|
|
653
|
+
```json
|
|
654
|
+
{
|
|
655
|
+
"status": "success",
|
|
656
|
+
"predictions": {
|
|
657
|
+
"classes": ["contains_cat", "contains_bird"],
|
|
658
|
+
"class_scores": {"contains_cat": 0.95, "contains_dog": 0.03, "contains_bird": 0.82}
|
|
659
|
+
},
|
|
660
|
+
"debug_image": null
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
`debug_image` is always `null` for multi-label.
|
|
665
|
+
|
|
666
|
+
#### Object detection
|
|
667
|
+
|
|
668
|
+
For models uploaded as `BoundingBoxPredictor`. `parameters.confidence` filters detections below the threshold (default `0.5`).
|
|
669
|
+
|
|
670
|
+
Per-image response:
|
|
671
|
+
|
|
672
|
+
```json
|
|
673
|
+
{
|
|
674
|
+
"status": "success",
|
|
675
|
+
"predictions": [
|
|
676
|
+
{"class": "person", "score": 0.92, "bbox": {"x1": 100.5, "y1": 50.2, "x2": 300.8, "y2": 400.1}}
|
|
677
|
+
],
|
|
678
|
+
"debug_image": "<base64 JPEG with boxes overlaid>"
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
`bbox` coordinates are in pixels of the original (EXIF-corrected) image. `predictions` is an empty list when nothing clears the confidence threshold.
|
|
683
|
+
|
|
684
|
+
#### Instance segmentation
|
|
685
|
+
|
|
686
|
+
For models uploaded as `InstanceSegmentationPredictor`. `parameters.confidence` filters detections (default `0.5`).
|
|
687
|
+
|
|
688
|
+
Per-image response:
|
|
689
|
+
|
|
690
|
+
```json
|
|
691
|
+
{
|
|
692
|
+
"status": "success",
|
|
693
|
+
"predictions": [
|
|
694
|
+
{"class": "person", "score": 0.89, "bbox": {"x1": 100.5, "y1": 50.2, "x2": 300.8, "y2": 400.1}}
|
|
695
|
+
],
|
|
696
|
+
"debug_image": "<base64 JPEG with masks overlaid>"
|
|
697
|
+
}
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
Per-instance mask arrays are **not** included in the response by default (too large). The annotated mask overlay is baked into `debug_image`.
|
|
701
|
+
|
|
702
|
+
#### Semantic segmentation
|
|
703
|
+
|
|
704
|
+
For models uploaded as `SemanticSegmentationPredictor`. `parameters.confidence` is the per-pixel threshold above which a class is considered "valid" (default `0.5`).
|
|
705
|
+
|
|
706
|
+
Per-image response:
|
|
707
|
+
|
|
708
|
+
```json
|
|
709
|
+
{
|
|
710
|
+
"status": "success",
|
|
711
|
+
"predictions": {
|
|
712
|
+
"class_mask": "<base64 PNG, uint8, per-pixel class id>",
|
|
713
|
+
"valid_mask": "<base64 PNG, uint8, 0=below threshold, 1=above>"
|
|
714
|
+
},
|
|
715
|
+
"debug_image": "<base64 JPEG with color-coded overlay>"
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
`class_mask` always paints every pixel with the argmax winner. `valid_mask` tells you which pixels actually cleared the confidence threshold — AND them together client-side to get the "real" segmentation.
|
|
720
|
+
|
|
721
|
+
### XGBoost / scikit-learn Endpoint
|
|
722
|
+
|
|
723
|
+
Image: `us-west1-docker.pkg.dev/shiftsmart-api/orient-express/xgboost-scikit-learn:<tag>`
|
|
724
|
+
|
|
725
|
+
For models uploaded via `upload_model_joblib` — sklearn pipelines, xgboost models, or anything `joblib.load`-able with a `.predict(DataFrame)` method.
|
|
726
|
+
|
|
727
|
+
Request shape — `instances` is a list of dicts, one row per input:
|
|
728
|
+
|
|
729
|
+
```json
|
|
730
|
+
{
|
|
731
|
+
"instances": [
|
|
732
|
+
{"pclass": 1, "sex": "female", "age": 29, "fare": 100.0, "embarked": "S"},
|
|
733
|
+
{"pclass": 3, "sex": "male", "age": 35, "fare": 8.05, "embarked": "S"}
|
|
734
|
+
]
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
The server constructs `pd.DataFrame(instances)` and calls `model.predict(df)` on it. The columns your pipeline expects must be present in each instance dict.
|
|
739
|
+
|
|
740
|
+
Response shape — one prediction per input row:
|
|
741
|
+
|
|
742
|
+
```json
|
|
743
|
+
{
|
|
744
|
+
"predictions": [0, 1]
|
|
745
|
+
}
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
Each element is whatever your `.predict()` returns — a class label for classifiers, a numeric value for regressors, an array for multi-output models.
|
|
749
|
+
|
|
750
|
+
`parameters` is ignored — there's no per-request configuration for this image.
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
561
754
|
## Color Schemes
|
|
562
755
|
|
|
563
756
|
For predictors that support annotation (`BoundingBoxPredictor`, `InstanceSegmentationPredictor`, `SemanticSegmentationPredictor`), you can set a custom color scheme:
|
|
@@ -99,6 +99,7 @@ def load_vector_index(dir: str, metadata: dict | None = None):
|
|
|
99
99
|
data = np.load(artifact_path, allow_pickle=False)
|
|
100
100
|
vectors = data["vectors"]
|
|
101
101
|
labels = json.loads(str(data["labels_json"]))
|
|
102
|
+
labels = [tuple(label) if isinstance(label, list) else label for label in labels]
|
|
102
103
|
index = VectorIndex(vectors=vectors, labels=labels)
|
|
103
104
|
index.model_path = artifact_path
|
|
104
105
|
return index
|
|
@@ -46,7 +46,7 @@ class ImagePredictor(Predictor):
|
|
|
46
46
|
self.model_path = model_path
|
|
47
47
|
|
|
48
48
|
def get_serving_container_image_uri(self):
|
|
49
|
-
return "us-west1-docker.pkg.dev/shiftsmart-api/orient-express/image-onnx:v2.1
|
|
49
|
+
return "us-west1-docker.pkg.dev/shiftsmart-api/orient-express/image-onnx:v2.4.1"
|
|
50
50
|
|
|
51
51
|
def get_serving_container_health_route(self, model_name):
|
|
52
52
|
return f"/v1/models/{model_name}"
|
{orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/semantic_segmentation.py
RENAMED
|
@@ -7,17 +7,19 @@ import torch
|
|
|
7
7
|
import torch.nn.functional as F
|
|
8
8
|
|
|
9
9
|
from .predictor import OnnxSessionWrapper, ImagePredictor
|
|
10
|
-
from ..utils.image_processor import pil_to_opencv, opencv_to_pil
|
|
10
|
+
from ..utils.image_processor import mask_to_base64, pil_to_opencv, opencv_to_pil
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@dataclass
|
|
14
14
|
class SemanticSegmentationPrediction:
|
|
15
15
|
class_mask: np.ndarray
|
|
16
|
+
valid_mask: np.ndarray
|
|
16
17
|
conf_masks: np.ndarray
|
|
17
18
|
|
|
18
19
|
def to_dict(self, include_conf_masks: bool = False):
|
|
19
20
|
dict_repr = {
|
|
20
|
-
"class_mask": self.class_mask,
|
|
21
|
+
"class_mask": mask_to_base64(self.class_mask.astype(np.uint8)),
|
|
22
|
+
"valid_mask": mask_to_base64(self.valid_mask.astype(np.uint8)),
|
|
21
23
|
}
|
|
22
24
|
if include_conf_masks:
|
|
23
25
|
dict_repr["conf_masks"] = self.conf_masks.tolist()
|
|
@@ -60,31 +62,38 @@ class SemanticSegmentationPredictor(ImagePredictor):
|
|
|
60
62
|
backend_model = OnnxSemanticSegmentation
|
|
61
63
|
|
|
62
64
|
def predict(
|
|
63
|
-
self, images: list[Image.Image]
|
|
65
|
+
self, images: list[Image.Image], confidence: float = 0.5
|
|
64
66
|
) -> list[SemanticSegmentationPrediction]:
|
|
65
67
|
if not images:
|
|
66
68
|
return []
|
|
67
69
|
raw_outputs = self.model(images)
|
|
68
70
|
outputs = []
|
|
69
71
|
for masks in raw_outputs:
|
|
70
|
-
class_mask = np.argmax(masks, axis=0)
|
|
72
|
+
class_mask = np.argmax(masks, axis=0).astype(np.uint8)
|
|
73
|
+
valid_mask = np.max(masks, axis=0) >= confidence
|
|
71
74
|
outputs.append(
|
|
72
75
|
SemanticSegmentationPrediction(
|
|
73
76
|
class_mask=class_mask,
|
|
77
|
+
valid_mask=valid_mask,
|
|
74
78
|
conf_masks=masks,
|
|
75
79
|
)
|
|
76
80
|
)
|
|
77
81
|
return outputs
|
|
78
82
|
|
|
79
83
|
def get_annotated_image(
|
|
80
|
-
self,
|
|
84
|
+
self,
|
|
85
|
+
image: Image.Image,
|
|
86
|
+
prediction: SemanticSegmentationPrediction,
|
|
87
|
+
mask_opacity: float = 0.3,
|
|
81
88
|
) -> Image.Image:
|
|
82
89
|
opencv_image = pil_to_opencv(image)
|
|
83
90
|
mask_overlay = opencv_image.copy()
|
|
84
91
|
|
|
85
92
|
for class_id, class_name in self.classes.items():
|
|
86
93
|
fill_color = self.color_scheme.get(class_name, (120, 120, 120))
|
|
87
|
-
mask_overlay[
|
|
94
|
+
mask_overlay[
|
|
95
|
+
(prediction.class_mask == class_id) & prediction.valid_mask
|
|
96
|
+
] = fill_color[:3]
|
|
88
97
|
|
|
89
98
|
annotated_image = cv2.addWeighted(
|
|
90
99
|
mask_overlay, mask_opacity, opencv_image, 1 - mask_opacity, 0
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import json
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
import warnings
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
import yaml
|
|
7
8
|
import numpy as np
|
|
@@ -37,7 +38,7 @@ class VectorIndex(Predictor):
|
|
|
37
38
|
def __init__(
|
|
38
39
|
self,
|
|
39
40
|
vectors: np.ndarray,
|
|
40
|
-
labels: list[
|
|
41
|
+
labels: list[int | str | tuple],
|
|
41
42
|
normalize: bool = False,
|
|
42
43
|
):
|
|
43
44
|
if vectors.ndim != 2:
|
|
@@ -53,8 +54,16 @@ class VectorIndex(Predictor):
|
|
|
53
54
|
# Avoid division by zero for zero vectors
|
|
54
55
|
norms = np.where(norms == 0, 1, norms)
|
|
55
56
|
vectors = vectors / norms
|
|
57
|
+
|
|
56
58
|
self.vectors = vectors
|
|
57
59
|
self.labels = labels
|
|
60
|
+
self.label_to_idx = {}
|
|
61
|
+
|
|
62
|
+
for i, label in enumerate(labels):
|
|
63
|
+
if label not in self.label_to_idx:
|
|
64
|
+
self.label_to_idx[label] = []
|
|
65
|
+
self.label_to_idx[label].append(i)
|
|
66
|
+
|
|
58
67
|
self.model_path = None
|
|
59
68
|
|
|
60
69
|
def search(self, query: np.ndarray, k: int = 1) -> list[SearchResult]:
|
|
@@ -86,21 +95,36 @@ class VectorIndex(Predictor):
|
|
|
86
95
|
)
|
|
87
96
|
return all_results
|
|
88
97
|
|
|
98
|
+
def get_by_idx(self, idx: int) -> np.ndarray:
|
|
99
|
+
return self.vectors[idx]
|
|
100
|
+
|
|
101
|
+
def get_by_idxs(self, idxs: list[int]) -> np.ndarray:
|
|
102
|
+
return self.vectors[idxs]
|
|
103
|
+
|
|
104
|
+
def get_by_label(self, label: Any) -> np.ndarray:
|
|
105
|
+
return self.vectors[self.label_to_idx[label]]
|
|
106
|
+
|
|
107
|
+
def get_by_labels(self, labels: list[Any]) -> np.ndarray:
|
|
108
|
+
idxs = []
|
|
109
|
+
for label in labels:
|
|
110
|
+
idxs.extend(self.label_to_idx[label])
|
|
111
|
+
return self.vectors[idxs]
|
|
112
|
+
|
|
89
113
|
def aggregate(self, per_label: bool = False) -> "VectorIndex":
|
|
90
114
|
"""
|
|
91
115
|
Aggregate the vectors into a single centroid per unique label group.
|
|
92
116
|
|
|
93
117
|
Args:
|
|
94
118
|
per_label: if True, create one centroid per individual label
|
|
95
|
-
(splitting label groups apart). If False (default),
|
|
96
|
-
one centroid per unique label group.
|
|
119
|
+
(splitting label groups (tuples) apart). If False (default),
|
|
120
|
+
create one centroid per unique label group.
|
|
97
121
|
"""
|
|
98
122
|
key_to_indices: dict = {}
|
|
99
|
-
for i,
|
|
100
|
-
if per_label:
|
|
101
|
-
keys =
|
|
123
|
+
for i, label in enumerate(self.labels):
|
|
124
|
+
if per_label and isinstance(label, tuple):
|
|
125
|
+
keys = label
|
|
102
126
|
else:
|
|
103
|
-
keys = [
|
|
127
|
+
keys = [label]
|
|
104
128
|
for key in keys:
|
|
105
129
|
if key not in key_to_indices:
|
|
106
130
|
key_to_indices[key] = []
|
|
@@ -119,7 +143,7 @@ class VectorIndex(Predictor):
|
|
|
119
143
|
if norm > 0:
|
|
120
144
|
centroid = centroid / norm
|
|
121
145
|
centroids[i] = centroid
|
|
122
|
-
new_labels.append(
|
|
146
|
+
new_labels.append(key)
|
|
123
147
|
|
|
124
148
|
return VectorIndex(vectors=centroids, labels=new_labels)
|
|
125
149
|
|
|
@@ -161,12 +185,9 @@ class VectorIndex(Predictor):
|
|
|
161
185
|
return self.vectors.shape[0]
|
|
162
186
|
|
|
163
187
|
def __repr__(self) -> str:
|
|
164
|
-
unique_labels = set()
|
|
165
|
-
for group in self.labels:
|
|
166
|
-
unique_labels.update(group)
|
|
167
188
|
return (
|
|
168
189
|
f"VectorIndex({len(self)} vectors, dim={self.dim}, "
|
|
169
|
-
f"{len(
|
|
190
|
+
f"{len(set(self.labels))} unique labels)"
|
|
170
191
|
)
|
|
171
192
|
|
|
172
193
|
|
|
@@ -197,11 +218,10 @@ def _collate_pil(batch):
|
|
|
197
218
|
|
|
198
219
|
def build_vector_index(
|
|
199
220
|
crops: list[Image.Image | str | CropSpec],
|
|
200
|
-
labels: list,
|
|
221
|
+
labels: list[int | str | tuple],
|
|
201
222
|
feature_extractor,
|
|
202
223
|
batch_size: int = 128,
|
|
203
224
|
normalize: bool = True,
|
|
204
|
-
multi_label: bool = False,
|
|
205
225
|
num_workers: int = 0,
|
|
206
226
|
) -> VectorIndex:
|
|
207
227
|
"""Build a VectorIndex by extracting features from crops.
|
|
@@ -210,14 +230,12 @@ def build_vector_index(
|
|
|
210
230
|
crops: images to extract features from. Each element can be a PIL
|
|
211
231
|
Image (used directly), a file path string (loaded as a whole
|
|
212
232
|
image), or a CropSpec (loaded and cropped to the specified bbox).
|
|
213
|
-
labels: one label per crop.
|
|
214
|
-
|
|
215
|
-
of labels.
|
|
233
|
+
labels: one label per crop. labels can be any hashable type, includeing
|
|
234
|
+
iterables like tuples.
|
|
216
235
|
feature_extractor: anything with a .predict(list[Image]) method that
|
|
217
236
|
returns objects with a .feature attribute (e.g. FeatureExtractionPredictor).
|
|
218
237
|
batch_size: number of crops to process at once.
|
|
219
238
|
normalize: whether to L2-normalize the resulting feature vectors.
|
|
220
|
-
multi_label: if True, labels are already lists of labels per crop.
|
|
221
239
|
num_workers: number of DataLoader workers for parallel image loading.
|
|
222
240
|
"""
|
|
223
241
|
if len(crops) != len(labels):
|
|
@@ -225,11 +243,6 @@ def build_vector_index(
|
|
|
225
243
|
f"crops length ({len(crops)}) must match labels length ({len(labels)})"
|
|
226
244
|
)
|
|
227
245
|
|
|
228
|
-
if multi_label:
|
|
229
|
-
index_labels = [list(group) for group in labels]
|
|
230
|
-
else:
|
|
231
|
-
index_labels = [[label] for label in labels]
|
|
232
|
-
|
|
233
246
|
loader = DataLoader(
|
|
234
247
|
_CropDataset(crops),
|
|
235
248
|
batch_size=batch_size,
|
|
@@ -243,4 +256,4 @@ def build_vector_index(
|
|
|
243
256
|
all_features.extend([r.feature for r in results])
|
|
244
257
|
|
|
245
258
|
vectors = np.vstack(all_features)
|
|
246
|
-
return VectorIndex(vectors=vectors, labels=
|
|
259
|
+
return VectorIndex(vectors=vectors, labels=labels, normalize=normalize)
|
|
@@ -74,6 +74,12 @@ def image_to_base64(image):
|
|
|
74
74
|
return base64.b64encode(bytes_content).decode("utf-8")
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def mask_to_base64(mask: np.ndarray) -> str:
|
|
78
|
+
buffered = BytesIO()
|
|
79
|
+
Image.fromarray(mask).save(buffered, format="PNG")
|
|
80
|
+
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
81
|
+
|
|
82
|
+
|
|
77
83
|
def image_to_bytes(image):
|
|
78
84
|
buffered = BytesIO()
|
|
79
85
|
if image.mode == "RGBA":
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import tempfile
|
|
3
|
+
import warnings
|
|
3
4
|
|
|
4
5
|
import yaml
|
|
5
6
|
import joblib
|
|
@@ -287,7 +288,6 @@ def get_vertex_model(
|
|
|
287
288
|
):
|
|
288
289
|
vertex_init(project_name, region)
|
|
289
290
|
models = aiplatform.Model.list(filter=f"display_name={model_name}")
|
|
290
|
-
|
|
291
291
|
if not models:
|
|
292
292
|
if raise_exception:
|
|
293
293
|
raise Exception(
|
|
@@ -295,19 +295,26 @@ def get_vertex_model(
|
|
|
295
295
|
)
|
|
296
296
|
else:
|
|
297
297
|
return None
|
|
298
|
+
if len(models) > 1:
|
|
299
|
+
warnings.warn(
|
|
300
|
+
f"Multiple models found with name '{model_name}'. Using the latest one."
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
latest_model = sorted(models, key=lambda x: x.update_time, reverse=True)[0]
|
|
304
|
+
resource_name = latest_model.resource_name
|
|
298
305
|
|
|
299
306
|
if version is None:
|
|
300
|
-
latest_model = sorted(models, key=lambda x: x.update_time, reverse=True)[0]
|
|
301
307
|
return VertexModel(
|
|
302
308
|
latest_model, model_name, project_name, region, int(latest_model.version_id)
|
|
303
309
|
)
|
|
304
310
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
if version is not None:
|
|
312
|
+
try:
|
|
313
|
+
model = aiplatform.Model(model_name=resource_name, version=str(version))
|
|
314
|
+
except Exception:
|
|
315
|
+
if raise_exception:
|
|
316
|
+
raise Exception(
|
|
317
|
+
f"Failed to fetch model '{model_name}' with version '{version}' in registry for project '{project_name}' region '{region}'"
|
|
318
|
+
)
|
|
319
|
+
return None
|
|
320
|
+
return VertexModel(model, model_name, project_name, region, version)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "orient_express"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.1"
|
|
4
4
|
description = "A library to simplify model deployment to Vertex AI"
|
|
5
5
|
authors = ["Alexey Zankevich <alex.zankevich@shiftsmart.com>", "Ian Myers <ian.myers@shiftsmart.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -25,6 +25,9 @@ onnxruntime = {version = "^1.20", markers = "sys_platform == 'darwin' or platfor
|
|
|
25
25
|
[tool.poetry.group.dev.dependencies]
|
|
26
26
|
black = "24.10.0"
|
|
27
27
|
pytest = "8.3.3"
|
|
28
|
+
ipykernel = "^7.2.0"
|
|
29
|
+
|
|
30
|
+
[tool.poetry.group.server.dependencies]
|
|
28
31
|
kserve = "^0.16.0"
|
|
29
32
|
python-json-logger = "^4.0.0"
|
|
30
33
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/feature_extraction.py
RENAMED
|
File without changes
|
{orient_express-2.3.1 → orient_express-2.4.1}/orient_express/predictors/instance_segmentation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|