feat(species): auto-classify variety species from its label
Infer the catalog species a free-text variety label names ("Maiz de la
abuela" -> Zea mays) and prefill the category from the species family.
- Pure, testable matcher (domain/species_autoclassify.dart): whole-word,
accent/case-insensitive, Unicode-aware (any script), longest-name-wins,
ambiguous names left unclassified, light plural fold.
- Quick-add and draft naming auto-link the species when the field is empty
(non-destructive; an explicit category is kept).
- Edit sheet offers a one-tap suggestion from the typed name.
Tests: matcher unit, repository integration, SpeciesRepository.classifyLabel,
and an edit-sheet widget test.
This commit is contained in:
parent
0e6ef13d00
commit
b840b83c42
15 changed files with 510 additions and 3 deletions
|
|
@ -224,4 +224,17 @@ void main() {
|
|||
final results = await repo.search('Genus', limit: 5);
|
||||
expect(results.length, 5);
|
||||
});
|
||||
|
||||
test('classifyLabel infers the species named in a free-text label', () async {
|
||||
await repo.seedBundled(_seeds);
|
||||
|
||||
final match = await repo.classifyLabel('Tomate cherry', languageCode: 'es');
|
||||
expect(match, isNotNull);
|
||||
expect(match!.scientificName, 'Solanum lycopersicum');
|
||||
expect(match.family, 'Solanaceae');
|
||||
expect(match.commonName, 'Tomate'); // best name for the locale
|
||||
|
||||
// No species named → no suggestion.
|
||||
expect(await repo.classifyLabel('Girasol gigante'), isNull);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
107
apps/app_seeds/test/data/variety_autoclassify_test.dart
Normal file
107
apps/app_seeds/test/data/variety_autoclassify_test.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/data/species_repository.dart';
|
||||
import 'package:tane/data/variety_repository.dart';
|
||||
import 'package:tane/db/database.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
/// A starter catalog, including a name ("Calabaza") shared across two species
|
||||
/// so the ambiguous case can be exercised end-to-end.
|
||||
const _seeds = [
|
||||
SpeciesSeed(
|
||||
scientificName: 'Zea mays',
|
||||
family: 'Poaceae',
|
||||
commonNames: {
|
||||
'es': ['Maíz'],
|
||||
'en': ['Maize', 'Corn'],
|
||||
},
|
||||
),
|
||||
SpeciesSeed(
|
||||
scientificName: 'Solanum lycopersicum',
|
||||
family: 'Solanaceae',
|
||||
commonNames: {
|
||||
'es': ['Tomate'],
|
||||
'en': ['Tomato'],
|
||||
},
|
||||
),
|
||||
SpeciesSeed(
|
||||
scientificName: 'Cucurbita pepo',
|
||||
family: 'Cucurbitaceae',
|
||||
commonNames: {
|
||||
'es': ['Calabaza', 'Calabacín'],
|
||||
},
|
||||
),
|
||||
SpeciesSeed(
|
||||
scientificName: 'Cucurbita maxima',
|
||||
family: 'Cucurbitaceae',
|
||||
commonNames: {
|
||||
'es': ['Calabaza'],
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
void main() {
|
||||
late AppDatabase db;
|
||||
late VarietyRepository repo;
|
||||
|
||||
setUp(() async {
|
||||
db = newTestDatabase();
|
||||
repo = newTestRepository(db);
|
||||
await SpeciesRepository(db, idGen: IdGen()).seedBundled(_seeds);
|
||||
});
|
||||
tearDown(() => db.close());
|
||||
|
||||
Future<Variety> only() async => db.select(db.varieties).getSingle();
|
||||
|
||||
test('addQuickVariety auto-links the species named in the label', () async {
|
||||
await repo.addQuickVariety(label: 'Maíz de la abuela');
|
||||
final v = await only();
|
||||
final species = await (db.select(
|
||||
db.species,
|
||||
)..where((s) => s.id.equals(v.speciesId!))).getSingle();
|
||||
expect(species.scientificName, 'Zea mays');
|
||||
// Category is prefilled from the species family.
|
||||
expect(v.category, 'Poaceae');
|
||||
});
|
||||
|
||||
test('auto-classification is accent-insensitive', () async {
|
||||
await repo.addQuickVariety(label: 'Maiz dulce'); // typed without the accent
|
||||
final v = await only();
|
||||
expect(v.speciesId, isNotNull);
|
||||
expect(v.category, 'Poaceae');
|
||||
});
|
||||
|
||||
test('an explicit category is kept, species still links', () async {
|
||||
await repo.addQuickVariety(label: 'Tomate cherry', category: 'Mi huerto');
|
||||
final v = await only();
|
||||
expect(v.speciesId, isNotNull);
|
||||
expect(v.category, 'Mi huerto'); // not overwritten by the family
|
||||
});
|
||||
|
||||
test('an unknown label leaves the variety unclassified', () async {
|
||||
await repo.addQuickVariety(label: 'Girasol gigante');
|
||||
final v = await only();
|
||||
expect(v.speciesId, isNull);
|
||||
expect(v.category, isNull);
|
||||
});
|
||||
|
||||
test('an ambiguous name is left for the grower to disambiguate', () async {
|
||||
await repo.addQuickVariety(label: 'Calabaza de invierno');
|
||||
final v = await only();
|
||||
expect(v.speciesId, isNull);
|
||||
expect(v.category, isNull);
|
||||
});
|
||||
|
||||
test('nameDraft auto-classifies when the draft has no species yet', () async {
|
||||
final id = await repo.addDraftVariety(Uint8List.fromList([1, 2, 3]));
|
||||
await repo.nameDraft(id, 'Tomate rosa');
|
||||
final v = await only();
|
||||
expect(v.isDraft, isFalse);
|
||||
expect(v.label, 'Tomate rosa');
|
||||
expect(v.speciesId, isNotNull);
|
||||
expect(v.category, 'Solanaceae');
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue