| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 |
- 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<AmicaLifeEvents>;
- 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<AmicaLifeEvents>();
- 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<AmicaLifeEvents>();
- 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<AmicaLifeEvents>();
- 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<void>((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<boolean> {
- 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;
- }
- }
|