Умные приложения

Искусственный интеллект на устройстве. Оптимизированная производительность и качество благодаря API Android ML Kit.

Колонки

О приложении

О выступлении

На этом занятии мы рассмотрим разные варианты использования генеративного ИИ с помощью API ML Kit, чтобы понять, когда генеративный ИИ на устройстве идеально подходит для приложения, а также как оценить существующие варианты использования генеративного ИИ, чтобы понять, могут ли модели на устройстве соответствовать их планке качества.

Сводка

  • ИИ на устройстве имеет значительные преимущества в скорости и конфиденциальности по сравнению с облачными вычислениями. Внутреннее тестирование показало, что скорость работы моделей увеличивается в два раза по сравнению с облаком.
  • Использование памяти Gemini Nano снизилось с 3,6 до 1,1 ГБ. Основные оптимизации включают 2-битное квантование и общий кеш KV.
  • Кеширование префиксов и спекулятивное декодирование значительно ускоряют инференс. Повторное использование системных подсказок сокращает количество обрабатываемых токенов на 60 %, а меньшая модель-черновик обеспечивает в среднем двукратное ускорение.
  • Новые возможности на устройстве расширяют взаимодействие с приложением за счет перевода речи и вызова функций. Режим размышления повышает точность вызова инструментов в Gemini Nano 4 на 13 баллов из 100.
  • Мультимодальные эмбеддинги в сочетании с AppSearch позволяют хранить и извлекать данные с помощью косинусного сходства. Логика поиска выполняется локально, что обеспечивает конфиденциальность пользователей и защиту данных.

Ресурсы

Gemini Notebook

Темы

  • on-device-ai
  • ml-kit
  • gemini-nano
  • aicore
  • вызов функций
  • встраивания
  • перевод речи

Расшифровка

Введение в ИИ на устройстве

  • Дэвид Чоу (David Chou)

    хотите

  • Добрый день!

  • Меня зовут Дэвид Чоу.

  • Я руководитель по продукту Android AICore.

  • Я очень рад рассказать вам о возможностях ИИ на устройстве.

  • Сегодня мы обсудим несколько основных тем.

  • Во-первых, подумайте, почему вам нужен локальный ИИ, особенно учитывая ускорение прогресса ИИ в облаке.

  • Во-вторых, мы думаем о том, как улучшить ИИ-модели на устройстве, чтобы они стали ещё мощнее.

  • И наконец, мы рассмотрим пример, который показывает, как можно использовать все эти преимущества с помощью ИИ на устройстве.

  • Итак, для чего нужен локальный ИИ?

  • На это есть четыре основные причины.

  • Первый – скорость, второй – стоимость, третий – доступность, а четвертый – конфиденциальность.

  • Что касается скорости, то сейчас инференс ИИ на устройстве работает быстрее, чем в облаке, потому что ему не нужно передавать информацию туда и обратно между устройством пользователя и облаком.

  • Во многих случаях, которые мы тестировали, ИИ на устройстве работал в два раза быстрее, чем в облаке.

  • Второе – это стоимость.

  • Представьте, что вы выполняете миллион выводов в день.

  • Если вы будете запускать ИИ на устройстве пользователя, а не в облаке, то сможете сэкономить сотни тысяч долларов.

  • Третье – доступность в офлайн-режиме.

  • Ранее в этом году мы провели опрос, и 62 % разработчиков, с которыми мы общались, сказали, что доступность в офлайн-режиме – одна из трех основных причин использовать ИИ на устройстве.

  • Поэтому,даже если пользователь находится на высоте 10 000 метров или на подземном этаже станции метро в Токио и у него нет доступа к интернету, он все равно может использовать все эти возможности.

  • И наконец, конфиденциальность была одной из главных причин, которые мы увидели, когда опрашивали разработчиков.

  • Итак, 72% оценили его как одну из трех основных причин.

  • 40 % поставили его на первое место.

  • Поэтому, когда речь идет о сообщениях, голосовых заметках, фотографиях или даже финансовой или медицинской информации, пользователи хотят сохранить конфиденциальность, и поэтому запуск инференса на устройстве будет для них действительно ценным.

  • Учитывая все эти преимущества, мы создали ML Kit, чтобы вы могли использовать все эти возможности.

  • Мы стараемся сделать это максимально простым.

  • Если у вас есть жалобы, свяжитесь со мной.

  • В конце выступления я оставлю свою контактную информацию.

  • Мы постараемся найти решение.

  • Для начала мы добавили ряд возможностей, например для речи и творчества.

  • Например, вычитка, переписывание и гибкие мультимодальные запросы.

  • Поэтому наша цель – сделать его максимально удобным.

  • Именно поэтому многие наши пользователи и разработчики уже начали внедрять эту технологию.

  • Уже сейчас у нас есть девять сторонних приложений с 10 миллионами скачиваний, в которых используются эти функции генеративного ИИ.

  • Мы наблюдаем значительный рост как в наших собственных, так и в сторонних приложениях, а также увеличение числа запусков приложений с функциями ИИ на 110 % по сравнению с предыдущим кварталом.

  • Поэтому мы рекомендуем вам попробовать эти API для обмена сообщениями, креативов и финансов.

  • Мы считаем, что возможности безграничны, и у вас, вероятно, лучшее воображение, чем у нас, во многих из этих вариантов использования, и поэтому мы хотели бы сотрудничать вместе и выяснить, как мы можем работать вместе.

  • Теперь вы понимаете, в чем основные преимущества.

  • Возможно, у вас возникнут вопросы о том, каков на самом деле охват.

  • Как работает приложение на устройстве?

  • Когда мы говорим о скорости, насколько она хороша на самом деле?

  • Или, например, качество самой модели, верно?

  • Давайте рассмотрим их по очереди.

  • Что касается охвата, то сегодня мы уже доступны на 175 миллионах устройств AICore.

  • Мы ожидаем, что в следующем году к ней присоединятся сотни миллионов других устройств.

  • Частично это возможно благодаря тому, что мы создали очень простую упряжь.

  • Таким образом, вам нужно интегрировать только ML Kit API.

  • AICore – это сложная система, которая управляет сигналами безопасности на входе и выходе, скачиванием модели и ее весов, а также настройкой среды выполнения.

  • Поэтому мы стремимся сделать ML Kit максимально простым в использовании, чтобы вы могли сосредоточиться на создании отличного продукта, а мы позаботимся обо всем остальном.

Оптимизация производительности и контрольные показатели Gemini Nano

  • Дэвид Чоу (David Chou)

    Мы также рады сообщить, что нам удалось значительно снизить объем ОЗУ, необходимый для работы Gemini Nano – основной модели, на которой работают многие из этих API генеративного ИИ.

  • В результате мы снизили объем используемой ОЗУ с 3,6 ГБ до 1,1 ГБ.

  • Чтобы это стало возможным, мы внесли четыре основных улучшения.

  • Первый – использование 2-битного квантования вместо 4-битного.

  • Таким образом, половина битов означает половину байтов, которые вам нужно передать через модель.

  • Второй – с помощью общего кеша KV.

  • Поэтому мы значительно уменьшили его, позволив первым 20 слоям модели использовать его совместно в пакете размером 35 МБ. Это означает, что вы по-прежнему можете получить доступ к размеру контекста токена 16K в этом очень маленьком пакете.

  • Третий – включение ленивых кодировщиков видео и аудио.

  • Поэтому для многих текстовых сценариев использования, которые мы видим, вам на самом деле не нужно загружать их для начала.

  • Это сэкономит ещё полгигабайта ОЗУ.

  • И наконец, чтобы продлить время работы от батареи, мы добавили масштабирование тактовой частоты нейропроцессора.

  • Это означает, что NPU может работать так быстро, как это необходимо для экономии энергии, и при этом обеспечивать высокое качество в ваших сценариях использования.

  • Мы также внесли несколько других улучшений, связанных с ускорением.

  • Первый – включение кеширования префиксов.

  • Как видно из этого примера, системный запрос используется повторно для нескольких ходов, верно?

  • И поэтому обработка этого становится очень дорогой.

  • Но благодаря кешированию префиксов нам не нужно заново обрабатывать системную подсказку, верно?

  • Вы можете сохранить их в кеше префиксов. Это значит, что в данном примере вы сохраняете 60 % токенов, которые нужно передать, а также получаете ускорение на 60 % на любом устройстве.

  • Второе улучшение – спекулятивное декодирование.

  • В этом примере Gemini Nano генерирует один токен за проход.

  • Это связано с тем, что LLM являются авторегрессионными, то есть они должны продолжать смотреть на предыдущие сгенерированные токены, чтобы определить следующие.

  • Поэтому мы создали модель-черновик, которая генерирует множество токенов гораздо быстрее.

  • Эта модель-черновик значительно меньше, чем Gemini Nano, поэтому она может генерировать сразу несколько токенов и передавать их Gemini, чтобы проверить, какие из них действительно совпадают с предположениями Gemini Nano.

  • В этом примере на первом проходе генерируется шесть токенов, на втором – пять, а на третьем генерация завершается.

  • И мы видим, что для многих внутренних сценариев использования, которые мы рассматриваем, это приводит к ускорению в среднем в 2 раза.

  • В зависимости от вашего оборудования улучшения могут быть более быстрыми.

  • Окей.

  • Вот почему, возвращаясь к скорости, вы можете увидеть это резкое ускорение между устройством и облаком даже в сети 5G.

  • Вы видите, что просто загрузка этих данных в облако займет даже одну секунду, верно, для 1000 для 1300 входных токенов, и поэтому, хотя предварительное заполнение и декодирование в облачном путешествии могут быть быстрее, сквозное путешествие все равно значительно быстрее, когда вы работаете на устройстве.

  • Что касается качества, то эта модель с 2 миллиардами параметров работает на уровне моделей с 27 миллиардами параметров, выпущенных в прошлом году.

  • Хотя она не так эффективна, как современные передовые модели, мы видим, что в области ИИ на устройствах достигнут значительный прогресс.

  • Как видите, в тесте MMLU-Pro, который является стандартным тестом на знания, модель 2B почти так же хороша, как модель с 27 миллиардами параметров, выпущенная в прошлом году.

  • А в тестах на научные знания, логику и использование инструментов агентов эта модель 2B даже превосходит прошлогоднюю, хотя она меньше ее более чем в 10 раз.

  • Мы ожидаем, что эти тенденции сохранятся.

  • Мы продолжаем тесно сотрудничать с командой GDM, чтобы улучшать модель и помогать вам получать наилучшие результаты.

Обзор новых возможностей Gemini API

  • Дэвид Чоу (David Chou)

    Окей.

  • Я рассказал о множестве преимуществ и улучшений, и у вас мог возникнуть вопрос: "Что же можно делать с помощью этих моделей?"

  • Мы также улучшаем возможности API.

  • Таким образом, с помощью системных инструкций вы можете задать персону.

  • Вы можете загрузить сразу несколько изображений.

  • Ранее в этом году мы также добавили режим размышления, чтобы вы могли уделять больше времени задачам на логику и вызовам инструментов.

  • И наконец, благодаря структурированному выводу вы можете аннотировать класс Kotlin и получить объект, соответствующий вашей схеме.

  • Кроме того, мы стараемся спрашивать разработчиков о том, какие улучшения они хотели бы внести.

  • 91 % разработчиков, с которыми мы общались, сказали, что запустили бы продукт, если бы была доступна одна из этих возможностей.

  • Ранее на этой неделе мы запустили четыре функции: перевод речи, эмбеддинги, преобразование текста в речь и вызов функций.

  • Вы можете расшифровывать аудио более чем на 20 языках.

  • Эмбеддинги позволяют искать контент в приложении по семантическому значению, а не только по ключевым словам.

  • Благодаря синтезу речи вы можете использовать в своих проектах голоса, похожие на человеческие, а не роботизированные.

  • И наконец, благодаря вызову функций ваше приложение может выполнять действия от имени пользователей.

  • Мы уже сотрудничаем с несколькими разработчиками, чтобы понять, как они могут создавать агентов в своих приложениях, и надеемся запустить эту функцию в следующем году.

Пошаговое руководство по распознаванию мошенничества

  • Дэвид Чоу (David Chou)

    Давайте рассмотрим пример, в котором используются все эти возможности.

  • Представим, что американский фотограф путешествует по Японии.

  • И, как видно на фото, у них явно недостаточно камер и объективов.

  • Поэтому они заказывают новый объектив с доставкой в отель.

  • Но есть одна загвоздка: в течение дня они получают голосовое сообщение о том, что им нужно заплатить 6800 иен за таможенное оформление, чтобы получить посылку до конца дня.

  • Также они получили текстовое сообщение с фотографией посылки.

  • Итак, в этом случае пользователь заинтересован в наличии какой-то защиты от мошенничества, верно?

  • Они хотят понять, как определить, является ли голосовое сообщение законным или мошенническим.

  • Поэтому мы хотим, чтобы вся информация была конфиденциальной, обоснованной и объяснимой.

  • Поэтому мы должны убедиться, что они смогут понять японский текст, а также использовать любые доступные языковые возможности, если у них есть потребности в специальных возможностях.

  • Итак, здесь будет четыре шага, верно?

  • Первый шаг – это просто обработка голосовой почты в тексте, поэтому она будет переводить эту голосовую почту.

  • Для этого мы будем использовать Speech Translation API.

  • Затем они смогут задать вопрос, если захотят, поэтому мы будем использовать Speech Recognition API.

  • Затем мы хотим определить функции, которые могут извлекать информацию, необходимую пользователю для получения того, что ему нужно.

  • Поэтому мы будем использовать Prompt API и вызов функций.

  • Теперь это будет выполняться в фоновом режиме, и пользователь ничего не увидит.

  • В-третьих, мы разберемся, как хранить и извлекать эту информацию с помощью Embedding API и AppSearch.

  • Оно также будет работать в фоновом режиме.

  • И наконец, мы вынесем вердикт на основе собранной информации, используя Prompt API, структурированный вывод и синтез речи.

  • Окей.

  • Давайте начнем.

  • Мы будем использовать перевод речи и распознавание речи.

  • Итак, первый шаг – это просто взять голосовую почту и текст и превратить их в полезную информацию, верно?

  • И вот пользователь получил сообщение: "Эй, тебе нужно заплатить 6800 иен.

  • Возможно, пошлины не были оплачены заранее, и вам нужно их оплатить".

  • И поэтому они хотят иметь возможность получить этот перевод, верно?

  • И поэтому одна из вещей, на которую мы хотели бы обратить внимание, заключается в том, что и перевод, и вопрос пользователя об информации поступают в 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
        }
    }
  • Затем мы можем собрать ответ, используя переведенный текст.

  • Окей.

  • Отлично.

  • Теперь мы понимаем, о чем на самом деле говорится в голосовом сообщении, верно?

  • Посылка с объективом для камеры.

  • Она задержана на таможне.

  • Они должны заплатить 6800 иен.

  • Теперь пользователь хочет задать вопрос об этом.

  • Окей.

  • Так что, эта задержка на таможне реальна?

  • В этом случае мы будем использовать 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 и вызова функций

  • Дэвид Чоу (David Chou)

    Вторая часть – сортировка. Нужно подумать, как использовать 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 */ }
  • Затем мы хотим определить функцию, которую мы можем использовать.

  • В этом примере мы рады сообщить, что интегрировали функцию приложения.

  • Если вы знакомы с этим инструментом, то, скорее всего, уже примерно понимаете, как работает Google Search Console.

  • Здесь мы настраиваем точку входа 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()
  • Настоятельно рекомендуем использовать его при вызове инструментов.

  • Причина в том, что мы провели множество тестов, которые показали, что при вызове инструментов в Gemini Nano 4 точность повышается на 13 баллов из 100.

  • В некоторых случаях он может быть значительно выше, что позволит вам получить гораздо лучшие результаты.

  • Если вы хотите узнать больше о вызове функций, посетите сеанс моих коллег Иоланды и Виктории, который состоится позже.

  • Это агентные возможности в вашем приложении.

  • Кажется, Виктория тоже где-то в зале. Не стесняйтесь поздороваться и с ней.

Извлечение данных о заказах с помощью эмбеддингов и AppSearch

  • Дэвид Чоу (David Chou)

    Отлично.

  • Итак, мы выполнили первые два шага.

  • Давайте поговорим о том, как работает извлечение.

  • Поэтому мы будем использовать эмбеддинги и 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, а также помогает обеспечить большую конфиденциальность и безопасность для ваших пользователей.

Сравнение сведений о заказе и определение результатов аудита

  • Дэвид Чоу (David Chou)

    Хорошо, а теперь последний шаг.

  • Итак, у нас есть вся необходимая информация.

  • Мы будем использовать 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 и настраиваем несколько разных перечислений для разных видов возможных противоречий, верно?

  • 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."
    }
  • И поэтому мы подтверждаем, что это мошенничество, мы получаем очень высокий балл риска мошенничества, это объясняет эти противоречия, которые очевидны в данных, которые вы получили, сравнивая их с информацией о звонящем.

  • Мы также сэкономили этому пользователю определенную сумму денег, поэтому мы можем отслеживать ее.

  • Да.

  • И это своего рода инкапсулирует всю информацию, которая нам нужна.

  • И, наконец, последний шаг – показать это пользователю в доступной форме.

  • Например, если они в пути или у них плохое зрение, вы можете прочитать эту информацию вслух, а не заставлять их читать этот текст вручную.

  • Поэтому мы собираемся реализовать возможность преобразования текста в речь, чтобы показать, как это будет выглядеть.

    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 ...
  • В этом случае пользователь, который не умеет читать, сможет взаимодействовать с интерфейсом, хотя и будет слышать роботизированный голос.

Итоги и заключительные замечания

  • Дэвид Чоу (David Chou)

    Окей.

  • Итак, мы проделали большую работу. Давайте подведем итоги. Мы использовали разные API для приема, сортировки, извлечения и аудита.

  • Например, API распознавания речи и перевода, Prompt API, вызов функций, эмбеддинги, AppSearch, структурированные выходные данные и синтез речи.

  • Это лишь один пример того, как можно использовать все эти возможности и запускать их полностью на устройстве.

  • И возвращаясь к тому, о чем мы говорили в начале, верно, есть существенные преимущества от запуска этого, верно?

  • Если бы вы запустили этот код на своем устройстве сегодня, то увидели бы, что на большинстве устройств он выполняется значительно быстрее.

  • Это ничего не стоит, но может сэкономить сотни тысяч долларов, если вы будете делать это постоянно.

  • И это доступно независимо от того, находится ли пользователь в горах или в подвале и не имеет доступа к интернету.

  • Это полностью конфиденциально, потому что вам не нужно отправлять информацию в облако.

  • Чтобы узнать больше о возможностях, ознакомьтесь с документацией. Для этого отсканируйте QR-код или перейдите по ссылке.

  • Это все возможности, которые мы запустили примерно за последние шесть месяцев.

  • Большое спасибо за внимание.

  • Спасибо, что уделили нам время.

  • Большое спасибо!

Код

Фрагменты кода

Фрагмент кода на Kotlin в видео "End-to-end scam detection walkthrough" (Пошаговое руководство по работе детектора мошенников)
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 в видео "Пошаговое руководство по сквозному детектору мошенников"
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 в видео "Сортировка входных данных с помощью Prompt API и вызова функций"
retrieveOrderAndDeliveryPhotos(
  searchQuery = "camera lens customs",
  claimedFeeUsd = 45,
  comparePackagePhotos = true
)
Фрагмент кода на Kotlin в видео "Сортировка входных данных с помощью Prompt API и вызова функций"
// 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 в видео "Сортировка входных данных с помощью Prompt API и вызова функций"
// 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 в видео "Сортировка входных данных с помощью Prompt API и вызова функций" (15:04)
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {

}
Фрагмент кода на Kotlin в видео "Сортировка входных данных с помощью Prompt API и вызова функций"
// 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 в видео "Сортировка входных данных с помощью Prompt API и вызова функций"
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 в видео "Получение данных о заказах с помощью эмбеддингов и 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 в видео "Получение данных о заказах с помощью эмбеддингов и 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 в видео "Получение данных о заказах с помощью эмбеддингов и AppSearch"
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
    val qVec = embedModel.generateEmbedding(
        EmbeddingRequest(
            content = Content(listOf(TextPart(searchQuery)))
        )
    ).embedding.vector

}
Фрагмент кода на Kotlin в видео "Получение данных о заказах с помощью эмбеддингов и 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 в видео "Получение данных о заказах с помощью эмбеддингов и 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 в разделе "Сравнение сведений о заказе и определение результатов аудита"
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 в разделе "Сравнение сведений о заказе и определение результатов аудита"
val forensicRequest = generateContentRequest(
    securitySystemInstruction,
    content {
        image(callerParcelPhotoBmp)
        image(orderEmailLensBmp)
        text(forensicAuditPrompt)
    }
) {

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

}
Фрагмент кода на Kotlin в разделе "Сравнение сведений о заказе и определение результатов аудита"
@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 в разделе "Сравнение сведений о заказах и определение результатов аудита"
@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 в разделе "Сравнение сведений о заказе и определение результатов аудита"
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().response
Фрагмент JSON на 21:44 в разделе "Сравнение сведений о заказе и определение результатов аудита"
{
  "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 в разделе "Сравнение сведений о заказе и определение результатов аудита"
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 в разделе "Сравнение сведений о заказе и определение результатов аудита"
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)
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 ...