In dieser Session sehen wir uns verschiedene Anwendungsfälle für generative KI mit ML Kit APIs an, um herauszufinden, wann On-Device-GenAI für eine App ideal ist. Außerdem erfahren Sie, wie Sie bestehende Anwendungsfälle für generative KI bewerten können, um festzustellen, ob On-Device-Modelle die Qualitätsanforderungen erfüllen.
Transkript
David Chou
Schau mal
Guten Tag, alle zusammen.
Ich heiße David Chou.
Ich bin Product Lead für Android AICore.
Ich freue mich sehr, mit Ihnen über unsere On-Device-KI-Funktionen zu sprechen.
Heute sprechen wir über einige Kernbereiche.
Zuerst sollten wir uns überlegen, warum wir On-Device-KI verwenden sollten, insbesondere angesichts der rasanten Fortschritte bei der KI in der Cloud.
Zweitens haben wir im letzten Jahr einige Verbesserungen vorgenommen, um diese On-Device-KI-Modelle extrem leistungsfähig zu machen.
Zum Schluss sehen wir uns ein End-to-End-Beispiel an, das zeigt, wie Sie alle diese Vorteile mit On-Device-KI nutzen können.
Warum sollte man On-Device-KI verwenden?
Dafür gibt es vier wichtige Gründe.
Der erste ist die Geschwindigkeit, der zweite die Kosten, der dritte die Verfügbarkeit und der vierte der Datenschutz.
In Bezug auf die Geschwindigkeit ist die KI-Inferenz auf dem Gerät mittlerweile so weit, dass sie schneller sein kann als die Ausführung in der Cloud, da keine Informationen zwischen dem Gerät des Nutzers und der Cloud hin- und hergesendet werden müssen.
Bei vielen der von uns getesteten Anwendungsfälle haben wir festgestellt, dass die On-Device-KI im Vergleich zur Cloud doppelt so schnell ist.
Der zweite Aspekt sind die Kosten.
Stellen wir uns vor, Sie würden eine Million Inferenzen pro Tag ausführen.
Wenn Sie On-Device-KI auf dem Gerät des Nutzers statt in der Cloud ausführen, können Sie Hunderttausende Dollar sparen.
Der dritte Punkt ist die Offlineverfügbarkeit.
Anfang des Jahres haben wir eine Umfrage durchgeführt. 62 % der befragten Entwickler gaben an, dass die Offline-Verfügbarkeit einer der drei wichtigsten Gründe für die Verwendung von On-Device-KI ist.
Egal,ob sich Ihre Nutzer in 10.000 Metern Höhe oder im Untergeschoss des Bahnhofs Tokio befinden und keinen Internetzugang haben, sie können alle diese Funktionen nutzen.
Und schließlich war der Datenschutz einer der wichtigsten Gründe, die wir bei der Befragung von Entwicklern gesehen haben.
72 % der Befragten nannten es also als einen der drei wichtigsten Gründe.
40 % haben es auf Platz 1 gesetzt.
Wenn es also um Nachrichten, Sprachnotizen, Fotos oder sogar Finanz- oder medizinische Informationen geht, möchten Nutzer diese privat halten. Daher ist die Ausführung von Inferenz auf dem Gerät für sie sehr wertvoll.
Angesichts all dieser Vorteile haben wir ML Kit entwickelt, damit Sie alle diese Funktionen nutzen können.
Wir versuchen, das so einfach wie möglich zu machen.
Wenn Sie also Beschwerden haben, können Sie sich jederzeit an mich wenden.
Am Ende dieses Vortrags gebe ich Ihnen meine Kontaktdaten.
Wir würden gern herausfinden, wie wir Ihre Anwendungsfälle ermöglichen können.
Zu Beginn haben wir eine Reihe von Funktionen für Sprach- und Kreativanwendungen aktiviert.
Zum Beispiel Korrekturlesen, Umschreiben und flexible multimodale Prompts.
Unser Ziel ist es, die Nutzung so einfach wie möglich zu gestalten.
Deshalb haben viele unserer Nutzer und Entwickler bereits damit begonnen, diese Funktion zu verwenden.
Wir haben bereits 9 Drittanbieter-Apps mit 10 Millionen Downloads, die diese GenAI-Funktionen in der Produktion verwenden.
Wir sehen auch eine viel stärkere Skalierung sowohl bei unseren eigenen Apps als auch bei Drittanbieter-Apps und ein Wachstum von 110 % im Quartalsvergleich bei App-Starts für KI-Produktionsfunktionen.
Wir empfehlen Ihnen daher, die verfügbaren APIs für Messaging, Creatives und Finanzen auszuprobieren.
Wir glauben, dass die Möglichkeiten endlos sind, und Sie haben wahrscheinlich eine bessere Vorstellungskraft als wir, was viele dieser Anwendungsfälle angeht. Deshalb würden wir uns freuen, mit Ihnen zusammenzuarbeiten und herauszufinden, wie wir gemeinsam etwas erreichen können.
Sie kennen jetzt die allgemeinen Vorteile.
Sie fragen sich vielleicht, wie die Reichweite tatsächlich ist.
Wie ist die Leistung auf dem Gerät?
Wie schnell ist es wirklich?
Oder die Qualität des Modells selbst, richtig?
Wir gehen sie nacheinander durch.
In Sachen Reichweite sind wir bereits auf 175 Millionen AICore-Geräten verfügbar.
Wir gehen davon aus, dass sich diese Zahl im kommenden Jahr deutlich erhöhen wird und Hunderte Millionen weitere Geräte hinzukommen.
Das ist unter anderem möglich, weil wir ein sehr einfaches Harness entwickelt haben.
Sie müssen also nur die ML Kit API einbinden.
AICore ist sehr komplex, um die Sicherheits-Signale für Ein- und Ausgaben zu verwalten, das Modell zusammen mit den Gewichtungen herunterzuladen und es in der Laufzeit einzurichten.
Unser Ziel ist es, ML Kit so nutzerfreundlich wie möglich zu gestalten, damit Sie sich darauf konzentrieren können, eine großartige Nutzererfahrung zu bieten, und wir uns um den Rest kümmern können.
David Chou
Außerdem haben wir den RAM-Bedarf für Gemini Nano, das Hauptmodell für viele dieser GenAI-APIs, erheblich reduziert.
Wir haben die RAM-Nutzung von 3,6 GB auf 1,1 GB reduziert.
Wir haben vier wesentliche Verbesserungen vorgenommen, um dies zu ermöglichen.
Die erste ist die Verwendung einer 2-Bit-Quantisierung anstelle einer 4-Bit-Quantisierung.
Die Hälfte der Bits bedeutet also nur die Hälfte der Bytes, die Sie durch das Modell leiten müssen.
Die zweite Möglichkeit ist die Verwendung eines freigegebenen KV-Caches.
Wir haben dies erheblich reduziert, indem wir den ersten 20 Ebenen des Modells erlaubt haben, diese im 35-MB-Paket zu teilen. Das bedeutet, dass Sie trotz dieser sehr kleinen Paketgröße weiterhin auf die Kontextgröße von 16.000 Tokens zugreifen können.
Der dritte Schritt ist die Aktivierung von Lazy Vision- und Audio-Encodern.
Für viele der Textanwendungsfälle, die wir sehen, müssen Sie diese also gar nicht erst laden.
Dadurch sparen Sie ein weiteres halbes Gigabyte RAM.
Und schließlich haben wir zur Verbesserung der Akkulaufzeit auch die NPU-Taktskalierung aktiviert.
Das bedeutet, dass die NPU so schnell arbeiten kann, wie es nötig ist, um Energie zu sparen, und gleichzeitig eine hohe Qualität für deine Anwendungsfälle liefert.
Wir haben noch einige weitere Verbesserungen vorgenommen, um die Geschwindigkeit zu erhöhen.
Die erste ist die Aktivierung des Präfix-Caching.
In diesem Beispiel wird der System-Prompt für mehrere Turns wiederverwendet.
Die Verarbeitung wird dadurch sehr teuer.
Mit Präfix-Caching müssen Sie diesen System-Prompt nicht immer wieder neu verarbeiten, oder?
Sie können diese im Präfix-Cache speichern. In diesem Beispiel sparen Sie 60 % der Tokens, die Sie übergeben müssen, was auch eine 60 %ige Beschleunigung auf dem jeweiligen Gerät bedeutet.
Die zweite Verbesserung betrifft die spekulative Decodierung.
In diesem Beispiel generiert Gemini Nano ein Token pro Durchlauf.
Das liegt daran, dass LLMs autoregressiv sind. Sie müssen also immer die zuvor generierten Tokens berücksichtigen, um die nächsten Tokens zu bestimmen.
Wir haben das Problem gelöst, indem wir ein Drafter-Modell entwickelt haben, das eine Reihe von Tokens viel schneller generiert.
Dieses Drafter-Modell ist wesentlich kleiner als Gemini Nano und kann daher eine Reihe von Tokens auf einmal generieren und an Gemini weitergeben, um zu sehen, welche davon tatsächlich mit den Vermutungen von Gemini Nano übereinstimmen.
In diesem Beispiel werden im ersten Durchlauf sechs Tokens generiert, im nächsten fünf und im dritten ist die Generierung abgeschlossen.
Wir haben festgestellt, dass dies bei vielen der von uns untersuchten internen Anwendungsfälle zu einer durchschnittlich zweifachen Beschleunigung führt.
Je nach Hardware können Sie möglicherweise schneller Verbesserungen erzielen.
Okay.
Das ist also der Grund, warum man, wenn man auf die Geschwindigkeit zurückkommt, diesen dramatischen Geschwindigkeitsunterschied zwischen On-Device und Cloud sogar in einem 5G-Netzwerk sehen kann.
Sie sehen, dass allein das Hochladen dieser Daten in die Cloud etwa eine Sekunde dauert, und zwar für 1.300 Eingabetokens. Auch wenn das Vorbefüllen und Decodieren in der Cloud schneller sein mag, ist die End-to-End-Verarbeitung auf dem Gerät immer noch wesentlich schneller.
Und was die Qualität angeht, freuen wir uns, Ihnen mitteilen zu können, dass dieses 2B-Modell tatsächlich mit den 27B-Modellen des letzten Jahres mithalten kann.
Sie sind zwar nicht so leistungsfähig wie die heutigen Frontier-Modelle, aber wir zeigen hier, dass es bei der KI auf diesen On-Device-Modellen erhebliche Fortschritte gibt.
Sie sehen, dass das 2B-Modell bei MMLU-Pro, einem Standard-Benchmark für Wissen, fast so gut ist wie das 27B-Parametermodell vom letzten Jahr.
Und bei Benchmarks für wissenschaftliche Aufgaben auf Hochschulniveau, logisches Denken und die Nutzung agentischer Tools ist dieses 2B-Modell sogar besser, obwohl es weniger als ein Zehntel der Größe des letztjährigen Modells hat.
Wir gehen davon aus, dass sich diese Trends fortsetzen werden.
Wir arbeiten weiterhin eng mit dem GDM-Team zusammen, um diese Verbesserungen am Modell vorzunehmen, damit Sie den größtmöglichen Nutzen daraus ziehen können.
David Chou
Okay.
Ich habe Ihnen jetzt viele Vorteile und neue Verbesserungen vorgestellt. Vielleicht fragen Sie sich: „Was können wir mit diesen Modellen eigentlich machen?“
Und so haben wir auch in der API all diese Verbesserungen vorgenommen.
Mit Systemanweisungen können Sie eine Persona festlegen.
Mit der Eingabe mehrerer Bilder können Sie mehrere Bilder gleichzeitig hochladen.
Anfang des Jahres haben wir auch den Denkmodus aktiviert, damit Sie mehr Zeit mit logischen Problemen und Toolaufrufen verbringen können.
Und schließlich haben wir mit der strukturierten Ausgabe auch dafür gesorgt, dass Sie eine Kotlin-Klasse annotieren und ein Objekt zurückerhalten können, das Ihrem spezifischen Schema entspricht.
Darüber hinaus fragen wir Entwickler auch nach neuen Verbesserungen, die sie gerne vornehmen würden.
91 % der Entwickler, mit denen wir gesprochen haben, gaben an, dass sie ein Erlebnis veröffentlichen würden, wenn eine dieser Funktionen verfügbar wäre.
Anfang dieser Woche haben wir diese vier Funktionen eingeführt: Sprachübersetzung, Einbettungen, Text-zu-Sprache und Funktionsaufrufe.
Mit der Sprachübersetzung können Sie Audioinhalte aus über 20 Sprachen transkribieren.
Mit Einbettungen können Sie die Inhalte Ihrer App nach semantischer Bedeutung durchsuchen, anstatt nur die Keyword-Suche zu verwenden.
Mit Text-to-Speech können Sie jetzt menschenähnliche Stimmen für Ihre Anwendungen bereitstellen, sodass Sie keine Roboterstimmen mehr verwenden müssen.
Und schließlich können Sie mit Funktionsaufrufen Ihre App im Namen Ihrer Nutzer Aktionen ausführen lassen.
Wir arbeiten bereits mit einigen Entwicklern zusammen, um zu sehen, wie sie In-App-Agenten in ihre Produkte einbinden können. Wir hoffen, dass wir diese Funktion im Laufe des nächsten Jahres einführen können.
David Chou
Sehen wir uns nun ein Beispiel an, in dem wir alle diese Funktionen nutzen.
Stellen wir uns einen amerikanischen Fotografen vor, der in Japan unterwegs ist.
Und wie Sie auf dem Foto sehen können, haben sie eindeutig nicht genug Kameras oder Objektive.
Also bestellt er ein neues Objektiv und lässt es sich ins Hotel liefern.
Aber es gibt einen Haken: Im Laufe des Tages erhält er eine Voicemail, in der ihm mitgeteilt wird, dass er eine Zollgebühr von 6.800 Yen zahlen muss, um sein Paket noch am selben Tag zu erhalten.
Sie hat auch eine Nachricht mit einem Foto des zurückgehaltenen Pakets erhalten.
In diesem Fall ist der Nutzer also an einer Art Betrugserkennung interessiert, richtig?
Sie möchten wissen, wie sie legitime Mailboxnachrichten von betrügerischen unterscheiden können.
Wir möchten alle Informationen privat halten und sicherstellen, dass die Informationen fundiert, aber auch erklärbar sind.
Wir möchten also sicherstellen, dass sie den japanischen Text verstehen und alle verfügbaren Sprachfunktionen nutzen können, falls sie Barrierefreiheit im Internet benötigen.
Es gibt also vier Schritte, richtig?
Im ersten Schritt wird die Mailboxnachricht in Text umgewandelt.
Dafür verwenden wir die Speech Translation API.
Und dann können sie eine Frage stellen, wenn sie möchten. Dafür verwenden wir die Speech Recognition API.
Anschließend definieren wir Funktionen, die die Informationen abrufen können, die der Nutzer benötigt.
Wir verwenden also die Prompt API und Funktionsaufrufe.
Dies wird im Hintergrund ausgeführt, ohne dass der Nutzer etwas sieht.
Drittens finden wir heraus, wie wir diese Informationen mithilfe der Embedding API und AppSearch speichern und abrufen können.
Dies wird auch im Hintergrund ausgeführt.
Anschließend fällen wir ein Urteil auf Grundlage der Informationen, die wir mithilfe der Prompt API, der strukturierten Ausgabe und der Sprachsynthese erfassen.
Okay.
Fangen wir an.
Wir verwenden Sprachübersetzung und Spracherkennung.
Der erste Schritt besteht also darin, die Mailboxnachricht und den Text in nützliche Informationen umzuwandeln, richtig?
Der Nutzer hat also die Nachricht erhalten: „Hey, du musst diese Gebühr von 6.800 Yen bezahlen.
Zölle wurden möglicherweise nicht im Voraus bezahlt, sodass du sie bezahlen musst.“
Und sie möchten diese Übersetzung haben, oder?
Wir möchten hier darauf hinweisen, dass sowohl die Übersetzung als auch die Frage des Nutzers zu den Informationen an Gemini Nano 4 gesendet werden, wobei derselbe Audio-Encoder verwendet wird, da der Audio-Encoder mit über 100 Sprachen trainiert wurde.
So erhalten Sie die Übersetzung und die automatische Spracherkennung für den Text der Nutzerfrage.
Für die Übersetzung müssen wir also nur eine Ausgangs- und eine Zielsprache festlegen.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)In diesem Fall verwenden wir sowohl Japanisch als auch amerikanisches Englisch.
Wir richten einen Kotlin-Flow ein, der die Byte direkt an den Sprachübersetzer übergibt.
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
}
}Anschließend können wir die Antwort mit dem übersetzten Text erfassen.
Okay.
Sehr gut.
Jetzt wissen wir also, was in der Mailboxnachricht gesagt wird, oder?
Es gibt ein Paket mit einem Kameraobjektiv.
Es wird beim Zoll festgehalten.
Sie müssen die Gebühr von 6.800 Yen bezahlen.
Und jetzt möchte der Nutzer eine Frage dazu stellen.
Okay.
Ist diese Zollabfertigung also echt?
In diesem Fall verwenden wir die ASR API.
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)Wir legen weiterhin US-amerikanisches Englisch als Sprache fest.
Dann können wir den erweiterten Modus festlegen, der den Audio-Encoder nutzt.
Diese Informationen können wir dann ebenfalls übergeben, die endgültige Antwort erfassen und in Informationen umwandeln, die wir an Gemini Nano übergeben.
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)
}
}Okay.
Wir haben jetzt den ersten Teil der Aufnahme abgeschlossen.
Wie Sie sehen, waren dafür nicht viele Codezeilen erforderlich.
David Chou
Der zweite Teil ist die Triage. Hier geht es darum, wie die Prompt API und Funktionsaufrufe genutzt werden können, um herauszufinden, welche Art von Informationen benötigt wird.
Okay.
Um die Architektur dafür zu erstellen, richten wir zuerst einige Systemanweisungen ein.
Außerdem werden die Mailboxnachricht, die SMS, das Foto und die Frage des Nutzers als Eingabe an Gemini Nano übergeben.
Anschließend definieren wir Funktionen, die die benötigten Informationen abrufen.
retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)Zuerst richten wir die Systemanweisungen ein.
// 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()
)Hier sehen Sie ein sehr einfaches Beispiel dafür, wie die Einrichtung eines Sicherheitsauditors aussehen würde. Es wird angegeben, dass Verpackungen und Etiketten überprüft, benutzerdefinierte Gebühren geprüft und alle erforderlichen Zahlungsforderungen gekennzeichnet werden.
Möglicherweise stellen Sie fest, dass die Prompt-Optimierung hier sehr hilfreich ist, um eine deutlich bessere Qualität zu erzielen.
Sobald wir diesen System-Befehlssatz festgelegt haben, können wir alle Inhalte aus dem vorherigen Schritt, die Übersetzung der Mailbox und des Textes sowie die Frage des Nutzers, zusammen mit den Systemanweisungen in dieses TextPart-Feld übergeben, um die gewünschten Informationen zu erhalten.
// 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 */ }Dann definieren wir eine Funktion, die wir verwenden können.
In diesem Beispiel haben wir die App-Funktion integriert.
Wenn Sie damit vertraut sind, verstehen Sie wahrscheinlich schon ungefähr, wie das funktioniert.
Hier richten wir einen AppFunctionService-Einstiegspunkt ein und definieren dann eine App-Funktion.
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}Das funktioniert genauso wie das Einrichten von App-Funktionen für einen System-Agenten.
Sie legen in den Parametern von searchQuery die Gebühr für die Schadensmeldung fest und auch, ob wir Paketfotos vergleichen sollen. Das sind nur einige Beispielparameter, richtig?
// 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) }
}Sie können auch andere definieren.
Und schließlich haben wir für diese Funktion eine executeVaultSearch-Funktion, die wir später definieren werden.
Ich möchte hier noch erwähnen, dass der Denkmodus verfügbar ist.
val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()Ich empfehle dringend, ihn bei Toolaufrufen zu verwenden.
Der Grund dafür ist, dass wir eine Reihe von Tests durchgeführt haben, die zeigen, dass sich die Accuracy bei Toolaufrufen auf Gemini Nano 4 um 13 Punkte von 100 verbessert.
In einigen Fällen ist sie sogar wesentlich höher, was zu viel besseren Ergebnissen führen kann.
Wenn Sie mehr über Funktionsaufrufe erfahren möchten, besuchen Sie die Session meiner Kolleginnen Jolanda und Victoria, die später stattfindet.
In diesem Video geht es um agentische Funktionen in Ihrer App.
Ich glaube, Victoria sitzt irgendwo im Publikum. Sagt ihr also auch gerne Hallo.
David Chou
Okay, cool.
Wir haben jetzt die ersten beiden Schritte abgeschlossen.
Sehen wir uns an, wie der Abruf funktioniert.
Wir verwenden also Einbettungen und AppSearch.
Gehen wir also ein paar Schritte zurück, d. h. zu dem Punkt, an dem die Nutzer die Linse bestellt haben. Wir möchten diese Informationen indexieren, damit sie sowohl für die Nutzer als auch für die Entwickler zugänglich sind, richtig?
Hier finden Sie Informationen zur Paketnummer der Bestellung.
Sie hat auch eine Sendungsnummer.
Es enthält einige Details dazu, wohin es gesendet wird.
Hier wird bereits erwähnt, dass die Zölle im Voraus bezahlt wurden.
Wir generieren also eine multimodale Einbettung, um diese Informationen zusammen mit allen anderen Informationen zu indexieren, die für uns wichtig sein könnten.
So sieht das aus.
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.vectorWir nehmen die E-Mail-Adresse aus der Bestellung.
Auch hier ist es sehr einfach: Wir übergeben nur ein Bild und einen Text, aber Sie haben möglicherweise mehr Inhalte, die Sie hier indexieren möchten.
Anschließend generieren wir den Einbettungsvektor.
Danach erstellen wir mit AppSearch ein Dokument und legen die Attribute fest, die wir für die Erstellung dieses Dokuments für erforderlich halten.
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()Ein wichtiger Punkt: Der Grund, warum wir multimodale Einbettungen verwenden, ist, dass es hier möglicherweise keine gemeinsamen Wörter gibt. Da die Bilder und der Text jedoch ähnliche Inhalte haben, können wir so die semantische Ähnlichkeit ermitteln.
Mit der Kosinusähnlichkeit können wir also Elemente finden, die keine gemeinsamen Wörter haben.
Das ist die executeVaultSearch-Funktion, über die wir vorhin gesprochen haben.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}Diese wird von der von uns definierten App-Funktion aufgerufen.
In diesem Beispiel übergeben wir die Suchanfrage, die von Gemini Nano generiert wurde.
Anschließend verwenden wir eine Ranking-Strategie, die Kosinusähnlichkeit.
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()
}Und this.matchedSemanticScores hilft uns dann, diese Informationen zu erhalten.
Schließlich geben wir einen Schwellenwert von 0,68 an, um die minimale Kosinusähnlichkeit zu bestimmen, die erforderlich ist, damit wir Informationen abrufen.
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() }
}Sie können also mit diesen Optionen experimentieren, um herauszufinden, was für Ihre Anwendungsfälle am besten funktioniert.
Ein wichtiger Punkt, den wir hier erwähnen möchten, ist die Sicherheit dieses Modells. Gemini Nano führt den Code nicht aus, oder?
Gemini Nano stellt also nur eine Frage, aber Ihre App definiert den Code und führt ihn dann auch aus, ruft die Informationen ab und bestimmt, was an Gemini Nano übergeben werden soll.
So haben Sie mehr Kontrolle darüber, was Sie Gemini Nano zugänglich machen möchten, und können gleichzeitig für mehr Datenschutz und Sicherheit für Ihre Nutzer sorgen.
David Chou
Okay, jetzt kommt der letzte Schritt.
Wir haben also alle Informationen, die wir brauchen.
Wir verwenden die Prompt API, die strukturierte Ausgabe und die Sprachsynthese, um die Vergleiche durchzuführen und die Informationen dann auf verständliche Weise an den Nutzer zurückzugeben.
Im Grunde wollen wir also wissen, ob dieses Paket tatsächlich mit den Informationen des Nutzers übereinstimmt, also der echten Bestellung.
Dazu generieren wir auch eine Einbettung für die Informationen aus der Mailboxnachricht und dem Text und wandeln sie in denselben Einbettungsvektor um, den wir zuvor erstellt haben.
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)Anschließend verwenden wir matchScore, um mithilfe der Kosinusähnlichkeit einen Vergleich zwischen der Übereinstimmung, die wir aus dem Vault abgerufen haben, und den Informationen des Anrufers anzustellen.
Wie Sie hier sehen, ist der Text auch dann sehr hilfreich, wenn die Bilder nicht übereinstimmen.
Hier sind multimodale Einbettungen also sehr nützlich.
Sie können verschiedene Informationen eingeben und dann eine gemeinsame semantische Bedeutung erhalten, oder?
Mit der multimodalen Einbettung können Sie also alles demselben Vektorraum zuordnen.
Auch hier verwenden wir den Thinking-Modus, um unsere Überlegungen zu verbessern.
val forensicRequest = generateContentRequest(
securitySystemInstruction,
content {
image(callerParcelPhotoBmp)
image(orderEmailLensBmp)
text(forensicAuditPrompt)
}
) {
// Enable Gemini Nano 4 reasoning before it answers
enableThinking = true
}Außerdem definieren wir eine Ausgabeklasse.
@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,
)Wir richten hier also nur eine VisualContradiction und ein paar verschiedene Enums für die verschiedenen Arten von Widersprüchen ein, die wir haben könnten, richtig?
Egal, ob es sich um DUTY_FRAUD, PACKAGING_MISMATCH oder STATUS_CONFLICT handelt.
Und dann haben wir hier eine Erklärung.
Anschließend werden alle diese Informationen in ein Audit-Urteil einfließen, richtig?
@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,
)Wie Sie sehen, ist das sehr einfach.
Wir wollen nur wissen, ob es sich um einen Betrug handelt oder nicht.
Vielleicht haben wir ein fraudRiskScore, bei dem Sie sowohl einen Mindest- als auch einen Höchstwert von 0 bis 100 festlegen können.
Sie können den Nutzern sogar mitteilen, wie viel Geld sie gespart haben, wenn Sie das im Laufe der Zeit im Blick behalten möchten.
Und dann können Sie auch die visuellen Widersprüche auflisten, die wir gerade definiert haben.
Beachten Sie auch, dass die Annotation @Guide optional ist.
Sie müssen sie nicht unbedingt festlegen und eine solche Beschreibung angeben.
Das ist hilfreich, weil Sie die Vorgänge möglicherweise nicht allein anhand der Variablendefinition erklären können.
Diese Beschreibung kann Gemini Nano dabei helfen, herauszufinden, welche spezifischen Funktionen aufgerufen werden sollen und wie es eingerichtet werden muss, damit die richtigen Informationen in die richtigen Felder eingegeben werden.
Nach der Einrichtung ist es also sehr einfach.
Sie führen hier nur drei Codezeilen aus.
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().responsegenerateTypedContentRequest, Sie generieren diese Inhalte und können dann die Antwort abrufen.
Und hier ist ein Beispiel dafür, wie das aussehen würde.
{
"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."
}Wir bestätigen also, dass es sich um einen Betrug handelt, und erhalten eine sehr hohe Risikobewertung für Betrug. Das erklärt die Widersprüche, die in den von Ihnen abgerufenen Daten im Vergleich zu den Anruferinformationen offensichtlich sind.
Wir haben diesem Nutzer auch einen bestimmten Geldbetrag gespart, den wir hier im Auge behalten können.
Ja.
Damit sind alle Informationen abgedeckt, die wir benötigen.
Der letzte Schritt besteht darin, diese Informationen dem Nutzer auf zugängliche Weise zu präsentieren.
Wenn sie beispielsweise unterwegs sind oder eine Sehbehinderung haben, ist es möglicherweise besser, die Informationen vorzulesen, anstatt sie den Text lesen zu lassen.
Wir implementieren also die Text-zu-Sprache-Funktion, um zu zeigen, wie das aussehen würde.
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
}Auch das ist ganz einfach: Wir geben eine Sprache und eine Region an und übergeben dann das Urteil, das wir gerade mit @Generable und @Guide generiert haben.
Und wenn wir das haben, geht es fast sofort.
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
}Auf den meisten Geräten dauert es weniger als 120 ms, bis diese Antwort generiert wird.
Wir löschen alle erforderlichen gemerkten Informationen.
Bei Bedarf können wir auch ein Fallback festlegen, indem wir Plattform-APIs verwenden.
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 ...Die Stimme wäre zwar immer noch roboterhaft, aber zumindest gäbe es etwas, mit dem der Nutzer interagieren könnte, falls er nicht lesen kann.
David Chou
Okay.
Das war eine Menge, aber fassen wir noch einmal zusammen, was wir getan haben: Wir haben all diese verschiedenen APIs für die Aufnahme, die Triage, den Abruf und die Prüfung genutzt.
Dazu gehören Spracherkennungs- und Übersetzungs-APIs, die Prompt API und Funktionsaufrufe, Einbettungen und AppSearch sowie strukturierte Ausgaben und Sprachsynthese.
Dies ist nur ein Beispiel dafür, wie Sie all diese Funktionen auf kohärente Weise nutzen und vollständig auf dem Gerät ausführen können.
Und wenn wir auf das zurückkommen, worüber wir am Anfang gesprochen haben, gibt es erhebliche Vorteile, wenn man das macht, oder?
Wenn Sie dies heute auf Ihrem Gerät ausführen würden, würden Sie feststellen, dass dies auf den meisten Geräten wesentlich schneller ist.
Es kostet Sie nichts und Sie können potenziell Hunderttausende von Dollar sparen, wenn Sie das Tool ständig ausführen.
Und das ist dann verfügbar, egal ob der Nutzer in den Bergen oder in einem Keller ist und keinen Internetzugang hat.
Und es ist absolut privat, da keine Informationen an die Cloud gesendet werden müssen.
Angesichts all dieser Funktionen können Sie sich gerne die Dokumente ansehen, den QR-Code scannen oder den Link aufrufen.
Das sind alle Funktionen, die wir in den letzten sechs Monaten eingeführt haben.
Vielen Dank, dass Sie sich die Zeit genommen haben, an diesem Vortrag teilzunehmen.
Vielen Dank, dass Sie sich die Zeit genommen haben.
Vielen Dank.
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 ...