Lower the bulk-digitization cliff with two more routes on top of the already-landed CSV import and "save and add another": - Photo-first drafts (capture now, catalogue later): burst-capture photos (camera or multi-gallery) into unnamed draft varieties, shown in a "to catalogue" tray, hidden from the main list until named. Adds Variety.isDraft (schema), addDraftVariety/watchDrafts/nameDraft, the triage sheet and the inventory banner. - On-device OCR label suggestion (Tesseract, offline, no Google): a "Suggest name from photo" button in the naming dialog behind a LabelTextExtractor interface (Tesseract on Android/iOS, no-op elsewhere). Reads the largest print via hOCR bounding boxes, drops boilerplate/low-confidence noise, preprocesses (grayscale, contrast, upscale) and sweeps rotations (0-315 deg) so tilted packets still read. Bundles tessdata_fast eng+spa; validated on-device against real packets. The photo is written to a temp file deleted immediately in a finally block (the plugin needs a path) - a bounded, documented exception to no-plaintext-at-rest. This commit also carries the co-developed schema evolution v5 to v8 that shares these files (organic flag, species viability years, crop calendar, lot provenance/abundance/preservation format, condition checks) plus their exports/migrations and i18n. Tests: CSV/draft/OCR unit + widget + migration green in isolation. Note: the full widget suite currently hangs (>10 min) - under investigation.
110 lines
3.6 KiB
Dart
110 lines
3.6 KiB
Dart
/// Locale-aware selection of Tesseract OCR language packs.
|
|
///
|
|
/// Tane is international by design: the OCR must read seed packets in whatever
|
|
/// language the user runs the app, not a fixed `eng`/`spa`. This maps the app's
|
|
/// preferred locales to the traineddata packs we actually bundle and degrades
|
|
/// gracefully — an unbundled locale simply falls back to the Latin baseline
|
|
/// instead of failing. As more packs are bundled (incl. RTL/CJK scripts) they
|
|
/// light up automatically with no code change here.
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
|
|
const _trainedDataSuffix = '.traineddata';
|
|
|
|
/// ISO 639-1 (locale) → Tesseract language code (mostly ISO 639-2/T). Covers the
|
|
/// scripts we plausibly bundle; extend as new packs are added. Kept
|
|
/// intentionally broad — international first, not one region.
|
|
const Map<String, String> isoToTesseract = {
|
|
// Latin-script European
|
|
'en': 'eng',
|
|
'es': 'spa',
|
|
'ca': 'cat',
|
|
'gl': 'glg',
|
|
'eu': 'eus',
|
|
'pt': 'por',
|
|
'fr': 'fra',
|
|
'de': 'deu',
|
|
'it': 'ita',
|
|
'nl': 'nld',
|
|
'pl': 'pol',
|
|
'cs': 'ces',
|
|
'ro': 'ron',
|
|
'sv': 'swe',
|
|
'da': 'dan',
|
|
'fi': 'fin',
|
|
'tr': 'tur',
|
|
// Cyrillic / Greek
|
|
'ru': 'rus',
|
|
'uk': 'ukr',
|
|
'el': 'ell',
|
|
// RTL scripts
|
|
'ar': 'ara',
|
|
'he': 'heb',
|
|
'fa': 'fas',
|
|
'ur': 'urd',
|
|
// CJK & other non-Latin
|
|
'zh': 'chi_sim',
|
|
'ja': 'jpn',
|
|
'ko': 'kor',
|
|
'hi': 'hin',
|
|
'th': 'tha',
|
|
};
|
|
|
|
/// Parses the bundled OCR config (`assets/tessdata_config.json`) into the set of
|
|
/// available Tesseract language codes (basenames, e.g. `eng`, `spa`). The config
|
|
/// is the single source of truth for what is shipped, so this stays in sync with
|
|
/// the assets automatically.
|
|
Set<String> parseAvailableOcrLanguages(String configJson) {
|
|
final data = jsonDecode(configJson) as Map<String, dynamic>;
|
|
final files = (data['files'] as List?)?.cast<String>() ?? const [];
|
|
return files
|
|
.map(
|
|
(f) => f.endsWith(_trainedDataSuffix)
|
|
? f.substring(0, f.length - _trainedDataSuffix.length)
|
|
: f,
|
|
)
|
|
.where((code) => code.isNotEmpty)
|
|
.toSet();
|
|
}
|
|
|
|
/// Resolves the `+`-joined Tesseract language string for [preferredLocales]
|
|
/// (ordered most-preferred first; each an ISO 639-1 code or a fuller tag like
|
|
/// `es_ES`/`zh-Hans`), limited to the [available] bundled packs.
|
|
///
|
|
/// Rules, in order:
|
|
/// 1. Each preferred locale that maps to an available pack is included, in
|
|
/// order, de-duplicated.
|
|
/// 2. The Latin [baseline] (`eng`) is appended if bundled — Latin text is
|
|
/// common on packets even in non-Latin locales, and it's a safe default.
|
|
/// 3. If nothing matched and the baseline is unavailable, fall back to every
|
|
/// available pack (sorted) so OCR still runs with whatever we shipped.
|
|
///
|
|
/// Returns an empty string only when [available] is empty.
|
|
String resolveOcrLanguages({
|
|
required List<String> preferredLocales,
|
|
required Set<String> available,
|
|
String baseline = 'eng',
|
|
}) {
|
|
final ordered = <String>[];
|
|
for (final locale in preferredLocales) {
|
|
final code = tesseractCodeForLocale(locale);
|
|
if (code != null && available.contains(code) && !ordered.contains(code)) {
|
|
ordered.add(code);
|
|
}
|
|
}
|
|
if (available.contains(baseline) && !ordered.contains(baseline)) {
|
|
ordered.add(baseline);
|
|
}
|
|
if (ordered.isEmpty) {
|
|
ordered.addAll(available.toList()..sort());
|
|
}
|
|
return ordered.join('+');
|
|
}
|
|
|
|
/// Maps a locale tag to its Tesseract code, or null if unmapped. Accepts full
|
|
/// tags (`es_ES`, `zh-Hans-CN`) and uses the primary language subtag.
|
|
String? tesseractCodeForLocale(String localeTag) {
|
|
final primary = localeTag.split(RegExp('[_-]')).first.toLowerCase();
|
|
return isoToTesseract[primary];
|
|
}
|