智能应用

设备端智能:使用 Android 机器学习套件 API 优化性能和质量

扬声器

关于

演讲简介

在本课程中,我们将探索使用机器学习套件 API 的各种生成式 AI 应用场景,了解何时适合在应用中使用设备端生成式 AI,并了解如何评估现有生成式 AI 应用场景,以确定设备端模型是否能满足其质量标准。

摘要

  • 与云端推理相比,设备端 AI 在速度和隐私保护方面具有显著优势。内部测试表明,与在云端运行模型相比,速度提升了 2 倍。
  • Gemini Nano 的内存用量从 3.6 GB 降至 1.1 GB。主要优化包括 2 位量化和共享 KV 缓存。
  • 前缀缓存和推测解码可显著加快推理速度。重复使用系统提示可将处理的 token 数量减少 60%,而较小的起草者模型可将速度平均提升 2 倍。
  • 新的设备端功能通过语音翻译和函数调用扩展了应用互动。添加思考模式后,Gemini Nano 4 的工具调用准确率提高了 13 个百分点(满分 100 分)。
  • 多模态嵌入与 AppSearch 结合使用,可通过余弦相似度存储和检索数据。您的应用在本地执行搜索逻辑,从而保护用户隐私和数据安全。

资源

Gemini Notebook

主题

  • on-device-ai
  • ml-kit
  • gemini-nano
  • aicore
  • 函数调用
  • 嵌入
  • speech-translation

转写文稿

设备端 AI 简介

  • David Chou

    你好。

  • 大家下午好!

  • 我叫 David Chou。

  • 我是 Android AICore 的产品主管。

  • 很高兴向各位介绍我们的设备端 AI 功能。

  • 今天,我们将探讨几个核心领域。

  • 首先,我们要思考为什么要使用设备端 AI,尤其是在云端 AI 发展如此迅速的情况下。

  • 第二,思考我们在过去一年中做出的一些改进,这些改进让这些设备端 AI 模型变得非常强大。

  • 最后,我们将通过一个端到端示例,展示如何利用设备端 AI 充分发挥这些优势。

  • 首先,为什么要使用设备端 AI?

  • 主要有 4 个原因。

  • 首先是速度,其次是费用,第三是可用性,第四是隐私。

  • 因此,在速度方面,您可以看到,设备端 AI 推理现在已经可以比在云端运行更快,这仅仅是因为您不必在用户设备和云端之间来回传递信息。

  • 因此,在我们测试过的许多用例中,我们实际上看到设备端 AI 的速度比云端 AI 快 2 倍。

  • 第二点是考虑成本。

  • 假设您每天要运行 100 万次推理。

  • 如果您运行的是设备端 AI(具体而言,是在用户设备上运行,而不是在云端运行),实际上可以节省数十万美元。

  • 第三个是离线可用性。

  • 因此,我们在今年早些时候发出了一份调查问卷,结果显示,在我们访谈的开发者中,有 62% 的人表示,离线可用性是使用设备端 AI 的三大原因之一。

  • 因此,无论用户是在 35,000 英尺的高空,还是在东京车站的地下室,即使无法访问互联网,他们仍然可以利用所有这些功能。

  • 最后,根据我们对开发者的调查,隐私保护是他们选择 Google Play 的最重要原因之一。

  • 因此,72% 的受访者将其列为前三大原因之一。

  • 40% 的受访者将其列为首要因素。

  • 因此,对于消息、语音备忘录、照片,甚至财务或医疗信息,用户都希望保持私密,因此在设备上运行推理对他们来说非常有价值。

  • 鉴于这些优势,我们构建了机器学习套件,以便您能够维护和利用所有这些功能。

  • 我们正在努力让这一过程变得极其简单。

  • 因此,如果您有任何投诉,欢迎随时与我联系。

  • 我会在本次演讲结束时留下我的联系信息。

  • 我们很乐意了解如何才能满足您的使用情形。

  • 因此,我们首先针对语音和创意应用场景启用了一系列功能。

  • 例如校对、重写,以及支持灵活的多模态提示词。

  • 因此,我们的目标是尽可能提高其可用性。

  • 这就是为什么我们的许多用户和开发者已经开始采用这种方式。

  • 目前,已有 9 款下载量达 1,000 万的第三方应用在生产环境中使用这些生成式 AI 功能。

  • 我们看到,在我们的第一方应用和第三方应用中,AI 生产力功能的使用规模都在大幅提升,应用启动次数季环比增长 110%。

  • 因此,我们鼓励您在消息传递、创意和财务等领域尝试使用我们提供的这些 API。

  • 我们认为,生成式 AI 的应用场景不胜枚举,而且在许多用例中,您的想象力可能比我们更丰富,因此我们非常乐意与您合作,共同探索如何携手共进。

  • 现在,您已经了解了大致优势。

  • 您可能会问,实际覆盖面如何?

  • 设备上的性能如何?

  • 当我们谈论速度时,它到底有多快?

  • 或者模型本身的质量,对吧?

  • 我们将逐一介绍这些步骤。

  • 在覆盖范围方面,我们目前已覆盖 1.75 亿台 AICore 设备。

  • 我们预计,这一数字在未来一年内将大幅增长,届时将有数亿台其他设备加入。

  • 之所以能做到这一点,部分原因在于我们构建了一个非常简单的测试框架。

  • 因此,您只需集成机器学习套件 API 即可。

  • AICore 内部集成了许多复杂机制,用于管理输入和输出安全信号、管理模型及权重的下载,以及考虑如何在运行时进行设置。

  • 因此,我们的目标是让机器学习套件尽可能易于使用,这样您只需专注于如何提供出色的体验,而其他事情则由我们来处理。

优化 Gemini Nano 性能和基准

  • David Chou

    我们还很高兴地宣布,我们已大幅降低运行 Gemini Nano 所需的 RAM,Gemini Nano 是为许多此类生成式 AI API 提供支持的主要模型。

  • 因此,我们将 RAM 用量从 3.6 GB 降到了 1.1 GB。

  • 为了实现这一目标,我们做出了 4 项核心改进。

  • 第一种是使用 2 位量化,而不是 4 位量化。

  • 因此,位数减半意味着需要通过模型传输的字节数也减半。

  • 第二种是使用共享 KV 缓存。

  • 因此,我们大幅缩减了这一大小,允许模型的前 20 层在 35 MB 的软件包中共享该大小,这意味着您仍然可以在这个非常非常小的软件包大小内访问 16K 词元的上下文大小。

  • 第三个是启用延迟视觉和音频编码器。

  • 因此,对于我们看到的许多文本用例,您实际上不需要一开始就加载这些内容。

  • 这样可以再节省 0.5 GB 的 RAM。

  • 最后,为了延长电池续航时间,我们还启用了 NPU 时钟调节功能。

  • 这意味着,NPU 可以根据需要快速运行,以节省电量,同时确保在您的应用场景中仍能提供高画质。

  • 在提速方面,我们还做了一些其他改进。

  • 第一种是启用前缀缓存。

  • 您可以看到,在此示例中,系统提示在多轮对话中重复使用,对吧?

  • 因此,处理这些数据的成本非常高。

  • 但有了前缀缓存,我们基本上就不必反复重新处理该系统提示了,对吧?

  • 您可以将其保存在前缀缓存中,这意味着,在本例中,您节省了 60% 的必须传入的 token,这也意味着,无论您使用何种设备,速度都会提高 60%。

  • 我们做出的第二项改进是推测性解码。

  • 在这个示例中,您可以看到 Gemini Nano 每次生成一个 token。

  • 这是因为 LLM 是自回归的,这意味着它们必须不断查看之前生成的 token,才能确定下一个 token。

  • 因此,我们的处理方式是构建一个起草者模型,该模型可以更快地生成大量 token。

  • 这个起草者模型比 Gemini Nano 小得多,因此可以一次生成一堆 token,然后将其传递给 Gemini,看看哪些 token 实际上与 Gemini Nano 的猜测一致。

  • 在这个例子中,你可以看到,在第一轮中,它生成了 6 个 token,下一轮生成了 5 个,然后到第三轮,它实际上已经完成了。

  • 我们发现,对于我们正在研究的许多内部用例,这使得速度平均提高了 2 倍。

  • 根据您的硬件,您可能会获得更快的改进。

  • 好的。

  • 所以,这就是为什么,你知道,回到速度,你实际上可以看到这种戏剧性的加速,对吧,在设备和云之间,即使在 5G 网络上也是如此。

  • 可以看到,仅仅将这些数据上传到云端就需要大约 1 秒钟(对于 1,300 个输入 token),因此,即使 Cloud Journey 中的预填充和解码速度可能更快,但端到端流程在设备上运行时仍然要快得多。

  • 在质量方面,我们很高兴地宣布,这款 2B 模型的性能实际上可以与去年的 27B 模型相媲美。

  • 虽然它的性能不如我们今天拥有的这些前沿模型,但我们在这里展示的是,这些设备端模型在 AI 方面取得了实质性进展,对吧?

  • 可以看到,在标准知识基准 MMLU-Pro 上,2B 模型的表现几乎与去年的 27B 参数模型相当。

  • 在研究生科学、推理和智能体工具使用基准测试中,这款 2B 模型的表现实际上更出色,尽管其规模还不到去年模型的十分之一。

  • 因此,我们预计这些趋势将会继续。

  • 我们将继续与 GDM 团队密切合作,确保能够持续改进模型,让您从中获得最大价值。

Gemini API 新功能概览

  • David Chou

    好的。

  • 刚才我向大家介绍了许多优势和新改进,大家可能会问:“我们实际上可以用这些模型做什么?”

  • 因此,我们一直在 API 中进行这些功能改进。

  • 因此,您可以使用系统指令设置角色。

  • 借助多图片输入功能,您可以一次上传多张图片。

  • 今年早些时候,我们还启用了思考模式,以便您有更多时间处理推理问题和工具调用。

  • 最后,借助结构化输出,您还可以为 Kotlin 类添加注解,并获取与特定架构匹配的对象。

  • 除此之外,我们还会尝试询问开发者他们希望做出哪些新的改进。

  • 因此,在接受调查的开发者中,有 91% 的人表示,如果这些功能之一可用,他们就会推出相应体验。

  • 因此,本周早些时候,我们推出了以下四项功能:语音翻译、嵌入、文字转语音和函数调用。

  • 借助语音翻译功能,您可以转写 20 多种语言的音频。

  • 借助嵌入,您可以按语义含义搜索应用的内容,而不仅仅是使用关键字搜索。

  • 借助文字转语音功能,您现在可以为体验提供类似真人的语音,而不再需要使用机器人语音。

  • 最后,借助函数调用,您还可以让应用代表用户执行操作。

  • 因此,我们已经与一些开发者合作,探索如何在他们的体验中构建应用内智能体,并希望在明年晚些时候推出。

端到端诈骗检测演示

  • David Chou

    了解了所有这些功能后,我们来看一个端到端示例,其中会用到所有这些功能。

  • 假设有一位美国摄影师在日本旅行。

  • 从照片中可以看出,他们的相机和镜头显然不够用。

  • 于是,他们订购了一个新镜头,并让商家寄到他们入住的酒店。

  • 但有一个问题:在他们忙碌的一天中,他们收到了一条语音邮件,被告知需要支付 6800 日元的清关费,才能在当天结束前收到包裹。

  • 他们还收到了一条短信,其中包含被扣留包裹的照片。

  • 因此,在这种情况下,用户希望获得某种诈骗检测保护,对吗?

  • 他们想了解如何识别语音邮件是合法的还是欺诈性的。

  • 因此,我们希望确保所有信息的私密性,确保信息有据可依,同时确保信息可解释。

  • 因此,我们要确保他们能够理解日语文本,并且能够使用任何可用的语言功能,以防他们有无障碍需求。

  • 所以,这里有四个步骤,对吧?

  • 第一步是将语音信息转换为文本,也就是翻译语音信息。

  • 我们将使用 Speech Translation API 来实现这一目标。

  • 然后,如果他们想提问,就可以提问,因此我们将使用 Speech Recognition API。

  • 然后,我们要定义能够检索这些信息的函数,以便用户获取所需内容。

  • 因此,我们将使用 Prompt API 和函数调用。

  • 现在,此操作将在后台运行,用户看不到任何内容。

  • 第三,我们将实际了解如何使用嵌入 API 和 AppSearch 存储和检索这些信息。

  • 此操作也会在后台运行。

  • 最后,我们将使用 Prompt API、结构化输出和语音合成,根据收集到的信息做出判定。

  • 好的。

  • 我们开始上课。

  • 我们将使用语音翻译和语音识别功能。

  • 所以,第一步实际上就是把语音留言和文字转换成有用的信息,对吧?

  • 因此,用户收到了这条消息:“您好,您需要支付这笔 6,800 日元的费用。

  • 关税可能未预付,因此您需要支付。”

  • 因此,他们希望能够获得翻译,对吧?

  • 因此,我们想在这里指出的一点是,翻译以及用户对信息的提问都会使用同一个音频编码器发送到 Gemini Nano 4,因为该音频编码器已经过 100 多种语言的训练。

  • 这样,您就可以获得该翻译,以及用户问题文本的 ASR。

  • 因此,对于翻译,我们只需设置源语言和目标语言。

    val translator = SpeechTranslation.getClient(
        SpeechTranslatorOptions.Builder(
            Locale.JAPANESE, Locale.US
        ).build()
    )
  • 在本例中,我们将使用日语和美式英语。

  • 我们将设置一个 Kotlin flow,将字节直接传递到语音翻译器。

    val translator = SpeechTranslation.getClient(
        SpeechTranslatorOptions.Builder(
            Locale.JAPANESE, Locale.US
        ).build()
    )
    
    val audioFlow = flow {
        emit(TranslationAudio(Locale.JAPANESE, courierVoicemailBytes))
    }
    
    val request = SpeechTranslatorRequest.Builder(audioFlow).build()
    translator.startTranslation(request).collect { response ->
        if (response is SpeechTranslatorResponse.Text) {
            translatedCourierClaim = response.translatedText
        }
    }
  • 然后,我们就可以收集使用翻译后的文本生成的回答。

  • 好的。

  • 太好了。

  • 现在我们知道语音留言的实际内容了,对吧?

  • 有一个相机镜头包裹。

  • 包裹被海关扣留。

  • 他们必须支付 6,800 日元的费用。

  • 现在,用户希望能够就此提出问题。

  • 好的。

  • 那么,这个海关扣留通知是真的吗?

  • 在本例中,我们将使用 ASR API。

    val recognizer = SpeechRecognition.getClient(
        speechRecognizerOptions {
            locale = Locale.US
            preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
        }
    )
  • 我们仍将美式英语设置为语言区域。

  • 然后,我们可以将其设置为高级模式,该模式会利用音频编码器。

  • 然后,我们也可以传入这些信息,收集最终回答,并将其转换为要传递给 Gemini Nano 的信息。

    val recognizer = SpeechRecognition.getClient(
        speechRecognizerOptions {
            locale = Locale.US
            preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
        }
    )
    
    val request = speechRecognizerRequest {
        audioSource = AudioSource.fromMic()
        recognitionOptions { enableWordTiming = true } // optional
    }
    
    recognizer.startRecognition(request).collect { response ->
        when (response) {
            is SpeechRecognizerResponse.PartialTextResponse ->
                liveCaption = response.text
            is SpeechRecognizerResponse.FinalTextResponse ->
                planAndCallVaultTool(translatedCourierClaim, response.text)
        }
    }
  • 好的。

  • 现在,我们已经完成了信息收集的第一部分。

  • 如您所见,这并没有用到很多行代码。

使用 Prompt API 和函数调用对输入进行分类

  • 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 */ }
  • 然后,我们要定义一个可以使用的函数。

  • 因此,对于这个特定示例,我们很高兴地宣布,我们已经集成了应用功能。

  • 因此,如果您熟悉该功能,可能已经大致了解了其运作方式。

  • 因此,我们在这里设置了一个 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 分)。

  • 在某些情况下,实际效果甚至会大幅提升,因此这可能会为您带来更好的结果。

  • 如果您想详细了解函数调用,请参加我的同事 Jolanda 和 Victoria 稍后举办的专题演讲。

  • 本课程将介绍应用中的智能体功能。

  • 我想 Victoria 可能就坐在观众席的某个地方,所以,也欢迎大家向她打招呼。

使用嵌入和 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,以及结构化输出和语音合成。

  • 这只是一个示例,展示了如何以连贯的方式利用所有这些功能,并完全在设备上运行。

  • 回到我们一开始讨论的内容,运行这项测试会带来巨大的好处,对吧?

  • 如果您今天在设备上实际运行此模型,就会发现它在大多数硬件上的设备端运行速度都明显更快。

  • 这不会产生任何费用,对吧?如果您一直运行此工具,可能会节省数十万美元。

  • 无论用户是在深山老林还是在地下室,即使无法访问互联网,也能使用该功能。

  • 而且完全私密,因为您无需向云端发送任何信息。

  • 鉴于这些功能,欢迎随时查看文档、扫描二维码或访问链接。

  • 这些是我们在过去大约六个月内推出的所有功能。

  • 非常感谢大家抽出时间来参加这次演讲。

  • 非常感谢您抽出宝贵时间与我们交流。

  • 非常感谢!

代码

代码段

“端到端诈骗检测演示”中 12:38 处的 Kotlin 代码段
val translator = SpeechTranslation.getClient(
    SpeechTranslatorOptions.Builder(
        Locale.JAPANESE, Locale.US
    ).build()
)
“端到端诈骗检测演示”中 12:47 处的 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
    }
}
“端到端诈骗检测演示”中 13:13 处的 Kotlin 代码段
val recognizer = SpeechRecognition.getClient(
    speechRecognizerOptions {
        locale = Locale.US
        preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
    }
)
“端到端诈骗检测演示”中 13:24 处的 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)
    }
}
“使用 Prompt API 和函数调用对输入进行分类”中 14:03 处的 Kotlin 代码段
retrieveOrderAndDeliveryPhotos(
  searchQuery = "camera lens customs",
  claimedFeeUsd = 45,
  comparePackagePhotos = true
)
“使用 Prompt API 和函数调用对输入进行分类”中 14:07 处的 Kotlin 代码段
// 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()
)
“使用 Prompt API 和函数调用对输入进行分类”中 14:33 处的 Kotlin 代码段
// 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 */ }
“使用 Prompt API 和函数调用对输入进行分类”中 15:04 处的 Kotlin 代码段
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {

}
“使用 Prompt API 和函数调用对输入进行分类”中 15:15 处的 Kotlin 代码段
// 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) }
}
“使用 Prompt API 和函数调用对输入进行分类”中 15:35 处的 Kotlin 代码段
val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {

    enableThinking = true
    setTools(vaultTools)

}
val call = generativeModel.generateContent(turn1Request)
    .candidates.first().functionCalls.first()
“使用嵌入和 AppSearch 检索订单数据”中 17:03 处的 Kotlin 代码段
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 检索订单数据”中 17:16 处的 Kotlin 代码段
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()
“使用嵌入和 AppSearch 检索订单数据”中 17:48 处的 Kotlin 代码段
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
    val qVec = embedModel.generateEmbedding(
        EmbeddingRequest(
            content = Content(listOf(TextPart(searchQuery)))
        )
    ).embedding.vector

}
“使用嵌入和 AppSearch 检索订单数据”中 18:03 处的 Kotlin 代码段
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()

}
“使用嵌入和 AppSearch 检索订单数据”中 18:12 处的 Kotlin 代码段
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() }
}
“Comparing order details and defining audit verdicts”(比较订单详情并定义审核结论)中 19:24 处的 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)
“比较订单详情并定义审核结论”中 20:10 处的 Kotlin 代码段
val forensicRequest = generateContentRequest(
    securitySystemInstruction,
    content {
        image(callerParcelPhotoBmp)
        image(orderEmailLensBmp)
        text(forensicAuditPrompt)
    }
) {

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

}
“比较订单详情并定义审核结论”中 20:15 处的 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,
)
“比较订单详情并定义审核结论”中 20:35 处的 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,
)
“Comparing order details and defining audit verdicts”中 21:35 处的 Kotlin 代码段
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().response
“比较订单详情并定义审核判定结果”中 21:44 处的 JSON 代码段
{
  "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."
}
“比较订单详情并定义审核结果”中 22:22 处的 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
}
“比较订单详情并定义审核结论”中 22:38 处的 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
}
“比较订单详情并定义审核结论”中 22:49 处的 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 -> {
     /* handle fallback to platform API */
 }
                else -> {}
            }
        }
    }
// more code here ...