Android XR

Создание аудиофункций для умных очков с помощью Gemini Nano 4

Колонки

О приложении

О выступлении

Узнайте, как создавать надежные аудиофункции с голосовым управлением на основе ИИ на устройстве. Мы расскажем обо всем, что вам нужно: как настроить активность очков, как записывать видеопоток с камеры и аудио с микрофона и как использовать голос и другие звуковые подсказки в качестве основного пользовательского интерфейса. На примере XR Language Teacher – приложения с открытым исходным кодом, созданного в Google AI Edge Gallery, – вы узнаете, как использовать Gemini Nano 4 (на основе Gemma 4, оптимизированной для устройств Android) и ML Kit, а также как с помощью среды выполнения LiteRT создавать быстрые XR-приложения, которые слышат, видят и говорят естественно.

Сводка

  • Библиотека Jetpack Projected (альфа-версия) позволяет подключать телефоны Android к умным очкам. Добавление проецируемого действия в существующее приложение позволяет проецировать контент на дисплей или аудиоочки.
  • В ML Kit добавлены альфа-версии библиотек для распознавания речи на устройстве, которые позволяют взаимодействовать с некоторыми устройствами с помощью голоса. Она предлагает синтез речи с восемью голосами на 21 языке и распознавание речи с расширенным режимом Gemini Nano на Pixel 10.
  • Gemini Nano 4 обрабатывает кадры с камеры и расшифрованные вопросы за один проход. Она обеспечивает работу офлайн-функций, объединяя локально речевые модели на устройстве на поддерживаемых флагманских телефонах.
  • Двухпроходная обработка позволяет умным очкам воспроизводить аудио в реальном времени. На первом этапе устный текст передается с помощью встроенных XML-тегов для переключения языков в середине предложения, а на втором – в фоновом режиме извлекаются структурированные данные.
  • Реализация XR Language Teacher с открытым исходным кодом поставляется в приложении Google AI Edge Gallery. Разработчики могут адаптировать его код для других вариантов использования мультимодального помощника.

Ресурсы

Gemini Notebook

Темы

  • intelligent-eyewear
  • android-xr
  • gemini-nano
  • on-device-ai
  • ml-kit
  • распознавание речи
  • синтезе речи

Расшифровка

Введение

  • Здравствуйте!

  • Рад всех видеть.

  • Спасибо, что пришли.

  • Меня зовут Эми.

  • Сегодня мы с моей коллегой Элис расскажем, как создавать невероятные аудиофункции для умных очков с помощью Gemini Nano 4.

  • Давайте начнем.

  • Когда мы говорим об умных очках, мы имеем в виду портативные, легкие очки, которые предназначены для ношения в течение всего дня.

  • В эту категорию входят как очки с дисплеем, так и аудиоочки. Устройства могут сильно различаться по аппаратным характеристикам.

  • Первое, что вы заметите, – это сенсорная панель, которую можно использовать для ввода данных, например касаний и пролистываний.

  • Вам также понадобятся динамики, камера и микрофон.

  • И у них также может быть дисплей или нет, или, возможно, какая-то комбинация этого.

  • Сегодня мы поговорим о том, как создавать увлекательные и полезные аудиоподкасты, которые удобно слушать в дороге.

  • И одна из интересных особенностей аудиорежима заключается в том, что голос становится вашим пользовательским интерфейсом.

  • ИИ на устройстве отлично подходит для разработчиков, которые хотят создавать решения, работающие офлайн и на ходу.

Разработка для Android XR

  • И у нас отличные новости.

  • Если вы уже создаете приложения для Android, то вы уже создаете приложения для Android XR.

  • Существующие приложения для Android хорошо работают с аудиоочками, передавая голосовые уведомления и другие аудиоданные по Bluetooth.

  • Если вы хотите пойти дальше и адаптировать приложение для XR, есть много способов сделать это.

  • В этом докладе мы говорим о моделях на устройстве, но это далеко не исчерпывающий список.

  • Есть также облачные модели.

  • Например, Gemini Live API.

  • Вы можете интегрировать системных агентов, таких как Gemini, и все они помогут вам создать оптимальный интерфейс.

  • Как же создать систему, которая слышит, видит и говорит естественно?

  • Сегодня мы расскажем, как создавать такие надежные приложения с голосовым управлением на основе ИИ на устройстве.

  • Я расскажу вам об основах: как создавать приложения для очков, как настраивать действия, получать необходимые разрешения и использовать ML Kit для диалогового интерфейса.

  • Затем я передам слово Элис, и она расскажет о реальных примерах использования Gemini Nano 4.

  • Давайте посмотрим на код.

  • В случае с очками основную работу выполняет телефон, а контент проецируется с телефона на очки.

  • Для этого вам понадобятся две вещи.

  • Сначала вам понадобится библиотека Jetpack Projected.

  • Вам нужно будет добавить эту зависимость, и именно она будет содержать API для обеспечения этого проецируемого опыта.

    dependencies {
       implementation("androidx.xr.projected:projected:1.0.0-alpha13")
    }
  • Затем вам нужно будет создать новое действие специально для очков.

    class ProjectedMainActivity : ComponentActivity() {
       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           setContent {
               // Glasses content
           }
       }
    }
  • Затем вы будете использовать эту библиотеку Jetpack Projected как в существующем приложении, так и в новом действии, чтобы два устройства могли беспрепятственно обмениваться данными, когда это необходимо.

  • Как и в случае с обычными действиями, вам нужно объявить его в манифесте.

    <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>
  • Основное отличие заключается в том, что вам нужно использовать XR_PROJECTED в качестве requiredDisplayCategory.

  • Это нужно, чтобы система знала, что эта деятельность связана с очками.

  • Затем выберите категорию намерений XR_PROJECTED_LAUNCHER.

  • Это позволит Gemini находить ваше приложение, чтобы пользователи могли запускать его с голосовым управлением.

  • Теперь

  • После настройки действия вы сможете использовать библиотеку Projected для работы приложения.

  • В этой библиотеке есть все инструменты, необходимые для создания полноценного интерфейса очков.

  • Давайте рассмотрим несколько ключевых API, которые вы будете использовать.

  • Первый метод, о котором я хочу рассказать, – это 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)
  • Так вы будете постоянно отслеживать подключение очков.

  • Вы используете это вместе с жизненным циклом устройства, чтобы знать, когда очки действительно доступны, то есть не только когда они сопряжены или включены, но и, например, когда их носят и они готовы к использованию.

  • И вот что, так что да.

  • Так вы будете точно знать, когда можно запускать мероприятие.

  • Для этого при запуске активности нужно задать определенные параметры.

  • Для этого вы будете использовать createProjectedActivityOptions в ProjectedContext.

  • И опять же, вы будете использовать это с контекстом намерения.

  • Ещё один инструмент, который вам понадобится, – ProjectedDeviceController.

  • Так вы сможете проверить, какой тип очков использует пользователь.

  • Как я уже говорил, умные очки могут быть с дисплеем или только с аудио. Вам нужно проверить, есть ли у них возможность CAPABILITY_VISUAL_UI. Это позволит вам адаптировать приложение для разных устройств и оптимизировать его работу на них.

  • При создании такого уникального опыта вам может потребоваться доступ к аппаратным компонентам устройства, например к камере очков.

  • Чтобы сделать это из транслируемого действия, вам нужно запросить соответствующие разрешения с помощью 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)
           }
       }
    }
  • Даже если у вашего приложения уже есть разрешения, необходимые для телефона, вам все равно нужно запросить специальные разрешения для аппаратного обеспечения очков.

  • При работе с разрешениями всегда следуйте рекомендациям для Android.

  • Объявите необходимые разрешения в манифесте, запрашивайте их в контексте, в котором они нужны, и только те, которые действительно необходимы для работы приложения.

  • Конечно, у нас есть документация на сайте developer.android.com.

  • Но у нас также есть совершенно новый навык, навык прогнозируемого оборудования.

  • Вы можете использовать его с Android CLI в Android Studio, чтобы настроить рабочий процесс разрешений, и вы можете получить доступ к этому навыку, а затем и ко всем остальным навыкам Android.

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

Как создавать аудиоконтент

  • Итак, мы подключились, знаем возможности нашего оборудования и имеем нужные разрешения. Теперь давайте поговорим об инструментах, которые у вас есть для создания звука.

  • Поэтому при разработке аудиоконтента мы хотим убедиться, что создаем высококачественный продукт для пользователей.

  • И это может включать в себя несколько ключевых взаимодействий.

  • Во-первых, это использование голоса в качестве вывода, то есть возможность естественного общения с пользователем.

  • Далее мы будем использовать звуковые сигналы, чтобы отправлять пользователю определенные аудиосигналы.

  • А также обрабатывать ввод от пользователя, будь то через сенсорную панель с помощью касаний и пролистываний, или если вы хотите разрешить пользователю говорить с вашим приложением.

  • Итак, первое, о чем мы поговорим, – это использование голоса в качестве вывода.

  • Для этого я хочу представить новую библиотеку GenAI Text To Speech в ML Kit.

  • Сейчас она доступна в альфа-версии на некоторых устройствах Pixel и Samsung.

  • Мы очень рады, что у нас есть такая возможность, особенно для аудиоподкастов.

  • Если вы уже работали с синтезом речи, то, вероятно, знаете, что одна из проблем заключается в том, что системный голос не всегда звучит так естественно, как хотелось бы.

  • А подключение к облачным моделям для других голосов может привести к задержкам или дополнительным расходам.

  • Поэтому ML Kit обеспечивает гораздо более естественное звучание речи по сравнению с системным TTS.

  • ML Kit предлагает восемь уникальных голосов с разными характеристиками, которые вы можете выбрать для своего приложения.

  • ML Kit также предлагает надежную языковую поддержку с 21 различными локалями, которые вы можете выбрать.

  • И ещё одно огромное преимущество: после первоначального скачивания все процессы будут выполняться на устройстве.

  • Модель будет скачана в систему и станет доступна для всех приложений, поэтому размер вашего APK-файла не увеличится.

  • И она не только доступна офлайн, что особенно важно для пользователей очков, которые находятся в пути, у них может не быть Wi-Fi или они могут находиться в зонах с плохим покрытием, но вы также, скорее всего, снова увидите лучшую производительность, поскольку вам не нужно совершать этот вызов сервера туда и обратно.

  • Теперь

  • Давайте подробнее рассмотрим библиотеку и то, как с ней работать.

  • Для начала вам нужно добавить зависимость.

    dependencies {
       // ML Kit GenAI TTS SDK
       implementation("com.google.mlkit:genai-text-to-speech:1.0.0-alpha1")
    }
  • Вам понадобится только ML Kit Text To Speech SDK.

  • И тогда вы можете начать писать свой код.

  • Поэтому библиотека GenAI ML Kit включает API синтезатора речи.

    // 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
    }
  • Это позволяет легко внедрять выбранные голоса.

  • Первое, что нужно сделать, – инициализировать клиент с помощью синтезатора речи и задать региональные настройки.

  • Затем проверьте статус скачивания и выберите голос.

  • В этом примере мы выбираем первый голос, но я также хочу обратить ваше внимание на то, что мы используем firstOrNull.

  • Как я уже говорил, для ML Kit требуется телефон с определенными характеристиками, например Samsung или Pixel.

  • Если вы получите нулевое значение, это может означать, что пользователь не использует совместимый телефон.

  • И затем вам просто нужно убедиться, что у вас есть резервный вариант, будь то возврат к системному преобразованию текста в речь или воспроизведение записанного сообщения или что-то в этом роде.

  • Хорошо, а затем вы будете использовать синтез речи SpeechSynthesizerRequest для создания запроса, передавая ему входной текст, а затем устанавливая использование и тип контента.

    // 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}")
       }
    }
  • Воспроизведите речь, отслеживайте статус и нет, чтобы вы знали, когда речь завершится, а затем соберите все ошибки, которые могут возникнуть.

  • и закрывать ресурсы по мере необходимости.

    // 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()
    }
  • Это довольно просто, не так ли?

  • С помощью этих шагов вы действительно можете преобразовать звук вашего приложения, переходя от чего-то, что звучит так…

  • Здравствуйте, разработчики Android!

  • Рад всех видеть.

  • Надеемся, вам нравится наша презентация об аудиоконтенте.

  • и превратить его в нечто подобное.

  • Здравствуйте, разработчики Android!

  • Рад всех видеть.

  • Надеемся, вам нравится наша презентация об аудиоконтенте.

  • Так намного лучше, правда?

  • Окей.

  • Библиотека Text to Speech в ML Kit – это очень мощный инструмент, но вы не ограничены только этими голосами. С помощью этой библиотеки вы также можете воспроизводить звуковые сигналы.

    // 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()
    )
  • Звуковые сигналы, как следует из названия, по сути являются звуковым эквивалентом значков.

  • Это позволит вам просто воспроизводить короткие звуки для общения с пользователем.

  • И так же, как и в случае с визуальными значками и звуковыми сигналами, пользователю нужно будет знать, что это такое и что они означают.

  • Поэтому поначалу вы можете захотеть соединить их с голосовыми уведомлениями, чтобы сделать это понятным.

  • Мы уже создали несколько стандартных звуков, которые доступны в Android Design Hub. Вы можете скачать их оттуда и использовать для таких событий, как открытие и закрытие приложения, уведомления об ошибках, а также включение и отключение звука.

  • Но у вас также есть возможность создавать свои собственные звуковые сигналы.

  • Есть несколько рекомендаций, которым мы советуем следовать, независимо от того, используете ли вы скачанные из Design Hub звуковые сигналы или создаете свои собственные.

  • Поэтому вам нужно быть внимательными к тому, как вы используете звуковые сигналы и как вы их используете.

  • Поэтому есть несколько вещей, о которых следует помнить, например, для начала просто постарайтесь не злоупотреблять звуковыми сигналами.

  • Не проигрывайте одни и те же звуки снова и снова, это может вызвать так называемую слуховую усталость.

  • И помните, что ваши звуковые сигналы должны быть супер и интуитивно понятными, а также используйте знакомые высоты и ритмы, когда вы их реализуете.

  • И, наконец, старайтесь избегать слишком сложных звуковых сигналов, которые требуют объяснения. Для таких случаев лучше подойдут голосовые уведомления.

  • И всегда помните, что то, что может звучать незаметно в студии или на вашей системе, на динамиках вашего компьютера, может стать раздражающим после повторного воспроизведения для ваших пользователей.

  • Поэтому старайтесь не перегружать пользователей звуками, которые могут их запутать или заставить игнорировать важные оповещения.

  • Теперь

  • Теперь у нас есть возможность предоставлять звуковые подсказки и обратную связь, будь то естественная речь или звуковые сигналы.

  • Но как же ввод?

  • Конечно, на устройстве есть сенсорная панель и специальные кнопки, которые можно использовать для нажатий и пролистываний.

  • Но вы также можете захотеть разрешить этот диалоговый интерфейс, для этого вы можете использовать API распознавания речи GenAI от ML Kit.

  • Сейчас функция находится на стадии альфа-тестирования и доступна на некоторых устройствах. Она работает в двух режимах.

  • Базовый режим использует традиционное распознавание речи на устройстве и доступен на устройствах Android с API уровня 31 и выше.

  • А в расширенном режиме используется Gemini Nano. Он доступен на устройствах Pixel 10 и более новых моделях и обеспечивает более высокое качество и поддержку большего количества языков.

  • Таким образом, SpeechRecognizer следует очень похожему шаблону кода, что и SpeechSynthesizer.

  • Вы создаете клиент, проверяете, доступна ли модель, скачиваете ее при необходимости, создаете запрос и освобождаете ресурсы.

  • Поэтому давайте сосредоточимся на основных различиях, когда вы создаете запрос, записываете звук с микрофона, а затем обрабатываете результат.

  • Опять же, вам нужно будет добавить необходимые зависимости.

    dependencies {
       // ML Kit Speech Recognition
       implementation("com.google.mlkit:genai-speech-recognition:1.0.0-alpha1")
    }
  • Вам понадобится только Speech Recognition API.

  • После этого вы сможете создать запрос.

  • Поэтому вы будете использовать функцию приостановки, когда будете определять логику распознавания.

    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() }
    ...
  • Затем укажите параметры распознавания речи, задав региональные настройки и preferredMode.

  • Здесь же вы выбираете audioSource и определяете сам запрос.

  • Затем вы запустите распознавание и получите ответ.

    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()
           }
    ...
  • Таким образом, при использовании SpeechRecognizer вы можете получить четыре возможных ответа: частичный, окончательный, ошибка и завершено.

  • Частичное распознавание будет выполняться в реальном времени, поэтому оно используется для таких вещей, как горячие слова или ключевые слова.

  • Final – это окончательный аудиопоток после полного анализа и обновления. Completed – это поток после завершения речи. Error – это поток, в котором собираются все возможные ошибки.

  • А вот как мы объединяем все это.

    // 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")
       }
    }
  • Допустим, мы хотим, чтобы пользователь мог с помощью голосовой команды сказать приложению сохранить какой-то контент на потом.

  • Вы можете использовать одну явную голосовую команду, которую вы им сказали использовать, или вы можете разрешить им использовать ряд ключевых слов и фраз.

  • Итак, мы разрешаем использовать слова "сохранить", "добавить в закладки" и "запомнить".

  • Как только SpeechRecognizer обнаружит, что пользователь произнес одну из этих фраз, она остановит распознавание речи, сохранит контент, воспроизведет звуковой сигнал и с помощью функции преобразования текста в речь на основе генеративного ИИ в ML Kit сообщит пользователю, что контент сохранен.

История успеха XR Language Teacher

  • Окей.

  • Мы многое пережили.

  • Это может показаться немного абстрактным.

  • Если вы похожи на меня, вы хотите увидеть несколько примеров из реальной жизни.

  • И мы с Алисой хотим рассказать вам именно об этом.

  • Она расскажет вам об XR-учителе языков в приложении Галерея Google AI Edge.

  • Его создала команда Алисы, и она покажет вам, как он работает.

  • Теперь ваша очередь.

  • Спасибо, Эми.

  • Да.

  • Я хочу начать с того, что многие из вас узнают.

  • Я учу японский и уже потратил кучу времени на приложения, карточки, серии занятий и все такое.

  • А потом я приземляюсь в Токио, захожу на рынок и понимаю, что почти ничего из того, что я вижу, не соответствует словам, которые я учил.

  • На прилавке лежит деревянная игрушка бильбоке. Я узнаю ее, но не знаю, как она называется по-японски.

  • Я хочу купить украшение в витрине магазина, но не знаю, как оно называется.

  • Именно в этот момент у меня есть наибольшая мотивация выучить слово.

  • а не на диване с приложением для изучения языков.

  • В этот момент я смотрю на то, что хочу изучить.

  • Вот что я хочу.

  • Я хочу носить умные очки весь день, просто ходить в них.

  • Если что-то привлечет мое внимание, я просто спрошу об этом вслух.

  • А вечером, когда я вернусь в отель, все, о чем я спрашивал, будет ждать меня в телефоне в виде карточек со словами.

  • И вот что мы создали.

  • Давайте посмотрим короткий ролик.

  • Мы назвали его XR-учитель языков.

  • Здравствуйте!

  • Что вы хотите узнать сегодня?

  • Вы можете настроить обучение в настройках преподавателя.

  • Например, выберите язык, который хотите выучить, и преподавателя, который вам подходит.

  • А, да.

  • Вы можете переключаться между режимами очков и телефона.

  • Как сказать "этот виноград" по-испански?

  • Судя по всему, этот вкусный виноград называется uvas.

  • По-испански вы можете сказать: "Estas uvas son deliciosas".

  • Я хочу сказать, что это зеленый виноград.

  • Давайте разберемся.

  • Чтобы указать зеленый виноград, вы можете сказать "Uvas verdes.

  • Me gustan las uvas verdes," is a great way to tell someone you like them.

  • Как это сказать по-французски и как попросить три штуки?

  • Давайте посмотрим.

  • Эти аппетитные пирожные называются des madeleines.

  • По-французски, чтобы попросить три, вы можете сказать: "Je voudrais trois madeleines, s'il vous plaît."

  • Как попросить сдачу?

  • Посмотрим, что у нас тут.

  • Чтобы попросить сдачу, скажите "Otsuri wa arimasu ka?".

  • что означает "У вас есть сдача?"

  • Вы также можете сказать "Otsuri o onegaishimasu", чтобы вежливо попросить сдачу.

  • А затем всё, что вы узнали, автоматически сохраняется в виде карточек со словами.

  • La botella de agua.

  • Esta es una botella de agua.

  • Да, а на вкладке "Учеба" можно проверить память и потренироваться.

  • Теперь

  • Да.

  • То есть всё, что вы только что видели, работало на устройстве, без подключения к сети.

  • В поездках я пользуюсь международным тарифным планом. Обычно все работает хорошо, пока я не захожу в торговый центр, метро или здание с толстыми бетонными стенами.

  • Часто бывает так, что в самый нужный момент сигнал оказывается самым слабым.

  • И я бы сказал, что опыт, который умирает, когда падает уровень сигнала, не является настоящим спутником в путешествии, поэтому мы сделали так, чтобы XR Language Teacher работал полностью в автономном режиме и на устройстве.

Объединение локальных моделей

  • Как это было сделано?

  • Эми уже показала вам, как использовать кадр, проецируемый с камеры очков на телефон.

  • Она показала вам, как использовать ML Kit, распознавание речи, чтобы превратить текст в речь, а также ML Kit Text to Speech, чтобы превратить текст обратно в речь.

  • Нам не хватало только "мозга", который мог бы сопоставить изображение с камеры и вопрос и понять, что именно хочет узнать пользователь.

  • Это Gemini Nano 4, созданная на базе Gemma 4 – модели Google с открытым исходным кодом, которая работает локально на телефоне.

  • Поэтому на поддерживаемых флагманских устройствах Android вы можете оптимизировать производительность, используя API ML Kit напрямую.

  • Система Android сама позаботится о доставке модели, поэтому вам не нужно ничего добавлять в приложение.

  • Итак, микрофон передает данные в систему распознавания речи, а кадр с камеры и расшифровка вместе поступают в Gemini Nano 4. Затем Gemini Nano 4 передает данные в систему синтеза речи, и звук выводится через динамик.

  • Три модели, связанные друг с другом, работают локально на вашем телефоне.

  • Вот несколько причин, по которым Gemini Nano 4 особенно хорошо подходит для решения этой проблемы.

  • Во-первых, он видит и читает за один проход.

  • Благодаря ML Kit модель Gemini Nano 4 обрабатывает текст и изображения нативно, поэтому вам не нужно использовать детектор, классификатор и переводчик.

  • Это одна модель, которая анализирует кадр камеры и вопрос вместе.

  • Во-вторых, он поддерживает множество языков.

  • Gemini Nano 4 обучалась на более чем 140 языках.

  • Кроме того, для репетитора по языку одного перевода недостаточно.

  • Вам нужны идиомы, формальный и неформальный регистры, а также примеры предложений, которые мог бы сказать реальный человек.

  • И, наконец, она достаточно управляема, чтобы иметь индивидуальность.

  • Тренироваться произносить слова вслух может быть страшно.

  • Эта модель хорошо следует системным инструкциям, поэтому пользователь может выбрать образ учителя.

  • А терпеливый и подбадривающий ИИ-тренер может мотивировать людей попробовать.

  • И вот ещё одна вещь, которую я хотел бы подчеркнуть в дизайне.

  • Пока вы разговариваете в режиме очков, приложение в фоновом режиме создает карточки со словами.

  • Слово, перевод, пример предложения и фотография с момента, когда вы задали вопрос.

  • Пользователю не нужно ничего нажимать.

  • Они просто гуляют и разговаривают.

  • А потом, в тот же день, все это уже ждет их в телефоне. Именно этого мне не хватало, когда я был в Токио.

  • Поэтому аудиоформат не обязательно должен быть единственным.

  • Очки – это подходящая поверхность для любопытства, а телефон – для последующего размышления.

  • Пусть у вас останется что-то на память.

  • Теперь давайте рассмотрим несколько практических советов по созданию функций, работающих в реальном времени, с помощью Gemini Nano 4 и ML Kit, которые мы узнали при создании этой демоверсии.

Приоритет задержки в реальном времени с двухпроходным дизайном

  • Совет 1. Приоритизируйте задержку в реальном времени с помощью двухпроходного дизайна.

  • На аудиоочках, когда вы спрашиваете "Что это?".

  • Каждый момент тишины заметен.

  • В примере с XR Language Teacher нам нужны две вещи для каждого хода взаимодействия пользователя с моделью: немедленный устный ответ, а также структурированная словарная карточка для сохранения в приложении.

  • Если мы попросим модель сделать две вещи за один проход, то каждый токен, потраченный на генерацию фоновых данных, задержит первое произнесенное предложение.

  • Поэтому мы разделяем то, что нужно пользователю прямо сейчас, и то, что понадобится приложению через несколько секунд.

  • Поэтому мы разделили каждый ход на два этапа.

  • На первом этапе мы передаем кадр с камеры и вопрос только для устного ответа, запуская преобразование текста в речь, как только будет готово первое предложение.

  • И поскольку TPU или NPU заканчивают генерировать этот короткий ответ задолго до того, как преобразование текста в речь заканчивает говорить вслух, у нас фактически есть многосекундное окно, в котором пользователь все ещё слушает, а чип уже простаивает.

  • А поскольку на первом этапе объект уже был идентифицирован в текстовом ответе, на втором этапе изображение даже не требуется.

  • Мы просто передаем ответ первого этапа обратно в этот бездействующий чип, чтобы извлечь и сохранить структурированную карточку ещё до того, как учитель закончит говорить.

  • Теперь давайте подробнее рассмотрим первый этап, что подводит нас ко второму совету.

  • При трансляции используйте простые XML-теги.

  • Поскольку на первом этапе поток передается напрямую в систему синтеза речи, не стоит использовать тяжелые объекты JSON, чтобы не задерживать первое произнесенное слово.

  • Вместо этого вам нужен обычный текст, который можно сразу же произнести, с легкими встроенными тегами, когда приложению нужно выполнить действие в середине потока.

  • Позвольте мне показать вам проблему, которая привела нас сюда.

  • Помните деревянную игрушку бильбоке на прилавке, о которой я упоминал?

  • По-японски она называется "кендама".

  • Модель сразу распознала ее по изображению с камеры и дала правильный ответ: "Это кендама, деревянная игрушка".

  • Мы передали это предложение в систему синтеза речи на английском языке, и вот что получилось.

  • Это кен-ми, деревянная игрушка.

  • Это не совсем правильно.

  • Instead, that sentence should actually sound…

  • Это кендама, деревянная игрушка.

  • Почему так произошло?

  • Слово "кендама" пишется с использованием хираганы ("кен") и иероглифа кандзи ("дама").

  • Английский синтезатор речи отлично справляется с фонетической хираганой, например "кен", но совершенно не справляется с кандзи, превращая "дама" в нечто, похожее на "ми".

  • И это происходит на разных языках по-разному, если вы не переключаете региональные настройки.

  • Например, на испанском языке вы можете услышать…

  • Suh-BOH-luh…

  • Когда вы на самом деле ждете…

  • Cebolla.

  • Аналогично, на французском вы можете услышать…

  • Пэн-о-шо-ко-ля…

  • Когда вы на самом деле ждете…

  • Шоколадный хлеб.

  • Да, сеанс TTS обычно предназначен для одного региона, но преподавателю языка необходимо переключать код в каждом предложении.

  • Поэтому на первом этапе мы просим модель заключить каждое слово или фразу на языке перевода в целевой XML-тег по мере их поступления.

  • По мере поступления фрагментов наш клиент разделяет их по целевым языкам, направляет каждый фрагмент в соответствующий языковой модуль Text To Speech (японский, испанский, французский или любой другой язык, который вы изучаете), а затем воспроизводит их без задержек, переключая акцент в середине предложения, чтобы такие слова, как "кендама", звучали правильно.

  • Итак, вывод здесь заключается в том, что модель уже знает, какой словарный запас принадлежит какому языку, какие слова принадлежат какому языку, и наличие у нее тегов целевых диапазонов в строке во время потоковой передачи намного чище, чем последующая постобработка всего этого самостоятельно.

  • А вот как выглядит первый проход в коде.

    // 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>"
  • Вверху мы используем запрос, чтобы указать модели, что она должна ответить одним-тремя предложениями внутри тегов reply, а затем заключить каждое японское слово или пример предложения в теги target.

  • Затем мы передаем камеру ImagePart и запрос TextPart в generateContentStream ML Kit, разбивая целевые фрагменты на преобразование текста в речь по мере поступления каждого предложения.

  • Это подводит меня ко второму проходу и нашему третьему совету.

  • Простые XML-теги отлично подходят для аудиопотока, но для сохранения данных в приложении лучше использовать структурированный вывод, который возвращает чистый типизированный объект.

  • Поскольку XR-учитель языков работает в режиме диалога, пользователи могут задавать уточняющие вопросы, не связанные с изучением новых слов.

  • А когда слово выучено, базе данных нужны чистые поля словаря, чтобы создать карточки со словами.

  • Поэтому вместо того, чтобы писать хрупкие регулярные выражения для синтаксического анализа расшифровки первого прохода, мы позволяем второму проходу извлекать типобезопасный объект в фоновом режиме.

  • А чтобы применить схему на втором этапе, мы используем API структурированного вывода ML Kit, который задействует ограниченное декодирование.

    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?
    )
  • Вам нужно просто добавить аннотацию @Generable к классу данных и @Guide к каждому полю, как в примере с классом карточки со словом.

  • А благодаря тому, что поле card может принимать значение null, на втором этапе возвращается заполненная карточка, если было выучено новое слово, или null, если это был просто разговор.

  • А вот как выглядит код для второго прохода.

    // 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": "けん玉をください。", ...}
  • Вверху мы берем текст ответа, полученный на первом этапе, и создаем запрос на извлечение. Изображение не требуется.

  • Затем, пока пользователь слушает синтез речи, мы передаем запрос и класс-оболочку в generateTypedContentRequest и извлекаем чистую карточку без тегов или null, если слово не было изучено.

  • Теперь

  • Мы многое обсудили.

  • Вы можете подумать: "Отлично, ещё одна вещь, которая прекрасно выглядит на сцене, но которой я никогда не воспользуюсь".

  • Не этот!

  • XR Language Teacher поставляется в приложении Галерея Google AI Edge вместе с другими ИИ-функциями на устройстве.

  • Вы можете скачать его сегодня, установить на свой телефон и навести на любой предмет, чтобы узнать, как он называется на другом языке.

  • Если вы хотите узнать, как устроена эта реализация, ее исходный код доступен на GitHub.

  • Не беспокойтесь, если не у всех ваших пользователей есть аудио- или видеоочки или поддерживаемые флагманские устройства Android.

  • В нашей базе кода также есть примеры того, как адаптировать этот интерфейс для мобильных устройств.

  • Если вы хотите охватить и другие платформы, в коде приложения Галерея Google AI Edge также показано, как создать эту функцию с помощью среды выполнения Google LiteRT и MediaPipe Tasks API.

  • Если вы хотите использовать другие языковые или аудиомодели с открытым исходным кодом, работающие на устройстве, вы можете узнать, как это сделать, из кода.

Адаптация к другим сценариям использования с помощью ИИ-агентов

  • Поэтому мы открыли исходный код XR-учителя языков, чтобы вам было проще создавать прототипы.

  • Укажите агенту на наш репозиторий GitHub, чтобы превратить этот ИИ-инструмент во что-то другое.

  • Это может быть гид по музею, помощник по кулинарии, который смотрит на вашу стойку, пошаговое руководство по ремонту, рассказчик о специальных возможностях, инструмент для полевой инспекции.

  • Модель, среда выполнения, реализация с открытым исходным кодом и инструменты уже доступны, чтобы вы могли подготовиться к запуску умных очков этой осенью.

  • Нам будет интересно посмотреть на результаты вашей работы с ними!

  • Сегодня мы показали, на что способны ИИ-модели, работающие на устройстве.

  • Вот несколько способов начать.

  • Сначала соберите ресурсы на экране.

  • Скачайте приложение AI Edge Gallery, чтобы попробовать демоверсию и посмотреть пример кода на GitHub.

  • Отсканируйте QR-коды, чтобы получить доступ к Android XR SDK, ML Kit API и Google AI Edge.

  • Во-вторых, вы можете начать экспериментировать прямо на телефоне уже сегодня.

  • Мы уже рассмотрели множество вещей, которые можно создать с помощью подхода "сначала мобильные".

  • В-третьих, посмотрите на свое мобильное приложение и подумайте, какие основные функции было бы удобнее использовать с голосовым управлением.

  • Чтобы добавить поддержку очков, достаточно добавить в существующий APK-файл Projected Activity.

  • Большое спасибо за внимание.

  • Нам не терпится увидеть, какие невероятные аудиофункции вы создадите для Android и умных очков.

  • Спасибо!

Код

Фрагменты кода

Фрагмент кода на Kotlin в видео "Разработка для Android XR"
dependencies {
   implementation("androidx.xr.projected:projected:1.0.0-alpha13")
}
Фрагмент кода на Kotlin в видео "Разработка для Android XR"
class ProjectedMainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           // Glasses content
       }
   }
}
Фрагмент XML-кода на 03:02 в видео "Разработка для 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>
Фрагмент кода на Kotlin в видео "Разработка для 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)
Фрагмент кода на Kotlin в видео "Разработка для 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)
       }
   }
}
Фрагмент кода оболочки на отметке 05:46 в видео "Разработка для Android XR"
android skills add projected-hardware
android skills add --all
Фрагмент кода на Kotlin в видео "Разработка приложений, ориентированных на аудио"
dependencies {
   // ML Kit GenAI TTS SDK
   implementation("com.google.mlkit:genai-text-to-speech:1.0.0-alpha1")
}
Фрагмент кода на Kotlin в видео Designing audio-first experiences (08:26)
// 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
}
Фрагмент кода на Kotlin в видео Designing audio-first experiences (09:26)
// 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}")
   }
}
Фрагмент кода на Kotlin в видео Designing audio-first experiences (09:47)
// 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()
}
Фрагмент кода на Kotlin в видео 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()
)
Фрагмент кода на Kotlin в видео Designing audio-first experiences (Разработка приложений, ориентированных на аудио)
dependencies {
   // ML Kit Speech Recognition
   implementation("com.google.mlkit:genai-speech-recognition:1.0.0-alpha1")
}
Фрагмент кода на Kotlin в видео Designing audio-first experiences (13:52)
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() }
...
Фрагмент кода Kotlin на 14:12 в видео "Разработка приложений, ориентированных на аудио"
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()
       }
...
Фрагмент кода на Kotlin в видео Designing audio-first experiences (14:41)
// 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")
   }
}
Фрагмент кода на Kotlin в видео "Приоритизация задержки в реальном времени с помощью двухпроходного дизайна" (26:27)
// 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>"
Фрагмент кода на Kotlin в видео "Приоритизация задержки в реальном времени с помощью двухпроходного дизайна" (27:33)
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?
)
Фрагмент кода на Kotlin в видео "Приоритизация задержки в реальном времени с помощью двухпроходного дизайна"
// 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": "けん玉をください。", ...}