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>
This commit is contained in:
@@ -18,7 +18,7 @@ const (
|
|||||||
groqModel = "llama-3.3-70b-versatile"
|
groqModel = "llama-3.3-70b-versatile"
|
||||||
|
|
||||||
// groqVisionModel supports image inputs in OpenAI vision format.
|
// groqVisionModel supports image inputs in OpenAI vision format.
|
||||||
groqVisionModel = "llama-3.2-11b-vision-preview"
|
groqVisionModel = "meta-llama/llama-4-scout-17b-16e-instruct"
|
||||||
|
|
||||||
maxRetries = 3
|
maxRetries = 3
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class ApiClient {
|
|||||||
_dio = Dio(BaseOptions(
|
_dio = Dio(BaseOptions(
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
connectTimeout: const Duration(seconds: 10),
|
connectTimeout: const Duration(seconds: 10),
|
||||||
receiveTimeout: const Duration(seconds: 30),
|
receiveTimeout: const Duration(seconds: 120),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
import '../../core/api/api_client.dart';
|
import '../../core/api/api_client.dart';
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ class RecognitionService {
|
|||||||
final ApiClient _client;
|
final ApiClient _client;
|
||||||
|
|
||||||
/// Recognizes food items from a receipt photo.
|
/// Recognizes food items from a receipt photo.
|
||||||
Future<ReceiptResult> recognizeReceipt(File image) async {
|
Future<ReceiptResult> recognizeReceipt(XFile image) async {
|
||||||
final payload = await _buildImagePayload(image);
|
final payload = await _buildImagePayload(image);
|
||||||
final data = await _client.post('/ai/recognize-receipt', data: payload);
|
final data = await _client.post('/ai/recognize-receipt', data: payload);
|
||||||
return ReceiptResult(
|
return ReceiptResult(
|
||||||
@@ -122,7 +123,7 @@ class RecognitionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Recognizes food items from 1–3 product photos.
|
/// Recognizes food items from 1–3 product photos.
|
||||||
Future<List<RecognizedItem>> recognizeProducts(List<File> images) async {
|
Future<List<RecognizedItem>> recognizeProducts(List<XFile> images) async {
|
||||||
final imageList = await Future.wait(images.map(_buildImagePayload));
|
final imageList = await Future.wait(images.map(_buildImagePayload));
|
||||||
final data = await _client.post(
|
final data = await _client.post(
|
||||||
'/ai/recognize-products',
|
'/ai/recognize-products',
|
||||||
@@ -134,17 +135,18 @@ class RecognitionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Recognizes a dish and estimates its nutritional content.
|
/// Recognizes a dish and estimates its nutritional content.
|
||||||
Future<DishResult> recognizeDish(File image) async {
|
Future<DishResult> recognizeDish(XFile image) async {
|
||||||
final payload = await _buildImagePayload(image);
|
final payload = await _buildImagePayload(image);
|
||||||
final data = await _client.post('/ai/recognize-dish', data: payload);
|
final data = await _client.post('/ai/recognize-dish', data: payload);
|
||||||
return DishResult.fromJson(data);
|
return DishResult.fromJson(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, String>> _buildImagePayload(File image) async {
|
Future<Map<String, String>> _buildImagePayload(XFile image) async {
|
||||||
final bytes = await image.readAsBytes();
|
final bytes = await image.readAsBytes();
|
||||||
final base64Data = base64Encode(bytes);
|
final base64Data = base64Encode(bytes);
|
||||||
final ext = image.path.split('.').last.toLowerCase();
|
// XFile.mimeType may be null on some platforms; fall back to path extension.
|
||||||
final mimeType = ext == 'png' ? 'image/png' : 'image/jpeg';
|
final mimeType = image.mimeType ??
|
||||||
|
(image.path.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg');
|
||||||
return {'image_base64': base64Data, 'mime_type': mimeType};
|
return {'image_base64': base64Data, 'mime_type': mimeType};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:io';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -70,19 +69,28 @@ class ScanScreen extends ConsumerWidget {
|
|||||||
) async {
|
) async {
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
|
|
||||||
List<File> files = [];
|
List<XFile> files = [];
|
||||||
|
|
||||||
if (mode == _Mode.products) {
|
if (mode == _Mode.products) {
|
||||||
// Allow up to 3 images.
|
// Allow up to 3 images.
|
||||||
final picked = await picker.pickMultiImage(imageQuality: 70);
|
final picked = await picker.pickMultiImage(
|
||||||
|
imageQuality: 70,
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
);
|
||||||
if (picked.isEmpty) return;
|
if (picked.isEmpty) return;
|
||||||
files = picked.take(3).map((x) => File(x.path)).toList();
|
files = picked.take(3).toList();
|
||||||
} else {
|
} else {
|
||||||
final source = await _chooseSource(context);
|
final source = await _chooseSource(context);
|
||||||
if (source == null) return;
|
if (source == null) return;
|
||||||
final picked = await picker.pickImage(source: source, imageQuality: 70);
|
final picked = await picker.pickImage(
|
||||||
|
source: source,
|
||||||
|
imageQuality: 70,
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
);
|
||||||
if (picked == null) return;
|
if (picked == null) return;
|
||||||
files = [File(picked.path)];
|
files = [picked];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -116,7 +124,8 @@ class ScanScreen extends ConsumerWidget {
|
|||||||
context.push('/scan/dish', extra: dish);
|
context.push('/scan/dish', extra: dish);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
debugPrint('Recognition error: $e\n$s');
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context); // close loading
|
Navigator.pop(context); // close loading
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|||||||
Reference in New Issue
Block a user