createEmbeddingModel method

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

Creates and returns a new EmbeddingModel instance.

Modern API: If paths are not provided, uses the active embedding model set via FlutterGemma.installEmbedder() or modelManager.setActiveModel().

Legacy API: Provide explicit paths for backward compatibility.

modelPath — path to the embedding 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<EmbeddingModel> createEmbeddingModel({
  String? modelPath,
  String? tokenizerPath,
  PreferredBackend? preferredBackend,
}) async {
  // Check if active embedding model changed
  final currentActiveModel = _modelManager.activeEmbeddingModel;
  if (_initEmbeddingCompleter != null &&
      _initializedEmbeddingModel != null &&
      _lastActiveEmbeddingModelName != null) {
    final modelChanged = currentActiveModel == null ||
        currentActiveModel.name != _lastActiveEmbeddingModelName;
    if (modelChanged) {
      await _initializedEmbeddingModel?.close();
      _initEmbeddingCompleter = null;
      _initializedEmbeddingModel = null;
      _lastActiveEmbeddingModelName = null;
    } else {
      return _initEmbeddingCompleter!.future;
    }
  }

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

  final completer = _initEmbeddingCompleter = Completer<EmbeddingModel>();

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

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

      modelPath ??= filePaths[PreferencesKeys.embeddingModelFile];
      tokenizerPath ??= filePaths[PreferencesKeys.embeddingTokenizerFile];
    }

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

    debugPrint('[FlutterGemmaDesktop] Loading embedding model: $modelPath');

    // Load TFLite interpreter via dart:ffi
    final numThreads =
        preferredBackend == PreferredBackend.cpu ? 4 : 6;
    final interpreter = TfLiteInterpreter.fromFile(
      modelPath,
      numThreads: numThreads,
    );

    debugPrint(
      '[FlutterGemmaDesktop] Embedding model loaded: '
      'seqLen=${interpreter.inputSequenceLength}, '
      'dim=${interpreter.outputDimension}',
    );

    // Load tokenizer - auto-detect format by file extension
    // BOS/EOS are added manually in DesktopEmbeddingModel with correct Gemma IDs
    // (dart_sentencepiece_tokenizer defaults to bosId=1/eosId=2, but Gemma uses bosId=2/eosId=1)
    if (tokenizerPath == null) {
      interpreter.close();
      throw StateError('Tokenizer path is required for desktop embeddings');
    }
    final SentencePieceTokenizer tokenizer;
    try {
      if (tokenizerPath.endsWith('.json')) {
        tokenizer = await TokenizerJsonLoader.fromJsonFile(
          tokenizerPath,
          config: const SentencePieceConfig(),
        );
      } else {
        tokenizer = await SentencePieceTokenizer.fromModelFile(
          tokenizerPath,
          config: const SentencePieceConfig(),
        );
      }
    } catch (e) {
      interpreter.close();
      rethrow;
    }

    List<int> tokenize(String text) {
      return tokenizer.encode(text).ids.toList();
    }

    final model = _initializedEmbeddingModel = DesktopEmbeddingModel(
      interpreter: interpreter,
      tokenize: tokenize,
      onClose: () {
        _initializedEmbeddingModel = null;
        _initEmbeddingCompleter = null;
        _lastActiveEmbeddingModelName = null;
      },
    );

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