createTtsModel method
Future<SpeechSynthesizer>
createTtsModel({
- PreferredBackend? preferredBackend,
- String? language,
override
Creates and returns a new SpeechSynthesizer for the active TTS model.
Uses the active TTS model set via FlutterGemma.installTts() /
modelManager.setActiveModel(). Native-only — throws on web.
language is TTS-only (Qwen3; ignored by Matcha) — see
RuntimeConfig.language's doc. Forwarded into the RuntimeConfig the
active model's backend builds from. A singleton for the same active
model is reused across calls, so switching languages requires
explicitly closing the previous SpeechSynthesizer first — a new
language alone does not force a rebuild.
Implementation
@override
Future<SpeechSynthesizer> createTtsModel({
PreferredBackend? preferredBackend,
String? language,
}) async {
final activeModel = _modelManager.activeTtsModel;
if (activeModel is! TtsModelSpec) {
throw StateError(
'No active TTS model set. Use FlutterGemma.installTts() first.',
);
}
// Check if singleton exists and matches the active model
if (_initTtsCompleter != null &&
_initializedTtsModel != null &&
_lastActiveTtsSpec != null) {
if (_lastActiveTtsSpec!.name != activeModel.name) {
// Active model changed - close old model and create new one.
// Reset the singleton fields BEFORE awaiting close() so a concurrent
// createTtsModel() can't pass the guard mid-close and spawn a duplicate
// worker (which would then be orphaned → native leak). Capture the old
// model first, then close it after the reset.
final old = _initializedTtsModel;
_initTtsCompleter = null;
_initializedTtsModel = null;
_lastActiveTtsSpec = null;
_lastActiveTtsLanguage = null;
await old?.close();
} else if (_normalizeTtsLanguage(language) != _lastActiveTtsLanguage) {
// Same model, but a DIFFERENT language was requested — reusing the
// singleton here would silently emit WRONG-LANGUAGE audio with no
// error. Fail loud instead of reusing: the caller must close() the
// existing synthesizer first (tts_screen.dart already does this on
// every model/language switch — see LiteRtSpeechSynthesizer/
// getActiveTts's docs). Both
// sides are normalized ([_normalizeTtsLanguage]) so this only fires
// for a GENUINELY different language, not e.g. null vs. 'english'
// or 'English' vs. 'english'.
throw StateError(
'Active TTS synthesizer was created for language '
"'$_lastActiveTtsLanguage'; call close() before requesting "
"'${_normalizeTtsLanguage(language)}'.",
);
} else {
// Same model, same language - return existing singleton
return _initTtsCompleter!.future;
}
}
// Return existing if initialization in progress
if (_initTtsCompleter case Completer<SpeechSynthesizer> completer) {
return completer.future;
}
final completer = _initTtsCompleter = Completer<SpeechSynthesizer>();
try {
final filePaths = await _modelManager.getModelFilePaths(activeModel);
if (filePaths == null || filePaths.isEmpty) {
throw StateError(
'Active TTS model files not found on disk. Reinstall via installTts().',
);
}
final config = RuntimeConfig(
maxTokens: 0,
modelPath: filePaths
.values
.first, // representative; TTS backend uses artifactPaths
artifactPaths: filePaths,
preferredBackend: preferredBackend,
language: language,
);
final backend = TtsRegistry.instance.findFor(activeModel);
if (backend == null) {
throw StateError(
TtsRegistry.instance.hasAny
? 'No registered TTS backend can handle this model '
'(${activeModel.ttsModelType}). Registered: '
'${TtsRegistry.instance.registered.map((b) => b.name).join(", ")}.'
: 'No TTS backend registered. Pass ttsBackends: to FlutterGemma.initialize().',
);
}
gemmaLog(
'Using active TTS model: ${activeModel.name} (${filePaths.length} files)',
);
final synth = await backend.createModel(activeModel, config);
// Core owns the singleton lifecycle: track it + reset on close. The
// package-built model fires this via CloseNotifier (addCloseListener).
_initializedTtsModel = synth;
_lastActiveTtsSpec = activeModel;
_lastActiveTtsLanguage = _normalizeTtsLanguage(language);
synth.addCloseListener(() {
// Only reset if this close-listener still belongs to the current
// singleton — a newer model may already have replaced it (the
// model-changed branch above resets the fields synchronously).
if (identical(_initializedTtsModel, synth)) {
_initializedTtsModel = null;
_initTtsCompleter = null;
_lastActiveTtsSpec = null;
_lastActiveTtsLanguage = null;
}
});
completer.complete(synth);
return synth;
} catch (e, st) {
completer.completeError(e, st);
_initTtsCompleter = null;
_initializedTtsModel = null;
_lastActiveTtsSpec = null;
_lastActiveTtsLanguage = null;
// Return the completer's future (rather than a bare rethrow) so there
// is exactly one Future in flight for this call — an unheeded
// `completer.future` (left behind whenever no concurrent caller
// grabbed a reference to it first) would otherwise surface as an
// unhandled async error.
return completer.future;
}
}