Seamos honestos, ¿qué otro uso tiene la IA generativa además de trabajar con gatos? Si leíste mi publicación anterior sobre el lanzamiento de Gemini AI de Google, es posible que hayas visto mis indicaciones de prueba pidiéndole que identifique el tipo de gato que se muestra en una imagen.
Decidí convertir esto en una aplicación web adecuada como un ejemplo real de la API en acción. Esto es lo que se me ocurrió.
Para la interfaz, decidí utilizar una función de plataforma web nativa para acceder a la cámara del usuario a través de un simple campo de formulario HTML. Al usar capture="camera"
en una etiqueta input
, obtienes acceso directo a la cámara del dispositivo.
Hay formas más avanzadas de hacer esto, pero si es rápido y sencillo, funciona bien. Aún mejor, en un escritorio, simplemente actúa como un selector de archivos.
Mi idea era proporcionar una forma de obtener una imagen (ya sea a través de la cámara o de la selección de archivos), mostrar la imagen y enviarla al final. Si bien JS increíblemente simple y básico hubiera estado bien, seguí adelante y usé Alpine.js para la interactividad.
Primero, el HTML debe proporcionar la interfaz de usuario para la imagen, un lugar para mostrar la imagen y otro lugar para mostrar el resultado.
<!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>
Ahora, pasemos a JavaScript. Voy a empezar compartiendo la versión inicial ya que es más sencillo que explicar cómo falló. Inicialmente, todo lo que tenía que hacer era notar cuándo se seleccionaba un archivo o se tomaba una fotografía y renderizarlo en el DOM.
(Utilicé un poco de CSS que no se comparte aquí para mantener el tamaño visible bajo control. Más sobre eso en un momento). También necesitaba enviar una versión base64 del archivo al código del lado del servidor. Aquí está la versión inicial:
//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; } } })) });
El método gotPic
se activa cada vez que el campo input
activa un evento onchange
. Tomo el archivo/imagen utilizado, lo leo como una URL de datos (base64), luego lo asigno a la imagen en el DOM y lo envío al servidor. Bonito y sencillo, ¿verdad?
Bueno, todo funcionó bien en el escritorio, pero cuando cambié a mi cámara, la Samsung S22 Ultra Magnus Extreme 200 Camera Lens Edition (no es el nombre real), me encontré con problemas en los que la API de Google se quejaba de que estaba enviando demasiados datos. Entonces recordé que mi cámara toma fotografías muy detalladas y necesitaba cambiar el tamaño de la imagen antes de enviarla.
Ya estaba cambiando el tamaño en CSS, pero obviamente, eso no es lo mismo que realmente cambiar el tamaño. Encontré este excelente artículo en el sitio de ImageKit: ¿Cómo cambiar el tamaño de las imágenes en Javascript? En este artículo, describen el uso de un elemento canvas
HTML para realizar el cambio de tamaño.
Probablemente no había usado Canvas en cerca de una década, pero pude reutilizar su código en mi interfaz bastante bien:
//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; } } })) });
Notarás que el código usa una dimensión máxima tanto para el ancho como para el alto y maneja correctamente el cambio de tamaño manteniendo la misma relación de aspecto. Nuevamente, no puedo atribuirme el mérito de nada de eso, gracias a Manu Chaudhary por su publicación en el blog.
El resultado neto de este cambio es que ahora envío una imagen mucho más pequeña al servicio back-end y finalmente es hora de echarle un vistazo.
Para mi backend, decidí usar Cloudflare Workers nuevamente. Estaba un poco indeciso ya que había tenido algunos problemas con los paquetes NPM y mis demostraciones antes, pero esta vez no tuvo ningún problema. Si recuerdas mi última publicación , AI Studio de Google te permite generar fácilmente código de muestra a partir de tus indicaciones, por lo que todo lo que tuve que hacer fue incorporarlo en Cloudflare Worker.
Aquí está el código completo:
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 mayor parte del código es un modelo repetitivo de la exportación de AI Studio, excepto que ahora obtengo los datos de mi imagen de la información PUBLICADA al trabajador. Quiero mencionar especialmente el mensaje:
Look at this picture and if you see a cat, return the breed of the cat.
Inicialmente, me esforcé por obtener un resultado en JSON y hacer que devolviera una cadena en blanco si la imagen no era un gato. Pero luego noté algo interesante: Gemini hizo un muy buen trabajo manejando imágenes que no eran de gatos. Como... sorprendentemente bueno.
De hecho, aprecié mucho que la aplicación no dijera simplemente "No es un gato", sino que explicara qué era. Obviamente, hay espacio para ambos estilos para una aplicación como esta, pero seguí las respuestas detalladas y útiles de Google.
Ahora, viene la parte divertida... los resultados.
Empecemos con algunas fotografías de gatos.
El primero es Pig, mi gato favorito que no está gordo y no se parece en nada a Jabba the Hut:
A continuación, una foto de Luna. En este caso, la raza es incorrecta, pero al menos cercana.
Ahora, lancemos una bola curva a Google:
Esto realmente me sorprendió. La descripción es 100% precisa y, sinceramente, si hubiera visto esta imagen y no lo hubiera sabido, la habría reconocido como la escultura de un gato, no como una regadera. Quiero decir, supongo que es algo obvio, pero honestamente no creo que yo mismo lo hubiera notado.
Ahora, volvámonos totalmente locos:
Sí, así es Google. ¿Qué tal las imágenes de gatos generadas por IA?
Creo que lo manejó bastante bien.
Próximo...
Sí, ese es Bigfoot. ¡Piénselo, todos esos "investigadores" de Bigfoot podrían jubilarse y simplemente conectar sus cámaras de seguimiento a la IA!
Debo decir: estoy impresionado de que Google no solo haya reconocido la franquicia sino también el personaje real, pero para ser justos, Skeletor tiene una apariencia bastante distinta.
Y finalmente, dado que (injustamente, por supuesto) comparé a mi gato con Jabba, veamos cómo se maneja:
Oh, ya sé que dije que ya había terminado, pero, por supuesto, tenía que probar un perro:
Muy buen trabajo, Géminis. Desafortunadamente, no organizaré esta demostración en vivo (la URL 'en vivo' de Cloudflare que aparece anteriormente en el código no funcionará), pero puedo compartir el código.