From c036b9f40841f2208be25f4993329419d50d5b17 Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Sat, 8 Apr 2023 22:27:42 -0700 Subject: [PATCH] Adds a LanguageDetector Java API. PiperOrigin-RevId: 522895455 --- .../framework/MediaPipeException.java | 5 +- .../com/google/mediapipe/tasks/text/BUILD | 28 ++ .../text/languagedetector/AndroidManifest.xml | 8 + .../languagedetector/LanguageDetector.java | 330 ++++++++++++++++++ .../LanguageDetectorResult.java | 72 ++++ .../languagedetector/LanguagePrediction.java | 38 ++ .../text/languagedetector/AndroidManifest.xml | 24 ++ .../tasks/text/languagedetector/BUILD | 19 + .../LanguageDetectorTest.java | 110 ++++++ 9 files changed, 632 insertions(+), 2 deletions(-) create mode 100644 mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/AndroidManifest.xml create mode 100644 mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguageDetector.java create mode 100644 mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguageDetectorResult.java create mode 100644 mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguagePrediction.java create mode 100644 mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/AndroidManifest.xml create mode 100644 mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/BUILD create mode 100644 mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/LanguageDetectorTest.java diff --git a/mediapipe/java/com/google/mediapipe/framework/MediaPipeException.java b/mediapipe/java/com/google/mediapipe/framework/MediaPipeException.java index 90058577..bc9cb937 100644 --- a/mediapipe/java/com/google/mediapipe/framework/MediaPipeException.java +++ b/mediapipe/java/com/google/mediapipe/framework/MediaPipeException.java @@ -40,7 +40,7 @@ public class MediaPipeException extends RuntimeException { return statusMessage; } - /** The 17 canonical status codes. */ + /** The 18 canonical status codes. */ public enum StatusCode { OK("ok"), CANCELLED("canceled"), @@ -58,7 +58,8 @@ public class MediaPipeException extends RuntimeException { INTERNAL("internal"), UNAVAILABLE("unavailable"), DATA_LOSS("data loss"), - UNAUTHENTICATED("unauthenticated"); + UNAUTHENTICATED("unauthenticated"), + IO_EXCEPTION("i/o exception"); StatusCode(String description) { this.description = description; diff --git a/mediapipe/tasks/java/com/google/mediapipe/tasks/text/BUILD b/mediapipe/tasks/java/com/google/mediapipe/tasks/text/BUILD index 69bc2ab1..74ca87b5 100644 --- a/mediapipe/tasks/java/com/google/mediapipe/tasks/text/BUILD +++ b/mediapipe/tasks/java/com/google/mediapipe/tasks/text/BUILD @@ -92,6 +92,34 @@ android_library( ], ) +android_library( + name = "languagedetector", + srcs = [ + "languagedetector/LanguageDetector.java", + "languagedetector/LanguageDetectorResult.java", + "languagedetector/LanguagePrediction.java", + ], + javacopts = [ + "-Xep:AndroidJdkLibsChecker:OFF", + ], + manifest = "textembedder/AndroidManifest.xml", + deps = [ + ":libmediapipe_tasks_text_jni_lib", + "//mediapipe/framework:calculator_options_java_proto_lite", + "//mediapipe/java/com/google/mediapipe/framework:android_framework", + "//mediapipe/tasks/cc/components/containers/proto:classifications_java_proto_lite", + "//mediapipe/tasks/cc/components/processors/proto:classifier_options_java_proto_lite", + "//mediapipe/tasks/cc/core/proto:base_options_java_proto_lite", + "//mediapipe/tasks/cc/text/text_classifier/proto:text_classifier_graph_options_java_proto_lite", + "//mediapipe/tasks/java/com/google/mediapipe/tasks/components/containers:category", + "//mediapipe/tasks/java/com/google/mediapipe/tasks/components/containers:classificationresult", + "//mediapipe/tasks/java/com/google/mediapipe/tasks/components/containers:classifications", + "//mediapipe/tasks/java/com/google/mediapipe/tasks/core", + "//third_party:autovalue", + "@maven//:com_google_guava_guava", + ], +) + load("//mediapipe/tasks/java/com/google/mediapipe/tasks:mediapipe_tasks_aar.bzl", "mediapipe_tasks_text_aar") mediapipe_tasks_text_aar( diff --git a/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/AndroidManifest.xml b/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/AndroidManifest.xml new file mode 100644 index 00000000..1c436212 --- /dev/null +++ b/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguageDetector.java b/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguageDetector.java new file mode 100644 index 00000000..216fae6d --- /dev/null +++ b/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguageDetector.java @@ -0,0 +1,330 @@ +// Copyright 2023 The MediaPipe Authors. 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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 com.google.mediapipe.tasks.text.languagedetector; + +import android.content.Context; +import android.os.ParcelFileDescriptor; +import com.google.auto.value.AutoValue; +import com.google.mediapipe.proto.CalculatorOptionsProto.CalculatorOptions; +import com.google.mediapipe.framework.MediaPipeException; +import com.google.mediapipe.framework.Packet; +import com.google.mediapipe.framework.PacketGetter; +import com.google.mediapipe.framework.ProtoUtil; +import com.google.mediapipe.tasks.components.containers.Category; +import com.google.mediapipe.tasks.components.containers.ClassificationResult; +import com.google.mediapipe.tasks.components.containers.Classifications; +import com.google.mediapipe.tasks.components.containers.proto.ClassificationsProto; +import com.google.mediapipe.tasks.components.processors.proto.ClassifierOptionsProto; +import com.google.mediapipe.tasks.core.BaseOptions; +import com.google.mediapipe.tasks.core.OutputHandler; +import com.google.mediapipe.tasks.core.TaskInfo; +import com.google.mediapipe.tasks.core.TaskOptions; +import com.google.mediapipe.tasks.core.TaskRunner; +import com.google.mediapipe.tasks.core.proto.BaseOptionsProto; +import com.google.mediapipe.tasks.text.textclassifier.proto.TextClassifierGraphOptionsProto; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Predicts the language of an input text. + * + *

This API expects a TFLite model with TFLite Model Metadata that contains + * the mandatory (described below) input tensors, output tensor, and the language codes in an + * AssociatedFile. + * + *

+ */ +public final class LanguageDetector implements AutoCloseable { + private static final String TAG = LanguageDetector.class.getSimpleName(); + private static final String TEXT_IN_STREAM_NAME = "text_in"; + + private static final List inputStreams = + Collections.unmodifiableList(Arrays.asList("TEXT:" + TEXT_IN_STREAM_NAME)); + + private static final List outputStreams = + Collections.unmodifiableList(Arrays.asList("CLASSIFICATIONS:classifications_out")); + + private static final int CLASSIFICATIONS_OUT_STREAM_INDEX = 0; + private static final String TASK_GRAPH_NAME = + "mediapipe.tasks.text.text_classifier.TextClassifierGraph"; + private final TaskRunner runner; + + static { + System.loadLibrary("mediapipe_tasks_text_jni"); + ProtoUtil.registerTypeName( + ClassificationsProto.ClassificationResult.class, + "mediapipe.tasks.components.containers.proto.ClassificationResult"); + } + + /** + * Creates a {@link LanguageDetector} instance from a model file and the default {@link + * LanguageDetectorOptions}. + * + * @param context an Android {@link Context}. + * @param modelPath path to the text model with metadata in the assets. + * @throws MediaPipeException if there is is an error during {@link LanguageDetector} creation. + */ + public static LanguageDetector createFromFile(Context context, String modelPath) { + BaseOptions baseOptions = BaseOptions.builder().setModelAssetPath(modelPath).build(); + return createFromOptions( + context, LanguageDetectorOptions.builder().setBaseOptions(baseOptions).build()); + } + + /** + * Creates a {@link LanguageDetector} instance from a model file and the default {@link + * LanguageDetectorOptions}. + * + * @param context an Android {@link Context}. + * @param modelFile the text model {@link File} instance. + * @throws IOException if an I/O error occurs when opening the tflite model file. + * @throws MediaPipeException if there is an error during {@link LanguageDetector} creation. + */ + public static LanguageDetector createFromFile(Context context, File modelFile) + throws IOException { + try (ParcelFileDescriptor descriptor = + ParcelFileDescriptor.open(modelFile, ParcelFileDescriptor.MODE_READ_ONLY)) { + BaseOptions baseOptions = + BaseOptions.builder().setModelAssetFileDescriptor(descriptor.getFd()).build(); + return createFromOptions( + context, LanguageDetectorOptions.builder().setBaseOptions(baseOptions).build()); + } + } + + /** + * Creates a {@link LanguageDetector} instance from {@link LanguageDetectorOptions}. + * + * @param context an Android {@link Context}. + * @param options a {@link LanguageDetectorOptions} instance. + * @throws MediaPipeException if there is an error during {@link LanguageDetector} creation. + */ + public static LanguageDetector createFromOptions( + Context context, LanguageDetectorOptions options) { + OutputHandler handler = new OutputHandler<>(); + handler.setOutputPacketConverter( + new OutputHandler.OutputPacketConverter() { + @Override + public LanguageDetectorResult convertToTaskResult(List packets) { + if (packets.get(CLASSIFICATIONS_OUT_STREAM_INDEX).isEmpty()) { + List emptyCategories = new ArrayList<>(); + Classifications emptyClassifications = + Classifications.create(emptyCategories, 0, Optional.empty()); + ClassificationResult classificationResult = + ClassificationResult.create( + Arrays.asList(emptyClassifications), Optional.empty()); + return LanguageDetectorResult.create( + classificationResult, + packets.get(CLASSIFICATIONS_OUT_STREAM_INDEX).getTimestamp()); + } + try { + return LanguageDetectorResult.create( + ClassificationResult.createFromProto( + PacketGetter.getProto( + packets.get(CLASSIFICATIONS_OUT_STREAM_INDEX), + ClassificationsProto.ClassificationResult.getDefaultInstance())), + packets.get(CLASSIFICATIONS_OUT_STREAM_INDEX).getTimestamp()); + } catch (IOException e) { + throw new MediaPipeException( + MediaPipeException.StatusCode.IO_EXCEPTION.ordinal(), e.getMessage()); + } catch (IllegalArgumentException e) { + throw new MediaPipeException( + MediaPipeException.StatusCode.INVALID_ARGUMENT.ordinal(), e.getMessage()); + } + } + + @Override + public Void convertToTaskInput(List packets) { + return null; + } + }); + TaskRunner runner = + TaskRunner.create( + context, + TaskInfo.builder() + .setTaskName(LanguageDetector.class.getSimpleName()) + .setTaskGraphName(TASK_GRAPH_NAME) + .setInputStreams(inputStreams) + .setOutputStreams(outputStreams) + .setTaskOptions(options) + .setEnableFlowLimiting(false) + .build(), + handler); + return new LanguageDetector(runner); + } + + /** + * Constructor to initialize a {@link LanguageDetector} from a {@link TaskRunner}. + * + * @param runner a {@link TaskRunner}. + */ + private LanguageDetector(TaskRunner runner) { + this.runner = runner; + } + + /** + * Predicts the language of the input text. + * + * @param inputText a {@link String} for processing. + */ + public LanguageDetectorResult detect(String inputText) { + Map inputPackets = new HashMap<>(); + inputPackets.put(TEXT_IN_STREAM_NAME, runner.getPacketCreator().createString(inputText)); + return (LanguageDetectorResult) runner.process(inputPackets); + } + + /** Closes and cleans up the {@link LanguageDetector}. */ + @Override + public void close() { + runner.close(); + } + + /** Options for setting up a {@link LanguageDetector}. */ + @AutoValue + public abstract static class LanguageDetectorOptions extends TaskOptions { + + /** Builder for {@link LanguageDetectorOptions}. */ + @AutoValue.Builder + public abstract static class Builder { + /** Sets the base options for the text classifier task. */ + public abstract Builder setBaseOptions(BaseOptions value); + + /** + * Sets the optional locale to use for display names specified through the TFLite Model + * Metadata, if any. + */ + public abstract Builder setDisplayNamesLocale(String locale); + + /** + * Sets the optional maximum number of top-scored classification results to return. + * + *

If not set, all available results are returned. If set, must be > 0. + */ + public abstract Builder setMaxResults(Integer maxResults); + + /** + * Sets the optional score threshold. Results with score below this value are rejected. + * + *

Overrides the score threshold specified in the TFLite Model Metadata, if any. + */ + public abstract Builder setScoreThreshold(Float scoreThreshold); + + /** + * Sets the optional allowlist of category names. + * + *

If non-empty, detection results whose category name is not in this set will be filtered + * out. Duplicate or unknown category names are ignored. Mutually exclusive with {@code + * categoryDenylist}. + */ + public abstract Builder setCategoryAllowlist(List categoryAllowlist); + + /** + * Sets the optional denylist of category names. + * + *

If non-empty, detection results whose category name is in this set will be filtered out. + * Duplicate or unknown category names are ignored. Mutually exclusive with {@code + * categoryAllowlist}. + */ + public abstract Builder setCategoryDenylist(List categoryDenylist); + + abstract LanguageDetectorOptions autoBuild(); + + /** + * Validates and builds the {@link LanguageDetectorOptions} instance. + * + * @throws IllegalArgumentException if any of the set options are invalid. + */ + public final LanguageDetectorOptions build() { + LanguageDetectorOptions options = autoBuild(); + if (options.maxResults().isPresent() && options.maxResults().get() <= 0) { + throw new IllegalArgumentException("If specified, maxResults must be > 0."); + } + if (!options.categoryAllowlist().isEmpty() && !options.categoryDenylist().isEmpty()) { + throw new IllegalArgumentException( + "Category allowlist and denylist are mutually exclusive."); + } + return options; + } + } + + abstract BaseOptions baseOptions(); + + abstract Optional displayNamesLocale(); + + abstract Optional maxResults(); + + abstract Optional scoreThreshold(); + + // For backwards-compatibility reasons in OSS we want to avoid dependencies on libraries like + // Guava, so we don't use ImmutableList. + @SuppressWarnings("AutoValueImmutableFields") + abstract List categoryAllowlist(); + + @SuppressWarnings("AutoValueImmutableFields") + abstract List categoryDenylist(); + + @SuppressWarnings("AutoValueImmutableFields") + public static Builder builder() { + return new AutoValue_LanguageDetector_LanguageDetectorOptions.Builder() + .setCategoryAllowlist(Collections.emptyList()) + .setCategoryDenylist(Collections.emptyList()); + } + + /** + * Converts a {@link LanguageDetectorOptions} to a {@link CalculatorOptions} protobuf message. + */ + @Override + public CalculatorOptions convertToCalculatorOptionsProto() { + BaseOptionsProto.BaseOptions.Builder baseOptionsBuilder = + BaseOptionsProto.BaseOptions.newBuilder(); + baseOptionsBuilder.mergeFrom(convertBaseOptionsToProto(baseOptions())); + ClassifierOptionsProto.ClassifierOptions.Builder classifierOptionsBuilder = + ClassifierOptionsProto.ClassifierOptions.newBuilder(); + displayNamesLocale().ifPresent(classifierOptionsBuilder::setDisplayNamesLocale); + maxResults().ifPresent(classifierOptionsBuilder::setMaxResults); + scoreThreshold().ifPresent(classifierOptionsBuilder::setScoreThreshold); + if (!categoryAllowlist().isEmpty()) { + classifierOptionsBuilder.addAllCategoryAllowlist(categoryAllowlist()); + } + if (!categoryDenylist().isEmpty()) { + classifierOptionsBuilder.addAllCategoryDenylist(categoryDenylist()); + } + TextClassifierGraphOptionsProto.TextClassifierGraphOptions taskOptions = + TextClassifierGraphOptionsProto.TextClassifierGraphOptions.newBuilder() + .setBaseOptions(baseOptionsBuilder) + .setClassifierOptions(classifierOptionsBuilder) + .build(); + return CalculatorOptions.newBuilder() + .setExtension(TextClassifierGraphOptionsProto.TextClassifierGraphOptions.ext, taskOptions) + .build(); + } + } +} diff --git a/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguageDetectorResult.java b/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguageDetectorResult.java new file mode 100644 index 00000000..10b8dac4 --- /dev/null +++ b/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguageDetectorResult.java @@ -0,0 +1,72 @@ +// Copyright 2023 The MediaPipe Authors. 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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 com.google.mediapipe.tasks.text.languagedetector; + +import com.google.auto.value.AutoValue; +import com.google.mediapipe.tasks.components.containers.Category; +import com.google.mediapipe.tasks.components.containers.ClassificationResult; +import com.google.mediapipe.tasks.components.containers.Classifications; +import com.google.mediapipe.tasks.components.containers.proto.ClassificationsProto; +import com.google.mediapipe.tasks.core.TaskResult; +import java.util.ArrayList; +import java.util.List; + +/** Represents the prediction results generated by {@link LanguageDetector}. */ +@AutoValue +public abstract class LanguageDetectorResult implements TaskResult { + + /** + * Creates an {@link LanguageDetectorResult} instance. + * + * @param classificationResult the {@link ClassificationResult} object containing one set of + * results per classifier head. + * @param timestampMs a timestamp for this result. + */ + static LanguageDetectorResult create( + ClassificationResult classificationResult, long timestampMs) { + if (classificationResult.classifications().size() != 1) { + throw new IllegalArgumentException( + "Expected 1 classification head, got " + classificationResult.classifications().size()); + } + Classifications classifications = classificationResult.classifications().get(0); + List languagePredictions = new ArrayList<>(); + for (Category category : classifications.categories()) { + languagePredictions.add(LanguagePrediction.create(category.categoryName(), category.score())); + } + + return new AutoValue_LanguageDetectorResult(languagePredictions, timestampMs); + } + + /** + * Creates an {@link LanguageDetectorResult} instance from a {@link + * ClassificationsProto.ClassificationResult} protobuf message. + * + * @param proto the {@link ClassificationsProto.ClassificationResult} protobuf message to convert. + * @param timestampMs a timestamp for this result. + */ + static LanguageDetectorResult createFromProto( + ClassificationsProto.ClassificationResult proto, long timestampMs) { + return create(ClassificationResult.createFromProto(proto), timestampMs); + } + + /** A list of predictions from the LanguageDetector. */ + // For backwards-compatibility reasons in OSS we want to avoid dependencies on libraries like + // Guava, so we don't use ImmutableList. + @SuppressWarnings("AutoValueImmutableFields") + public abstract List languagesAndScores(); + + @Override + public abstract long timestampMs(); +} diff --git a/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguagePrediction.java b/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguagePrediction.java new file mode 100644 index 00000000..e2e4321a --- /dev/null +++ b/mediapipe/tasks/java/com/google/mediapipe/tasks/text/languagedetector/LanguagePrediction.java @@ -0,0 +1,38 @@ +// Copyright 2023 The MediaPipe Authors. 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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 com.google.mediapipe.tasks.text.languagedetector; + +import com.google.auto.value.AutoValue; + +/** A language code and its probability. Used as part of the output of {@link LanguageDetector}. */ +@AutoValue +public abstract class LanguagePrediction { + /** + * Creates a {@link LanguageDetectorPrediction} instance. + * + * @param languageCode An i18n language / locale code, e.g. "en" for English, "uz" for Uzbek, + * "ja"-Latn for Japanese (romaji). + * @param probability The probability for the prediction. + */ + public static LanguagePrediction create(String languageCode, float probability) { + return new AutoValue_LanguagePrediction(languageCode, probability); + } + + /** The i18n language / locale code for the prediction. */ + public abstract String languageCode(); + + /** The probability for the prediction. */ + public abstract float probability(); +} diff --git a/mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/AndroidManifest.xml b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/AndroidManifest.xml new file mode 100644 index 00000000..782fe8df --- /dev/null +++ b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/BUILD b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/BUILD new file mode 100644 index 00000000..c1448676 --- /dev/null +++ b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/BUILD @@ -0,0 +1,19 @@ +# Copyright 2023 The MediaPipe Authors. 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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(default_visibility = ["//mediapipe/tasks:internal"]) + +licenses(["notice"]) + +# TODO: Enable this in OSS diff --git a/mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/LanguageDetectorTest.java b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/LanguageDetectorTest.java new file mode 100644 index 00000000..fe6d28f4 --- /dev/null +++ b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/text/languagedetector/LanguageDetectorTest.java @@ -0,0 +1,110 @@ +// Copyright 2023 The MediaPipe Authors. 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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 com.google.mediapipe.tasks.text.languagedetector; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.mediapipe.framework.MediaPipeException; +import com.google.mediapipe.tasks.core.BaseOptions; +import com.google.mediapipe.tasks.core.TestUtils; +import com.google.mediapipe.tasks.text.languagedetector.LanguageDetector.LanguageDetectorOptions; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link LanguageDetector}/ */ +@RunWith(AndroidJUnit4.class) +public class LanguageDetectorTest { + private static final String MODEL_FILE = "language_detector.tflite"; + + @Test + public void options_failsWithNegativeMaxResults() throws Exception { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + LanguageDetectorOptions.builder() + .setBaseOptions(BaseOptions.builder().setModelAssetPath(MODEL_FILE).build()) + .setMaxResults(-1) + .build()); + assertThat(exception).hasMessageThat().contains("If specified, maxResults must be > 0"); + } + + @Test + public void options_failsWithBothAllowlistAndDenylist() throws Exception { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + LanguageDetectorOptions.builder() + .setBaseOptions(BaseOptions.builder().setModelAssetPath(MODEL_FILE).build()) + .setCategoryAllowlist(Arrays.asList("foo")) + .setCategoryDenylist(Arrays.asList("bar")) + .build()); + assertThat(exception) + .hasMessageThat() + .contains("Category allowlist and denylist are mutually exclusive"); + } + + @Test + public void create_failsWithMissingModel() throws Exception { + String nonExistentFile = "/path/to/non/existent/file"; + MediaPipeException exception = + assertThrows( + MediaPipeException.class, + () -> + LanguageDetector.createFromFile( + ApplicationProvider.getApplicationContext(), nonExistentFile)); + assertThat(exception).hasMessageThat().contains(nonExistentFile); + } + + @Test + public void detect_succeedsWithL2CModel() throws Exception { + LanguageDetector languageDetector = + LanguageDetector.createFromFile(ApplicationProvider.getApplicationContext(), MODEL_FILE); + LanguageDetectorResult enResult = + languageDetector.detect("To be, or not to be, that is the question"); + assertThat(enResult.languagesAndScores().size()).isEqualTo(1); + assertThat(enResult.languagesAndScores().get(0)) + .isEqualTo(LanguagePrediction.create("en", 0.9998559f)); + LanguageDetectorResult frResult = + languageDetector.detect( + "Il y a beaucoup de bouches qui parlent et fort peu de têtes qui pensent."); + assertThat(frResult.languagesAndScores().size()).isEqualTo(1); + assertThat(frResult.languagesAndScores().get(0)) + .isEqualTo(LanguagePrediction.create("fr", 0.9997813f)); + LanguageDetectorResult ruResult = languageDetector.detect("это какой-то английский язык"); + assertThat(ruResult.languagesAndScores().size()).isEqualTo(1); + assertThat(ruResult.languagesAndScores().get(0)) + .isEqualTo(LanguagePrediction.create("ru", 0.9933616f)); + } + + @Test + public void detect_succeedsWithFileObject() throws Exception { + LanguageDetector languageDetector = + LanguageDetector.createFromFile( + ApplicationProvider.getApplicationContext(), + TestUtils.loadFile(ApplicationProvider.getApplicationContext(), MODEL_FILE)); + LanguageDetectorResult mixedResult = languageDetector.detect("分久必合合久必分"); + assertThat(mixedResult.languagesAndScores().size()).isEqualTo(2); + assertThat(mixedResult.languagesAndScores().get(0)) + .isEqualTo(LanguagePrediction.create("zh", 0.50542367f)); + assertThat(mixedResult.languagesAndScores().get(1)) + .isEqualTo(LanguagePrediction.create("ja", 0.4816168f)); + } +}