createSttModel method

  1. @override
Future<SpeechRecognizer> createSttModel({
  1. String? modelPath,
  2. String? tokenizerPath,
  3. PreferredBackend? preferredBackend,
})
override

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).

Implementation

@override
Future<SpeechRecognizer> createSttModel({
  String? modelPath,
  String? tokenizerPath,
  PreferredBackend? preferredBackend,
}) async {
  // Check if active STT model changed
  final currentActiveModel = _modelManager.activeSttModel;
  if (_initSttCompleter != null &&
      _initializedSttModel != null &&
      _lastActiveSttModelName != null) {
    final modelChanged =
        currentActiveModel == null ||
        currentActiveModel.name != _lastActiveSttModelName;
    if (modelChanged) {
      await _initializedSttModel?.close();
      _initSttCompleter = null;
      _initializedSttModel = null;
      _lastActiveSttModelName = null;
    } else {
      return _initSttCompleter!.future;
    }
  }

  // Return existing if initialization in progress
  if (_initSttCompleter case Completer<SpeechRecognizer> completer) {
    return completer.future;
  }

  final completer = _initSttCompleter = Completer<SpeechRecognizer>();

  try {
    // Resolve model and tokenizer paths from active STT model
    if (modelPath == null || tokenizerPath == null) {
      final activeModel = _modelManager.activeSttModel;
      if (activeModel == null) {
        throw StateError(
          'No active STT model set. '
          'Use `FlutterGemma.installStt()` first.',
        );
      }

      final filePaths = await _modelManager.getModelFilePaths(activeModel);
      if (filePaths == null || filePaths.isEmpty) {
        throw StateError('STT model file paths not found');
      }

      modelPath ??= filePaths[PreferencesKeys.sttModelFile];
      tokenizerPath ??= filePaths[PreferencesKeys.sttTokenizerFile];
    }

    if (modelPath == null) {
      throw StateError('STT model path is required');
    }

    gemmaLog('[FlutterGemmaDesktop] Loading STT model: $modelPath');

    if (tokenizerPath == null) {
      throw StateError('Tokenizer path is required for desktop STT');
    }

    // 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 activeSpec = currentActiveModel is SttModelSpec
        ? currentActiveModel
        : null;
    final SttBackendProvider? backend = activeSpec != null
        ? SttRegistry.instance.findFor(activeSpec)
        : (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).
    // maxTokens is unused by STT.
    final sttConfig = RuntimeConfig(
      maxTokens: 0,
      modelPath: modelPath,
      tokenizerPath: tokenizerPath,
      preferredBackend: preferredBackend,
    );
    // The backend's createModel(spec, config) signature requires a non-null
    // spec, but it resolves paths exclusively from config. On the legacy
    // explicit-paths path there is no active spec, so synthesize one from the
    // resolved file paths (FileSource) purely to satisfy the signature.
    // sttModelType defaults to moonshine — the only shipped profile — for
    // this legacy-path fallback.
    final specForBackend =
        activeSpec ??
        SttModelSpec(
          name: 'legacy:${path.basename(modelPath)}',
          modelSource: ModelSource.file(modelPath),
          tokenizerSource: ModelSource.file(tokenizerPath),
          sttModelType: SttModelType.moonshine,
        );
    final model = await backend.createModel(specForBackend, sttConfig);

    // Core owns the singleton lifecycle: track it + reset on close. The
    // package-built model fires this via CloseNotifier (addCloseListener).
    _initializedSttModel = model;
    model.addCloseListener(() {
      _initializedSttModel = null;
      _initSttCompleter = null;
      _lastActiveSttModelName = null;
    });

    _lastActiveSttModelName = currentActiveModel?.name;
    completer.complete(model);
    return model;
  } catch (e, st) {
    completer.completeError(e, st);
    _initSttCompleter = null;
    _initializedSttModel = null;
    _lastActiveSttModelName = null;
    rethrow;
  }
}