jessehostetler commited on
Commit
c2feb3e
·
1 Parent(s): be5bf87

Default to CPU-only version of torch. Improve docs.

Browse files
.gitignore CHANGED
@@ -1,3 +1,4 @@
 
1
  models/
2
  venv/
3
  **/__pycache__
 
1
+ dyff-outputs/
2
  models/
3
  venv/
4
  **/__pycache__
Dockerfile CHANGED
@@ -8,8 +8,11 @@ RUN python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel
8
 
9
  WORKDIR /app/
10
 
11
- COPY requirements.txt ./
12
- RUN python3 -m pip install --no-cache-dir -r ./requirements.txt
 
 
 
13
 
14
  COPY app ./app
15
  COPY models ./models
 
8
 
9
  WORKDIR /app/
10
 
11
+ COPY requirements.cpu.txt ./
12
+ RUN python3 -m pip install --no-cache-dir -r ./requirements.cpu.txt
13
+
14
+ COPY requirements.torch.cpu.txt ./
15
+ RUN python3 -m pip install --no-cache-dir -r ./requirements.torch.cpu.txt
16
 
17
  COPY app ./app
18
  COPY models ./models
README.md CHANGED
@@ -8,18 +8,26 @@ FastAPI service for serving ML models over HTTP. Comes with ResNet-18 for image
8
 
9
  ## Quick Start
10
 
 
 
 
11
  **Local development:**
12
  ```bash
13
  # Install dependencies
14
- python -m venv .venv
15
- source .venv/bin/activate
16
- pip install -r requirements.txt
17
 
18
  # Download the example model
19
- bash scripts/model_download.bash
20
 
21
  # Run it
22
- uvicorn main:app --reload
 
 
 
 
 
 
23
  ```
24
 
25
  Server runs on `http://127.0.0.1:8000`. Check `/docs` for the interactive API documentation.
@@ -27,16 +35,13 @@ Server runs on `http://127.0.0.1:8000`. Check `/docs` for the interactive API do
27
  **Docker:**
28
  ```bash
29
  # Build
30
- docker build -t ml-inference-service:test .
31
 
32
  # Run
33
- docker run -d --name ml-inference-test -p 8000:8000 ml-inference-service:test
34
 
35
  # Check logs
36
- docker logs -f ml-inference-test
37
-
38
- # Stop
39
- docker stop ml-inference-test && docker rm ml-inference-test
40
  ```
41
 
42
  ## Testing the API
@@ -56,18 +61,18 @@ curl -X POST http://localhost:8000/predict \
56
  Example response:
57
  ```json
58
  {
59
- "prediction": "tiger cat",
60
- "confidence": 0.394,
61
- "predicted_label": 282,
62
- "model": "microsoft/resnet-18",
63
- "mediaType": "image/jpeg"
64
  }
65
  ```
66
 
67
  ## Project Structure
68
 
69
  ```
70
- ml-inference-service/
71
  ├── main.py # Entry point
72
  ├── app/
73
  │ ├── core/
@@ -75,7 +80,7 @@ ml-inference-service/
75
  │ │ └── logging.py # Logging setup
76
  │ ├── api/
77
  │ │ ├── models.py # Request/response schemas
78
- │ │ ├── controllers.py # Business logic
79
  │ │ └── routes/
80
  │ │ └── prediction.py # POST /predict
81
  │ └── services/
@@ -88,9 +93,14 @@ ml-inference-service/
88
  │ ├── model_download.bash
89
  │ ├── generate_test_datasets.py
90
  │ └── test_datasets.py
91
- ├── Dockerfile # Multi-stage build
92
  ├── .env.example # Environment config template
93
- └── requirements.txt
 
 
 
 
 
94
  ```
95
 
96
  The key design decision here is that `app/core/app.py` consolidates everything—config, dependency injection, lifecycle, and the app factory. This avoids the mess of managing global state across multiple files.
 
8
 
9
  ## Quick Start
10
 
11
+ **Install `uv`:**
12
+ https://docs.astral.sh/uv/getting-started/installation/
13
+
14
  **Local development:**
15
  ```bash
16
  # Install dependencies
17
+ make setup
18
+ source venv/bin/activate
 
19
 
20
  # Download the example model
21
+ make download
22
 
23
  # Run it
24
+ make serve
25
+ ```
26
+
27
+ In a second terminal:
28
+ ```bash
29
+ # Process an example input
30
+ ./prompt.sh cat.json
31
  ```
32
 
33
  Server runs on `http://127.0.0.1:8000`. Check `/docs` for the interactive API documentation.
 
35
  **Docker:**
36
  ```bash
37
  # Build
38
+ make docker-build
39
 
40
  # Run
41
+ make docker-run
42
 
43
  # Check logs
44
+ docker logs -f safe-challenge-2025/example-submission
 
 
 
45
  ```
46
 
47
  ## Testing the API
 
61
  Example response:
62
  ```json
63
  {
64
+ "logprobs": [-0.859380304813385,-1.2701971530914307,-2.1918208599090576,-1.69235098361969],
65
+ "localizationMask": {
66
+ "mediaType":"image/png",
67
+ "data":"iVBORw0KGgoAAAANSUhEUgAAA8AAAAKDAQAAAAD9Fl5AAAAAu0lEQVR4nO3NsREAMAgDMWD/nZMVKEwn1T5/FQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMCl3g5f+HC24TRhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAj70gwKsTlmdBwAAAABJRU5ErkJggg=="
68
+ }
69
  }
70
  ```
71
 
72
  ## Project Structure
73
 
74
  ```
75
+ example-submission/
76
  ├── main.py # Entry point
77
  ├── app/
78
  │ ├── core/
 
80
  │ │ └── logging.py # Logging setup
81
  │ ├── api/
82
  │ │ ├── models.py # Request/response schemas
83
+ │ │ ├── controllers.py # <= IMPLEMENT YOUR DETECTOR HERE
84
  │ │ └── routes/
85
  │ │ └── prediction.py # POST /predict
86
  │ └── services/
 
93
  │ ├── model_download.bash
94
  │ ├── generate_test_datasets.py
95
  │ └── test_datasets.py
96
+ ├── Dockerfile
97
  ├── .env.example # Environment config template
98
+ ├── cat.json # An example /predict request object
99
+ ├── makefile
100
+ ├── requirements.in
101
+ ├── requirements.txt
102
+ ├── response.json # An example /predict response object
103
+ └──
104
  ```
105
 
106
  The key design decision here is that `app/core/app.py` consolidates everything—config, dependency injection, lifecycle, and the app factory. This avoids the mess of managing global state across multiple files.
app/api/models.py CHANGED
@@ -1,18 +1,70 @@
1
  """
2
  Pydantic models for request/response validation.
3
  """
 
 
 
 
4
  import enum
5
- from typing import Optional
 
 
6
 
 
7
  import pydantic
 
8
 
9
- from dyff.schema.base import int32
 
10
 
11
 
12
  class ImageData(pydantic.BaseModel):
13
  """Image data model for base64 encoded images."""
14
- mediaType: str
15
- data: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
 
18
  class ImageRequest(pydantic.BaseModel):
@@ -27,36 +79,6 @@ class Labels(enum.IntEnum):
27
  LocallySynthesized = 3
28
 
29
 
30
- class LocalizationMask(pydantic.BaseModel):
31
- """A bit mask indicating which pixels are manipulated / synthesized.
32
-
33
- A bit value of ``1`` means that the model believes the corresponding pixel
34
- has been edited or synthesized (i.e., its label would be non-zero).
35
- A bit value of ``0`` means that the model believes the pixel is unaltered.
36
-
37
- The mask ``.width`` and ``.height`` should be the same as the input image.
38
- Extra bits at the end of ``.bitsRowMajor`` after the first
39
- ``width * height`` bits are **ignored**; for simplicity/efficiency,
40
- you should encode your bit mask into a byte array and not worry if the
41
- final byte isn't "full", then convert the byte array to base64.
42
- """
43
-
44
- width: int32() = pydantic.Field(
45
- description="The width of the mask."
46
- )
47
-
48
- height: int32() = pydantic.Field(
49
- description="The height of the mask."
50
- )
51
-
52
- bitsRowMajor: str = pydantic.Field(
53
- description="A base64 string encoding the bit mask in row-major order.",
54
- # Canonical base64 encoding
55
- # https://stackoverflow.com/a/64467300/3709935
56
- pattern=r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/][AQgw]==|[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=)?$",
57
- )
58
-
59
-
60
  class PredictionResponse(pydantic.BaseModel):
61
  """Response model for synthetic image classification results.
62
 
@@ -79,7 +101,7 @@ class PredictionResponse(pydantic.BaseModel):
79
  max_length=4,
80
  )
81
 
82
- localizationMask: Optional[LocalizationMask] = pydantic.Field(
83
  description="A bit mask localizing predicted edits. Models that are"
84
  " not capable of localization may omit this field. It may also be"
85
  " omitted if the predicted label is ``0`` or ``1``, in which case the"
 
1
  """
2
  Pydantic models for request/response validation.
3
  """
4
+
5
+ from __future__ import annotations
6
+
7
+ import base64
8
  import enum
9
+ import io
10
+ import typing
11
+ from typing import Literal, Optional
12
 
13
+ import numpy as np
14
  import pydantic
15
+ from PIL import Image
16
 
17
+ if typing.TYPE_CHECKING:
18
+ from numpy.typing import NDArray
19
 
20
 
21
  class ImageData(pydantic.BaseModel):
22
  """Image data model for base64 encoded images."""
23
+ mediaType: str = pydantic.Field(
24
+ description="The IETF Media Type (MIME type) of the data"
25
+ )
26
+ data: str = pydantic.Field(
27
+ description="A base64 string encoding of the data.",
28
+ # Canonical base64 encoding
29
+ # https://stackoverflow.com/a/64467300/3709935
30
+ pattern=r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/][AQgw]==|[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=)?$",
31
+ )
32
+
33
+
34
+ class BinaryMask(pydantic.BaseModel):
35
+ """A bit mask indicating which pixels are manipulated / synthesized. A
36
+ pixel value of ``0`` means "no detection", and a value of ``1`` means
37
+ "detection".
38
+
39
+ The mask data must be encoded in PNG format, so that typical mask data is
40
+ compressed effectively. The PNG encoding **should** use "bilevel" mode for
41
+ maximum compactness. You can use the ``BinaryMask.from_numpy()``
42
+ function to convert a 0-1 numpy array to a BinaryMask.
43
+ """
44
+ mediaType: Literal["image/png"] = pydantic.Field(
45
+ description="The IETF Media Type (MIME type) of the data."
46
+ )
47
+ data: str = pydantic.Field(
48
+ description="A base64 string encoding of the data.",
49
+ # Canonical base64 encoding
50
+ # https://stackoverflow.com/a/64467300/3709935
51
+ pattern=r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/][AQgw]==|[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=)?$",
52
+ )
53
+
54
+ @staticmethod
55
+ def from_numpy(mask: NDArray[np.uint8]) -> BinaryMask:
56
+ """Convert a 0-1 numpy array to a BinaryMask.
57
+
58
+ The numpy data must be in row-major order. That means the first
59
+ dimension corresponds to **height** and the second dimension corresponds
60
+ to **width**.
61
+ """
62
+ # Convert to "L" (grayscale) then "1" (bilevel) for compact binary representation
63
+ mask_img = Image.fromarray(mask * 255, mode="L").convert("1", dither=None)
64
+ mask_img_buffer = io.BytesIO()
65
+ mask_img.save(mask_img_buffer, format="png")
66
+ mask_data = base64.b64encode(mask_img_buffer.getbuffer()).decode("utf-8")
67
+ return BinaryMask(mediaType="image/png", data=mask_data)
68
 
69
 
70
  class ImageRequest(pydantic.BaseModel):
 
79
  LocallySynthesized = 3
80
 
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  class PredictionResponse(pydantic.BaseModel):
83
  """Response model for synthetic image classification results.
84
 
 
101
  max_length=4,
102
  )
103
 
104
+ localizationMask: Optional[BinaryMask] = pydantic.Field(
105
  description="A bit mask localizing predicted edits. Models that are"
106
  " not capable of localization may omit this field. It may also be"
107
  " omitted if the predicted label is ``0`` or ``1``, in which case the"
app/services/inference.py CHANGED
@@ -5,13 +5,14 @@ import os
5
  import random
6
  from io import BytesIO
7
 
 
8
  import torch
9
  from PIL import Image
10
  from transformers import AutoImageProcessor, ResNetForImageClassification # type: ignore[import-untyped]
11
 
12
  from app.core.logging import logger
13
  from app.services.base import InferenceService
14
- from app.api.models import ImageRequest, Labels, LocalizationMask, PredictionResponse
15
 
16
 
17
  class ResNetInferenceService(InferenceService[ImageRequest, PredictionResponse]):
@@ -70,17 +71,22 @@ class ResNetInferenceService(InferenceService[ImageRequest, PredictionResponse])
70
  with torch.no_grad():
71
  logits = self.model(**inputs).logits.squeeze() # pyright: ignore
72
 
73
- # Convert to expected output format. This is for demonstration purposes
 
74
  # and obviously will not perform well on the actual task.
75
  logprobs = torch.nn.functional.log_softmax(logits[:len(Labels)]).tolist()
76
- mask_bytes = random.randbytes((width*height + 7) // 8)
77
- mask_bits = base64.b64encode(mask_bytes).decode("utf-8")
 
 
 
 
 
 
78
 
79
  return PredictionResponse(
80
  logprobs=logprobs,
81
- localizationMask=LocalizationMask(
82
- width=width, height=height, bitsRowMajor=mask_bits
83
- )
84
  )
85
 
86
  @property
 
5
  import random
6
  from io import BytesIO
7
 
8
+ import numpy as np
9
  import torch
10
  from PIL import Image
11
  from transformers import AutoImageProcessor, ResNetForImageClassification # type: ignore[import-untyped]
12
 
13
  from app.core.logging import logger
14
  from app.services.base import InferenceService
15
+ from app.api.models import BinaryMask, ImageRequest, Labels, PredictionResponse
16
 
17
 
18
  class ResNetInferenceService(InferenceService[ImageRequest, PredictionResponse]):
 
71
  with torch.no_grad():
72
  logits = self.model(**inputs).logits.squeeze() # pyright: ignore
73
 
74
+ # Convert the ImageNet output vector of dimension 1000 to the expected
75
+ # output format. This is for demonstration purposes
76
  # and obviously will not perform well on the actual task.
77
  logprobs = torch.nn.functional.log_softmax(logits[:len(Labels)]).tolist()
78
+
79
+ # Dummy localization mask: a rectangle approximately in the middle
80
+ x = image.width // 3
81
+ y = image.height // 3
82
+ # Row-major order
83
+ mask = np.zeros((image.height, image.width), dtype=np.uint8)
84
+ mask[y:(2*y), x:(2*x)] = 1
85
+ mask_obj = BinaryMask.from_numpy(mask)
86
 
87
  return PredictionResponse(
88
  logprobs=logprobs,
89
+ localizationMask=mask_obj,
 
 
90
  )
91
 
92
  @property
makefile CHANGED
@@ -1,15 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
1
  .PHONY: download
2
  download:
3
  bash scripts/model_download.bash
4
 
5
- .PHONY: build
6
- build:
7
- docker build -t safe-challenge-2025/example-submission:latest .
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- .PHONY: run
10
- run:
11
- docker run -d --name example-submission -p 8000:8000 safe-challenge-2025/example-submission:latest
 
 
12
 
13
- .PHONY: stop
14
- stop:
15
- docker stop example-submission && docker rm example-submission
 
1
+ VENV ?= venv
2
+ PYTHON ?= $(VENV)/bin/python3
3
+ UVICORN ?= $(VENV)/bin/uvicorn
4
+ DOCKER ?= docker
5
+ IMAGE ?= safe-challenge-2025/example-submission
6
+ GID ?= $(shell id -g)
7
+ UID ?= $(shell id -u)
8
+
9
+ .PHONY: setup
10
+ setup: $(VENV)/requirements.cpu.txt
11
+
12
  .PHONY: download
13
  download:
14
  bash scripts/model_download.bash
15
 
16
+ .PHONY: serve
17
+ serve:
18
+ $(UVICORN) main:app
19
+
20
+ .PHONY: docker-build
21
+ docker-build:
22
+ docker build -t $(IMAGE) .
23
+
24
+ .PHONY: docker-run
25
+ docker-run:
26
+ docker run --rm -it -p 8000:8000 $(IMAGE)
27
+
28
+ .PHONY: compile
29
+ compile:
30
+ uv pip compile --python-version 3.12 --upgrade -o requirements.torch.cpu.txt.tmp requirements.torch.cpu.in
31
+ echo "--index-url https://download.pytorch.org/whl/cpu" > requirements.torch.cpu.txt
32
+ grep -e '^torch' requirements.torch.cpu.txt.tmp >> requirements.torch.cpu.txt
33
+ uv pip compile --python-version 3.12 --upgrade -o requirements.cpu.txt requirements.cpu.in
34
+
35
+ requirements.cpu.txt: requirements.in requirements.torch.cpu.txt | $(VENV)
36
+ uv pip compile --python-version 3.12 --upgrade -o requirements.cpu.txt requirements.cpu.in
37
+
38
+ requirements.torch.cpu.txt: requirements.torch.cpu.in | $(VENV)
39
+ uv pip compile --python-version 3.12 --upgrade -o requirements.torch.cpu.txt.tmp requirements.torch.cpu.in
40
+ echo "--index-url https://download.pytorch.org/whl/cpu" > requirements.torch.cpu.txt
41
+ cat requirements.torch.cpu.txt.tmp | grep '^torch' >> requirements.torch.cpu.txt
42
 
43
+ $(VENV)/requirements.cpu.txt: requirements.cpu.txt | $(VENV)
44
+ VIRTUAL_ENV=$(VENV) uv pip install -r requirements.cpu.txt
45
+ VIRTUAL_ENV=$(VENV) uv pip install -r requirements.torch.cpu.txt
46
+ cp -f requirements.cpu.txt $(VENV)/requirements.cpu.txt
47
+ cp -f requirements.torch.cpu.txt $(VENV)/requirements.torch.cpu.txt
48
 
49
+ $(VENV):
50
+ uv venv $(VENV)
 
requirements.cpu.in ADDED
@@ -0,0 +1 @@
 
 
1
+ -r requirements.in
requirements.cpu.txt ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv pip compile --python-version 3.12 -o requirements.cpu.txt requirements.cpu.in
3
+ absl-py==2.3.1
4
+ # via dyff-client
5
+ annotated-doc==0.0.4
6
+ # via fastapi
7
+ annotated-types==0.7.0
8
+ # via pydantic
9
+ anyio==4.11.0
10
+ # via
11
+ # httpx
12
+ # jupyter-server
13
+ # starlette
14
+ # watchfiles
15
+ argon2-cffi==25.1.0
16
+ # via jupyter-server
17
+ argon2-cffi-bindings==25.1.0
18
+ # via argon2-cffi
19
+ arrow==1.4.0
20
+ # via isoduration
21
+ asttokens==3.0.1
22
+ # via stack-data
23
+ async-lru==2.0.5
24
+ # via jupyterlab
25
+ attrs==25.4.0
26
+ # via
27
+ # jsonschema
28
+ # referencing
29
+ azure-core==1.36.0
30
+ # via dyff-client
31
+ babel==2.17.0
32
+ # via jupyterlab-server
33
+ beautifulsoup4==4.14.2
34
+ # via
35
+ # dyff-audit
36
+ # nbconvert
37
+ bleach==6.3.0
38
+ # via nbconvert
39
+ canonicaljson==2.0.0
40
+ # via dyff-schema
41
+ certifi==2025.11.12
42
+ # via
43
+ # httpcore
44
+ # httpx
45
+ # requests
46
+ cffi==2.0.0
47
+ # via argon2-cffi-bindings
48
+ charset-normalizer==3.4.4
49
+ # via requests
50
+ click==8.3.1
51
+ # via
52
+ # -r requirements.in
53
+ # uvicorn
54
+ comm==0.2.3
55
+ # via ipykernel
56
+ debugpy==1.8.17
57
+ # via ipykernel
58
+ decorator==5.2.1
59
+ # via ipython
60
+ defusedxml==0.7.1
61
+ # via nbconvert
62
+ dnspython==2.8.0
63
+ # via email-validator
64
+ dyff-audit==0.16.1
65
+ # via -r requirements.in
66
+ dyff-client==0.23.5
67
+ # via
68
+ # -r requirements.in
69
+ # dyff-audit
70
+ dyff-schema==0.39.1
71
+ # via
72
+ # -r requirements.in
73
+ # dyff-audit
74
+ # dyff-client
75
+ email-validator==2.3.0
76
+ # via dyff-schema
77
+ executing==2.2.1
78
+ # via stack-data
79
+ fastapi==0.121.2
80
+ # via -r requirements.in
81
+ fastjsonschema==2.21.2
82
+ # via nbformat
83
+ filelock==3.20.0
84
+ # via
85
+ # huggingface-hub
86
+ # transformers
87
+ fqdn==1.5.1
88
+ # via jsonschema
89
+ fsspec==2025.10.0
90
+ # via huggingface-hub
91
+ google-i18n-address==3.1.1
92
+ # via dyff-schema
93
+ h11==0.16.0
94
+ # via
95
+ # httpcore
96
+ # uvicorn
97
+ hf-xet==1.2.0
98
+ # via huggingface-hub
99
+ httpcore==1.0.9
100
+ # via httpx
101
+ httptools==0.7.1
102
+ # via uvicorn
103
+ httpx==0.28.1
104
+ # via
105
+ # dyff-client
106
+ # jupyterlab
107
+ huggingface-hub==0.36.0
108
+ # via
109
+ # tokenizers
110
+ # transformers
111
+ hypothesis==6.148.1
112
+ # via
113
+ # dyff-schema
114
+ # hypothesis-jsonschema
115
+ hypothesis-jsonschema==0.23.1
116
+ # via dyff-schema
117
+ idna==3.11
118
+ # via
119
+ # anyio
120
+ # email-validator
121
+ # httpx
122
+ # jsonschema
123
+ # requests
124
+ ipykernel==7.1.0
125
+ # via jupyterlab
126
+ ipython==9.7.0
127
+ # via ipykernel
128
+ ipython-pygments-lexers==1.1.1
129
+ # via ipython
130
+ isodate==0.7.2
131
+ # via dyff-client
132
+ isoduration==20.11.0
133
+ # via jsonschema
134
+ jedi==0.19.2
135
+ # via ipython
136
+ jinja2==3.1.6
137
+ # via
138
+ # jupyter-server
139
+ # jupyterlab
140
+ # jupyterlab-server
141
+ # nbconvert
142
+ json5==0.12.1
143
+ # via jupyterlab-server
144
+ jsonpath-ng==1.7.0
145
+ # via
146
+ # dyff-client
147
+ # dyff-schema
148
+ jsonpointer==3.0.0
149
+ # via jsonschema
150
+ jsonschema==4.25.1
151
+ # via
152
+ # hypothesis-jsonschema
153
+ # jupyter-events
154
+ # jupyterlab-server
155
+ # nbformat
156
+ jsonschema-specifications==2025.9.1
157
+ # via jsonschema
158
+ jupyter-client==8.6.3
159
+ # via
160
+ # ipykernel
161
+ # jupyter-server
162
+ # nbclient
163
+ jupyter-core==5.9.1
164
+ # via
165
+ # ipykernel
166
+ # jupyter-client
167
+ # jupyter-server
168
+ # jupyterlab
169
+ # nbclient
170
+ # nbconvert
171
+ # nbformat
172
+ jupyter-events==0.12.0
173
+ # via jupyter-server
174
+ jupyter-lsp==2.3.0
175
+ # via jupyterlab
176
+ jupyter-server==2.17.0
177
+ # via
178
+ # jupyter-lsp
179
+ # jupyterlab
180
+ # jupyterlab-server
181
+ # notebook
182
+ # notebook-shim
183
+ jupyter-server-terminals==0.5.3
184
+ # via jupyter-server
185
+ jupyterlab==4.4.10
186
+ # via notebook
187
+ jupyterlab-pygments==0.3.0
188
+ # via nbconvert
189
+ jupyterlab-server==2.28.0
190
+ # via
191
+ # jupyterlab
192
+ # notebook
193
+ lark==1.3.1
194
+ # via rfc3987-syntax
195
+ lxml==6.0.2
196
+ # via dyff-audit
197
+ markupsafe==3.0.3
198
+ # via
199
+ # jinja2
200
+ # nbconvert
201
+ matplotlib-inline==0.2.1
202
+ # via
203
+ # ipykernel
204
+ # ipython
205
+ mistune==3.1.4
206
+ # via nbconvert
207
+ nbclient==0.10.2
208
+ # via nbconvert
209
+ nbconvert==7.16.6
210
+ # via
211
+ # dyff-audit
212
+ # jupyter-server
213
+ nbformat==5.10.4
214
+ # via
215
+ # dyff-audit
216
+ # jupyter-server
217
+ # nbclient
218
+ # nbconvert
219
+ nest-asyncio==1.6.0
220
+ # via ipykernel
221
+ notebook==7.4.7
222
+ # via dyff-audit
223
+ notebook-shim==0.2.4
224
+ # via
225
+ # jupyterlab
226
+ # notebook
227
+ numpy==1.26.4
228
+ # via
229
+ # -r requirements.in
230
+ # dyff-audit
231
+ # dyff-client
232
+ # dyff-schema
233
+ # pandas
234
+ # transformers
235
+ packaging==25.0
236
+ # via
237
+ # huggingface-hub
238
+ # ipykernel
239
+ # jupyter-events
240
+ # jupyter-server
241
+ # jupyterlab
242
+ # jupyterlab-server
243
+ # nbconvert
244
+ # transformers
245
+ pandas==2.3.3
246
+ # via
247
+ # -r requirements.in
248
+ # dyff-audit
249
+ # dyff-client
250
+ pandocfilters==1.5.1
251
+ # via nbconvert
252
+ parso==0.8.5
253
+ # via jedi
254
+ pexpect==4.9.0
255
+ # via ipython
256
+ pillow==12.0.0
257
+ # via -r requirements.in
258
+ platformdirs==4.5.0
259
+ # via jupyter-core
260
+ ply==3.11
261
+ # via jsonpath-ng
262
+ prometheus-client==0.23.1
263
+ # via jupyter-server
264
+ prompt-toolkit==3.0.52
265
+ # via ipython
266
+ psutil==7.1.3
267
+ # via ipykernel
268
+ ptyprocess==0.7.0
269
+ # via
270
+ # pexpect
271
+ # terminado
272
+ pure-eval==0.2.3
273
+ # via stack-data
274
+ pyarrow==22.0.0
275
+ # via
276
+ # -r requirements.in
277
+ # dyff-audit
278
+ # dyff-client
279
+ # dyff-schema
280
+ pycparser==2.23
281
+ # via cffi
282
+ pydantic==2.12.4
283
+ # via
284
+ # -r requirements.in
285
+ # dyff-audit
286
+ # dyff-client
287
+ # dyff-schema
288
+ # fastapi
289
+ # pydantic-settings
290
+ pydantic-core==2.41.5
291
+ # via pydantic
292
+ pydantic-settings==2.12.0
293
+ # via -r requirements.in
294
+ pygments==2.19.2
295
+ # via
296
+ # ipython
297
+ # ipython-pygments-lexers
298
+ # nbconvert
299
+ python-dateutil==2.9.0.post0
300
+ # via
301
+ # arrow
302
+ # jupyter-client
303
+ # pandas
304
+ python-dotenv==1.2.1
305
+ # via
306
+ # -r requirements.in
307
+ # pydantic-settings
308
+ # uvicorn
309
+ python-json-logger==4.0.0
310
+ # via jupyter-events
311
+ python-multipart==0.0.20
312
+ # via -r requirements.in
313
+ pytz==2025.2
314
+ # via pandas
315
+ pyyaml==6.0.3
316
+ # via
317
+ # huggingface-hub
318
+ # jupyter-events
319
+ # transformers
320
+ # uvicorn
321
+ pyzmq==27.1.0
322
+ # via
323
+ # ipykernel
324
+ # jupyter-client
325
+ # jupyter-server
326
+ referencing==0.37.0
327
+ # via
328
+ # jsonschema
329
+ # jsonschema-specifications
330
+ # jupyter-events
331
+ regex==2025.11.3
332
+ # via transformers
333
+ requests==2.32.5
334
+ # via
335
+ # -r requirements.in
336
+ # azure-core
337
+ # google-i18n-address
338
+ # huggingface-hub
339
+ # jupyterlab-server
340
+ # transformers
341
+ rfc3339-validator==0.1.4
342
+ # via
343
+ # jsonschema
344
+ # jupyter-events
345
+ rfc3986-validator==0.1.1
346
+ # via
347
+ # jsonschema
348
+ # jupyter-events
349
+ rfc3987-syntax==1.1.0
350
+ # via jsonschema
351
+ rpds-py==0.29.0
352
+ # via
353
+ # jsonschema
354
+ # referencing
355
+ ruamel-yaml==0.18.16
356
+ # via dyff-audit
357
+ ruamel-yaml-clib==0.2.15
358
+ # via ruamel-yaml
359
+ safetensors==0.6.2
360
+ # via transformers
361
+ send2trash==1.8.3
362
+ # via jupyter-server
363
+ setuptools==80.9.0
364
+ # via jupyterlab
365
+ six==1.17.0
366
+ # via
367
+ # python-dateutil
368
+ # rfc3339-validator
369
+ sniffio==1.3.1
370
+ # via anyio
371
+ sortedcontainers==2.4.0
372
+ # via hypothesis
373
+ soupsieve==2.8
374
+ # via beautifulsoup4
375
+ stack-data==0.6.3
376
+ # via ipython
377
+ starlette==0.49.3
378
+ # via fastapi
379
+ terminado==0.18.1
380
+ # via
381
+ # jupyter-server
382
+ # jupyter-server-terminals
383
+ tinycss2==1.4.0
384
+ # via bleach
385
+ tokenizers==0.22.1
386
+ # via transformers
387
+ tornado==6.5.2
388
+ # via
389
+ # ipykernel
390
+ # jupyter-client
391
+ # jupyter-server
392
+ # jupyterlab
393
+ # notebook
394
+ # terminado
395
+ tqdm==4.67.1
396
+ # via
397
+ # dyff-client
398
+ # huggingface-hub
399
+ # transformers
400
+ traitlets==5.14.3
401
+ # via
402
+ # ipykernel
403
+ # ipython
404
+ # jupyter-client
405
+ # jupyter-core
406
+ # jupyter-events
407
+ # jupyter-server
408
+ # jupyterlab
409
+ # matplotlib-inline
410
+ # nbclient
411
+ # nbconvert
412
+ # nbformat
413
+ transformers==4.57.1
414
+ # via -r requirements.in
415
+ typing-extensions==4.15.0
416
+ # via
417
+ # anyio
418
+ # azure-core
419
+ # beautifulsoup4
420
+ # fastapi
421
+ # huggingface-hub
422
+ # pydantic
423
+ # pydantic-core
424
+ # referencing
425
+ # starlette
426
+ # typing-inspection
427
+ typing-inspection==0.4.2
428
+ # via
429
+ # pydantic
430
+ # pydantic-settings
431
+ tzdata==2025.2
432
+ # via
433
+ # arrow
434
+ # pandas
435
+ uri-template==1.3.0
436
+ # via jsonschema
437
+ urllib3==2.5.0
438
+ # via requests
439
+ uvicorn==0.38.0
440
+ # via -r requirements.in
441
+ uvloop==0.22.1
442
+ # via uvicorn
443
+ watchfiles==1.1.1
444
+ # via uvicorn
445
+ wcwidth==0.2.14
446
+ # via prompt-toolkit
447
+ webcolors==25.10.0
448
+ # via jsonschema
449
+ webencodings==0.5.1
450
+ # via
451
+ # bleach
452
+ # tinycss2
453
+ websocket-client==1.9.0
454
+ # via jupyter-server
455
+ websockets==15.0.1
456
+ # via
457
+ # dyff-client
458
+ # uvicorn
requirements.in CHANGED
@@ -1,20 +1,23 @@
1
- dyff
 
 
 
 
2
 
3
  # Web framework
4
- fastapi==0.104.1
5
- uvicorn[standard]==0.24.0
6
 
7
  # Configuration management
8
- pydantic==2.5.0
9
- pydantic-settings==2.0.3
10
- python-dotenv==0.21.0
11
 
12
  # File upload handling
13
- python-multipart==0.0.6
14
 
15
  # ML/AI dependencies (newly added)
16
  transformers>=4.35.0
17
- torch>=2.4.0 # Newer PyTorch with NumPy 2.x support
18
  pillow>=10.0.0
19
 
20
  # Dataset generation and testing
 
1
+ dyff-audit
2
+ dyff-client
3
+ dyff-schema>=0.39.1
4
+
5
+ click
6
 
7
  # Web framework
8
+ fastapi
9
+ uvicorn[standard]
10
 
11
  # Configuration management
12
+ pydantic>=2.0.0,<3.0.0
13
+ pydantic-settings
14
+ python-dotenv
15
 
16
  # File upload handling
17
+ python-multipart
18
 
19
  # ML/AI dependencies (newly added)
20
  transformers>=4.35.0
 
21
  pillow>=10.0.0
22
 
23
  # Dataset generation and testing
requirements.torch.cpu.in ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ --index-url https://download.pytorch.org/whl/cpu
2
+ torch
3
+ torchvision
requirements.torch.cpu.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ --index-url https://download.pytorch.org/whl/cpu
2
+ torch==2.9.1+cpu
3
+ torchvision==0.24.1+cpu
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
  # This file was autogenerated by uv via the following command:
2
- # uv pip compile requirements.in
3
  absl-py==2.3.1
4
  # via dyff-client
5
  annotated-types==0.7.0
@@ -17,7 +17,7 @@ argon2-cffi-bindings==25.1.0
17
  # via argon2-cffi
18
  arrow==1.4.0
19
  # via isoduration
20
- asttokens==3.0.0
21
  # via stack-data
22
  async-lru==2.0.5
23
  # via jupyterlab
@@ -46,8 +46,10 @@ cffi==2.0.0
46
  # via argon2-cffi-bindings
47
  charset-normalizer==3.4.4
48
  # via requests
49
- click==8.3.0
50
- # via uvicorn
 
 
51
  comm==0.2.3
52
  # via ipykernel
53
  debugpy==1.8.17
@@ -58,17 +60,15 @@ defusedxml==0.7.1
58
  # via nbconvert
59
  dnspython==2.8.0
60
  # via email-validator
61
- # dyff==0.36.1
62
- # via -r requirements.in
63
  dyff-audit==0.16.1
64
- # via dyff
65
  dyff-client==0.23.5
66
  # via
67
- # dyff
68
  # dyff-audit
69
  dyff-schema==0.39.1
70
  # via
71
- # dyff
72
  # dyff-audit
73
  # dyff-client
74
  email-validator==2.3.0
@@ -110,7 +110,7 @@ huggingface-hub==0.36.0
110
  # via
111
  # tokenizers
112
  # transformers
113
- hypothesis==6.147.0
114
  # via
115
  # dyff-schema
116
  # hypothesis-jsonschema
@@ -394,13 +394,13 @@ rfc3986-validator==0.1.1
394
  # jupyter-events
395
  rfc3987-syntax==1.1.0
396
  # via jsonschema
397
- rpds-py==0.28.0
398
  # via
399
  # jsonschema
400
  # referencing
401
  ruamel-yaml==0.18.16
402
  # via dyff-audit
403
- ruamel-yaml-clib==0.2.14
404
  # via ruamel-yaml
405
  safetensors==0.6.2
406
  # via transformers
 
1
  # This file was autogenerated by uv via the following command:
2
+ # uv pip compile --python-version 3.12 -o requirements.txt requirements.in
3
  absl-py==2.3.1
4
  # via dyff-client
5
  annotated-types==0.7.0
 
17
  # via argon2-cffi
18
  arrow==1.4.0
19
  # via isoduration
20
+ asttokens==3.0.1
21
  # via stack-data
22
  async-lru==2.0.5
23
  # via jupyterlab
 
46
  # via argon2-cffi-bindings
47
  charset-normalizer==3.4.4
48
  # via requests
49
+ click==8.3.1
50
+ # via
51
+ # -r requirements.in
52
+ # uvicorn
53
  comm==0.2.3
54
  # via ipykernel
55
  debugpy==1.8.17
 
60
  # via nbconvert
61
  dnspython==2.8.0
62
  # via email-validator
 
 
63
  dyff-audit==0.16.1
64
+ # via -r requirements.in
65
  dyff-client==0.23.5
66
  # via
67
+ # -r requirements.in
68
  # dyff-audit
69
  dyff-schema==0.39.1
70
  # via
71
+ # -r requirements.in
72
  # dyff-audit
73
  # dyff-client
74
  email-validator==2.3.0
 
110
  # via
111
  # tokenizers
112
  # transformers
113
+ hypothesis==6.148.1
114
  # via
115
  # dyff-schema
116
  # hypothesis-jsonschema
 
394
  # jupyter-events
395
  rfc3987-syntax==1.1.0
396
  # via jsonschema
397
+ rpds-py==0.29.0
398
  # via
399
  # jsonschema
400
  # referencing
401
  ruamel-yaml==0.18.16
402
  # via dyff-audit
403
+ ruamel-yaml-clib==0.2.15
404
  # via ruamel-yaml
405
  safetensors==0.6.2
406
  # via transformers
response.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"logprobs":[-0.859380304813385,-1.2701971530914307,-2.1918208599090576,-1.69235098361969],"localizationMask":{"mediaType":"image/png","data":"iVBORw0KGgoAAAANSUhEUgAAA8AAAAKDAQAAAAD9Fl5AAAAAu0lEQVR4nO3NsREAMAgDMWD/nZMVKEwn1T5/FQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMCl3g5f+HC24TRhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAj70gwKsTlmdBwAAAABJRU5ErkJggg=="}}
upload_model.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2025 UL Research Institutes
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import sys
5
+ import time
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from dyff.client import Client
11
+ from dyff.schema.platform import *
12
+ from dyff.schema.requests import *
13
+
14
+ from app.api.models import PredictionResponse
15
+
16
+ # ----------------------------------------------------------------------------
17
+
18
+ WORKDIR = Path(__file__).resolve().parent
19
+
20
+
21
+ @click.command()
22
+ @click.option(
23
+ "--account",
24
+ type=str,
25
+ required=True,
26
+ help="Your account ID",
27
+ )
28
+ @click.option(
29
+ "--name",
30
+ type=str,
31
+ required=True,
32
+ help="The name of your detector model. For display and querying purposes only.",
33
+ )
34
+ @click.option(
35
+ "--image",
36
+ type=str,
37
+ required=True,
38
+ help="The Docker image to upload. Must exist in your local Docker deamon.",
39
+ )
40
+ @click.option(
41
+ "--endpoint",
42
+ type=str,
43
+ default="predict",
44
+ help="The endpoint to call on your model to make a prediction.",
45
+ )
46
+ def main(account: str, name: str, image: str, endpoint: str) -> None:
47
+ dyffapi = Client()
48
+
49
+ # You can set these to a known ID to skip that step
50
+ artifact_id = None
51
+ service_id = None
52
+
53
+ # Upload the image
54
+ if artifact_id is None:
55
+ # Create an Artifact resource
56
+ artifact = dyffapi.artifacts.create(ArtifactCreateRequest(account=account))
57
+ click.echo(f"artifact_id = \"{artifact.id}\"")
58
+ time.sleep(5)
59
+ # Push the image from the local Docker daemon
60
+ dyffapi.artifacts.push(artifact, source=f"docker-daemon:{image}")
61
+ time.sleep(5)
62
+ # Indicate that we're done pushing
63
+ dyffapi.artifacts.finalize(artifact.id)
64
+ else:
65
+ artifact = dyffapi.artifacts.get(artifact_id)
66
+ assert artifact is not None
67
+
68
+ # Create a runnable InferenceService
69
+ if service_id is None:
70
+ # Don't change this
71
+ service_request = InferenceServiceCreateRequest(
72
+ account=account,
73
+ name=name,
74
+ model=None,
75
+ runner=InferenceServiceRunner(
76
+ kind=InferenceServiceRunnerKind.CONTAINER,
77
+ imageRef=EntityIdentifier.of(artifact),
78
+ resources=ModelResources(),
79
+ ),
80
+ interface=InferenceInterface(
81
+ endpoint=endpoint,
82
+ outputSchema=DataSchema.make_output_schema(PredictionResponse),
83
+ ),
84
+ )
85
+ service = dyffapi.inferenceservices.create(service_request)
86
+ click.echo(f"service_id = \"{service.id}\"")
87
+ else:
88
+ service = dyffapi.inferenceservices.get(service_id)
89
+ assert service is not None
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()