Soyons honnêtes, quelle autre utilité de l’IA générative que de travailler avec des chats ? Si vous avez lu mon article précédent sur le lancement de Gemini AI par Google, vous avez peut-être vu mes invites de test lui demandant d'identifier le type de chat montré sur une image.
J'ai décidé d'en faire une véritable application Web comme exemple réel de l'API en action. Voici ce que j'ai trouvé.
Pour le front-end, j'ai décidé d'utiliser une fonctionnalité native de la plateforme Web pour accéder à la caméra de l'utilisateur via un simple champ de formulaire HTML. En utilisant capture="camera"
sur une balise input
, vous accédez directement à la caméra de l'appareil.
Il existe des moyens plus avancés de procéder, mais pour une utilisation simple et rapide, cela fonctionne très bien. Mieux encore, sur un ordinateur de bureau, il fait simplement office de sélecteur de fichiers.
Ma pensée était de fournir un moyen d'obtenir une image (via un appareil photo ou une sélection de fichiers), d'afficher l'image et de l'envoyer au back-end. Bien que JS incroyablement simple et vanille aurait été parfait, j'ai continué et utilisé Alpine.js pour l'interactivité.
Premièrement, le HTML doit fournir l’interface utilisateur de l’image, un emplacement pour afficher l’image et un autre emplacement pour afficher le résultat.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="app.css"> <title></title> </head> <body> <h2>🐈 Detector</h2> <div x-data="catDetector"> <input type="file" capture="camera" accept="image/*" @change="gotPic" :disabled="working"> <template x-if="imageSrc"> <p> <img :src="imageSrc"> </p> </template> <div x-html="status"></div> </div> <script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script> <script src="app.js"></script> </body> </html>
Passons maintenant à JavaScript. Je vais commencer par partager la version initiale car c'est plus simple que d'expliquer comment elle a échoué. Tout ce que j'avais à faire, au départ, c'était de remarquer quand un fichier était sélectionné ou une photo prise, et de la restituer dans le DOM.
(J'ai utilisé un peu de CSS non partagé ici pour contrôler la taille visible. Plus d'informations à ce sujet dans un instant.) J'avais également besoin d'envoyer une version base64 du fichier au code côté serveur. Voici la version initiale :
//const IMG_FUNC = 'http://localhost:8787/'; const IMG_FUNC = 'https://catdetector.raymondcamden.workers.dev'; document.addEventListener('alpine:init', () => { Alpine.data('catDetector', () => ({ imageSrc:null, working:false, status:'', async init() { console.log('init'); }, async gotPic(e) { let file = e.target.files[0]; if(!file) return; let reader = new FileReader(); reader.readAsDataURL(file); reader.onload = async e => { this.imageSrc = e.target.result; this.working = true; this.status = '<i>Sending image data to Google Gemini...</i>'; let body = { imgdata:this.imageSrc } let resp = await fetch(IMG_FUNC, { method:'POST', body: JSON.stringify(body) }); let result = await resp.json(); this.working = false; this.status = result.text; } } })) });
La méthode gotPic
est déclenchée chaque fois que le champ input
déclenche un événement onchange
. Je prends le fichier/l'image utilisé, le lis comme une URL de données (base64), puis l'attribue à l'image dans le DOM et l'envoie au serveur. Agréable et simple, non ?
Eh bien, tout a bien fonctionné sur le bureau, mais lorsque je suis passé à mon appareil photo, le Samsung S22 Ultra Magnus Extreme 200 Camera Lens Edition (ce n'est pas le vrai nom), j'ai rencontré des problèmes où l'API de Google se plaignait que j'envoyais trop de données. Je me suis alors rappelé que mon appareil photo prenait des photos très détaillées et que je devais redimensionner l'image avant de l'envoyer.
J'étais déjà en train de redimensionner en CSS, mais évidemment, ce n'est pas la même chose qu'un véritable redimensionnement. J'ai trouvé cet excellent article sur le site d'ImageKit : Comment redimensionner des images en Javascript ? Dans cet article, ils décrivent l'utilisation d'un élément canvas
HTML pour effectuer le redimensionnement.
Je n'avais pas utilisé Canvas depuis probablement près d'une décennie, mais j'ai réussi à réutiliser assez bien leur code dans mon front-end :
//const IMG_FUNC = 'http://localhost:8787/'; const IMG_FUNC = 'https://catdetector.raymondcamden.workers.dev'; // Resize logic: https://imagekit.io/blog/how-to-resize-image-in-javascript/ const MAX_WIDTH = 400; const MAX_HEIGHT = 400; document.addEventListener('alpine:init', () => { Alpine.data('catDetector', () => ({ imageSrc:null, working:false, status:'', async init() { console.log('init'); }, async gotPic(e) { let file = e.target.files[0]; if(!file) return; let reader = new FileReader(); reader.readAsDataURL(file); reader.onload = async e => { let img = document.createElement('img'); img.onload = async e => { let width = img.width; let height = img.height; if(width > height) { if(width > MAX_WIDTH) { height = height * (MAX_WIDTH / width); width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width = width * (MAX_HEIGHT / height); height = MAX_HEIGHT; } } let canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; let ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); this.imageSrc = canvas.toDataURL(file.type); this.working = true; this.status = '<i>Sending image data to Google Gemini...</i>'; let body = { imgdata:this.imageSrc } let resp = await fetch(IMG_FUNC, { method:'POST', body: JSON.stringify(body) }); let result = await resp.json(); this.working = false; this.status = result.text; }; img.src = e.target.result; } } })) });
Vous remarquerez que le code utilise une dimension maximale pour la largeur et la hauteur et gère correctement le redimensionnement tout en conservant le même rapport hauteur/largeur. Encore une fois, je ne peux m'attribuer le mérite de rien de tout cela, merci à Manu Chaudhary pour son article de blog.
Le résultat net de ce changement est que j'envoie désormais une image beaucoup plus petite au service back-end, et il est enfin temps d'y jeter un œil.
Pour mon back-end, j'ai décidé d'utiliser à nouveau Cloudflare Workers . J'étais un peu hésitant car il y avait déjà eu quelques problèmes avec les packages NPM et mes démos, mais il n'y a eu aucun problème cette fois. Si vous vous souvenez de mon dernier article , AI Studio de Google vous permet de générer facilement des exemples de code à partir de vos invites, donc tout ce que j'avais à faire était de l'incorporer dans Cloudflare Worker.
Voici l'intégralité du code :
const { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold, } = require("@google/generative-ai"); const MODEL_NAME = "gemini-pro-vision"; export default { async fetch(request, env, ctx) { const API_KEY = env.GEMINI_KEY; console.log('begin serverless logic'); const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Access-Control-Max-Age": "86400", }; let { imgdata } = await request.json(); imgdata = imgdata.replace(/data:.*?;base64,/, ''); const genAI = new GoogleGenerativeAI(API_KEY); const model = genAI.getGenerativeModel({ model: MODEL_NAME }); const generationConfig = { temperature: 0.4, topK: 32, topP: 1, maxOutputTokens: 4096, }; const safetySettings = [ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, ]; const parts = [ {text: "Look at this picture and if you see a cat, return the breed of the cat."}, { inlineData: { mimeType: "image/jpeg", data: imgdata } } ]; console.log('calling google'); const result = await model.generateContent({ contents: [{ role: "user", parts }], generationConfig, safetySettings, }); const response = result.response; let finalResult = { text: response.text() }; return new Response(JSON.stringify(finalResult), { headers: {...corsHeaders}}); }, };
La majorité du code est un passe-partout issu de l'exportation d'AI Studio, sauf que maintenant, j'obtiens mes données d'image à partir des informations POSTées au travailleur. Je veux particulièrement appeler l'invite :
Look at this picture and if you see a cat, return the breed of the cat.
Au départ, j'ai essayé d'obtenir un résultat en JSON et de lui faire renvoyer une chaîne vide si l'image n'était pas un chat. Mais ensuite j'ai remarqué quelque chose de sympa : Gemini a fait un très bon travail en traitant des images qui ne représentaient pas des chats. Genre... incroyablement bon.
En fait, j'ai vraiment apprécié que l'application ne dise pas simplement "Pas un chat", mais explique à la place ce que c'était. Évidemment, il y a de la place pour les deux styles pour une application comme celle-ci, mais j'ai conservé les réponses verbeuses et utiles de Google.
Maintenant, passons à la partie amusante... les résultats.
Commençons par quelques photos de chats.
Le premier est Pig, mon chat préféré qui n'est pas gros et ne ressemble pas du tout à Jabba la Cabane :
Ensuite, une photo de Luna. Dans ce cas, la race est incorrecte – mais au moins proche.
Maintenant, lançons une courbe à Google :
Cela m'a vraiment surpris. La description est précise à 100%, et honnêtement, si j'avais vu cette photo et ne le savais pas, je l'aurais reconnue comme une sculpture de chat, pas comme un arrosoir. Je veux dire, je suppose que c'est assez évident, mais honnêtement, je ne pense pas que je l'aurais remarqué moi-même.
Maintenant, devenons complètement fous :
Oui, c'est vrai Google. Qu’en est-il des images de chats générées par l’IA ?
Je pense qu'il a plutôt bien géré cela.
Suivant...
Oui, c'est bien Bigfoot. Pensez-y, tous ces « chercheurs » de Bigfoot pourraient prendre leur retraite et simplement connecter leurs caméras de trail à l’IA !
Je dois dire : je suis impressionné que Google ait non seulement reconnu la franchise mais aussi le personnage lui-même, mais pour être honnête, Skeletor a un look assez distinct.
Et enfin, puisque j'ai (injustement bien sûr) comparé mon chat à Jabba, voyons comment il est géré :
Oh, je sais, j'ai dit que j'avais fini, mais bien sûr, je devais tester un chien :
Bon travail, Gémeaux. Malheureusement, je n'hébergerai pas cette démo en direct (l'URL « live » de Cloudflare plus tôt dans le code ne fonctionnera pas), mais je peux partager le code.