Repository: deepjavalibrary/djl
Pull request #3630 (2025-03-07 16:59:29)

[onnxruntime] Fixes intraOpNumThreads bug
## Description ##

Brief description of what this PR is about

Fixes https://github.com/deepjavalibrary/djl/issues/3629


Patch:
diff --git a/api/src/main/java/ai/djl/modality/cv/translator/ZeroShotImageClassificationServingTranslator.java b/api/src/main/java/ai/djl/modality/cv/translator/ZeroShotImageClassificationServingTranslator.java
new file mode 100644
index 000000000..0eb352b1a
--- /dev/null
+++ b/api/src/main/java/ai/djl/modality/cv/translator/ZeroShotImageClassificationServingTranslator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package ai.djl.modality.cv.translator;
+
+import ai.djl.modality.Classifications;
+import ai.djl.modality.Input;
+import ai.djl.modality.Output;
+import ai.djl.modality.cv.VisionLanguageInput;
+import ai.djl.ndarray.BytesSupplier;
+import ai.djl.ndarray.NDList;
+import ai.djl.translate.Batchifier;
+import ai.djl.translate.NoBatchifyTranslator;
+import ai.djl.translate.TranslateException;
+import ai.djl.translate.Translator;
+import ai.djl.translate.TranslatorContext;
+
+/**
+ * A {@link Translator} that can handle generic zero-shot-image-classification {@link Input} and
+ * {@link Output}.
+ */
+public class ZeroShotImageClassificationServingTranslator
+        implements NoBatchifyTranslator<Input, Output> {
+
+    private Translator<VisionLanguageInput, Classifications> translator;
+
+    /**
+     * Constructs a {@code ZeroShotImageClassificationTranslator} instance.
+     *
+     * @param translator a {@code Translator} processes token classification input
+     */
+    public ZeroShotImageClassificationServingTranslator(
+            Translator<VisionLanguageInput, Classifications> translator) {
+        this.translator = translator;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void prepare(TranslatorContext ctx) throws Exception {
+        translator.prepare(ctx);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public NDList processInput(TranslatorContext ctx, Input input) throws Exception {
+        if (input.getContent().isEmpty()) {
+            throw new TranslateException("Input data is empty.");
+        }
+
+        VisionLanguageInput prompt = VisionLanguageInput.parseInput(input);
+        NDList ret = translator.processInput(ctx, prompt);
+        Batchifier batchifier = translator.getBatchifier();
+        if (batchifier != null) {
+            NDList[] batch = {ret};
+            return batchifier.batchify(batch);
+        }
+        return ret;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Output processOutput(TranslatorContext ctx, NDList list) throws Exception {
+        Output output = new Output();
+        output.addProperty("Content-Type", "application/json");
+        Batchifier batchifier = translator.getBatchifier();
+        if (batchifier != null) {
+            list = batchifier.unbatchify(list)[0];
+        }
+        output.add(BytesSupplier.wrapAsJson(translator.processOutput(ctx, list)));
+        return output;
+    }
+}
diff --git a/engines/onnxruntime/onnxruntime-engine/src/main/java/ai/djl/onnxruntime/engine/OrtModel.java b/engines/onnxruntime/onnxruntime-engine/src/main/java/ai/djl/onnxruntime/engine/OrtModel.java
index a5cbaea31..ddf68396a 100644
--- a/engines/onnxruntime/onnxruntime-engine/src/main/java/ai/djl/onnxruntime/engine/OrtModel.java
+++ b/engines/onnxruntime/onnxruntime-engine/src/main/java/ai/djl/onnxruntime/engine/OrtModel.java
@@ -170,7 +170,7 @@ public class OrtModel extends BaseModel {
             ortSession.setInterOpNumThreads(Integer.parseInt(interOpNumThreads));
         }
         String intraOpNumThreads = (String) options.get("intraOpNumThreads");
-        if (interOpNumThreads != null) {
+        if (intraOpNumThreads != null) {
             ortSession.setIntraOpNumThreads(Integer.parseInt(intraOpNumThreads));
         }
         String executionMode = (String) options.get("executionMode");
diff --git a/examples/src/main/java/ai/djl/examples/inference/cv/ZeroShotImageClassification.java b/examples/src/main/java/ai/djl/examples/inference/cv/ZeroShotImageClassification.java
new file mode 100644
index 000000000..ecd308294
--- /dev/null
+++ b/examples/src/main/java/ai/djl/examples/inference/cv/ZeroShotImageClassification.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package ai.djl.examples.inference.cv;
+
+import ai.djl.Device;
+import ai.djl.ModelException;
+import ai.djl.huggingface.translator.ZeroShotImageClassificationTranslatorFactory;
+import ai.djl.inference.Predictor;
+import ai.djl.modality.Classifications;
+import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.ImageFactory;
+import ai.djl.modality.cv.VisionLanguageInput;
+import ai.djl.repository.zoo.Criteria;
+import ai.djl.repository.zoo.ZooModel;
+import ai.djl.training.util.ProgressBar;
+import ai.djl.translate.TranslateException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+public final class ZeroShotImageClassification {
+
+    private static final Logger logger = LoggerFactory.getLogger(ZeroShotImageClassification.class);
+
+    private ZeroShotImageClassification() {}
+
+    public static void main(String[] args) throws IOException, ModelException, TranslateException {
+        Classifications classifications = predict();
+        logger.info("{}", classifications);
+    }
+
+    public static Classifications predict() throws IOException, ModelException, TranslateException {
+        String url = "http://images.cocodataset.org/val2017/000000039769.jpg";
+        Image img = ImageFactory.getInstance().fromUrl(url);
+
+        VisionLanguageInput input =
+                new VisionLanguageInput(img, new String[] {"cat", "remote control"});
+
+        Criteria<VisionLanguageInput, Classifications> criteria =
+                Criteria.builder()
+                        .setTypes(VisionLanguageInput.class, Classifications.class)
+                        .optModelUrls(
+                                "djl://ai.djl.huggingface.pytorch/openai/clip-vit-large-patch14")
+                        .optEngine("PyTorch")
+                        .optDevice(Device.cpu()) // Only support CPU
+                        .optTranslatorFactory(new ZeroShotImageClassificationTranslatorFactory())
+                        .optProgress(new ProgressBar())
+                        .build();
+
+        try (ZooModel<VisionLanguageInput, Classifications> model = criteria.loadModel();
+                Predictor<VisionLanguageInput, Classifications> predictor = model.newPredictor()) {
+            return predictor.predict(input);
+        }
+    }
+}
diff --git a/examples/src/main/python/trace_clip_vit.py b/examples/src/main/python/trace_clip_vit.py
new file mode 100644
index 000000000..2831fe14c
--- /dev/null
+++ b/examples/src/main/python/trace_clip_vit.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+
+import os
+from collections import OrderedDict
+from typing import Any
+
+import requests
+import torch
+from PIL import Image
+from torch import nn
+from transformers import pipeline
+
+
+class ModelWrapper(nn.Module):
+
+    def __init__(self, model) -> None:
+        super().__init__()
+        self.model = model
+
+    def forward(
+        self,
+        input_ids: torch.Tensor,
+        attention_mask: torch.Tensor,
+        pixel_values: torch.Tensor,
+    ) -> dict[Any, torch.Tensor]:
+        output = self.model(input_ids=input_ids,
+                            attention_mask=attention_mask,
+                            pixel_values=pixel_values)
+        # filter non-Tensor
+        ret = OrderedDict()
+        for k, v in output.items():
+            if isinstance(v, torch.Tensor):
+                ret[k] = v
+        return ret
+
+
+def jit_trace():
+    model_id = "openai/clip-vit-large-patch14"
+    pipe = pipeline(model=model_id, framework="pt", device="cpu")
+
+    image_url = "http://images.cocodataset.org/val2017/000000039769.jpg"
+    image = Image.open(requests.get(image_url, stream=True).raw)
+    texts = ["cat", "remote control"]
+    hypothesis_template = "This is a photo of {}."
+    sequences = [hypothesis_template.format(x) for x in texts]
+
+    encoding = pipe.tokenizer(sequences, padding=True, return_tensors='pt')
+    input_ids = encoding["input_ids"]
+    attention_mask = encoding["attention_mask"]
+
+    image_features = pipe.image_processor(images=image, return_tensors='pt')
+    pixel_values = image_features["pixel_values"]
+
+    traced_model = torch.jit.trace(ModelWrapper(pipe.model),
+                                   (input_ids, attention_mask, pixel_values),
+                                   strict=False)
+
+    model_name = model_id.split("/")[1]
+    os.makedirs(model_name, exist_ok=True)
+
+    pipe.tokenizer.save_pretrained(model_name)
+    for path in os.listdir(model_name):
+        if path != "tokenizer.json" and path != "tokenizer_config.json":
+            os.remove(os.path.join(model_name, path))
+
+    torch.jit.save(traced_model, f"{model_name}/{model_name}.pt")
+    serving_file = os.path.join(model_name, "serving.properties")
+    arguments = {
+        "engine":
+        "PyTorch",
+        "option.modelName":
+        model_name,
+        "option.mapLocation":
+        "true",
+        "width":
+        "224",
+        "height":
+        "224",
+        "resizeShort":
+        "true",
+        "toTensor":
+        "true",
+        "centerCrop":
+        "true",
+        "normalize":
+        "0.48145466,0.4578275,0.40821073,0.26862954,0.26130258,0.27577711",
+        "translatorFactory":
+        "ai.djl.huggingface.translator.ZeroShotImageClassificationTranslatorFactory",
+    }
+
+    with open(serving_file, 'w') as f:
+        for k, v in arguments.items():
+            f.write(f"{k}={v}\n")
+
+
+if __name__ == '__main__':
+    # main()
+    jit_trace()
diff --git a/extensions/tokenizers/src/main/java/ai/djl/huggingface/translator/ZeroShotImageClassificationTranslator.java b/extensions/tokenizers/src/main/java/ai/djl/huggingface/translator/ZeroShotImageClassificationTranslator.java
new file mode 100644
index 000000000..5bdba7f19
--- /dev/null
+++ b/extensions/tokenizers/src/main/java/ai/djl/huggingface/translator/ZeroShotImageClassificationTranslator.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package ai.djl.huggingface.translator;
+
+import ai.djl.huggingface.tokenizers.Encoding;
+import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
+import ai.djl.modality.Classifications;
+import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.VisionLanguageInput;
+import ai.djl.modality.cv.translator.BaseImageTranslator;
+import ai.djl.modality.cv.translator.ImageClassificationTranslator;
+import ai.djl.ndarray.NDArray;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.translate.ArgumentsUtil;
+import ai.djl.translate.NoBatchifyTranslator;
+import ai.djl.translate.TranslateException;
+import ai.djl.translate.Translator;
+import ai.djl.translate.TranslatorContext;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/** The translator for Huggingface zero-shot-image-classification model. */
+public class ZeroShotImageClassificationTranslator
+        implements NoBatchifyTranslator<VisionLanguageInput, Classifications> {
+
+    private HuggingFaceTokenizer tokenizer;
+    private BaseImageTranslator<?> imageProcessor;
+    private boolean int32;
+
+    ZeroShotImageClassificationTranslator(
+            HuggingFaceTokenizer tokenizer, BaseImageTranslator<?> imageProcessor, boolean int32) {
+        this.tokenizer = tokenizer;
+        this.imageProcessor = imageProcessor;
+        this.int32 = int32;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public NDList processInput(TranslatorContext ctx, VisionLanguageInput input)
+            throws TranslateException {
+        NDManager manager = ctx.getNDManager();
+
+        String template = input.getHypothesisTemplate();
+        String[] candidates = input.getCandidates();
+        if (candidates == null || candidates.length == 0) {
+            throw new TranslateException("Missing candidates in input");
+        }
+        List<String> sequences = new ArrayList<>(candidates.length);
+        for (String candidate : candidates) {
+            sequences.add(applyTemplate(template, candidate));
+        }
+
+        Encoding[] encodings = tokenizer.batchEncode(sequences);
+        NDList list = Encoding.toNDList(encodings, manager, false, int32);
+        Image img = input.getImage();
+        NDList imageFeatures = imageProcessor.processInput(ctx, img);
+        NDArray array = imageFeatures.get(0).expandDims(0);
+        list.add(array);
+
+        ctx.setAttachment("candidates", candidates);
+        return list;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Classifications processOutput(TranslatorContext ctx, NDList list)
+            throws TranslateException {
+        NDArray logits = list.get("logits_per_image");
+        logits = logits.squeeze().softmax(0);
+        String[] candidates = (String[]) ctx.getAttachment("candidates");
+        List<String> classes = Arrays.asList(candidates);
+        return new Classifications(classes, logits, candidates.length);
+    }
+
+    private String applyTemplate(String template, String arg) {
+        int pos = template.indexOf("{}");
+        if (pos == -1) {
+            return template + arg;
+        }
+        int len = template.length();
+        return template.substring(0, pos) + arg + template.substring(pos + 2, len);
+    }
+
+    /**
+     * Creates a builder to build a {@code ZeroShotImageClassificationTranslator}.
+     *
+     * @param tokenizer the tokenizer
+     * @return a new builder
+     */
+    public static Builder builder(HuggingFaceTokenizer tokenizer) {
+        return new Builder(tokenizer);
+    }
+
+    /**
+     * Creates a builder to build a {@code ZeroShotImageClassificationTranslator}.
+     *
+     * @param tokenizer the tokenizer
+     * @param arguments the models' arguments
+     * @return a new builder
+     */
+    public static Builder builder(HuggingFaceTokenizer tokenizer, Map<String, ?> arguments) {
+        Builder builder = builder(tokenizer);
+        builder.configure(arguments);
+
+        return builder;
+    }
+
+    /** The builder for zero-shot classification translator. */
+    public static final class Builder {
+
+        private HuggingFaceTokenizer tokenizer;
+        private ImageTranslator.Builder builder = new ImageTranslator.Builder();
+        private boolean int32;
+
+        Builder(HuggingFaceTokenizer tokenizer) {
+            this.tokenizer = tokenizer;
+        }
+
+        /**
+         * Sets if use int32 datatype for the {@link Translator}.
+         *
+         * @param int32 true to include token types
+         * @return this builder
+         */
+        public Builder optInt32(boolean int32) {
+            this.int32 = int32;
+            return this;
+        }
+
+        /**
+         * Configures the builder with the model arguments.
+         *
+         * @param arguments the model arguments
+         */
+        public void configure(Map<String, ?> arguments) {
+            optInt32(ArgumentsUtil.booleanValue(arguments, "int32"));
+            builder.configPreProcess(arguments);
+        }
+
+        /**
+         * Builds the translator.
+         *
+         * @return the new translator
+         * @throws IOException if I/O error occurs
+         */
+        public ZeroShotImageClassificationTranslator build() throws IOException {
+            return new ZeroShotImageClassificationTranslator(tokenizer, builder.build(), int32);
+        }
+    }
+
+    private static final class ImageTranslator extends BaseImageTranslator<Classifications> {
+
+        /**
+         * Constructs an {@code ImageTranslator} with the provided builder.
+         *
+         * @param builder the data to build with
+         */
+        public ImageTranslator(BaseBuilder<?> builder) {
+            super(builder);
+        }
+
+        @Override
+        public Classifications processOutput(TranslatorContext ctx, NDList list) {
+            return null;
+        }
+
+        static class Builder extends BaseImageTranslator.BaseBuilder<Builder> {
+
+            /** {@inheritDoc} */
+            @Override
+            protected Builder self() {
+                return this;
+            }
+
+            /** {@inheritDoc} */
+            @Override
+            protected void configPreProcess(Map<String, ?> arguments) {
+                super.configPreProcess(arguments);
+            }
+
+            /**
+             * Builds the {@link ImageClassificationTranslator} with the provided data.
+             *
+             * @return an {@link ImageClassificationTranslator}
+             */
+            public ImageTranslator build() {
+                return new ImageTranslator(this);
+            }
+        }
+    }
+}
diff --git a/extensions/tokenizers/src/main/java/ai/djl/huggingface/translator/ZeroShotImageClassificationTranslatorFactory.java b/extensions/tokenizers/src/main/java/ai/djl/huggingface/translator/ZeroShotImageClassificationTranslatorFactory.java
new file mode 100644
index 000000000..aadfe1b3e
--- /dev/null
+++ b/extensions/tokenizers/src/main/java/ai/djl/huggingface/translator/ZeroShotImageClassificationTranslatorFactory.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package ai.djl.huggingface.translator;
+
+import ai.djl.Model;
+import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
+import ai.djl.modality.Classifications;
+import ai.djl.modality.Input;
+import ai.djl.modality.Output;
+import ai.djl.modality.cv.VisionLanguageInput;
+import ai.djl.modality.cv.translator.ZeroShotImageClassificationServingTranslator;
+import ai.djl.translate.TranslateException;
+import ai.djl.translate.Translator;
+import ai.djl.translate.TranslatorFactory;
+import ai.djl.util.Pair;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Type;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A {@link TranslatorFactory} that creates a {@link ZeroShotImageClassificationTranslatorFactory}
+ * instance.
+ */
+public class ZeroShotImageClassificationTranslatorFactory
+        implements TranslatorFactory, Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private static final Set<Pair<Type, Type>> SUPPORTED_TYPES = new HashSet<>();
+
+    static {
+        SUPPORTED_TYPES.add(new Pair<>(VisionLanguageInput.class, Classifications.class));
+        SUPPORTED_TYPES.add(new Pair<>(Input.class, Output.class));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<Pair<Type, Type>> getSupportedTypes() {
+        return SUPPORTED_TYPES;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @SuppressWarnings("unchecked")
+    public <I, O> Translator<I, O> newInstance(
+            Class<I> input, Class<O> output, Model model, Map<String, ?> arguments)
+            throws TranslateException {
+        Path modelPath = model.getModelPath();
+        try {
+            HuggingFaceTokenizer tokenizer =
+                    HuggingFaceTokenizer.builder(arguments)
+                            .optTokenizerPath(modelPath)
+                            .optManager(model.getNDManager())
+                            .build();
+            ZeroShotImageClassificationTranslator translator =
+                    ZeroShotImageClassificationTranslator.builder(tokenizer, arguments).build();
+            if (input == VisionLanguageInput.class && output == Classifications.class) {
+                return (Translator<I, O>) translator;
+            } else if (input == Input.class && output == Output.class) {
+                return (Translator<I, O>)
+                        new ZeroShotImageClassificationServingTranslator(translator);
+            }
+            throw new IllegalArgumentException("Unsupported input/output types.");
+        } catch (IOException e) {
+            throw new TranslateException("Failed to load tokenizer.", e);
+        }
+    }
+}
diff --git a/extensions/tokenizers/src/main/java/ai/djl/huggingface/zoo/HfModelZoo.java b/extensions/tokenizers/src/main/java/ai/djl/huggingface/zoo/HfModelZoo.java
index 7b844dd20..0a0c4cd43 100644
--- a/extensions/tokenizers/src/main/java/ai/djl/huggingface/zoo/HfModelZoo.java
+++ b/extensions/tokenizers/src/main/java/ai/djl/huggingface/zoo/HfModelZoo.java
@@ -69,6 +69,7 @@ public class HfModelZoo extends ModelZoo {
             synchronized (HfModelZoo.class) {
                 if (!initialized) {
                     Version version = new Version(Engine.getDjlVersion());
+                    addModels(CV.ZERO_SHOT_IMAGE_CLASSIFICATION, version);
                     addModels(CV.ZERO_SHOT_OBJECT_DETECTION, version);
                     addModels(NLP.FILL_MASK, version);
                     addModels(NLP.QUESTION_ANSWER, version);


Test patch:
diff --git a/extensions/tokenizers/src/test/java/ai/djl/huggingface/tokenizers/ZeroShotImageClassificationTranslatorTest.java b/extensions/tokenizers/src/test/java/ai/djl/huggingface/tokenizers/ZeroShotImageClassificationTranslatorTest.java
new file mode 100644
index 000000000..e6c62a49b
--- /dev/null
+++ b/extensions/tokenizers/src/test/java/ai/djl/huggingface/tokenizers/ZeroShotImageClassificationTranslatorTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package ai.djl.huggingface.tokenizers;
+
+import ai.djl.Model;
+import ai.djl.ModelException;
+import ai.djl.huggingface.translator.ZeroShotImageClassificationTranslatorFactory;
+import ai.djl.inference.Predictor;
+import ai.djl.modality.Classifications;
+import ai.djl.modality.Input;
+import ai.djl.modality.Output;
+import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.ImageFactory;
+import ai.djl.modality.cv.VisionLanguageInput;
+import ai.djl.ndarray.NDArray;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.ndarray.types.Shape;
+import ai.djl.nn.Block;
+import ai.djl.nn.LambdaBlock;
+import ai.djl.repository.zoo.Criteria;
+import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.Assertions;
+import ai.djl.translate.TranslateException;
+import ai.djl.util.JsonUtils;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class ZeroShotImageClassificationTranslatorTest {
+
+    @Test
+    public void testZeroShotImageClassificationTranslator()
+            throws ModelException, IOException, TranslateException {
+        Path file = Paths.get("../../examples/src/test/resources/kitten.jpg");
+        Image img = ImageFactory.getInstance().fromFile(file);
+        VisionLanguageInput textInput =
+                new VisionLanguageInput(img, new String[] {"a cat", "a remote control"});
+
+        Block block =
+                new LambdaBlock(
+                        a -> {
+                            float[] data = {18.4394f, 18.908f};
+                            NDManager manager = a.getManager();
+                            NDArray logits = manager.create(data, new Shape(1, 2));
+                            logits.setName("logits_per_image");
+                            return new NDList(logits);
+                        },
+                        "model");
+
+        Path modelDir = Paths.get("build/model");
+        Files.createDirectories(modelDir);
+
+        Criteria<VisionLanguageInput, Classifications> criteria =
+                Criteria.builder()
+                        .setTypes(VisionLanguageInput.class, Classifications.class)
+                        .optModelPath(modelDir)
+                        .optBlock(block)
+                        .optEngine("PyTorch")
+                        .optArgument("width", 224)
+                        .optArgument("height", 224)
+                        .optArgument("resizeShort", "true")
+                        .optArgument("centerCrop", "true")
+                        .optArgument("toTensor", "true")
+                        .optArgument("tokenizer", "bert-base-uncased")
+                        .optOption("hasParameter", "false")
+                        .optTranslatorFactory(new ZeroShotImageClassificationTranslatorFactory())
+                        .build();
+
+        try (ZooModel<VisionLanguageInput, Classifications> model = criteria.loadModel();
+                Predictor<VisionLanguageInput, Classifications> predictor = model.newPredictor()) {
+            Classifications.Classification res = predictor.predict(textInput).best();
+            Assertions.assertAlmostEquals(res.getProbability(), 0.61505);
+            Assert.assertEquals(res.getClassName(), "a remote control");
+        }
+
+        Criteria<Input, Output> criteria2 =
+                Criteria.builder()
+                        .setTypes(Input.class, Output.class)
+                        .optModelPath(modelDir)
+                        .optBlock(block)
+                        .optEngine("PyTorch")
+                        .optArgument("tokenizer", "bert-base-uncased")
+                        .optOption("hasParameter", "false")
+                        .optTranslatorFactory(new ZeroShotImageClassificationTranslatorFactory())
+                        .build();
+
+        try (ZooModel<Input, Output> model = criteria2.loadModel();
+                Predictor<Input, Output> predictor = model.newPredictor()) {
+            Input input = new Input();
+            Map<String, Object> map = new ConcurrentHashMap<>();
+            map.put("image_url", file.toUri().toURL());
+            map.put("candidate_labels", textInput.getCandidates());
+            input.add(JsonUtils.toJson(map));
+            Output out = predictor.predict(input);
+            Classifications res = (Classifications) out.getData().getAsObject();
+            Assert.assertEquals(res.items().size(), 2);
+            Classifications.Classification classification = res.best();
+            Assertions.assertAlmostEquals(classification.getProbability(), 0.61505);
+            Assert.assertEquals(classification.getClassName(), "a remote control");
+
+            map.remove("candidate_labels");
+            Input input1 = new Input();
+            input1.add(JsonUtils.toJson(map));
+            Assert.assertThrows(TranslateException.class, () -> predictor.predict(input1));
+        }
+
+        try (Model model = Model.newInstance("test")) {
+            model.setBlock(block);
+            Map<String, String> options = new HashMap<>();
+            options.put("hasParameter", "false");
+            model.load(modelDir, "test", options);
+
+            ZeroShotImageClassificationTranslatorFactory factory =
+                    new ZeroShotImageClassificationTranslatorFactory();
+            Map<String, String> arguments = new HashMap<>();
+
+            Assert.assertThrows(
+                    TranslateException.class,
+                    () -> factory.newInstance(String.class, Integer.class, model, arguments));
+
+            arguments.put("tokenizer", "bert-base-uncased");
+
+            Assert.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> factory.newInstance(String.class, Integer.class, model, arguments));
+        }
+    }
+}
diff --git a/extensions/tokenizers/src/test/java/ai/djl/huggingface/tokenizers/ZeroShotObjectDetectionTranslatorTest.java b/extensions/tokenizers/src/test/java/ai/djl/huggingface/tokenizers/ZeroShotObjectDetectionTranslatorTest.java
index 23c1acbe2..0ee22875e 100644
--- a/extensions/tokenizers/src/test/java/ai/djl/huggingface/tokenizers/ZeroShotObjectDetectionTranslatorTest.java
+++ b/extensions/tokenizers/src/test/java/ai/djl/huggingface/tokenizers/ZeroShotObjectDetectionTranslatorTest.java
@@ -51,8 +51,8 @@ public class ZeroShotObjectDetectionTranslatorTest {
     @Test
     public void testZeroShotObjectDetectionTranslator()
             throws ModelException, IOException, TranslateException {
-        String url = "http://images.cocodataset.org/val2017/000000039769.jpg";
-        Image img = ImageFactory.getInstance().fromUrl(url);
+        Path file = Paths.get("../../examples/src/test/resources/kitten.jpg");
+        Image img = ImageFactory.getInstance().fromFile(file);
         VisionLanguageInput textInput = new VisionLanguageInput(img, new String[] {"a cat"});
 
         Block block =
@@ -115,7 +115,7 @@ public class ZeroShotObjectDetectionTranslatorTest {
                 Predictor<Input, Output> predictor = model.newPredictor()) {
             Input input = new Input();
             Map<String, Object> map = new ConcurrentHashMap<>();
-            map.put("image_url", url);
+            map.put("image_url", file.toUri());
             map.put("candidate_labels", textInput.getCandidates());
             input.add(JsonUtils.toJson(map));
             Output out = predictor.predict(input);