Inteligentne aplikacje

Inteligentne technologie na urządzeniu: zoptymalizowana wydajność i jakość dzięki interfejsom API ML Kit na Androida

Głośniki

Informacje

Informacje o wystąpieniu

Podczas tej sesji omówimy różne przypadki użycia generatywnej AI z wykorzystaniem interfejsów API ML Kit, aby dowiedzieć się, kiedy generatywna AI na urządzeniu jest idealnym rozwiązaniem dla aplikacji. Dowiemy się też, jak oceniać istniejące przypadki użycia generatywnej AI, aby sprawdzić, czy modele na urządzeniu mogą spełnić wymagania dotyczące jakości.

Podsumowanie

  • AI na urządzeniu oferuje znaczne korzyści w zakresie szybkości i prywatności w porównaniu z wnioskowaniem w chmurze. Testy wewnętrzne wykazały 2-krotne przyspieszenie w porównaniu z uruchamianiem modeli w chmurze.
  • Wykorzystanie pamięci przez Gemini Nano spadło z 3,6 GB do 1,1 GB. Najważniejsze optymalizacje obejmują 2-bitową kwantyzację i współdzieloną pamięć podręczną KV.
  • Buforowanie prefiksów i dekodowanie spekulatywne znacznie przyspieszają wnioskowanie. Ponowne używanie promptów systemowych zmniejsza liczbę przetwarzanych tokenów o 60%, a mniejszy model generujący zapewnia średnio 2-krotne przyspieszenie.
  • Nowe funkcje na urządzeniu rozszerzają interakcje z aplikacjami dzięki tłumaczeniu mowy i wywoływaniu funkcji. Dodanie trybu rozumowania zwiększa dokładność wywoływania narzędzi w Gemini Nano 4 o 13 punktów na 100.
  • Multimodalne wektory dystrybucyjne łączą się z AppSearch, aby przechowywać i pobierać dane za pomocą podobieństwa cosinusowego. Aplikacja wykonuje logikę wyszukiwania lokalnie, chroniąc prywatność użytkownika i zapewniając bezpieczeństwo danych.

Zasoby

Gemini Notebook

Tematy

  • on-device-ai
  • ml-kit
  • gemini-nano
  • aicore
  • wywoływanie funkcji
  • reprezentacje właściwościowe
  • speech-translation

Transkrypcja

Pobierz transkrypcję

Wprowadzenie do AI na urządzeniu

  • David Chou

    Hej,

  • Dzień dobry wszystkim.

  • Nazywam się David Chou.

  • Jestem kierownikiem ds. usług produktowych w zespole Android AICore.

  • Chętnie opowiem o naszych funkcjach AI na urządzeniu.

  • Dzisiaj omówimy kilka kluczowych obszarów.

  • Po pierwsze, zastanówmy się, dlaczego warto korzystać z AI na urządzeniu, zwłaszcza biorąc pod uwagę przyspieszenie rozwoju AI w chmurze.

  • Po drugie, zastanawiamy się nad ulepszeniami, które wprowadziliśmy w ciągu ostatniego roku, aby te modele AI na urządzeniu były niezwykle wydajne.

  • Na koniec omówimy kompleksowy przykład, który pokazuje, jak można wykorzystać wszystkie te zalety dzięki AI na urządzeniu.

  • Zacznijmy od tego, dlaczego warto korzystać z AI na urządzeniu.

  • Tak naprawdę istnieją 4 główne powody.

  • Pierwsza to szybkość, druga to koszt, trzecia to dostępność, a czwarta to prywatność.

  • Jeśli chodzi o szybkość, wnioskowanie AI na urządzeniu osiąga już poziom, na którym może być szybsze niż w chmurze, ponieważ nie trzeba przesyłać informacji między urządzeniem użytkownika a chmurą.

  • W przypadku wielu testowanych przez nas zastosowań obserwujemy dwukrotne przyspieszenie działania AI na urządzeniu w porównaniu z AI w chmurze.

  • Drugi to koszty.

  • Wyobraźmy sobie, że wykonujesz milion wnioskowań dziennie.

  • Możesz zaoszczędzić setki tysięcy dolarów, jeśli uruchomisz AI na urządzeniu, a nie w chmurze.

  • Trzecia to dostępność offline.

  • Na początku tego roku wysłaliśmy ankietę, w której 62% programistów stwierdziło, że dostępność offline jest jednym z 3 głównych powodów korzystania z AI na urządzeniu.

  • Niezależnie od tego, czy użytkownik znajduje się na wysokości 10 tysięcy metrów, czy w podziemiach dworca w Tokio i nie ma dostępu do internetu, nadal może korzystać ze wszystkich tych funkcji.

  • Ostatnim z najważniejszych powodów, które podawali deweloperzy w ankietach, była prywatność.

  • 72% ankietowanych uznało, że jest to jeden z 3 najważniejszych powodów.

  • 40% ankietowanych uznało go za najważniejszy.

  • Użytkownicy chcą zachować prywatność swoich wiadomości, notatek głosowych, zdjęć, a nawet informacji finansowych czy medycznych, dlatego wnioskowanie na urządzeniu będzie dla nich bardzo przydatne.

  • Biorąc pod uwagę wszystkie te korzyści, stworzyliśmy ML Kit, aby umożliwić Ci utrzymanie i wykorzystanie wszystkich tych możliwości.

  • Staramy się, aby było to jak najprostsze.

  • Jeśli masz jakieś skargi, skontaktuj się ze mną.

  • Na koniec podam moje dane kontaktowe.

  • Chętnie dowiemy się, jak możemy Ci pomóc w realizacji Twoich przypadków użycia.

  • Na początek udostępniliśmy szereg funkcji, takich jak mowa i kreatywne zastosowania.

  • Możesz na przykład sprawdzać i poprawiać tekst, a także korzystać z elastycznego tworzenia promptów multimodalnych.

  • Naszym celem jest więc, aby było to jak najbardziej użyteczne.

  • Dlatego wielu naszych użytkowników i deweloperów już zaczęło korzystać z tego rozwiązania.

  • Mamy już 9 aplikacji innych firm, które zostały pobrane 10 milionów razy i korzystają z tych funkcji generatywnej AI w środowisku produkcyjnym.

  • Widzimy też znacznie większe skalowanie zarówno w przypadku naszych aplikacji, jak i aplikacji innych firm, a także wzrost liczby uruchomień aplikacji z funkcjami AI o 110% kwartał do kwartału.

  • Dlatego zachęcamy do wypróbowania naszych interfejsów API w zakresie komunikacji, kreacji i finansów.

  • Uważamy, że możliwości są nieograniczone, a Ty prawdopodobnie masz lepszą wyobraźnię niż my w przypadku wielu z tych zastosowań, dlatego chcielibyśmy nawiązać współpracę i dowiedzieć się, jak możemy razem działać.

  • Znasz już ogólne korzyści.

  • Możesz się zastanawiać, jaki jest rzeczywisty zasięg.

  • Jak wygląda wydajność na urządzeniu?

  • Jak szybki jest ten model?

  • albo jakość samego modelu, prawda?

  • Omówimy je po kolei.

  • Jeśli chodzi o zasięg, jesteśmy już dostępni na 175 milionach urządzeń z AICore.

  • Spodziewamy się, że w ciągu najbliższego roku ta liczba znacznie wzrośnie i obejmie setki milionów innych urządzeń.

  • Jest to możliwe m.in. dzięki temu, że stworzyliśmy bardzo prostą uprząż.

  • Wystarczy więc zintegrować interfejs API ML Kit.

  • AICore to bardzo złożone rozwiązanie, które zarządza sygnałami bezpieczeństwa wejściowymi i wyjściowymi, pobieraniem modelu wraz z wagami oraz konfiguracją w środowisku wykonawczym.

  • Naszym celem jest, aby ML Kit był jak najłatwiejszy w użyciu, dzięki czemu Ty możesz skupić się na zapewnieniu użytkownikom jak najlepszych wrażeń, a my zajmiemy się resztą.

Optymalizacja wydajności i testów porównawczych Gemini Nano

  • David Chou

    Z przyjemnością informujemy też, że znacznie zmniejszyliśmy ilość pamięci RAM wymaganej do uruchomienia Gemini Nano, który jest głównym modelem obsługującym wiele z tych interfejsów API generatywnej AI.

  • Dzięki temu udało nam się zmniejszyć wykorzystanie pamięci RAM z 3,6 GB do 1,1 GB.

  • Aby to umożliwić, wprowadziliśmy 4 główne ulepszenia.

  • Pierwsza to użycie kwantyzacji 2-bitowej zamiast 4-bitowej.

  • Połowa bitów oznacza więc połowę bajtów, które trzeba przesłać przez model.

  • Drugi sposób to użycie współdzielonej pamięci podręcznej KV.

  • Znacznie zmniejszyliśmy ten rozmiar, umożliwiając pierwszym 20 warstwom modelu współdzielenie tych danych w pakiecie o rozmiarze 35 MB. Oznacza to, że nadal możesz korzystać z rozmiaru kontekstu wynoszącego 16 tys. tokenów w ramach tego bardzo małego pakietu.

  • Trzeci to włączenie leniwych koderów obrazu i dźwięku.

  • W przypadku wielu zastosowań związanych z tekstem nie trzeba ich wczytywać.

  • Dzięki temu zaoszczędzisz kolejne pół gigabajta pamięci RAM.

  • Aby wydłużyć czas pracy na baterii, włączyliśmy też skalowanie zegara NPU.

  • Oznacza to, że NPU może działać tak szybko, jak to konieczne, aby oszczędzać energię, a jednocześnie zapewniać wysoką jakość w przypadkach użycia.

  • Wprowadziliśmy też kilka innych ulepszeń, które przyspieszają działanie.

  • Pierwsza z nich to włączenie buforowania prefiksów.

  • W tym przykładzie widać, że prompt systemowy jest używany wielokrotnie, prawda?

  • Przetwarzanie tych danych jest bardzo kosztowne.

  • Ale dzięki buforowaniu prefiksów nie musimy wielokrotnie przetwarzać tego promptu systemowego, prawda?

  • Możesz zapisać to w pamięci podręcznej prefiksów, co oznacza, że w tym przykładzie oszczędzasz 60% tokenów, które musisz przekazać, co oznacza również 60% przyspieszenie na dowolnym urządzeniu.

  • Drugie ulepszenie, które wprowadziliśmy, dotyczy dekodowania spekulatywnego.

  • W tym przykładzie widać, że Gemini Nano generuje 1 token na przebieg.

  • Dzieje się tak, ponieważ LLM są autoregresywne, co oznacza, że muszą stale analizować wcześniej wygenerowane tokeny, aby określić kolejne.

  • Rozwiązaliśmy ten problem, tworząc model generujący, który generuje wiele tokenów znacznie szybciej.

  • Ten model jest znacznie mniejszy niż Gemini Nano, więc może wygenerować wiele tokenów naraz i przekazać je do Gemini, aby sprawdzić, które z nich faktycznie pasują do przewidywań Gemini Nano.

  • W tym przykładzie widać, że w pierwszym przebiegu generuje 6 tokenów, w następnym 5, a w trzecim kończy działanie.

  • Zauważyliśmy, że w przypadku wielu wewnętrznych zastosowań, które analizujemy, prowadzi to do średnio 2-krotnego przyspieszenia.

  • W zależności od sprzętu możesz zauważyć szybszą poprawę.

  • OK.

  • Dlatego, wracając do szybkości, można zauważyć tak znaczną różnicę między urządzeniem a chmurą, nawet w sieci 5G.

  • Samo przesłanie tych danych do chmury zajmie około sekundy w przypadku 1000–1300 tokenów wejściowych. Chociaż wstępne wypełnienie i dekodowanie w Cloud Journey może być szybsze, cały proces jest znacznie szybszy, gdy jest realizowany na urządzeniu.

  • Jeśli chodzi o jakość, z przyjemnością informujemy, że ten model 2B działa lepiej niż zeszłoroczne modele 27B.

  • Chociaż nie jest on tak wydajny jak te modele graniczne, które mamy dzisiaj, pokazujemy, że w przypadku modeli AI działających na urządzeniu nastąpił znaczny postęp.

  • Jak widać, w przypadku MMLU-Pro, który jest standardowym testem porównawczym wiedzy, model 2B jest prawie tak dobry jak model z 27 miliardami parametrów z zeszłego roku.

  • Jeśli chodzi o testy porównawcze dotyczące nauk ścisłych na poziomie akademickim, rozumowania i korzystania z narzędzi agentowych, ten model 2B jest lepszy, mimo że jest ponad 10 razy mniejszy od zeszłorocznego modelu.

  • Spodziewamy się, że te trendy będą się utrzymywać.

  • Nadal ściśle współpracujemy z zespołem GDM, aby ulepszać model i zapewniać Ci jak największe korzyści.

Omówienie nowych funkcji interfejsu Gemini API

  • David Chou

    OK.

  • Przedstawiłem wiele korzyści i nowych ulepszeń, więc może zastanawiasz się, co właściwie można zrobić z tymi modelami.

  • Wprowadzamy też wiele ulepszeń w interfejsie API.

  • Dzięki instrukcjom systemowym możesz ustawić personę.

  • Dzięki funkcji wprowadzania wielu obrazów możesz przesłać wiele obrazów naraz.

  • Wcześniej w tym roku udostępniliśmy też tryb myślenia, dzięki któremu możesz poświęcić więcej czasu na rozwiązywanie problemów i wywoływanie narzędzi.

  • W przypadku ustrukturyzowanych danych wyjściowych umożliwiliśmy dodawanie adnotacji do klasy Kotlin i uzyskiwanie obiektu zgodnego z określonym schematem.

  • Oprócz tego staramy się też pytać deweloperów o nowe ulepszenia, które chcieliby wprowadzić.

  • 91% deweloperów, z którymi rozmawialiśmy, stwierdziło, że uruchomiłoby środowisko, gdyby jedna z tych funkcji była dostępna.

  • W tym tygodniu wprowadziliśmy te 4 funkcje: tłumaczenie mowy, wektory dystrybucyjne, zamianę tekstu na mowę i wywoływanie funkcji.

  • Dzięki tłumaczeniu mowy możesz transkrybować dźwięk w ponad 20 językach.

  • Dzięki wektorom dystrybucyjnym możesz wyszukiwać treści w aplikacji według znaczenia semantycznego, a nie tylko za pomocą słów kluczowych.

  • Dzięki zamianie tekstu na mowę możesz teraz używać w swoich projektach głosów przypominających ludzkie, więc nie musisz już korzystać z głosów robotycznych.

  • I wreszcie, dzięki wywoływaniu funkcji, możesz pozwolić aplikacji na podejmowanie działań w imieniu użytkowników.

  • Współpracujemy już z kilkoma deweloperami, aby zobaczyć, jak mogą oni tworzyć agentów w aplikacjach, i mamy nadzieję, że uda nam się wprowadzić tę funkcję w przyszłym roku.

Przewodnik po kompleksowym wykrywaniu oszustw

  • David Chou

    Biorąc pod uwagę wszystkie te możliwości, przeanalizujmy kompleksowy przykład, w którym wykorzystujemy je wszystkie.

  • Wyobraźmy sobie, że amerykański fotograf podróżuje po Japonii.

  • Jak widać na zdjęciu, wyraźnie brakuje im aparatów i obiektywów.

  • Zamawia więc nowy obiektyw i wysyła go do hotelu.

  • Jest jednak pewien haczyk: w ciągu dnia otrzymuje wiadomość głosową z informacją, że musi zapłacić 6800 jenów opłaty celnej, aby otrzymać paczkę do końca dnia.

  • Otrzymali też SMS-a ze zdjęciem zatrzymanej przesyłki.

  • W tym przypadku użytkownik chce mieć ochronę przed wykrywaniem oszustw, prawda?

  • Chce się dowiedzieć, jak może rozpoznać, czy wiadomości głosowe są prawdziwe czy fałszywe.

  • Dlatego chcemy, aby wszystkie informacje były prywatne, ugruntowane i wyjaśnialne.

  • Dlatego upewnimy się, że będą mogli zrozumieć tekst w języku japońskim, a także korzystać z dowolnych funkcji językowych, które są dostępne, jeśli mają potrzeby związane z ułatwieniami dostępu.

  • Będą więc 4 kroki, prawda?

  • Pierwszym krokiem jest przetworzenie wiadomości głosowej na tekst, czyli jej przetłumaczenie.

  • W tym celu użyjemy interfejsu Speech Translation API.

  • Następnie będą mogli zadać pytanie, jeśli będą chcieli, więc użyjemy interfejsu Speech Recognition API.

  • Następnie chcemy zdefiniować funkcje, które mogą pobierać informacje niezbędne do tego, aby użytkownik otrzymał to, czego potrzebuje.

  • Użyjemy więc Prompt API i wywoływania funkcji.

  • Teraz będzie to działać w tle, bez żadnych widocznych dla użytkownika elementów.

  • Po trzecie, dowiemy się, jak przechowywać i pobierać te informacje za pomocą interfejsu API wektorów dystrybucyjnych i AppSearch.

  • Będzie też działać w tle.

  • Na koniec wydamy werdykt na podstawie zebranych informacji, korzystając z interfejsu Prompt API, uporządkowanych danych wyjściowych i syntezy mowy.

  • OK.

  • Zaczynajmy.

  • Będziemy używać tłumaczenia mowy i rozpoznawania mowy.

  • Pierwszym krokiem jest więc przekształcenie wiadomości głosowej i tekstu w przydatne informacje, prawda?

  • Użytkownik otrzymał wiadomość: „Musisz zapłacić 6800 jenów.

  • Cła mogły nie zostać opłacone z góry, więc musisz je zapłacić”.

  • Dlatego chcą mieć możliwość tłumaczenia.

  • Chcemy zwrócić uwagę na to, że zarówno tłumaczenie, jak i pytanie użytkownika o informacje trafiają do Gemini Nano 4 przy użyciu tego samego kodera dźwięku, ponieważ został on wytrenowany na ponad 100 językach.

  • Możesz uzyskać to tłumaczenie, a także automatyczne rozpoznawanie mowy w przypadku tekstu pytania użytkownika.

  • W przypadku tłumaczenia wystarczy skonfigurować język źródłowy i docelowy.

    val translator = SpeechTranslation.getClient(
        SpeechTranslatorOptions.Builder(
            Locale.JAPANESE, Locale.US
        ).build()
    )
  • W tym przypadku użyjemy języka japońskiego i amerykańskiej odmiany języka angielskiego.

  • Skonfigurujemy przepływ Kotlin, który przekazuje bajty bezpośrednio do translatora mowy.

    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
        }
    }
  • Następnie możemy zebrać odpowiedź, używając przetłumaczonego tekstu.

  • OK.

  • Świetnie.

  • Teraz rozumiemy, co właściwie jest w tej wiadomości, prawda?

  • Jest tam paczka z obiektywem aparatu.

  • Została zatrzymana w urzędzie celnym.

  • Musi zapłacić 6800 jenów.

  • Teraz użytkownik chce zadać pytanie na ten temat.

  • OK.

  • Czy to zatrzymanie przez urząd celny jest prawdziwe?

  • W tym przypadku użyjemy interfejsu ASR API.

    val recognizer = SpeechRecognition.getClient(
        speechRecognizerOptions {
            locale = Locale.US
            preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
        }
    )
  • Nadal ustawiamy amerykański angielski jako język.

  • Następnie możemy ustawić tryb zaawansowany, który wykorzystuje koder dźwięku.

  • Następnie możemy przekazać te informacje, zebrać ostateczną odpowiedź i przekształcić ją w informacje, które przekażemy do 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)
        }
    }
  • OK.

  • Zakończyliśmy pierwszą część procesu przyjmowania zgłoszenia.

  • Jak widać, nie wymagało to wielu wierszy kodu.

Triaging inputs using Prompt API and function calling

  • David Chou

    Druga część to selekcja, czyli zastanowienie się, jak wykorzystać Prompt API i wywoływanie funkcji, aby dowiedzieć się, jakich informacji potrzebujemy.

  • OK.

  • Aby utworzyć architekturę, najpierw skonfigurujemy instrukcje systemowe.

  • Następnie wiadomość głosowa, SMS i zdjęcie, a także pytanie użytkownika, zostaną przekazane jako dane wejściowe do Gemini Nano.

  • Następnie zdefiniujemy funkcje, które będą pobierać potrzebne nam informacje.

    retrieveOrderAndDeliveryPhotos(
      searchQuery = "camera lens customs",
      claimedFeeUsd = 45,
      comparePackagePhotos = true
    )
  • Na początek skonfigurujemy instrukcje systemowe.

    // 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()
    )
  • Tutaj widzimy bardzo prosty przykład tego, jak wyglądałoby skonfigurowanie audytora bezpieczeństwa. Chodzi o to, że będziemy sprawdzać opakowania i etykiety, przyglądać się opłatom niestandardowym, a także oznaczać wszelkie niezbędne żądania płatności.

  • Może się okazać, że optymalizacja promptów będzie bardzo pomocna w uzyskaniu znacznie lepszej jakości.

  • Gdy mamy już instrukcje systemowe, możemy wziąć całą treść z poprzedniego kroku, czyli tłumaczenie wiadomości głosowej i SMS-a, a także pytanie użytkownika, i przekazać to wszystko do pola TextPart wraz z instrukcjami systemowymi, aby uzyskać te informacje.

    // 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 */ }
  • Następnie chcemy zdefiniować funkcję, której możemy użyć.

  • W tym konkretnym przypadku z przyjemnością informujemy, że wprowadziliśmy integrację z funkcją aplikacji.

  • Jeśli więc ktoś z Was zna to narzędzie, prawdopodobnie wie już, jak to działa.

  • W tym miejscu konfigurujemy punkt wejścia AppFunctionService, a następnie definiujemy funkcję aplikacji.

    // Registered in AndroidManifest.xml
    @AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
    abstract class ScamInvestigationTools : AppFunctionService() {
    
    }
  • Jak widać, jest to dokładnie to samo, co w przypadku konfigurowania funkcji aplikacji dla agenta systemowego.

  • W parametrach searchQuery ustawiasz opłatę za roszczenie, a także to, czy mamy porównywać zdjęcia paczek. To tylko niektóre przykładowe parametry, prawda?

    // 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) }
    }
  • Możesz zdefiniować inne.

  • Na koniec mamy funkcję executeVaultSearch, którą zdefiniujemy później.

  • Warto zwrócić uwagę na to, że dostępny jest tryb myślenia.

    val vaultTools = AppFunctionTools.getClient().getAppFunctions()
    val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
    
        enableThinking = true
        setTools(vaultTools)
    
    }
    val call = generativeModel.generateContent(turn1Request)
        .candidates.first().functionCalls.first()
  • Zdecydowanie zalecam korzystanie z niego podczas wywoływania narzędzi.

  • Powodem jest to, że przeprowadziliśmy wiele testów, które wykazały, że dokładność wywoływania narzędzi w Gemini Nano 4 jest o 13 punktów wyższa (na 100).

  • W niektórych przypadkach jest on nawet znacznie wyższy, co może przynieść znacznie lepsze wyniki.

  • Jeśli chcesz dowiedzieć się więcej o wywoływaniu funkcji, weź udział w sesji, którą poprowadzą Jolanda i Victoria.

  • Dotyczy to funkcji agentowych w aplikacji.

  • Myślę, że Victoria może siedzieć gdzieś w tłumie, więc możecie się z nią przywitać.

Pobieranie danych zamówienia za pomocą wektorów dystrybucyjnych i AppSearch

  • David Chou

    OK.

  • Ukończyliśmy już 2 pierwsze kroki.

  • Porozmawiajmy o tym, jak działa pobieranie.

  • Użyjemy więc wektorów dystrybucyjnych i AppSearch.

  • Może więc warto cofnąć się o kilka kroków na ścieżce użytkownika do momentu, w którym zamówił soczewki. Chcielibyśmy zindeksować te informacje, aby były dostępne zarówno dla użytkownika, jak i dewelopera.

  • W tym miejscu znajdziesz informacje o numerze zamówienia.

  • Ma też numer śledzenia.

  • Zawiera on informacje o tym, gdzie został wysłany.

  • Jak widać, jest tu już informacja, że cła zostały opłacone z góry.

  • Wygenerujemy więc wektor dystrybucyjny multimodalny, aby zindeksować te informacje wraz z innymi informacjami, które mogą być dla nas ważne.

  • Wygląda to tak.

    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
  • Otrzymujemy e-maila z zamówieniem.

  • To bardzo proste – przekazujemy tylko 1 obraz i 1 tekst, ale możesz mieć więcej treści, które chcesz tu zindeksować.

  • Następnie wygenerujemy wektor dystrybucyjny.

  • Po wykonaniu tej czynności użyjemy AppSearch, aby utworzyć dokument, a następnie ustawimy właściwości, które naszym zdaniem są niezbędne do wygenerowania tego konkretnego dokumentu.

    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()
  • Warto zauważyć, że chcemy używać wektorów dystrybucyjnych multimodalnych, ponieważ może nie być żadnych wspólnych słów, ale ponieważ obrazy i tekst dotyczą podobnych treści, pomaga nam to uzyskać podobieństwo semantyczne.

  • Możemy więc użyć podobieństwa cosinusowego, aby znaleźć elementy, które nie mają żadnych wspólnych słów.

  • To jest funkcja executeVaultSearch, o której mówiliśmy wcześniej.

    suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
        val qVec = embedModel.generateEmbedding(
            EmbeddingRequest(
                content = Content(listOf(TextPart(searchQuery)))
            )
        ).embedding.vector
    
    }
  • To jest wywoływane przez zdefiniowaną przez nas funkcję aplikacji.

  • W tym przykładzie przekazujemy zapytanie wygenerowane przez Gemini Nano.

  • Następnie użyjemy strategii rankingu, czyli podobieństwa cosinusowego.

    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 pomaga nam uzyskać te informacje.

  • Na koniec określimy próg 0,68, aby określić minimalne podobieństwo cosinusowe, które pozwoli nam pobrać informacje.

    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() }
    }
  • Możesz więc poeksperymentować z tymi ustawieniami, aby sprawdzić, co najlepiej sprawdza się w Twoim przypadku.

  • Warto też wspomnieć o bezpieczeństwie tego modelu. Gemini Nano nie uruchamia żadnego kodu.

  • Gemini Nano zadaje pytanie, ale to aplikacja definiuje kod, uruchamia go, pobiera informacje i określa, co przekazać Gemini Nano.

  • Dzięki temu masz większą kontrolę nad tym, co chcesz udostępnić Gemini Nano, a także możesz zapewnić użytkownikom większą prywatność i bezpieczeństwo.

Porównywanie szczegółów zamówienia i definiowanie ocen audytu

  • David Chou

    Teraz przejdziemy do ostatniego kroku.

  • Mamy już wszystkie potrzebne informacje.

  • Użyjemy interfejsu Prompt API, ustrukturyzowanych danych wyjściowych i syntezy mowy, aby przeprowadzić porównania, a następnie przekazać te informacje użytkownikowi w zrozumiały sposób.

  • Zasadniczo chcemy wiedzieć, czy ta przesyłka jest rzeczywiście taka sama jak informacje od użytkownika, które są prawdziwym zamówieniem.

  • Aby to zrobić, wygenerujemy wektor dystrybucyjny na podstawie informacji z poczty głosowej i tekstu. Przekształcimy je w taki sam wektor dystrybucyjny jak wcześniej.

    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)
  • Następnie użyjemy matchScore, aby za pomocą podobieństwa cosinusowego porównać dopasowanie pobrane z magazynu z informacjami od rozmówcy.

  • Jak widać, nawet jeśli obrazy nie pasują, tekst jest bardzo pomocny.

  • W takich przypadkach multimodalne wektory dystrybucyjne są bardzo przydatne.

  • Możesz wprowadzić różne informacje, a następnie uzyskać wspólne znaczenie semantyczne, prawda?

  • Dzięki temu wektoryzacja multimodalna umożliwia mapowanie wszystkich tych danych na tę samą przestrzeń wektorową.

  • Ponownie użyjemy trybu myślącego, aby ulepszyć nasze rozumowanie.

    val forensicRequest = generateContentRequest(
        securitySystemInstruction,
        content {
            image(callerParcelPhotoBmp)
            image(orderEmailLensBmp)
            text(forensicAuditPrompt)
        }
    ) {
    
        // Enable Gemini Nano 4 reasoning before it answers
        enableThinking = true
    
    }
  • Następnie zdefiniujemy klasę wyjściową.

    @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,
    )
  • Wszystko, co tu robimy, to skonfigurowanie VisualContradiction i kilku różnych typów wyliczeniowych dla różnych rodzajów możliwych sprzeczności, prawda?

  • Może to być DUTY_FRAUD, PACKAGING_MISMATCH lub STATUS_CONFLICT.

  • Następnie dodamy wyjaśnienie.

  • Następnie wszystkie te informacje zostaną przekazane do werdyktu audytu.

    @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,
    )
  • Jak widać, jest to bardzo proste.

  • Chodzi nam tylko o to, czy to oszustwo, czy nie.

  • Możemy mieć fraudRiskScore, w którym można ustawić wartość minimalną i maksymalną od 0 do 100.

  • Możesz nawet poinformować użytkownika, ile pieniędzy udało mu się zaoszczędzić, jeśli chcesz to śledzić.

  • Możesz też wymienić wszystkie zdefiniowane przez nas sprzeczności wizualne.

  • Warto też zauważyć, że adnotacja @Guide jest opcjonalna.

  • Nie musisz go ustawiać i podawać takiego opisu.

  • Jest to przydatne, ponieważ nie zawsze można wyjaśnić, co się dzieje, na podstawie samej definicji zmiennej.

  • Dzięki temu opisowi Gemini Nano może określić, jakie konkretne funkcje wywołać i jak je skonfigurować, aby wprowadzały odpowiednie informacje we właściwych polach.

  • Po skonfigurowaniu jest to bardzo proste.

  • Uruchamiasz tu tylko 3 wiersze kodu.

    val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
    val response = generativeModel.generateContent(typedRequest)
    val verdict: PhishingAuditVerdict? = response.candidates.first().response
  • generateTypedContentRequest, generujesz te treści, a następnie możesz pobrać odpowiedź.

  • A tutaj jest przykład, jak to może wyglądać.

    {
      "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."
    }
  • Potwierdzamy, że to oszustwo, i uzyskujemy bardzo wysoki wynik oceny ryzyka oszustwa. To wyjaśnia sprzeczności, które są widoczne w pobranych danych w porównaniu z informacjami o rozmówcy.

  • Udało nam się też zaoszczędzić dla tego użytkownika określoną kwotę, więc możemy to śledzić.

  • Tak.

  • To podsumowuje wszystkie informacje, których potrzebujemy.

  • Ostatnim krokiem jest pokazanie tego użytkownikowi w przystępny sposób.

  • Jeśli na przykład użytkownik jest w ruchu lub ma wadę wzroku, możesz odczytać te informacje na głos, zamiast zmuszać go do czytania tekstu.

  • Zaimplementujemy funkcję zamiany tekstu na mowę, aby pokazać, jak to będzie wyglądać.

    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
    }
  • To bardzo proste. Określamy region i język, a następnie przekazujemy werdykt, który właśnie wygenerowaliśmy za pomocą @Generable i @Guide.

  • Gdy już to mamy, wszystko dzieje się niemal natychmiast.

    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
    }
  • Na większości urządzeń wygenerowanie odpowiedzi zajmie mniej niż 120 ms.

  • Wyczyścimy pamięć, jeśli będzie to konieczne.

  • W razie potrzeby możemy też ustawić kreację zastępczą, korzystając z interfejsów API platformy.

    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 ...
  • Nadal będzie to głos robota, ale przynajmniej da użytkownikowi możliwość interakcji, jeśli nie potrafi czytać.

Podsumowanie i uwagi końcowe

  • David Chou

    OK.

  • To było sporo informacji, ale podsumowując, wykorzystaliśmy wszystkie te różne interfejsy API w procesach przyjmowania, klasyfikacji, pobierania i kontroli.

  • Wiesz, korzystając z interfejsów API do rozpoznawania mowy i tłumaczenia, interfejsu Prompt API i wywoływania funkcji, wektorów dystrybucyjnych i AppSearch, a także ustrukturyzowanych danych wyjściowych i syntezy mowy.

  • To tylko przykład, jak można wykorzystać wszystkie te możliwości w spójny sposób i uruchomić je całkowicie na urządzeniu.

  • Wracając do tego, o czym rozmawialiśmy na początku, korzystanie z tego rozwiązania przynosi znaczne korzyści, prawda?

  • Jeśli uruchomisz ten model na swoim urządzeniu, zobaczysz, że na większości urządzeń działa on znacznie szybciej.

  • To nic nie kosztuje, a możesz potencjalnie zaoszczędzić setki tysięcy dolarów, jeśli będziesz z tego korzystać przez cały czas.

  • Te informacje są dostępne niezależnie od tego, czy użytkownik jest w górach, czy w piwnicy i nie ma dostępu do internetu.

  • Jest też w pełni prywatny, ponieważ nie musisz wysyłać żadnych informacji do chmury.

  • Biorąc pod uwagę wszystkie te możliwości, zapoznaj się z dokumentacją, zeskanuj kod QR lub kliknij link.

  • To są wszystkie funkcje, które wprowadziliśmy w ciągu ostatnich 6 miesięcy.

  • Dziękuję za udział w tej sesji.

  • Dziękujemy za poświęcony czas.

  • Bardzo dziękuję.

Kod

Fragmenty kodu

Fragment kodu w języku Kotlin w filmie „Kompletna instrukcja wykrywania oszustw” (12:38)
val translator = SpeechTranslation.getClient(
    SpeechTranslatorOptions.Builder(
        Locale.JAPANESE, Locale.US
    ).build()
)
Fragment kodu w Kotlinie w 12:47 w filmie „End-to-end scam detection walkthrough”
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
    }
}
Fragment kodu w Kotlinie w 13:13 w filmie „Kompleksowe omówienie wykrywania oszustw”
val recognizer = SpeechRecognition.getClient(
    speechRecognizerOptions {
        locale = Locale.US
        preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
    }
)
Fragment kodu w Kotlinie w 13:24 w filmie „Kompleksowe omówienie wykrywania oszustw”
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)
    }
}
Fragment kodu w języku Kotlin w 14:03 w filmie „Triaging inputs using Prompt API and wywoływanie funkcji”
retrieveOrderAndDeliveryPhotos(
  searchQuery = "camera lens customs",
  claimedFeeUsd = 45,
  comparePackagePhotos = true
)
Fragment kodu w języku Kotlin w 14:07 w filmie „Triaging inputs using Prompt API and wywoływanie funkcji”
// 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()
)
Fragment kodu w języku Kotlin w 14:33 w filmie „Triaging inputs using Prompt API and wywoływanie funkcji”
// 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 */ }
Fragment kodu w języku Kotlin w 15:04 w filmie „Triaging inputs using Prompt API and wywoływanie funkcji”
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {

}
Fragment kodu w języku Kotlin w 15:15 w filmie „Triaging inputs using Prompt API and wywoływanie funkcji”
// 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) }
}
Fragment kodu w języku Kotlin w 15:35 w filmie „Triaging inputs using Prompt API and wywoływanie funkcji”
val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {

    enableThinking = true
    setTools(vaultTools)

}
val call = generativeModel.generateContent(turn1Request)
    .candidates.first().functionCalls.first()
Fragment kodu w języku Kotlin w 17:03 w filmie „Pobieranie danych zamówienia za pomocą wektorów dystrybucyjnych i 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
Fragment kodu w języku Kotlin w 17:16 w filmie „Pobieranie danych zamówienia za pomocą wektorów dystrybucyjnych i 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()
Fragment kodu w Kotlinie w 17:48 w filmie „Pobieranie danych zamówienia za pomocą wektorów dystrybucyjnych i AppSearch”
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
    val qVec = embedModel.generateEmbedding(
        EmbeddingRequest(
            content = Content(listOf(TextPart(searchQuery)))
        )
    ).embedding.vector

}
Fragment kodu w języku Kotlin w 18:03 w filmie „Pobieranie danych zamówienia za pomocą wektorów dystrybucyjnych i 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()

}
Fragment kodu w języku Kotlin w 18:12 w filmie „Retrieving order data with embeddings and 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() }
}
Fragment kodu w języku Kotlin w 19:24 w filmie „Porównywanie szczegółów zamówienia i definiowanie wyników kontroli”
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)
Fragment kodu w języku Kotlin w 20:10 w filmie „Porównywanie szczegółów zamówienia i definiowanie werdyktów kontroli”
val forensicRequest = generateContentRequest(
    securitySystemInstruction,
    content {
        image(callerParcelPhotoBmp)
        image(orderEmailLensBmp)
        text(forensicAuditPrompt)
    }
) {

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

}
Fragment kodu w języku Kotlin w 20:15 w filmie „Porównywanie szczegółów zamówienia i definiowanie werdyktów kontroli”
@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,
)
Fragment kodu w języku Kotlin w 20:35 w filmie „Porównywanie szczegółów zamówienia i definiowanie werdyktów kontroli”
@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,
)
Fragment kodu w języku Kotlin w 21:35 w filmie „Porównywanie szczegółów zamówienia i definiowanie werdyktów kontroli”
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().response
Fragment kodu JSON w 21:44 w filmie „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."
}
Fragment kodu w Kotlinie w 22:22 w filmie „Porównywanie szczegółów zamówienia i definiowanie werdyktów kontroli”
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
}
Fragment kodu w języku Kotlin w 22:38 w filmie „Porównywanie szczegółów zamówienia i definiowanie werdyktów kontroli”
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
}
Fragment kodu w języku Kotlin w 22:49 w sekcji „Porównywanie szczegółów zamówienia i definiowanie werdyktów kontroli”
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 ...