import { Queue } from "@/utils/queue"; import { wait } from "@/utils/wait"; import { pauseIdleTimer, resumeIdleTimer } from "@/utils/isIdle"; import { Chat, ChatConfig } from "@/features/chat/chat"; import { AmicaLifeEvents, idleEvents, handleIdleEvent, basedPrompt, TimestampedPrompt, } from "@/features/amicaLife/eventHandler"; import { Viewer } from "../vrmViewer/viewer"; export class AmicaLife { public initialized: boolean; public mainEvents: Queue; public viewer?: Viewer; public chat?: Chat; public setSubconciousLogs?: (subconciousLogs: TimestampedPrompt[]) => void; public isChatSpeaking?: boolean; public triggerMessage: boolean; public eventProcessing?: boolean; public isSleep: boolean; private isSettingOff: boolean; private isPause: boolean; private isProcessingEventRunning?: boolean; private isProcessingIdleRunning?: boolean; constructor() { this.initialized = false; this.mainEvents = new Queue(); this.triggerMessage = false; this.eventProcessing = false; this.isSleep = false; this.isPause = false; this.isSettingOff = false; this.isProcessingEventRunning = false; this.isProcessingIdleRunning = false; } public initialize( config: ChatConfig, viewer: Viewer, chat: Chat, isChatSpeaking: boolean, setSubconciousLogs: (subconciousLogs: TimestampedPrompt[]) => void, ) { this.viewer = viewer; this.chat = chat; this.isChatSpeaking = isChatSpeaking; this.setSubconciousLogs = setSubconciousLogs; this.loadIdleTextPrompt(null); // This loop will run depending on Amica Life Enabled/Disabled config this.processingIdle(config); this.initialized = true; } // These are function to coonfigure mainEvents queue // Function for loaded idle text prompt public async loadIdleTextPrompt(prompts: string[] | null) { if (prompts === null) { idleEvents.forEach((prompt) => this.mainEvents.enqueue({ events: prompt }), ); } else { if (prompts.length > 0) { this.mainEvents.clear(); prompts.forEach((prompt: string) => basedPrompt.idleTextPrompt.push(prompt), ); } } } // Function to insert event to the front of the mainEvents Queue public insertFront(event: AmicaLifeEvents) { const newQueue = new Queue(); newQueue.enqueue(event); while (!this.mainEvents.isEmpty()) { newQueue.enqueue(this.mainEvents.dequeue()!); } this.mainEvents = newQueue; } // Function to remove a specific event from the mainEvents queue public removeEvent(eventName: string) { const newQueue = new Queue(); let found = false; while (!this.mainEvents.isEmpty()) { const event = this.mainEvents.dequeue(); if (event && event.events !== eventName) { newQueue.enqueue(event); } else { found = true; } } this.mainEvents = newQueue; } // Function to check if a specific event exists in the mainEvents queue public containsEvent(eventName: string): boolean { let contains = false; this.mainEvents.forEach((event) => { if (event.events === eventName) { contains = true; } }); return contains; } // These are function to handle idle event // Function to check message from user public receiveMessageFromUser(message: string) { if (message.toLowerCase().includes("news")) { console.log("Triggering news function call."); this.insertFront({ events: "News" }); } // Re-enqueue subconcious event after get the user input (1 Subconcious events per idle cycle) !this.containsEvent("Subconcious") ? this.mainEvents.enqueue({ events: "Subconcious" }) : null; this.pause(); this.wakeFromSleep(); this.triggerMessage = true; } // Function handle when amica got poked in amica life event // public handlePoked() { // if (!this.chat?.isAwake() && config("amica_life_enabled") === "true") { // console.log("Handling idle event:", "I just poked you!"); // this.chat?.receiveMessageFromUser("I just poked you!",true); // } // } public async processingIdle(config: ChatConfig) { // Preventing duplicate processingIdle loop if (this.isProcessingIdleRunning) { return; } this.isProcessingIdleRunning = true; console.log("Starting Amica Life"); while (config.amica_life_params.amica_life_enabled === "true") { // Check if amica is in idle state trigger processingEvent loop if (!this.chat?.isAwake()) { this.processingEvent(config); } await wait(50); } this.isProcessingIdleRunning = false; this.isProcessingEventRunning = false; this.triggerMessage = false; console.log("Stopping idle loop"); } public async processingEvent(config: ChatConfig) { // Preventing duplicate processing event loop if (this.isProcessingEventRunning) { // Check for resume if (!(await this.checkResume())) { return; } return; } // User must start the conversation with amica first to activate amica life if (!this.triggerMessage) { return; } this.isProcessingEventRunning = true; while (this.isProcessingEventRunning) { // Wait for current event to finish before processing next event if ( this.chat!.speakJobs.size() < 1 && this.chat!.ttsJobs.size() < 1 && !this.isChatSpeaking && !this.eventProcessing ) { resumeIdleTimer(); // Check for pause and sleep await this.checkSleep(config); await this.checkPause(); // Random chance for doing nothing (25% chance) if (Math.random() <= 0.25) { // removed for staging usage //console.log("Handling idle event:", "Doing nothing this cycle"); await this.waitInterval(config); continue; } // Main event handling const idleEvent = this.mainEvents.dequeue(); if (idleEvent) { console.time(`processing_event ${idleEvent.events}`); this.eventProcessing = true; await handleIdleEvent( config, idleEvent, this, this.chat!, this.viewer!, ); !(idleEvent.events === "Subconcious" || idleEvent.events === "Sleep") ? this.mainEvents.enqueue(idleEvent) : null; } else { //removed for staging usage //console.log("Handling idle event:", "No idle events in queue"); } } else if ( this.chat!.speakJobs.size() > 0 || this.chat!.ttsJobs.size() > 0 || this.isChatSpeaking ) { pauseIdleTimer(); } await this.waitInterval(config); } this.isProcessingEventRunning = false; } public async pause() { await this.chat?.interrupt(); this.isPause = true; } public resume() { this.isPause = false; } // Function to check for sleep event if idleTime > time_to_sleep add Sleep event to the front of amica queue private async checkSleep(config: ChatConfig) { if (!this.isSleep) { const chat = this.chat; if (!chat) { console.error("Chat instance is not available"); return; } const idleTime = chat.idleTime(); // If character being idle morethan 120 sec or 2 min, play handle sleep event if (!this.containsEvent("Sleep")) { if (idleTime > parseInt(config.amica_life_params.time_to_sleep_sec)) { this.insertFront({ events: "Sleep" }); } } } } // Function to pause the processingEvent loop is pauseFlag is true/false private async checkPause() { if (this.isPause) { console.log("Amica Life Paused"); await new Promise((resolve) => { const checkPause = setInterval(() => { if (!this.isPause) { clearInterval(checkPause); resolve(console.log("Amica Life Initiated")); } }, 50); }); } } // Function to resume the processingEvent loop from pause private async checkResume(): Promise { if (this.isPause === true && !this.isSleep && this.isSettingOff) { this.resume(); return true; } return false; } // Function to pause/resume the loop when setting page is open/close public checkSettingOff(off: boolean) { if (off) { this.isSettingOff = true; this.wakeFromSleep(); this.chat?.updateAwake(); // Update awake when user exit the setting page this.resume(); } else { this.isSettingOff = false; this.pause(); } } // These is amica life utils // Update time before idle increase by 1.25 times // public updatedIdleTime() { // const idleTimeSec = Math.min( // parseInt(config("time_before_idle_sec")) * 1.25, // 240, // ); // // updateConfig("time_before_idle_sec", idleTimeSec.toString()); // // removed for staging // //console.log(`Updated time before idle to ${idleTimeSec} seconds`); // } public async waitInterval(config: ChatConfig) { const [minMs, maxMs] = [ parseInt(config.amica_life_params.min_time_interval_sec), parseInt(config.amica_life_params.max_time_interval_sec), ]; const interval = Math.floor(Math.random() * (maxMs - minMs + 1) + minMs) * 1000; return new Promise((resolve) => setTimeout(resolve, interval)); } public wakeFromSleep() { this.isSleep = false; this.viewer?.model?.playEmotion("Neutral"); } public async clean() { console.log("Stopping all AmicaLife processes..."); // Stop processing loops this.isProcessingEventRunning = false; this.isProcessingIdleRunning = false; // Pause Amica life this.isPause = true; // Interrupt any ongoing chat speech or TTS jobs if (this.chat) { await this.chat.interrupt(); // assuming interrupt stops speech & jobs this.chat.speakJobs.clear(); // clear any queued speak jobs if possible this.chat.ttsJobs.clear(); // clear TTS jobs queue if possible } // Clear the main event queue this.mainEvents.clear(); // Reset trigger and sleep flags this.triggerMessage = false; this.isSleep = false; this.isSettingOff = false; } }