Pour la réalisation de cette application, nous utiliserons google_generative_ai, le package officiel de Gemini pour flutter.
Aperçu de l’application
Conception d’une application de jeu en Flutter où le joueur doit repérer un objet dans son environnement (Maison/Travail). Une fois que l’objet est capturé, l’application procède à une vérification pour confirmer si l’objet correspond bien à celui recherché.
Structure du projet
Rien de compliqué pour la structure. J’ai décidé d’utiliser Riverpod.
Intégration
Voici le contenu du main.dart, je charge les variables d’environnements à partir du package flutter_dotenv
Nous commençons à déclarer une énumération dans models/environment.dart pour gérer le type d’environnement.
enum GameEnvironment { home, work }
Ensuite, nous passons à la création de la classe GeminiApi pour la communication avec Gemini. Principalement, il nous faut 2 fonctions : la première pour générer les objets et la seconde pour la validation des images.
Le constructeur de notre classe prend en paramètre le modèle de Gemini à utiliser : gemini-pro pour la génération des objets et gemini-pro-vision pour la validation des images.
import 'dart:typed_data';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
class GeminiApi {
GeminiApi({required String type})
: _model = GenerativeModel(
model: type,
apiKey: dotenv.get("API_KEY"),
);
final GenerativeModel _model;
Future<String?> generateObjects(String location) async {
final prompt = '''
Vous êtes un jeu où les objets sont trouvés en les prenant en photo. Créez une liste de 5 objets qui pourraient être trouvés $location.
Les objets doivent être faciles à trouver, mais certains peuvent être un peu plus difficiles à trouver. Le nom de l'objet doit être concis.
Toutes les lettres doivent être majuscules. N'incluez pas d'articles (un, une, le ou la).
Fournissez votre réponse sous le format suivant : {"items" : ["", "", ...]}.
Ne pas renvoyez le résultat sous forme de Markdown.
''';
final response = await _model.generateContent([
Content.text(prompt),
]);
return response.text;
}
Future<String?> validateImage(String item, Uint8List image) async {
final prompt = '''
Vous êtes un jeu où l'on trouve des objets en les prenant en photo.
On vous a donné l'objet "$item" et une photo de l'objet.
Déterminez si la photo est une photo valide de l'objet et donnez une note entre 0 à 100 du matching.
Fournissez votre réponse sous le format suivant : {"valid" : true/false,"score" : 0/100}.
Ne pas renvoyez le résultat sous forme de Markdown.
''';
final response = await _model.generateContent([
Content.multi([TextPart(prompt), DataPart('image/jpeg', image)]),
]);
return response.text;
}
}
Après la création de classe, il nous faut un repository pour la manipulation des données retournées par Gemini.
import 'dart:convert';
import 'dart:typed_data';
import 'package:gemini_game/api/gemini_api.dart';
import 'package:gemini_game/exceptions/gemini_exception.dart';
import 'package:gemini_game/models/environment.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
class GeminiRepository {
const GeminiRepository({
required GeminiApi client,
}) : _client = client;
final GeminiApi _client;
Future<List<String>> loadObjects(GameEnvironment gameEnvironment) async {
final location = switch (gameEnvironment) {
GameEnvironment.home => 'à la maison',
GameEnvironment.work => 'au travail',
};
try {
final response = await _client.generateObjects(location);
if (response == null) {
throw const GeminiException('Response is empty');
}
if (jsonDecode(response) case {'items': List<dynamic> items}) {
return List<String>.from(items);
}
throw const GeminiException('Invalid JSON schema');
} on GenerativeAIException {
throw const GeminiException(
'Problem with the Generative AI service',
);
} catch (e) {
if (e is GeminiException) rethrow;
throw const GeminiException();
}
}
Future<(bool, int)> validateImage(String item, Uint8List image) async {
try {
final response = await _client.validateImage(item, image);
if (response == null) {
throw const GeminiException('Response is empty');
}
if (jsonDecode(response) case {'valid': bool valid, 'score': int score}) {
return (valid, score);
}
throw const GeminiException('Invalid JSON schema');
} on GenerativeAIException {
throw const GeminiException(
'Problem with the Generative AI service',
);
} catch (e) {
if (e is GeminiException) rethrow;
throw const GeminiException();
}
}
}
Nous aurons 3 interfaces principales : Le choix de l’environnement, l’interface de jeu et l’interface d’après jeu. Il faudra une page de transition pour le chargement.
Pour la gestion d’état, mettons en place le riverpod. Il nous faut le modèle et ensuite notre notifier.
class Game {
final int index;
final int score;
final bool isLoading;
final int noteToAdd;
final bool isCorrect;
final bool showAnswer;
Game(
{required this.index,
required this.score,
required this.isLoading,
required this.noteToAdd,
required this.isCorrect,
required this.showAnswer});
Game copyWith(
{int? index,
int? score,
String? itemToFind,
bool? isLoading,
int? noteToAdd,
bool? isCorrect,
bool? showAnswer}) {
return Game(
index: index ?? this.index,
score: score ?? this.score,
isLoading: isLoading ?? this.isLoading,
noteToAdd: noteToAdd ?? this.noteToAdd,
isCorrect: isCorrect ?? this.isCorrect,
showAnswer: showAnswer ?? this.showAnswer);
}
}
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gemini_game/models/game.dart';
final gameProvider = StateNotifierProvider<GameNotifier, Game>((ref) {
return GameNotifier();
});
class GameNotifier extends StateNotifier<Game> {
GameNotifier()
: super(Game(
index: 1,
score: 0,
isLoading: false,
noteToAdd: 0,
isCorrect: false,
showAnswer: false,
));
void startLoading() {
state = state.copyWith(isLoading: true);
}
void stopLoading() {
state = state.copyWith(isLoading: false);
}
void setShowAnswer(bool value) {
state = state.copyWith(showAnswer: value);
}
void setCorrect(bool value) {
state = state.copyWith(isCorrect: value);
}
void setNoteToAdd(int value) {
state = state.copyWith(noteToAdd: value);
}
void incrementIndex() {
state = state.copyWith(
index: state.index + 1,
showAnswer: false,
noteToAdd: 0,
);
}
void updateScore() {
state = state.copyWith(score: state.score + state.noteToAdd);
}
}
Pour la prise de l’image, nous avons une classe utilitaire PhotoPicker basé sur image_picker.
import 'dart:typed_data';
import 'package:gemini_game/exceptions/photo_exception.dart';
import 'package:image_picker/image_picker.dart';
class PhotoPicker {
const PhotoPicker({
required this.imagePicker,
});
final ImagePicker imagePicker;
Future<Uint8List> takePhoto() async {
try {
final photo = await imagePicker.pickImage(source: ImageSource.camera);
if (photo == null) throw const PhotoPickerException();
final bytes = await photo.readAsBytes();
return bytes;
} on Exception {
throw const PhotoPickerException();
}
}
}
Le code complet est disponible sur Github.
Conclusion
Voilà le résultat final, une application basée sur l’IA. La structure du code ressemble à celle de n’importe quelle autre API publique, mais le résultat varie à chaque utilisation.