Inżynier ds. relacji z deweloperami | Android XR
Dowiedz się, jak tworzyć niezawodne, obsługiwane bez użycia rąk funkcje audio oparte na AI na urządzeniu. Omówimy wszystko, czego potrzebujesz: konfigurowanie aktywności okularów, przechwytywanie przekazu z kamery i dźwięku z mikrofonu oraz używanie głosu i innych wskazówek dźwiękowych jako interfejsu użytkownika. Podczas prezentacji aplikacji XR Language Teacher o otwartym kodzie źródłowym, która została utworzona w aplikacji Galeria Google AI Edge, dowiesz się, jak zarządzać Gemini Nano 4 (opartym na Gemma 4 i zoptymalizowanym pod kątem urządzeń z Androidem) i ML Kit oraz jak wykorzystać środowisko wykonawcze LiteRT do tworzenia szybkich rozwiązań XR, które słyszą, widzą i mówią w naturalny sposób.
Transkrypcja
Cześć, witam wszystkich.
Świetnie Was wszystkich zobaczyć.
Dziękuję za przybycie.
Jestem Amy.
Dzisiaj razem z moją koleżanką Alice opowiemy o tym, jak za pomocą Gemini Nano 4 tworzyć niesamowite funkcje audio na inteligentne okulary.
Zaczynajmy.
Gdy mówimy o inteligentnych okularach, mamy na myśli przenośne, lekkie okulary, które można nosić przez cały dzień.
Ta kategoria obejmuje zarówno okulary z wyświetlaczem, jak i okulary tylko z dźwiękiem. W przypadku tych urządzeń znajdziesz tu różne rodzaje sprzętu.
Pierwszą rzeczą, którą zauważysz, jest panel dotykowy, który może służyć do wprowadzania danych przez użytkownika, np. przez kliknięcia i przesunięcia.
Będą też mieć głośniki, kamerę i mikrofon.
Mogą też mieć wyświetlacz lub nie, albo jakąś kombinację tych elementów.
Dzisiaj porozmawiamy o podcastach audio i o tym, jak tworzyć angażujące, inteligentne i dostępne z dowolnego miejsca treści.
Jedną z ciekawych rzeczy związanych z trybem tylko audio jest to, że interfejsem staje się głos.
AI na urządzeniu to świetne rozwiązanie dla deweloperów, którzy chcą tworzyć aplikacje działające offline i w ruchu.
Mamy też świetną wiadomość.
Jeśli więc tworzysz już aplikacje na Androida, tworzysz je też na Androida XR.
Istniejące aplikacje na Androida są zaprojektowane tak, aby dobrze współpracowały z okularami audio, dostarczając powiadomienia głosowe i inne dźwięki przez Bluetooth.
Jeśli chcesz pójść o krok dalej i wyróżnić swoją aplikację na XR, możesz to zrobić na wiele sposobów.
W tej prezentacji mówimy o modelach na urządzeniu, ale nie jest to wyczerpująca lista.
Są też modele chmurowe.
Są to na przykład interfejs Gemini Live API,
Możesz zintegrować się z agentami systemowymi, takimi jak Gemini, i wszystkie te rozwiązania mogą pomóc Ci stworzyć najlepsze dla Ciebie środowisko.
Jak więc stworzyć rozwiązanie, które słyszy, widzi i mówi w naturalny sposób?
Dzisiaj pokażemy, jak tworzyć te zaawansowane funkcje obsługiwane bez użycia rąk, które są oparte na AI na urządzeniu.
Przedstawię Ci podstawy: jak tworzyć aplikacje na okulary, jak skonfigurować aktywność, uzyskać potrzebne uprawnienia, a następnie jak używać ML Kit do interfejsu konwersacyjnego.
Następnie przekażę głos Alice, która omówi kilka rzeczywistych przykładów wykorzystania Gemini Nano 4.
Przejdźmy do kodu.
W przypadku okularów to telefon wykonuje najcięższą pracę, a treści są wyświetlane z telefonu na okularach.
Podczas pierwszej konfiguracji potrzebne są 2 rzeczy, aby połączyć te urządzenia.
Najpierw potrzebujesz biblioteki Jetpack Projected.
Musisz dodać tę zależność, która będzie zawierać interfejsy API do obsługi wyświetlania.
dependencies {
implementation("androidx.xr.projected:projected:1.0.0-alpha13")
}Musisz też utworzyć nową aktywność specjalnie dla okularów.
class ProjectedMainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Glasses content
}
}
}Następnie wykorzystasz bibliotekę Jetpack Projected zarówno w dotychczasowej aplikacji, jak i w nowej aktywności, aby oba urządzenia mogły się ze sobą bezproblemowo komunikować, gdy będzie to potrzebne.
Tak jak w przypadku każdej zwykłej aktywności, musisz zadeklarować ją w pliku manifestu.
<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>Główna różnica polega na tym, że musisz użyć XR_PROJECTED jako requiredDisplayCategory.
Dzięki temu system wie, że ta aktywność dotyczy okularów.
Następnie użyj tej kategorii zamiaru: XR_PROJECTED_LAUNCHER.
Dzięki temu Gemini może wykryć Twoją aplikację, aby użytkownicy mogli ją uruchamiać obsługiwany bez użycia rąk, gdy są poza domem.
OK.
Gdy skonfigurujesz tę aktywność, możesz użyć biblioteki Projected, aby zasilać wrażenia użytkownika aplikacji.
Ta biblioteka zawiera wszystkie narzędzia potrzebne do utworzenia pełnego środowiska okularów.
Omówmy kilka kluczowych interfejsów API, których będziesz używać.
Pierwsza metoda, o której chcę wspomnieć, to 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)W ten sposób będziesz stale monitorować połączenie z okularami.
Możesz używać tego stanu wraz z cyklem życia urządzenia, aby wiedzieć, kiedy okulary są faktycznie dostępne, czyli nie tylko wtedy, gdy są sparowane lub włączone, ale także wtedy, gdy są noszone i gotowe do użycia.
I to jest właśnie to.
Dzięki temu będziesz dokładnie wiedzieć, kiedy możesz rozpocząć aktywność.
Aby to zrobić, gdy będziesz gotowy do uruchomienia aktywności, musisz ustawić pewne opcje.
W tym celu użyjesz createProjectedActivityOptions na ProjectedContext.
Następnie użyjesz go z kontekstem intencji.
Kolejnym narzędziem, którego warto użyć, jest ProjectedDeviceController.
W ten sposób sprawdzisz, jakiego typu okularów używa użytkownik.
Jak już wspominałem, inteligentne okulary obejmują okulary z wyświetlaczem i okulary tylko z dźwiękiem. Musisz sprawdzić, czy możliwości obejmują CAPABILITY_VISUAL_UI. Dzięki temu będziesz w stanie dostosować aplikację do różnych urządzeń i zapewnić użytkownikom optymalne wrażenia.
Podczas tworzenia tego zróżnicowanego środowiska możesz chcieć uzyskać dostęp do sprzętu urządzenia, na przykład do aparatu w okularach.
Aby zrobić to z poziomu wyświetlanej aktywności, musisz poprosić o odpowiednie uprawnienia za pomocą 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)
}
}
}Nawet jeśli Twoja aplikacja ma już uprawnienia wymagane na telefonie, musisz też poprosić o określone uprawnienia dotyczące hardware okularów.
Podczas pracy z uprawnieniami pamiętaj o stosowaniu sprawdzonych metod dotyczących Androida.
Zadeklaruj niezbędne uprawnienia w pliku manifestu, poproś o uprawnienia w kontekście, w którym są potrzebne, i proś tylko o te uprawnienia, które są rzeczywiście niezbędne do działania aplikacji.
Oczywiście opisaliśmy to na naszej stronie developer.android.com.
Mamy też zupełnie nową umiejętność – wyświetlanie sprzętu.
Możesz użyć tego narzędzia z interfejsem wiersza poleceń Androida w Android Studio, aby skonfigurować przepływ pracy związany z uprawnieniami. Możesz też uzyskać dostęp do tej umiejętności, a następnie do wszystkich innych umiejętności związanych z Androidem.
android skills add projected-hardware
android skills add --allSkoro jesteśmy już połączeni, znamy możliwości sprzętowe i mamy odpowiednie uprawnienia, porozmawiajmy o narzędziach, które pozwolą nam stworzyć odpowiednie wrażenia dźwiękowe.
Dlatego projektując treści audio, chcemy mieć pewność, że zapewniamy użytkownikom wysoką jakość.
Może to obejmować kilka kluczowych interakcji.
Po pierwsze, używanie głosu jako danych wyjściowych, czyli umożliwienie głosowi naturalnego mówienia do użytkownika.
Następnie użyj ikon dźwiękowych, aby wysyłać użytkownikowi określone sygnały dźwiękowe.
oraz obsługę danych wejściowych od użytkownika, niezależnie od tego, czy są to kliknięcia i przesunięcia na touchpadzie, czy też chcesz umożliwić użytkownikowi mówienie do aplikacji.
Pierwszą rzeczą, o której będziemy rozmawiać, jest używanie głosu jako danych wyjściowych.
Dlatego chcę przedstawić nową bibliotekę zamiany tekstu na mowę GenAI w ML Kit.
Ta funkcja jest obecnie dostępna w wersji alfa na wybranych urządzeniach Pixel i Samsung.
To coś, co bardzo nas cieszy, zwłaszcza w przypadku podcastów dźwiękowych.
Jeśli masz już doświadczenie z zamianą tekstu na mowę, prawdopodobnie wiesz, że jednym z problemów może być to, że głos systemowy nie zawsze brzmi tak naturalnie, jak byśmy chcieli.
Połączenie z modelami w chmurze w przypadku innych głosów może powodować opóźnienia lub dodatkowe koszty.
ML Kit zapewnia więc znacznie bardziej naturalnie brzmiącą mowę w porównaniu z systemową zamianą tekstu na mowę.
ML Kit oferuje 8 unikalnych głosów o różnych cechach, które możesz wybrać, aby dopasować je do swojej aplikacji.
ML Kit zapewnia też solidną obsługę językową – do wyboru jest 21 różnych języków.
Co więcej, jedną z największych zalet jest to, że po pierwszym pobraniu będzie działać całkowicie na urządzeniu.
Ten model zostanie pobrany do systemu i będzie udostępniany aplikacjom, więc nie zwiększy rozmiaru pakietu APK.
Jest on dostępny nie tylko offline, co jest szczególnie ważne dla użytkowników okularów, którzy są w ruchu i mogą nie mieć Wi-Fi lub znajdować się w obszarach o słabym zasięgu, ale prawdopodobnie zauważysz też lepszą wydajność, ponieważ nie musisz wykonywać połączenia z serwerem.
OK.
Przyjrzyjmy się teraz szczegółom biblioteki i temu, jak możemy z niej korzystać.
Najpierw musisz dodać zależność.
dependencies {
// ML Kit GenAI TTS SDK
implementation("com.google.mlkit:genai-text-to-speech:1.0.0-alpha1")
}W tym przypadku potrzebujesz tylko pakietu SDK ML Kit do zamiany tekstu na mowę.
Następnie możesz zacząć pisać kod.
Dlatego biblioteka ML Kit GenAI zawiera interfejsy API syntezatora mowy.
// 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
}Dzięki temu możesz w prosty sposób wdrożyć wybrane głosy.
Pierwszym krokiem, który wykonasz, będzie zainicjowanie klienta za pomocą syntezatora mowy i ustawienie wybranego języka.
Następnie sprawdź stan pobierania i wybierz głos.
W tym przykładzie wybieramy pierwszy głos, ale chcę też zwrócić uwagę na to, że używamy firstOrNull.
Jak już wspominałem, ML Kit wymaga konkretnego telefonu, czyli urządzenia Samsung lub Pixel.
Jeśli więc otrzymasz wartość null, może to oznaczać, że użytkownik nie korzysta ze zgodnego telefonu.
Następnie musisz się upewnić, że masz skonfigurowaną opcję zastępczą, niezależnie od tego, czy jest to powrót do systemowej zamiany tekstu na mowę, czy odtworzenie nagranej wiadomości lub coś podobnego.
Następnie użyjesz syntezy mowy SpeechSynthesizerRequest, aby utworzyć żądanie, podając tekst wejściowy, a potem ustawiając sposób użycia i typ treści.
// 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}")
}
}Odtwórz mowę, monitoruj stan i zwróć uwagę na to, kiedy mowa się kończy, a następnie zbierz informacje o błędach, które mogą wystąpić.
W razie potrzeby możesz też zamknąć zasoby.
// 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()
}To dość proste, prawda?
Dzięki tym krokom możesz naprawdę zmienić dźwięk aplikacji z czegoś, co brzmi tak…
Dzień dobry, programiści aplikacji na Androida.
Cieszę się, że Was wszystkich widzę.
Mamy nadzieję, że podoba Wam się nasza prezentacja na temat funkcji audio-first.
na coś, co brzmi bardziej tak.
Witajcie, programiści aplikacji na Androida.
Cieszę się, że Was wszystkich widzę.
Mamy nadzieję, że podoba Wam się nasza prezentacja na temat funkcji audio-first.
O wiele lepiej, prawda?
OK.
Biblioteka zamiany tekstu na mowę w ML Kit to bardzo zaawansowane narzędzie, ale nie ogranicza się tylko do tych głosów. Można też odtwarzać za jej pomocą ikony dźwiękowe.
// 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()
)Sygnały dźwiękowe, jak sama nazwa wskazuje, są dźwiękowym odpowiednikiem ikon.
Dzięki temu możesz odtwarzać krótkie dźwięki, aby komunikować się z użytkownikiem.
Podobnie jak w przypadku ikon wizualnych z ikonami dźwiękowymi, użytkownik musi wiedzieć, czym są i co oznaczają.
Dlatego na początku warto połączyć je z powiadomieniami głosowymi, aby były zrozumiałe.
Stworzyliśmy już kilka standardowych dźwięków, które są dostępne w Android Design Hub. Możesz je pobrać bezpośrednio z tej strony i używać ich do takich czynności jak otwieranie i zamykanie aplikacji, powiadomienia o błędach oraz wyciszanie i wyłączanie wyciszenia.
Możesz też tworzyć własne niestandardowe ikony dźwiękowe.
Jeśli chodzi o ikony dźwiękowe, zalecamy stosowanie kilku sprawdzonych metod, niezależnie od tego, czy używasz ikon pobranych z Design Hub, czy tworzysz własne.
Dlatego należy przemyśleć, jak i kiedy używać sygnałów dźwiękowych.
Warto pamiętać o kilku kwestiach. Na początek staraj się nie nadużywać sygnałów dźwiękowych.
Nie odtwarzaj wielokrotnie tych samych dźwięków, ponieważ może to spowodować tzw. zmęczenie słuchu.
Pamiętaj, że ikony dźwiękowe powinny być intuicyjne i łatwe do zapamiętania. Podczas ich wdrażania używaj znanych tonów i rytmów.
Staraj się unikać zbyt skomplikowanych sygnałów dźwiękowych, które wymagają wyjaśnienia. W takich przypadkach lepiej sprawdzi się powiadomienie głosowe.
Pamiętaj, że to, co brzmi subtelnie w studiu lub na Twoim systemie, na głośnikach komputera, może stać się irytujące po wielokrotnym odtworzeniu przez użytkowników.
Dlatego staraj się nie przeciążać użytkowników dźwiękami, które mogą ich dezorientować lub prowadzić do ignorowania ważnych alertów.
OK.
Teraz możemy przekazywać wskazówki i informacje zwrotne za pomocą dźwięku, czy to w formie naturalnej mowy, czy sygnałów dźwiękowych.
A co z danymi wejściowymi?
Oczywiście jest też touchpad lub specjalne przyciski na urządzeniu, których można używać do interakcji przez kliknięcie i przesunięcie.
Możesz też zezwolić na interfejs konwersacyjny. W tym celu możesz użyć interfejsu GenAI Speech Recognition API z ML Kit.
Obecnie jest w wersji alfa i jest dostępna na wybranych urządzeniach, ale działa w 2 różnych trybach.
Tryb podstawowy korzysta z tradycyjnego rozpoznawania mowy na urządzeniu i jest dostępny na urządzeniach z Androidem z interfejsem API na poziomie 31 lub wyższym.
Jest też tryb zaawansowany, który korzysta z Gemini Nano. Jest on dostępny na urządzeniach Pixel 10 i nowszych. Zapewnia on lepszą jakość i obsługę większej liczby języków.
Dlatego SpeechRecognizer ma bardzo podobny wzorzec kodu jak SpeechSynthesizer.
Tworzysz klienta, sprawdzasz, czy model jest dostępny, w razie potrzeby pobierasz go, tworzysz żądanie i udostępniasz zasoby.
Skupmy się na głównych różnicach, które dotyczą tworzenia żądania, przechwytywania dźwięku z mikrofonu i przetwarzania wyniku.
Ponownie musisz dodać niezbędne zależności.
dependencies {
// ML Kit Speech Recognition
implementation("com.google.mlkit:genai-speech-recognition:1.0.0-alpha1")
}Wszystko, czego potrzebujesz, to interfejs Speech Recognition API.
Następnie możesz zacząć tworzyć prośbę.
Dlatego podczas definiowania logiki rozpoznawania użyjesz funkcji zawieszenia.
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() }
...Następnie określ opcje rozpoznawania mowy, ustawiając język i preferredMode.
Tutaj też wybierzesz audioSource i zdefiniujesz samo żądanie.
Następnie rozpoczniesz rozpoznawanie i zbierzesz odpowiedź.
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()
}
...W przypadku SpeechRecognizer masz 4 potencjalne odpowiedzi: częściowa, końcowa, błąd i ukończona.
Częściowe rozpoznawanie będzie analizować strumień w czasie rzeczywistym, więc jest używane do rozpoznawania słów aktywujących lub słów kluczowych.
Final (Ostateczny) pobiera ostateczny strumień audio po pełnej analizie i aktualizacji, Completed (Ukończony) jest po zakończeniu mowy, a Error (Błąd) zbiera wszelkie błędy, które mogą wystąpić.
A oto jak to wszystko połączyć.
// 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")
}
}Załóżmy, że chcemy, aby użytkownik mógł za pomocą polecenia głosowego poprosić aplikację o zapisanie określonego typu treści na później.
Możemy użyć jednego konkretnego polecenia głosowego, które im podaliśmy, lub pozwolić im na używanie wielu słów kluczowych i wyrażeń.
W tym przypadku zezwalamy na używanie słów: „save”, „bookmark” lub „remember”.
Gdy tylko SpeechRecognizer wykryje, że użytkownik użył jednej z tych fraz, zapisze treść, a następnie zatrzyma rozpoznawanie mowy, odtworzy niestandardowy sygnał dźwiękowy i użyje funkcji zamiany tekstu na mowę GenAI z ML Kit, aby poinformować użytkownika, że treść została zapisana.
OK.
Omówiliśmy wiele kwestii.
Może to brzmieć nieco abstrakcyjnie.
Jeśli jesteś jak ja, chcesz zobaczyć kilka przykładów z życia.
Razem z Alicją chcemy Wam pokazać, jak to zrobić.
Pokaże Ci, jak korzystać z Nauczyciela języka w XR w aplikacji Galeria Google AI Edge.
Zespół Alice stworzył to rozwiązanie, a ona pokaże nam dokładnie, jak ono działa.
Teraz Twoja kolej.
Dziękuję, Aniu.
Tak.
Zacznijmy od czegoś, co wielu z Was rozpozna.
Uczę się japońskiego i spędzam godziny w aplikacjach do nauki języków, korzystając z fiszek, serii i innych funkcji.
Potem ląduję w Tokio, wchodzę do hali targowej i zdaję sobie sprawę, że prawie nic z tego, co widzę, nie odpowiada słownictwu, którego się uczyłem.
Na straganie widzę drewnianą zabawkę z kubkiem i piłką, którą rozpoznaję, ale nie wiem, jak powiedzieć po japońsku, o co mi chodzi.
W witrynie sklepowej jest ozdoba, którą chcę kupić, ale nie znam jej nazwy.
To jest moment, w którym mam największą motywację do nauki słowa.
Nie na kanapie, patrząc na aplikację językową.
W tym momencie patrzę na to, czego chcę się nauczyć.
Oto czego potrzebuję.
Chcę nosić inteligentne okulary przez cały dzień, po prostu spacerując.
Gdy coś przyciągnie moją uwagę, po prostu pytam o to na głos.
A wieczorem, gdy wracam do hotelu, wszystko, o co pytałem, czeka na mnie w telefonie w postaci kart ze słownictwem.
I oto, co stworzyliśmy.
Obejrzyjmy krótki klip.
Nazywamy to nauczycielem języka w XR.
Cześć.
Czego chcesz się dziś dowiedzieć?
Możesz też dostosować proces nauki w ustawieniach nauczyciela.
Możesz na przykład wybrać język, którego chcesz się uczyć, i konkretną postać nauczyciela, która Ci odpowiada.
O, tak.
Możesz przełączać się między trybem okularów i telefonu.
Jak powiedzieć „te winogrona” po hiszpańsku?
Jak widać, te pyszne winogrona nazywają się uvas.
Po hiszpańsku możesz powiedzieć „Estas uvas son deliciosas”, aby je opisać.
Chodzi mi o zielone winogrona.
Przyjrzę się tej sprawie.
Aby określić zielone winogrona, możesz powiedzieć „Uvas verdes.
Me gustan las uvas verdes” to świetny sposób, aby powiedzieć komuś, że go lubisz.
Jak to powiedzieć po francusku i jak poprosić o 3 sztuki?
Sprawdźmy to.
Te pysznie wyglądające wypieki to magdalenki.
Po francusku, aby poprosić o 3 magdalenki, możesz powiedzieć: „Je voudrais trois madeleines, s'il vous plaît”.
Jak poprosić o zmianę?
Zobaczmy, co tu mamy.
Aby poprosić o resztę, możesz powiedzieć „Otsuri wa arimasu ka?”.
Co oznacza: „Czy masz drobne?”.
Możesz też powiedzieć „Otsuri o onegaishimasu”, aby grzecznie poprosić o resztę.
Wszystko, czego się nauczysz, jest automatycznie zapisywane w postaci kart ze słownictwem.
La botella de agua.
Esta es una botella de agua.
Tak, a na karcie nauki możesz sprawdzić swoją pamięć i poćwiczyć.
OK.
Tak.
Wszystko, co właśnie widzieliście, działo się na urządzeniu, bez połączenia z siecią.
Podróżuję z międzynarodowym pakietem danych, który zwykle działa dobrze, dopóki nie wejdę do hali targowej, podziemi lub za grubą betonową ścianę.
Często zdarza się, że gdy najbardziej potrzebujesz połączenia, sygnał jest najsłabszy.
Moim zdaniem funkcja, która przestaje działać, gdy nie ma zasięgu, nie jest prawdziwym towarzyszem podróży. Dlatego zadbaliśmy o to, aby XR Language Teacher działał całkowicie offline i na urządzeniu.
Jak to zostało zbudowane?
Jeśli chodzi o architekturę, Amy pokazała już, jak używać ramki kamery wyświetlanej z okularów na telefonie.
Pokazała, jak używać ML Kit i rozpoznawania mowy do zamiany tekstu na mowę, a także jak ML Kit zamienia tekst na mowę.
Nadal potrzebowaliśmy jednak „mózgu”, który mógłby przeanalizować kadr z kamery i pytanie, a następnie ustalić, o co właściwie pyta użytkownik.
Tym mózgiem jest Gemini Nano 4, który powstał na bazie Gemma 4, modelu open source od Google działającego lokalnie na telefonie.
Dlatego na obsługiwanych flagowych urządzeniach z Androidem możesz uzyskać optymalną wydajność, korzystając bezpośrednio z interfejsów API ML Kit.
System Android zajmie się dostarczeniem modelu, więc nie musisz niczego dołączać do aplikacji.
Podsumowując: mikrofon przekazuje dźwięk do funkcji rozpoznawania mowy, a obraz z kamery i transkrypcja trafiają do Gemini Nano 4, który przekazuje tekst do funkcji syntezy mowy, a ta odtwarza go przez głośnik.
Trzy połączone modele, które działają lokalnie na telefonie.
Oto kilka powodów, dla których Gemini Nano 4 jest szczególnie odpowiedni do tego zadania.
Po pierwsze, widzi i czyta za jednym razem.
Dzięki ML Kit Gemini Nano 4 natywnie obsługuje tekst i obrazy, więc nie musisz używać detektora, klasyfikatora ani translatora.
Jest to jeden model, który analizuje kadr z kamery i pytanie jednocześnie.
Drugą zaletą jest obsługa wielu języków.
Gemini Nano 4 został wytrenowany w ponad 140 językach.
W przypadku nauczyciela języka samo tłumaczenie nie wystarczy.
Potrzebujesz idiomów, rozróżnienia między stylem formalnym i nieformalnym, a także przykładowych zdań, które wypowiedziałaby prawdziwa osoba.
I wreszcie – jest na tyle sterowalny, że ma osobowość.
Ćwiczenie wymowy na głos może być onieśmielające.
Ten model dobrze reaguje na prompt systemowy, więc użytkownik może wybrać własną postać nauczyciela.
Może cierpliwy i zachęcający ton sprawi, że ludzie będą bardziej skłonni do spróbowania.
Chcę zwrócić uwagę na jeszcze jedną rzecz związaną z projektem.
Podczas rozmowy w trybie okularów aplikacja po cichu tworzy w tle karty ze słownictwem.
Słowo, tłumaczenie, przykładowe zdanie i zdjęcie z momentu, gdy zadajesz pytanie.
Użytkownik nie musi niczego klikać ręcznie.
Po prostu idą i rozmawiają.
Później tego samego dnia wszystko czeka na nich na telefonie. Dokładnie tego mi brakowało, gdy byłam w Tokio.
Dlatego „najpierw dźwięk” nie musi oznaczać „tylko dźwięk”.
Okulary to odpowiednie urządzenie do zaspokajania ciekawości, a telefon – do późniejszej refleksji.
Niech chwila obsługiwana bez użycia rąk pozostawi po sobie coś więcej.
Teraz przyjrzyjmy się kilku praktycznym wskazówkom dotyczącym tworzenia funkcji działających w czasie rzeczywistym za pomocą Gemini Nano 4 i ML Kit, które poznaliśmy podczas tworzenia tej wersji demonstracyjnej.
Wskazówka 1: nadaj priorytet czasowi oczekiwania w czasie rzeczywistym, korzystając z projektu dwuprzebiegowego.
gdy zapytasz „Co to jest?” na okularach audio,
Każda chwila ciszy jest zauważalna.
W przypadku XR Language Teacher potrzebujemy od modelu 2 rzeczy na każdą turę interakcji z użytkownikiem: natychmiastowej odpowiedzi głosowej i ustrukturyzowanej karty ze słownictwem, którą można zapisać w aplikacji.
Jeśli poprosimy model o 2 rzeczy naraz w jednym przebiegu, każdy token wydany na wygenerowanie danych w tle opóźni pierwsze wypowiedziane zdanie.
Zamiast tego oddzielamy to, czego użytkownik potrzebuje w danym momencie, od tego, czego aplikacja potrzebuje kilka sekund później.
Dlatego podzieliliśmy każdą turę na 2 etapy.
W etapie 1 przesyłamy strumieniowo kadr z kamery i pytanie dotyczące odpowiedzi głosowej, uruchamiając zamianę tekstu na mowę, gdy tylko zostanie ukończone pierwsze zdanie.
Ponieważ TPU lub NPU kończy generowanie krótkiej odpowiedzi na długo przed tym, jak funkcja zamiany tekstu na mowę skończy ją odczytywać, mamy kilkusekundowe okno, w którym użytkownik nadal słucha, a procesor jest już bezczynny.
Ponieważ w etapie 1 obiekt został już zidentyfikowany w odpowiedzi tekstowej, w etapie 2 nie jest nawet potrzebny obraz.
Przekazujemy odpowiedź z etapu 1 z powrotem do nieaktywnego układu, aby wyodrębnić i zapisać ustrukturyzowaną kartę, zanim nauczyciel skończy mówić.
Teraz przyjrzyjmy się bliżej etapowi 1, co prowadzi nas do drugiej wskazówki.
Podczas strumieniowania używaj lekkich tagów XML.
Ponieważ etap 1 przesyła dane bezpośrednio do zamiany tekstu na mowę, nie chcesz, aby ciężki obiekt JSON opóźniał pierwsze wypowiedziane słowo.
Zamiast tego potrzebujesz zwykłego tekstu, który można natychmiast odczytać za pomocą lekkich tagów wbudowanych, gdy aplikacja musi podjąć działanie w trakcie odtwarzania.
Pokażę Ci problem, który nas tu sprowadził.
Pamiętasz tę drewnianą zabawkę z kubkiem i piłką na straganie, o której wspominałem?
Po japońsku nazywa się „kendama”.
Model szybko rozpoznał ją w kadrze i podał prawidłową odpowiedź: „To kendama, drewniana zabawka”.
Przekazaliśmy więc całe zdanie do sesji zamiany tekstu na mowę w języku angielskim i oto, co otrzymaliśmy.
To ken-mi, drewniana zabawka.
To nie brzmi zbyt dobrze.
Zamiast tego to zdanie powinno brzmieć…
To kendama, drewniana zabawka.
Dlaczego tak się stało?
Słowo „kendama” jest zapisywane za pomocą hiragany „ken” i znaku kanji „dama”.
Angielski mechanizm zamiany tekstu na mowę dobrze radzi sobie z fonetyczną hiraganą, np. „ken”, ale zupełnie nie radzi sobie z kanji, przez co „dama” brzmi jak „me”.
Jeśli nie zmienisz ustawień regionalnych, może to dotyczyć wielu różnych języków na różne sposoby.
Na przykład w języku hiszpańskim możesz usłyszeć…
Suh-BOH-luh…
Kiedy spodziewasz się dziecka…
Cebolla.
Podobnie w języku francuskim możesz usłyszeć…
PAIN oh chock-uh-lat…
Kiedy spodziewasz się dziecka…
Pain au chocolat.
Tak, sesja TTS jest zwykle przeznaczona dla jednego języka, ale nauczyciel języka musi przełączać się między językami w każdym zdaniu.
W pierwszym etapie prosimy model o umieszczenie każdego słowa lub wyrażenia w języku docelowym w docelowym tagu XML podczas przesyłania strumieniowego.
Gdy fragmenty docierają, nasz klient dzieli je na cele, kieruje każdy fragment do odpowiedniego języka zamiany tekstu na mowę, którym może być japoński, hiszpański, francuski lub dowolny inny język, którego się uczysz, a następnie odtwarza je płynnie, zmieniając akcent w środku zdania, dzięki czemu słowo takie jak kendama jest wymawiane prawidłowo.
Model wie już, które słownictwo i które słowa należą do danego języka. Oznaczanie docelowych fragmentów w trybie inline podczas strumieniowania jest znacznie bardziej przejrzyste niż samodzielne przetwarzanie całości po zakończeniu.
A oto kod dla etapu 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>"Na górze używamy prompta, aby poinstruować model, żeby odpowiedział w 1–3 zdaniach wewnątrz tagów odpowiedzi, a następnie umieścił każde japońskie słowo lub przykładowe zdanie wewnątrz tagów docelowych.
Następnie przekazujemy kamerę ImagePart i prompt TextPart do generateContentStream ML Kit, dzieląc docelowe zakresy na zamianę tekstu na mowę w miarę pojawiania się kolejnych zdań.
To prowadzi nas do drugiego etapu i trzeciej wskazówki.
Lekkie tagi XML świetnie sprawdzają się w przypadku strumienia audio na żywo, ale do zapisywania danych w aplikacji używaj uporządkowanych danych wyjściowych, które są przeznaczone do zwracania czystego, wpisanego obiektu.
Nasz nauczyciel języka w XR prowadzi rozmowę, więc użytkownicy mogą też zadawać dodatkowe pytania, które nie dotyczą nauki nowych słów.
Gdy słowo zostanie nauczone, baza danych potrzebuje czystych pól słownika, aby utworzyć karty słownictwa.
Zamiast pisać niestabilne wyrażenia regularne do analizowania transkrypcji z pierwszego etapu, pozwalamy, aby w tle drugi etap wyodrębnił obiekt z zabezpieczeniami wpisywania.
Aby wymusić schemat w drugim przebiegu, używamy interfejsu API ML Kit do generowania uporządkowanych danych wyjściowych, który pod maską korzysta z dekodowania z ograniczeniami.
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?
)Wystarczy dodać adnotację @Generable do klasy danych i @Guide do każdego pola, tak jak w naszym przykładzie klasy karty słownictwa.
Dzięki opakowaniu pola karty, które może mieć wartość null, drugi etap zwraca wypełnioną kartę, gdy użytkownik nauczył się nowego słowa, lub wartość null w przypadku czysto konwersacyjnych tur.
A tutaj jest kod dla etapu 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": "けん玉をください。", ...}U góry bierzemy tekst odpowiedzi przesyłanej strumieniowo w kroku 1, bez obrazu, i tworzymy prompta do wyodrębniania.
Następnie, gdy użytkownik nadal słucha zamiany tekstu na mowę, przekazujemy prompt i naszą klasę otoki do generateTypedContentRequest i wyodrębniamy czystą, pozbawioną tagów kartę słownictwa lub wartość null, jeśli nie nauczono żadnego słowa.
OK.
Omówiliśmy wiele kwestii.
Możecie teraz pomyśleć: „Świetnie, kolejna rzecz, która wygląda pięknie na scenie, ale której nigdy nie będę używać”.
Nie ten!
Nauczyciel języka XR jest dostępny w aplikacji Galeria Google AI Edge wraz z innymi funkcjami AI na urządzeniu.
Możesz ją pobrać już dziś, zainstalować na telefonie i skierować na dowolny obiekt, aby dowiedzieć się, jak nazwać go w innym języku.
Jeśli chcesz dowiedzieć się, jak to działa, cała implementacja jest dostępna jako open source na GitHubie.
Nie martw się, jeśli nie wszyscy użytkownicy mają już okulary audio, okulary z wyświetlaczem lub obsługiwane flagowe urządzenia z Androidem.
Nasza baza kodu zawiera też przykłady, jak można dostosować to samo rozwiązanie do urządzeń mobilnych.
Jeśli chcesz dotrzeć do innych platform, kod aplikacji Galeria Google AI Edge pokazuje też, jak utworzyć tę funkcję za pomocą środowiska wykonawczego Google LiteRT i interfejsów API MediaPipe Tasks.
Jeśli chcesz użyć innych modeli audio lub językowych open source na urządzeniu, możesz też zobaczyć, jak to zrobić, w kodzie.
Dlatego udostępniliśmy Nauczyciela języka w XR jako oprogramowanie open source, aby ułatwić Ci prototypowanie.
Wskaż wybranemu agentowi nasze repozytorium GitHub, aby przekształcić to działające na urządzeniu rozwiązanie oparte na AI w coś innego.
Może to być przewodnik po muzeum, asystent kulinarny, który widzi Twój blat, instrukcja naprawy, narrator ułatwień dostępu czy narzędzie do kontroli w terenie.
Model, środowisko wykonawcze, implementacja open source i narzędzia są już dostępne, dzięki czemu możesz przygotować się na nadchodzącą jesienną premierę inteligentnych okularów.
Chętnie dowiemy się, co udało Ci się z nim zrobić.
Podsumowując, pokazaliśmy dziś, jakie możliwości dają modele AI działające płynnie na urządzeniu.
Oto kilka sposobów na rozpoczęcie.
Najpierw pobierz zasoby widoczne na ekranie.
Pobierz aplikację AI Edge Gallery, aby wypróbować wersję demonstracyjną i sprawdzić kod referencyjny na GitHubie.
Zeskanuj kody QR, aby uzyskać dostęp do pakietu Android XR SDK, interfejsów API ML Kit i ofert Google AI Edge.
Po drugie, zacznij eksperymentować na telefonie już dziś.
Omówiliśmy wiele kwestii, które można już teraz wykorzystać do tworzenia aplikacji zoptymalizowanych pod kątem urządzeń mobilnych.
Po trzecie, przyjrzyj się aplikacji mobilnej, którą już udostępniasz, i zastanów się, które z jej głównych funkcji lepiej działałyby obsługiwane bez użycia rąk.
Aby zapewnić takie wrażenia również na okularach, wystarczy dodać do istniejącego pliku APK aktywność wyświetlaną.
Dziękujemy za udział w dzisiejszych warsztatach.
Nie możemy się doczekać, aby zobaczyć, jakie niesamowite funkcje audio stworzysz na Androida i inteligentne okulary.
Dziękujemy.
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": "けん玉をください。", ...}