Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .github/workflows/remote-real-model-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Remote real model E2E

on:
pull_request:
branches: [develop]
paths:
- "anylabeling/services/auto_labeling/remote_client.py"
- "anylabeling/services/auto_labeling/remote_model.py"
- "anylabeling/services/auto_labeling/model_manager.py"
- "scripts/validate_remote_inference.py"
- ".github/workflows/remote-real-model-e2e.yml"
push:
branches: [develop]
paths:
- "anylabeling/services/auto_labeling/remote_client.py"
- "anylabeling/services/auto_labeling/remote_model.py"
- "anylabeling/services/auto_labeling/model_manager.py"
- "scripts/validate_remote_inference.py"
- ".github/workflows/remote-real-model-e2e.yml"
schedule:
- cron: "41 3 * * 1"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: remote-real-model-e2e-${{ github.ref }}
cancel-in-progress: true

jobs:
yolox-s:
name: Authenticated YOLOX-S / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
env:
ASSET_DIR: _anylearning/tests/fixtures/inference/real_models
MODEL_SHA256: c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063
IMAGE_SHA256: 5a9522051c3cec2bbd2f6323fccba32e8fbf3ddcc2b3e2fd46b04c720bc6f866
OPENBLAS_NUM_THREADS: "1"
OMP_NUM_THREADS: "1"
QT_QPA_PLATFORM: offscreen
steps:
- uses: actions/checkout@v7

- name: Check out the pinned AnyLearning server contract
uses: actions/checkout@v7
with:
repository: nrl-ai/anylearning-oss
ref: 7cabc1e8caaec070410fbc47e8ee250ae50454ce
path: _anylearning

- uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: pip

- name: Install Qt system libraries (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libegl1 libxkbcommon-x11-0 libdbus-1-3 libxcb-cursor0 \
libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 \
libxcb-render-util0 libxcb-shape0 libxcb-xinerama0 libxcb-xkb1

- name: Install PyQt6 (macOS)
if: runner.os == 'macOS'
run: python -m pip install "PyQt6>=6.7.0"

- name: Install client and isolated server dependencies
run: |
python -m pip install --upgrade pip
python -m pip install .
python -m pip install "fastapi>=0.141,<1" "pydantic>=2.13,<3" "argon2-cffi>=25.1,<26" "psutil>=7.2,<8" "uvicorn>=0.41,<1"

- name: Download immutable Apache-2.0 test assets
shell: bash
run: |
set -euo pipefail
python _anylearning/scripts/download_verified_file.py \
"https://github.com/Megvii-BaseDetection/YOLOX/releases/download/0.1.1rc0/yolox_s.onnx" \
"$ASSET_DIR/yolox_s.onnx" --sha256 "$MODEL_SHA256" --max-bytes 104857600
python _anylearning/scripts/download_verified_file.py \
"https://raw.githubusercontent.com/Megvii-BaseDetection/YOLOX/6ddff4824372906469a7fae2dc3206c7aa4bbaee/assets/dog.jpg" \
"$ASSET_DIR/dog.jpg" --sha256 "$IMAGE_SHA256" --max-bytes 10485760

- name: Run AnyLabeling through the real authenticated server
run: >-
python scripts/validate_remote_inference.py
--anylearning-root _anylearning
--manifest _anylearning/tests/fixtures/inference/real_models/yolox_s_official.json
--model _anylearning/tests/fixtures/inference/real_models/yolox_s.onnx
--image _anylearning/tests/fixtures/inference/real_models/dog.jpg
--output-root validation-results/${{ runner.os }}

- name: Retain visual, result, and timing evidence
if: always()
uses: actions/upload-artifact@v6
with:
name: anylabeling-remote-yolox-s-${{ runner.os }}
path: validation-results/
if-no-files-found: error
retention-days: 30
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
- **SAM 3** (ViT-H) — open-vocabulary segmentation with text prompts
- [x] Text detection, recognition and KIE (Key Information Extraction) labeling.
- [x] Hardware acceleration with CUDA, CoreML, DirectML, OpenVINO, and vendor NPU providers.
- [x] Authenticated shared ONNX inference through an AnyLearning server.
- [x] Multiple languages available: English, Vietnamese, Chinese.

### Supported Models
Expand All @@ -57,6 +58,8 @@

Required model weights are downloaded automatically on first use.

For centrally hosted models, see the [authenticated remote inference guide](docs/remote_inference.md).

## Latest Release

[AnyLabeling v0.4.43](https://github.com/vietanhdev/anylabeling/releases/tag/v0.4.43) is the current stable release. It fixes recovery after model download/load failures, skips invalid images during SAM preload, persists grouped shapes with undo support, and closes label files reliably after saving and loading.
Expand Down
1 change: 1 addition & 0 deletions anylabeling/services/auto_labeling/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Import models to ensure they register themselves via @ModelRegistry.register
from . import remote_model as remote_model # noqa: F401
from . import segment_anything as segment_anything # noqa: F401
from . import yolov5 as yolov5 # noqa: F401
from . import yolov8 as yolov8 # noqa: F401
Expand Down
6 changes: 0 additions & 6 deletions anylabeling/services/auto_labeling/model.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
import os
import socket
import ssl
from abc import abstractmethod

import yaml
Expand All @@ -18,11 +17,6 @@
socket.setdefaulttimeout(240) # Prevent timeout when downloading models


ssl._create_default_https_context = (
ssl._create_unverified_context
) # Prevent issue when downloading models behind a proxy


class Model(QObject):
BASE_DOWNLOAD_URL = "https://github.com/vietanhdev/anylabeling-assets/raw/main/"

Expand Down
44 changes: 30 additions & 14 deletions anylabeling/services/auto_labeling/model_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os
import pathlib
import shutil
import ssl
import tempfile
import time
import urllib.request
Expand All @@ -22,10 +21,6 @@

from .registry import ModelRegistry

ssl._create_default_https_context = (
ssl._create_unverified_context
) # Prevent issue when downloading models behind a proxy


class ModelManager(QObject):
"""Model manager"""
Expand Down Expand Up @@ -162,7 +157,11 @@ def on_model_download_finished(self):
self.model_loaded.emit(self.loaded_model_config)
self.output_modes_changed.emit(
self.loaded_model_config["model"].Meta.output_modes,
self.loaded_model_config["model"].Meta.default_output_mode,
getattr(
self.loaded_model_config["model"],
"output_mode",
self.loaded_model_config["model"].Meta.default_output_mode,
),
)
else:
self.model_loaded.emit({})
Expand Down Expand Up @@ -208,7 +207,8 @@ def load_custom_model(self, config_file):
"type" not in model_config
or "display_name" not in model_config
or "name" not in model_config
or model_config["type"] not in ["segment_anything", "yolov5", "yolov8"]
or model_config["type"]
not in ["remote", "segment_anything", "yolov5", "yolov8"]
):
self._report_model_load_error(
self.tr("Error in loading custom model: Invalid config file format.")
Expand Down Expand Up @@ -439,7 +439,9 @@ def _load_model(self, model_id):

# Specific logic for interactive models (like SAM) vs detection models
# Ideally this should be a property of the model class (capabilities)
if model_type == "segment_anything":
if model_type == "segment_anything" or getattr(
model_config["model"], "supports_interactive_prompts", False
):
self.auto_segmentation_model_selected.emit()
# Request next files for prediction
self.request_next_files_requested.emit()
Expand All @@ -460,9 +462,13 @@ def set_auto_labeling_marks(self, marks):
"""Set auto labeling marks
(For example, for segment_anything model, it is the marks for)
"""
if (
self.loaded_model_config is None
or self.loaded_model_config["type"] != "segment_anything"
if self.loaded_model_config is None or not (
self.loaded_model_config["type"] == "segment_anything"
or getattr(
self.loaded_model_config["model"],
"supports_interactive_prompts",
False,
)
):
return
self.loaded_model_config["model"].set_auto_labeling_marks(marks)
Expand Down Expand Up @@ -534,8 +540,11 @@ def predict_shapes_threading(self, image, filename=None):
self.model_execution_thread is not None
and self.model_execution_thread.isRunning()
):
if hasattr(self.loaded_model_config["model"], "unload"):
self.loaded_model_config["model"].unload()
model = self.loaded_model_config["model"]
if hasattr(model, "cancel_prediction"):
model.cancel_prediction()
elif hasattr(model, "unload"):
model.unload()

# Wait for the thread to finish
self.model_execution_thread.quit()
Expand Down Expand Up @@ -573,7 +582,14 @@ def on_next_files_changed(self, next_files):
return

# Currently only segment_anything model supports this feature
if self.loaded_model_config["type"] != "segment_anything":
if not (
self.loaded_model_config["type"] == "segment_anything"
or getattr(
self.loaded_model_config["model"],
"supports_interactive_prompts",
False,
)
):
return

self.loaded_model_config["model"].on_next_files_changed(next_files)
Loading
Loading