במפגש הזה נבחן תרחישים שונים של שימוש ב-AI גנרטיבי באמצעות ממשקי ה-API של ML Kit, כדי להבין מתי AI גנרטיבי במכשיר מתאים לאפליקציה, וגם כדי להבין איך להעריך תרחישים קיימים של שימוש ב-AI גנרטיבי כדי לבדוק אם מודלים במכשיר יכולים לעמוד ברף האיכות שלהם.
תמליל
דייוויד צ'ו
היי.
צהריים טובים לכולם.
קוראים לי דוד צ'ו.
אני מנהל מוצר ב-Android AICore.
אני ממש מתרגש לדבר איתכם על יכולות ה-AI במכשיר.
היום נדבר על כמה תחומים מרכזיים.
הראשונה היא פשוט לחשוב למה כדאי להשתמש ב-AI במכשיר, במיוחד לאור ההתקדמות המהירה של ה-AI בענן.
הדבר השני הוא לחשוב על כמה מהשיפורים שביצענו בשנה האחרונה כדי להפוך את מודלי ה-AI האלה במכשיר ליעילים במיוחד.
הדבר האחרון שנדבר עליו הוא דוגמה מקצה לקצה שמראה איך אפשר לנצל את כל היתרונות האלה באמצעות AI במכשיר.
אז קודם כול, למה כדאי להשתמש ב-AI במכשיר?
יש 4 סיבות עיקריות.
הראשון הוא מהירות, השני הוא עלות, השלישי הוא זמינות והרביעי הוא פרטיות.
אז כשמדובר במהירות, אפשר לראות שהסקת מסקנות של AI במכשיר מגיעה עכשיו לנקודה שבה היא יכולה להיות מהירה יותר מאשר הפעלה בענן, פשוט כי לא צריך להעביר מידע הלוך ושוב בין המכשיר של המשתמש לבין הענן.
לכן, בהרבה מהתרחישים לדוגמה שבדקנו, אנחנו רואים מהירות גבוהה פי 2 כשמשווים בין AI במכשיר לבין AI בענן.
השני הוא העלות.
נניח שאתם מריצים משהו עם מיליון הסקות ביום.
למעשה, אתם יכולים לחסוך מאות אלפי דולרים אם אתם מריצים AI במכשיר, במיוחד מהמכשיר של המשתמש, בהשוואה להרצה בענן.
והשלישית היא זמינות במצב אופליין.
לכן, שלחנו סקר מוקדם יותר השנה, ו-62% מהמפתחים שדיברנו איתם אמרו שהזמינות אופליין היא אחת משלוש הסיבות העיקריות לשימוש ב-AI במכשיר.
כך, גם אם המשתמש נמצא בגובה 10,000 מטר באוויר או במרתף של תחנת הרכבת בטוקיו ואין לו גישה לאינטרנט, הוא עדיין יכול להשתמש בכל היכולות האלה.
ולבסוף, פרטיות הייתה אחת הסיבות העיקריות שראינו כשסקרנו מפתחים.
כלומר, 72% דירגו את הסיבה הזו כאחת משלוש הסיבות המובילות.
40% דירגו אותו במקום הראשון.
לכן, כשמדובר בהודעות, בהודעות הקוליות, בתמונות או אפילו במידע הפיננסי או הרפואי של המשתמשים, הם רוצים לשמור על הפרטיות שלהם, והרצת הסקה במכשיר תהיה חשובה מאוד בשבילם.
אז בהתחשב בכל היתרונות האלה, פיתחנו את ML Kit כדי שתוכלו לשמור על כל היכולות האלה ולנצל אותן.
אנחנו מנסים להפוך את התהליך הזה לפשוט ככל האפשר.
אם יש לך תלונות, אפשר לפנות אליי.
בסוף השיחה אשאיר את הפרטים שלי ליצירת קשר.
נשמח להבין איך אנחנו יכולים לעזור לכם להשיג את המטרות שלכם.
אז קודם כל, הפעלנו כמה יכולות, כמו דיבור ותרחישי שימוש יצירתיים.
למשל, הגהה, שכתוב וגם פרומפטים מולטי-מודאליים גמישים.
לכן, המטרה שלנו היא לנסות להפוך את זה לכלי שימושי ככל האפשר.
וזו הסיבה שהרבה מהמשתמשים והמפתחים שלנו כבר התחילו להשתמש בזה.
כבר היום יש לנו 9 אפליקציות של צד שלישי עם 10 מיליון הורדות שמשתמשות בתכונות האלה של AI גנרטיבי בסביבת הייצור.
אנחנו רואים גם עלייה משמעותית בהיקף השימוש באפליקציות שלנו וגם באפליקציות של צד שלישי, וצמיחה של 110% בהשוואה לרבעון הקודם (QoQ) בהשקות של אפליקציות עם תכונות AI בסביבת הייצור.
לכן, אנחנו ממליצים לכם לנסות את ממשקי ה-API הזמינים שלנו בתחומים של העברת מסרים, קריאייטיב ופיננסים.
אנחנו חושבים שהאפשרויות הן אינסופיות, וסביר להניח שהדמיון שלכם מפותח יותר משלנו בהרבה מהמקרים האלה, אז נשמח לשתף פעולה ולגלות איך אנחנו יכולים לעבוד יחד.
עכשיו אתם מבינים את היתרונות העיקריים.
אז אולי אתם שואלים את עצמכם, למשל, מהו פוטנציאל החשיפה בפועל?
איך הביצועים במכשיר?
כשאנחנו מדברים על מהירות, עד כמה היא טובה בפועל?
או כמו האיכות של המודל עצמו, נכון?
נעבור עליהם אחד אחד.
מבחינת היקף השימוש, אנחנו כבר זמינים ב-175 מיליון מכשירי AICore.
אנחנו מצפים שהרשת תתרחב באופן משמעותי במהלך השנה הקרובה, ותכלול מאות מיליוני מכשירים נוספים.
חלק ממה שמאפשר את זה הוא שבנינו רתמה פשוטה מאוד.
לכן, אתם צריכים לשלב רק את ML Kit API.
יש הרבה מורכבות ב-AICore כדי לנהל את אותות הבטיחות של הקלט והפלט, לנהל את הורדת המודל יחד עם המשקלים, ולחשוב איך להגדיר אותו בזמן הריצה.
לכן, המטרה היא להפוך את ML Kit לכלי שקל להשתמש בו, כדי שתוכלו להתמקד ביצירת חוויית שימוש נהדרת, ואנחנו נדאג לכל השאר.
דייוויד צ'ו
אנחנו גם שמחים להודיע שהפחתנו משמעותית את נפח ה-RAM שנדרש כדי להפעיל את Gemini Nano, המודל העיקרי שמפעיל הרבה מממשקי ה-API האלה של GenAI.
לכן, הפחתנו את השימוש ב-RAM מ-3.6GB ל-1.1GB.
למעשה, יש 4 שיפורים עיקריים שביצענו כדי לאפשר את זה.
הראשונה היא שימוש בקוונטיזציה של 2 ביטים במקום 4 ביטים.
לכן, חצי מהביטים פירושו חצי מהבייטים שצריך להעביר דרך המודל.
השנייה היא שימוש במטמון KV משותף.
לכן, צמצמנו את זה באופן משמעותי על ידי כך שאפשרנו ל-20 השכבות הראשונות של המודל לשתף את זה בחבילה של 35MB, מה שאומר שעדיין אפשר לגשת לגודל ההקשר של 16, 000 טוקנים בתוך גודל החבילה הקטן מאוד הזה.
השלישית היא הפעלת מקודדי ראייה ואודיו עצלים.
לכן, בהרבה מהתרחישים לדוגמה של טקסט שאנחנו רואים, לא צריך לטעון את הגופנים האלה מלכתחילה.
כך תוכלו לחסוך עוד חצי ג'יגה בייט של זיכרון RAM.
ולבסוף, כדי לשפר את חיי הסוללה, הפעלנו גם את התאמת התדר של ה-NPU.
זה אומר שה-NPU יכול לפעול במהירות שנדרשת כדי לחסוך בחשמל, ועדיין לספק איכות גבוהה בתרחישי השימוש שלכם.
יש עוד כמה שיפורים שביצענו בנוגע למהירות.
הראשונה היא הפעלת שמירה במטמון של קידומות.
בדוגמה הזו, אפשר לראות שהמערכת משתמשת שוב בהנחיה בכמה סבבים, נכון?
ולכן העיבוד של זה הופך להיות יקר מאוד.
אבל עם שמירת קידומות במטמון, אנחנו בעצם אומרים שלא צריך לעבד מחדש את ההנחיה הזו למערכת שוב ושוב, נכון?
אפשר לשמור את זה במטמון של התחילית, כלומר בדוגמה הזו, אתם חוסכים 60% מהאסימונים שאתם צריכים להעביר, וזה גם אומר שהמהירות עולה ב-60% בכל מכשיר שבו אתם משתמשים.
השיפור השני שביצענו הוא פענוח ספקולטיבי.
בדוגמה הזו, אתם יכולים לראות ש-Gemini Nano יוצר אסימון אחד בכל מעבר.
הסיבה לכך היא שמודלים מסוג LLM הם אוטורגרסיביים, כלומר הם צריכים להמשיך לבחון את האסימונים הקודמים שנוצרו כדי להבין מה יהיו האסימונים הבאים.
כדי לפתור את הבעיה הזו, פיתחנו מודל טיוטה שיוצר הרבה אסימונים במהירות גבוהה יותר.
המודל הזה קטן בהרבה מ-Gemini Nano, ולכן הוא יכול ליצור הרבה טוקנים בבת אחת, להעביר אותם ל-Gemini ולבדוק אילו מהם תואמים לניחושים של Gemini Nano.
בדוגמה הזו, בסבב הראשון נוצרים שישה אסימונים, בסבב הבא נוצרים חמישה, ובסבב השלישי התהליך מסתיים.
אז מה שאנחנו רואים זה שבהרבה מתרחישי השימוש הפנימיים שאנחנו בוחנים, זה מוביל להאצה של פי 2 בממוצע.
יכול להיות שהשיפורים יהיו מהירים יותר, בהתאם לחומרה שלכם.
אוקיי.
אז זו אחת הסיבות, אתם יודעים, אם נחזור למהירות, אפשר לראות את ההאצה הדרמטית הזו, נכון, בין המכשיר לענן, אפילו ברשת 5G.
אפשר לראות שרק העלאת הנתונים האלה לענן תיקח אפילו שנייה אחת, נכון, עבור 1,000 עבור 1,300 אסימוני קלט, ולכן למרות שהמילוי מראש והפענוח במסע Cloud Journey עשויים להיות מהירים יותר, המסע מקצה לקצה עדיין מהיר משמעותית כשאתה פועל במכשיר.
ובכל מה שקשור לאיכות, אני שמח להודיע שהמודל הזה, עם 2 מיליארד פרמטרים, מציג ביצועים דומים לאלה של המודלים עם 27 מיליארד פרמטרים מהשנה שעברה.
אמנם הביצועים של המודלים האלה במכשיר לא טובים כמו של מודלים פורצי דרך שקיימים היום, אבל מה שאנחנו מראים כאן הוא שיש התקדמות משמעותית בכל מה שקשור ל-AI במודלים האלה במכשיר, נכון?
אפשר לראות שב-MMLU-Pro, שהוא מבחן השוואה סטנדרטי לידע, המודל 2B טוב כמעט כמו המודל עם 27 מיליארד פרמטרים מהשנה שעברה.
ובכל מה שקשור למדדים של מדע ברמת אוניברסיטה, הסקת מסקנות ושימוש בכלים של סוכנים, מודל 2B הזה למעשה טוב יותר, למרות שהוא פחות מעשירית מהגודל של המודל של השנה שעברה.
אנחנו צופים שהמגמות האלה יימשכו.
אנחנו ממשיכים לעבוד בשיתוף פעולה הדוק עם צוות GDM כדי לוודא שנוכל להמשיך לבצע את השיפורים האלה במודל, כך שתוכלו להפיק מהם את הערך המקסימלי.
דייוויד צ'ו
אוקיי.
אז הצגתי לכם הרבה יתרונות ושיפורים חדשים, ואולי אתם שואלים את עצמכם: "מה אנחנו יכולים לעשות בפועל עם המודלים האלה?"
אז יש את כל השיפורים האלה ביכולות שביצענו גם ב-API.
אז בעזרת הוראות המערכת, אתם יכולים להגדיר פרסונה.
עם קלט של כמה תמונות, אתם יכולים להעלות הרבה תמונות בבת אחת.
מוקדם יותר השנה הפעלנו גם את מצב החשיבה, כדי שתוכלו להקדיש יותר זמן לבעיות שדורשות הסקת מסקנות וגם לקריאות API לכלים.
ולבסוף, עם פלט מובנה, אפשר גם להוסיף הערות לכיתה של Kotlin ולקבל בחזרה אובייקט שתואם לסכימה הספציפית שלכם.
בנוסף, אנחנו גם מנסים לבקש מהמפתחים שיפורים חדשים שהם היו רוצים לבצע.
91% מהמפתחים שדיברנו איתם בסקר אמרו שהם היו משיקים חוויה אם אחת מהיכולות האלה הייתה זמינה.
לכן, מוקדם יותר השבוע השקנו את ארבע היכולות האלה: תרגום דיבור, הטמעות, המרת טקסט לדיבור וקריאה להפעלת פונקציות.
אתם יכולים לתמלל אודיו ביותר מ-20 שפות.
בעזרת הטמעות, אתם יכולים לחפש בתוכן של האפליקציה לפי משמעות סמנטית, ולא רק באמצעות חיפוש של מילות מפתח.
בעזרת המרת טקסט לדיבור (TTS), אתם יכולים לספק קולות אנושיים לחוויית השימוש שלכם, כך שלא תצטרכו יותר להשתמש בקולות רובוטיים.
ולבסוף, באמצעות קריאה לפונקציות, אתם יכולים לאפשר לאפליקציה לבצע פעולות בשם המשתמשים.
אנחנו כבר משתפים פעולה עם כמה מפתחים כדי לבדוק איך הם יכולים לשלב סוכנים באפליקציות שלהם, ואנחנו מקווים להשיק את האפשרות הזו בהמשך השנה הבאה.
דייוויד צ'ו
אז בהתחשב בכל היכולות האלה, בואו נעבור על דוגמה מקצה לקצה שבה אנחנו בונים עם כל היכולות האלה.
נניח שיש צלם אמריקאי שמטייל ביפן.
וכמו שאתם יכולים לראות בתמונה, ברור שאין להם מספיק מצלמות או עדשות.
הם מזמינים עדשה חדשה ומבקשים שישלחו אותה למלון.
אבל יש בעיה: במהלך היום, הם מקבלים הודעה קולית שבה נאמר להם שהם צריכים לשלם עמלת שחרור בסך 6, 800 ין כדי לקבל את החבילה עד סוף היום.
הם גם קיבלו הודעת טקסט עם תמונה של החבילה שמעוכבת.
אז במקרה הזה, המשתמש מעוניין בהגנה מסוג כלשהו של זיהוי תרמיות, נכון?
הם רוצים להבין איך אפשר לזהות אם הודעות קוליות הן לגיטימיות או שמקורן בתרמית.
לכן אנחנו רוצים לשמור על פרטיות המידע, לוודא שהמידע מבוסס, אבל גם שניתן להסביר אותו.
לכן אנחנו צריכים לוודא שהם יוכלו להבין את הטקסט ביפנית, וגם להשתמש ביכולות השפה שזמינות במקרה שיש להם צורכי נגישות.
אז יהיו כאן ארבעה שלבים, נכון?
השלב הראשון הוא פשוט לעבד את ההודעה הקולית לטקסט, כלומר לתרגם את ההודעה הקולית.
לשם כך נשתמש ב-Speech Translation API.
ואז הם יוכלו לשאול שאלה אם הם רוצים, אז נשתמש ב-Speech Recognition API.
אחר כך אנחנו רוצים להגדיר פונקציות שיכולות לאחזר את המידע שהמשתמש צריך כדי לקבל את מה שהוא רוצה.
לכן, נשתמש ב-Prompt API ובקריאה להפעלת פונקציות.
עכשיו, הפעולה הזו תתבצע ברקע בלי שהמשתמש יראה אותה.
בשלב השלישי, נבין איך לאחסן את המידע הזה ולאחזר אותו באמצעות ממשק ה-API להטמעה ו-AppSearch.
התהליך הזה יפעל גם ברקע.
בסופו של דבר, נגיע להחלטה על סמך המידע שאנחנו אוספים, באמצעות Prompt API, פלט מובנה וסינתזת דיבור.
אוקיי.
So, let's begin.
אנחנו נשתמש בתרגום דיבור ובזיהוי דיבור.
אז השלב הראשון הוא בעצם לקחת את ההודעה הקולית והטקסט, ולהפוך אותם למידע שימושי, נכון?
המשתמש קיבל את ההודעה הזו: "היי, אתה צריך לשלם את העמלה הזו בסך 6,800 ין.
יכול להיות שלא שילמתם מראש את המיסים, ולכן אתם צריכים לשלם אותם".
ולכן הם רוצים שתהיה להם אפשרות לתרגם את זה, נכון?
אחד הדברים שחשוב לציין כאן הוא שגם התרגום וגם השאלה של המשתמש לגבי המידע מועברים ל-Gemini Nano 4 באמצעות אותו מקודד אודיו, כי מקודד האודיו אומן על יותר מ-100 שפות.
כך תוכלו לקבל את התרגום הזה, וגם את ה-ASR של הטקסט של שאלת המשתמש.
כדי לתרגם, כל מה שצריך לעשות זה להגדיר את שפת המקור ואת שפת היעד.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)במקרה הזה, נשתמש ביפנית ובאנגלית אמריקאית.
אנחנו נגדיר תהליך Kotlin שמעביר את הבייטים ישירות למתרגם הדיבור.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)
val audioFlow = flow {
emit(TranslationAudio(Locale.JAPANESE, courierVoicemailBytes))
}
val request = SpeechTranslatorRequest.Builder(audioFlow).build()
translator.startTranslation(request).collect { response ->
if (response is SpeechTranslatorResponse.Text) {
translatedCourierClaim = response.translatedText
}
}ואז אנחנו יכולים לאסוף את התשובה באמצעות הטקסט המתורגם.
אוקיי.
מצוין.
אז עכשיו אנחנו מבינים מה באמת נאמר בהודעה הקולית, נכון?
יש חבילה של עדשת מצלמה.
היא מעוכבת במכס.
הם צריכים לשלם את העמלה בסך 6,800 ין.
ועכשיו המשתמש רוצה לשאול שאלה לגביו.
אוקיי.
אז האם העיכוב במכס אמיתי?
במקרה הזה, נשתמש ב-ASR API.
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)אנחנו עדיין מגדירים את השפה לאנגלית אמריקאית.
ואז אנחנו יכולים להגדיר אותו למצב מתקדם, שמשתמש במקודד האודיו.
אחר כך אנחנו יכולים להעביר גם את המידע הזה, לאסוף את התשובה הסופית, ואז אנחנו הופכים את זה למידע שאנחנו מעבירים ל-Gemini Nano.
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)
val request = speechRecognizerRequest {
audioSource = AudioSource.fromMic()
recognitionOptions { enableWordTiming = true } // optional
}
recognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse ->
liveCaption = response.text
is SpeechRecognizerResponse.FinalTextResponse ->
planAndCallVaultTool(translatedCourierClaim, response.text)
}
}אוקיי.
עכשיו סיימנו את החלק הראשון של תהליך הקליטה.
כמו שאפשר לראות, לא נדרשו הרבה שורות קוד.
דייוויד צ'ו
החלק השני הוא מיון, שבו חושבים איך להשתמש ב-Prompt API ובקריאה להפעלת פונקציות כדי להבין איזה סוג מידע נדרש.
אוקיי.
כדי לבנות את הארכיטקטורה של זה, נגדיר קודם כמה הוראות מערכת.
אחר כך, גם ההודעה הקולית, הטקסט, התמונה והשאלה של המשתמש יועברו כקלט ל-Gemini Nano.
ואז נגדיר פונקציות שבאמת מאחזרות את המידע שאנחנו צריכים.
retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)אז קודם כל, נגדיר את הוראות המערכת.
// Keep SystemInstruction static across Turn 1 & Turn 2:
val securitySystemInstruction = SystemInstruction(
"""
You are an on-device parcel & phishing security auditor.
1. Cross-check packaging, carrier labels and tracking across all images.
2. Verify customs fee claims against local DDP (Delivered Duty Paid) order records.
3. Flag any payment demand when the order record shows duties prepaid.
""".trimIndent()
)כאן אפשר לראות דוגמה פשוטה מאוד לאיך זה נראה, להגדיר מבקר אבטחה, מה שזה אומר זה שאנחנו הולכים להצליב אריזות ותוויות, להסתכל על עמלות מותאמות אישית, וגם לסמן כל דרישות תשלום נחוצות.
יכול להיות שתגלו שאופטימיזציה של ההנחיות תעזור לכם מאוד לשפר את האיכות באופן משמעותי.
אחרי שנגדיר את סט פקודות המערכת, נוכל לקחת את כל התוכן מהשלב הקודם, התרגום של ההודעה הקולית והטקסט, וגם את השאלה של המשתמש, ולהעביר את כל זה לשדה TextPart, יחד עם הוראות המערכת כדי לקבל את המידע הזה.
// Pull in the Speech Translation (AST) & Speech Recognition (ASR) outputs:
val turn1Prompt = """
Translated voicemail: $translatedCourierClaim
Incoming text message: $callerText
User's spoken question: $userQuestion
""".trimIndent()
val turn1Request = generateContentRequest(
securitySystemInstruction,
TextPart(turn1Prompt)
) { /* enableThinking & setTools */ }ואז אנחנו רוצים להגדיר פונקציה שנוכל להשתמש בה.
לכן, בדוגמה הספציפית הזו, אנחנו שמחים להודיע ששילבנו את פונקציית האפליקציה.
אז אם אתם מכירים את זה, אתם בטח כבר מבינים פחות או יותר איך זה עובד.
כאן אנחנו מגדירים נקודת כניסה של AppFunctionService, ואז מגדירים פונקציית אפליקציה.
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}אפשר לראות שזה בדיוק אותו תהליך כמו הגדרת פונקציות של אפליקציה לסוכן מערכת.
אתה מגדיר בפרמטרים של searchQuery את עמלת התביעה, וגם אם אנחנו צריכים להשוות בין תמונות של חבילות, רק כדוגמה לפרמטרים, נכון?
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
/**
* Searches local orders and order images in AppSearch.
* @param searchQuery Semantic query to match the order
* @param claimedFeeUsd Customs fee claimed in USD
* @param comparePackagePhotos Whether to fetch order images to compare
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun retrieveOrderAndDeliveryPhotos(
searchQuery: String,
claimedFeeUsd: Int,
comparePackagePhotos: Boolean
): VaultMatch = withContext(Dispatchers.IO) { executeVaultSearch(searchQuery) }
}אתם יכולים להגדיר יעדים אחרים.
ולבסוף, לפונקציה הזו יש פונקציית executeVaultSearch שנגדיר בהמשך.
אחד הדברים שאני רוצה לציין כאן הוא שיש לנו מצב חשיבה זמין.
val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()מומלץ מאוד להשתמש בו כשמפעילים קריאות API לכלים.
הסיבה לכך היא שערכנו הרבה בדיקות שהראו שיפור של 13 נקודות בדיוק מתוך 100 כשמפעילים קריאות API לכלים ב-Gemini Nano 4.
במקרים מסוימים, הוא אפילו גבוה בהרבה, ולכן יכול להיות שהתוצאות יהיו הרבה יותר טובות.
אם אתם רוצים לדעת יותר על קריאה להפעלת פונקציות, אתם מוזמנים להשתתף בסשן של הקולגות שלי, יולנדה וויקטוריה, שיתקיים בהמשך.
המידע הזה מתייחס ליכולות אג'נטיות באפליקציה.
אני חושב שוויקטוריה יושבת איפשהו בקהל, אז כן, אתם מוזמנים להגיד גם לה שלום.
דייוויד צ'ו
אוקיי, מגניב.
עכשיו סיימנו את שני השלבים הראשונים.
בואו נדבר על איך השליפה עובדת.
לכן נשתמש בהטמעות וב-AppSearch.
אז אולי נחזור כמה צעדים אחורה, נכון, במסלול של המשתמש, בנקודה שבה הוא הזמין את העדשה, מה שנרצה לעשות זה בעצם להוסיף את המידע הזה לאינדקס כדי שהוא יהיה נגיש למשתמש וגם למפתח, נכון?
אז כאן יש לנו פרטים על מספר החבילה של ההזמנה.
יש לו גם מספר מעקב.
הוא מכיל פרטים על המקום שאליו הוא נשלח.
וכאן אפשר לראות שכבר מצוין שהמיסים שולמו מראש.
לכן, מה שנעשה זה ליצור הטמעה מולטי-מודאלית כדי להוסיף את המידע הזה לאינדקס יחד עם כל מידע אחר שעשוי להיות חשוב לנו.
ככה זה נראה.
val embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vectorאנחנו לוקחים את האימייל של ההזמנה.
שוב, פשוט מאוד, אנחנו פשוט מעבירים תמונה אחת וטקסט אחד, אבל יכול להיות שיש לכם עוד תוכן שאתם רוצים להוסיף לאינדקס כאן.
ואז ניצור את וקטור ההטמעה.
אחרי שנעשה את זה, נשתמש ב-AppSearch כדי ליצור מסמך, ואז נגדיר את המאפיינים שנחוצים ליצירת המסמך הספציפי הזה.
val embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vector
val doc = GenericDocument.Builder<GenericDocument.Builder<*>>(
"orders", "order_jp_904", "OrderRecord"
)
.setPropertyString("summary", "Shipped DDP, duties prepaid ($0 owed). In transit.")
.setPropertyString("photoUris", "order_email_lens.png")
.setPropertyEmbedding(
"embedding",
EmbeddingVector(docVector, embedModel.getModelSignature())
).build()חשוב לציין שהסיבה שאנחנו רוצים להשתמש בהטמעות מולטי-מודאליות היא שיכול להיות שאין כאן מילים משותפות, אבל בגלל שהתמונות והטקסט עוסקים בתוכן דומה, זה עוזר לנו לקבל את הדמיון הסמנטי.
לכן, אנחנו יכולים להשתמש בדמיון קוסינוס כדי למצוא פריטים שאין להם מילים משותפות.
אוקיי, אז זו הפונקציה executeVaultSearch שדיברנו עליה קודם.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}זה מה שנקרא על ידי פונקציית האפליקציה שהגדרנו.
אז בדוגמה הזו, אנחנו מעבירים את שאילתת החיפוש שנוצרה על ידי Gemini Nano.
ואז נשתמש באסטרטגיית דירוג, דמיון קוסינוס.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
}ואז, this.matchedSemanticScores פשוט עוזר לנו לקבל את המידע הזה.
ולבסוף, נציין סף של 0.68 כדי לקבוע מהו דמיון הקוסינוס המינימלי כדי שנוכל לאחזר מידע.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
val results = appSearchSession.search(
"semanticSearch(getEmbeddingParameter(0), 0.68)", spec
)
// Cleanly close the cursor.
return results.use { it.nextPageAsync.await().first().genericDocument.toVaultMatch() }
}אז כדאי לכם להתנסות בזה כשאתם עוברים על תרחישי השימוש שלכם, כדי לראות מה באמת עובד בשבילכם.
עוד דבר שחשוב לציין לגבי האבטחה של המודל הזה, הוא ש-Gemini Nano לא מריץ את הקוד, נכון?
אז Gemini Nano רק שואל שאלה, אבל האפליקציה שלכם מגדירה את הקוד, ואז היא למעשה זו שמריצה את הקוד עצמו, מאחזרת את המידע הזה, ואז קובעת מה להעביר ל-Gemini Nano.
כך יש לכם יותר שליטה על מה שאתם רוצים לחשוף ל-Gemini Nano, וזה גם עוזר להבטיח יותר פרטיות ואבטחה למשתמשים שלכם.
דייוויד צ'ו
אוקיי, ועכשיו השלב האחרון.
אז יש לנו את כל המידע שאנחנו צריכים.
אנחנו נשתמש ב-Prompt API, בפלט מובנה ובהמרת טקסט לדיבור כדי לבצע את ההשוואות, ואז נחזיר את המידע הזה למשתמש בצורה מובנת.
אוקיי, אז באופן בסיסי, מה שאנחנו רוצים לדעת זה אם החבילה הזו היא בעצם אותה חבילה כמו המידע מהמשתמש, שהוא ההזמנה האמיתית.
כדי לעשות את זה, אנחנו ניצור הטמעה גם על המידע מההודעה הקולית וגם על הטקסט, ונהפוך את זה לאותו וקטור הטמעה שיצרנו קודם.
val callerVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(callerParcelPhotoBmp), // brown box w/ shipping label
TextPart(callerText) // "Clearance fee ¥6,800 due today."
))
)
).embedding.vector
val matchScore = cosineSimilarity(callerVector, vaultMatch.embedding)ואז, נשתמש ב-matchScore, באמצעות דמיון קוסינוס, נוכל להשוות בין ההתאמה ששלפנו מהכספת לבין המידע מהמתקשר.
כמו שאתם רואים, גם אם התמונות לא תואמות, הטקסט עדיין מאוד עוזר.
כאן הטמעות מולטי-מודאליות יכולות להיות שימושיות מאוד.
אפשר להזין חלקי מידע שונים ואז להגיע למשמעות סמנטית משותפת, נכון?
לכן, ההטמעה המולטי-מודאלית מאפשרת למפות את כל הנתונים לאותו מרחב וקטורי.
שוב, נשתמש כאן במצב חשיבה כדי לשפר את החשיבה שלנו.
val forensicRequest = generateContentRequest(
securitySystemInstruction,
content {
image(callerParcelPhotoBmp)
image(orderEmailLensBmp)
text(forensicAuditPrompt)
}
) {
// Enable Gemini Nano 4 reasoning before it answers
enableThinking = true
}ואז נגדיר גם מחלקת פלט, נכון?
@Generable(description = "Details of a visual contradiction found during an audit")
data class VisualContradiction(
@Guide(
description = "The type of contradiction",
enumValues = ["PACKAGING_MISMATCH", "STATUS_CONFLICT", "DUTY_FRAUD"],
)
val contradictionType: String,
@Guide(description = "Detailed explanation of the visual contradiction")
val explanation: String,
)אז כל מה שאנחנו עושים כאן זה להגדיר VisualContradiction ולהגדיר כמה ערכי enum שונים של הסוגים השונים של הסתירות האפשריות שיכולות להיות לנו, נכון?
לא משנה אם זה DUTY_FRAUD, PACKAGING_MISMATCH או STATUS_CONFLICT.
וכאן יהיה הסבר.
ואז אנחנו הולכים להעביר את כל המידע הזה לפסיקת ביקורת, נכון?
@Generable(description = "Verdict and details of a phishing audit")
data class PhishingAuditVerdict(
@Guide(
description = "The audit verdict",
enumValues = ["CONFIRMED_PHISHING_SCAM", "LEGITIMATE_HOLD"],
)
val verdict: String,
@Guide(description = "The fraud risk score from 0 to 100", minimum = 0.0, maximum = 100.0)
val fraudRiskScore: Int,
@Guide(description = "Estimated amount of money saved in USD", minimum = 0.0)
val moneySavedUsd: Int,
@Guide(description = "List of visual contradictions identified during audit", minItems = 0)
val contradictions: List<VisualContradiction>,
// @Guide(description = "Summary of the warning to be spoken to the user")
// `@Guide` annotations are optional.
val spokenWarningSummary: String,
)כמו שאתם רואים, זה מאוד פשוט.
אנחנו רק שואלים אם זו תרמית או לא.
יכול להיות שיש לנו fraudRiskScore שבו אפשר להגדיר ערך מינימלי וערך מקסימלי מ-0 עד 100.
אתם יכולים אפילו להראות למשתמש כמה כסף חסכתם לו, אם אתם רוצים לעקוב אחרי הנתון הזה לאורך זמן.
ואז אתם יכולים גם לפרט את כל הסתירות החזותיות שהגדרנו.
עוד דבר שחשוב לציין הוא שההערה @Guide היא אופציונלית.
אתם לא חייבים להגדיר את זה ולספק תיאור כזה.
הסיבה לכך היא שלפעמים אי אפשר להסביר מה קורה רק באמצעות הגדרת המשתנה.
התיאור הזה יכול לעזור ל-Gemini Nano להבין אילו פונקציות ספציפיות להפעיל ואיך להגדיר אותן כך שהמידע הנכון יוזן בשדות הנכונים.
אז כן, אחרי שמגדירים את זה, זה מאוד פשוט.
אתם פשוט מריצים כאן שלוש שורות קוד.
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().responsegenerateTypedContentRequest, אתם יוצרים את התוכן הזה ואז אתם יכולים לאחזר את התשובה הזו.
וכאן יש דוגמה לאיך זה ייראה, נכון?
{
"verdict": "CONFIRMED_PHISHING_SCAM",
"fraudRiskScore": 98,
"moneySavedUsd": 45,
"contradictions": [
{ "contradictionType": "DUTY_FRAUD", "explanation": "Fee demanded, but duties were prepaid." },
{ "contradictionType": "PACKAGING_MISMATCH", "explanation": "Carton has no carrier label or tracking." },
],
"spokenWarningSummary": "Scam blocked. Do not pay the 45 dollar fee. Your lens shipped duties prepaid."
}כך אנחנו מאשרים שמדובר בתרמית, מקבלים חישוב של ציון סיכונים גבוה מאוד, וזה מסביר את הסתירות שברורות בנתונים שאחזרתם בהשוואה לפרטי המתקשר.
בנוסף, חסכנו למשתמש הזה סכום כסף מסוים, אז אנחנו יכולים להוסיף שם מעקב.
כן.
אז זה פחות או יותר מסכם את כל המידע שאנחנו צריכים.
והשלב האחרון הוא להציג את זה למשתמש בצורה נגישה.
למשל, אם הם בדרכים או אם יש להם לקות ראייה, אולי כדאי להקריא את המידע הזה בקול במקום שהם יצטרכו לקרוא את הטקסט בעצמם.
לכן נטמיע את יכולת המרת הטקסט לדיבור (TTS) כדי להראות איך זה ייראה.
val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
/* more code here */
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}אז שוב, זה ממש פשוט, אנחנו מציינים לוקאל וגם שפה, ואז אנחנו מעבירים את פסק הדין שיצרנו באמצעות @Generable ו-@Guide.
ואחרי שזה קורה, זה כמעט מיידי.
val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> { }
else -> {}
}
}
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}ברוב המכשירים, יצירת התשובה הזו תימשך פחות מ-120 אלפיות השנייה.
אנחנו נפנה את הזיכרון שצריך.
ואז, אם צריך, אנחנו יכולים גם להגדיר חלופה, נכון, באמצעות ממשקי API של הפלטפורמה.
val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> {
/* handle fallback to platform API */
}
else -> {}
}
}
}
// more code here ...הקול עדיין יהיה רובוטי, אבל לפחות המשתמשים יוכלו לשמוע את הטקסט אם הם לא יכולים לקרוא אותו.
דייוויד צ'ו
אוקיי.
אז זה היה הרבה, אבל כדי לסכם את מה שעשינו, נכון, השתמשנו בכל ממשקי ה-API השונים האלה בתהליכי הקליטה, המיון, השליפה והביקורת.
למשל, שימוש בממשקי API לזיהוי דיבור ולתרגום, ב-Prompt API ובקריאה להפעלת פונקציות, בהטמעות וב-AppSearch, וגם בפלט מובנה ובהמרת טקסט לדיבור (TTS).
זו רק דוגמה לאופן שבו אפשר לנצל את כל היכולות האלה בצורה עקבית ולהפעיל את זה לגמרי במכשיר.
ובחזרה למה שדיברנו עליו בהתחלה, יש יתרונות משמעותיים להפעלת הקמפיין הזה, נכון?
אם תפעילו את זה במכשיר שלכם, תראו שזה מהיר יותר במכשיר ברוב סוגי החומרה.
זה לא עולה לכם כלום, נכון? ואתם יכולים לחסוך מאות אלפי דולרים אם אתם מפעילים את זה כל הזמן.
והיא זמינה גם אם המשתמש נמצא בהרים או במרתף ואין לו גישה לאינטרנט.
הוא גם פרטי לחלוטין, כי לא צריך לשלוח מידע לענן.
אז בהתחשב בכל היכולות האלה, אתם מוזמנים לעיין במסמכים, להסתכל על קוד ה-QR או לעבור לקישור.
אלה כל היכולות שהשקנו בערך בששת החודשים האחרונים.
תודה רבה לכולכם שהקדשתם זמן להשתתף בשיחה הזו.
תודה שהקדשת לנו זמן.
אז כן, תודה רבה.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)
val audioFlow = flow {
emit(TranslationAudio(Locale.JAPANESE, courierVoicemailBytes))
}
val request = SpeechTranslatorRequest.Builder(audioFlow).build()
translator.startTranslation(request).collect { response ->
if (response is SpeechTranslatorResponse.Text) {
translatedCourierClaim = response.translatedText
}
}val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)
val request = speechRecognizerRequest {
audioSource = AudioSource.fromMic()
recognitionOptions { enableWordTiming = true } // optional
}
recognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse ->
liveCaption = response.text
is SpeechRecognizerResponse.FinalTextResponse ->
planAndCallVaultTool(translatedCourierClaim, response.text)
}
}retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)// Keep SystemInstruction static across Turn 1 & Turn 2:
val securitySystemInstruction = SystemInstruction(
"""
You are an on-device parcel & phishing security auditor.
1. Cross-check packaging, carrier labels and tracking across all images.
2. Verify customs fee claims against local DDP (Delivered Duty Paid) order records.
3. Flag any payment demand when the order record shows duties prepaid.
""".trimIndent()
)// Pull in the Speech Translation (AST) & Speech Recognition (ASR) outputs:
val turn1Prompt = """
Translated voicemail: $translatedCourierClaim
Incoming text message: $callerText
User's spoken question: $userQuestion
""".trimIndent()
val turn1Request = generateContentRequest(
securitySystemInstruction,
TextPart(turn1Prompt)
) { /* enableThinking & setTools */ }// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
/**
* Searches local orders and order images in AppSearch.
* @param searchQuery Semantic query to match the order
* @param claimedFeeUsd Customs fee claimed in USD
* @param comparePackagePhotos Whether to fetch order images to compare
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun retrieveOrderAndDeliveryPhotos(
searchQuery: String,
claimedFeeUsd: Int,
comparePackagePhotos: Boolean
): VaultMatch = withContext(Dispatchers.IO) { executeVaultSearch(searchQuery) }
}val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()val embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vectorval embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vector
val doc = GenericDocument.Builder<GenericDocument.Builder<*>>(
"orders", "order_jp_904", "OrderRecord"
)
.setPropertyString("summary", "Shipped DDP, duties prepaid ($0 owed). In transit.")
.setPropertyString("photoUris", "order_email_lens.png")
.setPropertyEmbedding(
"embedding",
EmbeddingVector(docVector, embedModel.getModelSignature())
).build()suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
}suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
val results = appSearchSession.search(
"semanticSearch(getEmbeddingParameter(0), 0.68)", spec
)
// Cleanly close the cursor.
return results.use { it.nextPageAsync.await().first().genericDocument.toVaultMatch() }
}val callerVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(callerParcelPhotoBmp), // brown box w/ shipping label
TextPart(callerText) // "Clearance fee ¥6,800 due today."
))
)
).embedding.vector
val matchScore = cosineSimilarity(callerVector, vaultMatch.embedding)val forensicRequest = generateContentRequest(
securitySystemInstruction,
content {
image(callerParcelPhotoBmp)
image(orderEmailLensBmp)
text(forensicAuditPrompt)
}
) {
// Enable Gemini Nano 4 reasoning before it answers
enableThinking = true
}@Generable(description = "Details of a visual contradiction found during an audit")
data class VisualContradiction(
@Guide(
description = "The type of contradiction",
enumValues = ["PACKAGING_MISMATCH", "STATUS_CONFLICT", "DUTY_FRAUD"],
)
val contradictionType: String,
@Guide(description = "Detailed explanation of the visual contradiction")
val explanation: String,
)@Generable(description = "Verdict and details of a phishing audit")
data class PhishingAuditVerdict(
@Guide(
description = "The audit verdict",
enumValues = ["CONFIRMED_PHISHING_SCAM", "LEGITIMATE_HOLD"],
)
val verdict: String,
@Guide(description = "The fraud risk score from 0 to 100", minimum = 0.0, maximum = 100.0)
val fraudRiskScore: Int,
@Guide(description = "Estimated amount of money saved in USD", minimum = 0.0)
val moneySavedUsd: Int,
@Guide(description = "List of visual contradictions identified during audit", minItems = 0)
val contradictions: List<VisualContradiction>,
// @Guide(description = "Summary of the warning to be spoken to the user")
// `@Guide` annotations are optional.
val spokenWarningSummary: String,
)val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().response{
"verdict": "CONFIRMED_PHISHING_SCAM",
"fraudRiskScore": 98,
"moneySavedUsd": 45,
"contradictions": [
{ "contradictionType": "DUTY_FRAUD", "explanation": "Fee demanded, but duties were prepaid." },
{ "contradictionType": "PACKAGING_MISMATCH", "explanation": "Carton has no carrier label or tracking." },
],
"spokenWarningSummary": "Scam blocked. Do not pay the 45 dollar fee. Your lens shipped duties prepaid."
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
/* more code here */
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> { }
else -> {}
}
}
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> {
/* handle fallback to platform API */
}
else -> {}
}
}
}
// more code here ...