En esta sesión, exploraremos diferentes casos de uso de la IA generativa con las APIs de ML Kit para comprender cuándo la IA generativa integrada en el dispositivo es ideal para una app y también entender cómo evaluar los casos de uso existentes de la IA generativa para ver si los modelos integrados en el dispositivo pueden satisfacer su nivel de calidad.
Transcripción
David Chou
¡Sí!
Buenas tardes a todos.
Me llamo David Chou.
Soy líder de producto de Android AICore.
Me entusiasma mucho hablar contigo sobre nuestras capacidades de IA integrada en el dispositivo.
Hoy hablaremos sobre algunas áreas clave.
La primera es pensar por qué deberías usar la IA integrada en el dispositivo, especialmente dado el avance acelerado de la IA en la nube.
En segundo lugar, pensamos en algunas de las mejoras que estuvimos haciendo durante el último año para que estos modelos de IA integrados en el dispositivo sean extremadamente potentes.
Por último, veremos un ejemplo de extremo a extremo que muestra cómo puedes aprovechar todos estos beneficios con la IA integrada en el dispositivo.
En primer lugar, ¿por qué usar la IA integrada en el dispositivo?
En realidad, hay 4 motivos principales.
El primero es la velocidad; el segundo, el costo; el tercero, la disponibilidad, y el cuarto, la privacidad.
Por lo tanto, en lo que respecta a la velocidad, puedes ver que la inferencia de IA integrada en el dispositivo está llegando al punto en el que puede ser más rápida que la ejecución en la nube, simplemente porque no tienes que pasar información de un lado a otro entre el dispositivo del usuario y la nube.
Por lo tanto, en muchos de los casos de uso que estuvimos probando, vemos una velocidad 2 veces mayor cuando comparamos la IA integrada en el dispositivo con la nube.
La segunda es pensar en el costo.
Imaginemos que ejecutas un millón de inferencias por día.
De hecho, podrías ahorrar cientos de miles de dólares si ejecutas la IA integrada en el dispositivo, específicamente desde el dispositivo del usuario, en lugar de ejecutarla en la nube.
Y el tercero es la disponibilidad sin conexión.
Por eso, enviamos una encuesta a principios de este año, y el 62% de los desarrolladores con los que hablamos dijeron que la disponibilidad sin conexión era una de las tres razones principales para usar la IA integrada en el dispositivo.
Por lo tanto, ya sea que el usuario esté a 10,000 metros de altura o en el subsuelo de la estación de Tokio y no tenga acceso a Internet, podrá aprovechar todas estas funciones.
Por último, la privacidad fue uno de los motivos más importantes que vimos cuando encuestamos a los desarrolladores.
Por lo tanto, el 72% lo clasificó como uno de los tres motivos principales.
El 40% lo clasificó como el número uno.
Por lo tanto, cuando se trata de mensajes, notas de voz, fotos o incluso información financiera o médica, los usuarios quieren mantener la privacidad, por lo que ejecutar la inferencia integrado en el dispositivo será muy valioso para ellos.
Por lo tanto, teniendo en cuenta todos estos beneficios, creamos el ML Kit para que puedas mantener y aprovechar todas estas funciones.
Intentamos que esto sea extremadamente simple.
Por lo tanto, si tienes algún reclamo, no dudes en comunicarte conmigo.
Dejaré mi información de contacto al final de esta charla.
Nos encantaría descubrir cómo podemos habilitar tus casos de uso.
Para empezar, habilitamos un montón de funciones para casos de uso de voz y creativos.
Por ejemplo, corregir, reescribir y también habilitar instrucciones multimodales flexibles.
Por lo tanto, nuestro objetivo es tratar de que sea lo más útil posible.
Por eso, muchos de nuestros usuarios y desarrolladores ya comenzaron a adoptarlo.
Ya tenemos 9 apps de terceros con 10 millones de descargas que usan estas funciones de IA generativa en producción.
Además, estamos viendo un aumento de escala mucho mayor en nuestras apps propias y apps de terceros, y un crecimiento del 110% intertrimestral en los lanzamientos de apps para funciones de producción de IA.
Por lo tanto, en lo que respecta a los mensajes, las creatividades y las finanzas, te recomendamos que pruebes estas APIs que tenemos disponibles.
Creemos que las posibilidades son infinitas y que, probablemente, tengas más imaginación que nosotros en muchos de estos casos de uso, por lo que nos encantaría asociarnos y descubrir cómo podemos trabajar juntos.
Ahora que comprendes los beneficios generales,
Por lo tanto, es posible que te preguntes cómo es el alcance en realidad.
¿Cómo es el rendimiento en el dispositivo?
Cuando hablamos de velocidad, ¿qué tan buena es en realidad?
O como la calidad del modelo en sí, ¿verdad?
Así que las analizaremos una por una.
En cuanto al alcance, ya estamos disponibles en 175 millones de dispositivos con AICore.
Esperamos que esta cifra aumente significativamente durante el próximo año, con la incorporación de cientos de millones de dispositivos más.
Parte de lo que hace que esto sea posible es que creamos un arnés muy simple.
Por lo tanto, la API de ML Kit es todo lo que tienes que integrar.
AICore tiene mucha complejidad integrada para administrar las señales de seguridad de entrada y salida, administrar la descarga del modelo junto con los pesos y pensar en cómo configurarlo en el entorno de ejecución.
Por lo tanto, el objetivo es hacer que el ML Kit sea lo más fácil de usar posible para que solo tengas que preocuparte por cómo ofrecer una gran experiencia, y nosotros nos encarguemos del resto.
David Chou
También nos complace anunciar que redujimos significativamente la RAM necesaria para ejecutar Gemini Nano, que es el modelo principal que potencia muchas de estas APIs de IA generativa.
Por lo tanto, redujimos el uso de RAM de 3.6 GB a 1.1 GB.
Por lo tanto, implementamos 4 mejoras principales para que esto sea posible.
La primera es usar la cuantización de 2 bits en lugar de la de 4 bits.
Por lo tanto, la mitad de los bits solo significa la mitad de los bytes que tienes que canalizar a través del modelo.
La segunda es usar una caché de KV compartida.
Por lo tanto, redujimos esto de forma significativa permitiendo que las primeras 20 capas del modelo compartan eso en el paquete de 35 MB, lo que significa que aún puedes acceder al tamaño de contexto de 16, 000 tokens dentro de este tamaño de paquete tan pequeño.
El tercero es habilitar los codificadores de visión y audio diferidos.
Por lo tanto, para muchos de los casos de uso de texto que vemos, no es necesario cargarlos para empezar.
Y eso te ahorrará otro medio gigabyte de RAM.
Por último, para mejorar la duración de la batería, también habilitamos el escalamiento de la frecuencia de reloj de la NPU.
Esto significa que la NPU puede ejecutarse tan rápido como sea necesario para ahorrar energía y, al mismo tiempo, garantizar que siga ofreciendo una alta calidad en tus casos de uso.
Realizamos algunas otras mejoras en lo que respecta a la aceleración.
La primera es habilitar el almacenamiento en caché de prefijos.
Como puedes ver en este ejemplo, la instrucción del sistema se reutiliza en varios turnos, ¿verdad?
Por lo tanto, procesar esto se vuelve muy costoso.
Pero con el almacenamiento en caché de prefijos, básicamente decimos que no tienes que volver a procesar esa instrucción del sistema una y otra vez, ¿verdad?
Puedes guardar eso en la caché de prefijos, lo que significa que, para este ejemplo, estás ahorrando el 60% de los tokens que tienes que pasar, lo que también significa un aumento del 60% en la velocidad en cualquier dispositivo en el que te encuentres.
La segunda mejora que realizamos es con la decodificación especulativa.
En este ejemplo, puedes ver que Gemini Nano genera un token por pasada.
Esto se debe a que los LLM son autorregresivos, lo que significa que deben seguir analizando los tokens anteriores que se generaron para determinar los siguientes.
Por lo tanto, la forma en que manejamos esto fue creando un modelo de borrador que genera un montón de tokens a una velocidad mucho más rápida.
Este modelo de borrador es mucho más pequeño que Gemini Nano, por lo que puede generar un montón de tokens a la vez y pasarlos a Gemini para ver cuáles se alinean con las suposiciones de Gemini Nano.
En este ejemplo, puedes ver que, en la primera pasada, genera seis tokens, en la siguiente genera cinco y, en el tercer turno, ya terminó.
Y lo que hemos estado viendo es que, para muchos de los casos de uso internos que estamos analizando, esto está llevando a una aceleración promedio de 2 veces.
Es posible que obtengas mejoras más rápido según tu hardware.
Bien.
Por eso, volviendo a la velocidad, puedes ver esta aceleración drástica, ¿verdad?, entre el dispositivo y la nube, incluso en una red 5G.
Puedes ver que simplemente subir esos datos a la nube va a tomar incluso solo un segundo, ¿verdad?, para 1,000 o 1,300 tokens de entrada, por lo que, aunque el precompletado y la decodificación en un recorrido en la nube podrían ser más rápidos, el recorrido de extremo a extremo sigue siendo sustancialmente más rápido cuando se ejecuta en el dispositivo.
Y, en cuanto a la calidad, nos complace anunciar que este modelo de 2,000 millones de parámetros tiene un rendimiento similar al de los modelos de 27,000 millones de parámetros del año pasado.
Si bien no tiene el mismo rendimiento que los modelos de frontera que tenemos hoy en día, lo que mostramos aquí es que hay un progreso sustancial en lo que respecta a la IA en estos modelos integrados en el dispositivo, ¿verdad?
Y, como puedes ver, para MMLU-Pro, que es una comparativa de conocimiento estándar, el modelo 2B es casi tan bueno como el modelo de 27,000 millones de parámetros del año pasado.
Y, en lo que respecta a las comparativas de ciencias de posgrado, razonamiento y uso de herramientas de agentes, este modelo de 2B es, en realidad, mejor, aunque tiene menos de una décima parte del tamaño del modelo del año pasado.
Por lo tanto, esperamos que estas tendencias continúen.
Seguimos trabajando en estrecha colaboración con el equipo de GDM para asegurarnos de que podamos seguir realizando estas mejoras en el modelo y que puedas obtener el mayor valor posible de ellas.
David Chou
Bien.
Te acabo de presentar muchos beneficios y mejoras nuevas, y tal vez te preguntes: "¿Qué podemos hacer realmente con estos modelos?".
Por lo tanto, también hemos estado realizando todas estas mejoras de capacidad en la API.
Por lo tanto, con las instrucciones del sistema, puedes establecer una personificación.
Con la entrada de varias imágenes, puedes subir un montón de imágenes a la vez.
También habilitamos el modo de razonamiento a principios de este año, para que puedas dedicar más tiempo a los problemas de razonamiento y a las llamadas a herramientas.
Por último, con el resultado estructurado, también hicimos posible que puedas anotar una clase de Kotlin y obtener un objeto que coincida con tu esquema específico.
Además, también intentamos pedirles a los desarrolladores que realicen las mejoras que deseen.
Por lo tanto, el 91% de los desarrolladores con los que hablamos cuando se realizó la encuesta dijeron que lanzarían una experiencia si una de estas funciones estuviera disponible.
Por eso, a principios de esta semana, lanzamos estas cuatro funciones: traducción de voz, embeddings, texto a voz y llamada a funciones.
Con la traducción de voz, puedes transcribir audio de más de 20 idiomas.
Con los embeddings, puedes buscar el contenido de tu app por significado semántico, en lugar de solo usar la búsqueda por palabras clave.
Con Text-to-Speech, ahora puedes proporcionar voces similares a las humanas para tus experiencias, por lo que ya no tienes que tener voces robóticas.
Por último, con la llamada a funciones, puedes permitir que tu app realice acciones en nombre de tus usuarios.
Por eso, ya nos asociamos con algunos desarrolladores para ver cómo podrían crear agentes integrados en la app en sus experiencias, y esperamos lanzarlos más adelante el próximo año.
David Chou
Dadas todas estas capacidades, veamos un ejemplo de extremo a extremo en el que compilamos con todas estas capacidades.
Imaginemos que hay un fotógrafo estadounidense que está viajando por Japón.
Y, como puedes ver en la foto, claramente no tienen suficientes cámaras ni lentes.
Por lo tanto, pide un lente nuevo y lo envía a su hotel.
Pero hay un problema: mientras realiza sus actividades diarias, recibe un mensaje de voz en el que se le indica que debe pagar una tarifa de despacho de aduana de 6, 800 yenes para recibir su paquete antes de que termine el día.
También recibió un mensaje de texto con una foto del paquete retenido.
En este caso, el usuario está interesado en tener algún tipo de protección de Detección de estafas, ¿verdad?
Quiere entender cómo puede identificar los mensajes de voz como legítimos o fraudulentos.
Por lo tanto, queremos mantener toda la información privada, asegurarnos de que esté fundamentada y que también sea explicable.
Por lo tanto, nos aseguraremos de que puedan comprender el texto en japonés y, además, de que puedan usar las funciones de idioma disponibles en caso de que tengan necesidades de accesibilidad.
Entonces, aquí habrá cuatro pasos, ¿verdad?
El primer paso es procesar ese mensaje de voz en el texto, por lo que se traducirá ese mensaje de voz.
Para ello, usaremos la API de Speech Translation.
Luego, podrán hacer una pregunta si lo desean, por lo que usaremos la API de Speech Recognition.
Luego, queremos definir funciones que puedan recuperar la información necesaria para que el usuario obtenga lo que necesita.
Por lo tanto, usaremos la API de Prompt y la llamada a funciones.
Ahora, esto se ejecutará en segundo plano sin que el usuario vea nada.
Y, en tercer lugar, descubriremos cómo almacenar y recuperar esa información con la API de Embedding y AppSearch.
Esto también se ejecutará en segundo plano.
Por último, emitiremos un veredicto en función de la información que recopilemos con la API de Prompt, el resultado estructurado y la síntesis de voz.
Bien.
Comencemos.
Usaremos la traducción y el reconocimiento de voz.
Por lo tanto, el primer paso es tomar ese mensaje de voz y el texto, y convertirlos en información útil, ¿verdad?
Por lo tanto, el usuario recibió este mensaje: "Hola, debes pagar esta tarifa de JPY 6,800.
Es posible que no se hayan pagado por adelantado los aranceles, por lo que debes pagarlos".
Por lo tanto, quieren poder tener esa traducción, ¿verdad?
Por lo tanto, una de las cosas que nos gustaría señalar aquí es que tanto la traducción como la pregunta del usuario sobre la información se envían a Gemini Nano 4 con el mismo codificador de audio, ya que este se entrenó con más de 100 idiomas.
Por lo tanto, puedes obtener esa traducción, así como el ASR para el texto de la pregunta del usuario.
Por lo tanto, para la traducción, todo lo que tenemos que hacer es configurar un idioma de origen y uno de destino.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)En este caso, usaremos japonés y también inglés estadounidense.
Configuraremos un flujo de Kotlin que pase los bytes directamente al traductor de voz.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)
val audioFlow = flow {
emit(TranslationAudio(Locale.JAPANESE, courierVoicemailBytes))
}
val request = SpeechTranslatorRequest.Builder(audioFlow).build()
translator.startTranslation(request).collect { response ->
if (response is SpeechTranslatorResponse.Text) {
translatedCourierClaim = response.translatedText
}
}Luego, podemos recopilar la respuesta con el texto traducido.
Bien.
Muy bien.
Ahora entendemos lo que realmente dice el mensaje de voz, ¿verdad?
Hay un paquete con un lente de cámara.
Está retenido en la aduana.
Debe pagar esa tarifa de JPY 6,800.
Y ahora el usuario quiere poder hacer una pregunta al respecto.
Bien.
Entonces, ¿esta retención de aduana es real?
En este caso, usaremos la API de ASR.
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)Lo que estamos haciendo es seguir configurando el inglés estadounidense como configuración regional.
Luego, podemos establecerlo en el modo avanzado, que aprovecha el codificador de audio.
Luego, podemos pasar esa información, recopilar la respuesta final y, después, convertirla en información que pasaremos a Gemini Nano.
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)
val request = speechRecognizerRequest {
audioSource = AudioSource.fromMic()
recognitionOptions { enableWordTiming = true } // optional
}
recognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse ->
liveCaption = response.text
is SpeechRecognizerResponse.FinalTextResponse ->
planAndCallVaultTool(translatedCourierClaim, response.text)
}
}Bien.
Ahora, terminamos la primera parte de la recopilación de información.
Como puedes ver, no se necesitaron muchas líneas de código.
David Chou
Por lo tanto, la segunda parte es la clasificación, que consiste en pensar cómo aprovechar la API de Prompt y la llamada a funciones para determinar qué tipo de información se necesita.
Bien.
Para compilar la arquitectura de esto, primero configuraremos algunas instrucciones del sistema.
Y, luego, también vamos a pasar el mensaje de voz, el texto y la foto, así como la pregunta del usuario, como entrada a Gemini Nano.
Luego, definiremos funciones que recuperen la información que necesitamos.
retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)Para empezar, configuraremos las instrucciones del sistema.
// Keep SystemInstruction static across Turn 1 & Turn 2:
val securitySystemInstruction = SystemInstruction(
"""
You are an on-device parcel & phishing security auditor.
1. Cross-check packaging, carrier labels and tracking across all images.
2. Verify customs fee claims against local DDP (Delivered Duty Paid) order records.
3. Flag any payment demand when the order record shows duties prepaid.
""".trimIndent()
)Aquí puedes ver un ejemplo muy sencillo de cómo se vería configurar un auditor de seguridad. Lo que dice es que vamos a verificar el empaquetado y las etiquetas, analizar las tarifas personalizadas y marcar cualquier demanda de pago que sea necesaria.
Es posible que descubras que la optimización de instrucciones será muy útil para obtener una calidad significativamente mejor.
Y, luego, una vez que tengamos ese conjunto de instrucciones del sistema, podemos tomar todo el contenido que teníamos del paso anterior, la traducción del mensaje de voz y del texto, así como la pregunta del usuario, y pasarlo todo a este campo TextPart, junto con las instrucciones del sistema para obtener esa información.
// Pull in the Speech Translation (AST) & Speech Recognition (ASR) outputs:
val turn1Prompt = """
Translated voicemail: $translatedCourierClaim
Incoming text message: $callerText
User's spoken question: $userQuestion
""".trimIndent()
val turn1Request = generateContentRequest(
securitySystemInstruction,
TextPart(turn1Prompt)
) { /* enableThinking & setTools */ }Y, luego, queremos definir una función que podamos usar.
Por lo tanto, para este ejemplo en particular, nos complace anunciar que nos integramos con la función de la app.
Por lo tanto, si alguno de ustedes está familiarizado con eso, probablemente ya entienda, a grandes rasgos, cómo funciona esto.
Aquí configuramos un punto de entrada AppFunctionService y, luego, definimos una función de la app.
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}Puedes ver que es exactamente igual que si estuvieras configurando las funciones de la app para un agente del sistema.
En los parámetros de searchQuery, estableces la tarifa de reclamo y también si debemos comparar las fotos de los paquetes, solo como algunos parámetros de ejemplo, ¿verdad?
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
/**
* Searches local orders and order images in AppSearch.
* @param searchQuery Semantic query to match the order
* @param claimedFeeUsd Customs fee claimed in USD
* @param comparePackagePhotos Whether to fetch order images to compare
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun retrieveOrderAndDeliveryPhotos(
searchQuery: String,
claimedFeeUsd: Int,
comparePackagePhotos: Boolean
): VaultMatch = withContext(Dispatchers.IO) { executeVaultSearch(searchQuery) }
}Puedes definir otras diferentes.
Por último, para esa función, tenemos una función executeVaultSearch que definiremos más adelante.
Un aspecto que quiero destacar aquí es que tenemos disponible el modo de pensamiento.
val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()Te recomiendo que lo uses cuando ejecutes llamadas a herramientas.
Esto se debe a que realizamos una serie de pruebas que demuestran que hay una mejora de 13 puntos en la precisión de 100 cuando se ejecuta la llamada a la herramienta en Gemini Nano 4.
Y, en algunos casos, es sustancialmente más alto, por lo que podría darte resultados mucho mejores.
Y si quieres obtener más información sobre la llamada a funciones, asiste a la sesión de mis colegas Jolanda y Victoria, que se llevará a cabo más adelante.
Se trata de las capacidades agénticas en tu app.
Creo que Victoria está sentada en algún lugar entre el público, así que no duden en saludarla también.
David Chou
Genial.
Ya completamos los primeros dos pasos.
Hablemos de cómo funciona la recuperación.
Por lo tanto, usaremos embeddings y AppSearch.
Por lo tanto, tal vez retrocediendo algunos pasos, ¿verdad?, en el recorrido del usuario, en el punto en el que pidió el lente, lo que querríamos hacer es indexar esa información para que sea accesible para el usuario y el desarrollador, ¿verdad?
En este caso, tenemos detalles de la información sobre el número de paquete del pedido.
También tiene un número de seguimiento.
Tiene algunos detalles sobre dónde se envía.
Y puedes ver que aquí ya se menciona que los aranceles se pagaron por adelantado.
Por lo tanto, lo que haremos es generar un embedding multimodal para indexar esa información junto con cualquier otra información que pueda ser importante para nosotros.
Así es como se ve.
val embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vectorTomamos el correo electrónico del pedido.
Nuevamente, es muy simple, solo pasamos una imagen y un texto, pero es posible que tengas más contenido que quieras indexar aquí.
Luego, generaremos el vector de embedding.
Luego, usaremos AppSearch para crear un documento y, luego, estableceremos las propiedades que consideremos necesarias para generar ese documento específico.
val embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vector
val doc = GenericDocument.Builder<GenericDocument.Builder<*>>(
"orders", "order_jp_904", "OrderRecord"
)
.setPropertyString("summary", "Shipped DDP, duties prepaid ($0 owed). In transit.")
.setPropertyString("photoUris", "order_email_lens.png")
.setPropertyEmbedding(
"embedding",
EmbeddingVector(docVector, embedModel.getModelSignature())
).build()Y, luego, un aspecto que se debe tener en cuenta aquí es que el motivo por el que queremos realizar embeddings multimodales, ¿verdad?, es porque es posible que no haya palabras compartidas aquí, pero, como las imágenes y el texto tratan sobre contenido similar, esto nos ayuda a obtener la similitud semántica.
Por lo tanto, podemos usar la similitud de coseno para encontrar elementos que no tengan palabras en común.
Bien, esta es la función executeVaultSearch de la que hablamos antes.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}Esto es lo que llama la función de la app que definimos.
En este ejemplo, pasamos la búsqueda que generó Gemini Nano.
Luego, usaremos una estrategia de clasificación, la similitud del coseno.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
}Y, luego, this.matchedSemanticScores solo nos ayuda a obtener esa información.
Por último, especificaremos un umbral de 0.68 para determinar cuál es la similitud del coseno mínima para que recuperemos información.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
val results = appSearchSession.search(
"semanticSearch(getEmbeddingParameter(0), 0.68)", spec
)
// Cleanly close the cursor.
return results.use { it.nextPageAsync.await().first().genericDocument.toVaultMatch() }
}Por lo tanto, es posible que quieras probar esto mientras ejecutas tus propios casos de uso para ver qué funciona realmente para ti.
Y, luego, algo que también queremos destacar aquí es que, cuando piensas en la seguridad de este modelo, Gemini Nano no es el que ejecuta el código, ¿verdad?
Por lo tanto, Gemini Nano solo hace una pregunta, pero tu app define el código y, luego, es la que ejecuta el código, recupera esa información y, luego, determina qué pasarle a Gemini Nano.
Esto te da más control sobre lo que quieres exponer a Gemini Nano y también ayuda a garantizar más privacidad y seguridad para tus usuarios.
David Chou
Bien, ahora este es el paso final.
Por lo tanto, tenemos toda la información que necesitamos.
Usaremos la API de Prompt, el resultado estructurado y la síntesis de voz para hacer las comparaciones y, luego, devolver esa información al usuario de una manera comprensible.
Bien, básicamente, lo que queremos saber es si este paquete es realmente el mismo que el de la información del usuario, que es el pedido real.
Para ello, también generaremos un embedding, ¿verdad?, con esa información del mensaje de voz y el texto, y lo convertiremos en el mismo vector de embedding que creamos antes.
val callerVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(callerParcelPhotoBmp), // brown box w/ shipping label
TextPart(callerText) // "Clearance fee ¥6,800 due today."
))
)
).embedding.vector
val matchScore = cosineSimilarity(callerVector, vaultMatch.embedding)Luego, usaremos matchScore. Con la similitud de coseno, podemos hacer una comparación entre la coincidencia que recuperamos de la bóveda y la información de ese emisor.
Como puedes ver aquí, incluso si las imágenes no coinciden, el texto sigue siendo muy útil.
Aquí es donde los embeddings multimodales son realmente valiosos.
Puedes ingresar partes de información dispares y, luego, obtener un significado semántico compartido, ¿verdad?
Por lo tanto, el embedding multimodal permite que puedas mapear todo en el mismo espacio vectorial.
Nuevamente, usaremos el modo de razonamiento para mejorar nuestro pensamiento.
val forensicRequest = generateContentRequest(
securitySystemInstruction,
content {
image(callerParcelPhotoBmp)
image(orderEmailLensBmp)
text(forensicAuditPrompt)
}
) {
// Enable Gemini Nano 4 reasoning before it answers
enableThinking = true
}Y, luego, también definiremos una clase de salida, ¿verdad?
@Generable(description = "Details of a visual contradiction found during an audit")
data class VisualContradiction(
@Guide(
description = "The type of contradiction",
enumValues = ["PACKAGING_MISMATCH", "STATUS_CONFLICT", "DUTY_FRAUD"],
)
val contradictionType: String,
@Guide(description = "Detailed explanation of the visual contradiction")
val explanation: String,
)Así que todo lo que estamos haciendo aquí es configurar un VisualContradiction y configurar algunas enumeraciones diferentes de los distintos tipos de contradicciones posibles que podríamos tener, ¿verdad?
Ya sea DUTY_FRAUD, PACKAGING_MISMATCH o STATUS_CONFLICT.
Y, luego, tendremos una explicación aquí.
Luego, pasaremos toda esa información a un veredicto de auditoría, ¿verdad?
@Generable(description = "Verdict and details of a phishing audit")
data class PhishingAuditVerdict(
@Guide(
description = "The audit verdict",
enumValues = ["CONFIRMED_PHISHING_SCAM", "LEGITIMATE_HOLD"],
)
val verdict: String,
@Guide(description = "The fraud risk score from 0 to 100", minimum = 0.0, maximum = 100.0)
val fraudRiskScore: Int,
@Guide(description = "Estimated amount of money saved in USD", minimum = 0.0)
val moneySavedUsd: Int,
@Guide(description = "List of visual contradictions identified during audit", minItems = 0)
val contradictions: List<VisualContradiction>,
// @Guide(description = "Summary of the warning to be spoken to the user")
// `@Guide` annotations are optional.
val spokenWarningSummary: String,
)Como puedes ver aquí, es muy sencillo.
Solo queremos saber si es una estafa o no.
Tal vez tengamos un fraudRiskScore en el que puedas establecer un valor mínimo y uno máximo de 0 a 100.
Incluso puedes informarle al usuario cuánto dinero le ahorraste si quieres mantener un registro de eso a lo largo del tiempo.
Y luego, también puedes enumerar las contradicciones visuales que acabamos de definir.
Otra cosa que debes tener en cuenta aquí es que la anotación @Guide es opcional.
No es necesario que lo configures y proporciones ese tipo de descripción.
Es útil porque es posible que no puedas explicar lo que sucede solo con la definición de la variable.
Por lo tanto, tener esa descripción puede ser útil para que Gemini Nano determine qué funciones específicas llamar y cómo configurarla para que ingrese la información correcta en los campos adecuados.
Y, sí, después de configurarlo, es muy sencillo.
Solo debes ejecutar tres líneas de código aquí.
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().responsegenerateTypedContentRequest, generas ese contenido y, luego, puedes recuperar esa respuesta.
Y aquí hay algo que muestra cómo se vería, ¿verdad?
{
"verdict": "CONFIRMED_PHISHING_SCAM",
"fraudRiskScore": 98,
"moneySavedUsd": 45,
"contradictions": [
{ "contradictionType": "DUTY_FRAUD", "explanation": "Fee demanded, but duties were prepaid." },
{ "contradictionType": "PACKAGING_MISMATCH", "explanation": "Carton has no carrier label or tracking." },
],
"spokenWarningSummary": "Scam blocked. Do not pay the 45 dollar fee. Your lens shipped duties prepaid."
}Y, por lo tanto, confirmamos que se trata de una estafa, obtenemos una puntuación de riesgo de fraude muy alta, lo que explica estas contradicciones que son evidentes en los datos que recuperaste en comparación con la información de la persona que llama.
También le ahorramos a este usuario una cierta cantidad de dinero, por lo que podemos mantener un seguimiento allí.
Sí.
Y eso resume toda la información que necesitamos.
Y, luego, el paso final sería mostrarle esto al usuario de una manera que sea accesible.
Por ejemplo, si están en movimiento o tienen problemas de visión, es posible que quieras leer esa información en voz alta en lugar de que tengan que leer ese texto manualmente.
Por lo tanto, implementaremos la función de texto a voz para mostrar cómo se vería.
val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
/* more code here */
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}De nuevo, es muy sencillo: especificamos una configuración regional y un idioma, y luego pasamos ese veredicto que acabamos de generar con @Generable y @Guide.
Y, luego de eso, es casi instantáneo.
val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> { }
else -> {}
}
}
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}En la mayoría de los dispositivos, tardará menos de 120 ms en generar esa respuesta.
Borraremos la memoria que sea necesaria.
Y, luego, si es necesario, también podemos establecer un resguardo, ¿verdad?, con las APIs de la plataforma.
val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> {
/* handle fallback to platform API */
}
else -> {}
}
}
}
// more code here ...Esto aún tendría la voz robótica, pero al menos le daría al usuario algo con lo que interactuar en caso de que no pueda leer.
David Chou
Bien.
Fue mucha información, pero para resumir lo que hicimos, aprovechamos todas estas APIs diferentes en los procesos de admisión, clasificación, recuperación y auditoría.
Por ejemplo, usar las APIs de reconocimiento de voz y traducción, la API de Prompt y la llamada a funciones, los embeddings y AppSearch, así como el resultado estructurado y la síntesis de voz.
Este es solo un ejemplo de cómo podrías aprovechar todas estas funciones de manera coherente y ejecutarlas por completo en el dispositivo.
Y, luego, volviendo a lo que hablamos al principio, hay beneficios sustanciales al ejecutar esto, ¿verdad?
Si realmente ejecutaras esto en tu dispositivo hoy, verías que es mucho más rápido en el dispositivo en la mayoría de los hardware.
No te cuesta nada, ¿verdad?, y podrías ahorrar cientos de miles de dólares si ejecutas esto todo el tiempo.
Y, luego, esto está disponible, ¿verdad?, independientemente de si el usuario está en las montañas o en un sótano y no tiene acceso a Internet.
Además, es completamente privado, ya que no tienes que enviar ninguna información a la nube.
Dadas todas estas funciones, no duden en consultar la documentación, escanear el código QR o visitar el vínculo.
Estas son todas las funciones que lanzamos en los últimos seis meses, aproximadamente.
Muchas gracias a todos por tomarse el tiempo de asistir a esta charla.
Gracias por dedicarnos tu tiempo.
Así que, muchas gracias.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)
val audioFlow = flow {
emit(TranslationAudio(Locale.JAPANESE, courierVoicemailBytes))
}
val request = SpeechTranslatorRequest.Builder(audioFlow).build()
translator.startTranslation(request).collect { response ->
if (response is SpeechTranslatorResponse.Text) {
translatedCourierClaim = response.translatedText
}
}val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)
val request = speechRecognizerRequest {
audioSource = AudioSource.fromMic()
recognitionOptions { enableWordTiming = true } // optional
}
recognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse ->
liveCaption = response.text
is SpeechRecognizerResponse.FinalTextResponse ->
planAndCallVaultTool(translatedCourierClaim, response.text)
}
}retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)// Keep SystemInstruction static across Turn 1 & Turn 2:
val securitySystemInstruction = SystemInstruction(
"""
You are an on-device parcel & phishing security auditor.
1. Cross-check packaging, carrier labels and tracking across all images.
2. Verify customs fee claims against local DDP (Delivered Duty Paid) order records.
3. Flag any payment demand when the order record shows duties prepaid.
""".trimIndent()
)// Pull in the Speech Translation (AST) & Speech Recognition (ASR) outputs:
val turn1Prompt = """
Translated voicemail: $translatedCourierClaim
Incoming text message: $callerText
User's spoken question: $userQuestion
""".trimIndent()
val turn1Request = generateContentRequest(
securitySystemInstruction,
TextPart(turn1Prompt)
) { /* enableThinking & setTools */ }// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
/**
* Searches local orders and order images in AppSearch.
* @param searchQuery Semantic query to match the order
* @param claimedFeeUsd Customs fee claimed in USD
* @param comparePackagePhotos Whether to fetch order images to compare
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun retrieveOrderAndDeliveryPhotos(
searchQuery: String,
claimedFeeUsd: Int,
comparePackagePhotos: Boolean
): VaultMatch = withContext(Dispatchers.IO) { executeVaultSearch(searchQuery) }
}val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()val embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vectorval embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vector
val doc = GenericDocument.Builder<GenericDocument.Builder<*>>(
"orders", "order_jp_904", "OrderRecord"
)
.setPropertyString("summary", "Shipped DDP, duties prepaid ($0 owed). In transit.")
.setPropertyString("photoUris", "order_email_lens.png")
.setPropertyEmbedding(
"embedding",
EmbeddingVector(docVector, embedModel.getModelSignature())
).build()suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
}suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
val results = appSearchSession.search(
"semanticSearch(getEmbeddingParameter(0), 0.68)", spec
)
// Cleanly close the cursor.
return results.use { it.nextPageAsync.await().first().genericDocument.toVaultMatch() }
}val callerVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(callerParcelPhotoBmp), // brown box w/ shipping label
TextPart(callerText) // "Clearance fee ¥6,800 due today."
))
)
).embedding.vector
val matchScore = cosineSimilarity(callerVector, vaultMatch.embedding)val forensicRequest = generateContentRequest(
securitySystemInstruction,
content {
image(callerParcelPhotoBmp)
image(orderEmailLensBmp)
text(forensicAuditPrompt)
}
) {
// Enable Gemini Nano 4 reasoning before it answers
enableThinking = true
}@Generable(description = "Details of a visual contradiction found during an audit")
data class VisualContradiction(
@Guide(
description = "The type of contradiction",
enumValues = ["PACKAGING_MISMATCH", "STATUS_CONFLICT", "DUTY_FRAUD"],
)
val contradictionType: String,
@Guide(description = "Detailed explanation of the visual contradiction")
val explanation: String,
)@Generable(description = "Verdict and details of a phishing audit")
data class PhishingAuditVerdict(
@Guide(
description = "The audit verdict",
enumValues = ["CONFIRMED_PHISHING_SCAM", "LEGITIMATE_HOLD"],
)
val verdict: String,
@Guide(description = "The fraud risk score from 0 to 100", minimum = 0.0, maximum = 100.0)
val fraudRiskScore: Int,
@Guide(description = "Estimated amount of money saved in USD", minimum = 0.0)
val moneySavedUsd: Int,
@Guide(description = "List of visual contradictions identified during audit", minItems = 0)
val contradictions: List<VisualContradiction>,
// @Guide(description = "Summary of the warning to be spoken to the user")
// `@Guide` annotations are optional.
val spokenWarningSummary: String,
)val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().response{
"verdict": "CONFIRMED_PHISHING_SCAM",
"fraudRiskScore": 98,
"moneySavedUsd": 45,
"contradictions": [
{ "contradictionType": "DUTY_FRAUD", "explanation": "Fee demanded, but duties were prepaid." },
{ "contradictionType": "PACKAGING_MISMATCH", "explanation": "Carton has no carrier label or tracking." },
],
"spokenWarningSummary": "Scam blocked. Do not pay the 45 dollar fee. Your lens shipped duties prepaid."
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
/* more code here */
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> { }
else -> {}
}
}
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> {
/* handle fallback to platform API */
}
else -> {}
}
}
}
// more code here ...