瞭解如何運用裝置端 AI,打造強大的免持音訊體驗。我們會涵蓋所有必要步驟,包括設定眼鏡活動、擷取麥克風和攝影機畫面,以及使用語音和其他音訊提示做為最終 UI。Google AI Edge Gallery 應用程式內建開放原始碼的 XR Language Teacher,我們將透過精彩的實際案例,說明如何自動化調度管理 Gemini Nano 4 (以 Gemma 4 為基礎,並針對 Android 裝置最佳化) 和 ML Kit,以及如何運用 LiteRT 執行階段,打造能自然聽、看、說的快速 XR 體驗。
轉錄稿
大家好,歡迎各位。
很高興今天能與大家見面。
感謝各位蒞臨。
我是 Amy。
今天,我和同事 Alice 將介紹如何使用 Gemini Nano 4,為智慧眼鏡打造出色的音訊優先體驗。
現在就開始吧。
因此,我們所說的智慧眼鏡,是指適合全天配戴的輕便眼鏡。
這個類別包含顯示和純音訊眼鏡,這些裝置的硬體規格不盡相同。
首先,您會注意到這類裝置有觸控板,可供使用者輸入內容,例如輕觸和滑動。
此外,這些裝置也會配備喇叭、攝影機和麥克風。
也可能配備螢幕,或結合上述功能。
今天,我們將探討語音優先的概念,以及如何打造引人入勝、智慧且隨時隨地可用的體驗。
純音訊內容的有趣之處在於,語音會成為使用者介面。
如果開發人員想提供離線和行動體驗,裝置端 AI 是絕佳選擇。
好消息是,
因此,如果您已在建構 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 告訴使用者內容已儲存。
好的。
我們探討了許多內容。
這可能有點抽象。
如果你和我一樣,想看看實際範例,
我和 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 建構這項功能。
如果想使用其他開放原始碼的裝置端音訊或語言模型,也可以透過程式碼瞭解操作方式。
因此,我們開放 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 和智慧眼鏡上,打造出精彩的音訊優先體驗。
感謝您!
dependencies {
implementation("androidx.xr.projected:projected:1.0.0-alpha13")
}class ProjectedMainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Glasses content
}
}
}<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>// 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)// 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 skills add projected-hardware
android skills add --alldependencies {
// ML Kit GenAI TTS SDK
implementation("com.google.mlkit:genai-text-to-speech:1.0.0-alpha1")
}// 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
}// 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()
}// 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()
)dependencies {
// ML Kit Speech Recognition
implementation("com.google.mlkit:genai-speech-recognition:1.0.0-alpha1")
}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() }
...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()
}
...// 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")
}
}// 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>"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?
)// 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": "けん玉をください。", ...}