Nesta sessão, vamos analisar diferentes casos de uso da GenAI usando as APIs do Kit de ML para entender quando a GenAI no dispositivo é ideal para um app e como avaliar os casos de uso atuais da GenAI para conferir se os modelos no dispositivo atendem aos requisitos de qualidade.
Transcrição
David Chou
Ei,
Boa tarde, pessoal.
Meu nome é David Chou.
Sou líder de produto no Android AICore.
É um prazer falar com você sobre nossos recursos de IA no dispositivo.
Hoje, vamos falar sobre algumas áreas principais.
A primeira é pensar por que você deve usar a IA no dispositivo, especialmente considerando a aceleração do progresso da IA na nuvem.
Em segundo lugar, pense em algumas das melhorias que fizemos no último ano para tornar esses modelos de IA no dispositivo extremamente potentes.
Por fim, vamos falar sobre um exemplo completo que mostra como aproveitar todos esses benefícios usando a IA no dispositivo.
Para começar, por que usar a IA no dispositivo?
Há quatro razões principais.
O primeiro é a velocidade, o segundo é o custo, o terceiro é a disponibilidade e o quarto é a privacidade.
Então, quando se trata de velocidade, a inferência de IA no dispositivo está chegando ao ponto de ser mais rápida do que na nuvem, simplesmente porque não é necessário transmitir informações entre o dispositivo do usuário e a nuvem.
Em muitos dos casos de uso que testamos, estamos vendo uma velocidade duas vezes maior ao comparar a IA no dispositivo com a nuvem.
A segunda é pensar no custo.
Imagine que você precise executar um milhão de inferências por dia.
Você pode economizar centenas de milhares de dólares se executar a IA no dispositivo, especificamente no dispositivo do usuário, em vez de executá-la na nuvem.
E o terceiro é a disponibilidade off-line.
Por isso, enviamos uma pesquisa no início deste ano, e 62% dos desenvolvedores com quem conversamos disseram que a disponibilidade off-line era um dos três principais motivos para usar a IA no dispositivo.
Assim,mesmo que o usuário esteja a 10 mil metros de altura ou no subsolo da estação de Tóquio sem acesso à Internet, ele ainda poderá aproveitar todos esses recursos.
Por fim, a privacidade foi um dos principais motivos que identificamos ao entrevistar desenvolvedores.
Então, 72% classificaram isso como um dos três principais motivos.
40% o classificaram como número um.
Portanto, quando se trata de mensagens, notas de voz, fotos ou até mesmo informações financeiras ou médicas, os usuários querem manter a privacidade. Por isso, executar a inferência no dispositivo será muito útil para eles.
Considerando todos esses benefícios, criamos o Kit de ML para que você possa manter e aproveitar todos esses recursos.
Estamos tentando simplificar ao máximo.
Se tiver alguma reclamação, entre em contato comigo.
Vou deixar meus dados de contato no final desta conversa.
Queremos descobrir como podemos viabilizar seus casos de uso.
Para começar, ativamos vários recursos para casos de uso de fala e criatividade.
Como revisão, reescrita e comandos multimodais flexíveis.
Por isso, nosso objetivo é tornar o recurso o mais útil possível.
É por isso que muitos dos nossos usuários e desenvolvedores já começaram a adotar essa abordagem.
Já temos nove apps de terceiros com 10 milhões de downloads que usam esses recursos de IA generativa em produção.
E estamos vendo um aumento muito maior em nossos apps próprios e apps de terceiros, além de um crescimento de 110% QoQ nos lançamentos de apps para recursos de produção de IA.
Por isso, em mensagens, criativos e finanças, recomendamos que você teste as APIs disponíveis.
As possibilidades são infinitas, e você provavelmente tem uma imaginação melhor do que a nossa em muitos desses casos de uso. Por isso, gostaríamos de fazer uma parceria e descobrir como podemos trabalhar juntos.
Agora você entende os benefícios gerais.
Você pode estar se perguntando: "Como é o alcance?"
Como é o desempenho no dispositivo?
Quando falamos de velocidade, qual é o desempenho real?
Ou como a qualidade do próprio modelo, certo?
Vamos analisar cada uma delas.
Em termos de alcance, já estamos disponíveis em 175 milhões de dispositivos AICore.
Esperamos que esse número aumente significativamente no próximo ano, com a inclusão de centenas de milhões de outros dispositivos.
Parte do que torna isso possível é que criamos um harness muito simples.
Portanto, a API Kit de ML é tudo o que você precisa integrar.
Há muita complexidade integrada ao AICore para gerenciar os sinais de segurança de entrada e saída, o download do modelo com os pesos e a configuração no ambiente de execução.
O objetivo é tornar o Kit de ML tão fácil de usar que você só precisa se preocupar em oferecer uma ótima experiência, e nós cuidamos do resto.
David Chou
Também temos o prazer de anunciar que reduzimos significativamente a RAM necessária para executar o Gemini Nano, que é o modelo principal que alimenta muitas dessas APIs de IA generativa.
Por isso, reduzimos o uso de RAM de 3,6 GB para 1,1 GB.
Para que isso fosse possível, fizemos quatro melhorias principais.
A primeira é usar a quantização de 2 bits em vez de 4 bits.
Portanto, metade dos bits significa metade dos bytes que você precisa canalizar pelo modelo.
A segunda é usar um cache de chave-valor compartilhado.
Por isso, reduzimos significativamente esse valor, permitindo que as primeiras 20 camadas do modelo compartilhem isso no pacote de 35 MB. Isso significa que você ainda pode acessar o tamanho de contexto de 16 mil tokens em um pacote muito, muito pequeno.
A terceira é ativar codificadores de visão e áudio lentos.
E, para muitos dos casos de uso de texto que estamos vendo, você não precisa carregar esses dados para começar.
Isso vai economizar mais meio gigabyte de RAM.
Por fim, para melhorar a duração da bateria, também ativamos o escalonamento de clock da NPU.
Isso significa que a NPU pode funcionar na velocidade necessária para economizar energia, sem deixar de oferecer alta qualidade nos seus casos de uso.
Fizemos algumas outras melhorias em relação à aceleração.
A primeira é ativar o armazenamento em cache de prefixos.
Neste exemplo, o comando do sistema é reutilizado em vários turnos, certo?
Por isso, o processamento se torna muito caro.
Mas com o armazenamento em cache de prefixos, não é preciso reprocessar esse comando do sistema repetidamente, certo?
Você pode salvar isso no cache de prefixo, o que significa que, neste exemplo, você está economizando 60% dos tokens que precisa transmitir, o que também significa uma aceleração de 60% em qualquer dispositivo que você esteja usando.
A segunda melhoria que fizemos foi com a decodificação especulativa.
Neste exemplo, o Gemini Nano gera um token por passagem.
Isso acontece porque os LLMs são autorregressivos, ou seja, eles precisam continuar analisando os tokens anteriores que foram gerados para descobrir os próximos.
Para lidar com isso, criamos um modelo de rascunho que gera um monte de tokens em uma velocidade muito maior.
Esse modelo de rascunho é bem menor que o Gemini Nano e, por isso, pode gerar vários tokens de uma vez, passar para o Gemini e ver quais deles realmente se alinham com as suposições do Gemini Nano.
Neste exemplo, na primeira passagem, ele gera seis tokens, na próxima, cinco, e, na terceira, ele já terminou.
E o que temos visto é que, para muitos dos casos de uso internos que estamos analisando, isso está levando a uma aceleração média de 2x.
Dependendo do hardware, as melhorias podem ser mais rápidas.
Ok.
Por isso, voltando à velocidade, é possível notar essa aceleração drástica entre o dispositivo e a nuvem, mesmo em uma rede 5G.
O upload desses dados para a nuvem leva apenas um segundo, certo, para 1.000 ou 1.300 tokens de entrada. Portanto, mesmo que o preenchimento prévio e a decodificação em uma Cloud Journey sejam mais rápidos, a jornada completa ainda é substancialmente mais rápida quando você executa no dispositivo.
E, quando se trata de qualidade, temos o prazer de anunciar que esse modelo 2B está realmente se saindo melhor do que os modelos 27B do ano passado.
Embora não tenha o mesmo desempenho dos modelos de fronteira que temos hoje, o que estamos mostrando aqui é que há um progresso substancial quando se trata de IA nesses modelos no dispositivo, certo?
E, para o MMLU-Pro, que é um comparativo padrão de conhecimento, o modelo 2B é quase tão bom quanto o modelo de 27B parâmetros do ano passado.
E quando se trata de ciência de pós-graduação, raciocínio e benchmarks de uso de ferramentas agênticas, esse modelo 2B é realmente melhor, mesmo que tenha menos de um décimo do tamanho do modelo do ano passado.
Por isso, esperamos que essas tendências continuem.
Continuamos trabalhando em estreita colaboração com a equipe do GDM para garantir que possamos continuar fazendo essas melhorias no modelo para que você possa obter o melhor valor delas.
David Chou
Ok.
Acabei de apresentar muitos benefícios e melhorias, e talvez você esteja se perguntando: "O que podemos realmente fazer com esses modelos?"
E também estamos fazendo todas essas melhorias de recursos na API.
Com as instruções do sistema, você pode definir uma persona.
Com a entrada de várias imagens, você pode fazer upload de várias imagens de uma só vez.
Também ativamos o modo de pensamento no início deste ano, para que você possa passar mais tempo com problemas de raciocínio e chamadas de ferramentas.
Por fim, com a saída estruturada, também é possível anotar uma classe Kotlin e receber um objeto que corresponde ao seu esquema específico.
Além disso, também tentamos pedir aos desenvolvedores novas melhorias que eles gostariam de fazer.
Por isso, 91% dos desenvolvedores que entrevistamos disseram que lançariam uma experiência se um desses recursos estivesse disponível.
No início desta semana, lançamos estes quatro recursos: tradução de voz, embeddings, conversão de texto em voz e chamada de função.
Com a tradução simultânea, você pode transcrever áudio de mais de 20 idiomas.
Com os embeddings, é possível pesquisar o conteúdo do app por significado semântico, em vez de usar apenas a pesquisa por palavra-chave.
Com a conversão de texto em voz, agora você pode usar vozes semelhantes às humanas nas suas experiências, sem precisar de vozes robóticas.
Por fim, com a chamada de função, você pode permitir que o app realize ações em nome dos usuários.
Por isso, já estamos em parceria com alguns desenvolvedores para ver como eles podem criar agentes no app nas experiências deles, e esperamos lançar isso no final do ano que vem.
David Chou
Considerando todos esses recursos, vamos analisar um exemplo completo em que criamos com todos eles.
Imagine que um fotógrafo americano está viajando pelo Japão.
E, como você pode ver na foto, eles claramente não têm câmeras ou lentes suficientes.
Por isso, ela compra uma nova lente e pede para entregar no hotel.
Mas tem um porém: durante o dia, eles recebem uma mensagem de voz informando que precisam pagar uma taxa de liberação de 6.800 ienes para receber o pacote até o fim do dia.
Eles também receberam uma mensagem de texto com uma foto do pacote retido.
Nesse caso, o usuário quer ter algum tipo de proteção de Detecção de Golpe, certo?
Ela quer entender como identificar se as mensagens de voz são legítimas ou fraudulentas.
Por isso, queremos manter todas as informações privadas, garantir que elas sejam fundamentadas e também explicáveis.
Por isso, vamos garantir que eles entendam o texto em japonês e também possam usar os recursos de idioma disponíveis, caso tenham necessidades de acessibilidade.
Então, serão quatro etapas aqui, certo?
A primeira etapa é processar o correio de voz em texto, ou seja, traduzir o correio de voz.
Vamos usar a API Speech Translation para isso.
E, se quiserem, poderão fazer uma pergunta. Para isso, vamos usar a API Speech Recognition.
Depois, queremos definir funções que possam recuperar as informações necessárias para o usuário conseguir o que precisa.
Por isso, vamos usar a API Prompt e a chamada de função.
Agora, isso será executado em segundo plano sem que nada fique visível para o usuário.
Por fim, vamos descobrir como armazenar e recuperar essas informações usando a API de embedding e o AppSearch.
Isso também será executado em segundo plano.
Por fim, vamos dar um veredito com base nas informações que coletamos, usando a API Prompt, a saída estruturada e a síntese de fala.
Ok.
Vamos começar.
Vamos usar a tradução e o reconhecimento de fala.
A primeira etapa é pegar o correio de voz e o texto e transformá-los em informações úteis, certo?
O usuário recebeu esta mensagem: "Você precisa pagar uma taxa de 6.800 ienes.
Os tributos podem não ter sido pré-pagos, então você precisa pagar."
Por isso, eles querem ter essa tradução, certo?
Por isso, uma das coisas que gostaríamos de destacar aqui é que tanto a tradução quanto a pergunta do usuário sobre as informações vão para o Gemini Nano 4 usando o mesmo codificador de áudio, porque ele foi treinado em mais de 100 idiomas.
Assim, você pode receber essa tradução, além do ASR para o texto da pergunta do usuário.
Para a tradução, basta configurar um idioma de origem e um de destino.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)Neste caso, vamos usar japonês e inglês americano.
Vamos configurar um fluxo do Kotlin que transmite os bytes diretamente para o tradutor de voz.
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
}
}Em seguida, podemos coletar a resposta usando o texto traduzido.
Ok.
Ótimo.
Agora entendemos o que a mensagem de voz está dizendo, certo?
Há um pacote com lentes de câmera.
Está retido na alfândega.
Eles precisam pagar essa taxa de 6.800 ienes.
Agora, o usuário quer fazer uma pergunta sobre ele.
Ok.
Então, essa retenção alfandegária é real?
Nesse caso, vamos usar a API ASR.
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)O que estamos fazendo é definir o inglês americano como localidade.
Depois, podemos definir o modo avançado, que usa o codificador de áudio.
Depois, podemos transmitir essas informações, coletar a resposta final e transformá-la em informações que serão transmitidas ao 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)
}
}Ok.
Agora, terminamos a primeira parte da ingestão.
Como você pode ver, não foram necessárias muitas linhas de código.
David Chou
A segunda parte é a triagem, que consiste em pensar em como usar a API Prompt e a chamada de função para descobrir que tipo de informação é necessária.
Ok.
Para criar a arquitetura, vamos configurar algumas instruções do sistema primeiro.
Em seguida, vamos transmitir o correio de voz, o texto, a foto e a pergunta do usuário como entrada para o Gemini Nano.
Em seguida, vamos definir funções que recuperam as informações necessárias.
retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)Para começar, vamos configurar as instruções do sistema.
// 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()
)Aqui você pode ver um exemplo muito simples de como seria configurar um auditor de segurança. O que ele diz é que vamos verificar as embalagens e os rótulos, analisar as taxas personalizadas e sinalizar as demandas de pagamento necessárias.
A otimização de comandos pode ser muito útil para melhorar significativamente a qualidade.
Depois de definir o conjunto de instruções do sistema, podemos pegar todo o conteúdo da etapa anterior, a tradução do correio de voz e do texto, além da pergunta do usuário, e passar tudo isso para o campo TextPart, junto com as instruções do sistema para receber essas informações.
// 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 */ }Em seguida, queremos definir uma função que possamos usar.
Para este exemplo específico, temos o prazer de anunciar que fizemos a integração com a função do app.
Se você já conhece esse conceito, provavelmente entende como isso funciona.
Aqui, configuramos um ponto de entrada AppFunctionService e definimos uma função de app.
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}O processo é exatamente o mesmo que você seguiria para configurar as funções de um agente do sistema.
Você define nos parâmetros do searchQuery a taxa de sinistro e se devemos ou não comparar fotos de pacotes, como alguns exemplos de parâmetros, certo?
// 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) }
}Você pode definir outras.
Por fim, para essa função, temos uma função executeVaultSearch que vamos definir mais tarde.
Uma das coisas que quero destacar aqui é que temos o modo de raciocínio disponível.
val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()Recomendo que você o use ao executar chamadas de ferramentas.
Isso porque fizemos vários testes que mostram uma melhoria de 13 pontos na precisão de 100 ao executar chamadas de ferramentas no Gemini Nano 4.
Em alguns casos, é até bem maior, o que pode gerar resultados muito melhores.
E se quiser saber mais sobre chamadas de função, assista à sessão das minhas colegas Jolanda e Victoria, que vai acontecer mais tarde.
Isso se refere aos recursos agênticos no seu app.
Acho que a Victoria está em algum lugar na plateia, então fiquem à vontade para falar com ela também.
David Chou
Certo, legal.
Agora, concluímos as duas primeiras etapas.
Vamos falar sobre como funciona a recuperação.
Por isso, vamos usar embeddings e o AppSearch.
Então, talvez voltando alguns passos, certo, na jornada do usuário, no momento em que ele pediu a lente, o que queremos fazer é indexar essas informações para que elas fiquem acessíveis ao usuário e ao desenvolvedor, certo?
Neste caso, temos detalhes sobre o número do pacote do pedido.
Ele também tem um número de rastreamento.
Ele tem alguns detalhes sobre para onde é enviado.
E aqui já é mencionado que os tributos foram pré-pagos.
Por isso, vamos gerar um embedding multimodal para indexar essas informações junto com outras que possam ser importantes para nós.
Veja como fica.
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.vectorRecebemos o e-mail do pedido.
Novamente, é muito simples: basta transmitir uma imagem e um texto, mas você pode ter mais conteúdo para indexar aqui.
Em seguida, vamos gerar o vetor de embedding.
Depois disso, vamos usar o AppSearch para criar um documento e definir as propriedades que achamos necessárias para gerar esse documento específico.
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()E uma coisa que vale a pena notar aqui é que o motivo pelo qual queremos fazer embeddings multimodais é que pode não haver palavras compartilhadas aqui, mas como as imagens e o texto são sobre conteúdo semelhante, isso nos ajuda a obter a similaridade semântica.
Portanto, podemos usar a similaridade de cosseno para encontrar itens que não têm palavras em comum.
Certo, essa é a função executeVaultSearch que mencionamos anteriormente.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}Isso é o que está sendo chamado pela função do app que definimos.
Neste exemplo, estamos transmitindo a consulta de pesquisa gerada pelo Gemini Nano.
Em seguida, vamos usar uma estratégia de classificação, a similaridade de cosseno.
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()
}E o this.matchedSemanticScores nos ajuda a conseguir essas informações.
Por fim, vamos especificar um limite de 0, 68 para determinar qual é a similaridade de cosseno mínima para buscar informações.
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() }
}Então, você pode testar isso ao analisar seus próprios casos de uso para ver o que realmente funciona para você.
E uma coisa que queremos destacar aqui também é que, quando você pensa na segurança desse modelo, o Gemini Nano não é o que está executando o código, certo?
O Gemini Nano só faz uma pergunta, mas o app define o código, que é o que realmente executa o código, recupera as informações e determina o que transmitir ao Gemini Nano.
Assim, você tem mais controle sobre o que quer expor ao Gemini Nano e também ajuda a garantir mais privacidade e segurança para os usuários.
David Chou
Certo, agora é a etapa final.
Agora temos todas as informações necessárias.
Vamos usar a API Prompt, a saída estruturada e a síntese de fala para fazer as comparações e retornar essas informações ao usuário de forma compreensível.
Basicamente, queremos saber se esse pacote é o mesmo que as informações do usuário, que é o pedido real.
Para isso, também vamos gerar embeddings com base nas informações do correio de voz e do texto, transformando-as no mesmo vetor de embedding que criamos antes.
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)Em seguida, vamos usar o matchScore. Com a similaridade de cosseno, podemos comparar a correspondência que recuperamos do Vault com as informações do autor da chamada.
Como você pode ver, mesmo que as imagens não correspondam, o texto ainda é muito útil.
É aqui que os embeddings multimodais são realmente valiosos.
Você pode inserir partes diferentes de informações e chegar a um significado semântico compartilhado, certo?
Assim, o embedding multimodal permite mapear tudo no mesmo espaço vetorial.
Mais uma vez, vamos usar o modo de raciocínio para melhorar nosso pensamento.
val forensicRequest = generateContentRequest(
securitySystemInstruction,
content {
image(callerParcelPhotoBmp)
image(orderEmailLensBmp)
text(forensicAuditPrompt)
}
) {
// Enable Gemini Nano 4 reasoning before it answers
enableThinking = true
}E também vamos definir uma classe de saída, certo?
@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,
)Então, o que estamos fazendo aqui é configurar um VisualContradiction e alguns tipos enumerados diferentes dos tipos de contradições possíveis, certo?
Seja um DUTY_FRAUD, um PACKAGING_MISMATCH ou um STATUS_CONFLICT.
Depois, vamos ter uma explicação aqui.
Em seguida, vamos transmitir todas essas informações para um veredito de auditoria, certo?
@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,
)Como você pode ver, é muito simples.
Só queremos saber se isso é um golpe ou não.
Talvez tenhamos um fraudRiskScore em que você pode definir um valor mínimo e máximo de 0 a 100.
Você pode até informar ao usuário quanto dinheiro ele economizou se quiser manter um registro disso ao longo do tempo.
E também pode listar as contradições visuais que acabamos de definir.
Outra observação é que a anotação @Guide é opcional.
Não é necessário definir e fornecer esse tipo de descrição.
Isso é útil porque talvez você não consiga explicar o que está acontecendo apenas com a definição da variável.
Essa descrição ajuda o Gemini Nano a descobrir quais funções específicas chamar e como configurar para que as informações corretas sejam inseridas nos campos certos.
Depois de configurar, é muito simples.
Basta executar três linhas de código aqui.
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().responsegenerateTypedContentRequest, você gera esse conteúdo e pode recuperar essa resposta.
E aqui temos algo que mostra como seria, certo?
{
"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."
}E assim confirmamos que se trata de um golpe, recebemos uma pontuação de risco de fraude muito alta, o que explica essas contradições evidentes nos dados que você recuperou em comparação com as informações do autor da chamada.
Também economizamos uma certa quantia de dinheiro para esse usuário, então podemos manter um rastreador lá.
Sim.
Isso resume todas as informações de que precisamos.
A etapa final seria mostrar isso ao usuário de uma forma acessível.
Por exemplo, se o usuário estiver em movimento ou tiver deficiência visual, talvez seja melhor ler as informações em voz alta em vez de fazê-lo ler o texto manualmente.
Por isso, vamos implementar o recurso de conversão de texto em voz para mostrar como isso funcionaria.
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
}Mais uma vez, é muito simples: especificamos uma localidade e um idioma e, em seguida, transmitimos o veredito que acabamos de gerar usando @Generable e @Guide.
Depois disso, é quase instantâneo.
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
}Na maioria dos dispositivos, leva menos de 120 ms para gerar essa resposta.
Vamos limpar o que for necessário.
E, se necessário, também podemos definir um substituto usando as APIs da plataforma.
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 ...A voz ainda seria robótica, mas pelo menos o usuário teria algo com que interagir caso não consiga ler.
David Chou
Ok.
Foi muita coisa, mas para recapitular o que fizemos, aproveitamos todas essas APIs diferentes nos processos de entrada, triagem, recuperação e auditoria.
Por exemplo, usando APIs de reconhecimento de fala e tradução, API Prompt e chamada de função, embeddings e AppSearch, além de saída estruturada e síntese de fala.
Esse é apenas um exemplo de como você pode aproveitar todos esses recursos de maneira coerente e executar tudo no dispositivo.
E voltando ao que falamos no início, há benefícios substanciais em fazer isso, certo?
Se você executasse isso no seu dispositivo hoje, veria que é muito mais rápido na maioria dos hardwares.
Não custa nada, certo? E você pode economizar centenas de milhares de dólares se executar isso o tempo todo.
E isso fica disponível, certo, mesmo que o usuário esteja nas montanhas ou em um porão e não tenha acesso à Internet.
E é totalmente particular, porque você não precisa enviar nenhuma informação para a nuvem.
Considerando todos esses recursos, confira a documentação, o QR code ou acesse o link.
Esses são todos os recursos que lançamos nos últimos seis meses, aproximadamente.
Agradeço a todos por dedicarem um tempo para assistir a esta palestra.
Agradecemos por dedicar seu tempo a nós.
Então, muito obrigado.
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 ...