איך יוצרים חוויות אודיו חזקות בהפעלה קולית, שמבוססות על AI במכשיר. נסביר על כל מה שצריך: הגדרת הפעילות של המשקפיים, צילום פיד המצלמה ושימוש בקול יחד עם אותות אודיו אחרים כממשק המשתמש האולטימטיבי. בסדנה נציג דוגמה מעשית ומעניינת של XR Language Teacher בקוד פתוח, שנוצר באפליקציית Google AI Edge Gallery. נלמד איך לתזמר את Gemini Nano 4 (שמבוסס על Gemma 4 ומותאם למכשירי Android) ואת ML Kit, ואיך להשתמש בסביבת זמן הריצה של LiteRT כדי ליצור חוויות XR מהירות ששומעות, רואות ומדברות באופן טבעי.
תמליל
שלום לכולם, ברוכים הבאים.
כיף לראות את כולכם היום.
תודה שבאתם.
אני איימי.
היום תצטרף אליי הקולגה שלי, אליס, ונדבר על איך אפשר ליצור חוויות מדהימות שמתמקדות באודיו למשקפיים חכמים באמצעות Gemini Nano 4.
בואו נתחיל.
אז כשאנחנו מדברים על משקפיים חכמים, אנחנו מתכוונים למשקפיים ניידים וקלים שמיועדים לשימוש לאורך כל היום.
הקטגוריה הזו כוללת גם משקפיים עם מסך וגם משקפיים עם אודיו בלבד, ובמכשירים האלה תראו חומרה מגוונת.
הדבר הראשון שתבחינו בו הוא שיש להם משטח מגע, שניתן להשתמש בו לקלט של המשתמש, דברים כמו הקשות והחלקות.
הם כוללים גם רמקולים, מצלמה ומיקרופון.
יכול להיות שיהיה להם מסך או לא, או שילוב של שניהם.
היום אנחנו מדברים על חוויית אודיו ואיך ליצור חוויות מעניינות, חכמות ונוחות לשימוש בכל מקום.
אחד הדברים המעניינים באודיו בלבד הוא שהקול הופך לממשק המשתמש.
ה-AI במכשיר מתאים במיוחד למפתחים שרוצים ליצור חוויות אופליין וחוויות תוך כדי תנועה.
ויש לנו חדשות טובות.
לכן, אם אתם כבר מפתחים ל-Android, אתם כבר מפתחים ל-Android XR.
אפליקציות קיימות ל-Android מיועדות לפעול היטב במשקפיים עם ממשק קולי, ומעבירות התראות קוליות וצלילים אחרים באמצעות Bluetooth.
אם אתם רוצים להרחיב את האפליקציה ולהתאים אותה ל-XR, יש הרבה דרכים לעשות את זה.
אז בהרצאה הזו אנחנו מדברים על מודלים במכשיר, אבל זו ממש לא רשימה ממצה.
יש גם מודלים בענן.
יש דברים כמו Gemini Live API.
יש סוכני מערכת שאפשר לשלב, כמו Gemini, וכולם יכולים לעזור לכם ליצור את החוויה שהכי מתאימה לכם.
אז איך יוצרים חוויה כזו, שבה המודל שומע, רואה ומדבר באופן טבעי?
היום נסביר איך ליצור את החוויות המתקדמות האלה, שמופעלות באמצעות AI במכשיר ולא דורשות שימוש בידיים.
אני אסביר לכם את העקרונות הבסיסיים: איך לפתח למשקפיים, איך להגדיר את הפעילות, איך לקבל את ההרשאות שאתם צריכים ואיך להשתמש ב-ML Kit לממשק משתמש של שיחה.
אחר כך אעביר את השרביט לאליס, והיא תציג לכם כמה דוגמאות אמיתיות לשימוש ב-Gemini Nano 4.
בואו נתחיל ונצלול לתוך הקוד.
אז עם המשקפיים, הטלפון הוא זה שעושה את העבודה הקשה, והתוכן של המשקפיים יוקרן מהטלפון למשקפיים.
אז יש שני דברים שצריך כדי לגשר בין המכשירים האלה כשמגדירים אותם בפעם הראשונה.
קודם כל, צריך את ספריית Jetpack Projected.
צריך להוסיף את התלות הזו, והיא תכיל את ממשקי ה-API שיפעילו את חוויית ההקרנה.
dependencies {
implementation("androidx.xr.projected:projected:1.0.0-alpha13")
}ואז תצטרכו גם ליצור פעילות חדשה במיוחד למשקפיים.
class ProjectedMainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Glasses content
}
}
}לאחר מכן, תשתמשו בספריית Jetpack Projected גם באפליקציה הקיימת וגם בפעילות החדשה הזו, כדי ששני המכשירים יוכלו לתקשר בצורה חלקה בכל פעם שצריך.
אז כמו כל פעילות רגילה שראיתם, אתם צריכים להצהיר על כך במניפסט.
<activity
android:name=".ProjectedMainActivity"
android:exported="false"
android:requiredDisplayCategory="android.hardware.display.category.XR_PROJECTED"
android:label="Example activity for audio glasses">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.XR_PROJECTED_LAUNCHER"/>
</intent-filter>
</activity>ההבדל העיקרי הוא שכאן צריך להשתמש ב-XR_PROJECTED בתור requiredDisplayCategory.
כדי שהמערכת תדע שהפעילות הזו קשורה למשקפיים.
ואז תרצו להשתמש גם בקטגוריית הכוונה הזו, XR_PROJECTED_LAUNCHER.
כך Gemini יכול לגלות את האפליקציה שלכם, והמשתמשים יכולים להפעיל אותה באמצעות הקול כשהם בדרכים.
טוב.
אז אחרי שמגדירים את הפעילות הזו, כמו שציינתי, משתמשים בספרייה המוקרנת כדי להפעיל את חוויית השימוש באפליקציה.
הספרייה הזו מכילה את כל הכלים שצריך כדי ליצור את חוויית המשקפיים המלאה.
אז בואו נעבור על כמה מממשקי ה-API העיקריים שבהם תשתמשו כאן.
השיטה הראשונה שאני רוצה להזכיר היא isProjectedDeviceConnected.
// Continuously monitor whether the glasses are connected
val isGlassesConnected by ProjectedContext.isProjectedDeviceConnected(
context,
scope.coroutineContext
).collectAsStateWithLifecycle(initialValue = false)
// Launch the activity
val options = ProjectedContext.createProjectedActivityOptions(context)
val intent = Intent(context, ProjectedMainActivity::class.java)
context.startActivity(intent, options.toBundle())
// Check if the device is audio only or has a display
val projectedDeviceController = ProjectedDeviceController.create(this@ProjectedMainActivity)
isVisualUiSupported = projectedDeviceController.capabilities.contains(CAPABILITY_VISUAL_UI)ככה אתם יכולים לעקוב באופן רציף אחרי החיבור של המשקפיים.
משתמשים בזה יחד עם דוחות מחזור חיים של המכשיר כדי לדעת מתי המשקפיים זמינים בפועל, לא רק כשהם מותאמים או מופעלים, אלא גם כשהם בשימוש ומוכנים לפעולה.
וזה מה שקורה, אז כן.
כך תוכלו לדעת בדיוק מתי אתם מוכנים להשיק את הפעילות.
כדי לעשות את זה, כשתהיו מוכנים להפעיל את הפעילות, תצטרכו להגדיר כמה אפשרויות ספציפיות כשתתחילו את הפעילות.
ובשביל זה, אתם צריכים להשתמש בcreateProjectedActivityOptions בProjectedContext.
ושוב, אתם משתמשים בזה עם ההקשר של כוונת הרכישה.
כלי נוסף שכדאי להשתמש בו הוא ProjectedDeviceController.
כך בודקים באיזה סוג משקפיים המשתמש משתמש.
כמו שאמרתי קודם, משקפיים חכמים כוללים משקפיים עם תצוגה ומשקפיים עם אודיו בלבד. כדאי לבדוק אם היכולות כוללות את CAPABILITY_VISUAL_UI, כדי להתאים את האפליקציה בצורה חלקה למכשירים שונים וליצור חוויה שמותאמת למכשיר.
לכן, כשאתם מפתחים את החוויה הייחודית הזו, יכול להיות שתרצו לגשת לחומרה של המכשיר, למשל, להשתמש במצלמה של המשקפיים.
כדי לעשות את זה מפעילות חזויה, צריך לבקש את ההרשאות המתאימות באמצעות ProjectedActivityCompat.
// In a projected activity
private fun requestProjectedPermissions() {
lifecycleScope.launch {
try {
ProjectedActivityCompat.requestPermissions(
this@PermissionsFromProjectedActivity,
arrayOf(Manifest.permission.CAMERA),
// CAMERA_PERMISSION_REQUEST_CODE is a developer defined constant
requestCode = CAMERA_PERMISSION_REQUEST_CODE,
)
} catch (e: IllegalStateException) {
// Thrown when the projected system service can't be bound.
Log.e(TAG, "Request failed: projected service unavailable.", e)
}
}
}גם אם לאפליקציה כבר יש את ההרשאות שנדרשות לטלפון, עדיין צריך לבקש את ההרשאות הספציפיות לחומרה של המשקפיים.
כשעובדים עם הרשאות, חשוב לזכור את השיטות המומלצות ל-Android באופן כללי.
מצהירים על ההרשאות הנדרשות בקובץ המניפסט, מבקשים הרשאות בהקשר שבו הן נדרשות ומבקשים רק את ההרשאות שבאמת נחוצות כדי שהאפליקציה תפעל.
כמובן שתיעדנו את זה באתר שלנו בכתובת developer.android.com.
אבל יש לנו גם מיומנות חדשה לגמרי, מיומנות החומרה החזויה.
אתם יכולים להשתמש במיומנות הזו עם ה-Android CLI ב-Android Studio כדי להגדיר את תהליך העבודה של ההרשאות, ולקבל גישה למיומנות הזו, וגם לכל שאר המיומנויות של Android.
android skills add projected-hardware
android skills add --allאוקיי, אז עכשיו כשאנחנו מחוברים, מודעים ליכולות החומרה שלנו ויש לנו את ההרשאות המתאימות, בואו נדבר על הכלים שיש לכם כדי ליצור את חוויית האודיו הזו.
לכן, כשמעצבים חוויה שמתמקדת באודיו, אנחנו רוצים לוודא שאנחנו יוצרים חוויה איכותית למשתמשים.
וזה יכול לכלול כמה אינטראקציות מרכזיות.
הראשון הוא שימוש בקול כפלט, כלומר לאפשר לקול שלכם לדבר באופן טבעי עם המשתמש.
השלב הבא הוא שימוש בצלילי חיווי כדי לשלוח אותות אודיו ספציפיים למשתמש.
וגם לטפל בקלט מהמשתמש, בין אם זה דרך לוח המגע באמצעות הקשות והחלקות, או אם רוצים לאפשר למשתמש לדבר אל האפליקציה.
אז הדבר הראשון שנדבר עליו הוא השימוש בקול כפלט.
לכן, אני רוצה להציג את הספרייה החדשה של ML Kit להמרת טקסט לדיבור (TTS) באמצעות GenAI.
התכונה הזו זמינה עכשיו בגרסת אלפא במכשירי Pixel ו-Samsung נבחרים.
אנחנו מאוד מתרגשים מהאפשרות הזו, במיוחד כשמדובר בחוויות שמתמקדות באודיו.
אם עבדתם עם המרת טקסט לדיבור (TTS) בעבר, אתם בטח יודעים שאחת הבעיות היא שהקול של המערכת לא תמיד נשמע טבעי כמו שהייתם רוצים.
בנוסף, אם תתחברו למודלים בענן כדי להשתמש בקולות אחרים, יכול להיות שתהיה לכם בעיית זמן אחזור או שתצטרכו לשלם עלויות נוספות.
לכן, הדיבור ב-ML Kit נשמע הרבה יותר טבעי בהשוואה ל-TTS של המערכת.
ב-ML Kit יש שמונה קולות ייחודיים עם מאפיינים שונים שאפשר לבחור מביניהם כדי להתאים אותם לאפליקציה.
בנוסף, ML Kit מציע תמיכה נרחבת בשפות עם 21 הגדרות מקומיות שונות שאפשר לבחור מתוכן.
בנוסף, אחד היתרונות הגדולים הוא שאחרי ההורדה הראשונית, המודל פועל לגמרי במכשיר.
המודל הזה יורד למערכת ומשותף בין אפליקציות, כך שהוא לא מגדיל את קובץ ה-APK.
הוא לא רק זמין אופליין, וזה חשוב במיוחד למשתמשים במשקפיים שנמצאים בתנועה, אולי אין להם Wi-Fi או שהם נמצאים באזורים עם קליטה חלשה, אלא גם סביר להניח שתראו ביצועים טובים יותר, כי לא צריך לבצע את השיחה הזו לשרת.
טוב.
בואו נבחן את הפרטים של הספרייה ונראה איך בדיוק אפשר להשתמש בה.
אז קודם כל, כמובן, צריך להוסיף את התלות.
dependencies {
// ML Kit GenAI TTS SDK
implementation("com.google.mlkit:genai-text-to-speech:1.0.0-alpha1")
}אז כאן כל מה שצריך זה את ה-SDK של המרת טקסט לדיבור (TTS) של ML Kit.
ואז אתם יכולים להתחיל לכתוב את הקוד.
לכן, ספריית GenAI ML Kit כוללת את ממשקי ה-API של Speech Synthesizer.
// 1. Initializing the SpeechSynthesizer client.
val options = SpeechSynthesizerOptions.Builder(Locale.US).build()
val client = SpeechSynthesis.getClient(options)
// 2. Check and download the voice model.
val voiceInfo = client.getSupportedVoices().firstOrNull()
if (voiceInfo != null && client.checkStatus(voiceInfo) == FeatureStatus.DOWNLOADABLE) {
client.download(voiceInfo).collect { status -> println("Download status: $status") }
} else if (voiceInfo == null) {
// Tip: Fallback to system TTS if ML Kit voice is unavailable
}כך תוכלו להטמיע בקלות את הקולות שבחרתם.
השלב הראשון הוא להפעיל את הלקוח באמצעות סינתזת הדיבור, ולהגדיר את הלוקאל שבחרתם.
אחר כך בודקים את סטטוס ההורדה ובוחרים את הקול.
בדוגמה הזו, אנחנו בוחרים את הקול הראשון, אבל אני גם רוצה לציין שאנחנו משתמשים ב-firstOrNull.
כמו שציינתי, כדי להשתמש ב-ML Kit צריך טלפון ספציפי עם יכולות מתאימות, כמו מכשירי סמסונג או Pixel.
לכן, אם תקבלו ערך null, יכול להיות שהמשתמש לא משתמש בטלפון תואם.
ואז צריך לוודא שיש לכם גיבוי, בין אם זה לחזור להמרת הטקסט לדיבור (TTS) של המערכת או להשמיע הודעה מוקלטת או משהו כזה.
אוקיי, ואז תשתמשו ב-speech synthe- 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.
כיף לראות את כולכם היום.
אנחנו מקווים שאתם נהנים מהמצגת שלנו על חוויות שמתמקדות באודיו.
הרבה יותר טוב, נכון?
אוקיי.
אז ספריית המרת הטקסט לדיבור (TTS) של 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 ובין אם אתם יוצרים צלילים משלכם.
לכן, חשוב להשתמש בצלילי חיווי בצורה מושכלת.
אז יש כמה דברים שכדאי לזכור, למשל, קודם כל, נסו לא להשתמש יותר מדי בצלילי חיווי.
אל תשמיעו את אותם צלילים שוב ושוב, כי זה עלול לגרום למה שנקרא "עייפות אוזניים".
חשוב לזכור שצלילי החיווי צריכים להיות ברורים ואינטואיטיביים, ומומלץ להשתמש בגובה צליל ובמקצבים מוכרים כשמטמיעים אותם.
כדאי להימנע משימוש בצלילי חיווי מורכבים מדי שמצריכים הסבר. יכול להיות שעדיף להשתמש בהתראה קולית.
חשוב לזכור שמה שנשמע עדין באולפן או במערכת, ברמקולים של המחשב, יכול להפוך למעצבן אחרי שהמשתמשים שומעים אותו שוב ושוב.
לכן, כדאי לא להעמיס על המשתמשים צלילים שעלולים לבלבל אותם או לגרום להם להתעלם מהתראות חשובות.
טוב.
עכשיו יש לנו אפשרות לספק רמזים קוליים ומשוב, באמצעות דיבור טבעי או צלילי חיווי.
אבל מה לגבי קלט?
כמובן שיש גם לוח מגע או כפתורים ייעודיים במכשיר, שבהם אפשר להשתמש כדי ליצור אינטראקציות של הקשה והחלקה.
אבל אולי תרצו גם לאפשר ממשק משתמש של שיחה, ולשם כך תוכלו להשתמש ב-GenAI Speech Recognition API של ML Kit.
התכונה נמצאת כרגע בגרסת אלפא וזמינה במכשירים נבחרים, אבל היא פועלת בשני מצבים נפרדים.
יש מצב בסיסי שמשתמש בזיהוי דיבור רגיל במכשיר, והוא זמין במכשירי Android עם רמת API 31 ומעלה.
יש גם את המצב המתקדם שמשתמש ב-Gemini Nano, שמוצע במכשירי Pixel 10 ומעלה. במצב הזה תראו איכות טובה יותר וגם כיסוי שפה טוב יותר.
לכן, תבנית הקוד של SpeechRecognizer דומה מאוד לזו של SpeechSynthesizer.
יוצרים את הלקוח, בודקים אם המודל זמין, מורידים אותו אם צריך, יוצרים את הבקשה ומשחררים את המשאבים.
אז בואו נתמקד כאן בהבדלים העיקריים שבהם אתם יוצרים את הבקשה, מקליטים את האודיו מהמיקרופון ואז מעבדים את התוצאה.
שוב, צריך להוסיף את יחסי התלות הנדרשים.
dependencies {
// ML Kit Speech Recognition
implementation("com.google.mlkit:genai-speech-recognition:1.0.0-alpha1")
}כל מה שצריך כאן הוא Speech Recognition API.
ואז תוכלו להתחיל ליצור את הבקשה.
לכן, תשתמשו בפונקציית השהיה כשאתם מגדירים את הלוגיקה של הזיהוי.
suspend fun startMyRecognition() {
// choose locale and mode
val options: SpeechRecognizerOptions =
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
val speechRecognizer: SpeechRecognizer = SpeechRecognition.getClient(options)
val request: SpeechRecognizerRequest =
speechRecognizerRequest { audioSource = AudioSource.fromMic() }
...ואז מציינים את אפשרויות זיהוי הדיבור, מגדירים את הלוקאל ואת preferredMode.
כאן גם בוחרים את audioSource ומגדירים את הבקשה עצמה.
ואז תתחילו את הזיהוי ותקבלו תשובה.
speechRecognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse -> {
// Real-time: Act immediately if trigger phrase is heard
handleResponse(response.text, speechRecognizer)
}
is SpeechRecognizerResponse.FinalTextResponse -> {
// Final result and recognition
handleResponse(response.text, speechRecognizer)
}
is SpeechRecognizerResponse.CompletedResponse -> {
resetListeningUi()
}
is SpeechRecognizerResponse.ErrorResponse -> {
Log.e("SpeechRec", "Error: ${response.e.errorCode}", response.e)
resetListeningUi()
}
...לכן, ב-SpeechRecognizer יש ארבע תגובות אפשריות: חלקית, סופית, שגיאה והושלמה.
ההתאמה החלקית בודקת את הסטרימינג בזמן אמת, ומשמשת לזיהוי של מילות הפעלה או מילות מפתח.
האירוע Final מתרחש כששידור האודיו הסופי מתקבל אחרי ניתוח ועדכון מלאים, האירוע Completed מתרחש אחרי שהדיבור מסתיים, והאירוע Error אוסף את כל השגיאות שעלולות להתרחש.
ככה אנחנו משלבים את כל החלקים.
// Helper function to check for multiple phrases and trigger the right action
private suspend fun handleResponse(text: String, recognizer: SpeechRecognizer) {
val savePhrases = listOf("save", "bookmark", "remember")
if (savePhrases.any { text.contains(it) }) {
// 1. Update app state immediately
saveItemForLater()
// 2. Stop the recognizer to enforce the action and prevent TTS echo
recognizer.stopRecognition()
// 3. Play the mute earcon for immediate, non-verbal confirmation
playEarcon()
// 4. Provide voice feedback
playAudioFeedback("Saved")
}
}נניח שאנחנו רוצים שהמשתמש יוכל להשתמש בפקודה קולית כדי להורות לאפליקציה לשמור סוג מסוים של תוכן למועד מאוחר יותר.
אפשר להשתמש בפקודה קולית מפורשת אחת שאמרנו להם להשתמש בה, או לאפשר להם להשתמש בכמה מילות מפתח וביטויים.
אז כאן אנחנו מאפשרים להם להשתמש במילים: save, bookmark או remember.
ברגע ש-SpeechRecognizer מזהה שהמשתמש אמר את אחד מהביטויים האלה, הוא מפסיק את זיהוי הדיבור, שומר את התוכן, משמיע צליל חיווי מותאם אישית ומשתמש ב-GenAI Text to Speech של ML Kit כדי להודיע למשתמש שהתוכן נשמר.
אוקיי.
אז עברנו הרבה.
זה אולי נשמע קצת מופשט.
אם אתם כמוני, אתם רוצים לראות כמה דוגמאות מהחיים האמיתיים.
אליס ואני נסביר לכם בדיוק איך לעשות את זה.
היא תדריך אתכם ב-XR Language Teacher באפליקציית Google AI Edge Gallery.
הצוות של אליס בנה אותו, והיא תראה לכם בדיוק איך הוא עובד.
עכשיו תורכם.
תודה, איימי.
כן.
אז אני רוצה להתחיל עם משהו שהרבה מכם יזהו.
אני לומד יפנית, והשקעתי שעות באפליקציות ללימוד שפות, בכרטיסיות תרגול, ברצפים, בהכול.
ואז, אני נוחת בטוקיו, נכנס לשוק ומבין שכמעט שום דבר ממה שאני רואה לא קשור לאוצר המילים שלמדתי.
יש צעצוע מעץ של כוס וכדור בדוכן בשוק שאני מזהה לפי המראה, אבל אני לא יודע איך אומרים את זה ביפנית.
יש קישוט בחלון ראווה של חנות שאני רוצה לקנות, אבל אני לא יודע איך קוראים לו.
זה בדיוק הרגע שבו יש לי הכי הרבה מוטיבציה ללמוד מילה.
לא על הספה שלי, כשאני מסתכל על אפליקציה ללימוד שפה.
באותו רגע, אני מסתכל על הדבר שאני רוצה ללמוד.
אז זה מה שאני רוצה.
הייתי רוצה להרכיב את המשקפיים החכמים שלי כל היום, פשוט להסתובב איתם.
אם משהו מושך את תשומת הלב שלי, אני יכול לשאול עליו בקול.
ואז, בלילה, כשאחזור למלון, כל מה ששאלתי עליו יחכה לי כמו כרטיסי אוצר מילים בטלפון.
וזה מה שיצרנו.
בואו נצפה בקליפ קצר.
לכן קראנו לו XR Language Teacher.
שלום.
מה מעניין אותך ללמוד היום?
אתם יכולים להתאים אישית את חוויית הלמידה בהגדרות של המורה.
לדוגמה, אתם יכולים לבחור את השפה שאתם רוצים ללמוד ואת דמות המורה הספציפית שמתאימה לכם.
אה, כן.
אתם יכולים לעבור בין מצב משקפיים למצב טלפון.
איך אומרים "הענבים האלה" בספרדית?
לפי זה, הענבים הטעימים האלה נקראים uvas.
בספרדית, אפשר לומר "Estas uvas son deliciosas" כדי לתאר אותם.
אני רוצה לומר ענבים ירוקים, באופן ספציפי.
אני רוצה לבדוק את זה.
כדי לציין את הענבים הירוקים, אפשר לומר "Uvas verdes".
Me gustan las uvas verdes" (אני אוהב ענבים ירוקים) היא דרך מצוינת להגיד למישהו שאתם אוהבים אותו.
איך אומרים את זה בצרפתית, ואיך אפשר לבקש שלושה כאלה?
בואו נבדוק את זה.
המאפים הטעימים האלה נקראים des madeleines.
בצרפתית, כדי לבקש שלוש, אפשר להגיד, "Je voudrais trois madeleines, s'il vous plaît".
איך מבקשים שינוי?
בואו נראה מה יש לנו כאן.
כדי לבקש עודף, אפשר להגיד "Otsuri wa arimasu ka?"
כלומר, "יש לך עודף?"
אפשר גם להשתמש בביטוי "Otsuri o onegaishimasu" כדי לבקש עודף בנימוס.
כל מה שלמדתם נשמר אוטומטית ככרטיסי אוצר מילים.
La botella de agua.
Esta es una botella de agua.
כן, ואפשר לבדוק את הזיכרון ולתרגל בכרטיסיית הלימוד.
טוב.
כן.
אז כל מה שראיתם עכשיו פעל במכשיר, בלי שום חיבור לרשת.
אני נוסע עם חבילת גלישה בינלאומית, שבדרך כלל עובדת מצוין עד שאני נכנס לאולם שוק, או אולי מתחת לאדמה או מאחורי בטון עבה.
הרגע שבו הכי רוצים את זה הוא בדרך כלל הרגע שבו הקליטה הכי גרועה.
אני חושב שחוויה שנגמרת כשאין קליטה היא לא באמת חוויה של שותף לטיול, ולכן פיתחנו את המורה לשפות ב-XR כך שיעבוד לגמרי אופליין ובמכשיר.
אז איך זה נוצר?
מבחינה ארכיטקטונית, איימי כבר הראתה לכם איך להשתמש בפריים של מצלמה שמוקרן מהמשקפיים לטלפון.
היא הראתה לכם איך להשתמש ב-ML Kit, זיהוי דיבור, כדי להמיר טקסט לדיבור, וגם ב-ML Kit המרת טקסט לדיבור כדי להמיר טקסט בחזרה לדיבור.
מה שעדיין היה חסר לנו זה מוח באמצע, משהו שיכול להסתכל על הפריים של המצלמה ועל השאלה ביחד, ולהבין מה המשתמש בעצם שואל.
המוח הזה הוא Gemini Nano 4, שמבוסס על Gemma 4, המודל בקוד פתוח של Google שפועל באופן מקומי בטלפון.
לכן, במכשירי Android נתמכים, אתם יכולים להשתמש ישירות בממשקי ה-API של ML Kit כדי לשפר את הביצועים.
מערכת Android תטפל בהעברת המודל בשבילכם, כך שאין צורך לכלול אותו באפליקציה.
אז כדי לחבר את כל זה, המיקרופון נכנס לזיהוי דיבור, והפריים של המצלמה והתמליל נכנסים יחד ל-Gemini Nano 4, ו-Gemini Nano 4 כותב לסינתזת הדיבור ואז יוצא מהרמקול.
שלושה מודלים, משורשרים, שכולם פועלים באופן מקומי בטלפון.
הנה כמה סיבות לכך ש-Gemini Nano 4 מתאים במיוחד לבעיה הזו.
קודם כול, הוא רואה וקורא בתהליך אחד.
באמצעות ML Kit, Gemini Nano 4 מטפל בטקסט ובתמונות באופן מקורי, כך שלא צריך להשתמש בגלאי, אחר כך במסווג ואז במתרגם.
זה מודל אחד שבוחן את הפריים של המצלמה ואת השאלה ביחד.
הסיבה השנייה היא התמיכה הנרחבת בשפות.
Gemini Nano 4 אומן ביותר מ-140 שפות.
בנוסף, כשמדובר בלימוד שפה, תרגום לבדו לא מספיק.
אתם רוצים ביטויים, אתם רוצים משלבים רשמיים לעומת משלבים לא רשמיים, ואתם רוצים גם משפטים לדוגמה שאדם אמיתי היה אומר.
ולבסוף, אפשר לכוון אותו מספיק כדי שתהיה לו אישיות.
תרגול ההגייה בקול רם יכול להיות מרתיע.
המודל הזה פועל היטב לפי הנחיית המערכת, כך שהמשתמש יכול לבחור את דמות המורה שלו.
ואולי אם הוא יהיה סבלני ומעודד, אנשים ירצו יותר לנסות.
והנה עוד דבר אחד שאני רוצה להדגיש לגבי העיצוב.
בזמן שהשיחה מתנהלת במצב משקפיים, האפליקציה יוצרת כרטיסי אוצר מילים ברקע.
המילה, התרגום, משפט לדוגמה והתמונה מהרגע שבו שאלתם את השאלה.
המשתמש לא צריך ללחוץ על שום דבר באופן ידני.
הם רק הולכים ומדברים.
ואז, בהמשך היום, כל המידע מחכה להם בטלפון, וזה בדיוק מה שרציתי כשהייתי בטוקיו.
לכן, קמפיינים שמתמקדים באודיו לא חייבים לכלול רק אודיו.
המשקפיים הם המכשיר המתאים לסקרנות, והטלפון הוא המכשיר המתאים להרהור לאחר מכן.
הפעלה קולית מאפשרת לכם להשאיר את הידיים פנויות.
עכשיו נתמקד בכמה טיפים מעשיים ליצירת חוויות בזמן אמת באמצעות Gemini Nano 4 ו-ML Kit, שלמדנו מהדגמה הזו.
טיפ 1: תנו עדיפות לזמן אחזור בזמן אמת באמצעות עיצוב בשני שלבים.
במשקפיים עם ממשק קולי, כששואלים "מה זה?"
כל רגע של שתיקה מורגש.
בדוגמה של המורה לשפות ב-XR, אנחנו צריכים שני דברים מהמודל בכל שלב באינטראקציה עם המשתמש: תשובה מיידית בדיבור, וגם כרטיס אוצר מילים מובנה לשמירה באפליקציה.
אם נבקש מהמודל שני דברים ביחד במעבר אחד, כל אסימון שיושקע ביצירת נתוני הרקע האלה יעכב את המשפט המדובר הראשון.
לכן, אנחנו מפרידים בין מה שהמשתמש צריך עכשיו לבין מה שהאפליקציה צריכה כמה שניות אחר כך.
לכן אנחנו מחלקים כל תור לשני שלבים.
בשלב הראשון, אנחנו מעבירים בסטרימינג את הפריים של המצלמה ואת השאלה לתשובה הקולית בלבד, ומפעילים את המרת הטקסט לדיבור (TTS) ברגע שהמשפט הראשון מסתיים.
ומכיוון שה-TPU או ה-NPU מסיימים ליצור את התשובה הקצרה הזו הרבה לפני שהמרת הטקסט לדיבור מסיימת להקריא אותה בקול, למעשה יש לנו חלון של כמה שניות שבו המשתמש עדיין מקשיב, והשבב כבר לא פעיל.
ומכיוון שהאובייקט כבר זוהה בתשובת הטקסט של שלב 1, בשלב 2 אין צורך בקלט של התמונה.
אנחנו פשוט מעבירים את התשובה מהשלב הראשון בחזרה לשבב הלא פעיל כדי לחלץ ולשמור את הכרטיס המובנה עוד לפני שהמורה מסיימת לדבר.
עכשיו, בואו נתמקד בשלב הראשון, וזה מביא אותי לטיפ השני.
השתמשו בתגי XML קלים כשאתם מעבירים נתונים בסטרימינג.
בגלל שהמעבר הראשון מועבר ישירות להמרת טקסט לדיבור, לא כדאי להשתמש באובייקט JSON כבד שיעכב את המילה הראשונה שנאמרת.
במקום זאת, אתם רוצים טקסט פשוט שיכול לדבר מיד עם תגים קלים מוטבעים, בכל פעם שהאפליקציה צריכה לבצע פעולה באמצע הזרם.
אז בואו נראה מה הבעיה שהובילה אותנו לכאן.
זוכרים את צעצוע הכוס והכדור מעץ בדוכן בשוק שהזכרתי?
ביפנית קוראים לזה "קנדמה".
בשלב מוקדם, המודל זיהה את הצעצוע מהפריים של המצלמה והחזיר את התשובה הנכונה, "זה קנדמה, צעצוע מעץ".
אז העברנו את המשפט המלא הזה לסשן של המרת טקסט לדיבור באנגלית, וזה מה שיצא.
זה קנדמה, צעצוע מעץ.
זה לא נשמע נכון.
במקום זאת, המשפט הזה צריך להישמע כך…
זה קנדמה, צעצוע מעץ.
אז למה זה קרה?
המילה קנדמה נכתבת עם היראגאנה, "קן", ותו הקאנג'י, "דאמה".
מנוע המרת הטקסט לדיבור באנגלית מטפל בהיברגאנה הפונטית כמו "ken" בצורה טובה, אבל נכשל לחלוטין כשהוא מגיע לחלק של הקאנג'י והופך את "dama" למשהו שנשמע כמו "me".
וזה קורה בהרבה שפות שונות בדרכים שונות אם לא מחליפים את הלוקאל.
לדוגמה, בספרדית, יכול להיות שתשמעו…
סו-בו-לה…
כשאתם באמת מצפים…
Cebolla.
ובאופן דומה, בצרפתית, יכול להיות שתשמעו…
פיין או שוקולה…
כשאתם מצפים למשהו אחר…
מאפה שוקולד.
כן, סשן TTS בדרך כלל מיועד לאזור אחד בכל פעם, אבל מורה לשפה צריך להחליף קוד בכל משפט.
לכן, בשלב הראשון, אנחנו מבקשים מהמודל לעטוף כל מילה או ביטוי בשפת היעד בתוך תג ה-XML של היעד בזמן הסטרימינג.
כשהקטעים מגיעים, הלקוח שלנו מפצל אותם לפי היעד, מנתב כל קטע לאזור המתאים של המרת טקסט לדיבור (TTS), שיכול להיות יפנית, ספרדית, צרפתית או כל שפה אחרת שאתם לומדים, ואז משמיע אותם בצורה חלקה, תוך החלפת המבטא באמצע המשפט, כך שמילה כמו קנדמה נשמעת נכון.
אז מה שאפשר להבין מזה הוא שהמודל כבר יודע איזה אוצר מילים שייך לאיזו שפה, אילו מילים שייכות לאיזו שפה, וכשהוא מתייג קטעי יעד בתוך השורה בזמן הסטרימינג, זה הרבה יותר נקי מאשר לעבד את כל העניין הזה בעצמך לאחר מכן.
והנה המעבר הראשון בקוד.
// 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 ל-generateContentStream של ML Kit, ומפצלים את טווחי הטקסט לדיבור (TTS) כשהמשפטים מגיעים.
זה מביא אותנו לשלב השני ולטיפ השלישי.
תגי XML קלים מתאימים לשידור אודיו חי, אבל כששומרים נתונים באפליקציה, כדאי להשתמש בפלט מובנה, שנועד להחזיר אובייקט נקי ומוקלד.
מכיוון שהמורה לשפות ב-XR מבוסס על שיחה, המשתמשים יכולים גם לשאול שאלות המשך שלא מלמדות מילה חדשה.
כדי ללמד מילה, מסד הנתונים צריך שדות מילון נקיים כדי ליצור את כרטיסי אוצר המילים.
אז במקום לכתוב ביטויים רגולריים שבירים כדי לנתח את התמליל של השלב הראשון, אנחנו מאפשרים לשלב השני לחלץ אובייקט עם מניעת שגיאות הקלדה ברקע.
כדי לאכוף את הסכימה בשלב השני, אנחנו משתמשים ב-API של הפלט המובנה של ML Kit, שמתחת למכסה המנוע משתמש בפענוח מוגבל.
import com.google.mlkit.genai.prompt.Generation
import com.google.mlkit.genai.prompt.generateTypedContentRequest
import com.google.mlkit.genai.schema.annotations.Generable
import com.google.mlkit.genai.schema.annotations.Guide
// Pass 2 schema (XrLanguageTeacherGenerableSchemas.kt):
@Generable(description = "Vocabulary flashcard for the taught word or short phrase")
data class GenerableVocabCard(
@Guide(description = TeacherResponseSchema.WORD_FIELD_DESCRIPTION) val word: String,
@Guide(description = "Native-language meaning or translation") val meaning: String,
@Guide(description = "Target-language example sentence") val example: String,
@Guide(description = "Native-language translation of the example sentence") val example_translation: String,
)
@Generable(description = "Optional vocabulary card extracted from a teacher reply")
data class GenerableOptionalVocabCardWrapper(
@Guide(description = "Vocabulary flashcard if a vocabulary term was taught, or null otherwise")
val card: GenerableVocabCard?
)פשוט מוסיפים הערה למחלקת נתונים באמצעות @Generable ולכל שדה באמצעות @Guide, בדיוק כמו בדוגמה הזו של מחלקת כרטיסי אוצר מילים.
ובאמצעות עטיפת שדה הכרטיס שניתן לאפס, השלב השני מחזיר כרטיס מאוכלס כשנלמדת מילה חדשה, או ערך null בתורות שיחה בלבד.
וכאן מוצג שלב 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, בלי צורך בתמונה, ויוצרים את הנחיית החילוץ.
בזמן שהמשתמש עדיין מאזין להמרת הטקסט לדיבור, אנחנו מעבירים את ההנחיה ואת מחלקת ה-wrapper שלנו אל generateTypedContentRequest ומחלצים כרטיס אוצר מילים נקי, ללא תגים, או null אם לא נלמדה אף מילה.
טוב.
דיברנו על הרבה דברים.
זה הגיוני לחשוב כרגע, "יופי, עוד משהו שנראה נהדר על הבמה אבל אני לא אגע בו אף פעם".
לא את זה!
הכלי XR Language Teacher מגיע עם אפליקציית Google AI Edge Gallery, לצד חוויות AI אחרות במכשיר.
אתם יכולים להוריד אותה כבר היום, להתקין אותה בטלפון שלכם, ולכוון אותה לכל דבר שאתם רוצים לדעת איך אומרים אותו בשפה אחרת.
ואם אתם רוצים לדעת איך הוא בנוי, כל ההטמעה הזו היא קוד פתוח ב-GitHub.
ואל דאגה אם לא לכל המשתמשים שלכם יש כבר משקפיים עם תצוגה פנימית או אודיו, או מכשירי Android נתמכים מדגמי הדגל.
ב-code base שלנו יש גם דוגמאות שמראות איך אפשר להתאים את אותה חוויה לנייד בלבד.
אם אתם רוצים להגיע גם לפלטפורמות אחרות, קוד האפליקציה ב-Google AI Edge Gallery מראה גם איך לבנות את התכונה הזו באמצעות ממשקי ה-API של Google LiteRT runtime ו-MediaPipe Tasks.
אם אתם רוצים להביא מודלים אחרים של שפה או אודיו במכשיר בקוד פתוח, אתם יכולים גם לראות איך לעשות את זה מהקוד.
לכן פתחנו את הקוד של XR Language Teacher, כדי שיהיה לכם קל יותר ליצור אב טיפוס.
כדי להפוך את חוויית השימוש הזו, שמופעלת על ידי AI במכשיר, למשהו אחר, אתם יכולים להפנות את הסוכן שבחרתם למאגר שלנו ב-GitHub.
זה יכול להיות מדריך במוזיאון, עוזר בישול שמסתכל על השיש, מדריך לתיקון, קריין נגישות או כלי לבדיקת שטח.
המודל, סביבת זמן הריצה, הטמעת הקוד הפתוח והכלים זמינים עכשיו, כדי שתוכלו להתכונן להשקה הקרובה של המשקפיים החכמים בסתיו הקרוב.
נשמח לראות מה תיצרו בעזרתו.
לסיכום, היום הראינו דוגמאות משמעותיות למה שאפשר לעשות עם מודלי AI שפועלים בצורה חלקה במכשיר.
הנה כמה דרכים להתחיל.
קודם כל, אוספים את המשאבים שעל המסך.
כדי לנסות את ההדגמה, הורידו את אפליקציית AI Edge Gallery ועיינו בקוד העזר ב-GitHub.
אתם יכולים לסרוק את קודי ה-QR כדי לגשת ל-Android XR SDK, לממשקי ה-API של ML Kit ולמוצרים של Google AI Edge.
שנית, אתם יכולים להתחיל להתנסות כבר היום ישירות בטלפון.
דיברנו על הרבה דברים שאפשר כבר לבנות עם חוויית משתמש שמותאמת קודם כל לנייד.
שלישית, תסתכלו על האפליקציה לנייד שאתם כבר מפיצים ותשאלו את עצמכם, אילו תכונות עיקריות יהיו טובות יותר בהפעלה קולית?
כדי להעביר את חוויית השימוש הזו גם למשקפיים, כל מה שצריך לעשות הוא להוסיף Projected Activity ל-APK הקיים.
תודה רבה שהצטרפת אלינו היום.
אנחנו מצפים לראות את החוויות המדהימות שתפתחו ב-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": "けん玉をください。", ...}