이 세션에서는 ML Kit API를 사용하는 다양한 생성형 AI 사용 사례를 살펴보고, 기기 내 생성형 AI가 앱에 적합한 경우를 파악하고, 기존 생성형 AI 사용 사례를 평가하여 기기 내 모델이 품질 기준을 충족할 수 있는지 확인하는 방법을 알아봅니다.
스크립트
데이비드 저우
바로
여러분, 안녕하세요.
제 이름은 데이비드 저우입니다.
저는 Android AICore의 제품 리드입니다.
온디바이스 AI 기능에 대해 이야기하게 되어 매우 기쁩니다.
오늘은 몇 가지 핵심 영역을 살펴보겠습니다.
첫 번째는 특히 클라우드에서 AI 발전이 가속화되고 있는 상황에서 온디바이스 AI를 사용해야 하는 이유를 생각해 보는 것입니다.
두 번째는 지난 1년 동안 온디바이스 AI 모델을 매우 강력하게 만들기 위해 진행한 몇 가지 개선사항을 살펴보는 것입니다.
마지막으로 온디바이스 AI를 사용하여 이러한 모든 이점을 활용하는 방법을 보여주는 엔드 투 엔드 예시를 살펴보겠습니다.
먼저 온디바이스 AI를 사용해야 하는 이유는 무엇일까요?
주요 이유는 4가지입니다.
첫 번째는 속도, 두 번째는 비용, 세 번째는 가용성, 네 번째는 개인 정보 보호입니다.
속도 측면에서 보면, 온디바이스 AI 추론은 사용자의 기기와 클라우드 간에 정보를 주고받을 필요가 없기 때문에 클라우드에서 실행하는 것보다 더 빠른 수준에 도달하고 있습니다.
따라서 테스트한 많은 사용 사례에서 온디바이스 AI와 클라우드를 비교했을 때 실제로 2배의 속도 향상을 확인할 수 있었습니다.
두 번째는 비용을 고려하는 것입니다.
하루에 100만 건의 추론을 실행한다고 가정해 보겠습니다.
온디바이스 AI를 실행하는 경우, 특히 클라우드에서 실행하는 대신 사용자의 기기에서 실행하는 경우 수십만 달러를 절약할 수 있습니다.
세 번째는 오프라인 사용 가능 여부입니다.
올해 초에 설문조사를 실시했는데, 응답한 개발자의 62% 가 오프라인 사용 가능 여부가 온디바이스 AI를 사용하는 세 가지 주요 이유 중 하나라고 답했습니다.
따라서 사용자가 35,000피트 상공에 있든, 도쿄역 지하에 있든, 인터넷에 액세스할 수 없든, 이 모든 기능을 활용할 수 있습니다.
마지막으로, 개발자를 대상으로 설문조사를 실시했을 때 개인 정보 보호가 가장 큰 이유 중 하나였습니다.
72% 가 상위 3가지 이유 중 하나로 꼽았습니다.
40% 가 1위로 꼽았습니다.
메시지, 음성 메모, 사진, 금융 또는 의료 정보와 같은 데이터는 사용자가 비공개로 유지하고 싶어 하므로 기기 내에서 추론을 실행하는 것이 매우 유용할 것입니다.
이러한 모든 이점을 고려하여 Google은 개발자가 이 모든 기능을 유지하고 활용할 수 있도록 ML Kit를 빌드했습니다.
Google은 이 과정을 매우 간단하게 만들고자 합니다.
불만사항이 있으면 언제든지 저에게 문의해 주세요.
이 강연이 끝나면 연락처 정보를 남겨두겠습니다.
귀하의 사용 사례를 지원할 수 있는 방법을 찾고 싶습니다.
우선 음성 및 창의적인 사용 사례를 위한 다양한 기능을 사용 설정했습니다.
교정, 다시 쓰기, 유연한 멀티모달 프롬프트 사용 등이 가능합니다.
따라서 Google의 목표는 이 기능을 최대한 사용하기 쉽게 만드는 것입니다.
이것이 바로 많은 사용자와 개발자가 이미 이 기능을 채택하기 시작한 이유입니다.
현재 1,000만 회 다운로드된 9개의 서드 파티 앱이 프로덕션에서 이러한 생성형 AI 기능을 사용하고 있습니다.
또한 퍼스트 파티 앱과 서드 파티 앱 모두에서 규모가 확대되고 있으며, AI 프로덕션 기능의 앱 출시가 전 분기 대비 110% 증가했습니다.
따라서 메시지, 광고 소재, 재무 전반에 걸쳐 Google에서 제공하는 API를 사용해 보시기 바랍니다.
가능성은 무궁무진하며, 이러한 사용 사례 중 상당수는 여러분이 저희보다 더 나은 상상력을 발휘할 수 있을 것입니다. 따라서 저희는 여러분과 파트너십을 맺고 함께 협력할 방법을 찾고 싶습니다.
이제 전반적인 이점을 이해하셨을 것입니다.
그렇다면 실제 도달범위는 어느 정도인지 궁금하실 수 있습니다.
기기에서의 성능은 어떤가요?
속도에 대해 이야기할 때 실제로 얼마나 좋은가요?
아니면 모델 자체의 품질과 같은 것일 수도 있습니다.
하나씩 살펴보겠습니다.
도달범위 측면에서 보면 현재 1억 7, 500만 대의 AICore 기기에서 사용할 수 있습니다.
내년에는 이 기능이 크게 확대되어 수억 대의 다른 기기에서도 사용할 수 있을 것으로 예상됩니다.
이것이 가능한 이유 중 하나는 매우 간단한 하네스를 만들었기 때문입니다.
따라서 ML Kit API만 통합하면 됩니다.
AICore에는 입력 및 출력 안전 신호를 관리하고, 가중치와 함께 모델을 다운로드하고, 런타임에서 설정하는 방법을 고려하는 등 복잡한 기능이 많이 내장되어 있습니다.
따라서 ML Kit의 목표는 사용하기 쉽게 만들어 개발자는 훌륭한 경험을 제공하는 방법에만 집중하고 나머지는 Google에서 처리하는 것입니다.
데이비드 저우
또한 이러한 생성형 AI API의 상당수를 구동하는 기본 모델인 Gemini Nano를 실행하는 데 필요한 RAM을 크게 줄였다는 소식도 전해 드립니다.
따라서 RAM 사용량을 3.6GB에서 1.1GB로 줄였습니다.
이를 위해 4가지 핵심 개선사항을 적용했습니다.
첫 번째는 4비트 대신 2비트 양자화를 사용하는 것입니다.
따라서 비트가 절반으로 줄어들면 모델을 통해 전달해야 하는 바이트도 절반으로 줄어듭니다.
두 번째는 공유 KV 캐시를 사용하는 것입니다.
따라서 모델의 처음 20개 레이어가 35MB 패키지에서 이를 공유하도록 허용하여 이 크기를 크게 줄였습니다. 즉, 이 매우 작은 패키지 크기 내에서 16,000개의 토큰 컨텍스트 크기에 계속 액세스할 수 있습니다.
세 번째는 지연 비전 및 오디오 인코더를 사용 설정하는 것입니다.
따라서 현재 사용 중인 많은 텍스트 사용 사례의 경우 처음부터 이러한 모델을 로드할 필요가 없습니다.
이렇게 하면 RAM을 0.5GB 더 절약할 수 있습니다.
마지막으로 배터리 수명을 개선하기 위해 NPU 클록 확장도 사용 설정했습니다.
즉, NPU는 사용 사례에서 고품질을 제공하면서도 전력을 절약하는 데 필요한 만큼 빠르게 실행될 수 있습니다.
속도 향상과 관련하여 몇 가지 다른 개선사항도 있습니다.
첫 번째는 프리픽스 캐싱을 사용 설정하는 것입니다.
이 예시에서 시스템 프롬프트가 여러 차례에 걸쳐 재사용되는 것을 볼 수 있습니다.
따라서 이를 처리하는 데 많은 비용이 듭니다.
하지만 프리픽스 캐싱을 사용하면 시스템 프롬프트를 반복해서 다시 처리할 필요가 없다는 말씀이시죠?
이를 프리픽스 캐시에 저장할 수 있습니다. 즉, 이 예시에서는 전달해야 하는 토큰의 60% 를 저장하므로 사용 중인 기기에서 속도가 60% 향상됩니다.
두 번째 개선사항은 추측 디코딩입니다.
이 예시에서는 Gemini Nano가 패스당 토큰을 하나씩 생성하는 것을 볼 수 있습니다.
이는 LLM이 자동 회귀적이기 때문입니다. 즉, 다음 토큰을 파악하기 위해 생성된 이전 토큰을 계속 살펴봐야 합니다.
이 문제를 해결하기 위해 Google은 훨씬 빠른 속도로 여러 토큰을 생성하는 초안 작성 모델을 빌드했습니다.
이 드래프터 모델은 Gemini Nano보다 훨씬 작기 때문에 한 번에 많은 토큰을 생성하여 Gemini에 전달하고, Gemini Nano의 추측과 실제로 일치하는 토큰을 확인할 수 있습니다.
이 예시에서는 첫 번째 패스에서 토큰 6개를 생성하고, 다음 패스에서 5개를 생성하고, 세 번째 패스에서 완료됩니다.
Google에서 살펴보고 있는 많은 내부 사용 사례에서 이로 인해 평균 2배의 속도 향상이 이루어지고 있습니다.
하드웨어에 따라 더 빠르게 개선될 수 있습니다.
좋습니다.
이것이 바로 속도에 관한 이야기로 돌아가서, 5G 네트워크에서도 기기 내와 클라우드 간에 극적인 속도 향상을 확인할 수 있는 이유입니다.
클라우드에 데이터를 업로드하는 데는 1,300개의 입력 토큰에 대해 1초 정도 걸립니다. 따라서 클라우드 여정에서 프리필과 디코딩이 더 빠를 수 있지만, 기기에서 실행할 때 엔드 투 엔드 여정은 여전히 훨씬 더 빠릅니다.
품질 측면에서는 이 2B 모델이 작년의 27B 모델과 비교해도 손색없는 성능을 발휘한다는 점을 알려드립니다.
현재의 프런티어 모델만큼 성능이 뛰어나지는 않지만, 온디바이스 모델의 AI 성능이 크게 향상되고 있음을 보여줍니다.
표준 지식 벤치마크인 MMLU-Pro의 경우 2B 모델이 작년의 27B 파라미터 모델과 거의 비슷한 성능을 보입니다.
그리고 대학원 과학, 추론, 에이전트 도구 사용 벤치마크에서는 이 2B 모델이 작년 모델의 10분의 1도 안 되는 크기임에도 불구하고 실제로 더 우수합니다.
따라서 이러한 추세는 계속될 것으로 예상됩니다.
YouTube는 GDM팀과 긴밀히 협력하여 모델을 지속적으로 개선하고 있으며, 이를 통해 크리에이터가 최대한의 가치를 얻을 수 있도록 노력하고 있습니다.
데이비드 저우
좋습니다.
지금까지 많은 이점과 새로운 개선사항을 소개해 드렸는데요. '이 모델로 실제로 무엇을 할 수 있을까?'라는 의문이 드실 수도 있습니다.
API에서도 이러한 기능 개선이 이루어지고 있습니다.
시스템 요청 사항을 사용하면 페르소나를 설정할 수 있습니다.
다중 이미지 입력을 사용하면 한 번에 여러 이미지를 업로드할 수 있습니다.
올해 초에는 사고 모드도 사용 설정하여 추론 문제와 도구 호출에 더 많은 시간을 할애할 수 있도록 했습니다.
마지막으로, 구조화된 출력을 사용하면 Kotlin 클래스에 주석을 달고 특정 스키마와 일치하는 객체를 반환할 수 있습니다.
이 외에도 개발자에게 어떤 새로운 개선사항을 원하는지 묻습니다.
설문조사에서 개발자의 91% 가 이러한 기능 중 하나를 사용할 수 있다면 환경을 출시할 것이라고 답했습니다.
그래서 이번 주 초에 음성 번역, 임베딩, 텍스트 음성 변환, 함수 호출이라는 네 가지 기능을 출시했습니다.
음성 번역을 사용하면 20개 이상의 언어로 오디오를 스크립트로 작성할 수 있습니다.
임베딩을 사용하면 키워드 검색뿐만 아니라 시맨틱 의미를 기준으로 앱 콘텐츠를 검색할 수 있습니다.
이제 텍스트 음성 변환을 사용하면 인간과 같은 음성을 제공할 수 있으므로 더 이상 로봇 음성을 사용할 필요가 없습니다.
마지막으로 함수 호출을 사용하면 앱이 사용자를 대신하여 작업을 수행할 수 있습니다.
따라서 Google은 이미 몇몇 개발자와 협력하여 개발자가 자신의 환경에 인앱 에이전트를 빌드하는 방법을 살펴보고 있으며, 내년 말에 출시할 예정입니다.
데이비드 저우
이러한 모든 기능을 고려하여, 이제 이 모든 기능을 사용하여 빌드하는 엔드 투 엔드 예시를 살펴보겠습니다.
미국인 사진작가가 일본을 여행하고 있다고 가정해 보겠습니다.
사진에서 볼 수 있듯이 카메라나 렌즈가 충분하지 않습니다.
그래서 새 렌즈를 주문하고 호텔로 배송해 달라고 합니다.
하지만 문제가 있습니다. 하루를 보내는 동안 음성사서함 메시지를 받게 되는데, 오늘 안에 패키지를 받으려면 6, 800엔의 통관 수수료를 지불해야 한다는 내용입니다.
또한 보관 중인 택배의 사진이 포함된 문자 메시지도 받았습니다.
이 경우 사용자는 사기 탐지 보호 기능에 관심이 있습니다.
음성메시지가 합법적인지 사기인지 식별하는 방법을 알고 싶어 합니다.
따라서 모든 정보를 비공개로 유지하고, 정보가 근거에 기반하고, 설명 가능하도록 해야 합니다.
따라서 일본어 텍스트를 이해할 수 있고 접근성 요구사항이 있는 경우 사용 가능한 모든 언어 기능을 사용할 수 있도록 할 것입니다.
여기에는 네 단계가 있습니다.
첫 번째 단계는 음성사서함 메시지를 텍스트로 처리하는 것으로, 음성사서함 메시지를 번역합니다.
이를 위해 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)
}
}좋습니다.
이제 접수의 첫 번째 단계를 완료했습니다.
보시다시피 코드 줄이 많지 않습니다.
데이비드 저우
두 번째 부분은 분류입니다. Prompt API와 함수 호출을 활용하여 어떤 종류의 정보가 필요한지 파악하는 방법을 생각해 봅니다.
좋습니다.
이 아키텍처를 빌드하기 위해 먼저 시스템 요청 사항을 설정하겠습니다.
그런 다음 음성사서함, 문자 메시지, 사진, 사용자의 질문도 Gemini Nano에 입력으로 전달됩니다.
그런 다음 필요한 정보를 실제로 가져오는 함수를 정의합니다.
retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)먼저 시스템 요청 사항을 설정하겠습니다.
// Keep SystemInstruction static across Turn 1 & Turn 2:
val securitySystemInstruction = SystemInstruction(
"""
You are an on-device parcel & phishing security auditor.
1. Cross-check packaging, carrier labels and tracking across all images.
2. Verify customs fee claims against local DDP (Delivered Duty Paid) order records.
3. Flag any payment demand when the order record shows duties prepaid.
""".trimIndent()
)여기에서 보안 감사자를 설정하는 매우 간단한 예를 볼 수 있습니다. 포장과 라벨을 교차 확인하고, 관세를 살펴보고, 필요한 결제 요구사항을 신고한다는 내용입니다.
프롬프트 최적화가 품질을 크게 개선하는 데 도움이 될 수 있습니다.
시스템 명령 집합을 설정한 후에는 이전 단계에서 확보한 모든 콘텐츠, 즉 음성사서함과 문자 메시지의 번역, 사용자의 질문을 가져와 시스템 요청 사항과 함께 이 TextPart 필드에 전달하여 해당 정보를 가져올 수 있습니다.
// Pull in the Speech Translation (AST) & Speech Recognition (ASR) outputs:
val turn1Prompt = """
Translated voicemail: $translatedCourierClaim
Incoming text message: $callerText
User's spoken question: $userQuestion
""".trimIndent()
val turn1Request = generateContentRequest(
securitySystemInstruction,
TextPart(turn1Prompt)
) { /* enableThinking & setTools */ }그런 다음 사용할 수 있는 함수를 정의합니다.
이 예시의 경우 앱 기능과 통합되었다는 소식을 전해드리게 되어 기쁩니다.
따라서 이 개념에 익숙하다면 이 기능의 작동 방식을 이미 대략적으로 이해하고 계실 것입니다.
여기서는 AppFunctionService 진입점을 설정한 다음 앱 함수를 정의합니다.
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}시스템 에이전트의 앱 기능을 설정하는 것과 정확히 동일하다는 것을 알 수 있습니다.
searchQuery의 매개변수, 보상 신청 수수료, 패키지 사진 비교 여부 등을 매개변수 예시로 설정하셨죠?
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
/**
* Searches local orders and order images in AppSearch.
* @param searchQuery Semantic query to match the order
* @param claimedFeeUsd Customs fee claimed in USD
* @param comparePackagePhotos Whether to fetch order images to compare
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun retrieveOrderAndDeliveryPhotos(
searchQuery: String,
claimedFeeUsd: Int,
comparePackagePhotos: Boolean
): VaultMatch = withContext(Dispatchers.IO) { executeVaultSearch(searchQuery) }
}다른 방식으로 정의할 수도 있습니다.
마지막으로 이 함수에는 나중에 정의할 executeVaultSearch 함수가 있습니다.
여기서 강조하고 싶은 점은 사고 모드를 사용할 수 있다는 것입니다.
val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()도구 호출을 실행할 때 이 기능을 사용하는 것이 좋습니다.
Gemini Nano 4에서 도구 호출을 실행할 때 정확도가 100점 만점에 13점 향상된다는 사실을 보여주는 여러 테스트를 진행했기 때문입니다.
경우에 따라서는 실제로 훨씬 더 높을 수 있으므로 훨씬 더 나은 결과를 얻을 수 있습니다.
함수 호출에 대해 자세히 알아보려면 나중에 진행되는 제 동료 Jolanda와 Victoria의 세션에 참석하세요.
앱의 에이전트 기능에 관한 내용입니다.
빅토리아도 청중석 어딘가에 앉아 있을 테니 편하게 인사해 주세요.
데이비드 저우
멋지네요.
이제 처음 두 단계를 완료했습니다.
검색이 어떻게 작동하는지 살펴보겠습니다.
따라서 임베딩과 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에 노출할 항목을 더 세부적으로 제어할 수 있으며 사용자의 개인 정보 보호와 보안을 강화하는 데도 도움이 됩니다.
데이비드 저우
이제 마지막 단계입니다.
따라서 필요한 모든 정보가 있습니다.
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를 설정하고 발생할 수 있는 다양한 모순에 대한 몇 가지 enum을 설정하는 것뿐입니다.
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,
)여기서 보시다시피 매우 간단합니다.
이게 사기인지 아닌지만 알려주세요.
0에서 100 사이의 최솟값과 최댓값을 모두 설정할 수 있는 fraudRiskScore가 있을 수 있습니다.
시간이 지남에 따라 절약한 금액을 추적하고 싶다면 사용자에게 절약한 금액을 알려줄 수도 있습니다.
그리고 방금 정의한 시각적 모순을 나열할 수도 있습니다.
여기서 주목해야 할 또 다른 점은 @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
}대부분의 기기에서 이 응답을 생성하는 데 120ms 미만이 소요됩니다.
필요한 메모리를 삭제하겠습니다.
필요한 경우 플랫폼 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 ...이 경우에도 로봇 음성이 사용되지만, 최소한 사용자가 읽을 수 없는 경우 상호작용할 수 있는 무언가를 제공합니다.
데이비드 저우
좋습니다.
많은 내용을 다루었지만, 지금까지 한 일을 요약하자면 수집, 분류, 검색, 감사 프로세스 전반에서 다양한 API를 활용했습니다.
음성 인식 및 번역 API, Prompt API, 함수 호출, 임베딩, AppSearch, 구조화된 출력, 음성 합성 등을 사용합니다.
이것은 이러한 모든 기능을 일관된 방식으로 활용하고 기기에서 완전히 실행할 수 있는 방법의 한 예입니다.
그리고 처음에 이야기했던 내용으로 돌아가서, 이 기능을 실행하면 상당한 이점이 있습니다.
오늘 기기에서 실제로 실행해 보면 대부분의 하드웨어에서 기기 내 실행이 훨씬 더 빠르다는 것을 알 수 있습니다.
비용이 들지 않으며, 항상 실행하면 수십만 달러를 절약할 수 있습니다.
사용자가 산에 있든 지하실에 있든, 인터넷에 액세스할 수 없는 경우에도 사용할 수 있습니다.
또한 클라우드로 정보를 전송할 필요가 없으므로 완전히 비공개입니다.
이러한 모든 기능을 고려하여 문서를 살펴보거나 QR 코드를 확인하거나 링크로 이동하세요.
지난 6개월 동안 출시된 모든 기능입니다.
시간을 내어 이 강연에 참석해 주신 모든 분께 감사드립니다.
시간을 내주셔서 정말 감사합니다.
정말 감사합니다.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)
val audioFlow = flow {
emit(TranslationAudio(Locale.JAPANESE, courierVoicemailBytes))
}
val request = SpeechTranslatorRequest.Builder(audioFlow).build()
translator.startTranslation(request).collect { response ->
if (response is SpeechTranslatorResponse.Text) {
translatedCourierClaim = response.translatedText
}
}val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)
val request = speechRecognizerRequest {
audioSource = AudioSource.fromMic()
recognitionOptions { enableWordTiming = true } // optional
}
recognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse ->
liveCaption = response.text
is SpeechRecognizerResponse.FinalTextResponse ->
planAndCallVaultTool(translatedCourierClaim, response.text)
}
}retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)// Keep SystemInstruction static across Turn 1 & Turn 2:
val securitySystemInstruction = SystemInstruction(
"""
You are an on-device parcel & phishing security auditor.
1. Cross-check packaging, carrier labels and tracking across all images.
2. Verify customs fee claims against local DDP (Delivered Duty Paid) order records.
3. Flag any payment demand when the order record shows duties prepaid.
""".trimIndent()
)// Pull in the Speech Translation (AST) & Speech Recognition (ASR) outputs:
val turn1Prompt = """
Translated voicemail: $translatedCourierClaim
Incoming text message: $callerText
User's spoken question: $userQuestion
""".trimIndent()
val turn1Request = generateContentRequest(
securitySystemInstruction,
TextPart(turn1Prompt)
) { /* enableThinking & setTools */ }// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
/**
* Searches local orders and order images in AppSearch.
* @param searchQuery Semantic query to match the order
* @param claimedFeeUsd Customs fee claimed in USD
* @param comparePackagePhotos Whether to fetch order images to compare
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun retrieveOrderAndDeliveryPhotos(
searchQuery: String,
claimedFeeUsd: Int,
comparePackagePhotos: Boolean
): VaultMatch = withContext(Dispatchers.IO) { executeVaultSearch(searchQuery) }
}val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()val embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vectorval embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vector
val doc = GenericDocument.Builder<GenericDocument.Builder<*>>(
"orders", "order_jp_904", "OrderRecord"
)
.setPropertyString("summary", "Shipped DDP, duties prepaid ($0 owed). In transit.")
.setPropertyString("photoUris", "order_email_lens.png")
.setPropertyEmbedding(
"embedding",
EmbeddingVector(docVector, embedModel.getModelSignature())
).build()suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
}suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
val results = appSearchSession.search(
"semanticSearch(getEmbeddingParameter(0), 0.68)", spec
)
// Cleanly close the cursor.
return results.use { it.nextPageAsync.await().first().genericDocument.toVaultMatch() }
}val callerVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(callerParcelPhotoBmp), // brown box w/ shipping label
TextPart(callerText) // "Clearance fee ¥6,800 due today."
))
)
).embedding.vector
val matchScore = cosineSimilarity(callerVector, vaultMatch.embedding)val forensicRequest = generateContentRequest(
securitySystemInstruction,
content {
image(callerParcelPhotoBmp)
image(orderEmailLensBmp)
text(forensicAuditPrompt)
}
) {
// Enable Gemini Nano 4 reasoning before it answers
enableThinking = true
}@Generable(description = "Details of a visual contradiction found during an audit")
data class VisualContradiction(
@Guide(
description = "The type of contradiction",
enumValues = ["PACKAGING_MISMATCH", "STATUS_CONFLICT", "DUTY_FRAUD"],
)
val contradictionType: String,
@Guide(description = "Detailed explanation of the visual contradiction")
val explanation: String,
)@Generable(description = "Verdict and details of a phishing audit")
data class PhishingAuditVerdict(
@Guide(
description = "The audit verdict",
enumValues = ["CONFIRMED_PHISHING_SCAM", "LEGITIMATE_HOLD"],
)
val verdict: String,
@Guide(description = "The fraud risk score from 0 to 100", minimum = 0.0, maximum = 100.0)
val fraudRiskScore: Int,
@Guide(description = "Estimated amount of money saved in USD", minimum = 0.0)
val moneySavedUsd: Int,
@Guide(description = "List of visual contradictions identified during audit", minItems = 0)
val contradictions: List<VisualContradiction>,
// @Guide(description = "Summary of the warning to be spoken to the user")
// `@Guide` annotations are optional.
val spokenWarningSummary: String,
)val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().response{
"verdict": "CONFIRMED_PHISHING_SCAM",
"fraudRiskScore": 98,
"moneySavedUsd": 45,
"contradictions": [
{ "contradictionType": "DUTY_FRAUD", "explanation": "Fee demanded, but duties were prepaid." },
{ "contradictionType": "PACKAGING_MISMATCH", "explanation": "Carton has no carrier label or tracking." },
],
"spokenWarningSummary": "Scam blocked. Do not pay the 45 dollar fee. Your lens shipped duties prepaid."
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
/* more code here */
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> { }
else -> {}
}
}
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> {
/* handle fallback to platform API */
}
else -> {}
}
}
}
// more code here ...