createSttModel method
- String? modelPath,
- String? tokenizerPath,
- PreferredBackend? preferredBackend,
- String? language,
Creates and returns a new SpeechRecognizer instance.
Modern API: If paths are not provided, uses the active STT model set via
FlutterGemma.installStt() or modelManager.setActiveModel().
modelPath — path to the STT model file (optional if active model set).
tokenizerPath — path to the tokenizer file (optional if active model set).
preferredBackend — backend preference (e.g., CPU, GPU).
language — the OUTPUT language for transcripts, Whisper only: a bare
lowercase code ('en', 'de', …). It sets SpeechRecognizer.language,
the default for calls to SpeechRecognizer.transcribe that pass none, and
it RETARGETS the recognizer when one already exists — this returns a
process-wide singleton, so a language fixed at construction would take
effect only on the first call in a process. Throws ArgumentError for a
malformed code, or for any language on a model with no language token
(moonshine, parakeet). null leaves the model's own default in place.
Implementation
@override
Future<SpeechRecognizer> createSttModel({
String? modelPath,
String? tokenizerPath,
PreferredBackend? preferredBackend,
String? language,
}) async {
// Modern API: Use active STT model if paths not provided
if (modelPath == null || tokenizerPath == null) {
final manager = modelManager as WebModelManager;
final activeModel = manager.activeSttModel;
// No active STT model - user must set one first
if (activeModel == null) {
throw StateError(
'No active STT model set. Use `FlutterGemma.installStt()` or `modelManager.setActiveModel()` to set a model first',
);
}
// Get the actual model file paths through unified system
final modelFilePaths = await manager.getModelFilePaths(activeModel);
if (modelFilePaths == null || modelFilePaths.isEmpty) {
throw StateError(
'STT model file paths not found. Use the `modelManager` to load the model first',
);
}
// Extract model and tokenizer paths from spec
final activeModelPath = modelFilePaths[PreferencesKeys.sttModelFile];
final activeTokenizerPath =
modelFilePaths[PreferencesKeys.sttTokenizerFile];
if (activeModelPath == null || activeTokenizerPath == null) {
throw StateError(
'Could not find model or tokenizer path in active STT model',
);
}
modelPath = activeModelPath;
tokenizerPath = activeTokenizerPath;
if (kDebugMode) {
gemmaLog(
'Using active STT model: $modelPath, tokenizer: $tokenizerPath',
);
}
}
// Check if model already exists with different parameters. Core can no
// longer downcast to the package's concrete STT model type, so it
// compares against the last resolved paths it cached itself (mirrors
// _lastEmbeddingPaths).
if (_initializedSttModel != null) {
final p = _lastSttPaths;
final modelChanged =
p == null ||
p.modelPath != modelPath ||
p.tokenizerPath != tokenizerPath;
if (modelChanged) {
if (kDebugMode) {
gemmaLog(
'[FlutterGemmaWeb] STT model paths changed, closing existing model',
);
}
await _initializedSttModel?.close();
_initializedSttModel = null;
_lastSttPaths = null;
}
}
if (_initializedSttModel != null) {
// Same model — reuse the singleton, RETARGETED to the requested
// language. Without this the caller gets back the recognizer built for
// the FIRST language and transcribes into it with no error; see the
// matching branch in the mobile shell. The decoder prompt is rebuilt
// per transcription, so this is a field write, not a reload.
_initializedSttModel!.language = language;
return _initializedSttModel!;
}
// Dispatches construction through the SttRegistry (probe-chain, mirrors
// EmbeddingRegistry). The backend reads spec.sttModelType to select its
// runtime profile, and ONLY config.modelPath/config.tokenizerPath for
// path resolution.
final activeStt = (modelManager as WebModelManager).activeSttModel;
final SttBackendProvider? backend = activeStt is SttModelSpec
? SttRegistry.instance.findFor(activeStt)
: (SttRegistry.instance.registered.isNotEmpty
? SttRegistry.instance.registered.first
: null);
if (backend == null) {
throw StateError(
'No STT backend registered. Add flutter_gemma_speech to '
'pubspec.yaml and pass it in sttBackends: of '
'FlutterGemma.initialize(...). Registered backends: '
'${SttRegistry.instance.registered.map((b) => b.name).join(", ")}.',
);
}
// modelPath/tokenizerPath are non-null here (resolved in the preamble or
// passed by the caller). maxTokens is unused by STT.
final sttConfig = RuntimeConfig(
maxTokens: 0,
modelPath: modelPath,
tokenizerPath: tokenizerPath,
preferredBackend: preferredBackend,
// Whisper's output language. Dropping it here is invisible: the
// recognizer still works and still returns text, just always in
// English, because the profile falls back to its `<|en|>` default.
language: language,
);
// The backend's createModel(spec, config) requires a non-null spec but
// resolves paths exclusively from config; synthesize one from the resolved
// file paths when there's no active SttModelSpec. sttModelType defaults to
// moonshine — the only shipped profile — for this legacy-path fallback.
final model = await backend.createModel(
activeStt is SttModelSpec
? activeStt
: SttModelSpec(
name: 'web-active-stt',
modelSource: ModelSource.file(modelPath),
tokenizerSource: ModelSource.file(tokenizerPath),
sttModelType: SttModelType.moonshine,
),
sttConfig,
);
_initializedSttModel = model;
_lastSttPaths = (modelPath: modelPath, tokenizerPath: tokenizerPath);
model.addCloseListener(() {
_initializedSttModel = null;
_lastSttPaths = null;
});
return model;
}