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.

classify_card #

pub package license: MIT platform

On-device Card Gate V3 for Flutter. Given a photo, it decides whether an identity document is the main subject — so you only spend a network round trip on photos that actually contain one.

It runs entirely on the device. Nothing is uploaded, and no network permission is needed.

What it does and does not do #

Does Does not
Answers card or other for a photo OCR, read fields, or return text
Returns a calibrated confidence score Identify the document subtype (NRC / passport / licence)
Runs offline in ~20 ms warm Produce bounding boxes or crop the card
Liveness, face matching, or anti-fraud

It is a pre-check, not a replacement for your classify/OCR backend.

Requirements #

Minimum Why
Android minSdkVersion 21 TFLite Task Vision needs API 21. The Flutter plugin template defaults to 19 — if your app is still on 19 you must raise it.
iOS 12.0 Flutter 3.x minimum; the pod is set to match.
App size +3.8 MB per platform The model ships inside the plugin.

You do not need to add noCompress 'tflite', register Xcode resources, or ship the model yourself — the plugin carries its own model and loads it in a way that does not depend on host-app packaging settings.

Install #

dependencies:
  classify_card: ^0.1.0

Two host-app changes are required, both one-liners.

Androidandroid/app/build.gradle:

android {
  defaultConfig {
    minSdkVersion 21   // was flutter.minSdkVersion / 19
  }
}

iOSios/Podfile. TensorFlowLiteSwift ships a static vendored framework, and CocoaPods refuses to embed one in a dynamically linked target:

target 'Runner' do
  use_frameworks! :linkage => :static   # was: use_frameworks!
  ...
end

Without it, pod install fails with The 'Pods-Runner' target has transitive dependencies that include statically linked binaries.

Then rebuild the app — this plugin contains native code, so a hot restart is not enough.

Quick start #

import 'package:classify_card/classify_card.dart';

final gate = ClassifyCard();

// Once, e.g. in initState of your capture screen. Never throws.
await gate.warmUp();

// After the user captures or picks a photo.
final decision = await gate.evaluate(photoPath, mode: CardGateMode.enforce);

if (!decision.allow) {
  showRetakeGuidance();   // no backend call
  return;
}

await myBackend.classify(photoPath);   // your existing flow, unchanged

photoPath should be the whole photo, upright. Do not crop it to the card first — the model was trained on full frames. A file:// prefix is fine, as is a bare path; Android also accepts a content:// URI. EXIF rotation is handled for you.

The two rules that matter #

1. It fails open #

decision.allow is false for exactly one reason: CardGateReason.other, meaning the model is confident there is no document. Every other outcome lets the request through — a missing plugin, a corrupt model, a decode error, a timeout. A broken classifier must never block a customer holding a valid card.

reason allow Meaning
card Document found
other No document — the only blocking case
shadow A result was produced but mode was shadow
disabled mode was off
unavailable Plugin absent from the binary, or the classifier could not be built
timeout Native call did not settle
failed Decode or inference failed

reason.isFailure is true for the last three. Log those — a spike means something is broken.

2. Start in shadow, not enforce #

mode defaults to CardGateMode.shadow: the model runs and reports, but never blocks. Ship that first, log the results next to what your backend decides, and only switch to CardGateMode.enforce once the two agree on your own traffic.

final decision = await gate.evaluate(path);         // shadow — cannot reject anyone
final r = decision.result;
if (r != null) {
  analytics.log('card_gate', r.toMap());            // safe to log: scores only, never the image
}

API #

Future<String?> warmUp();
Future<CardGateResult> classify(String imagePath, {Duration timeout});
Future<CardGateDecision> evaluate(String imagePath, {CardGateMode mode, Duration timeout});
Future<void> release();
Method Throws? Use when
warmUp() No — returns null on failure On screen mount, so the first photo is not charged the cold-start cost
evaluate() No Normal use. Handles fail-open for you
classify() CardGateException You want to own the error handling yourself
release() No Capture flow is done and you want the ~4 MB back

CardGateResult carries label, score (calibrated), rawScore (the model's own output), modelVersion, latencyMs and decodeMs. toMap() is safe to log — it never contains image data.

Do not re-threshold rawScore yourself. 0.5 is not the production operating point. score is the calibrated value that label was derived from, using the Platt transform and threshold in the bundled policy.

The plugin deliberately does no logging of its own. It hands you the data; you decide what goes to your own telemetry.

How each platform runs the model #

Android iOS
Runtime org.tensorflow:tensorflow-lite-task-vision:0.4.4 TensorFlowLiteSwift 2.10.0
Preprocessing Inside Task Vision Hand-written: EXIF-upright, then bilinear resize with half-pixel centers
Matches the vendor reference exactly Yes Equivalent, not bit-identical

iOS cannot use TensorFlowLiteTaskVision, and this is not a preference — the pod is unusable in a host app:

  • its 0.4.3 static framework exports the same TfLite* symbols as TensorFlowLiteC, so it collides with any app that already ships TFLite;
  • it redefines MLKit's GMLImage class, so it collides with any app using MLKit;
  • it has no arm64 simulator slice, so it cannot build for the iOS Simulator on Apple Silicon;
  • it has not been released since November 2022.

So iOS reproduces the same preprocessing explicitly. The consequence you should know about: iOS and Android resample the photo through slightly different code paths, so their rawScore for the same photo can differ a little. Measure it on your own images before enforcing — this is another reason to run shadow first.

Verifying it before you enforce #

  1. Run the example/ app, set the mode selector to shadow.
  2. Feed it ~10 real documents (including rotated 90° / 180° / 270°) and ~10 non-documents — portraits, landscapes, junk.
  3. Check the label is right in every case, and compare rawScore between an Android device and an iPhone on the same photos.
  4. Watch latencyMs. Target is p95 under 100 ms after warm-up.
  5. Only then switch to enforce.

Integrity #

The model is verified on every startup: its SHA-256 must equal metadata.model_sha256 in the bundled policy.json, and the policy's schema_version, score_transform.kind, scale and threshold are all range-checked. A mismatch makes the classifier unavailable — which fails open, it does not silently reject photos. The bundled model hashes to 361bbfc4e540e717d6a961ed119f47719416241ed939b2e0e90024ca14dd5855.

Model tensors, for reference: input input_1 UINT8 [1,224,224,3] scale 0.003921568859368563 zero-point 0; output Identity UINT8 [1,2] scale 0.00390625 zero-point 0.

Troubleshooting #

reason is always unavailable

Either the plugin is not in the binary — do a full rebuild, not a hot restart — or the native setup failed. Check the device log for card_gate_unavailable; the message names the exact cause (missing resource, SHA-256 mismatch, bad policy field).

Android build fails on minSdkVersion

Raise minSdkVersion to at least 21 in your app's android/app/build.gradle.

CocoaPods cannot resolve TensorFlowLiteSwift

Your app pins a different version. Override it in your own Podfile rather than editing the podspec, then re-run the verification steps above:

pod 'TensorFlowLiteSwift', '<your version>'

Duplicate symbol errors mentioning TfLite

Something else in your app links its own TFLite copy — most often TensorFlowLiteTaskVision, MediaPipeTasksVision, or a vendored .framework. Only one TFLite runtime can be linked; consolidate on TensorFlowLiteSwift.

iOS Simulator on an Apple Silicon Mac

Works. TensorFlowLiteSwift ships a proper xcframework with an arm64 simulator slice — this is one of the reasons Task Vision was rejected.

License #

See LICENSE. The bundled model and policy are covered by your agreement with the model provider, not by this package's license.

0
likes
0
points
296
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