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:
vjrj 2026-07-09 21:23:46 +02:00
parent 12a2ee2d64
commit 6809dc6143
89 changed files with 17141 additions and 228 deletions

View file

@ -1,76 +1,90 @@
{
"version": 1,
"note": "Curated starter set of Iberian horticultural species. Family and common names are stable; wikidata_qid/gbif_key are intentionally omitted here and enriched later from the varilla (Wikidata CC0 + GBIF) work.",
"version": 2,
"note": "Curated starter set of common horticultural species — a seed, not a region-locked list. `common` is a locale-keyed map extensible to ANY language (add es/en/fr/de/ar/zh/…); the app resolves names by the user's locale and falls back gracefully, so this must never assume es/en only. Names are enriched later per-locale from the varilla (Wikidata CC0 + GBIF) work, which carries vernacular names in every language. viability_years is typical seed longevity under normal home storage (public-domain agricultural-extension figures); a conservative single value, used to warn about aging lots.",
"species": [
{
"scientific_name": "Solanum lycopersicum",
"family": "Solanaceae",
"common": { "es": ["Tomate"], "en": ["Tomato"] }
"common": { "es": ["Tomate"], "en": ["Tomato"] },
"viability_years": 5
},
{
"scientific_name": "Solanum melongena",
"family": "Solanaceae",
"common": { "es": ["Berenjena"], "en": ["Aubergine", "Eggplant"] }
"common": { "es": ["Berenjena"], "en": ["Aubergine", "Eggplant"] },
"viability_years": 5
},
{
"scientific_name": "Capsicum annuum",
"family": "Solanaceae",
"common": { "es": ["Pimiento", "Guindilla"], "en": ["Pepper", "Chilli"] }
"common": { "es": ["Pimiento", "Guindilla"], "en": ["Pepper", "Chilli"] },
"viability_years": 3
},
{
"scientific_name": "Phaseolus vulgaris",
"family": "Fabaceae",
"common": { "es": ["Judía", "Alubia", "Habichuela"], "en": ["Common bean"] }
"common": { "es": ["Judía", "Alubia", "Habichuela"], "en": ["Common bean"] },
"viability_years": 3
},
{
"scientific_name": "Vicia faba",
"family": "Fabaceae",
"common": { "es": ["Haba"], "en": ["Broad bean", "Fava bean"] }
"common": { "es": ["Haba"], "en": ["Broad bean", "Fava bean"] },
"viability_years": 4
},
{
"scientific_name": "Pisum sativum",
"family": "Fabaceae",
"common": { "es": ["Guisante"], "en": ["Pea"] }
"common": { "es": ["Guisante"], "en": ["Pea"] },
"viability_years": 3
},
{
"scientific_name": "Zea mays",
"family": "Poaceae",
"common": { "es": ["Maíz"], "en": ["Maize", "Corn"] }
"common": { "es": ["Maíz"], "en": ["Maize", "Corn"] },
"viability_years": 2
},
{
"scientific_name": "Cucurbita pepo",
"family": "Cucurbitaceae",
"common": { "es": ["Calabacín", "Calabaza"], "en": ["Courgette", "Zucchini", "Squash"] }
"common": { "es": ["Calabacín", "Calabaza"], "en": ["Courgette", "Zucchini", "Squash"] },
"viability_years": 5
},
{
"scientific_name": "Cucumis sativus",
"family": "Cucurbitaceae",
"common": { "es": ["Pepino"], "en": ["Cucumber"] }
"common": { "es": ["Pepino"], "en": ["Cucumber"] },
"viability_years": 5
},
{
"scientific_name": "Lactuca sativa",
"family": "Asteraceae",
"common": { "es": ["Lechuga"], "en": ["Lettuce"] }
"common": { "es": ["Lechuga"], "en": ["Lettuce"] },
"viability_years": 3
},
{
"scientific_name": "Beta vulgaris",
"family": "Amaranthaceae",
"common": { "es": ["Acelga", "Remolacha"], "en": ["Chard", "Beetroot"] }
"common": { "es": ["Acelga", "Remolacha"], "en": ["Chard", "Beetroot"] },
"viability_years": 4
},
{
"scientific_name": "Allium cepa",
"family": "Amaryllidaceae",
"common": { "es": ["Cebolla"], "en": ["Onion"] }
"common": { "es": ["Cebolla"], "en": ["Onion"] },
"viability_years": 1
},
{
"scientific_name": "Allium sativum",
"family": "Amaryllidaceae",
"common": { "es": ["Ajo"], "en": ["Garlic"] }
"common": { "es": ["Ajo"], "en": ["Garlic"] },
"viability_years": 1
},
{
"scientific_name": "Daucus carota",
"family": "Apiaceae",
"common": { "es": ["Zanahoria"], "en": ["Carrot"] }
"common": { "es": ["Zanahoria"], "en": ["Carrot"] },
"viability_years": 3
}
]
}

View file

@ -0,0 +1,18 @@
# OCR validation fixtures
Real packet photos used by `integration_test/ocr_label_test.dart` to check that
on-device Tesseract reads the variety name correctly.
Drop your photos here with these names:
- `aubergine.jpg` — the AUBERGINE BLACK BEAUTY packet
- `cucumber.jpg` — the CUCUMBER SUPERMARKETER packet
Then run on a connected Android device/emulator (native OCR does not run on the
host test VM):
flutter test integration_test/ocr_label_test.dart -d <device>
Each test prints `OCR[<file>] => "<suggestion>"` so you can see exactly what
Tesseract read. These fixtures are only for validation — remove the folder from
`pubspec.yaml` assets before shipping if you don't want them in the bundle.

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,6 @@
{
"files": [
"eng.traineddata",
"spa.traineddata"
]
}