Android XR

Créer des expériences audio pour les lunettes connectées avec Gemini Nano 4

Haut-parleurs

À propos

À propos de l'intervention

Découvrez comment créer des expériences audio robustes et mains libres optimisées par l'IA sur l'appareil. Nous aborderons tout ce dont vous avez besoin : configurer l'activité de vos lunettes, capturer les flux du micro et de la caméra, et utiliser la voix ainsi que d'autres signaux audio comme interface utilisateur ultime. Grâce à une démonstration concrète et passionnante de l'application Open Source XR Language Teacher, intégrée à l'application Google AI Edge Gallery, apprenez à orchestrer Gemini Nano 4 (basé sur Gemma 4 et optimisé pour les appareils Android) et ML Kit, et à exploiter l'environnement d'exécution LiteRT pour créer des expériences XR rapides qui entendent, voient et parlent naturellement.

Résumé

  • La bibliothèque Jetpack Projected (alpha) permet de connecter les téléphones Android à des lunettes connectées. L'ajout d'une activité projetée à votre application existante projette du contenu sur des lunettes avec écran ou audio uniquement.
  • ML Kit lance des bibliothèques vocales alpha sur l'appareil pour les interactions audio sur certains appareils. Il propose la synthèse vocale avec huit voix dans 21 paramètres régionaux et la reconnaissance vocale avec un mode avancé Gemini Nano sur le Pixel 10.
  • Gemini Nano 4 traite les images de la caméra et les questions transcrites ensemble en une seule passe. Il permet d'utiliser des fonctionnalités hors connexion en enchaînant localement des modèles vocaux sur l'appareil sur les téléphones phares compatibles.
  • Une conception en deux passes donne la priorité à la latence audio en temps réel sur les lunettes connectées. La première passe diffuse le texte parlé en utilisant des balises XML intégrées pour changer de langue au milieu d'une phrase, tandis que la deuxième passe extrait les données structurées en arrière-plan.
  • L'implémentation Open Source de XR Language Teacher est disponible dans l'application Google AI Edge Gallery. Les développeurs peuvent adapter son code à d'autres cas d'utilisation d'assistant multimodal.

Ressources

Gemini Notebook

Thèmes

  • intelligent-eyewear
  • android-xr
  • gemini-nano
  • on-device-ai
  • ml-kit
  • reconnaissance vocale
  • synthèse vocale

Transcription

Introduction

  • Bonjour et bienvenue à tous.

  • C'est un plaisir de vous voir aujourd'hui.

  • Merci d'être venu.

  • Je m'appelle Amy.

  • Aujourd'hui, je suis accompagné de ma collègue Alice. Nous allons vous expliquer comment créer des expériences audio incroyables pour les lunettes connectées à l'aide de Gemini Nano 4.

  • Nous allons pouvoir commencer.

  • Quand nous parlons de lunettes connectées, nous faisons référence à des lunettes portables et légères, conçues pour être portées toute la journée.

  • Cette catégorie inclut à la fois les lunettes avec écran et les lunettes audio uniquement. Ces appareils présentent des caractéristiques matérielles variées.

  • La première chose que vous remarquerez, c'est qu'ils sont équipés d'un pavé tactile, qui peut être utilisé pour les entrées utilisateur, comme les appuis et les balayages.

  • Ils sont également équipés de haut-parleurs, d'une caméra et d'un micro.

  • Ils peuvent également être équipés d'un écran ou non, ou d'une combinaison de ces éléments.

  • Aujourd'hui, nous allons parler de l'audio-first et de la façon de créer des expériences intelligentes, engageantes et mobiles.

  • L'un des aspects intéressants de l'audio seul est que la voix devient votre UI.

  • L'IA sur l'appareil est idéale pour les développeurs qui souhaitent proposer des expériences hors connexion et en déplacement.

Développer pour Android XR

  • Et bonne nouvelle :

  • Si vous créez déjà des applications pour Android, vous les créez donc déjà pour Android XR.

  • Les applications Android existantes sont conçues pour s'adapter facilement aux lunettes audio, en diffusant des notifications vocales et d'autres contenus audio via Bluetooth.

  • Et si vous souhaitez aller plus loin et différencier votre application pour la XR, il existe de nombreuses façons de le faire.

  • Dans cette présentation, nous parlons des modèles sur l'appareil, mais cette liste n'est en aucun cas exhaustive.

  • Il existe également des modèles cloud.

  • Il existe des outils comme l'API Gemini Live.

  • Vous pouvez intégrer des agents système, comme Gemini, et tous ces éléments peuvent vous aider à créer l'expérience qui vous convient le mieux.

  • Alors, comment créer cette expérience qui entend, voit et parle naturellement ?

  • Aujourd'hui, nous allons vous expliquer comment créer ces expériences mains libres robustes, optimisées par l'IA sur l'appareil.

  • Je vais vous expliquer les bases : comment créer une application pour des lunettes, comment configurer votre activité, comment obtenir les autorisations dont vous avez besoin, puis comment utiliser ML Kit pour l'UI conversationnelle.

  • Je laisserai ensuite la parole à Alice, qui vous présentera des exemples concrets d'utilisation de Gemini Nano 4.

  • Examinons le code.

  • Avec les lunettes, c'est le téléphone qui fait le gros du travail, et le contenu des lunettes est projeté depuis le téléphone vers les lunettes.

  • Vous avez donc besoin de deux éléments pour connecter ces appareils lors de la configuration initiale.

  • Vous aurez d'abord besoin de la bibliothèque Jetpack Projected.

  • Vous devrez ajouter cette dépendance, qui contiendra les API permettant d'alimenter cette expérience projetée.

    dependencies {
       implementation("androidx.xr.projected:projected:1.0.0-alpha13")
    }
  • Vous devrez également créer une activité spécifiquement pour les lunettes.

    class ProjectedMainActivity : ComponentActivity() {
       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           setContent {
               // Glasses content
           }
       }
    }
  • Vous allez ensuite exploiter cette bibliothèque Jetpack Projected à la fois dans votre application existante et dans cette nouvelle activité, afin que les deux appareils puissent communiquer de manière fluide chaque fois que nécessaire.

  • Comme pour toute activité normale, vous allez la déclarer dans votre fichier manifeste.

    <activity
       android:name=".ProjectedMainActivity"
       android:exported="false"
       android:requiredDisplayCategory="android.hardware.display.category.XR_PROJECTED"
       android:label="Example activity for audio glasses">
       <intent-filter>
           <action android:name="android.intent.action.MAIN" />
           <category android:name="android.intent.category.XR_PROJECTED_LAUNCHER"/>
       </intent-filter>
    </activity>
  • La principale différence est que vous devez utiliser XR_PROJECTED comme requiredDisplayCategory.

  • Cela permet au système de savoir que cette activité concerne des lunettes.

  • Vous devez également utiliser la catégorie d'intention XR_PROJECTED_LAUNCHER.

  • Cela permet à Gemini de découvrir votre application pour que les utilisateurs puissent la lancer en mode mains libres lorsqu'ils sont en déplacement.

  • Très bien.

  • Une fois cette activité configurée, comme je l'ai mentionné, vous utiliserez la bibliothèque Projected pour optimiser l'expérience dans l'application.

  • Cette bibliothèque contient tous les outils dont vous avez besoin pour créer l'expérience complète des lunettes.

  • Passons en revue quelques-unes des principales API que vous allez utiliser ici.

  • La première méthode que je souhaite présenter est isProjectedDeviceConnected.

    // Continuously monitor whether the glasses are connected
    val isGlassesConnected by ProjectedContext.isProjectedDeviceConnected(
       context,
       scope.coroutineContext
    ).collectAsStateWithLifecycle(initialValue = false)
    
    // Launch the activity
    val options = ProjectedContext.createProjectedActivityOptions(context)
    val intent = Intent(context, ProjectedMainActivity::class.java)
    context.startActivity(intent, options.toBundle())
    
    // Check if the device is audio only or has a display
    val projectedDeviceController = ProjectedDeviceController.create(this@ProjectedMainActivity)
    isVisualUiSupported = projectedDeviceController.capabilities.contains(CAPABILITY_VISUAL_UI)
  • C'est ainsi que vous allez surveiller en permanence la connexion des lunettes.

  • Vous l'utilisez avec un cycle de vie de l'appareil pour savoir quand les lunettes sont réellement disponibles, c'est-à-dire non seulement lorsqu'elles sont associées ou allumées, mais aussi, par exemple, lorsqu'elles sont portées et prêtes à l'emploi.

  • Et c'est ce que, donc, oui.

  • Vous saurez ainsi exactement quand vous serez prêt à lancer cette activité.

  • Pour ce faire, lorsque vous êtes prêt à lancer l'activité, vous devez définir des options spécifiques au démarrage de l'activité.

  • Pour cela, vous allez utiliser le createProjectedActivityOptions sur le ProjectedContext.

  • Ensuite, vous l'utiliserez avec le contexte de l'intent.

  • Un autre outil que vous allez vouloir utiliser est un ProjectedDeviceController.

  • Cela vous permettra de vérifier le type de lunettes utilisé par l'utilisateur.

  • Comme je l'ai dit précédemment, les lunettes connectées incluent des lunettes avec écran et des lunettes audio uniquement. Vous devez donc vérifier si les fonctionnalités contiennent CAPABILITY_VISUAL_UI. Cela vous permettra d'adapter votre application de manière fluide à différents appareils et de proposer une expérience optimisée pour chaque appareil.

  • Lorsque vous créez cette expérience différenciée, vous pouvez avoir besoin d'accéder au matériel de l'appareil, par exemple pour utiliser la caméra des lunettes.

  • Pour ce faire à partir d'une activité projetée, vous devez demander les autorisations appropriées à l'aide de ProjectedActivityCompat.

    // In a projected activity
    private fun requestProjectedPermissions() {
       lifecycleScope.launch {
           try {
               ProjectedActivityCompat.requestPermissions(
                   this@PermissionsFromProjectedActivity,
                   arrayOf(Manifest.permission.CAMERA),
                   // CAMERA_PERMISSION_REQUEST_CODE is a developer defined constant
                   requestCode = CAMERA_PERMISSION_REQUEST_CODE,
               )
           } catch (e: IllegalStateException) {
               // Thrown when the projected system service can't be bound.
               Log.e(TAG, "Request failed: projected service unavailable.", e)
           }
       }
    }
  • Même si votre application dispose déjà des autorisations nécessaires pour le téléphone, vous devez également demander les autorisations spécifiques pour le matériel des lunettes.

  • Lorsque vous travaillez avec des autorisations, n'oubliez pas de suivre les bonnes pratiques pour Android en général.

  • Déclarez les autorisations nécessaires dans le fichier manifeste, demandez-les dans le contexte où elles sont requises et ne demandez que celles dont vous avez réellement besoin pour que votre application fonctionne.

  • Bien sûr, nous avons documenté cela sur notre site developer.android.com.

  • Mais nous avons également une toute nouvelle compétence, la compétence matérielle projetée.

  • Vous pouvez l'utiliser avec la CLI Android dans Android Studio pour configurer votre workflow d'autorisations, et vous pouvez accéder à cette compétence, ainsi qu'à toutes les autres compétences Android.

    android skills add projected-hardware
    android skills add --all

Concevoir des expériences audio

  • Maintenant que nous sommes connectés, que nous connaissons les capacités de notre matériel et que nous disposons des autorisations appropriées, parlons des outils dont vous disposez pour créer cette expérience audio.

  • Lorsque nous concevons une expérience audio, nous voulons nous assurer de créer une expérience de haute qualité pour les utilisateurs.

  • Cela peut inclure quelques interactions clés.

  • La première consiste à utiliser la voix comme sortie, ce qui permet à votre voix de s'adresser naturellement à l'utilisateur.

  • Ensuite, vous pouvez utiliser des earcons pour envoyer des signaux audio spécifiques à l'utilisateur.

  • Vous devez également gérer les entrées de l'utilisateur, qu'il s'agisse d'appuis et de balayages sur le pavé tactile, ou de commandes vocales.

  • Nous allons d'abord parler de l'utilisation de la voix comme sortie.

  • Pour cela, je souhaite vous présenter la nouvelle bibliothèque GenAI Text-to-Speech de ML Kit.

  • Cette fonctionnalité est désormais disponible en version alpha sur certains appareils Pixel et Samsung.

  • Nous sommes très enthousiastes à ce sujet, en particulier pour les expériences audio.

  • Si vous avez déjà utilisé la synthèse vocale, vous savez probablement que l'un des problèmes est que la voix du système ne semble pas toujours aussi naturelle que vous le souhaiteriez.

  • De plus, la connexion à des modèles cloud pour d'autres voix peut entraîner une latence ou des coûts supplémentaires.

  • ML Kit vous offre donc une voix beaucoup plus naturelle que celle du système TTS.

  • ML Kit propose huit voix uniques parmi lesquelles vous pouvez choisir, chacune ayant des caractéristiques différentes pour s'adapter à votre application.

  • De plus, ML Kit offre une compatibilité linguistique robuste avec 21 paramètres régionaux différents.

  • De plus, l'un des principaux avantages est qu'après ce téléchargement initial, l'application fonctionnera entièrement sur l'appareil.

  • Ce modèle sera donc téléchargé sur le système et partagé entre les applications. Il n'augmentera donc pas la taille de votre APK.

  • Et non seulement il est disponible hors connexion, ce qui est particulièrement important pour les utilisateurs de lunettes qui sont en déplacement, qui n'ont peut-être pas de Wi-Fi ou qui se trouvent dans des zones de faible couverture, mais vous obtiendrez probablement de meilleures performances, car vous n'avez pas besoin d'effectuer cet appel aller-retour vers un serveur.

  • Très bien.

  • Examinons donc les détails de la bibliothèque et comment l'utiliser.

  • Tout d'abord, vous devez bien sûr ajouter la dépendance.

    dependencies {
       // ML Kit GenAI TTS SDK
       implementation("com.google.mlkit:genai-text-to-speech:1.0.0-alpha1")
    }
  • Ici, vous n'avez besoin que du SDK Text-to-Speech de ML Kit.

  • Vous pouvez alors commencer à rédiger du code.

  • La bibliothèque ML Kit GenAI inclut donc les API Speech Synthesizer.

    // 1. Initializing the SpeechSynthesizer client.
    val options = SpeechSynthesizerOptions.Builder(Locale.US).build()
    val client = SpeechSynthesis.getClient(options)
    
    // 2. Check and download the voice model.
    val voiceInfo = client.getSupportedVoices().firstOrNull()
    
    if (voiceInfo != null && client.checkStatus(voiceInfo) == FeatureStatus.DOWNLOADABLE) {
       client.download(voiceInfo).collect { status -> println("Download status: $status") }
    } else if (voiceInfo == null) {
       // Tip: Fallback to system TTS if ML Kit voice is unavailable
    }
  • Vous pouvez ainsi implémenter facilement les voix de votre choix.

  • La première étape consiste à initialiser votre client à l'aide de ce synthétiseur vocal, en définissant les paramètres régionaux de votre choix.

  • Ensuite, vous allez vérifier l'état du téléchargement et sélectionner la voix.

  • Dans cet exemple, nous choisissons la première voix, mais je tiens également à souligner que nous utilisons firstOrNull.

  • Comme je l'ai mentionné, ML Kit nécessite un téléphone compatible spécifique, comme un appareil Samsung ou Pixel.

  • Si vous obtenez une valeur nulle, cela peut signifier que l'utilisateur n'utilise pas un téléphone compatible.

  • Ensuite, vous devez vous assurer que vous disposez d'une solution de remplacement, qu'il s'agisse de revenir à la synthèse vocale du système, de lire un message enregistré ou autre.

  • Ensuite, vous utiliserez la synthèse vocale SpeechSynthesizerRequest pour créer la requête, en lui fournissant le texte d'entrée, puis en définissant l'utilisation et le type de contenu.

    // 3. Construct the SpeechSynthesizerRequest
    val request = SpeechSynthesizerRequest(
       input = SpeechSynthesizerRequest.Input.Text("Hello world", voiceInfo),
       audioAttributes = AudioAttributes.Builder()
           .setUsage(AudioAttributes.USAGE_MEDIA)
           .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
           .build()
    )
    
    // 4. Speak and collect status
    client.synthesize(request).collect { response ->
       when (response) {
           is SpeechSynthesizerResponse.StatusUpdate -> println("Playback status: ${response.status}")
       }
    }
  • Lisez le discours, surveillez l'état et, non, pour savoir quand le discours se termine, puis collectez les erreurs qui pourraient se produire.

  • Ensuite, fermez toutes les ressources si nécessaire.

    // 4. Speak and collect status
    client.synthesize(request).collect { response ->
       when (response) {
           is SpeechSynthesizerResponse.StatusUpdate -> println("Playback status: ${response.status}")
       }
    }
    
    // 5. Release resources when finished (e.g., on Activity destroy)
    override fun onDestroy() {
       super.onDestroy()
       client.close()
    }
  • C'est assez simple, n'est-ce pas ?

  • En suivant ces étapes, vous pouvez vraiment transformer le son de votre application pour passer de quelque chose qui ressemble à ceci…

  • Bonjour, développeurs Android.

  • C'est un plaisir de vous voir tous aujourd'hui.

  • Nous espérons que vous appréciez notre présentation sur les expériences audio-first.

  • à quelque chose qui ressemble davantage à ceci.

  • Bonjour à tous les développeurs Android.

  • C'est un plaisir de vous voir tous aujourd'hui.

  • Nous espérons que vous appréciez notre présentation sur les expériences audio-first.

  • C'est beaucoup mieux, n'est-ce pas ?

  • OK.

  • La bibliothèque Text to Speech de ML Kit est un outil très puissant, mais vous n'êtes pas limité à l'utilisation de ces voix. Vous pouvez également lire des earcons avec cette bibliothèque.

    // 1. Register Earcon resource with Builder
    SpeechSynthesizerOptions.Builder(Locale.US)
       .addEarcon(
           name = "shutter_sound",
           Uri = "path/to/earcon.file".toUri()
       )
       .build()
    
    // 2. Request Earcon playback
    val request = SpeechSynthesizerRequest(
       input = SpeechSynthesizerRequest.Input.Earcon(
           name = "shutter_sound"
       ),
       audioAttributes = AudioAttributes.Builder()
           .setUsage(AudioAttributes
               .USAGE_ASSISTANCE_SONIFICATION)
           .setContentType(AudioAttributes
               .CONTENT_TYPE_SONIFICATION)
           .build()
    )
  • Comme leur nom l'indique, les earcons sont l'équivalent audio des icônes.

  • Cela vous permettra de lire des sons courts pour communiquer avec votre utilisateur.

  • Et tout comme pour les icônes visuelles avec les earcons, l'utilisateur doit savoir ce qu'ils sont et ce qu'ils signifient.

  • Au début, vous pouvez donc les associer à des notifications vocales pour que ce soit clair.

  • Nous avons déjà créé des sons standardisés disponibles sur Android Design Hub. Vous pouvez les télécharger directement depuis cette page et les utiliser pour des actions telles que l'ouverture ou la fermeture d'une application, les notifications d'erreur, ou l'activation ou la désactivation du son.

  • Vous pouvez également créer vos propres earcons personnalisés.

  • Nous vous recommandons de suivre certaines bonnes pratiques, que vous utilisiez les earcons téléchargés depuis le Design Hub ou que vous créiez les vôtres.

  • Vous devez donc réfléchir à la manière dont vous utilisez les earcons et à la façon dont vous les utilisez.

  • Voici quelques points à garder à l'esprit. Pour commencer, essayez de ne pas abuser des earcons.

  • Ne lisez pas les mêmes sons en boucle, car cela peut provoquer ce que l'on appelle une fatigue auditive.

  • N'oubliez pas que vos earcons doivent être simples et intuitifs. Utilisez également des tonalités et des rythmes familiers lorsque vous les implémentez.

  • Ensuite, essayez d'éviter les earcons trop complexes qui nécessitent des explications. Il est préférable de les remplacer par une notification vocale.

  • N'oubliez pas que ce qui peut sembler subtil en studio ou sur votre système, sur les haut-parleurs de votre ordinateur, peut devenir irritant après une lecture répétée pour vos utilisateurs.

  • Faites donc de votre mieux pour ne pas surcharger vos utilisateurs avec des sons qui pourraient les dérouter ou les amener à ignorer des alertes importantes.

  • Très bien.

  • Nous pouvons désormais fournir des signaux audio et des commentaires, que ce soit par le biais de la parole naturelle ou d'earcons.

  • Mais qu'en est-il de la saisie ?

  • Il existe bien sûr un pavé tactile, ou des boutons dédiés sur l'appareil, que vous pouvez utiliser pour créer des interactions de type "appuyer" et "balayer".

  • Mais vous pouvez aussi vouloir autoriser cette UI conversationnelle. Pour cela, vous pouvez utiliser l'API GenAI Speech Recognition de ML Kit.

  • Actuellement en version alpha et disponible sur certains appareils, il fonctionne en deux modes distincts.

  • Le mode de base utilise la reconnaissance vocale traditionnelle sur l'appareil. Il est disponible sur les appareils Android avec le niveau d'API 31 ou supérieur.

  • Il existe également un mode avancé qui utilise Gemini Nano. Il est proposé sur les appareils Pixel 10 et modèles ultérieurs, et offre une meilleure qualité et une meilleure couverture linguistique.

  • SpeechRecognizer suit donc un modèle de code très similaire à SpeechSynthesizer.

  • Vous créez votre client, vérifiez si le modèle est disponible, le téléchargez si nécessaire, construisez la requête et libérez vos ressources.

  • Concentrons-nous donc sur les principales différences lorsque vous créez votre requête, capturez l'audio du micro, puis traitez le résultat.

  • Là encore, vous devrez ajouter les dépendances nécessaires.

    dependencies {
       // ML Kit Speech Recognition
       implementation("com.google.mlkit:genai-speech-recognition:1.0.0-alpha1")
    }
  • Ici, vous n'avez besoin que de l'API Speech Recognition.

  • Vous pouvez ensuite commencer à créer votre demande.

  • Vous utiliserez donc une fonction de suspension lorsque vous définirez votre logique de reconnaissance.

    suspend fun startMyRecognition() {
       // choose locale and mode
       val options: SpeechRecognizerOptions =
           speechRecognizerOptions {
               locale = Locale.US
               preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
           }
    
       val speechRecognizer: SpeechRecognizer = SpeechRecognition.getClient(options)
    
       val request: SpeechRecognizerRequest =
           speechRecognizerRequest { audioSource = AudioSource.fromMic() }
    ...
  • Spécifiez ensuite vos options de reconnaissance vocale en définissant les paramètres régionaux et le preferredMode.

  • C'est également ici que vous choisirez le audioSource et définirez la requête elle-même.

  • Ensuite, vous lancerez la reconnaissance et collecterez une réponse.

    speechRecognizer.startRecognition(request).collect { response ->
       when (response) {
           is SpeechRecognizerResponse.PartialTextResponse -> {
               // Real-time: Act immediately if trigger phrase is heard
               handleResponse(response.text, speechRecognizer)
           }
           is SpeechRecognizerResponse.FinalTextResponse -> {
               // Final result and recognition
               handleResponse(response.text, speechRecognizer)
           }
           is SpeechRecognizerResponse.CompletedResponse -> {
               resetListeningUi()
           }
           is SpeechRecognizerResponse.ErrorResponse -> {
               Log.e("SpeechRec", "Error: ${response.e.errorCode}", response.e)
               resetListeningUi()
           }
    ...
  • Ainsi, avec SpeechRecognizer, vous avez quatre réponses potentielles : partielle, finale, erreur et complète.

  • "Partial" examine le flux en temps réel. Il est donc utilisé pour les mots clés ou les mots d'activation.

  • "Final" prend le flux audio final après l'avoir entièrement analysé et mis à jour. "Completed" est utilisé une fois que le discours est terminé. "Error" collecte les erreurs qui peuvent se produire.

  • Voici comment nous combinons tous ces éléments.

    // Helper function to check for multiple phrases and trigger the right action
    private suspend fun handleResponse(text: String, recognizer: SpeechRecognizer) {
       val savePhrases = listOf("save", "bookmark", "remember")
       if (savePhrases.any { text.contains(it) }) {
    
           // 1. Update app state immediately
           saveItemForLater()
    
           // 2. Stop the recognizer to enforce the action and prevent TTS echo
           recognizer.stopRecognition()
    
           // 3. Play the mute earcon for immediate, non-verbal confirmation
           playEarcon()
    
           // 4. Provide voice feedback
           playAudioFeedback("Saved")
       }
    }
  • Imaginons que nous voulions qu'un utilisateur puisse utiliser une commande vocale pour demander à l'application d'enregistrer un certain type de contenu pour plus tard.

  • Vous pouvez utiliser une seule commande vocale explicite que vous leur avez indiquée ou leur permettre d'utiliser plusieurs mots clés et expressions.

  • Ici, nous autorisons l'utilisation de "save", "bookmark" ou "remember".

  • Dès que SpeechRecognizer détecte que l'utilisateur a prononcé l'une de ces phrases, il arrête l'enregistrement, enregistre le contenu, puis arrête la reconnaissance vocale, émet un signal sonore personnalisé et utilise la synthèse vocale GenAI de ML Kit pour indiquer à l'utilisateur que le contenu a été enregistré.

Étude de cas sur le Prof de langue en XR

  • OK.

  • Nous avons donc abordé de nombreux points.

  • Cela peut sembler un peu abstrait.

  • Si vous êtes comme moi, vous voulez voir des exemples concrets.

  • Alice et moi allons vous expliquer comment faire.

  • Elle va vous présenter le Prof de langue en XR dans l'application Google AI Edge Gallery.

  • L'équipe d'Alice l'a conçu, et elle va vous montrer exactement comment il fonctionne.

  • À vous de jouer.

  • Merci, Amy.

  • Oui.

  • J'aimerais commencer par quelque chose que beaucoup d'entre vous reconnaîtront.

  • J'apprends le japonais et j'ai passé des heures sur des applications d'apprentissage des langues, des fiches, des séries, etc.

  • Puis, j'atterris à Tokyo, j'entre dans un marché et je me rends compte que presque rien de ce que je vois ne correspond au vocabulaire que j'ai étudié.

  • Sur un étal de marché, je reconnais un bilboquet en bois, mais je ne sais pas comment le dire en japonais.

  • Il y a un bibelot dans une vitrine que je veux acheter, mais je ne sais pas comment il s'appelle.

  • C'est exactement à ce moment-là que je suis le plus motivé pour apprendre un mot.

  • Pas sur mon canapé à regarder une application linguistique.

  • C'est à ce moment-là que je regarde ce que je veux apprendre.

  • Voici ce que je veux.

  • J'aimerais porter mes lunettes connectées toute la journée, juste pour me promener.

  • Et quand quelque chose attire mon attention, je n'ai qu'à poser une question à voix haute.

  • Et le soir, quand je rentre à l'hôtel, tout ce que j'ai demandé m'attend sur mon téléphone, comme des fiches de vocabulaire.

  • Et voici ce que nous avons créé.

  • Regardons une courte vidéo.

  • C'est ce que nous appelons le Prof de langue en XR.

  • Bonjour !

  • Que souhaitez-vous apprendre aujourd'hui ?

  • Vous pouvez personnaliser votre expérience d'apprentissage dans les paramètres de l'enseignant.

  • Par exemple, choisissez la langue que vous souhaitez apprendre et le persona d'enseignant qui vous convient.

  • Oui.

  • Vous pouvez basculer entre le mode Lunettes et le mode Téléphone.

  • Comment dit-on "ces raisins" en espagnol ?

  • En regardant cela, ces délicieux raisins sont appelés uvas.

  • En espagnol, vous pouvez dire "Estas uvas son deliciosas" pour les décrire.

  • Je veux dire des raisins verts, plus précisément.

  • Je vais y jeter un œil.

  • Pour spécifier les raisins verts, vous pouvez dire "Uvas verdes.

  • Me gustan las uvas verdes" est une excellente façon de dire à quelqu'un que vous l'aimez bien.

  • Comment dit-on cela en français, et comment puis-je en demander trois ?

  • Regardons ça.

  • Ces délicieuses pâtisseries s'appellent des madeleines.

  • En français, pour en demander trois, vous pouvez dire : "Je voudrais trois madeleines, s'il vous plaît."

  • Comment demander une modification ?

  • Voyons ce que nous avons là.

  • Pour demander de la monnaie, vous pouvez dire "Otsuri wa arimasu ka ?".

  • Ce qui signifie "Avez-vous de la monnaie ?"

  • Vous pouvez également dire "Otsuri o onegaishimasu" pour demander poliment de la monnaie.

  • Ensuite, tout ce que vous avez appris est automatiquement enregistré sous forme de fiches de vocabulaire.

  • La botella de agua.

  • Esta es una botella de agua.

  • Oui, et vous pouvez tester votre mémoire et vous entraîner dans l'onglet "Étude".

  • Très bien.

  • Oui.

  • Tout ce que vous venez de voir s'est donc exécuté sur l'appareil, sans aucune connexion réseau.

  • Je voyage avec un forfait de données international, qui fonctionne généralement bien jusqu'à ce que je me retrouve dans un marché couvert, sous terre ou derrière un mur en béton épais.

  • C'est souvent au moment où vous en avez le plus besoin que le signal est le plus faible.

  • Et je dirais qu'une expérience qui s'arrête quand on n'a plus de réseau n'est pas un vrai compagnon de voyage. C'est pourquoi nous avons fait en sorte que XR Language Teacher fonctionne entièrement hors connexion et sur l'appareil.

Enchaîner des modèles sur l'appareil

  • Comment ce modèle a-t-il été créé ?

  • Amy vous a déjà montré comment utiliser un cadre de caméra projeté depuis des lunettes vers un téléphone.

  • Elle vous a montré comment utiliser ML Kit et la reconnaissance vocale pour convertir du texte en voix, et comment utiliser la synthèse vocale de ML Kit pour convertir du texte en voix.

  • Il nous manquait encore un cerveau au milieu, quelque chose qui puisse examiner à la fois l'image de la caméra et la question, et déterminer ce que l'utilisateur demande réellement.

  • Ce cerveau, c'est Gemini Nano 4, basé sur Gemma 4, le modèle Open Source de Google qui s'exécute localement sur le téléphone.

  • Ainsi, sur les appareils Android phares compatibles, vous pouvez obtenir des performances optimisées en utilisant directement les API ML Kit.

  • Le système Android se charge de la diffusion du modèle pour vous. Vous n'avez donc rien à intégrer dans votre application.

  • Pour résumer, le micro transmet le son à la reconnaissance vocale, et l'image de la caméra ainsi que la transcription sont envoyées à Gemini Nano 4. Gemini Nano 4 écrit ensuite dans la synthèse vocale, qui est diffusée par le haut-parleur.

  • Trois modèles, enchaînés, s'exécutant tous localement sur votre téléphone.

  • Voici quelques raisons pour lesquelles Gemini Nano 4 est particulièrement adapté à ce problème.

  • Tout d'abord, il voit et lit en une seule passe.

  • Grâce à ML Kit, Gemini Nano 4 gère le texte et les images de manière native. Vous n'avez donc pas besoin d'utiliser un détecteur, puis un classificateur, puis un traducteur.

  • Il s'agit d'un modèle qui examine à la fois l'image de la caméra et la question.

  • Deuxièmement, il prend en charge de nombreuses langues.

  • Gemini Nano 4 a été entraîné sur plus de 140 langues.

  • De plus, pour un tuteur linguistique, la traduction seule ne suffit pas.

  • Vous voulez des expressions idiomatiques, vous voulez des registres formels et informels, et vous voulez aussi des exemples de phrases qu'une personne réelle dirait.

  • Enfin, il est suffisamment orientable pour avoir une personnalité.

  • S'entraîner à prononcer à voix haute peut être intimidant.

  • Ce modèle suit bien un prompt système, ce qui permet à l'utilisateur de choisir son propre persona d'enseignant.

  • Et peut-être qu'un ton patient et encourageant pourrait inciter les gens à essayer.

  • Voici un autre point que j'aimerais souligner concernant la conception.

  • Pendant que la conversation se déroule en mode Lunettes, l'application crée discrètement des fiches de vocabulaire en arrière-plan.

  • Le mot, la traduction, l'exemple de phrase et la photo au moment où vous posez votre question.

  • L'utilisateur n'a pas besoin d'appuyer manuellement sur quoi que ce soit.

  • Ils marchent et discutent.

  • Plus tard dans la journée, tout est disponible sur leur téléphone, ce qui est exactement ce que je voulais quand j'étais à Tokyo.

  • "Audio d'abord" ne signifie pas "audio uniquement".

  • Les lunettes sont un bon support pour la curiosité, et votre téléphone est un bon support pour la réflexion qui s'ensuit.

  • Laissez une trace de ce moment mains libres.

  • Examinons maintenant quelques conseils pratiques pour créer des expériences en temps réel avec Gemini Nano 4 et ML Kit, que nous avons appris en créant cette démo.

Prioriser la latence en temps réel avec une conception en deux passes

  • Conseil 1 : Donnez la priorité à la latence en temps réel avec une conception en deux passes.

  • Sur des lunettes audio, lorsque vous demandez "Qu'est-ce que c'est ?"

  • Chaque moment de silence est perceptible.

  • Dans l'exemple de XR Language Teacher, nous avons besoin de deux choses pour chaque tour de l'interaction de l'utilisateur avec le modèle : une réponse orale immédiate et une fiche de vocabulaire structurée à enregistrer dans l'application.

  • Si nous demandons au modèle de faire deux choses à la fois en une seule passe, chaque jeton dépensé pour la génération de ces données d'arrière-plan retardera la première phrase prononcée.

  • Nous séparons donc ce dont l'utilisateur a besoin immédiatement de ce dont l'application a besoin quelques secondes plus tard.

  • Nous divisons donc chaque tour en deux passes.

  • Lors de la première passe, nous diffusons l'image de la caméra et la question pour la réponse vocale uniquement, en déclenchant la synthèse vocale dès que la première phrase est terminée.

  • Et comme ce TPU ou ce NPU finit de générer cette réponse courte bien avant que la synthèse vocale ne finisse de parler, nous disposons d'une fenêtre de plusieurs secondes pendant laquelle l'utilisateur écoute encore et la puce est déjà inactive.

  • Comme la passe 1 a déjà identifié l'objet dans sa réponse textuelle, la passe 2 n'a même pas besoin de l'image en entrée.

  • Nous transmettons simplement la réponse de la passe 1 à cette puce inactive pour extraire et enregistrer la fiche structurée avant même que l'enseignant ait fini de parler.

  • Maintenant, concentrons-nous sur la première passe, ce qui m'amène au deuxième conseil.

  • Utilisez des balises XML légères lors du streaming.

  • Étant donné que la première passe est diffusée directement vers la synthèse vocale, vous ne voulez pas qu'un objet JSON lourd retarde le premier mot prononcé.

  • Vous voulez plutôt du texte brut qui peut être lu immédiatement avec des balises intégrées légères et activées chaque fois que l'application doit effectuer une action en cours de lecture.

  • Laissez-moi vous montrer le problème qui nous a menés jusqu'ici.

  • Vous vous souvenez du bilboquet en bois sur l'étal du marché dont j'ai parlé ?

  • En japonais, on l'appelle "kendama".

  • Dès le début, le modèle l'a reconnu à partir de l'image de la caméra et a renvoyé la bonne réponse : "C'est un kendama, un jouet en bois."

  • Nous avons donc transmis cette phrase complète à la session de synthèse vocale en anglais, et voici le résultat.

  • C'est un ken-mi, un jouet en bois.

  • Ce n'est pas tout à fait ça.

  • À la place, cette phrase devrait plutôt ressembler à…

  • C'est un kendama, un jouet en bois.

  • Pourquoi cela s'est-il produit ?

  • "Kendama" s'écrit avec le hiragana "ken" et le kanji "dama".

  • Le moteur de synthèse vocale anglais gère très bien les hiraganas phonétiques comme "ken", mais échoue complètement lorsqu'il atteint la partie kanji et transforme "dama" en quelque chose qui ressemble à "me".

  • Cela se produit dans de nombreuses langues de différentes manières si vous ne changez pas de paramètre régional.

  • Par exemple, en espagnol, vous pourriez entendre…

  • Suh-BOH-luh…

  • alors que vous vous attendez à…

  • Cebolla.

  • De même, en français, vous pourriez entendre…

  • PAIN oh chock-uh-lat…

  • alors que vous attendez en fait…

  • Pain au chocolat.

  • Oui, une session TTS est généralement conçue pour un seul paramètre régional à la fois, mais un professeur de langue doit changer de code dans chaque phrase.

  • Ainsi, lors de la première passe, nous demandons au modèle d'encapsuler chaque mot ou expression de la langue cible dans la balise XML cible au fur et à mesure de la diffusion.

  • À mesure que les fragments arrivent, notre client les divise en fonction de la cible, puis les achemine vers les paramètres régionaux Text-to-Speech correspondants (japonais, espagnol, français ou toute autre langue que vous apprenez). Il les lit ensuite de manière fluide, en changeant d'accent au milieu de la phrase, de sorte que des mots comme "kendama" soient prononcés correctement.

  • En résumé, le modèle sait déjà quel vocabulaire appartient à quelle langue, et quels mots appartiennent à quelle langue. Le fait de lui faire taguer les segments cibles de manière intégrée pendant le streaming est bien plus propre que de post-traiter tout cela vous-même par la suite.

  • Voici la première passe dans le code.

    // 1. Pass 1 System + Turn Prompt (XrLanguageTeacherPersona.kt)
    // System: "Always start immediately with <reply>...</reply> (strictly 1 to 3 sentences total).
    //   Inside <reply>, wrap every Japanese word AND every complete Japanese example sentence
    //   inside <target>...</target> tags (never output <card>)."
    val pass1TurnPrompt = """
      The student is looking at the camera view and asked: "$question". Answer in English
      teaching Japanese inside <reply>...</reply>: if they refer to 'this'/'that' or point to
      an object, ground it to the focal item in the camera frame; if they ask for a phrase or
      greeting, teach that phrase.
    """.trimIndent()
    
    // 2. Stream multimodal dialogue (ImagePart + TextPart) and split <target> tags to TTS
    val generativeModel = Generation.getClient()
    val pass1Request = generateContentRequest(ImagePart(frame), TextPart(pass1TurnPrompt)) {
      temperature = 1.0f; topK = 1; maxOutputTokens = 1024
    }
    generativeModel.generateContentStream(pass1Request).collect { chunk ->
      val delta = chunk.candidates.firstOrNull()?.text.orEmpty()
      streamReplyAndSplitTargetTagsToTts(delta) // Speaks 1st complete sentence immediately
    }
    // Streams: "<reply>That's <target>けん玉</target> — a wooden toy. <target>けん玉をください。</target></reply>"
  • En haut, nous utilisons le prompt pour demander au modèle de répondre en une à trois phrases dans les balises de réponse, puis d'encapsuler chaque mot ou exemple de phrase en japonais dans les balises cibles.

  • Nous transmettons ensuite la caméra ImagePart et l'invite TextPart au generateContentStream de ML Kit, en divisant les segments cibles en synthèse vocale à mesure que chaque phrase arrive.

  • Cela m'amène à la deuxième passe et à notre troisième conseil.

  • Bien que l'utilisation de balises XML légères soit idéale pour un flux audio en direct, utilisez une sortie structurée, conçue pour renvoyer un objet typé propre lors de l'enregistrement de données dans votre application.

  • Comme notre Prof de langue en XR est conversationnel, les utilisateurs peuvent également poser des questions complémentaires sans qu'un nouveau mot soit enseigné.

  • Et lorsqu'un mot est enseigné, la base de données a besoin de champs de dictionnaire propres pour créer les fiches de vocabulaire.

  • Ainsi, au lieu d'écrire des expressions régulières fragiles pour analyser la transcription de la première passe, nous laissons la deuxième passe extraire un objet de type sécurisé en arrière-plan.

  • Pour appliquer le schéma à la passe 2, nous utilisons l'API de sortie structurée de ML Kit, qui utilise le décodage contraint en arrière-plan.

    import com.google.mlkit.genai.prompt.Generation
    import com.google.mlkit.genai.prompt.generateTypedContentRequest
    import com.google.mlkit.genai.schema.annotations.Generable
    import com.google.mlkit.genai.schema.annotations.Guide
    
    // Pass 2 schema (XrLanguageTeacherGenerableSchemas.kt):
    @Generable(description = "Vocabulary flashcard for the taught word or short phrase")
    data class GenerableVocabCard(
      @Guide(description = TeacherResponseSchema.WORD_FIELD_DESCRIPTION) val word: String,
      @Guide(description = "Native-language meaning or translation") val meaning: String,
      @Guide(description = "Target-language example sentence") val example: String,
      @Guide(description = "Native-language translation of the example sentence") val example_translation: String,
    )
    
    @Generable(description = "Optional vocabulary card extracted from a teacher reply")
    data class GenerableOptionalVocabCardWrapper(
      @Guide(description = "Vocabulary flashcard if a vocabulary term was taught, or null otherwise")
      val card: GenerableVocabCard?
    )
  • Il vous suffit d'annoter une classe de données avec @Generable et chaque champ avec @Guide, comme dans l'exemple de classe de carte de vocabulaire ci-dessous.

  • En encapsulant le champ de carte nullable, la deuxième passe renvoie une carte renseignée lorsqu'un nouveau mot a été enseigné, ou null pour les tours de parole purement conversationnels.

  • Voici le code de la deuxième passe.

    // 1. Pass 1 Streamed Output -> Pass 2 Extraction Prompt (no image needed!)
    val replyWithTargetTags =
      "<reply>That's <target>けん玉</target> — a wooden toy. <target>けん玉をください。</target></reply>"
    
    val pass2Prompt = """
      If the teacher reply below teaches or introduces a specific Japanese vocabulary item, extract
      only its core focal dictionary term into "card" with keys "word", "meaning", "example", and
      "example_translation". If purely conversational with no focal term taught, set "card" to null:
      $replyWithTargetTags
    """.trimIndent()
    
    // 2. Extract @Generable card on idle NPU while TTS audio plays
    val pass2Request = generateContentRequest(TextPart(pass2Prompt)) {
      temperature = 1.0f; topK = 1; maxOutputTokens = 256
    }
    val typedRequest =
      generateTypedContentRequest(pass2Request, GenerableOptionalVocabCardWrapper::class)
    val card: GenerableVocabCard? =
      generativeModel.generateContent(typedRequest).candidates.firstOrNull()?.response?.card
    // Extracts: {"word": "けん玉", "meaning": "a wooden toy", "example": "けん玉をください。", ...}
  • En haut, nous prenons le texte de la réponse diffusée en streaming de la passe 1, sans image, et nous créons notre prompt d'extraction.

  • Ensuite, pendant que l'utilisateur écoute encore la synthèse vocale, nous transmettons la requête et notre classe wrapper à generateTypedContentRequest, et nous extrayons une carte de vocabulaire propre et sans balise, ou une valeur nulle si aucun mot n'a été enseigné.

  • Très bien.

  • Nous avons abordé de nombreux sujets.

  • Vous vous dites peut-être : "Super, encore un truc qui a l'air génial sur scène, mais que je n'utiliserai jamais."

  • Pas celui-là !

  • XR Language Teacher est disponible dans l'application Google AI Edge Gallery, aux côtés d'autres expériences d'IA sur l'appareil.

  • Vous pouvez la télécharger dès aujourd'hui, l'installer sur votre téléphone et pointer l'appareil photo vers n'importe quel objet pour savoir comment le dire dans une autre langue.

  • Et si vous voulez savoir comment elle est conçue, l'intégralité de cette implémentation est Open Source sur GitHub.

  • Et ne vous inquiétez pas si tous vos utilisateurs ne disposent pas encore de lunettes audio ou avec affichage, ou d'appareils Android phares compatibles.

  • Notre code base vous montre également des exemples de la façon dont cette même expérience peut être adaptée pour une expérience mobile uniquement.

  • Si vous souhaitez également cibler d'autres plates-formes, le code de l'application Google AI Edge Gallery vous montre comment créer cette fonctionnalité à l'aide de l'environnement d'exécution Google LiteRT et des API MediaPipe Tasks.

  • Si vous souhaitez utiliser d'autres modèles audio ou de langage Open Source sur l'appareil, vous pouvez également voir comment procéder à partir du code.

S'adapter à d'autres cas d'utilisation avec des agents IA

  • Nous avons donc mis le Prof de langue en XR en Open Source pour faciliter votre prototypage.

  • Pointez l'agent de votre choix vers notre dépôt GitHub pour transformer cette expérience d'IA en direct sur l'appareil en quelque chose d'autre.

  • Il peut s'agir d'un guide de musée, d'un assistant de cuisine qui regarde votre plan de travail, d'un guide de réparation, d'un narrateur d'accessibilité ou d'un outil d'inspection sur le terrain.

  • Le modèle, l'environnement d'exécution, l'implémentation Open Source et les outils sont tous disponibles dès maintenant pour que vous puissiez vous préparer au lancement des lunettes intelligentes cet automne.

  • Nous serions ravis de découvrir ce que vous pouvez créer avec.

  • Pour conclure, nous avons vu aujourd'hui des exemples impressionnants de ce que les modèles d'IA exécutés directement sur l'appareil peuvent accomplir.

  • Voici quelques méthodes pour y parvenir.

  • Tout d'abord, récupérez les ressources à l'écran.

  • Téléchargez l'application AI Edge Gallery pour essayer la démo et consultez le code de référence sur GitHub.

  • Scannez les QR codes pour accéder au SDK Android XR, aux API ML Kit et aux offres Google AI Edge.

  • Deuxièmement, commencez à tester dès aujourd'hui sur votre téléphone.

  • Nous avons abordé de nombreux points qui peuvent déjà être intégrés à une expérience axée sur les mobiles.

  • Troisièmement, examinez l'application mobile que vous proposez déjà et demandez-vous quelles fonctionnalités essentielles seraient plus pratiques si elles étaient utilisables sans les mains.

  • Il vous suffit d'ajouter une Projected Activity à votre APK existant pour proposer cette expérience sur les lunettes.

  • Merci beaucoup d'avoir été parmi nous aujourd'hui.

  • Nous avons hâte de découvrir les incroyables expériences audio que vous allez créer sur Android et pour les lunettes connectées.

  • Merci.

Code

Extraits de code

Extrait de code Kotlin à 02:39 dans "Développer pour Android XR"
dependencies {
   implementation("androidx.xr.projected:projected:1.0.0-alpha13")
}
Extrait de code Kotlin à 02:45 dans "Développer pour Android XR"
class ProjectedMainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           // Glasses content
       }
   }
}
Extrait XML à 03:02 dans "Développer pour Android XR"
<activity
   android:name=".ProjectedMainActivity"
   android:exported="false"
   android:requiredDisplayCategory="android.hardware.display.category.XR_PROJECTED"
   android:label="Example activity for audio glasses">
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.XR_PROJECTED_LAUNCHER"/>
   </intent-filter>
</activity>
Extrait Kotlin à 03:44 dans "Développer pour Android XR"
// Continuously monitor whether the glasses are connected
val isGlassesConnected by ProjectedContext.isProjectedDeviceConnected(
   context,
   scope.coroutineContext
).collectAsStateWithLifecycle(initialValue = false)

// Launch the activity
val options = ProjectedContext.createProjectedActivityOptions(context)
val intent = Intent(context, ProjectedMainActivity::class.java)
context.startActivity(intent, options.toBundle())

// Check if the device is audio only or has a display
val projectedDeviceController = ProjectedDeviceController.create(this@ProjectedMainActivity)
isVisualUiSupported = projectedDeviceController.capabilities.contains(CAPABILITY_VISUAL_UI)
Extrait de code Kotlin à 05:05 dans "Développer pour Android XR"
// In a projected activity
private fun requestProjectedPermissions() {
   lifecycleScope.launch {
       try {
           ProjectedActivityCompat.requestPermissions(
               this@PermissionsFromProjectedActivity,
               arrayOf(Manifest.permission.CAMERA),
               // CAMERA_PERMISSION_REQUEST_CODE is a developer defined constant
               requestCode = CAMERA_PERMISSION_REQUEST_CODE,
           )
       } catch (e: IllegalStateException) {
           // Thrown when the projected system service can't be bound.
           Log.e(TAG, "Request failed: projected service unavailable.", e)
       }
   }
}
Extrait de code shell à 05:46 dans "Développer pour Android XR"
android skills add projected-hardware
android skills add --all
Extrait de code Kotlin à 08:14 dans "Designing audio-first experiences"
dependencies {
   // ML Kit GenAI TTS SDK
   implementation("com.google.mlkit:genai-text-to-speech:1.0.0-alpha1")
}
Extrait Kotlin à 08:26 dans "Designing audio-first experiences"
// 1. Initializing the SpeechSynthesizer client.
val options = SpeechSynthesizerOptions.Builder(Locale.US).build()
val client = SpeechSynthesis.getClient(options)

// 2. Check and download the voice model.
val voiceInfo = client.getSupportedVoices().firstOrNull()

if (voiceInfo != null && client.checkStatus(voiceInfo) == FeatureStatus.DOWNLOADABLE) {
   client.download(voiceInfo).collect { status -> println("Download status: $status") }
} else if (voiceInfo == null) {
   // Tip: Fallback to system TTS if ML Kit voice is unavailable
}
Extrait Kotlin à 09:26 dans "Designing audio-first experiences"
// 3. Construct the SpeechSynthesizerRequest
val request = SpeechSynthesizerRequest(
   input = SpeechSynthesizerRequest.Input.Text("Hello world", voiceInfo),
   audioAttributes = AudioAttributes.Builder()
       .setUsage(AudioAttributes.USAGE_MEDIA)
       .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
       .build()
)

// 4. Speak and collect status
client.synthesize(request).collect { response ->
   when (response) {
       is SpeechSynthesizerResponse.StatusUpdate -> println("Playback status: ${response.status}")
   }
}
Extrait Kotlin à 09:47 dans "Designing audio-first experiences"
// 4. Speak and collect status
client.synthesize(request).collect { response ->
   when (response) {
       is SpeechSynthesizerResponse.StatusUpdate -> println("Playback status: ${response.status}")
   }
}

// 5. Release resources when finished (e.g., on Activity destroy)
override fun onDestroy() {
   super.onDestroy()
   client.close()
}
Extrait de code Kotlin à 10:29 dans "Designing audio-first experiences"
// 1. Register Earcon resource with Builder
SpeechSynthesizerOptions.Builder(Locale.US)
   .addEarcon(
       name = "shutter_sound",
       Uri = "path/to/earcon.file".toUri()
   )
   .build()

// 2. Request Earcon playback
val request = SpeechSynthesizerRequest(
   input = SpeechSynthesizerRequest.Input.Earcon(
       name = "shutter_sound"
   ),
   audioAttributes = AudioAttributes.Builder()
       .setUsage(AudioAttributes
           .USAGE_ASSISTANCE_SONIFICATION)
       .setContentType(AudioAttributes
           .CONTENT_TYPE_SONIFICATION)
       .build()
)
Extrait de code Kotlin à 13:42 dans "Designing audio-first experiences"
dependencies {
   // ML Kit Speech Recognition
   implementation("com.google.mlkit:genai-speech-recognition:1.0.0-alpha1")
}
Extrait de code Kotlin à 13:52 dans "Designing audio-first experiences"
suspend fun startMyRecognition() {
   // choose locale and mode
   val options: SpeechRecognizerOptions =
       speechRecognizerOptions {
           locale = Locale.US
           preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
       }

   val speechRecognizer: SpeechRecognizer = SpeechRecognition.getClient(options)

   val request: SpeechRecognizerRequest =
       speechRecognizerRequest { audioSource = AudioSource.fromMic() }
...
Extrait de code Kotlin à 14:12 dans "Designing audio-first experiences"
speechRecognizer.startRecognition(request).collect { response ->
   when (response) {
       is SpeechRecognizerResponse.PartialTextResponse -> {
           // Real-time: Act immediately if trigger phrase is heard
           handleResponse(response.text, speechRecognizer)
       }
       is SpeechRecognizerResponse.FinalTextResponse -> {
           // Final result and recognition
           handleResponse(response.text, speechRecognizer)
       }
       is SpeechRecognizerResponse.CompletedResponse -> {
           resetListeningUi()
       }
       is SpeechRecognizerResponse.ErrorResponse -> {
           Log.e("SpeechRec", "Error: ${response.e.errorCode}", response.e)
           resetListeningUi()
       }
...
Extrait de code Kotlin à 14:41 dans "Designing audio-first experiences"
// Helper function to check for multiple phrases and trigger the right action
private suspend fun handleResponse(text: String, recognizer: SpeechRecognizer) {
   val savePhrases = listOf("save", "bookmark", "remember")
   if (savePhrases.any { text.contains(it) }) {

       // 1. Update app state immediately
       saveItemForLater()

       // 2. Stop the recognizer to enforce the action and prevent TTS echo
       recognizer.stopRecognition()

       // 3. Play the mute earcon for immediate, non-verbal confirmation
       playEarcon()

       // 4. Provide voice feedback
       playAudioFeedback("Saved")
   }
}
Extrait de code Kotlin à 26:27 dans "Prioritizing real-time latency with two-pass design"
// 1. Pass 1 System + Turn Prompt (XrLanguageTeacherPersona.kt)
// System: "Always start immediately with <reply>...</reply> (strictly 1 to 3 sentences total).
//   Inside <reply>, wrap every Japanese word AND every complete Japanese example sentence
//   inside <target>...</target> tags (never output <card>)."
val pass1TurnPrompt = """
  The student is looking at the camera view and asked: "$question". Answer in English
  teaching Japanese inside <reply>...</reply>: if they refer to 'this'/'that' or point to
  an object, ground it to the focal item in the camera frame; if they ask for a phrase or
  greeting, teach that phrase.
""".trimIndent()

// 2. Stream multimodal dialogue (ImagePart + TextPart) and split <target> tags to TTS
val generativeModel = Generation.getClient()
val pass1Request = generateContentRequest(ImagePart(frame), TextPart(pass1TurnPrompt)) {
  temperature = 1.0f; topK = 1; maxOutputTokens = 1024
}
generativeModel.generateContentStream(pass1Request).collect { chunk ->
  val delta = chunk.candidates.firstOrNull()?.text.orEmpty()
  streamReplyAndSplitTargetTagsToTts(delta) // Speaks 1st complete sentence immediately
}
// Streams: "<reply>That's <target>けん玉</target> — a wooden toy. <target>けん玉をください。</target></reply>"
Extrait de code Kotlin à 27:33 dans "Prioritizing real-time latency with two-pass design"
import com.google.mlkit.genai.prompt.Generation
import com.google.mlkit.genai.prompt.generateTypedContentRequest
import com.google.mlkit.genai.schema.annotations.Generable
import com.google.mlkit.genai.schema.annotations.Guide

// Pass 2 schema (XrLanguageTeacherGenerableSchemas.kt):
@Generable(description = "Vocabulary flashcard for the taught word or short phrase")
data class GenerableVocabCard(
  @Guide(description = TeacherResponseSchema.WORD_FIELD_DESCRIPTION) val word: String,
  @Guide(description = "Native-language meaning or translation") val meaning: String,
  @Guide(description = "Target-language example sentence") val example: String,
  @Guide(description = "Native-language translation of the example sentence") val example_translation: String,
)

@Generable(description = "Optional vocabulary card extracted from a teacher reply")
data class GenerableOptionalVocabCardWrapper(
  @Guide(description = "Vocabulary flashcard if a vocabulary term was taught, or null otherwise")
  val card: GenerableVocabCard?
)
Extrait Kotlin à 28:00 dans "Prioritizing real-time latency with two-pass design"
// 1. Pass 1 Streamed Output -> Pass 2 Extraction Prompt (no image needed!)
val replyWithTargetTags =
  "<reply>That's <target>けん玉</target> — a wooden toy. <target>けん玉をください。</target></reply>"

val pass2Prompt = """
  If the teacher reply below teaches or introduces a specific Japanese vocabulary item, extract
  only its core focal dictionary term into "card" with keys "word", "meaning", "example", and
  "example_translation". If purely conversational with no focal term taught, set "card" to null:
  $replyWithTargetTags
""".trimIndent()

// 2. Extract @Generable card on idle NPU while TTS audio plays
val pass2Request = generateContentRequest(TextPart(pass2Prompt)) {
  temperature = 1.0f; topK = 1; maxOutputTokens = 256
}
val typedRequest =
  generateTypedContentRequest(pass2Request, GenerableOptionalVocabCardWrapper::class)
val card: GenerableVocabCard? =
  generativeModel.generateContent(typedRequest).candidates.firstOrNull()?.response?.card
// Extracts: {"word": "けん玉", "meaning": "a wooden toy", "example": "けん玉をください。", ...}