From a850b3c4f32ddb599e122e8185568c681d2f6a97 Mon Sep 17 00:00:00 2001 From: rosariopf Date: Thu, 14 Aug 2025 17:33:02 +0100 Subject: [PATCH] feat: add ingredients OCR --- .../flutter/lib/recipe_generator_screen.dart | 64 ++++++++- .../flutter/pubspec.lock | 122 +++++++++++++++++- .../flutter/pubspec.yaml | 1 + 3 files changed, 184 insertions(+), 3 deletions(-) diff --git a/firebase-ai-friendly-meals/flutter/lib/recipe_generator_screen.dart b/firebase-ai-friendly-meals/flutter/lib/recipe_generator_screen.dart index 34c4d60..ed580f6 100644 --- a/firebase-ai-friendly-meals/flutter/lib/recipe_generator_screen.dart +++ b/firebase-ai-friendly-meals/flutter/lib/recipe_generator_screen.dart @@ -1,6 +1,9 @@ +import 'dart:typed_data'; + import 'package:firebase_ai/firebase_ai.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:image_picker/image_picker.dart'; class RecipeGeneratorScreen extends StatefulWidget { const RecipeGeneratorScreen({super.key}); @@ -15,6 +18,54 @@ class _RecipeGeneratorScreenState extends State { bool _showRecipeCard = false; String _generatedRecipe = ""; bool _isLoading = false; + bool _isExtractingIngredients = false; + + void _pickImageAndExtractIngredients() async { + setState(() { + _isExtractingIngredients = true; + }); + + try { + final imagePicker = ImagePicker(); + final pickedFile = await imagePicker.pickImage( + source: ImageSource.gallery, + ); + + if (pickedFile != null) { + final imageBytes = await pickedFile.readAsBytes(); + final model = FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-flash-lite', + ); + + final prompt = Content.multi([ + TextPart(""" + Please analyze this image and list all visible food ingredients. + Format the response as a comma-separated list of ingredients. + Be specific with measurements where possible, + but focus on identifying the ingredients accurately. + """), + InlineDataPart('image/jpeg', imageBytes), + ]); + + final response = await model.generateContent([prompt]); + + if (response.text != null) { + _ingredientsController.text = response.text!; + } + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error processing image: $e'), + backgroundColor: Colors.red, + ), + ); + } finally { + setState(() { + _isExtractingIngredients = false; + }); + } + } void _handleGenerateRecipe() async { if (_ingredientsController.text.isEmpty) { @@ -74,9 +125,18 @@ class _RecipeGeneratorScreenState extends State { const SizedBox(height: 16.0), TextField( controller: _ingredientsController, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Enter ingredients (comma separated)', - border: OutlineInputBorder(), + border: const OutlineInputBorder(), + suffixIcon: _isExtractingIngredients + ? const Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ) + : IconButton( + icon: const Icon(Icons.camera_alt), + onPressed: _pickImageAndExtractIngredients, + ), ), ), const SizedBox(height: 16.0), diff --git a/firebase-ai-friendly-meals/flutter/pubspec.lock b/firebase-ai-friendly-meals/flutter/pubspec.lock index ee42742..28a32d9 100644 --- a/firebase-ai-friendly-meals/flutter/pubspec.lock +++ b/firebase-ai-friendly-meals/flutter/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -81,6 +89,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711" + url: "https://pub.dev" + source: hosted + version: "0.9.4+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" firebase_ai: dependency: "direct main" description: @@ -182,6 +222,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.7+1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab" + url: "https://pub.dev" + source: hosted + version: "2.0.29" flutter_test: dependency: "direct dev" description: flutter @@ -208,6 +256,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: b08e9a04d0f8d91f4a6e767a745b9871bfbc585410205c311d0492de20a7ccd6 + url: "https://pub.dev" + source: hosted + version: "0.8.12+25" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" leak_tracker: dependency: transitive description: @@ -272,6 +384,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" path: dependency: transitive description: @@ -391,4 +511,4 @@ packages: version: "3.0.3" sdks: dart: ">=3.8.0-278.1.beta <4.0.0" - flutter: ">=3.22.0" + flutter: ">=3.27.0" diff --git a/firebase-ai-friendly-meals/flutter/pubspec.yaml b/firebase-ai-friendly-meals/flutter/pubspec.yaml index 3d2b974..2254446 100644 --- a/firebase-ai-friendly-meals/flutter/pubspec.yaml +++ b/firebase-ai-friendly-meals/flutter/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: flutter_markdown: ^0.7.7+1 firebase_core: ^4.0.0 firebase_ai: ^3.1.0 + image_picker: ^1.1.2 dev_dependencies: flutter_test: