在本課程中,我們將探討使用 ML Kit API 的各種生成式 AI 用途,瞭解何時適合在應用程式中使用裝置端生成式 AI,以及如何評估現有的生成式 AI 用途,判斷裝置端模型是否能達到品質標準。
轉錄稿
David Chou
,
各位午安。
我是 David Chou。
我是 Android AICore 的產品負責人。
很高興能與你分享裝置端 AI 功能。
今天,我們將討論幾個核心領域。
首先,請思考為什麼要使用裝置端 AI,尤其是雲端 AI 發展迅速的情況下。
第二,思考我們在過去一年中做出的改進,讓這些裝置端 AI 模型變得極為強大。
最後,我們將透過一個端對端範例,說明如何使用裝置端 AI 充分發揮這些優勢。
首先,為什麼要使用裝置端 AI?
主要有 4 個原因。
首先是速度,其次是成本,第三是可用性,第四是隱私權。
因此,在速度方面,您實際上可以看到裝置端 AI 推論現在已經達到比在雲端執行更快的程度,因為您不必在使用者裝置和雲端之間來回傳遞資訊。
因此,在我們測試的許多用途中,裝置端 AI 的速度實際上比雲端快 2 倍。
第二是考量成本。
假設您每天要執行 100 萬次推論。
如果是在裝置端 (尤其是使用者的裝置) 執行 AI,而非在雲端執行,實際上可以省下數十萬美元。
第三是離線可用性。
因此,我們在今年稍早發送問卷調查,在受訪的開發人員中,有 62% 表示離線可用性是使用裝置端 AI 的前三大原因。
因此,無論使用者是在 35,000 英尺的高空,還是在東京車站地下室,即使無法連上網際網路,仍可使用所有這些功能。
最後,根據我們對開發人員的調查,隱私權是他們最重視的原因之一。
因此,72% 的受訪者將其列為前三大原因。
40% 的受訪者將其列為首要考量。
因此,使用者希望訊息、語音記事、相片,甚至是財務或醫療資訊都能保持私密,因此在裝置端執行推論對他們來說非常有價值。
因此,我們建構了 ML Kit,讓您能維護及運用所有這些功能。
我們希望盡可能簡化這項程序。
因此,如有任何不滿,歡迎隨時與我聯絡。
我會在演講結束時留下聯絡資訊。
我們很樂意瞭解如何滿足您的使用需求。
因此,我們首先啟用了一系列功能,例如語音和創意用途。
例如校對、改寫,以及靈活的多模態提示。
因此,我們的目標是盡可能提高這項功能的實用性。
這也是為什麼許多使用者和開發人員都已開始採用這項功能。
目前已有 9 個第三方應用程式在正式環境中使用這些生成式 AI 功能,下載次數達 1,000 萬次。
我們也看到第一方和第三方應用程式的規模大幅擴張,AI 製作功能的應用程式啟動次數季增率達 110%。
因此,無論是訊息、廣告素材還是財務,我們都鼓勵您試用這些 API。
我們認為這項技術有無限可能,而您在許多用途上可能比我們更有想像力,因此我們很樂意與您合作,共同探討如何攜手並進。
現在您已瞭解大致上的優點。
你可能會想問,實際的觸及率如何?
裝置上的成效如何?
我們常說速度很快,但實際表現如何?
或是模型本身的品質,對吧?
我們將逐一介紹。
在觸及率方面,我們目前已在 1.75 億部 AICore 裝置上提供服務。
我們預計明年會大幅擴展,納入數億部其他裝置。
我們之所以能做到這一點,部分原因在於我們建構了非常簡單的測試架構。
因此,您只需要整合 ML Kit API 即可。
AICore 內建許多複雜功能,可管理輸入和輸出安全信號、管理模型和權重的下載作業,以及思考如何在執行階段設定模型。
因此,我們的目標是讓 ML Kit 盡可能易於使用,這樣您只需專注於如何提供優質體驗,其餘部分交給我們處理即可。
David Chou
我們也宣布,執行 Gemini Nano 所需的 RAM 已大幅減少。Gemini Nano 是許多 GenAI API 的主要模型。
因此,我們將 RAM 用量從 3.6 GB 降至 1.1 GB。
為了實現這個目標,我們進行了 4 項核心改善。
首先,我們使用 2 位元量化,而非 4 位元。
因此,位元數減半,就代表要透過模型傳送的位元組數也減半。
第二種是使用共用 KV 快取。
因此,我們大幅縮減了這個大小,讓模型的前 20 層共用 35 MB 套件中的資料,這表示您仍可在這個非常小的套件大小中,存取 16K 詞元脈絡大小。
第三個是啟用延遲視覺和音訊編碼器。
因此,在我們看到的許多文字用途中,您實際上不需要一開始就載入這些內容。
這樣一來,您就能再省下 0.5 GB 的 RAM。
最後,為了延長電池續航力,我們也啟用了 NPU 時脈調整功能。
這代表 NPU 能視需要調整運作速度,在節省電力的同時,確保仍能為你的用途提供高品質服務。
我們還做了其他幾項改進,以加快速度。
首先是啟用前置字元快取。
在這個範例中,系統提示詞會重複用於多輪對話,對吧?
因此處理這類資料的費用非常高昂。
但有了前置快取,我們基本上就不必反覆處理系統提示,對吧?
您可以將其儲存在前置字元快取中,這表示在這個範例中,您儲存了 60% 的權杖,這也表示在任何裝置上,速度都會提升 60%。
第二項改善是推測解碼。
在這個範例中,您可以看到 Gemini Nano 每次傳遞都會生成一個詞元。
這是因為大型語言模型具有自動迴歸特性,必須持續查看先前生成的詞元,才能判斷下一個詞元。
因此,我們建構了草稿模型,以更快的速度生成大量詞元。
這個草稿模型比 Gemini Nano 小得多,因此可以一次生成多個詞元,並傳送給 Gemini,看看哪些詞元與 Gemini Nano 的預測相符。
在這個範例中,您可以看到第一輪生成了六個詞元,下一輪生成了五個,到第三輪就完成了。
我們發現,在許多內部用途中,這項功能平均可將速度提升 2 倍。
視硬體而定,您可能會更快看到改善。
好的。
這就是為什麼回到速度方面,您可以看到在裝置上和雲端之間,即使在 5G 網路上,速度也有顯著提升。
可以看到,光是將資料上傳到雲端,1,000 到 1,300 個輸入詞元就要花一秒,因此即使雲端作業的預填和解碼速度可能較快,在裝置上執行的端對端作業速度仍明顯較快。
至於品質,我們很高興宣布,這款 2B 模型的效能實際上與去年的 27B 模型相當。
雖然效能不如現今的前沿模型,但我們可以看到,這些裝置端模型的 AI 技術已有大幅進展。
從 MMLU-Pro 這項標準知識基準測試中,可以看到 2B 模型的表現幾乎與去年的 27B 參數模型一樣好。
在研究生科學、推理和代理工具使用基準測試中,這款 2B 模型實際上表現更好,即使其大小不到去年模型的十分之一。
我們預期這些趨勢會持續下去。
我們將繼續與 GDM 團隊密切合作,確保能持續改善模型,讓您獲得最大價值。
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 流程,直接將位元組傳遞至語音翻譯工具。
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
第二部分是分類,也就是思考如何運用 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 可能就坐在觀眾席中,歡迎大家和她打招呼。
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().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."
}因此,我們確認這是詐騙,並獲得非常高的詐欺風險評分,這解釋了您擷取的資料與來電者資訊相比,資料中明顯存在的矛盾。
我們也為這位使用者省下了一筆錢,因此可以保留追蹤器。
是啊
這類資訊基本上涵蓋了我們需要的所有資訊。
最後一步是以無障礙方式向使用者顯示。
舉例來說,如果使用者正在移動或有視力障礙,您可能需要朗讀資訊,而不是讓他們手動閱讀文字。
因此,我們將實作文字轉語音功能,展示實際效果。
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 code 或前往連結。
這些是我們在過去六個月左右推出的所有功能。
非常感謝大家抽空參加這場講座。
感謝你抽出時間與我們交流。
非常感謝。
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 ...