flutter_gemma_embedder 0.0.1-dev.1
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();
}
}