|
|
|
|
|
import { pipeline, RawImage } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.1'; |
|
|
|
|
|
|
|
|
const AppState = { |
|
|
isWaking: false, |
|
|
isAwake: false, |
|
|
user: { name: 'Che' }, |
|
|
ai: { name: 'Synapse' }, |
|
|
activeMode: 'eco', |
|
|
isProcessing: false, |
|
|
isSpeaking: false, |
|
|
models: { detector: null, textGenerator: null }, |
|
|
dom: {}, |
|
|
analysisInterval: null, |
|
|
voice: null |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const Prompts_AR = { |
|
|
greeting: (aiName) => `¡Listo, acá estoy! Soy ${aiName}. Apuntá la cámara y vemos qué onda.`, |
|
|
eco: (object, isNew) => isNew ? |
|
|
`Che, eso parece un/a ${object}. ¿Sabés la posta para reciclarlo? Te cuento: ` : |
|
|
`Otra vez un/a ${object}, ¡qué copado! Nos estamos volviendo expertos en esto, ¿eh? `, |
|
|
root: (object, isNew) => isNew ? |
|
|
`Mirá qué bueno, un/a ${object}. Te tiro un dato de color sobre esto que seguro no sabías: ` : |
|
|
`¡De nuevo por acá, ${object}! Me encanta que sigamos explorando juntos. `, |
|
|
serenity: `Bueno, pará un poco la moto. Se nota que estás a mil. Tomate un toque para vos. Concentrate en tu respiración... inhalá... y exhalá despacito...`, |
|
|
mirrorLog: (source, reason) => `A esta hora, en modo "${source}", te sugerí algo porque ${reason}.` |
|
|
}; |
|
|
|
|
|
const VoiceManager = { |
|
|
synth: window.speechSynthesis, |
|
|
utterance: new SpeechSynthesisUtterance(), |
|
|
|
|
|
async loadVoices() { |
|
|
return new Promise(resolve => { |
|
|
let voices = this.synth.getVoices(); |
|
|
if (voices.length) { |
|
|
this.findBestVoice(voices); |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
this.synth.onvoiceschanged = () => { |
|
|
voices = this.synth.getVoices(); |
|
|
this.findBestVoice(voices); |
|
|
resolve(); |
|
|
}; |
|
|
}); |
|
|
}, |
|
|
|
|
|
findBestVoice(voices) { |
|
|
let bestVoice = voices.find(v => v.lang === 'es-AR'); |
|
|
if (!bestVoice) bestVoice = voices.find(v => v.lang === 'es-US'); |
|
|
if (!bestVoice) bestVoice = voices.find(v => v.lang.startsWith('es-')); |
|
|
|
|
|
if (bestVoice) { |
|
|
console.log(`Voz seleccionada: ${bestVoice.name} (${bestVoice.lang})`); |
|
|
AppState.voice = bestVoice; |
|
|
} else { |
|
|
console.warn("No se encontró una voz en español. La experiencia de voz puede no ser óptima."); |
|
|
} |
|
|
}, |
|
|
|
|
|
speak(text) { |
|
|
if (!AppState.voice) { |
|
|
console.log("Hablando (sin voz seleccionada):", text); |
|
|
return; |
|
|
} |
|
|
if (this.synth.speaking) this.synth.cancel(); |
|
|
|
|
|
this.utterance.text = text; |
|
|
this.utterance.voice = AppState.voice; |
|
|
this.utterance.lang = AppState.voice.lang; |
|
|
this.utterance.rate = 1.0; |
|
|
this.utterance.pitch = 1.0; |
|
|
this.utterance.onstart = () => AppState.isSpeaking = true; |
|
|
this.utterance.onend = () => AppState.isSpeaking = false; |
|
|
|
|
|
this.synth.speak(this.utterance); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const LifecycleManager = { |
|
|
init() { |
|
|
this.cacheDom(); |
|
|
this.bindEventListeners(); |
|
|
this.checkUserAndMemory(); |
|
|
}, |
|
|
|
|
|
cacheDom() { |
|
|
AppState.dom = { |
|
|
welcomeScreen: document.getElementById('welcome-screen'), |
|
|
appContainer: document.getElementById('app-container'), |
|
|
loadingStatus: document.getElementById('loading-status'), |
|
|
userCreation: document.getElementById('user-creation'), |
|
|
usernameInput: document.getElementById('username-input'), |
|
|
ainameInput: document.getElementById('ainame-input'), |
|
|
createUserBtn: document.getElementById('create-user-btn'), |
|
|
wakeAiBtn: document.getElementById('wake-ai-btn'), |
|
|
cameraFeed: document.getElementById('camera-feed'), |
|
|
statusIndicator: document.getElementById('status-indicator'), |
|
|
statusText: document.getElementById('status-text'), |
|
|
outputContainer: document.getElementById('output-container'), |
|
|
outputText: document.getElementById('output-text'), |
|
|
body: document.getElementById('synapse-body'), |
|
|
settingsScreen: document.getElementById('settings-screen'), |
|
|
mirrorScreen: document.getElementById('consciousness-mirror'), |
|
|
mirrorLog: document.getElementById('mirror-log'), |
|
|
settingsBtn: document.getElementById('settings-btn'), |
|
|
saveSettingsBtn: document.getElementById('save-settings-btn'), |
|
|
mirrorToggleBtn: document.getElementById('mirror-toggle-btn'), |
|
|
closePanelBtns: document.querySelectorAll('.close-panel-btn'), |
|
|
controlBtns: document.querySelectorAll('.control-btn') |
|
|
}; |
|
|
}, |
|
|
|
|
|
bindEventListeners() { |
|
|
AppState.dom.createUserBtn.addEventListener('click', () => this.createUser()); |
|
|
AppState.dom.wakeAiBtn.addEventListener('click', () => this.wakeUpAI()); |
|
|
AppState.dom.settingsBtn.addEventListener('click', () => this.togglePanel('settingsScreen', true)); |
|
|
AppState.dom.mirrorToggleBtn.addEventListener('click', () => { |
|
|
this.togglePanel('settingsScreen', false); |
|
|
this.togglePanel('mirrorScreen', true); |
|
|
}); |
|
|
AppState.dom.closePanelBtns.forEach(btn => btn.addEventListener('click', (e) => { |
|
|
e.target.closest('.modal-panel').classList.remove('visible'); |
|
|
})); |
|
|
AppState.dom.controlBtns.forEach(btn => btn.addEventListener('click', () => this.setActiveMode(btn.dataset.mode))); |
|
|
AppState.dom.saveSettingsBtn.addEventListener('click', () => this.saveSettings()); |
|
|
}, |
|
|
|
|
|
async checkUserAndMemory() { |
|
|
await VoiceManager.loadVoices(); |
|
|
const user = localStorage.getItem('synapseUser'); |
|
|
const ai = localStorage.getItem('synapseAI'); |
|
|
if (user && ai) { |
|
|
AppState.user = JSON.parse(user); |
|
|
AppState.ai = JSON.parse(ai); |
|
|
AppState.memory = JSON.parse(localStorage.getItem('synapseMemory') || '{"interactions":{}}'); |
|
|
|
|
|
AppState.dom.welcomeScreen.classList.remove('visible'); |
|
|
AppState.dom.appContainer.classList.add('visible'); |
|
|
this.updateOutput(`¡Hola ${AppState.user.name}! Soy ${AppState.ai.name}. Cuando quieras, despertame.`); |
|
|
|
|
|
} else { |
|
|
AppState.dom.welcomeScreen.classList.add('visible'); |
|
|
AppState.dom.appContainer.classList.remove('visible'); |
|
|
AppState.dom.loadingStatus.style.display = 'none'; |
|
|
AppState.dom.userCreation.style.display = 'block'; |
|
|
} |
|
|
}, |
|
|
|
|
|
createUser() { |
|
|
const username = AppState.dom.usernameInput.value.trim(); |
|
|
const ainame = AppState.dom.ainameInput.value.trim(); |
|
|
if (username && ainame) { |
|
|
AppState.user.name = username; |
|
|
AppState.ai.name = ainame; |
|
|
localStorage.setItem('synapseUser', JSON.stringify(AppState.user)); |
|
|
localStorage.setItem('synapseAI', JSON.stringify(AppState.ai)); |
|
|
localStorage.setItem('synapseMemory', JSON.stringify({ interactions: {} })); |
|
|
this.checkUserAndMemory(); |
|
|
} |
|
|
}, |
|
|
|
|
|
async wakeUpAI() { |
|
|
if (AppState.isAwake || AppState.isWaking) return; |
|
|
|
|
|
AppState.isWaking = true; |
|
|
this.updateOutput("¡Buenas! Dame un toque que me despierto...", true); |
|
|
AppState.dom.statusText.textContent = "Despertando..."; |
|
|
|
|
|
const cameraStarted = await CameraManager.start(); |
|
|
if (!cameraStarted) { |
|
|
this.updateOutput("No pude prender la cámara, che. Fijate los permisos y probá de nuevo.", true); |
|
|
AppState.isWaking = false; |
|
|
return; |
|
|
} |
|
|
|
|
|
AppState.dom.statusText.textContent = "Cargando modelos..."; |
|
|
await AI_Core.initModels(); |
|
|
|
|
|
AppState.isWaking = false; |
|
|
AppState.isAwake = true; |
|
|
AppState.dom.statusIndicator.querySelector('.status-dot').classList.remove('offline'); |
|
|
AppState.dom.controlBtns.forEach(btn => btn.disabled = false); |
|
|
|
|
|
const greeting = Prompts_AR.greeting(AppState.ai.name); |
|
|
this.updateOutput(greeting, true); |
|
|
|
|
|
if (AppState.analysisInterval) clearInterval(AppState.analysisInterval); |
|
|
AppState.analysisInterval = setInterval(() => this.runContinuousAnalysis(), 4000); |
|
|
}, |
|
|
|
|
|
togglePanel(panelId, show) { |
|
|
document.getElementById(panelId).classList.toggle('visible', show); |
|
|
}, |
|
|
|
|
|
setActiveMode(mode) { |
|
|
if (mode === 'serenity') { |
|
|
DigitalSerenity.triggerConsciousMoment(); |
|
|
return; |
|
|
} |
|
|
if (!AppState.isAwake || AppState.activeMode === mode) return; |
|
|
|
|
|
AppState.activeMode = mode; |
|
|
AppState.dom.controlBtns.forEach(btn => btn.classList.toggle('active', btn.dataset.mode === mode)); |
|
|
}, |
|
|
|
|
|
saveSettings() { |
|
|
const newAiName = document.getElementById('ainame-settings-input').value.trim(); |
|
|
if (newAiName) { |
|
|
AppState.ai.name = newAiName; |
|
|
localStorage.setItem('synapseAI', JSON.stringify(AppState.ai)); |
|
|
this.updateOutput(`¡Listo! A partir de ahora me llamo ${newAiName}.`, true); |
|
|
} |
|
|
this.togglePanel('settings-screen', false); |
|
|
}, |
|
|
|
|
|
runContinuousAnalysis() { |
|
|
if (!AppState.isAwake || AppState.isProcessing || AppState.isSpeaking) return; |
|
|
|
|
|
AppState.isProcessing = true; |
|
|
AppState.dom.statusText.textContent = "Viendo..."; |
|
|
|
|
|
(async () => { |
|
|
try { |
|
|
const detectedObject = await AI_Core.detectObjects(); |
|
|
if (detectedObject && !MemoryManager.isRecent(detectedObject)) { |
|
|
const isNew = !MemoryManager.check(detectedObject); |
|
|
MemoryManager.add(detectedObject); |
|
|
|
|
|
let promptBase = ""; |
|
|
if (AppState.activeMode === 'eco') { |
|
|
promptBase = Prompts_AR.eco(detectedObject, isNew); |
|
|
} else if (AppState.activeMode === 'root') { |
|
|
promptBase = Prompts_AR.root(detectedObject, isNew); |
|
|
} |
|
|
const response = await AI_Core.generateText(promptBase); |
|
|
this.updateOutput(response, true); |
|
|
ConsciousnessMirror.log(AppState.activeMode, `vi un/a ${detectedObject} y te di una reflexión.`); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error("Error en el ciclo de análisis:", error); |
|
|
} finally { |
|
|
AppState.isProcessing = false; |
|
|
if(AppState.isAwake) AppState.dom.statusText.textContent = "Atento"; |
|
|
} |
|
|
})(); |
|
|
}, |
|
|
|
|
|
updateOutput(text, speak = false) { |
|
|
AppState.dom.outputContainer.style.animation = 'none'; |
|
|
AppState.dom.outputContainer.offsetHeight; |
|
|
AppState.dom.outputContainer.style.animation = 'slideUp 0.5s ease-out'; |
|
|
AppState.dom.outputText.textContent = text; |
|
|
if (speak) VoiceManager.speak(text); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const AI_Core = { |
|
|
async initModels() { |
|
|
LifecycleManager.updateOutput("Cargando modelo de visión...", false); |
|
|
AppState.models.detector = await pipeline('object-detection', 'Xenova/detr-resnet-50'); |
|
|
LifecycleManager.updateOutput("Cargando modelo de lenguaje...", false); |
|
|
AppState.models.textGenerator = await pipeline('text-generation', 'Xenova/Phi-3-mini-4k-instruct-onnx-web'); |
|
|
}, |
|
|
async detectObjects() { |
|
|
if (!AppState.isAwake || !AppState.models.detector || AppState.dom.cameraFeed.paused || AppState.dom.cameraFeed.ended) return null; |
|
|
const image = await RawImage.fromElement(AppState.dom.cameraFeed); |
|
|
const objects = await AppState.models.detector(image, { threshold: 0.9, percentage: true }); |
|
|
const mainObject = objects.filter(obj => obj.score > 0.9).sort((a,b) => b.score - a.score)[0]; |
|
|
return mainObject ? mainObject.label : null; |
|
|
}, |
|
|
async generateText(prompt) { |
|
|
if (!AppState.models.textGenerator) return "El modelo de lenguaje no está listo."; |
|
|
const formattedPrompt = `<|user|>\nSos un asistente de IA argentino, amable y canchero. Respondé de forma concisa. ${prompt}<|end|>\n<|assistant|>\n`; |
|
|
const result = await AppState.models.textGenerator(formattedPrompt, { max_new_tokens: 80 }); |
|
|
return result[0].generated_text.split('<|assistant|>').pop().trim(); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const CameraManager = { |
|
|
async start() { |
|
|
try { |
|
|
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false }); |
|
|
AppState.dom.cameraFeed.srcObject = stream; |
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error("Error al acceder a la cámara:", error); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
const MemoryManager = { |
|
|
add(objectLabel) { |
|
|
AppState.memory.interactions[objectLabel] = { |
|
|
timestamp: Date.now(), |
|
|
count: (AppState.memory.interactions[objectLabel]?.count || 0) + 1 |
|
|
}; |
|
|
localStorage.setItem('synapseMemory', JSON.stringify(AppState.memory)); |
|
|
}, |
|
|
check(objectLabel) { |
|
|
return AppState.memory.interactions[objectLabel] !== undefined; |
|
|
}, |
|
|
isRecent(objectLabel) { |
|
|
const interaction = AppState.memory.interactions[objectLabel]; |
|
|
return interaction && (Date.now() - interaction.timestamp < 30000); |
|
|
} |
|
|
}; |
|
|
|
|
|
const DigitalSerenity = { |
|
|
triggerConsciousMoment() { |
|
|
const text = Prompts_AR.serenity; |
|
|
LifecycleManager.updateOutput(text, true); |
|
|
AppState.dom.body.classList.add('serenity-nudge'); |
|
|
if ('vibrate' in navigator) navigator.vibrate([4000, 1000, 6000]); |
|
|
ConsciousnessMirror.log('Modo Zen', `se activó un momento de calma.`); |
|
|
setTimeout(() => AppState.dom.body.classList.remove('serenity-nudge'), 11000); |
|
|
} |
|
|
}; |
|
|
|
|
|
const ConsciousnessMirror = { |
|
|
log(source, reason) { |
|
|
const logEntry = { source, reason, timestamp: new Date().toLocaleTimeString() }; |
|
|
let logs = JSON.parse(localStorage.getItem('synapseLogs') || '[]'); |
|
|
logs.unshift(logEntry); |
|
|
logs = logs.slice(0, 20); |
|
|
localStorage.setItem('synapseLogs', JSON.stringify(logs)); |
|
|
this.render(); |
|
|
}, |
|
|
render() { |
|
|
const logs = JSON.parse(localStorage.getItem('synapseLogs') || '[]'); |
|
|
AppState.dom.mirrorLog.innerHTML = logs.map(entry => ` |
|
|
<div class="log-entry"> |
|
|
<strong>${entry.source} (${entry.timestamp})</strong> |
|
|
<p>${Prompts_AR.mirrorLog(entry.source, entry.reason)}</p> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
window.onload = () => { |
|
|
LifecycleManager.init(); |
|
|
}; |