Ingeniero de Relaciones con Desarrolladores | Android XR
Descubre cómo crear experiencias de audio sólidas y con manos libres potenciadas por la IA integrada en el dispositivo. Abordaremos todo lo que necesitas: configurar la actividad de tus lentes, capturar los feeds del micrófono y la cámara, y usar la voz junto con otras indicaciones de audio como tu IU definitiva. A través de una emocionante demostración en el mundo real del XR Language Teacher de código abierto integrado en la app de Google AI Edge Gallery, aprende a organizar Gemini Nano 4 (basado en Gemma 4 y optimizado para dispositivos Android) y ML Kit, y aprovecha el entorno de ejecución de LiteRT para crear experiencias de XR rápidas que escuchen, vean y hablen con naturalidad.
Transcripción
Hola, les damos la bienvenida a todos.
Es un placer verlos a todos hoy.
Gracias por venir.
Soy Amy.
Hoy me acompañará mi colega Alice, y hablaremos sobre cómo puedes crear experiencias increíbles centradas en el audio para lentes inteligentes con Gemini Nano 4.
Así que comencemos.
Cuando hablamos de lentes inteligentes, nos referimos a lentes portátiles y livianos que se pueden usar todo el día.
Esta categoría incluirá lentes con pantalla y solo de audio, y con estos dispositivos, verás un hardware variado.
Lo primero que notarás es que tendrán un panel táctil, que se puede usar para la entrada del usuario, como presionar y deslizar.
También tendrán bocinas, una cámara y un micrófono.
Y, luego, también podrían tener una pantalla o no, o, posiblemente, una combinación de estas opciones.
Hoy hablaremos sobre el enfoque centrado en el audio y cómo crear experiencias atractivas, inteligentes y en cualquier lugar.
Y uno de los aspectos interesantes de la experiencia solo de audio es que la voz se convierte en la IU.
Y la IA integrada en el dispositivo es excelente para los desarrolladores que buscan experiencias sin conexión y en cualquier lugar.
Y hay buenas noticias.
Por lo tanto, si ya creas contenido para Android, también lo haces para Android XR.
Las apps para Android existentes están diseñadas para extenderse bien a los lentes con audio, ya que envían notificaciones por voz y otro tipo de audio a través de Bluetooth.
Y si quieres ir más allá y diferenciar tu app para la RE, hay muchas formas de hacerlo.
En esta charla, nos referimos a los modelos integrados en el dispositivo, pero esa no es una lista exhaustiva.
También hay modelos de nube.
Hay cosas como la API de Gemini Live.
Hay agentes del sistema con los que puedes integrarte, como Gemini, y todos ellos pueden ayudarte a crear la experiencia que mejor se adapte a tus necesidades.
Entonces, ¿cómo se crea esta experiencia que escucha, ve y habla de forma natural?
Hoy, te mostraremos cómo crear estas experiencias sólidas de manos libres potenciadas por la IA integrada en el dispositivo.
Te guiaré por los conceptos básicos: cómo compilar para lentes, cómo configurar tu actividad, obtener los permisos que necesitas y, luego, cómo usar el ML Kit para la IU conversacional.
Luego, le daré la palabra a Alice, quien les mostrará algunos ejemplos reales que usan Gemini Nano 4.
Así que pasemos al código.
Por lo tanto, con los lentes, el teléfono es el que hace el trabajo pesado, y el contenido de los lentes se proyectará desde el teléfono hacia los lentes.
Por lo tanto, hay dos cosas que necesitas para conectar estos dispositivos cuando los configures por primera vez.
Primero, necesitarás la biblioteca de Jetpack Projected.
Deberás agregar esta dependencia, que contendrá las APIs para potenciar esa experiencia proyectada.
dependencies {
implementation("androidx.xr.projected:projected:1.0.0-alpha13")
}Y, luego, también deberás crear una actividad nueva específicamente para los lentes.
class ProjectedMainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Glasses content
}
}
}Luego, aprovecharás esa biblioteca de Jetpack Projected tanto dentro de tu app existente como dentro de esta nueva actividad para que los dos dispositivos puedan comunicarse sin problemas cuando sea necesario.
Al igual que cualquier actividad normal que hayas visto, la declararás en tu manifiesto.
<activity
android:name=".ProjectedMainActivity"
android:exported="false"
android:requiredDisplayCategory="android.hardware.display.category.XR_PROJECTED"
android:label="Example activity for audio glasses">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.XR_PROJECTED_LAUNCHER"/>
</intent-filter>
</activity>La diferencia principal es que debes usar el XR_PROJECTED como el requiredDisplayCategory.
Esto es para que el sistema sepa que esta actividad es para anteojos.
Y, luego, también querrás usar esta categoría de intent aquí, XR_PROJECTED_LAUNCHER.
Esto permite que Gemini descubra tu app para que los usuarios puedan iniciarla sin usar las manos mientras están en movimiento.
Bien.
Una vez que hayas configurado esa actividad, como mencioné, usarás esa biblioteca proyectada para potenciar la experiencia en la app.
Esta biblioteca contiene todas las herramientas que necesitas para compilar la experiencia completa de los lentes.
Repasemos algunas de las APIs clave que usarás aquí.
La primera que quiero destacar es el método isProjectedDeviceConnected.
// Continuously monitor whether the glasses are connected
val isGlassesConnected by ProjectedContext.isProjectedDeviceConnected(
context,
scope.coroutineContext
).collectAsStateWithLifecycle(initialValue = false)
// Launch the activity
val options = ProjectedContext.createProjectedActivityOptions(context)
val intent = Intent(context, ProjectedMainActivity::class.java)
context.startActivity(intent, options.toBundle())
// Check if the device is audio only or has a display
val projectedDeviceController = ProjectedDeviceController.create(this@ProjectedMainActivity)
isVisualUiSupported = projectedDeviceController.capabilities.contains(CAPABILITY_VISUAL_UI)Así es como supervisarás de forma continua la conexión de los lentes.
Esto se usa junto con el ciclo de vida del dispositivo para saber cuándo están disponibles los lentes, no solo cuando están vinculados o encendidos, sino también cuando se usan y están listos para usarse.
Y esto es lo que, sí.
Esto te permitirá saber exactamente cuándo puedes iniciar esa actividad.
Para ello, cuando tengas todo listo para iniciar la actividad, deberás configurar algunas opciones específicas.
Para eso, usarás el createProjectedActivityOptions en el ProjectedContext.
Y, de nuevo, usarás eso con el contexto en el intent.
Y otra herramienta que querrás usar es un ProjectedDeviceController.
Así es como verificarás qué tipo de anteojos usa el usuario.
Como dije antes, los anteojos inteligentes incluyen anteojos con pantalla y solo audio, por lo que querrás ver si las capacidades contienen este CAPABILITY_VISUAL_UI, y eso te permitirá adaptar tu app de forma elegante para diferentes dispositivos y personalizar una experiencia que se optimice para ese dispositivo.
Por lo tanto, a medida que creas esta experiencia diferenciada, es posible que quieras acceder al hardware del dispositivo, por ejemplo, aprovechando la cámara de los lentes.
Por lo tanto, para hacerlo desde una actividad proyectada, deberás solicitar los permisos adecuados con ProjectedActivityCompat.
// In a projected activity
private fun requestProjectedPermissions() {
lifecycleScope.launch {
try {
ProjectedActivityCompat.requestPermissions(
this@PermissionsFromProjectedActivity,
arrayOf(Manifest.permission.CAMERA),
// CAMERA_PERMISSION_REQUEST_CODE is a developer defined constant
requestCode = CAMERA_PERMISSION_REQUEST_CODE,
)
} catch (e: IllegalStateException) {
// Thrown when the projected system service can't be bound.
Log.e(TAG, "Request failed: projected service unavailable.", e)
}
}
}Incluso si tu app ya tiene los permisos necesarios para el teléfono, debes solicitar los permisos específicos para el hardware de los lentes.
Cuando trabajes con permisos, recuerda tener en cuenta las prácticas recomendadas para Android en general.
Declara los permisos necesarios en el manifiesto, solicítalos en el contexto en el que se necesitan y solo pide los que realmente necesitas para que tu app funcione.
Por supuesto, tenemos esto documentado en nuestro sitio en developer.android.com.
Pero también tenemos una habilidad completamente nueva: la habilidad de hardware proyectado.
Puedes usarla con la CLI de Android en Android Studio para configurar tu flujo de trabajo de permisos y obtener acceso a esta habilidad y, luego, a todas las demás habilidades de Android.
android skills add projected-hardware
android skills add --allAhora que estamos conectados, conocemos las capacidades de nuestro hardware y tenemos los permisos adecuados, hablemos de las herramientas que tienes para crear esa experiencia de audio.
Por lo tanto, cuando diseñamos una experiencia de audio primero, queremos asegurarnos de crear una experiencia de alta calidad para los usuarios.
Y esto podría incluir algunas interacciones clave.
En primer lugar, se usa la voz como salida, lo que permite que tu voz hable de forma natural con el usuario.
A continuación, se usarían íconos sonoros para enviar señales de audio específicas al usuario.
También se encarga de controlar la entrada del usuario, ya sea a través del panel táctil con toques y deslizamientos, o si quieres permitir que el usuario hable con tu app.
Lo primero de lo que hablaremos es de usar la voz como salida.
Por eso, quiero presentar la nueva biblioteca de IA generativa de texto a voz de ML Kit.
Ya está disponible en versión alfa y en dispositivos Pixel y Samsung seleccionados.
Esto nos entusiasma mucho, en especial para estas experiencias que priorizan el audio.
Si ya trabajaste con Text-to-Speech, es probable que sepas que uno de los puntos problemáticos puede ser que la voz del sistema no siempre suena tan natural como te gustaría.
Además, conectarse a modelos en la nube para otras voces podría generar latencia o costos adicionales.
Por lo tanto, ML Kit te brinda una voz que suena mucho más natural en comparación con la de ese sistema de TTS.
Además, el ML Kit ofrece ocho voces únicas entre las que puedes elegir, con diferentes características para adaptarse a tu app.
Además, el ML Kit ofrece una sólida compatibilidad con 21 idiomas diferentes que puedes elegir.
Además, uno de los grandes beneficios es que, después de esa descarga inicial, funcionará por completo en el dispositivo.
Por lo tanto, este modelo se descargará en el sistema y se compartirá entre las apps, por lo que no aumentará el tamaño de tu APK.
Y no solo está disponible sin conexión, lo que es especialmente importante para los usuarios de lentes que están en movimiento, que podrían no tener Wi-Fi o estar en áreas con poca cobertura, sino que también es probable que vuelvas a ver un mejor rendimiento, ya que no tienes que hacer esa llamada de ida y vuelta a un servidor.
Bien.
Veamos los detalles de la biblioteca y cómo podemos compilar con ella.
Primero, por supuesto, deberás agregar la dependencia.
dependencies {
// ML Kit GenAI TTS SDK
implementation("com.google.mlkit:genai-text-to-speech:1.0.0-alpha1")
}Aquí, lo único que necesitas es el SDK de texto a voz del ML Kit.
Luego, puedes comenzar a escribir tu código.
Por lo tanto, la biblioteca de IA generativa de ML Kit incluye las APIs de Speech Synthesizer.
// 1. Initializing the SpeechSynthesizer client.
val options = SpeechSynthesizerOptions.Builder(Locale.US).build()
val client = SpeechSynthesis.getClient(options)
// 2. Check and download the voice model.
val voiceInfo = client.getSupportedVoices().firstOrNull()
if (voiceInfo != null && client.checkStatus(voiceInfo) == FeatureStatus.DOWNLOADABLE) {
client.download(voiceInfo).collect { status -> println("Download status: $status") }
} else if (voiceInfo == null) {
// Tip: Fallback to system TTS if ML Kit voice is unavailable
}Esto te ofrece una forma sencilla de implementar las voces que elijas.
Y el primer paso que vas a realizar aquí es inicializar tu cliente usando ese sintetizador de voz nuevamente, configurando la configuración regional que elijas.
Luego, verificarás el estado de la descarga y seleccionarás la voz.
En este ejemplo, elegiremos la primera voz, pero también quiero destacar que estamos usando firstOrNull.
Como mencioné, el ML Kit requiere un teléfono específico con capacidad, por lo que se necesitan dispositivos Samsung o Pixel.
Por lo tanto, si obtienes un valor nulo, podría significar que el usuario no está usando un teléfono compatible.
Y, luego, debes asegurarte de tener un resguardo, ya sea volver a la conversión de texto a voz del sistema o reproducir un mensaje grabado o algo similar.
Bien, luego usarás la síntesis de voz SpeechSynthesizerRequest para construir la solicitud, proporcionándole el texto de entrada y, luego, configurando el uso y el tipo de contenido.
// 3. Construct the SpeechSynthesizerRequest
val request = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text("Hello world", voiceInfo),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
// 4. Speak and collect status
client.synthesize(request).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate -> println("Playback status: ${response.status}")
}
}Reproduce el discurso, supervisa el estado y no… para que sepas cuándo se completa el discurso y, luego, recopila los errores que puedan ocurrir.
Y, luego, cerrar los recursos cuando sea necesario.
// 4. Speak and collect status
client.synthesize(request).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate -> println("Playback status: ${response.status}")
}
}
// 5. Release resources when finished (e.g., on Activity destroy)
override fun onDestroy() {
super.onDestroy()
client.close()
}Eso es bastante sencillo, ¿verdad?
Con estos pasos, puedes transformar el sonido de tu app para que pase de sonar así…
Hola, desarrolladores de Android.
Es un placer verlos a todos hoy.
Esperamos que estén disfrutando nuestra presentación sobre las experiencias centradas en el audio.
A algo que suena más como esto.
Hola, desarrolladores de Android.
Es un placer verlos a todos hoy.
Esperamos que estén disfrutando nuestra presentación sobre las experiencias centradas en el audio.
Mucho mejor, ¿no?
Bien.
La biblioteca de texto a voz de ML Kit es una herramienta muy potente, pero no solo puedes usar estas voces, sino que también puedes reproducir íconos sonoros con esta biblioteca.
// 1. Register Earcon resource with Builder
SpeechSynthesizerOptions.Builder(Locale.US)
.addEarcon(
name = "shutter_sound",
Uri = "path/to/earcon.file".toUri()
)
.build()
// 2. Request Earcon playback
val request = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Earcon(
name = "shutter_sound"
),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes
.USAGE_ASSISTANCE_SONIFICATION)
.setContentType(AudioAttributes
.CONTENT_TYPE_SONIFICATION)
.build()
)Por lo tanto, los íconos sonoros, como su nombre lo indica, son básicamente el equivalente de audio de los íconos.
Esto te permitirá reproducir sonidos cortos para comunicarte con el usuario.
Y, al igual que con los íconos visuales y los íconos sonoros, el usuario necesitará saber qué son y qué significan.
Por lo tanto, al principio, es posible que quieras vincularlos con notificaciones de voz para que quede claro.
Ya creamos algunos sonidos estandarizados que están disponibles en Android Design Hub. Puedes usarlos, descargarlos directamente desde allí y usarlos para acciones como abrir o cerrar la app, recibir notificaciones de error y silenciar o activar el sonido.
También tienes la opción de crear tus propios íconos sonoros personalizados.
En cuanto a los íconos sonoros, te recomendamos seguir algunas prácticas recomendadas, ya sea que uses los que descargaste del Centro de diseño o que crees los tuyos.
Por lo tanto, debes tener cuidado con la forma en que usas los íconos sonoros y cómo los usas.
Por lo tanto, hay algunas cosas que debes tener en cuenta, como, para empezar, tratar de no usar en exceso los íconos sonoros.
No reproduzcas los mismos sonidos una y otra vez, ya que esto puede causar lo que se conoce como fatiga auditiva.
Y recuerda que tus íconos sonoros deben ser claros e intuitivos, y que debes usar tonos y ritmos familiares cuando los implementes.
Además, intenta evitar los íconos sonoros demasiado complejos que requieran una explicación, ya que estos podrían ser más adecuados como notificaciones de voz.
Y siempre ten en cuenta que lo que puede sonar sutil en el estudio o en tu sistema, en los altavoces de tu computadora, puede volverse irritante después de una reproducción repetida para tus usuarios.
Así que haz lo posible por no sobrecargar a los usuarios con sonidos que puedan confundirlos o llevarlos a ignorar alertas importantes.
Bien.
Ahora podemos proporcionar indicaciones y comentarios de audio, ya sea a través de la voz natural o con íconos sonoros.
¿Pero qué ocurre con la entrada?
Por supuesto, hay un panel táctil o botones dedicados en el dispositivo que puedes usar para crear interacciones de presionar y deslizar.
Pero también es posible que quieras permitir esta IU conversacional. Para ello, puedes usar la API de GenAI Speech Recognition de ML Kit.
Actualmente, se encuentra en versión alfa y está disponible en dispositivos seleccionados, pero, básicamente, se ejecuta en dos modos distintos.
El modo básico usa el reconocimiento de voz tradicional integrado en el dispositivo y está disponible en dispositivos Android con el nivel de API 31 y versiones posteriores.
Y también está el modo avanzado, que usa Gemini Nano, que se ofrece en los dispositivos Pixel 10 y superiores, y con el que verás una mejor calidad y una mejor cobertura de idiomas.
Por lo tanto, SpeechRecognizer sigue un patrón de código muy similar al de SpeechSynthesizer.
Creas tu cliente, verificas si el modelo está disponible, lo descargas si es necesario, construyes la solicitud y liberas tus recursos.
Así que enfoquémonos en las diferencias principales cuando creas tu solicitud, capturas el audio del micrófono y, luego, procesas el resultado.
Nuevamente, deberás agregar las dependencias necesarias.
dependencies {
// ML Kit Speech Recognition
implementation("com.google.mlkit:genai-speech-recognition:1.0.0-alpha1")
}Todo lo que necesitas aquí es la API de Speech Recognition.
Luego, puedes comenzar a crear tu solicitud.
Por lo tanto, usarás una función de suspensión cuando definas tu lógica de reconocimiento.
suspend fun startMyRecognition() {
// choose locale and mode
val options: SpeechRecognizerOptions =
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
val speechRecognizer: SpeechRecognizer = SpeechRecognition.getClient(options)
val request: SpeechRecognizerRequest =
speechRecognizerRequest { audioSource = AudioSource.fromMic() }
...Luego, especifica las opciones de reconocimiento de voz, configurando la configuración regional y el preferredMode.
Aquí también elegirás el audioSource y definirás la solicitud en sí.
Luego, iniciarás el reconocimiento y recopilarás una respuesta.
speechRecognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse -> {
// Real-time: Act immediately if trigger phrase is heard
handleResponse(response.text, speechRecognizer)
}
is SpeechRecognizerResponse.FinalTextResponse -> {
// Final result and recognition
handleResponse(response.text, speechRecognizer)
}
is SpeechRecognizerResponse.CompletedResponse -> {
resetListeningUi()
}
is SpeechRecognizerResponse.ErrorResponse -> {
Log.e("SpeechRec", "Error: ${response.e.errorCode}", response.e)
resetListeningUi()
}
...Por lo tanto, con SpeechRecognizer, tienes cuatro respuestas posibles: parcial, final, error y completa.
La parcial analizará la transmisión en tiempo real, por lo que se usa para detectar palabras clave o de activación.
Final toma la transmisión de audio final después de analizarla y actualizarla por completo, y Completed se produce después de que se completa la voz. Error recopila los errores que puedan ocurrir.
Y así es como unimos todo.
// Helper function to check for multiple phrases and trigger the right action
private suspend fun handleResponse(text: String, recognizer: SpeechRecognizer) {
val savePhrases = listOf("save", "bookmark", "remember")
if (savePhrases.any { text.contains(it) }) {
// 1. Update app state immediately
saveItemForLater()
// 2. Stop the recognizer to enforce the action and prevent TTS echo
recognizer.stopRecognition()
// 3. Play the mute earcon for immediate, non-verbal confirmation
playEarcon()
// 4. Provide voice feedback
playAudioFeedback("Saved")
}
}Supongamos que queremos que un usuario pueda usar un comando por voz para indicarle a la app que guarde algún tipo de contenido para más tarde.
Podríamos usar un solo comando por voz explícito que les hayamos indicado que usen, o bien permitirles usar una serie de palabras clave y frases.
Aquí, les permitimos usar: guardar, marcar o recordar.
En cuanto SpeechRecognizer detecte que el usuario usó una de esas frases, detendrá el reconocimiento de voz, guardará el contenido, reproducirá un ícono sonoro personalizado y usará la IA generativa de texto a voz de ML Kit para indicarle al usuario que se guardó el contenido.
Bien.
Así que repasamos muchos conceptos.
Puede parecer un poco abstracto.
Si eres como yo, querrás ver algunos ejemplos de la vida real.
Alicia y yo queremos explicarte exactamente eso.
Ella te guiará por XR Language Teacher en la app de Google AI Edge Gallery.
El equipo de Alice la creó y ella te mostrará exactamente cómo funciona.
Te cedo la palabra.
Gracias, Ana.
Sí.
Me gustaría comenzar con algo que muchos de ustedes reconocerían.
Estoy aprendiendo japonés y pasé horas en apps de aprendizaje de idiomas, tarjetas didácticas, rachas, todo.
Luego, aterrizo en Tokio, entro a un mercado y me doy cuenta de que casi nada de lo que tengo delante es el vocabulario que estudié.
En un puesto del mercado, hay un balero de madera que reconozco a simple vista, pero no sé cómo se dice en japonés.
Hay un adorno en una vidriera que quiero comprar y no puedo nombrar.
Ese es el momento exacto en el que tengo más motivación para aprender una palabra.
No en mi sofá mirando una app de idiomas.
En ese momento, estoy mirando lo que quiero aprender.
Esto es lo que quiero.
Me gustaría usar mis lentes inteligentes todo el día, solo caminar.
Y, cuando algo me llame la atención, solo tengo que preguntar al respecto en voz alta.
Y luego, por la noche, cuando volviera al hotel, todo lo que pregunté me estaría esperando como tarjetas de vocabulario en mi teléfono.
Y esto es lo que creamos.
Miremos un clip breve.
Por eso, lo llamamos XR Language Teacher.
Hola.
¿Qué quieres aprender hoy?
Además, puedes personalizar tu experiencia de aprendizaje en la configuración para profesores.
Por ejemplo, elige el idioma que quieres aprender y el perfil de profesor específico que más te convenga.
- Uf. - Sí.
Además, puedes cambiar entre el modo de anteojos y el de teléfono.
¿Cómo se dice "estas uvas" en inglés?
Si miramos esto, esas deliciosas uvas se llaman uvas.
En español, puedes decir "Estas uvas son deliciosas" para describirlas.
Quiero decir uvas verdes, específicamente.
Déjame echarle un vistazo.
Para especificar las uvas verdes, puedes decir "Uvas verdes".
Me gustan las uvas verdes", es una excelente manera de decirle a alguien que te gusta.
¿Cómo se dice esto en francés y cómo puedo pedir tres?
Veamos esto.
Esos deliciosos pasteles se llaman des madeleines.
En francés, para pedir tres, puedes decir: "Je voudrais trois madeleines, s'il vous plaît".
¿Cómo puedo pedir un cambio con esto?
Veamos qué tenemos aquí.
Para pedir el cambio, puedes decir "Otsuri wa arimasu ka?".
que significa "¿Tienes cambio?".
También puedes decir "Otsuri o onegaishimasu" para pedir el cambio de forma educada.
Y, luego, todo lo que aprendiste se guarda automáticamente como tarjetas de vocabulario.
La botella de agua.
Esta es una botella de agua.
Sí, y puedes poner a prueba tu memoria y practicar en la pestaña de estudio.
Bien.
Sí.
Como ves, todo lo que acabas de ver se ejecutó en el dispositivo, sin necesidad de una red.
Viajo con un plan de datos internacional, que suele funcionar bien hasta que entro a un mercado, un lugar subterráneo o un edificio con paredes de concreto gruesas.
El momento en que más lo necesitas suele ser cuando la señal es peor.
Y diría que una experiencia que muere cuando se pierde la señal no es un verdadero compañero de viaje, por lo que hicimos que XR Language Teacher funcione completamente sin conexión y en el dispositivo.
¿Cómo se creó?
En cuanto a la arquitectura, Amy ya te mostró cómo usar un fotograma de cámara proyectado desde los lentes hacia el teléfono.
Te mostró cómo usar el reconocimiento de voz de ML Kit para convertir texto en voz y también cómo usar la función de texto a voz de ML Kit para volver a convertir texto en voz.
Lo que aún necesitábamos era un cerebro en el medio, algo que pudiera analizar el fotograma de la cámara y la pregunta en conjunto, y determinar qué es lo que realmente pregunta el usuario.
Ese cerebro es Gemini Nano 4, que se basa en Gemma 4, el modelo de código abierto de Google que se ejecuta de forma local en el teléfono.
Por lo tanto, en los dispositivos Android insignia compatibles, puedes obtener un rendimiento optimizado usando las APIs de ML Kit directamente.
El sistema Android se encargará de la entrega del modelo, por lo que no tendrás que empaquetar nada en tu app.
En resumen, el micrófono se conecta al reconocimiento de voz, y el fotograma de la cámara y la transcripción se conectan a Gemini Nano 4, que escribe en la síntesis de voz y, luego, sale por la bocina.
Tres modelos encadenados que se ejecutan de forma local en tu teléfono.
Estas son algunas de las razones por las que Gemini Nano 4 es especialmente adecuado para este problema.
Primero, ve y lee en una sola pasada.
A través de ML Kit, Gemini Nano 4 controla texto e imágenes de forma nativa, por lo que no usas un detector, luego un clasificador y, luego, un traductor.
Es un modelo que analiza el fotograma de la cámara y la pregunta en conjunto.
En segundo lugar, su amplia compatibilidad con idiomas.
Gemini Nano 4 se entrenó en más de 140 idiomas.
Además, para el tutor de idiomas, la traducción por sí sola no es suficiente.
Quieres modismos, registros formales frente a informales y también oraciones de ejemplo que diría una persona real.
Por último, es lo suficientemente controlable como para tener una personalidad.
Practicar la pronunciación en voz alta puede ser intimidante.
Además, este modelo sigue bien las instrucciones del sistema, por lo que el usuario puede elegir su propio personaje de profesor.
Y tal vez una paciente y alentadora podría hacer que las personas estén más dispuestas a intentarlo.
Y hay un aspecto más que me gustaría destacar sobre el diseño.
Mientras se desarrolla la conversación en el modo de anteojos, la app escribe discretamente tarjetas de vocabulario en segundo plano.
La palabra, la traducción, la oración de ejemplo y la foto del momento en que haces la pregunta.
El usuario no necesita presionar nada de forma manual.
Solo están caminando y hablando.
Y, más tarde ese día, todo lo que necesitan está en su teléfono, que es exactamente lo que yo quería cuando estaba en Tokio.
Por lo tanto, que el audio sea lo primero no significa que sea lo único.
Los lentes son la superficie adecuada para la curiosidad, y el teléfono es la superficie adecuada para la reflexión posterior.
Deja que el momento de manos libres deje algo atrás.
Ahora, analicemos algunas sugerencias prácticas para crear experiencias en tiempo real con Gemini Nano 4 y ML Kit que aprendimos al crear esta demostración.
Sugerencia 1: Prioriza la latencia en tiempo real con un diseño de dos pasadas.
En los lentes con audio, cuando preguntas "¿Qué es eso?",
Cada momento de silencio se nota.
En el ejemplo del profesor de idiomas de RE, necesitamos dos cosas para cada turno de la interacción del usuario con el modelo: una respuesta hablada inmediata y una tarjeta de vocabulario estructurada para guardar en la app.
Si le pedimos al modelo que haga dos cosas juntas en una sola pasada, cada token que se use para generar esos datos de fondo retrasará la primera oración hablada.
Por lo tanto, separamos lo que el usuario necesita en este momento de lo que la app necesita unos segundos después.
Por lo tanto, dividimos cada turno en dos pases.
En el pase 1, transmitimos el fotograma de la cámara y la pregunta solo para la respuesta hablada, lo que activa la función de texto a voz en cuanto se completa la primera oración.
Y como esa TPU o NPU termina de generar esa respuesta corta mucho antes de que la conversión de texto a voz termine de hablar en voz alta, en realidad obtenemos una ventana de varios segundos en la que el usuario sigue escuchando y el chip ya está inactivo.
Y como el Pase 1 ya identificó el objeto en su respuesta de texto, el Pase 2 ni siquiera necesita la entrada de imagen.
Simplemente pasamos la respuesta del paso 1 a ese chip inactivo para extraer y guardar la tarjeta estructurada antes de que el profesor termine de hablar.
Ahora, acerquemos el paso 1, lo que me lleva al segundo consejo.
Usa etiquetas XML ligeras cuando transmitas.
Como el pase 1 se transmite directamente a la conversión de texto a voz, no querrás que un objeto JSON pesado retrase la primera palabra hablada.
En cambio, necesitas texto sin formato que pueda comunicarse de inmediato con etiquetas ligeras intercaladas cada vez que la app deba tomar medidas en medio de la transmisión.
Permíteme mostrarte el problema que nos trajo hasta aquí.
¿Recuerdas el balero de madera que mencioné en el puesto del mercado?
En japonés, se llama "kendama".
Al principio, el modelo lo reconoció desde el encuadre de la cámara y devolvió la respuesta correcta: "Eso es un kendama, un juguete de madera".
Por lo tanto, ingresamos esa oración completa en la sesión de texto a voz en inglés, y esto es lo que obtuvimos.
Es un ken-mi, un juguete de madera.
Eso no suena del todo bien.
En cambio, esa oración debería sonar…
Es un kendama, un juguete de madera.
¿Por qué ocurrió esto?
Kendama se escribe con hiragana, "ken", y el carácter kanji, "dama".
Y el motor de texto a voz en inglés maneja bien el hiragana fonético como "ken", pero falla por completo cuando llega a la parte del kanji y convierte dama en algo que suena como me.
Y esto sucede en muchos idiomas diferentes de distintas maneras si no cambias la configuración regional.
Por ejemplo, en español, podrías escuchar lo siguiente:
Suh-BOH-luh…
Cuando realmente esperas…
Cebolla.
De manera similar, en francés, podrías escuchar…
PAIN oh chock-uh-lat…
Cuando realmente esperas…
Pain au chocolat.
Sí, una sesión de TTS suele diseñarse para una sola configuración regional a la vez, pero un profesor de idiomas necesita cambiar de código en cada oración.
Por lo tanto, en el paso 1, le pedimos al modelo que encierre cada palabra o frase del idioma de destino dentro de la etiqueta XML de destino a medida que se transmite.
Y, a medida que llegan los fragmentos, nuestro cliente los divide según el objetivo y enruta cada intervalo a la configuración regional de texto a voz correspondiente, que podría ser japonés, español, francés o cualquier idioma que estés aprendiendo, y, luego, los reproduce sin problemas, cambiando el acento a mitad de la oración, para que palabras como kendama se pronuncien correctamente.
Por lo tanto, la conclusión aquí es que el modelo ya sabe qué vocabulario pertenece a qué idioma, qué palabras pertenecen a qué idioma, y que etiquete los intervalos de destino intercalados mientras se transmite, es mucho más limpio que procesar todo esto por tu cuenta después.
Y aquí está el paso 1 en el código.
// 1. Pass 1 System + Turn Prompt (XrLanguageTeacherPersona.kt)
// System: "Always start immediately with <reply>...</reply> (strictly 1 to 3 sentences total).
// Inside <reply>, wrap every Japanese word AND every complete Japanese example sentence
// inside <target>...</target> tags (never output <card>)."
val pass1TurnPrompt = """
The student is looking at the camera view and asked: "$question". Answer in English
teaching Japanese inside <reply>...</reply>: if they refer to 'this'/'that' or point to
an object, ground it to the focal item in the camera frame; if they ask for a phrase or
greeting, teach that phrase.
""".trimIndent()
// 2. Stream multimodal dialogue (ImagePart + TextPart) and split <target> tags to TTS
val generativeModel = Generation.getClient()
val pass1Request = generateContentRequest(ImagePart(frame), TextPart(pass1TurnPrompt)) {
temperature = 1.0f; topK = 1; maxOutputTokens = 1024
}
generativeModel.generateContentStream(pass1Request).collect { chunk ->
val delta = chunk.candidates.firstOrNull()?.text.orEmpty()
streamReplyAndSplitTargetTagsToTts(delta) // Speaks 1st complete sentence immediately
}
// Streams: "<reply>That's <target>けん玉</target> — a wooden toy. <target>けん玉をください。</target></reply>"En la parte superior, usamos la instrucción para indicarle al modelo que responda en una a tres oraciones dentro de las etiquetas de respuesta y, luego, que encierre cada palabra o frase de ejemplo en japonés dentro de las etiquetas de destino.
Luego, pasamos la cámara ImagePart y la instrucción TextPart al generateContentStream de ML Kit, y dividimos los intervalos de destino en texto a voz a medida que llega cada oración.
Y eso me lleva al segundo paso y a nuestro tercer consejo.
Si bien usar etiquetas XML ligeras es excelente para una transmisión de audio en vivo, usa un resultado estructurado, que se compila para devolver un objeto limpio y con tipo cuando se guardan datos en tu app.
Como nuestro XR Language Teacher es conversacional, los usuarios también pueden hacer preguntas de seguimiento en las que no se enseñe ninguna palabra nueva.
Y cuando se enseña una palabra, la base de datos necesita campos de diccionario limpios para crear las tarjetas de vocabulario.
Por lo tanto, en lugar de escribir expresiones regulares frágiles para analizar la transcripción del paso uno, dejamos que el paso dos extraiga el objeto con seguridad de tipos en segundo plano.
Para aplicar el esquema en el paso 2, usamos la API de salida estructurada de ML Kit, que, de forma interna, usa la decodificación restringida.
import com.google.mlkit.genai.prompt.Generation
import com.google.mlkit.genai.prompt.generateTypedContentRequest
import com.google.mlkit.genai.schema.annotations.Generable
import com.google.mlkit.genai.schema.annotations.Guide
// Pass 2 schema (XrLanguageTeacherGenerableSchemas.kt):
@Generable(description = "Vocabulary flashcard for the taught word or short phrase")
data class GenerableVocabCard(
@Guide(description = TeacherResponseSchema.WORD_FIELD_DESCRIPTION) val word: String,
@Guide(description = "Native-language meaning or translation") val meaning: String,
@Guide(description = "Target-language example sentence") val example: String,
@Guide(description = "Native-language translation of the example sentence") val example_translation: String,
)
@Generable(description = "Optional vocabulary card extracted from a teacher reply")
data class GenerableOptionalVocabCardWrapper(
@Guide(description = "Vocabulary flashcard if a vocabulary term was taught, or null otherwise")
val card: GenerableVocabCard?
)Solo debes anotar una clase de datos con @Generable y cada campo con @Guide, como en nuestro ejemplo de clase de tarjeta de vocabulario.
Y, si se une el campo de tarjeta anulable, el pase 2 devuelve una tarjeta propagada cuando se enseñó una palabra nueva, o un valor nulo en los turnos puramente conversacionales.
Y aquí está el paso 2 en el código.
// 1. Pass 1 Streamed Output -> Pass 2 Extraction Prompt (no image needed!)
val replyWithTargetTags =
"<reply>That's <target>けん玉</target> — a wooden toy. <target>けん玉をください。</target></reply>"
val pass2Prompt = """
If the teacher reply below teaches or introduces a specific Japanese vocabulary item, extract
only its core focal dictionary term into "card" with keys "word", "meaning", "example", and
"example_translation". If purely conversational with no focal term taught, set "card" to null:
$replyWithTargetTags
""".trimIndent()
// 2. Extract @Generable card on idle NPU while TTS audio plays
val pass2Request = generateContentRequest(TextPart(pass2Prompt)) {
temperature = 1.0f; topK = 1; maxOutputTokens = 256
}
val typedRequest =
generateTypedContentRequest(pass2Request, GenerableOptionalVocabCardWrapper::class)
val card: GenerableVocabCard? =
generativeModel.generateContent(typedRequest).candidates.firstOrNull()?.response?.card
// Extracts: {"word": "けん玉", "meaning": "a wooden toy", "example": "けん玉をください。", ...}En la parte superior, tomamos el texto de la respuesta transmitida del paso 1, no se necesita ninguna imagen, y creamos nuestra instrucción de extracción.
Luego, mientras el usuario sigue escuchando la conversión de texto a voz, pasamos la instrucción y nuestra clase de wrapper a generateTypedContentRequest y extraemos una tarjeta de vocabulario limpia y sin etiquetas, o un valor nulo si no se enseñó ninguna palabra.
Bien.
Abarcamos muchos temas.
Es razonable pensar en este momento: "Genial, otra cosa que se ve hermosa en el escenario y que nunca tocaré".
¡Esta no!
XR Language Teacher se incluye en la app de Google AI Edge Gallery junto con otras experiencias de IA integrada en el dispositivo.
Puedes descargarla hoy, ponerla en tu teléfono y apuntar a cualquier cosa que quieras aprender a decir en otro idioma.
Y si quieres saber cómo se compila, toda esta implementación es de código abierto en GitHub.
No te preocupes si no todos tus usuarios tienen lentes de audio o con pantalla, o dispositivos Android insignia compatibles.
Nuestra base de código también te muestra ejemplos de cómo se puede adaptar esta misma experiencia para dispositivos móviles.
Y si también quieres llegar a otras plataformas, el código de la app de Google AI Edge Gallery te muestra cómo compilar esta función con el entorno de ejecución de Google LiteRT y las APIs de MediaPipe Tasks.
Si quieres incorporar otros modelos de lenguaje o audio integrados en el dispositivo de código abierto, también puedes ver cómo hacerlo desde el código.
Por eso, lanzamos XR Language Teacher como código abierto para que puedas crear prototipos con mayor facilidad.
Dirige el agente que elijas a nuestro repositorio de GitHub para convertir esta experiencia en vivo, integrada en el dispositivo y potenciada por IA en algo más.
Podría ser un guía de museo, un asistente de cocina que mira tu mesada, un instructivo de reparación, un narrador de accesibilidad o una herramienta de inspección de campo.
El modelo, el entorno de ejecución, la implementación de código abierto y las herramientas ya están disponibles para que puedas prepararte para el próximo lanzamiento de los lentes inteligentes este otoño.
Nos encantaría ver lo que creas con ella.
Para concluir, hoy mostramos ejemplos potentes de lo que es posible con los modelos de IA que se ejecutan sin problemas integrados en el dispositivo.
Aquí tienes algunas ideas para empezar.
Primero, toma los recursos que aparecen en pantalla.
Descarga la app de AI Edge Gallery para probar la demostración y consultar el código de referencia en GitHub.
Escanea los códigos QR para acceder al SDK de Android XR, las APIs de ML Kit y las ofertas de Google AI Edge.
En segundo lugar, comienza a experimentar hoy mismo en el teléfono.
Hay muchos aspectos que abordamos que ya se pueden compilar con una experiencia centrada en dispositivos móviles.
En tercer lugar, analiza la app para dispositivos móviles que ya estás enviando y pregúntate qué funciones principales serían mejores con el modo de manos libres.
Solo tienes que agregar una actividad proyectada a tu APK existente para llevar esa experiencia también a los lentes.
Muchas gracias por acompañarnos hoy.
Estamos ansiosos por ver las increíbles experiencias centradas en el audio que crearás en Android y para lentes inteligentes.
Gracias.
dependencies {
implementation("androidx.xr.projected:projected:1.0.0-alpha13")
}class ProjectedMainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Glasses content
}
}
}<activity
android:name=".ProjectedMainActivity"
android:exported="false"
android:requiredDisplayCategory="android.hardware.display.category.XR_PROJECTED"
android:label="Example activity for audio glasses">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.XR_PROJECTED_LAUNCHER"/>
</intent-filter>
</activity>// Continuously monitor whether the glasses are connected
val isGlassesConnected by ProjectedContext.isProjectedDeviceConnected(
context,
scope.coroutineContext
).collectAsStateWithLifecycle(initialValue = false)
// Launch the activity
val options = ProjectedContext.createProjectedActivityOptions(context)
val intent = Intent(context, ProjectedMainActivity::class.java)
context.startActivity(intent, options.toBundle())
// Check if the device is audio only or has a display
val projectedDeviceController = ProjectedDeviceController.create(this@ProjectedMainActivity)
isVisualUiSupported = projectedDeviceController.capabilities.contains(CAPABILITY_VISUAL_UI)// In a projected activity
private fun requestProjectedPermissions() {
lifecycleScope.launch {
try {
ProjectedActivityCompat.requestPermissions(
this@PermissionsFromProjectedActivity,
arrayOf(Manifest.permission.CAMERA),
// CAMERA_PERMISSION_REQUEST_CODE is a developer defined constant
requestCode = CAMERA_PERMISSION_REQUEST_CODE,
)
} catch (e: IllegalStateException) {
// Thrown when the projected system service can't be bound.
Log.e(TAG, "Request failed: projected service unavailable.", e)
}
}
}android skills add projected-hardware
android skills add --alldependencies {
// ML Kit GenAI TTS SDK
implementation("com.google.mlkit:genai-text-to-speech:1.0.0-alpha1")
}// 1. Initializing the SpeechSynthesizer client.
val options = SpeechSynthesizerOptions.Builder(Locale.US).build()
val client = SpeechSynthesis.getClient(options)
// 2. Check and download the voice model.
val voiceInfo = client.getSupportedVoices().firstOrNull()
if (voiceInfo != null && client.checkStatus(voiceInfo) == FeatureStatus.DOWNLOADABLE) {
client.download(voiceInfo).collect { status -> println("Download status: $status") }
} else if (voiceInfo == null) {
// Tip: Fallback to system TTS if ML Kit voice is unavailable
}// 3. Construct the SpeechSynthesizerRequest
val request = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text("Hello world", voiceInfo),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
// 4. Speak and collect status
client.synthesize(request).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate -> println("Playback status: ${response.status}")
}
}// 4. Speak and collect status
client.synthesize(request).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate -> println("Playback status: ${response.status}")
}
}
// 5. Release resources when finished (e.g., on Activity destroy)
override fun onDestroy() {
super.onDestroy()
client.close()
}// 1. Register Earcon resource with Builder
SpeechSynthesizerOptions.Builder(Locale.US)
.addEarcon(
name = "shutter_sound",
Uri = "path/to/earcon.file".toUri()
)
.build()
// 2. Request Earcon playback
val request = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Earcon(
name = "shutter_sound"
),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes
.USAGE_ASSISTANCE_SONIFICATION)
.setContentType(AudioAttributes
.CONTENT_TYPE_SONIFICATION)
.build()
)dependencies {
// ML Kit Speech Recognition
implementation("com.google.mlkit:genai-speech-recognition:1.0.0-alpha1")
}suspend fun startMyRecognition() {
// choose locale and mode
val options: SpeechRecognizerOptions =
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
val speechRecognizer: SpeechRecognizer = SpeechRecognition.getClient(options)
val request: SpeechRecognizerRequest =
speechRecognizerRequest { audioSource = AudioSource.fromMic() }
...speechRecognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse -> {
// Real-time: Act immediately if trigger phrase is heard
handleResponse(response.text, speechRecognizer)
}
is SpeechRecognizerResponse.FinalTextResponse -> {
// Final result and recognition
handleResponse(response.text, speechRecognizer)
}
is SpeechRecognizerResponse.CompletedResponse -> {
resetListeningUi()
}
is SpeechRecognizerResponse.ErrorResponse -> {
Log.e("SpeechRec", "Error: ${response.e.errorCode}", response.e)
resetListeningUi()
}
...// Helper function to check for multiple phrases and trigger the right action
private suspend fun handleResponse(text: String, recognizer: SpeechRecognizer) {
val savePhrases = listOf("save", "bookmark", "remember")
if (savePhrases.any { text.contains(it) }) {
// 1. Update app state immediately
saveItemForLater()
// 2. Stop the recognizer to enforce the action and prevent TTS echo
recognizer.stopRecognition()
// 3. Play the mute earcon for immediate, non-verbal confirmation
playEarcon()
// 4. Provide voice feedback
playAudioFeedback("Saved")
}
}// 1. Pass 1 System + Turn Prompt (XrLanguageTeacherPersona.kt)
// System: "Always start immediately with <reply>...</reply> (strictly 1 to 3 sentences total).
// Inside <reply>, wrap every Japanese word AND every complete Japanese example sentence
// inside <target>...</target> tags (never output <card>)."
val pass1TurnPrompt = """
The student is looking at the camera view and asked: "$question". Answer in English
teaching Japanese inside <reply>...</reply>: if they refer to 'this'/'that' or point to
an object, ground it to the focal item in the camera frame; if they ask for a phrase or
greeting, teach that phrase.
""".trimIndent()
// 2. Stream multimodal dialogue (ImagePart + TextPart) and split <target> tags to TTS
val generativeModel = Generation.getClient()
val pass1Request = generateContentRequest(ImagePart(frame), TextPart(pass1TurnPrompt)) {
temperature = 1.0f; topK = 1; maxOutputTokens = 1024
}
generativeModel.generateContentStream(pass1Request).collect { chunk ->
val delta = chunk.candidates.firstOrNull()?.text.orEmpty()
streamReplyAndSplitTargetTagsToTts(delta) // Speaks 1st complete sentence immediately
}
// Streams: "<reply>That's <target>けん玉</target> — a wooden toy. <target>けん玉をください。</target></reply>"import com.google.mlkit.genai.prompt.Generation
import com.google.mlkit.genai.prompt.generateTypedContentRequest
import com.google.mlkit.genai.schema.annotations.Generable
import com.google.mlkit.genai.schema.annotations.Guide
// Pass 2 schema (XrLanguageTeacherGenerableSchemas.kt):
@Generable(description = "Vocabulary flashcard for the taught word or short phrase")
data class GenerableVocabCard(
@Guide(description = TeacherResponseSchema.WORD_FIELD_DESCRIPTION) val word: String,
@Guide(description = "Native-language meaning or translation") val meaning: String,
@Guide(description = "Target-language example sentence") val example: String,
@Guide(description = "Native-language translation of the example sentence") val example_translation: String,
)
@Generable(description = "Optional vocabulary card extracted from a teacher reply")
data class GenerableOptionalVocabCardWrapper(
@Guide(description = "Vocabulary flashcard if a vocabulary term was taught, or null otherwise")
val card: GenerableVocabCard?
)// 1. Pass 1 Streamed Output -> Pass 2 Extraction Prompt (no image needed!)
val replyWithTargetTags =
"<reply>That's <target>けん玉</target> — a wooden toy. <target>けん玉をください。</target></reply>"
val pass2Prompt = """
If the teacher reply below teaches or introduces a specific Japanese vocabulary item, extract
only its core focal dictionary term into "card" with keys "word", "meaning", "example", and
"example_translation". If purely conversational with no focal term taught, set "card" to null:
$replyWithTargetTags
""".trimIndent()
// 2. Extract @Generable card on idle NPU while TTS audio plays
val pass2Request = generateContentRequest(TextPart(pass2Prompt)) {
temperature = 1.0f; topK = 1; maxOutputTokens = 256
}
val typedRequest =
generateTypedContentRequest(pass2Request, GenerableOptionalVocabCardWrapper::class)
val card: GenerableVocabCard? =
generativeModel.generateContent(typedRequest).candidates.firstOrNull()?.response?.card
// Extracts: {"word": "けん玉", "meaning": "a wooden toy", "example": "けん玉をください。", ...}