Dans cette session, nous allons explorer différents cas d'utilisation de l'IA générative à l'aide des API ML Kit pour comprendre quand l'IA générative sur l'appareil est idéale pour une application, et comment évaluer les cas d'utilisation existants de l'IA générative pour déterminer si les modèles sur l'appareil peuvent répondre à leurs exigences de qualité.
Transcription
David Chou
Hé,
Bonjour à toutes et à tous.
Je m'appelle David Chou.
Je suis responsable produit pour Android AICore.
Je suis ravi de vous parler de nos fonctionnalités d'IA sur l'appareil.
Aujourd'hui, nous allons aborder quelques points essentiels.
La première consiste à réfléchir aux raisons d'utiliser l'IA sur l'appareil, en particulier compte tenu de l'accélération des progrès de l'IA dans le cloud.
Deuxièmement, nous devons réfléchir à certaines des améliorations que nous avons apportées au cours de l'année écoulée pour rendre ces modèles d'IA sur l'appareil extrêmement puissants.
Enfin, nous allons vous présenter un exemple de bout en bout qui montre comment vous pouvez exploiter tous ces avantages en utilisant l'IA sur l'appareil.
Tout d'abord, pourquoi utiliser l'IA sur l'appareil ?
Pour quatre raisons principales :
Le premier est la vitesse, le deuxième est le coût, le troisième est la disponibilité et le quatrième est la confidentialité.
En termes de vitesse, l'inférence d'IA sur l'appareil est désormais si rapide qu'elle peut être plus rapide que l'exécution dans le cloud, simplement parce qu'il n'est pas nécessaire de transférer des informations entre l'appareil de l'utilisateur et le cloud.
Ainsi, dans de nombreux cas d'utilisation que nous avons testés, nous constatons une vitesse deux fois plus élevée lorsque nous comparons l'IA sur l'appareil au cloud.
Le deuxième est de réfléchir au coût.
Imaginons que vous deviez exécuter un million d'inférences par jour.
Vous pourriez économiser des centaines de milliers de dollars en exécutant l'IA sur l'appareil de l'utilisateur plutôt que dans le cloud.
Troisièmement, la disponibilité hors connexion.
Nous avons donc envoyé une enquête plus tôt cette année, et 62 % des développeurs interrogés ont indiqué que la disponibilité hors connexion était l'une des trois principales raisons d'utiliser l'IA sur l'appareil.
Ainsi,que votre utilisateur se trouve à 10 000 mètres d'altitude ou dans le sous-sol de la gare de Tokyo sans accès à Internet, il peut toujours exploiter toutes ces fonctionnalités.
Enfin, la confidentialité est l'une des principales raisons évoquées par les développeurs que nous avons interrogés.
72 % d'entre eux l'ont classé parmi les trois raisons principales.
40 % l'ont classé en première position.
Les utilisateurs veulent que leurs messages, notes vocales, photos, ou même informations financières ou médicales restent privés. L'exécution de l'inférence sur l'appareil leur sera donc très utile.
Compte tenu de tous ces avantages, nous avons conçu ML Kit pour vous permettre de conserver et d'exploiter toutes ces fonctionnalités.
Nous essayons de rendre ce processus extrêmement simple.
Si vous avez des réclamations, n'hésitez pas à me contacter.
Je vous laisserai mes coordonnées à la fin de cette présentation.
Nous serions ravis de trouver un moyen de vous permettre d'utiliser vos cas d'utilisation.
Pour commencer, nous avons activé un certain nombre de fonctionnalités pour les cas d'utilisation vocaux et créatifs.
comme la relecture, la réécriture et l'activation du prompting multimodal flexible.
Notre objectif est donc de rendre cette technologie aussi utilisable que possible.
C'est pourquoi un grand nombre de nos utilisateurs et de nos développeurs ont déjà commencé à l'adopter.
Nous avons déjà neuf applications tierces qui comptent 10 millions de téléchargements et qui utilisent ces fonctionnalités d'IA générative en production.
Nous constatons également une augmentation de l'utilisation de nos applications propriétaires et des applications tierces, ainsi qu'une croissance de 110 % d'un trimestre à l'autre des lancements d'applications pour les fonctionnalités de production d'IA.
Nous vous encourageons donc à essayer les API que nous mettons à votre disposition pour la messagerie, la création et la finance.
Nous pensons que les possibilités sont infinies, et vous avez probablement plus d'imagination que nous pour bon nombre de ces cas d'utilisation. Nous serions donc ravis de collaborer avec vous et de voir comment nous pouvons travailler ensemble.
Vous comprenez maintenant les avantages généraux.
Vous vous demandez peut-être quelle est la couverture réelle.
Quelles sont les performances sur l'appareil ?
Quand on parle de vitesse, qu'en est-il vraiment ?
ou la qualité du modèle lui-même, n'est-ce pas ?
Nous allons donc les examiner un par un.
En termes de couverture, nous sommes déjà disponibles sur 175 millions d'appareils AICore.
Nous prévoyons d'étendre considérablement cette fonctionnalité au cours de l'année à venir, en l'ajoutant à des centaines de millions d'autres appareils.
Cela est rendu possible en partie grâce à un harnais très simple que nous avons conçu.
Il vous suffit donc d'intégrer l'API ML Kit.
AICore intègre de nombreux éléments complexes pour gérer les signaux de sécurité d'entrée et de sortie, gérer le téléchargement du modèle et des pondérations, et réfléchir à la configuration de l'environnement d'exécution.
L'objectif est donc de rendre ML Kit aussi simple à utiliser que possible, afin que vous puissiez vous concentrer sur la création d'une expérience de qualité, et que nous puissions nous occuper du reste.
David Chou
Nous sommes également heureux d'annoncer que nous avons considérablement réduit la RAM requise pour exécuter Gemini Nano, le modèle principal qui alimente un grand nombre de ces API d'IA générative.
Nous avons ainsi réduit l'utilisation de la RAM de 3,6 Go à 1,1 Go.
Nous avons donc apporté quatre améliorations fondamentales pour rendre cela possible.
La première consiste à utiliser une quantification de 2 bits au lieu de 4 bits.
Ainsi, diviser par deux le nombre de bits revient à diviser par deux le nombre d'octets à faire passer par le modèle.
La seconde consiste à utiliser un cache KV partagé.
Nous avons donc considérablement réduit cette taille en permettant aux 20 premières couches du modèle de partager ces informations dans le package de 35 Mo. Vous pouvez ainsi toujours accéder à la taille de contexte de 16 000 jetons dans ce package très, très petit.
La troisième consiste à activer les encodeurs de vision et audio différés.
Pour de nombreux cas d'utilisation de texte, vous n'avez pas besoin de les charger au départ.
Cela vous permettra d'économiser un demi-gigaoctet de RAM supplémentaire.
Enfin, pour améliorer l'autonomie de la batterie, nous avons également activé la mise à l'échelle de l'horloge du NPU.
Cela signifie que le NPU peut fonctionner aussi rapidement que nécessaire pour économiser de l'énergie, tout en garantissant une qualité élevée pour vos cas d'utilisation.
Nous avons apporté quelques autres améliorations en termes d'accélération.
La première consiste à activer la mise en cache des préfixes.
Comme vous pouvez le voir dans cet exemple, la requête système est réutilisée pour plusieurs tours.
Le traitement de ces données devient donc très coûteux.
Mais avec la mise en cache des préfixes, nous disons en gros que vous n'avez pas besoin de retraiter cette requête système encore et encore, n'est-ce pas ?
Vous pouvez l'enregistrer dans le cache de préfixe. Dans cet exemple, vous économisez 60 % des jetons que vous devez transmettre, ce qui signifie également une accélération de 60 % sur l'appareil que vous utilisez.
La deuxième amélioration que nous avons apportée concerne le décodage spéculatif.
Dans cet exemple, vous pouvez voir que Gemini Nano génère un jeton par passe.
En effet, les LLM sont autorégressifs, ce qui signifie qu'ils doivent continuer à examiner les jetons précédemment générés pour déterminer les jetons suivants.
Pour résoudre ce problème, nous avons créé un modèle de rédaction qui génère un ensemble de jetons beaucoup plus rapidement.
Ce modèle de rédaction est nettement plus petit que Gemini Nano. Il peut donc générer un ensemble de jetons à la fois, puis les transmettre à Gemini pour voir lesquels correspondent aux prédictions de Gemini Nano.
Dans cet exemple, vous pouvez voir que lors de la première passe, il génère six jetons, puis cinq lors de la suivante, et qu'au troisième tour, il a terminé.
Nous avons constaté que pour de nombreux cas d'utilisation internes que nous examinons, cela permet de doubler la vitesse en moyenne.
Vous pourriez obtenir des améliorations plus rapidement en fonction de votre matériel.
OK.
C'est pourquoi, pour en revenir à la vitesse, vous pouvez constater une accélération spectaculaire entre l'appareil et le cloud, même sur un réseau 5G.
Vous pouvez voir que le simple fait d'importer ces données dans le cloud prendra une seconde, n'est-ce pas, pour 1 000 à 1 300 jetons d'entrée. Ainsi, même si le préremplissage et le décodage dans un parcours cloud peuvent être plus rapides, le parcours de bout en bout est toujours beaucoup plus rapide lorsque vous l'exécutez sur l'appareil.
En ce qui concerne la qualité, nous sommes heureux d'annoncer que ce modèle 2B est en fait plus performant que les modèles 27B de l'année dernière.
Même s'ils ne sont pas aussi performants que les modèles de frontière actuels, nous montrons ici que des progrès substantiels ont été réalisés en matière d'IA sur ces modèles sur l'appareil.
Comme vous pouvez le voir, pour MMLU-Pro, qui est un benchmark de connaissances standard, le modèle 2B est presque aussi performant que le modèle à 27 milliards de paramètres de l'année dernière.
Et pour ce qui est des benchmarks de raisonnement, de science de niveau universitaire et d'utilisation d'outils agentiques, ce modèle 2B est en fait meilleur, même s'il est plus de 10 fois plus petit que le modèle de l'année dernière.
Nous nous attendons donc à ce que ces tendances se poursuivent.
Nous continuons à travailler en étroite collaboration avec l'équipe GDM pour nous assurer que nous pouvons continuer à apporter ces améliorations au modèle afin que vous puissiez en tirer le meilleur parti.
David Chou
OK.
Je viens de vous présenter de nombreux avantages et de nouvelles améliorations, et vous vous demandez peut-être : "Que pouvons-nous faire concrètement avec ces modèles ?"
Nous avons également apporté toutes ces améliorations aux fonctionnalités de l'API.
Les instructions système vous permettent donc de définir un persona.
Avec l'entrée multi-image, vous pouvez importer plusieurs images à la fois.
Nous avons également activé le mode de réflexion plus tôt cette année, ce qui vous permet de passer plus de temps sur les problèmes de raisonnement et l'appel d'outils.
Enfin, avec la sortie structurée, nous avons également fait en sorte que vous puissiez annoter une classe Kotlin et récupérer un objet correspondant à votre schéma spécifique.
En outre, nous essayons également de demander aux développeurs quelles nouvelles améliorations ils aimeraient apporter.
91 % des développeurs interrogés ont déclaré qu'ils lanceraient une expérience si l'une de ces fonctionnalités était disponible.
En début de semaine, nous avons lancé ces quatre fonctionnalités : la traduction vocale, les embeddings, la synthèse vocale et l'appel de fonction.
La traduction vocale vous permet de transcrire des contenus audio dans plus de 20 langues.
Grâce aux embeddings, vous pouvez rechercher le contenu de votre application par signification sémantique, plutôt que d'utiliser simplement la recherche par mots clés.
Avec la synthèse vocale, vous pouvez désormais proposer des voix semblables à des voix humaines pour vos expériences, de sorte que vous n'avez plus à utiliser des voix robotiques.
Enfin, avec l'appel de fonction, vous pouvez laisser votre application effectuer des actions au nom de vos utilisateurs.
Nous avons donc déjà collaboré avec quelques développeurs pour voir comment ils pourraient intégrer des agents dans leurs expériences, et nous espérons lancer cette fonctionnalité l'année prochaine.
David Chou
Compte tenu de toutes ces fonctionnalités, examinons un exemple de bout en bout dans lequel nous les utilisons toutes.
Imaginons qu'un photographe américain voyage au Japon.
Et comme vous pouvez le voir sur la photo, ils n'ont clairement pas assez d'appareils photo ni d'objectifs.
Il commande donc un nouvel objectif et le fait livrer à son hôtel.
Mais il y a un problème : au cours de la journée, ils reçoivent un message vocal leur indiquant qu'ils doivent payer des frais de dédouanement de 6 800 yens pour recevoir leur colis avant la fin de la journée.
Il a également reçu un message contenant une photo du colis retenu.
Dans ce cas, l'utilisateur souhaite bénéficier d'une protection contre les escroqueries, n'est-ce pas ?
Ils veulent savoir comment identifier les messages vocaux légitimes ou frauduleux.
Nous voulons donc que toutes les informations restent privées, nous voulons nous assurer qu'elles sont ancrées, mais aussi qu'elles sont explicables.
Nous allons donc nous assurer qu'ils peuvent comprendre le texte en japonais et utiliser les fonctionnalités linguistiques disponibles s'ils ont des besoins d'accessibilité.
Il y a donc quatre étapes ici, n'est-ce pas ?
La première étape consiste à traiter ce message vocal dans le texte, afin de le traduire.
Pour cela, nous allons utiliser l'API Speech Translation.
Ensuite, ils pourront poser une question s'ils le souhaitent. Nous allons donc utiliser l'API Speech Recognition.
Ensuite, nous voulons définir des fonctions capables de récupérer les informations dont l'utilisateur a besoin.
Nous allons donc utiliser l'API Prompt et l'appel de fonction.
Cette opération s'exécute en arrière-plan sans que l'utilisateur ne voie quoi que ce soit.
Troisièmement, nous verrons comment stocker et récupérer ces informations à l'aide de l'API d'embedding et d'AppSearch.
Cette opération s'exécute également en arrière-plan.
Enfin, nous rendrons un verdict basé sur les informations que nous collectons, à l'aide de l'API Prompt, de la sortie structurée et de la synthèse vocale.
OK.
Nous pouvons commencer.
Nous allons utiliser la traduction et la reconnaissance vocales.
La première étape consiste donc à prendre ce message vocal et ce texte, et à les transformer en informations utiles.
L'utilisateur a donc reçu ce message : "Vous devez payer ces frais de 6 800 yens.
Les droits de douane n'ont peut-être pas été prépayés, et vous devez donc les payer."
Ils veulent donc pouvoir obtenir cette traduction, n'est-ce pas ?
Nous aimerions souligner que la traduction et la question de l'utilisateur sur les informations sont toutes deux envoyées à Gemini Nano 4 à l'aide du même encodeur audio, car celui-ci a été entraîné sur plus de 100 langues.
Vous pouvez ainsi obtenir cette traduction, ainsi que la reconnaissance vocale automatique pour le texte de la question de l'utilisateur.
Pour la traduction, il nous suffit donc de configurer une langue source et une langue cible.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)Dans ce cas, nous allons utiliser le japonais et l'anglais américain.
Nous allons configurer un flux Kotlin qui transmet les octets directement au traducteur vocal.
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
}
}Nous pouvons ensuite collecter la réponse à l'aide du texte traduit.
OK.
Parfait.
Maintenant, nous comprenons ce que dit réellement le message vocal, n'est-ce pas ?
Il y a un colis d'objectif d'appareil photo.
Il est retenu à la douane.
Ils doivent payer ces frais de 6 800 JPY.
L'utilisateur souhaite maintenant pouvoir poser une question à ce sujet.
OK.
Alors, cette retenue en douane est-elle réelle ?
Dans ce cas, nous allons utiliser l'API ASR.
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)Nous définissons toujours l'anglais américain comme paramètre régional.
Ensuite, nous pouvons le définir sur le mode avancé, qui exploite l'encodeur audio.
Nous pouvons ensuite transmettre ces informations, collecter la réponse finale, puis la transformer en informations que nous transmettrons à 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)
}
}OK.
Nous avons maintenant terminé la première partie de l'admission.
Comme vous pouvez le constater, cela n'a pas nécessité beaucoup de lignes de code.
David Chou
La deuxième partie est le triage, qui consiste à réfléchir à la manière d'exploiter l'API Prompt et l'appel de fonction pour déterminer le type d'informations nécessaires.
OK.
Pour créer l'architecture de ce système, nous allons d'abord configurer des instructions système.
Ensuite, nous allons également transmettre le message vocal, le texte et la photo, ainsi que la question de l'utilisateur, en entrée à Gemini Nano.
Nous allons ensuite définir des fonctions qui récupèrent les informations dont nous avons besoin.
retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)Pour commencer, nous allons configurer les instructions système.
// 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()
)Voici un exemple très simple de ce à quoi ressemblerait la configuration d'un auditeur de sécurité. Il est indiqué que nous allons vérifier l'emballage et les étiquettes, examiner les frais de douane et signaler les demandes de paiement nécessaires.
Vous constaterez peut-être que l'optimisation des prompts vous sera très utile pour obtenir une qualité nettement supérieure.
Une fois que nous avons défini cet ensemble d'instructions système, nous pouvons prendre tout le contenu de l'étape précédente, la traduction du message vocal et du texte, ainsi que la question de l'utilisateur, et transmettre le tout dans ce champ TextPart, avec les instructions système, pour obtenir ces informations.
// 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 */ }Ensuite, nous voulons définir une fonction que nous pouvons utiliser.
Pour cet exemple particulier, nous sommes heureux d'annoncer que nous avons intégré la fonction d'application.
Si vous connaissez déjà ce concept, vous comprendrez probablement comment cela fonctionne.
Ici, nous configurons un point d'entrée AppFunctionService, puis nous définissons une fonction d'application.
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}Comme vous pouvez le voir, la procédure est exactement la même que pour configurer les fonctions d'application d'un agent système.
Vous définissez dans les paramètres de searchQuery les frais de réclamation, et si nous devons comparer les photos des colis ou non, par exemple.
// 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) }
}Vous pouvez en définir d'autres.
Enfin, pour cette fonction, nous avons une fonction executeVaultSearch que nous définirons plus tard.
Je tiens à souligner que le mode Réflexion est disponible.
val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()Je vous recommande vivement de l'utiliser lorsque vous exécutez des appels d'outils.
En effet, nous avons effectué de nombreux tests qui montrent une amélioration de 13 points sur 100 de la précision lors de l'exécution d'appels d'outils sur Gemini Nano 4.
Dans certains cas, il est même nettement plus élevé, ce qui peut vous donner de bien meilleurs résultats.
Si vous souhaitez en savoir plus sur les appels de fonction, assistez à la session de mes collègues Jolanda et Victoria, qui aura lieu plus tard.
Il s'agit des capacités agentiques de votre application.
Je pense que Victoria est peut-être assise quelque part dans le public, alors n'hésitez pas à la saluer également.
David Chou
Super.
Nous avons terminé les deux premières étapes.
Voyons comment fonctionne la récupération.
Nous allons donc utiliser les embeddings et AppSearch.
En revenant un peu en arrière dans le parcours utilisateur, au moment où il a commandé l'objectif, nous voudrions indexer cette information pour qu'elle soit accessible à l'utilisateur et au développeur.
Ici, nous avons des informations détaillées sur le numéro de colis de la commande.
Il comporte également un numéro de suivi.
Il contient des informations sur l'endroit où il est envoyé.
Comme vous pouvez le voir, il est déjà indiqué ici que les droits de douane ont été prépayés.
Nous allons donc générer un embedding multimodal afin d'indexer ces informations, ainsi que toutes les autres informations qui pourraient être importantes pour nous.
Voici à quoi cela ressemble.
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.vectorNous prenons l'e-mail de commande.
Là encore, c'est très simple : nous ne transmettons qu'une image et un texte, mais vous pouvez avoir plus de contenu à indexer ici.
Nous allons ensuite générer le vecteur d'embedding.
Ensuite, nous allons utiliser AppSearch pour créer un document, puis définir les propriétés que nous jugeons nécessaires pour générer ce document spécifique.
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()Notez que nous voulons effectuer des embeddings multimodaux, car il n'y a peut-être pas de mots partagés ici, mais comme les images et le texte portent sur un contenu similaire, cela nous aide à obtenir la similarité sémantique.
Nous pouvons donc utiliser la similarité cosinus pour trouver des éléments qui n'ont aucun mot en commun.
Il s'agit de la fonction executeVaultSearch dont nous avons parlé précédemment.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}C'est ce qui est appelé par la fonction d'application que nous avons définie.
Dans cet exemple, nous transmettons la requête de recherche générée par Gemini Nano.
Nous allons ensuite utiliser une stratégie de classement, la similarité cosinus.
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()
}this.matchedSemanticScores nous aide ensuite à obtenir ces informations.
Enfin, nous allons spécifier un seuil de 0, 68 pour déterminer la similarité cosinus minimale permettant de récupérer des informations.
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() }
}Vous pouvez donc tester cette option lorsque vous exécutez vos propres cas d'utilisation pour voir ce qui vous convient le mieux.
Un autre point important à souligner concernant la sécurité de ce modèle est que Gemini Nano n'exécute pas le code.
Gemini Nano ne fait donc que poser une question, mais votre application définit le code, puis l'exécute, récupère ces informations et détermine ce qu'il faut transmettre à Gemini Nano.
Vous avez ainsi plus de contrôle sur ce que vous souhaitez exposer à Gemini Nano, et vous pouvez garantir une meilleure confidentialité et une sécurité renforcée à vos utilisateurs.
David Chou
Passons maintenant à la dernière étape.
Nous disposons donc de toutes les informations dont nous avons besoin.
Nous allons utiliser l'API Prompt, la sortie structurée et la synthèse vocale pour effectuer les comparaisons, puis renvoyer ces informations à l'utilisateur de manière compréhensible.
En gros, ce que nous voulons savoir, c'est si ce colis correspond bien aux informations fournies par l'utilisateur, qui représentent la vraie commande.
Pour ce faire, nous allons également générer un embedding à partir des informations du message vocal et du texte, et le transformer en un vecteur d'embedding identique à celui que nous avons créé précédemment.
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)Ensuite, nous allons utiliser matchScore. En utilisant la similarité cosinus, nous pouvons comparer la correspondance que nous avons récupérée dans le coffre-fort avec les informations de l'appelant.
Comme vous pouvez le voir ici, même si les images ne correspondent pas, le texte reste très utile.
C'est là que les embeddings multimodaux sont très utiles.
Vous pouvez saisir des informations disparates, puis obtenir une signification sémantique partagée, n'est-ce pas ?
L'embedding multimodal vous permet donc de projeter toutes ces données dans le même espace vectoriel.
Là encore, nous allons utiliser le mode de réflexion pour améliorer notre raisonnement.
val forensicRequest = generateContentRequest(
securitySystemInstruction,
content {
image(callerParcelPhotoBmp)
image(orderEmailLensBmp)
text(forensicAuditPrompt)
}
) {
// Enable Gemini Nano 4 reasoning before it answers
enableThinking = true
}Nous allons également définir une classe de sortie.
@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,
)Ici, nous configurons simplement un VisualContradiction et quelques énumérations des différents types de contradictions possibles.
Qu'il s'agisse d'un DUTY_FRAUD, d'un PACKAGING_MISMATCH ou d'un STATUS_CONFLICT.
Nous allons ensuite ajouter une explication ici.
Nous allons ensuite transmettre toutes ces informations à un verdict d'audit.
@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,
)Comme vous pouvez le voir, c'est très simple.
Nous vous demandons simplement s'il s'agit d'une escroquerie ou non.
Nous avons peut-être un fraudRiskScore où vous pouvez définir une valeur minimale et une valeur maximale de 0 à 100.
Vous pouvez même indiquer à l'utilisateur le montant que vous lui avez fait économiser si vous souhaitez en garder une trace au fil du temps.
Vous pouvez également lister les contradictions visuelles que nous venons de définir.
Notez également que l'annotation @Guide est facultative.
Vous n'avez pas nécessairement besoin de le définir et de fournir ce type de description.
Cela est utile, car vous ne pourrez peut-être pas expliquer ce qui se passe uniquement à l'aide de la définition de la variable.
Cette description peut donc aider Gemini Nano à déterminer les fonctions spécifiques à appeler et à les configurer de manière à saisir les bonnes informations dans les bons champs.
Une fois que vous l'avez configuré, c'est très simple.
Il vous suffit d'exécuter trois lignes de code ici.
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().responsegenerateTypedContentRequest, vous générez ce contenu, puis vous pouvez récupérer cette réponse.
Voici un exemple de ce à quoi cela ressemblerait.
{
"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."
}Nous confirmons donc qu'il s'agit d'une escroquerie, nous obtenons un score de risque de fraude très élevé, ce qui explique les contradictions évidentes entre les données que vous avez récupérées et les informations sur l'appelant.
Nous avons également permis à cet utilisateur d'économiser une certaine somme d'argent, que nous pouvons suivre.
Oui.
Cela résume en quelque sorte toutes les informations dont nous avons besoin.
La dernière étape consiste à présenter ces informations à l'utilisateur de manière accessible.
Par exemple, si l'utilisateur est en déplacement ou s'il est malvoyant, il peut être préférable de lire ces informations à voix haute plutôt que de lui demander de lire le texte.
Nous allons donc implémenter la fonctionnalité de synthèse vocale pour montrer à quoi cela ressemblerait.
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
}Là encore, c'est très simple : nous spécifions des paramètres régionaux et une langue, puis nous transmettons le verdict que nous venons de générer à l'aide de @Generable et @Guide.
Une fois que nous l'avons, c'est presque instantané.
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
}Sur la plupart des appareils, la génération de cette réponse prendra moins de 120 ms.
Nous effacerons les informations mémorisées nécessaires.
Si nécessaire, nous pouvons également définir une solution de remplacement à l'aide des API de la plate-forme.
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 ...La voix robotique est toujours présente, mais l'utilisateur dispose au moins d'un élément avec lequel interagir s'il ne peut pas lire.
David Chou
OK.
C'était beaucoup, mais pour récapituler ce que nous avons fait, nous avons exploité toutes ces différentes API dans les processus d'ingestion, de tri, de récupération et d'audit.
Par exemple, en utilisant les API de reconnaissance vocale et de traduction, l'API Prompt et l'appel de fonction, les embeddings et AppSearch, ainsi que la sortie structurée et la synthèse vocale.
Il s'agit d'un exemple de la façon dont vous pouvez exploiter toutes ces fonctionnalités de manière cohérente et les exécuter entièrement sur l'appareil.
Pour revenir à ce que nous avons dit au début, l'exécution de cette opération présente des avantages considérables, n'est-ce pas ?
Si vous l'exécutiez sur votre appareil aujourd'hui, vous verriez que l'exécution sur l'appareil est beaucoup plus rapide sur la plupart des matériels.
Cela ne vous coûte rien, et vous pouvez potentiellement économiser des centaines de milliers de dollars si vous l'exécutez en permanence.
Et cela est disponible, n'est-ce pas, que l'utilisateur soit en montagne ou dans un sous-sol et n'ait pas accès à Internet.
Et elle est entièrement privée, car vous n'avez pas besoin d'envoyer d'informations vers le cloud.
Compte tenu de toutes ces fonctionnalités, n'hésitez pas à consulter la documentation, à scanner le QR code ou à cliquer sur le lien.
Il s'agit de toutes les fonctionnalités que nous avons lancées au cours des six derniers mois environ.
Merci à tous d'avoir pris le temps d'assister à cette présentation.
Merci d'avoir pris le temps de nous contacter.
Merci beaucoup.
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 ...