fasttext_flutter 0.1.2 copy "fasttext_flutter: ^0.1.2" to clipboard
fasttext_flutter: ^0.1.2 copied to clipboard

On-device text classification and sentence embeddings using fastText .ftz quantized models via Dart FFI. Verified on Android. Contributions welcome

example/lib/main.dart

// Copyright 2024 the fasttext_flutter authors. Apache-2.0 license.
//
// Example app: demonstrates fasttext_flutter for both:
//   - Supervised models (e.g. lid.176.ftz): text classification via predict()
//   - Unsupervised models (e.g. word-vector .ftz): embeddings + cosineSimilarity
//
// Place your model at example/assets/model.ftz (any model works).
// For a pre-trained language-ID model: https://fasttext.cc/docs/en/language-identification.html

import 'dart:typed_data';

import 'package:fasttext_flutter/fasttext_flutter.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const FastTextExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'fasttext_flutter Demo',
      theme: ThemeData.dark(useMaterial3: true).copyWith(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6C63FF),
          brightness: Brightness.dark,
        ),
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  FastTextModel? _model;
  bool _loading = true;
  String? _loadError;

  final _controller = TextEditingController();
  final _compareController = TextEditingController();
  List<FastTextPrediction> _predictions = [];
  Float32List? _embedding;
  double? _similarity;
  bool _inferring = false;

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

  Future<void> _initModel() async {
    print('--> App init: Loading fastText model...');
    try {
      final m = await FastTextModel.loadAsset('assets/model.ftz');
      print('--> App init: Model loaded successfully.');
      setState(() {
        _model = m;
        _loading = false;
      });
    } catch (e) {
      print('--> App init ERROR: $e');
      setState(() {
        _loadError = e.toString();
        _loading = false;
      });
    }
  }

  Future<void> _runInference() async {
    final text = _controller.text.trim();
    if (text.isEmpty || _model == null) return;

    setState(() => _inferring = true);
    try {
      // computeEmbedding() works for ALL model types.
      final emb = await _model!.computeEmbedding(text);
      setState(() => _embedding = emb);

      // predict() is supervised-only.
      if (_model!.isSupervised) {
        final preds = await _model!.predict(text, k: 5);
        setState(() => _predictions = preds);
      } else {
        setState(() => _predictions = []);
      }

      // Cosine similarity to the comparison text (if provided).
      final compareText = _compareController.text.trim();
      if (compareText.isNotEmpty) {
        final embB = await _model!.computeEmbedding(compareText);
        setState(() => _similarity = FastTextModel.cosineSimilarity(emb, embB));
      } else {
        setState(() => _similarity = null);
      }
    } on FastTextException catch (e) {
      _showError(e.message);
    } finally {
      setState(() => _inferring = false);
    }
  }

  void _showError(String msg) {
    if (!mounted) return;
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text(msg), backgroundColor: Colors.red));
  }

  @override
  void dispose() {
    _model?.close();
    _controller.dispose();
    _compareController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Scaffold(
      backgroundColor: cs.surface,
      appBar: AppBar(
        title: const Text('fasttext_flutter'),
        centerTitle: true,
        backgroundColor: cs.surfaceContainerHighest,
        actions: [
          if (_model != null)
            Padding(
              padding: const EdgeInsets.only(right: 12),
              child: Chip(
                label: Text(
                  'type: ${_model!.modelType.name} · dim: ${_model!.dimension} · labels: ${_model!.labelCount}',
                  style: const TextStyle(fontSize: 11),
                ),
                backgroundColor: cs.primaryContainer,
              ),
            ),
        ],
      ),
      body: _loading
          ? const Center(child: CircularProgressIndicator())
          : _loadError != null
          ? _ErrorView(error: _loadError!)
          : _MainView(
              controller: _controller,
              compareController: _compareController,
              predictions: _predictions,
              embedding: _embedding,
              similarity: _similarity,
              inferring: _inferring,
              isSupervised: _model?.isSupervised ?? false,
              onRun: _runInference,
            ),
    );
  }
}

// ---------------------------------------------------------------------------
// Error screen
// ---------------------------------------------------------------------------

class _ErrorView extends StatelessWidget {
  final String error;
  const _ErrorView({required this.error});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Icon(Icons.error_outline, size: 64, color: Colors.red),
            const SizedBox(height: 16),
            const Text(
              'Failed to load model',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text(
              error,
              textAlign: TextAlign.center,
              style: const TextStyle(color: Colors.grey),
            ),
            const SizedBox(height: 16),
            const Text(
              'Place model.ftz in example/assets/ and re-run.',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 12),
            ),
          ],
        ),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Main content
// ---------------------------------------------------------------------------

class _MainView extends StatelessWidget {
  final TextEditingController controller;
  final TextEditingController compareController;
  final List<FastTextPrediction> predictions;
  final Float32List? embedding;
  final double? similarity;
  final bool inferring;
  final bool isSupervised;
  final VoidCallback onRun;

  const _MainView({
    required this.controller,
    required this.compareController,
    required this.predictions,
    required this.embedding,
    required this.similarity,
    required this.inferring,
    required this.isSupervised,
    required this.onRun,
  });

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return SingleChildScrollView(
      padding: const EdgeInsets.all(20),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Input text
          _SectionHeader(isSupervised ? 'Classify text' : 'Compute embedding'),
          const SizedBox(height: 8),
          TextField(
            controller: controller,
            maxLines: 3,
            decoration: InputDecoration(
              hintText: 'Enter text…',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
              ),
              filled: true,
              fillColor: cs.surfaceContainerHighest,
            ),
          ),
          const SizedBox(height: 10),

          // Comparison text (for cosine similarity)
          _SectionHeader('Compare with (for cosine similarity)'),
          const SizedBox(height: 8),
          TextField(
            controller: compareController,
            decoration: InputDecoration(
              hintText: 'Enter a second text to compare…  (optional)',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
              ),
              filled: true,
              fillColor: cs.surfaceContainerHighest,
            ),
          ),
          const SizedBox(height: 12),

          // Run button
          FilledButton.icon(
            onPressed: inferring ? null : onRun,
            icon: inferring
                ? const SizedBox(
                    width: 18,
                    height: 18,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Icon(Icons.bolt),
            label: Text(inferring ? 'Running…' : 'Run'),
          ),
          const SizedBox(height: 28),

          // Cosine similarity card
          if (similarity != null) ...[
            _SectionHeader('Cosine Similarity'),
            const SizedBox(height: 8),
            _SimilarityCard(similarity: similarity!),
            const SizedBox(height: 24),
          ],

          // Classification predictions (supervised only)
          if (predictions.isNotEmpty) ...[
            _SectionHeader('Top Predictions'),
            const SizedBox(height: 8),
            ...predictions.map((p) => _PredictionCard(prediction: p)),
            const SizedBox(height: 24),
          ],

          // Embedding snippet
          if (embedding != null) ...[
            _SectionHeader('Embedding (first 8 of ${embedding!.length} dims)'),
            const SizedBox(height: 8),
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(12),
                color: cs.surfaceContainerHighest,
              ),
              child: Text(
                '${embedding!.take(8).map((v) => v.toStringAsFixed(4)).join(', ')}…',
                style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
              ),
            ),
          ],
        ],
      ),
    );
  }
}

class _SectionHeader extends StatelessWidget {
  final String title;
  const _SectionHeader(this.title);

  @override
  Widget build(BuildContext context) {
    return Text(
      title,
      style: Theme.of(context).textTheme.titleSmall?.copyWith(
        fontWeight: FontWeight.bold,
        color: Theme.of(context).colorScheme.primary,
      ),
    );
  }
}

class _SimilarityCard extends StatelessWidget {
  final double similarity;
  const _SimilarityCard({required this.similarity});

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    final pct = ((similarity + 1) / 2).clamp(0.0, 1.0); // map [-1,1]→[0,1]
    return Card(
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
        child: Row(
          children: [
            Expanded(
              child: ClipRRect(
                borderRadius: BorderRadius.circular(4),
                child: LinearProgressIndicator(
                  value: pct,
                  minHeight: 8,
                  backgroundColor: cs.surfaceContainerHighest,
                  color: cs.tertiary,
                ),
              ),
            ),
            const SizedBox(width: 16),
            Text(
              similarity.toStringAsFixed(4),
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 18,
                color: cs.tertiary,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _PredictionCard extends StatelessWidget {
  final FastTextPrediction prediction;
  const _PredictionCard({required this.prediction});

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Card(
      margin: const EdgeInsets.only(bottom: 8),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        child: Row(
          children: [
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    prediction.cleanLabel.toUpperCase(),
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 16,
                    ),
                  ),
                  const SizedBox(height: 4),
                  ClipRRect(
                    borderRadius: BorderRadius.circular(4),
                    child: LinearProgressIndicator(
                      value: prediction.probability,
                      minHeight: 6,
                      backgroundColor: cs.surfaceContainerHighest,
                      color: cs.primary,
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(width: 16),
            Text(
              '${(prediction.probability * 100).toStringAsFixed(1)}%',
              style: TextStyle(
                color: cs.primary,
                fontWeight: FontWeight.bold,
                fontSize: 16,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
150
points
138
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

On-device text classification and sentence embeddings using fastText .ftz quantized models via Dart FFI. Verified on Android. Contributions welcome

Repository (GitHub)
View/report issues

Topics

#nlp #machine-learning #ffi #text-classification #embeddings

License

unknown (license)

Dependencies

code_assets, ffi, flutter, hooks, logging, native_toolchain_c, path_provider

More

Packages that depend on fasttext_flutter