Files
food-ai/client/lib/features/scan/recognition_service.dart
dbastrikin deceedd4a7 feat: implement Iteration 3 — product/receipt/dish recognition
Backend:
- gemini/client.go: refactor to shared callGroq transport; add
  generateVisionContent using llama-3.2-11b-vision-preview model
- gemini/recognition.go: RecognizeReceipt, RecognizeProducts,
  RecognizeDish (vision), ClassifyIngredient (text); shared parseJSON helper
- ingredient/repository.go: add FuzzyMatch (wraps Search, returns best hit)
- recognition/handler.go: POST /ai/recognize-receipt, /ai/recognize-products,
  /ai/recognize-dish; enrichItems with fuzzy match + AI classify fallback;
  parallel multi-image processing with deduplication
- server.go + main.go: wire recognition handler under /ai routes

Flutter:
- pubspec.yaml: add image_picker ^1.1.0
- AndroidManifest.xml: add CAMERA and READ_EXTERNAL_STORAGE permissions
- Info.plist: add NSCameraUsageDescription and NSPhotoLibraryUsageDescription
- recognition_service.dart: RecognitionService wrapping /ai/* endpoints;
  RecognizedItem, ReceiptResult, DishResult models
- scan_screen.dart: mode selector (receipt / products / dish / manual);
  image source picker; loading overlay; navigates to confirm or dish screen
- recognition_confirm_screen.dart: editable list of recognized items;
  inline qty/unit editing; swipe-to-delete; batch-add to pantry
- dish_result_screen.dart: dish name, KBZHU breakdown, similar dishes chips
- app_router.dart: /scan, /scan/confirm, /scan/dish routes (no bottom nav)
- products_screen.dart: FAB now shows bottom sheet with Manual / Scan options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 10:54:03 +02:00

151 lines
4.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'dart:io';
import '../../core/api/api_client.dart';
// ---------------------------------------------------------------------------
// Models
// ---------------------------------------------------------------------------
class RecognizedItem {
final String name;
double quantity;
String unit;
final String category;
final double confidence;
final String? mappingId;
final int storageDays;
RecognizedItem({
required this.name,
required this.quantity,
required this.unit,
required this.category,
required this.confidence,
this.mappingId,
required this.storageDays,
});
factory RecognizedItem.fromJson(Map<String, dynamic> json) {
return RecognizedItem(
name: json['name'] as String? ?? '',
quantity: (json['quantity'] as num?)?.toDouble() ?? 1.0,
unit: json['unit'] as String? ?? 'шт',
category: json['category'] as String? ?? 'other',
confidence: (json['confidence'] as num?)?.toDouble() ?? 0.0,
mappingId: json['mapping_id'] as String?,
storageDays: json['storage_days'] as int? ?? 7,
);
}
}
class UnrecognizedItem {
final String rawText;
final double? price;
const UnrecognizedItem({required this.rawText, this.price});
factory UnrecognizedItem.fromJson(Map<String, dynamic> json) {
return UnrecognizedItem(
rawText: json['raw_text'] as String? ?? '',
price: (json['price'] as num?)?.toDouble(),
);
}
}
class ReceiptResult {
final List<RecognizedItem> items;
final List<UnrecognizedItem> unrecognized;
const ReceiptResult({required this.items, required this.unrecognized});
}
class DishResult {
final String dishName;
final int weightGrams;
final double calories;
final double proteinG;
final double fatG;
final double carbsG;
final double confidence;
final List<String> similarDishes;
const DishResult({
required this.dishName,
required this.weightGrams,
required this.calories,
required this.proteinG,
required this.fatG,
required this.carbsG,
required this.confidence,
required this.similarDishes,
});
factory DishResult.fromJson(Map<String, dynamic> json) {
return DishResult(
dishName: json['dish_name'] as String? ?? '',
weightGrams: json['weight_grams'] as int? ?? 0,
calories: (json['calories'] as num?)?.toDouble() ?? 0,
proteinG: (json['protein_g'] as num?)?.toDouble() ?? 0,
fatG: (json['fat_g'] as num?)?.toDouble() ?? 0,
carbsG: (json['carbs_g'] as num?)?.toDouble() ?? 0,
confidence: (json['confidence'] as num?)?.toDouble() ?? 0,
similarDishes: (json['similar_dishes'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
}
}
// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------
class RecognitionService {
const RecognitionService(this._client);
final ApiClient _client;
/// Recognizes food items from a receipt photo.
Future<ReceiptResult> recognizeReceipt(File image) async {
final payload = await _buildImagePayload(image);
final data = await _client.post('/ai/recognize-receipt', data: payload);
return ReceiptResult(
items: (data['items'] as List<dynamic>? ?? [])
.map((e) => RecognizedItem.fromJson(e as Map<String, dynamic>))
.toList(),
unrecognized: (data['unrecognized'] as List<dynamic>? ?? [])
.map((e) => UnrecognizedItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
/// Recognizes food items from 13 product photos.
Future<List<RecognizedItem>> recognizeProducts(List<File> images) async {
final imageList = await Future.wait(images.map(_buildImagePayload));
final data = await _client.post(
'/ai/recognize-products',
data: {'images': imageList},
);
return (data['items'] as List<dynamic>? ?? [])
.map((e) => RecognizedItem.fromJson(e as Map<String, dynamic>))
.toList();
}
/// Recognizes a dish and estimates its nutritional content.
Future<DishResult> recognizeDish(File image) async {
final payload = await _buildImagePayload(image);
final data = await _client.post('/ai/recognize-dish', data: payload);
return DishResult.fromJson(data);
}
Future<Map<String, String>> _buildImagePayload(File image) async {
final bytes = await image.readAsBytes();
final base64Data = base64Encode(bytes);
final ext = image.path.split('.').last.toLowerCase();
final mimeType = ext == 'png' ? 'image/png' : 'image/jpeg';
return {'image_base64': base64Data, 'mime_type': mimeType};
}
}