amicaLife.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import { Queue } from "@/utils/queue";
  2. import { wait } from "@/utils/wait";
  3. import { pauseIdleTimer, resumeIdleTimer } from "@/utils/isIdle";
  4. import { Chat, ChatConfig } from "@/features/chat/chat";
  5. import {
  6. AmicaLifeEvents,
  7. idleEvents,
  8. handleIdleEvent,
  9. basedPrompt,
  10. TimestampedPrompt,
  11. } from "@/features/amicaLife/eventHandler";
  12. import { Viewer } from "../vrmViewer/viewer";
  13. export class AmicaLife {
  14. public initialized: boolean;
  15. public mainEvents: Queue<AmicaLifeEvents>;
  16. public viewer?: Viewer;
  17. public chat?: Chat;
  18. public setSubconciousLogs?: (subconciousLogs: TimestampedPrompt[]) => void;
  19. public isChatSpeaking?: boolean;
  20. public triggerMessage: boolean;
  21. public eventProcessing?: boolean;
  22. public isSleep: boolean;
  23. private isSettingOff: boolean;
  24. private isPause: boolean;
  25. private isProcessingEventRunning?: boolean;
  26. private isProcessingIdleRunning?: boolean;
  27. constructor() {
  28. this.initialized = false;
  29. this.mainEvents = new Queue<AmicaLifeEvents>();
  30. this.triggerMessage = false;
  31. this.eventProcessing = false;
  32. this.isSleep = false;
  33. this.isPause = false;
  34. this.isSettingOff = false;
  35. this.isProcessingEventRunning = false;
  36. this.isProcessingIdleRunning = false;
  37. }
  38. public initialize(
  39. config: ChatConfig,
  40. viewer: Viewer,
  41. chat: Chat,
  42. isChatSpeaking: boolean,
  43. setSubconciousLogs: (subconciousLogs: TimestampedPrompt[]) => void,
  44. ) {
  45. this.viewer = viewer;
  46. this.chat = chat;
  47. this.isChatSpeaking = isChatSpeaking;
  48. this.setSubconciousLogs = setSubconciousLogs;
  49. this.loadIdleTextPrompt(null);
  50. // This loop will run depending on Amica Life Enabled/Disabled config
  51. this.processingIdle(config);
  52. this.initialized = true;
  53. }
  54. // These are function to coonfigure mainEvents queue
  55. // Function for loaded idle text prompt
  56. public async loadIdleTextPrompt(prompts: string[] | null) {
  57. if (prompts === null) {
  58. idleEvents.forEach((prompt) =>
  59. this.mainEvents.enqueue({ events: prompt }),
  60. );
  61. } else {
  62. if (prompts.length > 0) {
  63. this.mainEvents.clear();
  64. prompts.forEach((prompt: string) =>
  65. basedPrompt.idleTextPrompt.push(prompt),
  66. );
  67. }
  68. }
  69. }
  70. // Function to insert event to the front of the mainEvents Queue
  71. public insertFront(event: AmicaLifeEvents) {
  72. const newQueue = new Queue<AmicaLifeEvents>();
  73. newQueue.enqueue(event);
  74. while (!this.mainEvents.isEmpty()) {
  75. newQueue.enqueue(this.mainEvents.dequeue()!);
  76. }
  77. this.mainEvents = newQueue;
  78. }
  79. // Function to remove a specific event from the mainEvents queue
  80. public removeEvent(eventName: string) {
  81. const newQueue = new Queue<AmicaLifeEvents>();
  82. let found = false;
  83. while (!this.mainEvents.isEmpty()) {
  84. const event = this.mainEvents.dequeue();
  85. if (event && event.events !== eventName) {
  86. newQueue.enqueue(event);
  87. } else {
  88. found = true;
  89. }
  90. }
  91. this.mainEvents = newQueue;
  92. }
  93. // Function to check if a specific event exists in the mainEvents queue
  94. public containsEvent(eventName: string): boolean {
  95. let contains = false;
  96. this.mainEvents.forEach((event) => {
  97. if (event.events === eventName) {
  98. contains = true;
  99. }
  100. });
  101. return contains;
  102. }
  103. // These are function to handle idle event
  104. // Function to check message from user
  105. public receiveMessageFromUser(message: string) {
  106. if (message.toLowerCase().includes("news")) {
  107. console.log("Triggering news function call.");
  108. this.insertFront({ events: "News" });
  109. }
  110. // Re-enqueue subconcious event after get the user input (1 Subconcious events per idle cycle)
  111. !this.containsEvent("Subconcious")
  112. ? this.mainEvents.enqueue({ events: "Subconcious" })
  113. : null;
  114. this.pause();
  115. this.wakeFromSleep();
  116. this.triggerMessage = true;
  117. }
  118. // Function handle when amica got poked in amica life event
  119. // public handlePoked() {
  120. // if (!this.chat?.isAwake() && config("amica_life_enabled") === "true") {
  121. // console.log("Handling idle event:", "I just poked you!");
  122. // this.chat?.receiveMessageFromUser("I just poked you!",true);
  123. // }
  124. // }
  125. public async processingIdle(config: ChatConfig) {
  126. // Preventing duplicate processingIdle loop
  127. if (this.isProcessingIdleRunning) {
  128. return;
  129. }
  130. this.isProcessingIdleRunning = true;
  131. console.log("Starting Amica Life");
  132. while (config.amica_life_params.amica_life_enabled === "true") {
  133. // Check if amica is in idle state trigger processingEvent loop
  134. if (!this.chat?.isAwake()) {
  135. this.processingEvent(config);
  136. }
  137. await wait(50);
  138. }
  139. this.isProcessingIdleRunning = false;
  140. this.isProcessingEventRunning = false;
  141. this.triggerMessage = false;
  142. console.log("Stopping idle loop");
  143. }
  144. public async processingEvent(config: ChatConfig) {
  145. // Preventing duplicate processing event loop
  146. if (this.isProcessingEventRunning) {
  147. // Check for resume
  148. if (!(await this.checkResume())) {
  149. return;
  150. }
  151. return;
  152. }
  153. // User must start the conversation with amica first to activate amica life
  154. if (!this.triggerMessage) {
  155. return;
  156. }
  157. this.isProcessingEventRunning = true;
  158. while (this.isProcessingEventRunning) {
  159. // Wait for current event to finish before processing next event
  160. if (
  161. this.chat!.speakJobs.size() < 1 &&
  162. this.chat!.ttsJobs.size() < 1 &&
  163. !this.isChatSpeaking &&
  164. !this.eventProcessing
  165. ) {
  166. resumeIdleTimer();
  167. // Check for pause and sleep
  168. await this.checkSleep(config);
  169. await this.checkPause();
  170. // Random chance for doing nothing (25% chance)
  171. if (Math.random() <= 0.25) {
  172. // removed for staging usage
  173. //console.log("Handling idle event:", "Doing nothing this cycle");
  174. await this.waitInterval(config);
  175. continue;
  176. }
  177. // Main event handling
  178. const idleEvent = this.mainEvents.dequeue();
  179. if (idleEvent) {
  180. console.time(`processing_event ${idleEvent.events}`);
  181. this.eventProcessing = true;
  182. await handleIdleEvent(
  183. config,
  184. idleEvent,
  185. this,
  186. this.chat!,
  187. this.viewer!,
  188. );
  189. !(idleEvent.events === "Subconcious" || idleEvent.events === "Sleep")
  190. ? this.mainEvents.enqueue(idleEvent)
  191. : null;
  192. } else {
  193. //removed for staging usage
  194. //console.log("Handling idle event:", "No idle events in queue");
  195. }
  196. } else if (
  197. this.chat!.speakJobs.size() > 0 ||
  198. this.chat!.ttsJobs.size() > 0 ||
  199. this.isChatSpeaking
  200. ) {
  201. pauseIdleTimer();
  202. }
  203. await this.waitInterval(config);
  204. }
  205. this.isProcessingEventRunning = false;
  206. }
  207. public async pause() {
  208. await this.chat?.interrupt();
  209. this.isPause = true;
  210. }
  211. public resume() {
  212. this.isPause = false;
  213. }
  214. // Function to check for sleep event if idleTime > time_to_sleep add Sleep event to the front of amica queue
  215. private async checkSleep(config: ChatConfig) {
  216. if (!this.isSleep) {
  217. const chat = this.chat;
  218. if (!chat) {
  219. console.error("Chat instance is not available");
  220. return;
  221. }
  222. const idleTime = chat.idleTime();
  223. // If character being idle morethan 120 sec or 2 min, play handle sleep event
  224. if (!this.containsEvent("Sleep")) {
  225. if (idleTime > parseInt(config.amica_life_params.time_to_sleep_sec)) {
  226. this.insertFront({ events: "Sleep" });
  227. }
  228. }
  229. }
  230. }
  231. // Function to pause the processingEvent loop is pauseFlag is true/false
  232. private async checkPause() {
  233. if (this.isPause) {
  234. console.log("Amica Life Paused");
  235. await new Promise<void>((resolve) => {
  236. const checkPause = setInterval(() => {
  237. if (!this.isPause) {
  238. clearInterval(checkPause);
  239. resolve(console.log("Amica Life Initiated"));
  240. }
  241. }, 50);
  242. });
  243. }
  244. }
  245. // Function to resume the processingEvent loop from pause
  246. private async checkResume(): Promise<boolean> {
  247. if (this.isPause === true && !this.isSleep && this.isSettingOff) {
  248. this.resume();
  249. return true;
  250. }
  251. return false;
  252. }
  253. // Function to pause/resume the loop when setting page is open/close
  254. public checkSettingOff(off: boolean) {
  255. if (off) {
  256. this.isSettingOff = true;
  257. this.wakeFromSleep();
  258. this.chat?.updateAwake(); // Update awake when user exit the setting page
  259. this.resume();
  260. } else {
  261. this.isSettingOff = false;
  262. this.pause();
  263. }
  264. }
  265. // These is amica life utils
  266. // Update time before idle increase by 1.25 times
  267. // public updatedIdleTime() {
  268. // const idleTimeSec = Math.min(
  269. // parseInt(config("time_before_idle_sec")) * 1.25,
  270. // 240,
  271. // );
  272. // // updateConfig("time_before_idle_sec", idleTimeSec.toString());
  273. // // removed for staging
  274. // //console.log(`Updated time before idle to ${idleTimeSec} seconds`);
  275. // }
  276. public async waitInterval(config: ChatConfig) {
  277. const [minMs, maxMs] = [
  278. parseInt(config.amica_life_params.min_time_interval_sec),
  279. parseInt(config.amica_life_params.max_time_interval_sec),
  280. ];
  281. const interval =
  282. Math.floor(Math.random() * (maxMs - minMs + 1) + minMs) * 1000;
  283. return new Promise((resolve) => setTimeout(resolve, interval));
  284. }
  285. public wakeFromSleep() {
  286. this.isSleep = false;
  287. this.viewer?.model?.playEmotion("Neutral");
  288. }
  289. public async clean() {
  290. console.log("Stopping all AmicaLife processes...");
  291. // Stop processing loops
  292. this.isProcessingEventRunning = false;
  293. this.isProcessingIdleRunning = false;
  294. // Pause Amica life
  295. this.isPause = true;
  296. // Interrupt any ongoing chat speech or TTS jobs
  297. if (this.chat) {
  298. await this.chat.interrupt(); // assuming interrupt stops speech & jobs
  299. this.chat.speakJobs.clear(); // clear any queued speak jobs if possible
  300. this.chat.ttsJobs.clear(); // clear TTS jobs queue if possible
  301. }
  302. // Clear the main event queue
  303. this.mainEvents.clear();
  304. // Reset trigger and sleep flags
  305. this.triggerMessage = false;
  306. this.isSleep = false;
  307. this.isSettingOff = false;
  308. }
  309. }