flutter_embedder 0.0.1 copy "flutter_embedder: ^0.0.1" to clipboard
flutter_embedder: ^0.0.1 copied to clipboard

Flutter FFI plugin for Hugging Face tokenizers and ONNX embedding models using ONNX Runtime.

example/lib/main.dart

import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_embedder/flutter_embedder.dart';
import 'package:path_provider/path_provider.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initFlutterEmbedder();
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final List<String> _log = [];
  final TextEditingController _qwenModelController = TextEditingController();
  final TextEditingController _qwenTokenizerController =
      TextEditingController();
  final TextEditingController _gemmaModelController = TextEditingController();
  final TextEditingController _gemmaTokenizerController =
      TextEditingController();

  HfTokenizer? _tokenizer;
  Qwen3Embedder? _qwenEmbedder;
  GemmaEmbedder? _gemmaEmbedder;
  String? _qwenModelPath;
  String? _qwenTokenizerPath;
  String? _gemmaModelPath;
  String? _gemmaTokenizerPath;
  String _docsDir = '';
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    _bootstrap();
  }

  @override
  void dispose() {
    _qwenModelController.dispose();
    _qwenTokenizerController.dispose();
    _gemmaModelController.dispose();
    _gemmaTokenizerController.dispose();
    super.dispose();
  }

  Future<void> _bootstrap() async {
    if (!Platform.isAndroid) {
      _appendLog('This example targets Android only.');
      return;
    }

    final docs = await getApplicationDocumentsDirectory();
    _docsDir = docs.path;

    _tokenizer = await HfTokenizer.fromAsset('assets/tokenizer.json');
    final tokenizerPath = await _copyAsset(
      'assets/tokenizer.json',
      '${docs.path}/tokenizer.json',
    );

    _qwenTokenizerController.text = tokenizerPath;
    _gemmaTokenizerController.text = tokenizerPath;
    _qwenModelController.text = '${docs.path}/qwen3-embedding.onnx';
    _gemmaModelController.text = '${docs.path}/gemma-embedding.onnx';

    _appendLog('Documents dir: ${docs.path}');
    _appendLog('Tokenizer asset copied to: $tokenizerPath');
    _appendLog(
      'Place ONNX models at the paths above before running embeddings.',
    );
    if (!mounted) {
      return;
    }
    setState(() {});
  }

  Future<String> _copyAsset(String assetPath, String outputPath) async {
    final data = await rootBundle.load(assetPath);
    final bytes = data.buffer.asUint8List();
    final file = File(outputPath);
    await file.writeAsBytes(bytes, flush: true);
    return file.path;
  }

  void _appendLog(String message) {
    if (!mounted) {
      return;
    }
    setState(() {
      _log.insert(0, message);
    });
  }

  Future<void> _runTokenizerDemo() async {
    if (_tokenizer == null) {
      _appendLog('Tokenizer not ready yet.');
      return;
    }
    final encoding = _tokenizer!.encode('hello worlds', addSpecialTokens: true);
    final decoded = _tokenizer!.decode(encoding.ids, skipSpecialTokens: true);
    _appendLog('Tokenizer IDs: ${encoding.ids}');
    _appendLog('Tokenizer tokens: ${encoding.tokens}');
    _appendLog('Tokenizer decoded: $decoded');
  }

  Future<void> _runQwenDemo() async {
    final modelPath = _qwenModelController.text.trim();
    final tokenizerPath = _qwenTokenizerController.text.trim();
    if (!_requireFile(modelPath, 'Qwen3 model')) return;
    if (!_requireFile(tokenizerPath, 'Qwen3 tokenizer')) return;

    setState(() => _loading = true);
    try {
      if (_qwenEmbedder == null ||
          _qwenModelPath != modelPath ||
          _qwenTokenizerPath != tokenizerPath) {
        _qwenEmbedder = Qwen3Embedder.create(
          modelPath: modelPath,
          tokenizerPath: tokenizerPath,
        );
        _qwenModelPath = modelPath;
        _qwenTokenizerPath = tokenizerPath;
      }
      final embedder = _qwenEmbedder!;
      final inputs = <String>[
        Qwen3Embedder.formatQuery(query: 'What is the capital of China?'),
        Qwen3Embedder.formatQuery(query: 'Explain gravity'),
        Qwen3Embedder.formatDocument(text: 'The capital of China is Beijing.'),
        Qwen3Embedder.formatDocument(
          text:
              'Gravity is a force that attracts two bodies towards each other.',
        ),
      ];
      final embeddings = embedder.embed(texts: inputs);
      final sim = _cosine(embeddings[0], embeddings[2]);
      _appendLog('Qwen3 embeddings: ${embeddings.length} items');
      _appendLog('Qwen3 dim: ${embeddings[0].length}');
      _appendLog('Qwen3 cosine(query0, doc0): ${sim.toStringAsFixed(4)}');
    } catch (err) {
      _appendLog('Qwen3 embedding failed: $err');
    } finally {
      setState(() => _loading = false);
    }
  }

  Future<void> _runGemmaDemo() async {
    final modelPath = _gemmaModelController.text.trim();
    final tokenizerPath = _gemmaTokenizerController.text.trim();
    if (!_requireFile(modelPath, 'Gemma model')) return;
    if (!_requireFile(tokenizerPath, 'Gemma tokenizer')) return;

    setState(() => _loading = true);
    try {
      if (_gemmaEmbedder == null ||
          _gemmaModelPath != modelPath ||
          _gemmaTokenizerPath != tokenizerPath) {
        _gemmaEmbedder = GemmaEmbedder.create(
          modelPath: modelPath,
          tokenizerPath: tokenizerPath,
        );
        _gemmaModelPath = modelPath;
        _gemmaTokenizerPath = tokenizerPath;
      }
      final embedder = _gemmaEmbedder!;
      final query = GemmaEmbedder.formatQuery(
        query: 'Which planet is known as the Red Planet?',
      );
      const docs = [
        'Venus is often called Earth\'s twin.',
        'Mars, known for its reddish appearance, is often referred to as the Red Planet.',
        'Jupiter has a prominent red spot.',
        'Saturn is famous for its rings.',
      ];
      final embeddings = <Float32List>[
        ...embedder.embed(
          texts: [
            query,
            ...docs.map((text) => GemmaEmbedder.formatDocument(text: text)),
          ],
        ),
      ];
      final sim = _cosine(embeddings[0], embeddings[2]);
      _appendLog('Gemma embeddings: ${embeddings.length} items');
      _appendLog('Gemma dim: ${embeddings[0].length}');
      _appendLog('Gemma cosine(query, doc1): ${sim.toStringAsFixed(4)}');
    } catch (err) {
      _appendLog('Gemma embedding failed: $err');
    } finally {
      setState(() => _loading = false);
    }
  }

  bool _requireFile(String path, String label) {
    if (path.isEmpty) {
      _appendLog('$label path is empty.');
      return false;
    }
    if (!File(path).existsSync()) {
      _appendLog('$label not found: $path');
      return false;
    }
    return true;
  }

  double _cosine(Float32List a, Float32List b) {
    final aList = a.map((v) => v.toDouble()).toList(growable: false);
    final bList = b.map((v) => v.toDouble()).toList(growable: false);
    return cosineDistance(aList, bList);
  }

  @override
  Widget build(BuildContext context) {
    final controls = <Widget>[
      Text('Docs dir: $_docsDir'),
      const SizedBox(height: 8),
      const Text('Tokenizer demo'),
      ElevatedButton(
        onPressed: _loading ? null : _runTokenizerDemo,
        child: const Text('Run tokenizer demo'),
      ),
      const SizedBox(height: 16),
      const Text('Qwen3 paths'),
      TextField(
        controller: _qwenModelController,
        decoration: const InputDecoration(labelText: 'Qwen3 model path'),
      ),
      TextField(
        controller: _qwenTokenizerController,
        decoration: const InputDecoration(labelText: 'Qwen3 tokenizer path'),
      ),
      const SizedBox(height: 8),
      ElevatedButton(
        onPressed: _loading ? null : _runQwenDemo,
        child: const Text('Run Qwen3 embedding'),
      ),
      const SizedBox(height: 16),
      const Text('Gemma paths'),
      TextField(
        controller: _gemmaModelController,
        decoration: const InputDecoration(labelText: 'Gemma model path'),
      ),
      TextField(
        controller: _gemmaTokenizerController,
        decoration: const InputDecoration(labelText: 'Gemma tokenizer path'),
      ),
      const SizedBox(height: 8),
      ElevatedButton(
        onPressed: _loading ? null : _runGemmaDemo,
        child: const Text('Run Gemma embedding'),
      ),
    ];

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Flutter Embedding (Android)')),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              if (!Platform.isAndroid)
                const Text('This example only supports Android.'),
              Expanded(child: ListView(children: controls)),
              const SizedBox(height: 12),
              const Text('Logs'),
              const SizedBox(height: 8),
              Expanded(
                child: ListView.builder(
                  itemCount: _log.length,
                  itemBuilder: (context, index) => Text(_log[index]),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
0
likes
160
points
--
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter FFI plugin for Hugging Face tokenizers and ONNX embedding models using ONNX Runtime.

Repository (GitHub)

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter, flutter_rust_bridge, plugin_platform_interface

More

Packages that depend on flutter_embedder

Packages that implement flutter_embedder