Android XR

使用 Gemini Nano 4 為智慧眼鏡打造音訊優先體驗

喇叭

關於

講座簡介

瞭解如何運用裝置端 AI,打造強大的免持音訊體驗。我們會涵蓋所有必要步驟,包括設定眼鏡活動、擷取麥克風和攝影機畫面,以及使用語音和其他音訊提示做為最終 UI。Google AI Edge Gallery 應用程式內建開放原始碼的 XR Language Teacher,我們將透過精彩的實際案例,說明如何自動化調度管理 Gemini Nano 4 (以 Gemma 4 為基礎,並針對 Android 裝置最佳化) 和 ML Kit,以及如何運用 LiteRT 執行階段,打造能自然聽、看、說的快速 XR 體驗。

摘要

  • Jetpack Projected 程式庫 (Alpha 版) 可將 Android 手機連結至智慧眼鏡。將投影活動新增至現有應用程式,將內容投影到螢幕或純音訊眼鏡。
  • ML Kit 推出 Alpha 版裝置端語音程式庫,可在特定裝置上提供語音優先互動。Pixel 10 搭載 Gemini Nano 進階模式,提供文字轉語音功能 (支援 21 個語言代碼和 8 種語音) 和語音辨識功能。
  • Gemini Nano 4 會同時處理相機影格和轉錄的問題。在支援的旗艦手機上,這項技術會在本機串連裝置端語音模型,提供離線體驗。
  • 雙階段設計會優先處理智慧眼鏡的即時音訊延遲。Pass 1 會使用內嵌 XML 標記串流口語文字,以便在句子中間切換語言代碼,而 Pass 2 則會在背景擷取結構化資料。
  • Google AI Edge Gallery 應用程式提供開放原始碼的 XR Language Teacher 實作。開發人員可以調整程式碼,用於其他多模態助理用途。

資源

Gemini Notebook

主題

  • intelligent-eyewear
  • android-xr
  • gemini-nano
  • on-device-ai
  • ml-kit
  • speech-recognition
  • 文字轉語音

轉錄稿

簡介

  • 大家好,歡迎各位。

  • 很高興今天能與大家見面。

  • 感謝各位蒞臨。

  • 我是 Amy。

  • 今天,我和同事 Alice 將介紹如何使用 Gemini Nano 4,為智慧眼鏡打造出色的音訊優先體驗。

  • 現在就開始吧。

  • 因此,我們所說的智慧眼鏡,是指適合全天配戴的輕便眼鏡。

  • 這個類別包含顯示和純音訊眼鏡,這些裝置的硬體規格不盡相同。

  • 首先,您會注意到這類裝置有觸控板,可供使用者輸入內容,例如輕觸和滑動。

  • 此外,這些裝置也會配備喇叭、攝影機和麥克風。

  • 也可能配備螢幕,或結合上述功能。

  • 今天,我們將探討語音優先的概念,以及如何打造引人入勝、智慧且隨時隨地可用的體驗。

  • 純音訊內容的有趣之處在於,語音會成為使用者介面。

  • 如果開發人員想提供離線和行動體驗,裝置端 AI 是絕佳選擇。

建構 Android XR 應用程式

  • 好消息是,

  • 因此,如果您已在建構 Android 應用程式,就等於在建構 Android XR 應用程式。

  • 現有的 Android 應用程式可輕鬆延伸至語音眼鏡,透過藍牙提供語音通知和其他音訊。

  • 如果想進一步為 XR 打造獨特的應用程式,可以採取多種做法。

  • 因此,本次講座將著重於裝置端模型,但這絕非完整清單。

  • 我們還有雲端模型。

  • 像是 Gemini Live API。

  • 您也可以整合 Gemini 等系統代理,打造最適合自己的體驗。

  • 那麼,如何打造能自然聽、看和說的體驗呢?

  • 今天,我們將逐步引導您建構這些強大的免持體驗,並由裝置端 AI 提供技術支援。

  • 我會帶您瞭解基本概念:如何建構眼鏡、如何設定活動、取得所需權限,以及如何使用 ML Kit 打造對話式 UI。

  • 接著,我會將時間交給 Alice,她會介紹一些使用 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)
  • 這樣就能持續監控眼鏡連線。

  • 您可以搭配裝置生命週期使用這項功能,瞭解眼鏡何時可實際使用,不只是配對或開機時,還包括佩戴且準備就緒時。

  • 這就是,所以,是的。

  • 這樣一來,您就能確切知道何時可以啟動該活動。

  • 因此,當您準備好啟動活動時,必須在啟動活動時設定一些特定選項。

  • 這些資料會使用 ProjectedContext 上的 createProjectedActivityOptions。

  • 接著,您會將這個值與意圖的內容搭配使用。

  • 另外,您還需要使用 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 Studio 中搭配 Android CLI 使用這項功能,設定權限工作流程,並取得這項技能,以及所有其他 Android 技能。

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

設計以音訊為主的體驗

  • 現在我們已連線、瞭解硬體功能,並取得適當權限,接下來就來談談建構音訊體驗所需的工具。

  • 因此,在設計音訊優先體驗時,我們希望確保能為使用者提供優質體驗。

  • 這可能包括幾項重要互動。

  • 首先是使用語音做為輸出,讓您的語音自然地與使用者對話。

  • 接下來,您可以使用指示音效,向使用者傳送特定音訊信號。

  • 此外,您也可以處理使用者的輸入內容,無論是透過觸控板輕觸和滑動,還是允許使用者對應用程式說話。

  • 首先,我們要討論的是使用語音做為輸出。

  • 因此,我想介紹 ML Kit 的全新 GenAI Text To Speech 程式庫。

  • 這項功能目前為 Alpha 版,適用於特定 Pixel 和 Samsung 裝置。

  • 我們對這項功能感到非常興奮,尤其是音訊優先體驗。

  • 如果您曾使用過文字轉語音功能,可能知道其中一個難題是系統語音不一定能達到您想要的自然效果。

  • 如果連線至雲端模型來使用其他語音,可能會產生延遲或額外費用。

  • 因此,相較於系統 TTS,ML Kit 的語音聽起來更自然。

  • 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 程式庫包含 Speech Synthesizer 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 開發人員,您好:

  • 很高興今天能與大家見面。

  • 希望大家喜歡我們對音訊優先體驗的介紹。

  • 這樣好多了,對吧?

  • 好的。

  • 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 下載的指示音效,還是自行建立,都建議遵循下列最佳做法。

  • 因此,請務必謹慎思考指示音效的使用方式。

  • 因此,請留意一些事項,例如一開始請盡量不要過度使用指示音效。

  • 請勿反覆播放相同音效,否則會造成聽覺疲勞。

  • 請記得,指示音效應簡短且直覺,並使用熟悉的音高和節奏。

  • 此外,請盡量避免使用需要解釋的複雜指示音效,這類音效可能更適合做為語音通知。

  • 請記住,在錄音室、系統或電腦喇叭中聽起來可能很細微的聲音,在使用者重複播放後,可能會變得令人煩躁。

  • 因此,請盡量避免讓使用者聽到過多音效,以免他們感到困惑,或忽略重要快訊。

  • 現在我們來做個小測驗

  • 因此,我們現在可以透過自然語音或指示音效,提供音訊提示和回饋。

  • 但輸入內容呢?

  • 當然,裝置上也有觸控板或專用按鈕,可用來建立輕觸和滑動互動。

  • 但您可能也想提供對話式 UI,這時可以使用 ML Kit 的 GenAI 語音辨識 API。

  • 這項功能目前為 Alpha 版,僅適用於特定裝置,基本上有兩種模式。

  • 基本模式採用傳統的裝置端語音辨識技術,適用於 API 級別 31 以上的 Android 裝置。

  • 此外,Pixel 10 以上機型還提供進階模式,採用 Gemini Nano,可提供更優異的品質和更廣泛的語言支援。

  • 因此,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 有四種可能的回應:部分、最終、錯誤和完成。

  • Partial 會查看即時串流,因此可用於熱詞或關鍵字等。

  • 「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 的 GenAI Text to Speech 告訴使用者內容已儲存。

XR 語言教師個案研究

  • 好的。

  • 我們探討了許多內容。

  • 這可能有點抽象。

  • 如果你和我一樣,想看看實際範例,

  • 我和 Alice 將為您詳細說明。

  • 她會帶您瞭解 Google AI Edge Gallery 應用程式中的 XR 語言教師。

  • Alice 的團隊打造了這項工具,她將向大家展示這項工具的運作方式。

  • 現在交給你了。

  • 謝謝 Amy。

  • 是啊

  • 首先,我想從大家都很熟悉的東西開始。

  • 我正在學日文,花了好幾個小時使用語言學習應用程式、學習卡、連續學習等,各種方法都試過了。

  • 然後,我抵達東京,走進市場大廳,發現眼前幾乎沒有任何東西是我學過的實際詞彙。

  • 我認得市集攤位上的木製劍玉玩具,但不知道日文怎麼說。

  • 我想買商店櫥窗裡的某個飾品,但不知道名稱。

  • 這時我最想學習新單字。

  • 而不是坐在沙發上盯著語言應用程式。

  • 就在那一刻,我看到我想學的東西。

  • 所以,我想要的是:

  • 我想整天戴著智慧眼鏡到處走。

  • 看到感興趣的事物時,只要大聲提問,

  • 晚上回到飯店時,我問過的所有問題都會像單字卡一樣,在手機上等著我。

  • 這就是我們打造的成果。

  • 我們來看一段短片。

  • 因此,我們將這個應用程式稱為 XR 語言教師。

  • 您好

  • 今天想瞭解什麼?

  • 你也可以在老師設定中自訂學習體驗。

  • 舉例來說,你可以選擇想學習的語言,並挑選適合自己的特定老師角色。

  • 真的嗎?

  • 你也可以在眼鏡和手機模式之間切換。

  • 「這些葡萄」的西班牙文怎麼說?

  • 從這張圖來看,那些美味的葡萄叫做「uvas」。

  • 在西班牙文中,你可以說「Estas uvas son deliciosas」來描述葡萄。

  • 我想特別說一下綠葡萄。

  • 讓我看一下出了什麼問題。

  • 如要指定綠葡萄,可以說「Uvas verdes」。

  • Me gustan las uvas verdes」是向對方表達好感的好方法。

  • 這個東西的法文怎麼說?我要怎麼用法文說「我要三個」?

  • 我們來看看。

  • 那些看起來很美味的糕點叫做瑪德蓮。

  • 在法文中,如果要點三個,可以說「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 語言老師完全在裝置端運作,不需連上網路。

鏈結裝置端模型

  • 那麼,這個模型是如何建構的呢?

  • 在架構方面,Amy 已示範如何將眼鏡的相機畫面投影到手機上。

  • 她示範了如何使用 ML Kit 的語音辨識功能將文字轉換成語音,以及如何使用 ML Kit 的文字轉語音功能將文字轉換回語音。

  • 我們還需要一個中樞,可以同時查看相機畫面和問題,並找出使用者實際詢問的內容。

  • 這個大腦就是 Gemini Nano 4,以 Google 的開放原始碼模型 Gemma 4 為基礎打造,可在手機本機執行。

  • 因此,在支援的旗艦 Android 裝置上,您可以直接使用 ML Kit API,取得最佳效能。

  • Android 系統會負責提供模型,因此您不必在應用程式中組合任何內容。

  • 總結來說,麥克風會將音訊傳送至語音辨識系統,相機畫面和轉錄稿則會傳送至 Gemini Nano 4,Gemini Nano 4 會將內容寫入語音合成系統,然後透過喇叭播放。

  • 三個模型串連,全都在手機上執行。

  • 以下是 Gemini Nano 4 特別適合解決這個問題的幾個原因。

  • 首先,它會一次性查看並讀取。

  • Gemini Nano 4 透過 ML Kit 原生處理文字和圖片,因此您不必先使用偵測工具,再使用分類器,最後再使用翻譯工具。

  • 這個模型會同時查看相機畫面和問題。

  • 其次是廣泛的語言支援。

  • Gemini Nano 4 支援超過 140 種語言。

  • 此外,對語言家教來說,光是翻譯還不夠。

  • 您需要慣用語、正式與非正式語氣,以及真人會說的例句。

  • 最後,這項技術可引導出特定個性。

  • 大聲練習發音可能會讓人感到害怕。

  • 這個模型能確實遵循系統提示,因此使用者可以選擇自己的教師角色。

  • 如果能有耐心且鼓勵性質的語音,或許能讓使用者更願意嘗試。

  • 此外,我還想特別介紹這項設計。

  • 在眼鏡模式下進行對話時,應用程式會在背景默默製作單字卡。

  • 你提問時的字詞、翻譯、例句和相片。

  • 使用者不必手動輕觸任何項目。

  • 他們只是邊走邊聊。

  • 當天稍晚,所有資訊都會在手機上等著他們,這正是我在東京時想要的功能。

  • 因此,音訊優先不代表只能有音訊。

  • 眼鏡是滿足好奇心的絕佳媒介,手機則是事後回顧的理想工具。

  • 讓免持時刻留下美好回憶。

  • 接下來,我們將分享一些實用訣竅,說明如何運用 Gemini Nano 4 和 ML Kit 打造即時體驗,這些都是我們在建構這個示範時學到的。

採用雙階段設計,優先考量即時延遲

  • 訣竅 1:採用雙階段設計,優先考量即時延遲。

  • 在語音眼鏡上,當你問「那是什麼?」時

  • 任何無聲時刻都會很明顯。

  • 以 XR 語言老師為例,我們需要模型在每次使用者互動時提供兩項內容:即時口語回答,以及可儲存至應用程式的結構化單字卡。

  • 如果我們在單一傳遞中同時要求模型執行兩項工作,那麼用於生成背景資料的每個權杖都會延遲第一句語音。

  • 因此,我們將使用者目前需要的內容,與應用程式幾秒後需要的內容分開。

  • 因此,我們將每個回合分成兩個階段。

  • 在第一階段,我們只會串流攝影機畫面和口頭回覆的問題,並在第一個句子完成後立即觸發文字轉語音功能。

  • 由於 TPU 或 NPU 生成簡短回覆的速度,遠快於文字轉語音功能朗讀的速度,因此在使用者聆聽回覆的幾秒內,晶片其實已處於閒置狀態。

  • 由於 Pass 1 已在文字回覆中辨識出物件,Pass 2 甚至不需要圖片輸入。

  • 我們只要將 Pass 1 的回覆傳回閒置的晶片,就能在老師講完話之前,擷取並儲存結構化卡片。

  • 現在,讓我們放大 Pass 1,這會帶出第二個提示。

  • 串流時使用輕量 XML 標記。

  • 由於 Pass 1 會直接串流至文字轉語音,因此您不希望大型 JSON 物件阻礙第一個語音字詞。

  • 而是希望應用程式在需要中途採取行動時,能立即使用輕量內嵌標記朗讀純文字。

  • 接下來,我會說明促成這項發展的問題。

  • 還記得我提過市集攤位上的木製劍玉玩具嗎?

  • 日文稱為「劍玉」。

  • 一開始,模型從相機畫面中辨識出劍玉,並給出正確答案:「那是劍玉,一種木製玩具。」

  • 因此,我們將完整句子輸入英文文字轉語音工作階段,結果如下。

  • 那是木製玩具「劍玉」。

  • 這聽起來不太對。

  • 實際上,這句話應該聽起來像…

  • 那是劍玉,一種木製玩具。

  • 為什麼會這樣?

  • 劍玉的日文寫法是平假名「けん」(ken) 和漢字「玉」(dama)。

  • 英文文字轉語音引擎可以正確處理平假名「ken」的發音,但遇到漢字部分時就完全無法處理,將「dama」變成聽起來像「me」的發音。

  • 如果不切換語言代碼,許多不同的語言都會以不同方式發生這種情況。

  • 舉例來說,如果使用西班牙文,你可能會聽到…

  • Suh-BOH-luh…

  • 但實際預期的是…

  • Cebolla。

  • 同樣地,法文可能會聽到…

  • PAIN oh chock-uh-lat…

  • 但實際預期的是…

  • Pain au chocolat.

  • 是的,TTS 工作階段通常一次只針對一個語言代碼設計,但語言老師必須在每個句子中切換語言。

  • 因此,在第 1 階段,我們會要求模型在串流時,將每個譯文語言字詞或詞組包裝在目標 XML 標記中。

  • 當語音片段傳入時,用戶端會根據目標分割,將每個語音片段轉送至相應的 Text-to-Speech 語言代碼 (可能是日文、西班牙文、法文或任何你正在學習的語言),然後流暢播放,在句子中間切換口音,因此像「kendama」這樣的字詞就能正確發音。

  • 因此,這裡的重點是模型已經知道哪些詞彙屬於哪種語言,哪些字詞屬於哪種語言,並在串流時內嵌標記目標範圍,這比事後自行處理整個過程要乾淨得多。

  • 以下是第 1 階段的程式碼。

    // 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>"
  • 在頂端,我們使用提示詞指示模型在回覆標記中以一到三個句子回覆,然後將每個日文單字或例句包裝在目標標記中。

  • 接著,我們將相機 ImagePart 和提示 TextPart 傳遞至 ML Kit 的 generateContentStream,在每個句子抵達時,將目標範圍分割為文字轉語音。

  • 這就帶出了第二階段和第三個訣竅。

  • 雖然輕量 XML 標記很適合用於即時音訊串流,但將資料儲存至應用程式時,請使用結構化輸出,以便傳回乾淨、有類型的物件。

  • XR 語言教師採用對話式設計,因此使用者也可以提出後續問題,不一定要學習新單字。

  • 教導單字時,資料庫需要乾淨的字典欄位,才能建立單字卡。

  • 因此,我們不編寫脆弱的規則運算式來剖析第 1 階段的轉錄稿,而是讓第 2 階段在背景擷取類型安全物件。

  • 為了在第 2 階段強制執行結構定義,我們使用 ML Kit 的結構化輸出 API,其底層採用受限解碼。

    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 為每個欄位加上註解,就像這裡的單字卡類別範例一樣。

  • 透過包裝可為空值的資訊卡欄位,Pass 2 會在教授新字詞時傳回已填入內容的資訊卡,或在純對話回合中傳回空值。

  • 以下是第 2 階段的程式碼。

    // 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": "けん玉をください。", ...}
  • 在頂端,我們擷取第 1 階段的串流回覆文字 (不需要圖片),並建立擷取提示。

  • 接著,在使用者聆聽文字轉語音內容時,我們會將提示和包裝函式類別傳遞至 generateTypedContentRequest,並擷取不含標記的乾淨單字卡,如果沒有教授任何字詞,則會傳回空值。

  • 現在我們來做個小測驗

  • 我們介紹了許多內容。

  • 此時你可能會想:「太棒了,又是一個在台上看起來很棒,但我永遠不會碰的東西。」

  • 不是這個!

  • XR Language Teacher 與其他裝置端 AI 體驗,一同在 Google AI Edge Gallery 應用程式中推出。

  • 你現在就可以下載這款應用程式,安裝到自己的手機上,然後對著任何想學習的內容,瞭解如何用其他語言表達。

  • 如果想瞭解建構方式,GitHub 上有完整的開放原始碼實作。

  • 如果並非所有使用者都有音訊或顯示型眼鏡,或支援的旗艦級 Android 裝置,也不必擔心。

  • 我們的程式碼集也提供範例,說明如何將相同體驗調整為僅限行動裝置。

  • 如果您也想觸及其他平台,Google AI Edge Gallery 應用程式程式碼也會說明如何使用 Google LiteRT 執行階段和 MediaPipe Tasks API 建構這項功能。

  • 如果想使用其他開放原始碼的裝置端音訊或語言模型,也可以透過程式碼瞭解操作方式。

運用 AI 代理因應其他用途

  • 因此,我們開放 XR Language Teacher 的原始碼,讓您更輕鬆地設計原型。

  • 將您選擇的代理指向我們的 GitHub 存放區,將這個即時、裝置端、AI 輔助體驗轉換為其他內容。

  • 例如博物館導覽、廚房助理、維修教學、無障礙朗讀工具、現場檢查工具等。

  • 模型、執行階段、開放原始碼實作和工具都已推出,您可以為今年秋季即將推出的智慧眼鏡做好前置準備。

  • 我們很期待看到你的創作。

  • 總結來說,今天我們展示了 AI 模型在裝置端順暢運作的強大實例。

  • 以下是幾種開始使用的方法。

  • 首先,請取得畫面上的資源。

  • 下載 AI Edge Gallery 應用程式,試用示範並查看 GitHub 上的參考程式碼。

  • 掃描 QR code 即可存取 Android XR SDK、ML Kit API 和 Google AI Edge 產品。

  • 第二,立即開始在手機上試用。

  • 我們介紹了許多功能,這些功能都可用於打造行動優先體驗。

  • 第三,檢視已推出的行動應用程式,思考哪些核心功能適合免持操作。

  • 只要在現有 APK 中加入 Projected Activity,就能將這項體驗帶到智慧眼鏡上。

  • 非常感謝大家參與今天的活動。

  • 我們非常期待看到您在 Android 和智慧眼鏡上,打造出精彩的音訊優先體驗。

  • 感謝您!

程式碼

程式碼片段

「Building for Android XR」影片 02:39 處的 Kotlin 程式碼片段
dependencies {
   implementation("androidx.xr.projected:projected:1.0.0-alpha13")
}
「打造 Android XR 應用程式」影片 02:45 處的 Kotlin 程式碼片段
class ProjectedMainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           // Glasses content
       }
   }
}
「Building for Android XR」影片 03:02 處的 XML 程式碼片段
<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>
「Building for Android XR」影片 03:44 處的 Kotlin 程式碼片段
// 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)
「Building for Android XR」影片 05:05 處的 Kotlin 程式碼片段
// 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)
       }
   }
}
「Building for Android XR」影片 05:46 處的殼層程式碼片段
android skills add projected-hardware
android skills add --all
「設計音訊優先體驗」中 08:14 的 Kotlin 程式碼片段
dependencies {
   // ML Kit GenAI TTS SDK
   implementation("com.google.mlkit:genai-text-to-speech:1.0.0-alpha1")
}
「設計音訊優先體驗」影片 08:26 處的 Kotlin 程式碼片段
// 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
}
「設計音訊優先體驗」中 09:26 的 Kotlin 程式碼片段
// 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}")
   }
}
「設計音訊優先體驗」影片 09:47 處的 Kotlin 程式碼片段
// 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()
}
「設計音訊優先體驗」中 10:29 的 Kotlin 程式碼片段
// 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()
)
「設計音訊優先體驗」中 13:42 的 Kotlin 程式碼片段
dependencies {
   // ML Kit Speech Recognition
   implementation("com.google.mlkit:genai-speech-recognition:1.0.0-alpha1")
}
「設計音訊優先體驗」影片 13:52 處的 Kotlin 程式碼片段
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() }
...
「設計音訊優先體驗」影片 14:12 處的 Kotlin 程式碼片段
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()
       }
...
「Designing audio-first experiences」影片 14:41 處的 Kotlin 程式碼片段
// 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")
   }
}
「Prioritizing real-time latency with two-pass design」中 26:27 的 Kotlin 程式碼片段
// 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>"
「Prioritizing real-time latency with two-pass design」影片 27:33 處的 Kotlin 程式碼片段
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?
)
「Prioritizing 即時 latency with two-pass design」影片 28:00 處的 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": "けん玉をください。", ...}