このセッションでは、ML Kit API を使用したさまざまな生成 AI のユースケースを探索し、オンデバイス生成 AI がアプリに最適となるタイミングを理解します。また、既存の生成 AI のユースケースを評価して、オンデバイス モデルが品質基準を満たしているかどうかを確認する方法についても説明します。
文字起こし
David Chou
YouTube を
皆さん、こんにちは。
私の名前は David Chou です。
私は Android AICore のプロダクト リードです。
Google のオンデバイス AI 機能についてお話しできることをとても喜ばしく思っております。
本日は、いくつかの重要なトピックについてご説明します。
1 つ目は、クラウドでの AI の進歩が加速していることを踏まえ、オンデバイス AI を使用する理由について考えることです。
2 つ目は、これらのオンデバイス AI モデルを非常に強力にするために、昨年行ってきた改善について考えることです。
最後に、オンデバイス AI を使用してこれらのメリットをすべて活用する方法を示すエンドツーエンドの例について説明します。
まず、オンデバイス AI を使用する理由について説明します。
主な理由は 4 つあります。
1 つ目はスピード、2 つ目はコスト、3 つ目は可用性、4 つ目はプライバシーです。
速度に関しては、オンデバイス AI 推論は、ユーザーのデバイスとクラウドの間で情報をやり取りする必要がないため、クラウドで実行するよりも高速になる可能性があることがわかります。
テストしてきた多くのユースケースでは、オンデバイス AI とクラウドを比較すると、実際には 2 倍の高速化が確認されています。
2 つ目は、費用について考えることです。
1 日に 100 万回の推論を実行するとします。
オンデバイス AI を実行する場合、特にクラウドではなくユーザーのデバイスで実行する場合は、実際には数十万ドルを節約できます。
3 つ目はオフラインでの可用性です。
そこで、今年初めにアンケートを実施したところ、回答したデベロッパーの 62% が、オンデバイス AI を使用する理由の上位 3 つにオフラインでの可用性を挙げていました。
そのため、ユーザーが上空 35,000 フィートにいても、東京駅の地下にいてインターネットにアクセスできなくても、これらの機能をすべて利用できます。
最後に、デベロッパーを対象とした調査で、プライバシーが最大の理由の一つであることがわかりました。
72% が上位 3 つの理由に挙げました。
40% が 1 位にランク付け。
メッセージ、音声メモ、写真、財務情報、医療情報など、ユーザーはプライバシーを保護したいと考えています。そのため、オンデバイスで推論を実行することは、ユーザーにとって非常に価値のあることです。
これらのメリットをすべて活用できるように、ML Kit を構築しました。
このプロセスを極めてシンプルにしようとしています。
ご不満な点がございましたら、お気軽にお問い合わせください。
この講演の最後に私の連絡先情報をお知らせします。
お客様のユースケースを実現する方法を検討させていただきます。
まず、音声やクリエイティブなユースケース向けに、さまざまな機能を有効にしました。
たとえば、校正、書き換え、柔軟なマルチモーダル プロンプトの有効化などです。
そのため、Google の目標は、これをできるだけ使いやすくすることです。
そのため、多くのユーザーやデベロッパーがすでにこの機能を導入しています。
現在、1,000 万回ダウンロードされている 9 つのサードパーティ製アプリが、本番環境でこれらの生成 AI 機能を使用しています。
また、Google のファーストパーティ製アプリとサードパーティ製アプリの両方で、AI 制作機能のアプリ起動数が前四半期比で 110% 増加するなど、大幅な拡大が見られます。
メッセージ、クリエイティブ、ファイナンスの各分野で、利用可能な API をぜひお試しください。
可能性は無限に広がっています。これらのユースケースの多くにおいて、皆様は Google よりも優れたアイデアをお持ちのことと思います。ぜひ皆様とパートナーシップを組み、協力して取り組んでいきたいと考えています。
これで、大まかなメリットを理解できたと思います。
「実際のリーチはどのくらいなのか?」という疑問をお持ちの方もいらっしゃるかもしれません。
デバイスでのパフォーマンスはどうですか?
速度について、実際にはどの程度優れているのでしょうか。
モデル自体の品質などですね。
1 つずつ見ていきましょう。
リーチに関しては、すでに 1 億 7, 500 万台の AICore デバイスで利用可能です。
この数は来年中に大幅に増加し、数億台のデバイスが追加される見込みです。
これを可能にしている要因の一つは、非常にシンプルなハーネスを構築したことです。
そのため、統合する必要があるのは ML Kit API のみです。
AICore には、入力と出力の安全性シグナルの管理、モデルと重みのダウンロードの管理、ランタイムでの設定方法の検討など、多くの複雑な処理が組み込まれています。
そのため、ML Kit を使いやすくして、デベロッパーが優れたエクスペリエンスの提供に集中できるようにし、残りの部分は Google が担当することを目指しています。
David Chou
また、これらの GenAI API の多くを支える主要モデルである Gemini Nano の実行に必要な RAM を大幅に削減したこともお知らせします。
これにより、RAM の使用量を 3.6 GB から 1.1 GB に削減しました。
これを可能にするために、4 つの主要な改善を行いました。
1 つ目は、4 ビットではなく 2 ビットの量子化を使用することです。
ビット数が半分になるということは、モデルを通過するバイト数が半分になるということです。
2 つ目は、共有 KV キャッシュを使用することです。
そこで、モデルの最初の 20 レイヤが 35 MB のパッケージで共有できるようにすることで、このサイズを大幅に削減しました。つまり、この非常に小さなパッケージ サイズでも、16,000 トークンのコンテキスト サイズにアクセスできます。
3 つ目は、遅延ビジョン エンコーダと遅延オーディオ エンコーダを有効にすることです。
そのため、テキストのユースケースの多くでは、そもそもこれらを読み込む必要はありません。
これにより、さらに 0.5 GB の RAM を節約できます。
最後に、バッテリー駆動時間を改善するために、NPU クロック スケーリングも有効にしました。
つまり、NPU は、ユースケースで高品質を維持しながら、省電力に必要な速度で動作できます。
高速化に関しては、他にもいくつかの改善を行いました。
1 つ目は、プレフィックス キャッシュを有効にすることです。
この例では、システム プロンプトが複数のターンで再利用されています。
そのため、この処理は非常にコストがかかります。
しかし、プレフィックス キャッシュを使用すると、システム プロンプトを何度も再処理する必要がなくなります。
これをプレフィックス キャッシュに保存できます。この例では、渡す必要があるトークンの 60% を保存することになり、使用しているデバイスの速度が 60% 向上します。
2 つ目の改善点は、投機的デコードです。
この例では、Gemini Nano が 1 回のパスで 1 つのトークンを生成していることがわかります。
これは、LLM が自己回帰型であるためです。つまり、次のトークンを特定するために、生成された以前のトークンを調べ続ける必要があります。
そこで、この問題に対処するために、ドラフター モデルを構築して、大量のトークンをはるかに高速に生成できるようにしました。
このドラフター モデルは Gemini Nano よりも大幅に小さいため、一度に大量のトークンを生成し、それを Gemini に渡して、Gemini Nano の推測と実際に一致するトークンを確認できます。
この例では、最初のパスで 6 つのトークンが生成され、次のパスで 5 つのトークンが生成され、3 回目のパスで完了しています。
Google が検討している内部ユースケースの多くで、この方法により平均 2 倍の高速化が実現されています。
ハードウェアによっては、改善がより早く行われる場合があります。
違いますね。
速度の話に戻ると、5G ネットワークであっても、オンデバイスとクラウドの間で劇的な速度向上が見られるのは、このような理由があるからです。
1,300 個の入力トークンをクラウドにアップロードするだけでも 1 秒ほどかかることがわかります。Cloud Journey でのプリフィルとデコードは高速かもしれませんが、エンドツーエンドのジャーニーは、デバイス上で実行する方が大幅に高速です。
品質に関しては、この 2B モデルが昨年の 27B モデルと同等のパフォーマンスを発揮していることをお知らせします。
ご存じのとおり、現在のフロンティア モデルほどのパフォーマンスはありませんが、ここで示しているのは、これらのオンデバイス モデルにおける AI の大幅な進歩です。
標準的な知識ベンチマークである MMLU-Pro では、2B モデルが昨年の 27B パラメータ モデルとほぼ同等の性能を示しています。
さらに、大学院レベルの科学、推論、エージェント ツール使用のベンチマークでは、この 2B モデルは昨年のモデルの 10 分の 1 以下のサイズであるにもかかわらず、実際には優れています。
この傾向は今後も続くと予想されます。
Google は、引き続き GDM チームと緊密に連携して、モデルの改善を継続し、お客様に最大限の価値を提供できるように取り組んでいます。
David Chou
違いますね。
ここまで、多くのメリットと新しい改善点について説明してきましたが、「これらのモデルで実際に何ができるのか」という疑問をお持ちかもしれません。
API にも、このような機能改善が加えられています。
システム指示を使用すると、ペルソナを設定できます。
マルチ画像入力を使用すると、複数の画像を一度にアップロードできます。
また、今年の初めに思考モードを有効にしました。これにより、推論問題やツール呼び出しに、より多くの時間を費やすことができます。
最後に、構造化された出力により、Kotlin クラスにアノテーションを付けて、特定のスキーマに一致するオブジェクトを取得できるようになりました。
さらに、デベロッパーが希望する新しい改善点についても尋ねるようにしています。
調査対象のデベロッパーの 91% が、これらの機能のいずれかが利用可能であれば、エクスペリエンスをリリースすると回答しています。
そこで、今週初めに、音声翻訳、エンベディング、テキスト読み上げ、関数呼び出しの 4 つの機能をリリースしました。
音声翻訳では、20 以上の言語の音声を文字に変換できます。
エンベディングを使用すると、キーワード検索だけでなく、セマンティックな意味に基づいてアプリのコンテンツを検索できます。
テキスト読み上げ機能を使用すると、人間のような音声をエクスペリエンスに提供できるため、ロボットのような音声を使用する必要がなくなります。
最後に、関数呼び出しを使用すると、アプリがユーザーに代わってアクションを実行できるようになります。
すでに数社のデベロッパーと提携して、アプリ内エージェントをエクスペリエンスに組み込む方法を検討しています。来年後半にはリリースしたいと考えています。
David Chou
これらの機能をすべて使用して構築するエンドツーエンドの例を実際に見てみましょう。
たとえば、アメリカ人の写真家が日本を旅行しているとします。
写真からわかるように、カメラやレンズが明らかに不足しています。
そこで、新しいレンズを注文してホテルに送ってもらいます。
しかし、問題があります。その日のうちに、留守番電話にメッセージが届き、荷物をその日のうちに受け取るには、6,800 円の通関手数料を支払う必要があると言われます。
また、保留中の荷物の写真が添付されたテキスト メッセージも届いています。
この場合、お客様は詐欺検知による保護を求めています。
ボイスメールが正当なものか不正なものかを識別する方法を知りたいと考えています。
そのため、すべての情報を非公開に保ち、情報が根拠に基づいていること、そして説明可能であることを確認したいと考えています。
そのため、日本語のテキストを理解できるようにし、ユーザー補助のニーズがある場合は、利用可能な言語機能を使用できるようにします。
ここでは 4 つのステップがあります。
最初のステップは、ボイスメールをテキストに変換することです。
これには Speech Translation API を使用します。
そして、ユーザーが質問できるように、Speech Recognition API を使用します。
その後、ユーザーが必要な情報を取得できるように、必要な情報を取得できる関数を定義します。
そこで、Prompt API と関数呼び出しを使用します。
これはバックグラウンドで実行され、ユーザーには何も表示されません。
3 つ目は、Embedding API と AppSearch を使用して、その情報を保存および取得する方法を実際に確認します。
これはバックグラウンドでも実行されます。
最後に、収集した情報に基づいて、Prompt API、構造化出力、音声合成を使用して判定を行います。
違いますね。
始めましょう。
音声翻訳と音声認識を使用します。
最初のステップは、ボイスメールとテキストを実際に取得して、有用な情報に変換することです。
このユーザーは、「6,800 円の手数料を支払う必要があります。
関税が前払いされていない可能性があるため、お支払いいただく必要があります。」
そのため、翻訳機能が必要になります。
ここで注目していただきたいのは、翻訳と、情報に関するユーザーの質問の両方が、同じ音声エンコーダを使用して Gemini Nano 4 に送信されることです。音声エンコーダは 100 以上の言語でトレーニングされているためです。
そのため、その翻訳と、ユーザーの質問のテキストの ASR を取得できます。
翻訳の場合、必要なのはソース言語とターゲット言語を設定することだけです。
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)この例では、日本語とアメリカ英語を使用します。
バイトを Speech Translator に直接渡す Kotlin フローを設定します。
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)
val audioFlow = flow {
emit(TranslationAudio(Locale.JAPANESE, courierVoicemailBytes))
}
val request = SpeechTranslatorRequest.Builder(audioFlow).build()
translator.startTranslation(request).collect { response ->
if (response is SpeechTranslatorResponse.Text) {
translatedCourierClaim = response.translatedText
}
}そして、翻訳されたテキストを使用してレスポンスを収集できます。
違いますね。
ありがとうございます。
これで、ボイスメールの内容が実際に理解できるようになりました。
カメラのレンズの荷物がある。
税関で差し止められている。
6,800 円の手数料を支払う必要があります。
そして、ユーザーがそれについて質問できるようにしたいと考えています。
違いますね。
では、この税関保留は本当に実在するのでしょうか?
この例では、ASR API を使用します。
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)ここでは、ロケールとしてアメリカ英語を設定しています。
次に、音声エンコーダを活用する詳細モードに設定します。
そして、その情報も渡して最終的な回答を収集し、それを Gemini Nano に渡す情報に変換します。
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)
val request = speechRecognizerRequest {
audioSource = AudioSource.fromMic()
recognitionOptions { enableWordTiming = true } // optional
}
recognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse ->
liveCaption = response.text
is SpeechRecognizerResponse.FinalTextResponse ->
planAndCallVaultTool(translatedCourierClaim, response.text)
}
}違いますね。
これで、登録の最初の部分が完了しました。
ご覧のとおり、それほど多くのコード行は必要ありませんでした。
David Chou
2 つ目の部分はトリアージです。これは、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 でツール呼び出しを実行すると、精度が 100 点満点中 13 点向上することが、一連のテストで示されているためです。
場合によっては、実際には大幅に高くなることもあります。そのため、この方法ではるかに優れた結果が得られる可能性があります。
関数呼び出しについて詳しく知りたい方は、この後開催される、私の同僚の Jolanda と Victoria のセッションにご参加ください。
これは、アプリのエージェント機能に関するものです。
Victoria も会場のどこかにいると思いますので、ぜひ声をかけてみてください。
David Chou
どうでしょう。
これで、最初の 2 つのステップが完了しました。
では、情報取得の仕組みについて説明します。
そこで、エンベディングと 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注文メールを受け取ります。
ここでも、1 つの画像と 1 つのテキストを渡すだけという非常にシンプルなものですが、インデックスに登録するコンテンツがさらにある場合もあります。
次に、エンベディング ベクトルを生成します。
その後、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 を使用します。コサイン類似度を使用して、Vault から取得した一致と、発信者からの情報を比較できます。
このように、画像が一致しない場合でも、テキストは非常に役立ちます。
ここで、マルチモーダル エンベディングが非常に役立ちます。
異なる部分の情報を入力して、共通の意味を取得できます。
マルチモーダル エンベディングを使用すると、これらすべてを同じベクトル空間にマッピングできます。
ここでも、思考モードを使用して思考を改善します。
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 の範囲で最小値と最大値の両方を設定できます。
また、節約できた金額を記録しておき、お客様に伝えることもできます。
また、先ほど定義した視覚的な矛盾をリストアップすることもできます。
ここで注意すべきもう 1 つの点は、@Guide アノテーションは実際には省略可能であるということです。
必ずしも設定して、そのような説明を提供する必要はありません。
これが役立つのは、変数の定義だけでは何が起こっているのかを説明できない場合があるためです。
この説明は、Gemini Nano が呼び出す特定の関数を判断し、適切なフィールドに適切な情報を入力するように設定するのに役立ちます。
設定が完了すれば、あとはとても簡単です。
ここでは 3 行のコードを実行するだけです。
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().responsegenerateTypedContentRequest、そのコンテンツを生成し、そのレスポンスを取得できます。
そして、これがその様子を示すものです。
{
"verdict": "CONFIRMED_PHISHING_SCAM",
"fraudRiskScore": 98,
"moneySavedUsd": 45,
"contradictions": [
{ "contradictionType": "DUTY_FRAUD", "explanation": "Fee demanded, but duties were prepaid." },
{ "contradictionType": "PACKAGING_MISMATCH", "explanation": "Carton has no carrier label or tracking." },
],
"spokenWarningSummary": "Scam blocked. Do not pay the 45 dollar fee. Your lens shipped duties prepaid."
}そして、これが詐欺であることを確認し、非常に高い不正リスク スコアリングを取得します。これは、取得したデータと発信者情報を比較して明らかな矛盾を説明しています。
また、このユーザーが節約した金額も保存されているため、トラッカーを維持できます。
うん
この 1 つのファイルに、必要な情報がすべて含まれています。
最後のステップは、この情報をアクセス可能な方法でユーザーに表示することです。
たとえば、外出中や視覚障がいがある場合、テキストを手動で読むのではなく、その情報を音声で読み上げることができます。
そこで、テキスト読み上げ機能を実装して、どのようなものになるかを示します。
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、翻訳 API、Prompt API、関数呼び出し、エンベディング、AppSearch、構造化出力、音声合成などを使用しています。
これは、これらの機能をすべて一貫した方法で活用し、完全にデバイス上で実行する方法の一例です。
そして、最初に話した内容に戻ると、これを実行することには大きなメリットがあります。
実際にデバイスで実行してみると、ほとんどのハードウェアで大幅に高速化されていることがわかります。
費用はかかりません。これを常に実行していれば、数十万ドルを節約できる可能性があります。
そして、ユーザーが山の中にいるか地下にいるか、インターネットにアクセスできないかどうかにかかわらず、この機能を利用できます。
また、クラウドに情報を送信する必要がないため、完全に非公開です。
これらの機能について詳しくは、ドキュメント、QR コード、またはリンクをご覧ください。
これらはすべて、過去 6 か月ほどでリリースされた機能です。
本日はお時間をいただき、ありがとうございました。
お時間をいただきありがとうございました。
本当にありがとうございました。
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)
val audioFlow = flow {
emit(TranslationAudio(Locale.JAPANESE, courierVoicemailBytes))
}
val request = SpeechTranslatorRequest.Builder(audioFlow).build()
translator.startTranslation(request).collect { response ->
if (response is SpeechTranslatorResponse.Text) {
translatedCourierClaim = response.translatedText
}
}val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)
val request = speechRecognizerRequest {
audioSource = AudioSource.fromMic()
recognitionOptions { enableWordTiming = true } // optional
}
recognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse ->
liveCaption = response.text
is SpeechRecognizerResponse.FinalTextResponse ->
planAndCallVaultTool(translatedCourierClaim, response.text)
}
}retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)// Keep SystemInstruction static across Turn 1 & Turn 2:
val securitySystemInstruction = SystemInstruction(
"""
You are an on-device parcel & phishing security auditor.
1. Cross-check packaging, carrier labels and tracking across all images.
2. Verify customs fee claims against local DDP (Delivered Duty Paid) order records.
3. Flag any payment demand when the order record shows duties prepaid.
""".trimIndent()
)// Pull in the Speech Translation (AST) & Speech Recognition (ASR) outputs:
val turn1Prompt = """
Translated voicemail: $translatedCourierClaim
Incoming text message: $callerText
User's spoken question: $userQuestion
""".trimIndent()
val turn1Request = generateContentRequest(
securitySystemInstruction,
TextPart(turn1Prompt)
) { /* enableThinking & setTools */ }// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
/**
* Searches local orders and order images in AppSearch.
* @param searchQuery Semantic query to match the order
* @param claimedFeeUsd Customs fee claimed in USD
* @param comparePackagePhotos Whether to fetch order images to compare
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun retrieveOrderAndDeliveryPhotos(
searchQuery: String,
claimedFeeUsd: Int,
comparePackagePhotos: Boolean
): VaultMatch = withContext(Dispatchers.IO) { executeVaultSearch(searchQuery) }
}val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()val embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vectorval embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vector
val doc = GenericDocument.Builder<GenericDocument.Builder<*>>(
"orders", "order_jp_904", "OrderRecord"
)
.setPropertyString("summary", "Shipped DDP, duties prepaid ($0 owed). In transit.")
.setPropertyString("photoUris", "order_email_lens.png")
.setPropertyEmbedding(
"embedding",
EmbeddingVector(docVector, embedModel.getModelSignature())
).build()suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
}suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
val results = appSearchSession.search(
"semanticSearch(getEmbeddingParameter(0), 0.68)", spec
)
// Cleanly close the cursor.
return results.use { it.nextPageAsync.await().first().genericDocument.toVaultMatch() }
}val callerVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(callerParcelPhotoBmp), // brown box w/ shipping label
TextPart(callerText) // "Clearance fee ¥6,800 due today."
))
)
).embedding.vector
val matchScore = cosineSimilarity(callerVector, vaultMatch.embedding)val forensicRequest = generateContentRequest(
securitySystemInstruction,
content {
image(callerParcelPhotoBmp)
image(orderEmailLensBmp)
text(forensicAuditPrompt)
}
) {
// Enable Gemini Nano 4 reasoning before it answers
enableThinking = true
}@Generable(description = "Details of a visual contradiction found during an audit")
data class VisualContradiction(
@Guide(
description = "The type of contradiction",
enumValues = ["PACKAGING_MISMATCH", "STATUS_CONFLICT", "DUTY_FRAUD"],
)
val contradictionType: String,
@Guide(description = "Detailed explanation of the visual contradiction")
val explanation: String,
)@Generable(description = "Verdict and details of a phishing audit")
data class PhishingAuditVerdict(
@Guide(
description = "The audit verdict",
enumValues = ["CONFIRMED_PHISHING_SCAM", "LEGITIMATE_HOLD"],
)
val verdict: String,
@Guide(description = "The fraud risk score from 0 to 100", minimum = 0.0, maximum = 100.0)
val fraudRiskScore: Int,
@Guide(description = "Estimated amount of money saved in USD", minimum = 0.0)
val moneySavedUsd: Int,
@Guide(description = "List of visual contradictions identified during audit", minItems = 0)
val contradictions: List<VisualContradiction>,
// @Guide(description = "Summary of the warning to be spoken to the user")
// `@Guide` annotations are optional.
val spokenWarningSummary: String,
)val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().response{
"verdict": "CONFIRMED_PHISHING_SCAM",
"fraudRiskScore": 98,
"moneySavedUsd": 45,
"contradictions": [
{ "contradictionType": "DUTY_FRAUD", "explanation": "Fee demanded, but duties were prepaid." },
{ "contradictionType": "PACKAGING_MISMATCH", "explanation": "Carton has no carrier label or tracking." },
],
"spokenWarningSummary": "Scam blocked. Do not pay the 45 dollar fee. Your lens shipped duties prepaid."
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
/* more code here */
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> { }
else -> {}
}
}
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> {
/* handle fallback to platform API */
}
else -> {}
}
}
}
// more code here ...