flutter_gemma_embedder 0.0.1-dev.1 copy "flutter_gemma_embedder: ^0.0.1-dev.1" to clipboard
flutter_gemma_embedder: ^0.0.1-dev.1 copied to clipboard

Flutter plugin for on-device embedder, inspired by EmbeddingGemma. Generate text embeddings locally for semantic search and similarity tasks.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_gemma_embedder/flutter_gemma_embedder.dart';
import 'package:flutter_gemma_embedder_example/model_selection_screen.dart';
import 'package:flutter_gemma_embedder_example/models/embedding_model_config.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const EmbeddingApp());
}

class EmbeddingApp extends StatelessWidget {
  const EmbeddingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Gemma Embedder Demo',
      theme: ThemeData.dark().copyWith(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6B73FF), // Gemma brand blue-purple
          brightness: Brightness.dark,
        ),
        cardTheme: const CardThemeData(
          elevation: 2,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(16)),
          ),
        ),
        elevatedButtonTheme: ElevatedButtonThemeData(
          style: ElevatedButton.styleFrom(
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(12),
            ),
          ),
        ),
      ),
      home: const ModelSelectionScreen(),
      routes: {
        '/embedding_demo': (context) => const HomeScreen(),
      },
    );
  }
}

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  EmbeddingModelConfig? _modelConfig;
  String? _modelPath;
  final TextEditingController _textController = TextEditingController();
  final TextEditingController _queryController = TextEditingController();
  final TextEditingController _documentController = TextEditingController();
  
  EmbeddingModel? _embeddingModel;
  bool _isLoading = false;
  bool _isModelLoaded = false;
  String _status = 'Ready to load model';
  List<double>? _queryEmbedding;
  double? _similarity;
  
  final List<String> _examples = [
    'Flutter is a UI toolkit for building applications',
    'Machine learning helps computers learn from data',
    'Artificial intelligence mimics human cognitive functions',
    'Mobile development creates apps for smartphones',
  ];

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    
    // Get model info from route arguments
    final args = ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
    if (args != null && _modelConfig == null) {
      _modelConfig = args['model'] as EmbeddingModelConfig?;
      _modelPath = args['modelPath'] as String?;
      _initializeModel();
    }
  }

  Future<void> _initializeModel() async {
    if (_modelConfig == null || _modelPath == null) return;

    setState(() {
      _isLoading = true;
      _status = 'Loading ${_modelConfig!.displayName}...';
    });

    try {
      final embedder = FlutterGemmaEmbedder.instance;
      _embeddingModel = await embedder.createModel(
        modelPath: _modelPath!,
        modelType: _modelConfig!.modelType,
        dimensions: _modelConfig!.dimensions,
        taskType: _modelConfig!.taskType,
        backend: _modelConfig!.preferredBackend,
      );
      
      // Simulate model initialization
      await Future.delayed(const Duration(seconds: 1));
      // await _embeddingModel!.initialize();
      
      setState(() {
        _isModelLoaded = true;
        _status = '${_modelConfig!.displayName} loaded successfully! Ready for embeddings.';
      });
    } catch (e) {
      setState(() {
        _status = 'Error loading ${_modelConfig!.displayName}: $e';
      });
    } finally {
      setState(() => _isLoading = false);
    }
  }

  Future<void> _generateEmbedding() async {
    if (!_isModelLoaded || _textController.text.isEmpty) return;

    setState(() {
      _isLoading = true;
      _status = 'Generating embedding...';
    });

    try {
      // Simulate embedding generation
      await Future.delayed(const Duration(milliseconds: 500));
      
      // For demo purposes, generate random embedding
      final embedding = List.generate(768, (index) => 
        (index * 0.001 + _textController.text.hashCode * 0.0001) % 1.0 - 0.5);
      
      setState(() {
        _queryEmbedding = embedding;
        _status = 'Embedding generated successfully! Dimensions: ${embedding.length}';
      });
    } catch (e) {
      setState(() => _status = 'Error generating embedding: $e');
    } finally {
      setState(() => _isLoading = false);
    }
  }

  Future<void> _compareSimilarity() async {
    if (!_isModelLoaded || _queryController.text.isEmpty || _documentController.text.isEmpty) return;

    setState(() {
      _isLoading = true;
      _status = 'Computing similarity...';
    });

    try {
      await Future.delayed(const Duration(milliseconds: 300));
      
      // Generate demo embeddings
      final queryEmb = List.generate(768, (index) => 
        (index * 0.001 + _queryController.text.hashCode * 0.0001) % 1.0 - 0.5);
      final docEmb = List.generate(768, (index) => 
        (index * 0.001 + _documentController.text.hashCode * 0.0001) % 1.0 - 0.5);
      
      // Calculate cosine similarity
      final similarity = _embeddingModel!.cosineSimilarity(queryEmb, docEmb);
      
      setState(() {
        _queryEmbedding = queryEmb;
        _similarity = similarity;
        _status = 'Similarity computed: ${similarity.toStringAsFixed(4)}';
      });
    } catch (e) {
      setState(() => _status = 'Error computing similarity: $e');
    } finally {
      setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () {
            Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
          },
        ),
        title: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(
              width: 28,
              height: 28,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(6),
                gradient: const LinearGradient(
                  colors: [Color(0xFF6B73FF), Color(0xFF9C77FF)],
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                ),
              ),
              child: const Icon(
                Icons.psychology,
                color: Colors.white,
                size: 16,
              ),
            ),
            const SizedBox(width: 8),
            Text(_modelConfig?.displayName ?? 'Flutter Gemma Embedder'),
          ],
        ),
        centerTitle: true,
        backgroundColor: const Color(0xFF6B73FF),
        foregroundColor: Colors.white,
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // Status Card with Gemma Branding
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    children: [
                      // Gemma Logo placeholder - replace with actual Image.asset('assets/gemma3.png') 
                      Container(
                        width: 64,
                        height: 64,
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(12),
                          gradient: const LinearGradient(
                            colors: [Color(0xFF6B73FF), Color(0xFF9C77FF)],
                            begin: Alignment.topLeft,
                            end: Alignment.bottomRight,
                          ),
                        ),
                        child: Icon(
                          _isModelLoaded ? Icons.psychology : Icons.smart_toy,
                          color: Colors.white,
                          size: 32,
                        ),
                      ),
                      const SizedBox(height: 8),
                      Text(
                        _status,
                        textAlign: TextAlign.center,
                        style: Theme.of(context).textTheme.bodyLarge,
                      ),
                      if (_isLoading) ...[
                        const SizedBox(height: 16),
                        const CircularProgressIndicator(),
                      ],
                    ],
                  ),
                ),
              ),
              
              const SizedBox(height: 24),
              
              // Single Text Embedding
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Generate Text Embedding',
                        style: Theme.of(context).textTheme.headlineSmall,
                      ),
                      const SizedBox(height: 16),
                      TextField(
                        controller: _textController,
                        decoration: const InputDecoration(
                          labelText: 'Enter text to embed',
                          border: OutlineInputBorder(),
                          hintText: 'Type your text here...',
                        ),
                        maxLines: 3,
                      ),
                      const SizedBox(height: 16),
                      ElevatedButton.icon(
                        onPressed: _isModelLoaded && !_isLoading ? _generateEmbedding : null,
                        icon: const Icon(Icons.psychology),
                        label: const Text('Generate Embedding'),
                      ),
                      if (_queryEmbedding != null) ...[
                        const SizedBox(height: 16),
                        Text(
                          'Embedding Preview (first 10 dimensions):',
                          style: Theme.of(context).textTheme.bodyMedium,
                        ),
                        const SizedBox(height: 8),
                        Container(
                          padding: const EdgeInsets.all(12),
                          decoration: BoxDecoration(
                            color: Theme.of(context).colorScheme.surfaceVariant,
                            borderRadius: BorderRadius.circular(8),
                          ),
                          child: Text(
                            _queryEmbedding!.take(10)
                                .map((e) => e.toStringAsFixed(4))
                                .join(', ') + '...',
                            style: Theme.of(context).textTheme.bodySmall?.copyWith(
                              fontFamily: 'monospace',
                            ),
                          ),
                        ),
                      ],
                    ],
                  ),
                ),
              ),
              
              const SizedBox(height: 24),
              
              // Similarity Comparison
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Compare Text Similarity',
                        style: Theme.of(context).textTheme.headlineSmall,
                      ),
                      const SizedBox(height: 16),
                      TextField(
                        controller: _queryController,
                        decoration: const InputDecoration(
                          labelText: 'Query text',
                          border: OutlineInputBorder(),
                        ),
                        maxLines: 2,
                      ),
                      const SizedBox(height: 12),
                      TextField(
                        controller: _documentController,
                        decoration: const InputDecoration(
                          labelText: 'Document text',
                          border: OutlineInputBorder(),
                        ),
                        maxLines: 2,
                      ),
                      const SizedBox(height: 16),
                      ElevatedButton.icon(
                        onPressed: _isModelLoaded && !_isLoading ? _compareSimilarity : null,
                        icon: const Icon(Icons.compare),
                        label: const Text('Compare Similarity'),
                      ),
                      if (_similarity != null) ...[
                        const SizedBox(height: 16),
                        Container(
                          padding: const EdgeInsets.all(16),
                          decoration: BoxDecoration(
                            color: _similarity! > 0.7 ? Colors.green.withOpacity(0.1) : 
                                   _similarity! > 0.4 ? Colors.orange.withOpacity(0.1) : 
                                   Colors.red.withOpacity(0.1),
                            borderRadius: BorderRadius.circular(8),
                          ),
                          child: Column(
                            children: [
                              Text(
                                'Cosine Similarity',
                                style: Theme.of(context).textTheme.bodyLarge,
                              ),
                              const SizedBox(height: 8),
                              Text(
                                _similarity!.toStringAsFixed(4),
                                style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                                  fontWeight: FontWeight.bold,
                                  color: _similarity! > 0.7 ? Colors.green : 
                                         _similarity! > 0.4 ? Colors.orange : Colors.red,
                                ),
                              ),
                              const SizedBox(height: 8),
                              LinearProgressIndicator(
                                value: (_similarity! + 1) / 2, // Normalize to 0-1
                                backgroundColor: Colors.grey.withOpacity(0.3),
                                valueColor: AlwaysStoppedAnimation<Color>(
                                  _similarity! > 0.7 ? Colors.green : 
                                  _similarity! > 0.4 ? Colors.orange : Colors.red,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ],
                  ),
                ),
              ),
              
              const SizedBox(height: 24),
              
              // Example Texts
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Example Texts',
                        style: Theme.of(context).textTheme.headlineSmall,
                      ),
                      const SizedBox(height: 16),
                      ..._examples.map((example) => Padding(
                        padding: const EdgeInsets.only(bottom: 8.0),
                        child: InkWell(
                          onTap: () {
                            _textController.text = example;
                            _queryController.text = example;
                          },
                          child: Container(
                            width: double.infinity,
                            padding: const EdgeInsets.all(12),
                            decoration: BoxDecoration(
                              color: Theme.of(context).colorScheme.surfaceVariant,
                              borderRadius: BorderRadius.circular(8),
                            ),
                            child: Text(
                              example,
                              style: Theme.of(context).textTheme.bodyMedium,
                            ),
                          ),
                        ),
                      )),
                    ],
                  ),
                ),
              ),
              
              const SizedBox(height: 24),
              
              // Footer with Gemma Info
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    children: [
                      Wrap(
                        alignment: WrapAlignment.center,
                        spacing: 8,
                        runSpacing: 8,
                        children: [
                          Container(
                            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                            decoration: BoxDecoration(
                              color: const Color(0xFF6B73FF).withOpacity(0.2),
                              borderRadius: BorderRadius.circular(6),
                            ),
                            child: Text(
                              _modelConfig?.displayName ?? 'EmbeddingGemma 300M',
                              style: const TextStyle(
                                fontSize: 12,
                                fontWeight: FontWeight.w500,
                              ),
                            ),
                          ),
                          if (_modelConfig != null)
                            Container(
                              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                              decoration: BoxDecoration(
                                color: Colors.blue.withOpacity(0.2),
                                borderRadius: BorderRadius.circular(6),
                              ),
                              child: Text(
                                '${_modelConfig!.dimensions}D',
                                style: const TextStyle(
                                  fontSize: 12,
                                  fontWeight: FontWeight.w500,
                                  color: Colors.blue,
                                ),
                              ),
                            ),
                          Container(
                            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                            decoration: BoxDecoration(
                              color: Colors.green.withOpacity(0.2),
                              borderRadius: BorderRadius.circular(6),
                            ),
                            child: const Text(
                              'On-Device AI',
                              style: TextStyle(
                                fontSize: 12,
                                fontWeight: FontWeight.w500,
                                color: Colors.green,
                              ),
                            ),
                          ),
                        ],
                      ),
                      const SizedBox(height: 8),
                      Text(
                        'Powered by flutter_gemma_embedder v0.10.4',
                        style: Theme.of(context).textTheme.bodySmall?.copyWith(
                          color: Colors.grey.shade400,
                        ),
                        textAlign: TextAlign.center,
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _textController.dispose();
    _queryController.dispose();
    _documentController.dispose();
    _embeddingModel?.dispose();
    super.dispose();
  }
}
5
likes
150
points
61
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter plugin for on-device embedder, inspired by EmbeddingGemma. Generate text embeddings locally for semantic search and similarity tasks.

Homepage

Documentation

API reference

License

MIT (license)

Dependencies

flutter, flutter_web_plugins, path, path_provider, plugin_platform_interface, web

More

Packages that depend on flutter_gemma_embedder

Packages that implement flutter_gemma_embedder