Files
food-ai/client/lib/features/scan/recognition_service.dart
dbastrikin 612a0eda60 fix: repair recognition — migrate vision model and fix XFile handling
- Replace decommissioned llama-3.2-11b-vision-preview with
  meta-llama/llama-4-scout-17b-16e-instruct (Groq deprecation)
- Use XFile.readAsBytes() instead of File(path).readAsBytes() so
  Android content URIs (from gallery picks) are read correctly
- Add maxWidth/maxHeight constraints to image picker calls to reduce
  payload size
- Increase receiveTimeout from 30s to 120s to accommodate slow vision AI
- Log recognition errors via debugPrint instead of swallowing them

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 11:41:33 +02:00

153 lines
4.7 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 'package:image_picker/image_picker.dart';
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(XFile 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<XFile> 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(XFile 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(XFile image) async {
final bytes = await image.readAsBytes();
final base64Data = base64Encode(bytes);
// XFile.mimeType may be null on some platforms; fall back to path extension.
final mimeType = image.mimeType ??
(image.path.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg');
return {'image_base64': base64Data, 'mime_type': mimeType};
}
}