feat(inventory): photo-first drafts + on-device OCR (digitization R2+R4)
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.
This commit is contained in:
parent
12a2ee2d64
commit
6809dc6143
89 changed files with 17141 additions and 228 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
|
@ -10,11 +12,16 @@ import '../data/species_repository.dart';
|
|||
import '../data/variety_repository.dart';
|
||||
import '../db/database.dart';
|
||||
import '../db/encrypted_executor.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../security/secret_store.dart';
|
||||
import '../security/secure_key_store.dart';
|
||||
import '../services/export_import_service.dart';
|
||||
import '../services/file_picker_file_service.dart';
|
||||
import '../services/file_service.dart';
|
||||
import '../services/ocr/label_text_extractor.dart';
|
||||
import '../services/ocr/ocr_language.dart';
|
||||
import '../services/ocr/tesseract_label_extractor.dart';
|
||||
import '../services/onboarding_store.dart';
|
||||
|
||||
/// The app's service locator. Kept to the composition root — widgets get their
|
||||
/// repositories from here (or via BlocProvider), never by reaching into it deep
|
||||
|
|
@ -24,7 +31,8 @@ final GetIt getIt = GetIt.instance;
|
|||
/// Wires the encrypted DB, keystore and repositories. Call once from `main`
|
||||
/// before `runApp`; the DB key must exist before the DB opens.
|
||||
Future<void> configureDependencies() async {
|
||||
final keyStore = SecureKeyStore(store: FlutterSecretStore());
|
||||
final secretStore = FlutterSecretStore();
|
||||
final keyStore = SecureKeyStore(store: secretStore);
|
||||
final dbKeyHex = await keyStore.databaseKeyHex();
|
||||
final rootSeedHex = await keyStore.rootSeedHex();
|
||||
|
||||
|
|
@ -48,12 +56,23 @@ Future<void> configureDependencies() async {
|
|||
|
||||
const fileService = FilePickerFileService();
|
||||
|
||||
// OCR label suggestions only where a native Tesseract engine exists; every
|
||||
// other platform (desktop, web) degrades to the no-op and hides the button.
|
||||
// The language pack(s) follow the user's locale(s) — never a fixed region —
|
||||
// limited to what we actually bundle, always unioned with the Latin baseline.
|
||||
final LabelTextExtractor labelExtractor =
|
||||
(Platform.isAndroid || Platform.isIOS)
|
||||
? TesseractLabelExtractor(language: await _resolveOcrLanguage())
|
||||
: const NoOpLabelTextExtractor();
|
||||
|
||||
getIt
|
||||
..registerSingleton<SecureKeyStore>(keyStore)
|
||||
..registerSingleton<AppDatabase>(database)
|
||||
..registerSingleton<SpeciesRepository>(speciesRepository)
|
||||
..registerSingleton<VarietyRepository>(varietyRepository)
|
||||
..registerSingleton<FileService>(fileService)
|
||||
..registerSingleton<LabelTextExtractor>(labelExtractor)
|
||||
..registerSingleton<OnboardingStore>(OnboardingStore(secretStore))
|
||||
..registerSingleton<ExportImportService>(
|
||||
ExportImportService(repository: varietyRepository, files: fileService),
|
||||
);
|
||||
|
|
@ -63,3 +82,18 @@ Future<File> _databaseFile() async {
|
|||
final dir = await getApplicationDocumentsDirectory();
|
||||
return File(p.join(dir.path, 'tane_inventory.sqlite'));
|
||||
}
|
||||
|
||||
/// Picks the Tesseract language pack(s) for the current locale(s), limited to
|
||||
/// the bundled packs listed in `tessdata_config.json`. Prefers the app's chosen
|
||||
/// language, then the device's ordered locales.
|
||||
Future<String> _resolveOcrLanguage() async {
|
||||
final available = parseAvailableOcrLanguages(
|
||||
await rootBundle.loadString('assets/tessdata_config.json'),
|
||||
);
|
||||
final preferred = <String>[
|
||||
LocaleSettings.currentLocale.languageCode,
|
||||
for (final l in WidgetsBinding.instance.platformDispatcher.locales)
|
||||
l.languageCode,
|
||||
];
|
||||
return resolveOcrLanguages(preferredLocales: preferred, available: available);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue