diff --git a/.flutter-mediapipe-root b/.flutter-mediapipe-root new file mode 100644 index 0000000..c4598a0 --- /dev/null +++ b/.flutter-mediapipe-root @@ -0,0 +1,2 @@ +// Used to normalize the paths of commands. +// The contents of this file do not matter. diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..b24aba0 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,38 @@ +name: Main Branch CI + +# Declare default permissions as read only. +permissions: read-all + +on: + push: + branches: [main] + pull_request: + branches: [main, ffi-wrapper, ffi-wrapper-text-pkg] + workflow_dispatch: + schedule: + - cron: "0 0 * * *" # Every day at midnight + +defaults: + run: + shell: bash + +jobs: + flutter-tests: + name: Test mediapipe_core against ${{ matrix.flutter_version }} + runs-on: ${{ matrix.os }} + # Skip running job on forks + if: github.repository == 'google/flutter-mediapipe' + strategy: + fail-fast: false + matrix: + flutter_version: [master] + # TODO(craiglabenz): reverse once CI works + # flutter_version: [stable, beta, master] + # TODO(craiglabenz): Add `ubuntu-latest` and `windows-latest` when those artifacts exist + os: [macos-latest] + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: ${{ matrix.flutter_version }} + - run: ./tool/mediapipe_ci_script_${{ matrix.flutter_version }}.sh diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..63feeef --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.associations": { + "base_options.h": "c", + "__config": "c", + "text_classifier.h": "c", + "category.h": "c" + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1af52ff --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +# Runs the utility to pull in all header files from `google/mediapipe` +headers: + cd tool/builder && dart bin/main.dart headers + +# Downloads all necessary task models +models: + cd tool/builder && dart bin/main.dart model -m textclassification + cd tool/builder && dart bin/main.dart model -m textembedding + + +# Runs `ffigen` for all packages +generate: generate_core generate_text + +# Runs `ffigen` for all packages and runs all tests +test: generate_core test_core generate_text test_text + +# Runs all tests for all packages +test_only: test_core test_text + +# Rebuilds the MediaPipe task for macOS +# Assumes google/mediapipe and google/flutter-mediapipe are siblings on the file system +compile_text_classifier_macos_arm: + cd ../mediapipe && bazel build --linkopt -s --config darwin_arm64 --strip always --define MEDIAPIPE_DISABLE_GPU=1 mediapipe/tasks/c/text/text_classifier:libtext_classifier.dylib + cd ../mediapipe && sudo cp bazel-bin/mediapipe/tasks/c/text/text_classifier/libtext_classifier.dylib ../flutter-mediapipe/packages/mediapipe-task-text/example/assets/libtext_classifier_arm64.dylib + +compile_text_classifier_macos_x86: + cd ../mediapipe && bazel build --linkopt -s --config darwin_x86_64 --strip always --define MEDIAPIPE_DISABLE_GPU=1 mediapipe/tasks/c/text/text_classifier:libtext_classifier.dylib + cd ../mediapipe && sudo cp bazel-bin/mediapipe/tasks/c/text/text_classifier/libtext_classifier.dylib ../flutter-mediapipe/packages/mediapipe-task-text/example/assets/libtext_classifier_x64.dylib + +# Runs `sdks_finder` to update manifest files +sdks: + dart tool/builder/bin/main.dart sdks + +analyze: + cd packages/mediapipe-core && dart format -o write . + cd packages/mediapipe-task-text && dart format -o write . + +# Core --- + +# Runs `ffigen` for `mediapipe_core` +generate_core: + cd packages/mediapipe-core && dart run ffigen --config=ffigen.yaml + +# Runs unit tests for `mediapipe_core` +test_core: + cd packages/mediapipe-core && dart test + +# Text --- + +# Runs `ffigen` for `mediapipe_text` +generate_text: + cd packages/mediapipe-task-text && dart --enable-experiment=native-assets run ffigen --config=ffigen.yaml + +# Runs all text tests +test_text: + cd packages/mediapipe-task-text && flutter test + cd packages/mediapipe-task-text/example && flutter test + +example_text: + cd packages/mediapipe-task-text/example && flutter run -d macos \ No newline at end of file diff --git a/README.md b/README.md index 16bbaad..817b5d8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ # Flutter-MediaPipe This repository will be home to the source code for the mediapipe_task_vision, mediapipe_task_audio, and mediapipe_task_text plugins for Flutter. + +## Releasing + +### Updating MediaPipe SDKs + +Anytime MediaPipe releases new versions of their SDKs, this package will need to be updated to incorporate those latest builds. SDK versions are pinned in the `sdk_downloads.json` files in each package, which are updated by running the following command from the root of the repository: + +``` +$ make sdks +``` + +The Google Cloud Storage bucket in question only gives read-list access to a specific list of Googlers' accounts, so this command must be run from such a Googler's corp machines. + +After this, create and merge a PR with the changes and then proceed to `Releasing to pub.dev`. + +### Releasing to pub.dev + +TODO \ No newline at end of file diff --git a/packages/analysis_options.yaml b/packages/analysis_options.yaml new file mode 100644 index 0000000..c4d43fe --- /dev/null +++ b/packages/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:lints/recommended.yaml + +linter: + rules: + - public_member_api_docs # see https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#documentation-dartdocs-javadocs-etc + diff --git a/packages/mediapipe-core/.gitignore b/packages/mediapipe-core/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/packages/mediapipe-core/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/mediapipe-core/CHANGELOG.md b/packages/mediapipe-core/CHANGELOG.md new file mode 100644 index 0000000..b78d64c --- /dev/null +++ b/packages/mediapipe-core/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +- Initial version. diff --git a/packages/mediapipe-core/README.md b/packages/mediapipe-core/README.md new file mode 100644 index 0000000..ccdb17c --- /dev/null +++ b/packages/mediapipe-core/README.md @@ -0,0 +1,27 @@ +# MediaPipe Core for Flutter + +![pub package](https://img.shields.io/pub/v/mediapipe_core) + +A Flutter plugin to use the MediaPipe Core API, which enables multiple Mediapipe tasks. + +To learn more about MediaPipe, please visit the [MediaPipe website](https://developers.google.com/mediapipe) + +## Getting Started + +To get started with MediaPipe, please [see the documentation](https://developers.google.com/mediapipe/solutions/guide). + + + +## Issues and feedback + +Please file Flutter-MediaPipe specific issues, bugs, or feature requests in our [issue tracker](https://github.com/google/flutter-mediapipe/issues/new). + +Issues that are specific to Flutter can be filed in the [Flutter issue tracker](https://github.com/flutter/flutter/issues/new). + +To contribute a change to this plugin, +please review our [contribution guide](https://github.com/google/flutter-mediapipe/blob/master/CONTRIBUTING.md) +and open a [pull request](https://github.com/google/flutter-mediapipe/pulls). diff --git a/packages/mediapipe-core/analysis_options.yaml b/packages/mediapipe-core/analysis_options.yaml new file mode 100644 index 0000000..bbeec6f --- /dev/null +++ b/packages/mediapipe-core/analysis_options.yaml @@ -0,0 +1,9 @@ +include: ../analysis_options.yaml + +linter: + rules: + - public_member_api_docs # see https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#documentation-dartdocs-javadocs-etc + +analyzer: + exclude: + - "**/mediapipe_common_bindings.dart" diff --git a/packages/mediapipe-core/ffigen.yaml b/packages/mediapipe-core/ffigen.yaml new file mode 100644 index 0000000..78f89e0 --- /dev/null +++ b/packages/mediapipe-core/ffigen.yaml @@ -0,0 +1,26 @@ +name: "MediaPipeCommonBindings" +description: "Bindings for shared MediaPipe structs common across many tasks" +output: + bindings: "lib/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart" + symbol-file: + output: "package:mediapipe_core/generated/core_symbols.yaml" + import-path: "package:mediapipe_core/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart" +headers: + entry-points: + - "third_party/mediapipe/tasks/c/**.h" +preamble: | + /* Copyright 2023 The MediaPipe Authors. + + 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. + ==============================================================================*/ +ffi-native: diff --git a/packages/mediapipe-core/lib/generated/core_symbols.yaml b/packages/mediapipe-core/lib/generated/core_symbols.yaml new file mode 100644 index 0000000..19786c8 --- /dev/null +++ b/packages/mediapipe-core/lib/generated/core_symbols.yaml @@ -0,0 +1,46 @@ +format_version: 1.0.0 +files: + package:mediapipe_core/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart: + used-config: + ffi-native: false + symbols: + c:@S@BaseOptions: + name: BaseOptions + c:@S@Categories: + name: Categories + c:@S@Category: + name: Category + c:@S@ClassificationResult: + name: ClassificationResult + c:@S@Classifications: + name: Classifications + c:@S@ClassifierOptions: + name: ClassifierOptions + c:@S@EmbedderOptions: + name: EmbedderOptions + c:@S@Embedding: + name: Embedding + c:@S@EmbeddingResult: + name: EmbeddingResult + c:@S@__darwin_pthread_handler_rec: + name: __darwin_pthread_handler_rec + c:@S@_opaque_pthread_attr_t: + name: _opaque_pthread_attr_t + c:@S@_opaque_pthread_cond_t: + name: _opaque_pthread_cond_t + c:@S@_opaque_pthread_condattr_t: + name: _opaque_pthread_condattr_t + c:@S@_opaque_pthread_mutex_t: + name: _opaque_pthread_mutex_t + c:@S@_opaque_pthread_mutexattr_t: + name: _opaque_pthread_mutexattr_t + c:@S@_opaque_pthread_once_t: + name: _opaque_pthread_once_t + c:@S@_opaque_pthread_rwlock_t: + name: _opaque_pthread_rwlock_t + c:@S@_opaque_pthread_rwlockattr_t: + name: _opaque_pthread_rwlockattr_t + c:@S@_opaque_pthread_t: + name: _opaque_pthread_t + c:@UA@__mbstate_t: + name: __mbstate_t diff --git a/packages/mediapipe-core/lib/interface.dart b/packages/mediapipe-core/lib/interface.dart new file mode 100644 index 0000000..544bbeb --- /dev/null +++ b/packages/mediapipe-core/lib/interface.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/interface/interface.dart'; diff --git a/packages/mediapipe-core/lib/io.dart b/packages/mediapipe-core/lib/io.dart new file mode 100644 index 0000000..62f4b5b --- /dev/null +++ b/packages/mediapipe-core/lib/io.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/interface/containers.dart' show EmbeddingType; +export 'src/io/mediapipe_core.dart'; diff --git a/packages/mediapipe-core/lib/mediapipe_core.dart b/packages/mediapipe-core/lib/mediapipe_core.dart new file mode 100644 index 0000000..9864b8d --- /dev/null +++ b/packages/mediapipe-core/lib/mediapipe_core.dart @@ -0,0 +1,13 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Package containing core dependencies for MediaPipe's text, vision, and +/// audio-based tasks. +library mediapipe_core; + +export 'src/extensions.dart'; +export 'src/interface/containers.dart' show EmbeddingType; +export 'universal_mediapipe_core.dart' + if (dart.library.html) 'src/web/mediapipe_core.dart' + if (dart.library.io) 'src/io/mediapipe_core.dart'; diff --git a/packages/mediapipe-core/lib/src/extensions.dart b/packages/mediapipe-core/lib/src/extensions.dart new file mode 100644 index 0000000..3d100eb --- /dev/null +++ b/packages/mediapipe-core/lib/src/extensions.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Shortens long strings for logging, printing, etc. +extension DebuggableString on String { + /// Shortens the string to its first X characters, replacing the rest with an + /// ellipses and the total length. If the string is equal to or shorter than + /// the given length, then [shorten] is a no-op. + String shorten([int x = 10]) { + if (length <= x) return this; + return '${substring(0, x)}...[$length]'; + } +} diff --git a/packages/mediapipe-core/lib/src/interface/containers.dart b/packages/mediapipe-core/lib/src/interface/containers.dart new file mode 100644 index 0000000..fc434e4 --- /dev/null +++ b/packages/mediapipe-core/lib/src/interface/containers.dart @@ -0,0 +1,146 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:equatable/equatable.dart'; + +/// {@template Category} +/// Dart representation of MediaPipe's "Category" concept. +/// +/// Category is a util class that contains a [categoryName], its [displayName], +/// a float value as [score], and the [index] of the label in the corresponding +/// label file. It is typically used as result of classification or detection +/// tasks. +/// +/// See more: +/// * [MediaPipe's Category documentation](https://developers.google.com/mediapipe/api/solutions/java/com/google/mediapipe/tasks/components/containers/Category) +/// {@endtemplate} +abstract class BaseCategory extends Equatable { + /// The index of the label in the corresponding label file. + int get index; + + /// The probability score of this label category. + double get score; + + /// The label of this category object. + String? get categoryName; + + /// The display name of the label, which may be translated for different locales. + String? get displayName; + + @override + String toString() => 'Category(index=$index, score=$score, ' + 'categoryName=$categoryName, displayName=$displayName)'; + + @override + List get props => [index, score, categoryName, displayName]; +} + +/// {@template Classifications} +/// Dart representation of MediaPipe's "Classifications" concept. +/// +/// Represents the list of classifications for a given classifier head. +/// Typically used as a result for classification tasks. +/// +/// See also: +/// * [MediaPipe's Classifications documentation](https://developers.google.com/mediapipe/api/solutions/java/com/google/mediapipe/tasks/components/containers/Classifications) +/// {@endtemplate} +abstract class BaseClassifications extends Equatable { + /// A list of [Category] objects which contain the actual classification + /// information, including human-readable labels and probability scores. + Iterable get categories; + + /// The index of the classifier head these entries refer to. + int get headIndex; + + /// The optional name of the classifier head, which is the corresponding + /// tensor metadata name. + String? get headName; + + /// Convenience getter for the first [Category] out of the [categories] list. + BaseCategory? get firstCategory => + categories.isNotEmpty ? categories.first : null; + + @override + String toString() { + final categoryStrings = categories.map((cat) => cat.toString()).join(', '); + return 'Classification(categories=[$categoryStrings], ' + 'headIndex=$headIndex, headName=$headName)'; + } + + @override + List get props => [categories, headIndex, headName]; +} + +/// Marker for which flavor of analysis was performed for a specific +/// [Embedding] instance. +enum EmbeddingType { + /// Indicates an [Embedding] object has a non-null value for + /// [Embedding.floatEmbedding]. + float, + + /// Indicates an [Embedding] object has a non-null value for + /// [Embedding.quantizedEmbedding]. + quantized; + + /// Returns the opposite type. + EmbeddingType get opposite => switch (this) { + EmbeddingType.float => EmbeddingType.quantized, + EmbeddingType.quantized => EmbeddingType.float, + }; +} + +/// {@template Embedding} +/// Represents the embedding for a given embedder head. Typically used in +/// embedding tasks. +/// +/// One and only one of 'floatEmbedding' and 'quantizedEmbedding' will contain +/// data, based on whether or not the embedder was configured to perform scala +/// quantization. +/// {@endtemplate} +abstract class BaseEmbedding extends Equatable { + /// Length of this embedding. + int get length; + + /// The index of the embedder head to which these entries refer. + int get headIndex; + + /// The optional name of the embedder head, which is the corresponding tensor + /// metadata name. + String? get headName; + + /// Floating-point embedding. [null] if the embedder was configured to perform + /// scalar-quantization. + Float32List? get floatEmbedding; + + /// Scalar-quantized embedding. [null] if the embedder was not configured to + /// perform scalar quantization. + Uint8List? get quantizedEmbedding; + + /// [True] if this embedding came from an embedder configured to perform + /// scalar quantization. + bool get isQuantized => type == EmbeddingType.quantized; + + /// [True] if this embedding came from an embedder that was not configured to + /// perform scalar quantization. + bool get isFloat => type == EmbeddingType.float; + + /// Indicator for the type of results in this embedding. + EmbeddingType get type; + + @override + String toString() { + return 'Embedding(quantizedEmbedding=$quantizedEmbedding, floatEmbedding=' + '$floatEmbedding, headIndex=$headIndex, headName=$headName)'; + } + + @override + List get props => [ + quantizedEmbedding, + floatEmbedding, + headIndex, + headName, + ]; +} diff --git a/packages/mediapipe-core/lib/src/interface/interface.dart b/packages/mediapipe-core/lib/src/interface/interface.dart new file mode 100644 index 0000000..d442e10 --- /dev/null +++ b/packages/mediapipe-core/lib/src/interface/interface.dart @@ -0,0 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'containers.dart'; +export 'task_options.dart'; +export 'task_result.dart'; diff --git a/packages/mediapipe-core/lib/src/interface/task_options.dart b/packages/mediapipe-core/lib/src/interface/task_options.dart new file mode 100644 index 0000000..f39e5bd --- /dev/null +++ b/packages/mediapipe-core/lib/src/interface/task_options.dart @@ -0,0 +1,167 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:equatable/equatable.dart'; + +/// {@template TaskOptions} +/// Root class for options classes for MediaPipe tasks. +/// +/// Implementing classes will contain two [BaseInnerTaskOptions] subclasses, +/// including a descendent of the universal options struct, [BaseBaseOptions]. +/// The second field will be task-specific. +/// {@endtemplate} +/// +/// This implementation is not immutable to track whether `dispose` has been +/// called. All values used by pkg:equatable are in fact immutable. +// ignore: must_be_immutable +abstract class BaseTaskOptions extends Equatable { + /// {@macro TaskOptions} + BaseTaskOptions(); + + /// {@template TaskOptions.baseOptions} + /// Options class shared by all MediaPipe tasks - namely, how to find and load + /// the task-specific models at runtime. + /// {@endtemplate} + BaseBaseOptions get baseOptions; +} + +/// {@template InnerTaskOptions} +/// Parent class for building block options classes which are later grouped +/// together to comprise all relevant options for a specific MediaPipe task. +/// {@endtemplate} +abstract class BaseInnerTaskOptions extends Equatable { + /// {@macro InnerTaskOptions} + const BaseInnerTaskOptions(); +} + +/// Indicates how a [BaseOptions] instance informs surrounding code how to find +/// the MediaPipe model which will fulfill a given task. +enum BaseOptionsType { + /// Pointer to a MediaPipe model on disk. Not suitable for web. + path, + + /// Raw MediaPipe model bytes. Suitable for io or web. + memory +} + +/// {@template BaseOptions} +/// Dart representation of MediaPipe's "BaseOptions" concept. +/// +/// Used to configure various classifiers by specifying the model they will use +/// for computation. +/// +/// See also: +/// * [MediaPipe's BaseOptions documentation](https://developers.google.com/mediapipe/api/solutions/java/com/google/mediapipe/tasks/core/BaseOptions) +/// * [ClassifierOptions], which is often used in conjunction to specify a +/// classifier's desired behavior. +/// {@endtemplate} +abstract class BaseBaseOptions extends BaseInnerTaskOptions { + /// {@macro BaseOptions} + const BaseBaseOptions(); + + /// The model asset file contents as bytes; + Uint8List? get modelAssetBuffer; + + /// Path to the model asset file. + String? get modelAssetPath; + + /// Flavor of how this [BaseOptions] instance indicates where the rest of the + /// application should find the desired MediaPipe model. + BaseOptionsType get type; + + @override + List get props => [ + modelAssetBuffer, + modelAssetBuffer?.lengthInBytes, + modelAssetPath, + type, + ]; +} + +/// {@template ClassifierOptions} +/// Dart representation of MediaPipe's "ClassifierOptions" concept. +/// +/// Classifier options shared across MediaPipe classification tasks. +/// +/// See also: +/// * [MediaPipe's ClassifierOptions documentation](https://developers.google.com/mediapipe/api/solutions/java/com/google/mediapipe/tasks/components/processors/ClassifierOptions) +/// * [BaseOptions], which is often used in conjunction to specify a +/// classifier's desired behavior. +/// {@endtemplate} +abstract class BaseClassifierOptions extends BaseInnerTaskOptions { + /// {@macro ClassifierOptions} + const BaseClassifierOptions(); + + /// The locale to use for display names specified through the TFLite Model + /// Metadata. + String? get displayNamesLocale; + + /// The maximum number of top-scored classification results to return. + int? get maxResults; + + /// If set, establishes a minimum `score` and leads to the rejection of any + /// categories with lower `score` values. + double? get scoreThreshold; + + /// Allowlist of category names. + /// + /// If non-empty, classification results whose category name is not in + /// this set will be discarded. Duplicate or unknown category names + /// are ignored. Mutually exclusive with `categoryDenylist`. + List? get categoryAllowlist; + + /// Denylist of category names. + /// + /// If non-empty, classification results whose category name is in this set + /// will be discarded. Duplicate or unknown category names are ignored. + /// Mutually exclusive with `categoryAllowList`. + List? get categoryDenylist; + + @override + List get props => [ + displayNamesLocale, + maxResults, + scoreThreshold, + ...(categoryAllowlist ?? []), + ...(categoryDenylist ?? []), + ]; +} + +/// {@template EmbedderOptions} +/// Dart representation of MediaPipe's "EmbedderOptions" concept. +/// +/// Embedder options shared across MediaPipe embedding tasks. +/// +/// See also: +/// * [MediaPipe's EmbedderOptions documentation](https://developers.google.com/mediapipe/api/solutions/java/com/google/mediapipe/tasks/text/textembedder/TextEmbedder.TextEmbedderOptions) +/// * [BaseOptions], which is often used in conjunction to specify a +/// embedder's desired behavior. +/// {@endtemplate} +abstract class BaseEmbedderOptions extends BaseInnerTaskOptions { + /// {@macro EmbedderOptions} + const BaseEmbedderOptions(); + + /// Whether to normalize the returned feature vector with L2 norm. Use this + /// option only if the model does not already contain a native L2_NORMALIZATION + /// TF Lite Op. In most cases, this is already the case and L2 norm is thus + /// achieved through TF Lite inference. + /// + /// See also: + /// * [TutorialsPoint guide on L2 normalization](https://www.tutorialspoint.com/machine_learning_with_python/machine_learning_with_python_ltwo_normalization.htm) + bool get l2Normalize; + + /// Whether the returned embedding should be quantized to bytes via scalar + /// quantization. Embeddings are implicitly assumed to be unit-norm and + /// therefore any dimension is guaranteed to have a value in [-1.0, 1.0]. Use + /// the l2_normalize option if this is not the case. + /// + /// See also: + /// * [l2Normalize] + bool get quantize; + + @override + List get props => [l2Normalize, quantize]; +} diff --git a/packages/mediapipe-core/lib/src/interface/task_result.dart b/packages/mediapipe-core/lib/src/interface/task_result.dart new file mode 100644 index 0000000..9487551 --- /dev/null +++ b/packages/mediapipe-core/lib/src/interface/task_result.dart @@ -0,0 +1,98 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:mediapipe_core/interface.dart'; +import 'package:meta/meta.dart'; + +/// Anchor class for all result objects from MediaPipe tasks. +abstract class TaskResult { + /// {@template TaskResult.dispose} + /// Releases all resources for this object. + /// + /// See also: + /// * [isClosed] which tracks whether this method has been called. + /// {@endtemplate} + @mustCallSuper + void dispose() { + _isClosed = true; + } + + /// {@template TaskResult.isClosed} + /// Tracks whether this object has been properly released via `dispose`. + /// + /// See also: + /// * [dispose], whose calling should set this to `true`. + /// {@endtemplate} + bool get isClosed => _isClosed; + + /// Inner tracker for whether [dispose] has been called; + bool _isClosed = false; +} + +/// {@template ClassifierResult} +/// Container for classification results across any MediaPipe task. +/// {@endtemplate} +abstract class BaseClassifierResult extends TaskResult { + /// {@template ClassifierResult.classifications} + /// The classification results for each head of the model. + /// {@endtemplate} + Iterable get classifications; + + /// Convenience helper for the first [Classifications] object. + BaseClassifications? get firstClassification => + classifications.isNotEmpty ? classifications.first : null; + + @override + String toString() { + final classificationStrings = + classifications.map((cat) => cat.toString()).join(', '); + return '$runtimeType(classifications=[$classificationStrings])'; + } +} + +/// {@template TimestampedClassifierResult} +/// Container for classification results that may describe a slice of time +/// within a larger, streaming data source (.e.g, a video or audio file). +/// {@endtemplate} +mixin TimestampedResult on TaskResult { + /// The optional timestamp (as a [Duration]) of the start of the chunk of data + /// corresponding to these results. + /// + /// This is only used for classification on time series (e.g. audio + /// classification). In these use cases, the amount of data to process might + /// exceed the maximum size that the model can process: to solve this, the + /// input data is split into multiple chunks starting at different timestamps. + Duration? get timestamp; +} + +/// {@template EmbeddingResult} +/// Represents the embedding results of a model. Typically used as a result for +/// embedding tasks. +/// +/// This flavor of embedding result will never have a timestamp. +/// +/// See also: +/// * [TimestampedEmbeddingResult] for data which may have a timestamp. +/// +/// {@endtemplate} +abstract class BaseEmbedderResult extends TaskResult { + /// {@macro EmbeddingResult} + BaseEmbedderResult(); + + /// The embedding results for each head of the model. + Iterable get embeddings; + + @override + String toString() { + return '$runtimeType(embeddings=[...${embeddings.length} items])'; + } + + /// A [toString] variant that calls the full [toString] on each child + /// embedding. Use with caution - this can produce a long value. + String toStringVerbose() { + final embeddingStrings = + embeddings.map((emb) => emb.toString()).toList().join(', '); + return '$runtimeType(embeddings=[$embeddingStrings])'; + } +} diff --git a/packages/mediapipe-core/lib/src/io/containers.dart b/packages/mediapipe-core/lib/src/io/containers.dart new file mode 100644 index 0000000..efd4bd3 --- /dev/null +++ b/packages/mediapipe-core/lib/src/io/containers.dart @@ -0,0 +1,304 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'dart:typed_data'; +import 'package:mediapipe_core/interface.dart'; +import 'package:mediapipe_core/src/io/mediapipe_core.dart'; +import 'third_party/mediapipe/generated/mediapipe_common_bindings.dart' + as bindings; + +/// {@macro Category} +/// +/// This io-friendly implementation is not immutable strictly for memoization of +/// computed fields. All values used by pkg:equatable are in fact immutable. +// ignore: must_be_immutable +class Category extends BaseCategory { + /// {@macro Category.fake} + Category({ + required int index, + required double score, + required String? categoryName, + required String? displayName, + }) : _index = index, + _score = score, + _categoryName = categoryName, + _displayName = displayName, + _pointer = null; + + /// Instantiates a [Category] object as a wrapper around native memory. + /// + /// {@template Container.memoryManagement} + /// This memory is not owned or managed by this class, because all inner + /// container instances only exist as details within a larger "task result" + /// object. These task result objects have their own native dispose methods + /// which cascade down their full tree of structs, releasing everything + /// properly. Thus, it is not the job of this instance to ever release this + /// native memory. + /// {@endtemplate} + Category.native(this._pointer); + + final Pointer? _pointer; + + int? _index; + @override + int get index => _index ??= _getIndex(); + int _getIndex() { + if (_pointer.isNullOrNullPointer) { + throw Exception('Could not determine value for Category.index'); + } + return _pointer!.ref.index; + } + + double? _score; + @override + double get score => _score ??= _getScore(); + double _getScore() { + if (_pointer.isNullOrNullPointer) { + throw Exception('Could not determine value for Category.score'); + } + return _pointer!.ref.score; + } + + String? _categoryName; + @override + String? get categoryName => _categoryName ??= _getCategoryName(); + String? _getCategoryName() { + if (_pointer.isNullOrNullPointer) { + throw Exception('Could not determine value for Category.categoryName'); + } + return _pointer!.ref.category_name.isNotNullPointer + ? _pointer!.ref.category_name.toDartString() + : null; + } + + String? _displayName; + @override + String? get displayName => _displayName ??= _getDisplayName(); + String? _getDisplayName() { + if (_pointer.isNullOrNullPointer) { + throw Exception('Could not determine value for Category.displayName'); + } + return _pointer!.ref.display_name.isNotNullPointer + ? _pointer!.ref.display_name.toDartString() + : null; + } + + /// Accepts a pointer to a list of structs, and a count representing the length + /// of the list, and returns a list of pure-Dart [Category] instances. + static Iterable fromNativeArray( + Pointer structs, + int count, + ) sync* { + for (int i = 0; i < count; i++) { + yield Category.native(structs + i); + } + } +} + +/// {@macro Classifications} +/// +/// This io-friendly implementation is not immutable strictly for memoization of +/// computed fields. All values used by pkg:equatable are in fact immutable. +// ignore: must_be_immutable +class Classifications extends BaseClassifications { + /// {@macro Classifications.fake} + Classifications({ + required Iterable categories, + required int headIndex, + required String? headName, + }) : _categories = categories, + _headIndex = headIndex, + _headName = headName, + _pointer = null; + + /// Instatiates a [Classifications] object as a wrapper around native memory. + /// + /// {@macro Container.memoryManagement} + Classifications.native(this._pointer); + + final Pointer? _pointer; + + Iterable? _categories; + @override + Iterable get categories => _categories ??= _getCategories(); + Iterable _getCategories() { + if (_pointer.isNullOrNullPointer) { + throw Exception( + 'Could not determine value for Classifications.categories', + ); + } + return Category.fromNativeArray( + _pointer!.ref.categories, + _pointer!.ref.categories_count, + ); + } + + int? _headIndex; + @override + int get headIndex => _headIndex ??= _getHeadIndex(); + int _getHeadIndex() { + if (_pointer.isNullOrNullPointer) { + throw Exception( + 'Could not determine value for Classifications.headIndex', + ); + } + return _pointer!.ref.head_index; + } + + String? _headName; + @override + String? get headName => _headName ??= _getHeadName(); + String? _getHeadName() { + if (_pointer.isNullOrNullPointer) { + throw Exception('Could not determine value for Classifications.headName'); + } + return _pointer!.ref.head_name.isNotNullPointer + ? _pointer!.ref.head_name.toDartString() + : null; + } + + /// Accepts a pointer to a list of structs, and a count representing the length + /// of the list, and returns a list of pure-Dart [Classifications] instances. + static Iterable fromNativeArray( + Pointer structs, + int count, + ) sync* { + for (int i = 0; i < count; i++) { + yield Classifications.native(structs + i); + } + } +} + +/// {@macro Embedding} +/// +/// This io-friendly implementation is not immutable strictly for memoization of +/// computed fields. All values used by pkg:equatable are in fact immutable. +// ignore: must_be_immutable +class Embedding extends BaseEmbedding { + /// {@macro Embedding.fakeQuantized} + Embedding.quantized( + Uint8List quantizedEmbedding, { + required int headIndex, + String? headName, + }) : _floatEmbedding = null, + _headIndex = headIndex, + _headName = headName, + _quantizedEmbedding = quantizedEmbedding, + _pointer = null, + type = EmbeddingType.quantized; + + /// {@macro Embedding.fakeFloat} + Embedding.float( + Float32List floatEmbedding, { + required int headIndex, + String? headName, + }) : _floatEmbedding = floatEmbedding, + _headIndex = headIndex, + _headName = headName, + _quantizedEmbedding = null, + _pointer = null, + type = EmbeddingType.float; + + /// Instatiates a [Classifications] object as a wrapper around native memory. + /// + /// {@macro Container.memoryManagement} + Embedding.native(this._pointer) + : type = _pointer!.ref.float_embedding.isNotNullAndIsNotNullPointer + ? EmbeddingType.float + : EmbeddingType.quantized; + + final Pointer? _pointer; + + /// Read-only access to the internal pointer. + Pointer? get pointer => _pointer; + + @override + final EmbeddingType type; + + int? _headIndex; + @override + int get headIndex => _headIndex ??= _getHeadIndex(); + int _getHeadIndex() { + if (_pointer.isNullOrNullPointer) { + throw Exception( + 'Could not determine value for Embedding.headIndex', + ); + } + return _pointer!.ref.head_index; + } + + String? _headName; + @override + String? get headName => _headName ??= _getHeadName(); + String? _getHeadName() { + if (_pointer.isNullOrNullPointer) { + throw Exception('Could not determine value for Embedding.headName'); + } + return _pointer!.ref.head_name.isNotNullPointer + ? _pointer!.ref.head_name.toDartString() + : null; + } + + Uint8List? _quantizedEmbedding; + @override + Uint8List? get quantizedEmbedding => + _quantizedEmbedding ??= _getQuantizedEmbedding(); + Uint8List? _getQuantizedEmbedding() { + if (type != EmbeddingType.quantized) { + throw Exception( + 'Unexpected access of `quantizedEmbedding` for float embedding', + ); + } + if (_pointer.isNullOrNullPointer) { + throw Exception( + 'Could not determine value for Embedding.quantizedEmbedding', + ); + } + return _pointer!.ref.quantized_embedding.isNotNullPointer + ? _pointer!.ref.quantized_embedding + .toUint8List(_pointer!.ref.values_count) + : null; + } + + Float32List? _floatEmbedding; + @override + Float32List? get floatEmbedding => _floatEmbedding ??= _getFloatEmbedding(); + Float32List? _getFloatEmbedding() { + if (type != EmbeddingType.float) { + throw Exception( + 'Unexpected access of `floatEmbedding` for quantized embedding', + ); + } + if (_pointer.isNullOrNullPointer) { + throw Exception('Could not determine value for Embedding.floatEmbedding'); + } + return _pointer!.ref.float_embedding.isNotNullPointer + ? _pointer!.ref.float_embedding + .toFloat32List(_pointer!.ref.values_count) + : null; + } + + @override + int get length { + if (_pointer.isNotNullAndIsNotNullPointer) { + return _pointer!.ref.values_count; + } + return switch (type) { + EmbeddingType.float => _floatEmbedding!.length, + EmbeddingType.quantized => _quantizedEmbedding!.length, + }; + } + + /// Accepts a pointer to a list of structs, and a count representing the length + /// of the list, and returns a list of pure-Dart [Embedding] instances. + static Iterable fromNativeArray( + Pointer structs, + int count, + ) sync* { + for (int i = 0; i < count; i++) { + yield Embedding.native(structs + i); + } + } +} diff --git a/packages/mediapipe-core/lib/src/io/ffi_utils.dart b/packages/mediapipe-core/lib/src/io/ffi_utils.dart new file mode 100644 index 0000000..6403cee --- /dev/null +++ b/packages/mediapipe-core/lib/src/io/ffi_utils.dart @@ -0,0 +1,157 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'dart:typed_data'; +import 'package:ffi/ffi.dart'; + +/// Offers convenience and readability extensions for detecting null pointers. +extension NullAwarePtr on Pointer? { + bool get _isNull => this == null; + + bool get _isNotNull => !_isNull; + + /// Returns true if this is a `nullptr`. + bool get isNullPointer => _isNotNull && this!.address == nullptr.address; + + /// Returns true if this is not a `nullptr`, but also not [null]. + bool get isNotNullPointer => _isNotNull && this!.address != nullptr.address; + + /// Returns true if this is neither [null] nor a `nullptr`. + bool get isNotNullAndIsNotNullPointer => _isNotNull && isNotNullPointer; + + /// Returns true if this is either [null] or is a `nullptr`. + bool get isNullOrNullPointer => _isNull || isNullPointer; +} + +/// Helpers to convert between a [String] and a `Pointer`. +extension NativeStrings on String { + /// Copies a single Dart string into its C memory equivalent. + /// + /// See also: + /// * [toCharsPointerPointer] + Pointer copyToNative() => toNativeUtf8().cast(); +} + +/// Helpers to convert between a `List` and a `Pointer>`. +extension NativeListOfStrings on List { + /// Copies a list of Dart strings into native memory. + /// + /// See also: + /// * [Pointer.toNative] for a non-list equivalent. + Pointer> copyToNative() { + final ptrArray = calloc>(length); + for (var i = 0; i < length; i++) { + ptrArray[i] = this[i].copyToNative(); + } + return ptrArray; + } +} + +/// Helpers to convert a [Pointer] into a [Uint8List]. +extension DartAwareChars on Pointer { + /// Creates a Dart view on native memory, cast as unsigned, 8-bit integers. + /// This method does not copy the data out of native memory. + /// + /// Throws an exception if the method is called on a `nullptr`. + Uint8List toUint8List(int length) { + if (isNullPointer) { + throw Exception('Unexpectedly called `toUint8List` on nullptr'); + } + RangeError.checkNotNegative(length, 'length'); + return cast().asTypedList(length); + } + + /// Converts the C memory representation of a string into a Dart [String]. + /// + /// Throws an exception if the method is called on a `nullptr`. + /// + /// See also: + /// * [toDartStrings] + String toDartString({int? length}) { + if (isNullPointer) { + throw Exception('Unexpectedly called `toDartString` on nullptr'); + } + return cast().toDartString(length: length); + } + + /// Releases all native memory. + void free() => calloc.free(this); +} + +/// Helpers to convert between `Pointer>` and `List`. +extension DartAwarePointerChars on Pointer> { + /// Creates a Dart view on a pointer to a pointer to utf8 chars in native memory, + /// cast as a `List`. This method does not copy the data out of native + /// memory. + /// + /// Throws an exception if the method is called on a `nullptr`. + /// + /// See also: + /// * [toDartString], for a non-list equivalent. + List toDartStrings(int length) { + if (isNullPointer) { + throw Exception('Unexpectedly called `toDartStrings` on nullptr'); + } + final dartStrings = []; + int counter = 0; + while (counter < length) { + dartStrings.add(this[counter].toDartString()); + counter++; + } + return dartStrings; + } + + /// Releases all native memory. + void free(int length) { + if (isNullPointer) return; + int count = 0; + while (count < length) { + calloc.free(this[count]); + count++; + } + calloc.free(this); + } +} + +/// Helpers to convert a [Pointer] into a [Uint8List]. +extension DartAwareFloats on Pointer { + /// Creates a Dart view on a pointer to floats in native memory, cast as a + /// Float32List. This method does not copy the data out of native memory. + /// + /// Throws an exception if the method is called on a `nullptr`. + Float32List toFloat32List(int length) { + if (isNullPointer) { + throw Exception('Unexpectedly called `toFloat32List` on nullptr'); + } + RangeError.checkNotNegative(length, 'length'); + return cast().asTypedList(length); + } +} + +/// Extension method for copying a [Float32List] from Dart memory into native +/// memory. +extension NativeFloats on Float32List { + /// Copies a [Float32List] into native memory as a `float*`. + /// + /// Returns an [allocator]-allocated pointer to the result. + Pointer copyToNative({Allocator allocator = malloc}) { + final Pointer result = allocator(length) + ..asTypedList(length).setAll(0, this); + return result.cast(); + } +} + +/// Extension method for copying a [Uint8List] from Dart memory into native +/// memory. +extension NativeInts on Uint8List { + /// Copies a [Uint8List] into native memory as a `char*`. + /// + /// Returns an [allocator]-allocated pointer to the result. + Pointer copyToNative({Allocator allocator = malloc}) { + final Pointer result = allocator(length) + ..asTypedList(length).setAll(0, this); + return result.cast(); + } +} diff --git a/packages/mediapipe-core/lib/src/io/mediapipe_core.dart b/packages/mediapipe-core/lib/src/io/mediapipe_core.dart new file mode 100644 index 0000000..f7c7cd0 --- /dev/null +++ b/packages/mediapipe-core/lib/src/io/mediapipe_core.dart @@ -0,0 +1,9 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'containers.dart'; +export 'ffi_utils.dart'; +export 'task_options.dart'; +export 'task_executor.dart'; +export 'task_result.dart'; diff --git a/packages/mediapipe-core/lib/src/io/task_executor.dart b/packages/mediapipe-core/lib/src/io/task_executor.dart new file mode 100644 index 0000000..28d8bc7 --- /dev/null +++ b/packages/mediapipe-core/lib/src/io/task_executor.dart @@ -0,0 +1,111 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:mediapipe_core/io.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger('TaskExecutor'); + +/// {@template TaskExecutor} +/// Instantiates and manages an object which can complete MediaPipe tasks. The +/// managed task-completing object does not exist in Dart memory, but instead in +/// platform-dependent native memory. +/// +/// Extending classes should implement [createResultsPointer], [createWorker], +/// [closeWorker], and any additional task-specific methods. Applications will +/// only call those extra task-specific methods, which will throw an exception +/// on any error communicating with the native workers. It is the job of +/// application code to surround those task-specific methods with try-catch +/// clauses. +/// +/// Executors are separated from their public API counterparts because FFI and +/// MediaPipe have no concept of asynchrony or futures, so this pattern allows a +/// public-facing task handler to create an executor on a separate isolate, and +/// for Flutter apps to await the results of MediaPipe tasks. +/// {@endtemplate} +abstract class TaskExecutor< + NativeOptions extends Struct, + Options extends TaskOptions, + NativeResult extends Struct, + Result extends IOTaskResult> { + /// {@macro TaskExecutor} + TaskExecutor(this.options); + + /// Initialization values for the [worker]. + final Options options; + + /// Inner memoization cache for the [worker]. + Pointer? _worker; + + /// Debug value for log statements. + String get taskName; + + /// The native MediaPipe object which will complete this task. + /// + /// [worker] is a computed property and will return the same object on + /// repeated accesses. On the first access, the native [worker] object is + /// initialized, and this accessor with throw an [Exception] if that process + /// fails. + Pointer get worker { + if (_worker.isNullOrNullPointer) { + final errorMessageMemory = calloc>(); + _log.fine('Creating $taskName worker'); + _worker = createWorker( + options.copyToNative(), + errorMessageMemory, + ); + handleErrorMessage(errorMessageMemory); + errorMessageMemory.free(1); + options.dispose(); + } + return _worker!; + } + + /// Allocates this task's results struct in native memory. + Pointer createResultsPointer(); + + /// Allocates this task's [worker] object in native memory. + Pointer createWorker( + Pointer options, + Pointer> error, + ); + + /// Releases the [worker] object behind this task. + int closeWorker(Pointer worker, Pointer> error); + + /// Releases the [worker] and any remaining resources. After calling [dispose], + /// re-using the [worker] property will recreate the native object and will + /// require a second call to [dispose]. + void dispose() { + if (_worker != null) { + _log.fine('Closing $taskName worker'); + final errorMessageMemory = calloc>(); + final status = closeWorker(_worker!, errorMessageMemory); + _worker = null; + handleErrorMessage(errorMessageMemory, status); + errorMessageMemory.free(1); + } + } + + /// Throws an exception if [errorMessage] is non-empty. + void handleErrorMessage(Pointer> errorMessage, [int? status]) { + if (errorMessage.isNotNullPointer && errorMessage[0].isNotNullPointer) { + final dartErrorMessage = errorMessage.toDartStrings(1); + _log.severe('$taskName Error: $dartErrorMessage'); + + // If there is an exception, release this memory because the calling code + // will not get a chance to. + errorMessage.free(1); + + // Raise the exception. + if (status == null) { + throw Exception('$taskName Error: $dartErrorMessage'); + } else { + throw Exception('$taskName Error: Status $status :: $dartErrorMessage'); + } + } + } +} diff --git a/packages/mediapipe-core/lib/src/io/task_options.dart b/packages/mediapipe-core/lib/src/io/task_options.dart new file mode 100644 index 0000000..450ac31 --- /dev/null +++ b/packages/mediapipe-core/lib/src/io/task_options.dart @@ -0,0 +1,241 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'dart:typed_data'; +import 'package:ffi/ffi.dart'; +import 'ffi_utils.dart'; +import 'package:mediapipe_core/interface.dart'; +import 'third_party/mediapipe/generated/mediapipe_common_bindings.dart' + as bindings; + +/// {@template TaskOptions} +/// Provides a suggested API surface for how classes implementing [BaseTaskOptions] +/// should manage their [InnerTaskOptions] fields. The two suggested methods are +/// [copyToNative] and [dispose]. +/// {@endtemplate} +mixin TaskOptions on BaseTaskOptions { + /// {@template TaskOptions.copyToNative} + /// Copies these task options into native memory. Any fields of type + /// [InnerTaskOptions] should have their `assignToStruct` method called. + /// + /// It is the job of [copyToNative] to both create and cache the pointer to + /// native memory. This means that [copyToNative] can be called more than + /// once without causing problems other than redundant work. + /// {@endtemplate} + Pointer copyToNative() { + throw UnimplementedError('Must implement `$runtimeType.copyToNative`'); + } + + /// {@template TaskOptions.dispose} + /// Releases native memory created by [copyToNative]. + /// + /// This method is called within the IO implementation and should not be + /// callable or even visible to end developers. + /// {@endtemplate} + void dispose() { + throw UnimplementedError('Must implement method `$runtimeType.dispose`'); + } +} + +/// {@macro InnerTaskOptions} +/// +/// The native implementation suggests two methods each extending class should +/// implement: [assignToStruct] and [freeStructFields]. Both methods are passed +/// a struct created and managed elsewhere, and these methods either copy local +/// values into native memory ([assignToStruct]) or deallocate any native memory +/// that a naive `calloc.free()` from the parent options class would miss. +mixin InnerTaskOptions on BaseInnerTaskOptions { + /// Assigns all values to an existing struct. This method should be + /// implemented by all [InnerTaskOptions] types to hydrate a struct, but not + /// for the creation of that struct. Allocation and management of the actual + /// structs are handled by [TaskOptions.copyToNative] on the wrapping options + /// object. + void assignToStruct(T struct) { + throw UnimplementedError('Must implement $runtimeType.assignToStruct'); + } + + /// Deallocates any memory on the [struct] that would be missed simply by + /// calling `calloc.free(struct)`, which the parent who called this method + /// will do after this method completes. + void freeStructFields(T struct) { + throw UnimplementedError('Must implement $runtimeType.freeStructFields'); + } +} + +/// {@macro BaseOptions} +class BaseOptions extends BaseBaseOptions + with InnerTaskOptions { + /// Generative constructor that creates a [BaseOptions] instance. + const BaseOptions._({ + this.modelAssetBuffer, + this.modelAssetPath, + required this.type, + }) : assert( + !(modelAssetBuffer == null && modelAssetPath == null), + 'You must supply either `modelAssetBuffer` or `modelAssetPath`', + ), + assert( + !(modelAssetBuffer != null && modelAssetPath != null), + 'You must only supply one of `modelAssetBuffer` and `modelAssetPath`', + ); + + /// {@macro BaseOptions.path} + factory BaseOptions.path(String path) { + return BaseOptions._( + modelAssetPath: path, + type: BaseOptionsType.path, + ); + } + + /// {@macro BaseOptions.memory} + factory BaseOptions.memory(Uint8List buffer) { + return BaseOptions._( + modelAssetBuffer: buffer, + type: BaseOptionsType.memory, + ); + } + + @override + final Uint8List? modelAssetBuffer; + + @override + final String? modelAssetPath; + + @override + final BaseOptionsType type; + + @override + void assignToStruct(bindings.BaseOptions struct) { + switch (type) { + case BaseOptionsType.path: + { + struct.model_asset_path = modelAssetPath!.copyToNative(); + } + case BaseOptionsType.memory: + { + struct.model_asset_buffer = modelAssetBuffer!.copyToNative(); + struct.model_asset_buffer_count = modelAssetBuffer!.lengthInBytes; + } + } + } + + @override + void freeStructFields(bindings.BaseOptions struct) { + if (struct.model_asset_path.isNotNullPointer) { + calloc.free(struct.model_asset_path); + } + if (struct.model_asset_buffer.isNotNullPointer) { + calloc.free(struct.model_asset_buffer); + } + } +} + +/// {@macro ClassifierOptions} +class ClassifierOptions extends BaseClassifierOptions + with InnerTaskOptions { + /// {@macro ClassifierOptions} + const ClassifierOptions({ + this.displayNamesLocale, + this.maxResults, + this.scoreThreshold, + this.categoryAllowlist, + this.categoryDenylist, + }); + + @override + final String? displayNamesLocale; + + @override + final int? maxResults; + + @override + final double? scoreThreshold; + + @override + final List? categoryAllowlist; + + @override + final List? categoryDenylist; + + @override + void assignToStruct(bindings.ClassifierOptions struct) { + _setDisplayNamesLocale(struct); + _setMaxResults(struct); + _setScoreThreshold(struct); + _setAllowlist(struct); + _setDenylist(struct); + } + + void _setDisplayNamesLocale(bindings.ClassifierOptions struct) { + if (displayNamesLocale != null) { + struct.display_names_locale = displayNamesLocale!.copyToNative(); + } + } + + void _setMaxResults(bindings.ClassifierOptions struct) { + // This value must not be zero, and -1 implies no limit. + struct.max_results = maxResults ?? -1; + } + + void _setScoreThreshold(bindings.ClassifierOptions struct) { + if (scoreThreshold != null) { + struct.score_threshold = scoreThreshold!; + } + } + + void _setAllowlist(bindings.ClassifierOptions struct) { + if (categoryAllowlist != null) { + struct.category_allowlist = categoryAllowlist!.copyToNative(); + struct.category_allowlist_count = categoryAllowlist!.length; + } + } + + void _setDenylist(bindings.ClassifierOptions struct) { + if (categoryDenylist != null) { + struct.category_denylist = categoryDenylist!.copyToNative(); + struct.category_denylist_count = categoryDenylist!.length; + } + } + + @override + void freeStructFields(bindings.ClassifierOptions struct) { + if (struct.display_names_locale.address != 0) { + calloc.free(struct.display_names_locale); + } + if (struct.category_allowlist.address != 0) { + calloc.free(struct.category_allowlist); + } + if (struct.category_denylist.address != 0) { + calloc.free(struct.category_denylist); + } + } +} + +/// {@macro EmbedderOptions} +class EmbedderOptions extends BaseEmbedderOptions + with InnerTaskOptions { + /// {@macro EmbedderOptions} + const EmbedderOptions({ + this.l2Normalize = false, + this.quantize = false, + }); + + @override + final bool l2Normalize; + + @override + final bool quantize; + + @override + void assignToStruct(bindings.EmbedderOptions struct) { + struct.l2_normalize = l2Normalize; + struct.quantize = quantize; + } + + @override + void freeStructFields(bindings.EmbedderOptions struct) { + // no-op; nothing to free + } +} diff --git a/packages/mediapipe-core/lib/src/io/task_result.dart b/packages/mediapipe-core/lib/src/io/task_result.dart new file mode 100644 index 0000000..1eb6431 --- /dev/null +++ b/packages/mediapipe-core/lib/src/io/task_result.dart @@ -0,0 +1,77 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'package:mediapipe_core/interface.dart'; +import 'package:mediapipe_core/io.dart'; +import 'third_party/mediapipe/generated/mediapipe_common_bindings.dart' + as bindings; + +/// Anchor class for native memory implementations of task results. +mixin IOTaskResult {} + +/// {@macro ClassifierResult} +abstract class ClassifierResult extends BaseClassifierResult with IOTaskResult { + /// {@macro ClassifierResult.fake} + ClassifierResult({required Iterable classifications}) + : _classifications = classifications, + _pointer = null; + + final Pointer? _pointer; + + /// Internal storage for [classifications], used to cache values pulled out + /// of native memory, or passed in via the [fake] constructor. + late final Iterable? _classifications; + + @override + Iterable get classifications => + _classifications ??= _getClassifications(); + + /// Fallback for [classifications] if no direct value was supplied and native + /// memory has not yet been read. + Iterable _getClassifications() { + if (_pointer.isNullOrNullPointer) { + throw Exception( + 'No native memory for ClassifierResult.classifications', + ); + } + return Classifications.fromNativeArray( + _pointer!.ref.classifications, + _pointer!.ref.classifications_count, + ); + } +} + +/// {@macro TimestampedClassifierResult} +abstract class TimestampedClassifierResult extends ClassifierResult + with TimestampedResult { + /// {@macro TimestampedClassifierResult} + TimestampedClassifierResult({ + required Iterable classifications, + required Duration? timestamp, + }) : _timestamp = timestamp, + super(classifications: classifications); + + Duration? _timestamp; + @override + Duration? get timestamp => _timestamp ??= _getTimestamp(); + Duration? _getTimestamp() { + if (_pointer.isNullOrNullPointer) { + throw Exception( + 'No native memory for ClassifierResult.timestamp', + ); + } + return _pointer!.ref.has_timestamp_ms + ? Duration(milliseconds: _pointer!.ref.timestamp_ms) + : null; + } + + @override + String toString() { + final classificationStrings = + classifications.map((cat) => cat.toString()).join(', '); + return '$runtimeType(classifications=[$classificationStrings], ' + 'timestamp=$timestamp)'; + } +} diff --git a/packages/mediapipe-core/lib/src/io/test_utils.dart b/packages/mediapipe-core/lib/src/io/test_utils.dart new file mode 100644 index 0000000..d401b42 --- /dev/null +++ b/packages/mediapipe-core/lib/src/io/test_utils.dart @@ -0,0 +1,104 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:ffi/ffi.dart'; + +import 'package:mediapipe_core/src/io/mediapipe_core.dart'; +import 'package:mediapipe_core/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart' + as core_bindings; + +/// Hydrates a faked [core_bindings.Category] object. +void populateCategory( + core_bindings.Category category, { + int index = 1, + double score = 0.9, + String? categoryName = 'Positive', + String? displayName, +}) { + category.index = index; + category.score = score; + + if (categoryName != null) { + category.category_name = categoryName.copyToNative(); + } + if (displayName != null) { + category.display_name = displayName.copyToNative(); + } +} + +/// Hydrates a faked [core_bindings.Classifications] object. +/// +/// If [categories] is passed, [numCategories] must indicate its length; +/// otherwise, this function generates [numCategories] faked +/// [core_bindings.Category] objects. +void populateClassifications( + core_bindings.Classifications classifications, { + Pointer? categories, + int numCategories = 2, + int headIndex = 1, + String headName = 'Head', +}) { + if (categories.isNotNullAndIsNotNullPointer) { + classifications.categories = categories!; + classifications.categories_count = numCategories; + } else { + final ptrs = calloc(numCategories); + int count = 0; + while (count < numCategories) { + populateCategory(ptrs[count]); + count++; + } + classifications.categories = ptrs; + classifications.categories_count = numCategories; + } + classifications.head_name = headName.copyToNative(); + classifications.head_index = headIndex; +} + +/// Hydrates a faked [core_bindings.Embedding] object. +void populateEmbedding( + core_bindings.Embedding embedding, { + bool quantize = false, + bool l2Normalize = false, + int length = 100, + String headName = 'response_encoding', + int headIndex = 1, +}) { + embedding.values_count = length; + embedding.head_name = headName.copyToNative(); + embedding.head_index = headIndex; + + Random rnd = Random(); + + if (quantize) { + embedding.quantized_embedding = Uint8List.fromList( + _genInts(length, rnd: rnd).toList(), + ).copyToNative(); + } else { + embedding.float_embedding = Float32List.fromList( + _genFloats(length, l2Normalize: l2Normalize, rnd: rnd).toList(), + ).copyToNative(); + } +} + +Iterable _genInts(int count, {required Random rnd}) sync* { + int index = 0; + while (index < count) { + yield rnd.nextInt(127); + index++; + } +} + +Iterable _genFloats(int count, + {required bool l2Normalize, required Random rnd}) sync* { + int index = 0; + while (index < count) { + final dbl = rnd.nextDouble(); + yield l2Normalize ? dbl : dbl * 127; + index++; + } +} diff --git a/packages/mediapipe-core/lib/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart b/packages/mediapipe-core/lib/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart new file mode 100644 index 0000000..6531f3d --- /dev/null +++ b/packages/mediapipe-core/lib/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart @@ -0,0 +1,392 @@ +/* Copyright 2023 The MediaPipe Authors. + +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. +==============================================================================*/ + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; + +final class BaseOptions extends ffi.Struct { + external ffi.Pointer model_asset_buffer; + + @ffi.UnsignedInt() + external int model_asset_buffer_count; + + external ffi.Pointer model_asset_path; +} + +final class EmbedderOptions extends ffi.Struct { + @ffi.Bool() + external bool l2_normalize; + + @ffi.Bool() + external bool quantize; +} + +final class __mbstate_t extends ffi.Union { + @ffi.Array.multi([128]) + external ffi.Array __mbstate8; + + @ffi.LongLong() + external int _mbstateL; +} + +final class __darwin_pthread_handler_rec extends ffi.Struct { + external ffi + .Pointer)>> + __routine; + + external ffi.Pointer __arg; + + external ffi.Pointer<__darwin_pthread_handler_rec> __next; +} + +final class _opaque_pthread_attr_t extends ffi.Struct { + @ffi.Long() + external int __sig; + + @ffi.Array.multi([56]) + external ffi.Array __opaque; +} + +final class _opaque_pthread_cond_t extends ffi.Struct { + @ffi.Long() + external int __sig; + + @ffi.Array.multi([40]) + external ffi.Array __opaque; +} + +final class _opaque_pthread_condattr_t extends ffi.Struct { + @ffi.Long() + external int __sig; + + @ffi.Array.multi([8]) + external ffi.Array __opaque; +} + +final class _opaque_pthread_mutex_t extends ffi.Struct { + @ffi.Long() + external int __sig; + + @ffi.Array.multi([56]) + external ffi.Array __opaque; +} + +final class _opaque_pthread_mutexattr_t extends ffi.Struct { + @ffi.Long() + external int __sig; + + @ffi.Array.multi([8]) + external ffi.Array __opaque; +} + +final class _opaque_pthread_once_t extends ffi.Struct { + @ffi.Long() + external int __sig; + + @ffi.Array.multi([8]) + external ffi.Array __opaque; +} + +final class _opaque_pthread_rwlock_t extends ffi.Struct { + @ffi.Long() + external int __sig; + + @ffi.Array.multi([192]) + external ffi.Array __opaque; +} + +final class _opaque_pthread_rwlockattr_t extends ffi.Struct { + @ffi.Long() + external int __sig; + + @ffi.Array.multi([16]) + external ffi.Array __opaque; +} + +final class _opaque_pthread_t extends ffi.Struct { + @ffi.Long() + external int __sig; + + external ffi.Pointer<__darwin_pthread_handler_rec> __cleanup_stack; + + @ffi.Array.multi([8176]) + external ffi.Array __opaque; +} + +final class ClassifierOptions extends ffi.Struct { + external ffi.Pointer display_names_locale; + + @ffi.Int() + external int max_results; + + @ffi.Float() + external double score_threshold; + + external ffi.Pointer> category_allowlist; + + @ffi.Uint32() + external int category_allowlist_count; + + external ffi.Pointer> category_denylist; + + @ffi.Uint32() + external int category_denylist_count; +} + +final class Category extends ffi.Struct { + @ffi.Int() + external int index; + + @ffi.Float() + external double score; + + external ffi.Pointer category_name; + + external ffi.Pointer display_name; +} + +final class Categories extends ffi.Struct { + external ffi.Pointer categories; + + @ffi.Uint32() + external int categories_count; +} + +final class Embedding extends ffi.Struct { + external ffi.Pointer float_embedding; + + external ffi.Pointer quantized_embedding; + + @ffi.Uint32() + external int values_count; + + @ffi.Int() + external int head_index; + + external ffi.Pointer head_name; +} + +final class EmbeddingResult extends ffi.Struct { + external ffi.Pointer embeddings; + + @ffi.Uint32() + external int embeddings_count; + + @ffi.Int64() + external int timestamp_ms; + + @ffi.Bool() + external bool has_timestamp_ms; +} + +final class Classifications extends ffi.Struct { + external ffi.Pointer categories; + + @ffi.Uint32() + external int categories_count; + + @ffi.Int() + external int head_index; + + external ffi.Pointer head_name; +} + +final class ClassificationResult extends ffi.Struct { + external ffi.Pointer classifications; + + @ffi.Uint32() + external int classifications_count; + + @ffi.Int64() + external int timestamp_ms; + + @ffi.Bool() + external bool has_timestamp_ms; +} + +const int __bool_true_false_are_defined = 1; + +const int true1 = 1; + +const int false1 = 0; + +const int __WORDSIZE = 64; + +const int __DARWIN_ONLY_64_BIT_INO_T = 1; + +const int __DARWIN_ONLY_UNIX_CONFORMANCE = 1; + +const int __DARWIN_ONLY_VERS_1050 = 1; + +const int __DARWIN_UNIX03 = 1; + +const int __DARWIN_64_BIT_INO_T = 1; + +const int __DARWIN_VERS_1050 = 1; + +const int __DARWIN_NON_CANCELABLE = 0; + +const String __DARWIN_SUF_EXTSN = '\$DARWIN_EXTSN'; + +const int __DARWIN_C_ANSI = 4096; + +const int __DARWIN_C_FULL = 900000; + +const int __DARWIN_C_LEVEL = 900000; + +const int __STDC_WANT_LIB_EXT1__ = 1; + +const int __DARWIN_NO_LONG_LONG = 0; + +const int _DARWIN_FEATURE_64_BIT_INODE = 1; + +const int _DARWIN_FEATURE_ONLY_64_BIT_INODE = 1; + +const int _DARWIN_FEATURE_ONLY_VERS_1050 = 1; + +const int _DARWIN_FEATURE_ONLY_UNIX_CONFORMANCE = 1; + +const int _DARWIN_FEATURE_UNIX_CONFORMANCE = 3; + +const int __has_ptrcheck = 0; + +const int __DARWIN_NULL = 0; + +const int __PTHREAD_SIZE__ = 8176; + +const int __PTHREAD_ATTR_SIZE__ = 56; + +const int __PTHREAD_MUTEXATTR_SIZE__ = 8; + +const int __PTHREAD_MUTEX_SIZE__ = 56; + +const int __PTHREAD_CONDATTR_SIZE__ = 8; + +const int __PTHREAD_COND_SIZE__ = 40; + +const int __PTHREAD_ONCE_SIZE__ = 8; + +const int __PTHREAD_RWLOCK_SIZE__ = 192; + +const int __PTHREAD_RWLOCKATTR_SIZE__ = 16; + +const int USER_ADDR_NULL = 0; + +const int INT8_MAX = 127; + +const int INT16_MAX = 32767; + +const int INT32_MAX = 2147483647; + +const int INT64_MAX = 9223372036854775807; + +const int INT8_MIN = -128; + +const int INT16_MIN = -32768; + +const int INT32_MIN = -2147483648; + +const int INT64_MIN = -9223372036854775808; + +const int UINT8_MAX = 255; + +const int UINT16_MAX = 65535; + +const int UINT32_MAX = 4294967295; + +const int UINT64_MAX = -1; + +const int INT_LEAST8_MIN = -128; + +const int INT_LEAST16_MIN = -32768; + +const int INT_LEAST32_MIN = -2147483648; + +const int INT_LEAST64_MIN = -9223372036854775808; + +const int INT_LEAST8_MAX = 127; + +const int INT_LEAST16_MAX = 32767; + +const int INT_LEAST32_MAX = 2147483647; + +const int INT_LEAST64_MAX = 9223372036854775807; + +const int UINT_LEAST8_MAX = 255; + +const int UINT_LEAST16_MAX = 65535; + +const int UINT_LEAST32_MAX = 4294967295; + +const int UINT_LEAST64_MAX = -1; + +const int INT_FAST8_MIN = -128; + +const int INT_FAST16_MIN = -32768; + +const int INT_FAST32_MIN = -2147483648; + +const int INT_FAST64_MIN = -9223372036854775808; + +const int INT_FAST8_MAX = 127; + +const int INT_FAST16_MAX = 32767; + +const int INT_FAST32_MAX = 2147483647; + +const int INT_FAST64_MAX = 9223372036854775807; + +const int UINT_FAST8_MAX = 255; + +const int UINT_FAST16_MAX = 65535; + +const int UINT_FAST32_MAX = 4294967295; + +const int UINT_FAST64_MAX = -1; + +const int INTPTR_MAX = 9223372036854775807; + +const int INTPTR_MIN = -9223372036854775808; + +const int UINTPTR_MAX = -1; + +const int INTMAX_MAX = 9223372036854775807; + +const int UINTMAX_MAX = -1; + +const int INTMAX_MIN = -9223372036854775808; + +const int PTRDIFF_MIN = -9223372036854775808; + +const int PTRDIFF_MAX = 9223372036854775807; + +const int SIZE_MAX = -1; + +const int RSIZE_MAX = 9223372036854775807; + +const int WCHAR_MAX = 2147483647; + +const int WCHAR_MIN = -2147483648; + +const int WINT_MIN = -2147483648; + +const int WINT_MAX = 2147483647; + +const int SIG_ATOMIC_MIN = -2147483648; + +const int SIG_ATOMIC_MAX = 2147483647; diff --git a/packages/mediapipe-core/lib/src/web/mediapipe_core.dart b/packages/mediapipe-core/lib/src/web/mediapipe_core.dart new file mode 100644 index 0000000..854d12e --- /dev/null +++ b/packages/mediapipe-core/lib/src/web/mediapipe_core.dart @@ -0,0 +1,3 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. diff --git a/packages/mediapipe-core/lib/universal_mediapipe_core.dart b/packages/mediapipe-core/lib/universal_mediapipe_core.dart new file mode 100644 index 0000000..0c64ccc --- /dev/null +++ b/packages/mediapipe-core/lib/universal_mediapipe_core.dart @@ -0,0 +1,193 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:mediapipe_core/interface.dart'; + +/// {@macro ClassifierResult} +class ClassifierResult extends BaseClassifierResult { + /// {@template ClassifierResult.fake} + /// Instantiates a [ClassifierResult] instance with fake values for testing. + /// {@endtemplate} + ClassifierResult({required Iterable classifications}); + + @override + Iterable get classifications => + throw UnimplementedError(); + + @override + bool get isClosed => throw UnimplementedError(); + + @override + // ignore: must_call_super + void dispose() => throw UnimplementedError(); +} + +/// {@macro Category} +class Category extends BaseCategory { + /// {@template Category.fake} + /// Initializes a [Category] instance with mock values for testing. + /// {@endtemplate} + Category({ + required int index, + required double score, + required String? categoryName, + required String? displayName, + }); + + @override + String? get categoryName => throw UnimplementedError(); + + @override + String? get displayName => throw UnimplementedError(); + + @override + int get index => throw UnimplementedError(); + + @override + double get score => throw UnimplementedError(); +} + +/// {@macro Classifications} +class Classifications extends BaseClassifications { + /// {@template Classifications.fake} + /// Instantiates a [Classifications] object with fake values for testing. + /// {@endtemplate} + Classifications({ + required Iterable categories, + required int headIndex, + required String? headName, + }); + + @override + Iterable get categories => throw UnimplementedError(); + + @override + int get headIndex => throw UnimplementedError(); + + @override + String? get headName => throw UnimplementedError(); +} + +/// {@macro Embedding} +class Embedding extends BaseEmbedding { + /// {@template Embedding.fakeQuantized} + /// Instantiates a quantized [Embedding] object with fake values for testing. + /// + /// Usage: + /// ```dart + /// Embedding.quantized(Uint8List.fromList([1,2,3]), headIndex: 1); + /// ``` + /// {@endtemplate} + Embedding.quantized( + Uint8List quantizedEmbedding, { + required int headIndex, + String? headName, + }); + + /// {@template Embedding.fakeFloat} + /// Instantiates a floating point [Embedding] object with fake values for + /// testing. + /// + /// Usage: + /// ```dart + /// Embedding.float(Float32List.fromList([0.1, 0.2, 0.3]), headIndex: 1); + /// ``` + /// {@endtemplate} + Embedding.float( + Float32List floatEmbedding, { + required int headIndex, + String? headName, + }); + + @override + Float32List? get floatEmbedding => throw UnimplementedError(); + + @override + int get headIndex => throw UnimplementedError(); + + @override + String? get headName => throw UnimplementedError(); + + @override + int get length => throw UnimplementedError(); + + @override + Uint8List? get quantizedEmbedding => throw UnimplementedError(); + + @override + EmbeddingType get type => throw UnimplementedError(); +} + +/// {@macro BaseOptions} +class BaseOptions extends BaseBaseOptions { + /// {@template BaseOptions.path} + /// Constructor for [BaseOptions] classes using a file system path. + /// + /// In practice, this is unsupported, as assets in Flutter are bundled into + /// the build output and not available on disk. However, it can potentially + /// be helpful for testing / development purposes. + /// {@endtemplate} + BaseOptions.path(String path); + + /// {@template BaseOptions.memory} + /// Constructor for [BaseOptions] classes using an in-memory pointer to the + /// MediaPipe SDK. + /// + /// In practice, this is the only option supported for production builds. + /// {@endtemplate} + BaseOptions.memory(Uint8List buffer); + + @override + Uint8List? get modelAssetBuffer => throw UnimplementedError(); + + @override + String? get modelAssetPath => throw UnimplementedError(); + + @override + BaseOptionsType get type => throw UnimplementedError(); +} + +/// {@macro ClassifierOptions} +class ClassifierOptions extends BaseClassifierOptions { + /// {@macro ClassifierOptions} + const ClassifierOptions({ + String? displayNamesLocale, + int? maxResults, + double? scoreThreshold, + List? categoryAllowlist, + List? categoryDenylist, + }); + + @override + List? get categoryAllowlist => throw UnimplementedError(); + + @override + List? get categoryDenylist => throw UnimplementedError(); + + @override + String? get displayNamesLocale => throw UnimplementedError(); + + @override + int? get maxResults => throw UnimplementedError(); + + @override + double? get scoreThreshold => throw UnimplementedError(); +} + +/// {@macro EmbedderOptions} +class EmbedderOptions extends BaseEmbedderOptions { + /// {@macro EmbedderOptions} + const EmbedderOptions({ + bool l2Normalize = false, + bool quantize = false, + }); + + @override + bool get l2Normalize => throw UnimplementedError(); + + @override + bool get quantize => throw UnimplementedError(); +} diff --git a/packages/mediapipe-core/pubspec.yaml b/packages/mediapipe-core/pubspec.yaml new file mode 100644 index 0000000..e72d0fd --- /dev/null +++ b/packages/mediapipe-core/pubspec.yaml @@ -0,0 +1,22 @@ +name: mediapipe_core +description: Shared logic and utilities required by other MediaPipe Task packages. +version: 0.0.1-wip +publish_to: none + +repository: https://github.com/google/flutter-mediapipe +issue_tracker: https://github.com/google/flutter-mediapipe/issues + +environment: + sdk: ^3.1.0-333.0.dev + +# Add regular dependencies here. +dependencies: + equatable: ^2.0.5 + ffi: ^2.1.0 + logging: ^1.2.0 + meta: ^1.12.0 + +dev_dependencies: + ffigen: ^9.0.1 + lints: ^2.0.0 + test: ^1.21.0 diff --git a/packages/mediapipe-core/test/io/containers_test.dart b/packages/mediapipe-core/test/io/containers_test.dart new file mode 100644 index 0000000..ab12123 --- /dev/null +++ b/packages/mediapipe-core/test/io/containers_test.dart @@ -0,0 +1,117 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:mediapipe_core/src/io/mediapipe_core.dart'; +import 'package:test/test.dart'; +import 'package:mediapipe_core/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart' + as core_bindings; +import 'package:mediapipe_core/src/io/test_utils.dart'; + +void main() { + group('Category.native should', () { + test('load a Category', () { + using((arena) { + final categoryPtr = arena(); + populateCategory(categoryPtr.ref); + + final category = Category.native(categoryPtr); + expect(category.index, 1); + expect(category.categoryName, 'Positive'); + expect(category.displayName, isNull); + expect(category.score, closeTo(0.9, 0.0001)); + }); + }); + + test('load a Category with a display name', () { + using((arena) { + final categoryPtr = arena(); + populateCategory(categoryPtr.ref, displayName: 'v-good'); + + final category = Category.native(categoryPtr); + expect(category.index, 1); + expect(category.categoryName, 'Positive'); + expect(category.displayName, 'v-good'); + expect(category.score, closeTo(0.9, 0.0001)); + }); + }); + + test('load a Category with no names', () { + using((arena) { + final categoryPtr = arena(); + populateCategory(categoryPtr.ref, categoryName: null); + + final category = Category.native(categoryPtr); + expect(category.index, 1); + expect(category.categoryName, isNull); + expect(category.displayName, isNull); + expect(category.score, closeTo(0.9, 0.0001)); + }); + }); + + test('should load a list of structs', () { + using((arena) { + final ptrs = arena(2); + populateCategory(ptrs[0]); + populateCategory( + ptrs[1], + categoryName: 'Negative', + index: 2, + score: 0.01, + ); + final categories = Category.fromNativeArray(ptrs, 2).toList(); + expect(categories, hasLength(2)); + expect(categories[0].categoryName, 'Positive'); + expect(categories[0].index, 1); + expect(categories[0].score, closeTo(0.9, 0.0001)); + + expect(categories[1].categoryName, 'Negative'); + expect(categories[1].index, 2); + expect(categories[1].score, closeTo(0.01, 0.0001)); + }); + }); + }); + + group('Classifications.native should', () { + test('load a Classifications object', () { + using((arena) { + final classificationsPtr = arena(); + populateClassifications(classificationsPtr.ref); + + final classifications = Classifications.native(classificationsPtr); + expect(classifications.headIndex, 1); + expect(classifications.headName, 'Head'); + expect(classifications.categories.length, 2); + expect(classifications.categories.first.categoryName, 'Positive'); + expect(classifications.categories.last.categoryName, 'Positive'); + }); + }); + + test('load a Classifications object with 1 category', () { + using((arena) { + final classificationsPtr = calloc(); + populateClassifications(classificationsPtr.ref, numCategories: 1); + + final classifications = Classifications.native(classificationsPtr); + expect(classifications.headIndex, 1); + expect(classifications.headName, 'Head'); + expect(classifications.categories.length, 1); + expect(classifications.categories.first.categoryName, 'Positive'); + }); + }); + + test('load a Classifications object with no categories', () { + using((arena) { + final classificationsPtr = arena(); + populateClassifications(classificationsPtr.ref, numCategories: 0); + + final classifications = Classifications.native(classificationsPtr); + expect(classifications.headIndex, 1); + expect(classifications.headName, 'Head'); + expect(classifications.categories.length, 0); + }); + }); + }); +} diff --git a/packages/mediapipe-core/test/io/embedding_test.dart b/packages/mediapipe-core/test/io/embedding_test.dart new file mode 100644 index 0000000..a997192 --- /dev/null +++ b/packages/mediapipe-core/test/io/embedding_test.dart @@ -0,0 +1,89 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'dart:typed_data'; +import 'package:ffi/ffi.dart'; +import 'package:test/test.dart'; +import 'package:mediapipe_core/io.dart'; +import 'package:mediapipe_core/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart' + as bindings; + +void main() { + group('Native embeddings should', () { + test('represent a float embedding pointer correctly', () { + final ptr = malloc(); + ptr.ref.float_embedding = Float32List.fromList([0.1, 0.2]).copyToNative(); + ptr.ref.values_count = 2; + ptr.ref.head_index = 2394723; + ptr.ref.head_name = 'Head Name'.copyToNative(); + + final embedding = Embedding.native(ptr); + expect(embedding.type, equals(EmbeddingType.float)); + expect(embedding.length, 2); + expect(embedding.floatEmbedding!.length, 2); + expect(embedding.floatEmbedding![0], closeTo(0.1, 0.0001)); + expect(embedding.floatEmbedding![1], closeTo(0.2, 0.0001)); + expect(embedding.headIndex, 2394723); + expect(embedding.headName, 'Head Name'); + }); + + test('represent a quantized embedding pointer correctly', () { + final ptr = malloc(); + ptr.ref.quantized_embedding = + Uint8List.fromList([3, 2, 1]).copyToNative(); + ptr.ref.values_count = 3; + ptr.ref.head_index = 999; + ptr.ref.head_name = 'Tail Name'.copyToNative(); + + final embedding = Embedding.native(ptr); + expect(embedding.type, equals(EmbeddingType.quantized)); + expect(embedding.quantizedEmbedding!.length, 3); + expect(embedding.quantizedEmbedding![0], 3); + expect(embedding.quantizedEmbedding![1], 2); + expect(embedding.quantizedEmbedding![2], 1); + expect(embedding.headIndex, 999); + expect(embedding.headName, 'Tail Name'); + }); + }); + + group('Embedding fakes should', () { + test('store values as floats', () { + final embedding = Embedding.float( + Float32List.fromList([0.1, 0.2, 0.3]), + headIndex: 99, + headName: 'Embedding', + ); + + expect(() => embedding.quantizedEmbedding, throwsA(isA())); + expect(embedding.headIndex, 99); + expect(embedding.headName, 'Embedding'); + expect(embedding.isFloat, isTrue); + expect(embedding.isQuantized, isFalse); + expect(embedding.floatEmbedding!.length, 3); + expect(embedding.floatEmbedding![0], closeTo(0.1, 0.0001)); + expect(embedding.floatEmbedding![1], closeTo(0.2, 0.0001)); + expect(embedding.floatEmbedding![2], closeTo(0.3, 0.0001)); + }); + + test('store values as quantized', () { + final embedding = Embedding.quantized( + Uint8List.fromList([2, 0, 3, 1]), + headIndex: 99, + headName: 'Embedding', + ); + + expect(() => embedding.floatEmbedding, throwsA(isA())); + expect(embedding.headIndex, 99); + expect(embedding.headName, 'Embedding'); + expect(embedding.isFloat, isFalse); + expect(embedding.isQuantized, isTrue); + expect(embedding.quantizedEmbedding!.length, 4); + expect(embedding.quantizedEmbedding![0], 2); + expect(embedding.quantizedEmbedding![1], 0); + expect(embedding.quantizedEmbedding![2], 3); + expect(embedding.quantizedEmbedding![3], 1); + }); + }); +} diff --git a/packages/mediapipe-core/test/io/ffi_utils_test.dart b/packages/mediapipe-core/test/io/ffi_utils_test.dart new file mode 100644 index 0000000..60726ef --- /dev/null +++ b/packages/mediapipe-core/test/io/ffi_utils_test.dart @@ -0,0 +1,122 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'dart:typed_data'; +import 'package:test/test.dart'; +import 'package:ffi/ffi.dart'; +import 'package:mediapipe_core/src/io/ffi_utils.dart'; + +void main() { + group('NullAwarePtr should', () { + test('be nullable', () { + Pointer? nul; + expect(nul, isNull); + expect(nul.isNullPointer, false); + expect(nul.isNotNullPointer, false); + expect(nul.isNotNullAndIsNotNullPointer, false); + expect(nul.isNullOrNullPointer, true); + }); + test('detect null pointers', () { + Pointer? nulPtr = nullptr; + expect(nulPtr, isNotNull); + expect(nulPtr.isNullPointer, true); + expect(nulPtr.isNotNullPointer, false); + expect(nulPtr.isNotNullAndIsNotNullPointer, false); + expect(nulPtr.isNullOrNullPointer, true); + }); + + test('detect null pointers without nullability', () { + Pointer n = nullptr; + expect(n, isNotNull); + expect(n.isNullPointer, true); + expect(n.isNotNullPointer, false); + expect(n.isNotNullAndIsNotNullPointer, false); + expect(n.isNullOrNullPointer, true); + }); + + test('detect real pointers', () { + Pointer? ptr = calloc(1); + expect(ptr, isNotNull); + expect(ptr.isNullPointer, false); + expect(ptr.isNotNullPointer, true); + expect(ptr.isNotNullAndIsNotNullPointer, true); + expect(ptr.isNullOrNullPointer, false); + calloc.free(ptr); + }); + + test('detect real pointers without nullability', () { + Pointer ptr = calloc(1); + expect(ptr, isNotNull); + expect(ptr.isNullPointer, false); + expect(ptr.isNotNullPointer, true); + expect(ptr.isNotNullAndIsNotNullPointer, true); + expect(ptr.isNullOrNullPointer, false); + calloc.free(ptr); + }); + }); + + group('Strings should', () { + test('be convertable to char*', () { + final Pointer abc = 'abc'.copyToNative(); + calloc.free(abc); + }); + + test('be round-trippable', () { + expect('abc'.copyToNative().toDartString(), 'abc'); + }); + }); + + group('Lists of Strings should', () { + test('be convertable to char**', () { + final Pointer> strings = ['abc'].copyToNative(); + calloc.free(strings); + }); + + test('be round-trippable', () { + expect(['abc'].copyToNative().toDartStrings(1), ['abc']); + }); + }); + + group('Uint8List should', () { + test('be convertable to Pointer', () { + final Pointer ptr = Uint8List.fromList([1, 2, 3]).copyToNative(); + calloc.free(ptr); + }); + + test('be round-trippable', () { + expect( + Uint8List.fromList([1, 2, 3]).copyToNative().toUint8List(3).toList(), + [1, 2, 3], + ); + }); + + test('copy memory with toList', () { + final ptr = Uint8List.fromList([1, 2, 3]).copyToNative(); + final List ints = ptr.toUint8List(3).toList(); + calloc.free(ptr); + expect(ints[0], 1); + expect(ints[1], 2); + expect(ints[2], 3); + }); + }); + + group('Float32List should', () { + test('be convertable to Pointer', () { + final Pointer ptr = + Float32List.fromList([1.0, 2.0, 3.1]).copyToNative(); + calloc.free(ptr); + }); + + test('be round-trippable', () { + final doubles = Float32List.fromList([1.1, 2.2, 3.3]) + .copyToNative() + .toFloat32List(3) + .toList(); + expect(doubles[0], closeTo(1.1, 0.0001)); + expect(doubles[1], closeTo(2.2, 0.0001)); + expect(doubles[2], closeTo(3.3, 0.0001)); + }); + }); +} diff --git a/packages/mediapipe-core/test/io/task_options_test.dart b/packages/mediapipe-core/test/io/task_options_test.dart new file mode 100644 index 0000000..31f5b0d --- /dev/null +++ b/packages/mediapipe-core/test/io/task_options_test.dart @@ -0,0 +1,103 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; +import 'package:mediapipe_core/io.dart'; +import 'package:mediapipe_core/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart' + as bindings; +import 'package:test/test.dart'; + +void main() { + group('BaseOptions.toStruct/structToDart should', () { + test('allocate memory in C for a modelAssetPath', () { + using((arena) { + final ptr = arena(); + final options = BaseOptions.path('abc'); + options.assignToStruct(ptr.ref); + expect(ptr.ref.model_asset_path.toDartString(), 'abc'); + expectNullPtr(ptr.ref.model_asset_buffer); + }); + }); + + test('allocate memory in C for a modelAssetBuffer', () { + using((arena) { + final ptr = arena(); + final options = BaseOptions.memory(Uint8List.fromList([1, 2, 3])); + options.assignToStruct(ptr.ref); + expect( + ptr.ref.model_asset_buffer.toUint8List(3), + Uint8List.fromList([1, 2, 3]), + ); + expectNullPtr(ptr.ref.model_asset_path); + }); + }); + + test('allocate memory in C for a modelAssetBuffer with a lower length', () { + using((arena) { + final ptr = arena(); + final options = BaseOptions.memory(Uint8List.fromList([1, 2, 0, 3])); + options.assignToStruct(ptr.ref); + expect( + ptr.ref.model_asset_buffer.toUint8List(2), + Uint8List.fromList([1, 2]), + ); + expectNullPtr(ptr.ref.model_asset_path); + }); + }); + }); + + group('ClassifierOptions should', () { + test('allocate memory for empty fields', () { + using((arena) { + final ptr = arena(); + final options = ClassifierOptions(); + + options.assignToStruct(ptr.ref); + + expect(ptr.ref.max_results, -1); + expect(ptr.ref.score_threshold, 0.0); + expectNullPtr(ptr.ref.category_allowlist); + expect(ptr.ref.category_allowlist_count, 0); + expectNullPtr(ptr.ref.category_denylist); + expect(ptr.ref.category_denylist_count, 0); + expectNullPtr(ptr.ref.display_names_locale); + }); + }); + + test('allocate memory for full fields', () { + using((arena) { + final ptr = arena(); + final options = ClassifierOptions( + displayNamesLocale: 'en', + maxResults: 5, + scoreThreshold: 0.9, + categoryAllowlist: ['good', 'great', 'best'], + categoryDenylist: ['bad', 'terrible', 'worst', 'honestly come on'], + ); + options.assignToStruct(ptr.ref); + + expect(ptr.ref.display_names_locale.toDartString(), 'en'); + expect(ptr.ref.max_results, 5); + expect(ptr.ref.score_threshold, greaterThan(0.8999)); + expect(ptr.ref.score_threshold, lessThan(0.90001)); + expect(ptr.ref.category_allowlist_count, 3); + expect( + ptr.ref.category_allowlist.toDartStrings(3), + ['good', 'great', 'best'], + ); + + expect(ptr.ref.category_denylist_count, 4); + expect( + ptr.ref.category_denylist.toDartStrings(4), + ['bad', 'terrible', 'worst', 'honestly come on'], + ); + }); + }); + }); +} + +void expectNullPtr(Pointer ptr) => expect(ptr.address, nullptr.address); diff --git a/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/containers/category.h b/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/containers/category.h new file mode 100644 index 0000000..0338311 --- /dev/null +++ b/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/containers/category.h @@ -0,0 +1,58 @@ +/* Copyright 2023 The MediaPipe Authors. + +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. +==============================================================================*/ + +#ifndef MEDIAPIPE_TASKS_C_COMPONENTS_CONTAINERS_CATEGORY_H_ +#define MEDIAPIPE_TASKS_C_COMPONENTS_CONTAINERS_CATEGORY_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Defines a single classification result. +// +// The label maps packed into the TFLite Model Metadata [1] are used to populate +// the 'category_name' and 'display_name' fields. +// +// [1]: https://www.tensorflow.org/lite/convert/metadata +struct Category { + // The index of the category in the classification model output. + int index; + + // The score for this category, e.g. (but not necessarily) a probability in + // [0,1]. + float score; + + // The optional ID for the category, read from the label map packed in the + // TFLite Model Metadata if present. Not necessarily human-readable. + char* category_name; + + // The optional human-readable name for the category, read from the label map + // packed in the TFLite Model Metadata if present. + char* display_name; +}; + +// A list of categories. +struct Categories { + struct Category* categories; + uint32_t categories_count; +}; + +#ifdef __cplusplus +} // extern C +#endif + +#endif // MEDIAPIPE_TASKS_C_COMPONENTS_CONTAINERS_CATEGORY_H_ diff --git a/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/containers/classification_result.h b/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/containers/classification_result.h new file mode 100644 index 0000000..43bb87b --- /dev/null +++ b/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/containers/classification_result.h @@ -0,0 +1,70 @@ +/* Copyright 2023 The MediaPipe Authors. + +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. +==============================================================================*/ + +#ifndef MEDIAPIPE_TASKS_C_COMPONENTS_CONTAINERS_CLASSIFICATION_RESULT_H_ +#define MEDIAPIPE_TASKS_C_COMPONENTS_CONTAINERS_CLASSIFICATION_RESULT_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Defines classification results for a given classifier head. +struct Classifications { + // The array of predicted categories, usually sorted by descending scores, + // e.g. from high to low probability. + struct Category* categories; + + // The number of elements in the categories array. + uint32_t categories_count; + + // The index of the classifier head (i.e. output tensor) these categories + // refer to. This is useful for multi-head models. + int head_index; + + // The optional name of the classifier head, as provided in the TFLite Model + // Metadata [1] if present. This is useful for multi-head models. + // + // [1]: https://www.tensorflow.org/lite/convert/metadata + char* head_name; +}; + +// Defines classification results of a model. +struct ClassificationResult { + // The classification results for each head of the model. + struct Classifications* classifications; + // The number of classifications in the classifications array. + uint32_t classifications_count; + + // The optional timestamp (in milliseconds) of the start of the chunk of data + // corresponding to these results. + // + // This is only used for classification on time series (e.g. audio + // classification). In these use cases, the amount of data to process might + // exceed the maximum size that the model can process: to solve this, the + // input data is split into multiple chunks starting at different timestamps. + int64_t timestamp_ms; + + // Specifies whether the timestamp contains a valid value. + bool has_timestamp_ms; +}; + +#ifdef __cplusplus +} // extern C +#endif + +#endif // MEDIAPIPE_TASKS_C_COMPONENTS_CONTAINERS_CLASSIFICATION_RESULT_H_ diff --git a/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/containers/embedding_result.h b/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/containers/embedding_result.h new file mode 100644 index 0000000..09acadc --- /dev/null +++ b/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/containers/embedding_result.h @@ -0,0 +1,80 @@ +/* Copyright 2023 The MediaPipe Authors. + +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. +==============================================================================*/ + +#ifndef MEDIAPIPE_TASKS_C_COMPONENTS_CONTAINERS_EMBEDDING_RESULT_H_ +#define MEDIAPIPE_TASKS_C_COMPONENTS_CONTAINERS_EMBEDDING_RESULT_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + + // Embedding result for a given embedder head. + // + // One and only one of the two 'float_embedding' and 'quantized_embedding' will + // contain data, based on whether or not the embedder was configured to perform + // scalar quantization. + struct Embedding { + // Floating-point embedding. `nullptr` if the embedder was configured to + // perform scalar-quantization. + float *float_embedding; + + // Scalar-quantized embedding. `nullptr` if the embedder was not configured to + // perform scalar quantization. + char *quantized_embedding; + + // Keep the count of embedding values. + uint32_t values_count; + + // The index of the embedder head (i.e. output tensor) this embedding comes + // from. This is useful for multi-head models. + int head_index; + + // The optional name of the embedder head, as provided in the TFLite Model + // Metadata [1] if present. This is useful for multi-head models. + // Defaults to nullptr. + // + // [1]: https://www.tensorflow.org/lite/convert/metadata + char *head_name; + }; + + // Defines embedding results of a model. + struct EmbeddingResult { + // The embedding results for each head of the model. + struct Embedding *embeddings; + + // Keep the count of embeddings. + uint32_t embeddings_count; + + // The optional timestamp (in milliseconds) of the start of the chunk of data + // corresponding to these results. + // + // This is only used for embedding extraction on time series (e.g. audio + // embedding). In these use cases, the amount of data to process might + // exceed the maximum size that the model can process: to solve this, the + // input data is split into multiple chunks starting at different timestamps. + int64_t timestamp_ms; + + // Specifies whether the timestamp contains a valid value. + bool has_timestamp_ms; + }; + +#ifdef __cplusplus +} // extern C +#endif + +#endif // MEDIAPIPE_TASKS_C_COMPONENTS_CONTAINERS_EMBEDDING_RESULT_H_ diff --git a/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/processors/classifier_options.h b/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/processors/classifier_options.h new file mode 100644 index 0000000..be82bc7 --- /dev/null +++ b/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/processors/classifier_options.h @@ -0,0 +1,61 @@ +/* Copyright 2023 The MediaPipe Authors. + +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. +==============================================================================*/ + +#ifndef MEDIAPIPE_TASKS_C_COMPONENTS_PROCESSORS_CLASSIFIER_OPTIONS_H_ +#define MEDIAPIPE_TASKS_C_COMPONENTS_PROCESSORS_CLASSIFIER_OPTIONS_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Classifier options for MediaPipe C classification Tasks. +struct ClassifierOptions { + // The locale to use for display names specified through the TFLite Model + // Metadata, if any. Defaults to English. + const char* display_names_locale; + + // The maximum number of top-scored classification results to return. If < 0, + // all available results will be returned. If 0, an invalid argument error is + // returned. + int max_results; + + // Score threshold to override the one provided in the model metadata (if + // any). Results below this value are rejected. + float score_threshold; + + // The 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 category_denylist. + const char** category_allowlist; + + // The number of elements in the category allowlist. + uint32_t category_allowlist_count; + + // The 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 category_allowlist. + const char** category_denylist; + + // The number of elements in the category denylist. + uint32_t category_denylist_count; +}; + +#ifdef __cplusplus +} // extern C +#endif + +#endif // MEDIAPIPE_TASKS_C_COMPONENTS_PROCESSORS_CLASSIFIER_OPTIONS_H_ diff --git a/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/processors/embedder_options.h b/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/processors/embedder_options.h new file mode 100644 index 0000000..cb98f7d --- /dev/null +++ b/packages/mediapipe-core/third_party/mediapipe/tasks/c/components/processors/embedder_options.h @@ -0,0 +1,44 @@ +/* Copyright 2023 The MediaPipe Authors. + +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. +==============================================================================*/ + +#ifndef MEDIAPIPE_TASKS_C_COMPONENTS_PROCESSORS_EMBEDDER_OPTIONS_H_ +#define MEDIAPIPE_TASKS_C_COMPONENTS_PROCESSORS_EMBEDDER_OPTIONS_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Embedder options for MediaPipe C embedding extraction tasks. +struct EmbedderOptions { + // Whether to normalize the returned feature vector with L2 norm. Use this + // option only if the model does not already contain a native L2_NORMALIZATION + // TF Lite Op. In most cases, this is already the case and L2 norm is thus + // achieved through TF Lite inference. + bool l2_normalize; + + // Whether the returned embedding should be quantized to bytes via scalar + // quantization. Embeddings are implicitly assumed to be unit-norm and + // therefore any dimension is guaranteed to have a value in [-1.0, 1.0]. Use + // the l2_normalize option if this is not the case. + bool quantize; +}; + +#ifdef __cplusplus +} // extern C +#endif + +#endif // MEDIAPIPE_TASKS_C_COMPONENTS_PROCESSORS_EMBEDDER_OPTIONS_H_ diff --git a/packages/mediapipe-core/third_party/mediapipe/tasks/c/core/base_options.h b/packages/mediapipe-core/third_party/mediapipe/tasks/c/core/base_options.h new file mode 100644 index 0000000..20c068a --- /dev/null +++ b/packages/mediapipe-core/third_party/mediapipe/tasks/c/core/base_options.h @@ -0,0 +1,39 @@ +/* Copyright 2023 The MediaPipe Authors. + +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. +==============================================================================*/ + +#ifndef MEDIAPIPE_TASKS_C_CORE_BASE_OPTIONS_H_ +#define MEDIAPIPE_TASKS_C_CORE_BASE_OPTIONS_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +// Base options for MediaPipe C Tasks. +struct BaseOptions { + // The model asset file contents as bytes. + const char* model_asset_buffer; + + // The size of the model assets buffer (or `0` if not set). + unsigned int model_asset_buffer_count; + + // The path to the model asset to open and mmap in memory. + const char* model_asset_path; +}; + +#ifdef __cplusplus +} // extern C +#endif + +#endif // MEDIAPIPE_TASKS_C_CORE_BASE_OPTIONS_H_ diff --git a/packages/mediapipe-task-audio/sdk_downloads.dart b/packages/mediapipe-task-audio/sdk_downloads.dart new file mode 100644 index 0000000..1f0512e --- /dev/null +++ b/packages/mediapipe-task-audio/sdk_downloads.dart @@ -0,0 +1,2 @@ +// Generated file. Do not manually edit. +final Map> sdkDownloadUrls = {}; diff --git a/packages/mediapipe-task-text/.gitignore b/packages/mediapipe-task-text/.gitignore new file mode 100644 index 0000000..9edb1d5 --- /dev/null +++ b/packages/mediapipe-task-text/.gitignore @@ -0,0 +1,38 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ + + +# Test artifacts (these are re-compiled before running tests) +test/c/fake_text_classifier +test/c/fake_text_classifier.dylib +test/c/fake_text_classifier.o + +**/assets/libtext_classifier.dylib diff --git a/packages/mediapipe-task-text/.metadata b/packages/mediapipe-task-text/.metadata new file mode 100644 index 0000000..e498da5 --- /dev/null +++ b/packages/mediapipe-task-text/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "efbf63d9c66b9f6ec30e9ad4611189aa80003d31" + channel: "stable" + +project_type: package diff --git a/packages/mediapipe-task-text/CHANGELOG.md b/packages/mediapipe-task-text/CHANGELOG.md new file mode 100644 index 0000000..b78d64c --- /dev/null +++ b/packages/mediapipe-task-text/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +- Initial version. diff --git a/packages/mediapipe-task-text/analysis_options.yaml b/packages/mediapipe-task-text/analysis_options.yaml new file mode 100644 index 0000000..d2553f4 --- /dev/null +++ b/packages/mediapipe-task-text/analysis_options.yaml @@ -0,0 +1,9 @@ +include: ../analysis_options.yaml + +linter: + rules: + - public_member_api_docs # see https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#documentation-dartdocs-javadocs-etc + +analyzer: + exclude: + - "**/mediapipe_text_bindings.dart" diff --git a/packages/mediapipe-task-text/build.dart b/packages/mediapipe-task-text/build.dart new file mode 100644 index 0000000..b8a757c --- /dev/null +++ b/packages/mediapipe-task-text/build.dart @@ -0,0 +1,103 @@ +import 'dart:io'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as path; + +import 'sdk_downloads.dart'; + +late File logFile; + +final logs = <(DateTime, String)>[]; +void log(String msg) { + logs.add((DateTime.now(), msg)); + if (!logFile.parent.existsSync()) { + logFile.parent.createSync(); + } + + if (logFile.existsSync()) { + logFile.deleteSync(); + } + logFile.createSync(); + logFile.writeAsStringSync(logs + .map((rec) => '[${rec.$1.toIso8601String()}] ${rec.$2}') + .toList() + .join('\n\n')); +} + +Future main(List args) async { + final buildConfig = await BuildConfig.fromArgs(args); + logFile = File( + path.joinAll([ + Directory.current.path, // root dir of app using `mediapipe-task-xyz` + 'build/${buildConfig.dryRun ? "dryrun" : "live-run"}-build-log.txt', + ]), + ); + + log(args.join(' ')); + final String targetOs = buildConfig.targetOs.toString(); + + log('dir.current: ${Directory.current.absolute.path}'); + + // Throw if target runtime is unsupported. + if (!sdkDownloadUrls.containsKey(targetOs)) { + throw Exception('Unsupported target OS: $targetOs. ' + 'Supported values are: ${sdkDownloadUrls.keys.toSet()}'); + } + + final buildOutput = BuildOutput(); + buildOutput.dependencies.dependencies + .add(buildConfig.packageRoot.resolve('build.dart')); + buildOutput.dependencies.dependencies + .add(buildConfig.packageRoot.resolve('sdk_downloads.dart')); + + // final archKeys = sdkDownloadUrls[targetOs]!.keys; + final Iterable archKeys; + if (buildConfig.dryRun) { + archKeys = sdkDownloadUrls[targetOs]!.keys; + } else { + archKeys = [buildConfig.targetArchitecture.toString()]; + } + for (final String arch in archKeys) { + final assetUrl = sdkDownloadUrls[targetOs]![arch]!; + final downloadFileLocation = buildConfig.outDir.resolve( + '${arch}_${assetUrl.split('/').last}', + ); + log('downloadFileLocation: $downloadFileLocation'); + buildOutput.assets.add( + Asset( + id: 'package:mediapipe_text/src/io/third_party/mediapipe/generated/mediapipe_text_bindings.dart', + linkMode: LinkMode.dynamic, + target: Target.fromArchitectureAndOs( + Architecture.fromString(arch), buildConfig.targetOs), + path: AssetAbsolutePath(downloadFileLocation), + ), + ); + if (!buildConfig.dryRun) { + downloadAsset(assetUrl, downloadFileLocation); + } + } + + await buildOutput.writeToFile(outDir: buildConfig.outDir); +} + +Future downloadAsset(String assetUrl, Uri destinationFile) async { + final downloadUri = Uri.parse(assetUrl); + final downloadedFile = File(destinationFile.toFilePath()); + print('Saving file to ${downloadedFile.absolute.path}'); + + final downloadResponse = await http.get(downloadUri); + log('Download response: ${downloadResponse.statusCode}'); + + if (downloadResponse.statusCode == 200) { + if (downloadedFile.existsSync()) { + downloadedFile.deleteSync(); + } + downloadedFile.createSync(); + log('Saved file to ${downloadedFile.absolute.path}\n'); + downloadedFile.writeAsBytes(downloadResponse.bodyBytes); + } else { + log('${downloadResponse.statusCode} :: ${downloadResponse.body}'); + throw Exception( + '${downloadResponse.statusCode} :: ${downloadResponse.body}'); + } +} diff --git a/packages/mediapipe-task-text/dart_test.yaml b/packages/mediapipe-task-text/dart_test.yaml new file mode 100644 index 0000000..2cecf55 --- /dev/null +++ b/packages/mediapipe-task-text/dart_test.yaml @@ -0,0 +1,3 @@ +tags: + # Integration tests that talk to C code bundled via the native-assets feature + native-assets: diff --git a/packages/mediapipe-task-text/example/.gitignore b/packages/mediapipe-task-text/example/.gitignore new file mode 100644 index 0000000..6a67208 --- /dev/null +++ b/packages/mediapipe-task-text/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +assets/** diff --git a/packages/mediapipe-task-text/example/.metadata b/packages/mediapipe-task-text/example/.metadata new file mode 100644 index 0000000..767f80e --- /dev/null +++ b/packages/mediapipe-task-text/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "995a020e48aa6627ea3f904a59dd56349a819eb4" + channel: "master" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 995a020e48aa6627ea3f904a59dd56349a819eb4 + base_revision: 995a020e48aa6627ea3f904a59dd56349a819eb4 + - platform: android + create_revision: 995a020e48aa6627ea3f904a59dd56349a819eb4 + base_revision: 995a020e48aa6627ea3f904a59dd56349a819eb4 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/mediapipe-task-text/example/README.md b/packages/mediapipe-task-text/example/README.md new file mode 100644 index 0000000..1b7a4e3 --- /dev/null +++ b/packages/mediapipe-task-text/example/README.md @@ -0,0 +1,3 @@ +# example + +A new Flutter project. diff --git a/packages/mediapipe-task-text/example/analysis_options.yaml b/packages/mediapipe-task-text/example/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/packages/mediapipe-task-text/example/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/packages/mediapipe-task-text/example/android/.gitignore b/packages/mediapipe-task-text/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/packages/mediapipe-task-text/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/mediapipe-task-text/example/android/app/build.gradle b/packages/mediapipe-task-text/example/android/app/build.gradle new file mode 100644 index 0000000..e28b4e7 --- /dev/null +++ b/packages/mediapipe-task-text/example/android/app/build.gradle @@ -0,0 +1,67 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.example" + compileSdk flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/packages/mediapipe-task-text/example/android/app/src/debug/AndroidManifest.xml b/packages/mediapipe-task-text/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/packages/mediapipe-task-text/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/mediapipe-task-text/example/android/app/src/main/AndroidManifest.xml b/packages/mediapipe-task-text/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..aff7dec --- /dev/null +++ b/packages/mediapipe-task-text/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/mediapipe-task-text/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/mediapipe-task-text/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 0000000..70f8f08 --- /dev/null +++ b/packages/mediapipe-task-text/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/packages/mediapipe-task-text/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/mediapipe-task-text/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/packages/mediapipe-task-text/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/mediapipe-task-text/example/android/app/src/main/res/drawable/launch_background.xml b/packages/mediapipe-task-text/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/packages/mediapipe-task-text/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/packages/mediapipe-task-text/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/mediapipe-task-text/example/android/app/src/main/res/values-night/styles.xml b/packages/mediapipe-task-text/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/packages/mediapipe-task-text/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/mediapipe-task-text/example/android/app/src/main/res/values/styles.xml b/packages/mediapipe-task-text/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/packages/mediapipe-task-text/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/mediapipe-task-text/example/android/app/src/profile/AndroidManifest.xml b/packages/mediapipe-task-text/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/packages/mediapipe-task-text/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/mediapipe-task-text/example/android/build.gradle b/packages/mediapipe-task-text/example/android/build.gradle new file mode 100644 index 0000000..bc157bd --- /dev/null +++ b/packages/mediapipe-task-text/example/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/packages/mediapipe-task-text/example/android/gradle.properties b/packages/mediapipe-task-text/example/android/gradle.properties new file mode 100644 index 0000000..598d13f --- /dev/null +++ b/packages/mediapipe-task-text/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/mediapipe-task-text/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/mediapipe-task-text/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/packages/mediapipe-task-text/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/packages/mediapipe-task-text/example/android/settings.gradle b/packages/mediapipe-task-text/example/android/settings.gradle new file mode 100644 index 0000000..1d6d19b --- /dev/null +++ b/packages/mediapipe-task-text/example/android/settings.gradle @@ -0,0 +1,26 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} + +include ":app" diff --git a/packages/mediapipe-task-text/example/ios/.gitignore b/packages/mediapipe-task-text/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/mediapipe-task-text/example/ios/Flutter/AppFrameworkInfo.plist b/packages/mediapipe-task-text/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/packages/mediapipe-task-text/example/ios/Flutter/Debug.xcconfig b/packages/mediapipe-task-text/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/mediapipe-task-text/example/ios/Flutter/Release.xcconfig b/packages/mediapipe-task-text/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/mediapipe-task-text/example/ios/Podfile b/packages/mediapipe-task-text/example/ios/Podfile new file mode 100644 index 0000000..fdcc671 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.pbxproj b/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..05db82f --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,722 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 71879C8BE77D4D5612095357 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEDA1EC44568C4B37A9B3036 /* Pods_RunnerTests.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + CE036829A87D8829E6696512 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E3C39DBDCEC441A0FB3A9F6F /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1915FE89322DC8DA9FFBD472 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 251E7F79C4C4F22EC81E392F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 310ACF9BCE8ADE38F76AE396 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 61A51F982B5BB43CCAA4118C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8D741F75DA7F833954FC0919 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + ABAE2333F7B8485760E2779F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + BEDA1EC44568C4B37A9B3036 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E3C39DBDCEC441A0FB3A9F6F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CE036829A87D8829E6696512 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AF22DC6A7495BDDAB5D66A99 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 71879C8BE77D4D5612095357 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 35C9234736401A2B4BA4E5F1 /* Pods */ = { + isa = PBXGroup; + children = ( + ABAE2333F7B8485760E2779F /* Pods-Runner.debug.xcconfig */, + 1915FE89322DC8DA9FFBD472 /* Pods-Runner.release.xcconfig */, + 61A51F982B5BB43CCAA4118C /* Pods-Runner.profile.xcconfig */, + 310ACF9BCE8ADE38F76AE396 /* Pods-RunnerTests.debug.xcconfig */, + 251E7F79C4C4F22EC81E392F /* Pods-RunnerTests.release.xcconfig */, + 8D741F75DA7F833954FC0919 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 930208CDA6AD96300EFA759A /* Frameworks */ = { + isa = PBXGroup; + children = ( + E3C39DBDCEC441A0FB3A9F6F /* Pods_Runner.framework */, + BEDA1EC44568C4B37A9B3036 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 35C9234736401A2B4BA4E5F1 /* Pods */, + 930208CDA6AD96300EFA759A /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 62FED8461A298492BBA724F2 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + AF22DC6A7495BDDAB5D66A99 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 0D52ECC91CAEA2B20F0D8A4D /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 6F7441E5527CBB4F4A5EC94A /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0D52ECC91CAEA2B20F0D8A4D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 62FED8461A298492BBA724F2 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 6F7441E5527CBB4F4A5EC94A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 310ACF9BCE8ADE38F76AE396 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 251E7F79C4C4F22EC81E392F /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8D741F75DA7F833954FC0919 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..87131a0 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/mediapipe-task-text/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/mediapipe-task-text/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/mediapipe-task-text/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/mediapipe-task-text/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/mediapipe-task-text/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/mediapipe-task-text/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/mediapipe-task-text/example/ios/Runner/AppDelegate.swift b/packages/mediapipe-task-text/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/mediapipe-task-text/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/mediapipe-task-text/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/mediapipe-task-text/example/ios/Runner/Base.lproj/Main.storyboard b/packages/mediapipe-task-text/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/mediapipe-task-text/example/ios/Runner/Info.plist b/packages/mediapipe-task-text/example/ios/Runner/Info.plist new file mode 100644 index 0000000..5458fc4 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/mediapipe-task-text/example/ios/Runner/Runner-Bridging-Header.h b/packages/mediapipe-task-text/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/mediapipe-task-text/example/ios/RunnerTests/RunnerTests.swift b/packages/mediapipe-task-text/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/packages/mediapipe-task-text/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/mediapipe-task-text/example/lib/enumerate.dart b/packages/mediapipe-task-text/example/lib/enumerate.dart new file mode 100644 index 0000000..d0e7011 --- /dev/null +++ b/packages/mediapipe-task-text/example/lib/enumerate.dart @@ -0,0 +1,13 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +extension Enumeratable on List { + Iterable enumerate(S Function(T, int) fn) sync* { + int count = 0; + while (count < length) { + yield fn(this[count], count); + count++; + } + } +} diff --git a/packages/mediapipe-task-text/example/lib/logging.dart b/packages/mediapipe-task-text/example/lib/logging.dart new file mode 100644 index 0000000..1b89afe --- /dev/null +++ b/packages/mediapipe-task-text/example/lib/logging.dart @@ -0,0 +1,21 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; +import 'package:logging/logging.dart'; + +final log = Logger('TextTask'); + +void initLogging() { + Logger.root.level = Level.FINEST; + Logger.root.onRecord.listen((record) { + io.stdout.writeln('${record.level.name} [${record.loggerName}]' + '[' + '${record.time.hour.toString()}:' + '${record.time.minute.toString().padLeft(2, "0")}:' + '${record.time.second.toString().padLeft(2, "0")}.' + '${record.time.millisecond.toString().padRight(3, "0")}' + '] ${record.message}'); + }); +} diff --git a/packages/mediapipe-task-text/example/lib/main.dart b/packages/mediapipe-task-text/example/lib/main.dart new file mode 100644 index 0000000..db9ad11 --- /dev/null +++ b/packages/mediapipe-task-text/example/lib/main.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'logging.dart'; +import 'text_classification_demo.dart'; +import 'text_embedding_demo.dart'; + +void main() { + initLogging(); + runApp(const MainApp()); +} + +class MainApp extends StatefulWidget { + const MainApp({super.key}); + + @override + State createState() => _MainAppState(); +} + +class _MainAppState extends State { + @override + Widget build(BuildContext context) => + const MaterialApp(home: TextTaskPages()); +} + +class TextTaskPages extends StatefulWidget { + const TextTaskPages({super.key}); + + @override + State createState() => TextTaskPagesState(); +} + +class TextTaskPagesState extends State { + final PageController controller = PageController(); + + final titles = ['Classify', 'Embed']; + int titleIndex = 0; + + void switchToPage(int index) { + controller.animateToPage( + index, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + setState(() { + titleIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + const activeTextStyle = TextStyle( + fontWeight: FontWeight.bold, + color: Colors.orange, + ); + const inactiveTextStyle = TextStyle( + color: Colors.white, + ); + return Scaffold( + appBar: AppBar(title: Text(titles[titleIndex])), + body: PageView( + controller: controller, + children: const [ + TextClassificationDemo(), + TextEmbeddingDemo(), + ], + ), + bottomNavigationBar: ColoredBox( + color: Colors.blueGrey, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: () => switchToPage(0), + child: Text( + 'Classify', + style: titleIndex == 0 ? activeTextStyle : inactiveTextStyle, + ), + ), + TextButton( + onPressed: () => switchToPage(1), + child: Text( + 'Embed', + style: titleIndex == 1 ? activeTextStyle : inactiveTextStyle, + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/mediapipe-task-text/example/lib/text_classification_demo.dart b/packages/mediapipe-task-text/example/lib/text_classification_demo.dart new file mode 100644 index 0000000..38266e9 --- /dev/null +++ b/packages/mediapipe-task-text/example/lib/text_classification_demo.dart @@ -0,0 +1,114 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:mediapipe_text/mediapipe_text.dart'; +import 'logging.dart'; + +class TextClassificationDemo extends StatefulWidget { + const TextClassificationDemo({super.key, this.classifier}); + + final TextClassifier? classifier; + + @override + State createState() => _TextClassificationDemoState(); +} + +class _TextClassificationDemoState extends State { + final TextEditingController _controller = TextEditingController(); + List results = []; + String? _isProcessing; + + TextClassifier? _classifier; + Completer? _completer; + + @override + void initState() { + super.initState(); + _controller.text = 'Hello, world!'; + _initClassifier(); + } + + Future get classifier { + if (widget.classifier != null) { + return Future.value(widget.classifier!); + } + if (_completer == null) { + _initClassifier(); + } + return _completer!.future; + } + + Future _initClassifier() async { + _classifier?.dispose(); + _completer = Completer(); + ByteData? classifierBytes = await DefaultAssetBundle.of(context) + .load('assets/bert_classifier.tflite'); + + TextClassifier classifier = TextClassifier( + TextClassifierOptions.fromAssetBuffer( + classifierBytes.buffer.asUint8List(), + ), + ); + _completer!.complete(classifier); + classifierBytes = null; + } + + void _prepareForClassification() { + setState(() { + _isProcessing = _controller.text; + results.add(const CircularProgressIndicator.adaptive()); + }); + } + + void _showClassificationResults(TextClassifierResult classification) { + setState(() { + final categoryName = + classification.firstClassification?.firstCategory?.categoryName; + final score = classification.firstClassification?.firstCategory?.score; + // Replace "..." with the results + final message = '"$_isProcessing" $categoryName :: $score'; + log.info(message); + results.last = Card( + key: Key('Classification::"$_isProcessing" ${results.length}'), + margin: const EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.all(10), + child: Text(message), + ), + ); + _isProcessing = null; + }); + } + + Future _classify() async { + _prepareForClassification(); + final classification = await (await classifier).classify(_controller.text); + _showClassificationResults(classification); + } + + @override + Widget build(BuildContext context) => // + Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + TextField(controller: _controller), + ...results, + ], + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _isProcessing != null && _controller.text != '' + ? null + : _classify, + child: const Icon(Icons.search), + ), + ); +} diff --git a/packages/mediapipe-task-text/example/lib/text_embedding_demo.dart b/packages/mediapipe-task-text/example/lib/text_embedding_demo.dart new file mode 100644 index 0000000..9376fd4 --- /dev/null +++ b/packages/mediapipe-task-text/example/lib/text_embedding_demo.dart @@ -0,0 +1,452 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:getwidget/getwidget.dart'; +import 'package:logging/logging.dart'; +import 'package:mediapipe_core/mediapipe_core.dart'; +import 'package:mediapipe_text/mediapipe_text.dart'; +import 'enumerate.dart'; + +final _log = Logger('TextEmbeddingDemo'); + +class TextEmbeddingDemo extends StatefulWidget { + const TextEmbeddingDemo({super.key, this.embedder}); + + /// Overriding [BaseTextEmbedder] object. If supplied, the Embedder + /// configuration controls cannot be used. + final TextEmbedder? embedder; + + @override + State createState() => _TextEmbeddingDemoState(); +} + +class _TextEmbeddingDemoState extends State { + final TextEditingController _controller = TextEditingController(); + List feed = []; + EmbeddingType type = EmbeddingType.quantized; + bool l2Normalize = true; + + TextEmbedder? _embedder; + Completer? _completer; + + @override + void initState() { + super.initState(); + _controller.text = 'Embed me'; + _initEmbedder(); + } + + @override + void dispose() { + final resultsIter = feed.where( + (el) => el._type == _EmbeddingFeedItemType.result, + ); + for (final feedItem in resultsIter) { + feedItem.embeddingResult!.result?.dispose(); + } + super.dispose(); + } + + Future get embedder { + if (widget.embedder != null) { + return Future.value(widget.embedder!); + } + if (_completer == null) { + _initEmbedder(); + } + return _completer!.future; + } + + void toggleMode() { + assert( + widget.embedder == null, + 'Changing embedder configuration not supported when an embedder is ' + 'supplied to the widget.', + ); + setState(() => type = type.opposite); + _initEmbedder(); + } + + void toggleL2Normalize() { + assert( + widget.embedder == null, + 'Changing embedder configuration not supported when an embedder is ' + 'supplied to the widget.', + ); + setState(() => l2Normalize = !l2Normalize); + _initEmbedder(); + } + + Future _initEmbedder() async { + _embedder?.dispose(); + _completer = Completer(); + + ByteData? embedderBytes = await DefaultAssetBundle.of(context) + .load('assets/universal_sentence_encoder.tflite'); + + _embedder = TextEmbedder( + TextEmbedderOptions.fromAssetBuffer( + embedderBytes.buffer.asUint8List(), + embedderOptions: EmbedderOptions( + l2Normalize: l2Normalize, + quantize: type == EmbeddingType.quantized, + ), + ), + ); + _completer!.complete(_embedder); + embedderBytes = null; + } + + void _prepareForEmbedding() { + setState(() { + feed.add( + EmbeddingFeedItem.result( + TextWithEmbedding( + computedAt: DateTime.now(), + l2Normalized: l2Normalize, + value: _controller.text, + ), + ), + ); + }); + } + + void _showEmbeddingResults(TextEmbedderResult result) { + setState(() { + if (result.embeddings.isEmpty) return; + + // Replace the incomplete record for the last embedded value with the + // result of that embedding + feed.last = feed.last.complete(result); + + if (canCompareLastTwo) { + feed.insert(feed.length - 1, EmbeddingFeedItem.emptyComparison()); + } else if (lastTwoResultsIncomparable) { + feed.insert(feed.length - 1, EmbeddingFeedItem.incomparable()); + } + }); + } + + Future _embed() async { + _prepareForEmbedding(); + final embeddingResult = await (await embedder).embed(_controller.text); + _showEmbeddingResults(embeddingResult); + } + + /// True if there is an `embed(String)` request out and not yet fulfilled. + bool get isProcessing => + (feed.isNotEmpty && feed.last.embeddingResult?.result == null); + + /// True if the last two feed items are both results and have the same + /// metadata. + bool get canCompareLastTwo => + feed.length >= 2 && + feed.last._type == _EmbeddingFeedItemType.result && + feed[feed.length - 2]._type == _EmbeddingFeedItemType.result && + feed.last.embeddingResult! + .canComputeSimilarity(feed[feed.length - 2].embeddingResult!); + + /// True if the last two feed items are both results but have different + /// metadata. This is different than two feed items which are NOT both results + /// (aka, a comparison and a result side by side), which is a situation that + /// does not require special handling in the UI. + bool get lastTwoResultsIncomparable => + feed.length >= 2 && + feed.last._type == _EmbeddingFeedItemType.result && + feed[feed.length - 2]._type == _EmbeddingFeedItemType.result && + !feed.last.embeddingResult! + .canComputeSimilarity(feed[feed.length - 2].embeddingResult!); + + Future _compare(int index) async { + int lowIndex = index - 1; + int highIndex = index + 1; + _log.info( + 'Comparing "${feed[lowIndex].embeddingResult!.value}" and ' + '"${feed[highIndex].embeddingResult!.value}"', + ); + final similarity = await (await embedder).cosineSimilarity( + feed[lowIndex].embeddingResult!.result!.embeddings.first, + feed[highIndex].embeddingResult!.result!.embeddings.first, + ); + setState(() { + feed[index] = EmbeddingFeedItem.comparison(similarity); + }); + } + + @override + Widget build(BuildContext context) => // + Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text('Float:'), + Checkbox( + value: type == EmbeddingType.float, + onChanged: (_) { + toggleMode(); + }, + ), + ], + ), + // Quantized checkbox + Row( + children: [ + const Text('Quantize:'), + Checkbox( + value: type == EmbeddingType.quantized, + onChanged: (bool? newValue) { + toggleMode(); + }, + ), + ], + ), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + children: [ + const Text('L2 Normalize:'), + Checkbox( + value: l2Normalize, + onChanged: (_) { + toggleL2Normalize(); + }, + ), + ], + ), + ], + ), + ], + ), + // Float checkbox + TextField(controller: _controller), + ...feed.reversed.toList().enumerate( + (EmbeddingFeedItem feedItem, index) { + return switch (feedItem._type) { + _EmbeddingFeedItemType.result => + TextEmbedderResultDisplay( + embeddedText: feedItem.embeddingResult!, + index: index, + ), + _EmbeddingFeedItemType.emptyComparison => TextButton( + // Subtract `index` from `feed.length` because we + // are looping through the list in reverse order + onPressed: () => _compare(feed.length - index - 1), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.purple[100], + ), + ), + child: const Text('Compare'), + ), + _EmbeddingFeedItemType.comparison => + ComparisonDisplay(similarity: feedItem.similarity!), + _EmbeddingFeedItemType.incomparable => const Text( + 'Embeddings of different types cannot be compared'), + }; + }, + ), + ], + ), + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: isProcessing || _controller.text == '' ? null : _embed, + child: const Icon(Icons.search), + ), + ); +} + +/// Shows, in the activity feed, the results of invoking `cosineSimilarity` +/// between two [Embedding] objects (with identical metadata). +class ComparisonDisplay extends StatelessWidget { + const ComparisonDisplay({super.key, required this.similarity}); + + final double similarity; + + @override + Widget build(BuildContext context) => Text('Similarity score: $similarity'); +} + +/// Shows, in the activity feed, a visualiation of an [Embedding] object. +class TextEmbedderResultDisplay extends StatelessWidget { + const TextEmbedderResultDisplay({ + super.key, + required this.embeddedText, + required this.index, + }); + + final TextWithEmbedding embeddedText; + final int index; + + @override + Widget build(BuildContext context) { + if (embeddedText.result == null) { + return const CircularProgressIndicator.adaptive(); + } + final embedding = embeddedText.result!.embeddings.last; + String embeddingDisplay = switch (embedding.type) { + EmbeddingType.float => '${embedding.floatEmbedding!}', + EmbeddingType.quantized => '${embedding.quantizedEmbedding!}', + }; + // Replace "..." with the results + final message = '"${embeddedText.value}"\n$embeddingDisplay'; + return Card( + key: Key('Embedding::"${embeddedText.value}" $index'), + margin: const EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(message), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (embedding.type == EmbeddingType.float) + _embeddingAttribute('Float', Colors.blue[300]!), + if (embedding.type == EmbeddingType.quantized) + _embeddingAttribute('Quantized', Colors.orange[300]!), + if (embeddedText.l2Normalized) + _embeddingAttribute('L2 Normalized', Colors.green[300]!), + ], + ), + Text(embeddedText.computedAt.toIso8601String()), + ], + ), + ], + ), + ), + ); + } + + Widget _embeddingAttribute(String text, Color color) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: GFButton( + onPressed: null, + text: text, + shape: GFButtonShape.pills, + color: color, + ), + ); + } +} + +/// Bundled [TextEmbeddingResult], the original text, and whether +/// that value was embedded with L2 normalization. +/// +/// See also: +/// * [TextEmbedderResultDisplay] - the widget which displays this information. +class TextWithEmbedding { + const TextWithEmbedding._({ + required this.computedAt, + required this.l2Normalized, + required this.result, + required this.value, + }); + + const TextWithEmbedding({ + required this.computedAt, + required this.l2Normalized, + required this.value, + }) : result = null; + + final bool l2Normalized; + final TextEmbedderResult? result; + final String value; + final DateTime computedAt; + + TextWithEmbedding complete(TextEmbedderResult result) => TextWithEmbedding._( + computedAt: computedAt, + l2Normalized: l2Normalized, + result: result, + value: value, + ); + + bool canComputeSimilarity(TextWithEmbedding other) => + result != null && + result!.embeddings.isNotEmpty && + other.result != null && + other.result!.embeddings.isNotEmpty && + l2Normalized == other.l2Normalized && + result!.embeddings.first.type == other.result!.embeddings.first.type; +} + +/// Contains the various types of application state that can appear in the demo +/// app's activity feed. +/// +/// Union type of all possible items that can appear on the feed. +/// +/// See also: +/// * [_EmbeddingFeedItemType] - the enum which indicates the type of feed item. +class EmbeddingFeedItem { + const EmbeddingFeedItem._({ + required this.similarity, + required this.embeddingResult, + required _EmbeddingFeedItemType type, + }) : _type = type; + + factory EmbeddingFeedItem.comparison(double similarity) => + EmbeddingFeedItem._( + similarity: similarity, + embeddingResult: null, + type: _EmbeddingFeedItemType.comparison, + ); + + factory EmbeddingFeedItem.result(TextWithEmbedding result) => + EmbeddingFeedItem._( + similarity: null, + embeddingResult: result, + type: _EmbeddingFeedItemType.result, + ); + + factory EmbeddingFeedItem.emptyComparison() => const EmbeddingFeedItem._( + similarity: null, + embeddingResult: null, + type: _EmbeddingFeedItemType.emptyComparison, + ); + + factory EmbeddingFeedItem.incomparable() => const EmbeddingFeedItem._( + similarity: null, + embeddingResult: null, + type: _EmbeddingFeedItemType.incomparable, + ); + + EmbeddingFeedItem complete(TextEmbedderResult result) { + assert(_type == _EmbeddingFeedItemType.result); + return EmbeddingFeedItem.result(embeddingResult!.complete(result)); + } + + final TextWithEmbedding? embeddingResult; + final double? similarity; + final _EmbeddingFeedItemType _type; +} + +enum _EmbeddingFeedItemType { + result, + comparison, + emptyComparison, + incomparable +} diff --git a/packages/mediapipe-task-text/example/macos/.gitignore b/packages/mediapipe-task-text/example/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/mediapipe-task-text/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/mediapipe-task-text/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/mediapipe-task-text/example/macos/Flutter/Flutter-Release.xcconfig b/packages/mediapipe-task-text/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/mediapipe-task-text/example/macos/Podfile b/packages/mediapipe-task-text/example/macos/Podfile new file mode 100644 index 0000000..c795730 --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/mediapipe-task-text/example/macos/Runner.xcodeproj/project.pbxproj b/packages/mediapipe-task-text/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..56731fb --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,791 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + D0DDCE128FE337C5B9E9A295 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9935BA9A99DC221620926E9 /* Pods_RunnerTests.framework */; }; + FB57908BE6FAD5004855D15F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B35D5A8F1DC3B3FB038F4B63 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7CFD3790269492C588E2C9DD /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 855B62EE22ED91E4F519EFF9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A9935BA9A99DC221620926E9 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B35D5A8F1DC3B3FB038F4B63 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BA8913FFD9B9350A31F59BD6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + C97F93AAADAA2C44B25186EF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + EB9E9D1C00BE5727AB8A3D85 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F03DADF129363B5D003C7660 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0DDCE128FE337C5B9E9A295 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FB57908BE6FAD5004855D15F /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + C3E7E394D89CFA892EB04DF9 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + C3E7E394D89CFA892EB04DF9 /* Pods */ = { + isa = PBXGroup; + children = ( + F03DADF129363B5D003C7660 /* Pods-Runner.debug.xcconfig */, + C97F93AAADAA2C44B25186EF /* Pods-Runner.release.xcconfig */, + BA8913FFD9B9350A31F59BD6 /* Pods-Runner.profile.xcconfig */, + 855B62EE22ED91E4F519EFF9 /* Pods-RunnerTests.debug.xcconfig */, + EB9E9D1C00BE5727AB8A3D85 /* Pods-RunnerTests.release.xcconfig */, + 7CFD3790269492C588E2C9DD /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B35D5A8F1DC3B3FB038F4B63 /* Pods_Runner.framework */, + A9935BA9A99DC221620926E9 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + F88EC3E3BF8E6A496380B3C1 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B400223FD9484ECB1EA04E47 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 3C7112DEBC73266C27DBA095 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 3C7112DEBC73266C27DBA095 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B400223FD9484ECB1EA04E47 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F88EC3E3BF8E6A496380B3C1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 855B62EE22ED91E4F519EFF9 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EB9E9D1C00BE5727AB8A3D85 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7CFD3790269492C588E2C9DD /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/mediapipe-task-text/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/mediapipe-task-text/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/mediapipe-task-text/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/mediapipe-task-text/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..15368ec --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/mediapipe-task-text/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/mediapipe-task-text/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/mediapipe-task-text/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/mediapipe-task-text/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/mediapipe-task-text/example/macos/Runner/AppDelegate.swift b/packages/mediapipe-task-text/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/packages/mediapipe-task-text/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/mediapipe-task-text/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/mediapipe-task-text/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/mediapipe-task-text/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/mediapipe-task-text/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..dda192b --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. diff --git a/packages/mediapipe-task-text/example/macos/Runner/Configs/Debug.xcconfig b/packages/mediapipe-task-text/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/mediapipe-task-text/example/macos/Runner/Configs/Release.xcconfig b/packages/mediapipe-task-text/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/mediapipe-task-text/example/macos/Runner/Configs/Warnings.xcconfig b/packages/mediapipe-task-text/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/mediapipe-task-text/example/macos/Runner/DebugProfile.entitlements b/packages/mediapipe-task-text/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..1c073db --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.files.downloads.read-only + + com.apple.security.network.server + + + diff --git a/packages/mediapipe-task-text/example/macos/Runner/Info.plist b/packages/mediapipe-task-text/example/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/mediapipe-task-text/example/macos/Runner/MainFlutterWindow.swift b/packages/mediapipe-task-text/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/mediapipe-task-text/example/macos/Runner/Release.entitlements b/packages/mediapipe-task-text/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/mediapipe-task-text/example/macos/RunnerTests/RunnerTests.swift b/packages/mediapipe-task-text/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..5418c9f --- /dev/null +++ b/packages/mediapipe-task-text/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/mediapipe-task-text/example/pubspec.yaml b/packages/mediapipe-task-text/example/pubspec.yaml new file mode 100644 index 0000000..a8380eb --- /dev/null +++ b/packages/mediapipe-task-text/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: example +description: A new Flutter project. +publish_to: "none" +version: 0.1.0 + +environment: + sdk: ">=3.1.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + getwidget: ^4.0.0 + logging: ^1.2.0 + mediapipe_core: + path: ../../mediapipe-core + mediapipe_text: + path: ../ + path: ^1.8.3 + path_provider: ^2.1.1 + +dev_dependencies: + flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + + assets: + - assets/bert_classifier.tflite + - assets/universal_sentence_encoder.tflite diff --git a/packages/mediapipe-task-text/example/test/widgets_test.dart b/packages/mediapipe-task-text/example/test/widgets_test.dart new file mode 100644 index 0000000..c30892a --- /dev/null +++ b/packages/mediapipe-task-text/example/test/widgets_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mediapipe_core/mediapipe_core.dart'; +import 'package:mediapipe_text/mediapipe_text.dart'; +import 'package:example/text_classification_demo.dart'; + +class FakeTextClassifier extends TextClassifier { + FakeTextClassifier(TextClassifierOptions options) : super(options); + + @override + Future classify(String text) { + return Future.value( + TextClassifierResult( + classifications: [ + Classifications( + categories: [ + Category( + index: 0, + score: 0.9, + categoryName: 'happy-go-lucky', + displayName: null, + ), + ], + headIndex: 0, + headName: 'whatever', + ), + ], + ), + ); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + testWidgets('TextClassificationResults should show results', + (WidgetTester tester) async { + final app = MaterialApp( + home: TextClassificationDemo( + classifier: FakeTextClassifier( + TextClassifierOptions.fromAssetPath('fake'), + ), + ), + ); + + await tester.pumpWidget(app); + await tester.tap(find.byType(Icon)); + await tester.pumpAndSettle(); + expect( + find.byKey(const Key('Classification::"Hello, world!" 1')), + findsOneWidget, + ); + expect( + find.text('"Hello, world!" happy-go-lucky :: 0.9'), + findsOneWidget, + ); + }); +} diff --git a/packages/mediapipe-task-text/ffigen.yaml b/packages/mediapipe-task-text/ffigen.yaml new file mode 100644 index 0000000..d9a1d46 --- /dev/null +++ b/packages/mediapipe-task-text/ffigen.yaml @@ -0,0 +1,23 @@ +name: "MediaPipeTextBindings" +description: "Bindings for MediaPipe structs for text-related tasks" +output: + bindings: "lib/src/io/third_party/mediapipe/generated/mediapipe_text_bindings.dart" +headers: + entry-points: + - "third_party/mediapipe/tasks/c/**.h" +import: + symbol-files: + - "package:mediapipe_core/generated/core_symbols.yaml" +preamble: | + /* Copyright 2023 The MediaPipe Authors. + 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. + ==============================================================================*/ +ffi-native: diff --git a/packages/mediapipe-task-text/lib/interface.dart b/packages/mediapipe-task-text/lib/interface.dart new file mode 100644 index 0000000..544bbeb --- /dev/null +++ b/packages/mediapipe-task-text/lib/interface.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/interface/interface.dart'; diff --git a/packages/mediapipe-task-text/lib/io.dart b/packages/mediapipe-task-text/lib/io.dart new file mode 100644 index 0000000..3183001 --- /dev/null +++ b/packages/mediapipe-task-text/lib/io.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/io/mediapipe_text.dart'; diff --git a/packages/mediapipe-task-text/lib/mediapipe_text.dart b/packages/mediapipe-task-text/lib/mediapipe_text.dart new file mode 100644 index 0000000..3f8b3f3 --- /dev/null +++ b/packages/mediapipe-task-text/lib/mediapipe_text.dart @@ -0,0 +1,10 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Package containing MediaPipe's text-specific tasks. +library mediapipe_text; + +export 'universal_mediapipe_text.dart' + if (dart.library.html) 'src/web/mediapipe_text.dart' + if (dart.library.io) 'src/io/mediapipe_text.dart'; diff --git a/packages/mediapipe-task-text/lib/src/interface/interface.dart b/packages/mediapipe-task-text/lib/src/interface/interface.dart new file mode 100644 index 0000000..c1e2551 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/interface/interface.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'tasks/tasks.dart'; diff --git a/packages/mediapipe-task-text/lib/src/interface/tasks/tasks.dart b/packages/mediapipe-task-text/lib/src/interface/tasks/tasks.dart new file mode 100644 index 0000000..3f84692 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/interface/tasks/tasks.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'text_embedding/text_embedding.dart'; +export 'text_classification/text_classification.dart'; diff --git a/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classification.dart b/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classification.dart new file mode 100644 index 0000000..eaea8b3 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classification.dart @@ -0,0 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'text_classifier.dart'; +export 'text_classifier_options.dart'; +export 'text_classifier_result.dart'; diff --git a/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classifier.dart b/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classifier.dart new file mode 100644 index 0000000..dc50dde --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classifier.dart @@ -0,0 +1,18 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'text_classification.dart'; + +/// {@template TextClassifier} +/// Utility to analyze text via MediaPipe's text classification task. +/// {@endtemplate} +abstract class BaseTextClassifier { + /// {@template TextClassifier.classify} + /// Sends a [String] value to MediaPipe for classification. + /// {@endtemplate} + Future classify(String text); + + /// Cleans up all resources. + void dispose(); +} diff --git a/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classifier_options.dart b/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classifier_options.dart new file mode 100644 index 0000000..857bce8 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classifier_options.dart @@ -0,0 +1,31 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:mediapipe_core/interface.dart'; + +/// {@template TextClassifierOptions} +/// Configuration object for a MediaPipe text classifier. +/// +/// See also: +/// * [MediaPipe's TextClassifierOptions documentation](https://developers.google.com/mediapipe/api/solutions/js/tasks-text.textclassifieroptions) +/// {@endtemplate} +/// +/// This implementation is not immutable to track whether `dispose` has been +/// called. All values used by pkg:equatable are in fact immutable. +// ignore: must_be_immutable +abstract class BaseTextClassifierOptions extends BaseTaskOptions { + /// Contains parameter options for how this classifier should behave, + /// including allow and denylists, thresholds, maximum results, etc. + /// + /// See also: + /// * [BaseClassifierOptions] for each available field. + BaseClassifierOptions get classifierOptions; + + @override + String toString() => 'TextClassifierOptions(baseOptions: $baseOptions, ' + 'classifierOptions: $classifierOptions)'; + + @override + List get props => [baseOptions, classifierOptions]; +} diff --git a/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classifier_result.dart b/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classifier_result.dart new file mode 100644 index 0000000..84aa2de --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/interface/tasks/text_classification/text_classifier_result.dart @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:mediapipe_core/interface.dart'; + +/// {@template TextClassifierResult} +/// Container with results of MediaPipe's `classifyText` task. +/// +/// See also: +/// * [MediaPipe's TextClassifierResult documentation](https://developers.google.com/mediapipe/api/solutions/java/com/google/mediapipe/tasks/text/textclassifier/TextClassifierResult) +/// {@endtemplate} +abstract class BaseTextClassifierResult extends BaseClassifierResult { + /// {@macro TextClassifierResult} + BaseTextClassifierResult(); +} diff --git a/packages/mediapipe-task-text/lib/src/interface/tasks/text_embedding/text_embedder.dart b/packages/mediapipe-task-text/lib/src/interface/tasks/text_embedding/text_embedder.dart new file mode 100644 index 0000000..f4d7271 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/interface/tasks/text_embedding/text_embedder.dart @@ -0,0 +1,24 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:mediapipe_core/interface.dart'; +import 'package:mediapipe_core/mediapipe_core.dart'; + +/// {@template TextEmbedder} +/// Utility to convert text into an embedding suitable for other MediaPipe tasks. +/// {@endtemplate} +abstract class BaseTextEmbedder { + /// {@template TextEmbedder.embed} + /// Sends a [String] value to MediaPipe for conversion into an [Embedding]. + /// {@endtemplate} + Future embed(String text); + + /// {@template TextEmbedder.cosineSimilarity} + /// Sends a [String] value to MediaPipe for conversion into an [Embedding]. + /// {@endtemplate} + Future cosineSimilarity(Embedding a, Embedding b); + + /// Cleans up all resources. + void dispose(); +} diff --git a/packages/mediapipe-task-text/lib/src/interface/tasks/text_embedding/text_embedder_options.dart b/packages/mediapipe-task-text/lib/src/interface/tasks/text_embedding/text_embedder_options.dart new file mode 100644 index 0000000..78ad7f5 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/interface/tasks/text_embedding/text_embedder_options.dart @@ -0,0 +1,27 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:mediapipe_core/interface.dart'; + +/// {@template TextEmbedderOptions} +/// Configuration object for a MediaPipe text embedder. +/// +/// See also: +/// * [MediaPipe's TextEmbedderOptions documentation](https://developers.google.com/mediapipe/api/solutions/java/com/google/mediapipe/tasks/text/textembedder/TextEmbedder.TextEmbedderOptions) +/// {@endtemplate} +/// +/// This implementation is not immutable to track whether `dispose` has been +/// called. All values used by pkg:equatable are in fact immutable. +// ignore: must_be_immutable +abstract class BaseTextEmbedderOptions extends BaseTaskOptions { + /// Contains parameter options for how this embedder should behave. + BaseEmbedderOptions get embedderOptions; + + @override + String toString() => 'TextEmbedderOptions(baseOptions: $baseOptions, ' + 'embedderOptions: $embedderOptions)'; + + @override + List get props => [baseOptions, embedderOptions]; +} diff --git a/packages/mediapipe-task-text/lib/src/interface/tasks/text_embedding/text_embedding.dart b/packages/mediapipe-task-text/lib/src/interface/tasks/text_embedding/text_embedding.dart new file mode 100644 index 0000000..d17068f --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/interface/tasks/text_embedding/text_embedding.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'text_embedder.dart'; +export 'text_embedder_options.dart'; diff --git a/packages/mediapipe-task-text/lib/src/io/mediapipe_text.dart b/packages/mediapipe-task-text/lib/src/io/mediapipe_text.dart new file mode 100644 index 0000000..c1e2551 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/mediapipe_text.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'tasks/tasks.dart'; diff --git a/packages/mediapipe-task-text/lib/src/io/tasks/tasks.dart b/packages/mediapipe-task-text/lib/src/io/tasks/tasks.dart new file mode 100644 index 0000000..c7ff481 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/tasks/tasks.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'text_classification/text_classification.dart'; +export 'text_embedding/text_embedding.dart'; diff --git a/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classification.dart b/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classification.dart new file mode 100644 index 0000000..e971d5b --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classification.dart @@ -0,0 +1,8 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'text_classifier.dart'; +export 'text_classifier_executor.dart'; +export 'text_classifier_options.dart'; +export 'text_classifier_result.dart'; diff --git a/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier.dart b/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier.dart new file mode 100644 index 0000000..da6cf4c --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier.dart @@ -0,0 +1,82 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:isolate'; +import 'package:async/async.dart'; +import 'package:logging/logging.dart'; +import 'package:mediapipe_core/mediapipe_core.dart'; +import 'package:mediapipe_text/interface.dart'; +import 'package:mediapipe_text/io.dart'; + +final _log = Logger('TextClassifier'); + +/// {@macro TextClassifier} +class TextClassifier extends BaseTextClassifier { + /// {@macro TextClassifier} + TextClassifier(this._options) : _readyCompleter = Completer() { + _createIsolate(_options).then((results) { + _events = results.$1; + _sendPort = results.$2; + _readyCompleter.complete(); + }); + } + + late SendPort _sendPort; + late StreamQueue _events; + final Completer _readyCompleter; + Future get _ready => _readyCompleter.future; + + final TextClassifierOptions _options; + + @override + Future classify(String text) async { + _log.fine('Classifying ${text.shorten()}'); + await _ready; + _sendPort.send(text); + return await _events.next; + } + + /// Closes down the background isolate, releasing all resources. + @override + void dispose() => _sendPort.send(null); +} + +Future<(StreamQueue, SendPort)> _createIsolate( + TextClassifierOptions options, +) async { + final p = ReceivePort(); + await Isolate.spawn( + (SendPort port) => _classificationService( + port, + options, + ), + p.sendPort, + ); + + final events = StreamQueue(p); + final SendPort sendPort = await events.next; + return (events, sendPort); +} + +Future _classificationService( + SendPort p, + TextClassifierOptions options, +) async { + final commandPort = ReceivePort(); + p.send(commandPort.sendPort); + + final executor = TextClassifierExecutor(options); + + await for (final String? message in commandPort) { + if (message != null) { + final TextClassifierResult result = executor.classify(message); + p.send(result); + } else { + break; + } + } + executor.dispose(); + Isolate.exit(); +} diff --git a/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier_executor.dart b/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier_executor.dart new file mode 100644 index 0000000..2064198 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier_executor.dart @@ -0,0 +1,77 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:logging/logging.dart'; +import 'package:mediapipe_core/io.dart'; +import 'package:mediapipe_text/io.dart'; +import 'package:mediapipe_text/src/io/third_party/mediapipe/generated/mediapipe_text_bindings.dart' + as bindings; + +final _log = Logger('TextTaskExecutor'); + +/// Executes MediaPipe's "classifyText" task. +/// +/// {@macro TaskExecutor} +class TextClassifierExecutor extends TaskExecutor< + bindings.TextClassifierOptions, + TextClassifierOptions, + bindings.TextClassifierResult, + TextClassifierResult> { + /// {macro TextClassifierExecutor} + TextClassifierExecutor(super.options); + + @override + final String taskName = 'TextClassification'; + + @override + Pointer createWorker( + Pointer options, + Pointer> error, + ) { + _log.fine('Creating TextClassifier in native memory'); + final worker = bindings.text_classifier_create(options, error); + _log.finest( + 'Created TextClassifier at 0x${worker.address.toRadixString(16)}', + ); + return worker; + } + + @override + Pointer createResultsPointer() { + _log.fine('Allocating TextClassifierResult in native memory'); + final results = calloc(); + _log.finest( + 'Allocated TextClassifierResult at 0x${results.address.toRadixString(16)}', + ); + return results; + } + + @override + int closeWorker(Pointer worker, Pointer> error) { + final status = bindings.text_classifier_close(worker, error); + _log.finest('Closed TextClassifier in native memory with status $status'); + return status; + } + + /// Passes [text] to MediaPipe for classification, yielding a + /// [TextClassifierResult] or throwing an exception. + TextClassifierResult classify(String text) { + final resultPtr = createResultsPointer(); + final errorMessageMemory = calloc>(); + final textMemory = text.copyToNative(); + final status = bindings.text_classifier_classify( + worker, + textMemory, + resultPtr, + errorMessageMemory, + ); + _log.finest('Classified with status $status'); + textMemory.free(); + handleErrorMessage(errorMessageMemory, status); + errorMessageMemory.free(1); + return TextClassifierResult.native(resultPtr); + } +} diff --git a/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier_options.dart b/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier_options.dart new file mode 100644 index 0000000..6f2c420 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier_options.dart @@ -0,0 +1,101 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:mediapipe_core/io.dart'; +import 'package:mediapipe_text/interface.dart'; +import '../../third_party/mediapipe/generated/mediapipe_text_bindings.dart' + as bindings; + +/// {@macro TextClassifierOptions} +/// +/// This io-friendly implementation is not immutable strictly for memoization of +/// computed fields. All values used by pkg:equatable are in fact immutable. +// ignore: must_be_immutable +class TextClassifierOptions extends BaseTextClassifierOptions + with TaskOptions { + /// {@macro TextClassifierOptions} + TextClassifierOptions({ + required this.baseOptions, + this.classifierOptions = const ClassifierOptions(), + }); + + /// {@macro TextClassifierOptions.fromAssetPath} + factory TextClassifierOptions.fromAssetPath( + String assetPath, { + ClassifierOptions classifierOptions = const ClassifierOptions(), + }) { + return TextClassifierOptions( + baseOptions: BaseOptions.path(assetPath), + classifierOptions: classifierOptions, + ); + } + + /// {@macro TextClassifierOptions.fromAssetBuffer} + factory TextClassifierOptions.fromAssetBuffer( + Uint8List assetBuffer, { + ClassifierOptions classifierOptions = const ClassifierOptions(), + }) => + TextClassifierOptions( + baseOptions: BaseOptions.memory(assetBuffer), + classifierOptions: classifierOptions, + ); + + @override + final BaseOptions baseOptions; + + @override + final ClassifierOptions classifierOptions; + + /// {@template TaskOptions.memory} + /// Cache of the pointer to the native memory created in [copyToNative]. + /// This pointer is held locally to make `free` an instance method, which is + /// simpler to call in [TaskExecutor], which only knows about a [TaskOptions] + /// type and does not know the final type until runtime. This is relevant + /// because `TaskOptions.free` should be static if it accepted the pointer to + /// release, but Dart cannot call static methods off a generic type. + /// {@endtemplate} + Pointer? _pointer; + + @override + Pointer copyToNative() { + _pointer = calloc(); + baseOptions.assignToStruct(_pointer!.ref.base_options); + classifierOptions.assignToStruct(_pointer!.ref.classifier_options); + return _pointer!; + } + + bool _isClosed = false; + + /// Tracks whether [dispose] has been called. + bool get isClosed => _isClosed; + + @override + void dispose() { + assert(() { + if (isClosed) { + throw Exception( + 'Attempted to call dispose on an already-disposed task options' + 'object. Task options should only ever be disposed after they are at ' + 'end-of-life and will never be accessed again.', + ); + } + if (_pointer == null) { + throw Exception( + 'Attempted to call dispose on a TextClassifierOptions object which ' + 'was never used by a TextClassifier. Did you forget to create your ' + 'TextClassifier?', + ); + } + return true; + }()); + baseOptions.freeStructFields(_pointer!.ref.base_options); + classifierOptions.freeStructFields(_pointer!.ref.classifier_options); + calloc.free(_pointer!); + _isClosed = true; + } +} diff --git a/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier_result.dart b/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier_result.dart new file mode 100644 index 0000000..f3691fa --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/tasks/text_classification/text_classifier_result.dart @@ -0,0 +1,66 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; + +import 'package:mediapipe_core/io.dart'; +import 'package:mediapipe_text/interface.dart'; +import '../../third_party/mediapipe/generated/mediapipe_text_bindings.dart' + as bindings; + +/// {@macro TextClassifierResult} +class TextClassifierResult extends BaseTextClassifierResult with IOTaskResult { + /// {@macro TextClassifierResult.fake} + TextClassifierResult({required Iterable classifications}) + : _classifications = classifications, + _pointer = null; + + /// {@template TextClassifierResult.native} + /// Initializes a [TextClassifierResult] instance as a wrapper around native + /// memory. + /// + /// See also: + /// * [TextClassifierExecutor.classify] where this is called. + /// {@endtemplate} + TextClassifierResult.native(this._pointer); + + final Pointer? _pointer; + + Iterable? _classifications; + @override + Iterable get classifications => + _classifications ??= _getClassifications(); + Iterable _getClassifications() { + if (_pointer.isNullOrNullPointer) { + throw Exception( + 'No native memory for TextClassifierResult.classifications', + ); + } + return Classifications.fromNativeArray( + _pointer!.ref.classifications, + _pointer!.ref.classifications_count, + ); + } + + @override + void dispose() { + assert(() { + if (isClosed) { + throw Exception( + 'A TextClassifierResult was closed after it had already been closed. ' + 'TextClassifierResult objects should only be closed when they are at' + 'their end of life and will never be used again.', + ); + } + return true; + }()); + if (_pointer != null) { + // Only call the native finalizer if there actually is native memory, + // because tests may verify that faked results are also closed and calling + // this method in that scenario would cause a segfault. + bindings.text_classifier_close_result(_pointer!); + } + super.dispose(); + } +} diff --git a/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder.dart b/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder.dart new file mode 100644 index 0000000..5bc4fe4 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder.dart @@ -0,0 +1,166 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io' as io; +import 'dart:isolate'; +import 'package:async/async.dart'; +import 'package:logging/logging.dart'; +import 'package:mediapipe_core/mediapipe_core.dart'; +import 'package:mediapipe_core/io.dart' as core_io; +import 'package:mediapipe_text/interface.dart'; +import 'package:mediapipe_text/io.dart'; + +final _log = Logger('TextEmbedder'); + +/// TextEmbedder implementation able to use FFI and `dart:io`. +class TextEmbedder extends BaseTextEmbedder { + /// Generative constructor. + TextEmbedder(this._options) : _readyCompleter = Completer() { + _createIsolate(_options).then((results) { + _events = results.$1; + _sendPort = results.$2; + _readyCompleter.complete(); + }); + } + + late SendPort _sendPort; + late StreamQueue _events; + final Completer _readyCompleter; + Future get _ready => _readyCompleter.future; + + final TextEmbedderOptions _options; + + /// Closes down the background isolate, releasing all resources. + @override + void dispose() => _sendPort.send(null); + + /// {@macro TextEmbedder.embed} + @override + Future embed(String text) async { + _log.fine('Embedding "$text"'); + await _ready; + _sendPort.send(_EmbedderTask.embed(text)); + while (true) { + final response = await _events.next; + if (response is TextEmbedderResult) { + return response; + } else if (response is String) { + _log.fine(response); + } else { + throw Exception( + 'Unexpected embed result of type ${response.runtimeType} : $response', + ); + } + } + } + + /// {@macro TextEmbedder.cosineSimilarity} + @override + Future cosineSimilarity(Embedding a, Embedding b) async { + await _ready; + (a as core_io.Embedding).pointer; + _sendPort.send(_EmbedderTask.cosineSimilarity(a, b)); + while (true) { + final response = await _events.next; + if (response is double) { + return response; + } else if (response is String) { + _log.fine(response); + } else { + throw Exception( + 'Unexpected cosine similarity result of type ${response.runtimeType} : $response', + ); + } + } + } +} + +Future<(StreamQueue, SendPort)> _createIsolate( + TextEmbedderOptions options) async { + final p = ReceivePort(); + await Isolate.spawn( + (SendPort port) => _embedderService( + port, + options, + ), + p.sendPort, + ); + + final events = StreamQueue(p); + final SendPort sendPort = await events.next; + return (events, sendPort); +} + +Future _embedderService( + SendPort p, + TextEmbedderOptions options, +) async { + final commandPort = ReceivePort(); + p.send(commandPort.sendPort); + + Logger.root.level = Level.FINEST; + Logger.root.onRecord.listen((record) { + io.stdout.writeln('${record.level.name} [${record.loggerName}]' + '[' + '${record.time.hour.toString()}:' + '${record.time.minute.toString().padLeft(2, "0")}:' + '${record.time.second.toString().padLeft(2, "0")}.' + '${record.time.millisecond.toString().padRight(3, "0")}' + '] ${record.message}'); + }); + + final executor = TextEmbedderExecutor(options); + + await for (final _EmbedderTask? message in commandPort) { + if (message != null) { + switch (message.type) { + case _EmbedderTaskType._embed: + p.send(executor.embed(message.text!)); + case _EmbedderTaskType._cosineSimilarity: + p.send( + executor.cosineSimilarity( + (message.a as core_io.Embedding).pointer!, + (message.b as core_io.Embedding).pointer!, + ), + ); + } + } else { + break; + } + } + executor.dispose(); + Isolate.exit(); +} + +enum _EmbedderTaskType { _embed, _cosineSimilarity } + +class _EmbedderTask { + _EmbedderTask._({ + required this.type, + required this.text, + required this.a, + required this.b, + }); + + factory _EmbedderTask.embed(String text) => _EmbedderTask._( + type: _EmbedderTaskType._embed, + text: text, + a: null, + b: null, + ); + + factory _EmbedderTask.cosineSimilarity(Embedding a, Embedding b) => + _EmbedderTask._( + type: _EmbedderTaskType._cosineSimilarity, + text: null, + a: a, + b: b, + ); + + final _EmbedderTaskType type; + final String? text; + final Embedding? a; + final Embedding? b; +} diff --git a/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder_executor.dart b/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder_executor.dart new file mode 100644 index 0000000..88ec59f --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder_executor.dart @@ -0,0 +1,101 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:logging/logging.dart'; +import 'package:mediapipe_core/io.dart'; +import 'package:mediapipe_text/io.dart'; +// ignore: implementation_imports +import 'package:mediapipe_core/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart' + as core_bindings; +import 'package:mediapipe_text/src/io/third_party/mediapipe/generated/mediapipe_text_bindings.dart' + as bindings; + +final _log = Logger('TextTaskExecutor'); + +/// Executes MediaPipe's "embedText" task. +/// +/// {@macro TaskExecutor} +class TextEmbedderExecutor extends TaskExecutor { + /// {@macro TextEmbedderExecutor} + TextEmbedderExecutor(super.options); + + @override + final String taskName = 'TextEmbedding'; + + @override + int closeWorker(Pointer worker, Pointer> error) { + final status = bindings.text_embedder_close(worker, error); + _log.finest('Closed TextEmbedder in native memory with status $status'); + return status; + } + + @override + Pointer createResultsPointer() { + _log.fine('Allocating TextEmbedderResult in native memory'); + final results = calloc(); + _log.finest( + 'Allocated TextEmbedderResult at 0x${results.address.toRadixString(16)}', + ); + return results; + } + + @override + Pointer createWorker( + Pointer options, + Pointer> error, + ) { + _log.fine('Creating TextClassifier in native memory'); + final worker = bindings.text_embedder_create(options, error); + _log.finest( + 'Created TextClassifier at 0x${worker.address.toRadixString(16)}', + ); + return worker; + } + + /// Passes [text] to MediaPipe for conversion into an embedding, yielding a + /// [TextEmbedderResult] or throwing an exception. + TextEmbedderResult embed(String text) { + final resultPtr = createResultsPointer(); + final errorMessageMemory = calloc>(); + final textMemory = text.copyToNative(); + final status = bindings.text_embedder_embed( + worker, + textMemory, + resultPtr, + errorMessageMemory, + ); + _log.finest('Embedded with status $status'); + textMemory.free(); + handleErrorMessage(errorMessageMemory, status); + errorMessageMemory.free(1); + return TextEmbedderResult.native(resultPtr); + } + + /// Passes two embeddings to MediaPipe for comparison. Both embeddings should + /// have been made from embedders with the same configuration for the result + /// to be meaningful. + double cosineSimilarity( + Pointer a, + Pointer b, + ) { + final nativeSimilarity = calloc(); + final errorMessageMemory = calloc>(); + int status = bindings.text_embedder_cosine_similarity( + a, + b, + nativeSimilarity, + errorMessageMemory, + ); + _log.finest('Compared similarity with status $status'); + handleErrorMessage(errorMessageMemory, status); + errorMessageMemory.free(1); + final double similarity = nativeSimilarity.value; + calloc.free(nativeSimilarity); + return similarity; + } +} diff --git a/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder_options.dart b/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder_options.dart new file mode 100644 index 0000000..efd67f5 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder_options.dart @@ -0,0 +1,94 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; +import 'package:mediapipe_core/io.dart'; +import 'package:mediapipe_text/interface.dart'; +import '../../third_party/mediapipe/generated/mediapipe_text_bindings.dart' + as bindings; + +/// {@macro TextEmbedderOptions} +/// +/// This io-friendly implementation is not immutable strictly for memoization of +/// computed fields. All values used by pkg:equatable are in fact immutable. +// ignore: must_be_immutable +class TextEmbedderOptions extends BaseTextEmbedderOptions + with TaskOptions { + /// {@macro TextEmbedderOptions} + TextEmbedderOptions({ + required this.baseOptions, + this.embedderOptions = const EmbedderOptions(), + }); + + /// {@macro TextEmbedderOptions.fromAssetPath} + factory TextEmbedderOptions.fromAssetPath( + String assetPath, { + EmbedderOptions embedderOptions = const EmbedderOptions(), + }) { + return TextEmbedderOptions( + baseOptions: BaseOptions.path(assetPath), + embedderOptions: embedderOptions, + ); + } + + /// {@macro TextEmbedderOptions.fromAssetBuffer} + factory TextEmbedderOptions.fromAssetBuffer( + Uint8List assetBuffer, { + EmbedderOptions embedderOptions = const EmbedderOptions(), + }) => + TextEmbedderOptions( + baseOptions: BaseOptions.memory(assetBuffer), + embedderOptions: embedderOptions, + ); + + @override + final BaseOptions baseOptions; + + @override + final EmbedderOptions embedderOptions; + + /// {@macro TaskOptions.memory} + Pointer? _pointer; + + @override + Pointer copyToNative() { + _pointer = calloc(); + baseOptions.assignToStruct(_pointer!.ref.base_options); + embedderOptions.assignToStruct(_pointer!.ref.embedder_options); + return _pointer!; + } + + bool _isClosed = false; + + /// Tracks whether [dispose] has been called. + bool get isClosed => _isClosed; + + @override + void dispose() { + assert(() { + if (isClosed) { + throw Exception( + 'A TextEmbedderResult was closed after it had already been closed. ' + 'TextEmbedderResult objects should only be closed when they are at' + 'their end of life and will never be used again.', + ); + } + if (_pointer == null) { + throw Exception( + 'Attempted to call dispose on a TextEmbedderOptions object which ' + 'was never used by a TextEmbedder. Did you forget to create your ' + 'TextEmbedder?', + ); + } + return true; + }()); + baseOptions.freeStructFields(_pointer!.ref.base_options); + embedderOptions.freeStructFields(_pointer!.ref.embedder_options); + calloc.free(_pointer!); + _isClosed = true; + } +} diff --git a/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder_result.dart b/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder_result.dart new file mode 100644 index 0000000..58ffd0b --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedder_result.dart @@ -0,0 +1,62 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'package:mediapipe_core/io.dart'; +import 'package:mediapipe_core/interface.dart'; +import '../../third_party/mediapipe/generated/mediapipe_text_bindings.dart' + as bindings; + +/// {@macro TextEmbedderResult} +class TextEmbedderResult extends BaseEmbedderResult with IOTaskResult { + /// {@macro TextEmbedderResult.fake} + TextEmbedderResult({required Iterable embeddings}); + + /// {@template TextEmbedderResult.native} + /// Initializes a [TextEmbedderResult] instance as a wrapper around native + /// memory. + /// + /// See also: + /// * [TextEmbedderExecutor.embed] where this is called. + /// {@endtemplate} + TextEmbedderResult.native(this._pointer); + + Pointer? _pointer; + + Iterable? _embeddings; + @override + Iterable get embeddings => _embeddings ??= _getEmbeddings(); + Iterable _getEmbeddings() { + if (_pointer.isNullOrNullPointer) { + throw Exception( + 'No native memory for TextClassifierResult.classifications', + ); + } + return Embedding.fromNativeArray( + _pointer!.ref.embeddings, + _pointer!.ref.embeddings_count, + ); + } + + @override + void dispose() { + assert(() { + if (isClosed) { + throw Exception( + 'A TextEmbedderResult was closed after it had already been closed. ' + 'TextEmbedderResult objects should only be closed when they are at' + 'their end of life and will never be used again.', + ); + } + return true; + }()); + if (_pointer != null) { + // Only call the native finalizer if there actually is native memory, + // because tests may verify that faked results are also closed and calling + // this method in that scenario would cause a segfault. + bindings.text_embedder_close_result(_pointer!); + } + super.dispose(); + } +} diff --git a/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedding.dart b/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedding.dart new file mode 100644 index 0000000..98c0efe --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/tasks/text_embedding/text_embedding.dart @@ -0,0 +1,9 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'package:mediapipe_core/interface.dart' show EmbeddingType; +export 'text_embedder.dart'; +export 'text_embedder_executor.dart'; +export 'text_embedder_options.dart'; +export 'text_embedder_result.dart'; diff --git a/packages/mediapipe-task-text/lib/src/io/third_party/mediapipe/generated/mediapipe_text_bindings.dart b/packages/mediapipe-task-text/lib/src/io/third_party/mediapipe/generated/mediapipe_text_bindings.dart new file mode 100644 index 0000000..57653f0 --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/io/third_party/mediapipe/generated/mediapipe_text_bindings.dart @@ -0,0 +1,291 @@ +/* Copyright 2023 The MediaPipe Authors. +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. +==============================================================================*/ + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; +import 'package:mediapipe_core/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart' + as imp1; + +@ffi.Native< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer>)>(symbol: 'text_classifier_create') +external ffi.Pointer text_classifier_create( + ffi.Pointer options, + ffi.Pointer> error_msg, +); + +@ffi.Native< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>(symbol: 'text_classifier_classify') +external int text_classifier_classify( + ffi.Pointer classifier, + ffi.Pointer utf8_str, + ffi.Pointer result, + ffi.Pointer> error_msg, +); + +@ffi.Native)>( + symbol: 'text_classifier_close_result') +external void text_classifier_close_result( + ffi.Pointer result, +); + +@ffi.Native< + ffi.Int Function(ffi.Pointer, + ffi.Pointer>)>(symbol: 'text_classifier_close') +external int text_classifier_close( + ffi.Pointer classifier, + ffi.Pointer> error_msg, +); + +@ffi.Native< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer>)>(symbol: 'text_embedder_create') +external ffi.Pointer text_embedder_create( + ffi.Pointer options, + ffi.Pointer> error_msg, +); + +@ffi.Native< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>(symbol: 'text_embedder_embed') +external int text_embedder_embed( + ffi.Pointer embedder, + ffi.Pointer utf8_str, + ffi.Pointer result, + ffi.Pointer> error_msg, +); + +@ffi.Native)>( + symbol: 'text_embedder_close_result') +external void text_embedder_close_result( + ffi.Pointer result, +); + +@ffi.Native< + ffi.Int Function(ffi.Pointer, + ffi.Pointer>)>(symbol: 'text_embedder_close') +external int text_embedder_close( + ffi.Pointer embedder, + ffi.Pointer> error_msg, +); + +@ffi.Native< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>( + symbol: 'text_embedder_cosine_similarity') +external int text_embedder_cosine_similarity( + ffi.Pointer u, + ffi.Pointer v, + ffi.Pointer similarity, + ffi.Pointer> error_msg, +); + +final class TextClassifierOptions extends ffi.Struct { + external imp1.BaseOptions base_options; + + external imp1.ClassifierOptions classifier_options; +} + +typedef TextClassifierResult = imp1.ClassificationResult; + +final class TextEmbedderOptions extends ffi.Struct { + external imp1.BaseOptions base_options; + + external imp1.EmbedderOptions embedder_options; +} + +typedef TextEmbedderResult = imp1.EmbeddingResult; + +const int __bool_true_false_are_defined = 1; + +const int true1 = 1; + +const int false1 = 0; + +const int __WORDSIZE = 64; + +const int __DARWIN_ONLY_64_BIT_INO_T = 1; + +const int __DARWIN_ONLY_UNIX_CONFORMANCE = 1; + +const int __DARWIN_ONLY_VERS_1050 = 1; + +const int __DARWIN_UNIX03 = 1; + +const int __DARWIN_64_BIT_INO_T = 1; + +const int __DARWIN_VERS_1050 = 1; + +const int __DARWIN_NON_CANCELABLE = 0; + +const String __DARWIN_SUF_EXTSN = '\$DARWIN_EXTSN'; + +const int __DARWIN_C_ANSI = 4096; + +const int __DARWIN_C_FULL = 900000; + +const int __DARWIN_C_LEVEL = 900000; + +const int __STDC_WANT_LIB_EXT1__ = 1; + +const int __DARWIN_NO_LONG_LONG = 0; + +const int _DARWIN_FEATURE_64_BIT_INODE = 1; + +const int _DARWIN_FEATURE_ONLY_64_BIT_INODE = 1; + +const int _DARWIN_FEATURE_ONLY_VERS_1050 = 1; + +const int _DARWIN_FEATURE_ONLY_UNIX_CONFORMANCE = 1; + +const int _DARWIN_FEATURE_UNIX_CONFORMANCE = 3; + +const int __has_ptrcheck = 0; + +const int __DARWIN_NULL = 0; + +const int __PTHREAD_SIZE__ = 8176; + +const int __PTHREAD_ATTR_SIZE__ = 56; + +const int __PTHREAD_MUTEXATTR_SIZE__ = 8; + +const int __PTHREAD_MUTEX_SIZE__ = 56; + +const int __PTHREAD_CONDATTR_SIZE__ = 8; + +const int __PTHREAD_COND_SIZE__ = 40; + +const int __PTHREAD_ONCE_SIZE__ = 8; + +const int __PTHREAD_RWLOCK_SIZE__ = 192; + +const int __PTHREAD_RWLOCKATTR_SIZE__ = 16; + +const int USER_ADDR_NULL = 0; + +const int INT8_MAX = 127; + +const int INT16_MAX = 32767; + +const int INT32_MAX = 2147483647; + +const int INT64_MAX = 9223372036854775807; + +const int INT8_MIN = -128; + +const int INT16_MIN = -32768; + +const int INT32_MIN = -2147483648; + +const int INT64_MIN = -9223372036854775808; + +const int UINT8_MAX = 255; + +const int UINT16_MAX = 65535; + +const int UINT32_MAX = 4294967295; + +const int UINT64_MAX = -1; + +const int INT_LEAST8_MIN = -128; + +const int INT_LEAST16_MIN = -32768; + +const int INT_LEAST32_MIN = -2147483648; + +const int INT_LEAST64_MIN = -9223372036854775808; + +const int INT_LEAST8_MAX = 127; + +const int INT_LEAST16_MAX = 32767; + +const int INT_LEAST32_MAX = 2147483647; + +const int INT_LEAST64_MAX = 9223372036854775807; + +const int UINT_LEAST8_MAX = 255; + +const int UINT_LEAST16_MAX = 65535; + +const int UINT_LEAST32_MAX = 4294967295; + +const int UINT_LEAST64_MAX = -1; + +const int INT_FAST8_MIN = -128; + +const int INT_FAST16_MIN = -32768; + +const int INT_FAST32_MIN = -2147483648; + +const int INT_FAST64_MIN = -9223372036854775808; + +const int INT_FAST8_MAX = 127; + +const int INT_FAST16_MAX = 32767; + +const int INT_FAST32_MAX = 2147483647; + +const int INT_FAST64_MAX = 9223372036854775807; + +const int UINT_FAST8_MAX = 255; + +const int UINT_FAST16_MAX = 65535; + +const int UINT_FAST32_MAX = 4294967295; + +const int UINT_FAST64_MAX = -1; + +const int INTPTR_MAX = 9223372036854775807; + +const int INTPTR_MIN = -9223372036854775808; + +const int UINTPTR_MAX = -1; + +const int INTMAX_MAX = 9223372036854775807; + +const int UINTMAX_MAX = -1; + +const int INTMAX_MIN = -9223372036854775808; + +const int PTRDIFF_MIN = -9223372036854775808; + +const int PTRDIFF_MAX = 9223372036854775807; + +const int SIZE_MAX = -1; + +const int RSIZE_MAX = 9223372036854775807; + +const int WCHAR_MAX = 2147483647; + +const int WCHAR_MIN = -2147483648; + +const int WINT_MIN = -2147483648; + +const int WINT_MAX = 2147483647; + +const int SIG_ATOMIC_MIN = -2147483648; + +const int SIG_ATOMIC_MAX = 2147483647; diff --git a/packages/mediapipe-task-text/lib/src/web/mediapipe_text.dart b/packages/mediapipe-task-text/lib/src/web/mediapipe_text.dart new file mode 100644 index 0000000..854d12e --- /dev/null +++ b/packages/mediapipe-task-text/lib/src/web/mediapipe_text.dart @@ -0,0 +1,3 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. diff --git a/packages/mediapipe-task-text/lib/universal_mediapipe_text.dart b/packages/mediapipe-task-text/lib/universal_mediapipe_text.dart new file mode 100644 index 0000000..7bcf112 --- /dev/null +++ b/packages/mediapipe-task-text/lib/universal_mediapipe_text.dart @@ -0,0 +1,129 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:mediapipe_core/mediapipe_core.dart'; +import 'package:mediapipe_core/interface.dart'; +export 'package:mediapipe_core/interface.dart' show EmbeddingType; +import 'package:mediapipe_text/interface.dart'; + +/// {@macro TextClassifier} +class TextClassifier extends BaseTextClassifier { + /// {@macro TextClassifier} + TextClassifier(TextClassifierOptions options); + + @override + Future classify(String text) => + throw UnimplementedError(); + + @override + void dispose() => throw UnimplementedError(); +} + +/// {@macro TextClassifierOptions} +/// +/// This implementation is not immutable to track whether `dispose` has been +/// called. All values used by pkg:equatable are in fact immutable. +// ignore: must_be_immutable +class TextClassifierOptions extends BaseTextClassifierOptions { + /// {@template TextClassifierOptions.fromAssetPath} + /// Convenience constructor that looks for the model asset at the given file + /// system location. + /// {@endtemplate} + TextClassifierOptions.fromAssetPath( + String assetPath, { + ClassifierOptions classifierOptions = const ClassifierOptions(), + }); + + /// {@template TextClassifierOptions.fromAssetBuffer} + /// Convenience constructor that uses a model existing in memory. + /// {@endtemplate} + TextClassifierOptions.fromAssetBuffer( + Uint8List assetBuffer, { + ClassifierOptions classifierOptions = const ClassifierOptions(), + }); + + @override + BaseOptions get baseOptions => throw UnimplementedError(); + + @override + ClassifierOptions get classifierOptions => throw UnimplementedError(); +} + +/// {@macro TextClassifierResult} +class TextClassifierResult extends BaseTextClassifierResult { + /// {@template TextClassifierResult.fake} + /// Instantiates a [TextClassifierResult] with fake data for testing. + /// {@endtemplate} + TextClassifierResult({required Iterable classifications}); + + @override + Iterable get classifications => + throw UnimplementedError(); + + @override + // ignore: must_call_super + void dispose() => throw UnimplementedError(); +} + +/// {@macro TextEmbedder} +class TextEmbedder extends BaseTextEmbedder { + /// {@macro TextEmbedder} + TextEmbedder(TextEmbedderOptions options); + + @override + Future embed(String text) => throw UnimplementedError(); + + @override + Future cosineSimilarity(Embedding a, Embedding b) => + throw UnimplementedError(); + + @override + void dispose() => throw UnimplementedError(); +} + +/// {@macro TextEmbedderOptions} +/// This implementation is not immutable to track whether `dispose` has been +/// called. All values used by pkg:equatable are in fact immutable. +// ignore: must_be_immutable +class TextEmbedderOptions extends BaseTextEmbedderOptions { + /// {@template TextEmbedderOptions.fromAssetPath} + /// Convenience constructor that looks for the model asset at the given file + /// system location. + /// {@endtemplate} + TextEmbedderOptions.fromAssetPath( + String assetPath, { + EmbedderOptions embedderOptions = const EmbedderOptions(), + }); + + /// {@template TextEmbedderOptions.fromAssetBuffer} + /// Convenience constructor that uses a model existing in memory. + /// {@endtemplate} + TextEmbedderOptions.fromAssetBuffer( + Uint8List assetBuffer, { + EmbedderOptions embedderOptions = const EmbedderOptions(), + }); + + @override + BaseOptions get baseOptions => throw UnimplementedError(); + + @override + EmbedderOptions get embedderOptions => throw UnimplementedError(); +} + +/// {@macro TextEmbedderResult} +class TextEmbedderResult extends BaseEmbedderResult { + /// {@template TextEmbedderResult.fake} + /// Instantiates a [TextEmbedderResult] with fake data for testing. + /// {@endtemplate} + TextEmbedderResult({required Iterable embeddings}); + + @override + Iterable get embeddings => throw UnimplementedError(); + + @override + // ignore: must_call_super + void dispose() => throw UnimplementedError(); +} diff --git a/packages/mediapipe-task-text/pubspec.yaml b/packages/mediapipe-task-text/pubspec.yaml new file mode 100644 index 0000000..25beb57 --- /dev/null +++ b/packages/mediapipe-task-text/pubspec.yaml @@ -0,0 +1,29 @@ +name: mediapipe_text +description: A new Flutter package project. +version: 0.0.1 +repository: https://github.com/google/flutter-mediapipe +issue_tracker: https://github.com/google/flutter-mediapipe/issues/ +publish_to: none + +environment: + sdk: ">=3.1.0 <4.0.0" + flutter: ">=1.17.0" + +dependencies: + async: ^2.11.0 + ffi: ^2.1.0 + flutter: + sdk: flutter + http: ^1.1.0 + logging: ^1.2.0 + mediapipe_core: + path: ../mediapipe-core + native_assets_cli: ^0.3.0 + native_toolchain_c: ^0.3.0 + path: ^1.8.3 + +dev_dependencies: + ffigen: ^9.0.1 + flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter diff --git a/packages/mediapipe-task-text/sdk_downloads.dart b/packages/mediapipe-task-text/sdk_downloads.dart new file mode 100644 index 0000000..41f61c2 --- /dev/null +++ b/packages/mediapipe-task-text/sdk_downloads.dart @@ -0,0 +1,13 @@ +// Generated file. Do not manually edit. +final Map> sdkDownloadUrls = { + 'android': { + 'arm64': + 'https://storage.googleapis.com/mediapipe-nightly-public/prod/mediapipe/gcp_ubuntu_flutter/release/26/20240118-142736/android_arm64/libtext.so' + }, + 'macos': { + 'arm64': + 'https://storage.googleapis.com/mediapipe-nightly-public/prod/mediapipe/macos_flutter/release/9/20240118-123515/darwin_arm64/libtext.dylib', + 'x64': + 'https://storage.googleapis.com/mediapipe-nightly-public/prod/mediapipe/macos_flutter/release/9/20240118-123515/darwin_x86_64/libtext.dylib' + } +}; diff --git a/packages/mediapipe-task-text/test/text_classifier_executor_test.dart b/packages/mediapipe-task-text/test/text_classifier_executor_test.dart new file mode 100644 index 0000000..2ecc5ad --- /dev/null +++ b/packages/mediapipe-task-text/test/text_classifier_executor_test.dart @@ -0,0 +1,95 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// `native-assets` tag allows test runs to opt in or out of running integration +// tests via `flutter test -x native-assets` or `flutter test -t native-assets` +@Tags(['native-assets']) + +import 'dart:io' as io; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as path; +import 'package:mediapipe_core/io.dart'; +import 'package:mediapipe_text/io.dart'; + +void main() { + final pathToModel = path.joinAll([ + io.Directory.current.absolute.path, + 'example/assets/bert_classifier.tflite', + ]); + final modelBytes = io.File(pathToModel).readAsBytesSync(); + + group('TextClassifierExecutor should', () { + test('run a task', () { + final executor = TextClassifierExecutor( + TextClassifierOptions.fromAssetBuffer(modelBytes), + ); + final TextClassifierResult result = executor.classify('Hello, world!'); + expect(result.classifications, isNotEmpty); + executor.dispose(); + }); + + test('run multiple tasks', () { + final executor = TextClassifierExecutor( + TextClassifierOptions.fromAssetBuffer(modelBytes), + ); + final TextClassifierResult result = executor.classify('Hello, world!'); + expect(result.classifications, isNotEmpty); + final TextClassifierResult result2 = executor.classify('Hello, world!'); + expect(result2.classifications, isNotEmpty); + executor.dispose(); + }); + + test('unpack a result', () { + final executor = TextClassifierExecutor( + TextClassifierOptions.fromAssetBuffer(modelBytes), + ); + final TextClassifierResult result = executor.classify('Hello, world!'); + final classifications = result.classifications.first; + expect(classifications.headName, equals('probability')); + expect(classifications.categories, hasLength(2)); + expect(classifications.categories.first.categoryName, equals('positive')); + expect(classifications.categories.first.score, closeTo(0.9919, 0.0009)); + expect(classifications.categories.last.categoryName, equals('negative')); + expect(classifications.categories.last.score, closeTo(0.00804, 0.0009)); + executor.dispose(); + }); + + test('use the denylist', () { + final executor = TextClassifierExecutor( + TextClassifierOptions.fromAssetBuffer( + modelBytes, + classifierOptions: ClassifierOptions( + categoryDenylist: ['positive'], + ), + ), + ); + final TextClassifierResult result = executor.classify('Hello, world!'); + final classifications = result.classifications.first; + expect(classifications.headName, equals('probability')); + expect(classifications.categories, hasLength(1)); + expect(classifications.categories.first.categoryName, equals('negative')); + expect(classifications.categories.first.score, closeTo(0.00804, 0.0009)); + executor.dispose(); + }); + + test('use the allowlist', () { + final executor = TextClassifierExecutor( + TextClassifierOptions.fromAssetBuffer( + modelBytes, + classifierOptions: ClassifierOptions( + categoryAllowlist: ['positive'], + ), + ), + ); + final TextClassifierResult result = executor.classify('Hello, world!'); + final classifications = result.classifications.first; + expect(classifications.headName, equals('probability')); + expect(classifications.categories, hasLength(1)); + expect(classifications.categories.first.categoryName, equals('positive')); + expect(classifications.categories.first.score, closeTo(0.9919, 0.0009)); + result.dispose(); + executor.dispose(); + }); + }); +} diff --git a/packages/mediapipe-task-text/test/text_classifier_result_test.dart b/packages/mediapipe-task-text/test/text_classifier_result_test.dart new file mode 100644 index 0000000..98734ee --- /dev/null +++ b/packages/mediapipe-task-text/test/text_classifier_result_test.dart @@ -0,0 +1,46 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mediapipe_core/src/io/test_utils.dart'; +import 'package:mediapipe_text/io.dart'; +import 'package:mediapipe_core/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart' + as core_bindings; +import 'package:mediapipe_text/src/io/third_party/mediapipe/generated/mediapipe_text_bindings.dart' + as bindings; + +void main() { + group('TextClassifierResult.native should', () { + test('load an empty object', () { + final Pointer ptr = + calloc(); + // These fields are provided by the real MediaPipe implementation, but + // Dart ignores them because they are meaningless in context of text tasks + ptr.ref.classifications_count = 0; + ptr.ref.has_timestamp_ms = true; + + final result = TextClassifierResult.native(ptr); + expect(result.classifications, isEmpty); + }); + + test('load a hydrated object', () { + final Pointer resultPtr = + calloc(); + + final classificationsPtr = calloc(2); + populateClassifications(classificationsPtr[0]); + populateClassifications(classificationsPtr[1]); + + resultPtr.ref.classifications_count = 2; + resultPtr.ref.classifications = classificationsPtr; + resultPtr.ref.has_timestamp_ms = true; + resultPtr.ref.timestamp_ms = 0; + + final result = TextClassifierResult.native(resultPtr); + expect(result.classifications, hasLength(2)); + }, timeout: const Timeout(Duration(milliseconds: 10))); + }); +} diff --git a/packages/mediapipe-task-text/test/text_embedder_executor_test.dart b/packages/mediapipe-task-text/test/text_embedder_executor_test.dart new file mode 100644 index 0000000..6eb8d0b --- /dev/null +++ b/packages/mediapipe-task-text/test/text_embedder_executor_test.dart @@ -0,0 +1,100 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// `native-assets` tag allows test runs to opt in or out of running integration +// tests via `flutter test -x native-assets` or `flutter test -t native-assets` +@Tags(['native-assets']) + +import 'dart:io' as io; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as path; +import 'package:mediapipe_core/io.dart'; +import 'package:mediapipe_text/io.dart'; + +void main() { + final pathToModel = path.joinAll([ + io.Directory.current.absolute.path, + 'example/assets/universal_sentence_encoder.tflite', + ]); + final modelBytes = io.File(pathToModel).readAsBytesSync(); + + group('TextEmbedderExecutor should', () { + test('run a task', () { + final executor = TextEmbedderExecutor( + TextEmbedderOptions.fromAssetBuffer(modelBytes), + ); + final TextEmbedderResult result = executor.embed('Hello, world!'); + expect(result.embeddings, isNotEmpty); + result.dispose(); + executor.dispose(); + }); + + test('run multiple tasks', () { + final executor = TextEmbedderExecutor( + TextEmbedderOptions.fromAssetBuffer(modelBytes), + ); + final TextEmbedderResult result = executor.embed('Hello, world!'); + expect(result.embeddings, isNotEmpty); + final TextEmbedderResult result2 = executor.embed('Hello, world!'); + expect(result2.embeddings, isNotEmpty); + result.dispose(); + result2.dispose(); + executor.dispose(); + }); + + test('unpack a result', () { + final executor = TextEmbedderExecutor( + TextEmbedderOptions.fromAssetBuffer(modelBytes), + ); + final TextEmbedderResult result = executor.embed('Hello, world!'); + final embedding = result.embeddings.first; + expect(embedding.headName, 'response_encoding'); + expect(() => embedding.quantizedEmbedding, throwsA(isA())); + expect(embedding.floatEmbedding, isNotNull); + expect(embedding.length, 100); + expect(embedding.type, equals(EmbeddingType.float)); + expect(embedding.floatEmbedding![0], closeTo(1.7475, 0.0001)); + result.dispose(); + executor.dispose(); + }); + + test('quantize results when requested', () { + final executor = TextEmbedderExecutor( + TextEmbedderOptions.fromAssetBuffer( + modelBytes, + embedderOptions: EmbedderOptions(quantize: true), + ), + ); + final TextEmbedderResult result = executor.embed('Hello, world!'); + final embedding = result.embeddings.first; + expect(embedding.headName, 'response_encoding'); + expect(embedding.quantizedEmbedding, isNotNull); + expect(() => embedding.floatEmbedding, throwsA(isA())); + expect(embedding.quantizedEmbedding![0], 127); + expect(embedding.length, 100); + expect(embedding.type, equals(EmbeddingType.quantized)); + result.dispose(); + executor.dispose(); + }); + + test('normalize', () { + final executor = TextEmbedderExecutor( + TextEmbedderOptions.fromAssetBuffer( + modelBytes, + embedderOptions: EmbedderOptions(l2Normalize: true), + ), + ); + final TextEmbedderResult result = executor.embed('Hello, world!'); + final embedding = result.embeddings.first; + expect(embedding.headName, 'response_encoding'); + expect(() => embedding.quantizedEmbedding, throwsA(isA())); + expect(embedding.floatEmbedding, isNotNull); + expect(embedding.floatEmbedding![0], closeTo(0.1560, 0.0001)); + expect(embedding.length, 100); + expect(embedding.type, equals(EmbeddingType.float)); + result.dispose(); + executor.dispose(); + }); + }); +} diff --git a/packages/mediapipe-task-text/test/text_embedder_result_test.dart b/packages/mediapipe-task-text/test/text_embedder_result_test.dart new file mode 100644 index 0000000..8f310bd --- /dev/null +++ b/packages/mediapipe-task-text/test/text_embedder_result_test.dart @@ -0,0 +1,51 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mediapipe_core/src/io/test_utils.dart'; +import 'package:mediapipe_text/io.dart'; +import 'package:mediapipe_core/src/io/third_party/mediapipe/generated/mediapipe_common_bindings.dart' + as core_bindings; +import 'package:mediapipe_text/src/io/third_party/mediapipe/generated/mediapipe_text_bindings.dart' + as bindings; + +void main() { + group('TextEmbedderResult.native should', () { + test('load an empty object', () { + final Pointer ptr = + calloc(); + // These fields are provided by the real MediaPipe implementation, but + // Dart ignores them because they are meaningless in context of text tasks + ptr.ref.embeddings_count = 0; + ptr.ref.has_timestamp_ms = false; + + final result = TextEmbedderResult.native(ptr); + expect(result.embeddings, isEmpty); + }); + + test('load a hydrated object', () { + final Pointer resultPtr = + calloc(); + + final embeddingsPtr = calloc(2); + populateEmbedding(embeddingsPtr[0], length: 50); + populateEmbedding(embeddingsPtr[1], length: 25); + + resultPtr.ref.embeddings_count = 2; + resultPtr.ref.embeddings = embeddingsPtr; + resultPtr.ref.has_timestamp_ms = false; + + final result = TextEmbedderResult.native(resultPtr); + expect(result.embeddings, hasLength(2)); + final embedding = result.embeddings.take(1).toList().first; + expect(embedding.type, EmbeddingType.float); + expect(embedding.length, 50); + final embedding2 = result.embeddings.skip(1).take(1).toList().last; + expect(embedding2.type, EmbeddingType.float); + expect(embedding2.length, 25); + }, timeout: const Timeout(Duration(milliseconds: 10))); + }); +} diff --git a/packages/mediapipe-task-text/third_party/mediapipe/tasks/c/text/text_classifier/text_classifier.h b/packages/mediapipe-task-text/third_party/mediapipe/tasks/c/text/text_classifier/text_classifier.h new file mode 100644 index 0000000..fae6c26 --- /dev/null +++ b/packages/mediapipe-task-text/third_party/mediapipe/tasks/c/text/text_classifier/text_classifier.h @@ -0,0 +1,74 @@ +/* Copyright 2023 The MediaPipe Authors. + +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. +==============================================================================*/ + +#ifndef MEDIAPIPE_TASKS_C_TEXT_TEXT_CLASSIFIER_TEXT_CLASSIFIER_H_ +#define MEDIAPIPE_TASKS_C_TEXT_TEXT_CLASSIFIER_TEXT_CLASSIFIER_H_ + +#include "../../../../../../../mediapipe-core/third_party/mediapipe/tasks/c/components/containers/classification_result.h" +#include "../../../../../../../mediapipe-core/third_party/mediapipe/tasks/c/components/processors/classifier_options.h" +#include "../../../../../../../mediapipe-core/third_party/mediapipe/tasks/c/core/base_options.h" + +#ifndef MP_EXPORT +#define MP_EXPORT __attribute__((visibility("default"))) +#endif // MP_EXPORT + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ClassificationResult TextClassifierResult; + +// The options for configuring a MediaPipe text classifier task. +struct TextClassifierOptions { + // Base options for configuring MediaPipe Tasks, such as specifying the model + // file with metadata, accelerator options, op resolver, etc. + struct BaseOptions base_options; + + // Options for configuring the classifier behavior, such as score threshold, + // number of results, etc. + struct ClassifierOptions classifier_options; +}; + +// Creates a TextClassifier from the provided `options`. +// Returns a pointer to the text classifier on success. +// If an error occurs, returns `nullptr` and sets the error parameter to an +// an error message (if `error_msg` is not `nullptr`). You must free the memory +// allocated for the error message. +MP_EXPORT void* text_classifier_create(struct TextClassifierOptions* options, + char** error_msg); + +// Performs classification on the input `text`. Returns `0` on success. +// If an error occurs, returns an error code and sets the error parameter to an +// an error message (if `error_msg` is not `nullptr`). You must free the memory +// allocated for the error message. +MP_EXPORT int text_classifier_classify(void* classifier, const char* utf8_str, + TextClassifierResult* result, + char** error_msg); + +// Frees the memory allocated inside a TextClassifierResult result. Does not +// free the result pointer itself. +MP_EXPORT void text_classifier_close_result(TextClassifierResult* result); + +// Shuts down the TextClassifier when all the work is done. Frees all memory. +// If an error occurs, returns an error code and sets the error parameter to an +// an error message (if `error_msg` is not `nullptr`). You must free the memory +// allocated for the error message. +MP_EXPORT int text_classifier_close(void* classifier, char** error_msg); + +#ifdef __cplusplus +} // extern C +#endif + +#endif // MEDIAPIPE_TASKS_C_TEXT_TEXT_CLASSIFIER_TEXT_CLASSIFIER_H_ diff --git a/packages/mediapipe-task-text/third_party/mediapipe/tasks/c/text/text_embedder/text_embedder.h b/packages/mediapipe-task-text/third_party/mediapipe/tasks/c/text/text_embedder/text_embedder.h new file mode 100644 index 0000000..5b3b530 --- /dev/null +++ b/packages/mediapipe-task-text/third_party/mediapipe/tasks/c/text/text_embedder/text_embedder.h @@ -0,0 +1,84 @@ +/* Copyright 2023 The MediaPipe Authors. + +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. +==============================================================================*/ + +#ifndef MEDIAPIPE_TASKS_C_TEXT_TEXT_EMBEDDER_TEXT_EMBEDDER_H_ +#define MEDIAPIPE_TASKS_C_TEXT_TEXT_EMBEDDER_TEXT_EMBEDDER_H_ + +#include "../../../../../../../mediapipe-core/third_party/mediapipe/tasks/c/components/containers/embedding_result.h" +#include "../../../../../../../mediapipe-core/third_party/mediapipe/tasks/c/components/processors/embedder_options.h" +#include "../../../../../../../mediapipe-core/third_party/mediapipe/tasks/c/core/base_options.h" + +#ifndef MP_EXPORT +#define MP_EXPORT __attribute__((visibility("default"))) +#endif // MP_EXPORT + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct EmbeddingResult TextEmbedderResult; + +// The options for configuring a MediaPipe text embedder task. +struct TextEmbedderOptions { + // Base options for configuring MediaPipe Tasks, such as specifying the model + // file with metadata, accelerator options, op resolver, etc. + struct BaseOptions base_options; + + // Options for configuring the embedder behavior, such as l2_normalize + // and quantize. + struct EmbedderOptions embedder_options; +}; + +// Creates a TextEmbedder from the provided `options`. +// Returns a pointer to the text embedder on success. +// If an error occurs, returns `nullptr` and sets the error parameter to an +// an error message (if `error_msg` is not `nullptr`). You must free the memory +// allocated for the error message. +MP_EXPORT void* text_embedder_create(struct TextEmbedderOptions* options, + char** error_msg); + +// Performs embedding extraction on the input `text`. Returns `0` on success. +// If an error occurs, returns an error code and sets the error parameter to an +// an error message (if `error_msg` is not `nullptr`). You must free the memory +// allocated for the error message. +MP_EXPORT int text_embedder_embed(void* embedder, const char* utf8_str, + TextEmbedderResult* result, char** error_msg); + +// Frees the memory allocated inside a TextEmbedderResult result. Does not +// free the result pointer itself. +MP_EXPORT void text_embedder_close_result(TextEmbedderResult* result); + +// Shuts down the TextEmbedder when all the work is done. Frees all memory. +// If an error occurs, returns an error code and sets the error parameter to an +// an error message (if `error_msg` is not `nullptr`). You must free the memory +// allocated for the error message. +MP_EXPORT int text_embedder_close(void* embedder, char** error_msg); + +// Utility function to compute cosine similarity [1] between two embeddings. +// May return an InvalidArgumentError if e.g. the embeddings are of different +// types (quantized vs. float), have different sizes, or have a an L2-norm of +// 0. +// +// [1]: https://en.wikipedia.org/wiki/Cosine_similarity +MP_EXPORT int text_embedder_cosine_similarity(const struct Embedding* u, + const struct Embedding* v, + double* similarity, + char** error_msg); + +#ifdef __cplusplus +} // extern C +#endif + +#endif // MEDIAPIPE_TASKS_C_TEXT_TEXT_EMBEDDER_TEXT_EMBEDDER_H_ diff --git a/packages/mediapipe-task-vision/sdk_downloads.dart b/packages/mediapipe-task-vision/sdk_downloads.dart new file mode 100644 index 0000000..d01418e --- /dev/null +++ b/packages/mediapipe-task-vision/sdk_downloads.dart @@ -0,0 +1,13 @@ +// Generated file. Do not manually edit. +final Map> sdkDownloadUrls = { + 'android': { + 'arm64': + 'https://storage.googleapis.com/mediapipe-nightly-public/prod/mediapipe/gcp_ubuntu_flutter/release/26/20240118-142736/android_arm64/libvision.so' + }, + 'macos': { + 'arm64': + 'https://storage.googleapis.com/mediapipe-nightly-public/prod/mediapipe/macos_flutter/release/9/20240118-123515/darwin_arm64/libvision.dylib', + 'x64': + 'https://storage.googleapis.com/mediapipe-nightly-public/prod/mediapipe/macos_flutter/release/9/20240118-123515/darwin_x86_64/libvision.dylib' + } +}; diff --git a/tool/builder/.gitignore b/tool/builder/.gitignore new file mode 100644 index 0000000..3a85790 --- /dev/null +++ b/tool/builder/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/tool/builder/CHANGELOG.md b/tool/builder/CHANGELOG.md new file mode 100644 index 0000000..16c8153 --- /dev/null +++ b/tool/builder/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version with headers command to sync C headers from `google/mediapipe`. diff --git a/tool/builder/README.md b/tool/builder/README.md new file mode 100644 index 0000000..8f37332 --- /dev/null +++ b/tool/builder/README.md @@ -0,0 +1,20 @@ +# Flutter MediaPipe builder + +Helper utility which performs build or CI/CD step operations necessary to develop, release, and use `flutter-mediapipe`. + +### Usage: + +Usage depends on which task you need to accomplish. All supported workflows are described below. + +#### Header aggregation + +Header files across all tasks in `google/flutter-mediapipe` have to stay in sync with their origin, `google/mediapipe`. To +resync these files, check out both repositories on the same machine (ideally next to each other on the file system) and run: + +```sh +$ dart tool/builder/bin/main.dart headers +``` + +-- + +Check back in the future for any additional development workflows this command may support. \ No newline at end of file diff --git a/tool/builder/analysis_options.yaml b/tool/builder/analysis_options.yaml new file mode 100644 index 0000000..f70b759 --- /dev/null +++ b/tool/builder/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:lints/recommended.yaml + +analyzer: + errors: + todo: ignore diff --git a/tool/builder/bin/main.dart b/tool/builder/bin/main.dart new file mode 100644 index 0000000..ccf00cf --- /dev/null +++ b/tool/builder/bin/main.dart @@ -0,0 +1,19 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:args/command_runner.dart'; +import 'package:builder/download_model.dart'; +import 'package:builder/sdks_finder.dart'; +import 'package:builder/sync_headers.dart'; + +final runner = CommandRunner( + 'build', + 'Performs build operations for google/flutter-mediapipe that ' + 'depend on contents in this repository', +) + ..addCommand(DownloadModelCommand()) + ..addCommand(SdksFinderCommand()) + ..addCommand(SyncHeadersCommand()); + +void main(List arguments) => runner.run(arguments); diff --git a/tool/builder/lib/download_model.dart b/tool/builder/lib/download_model.dart new file mode 100644 index 0000000..bc5128c --- /dev/null +++ b/tool/builder/lib/download_model.dart @@ -0,0 +1,159 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; +import 'package:args/command_runner.dart'; +import 'package:builder/repo_finder.dart'; +import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +final _log = Logger('DownloadModelCommand'); + +enum Model { + textclassification, + textembedding, + languagedetection, +} + +class DownloadModelCommand extends Command with RepoFinderMixin { + @override + String description = 'Downloads a given MediaPipe model and places it in ' + 'the designated location.'; + @override + String name = 'model'; + + DownloadModelCommand() { + argParser + ..addOption( + 'model', + abbr: 'm', + allowed: [ + // Values will be added to this as the repository gets more + // integration tests that require new models. + Model.textclassification.name, + Model.textembedding.name, + Model.languagedetection.name, + ], + help: 'The desired model to download. Use this option if you want the ' + 'standard model for a given task. Using this option also removes any ' + 'need to use the `destination` option, as a value here implies a ' + 'destination. However, you still can specify a destination to ' + 'override the default location where the model is placed.\n' + '\n' + 'Note: Either this or `custommodel` must be used. If both are ' + 'supplied, `model` is used.', + ) + ..addOption( + 'custommodel', + abbr: 'c', + help: 'The desired model to download. Use this option if you want to ' + 'specify a specific and nonstandard model. Using this option means ' + 'you *must* use the `destination` option.\n' + '\n' + 'Note: Either this or `model` must be used. If both are supplied, ' + '`model` is used.', + ) + ..addOption( + 'destination', + abbr: 'd', + help: + 'The location to place the downloaded model. This value is required ' + 'if you use the `custommodel` option, but optional if you use the ' + '`model` option.', + ); + addVerboseOption(argParser); + } + + String getModelSource() => switch (model) { + Model.textclassification => + 'https://storage.googleapis.com/mediapipe-models/text_classifier/bert_classifier/float32/latest/bert_classifier.tflite', + Model.textembedding => + 'https://storage.googleapis.com/mediapipe-models/text_embedder/universal_sentence_encoder/float32/latest/universal_sentence_encoder.tflite', + Model.languagedetection => + 'https://storage.googleapis.com/mediapipe-models/language_detector/language_detector/float32/latest/language_detector.tflite', + }; + + String getModelDestination() => switch (model) { + Model.textclassification => + 'packages/mediapipe-task-text/example/assets/', + Model.textembedding => 'packages/mediapipe-task-text/example/assets/', + Model.languagedetection => + 'packages/mediapipe-task-text/example/assets/', + }; + + Model get model { + final value = Model.values.asNameMap()[argResults!['model']]; + if (value == null) { + throw ('Unexpected Model value ${argResults!['model']}'); + } + return value; + } + + @override + Future run() async { + setUpLogging(); + final io.Directory flutterMediaPipeDirectory = findFlutterMediaPipeRoot(); + + late final String modelSource; + late final String modelDestination; + + if (argResults!['model'] != null) { + modelSource = getModelSource(); + modelDestination = (_isArgProvided(argResults!['destination'])) + ? argResults!['destination'] + : getModelDestination(); + } else { + if (argResults!['custommodel'] == null) { + throw Exception( + 'You must use either the `model` or `custommodel` option.', + ); + } + if (argResults!['destination'] == null) { + throw Exception( + 'If you use the `custommodel` option, then you must supply a ' + '`destination`, as a "standard" destination cannot be used.', + ); + } + modelSource = argResults!['custommodel']; + modelDestination = argResults!['destination']; + } + + io.File destinationFile = io.File( + path.joinAll([ + flutterMediaPipeDirectory.absolute.path, + modelDestination, + modelSource.split('/').last, + ]), + ); + ensureFolders(destinationFile); + await downloadModel(modelSource, destinationFile); + } + + Future downloadModel( + String modelSource, + io.File destinationFile, + ) async { + _log.info('Downloading $modelSource'); + + // TODO(craiglabenz): Convert to StreamedResponse + final response = await http.get(Uri.parse(modelSource)); + + if (response.statusCode != 200) { + throw Exception('${response.statusCode} ${response.reasonPhrase} :: ' + '$modelSource'); + } + + if (!(await destinationFile.exists())) { + _log.fine('Creating file at ${destinationFile.absolute.path}'); + await destinationFile.create(); + } + + _log.fine('Downloaded ${response.contentLength} bytes'); + _log.info('Saving to ${destinationFile.absolute.path}'); + await destinationFile.writeAsBytes(response.bodyBytes); + } +} + +bool _isArgProvided(String? val) => val != null && val != ''; diff --git a/tool/builder/lib/extensions.dart b/tool/builder/lib/extensions.dart new file mode 100644 index 0000000..362d8a6 --- /dev/null +++ b/tool/builder/lib/extensions.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; +import 'dart:io'; + +extension EasyOutput on Process { + Future> get processedStdErr => _process(this.stderr); + + Future> get processedStdOut => _process(this.stdout); + + Future> _process(Stream> stream) async { + return utf8.decoder + .convert((await stream.toList()) + .fold>([], (arr, el) => arr..addAll(el))) + .split('\n'); + } +} + +/// Returns the last full chunk from a Url-like String. +/// +/// From "/an/awesome/url/", returns "url". +/// From "/an/awesome/url", returns "url". +/// From "/an/awesome/url/", with a depth of 1, returns "awesome" +/// From "/an/awesome/url", with a depth of 1, returns "awesome" +String lastChunk(String url, {int depth = 0, String delimiter = '/'}) { + final indexOffset = (url.endsWith(delimiter)) ? -2 - depth : -1 - depth; + final splitUrl = url.split(delimiter); + return splitUrl[splitUrl.length + indexOffset]; +} + +extension DefaultableMap on Map { + void setDefault(K key, V def) { + if (!containsKey(key)) { + this[key] = def; + } + } +} diff --git a/tool/builder/lib/repo_finder.dart b/tool/builder/lib/repo_finder.dart new file mode 100644 index 0000000..4faaa66 --- /dev/null +++ b/tool/builder/lib/repo_finder.dart @@ -0,0 +1,133 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; +import 'package:args/args.dart'; +import 'package:args/command_runner.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:io/ansi.dart'; + +/// Mixin to help [Command] subclasses locate both `google/mediapipe` and +/// the root of `google/flutter-mediapipe` (this repository). +/// +/// The primary methods are [findFlutterMediaPipeRoot] and [findMediaPipeRoot]. +/// +/// By default, the root for `google/flutter-mediapipe` is determined by the +/// firest ancestor directory which contains a `.flutter-mediapipe-root` file +/// (whose contents are irrelevant), and the root of `google/mediapipe` is +/// expected to be a sibling of that. However, the `--source` flag can overwrite +/// this expectation and specify an absolute path where to find `google/mediapipe`. +/// +/// Note that it is not possible to override the method of locating the root of +/// `google/flutter-mediapipe`. +mixin RepoFinderMixin on Command { + /// Name of the file which, when found, indicates the root of this repository. + static String sentinelFileName = '.flutter-mediapipe-root'; + + void addSourceOption(ArgParser argParser) { + argParser.addOption( + 'source', + abbr: 's', + help: 'The location of google/mediapipe. Defaults to being ' + 'adjacent to google/flutter-mediapipe.', + ); + } + + void addVerboseOption(ArgParser argParser) => + argParser.addFlag('verbose', abbr: 'v', defaultsTo: false); + + void setUpLogging() { + final bool verbose = argResults!['verbose']; + Logger.root.level = verbose ? Level.FINEST : Level.INFO; + Logger.root.onRecord.listen((LogRecord record) { + io.stdout.writeln( + '[${record.loggerName}][${record.level.name}] ${record.message}'); + }); + } + + /// Looks upward for the root of the `google/mediapipe` repository. This assumes + /// the `dart build` command is executed from within said repository. If it is + /// not executed from within, then this searching algorithm will reach the root + /// of the file system, log the error, and exit. + io.Directory findFlutterMediaPipeRoot() { + final placesChecked = []; + io.Directory dir = io.Directory(path.current); + while (true) { + if (_isFlutterMediaPipeRoot(dir)) { + return dir; + } + placesChecked.add(dir); + dir = dir.parent; + if (dir.parent.path == dir.path) { + io.stderr.writeln( + wrapWith( + 'Failed to find google/flutter-mediapipe root directory. ' + 'Did you execute this command from within the repository?\n' + 'Looked in:', + [red], + ), + ); + io.stderr.writeln( + wrapWith( + placesChecked + .map((dir) => ' - ${dir.absolute.path}') + .toList() + .join('\n'), + [red], + ), + ); + io.exit(1); + } + } + } + + /// Finds the `google/mediapipe` checkout where artifacts built in this + /// repository should be sourced. By default, this command assumes the two + /// repositories are siblings on the file system, but the `--source` flag + /// allows for this assumption to be overridden. + io.Directory findMediaPipeRoot( + io.Directory flutterMediaPipeDir, + String? source, + ) { + final mediaPipeDirectory = io.Directory( + source ?? + path.joinAll([flutterMediaPipeDir.parent.absolute.path, 'mediapipe']), + ); + + if (!mediaPipeDirectory.existsSync()) { + io.stderr.writeln( + 'Could not find ${mediaPipeDirectory.absolute.path}. ' + 'Folder does not exist.', + ); + io.exit(1); + } + return mediaPipeDirectory; + } + + /// Looks for the sentinel file of this repository's root directory. This allows + /// the `dart build` command to be run from various locations within the + /// `google/mediapipe` repository and still correctly set paths for all of its + /// operations. + bool _isFlutterMediaPipeRoot(io.Directory dir) { + return io.File( + path.joinAll( + [dir.absolute.path, sentinelFileName], + ), + ).existsSync(); + } + + /// Builds any missing folders between the file and the root of the repository + void ensureFolders(io.File file) { + io.Directory parent = file.parent; + List dirsToCreate = []; + while (!parent.existsSync()) { + dirsToCreate.add(parent); + parent = parent.parent; + } + for (io.Directory dir in dirsToCreate.reversed) { + dir.createSync(); + } + } +} diff --git a/tool/builder/lib/sdks_finder.dart b/tool/builder/lib/sdks_finder.dart new file mode 100644 index 0000000..c150270 --- /dev/null +++ b/tool/builder/lib/sdks_finder.dart @@ -0,0 +1,398 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:builder/extensions.dart'; +import 'package:builder/repo_finder.dart'; +import 'package:io/ansi.dart'; +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:path/path.dart' as path; + +final _log = Logger('SDKsFinder'); + +/// Structure of flattened build locations suitable for JSON serialization. +/// Format is: +/// { +/// : { +/// : , +/// ... +/// }, +/// ... +/// } +typedef _FlatResults = Map>; + +/// Container for the three flavors of MediaPipe tasks. +enum MediaPipeSdk { + libaudio, + libtext, + libvision; + + String toPackageDir() => switch (this) { + MediaPipeSdk.libaudio => 'mediapipe-task-audio', + MediaPipeSdk.libtext => 'mediapipe-task-text', + MediaPipeSdk.libvision => 'mediapipe-task-vision', + }; +} + +/// Scans the GCS buckets where MediaPipe SDKs are stored, identifies the latest +/// builds for each supported build target, and writes the output to a +/// designated file. +/// +/// [SdksFinderCommand] depends on `gsutil` and a Google-corp account that has +/// permissions to read the necessary buckets, so the idea is to run this +/// command on Googlers' work machines whenever MediaPipe rebuilds SDKs. The +/// manual nature of this command is suboptimal, but there is no precedent for +/// fully automating this portion of the MediaPipe release process, so this +/// command helps by automating a portion of the task. +/// +/// The cache-busting mechanism of Flutter's native assets feature is a hash +/// of the contents of any build dependencies. The output files of this command +/// are included in the build dependencies (as specified by the contents of each +/// package's `build.dart` file), so if this command generates new SDK locations +/// in those files, Flutter's CLI will have a cache miss, will re-run +/// `build.dart` during the build phase, and in turn will download the newest +/// versions of the MediaPipe SDKs onto the developer's machine. +/// +/// Operationally, [SdksFinderCommand]'s implementation involves orchestrating +/// one [_OsFinder] instance for each supported [OS] value, which in turn +/// identifies the correct build that has been uploaded to GCS by MediaPipe +/// release machinery. +/// +/// Usage: +/// ```sh +/// $ cd path/to/flutter-mediapipe +/// $ dart tool/builder/bin/main.dart sdks [-v] +/// ``` +class SdksFinderCommand extends Command with RepoFinderMixin { + SdksFinderCommand() { + addVerboseOption(argParser); + } + @override + String description = + 'Updates MediaPipe SDK manifest files for the current build target'; + + @override + String name = 'sdks'; + + static const _gcsPrefix = 'https://storage.googleapis.com'; + + /// Google Storage bucket which houses all MediaPipe SDK uploads. + static const _bucketName = 'mediapipe-nightly-public/prod/mediapipe'; + + final _finders = <_OsFinder>[ + _OsFinder(OS.android), + _OsFinder(OS.macOS), + // TODO: Add other values as their support is ready + ]; + + @override + Future run() async { + setUpLogging(); + _checkGsUtil(); + final results = _SdkLocations(); + + for (final finder in _finders) { + await for (final sdkLocation in finder.find()) { + results.add(sdkLocation); + } + } + for (final MediaPipeSdk sdk in MediaPipeSdk.values) { + _log.info('Saving locations for ${sdk.name}'); + _writeResults(sdk, results.toMap(sdk)); + } + } + + void _writeResults(MediaPipeSdk sdk, _FlatResults results) { + final file = _getOutputFile(sdk); + _log.fine('Writing data to "${file.absolute.path}"'); + var encoder = JsonEncoder.withIndent(' '); + file.writeAsStringSync('''// Generated file. Do not manually edit. +final Map> sdkDownloadUrls = ${encoder.convert(results).replaceAll('"', "'")}; +'''); + io.Process.start('dart', ['format', file.absolute.path]); + } + + File _getOutputFile(MediaPipeSdk sdk) { + return File( + path.joinAll([ + findFlutterMediaPipeRoot().absolute.path, + 'packages/${sdk.toPackageDir()}', + 'sdk_downloads.dart', + ]), + ); + } + + void _checkGsUtil() async { + if (!io.Platform.isMacOS && !io.Platform.isLinux) { + // `which` is not available on Windows, so allow the command to attempt + // to run on Windows + // TODO: possibly add Windows-specific support + return; + } + final process = await Process.start('which', ['gsutil']); + final exitCode = await process.exitCode; + final List processStdOut = await process.processedStdOut; + if (exitCode != 0) { + stderr.writeln( + wrapWith( + 'Warning: Unexpected exit code $exitCode checking for gsutil. Output:' + '${processStdOut.join('\n')}', + [yellow], + ), + ); + // Not exiting here, since this could be a false-negative. + } + if (processStdOut.isEmpty) { + stderr.writeln( + wrapWith( + 'gsutil command not found. Visit: ' + 'https://cloud.google.com/storage/docs/gsutil_install', + [red], + ), + ); + exit(1); + } + } +} + +/// Main workhorse of the SdksFinderCommand. Navigates folders in GCS to find +/// the location of the latest builds for each [MediaPipeSdk] / [Architecture] +/// combination for the given [OS]. +/// +/// Usage: +/// ```dart +/// final macOsFinder = _OsFinder(OS.macOS); +/// await for (final _SdkLocation sdkLoc in macOsFinder.find()) { +/// doSomethingWithLocation(sdkLoc); +/// } +/// ``` +class _OsFinder { + _OsFinder(this.os); + + /// OS-specific upload directories located immediately inside + /// [SdksFinderCommand._bucketName]. + static const _gcsFolderPaths = { + OS.android: 'gcp_ubuntu_flutter', + OS.iOS: null, + OS.macOS: 'macos_flutter', + }; + + /// File extensions for OS-specific SDKs. + static const _sdkExtensions = { + OS.android: 'so', + OS.iOS: 'dylib', + OS.macOS: 'dylib', + }; + + /// Folders for specific [Target] values, where a Target is an OS/architecture + /// combination. + static const targetFolders = >{ + OS.android: { + 'android_arm64': Architecture.arm64, + }, + OS.iOS: {}, + OS.macOS: { + 'darwin_arm64': Architecture.arm64, + 'darwin_x86_64': Architecture.x64, + }, + }; + + final OS os; + + String get folderPath => '${_gcsFolderPaths[os]!}/release'; + String get extension => _sdkExtensions[os]!; + + /// Scans the appropriate GCS location for all build Ids for the given OS and + /// returns the highest integer found. + Future _getBuildNumber(String path) async { + int highestBuildNumber = 0; + + for (final folder in await _gsUtil(path)) { + late int buildId; + try { + // Grab last chunk, since we're looking for a folder of the + // structure: `.../release/:int/` + buildId = int.parse(lastChunk(folder)); + } catch (e) { + // Probably the `{id}_$folder$` directory + continue; + } + if (buildId > highestBuildNumber) { + highestBuildNumber = buildId; + } + } + _log.fine('Highest build number for $os is $highestBuildNumber'); + return highestBuildNumber; + } + + /// Extracts the date within a build directory, which is where the final + /// artifacts can be found. + /// + /// Usage: + /// ```dart + /// final path = await _getDateOfBuildNumber('.../gcp_ubuntu_flutter/release/', 17); + /// print(path); + /// >>> ".../gcp_ubuntu_flutter/release/17/20231212-090734/" + /// ``` + Future _getDateOfBuildNumber(String path) async { + final foldersInBuild = await _gsUtil(path); + if (foldersInBuild.isEmpty || foldersInBuild.length > 2) { + final paths = + foldersInBuild.map((path) => ' • $path').toList().join('\n'); + _log.warning('Unexpectedly found ${foldersInBuild.length} entries inside ' + 'build folder: $path. Expected 1 or 2, of formats "/[date]/" and ' + 'optionally "/[date]_\$folder\$". Found:\n\n$paths\n'); + } + for (final folderPath in foldersInBuild) { + if (folderPath.endsWith('/')) { + final buildDateFolderPath = lastChunk(folderPath); + _log.fine('$folderPath :: $buildDateFolderPath'); + return buildDateFolderPath; + } + } + throw Exception( + 'Unexpected structure of build folder: "$path". Did not find match.', + ); + } + + /// Receives a [Path] like ".../[os_folder]/release/[build_number]/[date]/" + /// and yields all matching architecture folders within. + Stream _getArchitectectures(String path) async* { + final pathsWithinBuild = []; + final expectedFolders = targetFolders[os]!.keys.toSet(); + for (final pathWithinBuild in await _gsUtil(path)) { + pathsWithinBuild.add(lastChunk(pathWithinBuild)); + final maybeArchitecture = lastChunk(pathWithinBuild); + if (targetFolders[os]!.containsKey(maybeArchitecture)) { + expectedFolders.remove(maybeArchitecture); + yield maybeArchitecture; + } + } + if (expectedFolders.isNotEmpty) { + _log.warning( + 'Did not find all expected folders in "$path".\n ' + 'Expected to find ${targetFolders[os]!.keys.toSet()}.\n ' + 'Did not find $expectedFolders.\n ' + 'Folders in path were: $pathsWithinBuild.', + ); + } + } + + /// Combines the path, file name, and extension into the final, complete path. + /// Additionally, checks whether that file actually exists and returns the + /// String value if it does, or `null` if it does not. + Future _getAndCheckFullPath(String path, MediaPipeSdk sdk) async { + final pathToCheck = '$path/${sdk.name}.${_sdkExtensions[os]!}'; + final output = await _gsUtil(pathToCheck); + if (output.isEmpty) { + return null; + } + return pathToCheck; + } + + Stream<_SdkLocation> find() async* { + _log.info('Finding SDKs for $os'); + String path = folderPath; + final buildNumber = await _getBuildNumber(path); + path = '$path/$buildNumber'; + _log.finest('$os :: build number :: $path'); + final buildDate = await _getDateOfBuildNumber(path); + path = '$path/$buildDate'; + _log.finest('$os :: date :: $path'); + + await for (final String archPath in _getArchitectectures(path)) { + String pathWithArch = '$path/$archPath'; + _log.finest('$os :: $archPath :: $pathWithArch'); + for (final sdk in MediaPipeSdk.values) { + final maybeFinalPath = await _getAndCheckFullPath(pathWithArch, sdk); + _log.finest('$os :: maybeFinalPath :: $maybeFinalPath'); + if (maybeFinalPath != null) { + _log.fine('Found "$maybeFinalPath"'); + yield _SdkLocation( + os: os, + arch: targetFolders[os]![archPath]!, + sdk: sdk, + fullPath: '${SdksFinderCommand._gcsPrefix}/' + '${SdksFinderCommand._bucketName}/$maybeFinalPath', + ); + } + } + } + } +} + +// Runs `gsutil ls` against the path, optionally with the `-r` flag. +Future> _gsUtil(String path, {bool recursive = false}) async { + assert( + !path.startsWith('http'), + 'gsutil requires URIs with the "gs" scheme, which this function will add.', + ); + final cmd = [ + 'ls', + if (recursive) '-r', + 'gs://${SdksFinderCommand._bucketName}/$path', + ]; + _log.finest('Running: `gsutil ${cmd.join(' ')}`'); + final process = await Process.start('gsutil', cmd); + final exitCode = await process.exitCode; + if (exitCode > 1) { + // Exit codes of 1 appear when `gsutil` checks for a file that does not + // exist, which for our purposes does not constitute an actual error, and is + // handled later when `process.processedStdOut` is empty. + stderr.writeln( + wrapWith( + 'Warning: Unexpected exit code $exitCode running ' + '`gsutil ${cmd.join(' ')}`. Output: ' + '${(await process.processedStdOut).join('\n')}', + [red], + ), + ); + exit(exitCode); + } + final processStdout = await process.processedStdOut; + final filtered = (processStdout).where((String line) => line != '').toList(); + return filtered; +} + +/// Simple container for the location of a specific MediaPipe SDK in GCS. +class _SdkLocation { + _SdkLocation({ + required this.os, + required this.arch, + required this.sdk, + required this.fullPath, + }); + final OS os; + final Architecture arch; + final MediaPipeSdk sdk; + final String fullPath; + + @override + String toString() => '_SdkLocation(os: $os, arch: $arch, sdk: $sdk, ' + 'fullPath: $fullPath)'; +} + +/// Container for multiple [_SdkLocation] objects with support for quickly +/// extracting all records for a given MediaPipe task (audio, vision, or text). +class _SdkLocations { + final Map> _locations = {}; + + void add(_SdkLocation loc) { + _locations.setDefault(loc.sdk, <_SdkLocation>[]); + _locations[loc.sdk]!.add(loc); + } + + _FlatResults toMap(MediaPipeSdk sdk) { + if (!_locations.containsKey(sdk)) return {}; + + final _FlatResults results = >{}; + for (_SdkLocation loc in _locations[sdk]!) { + results.setDefault(loc.os.toString(), {}); + results[loc.os.toString()]![loc.arch.toString()] = loc.fullPath; + } + + return results; + } +} diff --git a/tool/builder/lib/sync_headers.dart b/tool/builder/lib/sync_headers.dart new file mode 100644 index 0000000..956805b --- /dev/null +++ b/tool/builder/lib/sync_headers.dart @@ -0,0 +1,177 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io' as io; +import 'package:args/command_runner.dart'; +import 'package:builder/extensions.dart'; +import 'package:builder/repo_finder.dart'; +import 'package:io/ansi.dart'; +import 'package:path/path.dart' as path; +import 'package:process/process.dart'; + +/// Relative header paths (in both repositories) +final containers = 'mediapipe/tasks/c/components/containers'; +final processors = 'mediapipe/tasks/c/components/processors'; +final core = 'mediapipe/tasks/c/core'; +final tc = 'mediapipe/tasks/c/text/text_classifier'; +final te = 'mediapipe/tasks/c/text/text_embedder'; + +/// google/flutter-mediapipe package paths +final corePackage = 'packages/mediapipe-core/third_party'; +final textPackage = 'packages/mediapipe-task-text/third_party'; + +/// First string is its relative location in both repositories, +/// Second string is its package location in `google/flutter-mediapipe`, +/// Third string is the file name +/// Fourth param is an optional function to modify the file +List<(String, String, String, Function(io.File)?)> headerPaths = [ + (containers, corePackage, 'category.h', null), + (containers, corePackage, 'classification_result.h', null), + (containers, corePackage, 'embedding_result.h', null), + (core, corePackage, 'base_options.h', null), + (processors, corePackage, 'classifier_options.h', null), + (processors, corePackage, 'embedder_options.h', null), + (tc, textPackage, 'text_classifier.h', relativeIncludes), + (te, textPackage, 'text_embedder.h', relativeIncludes), +]; + +/// Command to copy all necessary C header files into this repository. +/// +/// Pulls a list of hard-coded header files out of various destinations within +/// the google/mediapipe repository and places them in the same paths within +/// this repository. The only major change between their orientation within the +/// source repository (google/mediapipe) and their orientation here is that +/// shared header files are placed in `mediapipe-core` here, and no such +/// distinction exists in the source. This also implies rewriting the import +/// paths within these files to match, as their old relative positioning is +/// disrupted by the move. +class SyncHeadersCommand extends Command with RepoFinderMixin { + @override + String description = 'Syncs header files to google/flutter-mediapipe'; + @override + String name = 'headers'; + + SyncHeadersCommand() { + argParser.addFlag( + 'overwrite', + abbr: 'o', + defaultsTo: true, + help: 'If true, will overwrite existing header files ' + 'at destination locations.', + ); + addSourceOption(argParser); + addVerboseOption(argParser); + } + + @override + Future run() async { + setUpLogging(); + final io.Directory flutterMediaPipeDirectory = findFlutterMediaPipeRoot(); + final io.Directory mediaPipeDirectory = findMediaPipeRoot( + flutterMediaPipeDirectory, + argResults!['source'], + ); + + final config = Options( + allowOverwrite: argResults!['overwrite'], + mediaPipeDir: mediaPipeDirectory, + flutterMediaPipeDir: flutterMediaPipeDirectory, + ); + + await copyHeaders(config); + } + + /// Central method that does the work of actually moving the files into the + /// local repository and rewriting their relative import statements within + /// the files themselves. + Future copyHeaders(Options config) async { + final mgr = LocalProcessManager(); + for (final tup in headerPaths) { + final headerFile = io.File(path.joinAll( + [config.mediaPipeDir.absolute.path, tup.$1, tup.$3], + )); + if (!headerFile.existsSync()) { + io.stderr.writeln( + 'Expected to find ${headerFile.path}, but ' + 'file does not exist.', + ); + io.exit(1); + } + final destinationPath = path.joinAll( + [config.flutterMediaPipeDir.absolute.path, tup.$2, tup.$1, tup.$3], + ); + final destinationFile = io.File(destinationPath); + if (destinationFile.existsSync() && !config.allowOverwrite) { + io.stdout.writeAll( + [ + 'Warning: Not overwriting existing file at $destinationPath\n', + wrapWith('Skipping ${tup.$3}.\n', [cyan]), + ], + ); + continue; + } + + // MediaPipe header files often come from deeply nested locations, and new + // header files could require new folders. Thus, create any missing folders. + ensureFolders(io.File(destinationPath)); + + final process = await mgr.start(['cp', headerFile.path, destinationPath]); + int processExitCode = await process.exitCode; + if (processExitCode != 0) { + final processStdErr = utf8.decoder.convert( + (await process.stderr.toList()) + .fold>([], (arr, el) => arr..addAll(el))); + io.stderr.write(wrapWith(processStdErr, [red])); + + final processStdOut = utf8.decoder.convert( + (await process.stdout.toList()) + .fold>([], (arr, el) => arr..addAll(el))); + io.stderr.write(wrapWith(processStdOut, [red])); + io.exit(processExitCode); + } else { + io.stderr.writeln(wrapWith('Copied ${tup.$3}', [green])); + } + + // Call the final modification function, if supplied + if (tup.$4 != null) { + tup.$4!.call(destinationFile); + } + } + } +} + +class Options { + const Options({ + required this.allowOverwrite, + required this.mediaPipeDir, + required this.flutterMediaPipeDir, + }); + + final bool allowOverwrite; + final io.Directory mediaPipeDir; + final io.Directory flutterMediaPipeDir; +} + +void relativeIncludes(io.File file) { + assert(file.path.endsWith('.h')); + Map rewrites = { + containers: '../../../../../../../mediapipe-core/third_party/$containers', + processors: '../../../../../../../mediapipe-core/third_party/$processors', + core: '../../../../../../../mediapipe-core/third_party/$core', + }; + String contents = file.readAsStringSync(); + + for (final rewrite in rewrites.entries) { + contents = contents.replaceAll(rewrite.key, rewrite.value); + } + file.writeAsStringSync(contents); + io.stdout.writeln( + wrapWith( + 'Made includes relative for ' + '${lastChunk(file.absolute.path, delimiter: io.Platform.pathSeparator)}', + [green], + ), + ); +} diff --git a/tool/builder/pubspec.yaml b/tool/builder/pubspec.yaml new file mode 100644 index 0000000..33ca0c0 --- /dev/null +++ b/tool/builder/pubspec.yaml @@ -0,0 +1,22 @@ +name: builder +description: Performs build operations for google/flutter-mediapipe that depend + on contents in this repository. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo +environment: + sdk: ^3.1.5 + +# Add regular dependencies here. +dependencies: + args: ^2.4.2 + http: ^1.1.0 + io: ^1.0.4 + logging: ^1.2.0 + native_assets_cli: ^0.3.2 + path: ^1.8.0 + process: ^5.0.0 + +dev_dependencies: + ffigen: ^9.0.1 + lints: ^2.1.0 + test: ^1.24.0 diff --git a/tool/ci_script_shared.sh b/tool/ci_script_shared.sh new file mode 100644 index 0000000..787d327 --- /dev/null +++ b/tool/ci_script_shared.sh @@ -0,0 +1,66 @@ +function ci_text_package() { + # Download bert_classifier.tflite model into example/assets for integration tests + echo "Downloading TextClassification model" + pushd ../../tool/builder + dart pub get + dart bin/main.dart model -m textclassification + echo "Downloading TextEmbedding model" + dart bin/main.dart model -m textembedding + popd +} + +function ci_package () { + local channel="$1" + + shift + local arr=("$@") + for PACKAGE_NAME in "${arr[@]}" + do + echo "== Testing '${PACKAGE_NAME}' on Flutter's $channel channel ==" + pushd "packages/${PACKAGE_NAME}" + + if [[ $PACKAGE_NAME == "mediapipe-task-text" ]]; then + ci_text_package + fi + + # Grab packages. + flutter pub get + + # Run the analyzer to find any static analysis issues. + dart analyze --fatal-infos + + # Run the formatter on all the dart files to make sure everything's linted. + dart format --output none --set-exit-if-changed . + + # Turn on the native-assets feature required by flutter-mediapipe + flutter config --enable-native-assets + + # Run the actual tests if they exist. + if [ -d "test" ] + then + flutter test + fi + + # Run any example tests if they exist + if [ -d "example/test" ] + then + echo "Analyzing '${PACKAGE_NAME}/example'" + + pushd "example" + + flutter pub get + + # Run the analyzer to find any static analysis issues. + dart analyze --fatal-infos + + # Run the formatter on all the dart files to make sure everything's linted. + dart format --output none --set-exit-if-changed . + + flutter test + + popd + fi + + popd + done +} diff --git a/tool/mediapipe_ci_script_beta.sh b/tool/mediapipe_ci_script_beta.sh new file mode 100755 index 0000000..6948329 --- /dev/null +++ b/tool/mediapipe_ci_script_beta.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +DIR="${BASH_SOURCE%/*}" +source "$DIR/ci_script_shared.sh" + +flutter doctor -v + +declare -ar PACKAGE_NAMES=( + "mediapipe-core" + "mediapipe-task-text" +) + +ci_package "beta" "${PACKAGE_NAMES[@]}" + +echo "-- Success --" diff --git a/tool/mediapipe_ci_script_master.sh b/tool/mediapipe_ci_script_master.sh new file mode 100755 index 0000000..36b461b --- /dev/null +++ b/tool/mediapipe_ci_script_master.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +DIR="${BASH_SOURCE%/*}" +source "$DIR/ci_script_shared.sh" + +flutter doctor -v + +declare -ar PACKAGE_NAMES=( + "mediapipe-core" + "mediapipe-task-text" +) + +ci_package "master" "${PACKAGE_NAMES[@]}" + +echo "-- Success --" diff --git a/tool/mediapipe_ci_script_stable.sh b/tool/mediapipe_ci_script_stable.sh new file mode 100755 index 0000000..04a5579 --- /dev/null +++ b/tool/mediapipe_ci_script_stable.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +DIR="${BASH_SOURCE%/*}" +source "$DIR/ci_script_shared.sh" + +flutter doctor -v + +declare -ar PACKAGE_NAMES=( + "mediapipe-core" + "mediapipe-task-text" +) + +ci_package "stable" "${PACKAGE_NAMES[@]}" + +echo "-- Success --"