classify_card 1.0.0 copy "classify_card: ^1.0.0" to clipboard
classify_card: ^1.0.0 copied to clipboard

On-device Card Gate V3 pre-check: decides whether a photo holds an identity document before you call a classify/OCR backend. Runs offline in ~20ms, fails open, Android and iOS.

example/lib/main.dart

import 'dart:io';

import 'package:classify_card/classify_card.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Card Gate demo',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const CardGateDemoPage(),
    );
  }
}

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

  @override
  State<CardGateDemoPage> createState() => _CardGateDemoPageState();
}

class _CardGateDemoPageState extends State<CardGateDemoPage> {
  final ClassifyCard _gate = ClassifyCard();
  final ImagePicker _picker = ImagePicker();

  CardGateMode _mode = CardGateMode.enforce;
  String? _modelVersion;
  String? _photoPath;
  CardGateDecision? _decision;
  bool _busy = false;

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

  /// Building the interpreter takes a moment, so it is done up front rather than on the first
  /// photo. This never throws.
  Future<void> _warmUp() async {
    final modelVersion = await _gate.warmUp();
    if (!mounted) return;
    setState(() => _modelVersion = modelVersion);
  }

  Future<void> _pickAndEvaluate(ImageSource source) async {
    final photo = await _picker.pickImage(source: source);
    if (photo == null) return;

    setState(() {
      _busy = true;
      _photoPath = photo.path;
      _decision = null;
    });

    final decision = await _gate.evaluate(photo.path, mode: _mode);
    if (!mounted) return;

    setState(() {
      _busy = false;
      _decision = decision;
    });

    // This is where a real integration would decide whether to call its backend:
    //
    //   if (!decision.allow) { showRetakeGuidance(); return; }
    //   await myBackend.classify(photo.path);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Card Gate demo'),
        bottom: PreferredSize(
          preferredSize: const Size.fromHeight(24),
          child: Padding(
            padding: const EdgeInsets.only(bottom: 6),
            child: Text(
              _modelVersion == null
                  ? 'classifier unavailable - the gate will be bypassed'
                  : 'model $_modelVersion',
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ),
        ),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          SegmentedButton<CardGateMode>(
            segments: const [
              ButtonSegment(value: CardGateMode.off, label: Text('off')),
              ButtonSegment(value: CardGateMode.shadow, label: Text('shadow')),
              ButtonSegment(value: CardGateMode.enforce, label: Text('enforce')),
            ],
            selected: {_mode},
            onSelectionChanged: (selection) =>
                setState(() => _mode = selection.first),
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              Expanded(
                child: FilledButton.icon(
                  onPressed:
                      _busy ? null : () => _pickAndEvaluate(ImageSource.camera),
                  icon: const Icon(Icons.photo_camera_outlined),
                  label: const Text('Camera'),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: FilledButton.tonalIcon(
                  onPressed:
                      _busy ? null : () => _pickAndEvaluate(ImageSource.gallery),
                  icon: const Icon(Icons.photo_library_outlined),
                  label: const Text('Gallery'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 16),
          if (_photoPath != null)
            ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: Image.file(
                File(_photoPath!),
                height: 220,
                width: double.infinity,
                fit: BoxFit.cover,
              ),
            ),
          const SizedBox(height: 16),
          if (_busy) const Center(child: CircularProgressIndicator()),
          if (_decision != null) _DecisionCard(decision: _decision!),
        ],
      ),
    );
  }
}

class _DecisionCard extends StatelessWidget {
  const _DecisionCard({required this.decision});

  final CardGateDecision decision;

  @override
  Widget build(BuildContext context) {
    final result = decision.result;
    final headline =
        decision.allow ? 'Continue to the backend' : 'Blocked - ask for a retake';

    return Card(
      color: decision.allow
          ? Theme.of(context).colorScheme.secondaryContainer
          : Theme.of(context).colorScheme.errorContainer,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(headline, style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 8),
            _row('reason', decision.reason.name),
            if (decision.reason.isFailure)
              _row('note', 'gate failed open - request still allowed'),
            if (result != null) ...[
              _row('label', result.label.name),
              _row('score', result.score.toStringAsFixed(5)),
              _row('rawScore', result.rawScore.toStringAsFixed(8)),
              _row('inference', '${result.latencyMs.toStringAsFixed(1)} ms'),
              _row('decode', '${result.decodeMs.toStringAsFixed(1)} ms'),
            ],
          ],
        ),
      ),
    );
  }

  Widget _row(String label, String value) => Padding(
        padding: const EdgeInsets.symmetric(vertical: 2),
        child: Row(
          children: [
            SizedBox(width: 92, child: Text(label)),
            Expanded(child: Text(value)),
          ],
        ),
      );
}
0
likes
0
points
257
downloads

Documentation

Documentation

Publisher

unverified uploader

Weekly Downloads

On-device Card Gate V3 pre-check: decides whether a photo holds an identity document before you call a classify/OCR backend. Runs offline in ~20ms, fails open, Android and iOS.

Repository (GitHub)
View/report issues

Topics

#ekyc #kyc #document-detection #tflite #on-device

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on classify_card

Packages that implement classify_card