אפליקציות חכמות

בינה מובנית במכשיר: ביצועים ואיכות משופרים באמצעות ממשקי ה-API של Android ML Kit

רמקולים

מידע כללי

מידע על ההרצאה

במפגש הזה נבחן תרחישים שונים של שימוש ב-AI גנרטיבי באמצעות ממשקי ה-API של ML Kit, כדי להבין מתי AI גנרטיבי במכשיר מתאים לאפליקציה, וגם כדי להבין איך להעריך תרחישים קיימים של שימוש ב-AI גנרטיבי כדי לבדוק אם מודלים במכשיר יכולים לעמוד ברף האיכות שלהם.

סיכום

  • ל-AI במכשיר יש יתרונות משמעותיים במהירות ובפרטיות בהשוואה להסקת מסקנות בענן. בדיקות פנימיות מראות שהמהירות גדלה פי 2 בהשוואה להרצת מודלים בענן.
  • השימוש בזיכרון של Gemini Nano ירד מ-3.6GB ל-1.1GB. האופטימיזציות העיקריות כוללות קוונטיזציה של 2 ביטים ומטמון KV משותף.
  • שמירת קידומות במטמון ופענוח ספקולטיבי מאיצים באופן משמעותי את תהליך ההסקה. שימוש חוזר בהנחיות המערכת מפחית ב-60% את מספר הטוקנים שעוברים עיבוד, ומודל טיוטה קטן יותר מאיץ את התהליך פי 2 בממוצע.
  • יכולות חדשות במכשיר מרחיבות את האינטראקציות עם האפליקציה באמצעות תרגום דיבור וקריאה להפעלת פונקציות. הוספת מצב החשיבה משפרת את הדיוק של קריאות ה-API לכלי ב-Gemini Nano 4 ב-13 נקודות מתוך 100.
  • מערכי הטמעה מולטי-מודאליים משולבים עם AppSearch כדי לאחסן ולאחזר נתונים באמצעות דמיון קוסינוס. האפליקציה מפעילה את לוגיקת החיפוש באופן מקומי, וכך שומרת על פרטיות המשתמשים ועל אבטחת המידע.

משאבים

Gemini Notebook

נושאים

  • on-device-ai
  • ml-kit
  • gemini-nano
  • aicore
  • קריאה להפעלת פונקציות
  • הטמעות (embeddings)
  • תרגום דיבור

תמליל

הורדת התמליל

מבוא ל-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 לכלי שקל להשתמש בו, כדי שתוכלו להתמקד ביצירת חוויית שימוש נהדרת, ואנחנו נדאג לכל השאר.

אופטימיזציה של הביצועים והבנצ'מרקים של Gemini Nano

  • דייוויד צ'ו

    אנחנו גם שמחים להודיע שהפחתנו משמעותית את נפח ה-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 כדי לוודא שנוכל להמשיך לבצע את השיפורים האלה במודל, כך שתוכלו להפיק מהם את הערך המקסימלי.

סקירה כללית של היכולות החדשות של Gemini API

  • דייוויד צ'ו

    אוקיי.

  • אז הצגתי לכם הרבה יתרונות ושיפורים חדשים, ואולי אתם שואלים את עצמכם: "מה אנחנו יכולים לעשות בפועל עם המודלים האלה?"

  • אז יש את כל השיפורים האלה ביכולות שביצענו גם ב-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 וקריאה להפעלת פונקציות

  • דייוויד צ'ו

    החלק השני הוא מיון, שבו חושבים איך להשתמש ב-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

  • דייוויד צ'ו

    אוקיי, מגניב.

  • עכשיו סיימנו את שני השלבים הראשונים.

  • בואו נדבר על איך השליפה עובדת.

  • לכן נשתמש בהטמעות וב-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().response
  • generateTypedContentRequest, אתם יוצרים את התוכן הזה ואז אתם יכולים לאחזר את התשובה הזו.

  • וכאן יש דוגמה לאיך זה ייראה, נכון?

    {
      "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 או לעבור לקישור.

  • אלה כל היכולות שהשקנו בערך בששת החודשים האחרונים.

  • תודה רבה לכולכם שהקדשתם זמן להשתתף בשיחה הזו.

  • תודה שהקדשת לנו זמן.

  • אז כן, תודה רבה.

קוד

קטעי קוד

קטע קוד ב-Kotlin בדקה 12:38 בסרטון "הדרכה מקצה לקצה בנושא זיהוי תרמיות"
val translator = SpeechTranslation.getClient(
    SpeechTranslatorOptions.Builder(
        Locale.JAPANESE, Locale.US
    ).build()
)
קטע קוד ב-Kotlin בדקה 12:47 בסרטון "הדרכה מקיפה בנושא זיהוי תרמיות"
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
    }
}
קטע קוד ב-Kotlin בדקה 13:13 בסרטון "הדרכה מקיפה בנושא זיהוי תרמיות"
val recognizer = SpeechRecognition.getClient(
    speechRecognizerOptions {
        locale = Locale.US
        preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
    }
)
קטע קוד ב-Kotlin בדקה 13:24 בסרטון "הדרכה מקיפה בנושא זיהוי תרמיות"
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)
    }
}
קטע קוד ב-Kotlin בדקה 14:03 בסרטון "Triaging inputs using Prompt API and קריאה להפעלת פונקציות"
retrieveOrderAndDeliveryPhotos(
  searchQuery = "camera lens customs",
  claimedFeeUsd = 45,
  comparePackagePhotos = true
)
קטע קוד ב-Kotlin בדקה 14:07 בסרטון "Triaging inputs using Prompt API and קריאה להפעלת פונקציות"
// 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()
)
קטע קוד ב-Kotlin בדקה 14:33 בסרטון "Triaging inputs using Prompt API and קריאה להפעלת פונקציות"
// 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 */ }
קטע קוד ב-Kotlin בדקה 15:04 בסרטון "Triaging inputs using Prompt API and קריאה להפעלת פונקציות"
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {

}
קטע קוד ב-Kotlin בדקה 15:15 בסרטון "Triaging inputs using Prompt API and קריאה להפעלת פונקציות"
// 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) }
}
קטע קוד ב-Kotlin בדקה 15:35 בסרטון "Triaging inputs using Prompt API and קריאה להפעלת פונקציות"
val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {

    enableThinking = true
    setTools(vaultTools)

}
val call = generativeModel.generateContent(turn1Request)
    .candidates.first().functionCalls.first()
קטע קוד ב-Kotlin בדקה 17:03 בסרטון "אחזור נתוני הזמנות באמצעות הטמעות ו-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
קטע קוד ב-Kotlin בדקה 17:16 בסרטון "אחזור נתוני הזמנות באמצעות הטמעות ו-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()
קטע קוד ב-Kotlin בדקה 17:48 בסרטון "אחזור נתוני הזמנות באמצעות הטמעות ו-AppSearch"
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
    val qVec = embedModel.generateEmbedding(
        EmbeddingRequest(
            content = Content(listOf(TextPart(searchQuery)))
        )
    ).embedding.vector

}
קטע קוד ב-Kotlin בדקה 18:03 בסרטון "אחזור נתוני הזמנות באמצעות הטמעות ו-AppSearch"
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()

}
קטע קוד ב-Kotlin בדקה 18:12 בסרטון "אחזור נתוני הזמנות באמצעות הטמעות ו-AppSearch"
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() }
}
קטע קוד ב-Kotlin בדקה 19:24 בסרטון Comparing order details and defining audit verdicts
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)
קטע קוד ב-Kotlin בדקה 20:10 בסרטון "השוואה בין פרטי ההזמנה והגדרת תוצאות הביקורת"
val forensicRequest = generateContentRequest(
    securitySystemInstruction,
    content {
        image(callerParcelPhotoBmp)
        image(orderEmailLensBmp)
        text(forensicAuditPrompt)
    }
) {

    // Enable Gemini Nano 4 reasoning before it answers
    enableThinking = true

}
קטע קוד ב-Kotlin בדקה 20:15 בסרטון Comparing order details and defining audit verdicts
@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,
)
קטע קוד ב-Kotlin בדקה 20:35 בסרטון "Comparing order details and defining audit verdicts"
@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,
)
קטע קוד ב-Kotlin בדקה 21:35 בסרטון Comparing order details and defining audit verdicts
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().response
קטע קוד ב-JSON בדקה 21:44 בסרטון Comparing order details and defining audit verdicts
{
  "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."
}
קטע קוד ב-Kotlin בדקה 22:22 בסרטון "השוואה בין פרטי ההזמנה והגדרת תוצאות הביקורת"
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
}
קטע קוד ב-Kotlin בדקה 22:38 בסרטון "Comparing order details and defining audit verdicts"
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
}
קטע קוד ב-Kotlin בדקה 22:49 בסרטון "Comparing order details and defining audit verdicts"
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 ...