Instructions to use casawolice/small100-onnx with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers.js
How to use casawolice/small100-onnx with Transformers.js:
// npm i @huggingface/transformers import { pipeline } from '@huggingface/transformers'; // Allocate pipeline const pipe = await pipeline('translation', 'casawolice/small100-onnx');
Add Flutter/Dart example (production-verified) + pure-Dart BPE tokenizer
Browse filesflutter/small100_translator.dart is the actual translator class from a shipped Android/iOS S2S translator app (encoder+merged-decoder KV-cache greedy decode, verified on real devices). flutter/bpe_tokenizer.dart is a small pure-Dart SentencePiece-BPE tokenizer reading tokenizer.json directly, since Dart has no off-the-shelf HuggingFace-tokenizers binding. examples/README.md table updated to list Flutter as runnable/verified alongside Python and transformers.js.
- examples/README.md +13 -5
- examples/flutter/bpe_tokenizer.dart +133 -0
- examples/flutter/small100_translator.dart +241 -0
examples/README.md
CHANGED
|
@@ -8,14 +8,22 @@ cache), decode. 所有示例都遵循同一套 [`../USAGE.md`](../USAGE.md) 流
|
|
| 8 |
|---|---|---|---|
|
| 9 |
| **Python** | optimum / onnxruntime + `tokenizers` | [`python/translate.py`](python/translate.py) | ✅ runnable |
|
| 10 |
| **transformers.js** | `@huggingface/transformers` v3 | [`transformers-js/translate.mjs`](transformers-js/translate.mjs) | ✅ runnable (Node) |
|
|
|
|
| 11 |
| **Android** | onnxruntime-android + DJL `tokenizers` | [`android/Small100Translator.kt`](android/Small100Translator.kt) | 📝 reference |
|
| 12 |
| **iOS** | onnxruntime-swift + swift-transformers | [`ios/Small100Translator.swift`](ios/Small100Translator.swift) | 📝 reference |
|
| 13 |
|
| 14 |
-
Python
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
The tokenizer (`tokenizer.json`) and the language-token map (`lang_tokens.json`)
|
| 21 |
are identical across all platforms — that is the point of this repo.
|
|
|
|
| 8 |
|---|---|---|---|
|
| 9 |
| **Python** | optimum / onnxruntime + `tokenizers` | [`python/translate.py`](python/translate.py) | ✅ runnable |
|
| 10 |
| **transformers.js** | `@huggingface/transformers` v3 | [`transformers-js/translate.mjs`](transformers-js/translate.mjs) | ✅ runnable (Node) |
|
| 11 |
+
| **Flutter** | `onnxruntime` (Dart) + bundled pure-Dart BPE tokenizer | [`flutter/small100_translator.dart`](flutter/small100_translator.dart) | ✅ runnable, shipped in production |
|
| 12 |
| **Android** | onnxruntime-android + DJL `tokenizers` | [`android/Small100Translator.kt`](android/Small100Translator.kt) | 📝 reference |
|
| 13 |
| **iOS** | onnxruntime-swift + swift-transformers | [`ios/Small100Translator.swift`](ios/Small100Translator.swift) | 📝 reference |
|
| 14 |
|
| 15 |
+
Python, transformers.js, and Flutter are fully runnable and verified — the
|
| 16 |
+
Flutter one is the actual translator class from a shipped Android/iOS S2S
|
| 17 |
+
translator app (real devices, not a simulator). It also ships
|
| 18 |
+
[`flutter/bpe_tokenizer.dart`](flutter/bpe_tokenizer.dart): a small pure-Dart
|
| 19 |
+
SentencePiece-BPE tokenizer reading this repo's `tokenizer.json` directly,
|
| 20 |
+
since Dart has no off-the-shelf HuggingFace-tokenizers binding the way
|
| 21 |
+
DJL (Android) or swift-transformers (iOS) provide.
|
| 22 |
+
|
| 23 |
+
The Android/iOS files are **reference implementations** of the same algorithm
|
| 24 |
+
(the encoder/greedy-decode loop with KV cache is written out); wire them to
|
| 25 |
+
your asset loading and pin the dependency versions in the file headers.
|
| 26 |
+
Android/iOS 为参考实现,算法完整,接入时按文件头的依赖版本对齐即可。
|
| 27 |
|
| 28 |
The tokenizer (`tokenizer.json`) and the language-token map (`lang_tokens.json`)
|
| 29 |
are identical across all platforms — that is the point of this repo.
|
examples/flutter/bpe_tokenizer.dart
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// SentencePiece-BPE tokenizer for SMaLL-100 (M2M-100 vocab), reading this
|
| 2 |
+
// repo's tokenizer.json directly (vocab + merges) — no native/FFI tokenizer
|
| 3 |
+
// binding needed, pure Dart. Verified against the Python `tokenizers` output.
|
| 4 |
+
//
|
| 5 |
+
// pubspec.yaml: no extra dependency (dart:convert + dart:io only).
|
| 6 |
+
import 'dart:convert';
|
| 7 |
+
import 'dart:io';
|
| 8 |
+
|
| 9 |
+
/// [encode] returns the subword ids only — the caller prepends the target
|
| 10 |
+
/// language token and appends `</s>` (id 2, per lang_tokens.json). [decode]
|
| 11 |
+
/// joins content tokens back to text, skipping special tokens and turning the
|
| 12 |
+
/// metaspace marker `▁` into spaces.
|
| 13 |
+
class BpeTokenizer {
|
| 14 |
+
static const String _metaspace = '▁';
|
| 15 |
+
|
| 16 |
+
final Map<String, int> _vocab;
|
| 17 |
+
final List<String> _idToToken;
|
| 18 |
+
final Map<String, int> _mergeRank; // "a b" -> rank (lower = higher priority)
|
| 19 |
+
final Set<int> _specialIds;
|
| 20 |
+
final int _unkId;
|
| 21 |
+
|
| 22 |
+
BpeTokenizer._(this._vocab, this._idToToken, this._mergeRank, this._specialIds, this._unkId);
|
| 23 |
+
|
| 24 |
+
// Fullwidth (U+FFxx) forms the SentencePiece nmt_nfkc normalizer folds to
|
| 25 |
+
// ASCII (NFKC). CJK punctuation like `。`(U+3002) / `、`(U+3001) is NOT
|
| 26 |
+
// folded — leave it as-is.
|
| 27 |
+
static const Map<String, String> _punct = {
|
| 28 |
+
',': ',', '?': '?', '!': '!', ':': ':', ';': ';',
|
| 29 |
+
'(': '(', ')': ')', '%': '%', ' ': ' ',
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
static Future<BpeTokenizer> fromFile(String tokenizerJsonPath) async {
|
| 33 |
+
final json = jsonDecode(await File(tokenizerJsonPath).readAsString()) as Map<String, dynamic>;
|
| 34 |
+
final model = json['model'] as Map<String, dynamic>;
|
| 35 |
+
|
| 36 |
+
final rawVocab = (model['vocab'] as Map).cast<String, dynamic>();
|
| 37 |
+
final vocab = <String, int>{};
|
| 38 |
+
var maxId = 0;
|
| 39 |
+
rawVocab.forEach((k, v) {
|
| 40 |
+
final id = (v as num).toInt();
|
| 41 |
+
vocab[k] = id;
|
| 42 |
+
if (id > maxId) maxId = id;
|
| 43 |
+
});
|
| 44 |
+
final idToToken = List<String>.filled(maxId + 1, '');
|
| 45 |
+
vocab.forEach((k, v) {
|
| 46 |
+
if (v >= 0 && v <= maxId) idToToken[v] = k;
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
final merges = model['merges'] as List<dynamic>;
|
| 50 |
+
final mergeRank = <String, int>{};
|
| 51 |
+
for (var i = 0; i < merges.length; i++) {
|
| 52 |
+
final m = merges[i];
|
| 53 |
+
final String key;
|
| 54 |
+
if (m is String) {
|
| 55 |
+
key = m; // "a b"
|
| 56 |
+
} else {
|
| 57 |
+
final pair = (m as List).cast<String>();
|
| 58 |
+
key = '${pair[0]} ${pair[1]}';
|
| 59 |
+
}
|
| 60 |
+
mergeRank[key] = i;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Special ids (</s>, <pad>, <unk>, <s>, and the 100 __xx__ language tokens).
|
| 64 |
+
final specialIds = <int>{};
|
| 65 |
+
for (final t in (json['added_tokens'] as List<dynamic>? ?? const [])) {
|
| 66 |
+
final m = t as Map<String, dynamic>;
|
| 67 |
+
if (m['special'] == true) specialIds.add((m['id'] as num).toInt());
|
| 68 |
+
}
|
| 69 |
+
final unkId = vocab['<unk>'] ?? 3;
|
| 70 |
+
|
| 71 |
+
return BpeTokenizer._(vocab, idToToken, mergeRank, specialIds, unkId);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
String _normalize(String text) {
|
| 75 |
+
final sb = StringBuffer();
|
| 76 |
+
for (final r in text.runes) {
|
| 77 |
+
final ch = String.fromCharCode(r);
|
| 78 |
+
sb.write(_punct[ch] ?? ch);
|
| 79 |
+
}
|
| 80 |
+
return sb.toString();
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Greedy BPE: repeatedly merge the adjacent pair with the lowest merge rank.
|
| 84 |
+
List<String> _bpe(String word) {
|
| 85 |
+
final symbols = word.runes.map(String.fromCharCode).toList();
|
| 86 |
+
if (symbols.length < 2) return symbols;
|
| 87 |
+
while (true) {
|
| 88 |
+
var bestRank = 1 << 30;
|
| 89 |
+
var bestI = -1;
|
| 90 |
+
for (var i = 0; i < symbols.length - 1; i++) {
|
| 91 |
+
final r = _mergeRank['${symbols[i]} ${symbols[i + 1]}'];
|
| 92 |
+
if (r != null && r < bestRank) {
|
| 93 |
+
bestRank = r;
|
| 94 |
+
bestI = i;
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
if (bestI < 0) break;
|
| 98 |
+
symbols[bestI] = symbols[bestI] + symbols[bestI + 1];
|
| 99 |
+
symbols.removeAt(bestI + 1);
|
| 100 |
+
}
|
| 101 |
+
return symbols;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/// Text -> subword ids (no language token, no `</s>`).
|
| 105 |
+
List<int> encode(String text) {
|
| 106 |
+
final ids = <int>[];
|
| 107 |
+
var prevUnk = false;
|
| 108 |
+
for (final piece in _normalize(text).split(RegExp(r'\s+'))) {
|
| 109 |
+
if (piece.isEmpty) continue;
|
| 110 |
+
for (final sym in _bpe(_metaspace + piece)) {
|
| 111 |
+
final id = _vocab[sym];
|
| 112 |
+
if (id == null) {
|
| 113 |
+
if (!prevUnk) ids.add(_unkId); // fuse_unk
|
| 114 |
+
prevUnk = true;
|
| 115 |
+
} else {
|
| 116 |
+
ids.add(id);
|
| 117 |
+
prevUnk = false;
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
return ids;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/// Content ids -> text (skips special tokens, `▁` -> space).
|
| 125 |
+
String decode(List<int> ids) {
|
| 126 |
+
final sb = StringBuffer();
|
| 127 |
+
for (final id in ids) {
|
| 128 |
+
if (_specialIds.contains(id)) continue;
|
| 129 |
+
if (id >= 0 && id < _idToToken.length) sb.write(_idToToken[id]);
|
| 130 |
+
}
|
| 131 |
+
return sb.toString().replaceAll(_metaspace, ' ').trim();
|
| 132 |
+
}
|
| 133 |
+
}
|
examples/flutter/small100_translator.dart
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Reference SMaLL-100 translator for Flutter (onnxruntime Dart package).
|
| 2 |
+
// This is the actual implementation shipped in a production S2S translator
|
| 3 |
+
// app (Android + iOS) — fully runnable and verified, not pseudo-code.
|
| 4 |
+
//
|
| 5 |
+
// pubspec.yaml:
|
| 6 |
+
// dependencies:
|
| 7 |
+
// onnxruntime: ^1.4.1
|
| 8 |
+
//
|
| 9 |
+
// Ship onnx/encoder_model.onnx, onnx/decoder_model_merged.onnx, tokenizer.json,
|
| 10 |
+
// lang_tokens.json as assets (or download them into app storage — sherpa/
|
| 11 |
+
// onnxruntime need real filesystem paths, not asset-bundle URIs). Implements
|
| 12 |
+
// the algorithm in ../USAGE.md: encode + prepend target-lang token -> encoder
|
| 13 |
+
// -> greedy merged-decoder loop with KV cache -> decode. Needs bpe_tokenizer.dart
|
| 14 |
+
// (same directory) to read tokenizer.json — Dart has no off-the-shelf
|
| 15 |
+
// HuggingFace-tokenizers binding, so this repo ships a small pure-Dart one.
|
| 16 |
+
import 'dart:convert';
|
| 17 |
+
import 'dart:io';
|
| 18 |
+
import 'dart:typed_data';
|
| 19 |
+
|
| 20 |
+
import 'package:onnxruntime/onnxruntime.dart';
|
| 21 |
+
|
| 22 |
+
import 'bpe_tokenizer.dart';
|
| 23 |
+
|
| 24 |
+
class Small100Translator {
|
| 25 |
+
static const int _nLayers = 3;
|
| 26 |
+
static const int _nHeads = 16;
|
| 27 |
+
static const int _headDim = 64;
|
| 28 |
+
static const int _decoderStart = 2;
|
| 29 |
+
static const int _eos = 2;
|
| 30 |
+
static const int _pad = 1;
|
| 31 |
+
static const int _maxLen = 64;
|
| 32 |
+
|
| 33 |
+
final BpeTokenizer _tok;
|
| 34 |
+
final Map<String, int> _langToId;
|
| 35 |
+
final OrtSession _encoder;
|
| 36 |
+
final OrtSession _decoder;
|
| 37 |
+
|
| 38 |
+
Small100Translator._(this._tok, this._langToId, this._encoder, this._decoder);
|
| 39 |
+
|
| 40 |
+
static Future<Small100Translator> create({
|
| 41 |
+
required String tokenizerJsonPath,
|
| 42 |
+
required String langTokensPath,
|
| 43 |
+
required String encoderPath,
|
| 44 |
+
required String decoderPath,
|
| 45 |
+
}) async {
|
| 46 |
+
OrtEnv.instance.init();
|
| 47 |
+
final tok = await BpeTokenizer.fromFile(tokenizerJsonPath);
|
| 48 |
+
final lt = jsonDecode(await File(langTokensPath).readAsString()) as Map<String, dynamic>;
|
| 49 |
+
final langToId = (lt['lang_to_id'] as Map).map((k, v) => MapEntry(k as String, (v as num).toInt()));
|
| 50 |
+
final opts = OrtSessionOptions()..setIntraOpNumThreads(2);
|
| 51 |
+
// On-device acceleration: unsupported ops silently fall back to CPU.
|
| 52 |
+
// Some emulators (e.g. those that run on qemu but report a real device's
|
| 53 |
+
// Build.FINGERPRINT) crash inside libonnxruntime.so with NNAPI enabled —
|
| 54 |
+
// real devices are unaffected. If you hit that, detect the emulator and
|
| 55 |
+
// skip appendNnapiProvider rather than disabling acceleration everywhere.
|
| 56 |
+
if (Platform.isAndroid) {
|
| 57 |
+
opts.appendNnapiProvider(NnapiFlags.useNone);
|
| 58 |
+
} else if (Platform.isIOS) {
|
| 59 |
+
opts.appendCoreMLProvider(CoreMLFlags.useNone);
|
| 60 |
+
}
|
| 61 |
+
final encoder = OrtSession.fromFile(File(encoderPath), opts);
|
| 62 |
+
final decoder = OrtSession.fromFile(File(decoderPath), opts);
|
| 63 |
+
return Small100Translator._(tok, langToId, encoder, decoder);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
bool supportsLang(String code) => _langToId.containsKey(code);
|
| 67 |
+
|
| 68 |
+
/// Translate [text] into [tgtLang] (e.g. 'en'). Returns '' for empty input or
|
| 69 |
+
/// an unknown target language.
|
| 70 |
+
Future<String> translate(String text, String tgtLang) async {
|
| 71 |
+
if (text.trim().isEmpty) return '';
|
| 72 |
+
final langId = _langToId[tgtLang];
|
| 73 |
+
if (langId == null) return '';
|
| 74 |
+
|
| 75 |
+
final runOpts = OrtRunOptions();
|
| 76 |
+
// [tgt lang] + subwords + </s> — SMaLL-100 puts the target-language token
|
| 77 |
+
// on the *source* side, not via forced_bos_token_id.
|
| 78 |
+
final ids = <int>[langId, ..._tok.encode(text), _eos];
|
| 79 |
+
final seqLen = ids.length;
|
| 80 |
+
|
| 81 |
+
final inputIds = OrtValueTensor.createTensorWithDataList(Int64List.fromList(ids), [1, seqLen]);
|
| 82 |
+
final attnMask = OrtValueTensor.createTensorWithDataList(
|
| 83 |
+
Int64List.fromList(List<int>.filled(seqLen, 1)), [1, seqLen]);
|
| 84 |
+
|
| 85 |
+
final encOut = _encoder.run(runOpts, {'input_ids': inputIds, 'attention_mask': attnMask});
|
| 86 |
+
final encoderHidden = encOut[0]!; // frozen across steps
|
| 87 |
+
|
| 88 |
+
final past = <String, OrtValue>{};
|
| 89 |
+
for (var i = 0; i < _nLayers; i++) {
|
| 90 |
+
for (final kind in ['decoder', 'encoder']) {
|
| 91 |
+
for (final kv in ['key', 'value']) {
|
| 92 |
+
past['past_key_values.$i.$kind.$kv'] =
|
| 93 |
+
OrtValueTensor.createTensorWithDataList(Float32List(0), [1, _nHeads, 0, _headDim]);
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
final outIds = <int>[];
|
| 99 |
+
var cur = _decoderStart;
|
| 100 |
+
var useCache = false;
|
| 101 |
+
|
| 102 |
+
try {
|
| 103 |
+
for (var step = 0; step < _maxLen; step++) {
|
| 104 |
+
final stepInputIds = OrtValueTensor.createTensorWithDataList(Int64List.fromList([cur]), [1, 1]);
|
| 105 |
+
final useCacheT = OrtValueTensor.createTensorWithDataList([useCache], [1]);
|
| 106 |
+
|
| 107 |
+
final feeds = <String, OrtValue>{
|
| 108 |
+
'encoder_attention_mask': attnMask,
|
| 109 |
+
'input_ids': stepInputIds,
|
| 110 |
+
'encoder_hidden_states': encoderHidden,
|
| 111 |
+
'use_cache_branch': useCacheT,
|
| 112 |
+
...past,
|
| 113 |
+
};
|
| 114 |
+
|
| 115 |
+
final outputs = _decoder.run(runOpts, feeds);
|
| 116 |
+
final outByName = <String, OrtValue?>{};
|
| 117 |
+
for (var i = 0; i < _decoder.outputNames.length; i++) {
|
| 118 |
+
outByName[_decoder.outputNames[i]] = outputs[i];
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
final next = _argmaxLastLogits(outByName['logits']!);
|
| 122 |
+
stepInputIds.release();
|
| 123 |
+
useCacheT.release();
|
| 124 |
+
if (next == _eos) {
|
| 125 |
+
_releaseOutputs(outputs, keep: {});
|
| 126 |
+
break;
|
| 127 |
+
}
|
| 128 |
+
outIds.add(next);
|
| 129 |
+
// Greedy decode can degenerate into a repeated token/short cycle on
|
| 130 |
+
// out-of-distribution input; bail out instead of running to _maxLen.
|
| 131 |
+
if (_degenerate(outIds)) {
|
| 132 |
+
_releaseOutputs(outputs, keep: {});
|
| 133 |
+
break;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
final keep = <OrtValue>{};
|
| 137 |
+
for (var i = 0; i < _nLayers; i++) {
|
| 138 |
+
for (final kv in ['key', 'value']) {
|
| 139 |
+
final decKey = 'past_key_values.$i.decoder.$kv';
|
| 140 |
+
final newDec = outByName['present.$i.decoder.$kv']!;
|
| 141 |
+
past[decKey]!.release();
|
| 142 |
+
past[decKey] = newDec;
|
| 143 |
+
keep.add(newDec);
|
| 144 |
+
if (!useCache) {
|
| 145 |
+
// Encoder KV is only produced on step 1 (use_cache_branch=false),
|
| 146 |
+
// then frozen and reused for every later step.
|
| 147 |
+
final encKey = 'past_key_values.$i.encoder.$kv';
|
| 148 |
+
final newEnc = outByName['present.$i.encoder.$kv']!;
|
| 149 |
+
past[encKey]!.release();
|
| 150 |
+
past[encKey] = newEnc;
|
| 151 |
+
keep.add(newEnc);
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
_releaseOutputs(outputs, keep: keep);
|
| 156 |
+
|
| 157 |
+
cur = next;
|
| 158 |
+
useCache = true;
|
| 159 |
+
}
|
| 160 |
+
} finally {
|
| 161 |
+
for (final t in past.values) {
|
| 162 |
+
t.release();
|
| 163 |
+
}
|
| 164 |
+
encoderHidden.release();
|
| 165 |
+
inputIds.release();
|
| 166 |
+
attnMask.release();
|
| 167 |
+
runOpts.release();
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
return _tok.decode(outIds);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
int _argmaxLastLogits(OrtValue logits) {
|
| 174 |
+
final v = logits.value as List;
|
| 175 |
+
final row = (v[0] as List).last as List; // [1, seq, vocab] -> last position
|
| 176 |
+
var bestIdx = 0;
|
| 177 |
+
var bestVal = double.negativeInfinity;
|
| 178 |
+
for (var i = 0; i < row.length; i++) {
|
| 179 |
+
if (i == _pad) continue; // never emit <pad>
|
| 180 |
+
final x = (row[i] as num).toDouble();
|
| 181 |
+
if (x > bestVal) {
|
| 182 |
+
bestVal = x;
|
| 183 |
+
bestIdx = i;
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
return bestIdx;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// Bail out of greedy decoding if it degenerates into a repeated token or a
|
| 190 |
+
// short repeating cycle (period 2-4) — cheap safety net beyond ../USAGE.md's
|
| 191 |
+
// base algorithm, worth adopting on any platform.
|
| 192 |
+
static bool _degenerate(List<int> ids) {
|
| 193 |
+
final n = ids.length;
|
| 194 |
+
if (n >= 6) {
|
| 195 |
+
var same = true;
|
| 196 |
+
for (var i = n - 6; i < n - 1; i++) {
|
| 197 |
+
if (ids[i] != ids[i + 1]) {
|
| 198 |
+
same = false;
|
| 199 |
+
break;
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
if (same) return true;
|
| 203 |
+
}
|
| 204 |
+
for (var p = 2; p <= 4; p++) {
|
| 205 |
+
if (n >= p * 3) {
|
| 206 |
+
var cyc = true;
|
| 207 |
+
for (var i = 0; i < p * 2; i++) {
|
| 208 |
+
if (ids[n - 1 - i] != ids[n - 1 - i - p]) {
|
| 209 |
+
cyc = false;
|
| 210 |
+
break;
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
if (cyc) return true;
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
return false;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
void _releaseOutputs(List<OrtValue?> outputs, {required Set<OrtValue> keep}) {
|
| 220 |
+
for (final o in outputs) {
|
| 221 |
+
if (o != null && !keep.contains(o)) o.release();
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
void dispose() {
|
| 226 |
+
_encoder.release();
|
| 227 |
+
_decoder.release();
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
// Usage (e.g. from a Flutter widget's initState, after copying the asset
|
| 232 |
+
// files to real paths — see ModelManager-style staging in the app examples):
|
| 233 |
+
//
|
| 234 |
+
// final t = await Small100Translator.create(
|
| 235 |
+
// tokenizerJsonPath: '$modelsRoot/tokenizer.json',
|
| 236 |
+
// langTokensPath: '$modelsRoot/lang_tokens.json',
|
| 237 |
+
// encoderPath: '$modelsRoot/onnx/encoder_model.onnx',
|
| 238 |
+
// decoderPath: '$modelsRoot/onnx/decoder_model_merged.onnx',
|
| 239 |
+
// );
|
| 240 |
+
// print(await t.translate('你好,请问最近的地铁站怎么走?', 'en'));
|
| 241 |
+
// t.dispose();
|