react-native-sherpa-onnx 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -15
- package/SherpaOnnx.podspec +13 -5
- package/android/prebuilt-download.gradle +18 -5
- package/android/prebuilt-versions.gradle +8 -4
- package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect-helper.cpp +43 -142
- package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect-helper.h +12 -4
- package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect-stt.cpp +694 -307
- package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect-tts.cpp +194 -99
- package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect.h +90 -0
- package/android/src/main/cpp/jni/model_detect/sherpa-onnx-stt-wrapper.cpp +3 -0
- package/android/src/main/java/com/sherpaonnx/SherpaOnnxModule.kt +70 -0
- package/android/src/main/java/com/sherpaonnx/SherpaOnnxPcmCapture.kt +150 -0
- package/android/src/main/java/com/sherpaonnx/SherpaOnnxSttHelper.kt +39 -19
- package/ios/SherpaOnnx+PcmLiveStream.mm +288 -0
- package/ios/SherpaOnnx+STT.mm +2 -0
- package/ios/SherpaOnnx.mm +1 -1
- package/ios/model_detect/sherpa-onnx-model-detect-helper.h +9 -3
- package/ios/model_detect/sherpa-onnx-model-detect-helper.mm +38 -54
- package/ios/model_detect/sherpa-onnx-model-detect-stt.mm +620 -267
- package/ios/model_detect/sherpa-onnx-model-detect-tts.mm +131 -28
- package/ios/model_detect/sherpa-onnx-model-detect.h +70 -0
- package/ios/stt/sherpa-onnx-stt-wrapper.mm +4 -0
- package/lib/module/NativeSherpaOnnx.js.map +1 -1
- package/lib/module/audio/index.js +52 -0
- package/lib/module/audio/index.js.map +1 -1
- package/lib/module/stt/streaming.js +6 -3
- package/lib/module/stt/streaming.js.map +1 -1
- package/lib/typescript/src/NativeSherpaOnnx.d.ts +16 -2
- package/lib/typescript/src/NativeSherpaOnnx.d.ts.map +1 -1
- package/lib/typescript/src/audio/index.d.ts +17 -0
- package/lib/typescript/src/audio/index.d.ts.map +1 -1
- package/lib/typescript/src/stt/streaming.d.ts.map +1 -1
- package/lib/typescript/src/stt/streamingTypes.d.ts +1 -1
- package/lib/typescript/src/stt/streamingTypes.d.ts.map +1 -1
- package/package.json +6 -1
- package/scripts/check-model-csvs.sh +72 -0
- package/scripts/setup-ios-framework.sh +48 -48
- package/src/NativeSherpaOnnx.ts +18 -2
- package/src/audio/index.ts +81 -0
- package/src/stt/streaming.ts +10 -5
- package/src/stt/streamingTypes.ts +1 -1
- package/third_party/sherpa-onnx-prebuilt/ANDROID_RELEASE_TAG +1 -1
- package/third_party/sherpa-onnx-prebuilt/IOS_RELEASE_TAG +1 -1
|
@@ -2,19 +2,78 @@
|
|
|
2
2
|
* sherpa-onnx-model-detect-stt.mm
|
|
3
3
|
*
|
|
4
4
|
* Purpose: Detects STT (speech-to-text) model type and fills SttModelPaths from a model directory.
|
|
5
|
-
*
|
|
5
|
+
* Used by the STT wrapper on iOS. Supports transducer, paraformer, whisper, moonshine, etc.
|
|
6
|
+
*
|
|
7
|
+
* --- Detection pipeline (overview) ---
|
|
8
|
+
*
|
|
9
|
+
* 1. Gather files in modelDir (recursive), then:
|
|
10
|
+
* - SttCandidatePaths: map file names to logical paths (encoder, decoder, joiner, moonshine
|
|
11
|
+
* preprocessor/encoder/mergedDecoder, paraformer/ctc model, tokens, etc.).
|
|
12
|
+
* - SttPathHints: from directory name only (isLikelyMoonshine, isLikelyNemo, ...).
|
|
13
|
+
* - SttCapabilities: which model types are *possible* given paths + hints (hasWhisper,
|
|
14
|
+
* hasMoonshineV2, hasTransducer, ...). Multiple can be true at once (e.g. same files
|
|
15
|
+
* can satisfy both Whisper and Moonshine v2).
|
|
16
|
+
*
|
|
17
|
+
* 2. detectedModels (for UI "Select model type"): built from capabilities only. Every kind
|
|
18
|
+
* with has* == true is added. So the list shows all types that could work with the files,
|
|
19
|
+
* not the single chosen type.
|
|
20
|
+
*
|
|
21
|
+
* 3. selectedKind (which type we actually use): from ResolveSttKind():
|
|
22
|
+
* - If modelType is explicit (e.g. "whisper"): use it if capabilities allow.
|
|
23
|
+
* - If modelType == "auto": Priority 1 = folder name (GetKindsFromDirName: tokens like
|
|
24
|
+
* "moonshine", "whisper" in dir name → candidate kinds). Priority 2 = among those
|
|
25
|
+
* candidates, pick the first that CapabilitySupportsKind(). Fallback = if no name
|
|
26
|
+
* candidates, use file-only order (transducer → moonshine v2/v1 → CTC → paraformer →
|
|
27
|
+
* whisper → ...).
|
|
28
|
+
*
|
|
29
|
+
* 4. paths: ApplyPathsForSttKind(selectedKind) copies the relevant candidate paths into
|
|
30
|
+
* SttModelPaths (encoder/decoder, moonshine encoder/mergedDecoder, etc.) for the chosen kind.
|
|
31
|
+
*
|
|
32
|
+
* Result to caller: ok, error, detectedModels (list), selectedKind (single), paths (for selectedKind).
|
|
6
33
|
*/
|
|
7
34
|
|
|
35
|
+
#import <Foundation/Foundation.h>
|
|
8
36
|
#include "sherpa-onnx-model-detect.h"
|
|
9
37
|
#include "sherpa-onnx-model-detect-helper.h"
|
|
10
38
|
|
|
39
|
+
#include <algorithm>
|
|
11
40
|
#include <string>
|
|
12
41
|
|
|
42
|
+
#define LOGI(fmt, ...) NSLog(@"[SttModelDetect] " fmt, ##__VA_ARGS__)
|
|
43
|
+
|
|
13
44
|
namespace sherpaonnx {
|
|
14
45
|
namespace {
|
|
15
46
|
|
|
16
47
|
using namespace model_detect;
|
|
17
48
|
|
|
49
|
+
static const char* KindToName(SttModelKind k) {
|
|
50
|
+
switch (k) {
|
|
51
|
+
case SttModelKind::kTransducer: return "transducer";
|
|
52
|
+
case SttModelKind::kNemoTransducer: return "nemo_transducer";
|
|
53
|
+
case SttModelKind::kParaformer: return "paraformer";
|
|
54
|
+
case SttModelKind::kNemoCtc: return "nemo_ctc";
|
|
55
|
+
case SttModelKind::kWenetCtc: return "wenet_ctc";
|
|
56
|
+
case SttModelKind::kSenseVoice: return "sense_voice";
|
|
57
|
+
case SttModelKind::kZipformerCtc: return "zipformer_ctc";
|
|
58
|
+
case SttModelKind::kWhisper: return "whisper";
|
|
59
|
+
case SttModelKind::kFunAsrNano: return "funasr_nano";
|
|
60
|
+
case SttModelKind::kFireRedAsr: return "fire_red_asr";
|
|
61
|
+
case SttModelKind::kMoonshine: return "moonshine";
|
|
62
|
+
case SttModelKind::kMoonshineV2: return "moonshine_v2";
|
|
63
|
+
case SttModelKind::kDolphin: return "dolphin";
|
|
64
|
+
case SttModelKind::kCanary: return "canary";
|
|
65
|
+
case SttModelKind::kOmnilingual: return "omnilingual";
|
|
66
|
+
case SttModelKind::kMedAsr: return "medasr";
|
|
67
|
+
case SttModelKind::kTeleSpeechCtc: return "telespeech_ctc";
|
|
68
|
+
case SttModelKind::kToneCtc: return "tone_ctc";
|
|
69
|
+
default: return "unknown";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static const char* EmptyOrPath(const std::string& s) {
|
|
74
|
+
return s.empty() ? "(empty)" : s.c_str();
|
|
75
|
+
}
|
|
76
|
+
|
|
18
77
|
SttModelKind ParseSttModelType(const std::string& modelType) {
|
|
19
78
|
if (modelType == "transducer" || modelType == "zipformer") return SttModelKind::kTransducer;
|
|
20
79
|
if (modelType == "nemo_transducer") return SttModelKind::kNemoTransducer;
|
|
@@ -27,6 +86,7 @@ SttModelKind ParseSttModelType(const std::string& modelType) {
|
|
|
27
86
|
if (modelType == "funasr_nano") return SttModelKind::kFunAsrNano;
|
|
28
87
|
if (modelType == "fire_red_asr") return SttModelKind::kFireRedAsr;
|
|
29
88
|
if (modelType == "moonshine") return SttModelKind::kMoonshine;
|
|
89
|
+
if (modelType == "moonshine_v2") return SttModelKind::kMoonshineV2;
|
|
30
90
|
if (modelType == "dolphin") return SttModelKind::kDolphin;
|
|
31
91
|
if (modelType == "canary") return SttModelKind::kCanary;
|
|
32
92
|
if (modelType == "omnilingual") return SttModelKind::kOmnilingual;
|
|
@@ -36,324 +96,617 @@ SttModelKind ParseSttModelType(const std::string& modelType) {
|
|
|
36
96
|
return SttModelKind::kUnknown;
|
|
37
97
|
}
|
|
38
98
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
bool debug /* = false */
|
|
99
|
+
/** Returns true if \p cap and hints/paths support the given \p kind (required files present). */
|
|
100
|
+
static bool CapabilitySupportsKind(
|
|
101
|
+
SttModelKind kind,
|
|
102
|
+
const SttCapabilities& cap,
|
|
103
|
+
const SttPathHints& hints,
|
|
104
|
+
const SttCandidatePaths& paths
|
|
46
105
|
) {
|
|
47
|
-
|
|
106
|
+
switch (kind) {
|
|
107
|
+
case SttModelKind::kTransducer:
|
|
108
|
+
return cap.hasTransducer && !(hints.isLikelyNemo || hints.isLikelyTdt);
|
|
109
|
+
case SttModelKind::kNemoTransducer:
|
|
110
|
+
return cap.hasTransducer;
|
|
111
|
+
case SttModelKind::kParaformer:
|
|
112
|
+
return cap.hasParaformer;
|
|
113
|
+
case SttModelKind::kNemoCtc:
|
|
114
|
+
return !paths.ctcModel.empty() && hints.isLikelyNemo;
|
|
115
|
+
case SttModelKind::kWenetCtc:
|
|
116
|
+
return !paths.ctcModel.empty() && hints.isLikelyWenetCtc;
|
|
117
|
+
case SttModelKind::kSenseVoice:
|
|
118
|
+
return !paths.ctcModel.empty() && hints.isLikelySenseVoice;
|
|
119
|
+
case SttModelKind::kZipformerCtc:
|
|
120
|
+
return !paths.ctcModel.empty() && hints.isLikelyZipformer;
|
|
121
|
+
case SttModelKind::kWhisper:
|
|
122
|
+
return cap.hasWhisper;
|
|
123
|
+
case SttModelKind::kFunAsrNano:
|
|
124
|
+
return cap.hasFunAsrNano;
|
|
125
|
+
case SttModelKind::kFireRedAsr:
|
|
126
|
+
return cap.hasFireRedAsr;
|
|
127
|
+
case SttModelKind::kMoonshine:
|
|
128
|
+
return cap.hasMoonshine;
|
|
129
|
+
case SttModelKind::kMoonshineV2:
|
|
130
|
+
return cap.hasMoonshineV2;
|
|
131
|
+
case SttModelKind::kDolphin:
|
|
132
|
+
return cap.hasDolphin;
|
|
133
|
+
case SttModelKind::kCanary:
|
|
134
|
+
return cap.hasCanary;
|
|
135
|
+
case SttModelKind::kOmnilingual:
|
|
136
|
+
return cap.hasOmnilingual;
|
|
137
|
+
case SttModelKind::kMedAsr:
|
|
138
|
+
return cap.hasMedAsr;
|
|
139
|
+
case SttModelKind::kTeleSpeechCtc:
|
|
140
|
+
return cap.hasTeleSpeechCtc;
|
|
141
|
+
case SttModelKind::kToneCtc:
|
|
142
|
+
return cap.hasToneCtc;
|
|
143
|
+
default:
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
48
147
|
|
|
49
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Priority 1: Collect candidate STT kinds from the model directory name (last path component).
|
|
150
|
+
* Tokens like "moonshine", "whisper", "paraformer" are matched case-insensitively. Returns
|
|
151
|
+
* candidates in a fixed priority order so that when multiple kinds match the name, file-based
|
|
152
|
+
* disambiguation picks the first supported one.
|
|
153
|
+
*/
|
|
154
|
+
static std::vector<SttModelKind> GetKindsFromDirName(const std::string& modelDir) {
|
|
155
|
+
size_t pos = modelDir.find_last_of("/\\");
|
|
156
|
+
std::string base = (pos == std::string::npos) ? modelDir : modelDir.substr(pos + 1);
|
|
157
|
+
std::string lower = ToLower(base);
|
|
158
|
+
|
|
159
|
+
std::vector<SttModelKind> out;
|
|
160
|
+
auto add = [&out](SttModelKind k) {
|
|
161
|
+
if (std::find(out.begin(), out.end(), k) == out.end())
|
|
162
|
+
out.push_back(k);
|
|
163
|
+
};
|
|
50
164
|
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
165
|
+
if (lower.find("moonshine") != std::string::npos) {
|
|
166
|
+
add(SttModelKind::kMoonshineV2);
|
|
167
|
+
add(SttModelKind::kMoonshine);
|
|
54
168
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
169
|
+
if (lower.find("whisper") != std::string::npos)
|
|
170
|
+
add(SttModelKind::kWhisper);
|
|
171
|
+
if (lower.find("paraformer") != std::string::npos)
|
|
172
|
+
add(SttModelKind::kParaformer);
|
|
173
|
+
if (lower.find("nemo") != std::string::npos || lower.find("parakeet") != std::string::npos) {
|
|
174
|
+
add(SttModelKind::kNemoTransducer);
|
|
175
|
+
add(SttModelKind::kNemoCtc);
|
|
176
|
+
}
|
|
177
|
+
if (lower.find("tdt") != std::string::npos)
|
|
178
|
+
add(SttModelKind::kNemoTransducer);
|
|
179
|
+
if (lower.find("wenet") != std::string::npos)
|
|
180
|
+
add(SttModelKind::kWenetCtc);
|
|
181
|
+
if (lower.find("sense") != std::string::npos || lower.find("sensevoice") != std::string::npos)
|
|
182
|
+
add(SttModelKind::kSenseVoice);
|
|
183
|
+
if (lower.find("zipformer") != std::string::npos) {
|
|
184
|
+
add(SttModelKind::kTransducer);
|
|
185
|
+
add(SttModelKind::kZipformerCtc);
|
|
186
|
+
}
|
|
187
|
+
if (lower.find("funasr") != std::string::npos)
|
|
188
|
+
add(SttModelKind::kFunAsrNano);
|
|
189
|
+
if (lower.find("canary") != std::string::npos)
|
|
190
|
+
add(SttModelKind::kCanary);
|
|
191
|
+
if (lower.find("fire_red") != std::string::npos || lower.find("fire-red") != std::string::npos)
|
|
192
|
+
add(SttModelKind::kFireRedAsr);
|
|
193
|
+
if (lower.find("dolphin") != std::string::npos)
|
|
194
|
+
add(SttModelKind::kDolphin);
|
|
195
|
+
if (lower.find("omnilingual") != std::string::npos)
|
|
196
|
+
add(SttModelKind::kOmnilingual);
|
|
197
|
+
if (lower.find("medasr") != std::string::npos)
|
|
198
|
+
add(SttModelKind::kMedAsr);
|
|
199
|
+
if (lower.find("telespeech") != std::string::npos)
|
|
200
|
+
add(SttModelKind::kTeleSpeechCtc);
|
|
201
|
+
if (lower.find("t-one") != std::string::npos || lower.find("t_one") != std::string::npos ||
|
|
202
|
+
ContainsWord(lower, "tone"))
|
|
203
|
+
add(SttModelKind::kToneCtc);
|
|
204
|
+
if (lower.find("transducer") != std::string::npos) {
|
|
205
|
+
add(SttModelKind::kTransducer);
|
|
206
|
+
add(SttModelKind::kNemoTransducer);
|
|
59
207
|
}
|
|
60
208
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
std::string encoderPath = FindOnnxByAnyToken(files, {"encoder"}, preferInt8);
|
|
65
|
-
std::string decoderPath = FindOnnxByAnyToken(files, {"decoder"}, preferInt8);
|
|
66
|
-
std::string joinerPath = FindOnnxByAnyToken(files, {"joiner"}, preferInt8);
|
|
67
|
-
std::string tokensPath = FindFileEndingWith(files, "tokens.txt");
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
68
211
|
|
|
69
|
-
|
|
212
|
+
static SttCandidatePaths GatherSttCandidatePaths(
|
|
213
|
+
const std::vector<FileEntry>& files,
|
|
214
|
+
const std::string& modelDir,
|
|
215
|
+
int maxDepth,
|
|
216
|
+
const std::optional<bool>& preferInt8
|
|
217
|
+
) {
|
|
218
|
+
SttCandidatePaths p;
|
|
219
|
+
p.encoder = FindOnnxByAnyToken(files, {"encoder"}, preferInt8);
|
|
220
|
+
p.decoder = FindOnnxByAnyToken(files, {"decoder"}, preferInt8);
|
|
221
|
+
p.joiner = FindOnnxByAnyToken(files, {"joiner"}, preferInt8);
|
|
222
|
+
p.funasrEncoderAdaptor = FindOnnxByAnyToken(files, {"encoder_adaptor", "encoder-adaptor"}, preferInt8);
|
|
223
|
+
p.funasrLLM = FindOnnxByAnyToken(files, {"llm"}, preferInt8);
|
|
224
|
+
p.funasrEmbedding = FindOnnxByAnyToken(files, {"embedding"}, preferInt8);
|
|
225
|
+
{
|
|
226
|
+
std::string vocabInSubdir;
|
|
227
|
+
const std::string vocabName = "vocab.json";
|
|
228
|
+
for (const auto& entry : files) {
|
|
229
|
+
if (entry.nameLower != vocabName) continue;
|
|
230
|
+
const std::string& path = entry.path;
|
|
231
|
+
if (path.size() >= modelDir.size() && path.compare(0, modelDir.size(), modelDir) == 0 &&
|
|
232
|
+
(modelDir.empty() || path[modelDir.size()] == '/')) {
|
|
233
|
+
if (path.size() == modelDir.size() + 12 && path.compare(modelDir.size(), 12, "/vocab.json") == 0) {
|
|
234
|
+
p.funasrTokenizerDir = modelDir;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
if (vocabInSubdir.empty())
|
|
238
|
+
vocabInSubdir = path;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (p.funasrTokenizerDir.empty() && !vocabInSubdir.empty()) {
|
|
242
|
+
size_t lastSlash = vocabInSubdir.find_last_of("/\\");
|
|
243
|
+
if (lastSlash != std::string::npos)
|
|
244
|
+
p.funasrTokenizerDir = vocabInSubdir.substr(0, lastSlash);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
p.moonshinePreprocessor = FindOnnxByAnyToken(files, {"preprocess", "preprocessor"}, preferInt8);
|
|
248
|
+
p.moonshineEncoder = FindOnnxByAnyToken(files, {"encode", "encoder_model"}, preferInt8);
|
|
249
|
+
p.moonshineUncachedDecoder = FindOnnxByAnyToken(files, {"uncached_decode", "uncached"}, preferInt8);
|
|
250
|
+
p.moonshineCachedDecoder = FindOnnxByAnyTokenExcluding(
|
|
251
|
+
files, std::vector<std::string>{"cached_decode", "cached"}, std::vector<std::string>{"uncached"}, preferInt8);
|
|
252
|
+
p.moonshineMergedDecoder = FindOnnxByAnyToken(files, {"merged_decode", "merged_decoder", "decoder_model_merged", "merged"}, preferInt8);
|
|
253
|
+
static const std::vector<std::string> modelExcludes = {
|
|
70
254
|
"encoder", "decoder", "joiner", "vocoder", "acoustic", "embedding", "llm",
|
|
71
|
-
"encoder_adaptor", "encoder-adaptor"
|
|
255
|
+
"encoder_adaptor", "encoder-adaptor", "encoder_model", "decoder_model",
|
|
256
|
+
"merged_decoder", "decoder_model_merged", "preprocess", "encode", "uncached", "cached"
|
|
72
257
|
};
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
258
|
+
p.paraformerModel = FindOnnxByAnyToken(files, {"model"}, preferInt8);
|
|
259
|
+
if (!p.paraformerModel.empty()) {
|
|
260
|
+
std::string lower = ToLower(p.paraformerModel);
|
|
261
|
+
if (lower.find("encoder_model") != std::string::npos ||
|
|
262
|
+
lower.find("decoder_model") != std::string::npos ||
|
|
263
|
+
lower.find("merged_decoder") != std::string::npos)
|
|
264
|
+
p.paraformerModel.clear();
|
|
76
265
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
266
|
+
if (p.paraformerModel.empty())
|
|
267
|
+
p.paraformerModel = FindLargestOnnxExcludingTokens(files, modelExcludes);
|
|
268
|
+
p.ctcModel = FindOnnxByAnyToken(files, {"model"}, preferInt8);
|
|
269
|
+
if (!p.ctcModel.empty()) {
|
|
270
|
+
std::string lower = ToLower(p.ctcModel);
|
|
271
|
+
if (lower.find("encoder_model") != std::string::npos ||
|
|
272
|
+
lower.find("decoder_model") != std::string::npos ||
|
|
273
|
+
lower.find("merged_decoder") != std::string::npos)
|
|
274
|
+
p.ctcModel.clear();
|
|
80
275
|
}
|
|
276
|
+
if (p.ctcModel.empty())
|
|
277
|
+
p.ctcModel = FindLargestOnnxExcludingTokens(files, modelExcludes);
|
|
278
|
+
if (!p.paraformerModel.empty() &&
|
|
279
|
+
(p.paraformerModel == p.encoder || p.paraformerModel == p.decoder || p.paraformerModel == p.joiner))
|
|
280
|
+
p.paraformerModel.clear();
|
|
281
|
+
if (!p.ctcModel.empty() &&
|
|
282
|
+
(p.ctcModel == p.encoder || p.ctcModel == p.decoder || p.ctcModel == p.joiner))
|
|
283
|
+
p.ctcModel.clear();
|
|
284
|
+
p.tokens = FindFileEndingWith(files, "tokens.txt");
|
|
285
|
+
p.bpeVocab = FindFileByName(files, "bpe.vocab");
|
|
286
|
+
p.encoderForV2 = p.encoder.empty() ? FindOnnxByAnyToken(files, {"encoder", "encoder_model"}, preferInt8) : p.encoder;
|
|
287
|
+
return p;
|
|
288
|
+
}
|
|
81
289
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
std::string
|
|
85
|
-
std::string
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
std::string
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
bool isLikelyTdt = modelDirLower.find("tdt") != std::string::npos;
|
|
108
|
-
bool isLikelyWenetCtc = modelDirLower.find("wenet") != std::string::npos;
|
|
109
|
-
bool isLikelySenseVoice = modelDirLower.find("sense") != std::string::npos ||
|
|
110
|
-
modelDirLower.find("sensevoice") != std::string::npos;
|
|
111
|
-
bool isLikelyFunAsrNano = modelDirLower.find("funasr") != std::string::npos ||
|
|
112
|
-
modelDirLower.find("funasr-nano") != std::string::npos;
|
|
113
|
-
bool isLikelyZipformer = modelDirLower.find("zipformer") != std::string::npos;
|
|
114
|
-
bool isLikelyMoonshine = modelDirLower.find("moonshine") != std::string::npos;
|
|
115
|
-
bool isLikelyDolphin = modelDirLower.find("dolphin") != std::string::npos;
|
|
116
|
-
bool isLikelyFireRedAsr = modelDirLower.find("fire_red") != std::string::npos ||
|
|
117
|
-
modelDirLower.find("fire-red") != std::string::npos;
|
|
118
|
-
bool isLikelyCanary = modelDirLower.find("canary") != std::string::npos;
|
|
119
|
-
bool isLikelyOmnilingual = modelDirLower.find("omnilingual") != std::string::npos;
|
|
120
|
-
bool isLikelyMedAsr = modelDirLower.find("medasr") != std::string::npos;
|
|
121
|
-
bool isLikelyTeleSpeech = modelDirLower.find("telespeech") != std::string::npos;
|
|
122
|
-
// Tone CTC: match "tone" only as standalone word (not e.g. "cantonese"); also accept "t-one" / "t_one"
|
|
123
|
-
bool isLikelyToneCtc = modelDirLower.find("t-one") != std::string::npos ||
|
|
124
|
-
modelDirLower.find("t_one") != std::string::npos ||
|
|
125
|
-
ContainsWord(modelDirLower, "tone");
|
|
126
|
-
|
|
127
|
-
bool hasMoonshine = !moonshinePreprocess.empty() && !moonshineUncachedDecode.empty() &&
|
|
128
|
-
!moonshineCachedDecode.empty() && !moonshineEncode.empty();
|
|
129
|
-
bool hasDolphin = isLikelyDolphin && !ctcModelPath.empty();
|
|
130
|
-
bool hasFireRedAsr = hasTransducer && isLikelyFireRedAsr;
|
|
131
|
-
bool hasCanary = hasWhisperEncoder && hasWhisperDecoder && joinerPath.empty() && isLikelyCanary;
|
|
132
|
-
bool hasOmnilingual = !ctcModelPath.empty() && isLikelyOmnilingual;
|
|
133
|
-
bool hasMedAsr = !ctcModelPath.empty() && isLikelyMedAsr;
|
|
134
|
-
bool hasTeleSpeechCtc = (!ctcModelPath.empty() || !paraformerModelPath.empty()) && isLikelyTeleSpeech;
|
|
135
|
-
bool hasToneCtc = !ctcModelPath.empty() && isLikelyToneCtc;
|
|
136
|
-
|
|
137
|
-
if (hasTransducer) {
|
|
138
|
-
if (isLikelyNemo || isLikelyTdt) {
|
|
139
|
-
result.detectedModels.push_back({"nemo_transducer", modelDir});
|
|
140
|
-
} else {
|
|
141
|
-
result.detectedModels.push_back({isLikelyZipformer ? "zipformer" : "transducer", modelDir});
|
|
142
|
-
}
|
|
143
|
-
}
|
|
290
|
+
static SttPathHints GetSttPathHints(const std::string& modelDir) {
|
|
291
|
+
SttPathHints h;
|
|
292
|
+
std::string lower = ToLower(modelDir);
|
|
293
|
+
h.isLikelyNemo = lower.find("nemo") != std::string::npos || lower.find("parakeet") != std::string::npos;
|
|
294
|
+
h.isLikelyTdt = lower.find("tdt") != std::string::npos;
|
|
295
|
+
h.isLikelyWenetCtc = lower.find("wenet") != std::string::npos;
|
|
296
|
+
h.isLikelySenseVoice = lower.find("sense") != std::string::npos || lower.find("sensevoice") != std::string::npos;
|
|
297
|
+
h.isLikelyFunAsrNano = lower.find("funasr") != std::string::npos || lower.find("funasr-nano") != std::string::npos;
|
|
298
|
+
h.isLikelyZipformer = lower.find("zipformer") != std::string::npos;
|
|
299
|
+
h.isLikelyMoonshine = lower.find("moonshine") != std::string::npos;
|
|
300
|
+
h.isLikelyDolphin = lower.find("dolphin") != std::string::npos;
|
|
301
|
+
h.isLikelyFireRedAsr = lower.find("fire_red") != std::string::npos || lower.find("fire-red") != std::string::npos;
|
|
302
|
+
h.isLikelyCanary = lower.find("canary") != std::string::npos;
|
|
303
|
+
h.isLikelyOmnilingual = lower.find("omnilingual") != std::string::npos;
|
|
304
|
+
h.isLikelyMedAsr = lower.find("medasr") != std::string::npos;
|
|
305
|
+
h.isLikelyTeleSpeech = lower.find("telespeech") != std::string::npos;
|
|
306
|
+
// tone_ctc is for T-One models only (e.g. streaming-t-one-russian). WeNetSpeech CTC (yue, wu, etc.) uses wenet_ctc per sherpa-onnx docs.
|
|
307
|
+
h.isLikelyToneCtc = lower.find("t-one") != std::string::npos || lower.find("t_one") != std::string::npos ||
|
|
308
|
+
ContainsWord(lower, "tone");
|
|
309
|
+
h.isLikelyParaformer = lower.find("paraformer") != std::string::npos;
|
|
310
|
+
h.isLikelyVad = lower.find("vad") != std::string::npos || lower.find("silero") != std::string::npos ||
|
|
311
|
+
lower.find("ten-vad") != std::string::npos;
|
|
312
|
+
h.isLikelyTdnn = lower.find("tdnn") != std::string::npos;
|
|
313
|
+
return h;
|
|
314
|
+
}
|
|
144
315
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
} else if (isLikelyWenetCtc) {
|
|
149
|
-
result.detectedModels.push_back({"wenet_ctc", modelDir});
|
|
150
|
-
} else if (isLikelySenseVoice) {
|
|
151
|
-
result.detectedModels.push_back({"sense_voice", modelDir});
|
|
152
|
-
} else {
|
|
153
|
-
result.detectedModels.push_back({"ctc", modelDir});
|
|
154
|
-
}
|
|
155
|
-
} else if (!paraformerModelPath.empty()) {
|
|
156
|
-
result.detectedModels.push_back({"paraformer", modelDir});
|
|
157
|
-
}
|
|
316
|
+
/** Error message when model is for unsupported hardware (RK35xx, Ascend, etc.). */
|
|
317
|
+
static const char* kHardwareSpecificUnsupportedMessage =
|
|
318
|
+
"This model is built for hardware-specific acceleration (e.g. RK35xx, Ascend, CANN) and is not supported by the React Native SDK. Use an ONNX model for CPU/GPU or a QNN-capable model on supported devices.";
|
|
158
319
|
|
|
159
|
-
|
|
160
|
-
|
|
320
|
+
/** True if model dir name indicates a hardware-specific build (e.g. RK3588, Ascend). Not runnable on generic host. QNN is supported by the SDK. */
|
|
321
|
+
static bool IsHardwareSpecificModelDir(const std::string& modelDir) {
|
|
322
|
+
std::string lower = ToLower(modelDir);
|
|
323
|
+
const char* tokens[] = {
|
|
324
|
+
"rk3588", "rk3576", "rk3568", "rk3566", "rk3562", "rknn",
|
|
325
|
+
"ascend", "cann", "910b", "910b2", "310p3"
|
|
326
|
+
};
|
|
327
|
+
for (const char* t : tokens) {
|
|
328
|
+
if (lower.find(t) != std::string::npos)
|
|
329
|
+
return true;
|
|
161
330
|
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
162
333
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
334
|
+
static SttCapabilities ComputeSttCapabilities(const SttCandidatePaths& paths, const SttPathHints& hints) {
|
|
335
|
+
SttCapabilities c;
|
|
336
|
+
c.hasTransducer = !paths.encoder.empty() && !paths.decoder.empty() && !paths.joiner.empty();
|
|
337
|
+
bool hasWhisperEnc = !paths.encoder.empty();
|
|
338
|
+
bool hasWhisperDec = !paths.decoder.empty();
|
|
339
|
+
c.hasWhisper = hasWhisperEnc && hasWhisperDec && paths.joiner.empty();
|
|
340
|
+
bool hasFunAsrTok = !paths.funasrTokenizerDir.empty();
|
|
341
|
+
c.hasFunAsrNano = !paths.funasrEncoderAdaptor.empty() && !paths.funasrLLM.empty() &&
|
|
342
|
+
!paths.funasrEmbedding.empty() && hasFunAsrTok;
|
|
343
|
+
c.hasMoonshine = !paths.moonshinePreprocessor.empty() && !paths.moonshineUncachedDecoder.empty() &&
|
|
344
|
+
!paths.moonshineCachedDecoder.empty() && !paths.moonshineEncoder.empty();
|
|
345
|
+
c.hasMoonshineV2 = !paths.moonshineMergedDecoder.empty() && !paths.encoderForV2.empty() && paths.joiner.empty();
|
|
346
|
+
c.hasParaformer = !paths.paraformerModel.empty();
|
|
347
|
+
c.hasDolphin = hints.isLikelyDolphin && !paths.ctcModel.empty();
|
|
348
|
+
// Fire Red ASR: only encoder+decoder (two files). Single-file Fire Red (e.g. fire-red-asr2-ctc) uses CTC path to avoid native crash.
|
|
349
|
+
c.hasFireRedAsr = (c.hasTransducer || (hasWhisperEnc && hasWhisperDec && paths.joiner.empty())) && hints.isLikelyFireRedAsr;
|
|
350
|
+
c.hasFireRedCtc = hints.isLikelyFireRedAsr && paths.encoder.empty() && paths.decoder.empty() &&
|
|
351
|
+
(!paths.ctcModel.empty() || !paths.paraformerModel.empty());
|
|
352
|
+
c.hasCanary = hasWhisperEnc && hasWhisperDec && paths.joiner.empty() && hints.isLikelyCanary;
|
|
353
|
+
c.hasOmnilingual = !paths.ctcModel.empty() && hints.isLikelyOmnilingual;
|
|
354
|
+
c.hasMedAsr = !paths.ctcModel.empty() && hints.isLikelyMedAsr;
|
|
355
|
+
c.hasTeleSpeechCtc = (!paths.ctcModel.empty() || !paths.paraformerModel.empty()) && hints.isLikelyTeleSpeech;
|
|
356
|
+
c.hasToneCtc = !paths.ctcModel.empty() && hints.isLikelyToneCtc;
|
|
357
|
+
return c;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
static void CollectDetectedModels(
|
|
361
|
+
std::vector<DetectedModel>& out,
|
|
362
|
+
const SttCapabilities& cap,
|
|
363
|
+
const SttPathHints& hints,
|
|
364
|
+
const SttCandidatePaths& paths,
|
|
365
|
+
const std::string& modelDir
|
|
366
|
+
) {
|
|
367
|
+
if (cap.hasTransducer) {
|
|
368
|
+
out.push_back({(hints.isLikelyNemo || hints.isLikelyTdt) ? "nemo_transducer" : "transducer", modelDir});
|
|
180
369
|
}
|
|
181
|
-
if (
|
|
182
|
-
|
|
370
|
+
if (!paths.ctcModel.empty() && (hints.isLikelyNemo || hints.isLikelyWenetCtc || hints.isLikelySenseVoice || hints.isLikelyZipformer)) {
|
|
371
|
+
if (hints.isLikelyNemo) out.push_back({"nemo_ctc", modelDir});
|
|
372
|
+
else if (hints.isLikelyWenetCtc) out.push_back({"wenet_ctc", modelDir});
|
|
373
|
+
else if (hints.isLikelySenseVoice) out.push_back({"sense_voice", modelDir});
|
|
374
|
+
else out.push_back({"zipformer_ctc", modelDir});
|
|
375
|
+
} else if (!paths.paraformerModel.empty()) {
|
|
376
|
+
out.push_back({"paraformer", modelDir});
|
|
183
377
|
}
|
|
184
|
-
if (
|
|
185
|
-
|
|
378
|
+
if (cap.hasWhisper) out.push_back({"whisper", modelDir});
|
|
379
|
+
if (cap.hasFunAsrNano) out.push_back({"funasr_nano", modelDir});
|
|
380
|
+
if (cap.hasMoonshine) out.push_back({"moonshine", modelDir});
|
|
381
|
+
if (cap.hasMoonshineV2) out.push_back({"moonshine_v2", modelDir});
|
|
382
|
+
if (cap.hasDolphin) out.push_back({"dolphin", modelDir});
|
|
383
|
+
if (cap.hasFireRedAsr) out.push_back({"fire_red_asr", modelDir});
|
|
384
|
+
if (cap.hasCanary) out.push_back({"canary", modelDir});
|
|
385
|
+
if (cap.hasOmnilingual) out.push_back({"omnilingual", modelDir});
|
|
386
|
+
if (cap.hasMedAsr) out.push_back({"medasr", modelDir});
|
|
387
|
+
if (cap.hasTeleSpeechCtc) out.push_back({"telespeech_ctc", modelDir});
|
|
388
|
+
if (cap.hasToneCtc) out.push_back({"tone_ctc", modelDir});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static SttModelKind ResolveSttKind(
|
|
392
|
+
const std::optional<std::string>& modelType,
|
|
393
|
+
const SttCapabilities& cap,
|
|
394
|
+
const SttPathHints& hints,
|
|
395
|
+
const SttCandidatePaths& paths,
|
|
396
|
+
const std::string& modelDir,
|
|
397
|
+
std::string& outError
|
|
398
|
+
) {
|
|
399
|
+
outError.clear();
|
|
400
|
+
if (hints.isLikelyVad) {
|
|
401
|
+
outError = "VAD models are not yet supported by the React Native SDK.";
|
|
402
|
+
return SttModelKind::kUnknown;
|
|
186
403
|
}
|
|
187
|
-
if (
|
|
188
|
-
|
|
404
|
+
if (hints.isLikelyTdnn) {
|
|
405
|
+
outError = "TDNN (keyword/yesno) models are not yet supported by the React Native SDK.";
|
|
406
|
+
return SttModelKind::kUnknown;
|
|
189
407
|
}
|
|
190
|
-
|
|
191
|
-
SttModelKind selected = SttModelKind::kUnknown;
|
|
192
|
-
|
|
193
408
|
if (modelType.has_value() && modelType.value() != "auto") {
|
|
194
|
-
selected = ParseSttModelType(modelType.value());
|
|
409
|
+
SttModelKind selected = ParseSttModelType(modelType.value());
|
|
195
410
|
if (selected == SttModelKind::kUnknown) {
|
|
196
|
-
|
|
197
|
-
return
|
|
411
|
+
outError = "Unknown model type: " + modelType.value();
|
|
412
|
+
return SttModelKind::kUnknown;
|
|
198
413
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return result;
|
|
414
|
+
if (selected == SttModelKind::kTransducer && !cap.hasTransducer) {
|
|
415
|
+
outError = "Transducer model requested but files not found in " + modelDir;
|
|
416
|
+
return SttModelKind::kUnknown;
|
|
203
417
|
}
|
|
204
|
-
if (selected == SttModelKind::kNemoTransducer && !hasTransducer) {
|
|
205
|
-
|
|
206
|
-
return
|
|
418
|
+
if (selected == SttModelKind::kNemoTransducer && !cap.hasTransducer) {
|
|
419
|
+
outError = "NeMo Transducer model requested but encoder/decoder/joiner not found in " + modelDir;
|
|
420
|
+
return SttModelKind::kUnknown;
|
|
207
421
|
}
|
|
208
|
-
if (selected == SttModelKind::kParaformer &&
|
|
209
|
-
|
|
210
|
-
return
|
|
422
|
+
if (selected == SttModelKind::kParaformer && paths.paraformerModel.empty()) {
|
|
423
|
+
outError = "Paraformer model requested but model file not found in " + modelDir;
|
|
424
|
+
return SttModelKind::kUnknown;
|
|
211
425
|
}
|
|
212
426
|
if ((selected == SttModelKind::kNemoCtc || selected == SttModelKind::kWenetCtc ||
|
|
213
427
|
selected == SttModelKind::kSenseVoice || selected == SttModelKind::kZipformerCtc ||
|
|
214
|
-
selected == SttModelKind::kToneCtc) &&
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return result;
|
|
428
|
+
selected == SttModelKind::kToneCtc) && paths.ctcModel.empty()) {
|
|
429
|
+
outError = "CTC model requested but model file not found in " + modelDir;
|
|
430
|
+
return SttModelKind::kUnknown;
|
|
218
431
|
}
|
|
219
|
-
if (selected == SttModelKind::kWhisper && !hasWhisper) {
|
|
220
|
-
|
|
221
|
-
return
|
|
432
|
+
if (selected == SttModelKind::kWhisper && !cap.hasWhisper) {
|
|
433
|
+
outError = "Whisper model requested but encoder/decoder not found in " + modelDir;
|
|
434
|
+
return SttModelKind::kUnknown;
|
|
222
435
|
}
|
|
223
|
-
if (selected == SttModelKind::kFunAsrNano && !hasFunAsrNano) {
|
|
224
|
-
|
|
225
|
-
return
|
|
436
|
+
if (selected == SttModelKind::kFunAsrNano && !cap.hasFunAsrNano) {
|
|
437
|
+
outError = "FunASR Nano model requested but required files not found in " + modelDir;
|
|
438
|
+
return SttModelKind::kUnknown;
|
|
226
439
|
}
|
|
227
|
-
if (selected == SttModelKind::kMoonshine && !hasMoonshine) {
|
|
228
|
-
|
|
229
|
-
return
|
|
440
|
+
if (selected == SttModelKind::kMoonshine && !cap.hasMoonshine) {
|
|
441
|
+
outError = "Moonshine v1 model requested but preprocess/encode/uncached_decode/cached_decode not found in " + modelDir;
|
|
442
|
+
return SttModelKind::kUnknown;
|
|
230
443
|
}
|
|
231
|
-
if (selected == SttModelKind::
|
|
232
|
-
|
|
233
|
-
return
|
|
444
|
+
if (selected == SttModelKind::kMoonshineV2 && !cap.hasMoonshineV2) {
|
|
445
|
+
outError = "Moonshine v2 model requested but encoder/merged_decode not found in " + modelDir;
|
|
446
|
+
return SttModelKind::kUnknown;
|
|
234
447
|
}
|
|
235
|
-
if (selected == SttModelKind::
|
|
236
|
-
|
|
237
|
-
return
|
|
448
|
+
if (selected == SttModelKind::kDolphin && !cap.hasDolphin) {
|
|
449
|
+
outError = "Dolphin model requested but model not found in " + modelDir;
|
|
450
|
+
return SttModelKind::kUnknown;
|
|
238
451
|
}
|
|
239
|
-
if (selected == SttModelKind::
|
|
240
|
-
|
|
241
|
-
return
|
|
452
|
+
if (selected == SttModelKind::kFireRedAsr && !cap.hasFireRedAsr) {
|
|
453
|
+
outError = "FireRed ASR model requested but encoder/decoder not found in " + modelDir;
|
|
454
|
+
return SttModelKind::kUnknown;
|
|
242
455
|
}
|
|
243
|
-
if (selected == SttModelKind::
|
|
244
|
-
|
|
245
|
-
return
|
|
456
|
+
if (selected == SttModelKind::kCanary && !cap.hasCanary) {
|
|
457
|
+
outError = "Canary model requested but encoder/decoder not found in " + modelDir;
|
|
458
|
+
return SttModelKind::kUnknown;
|
|
246
459
|
}
|
|
247
|
-
if (selected == SttModelKind::
|
|
248
|
-
|
|
249
|
-
return
|
|
460
|
+
if (selected == SttModelKind::kOmnilingual && !cap.hasOmnilingual) {
|
|
461
|
+
outError = "Omnilingual model requested but model not found in " + modelDir;
|
|
462
|
+
return SttModelKind::kUnknown;
|
|
250
463
|
}
|
|
251
|
-
if (selected == SttModelKind::
|
|
252
|
-
|
|
253
|
-
return
|
|
464
|
+
if (selected == SttModelKind::kMedAsr && !cap.hasMedAsr) {
|
|
465
|
+
outError = "MedASR model requested but model not found in " + modelDir;
|
|
466
|
+
return SttModelKind::kUnknown;
|
|
254
467
|
}
|
|
255
|
-
if (selected == SttModelKind::
|
|
256
|
-
|
|
257
|
-
return
|
|
468
|
+
if (selected == SttModelKind::kTeleSpeechCtc && !cap.hasTeleSpeechCtc) {
|
|
469
|
+
outError = "TeleSpeech CTC model requested but model not found in " + modelDir;
|
|
470
|
+
return SttModelKind::kUnknown;
|
|
258
471
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
} else if (!ctcModelPath.empty() && (isLikelyNemo || isLikelyWenetCtc || isLikelySenseVoice)) {
|
|
263
|
-
if (isLikelyNemo) {
|
|
264
|
-
selected = SttModelKind::kNemoCtc;
|
|
265
|
-
} else if (isLikelyWenetCtc) {
|
|
266
|
-
selected = SttModelKind::kWenetCtc;
|
|
267
|
-
} else {
|
|
268
|
-
selected = SttModelKind::kSenseVoice;
|
|
269
|
-
}
|
|
270
|
-
} else if (hasFunAsrNano && isLikelyFunAsrNano) {
|
|
271
|
-
selected = SttModelKind::kFunAsrNano;
|
|
272
|
-
} else if (!paraformerModelPath.empty()) {
|
|
273
|
-
selected = SttModelKind::kParaformer;
|
|
274
|
-
} else if (hasCanary) {
|
|
275
|
-
selected = SttModelKind::kCanary;
|
|
276
|
-
} else if (hasFireRedAsr) {
|
|
277
|
-
selected = SttModelKind::kFireRedAsr;
|
|
278
|
-
} else if (hasWhisper) {
|
|
279
|
-
selected = SttModelKind::kWhisper;
|
|
280
|
-
} else if (hasFunAsrNano) {
|
|
281
|
-
selected = SttModelKind::kFunAsrNano;
|
|
282
|
-
} else if (hasMoonshine && isLikelyMoonshine) {
|
|
283
|
-
selected = SttModelKind::kMoonshine;
|
|
284
|
-
} else if (hasDolphin) {
|
|
285
|
-
selected = SttModelKind::kDolphin;
|
|
286
|
-
} else if (hasFireRedAsr) {
|
|
287
|
-
selected = SttModelKind::kFireRedAsr;
|
|
288
|
-
} else if (hasCanary) {
|
|
289
|
-
selected = SttModelKind::kCanary;
|
|
290
|
-
} else if (hasOmnilingual) {
|
|
291
|
-
selected = SttModelKind::kOmnilingual;
|
|
292
|
-
} else if (hasMedAsr) {
|
|
293
|
-
selected = SttModelKind::kMedAsr;
|
|
294
|
-
} else if (hasTeleSpeechCtc) {
|
|
295
|
-
selected = SttModelKind::kTeleSpeechCtc;
|
|
296
|
-
} else if (hasToneCtc) {
|
|
297
|
-
selected = SttModelKind::kToneCtc;
|
|
298
|
-
} else if (!ctcModelPath.empty()) {
|
|
299
|
-
selected = SttModelKind::kZipformerCtc;
|
|
472
|
+
if (selected == SttModelKind::kToneCtc && !cap.hasToneCtc) {
|
|
473
|
+
outError = "Tone CTC model requested but path does not contain 'tone' (as a word), 't-one', or 't_one' (e.g. sherpa-onnx-streaming-t-one-*) in " + modelDir;
|
|
474
|
+
return SttModelKind::kUnknown;
|
|
300
475
|
}
|
|
476
|
+
return selected;
|
|
301
477
|
}
|
|
302
478
|
|
|
303
|
-
|
|
304
|
-
|
|
479
|
+
// Auto: Priority 1 – resolve from folder name candidates; Priority 2 – file-based disambiguation.
|
|
480
|
+
std::vector<SttModelKind> nameCandidates = GetKindsFromDirName(modelDir);
|
|
481
|
+
if (!nameCandidates.empty()) {
|
|
482
|
+
for (SttModelKind k : nameCandidates) {
|
|
483
|
+
if (CapabilitySupportsKind(k, cap, hints, paths))
|
|
484
|
+
return k;
|
|
485
|
+
}
|
|
486
|
+
// Name hinted at a model type but no candidate had required files; fall through to file-only.
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Fallback: no name-based candidates, or none supported – use file-only detection order.
|
|
490
|
+
if (cap.hasTransducer) {
|
|
491
|
+
return (hints.isLikelyNemo || hints.isLikelyTdt) ? SttModelKind::kNemoTransducer : SttModelKind::kTransducer;
|
|
492
|
+
}
|
|
493
|
+
if (hints.isLikelyMoonshine && cap.hasMoonshineV2) return SttModelKind::kMoonshineV2;
|
|
494
|
+
if (hints.isLikelyMoonshine && cap.hasMoonshine) return SttModelKind::kMoonshine;
|
|
495
|
+
if (!paths.ctcModel.empty() && (hints.isLikelyToneCtc || hints.isLikelyNemo || hints.isLikelyWenetCtc || hints.isLikelySenseVoice)) {
|
|
496
|
+
if (hints.isLikelyToneCtc) return SttModelKind::kToneCtc;
|
|
497
|
+
if (hints.isLikelyNemo) return SttModelKind::kNemoCtc;
|
|
498
|
+
if (hints.isLikelyWenetCtc) return SttModelKind::kWenetCtc;
|
|
499
|
+
return SttModelKind::kSenseVoice;
|
|
500
|
+
}
|
|
501
|
+
if (cap.hasFunAsrNano && hints.isLikelyFunAsrNano) return SttModelKind::kFunAsrNano;
|
|
502
|
+
if (cap.hasFireRedCtc) return SttModelKind::kZipformerCtc;
|
|
503
|
+
if (!paths.paraformerModel.empty()) return SttModelKind::kParaformer;
|
|
504
|
+
if (cap.hasCanary) return SttModelKind::kCanary;
|
|
505
|
+
if (cap.hasFireRedAsr) return SttModelKind::kFireRedAsr;
|
|
506
|
+
if (cap.hasWhisper) return SttModelKind::kWhisper;
|
|
507
|
+
if (cap.hasFunAsrNano) return SttModelKind::kFunAsrNano;
|
|
508
|
+
if (cap.hasMoonshineV2) return SttModelKind::kMoonshineV2;
|
|
509
|
+
if (cap.hasDolphin) return SttModelKind::kDolphin;
|
|
510
|
+
if (cap.hasOmnilingual) return SttModelKind::kOmnilingual;
|
|
511
|
+
if (cap.hasMedAsr) return SttModelKind::kMedAsr;
|
|
512
|
+
if (cap.hasTeleSpeechCtc) return SttModelKind::kTeleSpeechCtc;
|
|
513
|
+
if (cap.hasToneCtc) return SttModelKind::kToneCtc;
|
|
514
|
+
if (!paths.ctcModel.empty()) return SttModelKind::kZipformerCtc;
|
|
515
|
+
return SttModelKind::kUnknown;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
static void ApplyPathsForSttKind(SttModelKind kind, const SttCandidatePaths& candidate, SttModelPaths& resultPaths) {
|
|
519
|
+
switch (kind) {
|
|
520
|
+
case SttModelKind::kTransducer:
|
|
521
|
+
case SttModelKind::kNemoTransducer:
|
|
522
|
+
resultPaths.encoder = candidate.encoder;
|
|
523
|
+
resultPaths.decoder = candidate.decoder;
|
|
524
|
+
resultPaths.joiner = candidate.joiner;
|
|
525
|
+
break;
|
|
526
|
+
case SttModelKind::kParaformer:
|
|
527
|
+
resultPaths.paraformerModel = candidate.paraformerModel;
|
|
528
|
+
break;
|
|
529
|
+
case SttModelKind::kNemoCtc:
|
|
530
|
+
case SttModelKind::kWenetCtc:
|
|
531
|
+
case SttModelKind::kSenseVoice:
|
|
532
|
+
case SttModelKind::kZipformerCtc:
|
|
533
|
+
case SttModelKind::kToneCtc:
|
|
534
|
+
resultPaths.ctcModel = candidate.ctcModel;
|
|
535
|
+
break;
|
|
536
|
+
case SttModelKind::kWhisper:
|
|
537
|
+
resultPaths.whisperEncoder = candidate.encoder;
|
|
538
|
+
resultPaths.whisperDecoder = candidate.decoder;
|
|
539
|
+
break;
|
|
540
|
+
case SttModelKind::kFireRedAsr: {
|
|
541
|
+
std::string singleModel = candidate.paraformerModel.empty() ? candidate.ctcModel : candidate.paraformerModel;
|
|
542
|
+
resultPaths.fireRedEncoder = candidate.encoder.empty() ? singleModel : candidate.encoder;
|
|
543
|
+
resultPaths.fireRedDecoder = candidate.decoder.empty() ? singleModel : candidate.decoder;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
case SttModelKind::kFunAsrNano:
|
|
547
|
+
resultPaths.funasrEncoderAdaptor = candidate.funasrEncoderAdaptor;
|
|
548
|
+
resultPaths.funasrLLM = candidate.funasrLLM;
|
|
549
|
+
resultPaths.funasrEmbedding = candidate.funasrEmbedding;
|
|
550
|
+
resultPaths.funasrTokenizer = candidate.funasrTokenizerDir;
|
|
551
|
+
break;
|
|
552
|
+
case SttModelKind::kMoonshine:
|
|
553
|
+
resultPaths.moonshinePreprocessor = candidate.moonshinePreprocessor;
|
|
554
|
+
resultPaths.moonshineEncoder = candidate.moonshineEncoder;
|
|
555
|
+
resultPaths.moonshineUncachedDecoder = candidate.moonshineUncachedDecoder;
|
|
556
|
+
resultPaths.moonshineCachedDecoder = candidate.moonshineCachedDecoder;
|
|
557
|
+
break;
|
|
558
|
+
case SttModelKind::kMoonshineV2:
|
|
559
|
+
resultPaths.moonshineEncoder = candidate.encoderForV2;
|
|
560
|
+
resultPaths.moonshineMergedDecoder = candidate.moonshineMergedDecoder;
|
|
561
|
+
break;
|
|
562
|
+
case SttModelKind::kDolphin:
|
|
563
|
+
resultPaths.dolphinModel = candidate.ctcModel.empty() ? candidate.paraformerModel : candidate.ctcModel;
|
|
564
|
+
break;
|
|
565
|
+
case SttModelKind::kCanary:
|
|
566
|
+
resultPaths.canaryEncoder = candidate.encoder;
|
|
567
|
+
resultPaths.canaryDecoder = candidate.decoder;
|
|
568
|
+
break;
|
|
569
|
+
case SttModelKind::kOmnilingual:
|
|
570
|
+
resultPaths.omnilingualModel = candidate.ctcModel;
|
|
571
|
+
break;
|
|
572
|
+
case SttModelKind::kMedAsr:
|
|
573
|
+
resultPaths.medasrModel = candidate.ctcModel;
|
|
574
|
+
break;
|
|
575
|
+
case SttModelKind::kTeleSpeechCtc:
|
|
576
|
+
resultPaths.telespeechCtcModel = candidate.ctcModel.empty() ? candidate.paraformerModel : candidate.ctcModel;
|
|
577
|
+
break;
|
|
578
|
+
default:
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
} // namespace
|
|
584
|
+
|
|
585
|
+
SttDetectResult DetectSttModel(
|
|
586
|
+
const std::string& modelDir,
|
|
587
|
+
const std::optional<bool>& preferInt8,
|
|
588
|
+
const std::optional<std::string>& modelType,
|
|
589
|
+
bool debug /* = false */
|
|
590
|
+
) {
|
|
591
|
+
using namespace model_detect;
|
|
592
|
+
|
|
593
|
+
SttDetectResult result;
|
|
594
|
+
|
|
595
|
+
if (modelDir.empty()) {
|
|
596
|
+
result.error = "Model directory is empty";
|
|
305
597
|
return result;
|
|
306
598
|
}
|
|
307
599
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (selected == SttModelKind::kTransducer || selected == SttModelKind::kNemoTransducer) {
|
|
312
|
-
result.paths.encoder = encoderPath;
|
|
313
|
-
result.paths.decoder = decoderPath;
|
|
314
|
-
result.paths.joiner = joinerPath;
|
|
315
|
-
} else if (selected == SttModelKind::kParaformer) {
|
|
316
|
-
result.paths.paraformerModel = paraformerModelPath;
|
|
317
|
-
} else if (selected == SttModelKind::kNemoCtc || selected == SttModelKind::kWenetCtc ||
|
|
318
|
-
selected == SttModelKind::kSenseVoice || selected == SttModelKind::kZipformerCtc ||
|
|
319
|
-
selected == SttModelKind::kToneCtc) {
|
|
320
|
-
result.paths.ctcModel = ctcModelPath;
|
|
321
|
-
} else if (selected == SttModelKind::kWhisper) {
|
|
322
|
-
result.paths.whisperEncoder = encoderPath;
|
|
323
|
-
result.paths.whisperDecoder = decoderPath;
|
|
324
|
-
} else if (selected == SttModelKind::kFunAsrNano) {
|
|
325
|
-
result.paths.funasrEncoderAdaptor = funasrEncoderAdaptor;
|
|
326
|
-
result.paths.funasrLLM = funasrLLM;
|
|
327
|
-
result.paths.funasrEmbedding = funasrEmbedding;
|
|
328
|
-
result.paths.funasrTokenizer = funasrTokenizerDir;
|
|
329
|
-
} else if (selected == SttModelKind::kMoonshine) {
|
|
330
|
-
result.paths.moonshinePreprocessor = moonshinePreprocess;
|
|
331
|
-
result.paths.moonshineEncoder = moonshineEncode;
|
|
332
|
-
result.paths.moonshineUncachedDecoder = moonshineUncachedDecode;
|
|
333
|
-
result.paths.moonshineCachedDecoder = moonshineCachedDecode;
|
|
334
|
-
} else if (selected == SttModelKind::kDolphin) {
|
|
335
|
-
result.paths.dolphinModel = ctcModelPath.empty() ? paraformerModelPath : ctcModelPath;
|
|
336
|
-
} else if (selected == SttModelKind::kFireRedAsr) {
|
|
337
|
-
result.paths.fireRedEncoder = encoderPath;
|
|
338
|
-
result.paths.fireRedDecoder = decoderPath;
|
|
339
|
-
} else if (selected == SttModelKind::kCanary) {
|
|
340
|
-
result.paths.canaryEncoder = encoderPath;
|
|
341
|
-
result.paths.canaryDecoder = decoderPath;
|
|
342
|
-
} else if (selected == SttModelKind::kOmnilingual) {
|
|
343
|
-
result.paths.omnilingualModel = ctcModelPath;
|
|
344
|
-
} else if (selected == SttModelKind::kMedAsr) {
|
|
345
|
-
result.paths.medasrModel = ctcModelPath;
|
|
346
|
-
} else if (selected == SttModelKind::kTeleSpeechCtc) {
|
|
347
|
-
result.paths.telespeechCtcModel = ctcModelPath.empty() ? paraformerModelPath : ctcModelPath;
|
|
600
|
+
if (!FileExists(modelDir) || !IsDirectory(modelDir)) {
|
|
601
|
+
result.error = "Model directory does not exist or is not a directory: " + modelDir;
|
|
602
|
+
return result;
|
|
348
603
|
}
|
|
349
604
|
|
|
350
|
-
|
|
351
|
-
|
|
605
|
+
const int kMaxSearchDepth = 4;
|
|
606
|
+
const std::vector<FileEntry> files = ListFilesRecursive(modelDir, kMaxSearchDepth);
|
|
607
|
+
|
|
608
|
+
SttCandidatePaths candidate = GatherSttCandidatePaths(files, modelDir, kMaxSearchDepth, preferInt8);
|
|
609
|
+
SttPathHints hints = GetSttPathHints(modelDir);
|
|
610
|
+
SttCapabilities cap = ComputeSttCapabilities(candidate, hints);
|
|
611
|
+
|
|
612
|
+
if (debug) {
|
|
613
|
+
LOGI("DetectSttModel: tokens=%s", EmptyOrPath(candidate.tokens));
|
|
614
|
+
LOGI("DetectSttModel: transducer encoder=%s decoder=%s joiner=%s",
|
|
615
|
+
EmptyOrPath(candidate.encoder), EmptyOrPath(candidate.decoder), EmptyOrPath(candidate.joiner));
|
|
616
|
+
LOGI("DetectSttModel: paraformerModel=%s ctcModel=%s tokens=%s bpeVocab=%s",
|
|
617
|
+
EmptyOrPath(candidate.paraformerModel), EmptyOrPath(candidate.ctcModel), EmptyOrPath(candidate.tokens), EmptyOrPath(candidate.bpeVocab));
|
|
618
|
+
LOGI("DetectSttModel: moonshine preprocessor=%s encoder=%s uncachedDecoder=%s cachedDecoder=%s mergedDecoder=%s",
|
|
619
|
+
EmptyOrPath(candidate.moonshinePreprocessor), EmptyOrPath(candidate.moonshineEncoder), EmptyOrPath(candidate.moonshineUncachedDecoder),
|
|
620
|
+
EmptyOrPath(candidate.moonshineCachedDecoder), EmptyOrPath(candidate.moonshineMergedDecoder));
|
|
621
|
+
LOGI("DetectSttModel: whisper encoder=%s decoder=%s (same as transducer; joiner empty => whisper)",
|
|
622
|
+
EmptyOrPath(candidate.encoder), EmptyOrPath(candidate.decoder));
|
|
623
|
+
LOGI("DetectSttModel: funasr encoderAdaptor=%s llm=%s embedding=%s tokenizerDir=%s",
|
|
624
|
+
EmptyOrPath(candidate.funasrEncoderAdaptor), EmptyOrPath(candidate.funasrLLM), EmptyOrPath(candidate.funasrEmbedding), EmptyOrPath(candidate.funasrTokenizerDir));
|
|
625
|
+
LOGI("DetectSttModel: hasTransducer=%d hasWhisper=%d hasMoonshine=%d hasMoonshineV2=%d hasParaformer=%d hasFunAsrNano=%d hasDolphin=%d hasFireRedAsr=%d hasFireRedCtc=%d hasCanary=%d hasOmnilingual=%d hasMedAsr=%d hasTeleSpeechCtc=%d hasToneCtc=%d",
|
|
626
|
+
(int)cap.hasTransducer, (int)cap.hasWhisper, (int)cap.hasMoonshine, (int)cap.hasMoonshineV2,
|
|
627
|
+
(int)cap.hasParaformer, (int)cap.hasFunAsrNano, (int)cap.hasDolphin, (int)cap.hasFireRedAsr, (int)cap.hasFireRedCtc,
|
|
628
|
+
(int)cap.hasCanary, (int)cap.hasOmnilingual, (int)cap.hasMedAsr, (int)cap.hasTeleSpeechCtc, (int)cap.hasToneCtc);
|
|
629
|
+
LOGI("DetectSttModel: hints isLikelyNemo=%d isLikelyTdt=%d isLikelyWenetCtc=%d isLikelySenseVoice=%d isLikelyFunAsrNano=%d isLikelyZipformer=%d isLikelyMoonshine=%d isLikelyDolphin=%d isLikelyFireRedAsr=%d isLikelyCanary=%d isLikelyOmnilingual=%d isLikelyMedAsr=%d isLikelyTeleSpeech=%d isLikelyToneCtc=%d isLikelyParaformer=%d isLikelyVad=%d isLikelyTdnn=%d",
|
|
630
|
+
(int)hints.isLikelyNemo, (int)hints.isLikelyTdt, (int)hints.isLikelyWenetCtc, (int)hints.isLikelySenseVoice,
|
|
631
|
+
(int)hints.isLikelyFunAsrNano, (int)hints.isLikelyZipformer, (int)hints.isLikelyMoonshine, (int)hints.isLikelyDolphin,
|
|
632
|
+
(int)hints.isLikelyFireRedAsr, (int)hints.isLikelyCanary, (int)hints.isLikelyOmnilingual, (int)hints.isLikelyMedAsr,
|
|
633
|
+
(int)hints.isLikelyTeleSpeech, (int)hints.isLikelyToneCtc, (int)hints.isLikelyParaformer, (int)hints.isLikelyVad, (int)hints.isLikelyTdnn);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
CollectDetectedModels(result.detectedModels, cap, hints, candidate, modelDir);
|
|
637
|
+
|
|
638
|
+
result.selectedKind = ResolveSttKind(modelType, cap, hints, candidate, modelDir, result.error);
|
|
639
|
+
if (result.selectedKind == SttModelKind::kUnknown) {
|
|
640
|
+
if (IsHardwareSpecificModelDir(modelDir)) {
|
|
641
|
+
result.ok = false;
|
|
642
|
+
result.isHardwareSpecificUnsupported = true;
|
|
643
|
+
result.error = kHardwareSpecificUnsupportedMessage;
|
|
644
|
+
return result;
|
|
645
|
+
}
|
|
646
|
+
if (!result.error.empty()) {
|
|
647
|
+
return result;
|
|
648
|
+
}
|
|
649
|
+
result.error = "No compatible model type detected in " + modelDir;
|
|
650
|
+
return result;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
LOGI("DetectSttModel: selected kind=%d (%s)", static_cast<int>(result.selectedKind), KindToName(result.selectedKind));
|
|
654
|
+
result.tokensRequired = (result.selectedKind != SttModelKind::kFunAsrNano);
|
|
655
|
+
ApplyPathsForSttKind(result.selectedKind, candidate, result.paths);
|
|
656
|
+
|
|
657
|
+
if (!candidate.tokens.empty() && FileExists(candidate.tokens)) {
|
|
658
|
+
result.paths.tokens = candidate.tokens;
|
|
352
659
|
} else if (result.tokensRequired) {
|
|
353
660
|
result.error = "Tokens file not found in " + modelDir;
|
|
354
661
|
return result;
|
|
355
662
|
}
|
|
663
|
+
if (!candidate.bpeVocab.empty() && FileExists(candidate.bpeVocab)) {
|
|
664
|
+
result.paths.bpeVocab = candidate.bpeVocab;
|
|
665
|
+
}
|
|
356
666
|
|
|
667
|
+
switch (result.selectedKind) {
|
|
668
|
+
case SttModelKind::kTransducer:
|
|
669
|
+
case SttModelKind::kNemoTransducer:
|
|
670
|
+
LOGI("DetectSttModel: paths set encoder=%s decoder=%s joiner=%s",
|
|
671
|
+
EmptyOrPath(result.paths.encoder), EmptyOrPath(result.paths.decoder), EmptyOrPath(result.paths.joiner));
|
|
672
|
+
break;
|
|
673
|
+
case SttModelKind::kParaformer:
|
|
674
|
+
LOGI("DetectSttModel: paths set paraformerModel=%s", EmptyOrPath(result.paths.paraformerModel));
|
|
675
|
+
break;
|
|
676
|
+
case SttModelKind::kWhisper:
|
|
677
|
+
LOGI("DetectSttModel: paths set whisperEncoder=%s whisperDecoder=%s",
|
|
678
|
+
EmptyOrPath(result.paths.whisperEncoder), EmptyOrPath(result.paths.whisperDecoder));
|
|
679
|
+
break;
|
|
680
|
+
case SttModelKind::kMoonshine:
|
|
681
|
+
LOGI("DetectSttModel: paths set moonshine preprocessor=%s encoder=%s uncachedDecoder=%s cachedDecoder=%s",
|
|
682
|
+
EmptyOrPath(result.paths.moonshinePreprocessor), EmptyOrPath(result.paths.moonshineEncoder),
|
|
683
|
+
EmptyOrPath(result.paths.moonshineUncachedDecoder), EmptyOrPath(result.paths.moonshineCachedDecoder));
|
|
684
|
+
break;
|
|
685
|
+
case SttModelKind::kMoonshineV2:
|
|
686
|
+
LOGI("DetectSttModel: paths set moonshine_v2 encoder=%s mergedDecoder=%s",
|
|
687
|
+
EmptyOrPath(result.paths.moonshineEncoder), EmptyOrPath(result.paths.moonshineMergedDecoder));
|
|
688
|
+
break;
|
|
689
|
+
case SttModelKind::kNemoCtc:
|
|
690
|
+
case SttModelKind::kWenetCtc:
|
|
691
|
+
case SttModelKind::kSenseVoice:
|
|
692
|
+
case SttModelKind::kZipformerCtc:
|
|
693
|
+
case SttModelKind::kToneCtc:
|
|
694
|
+
LOGI("DetectSttModel: paths set ctcModel=%s", EmptyOrPath(result.paths.ctcModel));
|
|
695
|
+
break;
|
|
696
|
+
case SttModelKind::kFireRedAsr:
|
|
697
|
+
LOGI("DetectSttModel: paths set fireRedEncoder=%s fireRedDecoder=%s",
|
|
698
|
+
EmptyOrPath(result.paths.fireRedEncoder), EmptyOrPath(result.paths.fireRedDecoder));
|
|
699
|
+
break;
|
|
700
|
+
case SttModelKind::kFunAsrNano:
|
|
701
|
+
LOGI("DetectSttModel: paths set funasr adaptor=%s llm=%s embedding=%s tokenizer=%s",
|
|
702
|
+
EmptyOrPath(result.paths.funasrEncoderAdaptor), EmptyOrPath(result.paths.funasrLLM),
|
|
703
|
+
EmptyOrPath(result.paths.funasrEmbedding), EmptyOrPath(result.paths.funasrTokenizer));
|
|
704
|
+
break;
|
|
705
|
+
default:
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
LOGI("DetectSttModel: tokens=%s (required=%d)", EmptyOrPath(result.paths.tokens), (int)result.tokensRequired);
|
|
709
|
+
LOGI("DetectSttModel: detection OK for %s", modelDir.c_str());
|
|
357
710
|
result.ok = true;
|
|
358
711
|
return result;
|
|
359
712
|
}
|