Realidad Aumentada en Javascript con inteligencia artificial

La fusión entre Realidad Aumentada e Inteligencia Artificial

Proyecto RA e IA por Samuel Díaz Ramírez
Proyecto RA e IA por Samuel Díaz Ramírez

Quiero extender un sincero agradecimiento a Samuel Díaz Ramírez por su valiosa contribución y por compartir su proyecto con la comunidad de «Realidad Aumentada Empezando desde Cero«. Proyectos como el suyo no solo demuestran el poder de la Realidad Aumentada y la Inteligencia Artificial, sino que también espero que inspiren a otros a explorar y crear.

La interacción y el feedback dentro de una comunidad son fundamentales para el crecimiento y el aprendizaje. Es en este intercambio donde las ideas florecen, los problemas se resuelven de forma colaborativa y el conocimiento se difunde. Cada proyecto compartido, cada pregunta respondida y cada sugerencia ofrecida construye una base sólida para que todos avancen.

El proyecto de Samuel Díaz Ramírez demuestra de manera efectiva cómo la combinación de Realidad Aumentada e Inteligencia Artificial puede crear experiencias enriquecedoras e interactivas. Desde aplicaciones educativas que muestran información sobre objetos históricos, hasta herramientas de asistencia técnica que guían a los usuarios a través de complejos procedimientos, las posibilidades son vastas.

Hablemos sobre cómo se creó la app de realidad aumentada e inteligencia artificial

Todo comienza con estos posts del blog:

 Ahora pasemos a integración de Inteligencia Artificial: De la voz a la búsqueda y texto con Gemini API

El aspecto más innovador de esta aplicación radica en su capacidad para interactuar con una Inteligencia Artificial a través de comandos de voz y texto. El código incorpora la API de reconocimiento de voz del navegador (webkitSpeechRecognition o SpeechRecognition) para transcribir el habla del usuario a texto.

Una vez que se detecta un comando o consulta, la aplicación envía esta información a la API de Gemini de Google. La función getAIDataForObject formula una solicitud a Gemini, pidiendo una descripción creativa o un dato curioso sobre el objeto que el usuario ha consultado. La respuesta de la IA se procesa y se muestra en una ventana emergente dentro de la aplicación, proporcionando información contextual al usuario en tiempo real.

Además, la aplicación incluye una funcionalidad de Texto a Voz (TTS), permitiendo que la descripción obtenida de la IA sea leída en voz alta al usuario. Esto mejora significativamente la accesibilidad y la experiencia interactiva, haciendo que la información sea fácil de consumir sin necesidad de mirar la pantalla.

Experiencia de Usuario y Controles Interactivos

 

La interfaz de usuario es simple e intuitiva:

Menu App RA e IA
Menu App RA e IA
  • Un campo de entrada de texto y un botón de búsqueda permiten a los usuarios escribir sus consultas manualmente.
  • Un botón de micrófono habilita o deshabilita el reconocimiento de voz.
  • Una ventana de resultados muestra la información obtenida de la IA, con un botón para minimizarla.
  • Un botón de lectura (TTS) permite escuchar la descripción generada por la IA.
  • Un botón de alternar (<i class=»fa-solid fa-bars»></i>) en la esquina superior derecha del diseño permite ocultar o mostrar los controles de entrada (micrófono y texto), lo que es útil para una experiencia más inmersiva cuando no se necesita interacción activa.

La aplicación también proporciona mensajes informativos en pantalla (messageBox) para guiar al usuario, indicando cuándo se está consultando a la IA, si hay errores, o si el reconocimiento de voz está activo.

Antes de comenzar con el código de la app te invito a que visites el siguiente post si estas comenzando con el mundo de la realidad aumentada: Aprende Realidad Aumentada Desde Cero: Guia para empezar |

Pasemos al código creado por Samuel y que amablemente nos comparte:


<!DOCTYPE html>
<html lang="es">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AR con Voz y Texto</title>

<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mind-ar@1.2.5/dist/mindar-image-aframe.prod.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">

<style>
/* Estilos básicos para el cuerpo y el lienzo */
body {
margin: 0;
overflow: hidden;
font-family: 'Inter', sans-serif;
background-color: #1a202c;
color: #e2e8f0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
min-height: 100vh;
}

a-scene {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 0;
}

#message-box {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 8px;
font-size: 1rem;
text-align: center;
z-index: 1001;
display: none;
max-width: 90%;
word-wrap: break-word;
}

#input-container {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 10px;
background-color: rgba(26, 32, 44, 0.7);
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
width: 90%;
max-width: 400px;
box-sizing: border-box;
display: none;
/* Oculto por defecto */
}

.mic-container {
display: flex;
align-items: center;
width: 100%;
}

#object-input-wrapper {
flex-grow: 1;
position: relative;
}

#object-input {
width: 100%;
padding: 10px;
border-radius: 8px 0 0 8px;
border: 1px solid #4a5568;
background-color: #2d3748;
color: #e2e8f0;
font-size: 1rem;
box-sizing: border-box;
}

#object-input::placeholder {
color: #a0aec0;
}

#search-button {
padding: 10px;
background-color: #63b3ed;
color: white;
border: none;
border-radius: 0;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
border-left: none;
border-right: 1px solid #4a5568;
}

#search-button:hover {
background-color: #4299e1;
}

#mic-button {
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 0 8px 8px 0;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
border-left: none;
}

#mic-button:hover {
background-color: #45a049;
}

#mic-button.mic-off {
background-color: #757575;
}

#mic-button.mic-off:hover {
background-color: #616161;
}

#loading-spinner {
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid #fff;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
display: none;
margin-left: 10px;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}

#tts-button {
padding: 8px 15px;
background-color: #f6ad55;
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
transition: background-color 0.3s ease;
margin-top: 10px;
display: none;
}

#tts-button:hover {
background-color: #ed8936;
}

#results-window {
position: fixed;
top: 50px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 8px;
font-size: 1rem;
text-align: center;
z-index: 1001;
display: none;
max-width: 90%;
width: 90%;
word-wrap: break-word;
}

#minimize-button {
position: absolute;
top: 5px;
right: 5px;
background-color: #f6ad55;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

#toggle-input-button {
position: fixed;
top: 20px;
right: 20px;
background-color: rgba(139, 88, 211, 0.5);
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.3s ease;
z-index: 1002;
}

#toggle-input-button:hover {
background-color: #6b46c1;
}

/* Media query para pantallas de 480px o menores */
@media (max-width: 480px) {
#input-container {
font-size: 11px;
}

#object-input {
font-size: 11px;
}

#object-input::placeholder {
font-size: 11px;
}

#search-button {
font-size: 11px;
}

#mic-button {
font-size: 11px;
}

#tts-button {
font-size: 11px;
}
}
</style>

</head>

<body>
<div id="message-box"></div>
<a-scene id="arScene" mindar-image="imageTargetSrc: ./targets.mind; filterMinCF: 0.001; filterBeta: 0.01; maxTrack: 2"
color-space="sRGB"
renderer="colorManagement: true, physicallyCorrectLights"
vr-mode-ui="enabled: false"
device-orientation-permission-ui="enabled: false">

<a-assets>
<!-- Aquí se cargarán las imágenes generadas dinámicamente si es necesario -->
</a-assets>
<a-camera position="0 0 0" look-controls="enabled: false"></a-camera>
<a-entity mindar-image-target="targetIndex: 0">
<a-sphere id="sphereModel" radius="0.5" color="#FFC107" position="0 0 0.1" visible="false"
material="shader: standard; metalness: 0.5; roughness: 0.5;"></a-sphere>
</a-entity>
</a-scene>
<div id="input-container">
<div class="mic-container">
<div id="object-input-wrapper">
<input type="text" id="object-input" placeholder="🔍">
</div>
<button id="search-button">🔍</button>
<button id="mic-button" class="mic-off">🔇</button>
</div>
<button id="tts-button">
<span>Leer Descripción 🔊</span>
</button>
</div>
<div id="results-window">
<button id="minimize-button">-</button>
<p id="results-text"></p>
</div>
<button id="toggle-input-button" title="Mostrar/Ocultar Controles">

<i class="fa-solid fa-bars"></i>
</button>
<script>
const messageBox = document.getElementById('message-box');
const objectInput = document.getElementById('object-input');
const searchButton = document.getElementById('search-button');
const micButton = document.getElementById('mic-button');
const ttsButton = document.getElementById('tts-button');
const resultsWindow = document.getElementById('results-window');
const resultsText = document.getElementById('results-text');
const minimizeButton = document.getElementById('minimize-button');
const arScene = document.getElementById('arScene');
const sphereModel = document.getElementById('sphereModel');
const toggleInputButton = document.getElementById('toggle-input-button');
const inputContainer = document.getElementById('input-container');

let recognition;
let isRecognizing = false;
let currentDescription = "";
let isInputVisible = false;

// Función para mostrar mensajes al usuario
function showMessage(message, duration = 3000) {
messageBox.textContent = message;
messageBox.style.display = 'block';
setTimeout(() => {
messageBox.style.display = 'none';
}, duration);
}

// Función para mostrar la esfera cuando se detecta el marcador
arScene.addEventListener('targetFound', function (event) {
console.log('Marcador encontrado!');
sphereModel.setAttribute('visible', 'true');
});

arScene.addEventListener('targetLost', function (event) {
console.log('Marcador perdido!');
sphereModel.setAttribute('visible', 'false');
});

// Función para minimizar la ventana de resultados
minimizeButton.addEventListener('click', () => {
resultsWindow.style.display = 'none';
});

// Función para mostrar los resultados en la ventana
function displayResults(text) {
resultsText.textContent = text;
resultsWindow.style.display = 'block';
currentDescription = text;
ttsButton.style.display = 'block';
}

// Función para Texto a Voz (TTS)
function speakDescription(text) {
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'es-ES';
window.speechSynthesis.speak(utterance);
} else {
showMessage('Tu navegador no soporta Texto a Voz.', 3000);
}
}

// Función para realizar la consulta a la IA
async function performSearch(query) {
showMessage('Consultando a la IA...', 5000);

try {
const aiData = await getAIDataForObject(query);
if (aiData && aiData.description) {
displayResults(aiData.description);
} else {
showMessage('No se pudo obtener la descripción de la IA. Inténtalo de nuevo.', 3000);
}
} catch (error) {
showMessage('Error al conectar con la IA. Revisa tu conexión y clave API.', 5000);
}
}

// Función para obtener datos de la IA (Gemini API)
async function getAIDataForObject(objectName) {
const apiKey = "AQUI ESCRIBE TU API"; // Reemplaza con tu clave API
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
const prompt = `Genera una descripción creativa y concisa o un dato curioso sobre el objeto "${objectName}". Responde en español.`;
const payload = {
contents: [{ role: "user", parts: [{ text: prompt }] }],
generationConfig: {
responseMimeType: "application/json",
responseSchema: {
type: "OBJECT",
properties: { "description": { "type": "STRING" } },
"propertyOrdering": ["description"]
}
}
};

const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});

if (!response.ok) {
throw new Error('Error en la API de Gemini');
}

const result = await response.json();
const jsonText = result.candidates[0].content.parts[0].text;
return JSON.parse(jsonText);
}

// Configuración del reconocimiento de voz
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = false;
recognition.lang = 'es-ES';

recognition.onstart = function () {
isRecognizing = true;
micButton.textContent = '🎙️';
micButton.classList.remove('mic-off');
showMessage('Escuchando...');
};

recognition.onresult = function (event) {
const transcript = event.results[event.results.length - 1][0].transcript.toLowerCase().trim();
console.log('Comando reconocido:', transcript);
performSearch(transcript);
};

recognition.onerror = function (event) {
isRecognizing = false;
console.error('Error de reconocimiento de voz:', event.error);
showMessage('Error en el reconocimiento de voz: ' + event.error + '. Intenta reiniciar.', 5000);
micButton.textContent = '🔇';
micButton.classList.add('mic-off');
};

recognition.onend = function () {
isRecognizing = false;
micButton.textContent = '🔇';
micButton.classList.add('mic-off');
showMessage('Iniciar Reconocimiento detenido.', 2000);
};

micButton.addEventListener('click', () => {
if (!isRecognizing) {
recognition.start();
} else {
recognition.stop();
}
});
} else {
showMessage('Tu navegador no soporta la API de reconocimiento de voz.', 5000);
micButton.disabled = true;
}

// Evento para el botón de consulta por texto
searchButton.addEventListener('click', () => {
const query = objectInput.value.trim();
if (query) {
performSearch(query);
} else {
showMessage('Por favor, escribe lo que quieres consultar.', 2000);
}
});

// Evento para el botón TTS
ttsButton.addEventListener('click', () => {
speakDescription(currentDescription);
});

// Evento para mostrar/ocultar el input-container
toggleInputButton.addEventListener('click', () => {
isInputVisible = !isInputVisible;
inputContainer.style.display = isInputVisible ? 'flex' : 'none';
toggleInputButton.innerHTML = isInputVisible ? '<i class="fas fa-times"></i>' : '<i class="fa-solid fa-bars"></i>';
if (!isInputVisible) {
messageBox.style.display = 'none';
resultsWindow.style.display = 'none';
ttsButton.style.display = 'none';
objectInput.value = '';
}
});

// Inicialización
window.onload = function () {
showMessage('Cargando aplicación AR. Por favor, espere y conceda permisos de cámara.', 5000);
};
</script>
</body>

</html>

Es muy posible que tu aplicación no funcione inmediatamente y es posible que se deba a la falta de la llave API.

la línea de código es la siguiente: const apiKey = «AQUI ESCRIBE TU API»; // Reemplaza con tu clave API

Si no sabes cómo conseguirla te invito a que visites este post para que aprendas como hacerlo: Inteligencia artificial en una app de realidad aumentada

Agrégate a la comunidad de WhatsApp de Realidad Aumentada Empezando desde cero: CLIC AQUI

¿Quieres el codigo de Samuel completo en un solo archivo zip? DESCARGALO AQUI

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Scroll al inicio
0
Would love your thoughts, please comment.x
()
x