createTtsModel method

  1. @override
Future<SpeechSynthesizer> createTtsModel({
  1. PreferredBackend? preferredBackend,
})
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.

Implementation

@override
Future<SpeechSynthesizer> createTtsModel({
  PreferredBackend? preferredBackend,
}) async {
  final manager = _unifiedManager;
  final activeModel = manager.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
      gemmaLog(
        'âš ī¸  Active TTS model changed: ${_lastActiveTtsSpec!.name} → ${activeModel.name}',
      );
      gemmaLog('🔄 Closing old TTS model and creating new one...');
      // Reset the singleton fields BEFORE awaiting close() so a concurrent
      // createTtsModel() that interleaves during the await cannot pass the
      // "all three non-null" guard 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;
      await old?.close();
    } else {
      // Same model - return existing singleton
      gemmaLog(
        'â„šī¸  Reusing existing TTS model instance for ${activeModel.name}',
      );
      return _initTtsCompleter!.future;
    }
  }

  // In-progress guard: a concurrent createTtsModel() during the initial
  // load — completer set but the model not yet published to
  // _initializedTtsModel — must return the existing completer, not fall
  // through and spawn a SECOND TtsWorker/native model.
  if (_initTtsCompleter case Completer<SpeechSynthesizer> completer) {
    return completer.future;
  }

  final completer = _initTtsCompleter = Completer<SpeechSynthesizer>();

  try {
    final filePaths = await manager.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,
    );
    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;
    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;
      }
    });

    completer.complete(synth);
    return synth;
  } catch (e, st) {
    _initTtsCompleter = null;
    _initializedTtsModel = null;
    _lastActiveTtsSpec = null;
    // Complete the completer and return its future (rather than
    // rethrowing separately) so there is exactly one Future in flight for
    // this call — a second, unheeded `completer.future` (as a bare
    // rethrow would leave behind whenever no concurrent caller grabbed a
    // reference to it first) would otherwise surface as an unhandled
    // async error.
    completer.completeError(e, st);
    return completer.future;
  }
}