naina — read text from images, on the device

pub license platforms

Read text from images, on the device.

Documentation · Try it in a browser · Source

final dir  = await getApplicationSupportDirectory();
final root = '${dir.path}/naina';

// Fetches ~11 MB once. Returns immediately afterwards.
await NainaModels.stage(modelsRoot: root);

final naina = await Naina.open(modelsRoot: root);
final page = naina.readRgbSync(rgb, width: w, height: h);
print(page.text);
naina.close();

Runs entirely on the device over FFI to naina's C++ core — no network calls, no API key. The same core and the same PP-OCRv6 models as naina's Python and Node packages.

Install

dependencies:
  naina: ^0.2.0

Model weights

Call NainaModels.stage once before Naina.open. It downloads ~11 MB for the default tier and is a no-op once the files are present.

await NainaModels.stage(
  modelsRoot: root,
  language: 'devanagari',                      // optional
  onProgress: (p) => print('${(p.fraction * 100).round()}%'),
);

Dart does the fetching because the Android core has no libcurl — the NDK ships none — exactly as JavaScript fetches for the browser. It does not decide where files go: naina_staging_plan in the C core returns both the URLs and the paths, so the cache layout has one definition and the core still sha256-verifies every file.

modelsRoot is required on mobile: the app sandbox cannot write to the default ~/.cache, where ~ does not even expand because there is no HOME.

Needs INTERNET permission on Android for the first run.

Getting RGB bytes

naina ships no image decoder — Flutter already has one.

import 'dart:ui' as ui;

Future<(Uint8List, int, int)> decodeToRgb(Uint8List fileBytes) async {
  final codec = await ui.instantiateImageCodec(fileBytes);
  final frame = await codec.getNextFrame();
  final image = frame.image;

  final rgba = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
  final src = rgba!.buffer.asUint8List();

  final rgb = Uint8List(image.width * image.height * 3);
  for (var i = 0, j = 0; i < src.length; i += 4, j += 3) {
    rgb[j] = src[i];
    rgb[j + 1] = src[i + 1];
    rgb[j + 2] = src[i + 2];
  }
  return (rgb, image.width, image.height);
}

Decode at native resolution and let naina resize; its resize is the same code on every platform, so results stay consistent across devices.

Keep it off the UI thread

A full page takes noticeable time. readRgbSync runs on the calling isolate, so use a background isolate for anything user-facing:

final page = await readRgbInIsolate(rgb, width: w, height: h);

That helper opens its own context and so reloads the models on each call. For repeated reads, keep one long-lived isolate.

Tiers

Tier Weights Use for
NainaTier.tiny (default) ~11 MB phones
NainaTier.small ~54 MB tablets, or when accuracy matters more than size

A tier picks model size, not capability.

Limitations

Unsupported scripts return confident nonsense. The character set covers Latin and CJK; there is no Devanagari. A Devanagari page comes back as plausible Latin at around 0.75 confidence, because confidence measures certainty within the model's own alphabet and cannot express "not in my alphabet". Check the expected language before calling naina rather than relying on confidence.

Handwriting is weak. PP-OCRv6 is trained on print.

Text only. Layout structure and markdown exist in the core and are exposed by naina's Python, Node and browser packages, not yet here.

arm64-v8a and x86_64 only. No 32-bit ABIs.

Full list: jvoltci.github.io/naina/doc/limits.

Status

Android is verified on a device. On an arm64 emulator: 4 weight files staged, then a real A4 page read as 33 lines at 0.992 mean confidence — the same result the native build gives. libnaina.so (1.4 MB) and libonnxruntime.so ship in the APK. Three on-device integration tests pass.

Also verified on the host: analysis clean, every C symbol resolves, and the FFI struct layout matches the C compiler's (naina_config is 48 bytes on both sides), with image wrapping, context lifecycle and ABI v1 compatibility exercised against a real libnaina.

iOS is not verified. The podspec is written but has never been built or run.

Requires Android API 28+: the core allocates with std::aligned_alloc, which Android's libc only declares from 28.

The name

naina means eyes in Hindi.

Libraries

naina
On-device OCR for Flutter.