kiku-agent 1.3.1 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +69 -12
  2. package/package.json +5 -12
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/AGENTS.md +228 -0
  18. package/server/agent/eval/analyze.py +684 -0
  19. package/server/agent/eval/gates.json +15 -0
  20. package/server/agent/eval/modules/analysis/compare.js +198 -0
  21. package/server/agent/eval/modules/analysis/gates.js +72 -0
  22. package/server/agent/eval/modules/analysis/judge.js +70 -0
  23. package/server/agent/eval/modules/core/cli.js +453 -0
  24. package/server/agent/eval/modules/core/config.js +60 -0
  25. package/server/agent/eval/modules/core/store.js +236 -0
  26. package/server/agent/eval/modules/evaluation/cases.js +689 -0
  27. package/server/agent/eval/modules/evaluation/runner.js +288 -0
  28. package/server/agent/eval/modules/evaluation/scoring.js +363 -0
  29. package/server/agent/eval/modules/evaluation/worker.js +122 -0
  30. package/server/agent/eval/modules/reporting/benchmark.js +265 -0
  31. package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
  32. package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
  33. package/server/agent/eval/modules/server/evalApi.js +1099 -0
  34. package/server/agent/eval/modules/ui/evalUi.js +4799 -0
  35. package/server/agent/eval/modules/utils/smokes.js +119 -0
  36. package/server/agent/eventBus.js +1 -0
  37. package/server/agent/ink/main.jsx +196 -191
  38. package/server/agent/jobManager.js +11 -1
  39. package/server/agent/lanDiscovery.js +721 -23
  40. package/server/agent/llmClient.js +317 -71
  41. package/server/agent/lmStudioWakeup.js +517 -65
  42. package/server/agent/locationService.js +797 -0
  43. package/server/agent/mcpQuizData.js +509 -0
  44. package/server/agent/memory/contextAllocator.js +19 -7
  45. package/server/agent/memory.js +3 -1
  46. package/server/agent/modelCompatibility.js +352 -0
  47. package/server/agent/modelDefaults.js +1 -0
  48. package/server/agent/modelManager.js +46 -2
  49. package/server/agent/newsWatch.js +9 -3
  50. package/server/agent/processManager.js +229 -0
  51. package/server/agent/prompts.js +174 -8
  52. package/server/agent/routingInterceptor.js +9 -1
  53. package/server/agent/sanitizer.js +41 -6
  54. package/server/agent/searchQueryPlanner.js +202 -0
  55. package/server/agent/session.js +59 -4
  56. package/server/agent/tests/agentBubbleSystem.test.js +65 -0
  57. package/server/agent/tests/agentFollowUps.test.js +253 -0
  58. package/server/agent/tests/agentMusicControl.test.js +245 -0
  59. package/server/agent/tests/apiRouter.test.js +108 -0
  60. package/server/agent/tests/architectureFixes.test.js +81 -0
  61. package/server/agent/tests/architectureRefactoring.test.js +194 -0
  62. package/server/agent/tests/autoContext.test.js +176 -0
  63. package/server/agent/tests/autoResearch.test.js +64 -0
  64. package/server/agent/tests/bilibiliEmbed.test.js +296 -0
  65. package/server/agent/tests/catalogSync.test.js +130 -0
  66. package/server/agent/tests/chatHardening.test.js +121 -0
  67. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  68. package/server/agent/tests/citations.test.js +180 -0
  69. package/server/agent/tests/clarificationAnalytics.test.js +219 -0
  70. package/server/agent/tests/clarifyingQuestions.test.js +627 -0
  71. package/server/agent/tests/cliAutomation.test.js +158 -0
  72. package/server/agent/tests/cloudProviders.test.js +322 -0
  73. package/server/agent/tests/commandInput.test.js +24 -0
  74. package/server/agent/tests/commands.test.js +506 -0
  75. package/server/agent/tests/contextIntelligence.test.js +209 -0
  76. package/server/agent/tests/conversationSafety.test.js +53 -0
  77. package/server/agent/tests/cpuMetrics.test.js +101 -0
  78. package/server/agent/tests/decisionAndSearchEnhancements.test.js +167 -0
  79. package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
  80. package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
  81. package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
  82. package/server/agent/tests/edgeCasesScenarios.test.js +131 -0
  83. package/server/agent/tests/engineService.test.js +436 -0
  84. package/server/agent/tests/engineUnload.test.js +94 -0
  85. package/server/agent/tests/entertainmentMetrics.test.js +126 -0
  86. package/server/agent/tests/envBootstrap.test.js +87 -0
  87. package/server/agent/tests/envCommand.test.js +284 -0
  88. package/server/agent/tests/errorHandling.test.js +80 -0
  89. package/server/agent/tests/errors.test.js +13 -0
  90. package/server/agent/tests/evalAnalyze.test.js +54 -0
  91. package/server/agent/tests/evalCommand.test.js +54 -0
  92. package/server/agent/tests/evalCorpus.test.js +126 -0
  93. package/server/agent/tests/evalHarness.test.js +62 -0
  94. package/server/agent/tests/evalJudgeContext.test.js +38 -0
  95. package/server/agent/tests/evalMetrics.test.js +320 -0
  96. package/server/agent/tests/evalStore.test.js +190 -0
  97. package/server/agent/tests/evalStudio.test.js +179 -0
  98. package/server/agent/tests/feedbackLearning.test.js +186 -0
  99. package/server/agent/tests/fetchApi.test.js +99 -0
  100. package/server/agent/tests/firstRun.test.js +44 -0
  101. package/server/agent/tests/gemma4eOptimization.test.js +95 -0
  102. package/server/agent/tests/hardwareTiers.test.js +206 -0
  103. package/server/agent/tests/hybridWebSearch.test.js +194 -0
  104. package/server/agent/tests/judgeService.test.js +251 -0
  105. package/server/agent/tests/lanDiscovery.test.js +324 -0
  106. package/server/agent/tests/lanInterfaces.test.js +510 -0
  107. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  108. package/server/agent/tests/locationTool.test.js +278 -0
  109. package/server/agent/tests/mcpClient.test.js +201 -0
  110. package/server/agent/tests/memoryRoute.test.js +173 -0
  111. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  112. package/server/agent/tests/modelCompatibility.test.js +148 -0
  113. package/server/agent/tests/modelManager.test.js +141 -0
  114. package/server/agent/tests/multilingualScenarios.test.js +69 -0
  115. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  116. package/server/agent/tests/newCommands.test.js +347 -0
  117. package/server/agent/tests/newCommandsTui.test.js +141 -0
  118. package/server/agent/tests/newTools.test.js +113 -0
  119. package/server/agent/tests/newsIntelligence.test.js +178 -0
  120. package/server/agent/tests/nextGenArchitecture.test.js +101 -0
  121. package/server/agent/tests/offlineInput.test.js +217 -0
  122. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  123. package/server/agent/tests/openDomainSearch.test.js +121 -0
  124. package/server/agent/tests/packagingGuard.test.js +57 -0
  125. package/server/agent/tests/presentation.test.js +61 -0
  126. package/server/agent/tests/processManager.test.js +144 -0
  127. package/server/agent/tests/providerPersistence.test.js +44 -0
  128. package/server/agent/tests/queryBenchmarks.test.js +83 -0
  129. package/server/agent/tests/queryDetails.test.js +126 -0
  130. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  131. package/server/agent/tests/realWorldValidation.test.js +391 -0
  132. package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
  133. package/server/agent/tests/runtimeWatchdog.test.js +32 -0
  134. package/server/agent/tests/sanitizer.test.js +37 -0
  135. package/server/agent/tests/scenariosData.js +378 -0
  136. package/server/agent/tests/schemaRouting.test.js +54 -0
  137. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  138. package/server/agent/tests/securityAuditRemediation.test.js +221 -0
  139. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  140. package/server/agent/tests/securityHardening.test.js +146 -0
  141. package/server/agent/tests/securitySanitizer.test.js +38 -0
  142. package/server/agent/tests/sessionAndCopy.test.js +267 -0
  143. package/server/agent/tests/sidebarControls.test.js +135 -0
  144. package/server/agent/tests/statefulMemory.test.js +277 -0
  145. package/server/agent/tests/subagent.test.js +216 -0
  146. package/server/agent/tests/subagentProgress.test.js +109 -0
  147. package/server/agent/tests/subagentTextFallback.test.js +115 -0
  148. package/server/agent/tests/suggestions.test.js +199 -0
  149. package/server/agent/tests/suggestionsTui.test.js +251 -0
  150. package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
  151. package/server/agent/tests/terminalImage.test.js +80 -0
  152. package/server/agent/tests/terminalMarkdown.test.js +166 -0
  153. package/server/agent/tests/terminalText.test.js +133 -0
  154. package/server/agent/tests/textProtocolLoop.test.js +197 -0
  155. package/server/agent/tests/textToolParsing.test.js +187 -0
  156. package/server/agent/tests/theme.test.js +26 -0
  157. package/server/agent/tests/toolRegistryParity.test.js +53 -0
  158. package/server/agent/tests/toolRoutingRegressions.test.js +160 -0
  159. package/server/agent/tests/tools.test.js +259 -0
  160. package/server/agent/tests/unifiedEndpoints.test.js +159 -0
  161. package/server/agent/tests/v110Features.test.js +262 -0
  162. package/server/agent/tests/versionBump.test.js +69 -0
  163. package/server/agent/tests/vramEstimator.test.js +595 -0
  164. package/server/agent/tests/webChatDocs.test.js +199 -0
  165. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  166. package/server/agent/tests/webChatInspection.test.js +551 -0
  167. package/server/agent/tests/webChatMcp.test.js +94 -0
  168. package/server/agent/tests/webChatMemory.test.js +112 -0
  169. package/server/agent/tests/webChatSecurity.test.js +169 -0
  170. package/server/agent/tests/webExtract.test.js +284 -0
  171. package/server/agent/tests/webSources.test.js +281 -0
  172. package/server/agent/textToolProtocol.js +173 -46
  173. package/server/agent/toolRouter.js +318 -44
  174. package/server/agent/tools.js +877 -96
  175. package/server/agent/ui.js +39 -34
  176. package/server/agent/vramService.js +35 -54
  177. package/server/agent/webSearch.js +441 -162
  178. package/server/agent/websearch/orchestrator.js +15 -5
  179. package/server/agent/websearch/resilience/cache.js +9 -2
  180. package/server/agent/websearch/security/ssrf.js +41 -10
  181. package/server/agent/youtubeAuth.js +5 -20
  182. package/server/apiRouter.js +5558 -4422
  183. package/server/eval/judgeRubrics.js +110 -0
  184. package/server/eval/judgeService.js +290 -0
  185. package/server/eval/runBenchmark.js +182 -0
  186. package/server/mcp/data/discography.js +2 -2
  187. package/server/mcp/examples/antigravity.example.json +17 -0
  188. package/server/mcp/examples/librechat.example.yaml +16 -0
  189. package/server/mcp/handler.js +8 -4
  190. package/server/mcp/security.js +220 -21
  191. package/server/mcp/server.js +2 -1
  192. package/server/mcp/tests/connection.test.js +162 -0
  193. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  194. package/server/mcp/tools.js +7 -1
  195. package/server/supabase_migration_prefix.sql +102 -0
  196. package/server/supabase_schema.sql +63 -0
  197. package/server/web-chat/core/connection.js +179 -218
  198. package/server/web-chat/core/conversationContext.js +9 -2
  199. package/server/web-chat/core/directCatalogEngine.js +52 -28
  200. package/server/web-chat/core/dramaKnowledge.js +33 -22
  201. package/server/web-chat/core/followUpEngine.js +735 -81
  202. package/server/web-chat/core/futureEventGuard.js +5 -1
  203. package/server/web-chat/core/musicControlAction.js +70 -0
  204. package/server/web-chat/core/security.js +32 -21
  205. package/server/web-chat/core/toolInspector.js +6 -6
  206. package/server/web-chat/core/urlGuard.js +45 -0
  207. package/server/web-chat/router.js +204 -157
  208. package/server/web-chat/routes/chatRoute.js +885 -488
  209. package/server/web-chat/routes/engineRoute.js +270 -0
  210. package/server/web-chat/routes/exportRoute.js +1 -1
  211. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  212. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  213. package/server/web-chat/routes/mcpRoute.js +386 -0
  214. package/server/web-chat/routes/memoryRoute.js +253 -0
  215. package/server/web-chat/routes/providerRoute.js +49 -16
  216. package/server/web-chat/routes/staticRoute.js +306 -17
  217. package/server/web-chat/routes/statusRoute.js +194 -73
  218. package/server/web-chat/routes/suggestRoute.js +11 -3
  219. package/server/web-chat/routes/verifyRoute.js +26 -4
  220. package/server/web-chat/server.js +7 -6
  221. package/server/web-chat/ui/docsBuilder.js +3558 -0
  222. package/server/web-chat/ui/htmlBuilder.js +12 -0
  223. package/server/web-chat/ui/scripts/client.js +3401 -1302
  224. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  225. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  226. package/server/web-chat/ui/styles/lan-card.css +165 -0
  227. package/server/web-chat/ui/styles/main.css +4289 -447
  228. package/server/web-chat/ui/sw.js +64 -0
  229. package/server/web-chat/ui/templates/body.html +407 -281
  230. package/src/data/awardsData.js +146 -146
  231. package/src/data/musicVideoData.js +6 -4
  232. package/src/lib/vram/estimator.js +195 -19
@@ -1,7 +1,10 @@
1
1
  import { spawn } from 'child_process';
2
- import { existsSync, unlinkSync, readdirSync, statSync } from 'node:fs';
2
+ import { killProcessTreeSync } from './processManager.js';
3
+ import { existsSync, unlinkSync, readdirSync, statSync, rmSync } from 'node:fs';
3
4
  import { join, dirname, basename } from 'node:path';
4
5
  import { ensureYouTubeCookies, getCookieStatus, isCookieFileValid } from './youtubeAuth.js';
6
+ import { discographyData } from '../../src/data/discographyData.js';
7
+ import { musicVideoData } from '../../src/data/musicVideoData.js';
5
8
 
6
9
  let _ffmpeg = null;
7
10
  async function loadFfmpeg() {
@@ -98,9 +101,23 @@ function ytDlpTry(query, opts) {
98
101
  let errBuf = '';
99
102
  proc.stdout.once('data', () => { gotData = true; resolve(proc); });
100
103
  proc.stderr.on('data', d => { errBuf += d.toString(); });
101
- proc.on('error', e => { if (!gotData) { try { proc.kill('SIGKILL'); } catch {} reject(e); } });
104
+ proc.on('error', e => {
105
+ if (!gotData) {
106
+ try {
107
+ if (proc.pid) killProcessTreeSync(proc.pid);
108
+ proc.kill('SIGKILL');
109
+ } catch {}
110
+ reject(e);
111
+ }
112
+ });
102
113
  proc.on('close', code => {
103
- if (!gotData) { try { proc.kill('SIGKILL'); } catch {} reject(new Error(errBuf.trim() || `yt-dlp exited with code ${code}`)); }
114
+ if (!gotData) {
115
+ try {
116
+ if (proc.pid) killProcessTreeSync(proc.pid);
117
+ proc.kill('SIGKILL');
118
+ } catch {}
119
+ reject(new Error(errBuf.trim() || `yt-dlp exited with code ${code}`));
120
+ }
104
121
  });
105
122
  });
106
123
  }
@@ -257,6 +274,9 @@ class AudioPlayer {
257
274
  this.isPlaying = false;
258
275
  this.isPaused = false;
259
276
  this.isMuted = false;
277
+ this.volume = 100;
278
+ this.repeatMode = 'off';
279
+ this.isShuffle = false;
260
280
  this.queue = [];
261
281
  this.currentIndex = -1;
262
282
  this._advancing = false;
@@ -274,11 +294,18 @@ class AudioPlayer {
274
294
  this.speaker = null;
275
295
  }
276
296
  if (this._ytDlpProc) {
277
- try { this._ytDlpProc.kill('SIGKILL'); } catch {}
297
+ try {
298
+ if (this._ytDlpProc.pid) killProcessTreeSync(this._ytDlpProc.pid);
299
+ this._ytDlpProc.kill('SIGKILL');
300
+ } catch {}
278
301
  this._ytDlpProc = null;
279
302
  }
280
303
  if (this.ffmpegProcess) {
281
- try { this.ffmpegProcess.kill('SIGKILL'); } catch {}
304
+ try {
305
+ const fPid = this.ffmpegProcess.pid || this.ffmpegProcess.ffmpegProc?.pid;
306
+ if (fPid) killProcessTreeSync(fPid);
307
+ this.ffmpegProcess.kill('SIGKILL');
308
+ } catch {}
282
309
  this.ffmpegProcess = null;
283
310
  }
284
311
  if (this.ytdlStream) {
@@ -300,6 +327,11 @@ class AudioPlayer {
300
327
  try { unlinkSync(join(dir, entry)); } catch {}
301
328
  }
302
329
  }
330
+ try {
331
+ if (basename(dir).startsWith('kiku-audio-')) {
332
+ rmSync(dir, { recursive: true, force: true });
333
+ }
334
+ } catch {}
303
335
  } catch {}
304
336
  this._tempFile = null;
305
337
  }
@@ -347,6 +379,11 @@ class AudioPlayer {
347
379
 
348
380
  if (!downloaded) {
349
381
  console.error(' [WARNING] yt-dlp could not download a playable audio source.');
382
+ try {
383
+ if (basename(tmpDir).startsWith('kiku-audio-')) {
384
+ rmSync(tmpDir, { recursive: true, force: true });
385
+ }
386
+ } catch {}
350
387
  tempFile = null;
351
388
  }
352
389
  } catch (err) {
@@ -376,7 +413,8 @@ class AudioPlayer {
376
413
 
377
414
  this.speaker = speaker;
378
415
 
379
- const volumeFilter = this.isMuted ? 'volume=0' : 'volume=1';
416
+ const volFactor = (Math.max(0, Math.min(100, Number(this.volume) || 0)) / 100).toFixed(2);
417
+ const volumeFilter = this.isMuted ? 'volume=0' : `volume=${volFactor}`;
380
418
 
381
419
  const ffmpeg = await loadFfmpeg();
382
420
  let ff = ffmpeg(tempFile);
@@ -415,7 +453,11 @@ class AudioPlayer {
415
453
  this.isPaused = false;
416
454
  resolve();
417
455
  } catch (err) {
456
+ if (!this._tempFile && tempFile) {
457
+ this._tempFile = tempFile;
458
+ }
418
459
  this._cleanup();
460
+ this._cleanupTempFile();
419
461
  this.isPlaying = false;
420
462
  this.isPaused = false;
421
463
  this._advancing = false;
@@ -466,13 +508,29 @@ class AudioPlayer {
466
508
  }
467
509
 
468
510
  next() {
511
+ if (this.isShuffle && this.queue.length > 1) {
512
+ let rand = Math.floor(Math.random() * this.queue.length);
513
+ if (rand === this.currentIndex) rand = (rand + 1) % this.queue.length;
514
+ this._cleanup();
515
+ this.currentIndex = rand;
516
+ this.currentTrack = this.queue[this.currentIndex];
517
+ this._playPromise = this._createStream(this.currentTrack).catch(() => {});
518
+ return `Playing next track [${this.currentIndex + 1}/${this.queue.length}]: ${this.currentTrack}`;
519
+ }
469
520
  if (this.currentIndex < this.queue.length - 1) {
470
521
  this._cleanup();
471
522
  this.currentIndex++;
472
523
  this.currentTrack = this.queue[this.currentIndex];
473
- this._playPromise = this._createStream(this.currentTrack);
524
+ this._playPromise = this._createStream(this.currentTrack).catch(() => {});
474
525
  return `Playing next track [${this.currentIndex + 1}/${this.queue.length}]: ${this.currentTrack}`;
475
526
  }
527
+ if (this.repeatMode === 'all' && this.queue.length > 0) {
528
+ this._cleanup();
529
+ this.currentIndex = 0;
530
+ this.currentTrack = this.queue[0];
531
+ this._playPromise = this._createStream(this.currentTrack).catch(() => {});
532
+ return `Playing next track [1/${this.queue.length}]: ${this.currentTrack}`;
533
+ }
476
534
  return 'No next track in queue';
477
535
  }
478
536
 
@@ -481,7 +539,14 @@ class AudioPlayer {
481
539
  this._cleanup();
482
540
  this.currentIndex--;
483
541
  this.currentTrack = this.queue[this.currentIndex];
484
- this._playPromise = this._createStream(this.currentTrack);
542
+ this._playPromise = this._createStream(this.currentTrack).catch(() => {});
543
+ return `Playing previous track [${this.currentIndex + 1}/${this.queue.length}]: ${this.currentTrack}`;
544
+ }
545
+ if (this.repeatMode === 'all' && this.queue.length > 0) {
546
+ this._cleanup();
547
+ this.currentIndex = this.queue.length - 1;
548
+ this.currentTrack = this.queue[this.currentIndex];
549
+ this._playPromise = this._createStream(this.currentTrack).catch(() => {});
485
550
  return `Playing previous track [${this.currentIndex + 1}/${this.queue.length}]: ${this.currentTrack}`;
486
551
  }
487
552
  return 'No previous track in queue';
@@ -555,7 +620,7 @@ class AudioPlayer {
555
620
  if (this._tempFile) {
556
621
  this._replayFromTemp();
557
622
  } else {
558
- this._playPromise = this._createStream(this.currentTrack);
623
+ this._playPromise = this._createStream(this.currentTrack).catch(() => {});
559
624
  }
560
625
  const pos = formatSeconds(this._currentOffsetSeconds);
561
626
  return `Resumed: ${this.currentTrack} (from ${pos})`;
@@ -587,6 +652,49 @@ class AudioPlayer {
587
652
  return `Unmuted: ${this.currentTrack}`;
588
653
  }
589
654
 
655
+ setVolume(volume) {
656
+ const v = Math.max(0, Math.min(100, Number(volume) || 0));
657
+ this.volume = v;
658
+ if (v > 0 && this.isMuted) this.isMuted = false;
659
+ if (this._playbackStartedAt > 0 && !this.isPaused) {
660
+ this._currentOffsetSeconds += (Date.now() - this._playbackStartedAt) / 1000;
661
+ this._playbackStartedAt = Date.now();
662
+ }
663
+ if (this._tempFile && !this.isPaused) {
664
+ this._replayFromTemp();
665
+ }
666
+ return `Volume set to ${v}%`;
667
+ }
668
+
669
+ seek(seconds) {
670
+ const sec = Math.max(0, Number(seconds) || 0);
671
+ this._currentOffsetSeconds = sec;
672
+ this._playbackStartedAt = this.isPaused ? 0 : Date.now();
673
+ if (this._tempFile && !this.isPaused) {
674
+ this._replayFromTemp();
675
+ }
676
+ return `Seeked to ${formatSeconds(sec)}`;
677
+ }
678
+
679
+ setRepeatMode(mode) {
680
+ const m = String(mode || '').toLowerCase().trim();
681
+ if (['one', 'all', 'off'].includes(m)) {
682
+ this.repeatMode = m;
683
+ } else if (m === 'repeat_one' || m === 'single') {
684
+ this.repeatMode = 'one';
685
+ } else if (m === 'repeat_all' || m === 'loop') {
686
+ this.repeatMode = 'all';
687
+ } else {
688
+ this.repeatMode = this.repeatMode === 'off' ? 'all' : (this.repeatMode === 'all' ? 'one' : 'off');
689
+ }
690
+ return `Repeat mode: ${this.repeatMode}`;
691
+ }
692
+
693
+ setShuffleMode(enabled) {
694
+ this.isShuffle = enabled !== undefined ? Boolean(enabled) : !this.isShuffle;
695
+ return `Shuffle mode: ${this.isShuffle ? 'on' : 'off'}`;
696
+ }
697
+
590
698
  _replayFromTemp() {
591
699
  this._startTempPlayback().catch((err) => {
592
700
  console.error('Replay error:', err.message);
@@ -606,7 +714,11 @@ class AudioPlayer {
606
714
  this.speaker = null;
607
715
  }
608
716
  if (this.ffmpegProcess) {
609
- try { this.ffmpegProcess.kill('SIGKILL'); } catch {}
717
+ try {
718
+ const fPid = this.ffmpegProcess.pid || this.ffmpegProcess.ffmpegProc?.pid;
719
+ if (fPid) killProcessTreeSync(fPid);
720
+ this.ffmpegProcess.kill('SIGKILL');
721
+ } catch {}
610
722
  this.ffmpegProcess = null;
611
723
  }
612
724
 
@@ -618,7 +730,8 @@ class AudioPlayer {
618
730
  });
619
731
  this.speaker = speaker;
620
732
 
621
- const volumeFilter = this.isMuted ? 'volume=0' : 'volume=1';
733
+ const volFactor = (Math.max(0, Math.min(100, Number(this.volume) || 0)) / 100).toFixed(2);
734
+ const volumeFilter = this.isMuted ? 'volume=0' : `volume=${volFactor}`;
622
735
 
623
736
  this._intentionalStop = false;
624
737
  const ffmpeg = await loadFfmpeg();
@@ -715,10 +828,20 @@ class AudioPlayer {
715
828
  }
716
829
 
717
830
  getFullState() {
831
+ let currentPos = this._currentOffsetSeconds;
832
+ if (!this.isPaused && this._playbackStartedAt > 0) {
833
+ currentPos += (Date.now() - this._playbackStartedAt) / 1000;
834
+ }
718
835
  return {
719
836
  isPlaying: this.isPlaying,
720
837
  isPaused: this.isPaused,
838
+ isMuted: this.isMuted,
839
+ volume: this.volume,
840
+ repeatMode: this.repeatMode,
841
+ isShuffle: this.isShuffle,
721
842
  currentTrack: this.currentTrack,
843
+ currentTime: Math.floor(currentPos),
844
+ currentTimeFormatted: formatSeconds(currentPos),
722
845
  queue: this.queue,
723
846
  currentIndex: this.currentIndex,
724
847
  queueLength: this.queue.length
@@ -727,15 +850,219 @@ class AudioPlayer {
727
850
 
728
851
  _autoNext() {
729
852
  if (this._advancing) return;
853
+ const playNext = (track) => {
854
+ this._createStream(track).catch(() => {});
855
+ };
856
+ if (this.repeatMode === 'one' && this.currentTrack) {
857
+ this._advancing = true;
858
+ playNext(this.currentTrack);
859
+ return;
860
+ }
861
+ if (this.isShuffle && this.queue.length > 1) {
862
+ let rand = Math.floor(Math.random() * this.queue.length);
863
+ if (rand === this.currentIndex) rand = (rand + 1) % this.queue.length;
864
+ this._advancing = true;
865
+ this.currentIndex = rand;
866
+ this.currentTrack = this.queue[this.currentIndex];
867
+ playNext(this.currentTrack);
868
+ return;
869
+ }
730
870
  if (this.queue.length > 0 && this.currentIndex < this.queue.length - 1) {
731
871
  this._advancing = true;
732
872
  this.currentIndex++;
733
873
  this.currentTrack = this.queue[this.currentIndex];
734
- this._createStream(this.currentTrack);
874
+ playNext(this.currentTrack);
875
+ } else if (this.repeatMode === 'all' && this.queue.length > 0) {
876
+ this._advancing = true;
877
+ this.currentIndex = 0;
878
+ this.currentTrack = this.queue[0];
879
+ playNext(this.currentTrack);
735
880
  } else {
736
881
  this.currentTrack = null;
737
882
  }
738
883
  }
739
884
  }
740
885
 
886
+ export function resolveCatalogSong(query = '') {
887
+ const q = String(query || '').trim().toLowerCase();
888
+ if (!q) return null;
889
+ const isRandom = q === 'random' || q === 'สุ่ม' || q === 'สุ่มเพลง' || q === 'เพลงอะไรก็ได้';
890
+ if (isRandom) {
891
+ const list = musicVideoData.filter(m => m.youtubeId);
892
+ if (list.length > 0) {
893
+ const picked = list[Math.floor(Math.random() * list.length)];
894
+ return {
895
+ id: picked.id,
896
+ title: picked.titleCn || picked.titleTh || picked.titleEn,
897
+ titleTh: picked.titleTh || '',
898
+ titleCn: picked.titleCn || '',
899
+ titleEn: picked.titleEn || '',
900
+ youtubeId: picked.youtubeId,
901
+ duration: picked.duration || '',
902
+ category: picked.category || 'Single',
903
+ dramaName: picked.dramaName || ''
904
+ };
905
+ }
906
+ }
907
+
908
+ const cleanQ = q
909
+ .replace(/(?:เพลง|ข้ามเพลง|เล่นเพลง|เปิดเพลง|ฟังเพลง|ost|mv|เพลงประกอบ|ซิงเกิล|โซโล่|จวีจิ้งอี|ju jingyi|kiku)/gi, '')
910
+ .trim();
911
+ const searchTerms = [q, cleanQ].filter(Boolean);
912
+
913
+ for (const item of musicVideoData) {
914
+ if (!item.youtubeId) continue;
915
+ const th = (item.titleTh || '').toLowerCase();
916
+ const cn = (item.titleCn || '').toLowerCase();
917
+ const en = (item.titleEn || '').toLowerCase();
918
+ const py = (item.titlePinyin || '').toLowerCase();
919
+ const drama = (item.dramaName || '').toLowerCase();
920
+ const aliases = (item.aliases || []).map(a => String(a).toLowerCase());
921
+ const keywords = (item.keywords || []).map(k => String(k).toLowerCase());
922
+ const hit = (term) => (field) => {
923
+ if (!field) return false;
924
+ if (field.includes(term)) return true;
925
+ return field.length >= 2 && term.includes(field);
926
+ };
927
+ for (const term of searchTerms) {
928
+ const matchTerm = hit(term);
929
+ if (
930
+ matchTerm(cn) ||
931
+ matchTerm(th) ||
932
+ matchTerm(en) ||
933
+ matchTerm(py) ||
934
+ matchTerm(drama) ||
935
+ aliases.some(matchTerm) ||
936
+ keywords.some(matchTerm)
937
+ ) {
938
+ return {
939
+ id: item.id,
940
+ title: item.titleCn || item.titleTh || item.titleEn,
941
+ titleTh: item.titleTh || '',
942
+ titleCn: item.titleCn || '',
943
+ titleEn: item.titleEn || '',
944
+ youtubeId: item.youtubeId,
945
+ duration: item.duration || '',
946
+ category: item.category || 'Single',
947
+ dramaName: item.dramaName || ''
948
+ };
949
+ }
950
+ }
951
+ }
952
+
953
+ if (isRandom || cleanQ === '' || cleanQ === 'เพลง') {
954
+ const list = musicVideoData.filter(m => m.youtubeId);
955
+ if (list.length > 0) {
956
+ const picked = list[Math.floor(Math.random() * list.length)];
957
+ return {
958
+ id: picked.id,
959
+ title: picked.titleCn || picked.titleTh || picked.titleEn,
960
+ titleTh: picked.titleTh || '',
961
+ titleCn: picked.titleCn || '',
962
+ titleEn: picked.titleEn || '',
963
+ youtubeId: picked.youtubeId,
964
+ duration: picked.duration || '',
965
+ category: picked.category || 'Single',
966
+ dramaName: picked.dramaName || ''
967
+ };
968
+ }
969
+ }
970
+
971
+ const discoList = Array.isArray(discographyData)
972
+ ? discographyData
973
+ : Object.values(discographyData || {}).filter(Array.isArray).flat();
974
+
975
+ for (const song of discoList) {
976
+ if (!song.youtubeId) continue;
977
+ const th = (song.titleTh || '').toLowerCase();
978
+ const cn = (song.titleCn || '').toLowerCase();
979
+ const en = (song.titleEn || '').toLowerCase();
980
+ const album = (song.album || song.work || '').toLowerCase();
981
+ const aliases = (song.aliases || []).map(a => String(a).toLowerCase());
982
+ const keywords = (song.keywords || []).map(k => String(k).toLowerCase());
983
+ for (const term of searchTerms) {
984
+ if (
985
+ (cn && cn.includes(term)) ||
986
+ (th && th.includes(term)) ||
987
+ (en && en.includes(term)) ||
988
+ (album && album.includes(term)) ||
989
+ aliases.some(a => a.includes(term)) ||
990
+ keywords.some(k => k.includes(term))
991
+ ) {
992
+ return {
993
+ id: song.id,
994
+ title: song.titleCn || song.titleTh || song.titleEn,
995
+ titleTh: song.titleTh || '',
996
+ titleCn: song.titleCn || '',
997
+ titleEn: song.titleEn || '',
998
+ youtubeId: song.youtubeId,
999
+ duration: song.trackDuration || song.duration || '',
1000
+ category: song.category || song.type || 'Single',
1001
+ dramaName: song.work || song.album || ''
1002
+ };
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ return null;
1008
+ }
1009
+
1010
+ export function evaluateMusicControlIntent(query = '') {
1011
+ const text = String(query || '').trim().toLowerCase();
1012
+ if (!text) return null;
1013
+
1014
+ const actions = [
1015
+ { action: 'pause', tool: 'pause_music', regex: /(?:หยุดเพลง|พักเพลง|pause|\bhold\b|暂停)/i, weight: 5 },
1016
+ { action: 'resume', tool: 'resume_music', regex: /(?:เล่นต่อ|resume|继续)/i, weight: 5 },
1017
+ { action: 'stop', tool: 'stop_music', regex: /(?:ปิดเพลง|เลิกฟังเพลง|stop\s*music|停止)/i, weight: 5 },
1018
+ { action: 'next', tool: 'next_track', regex: /(?:ข้ามเพลง|เพลงถัดไป|next\s*track|next\s*song|下一首)/i, weight: 5 },
1019
+ { action: 'prev', tool: 'prev_track', regex: /(?:เพลงก่อนหน้า|เพลงที่แล้ว|prev\s*track|previous\s*song|上一首)/i, weight: 5 },
1020
+ { action: 'mute', tool: 'mute_music', regex: /(?:ปิดเสียง|\bmute\b|静音)/i, weight: 5 },
1021
+ { action: 'unmute', tool: 'unmute_music', regex: /(?:เปิดเสียง|\bunmute\b|取消静音)/i, weight: 5 },
1022
+ { action: 'set_volume', tool: 'set_volume', regex: /(?:ปรับเสียง|เพิ่มเสียง|ลดเสียง|\bvolume\b|音量)/i, weight: 4 },
1023
+ { action: 'seek', tool: 'seek_music', regex: /(?:กรอเพลง|วินาทีที่|นาทีที่|\bseek\b|快进)/i, weight: 4 },
1024
+ { action: 'set_repeat', tool: 'set_repeat_mode', regex: /(?:วนซ้ำ|\brepeat\b|\bloop\b|循环)/i, weight: 4 },
1025
+ { action: 'set_shuffle', tool: 'set_shuffle_mode', regex: /(?:สุ่มเพลง|\bshuffle\b|随机)/i, weight: 4 },
1026
+ { action: 'toggle_video', tool: 'toggle_video_mode', regex: /(?:เปิด\s*mv|ดู\s*mv|สลับ\s*mv|toggle_video)/i, weight: 4 },
1027
+ { action: 'play', tool: 'play_music', regex: /(?:เล่นเพลง|เปิดเพลง|ฟังเพลง|play\s*song|放歌|听歌)/i, weight: 3 },
1028
+ ];
1029
+
1030
+ let best = null;
1031
+ let maxScore = 0;
1032
+
1033
+ for (const item of actions) {
1034
+ if (item.regex.test(text)) {
1035
+ if (item.weight > maxScore) {
1036
+ maxScore = item.weight;
1037
+ best = item;
1038
+ }
1039
+ }
1040
+ }
1041
+
1042
+ if (!best) return null;
1043
+
1044
+ const result = { action: best.action, tool: best.tool, score: maxScore };
1045
+
1046
+ if (best.action === 'set_volume') {
1047
+ const volMatch = text.match(/(?:ระดับ|volume|เสียง)?\s*(\d{1,3})\s*%?/);
1048
+ if (volMatch) {
1049
+ result.volume = Math.max(0, Math.min(100, Number(volMatch[1])));
1050
+ }
1051
+ }
1052
+
1053
+ if (best.action === 'seek') {
1054
+ const secMatch = text.match(/(\d{1,4})\s*(?:วินาที|sec|s)/);
1055
+ if (secMatch) {
1056
+ result.seconds = Number(secMatch[1]);
1057
+ } else {
1058
+ const minSecMatch = text.match(/(\d{1,2}):(\d{2})/);
1059
+ if (minSecMatch) {
1060
+ result.seconds = Number(minSecMatch[1]) * 60 + Number(minSecMatch[2]);
1061
+ }
1062
+ }
1063
+ }
1064
+
1065
+ return result;
1066
+ }
1067
+
741
1068
  export const audioPlayer = new AudioPlayer();
@@ -0,0 +1,193 @@
1
+ import { computeVramEstimate } from './vramService.js';
2
+
3
+ export const MODEL_FOLLOW_CTX_CEILING = 131072;
4
+ export const MODEL_FOLLOW_CTX_FLOOR = 4096;
5
+ export const MODEL_FOLLOW_CTX_FALLBACK = 32768;
6
+ export const MODEL_FOLLOW_CTX_LADDER = Object.freeze([131072, 65536, 32768, 16384, 8192, 4096]);
7
+
8
+ const FIT_ZONES = new Set(['GREEN', 'YELLOW']);
9
+
10
+ function toPositiveInt(value) {
11
+ const n = Math.floor(Number(value));
12
+ return Number.isFinite(n) && n > 0 ? n : null;
13
+ }
14
+
15
+ export function parseAutoContextFlag(value, defaultValue = false) {
16
+ if (value === undefined || value === null || value === '') return defaultValue;
17
+ if (typeof value === 'boolean') return value;
18
+ const text = String(value).trim().toLowerCase();
19
+ if (['true', '1', 'yes', 'on', 'auto'].includes(text)) return true;
20
+ if (['false', '0', 'no', 'off'].includes(text)) return false;
21
+ return defaultValue;
22
+ }
23
+
24
+ export function resolveModelFollowContext({
25
+ modelMaxContext = null,
26
+ loadedContext = null,
27
+ ceiling = MODEL_FOLLOW_CTX_CEILING,
28
+ floor = MODEL_FOLLOW_CTX_FLOOR,
29
+ fallback = MODEL_FOLLOW_CTX_FALLBACK,
30
+ } = {}) {
31
+ const max = toPositiveInt(modelMaxContext);
32
+ const loaded = toPositiveInt(loadedContext);
33
+ const hi = Math.min(toPositiveInt(ceiling) || MODEL_FOLLOW_CTX_CEILING, MODEL_FOLLOW_CTX_CEILING);
34
+ const lo = Math.max(toPositiveInt(floor) || MODEL_FOLLOW_CTX_FLOOR, 1024);
35
+ const base = toPositiveInt(fallback) || MODEL_FOLLOW_CTX_FALLBACK;
36
+
37
+ const usable = Math.min(max ?? loaded ?? base, hi);
38
+ const target = Math.max(usable, lo);
39
+ return {
40
+ target,
41
+ modelMax: max,
42
+ loaded,
43
+ ceiling: hi,
44
+ floor: lo,
45
+ cappedByCeiling: (max ?? loaded ?? base) > hi,
46
+ raisedToFloor: usable < lo,
47
+ source: max ? 'model-max' : (loaded ? 'loaded-context' : 'fallback'),
48
+ };
49
+ }
50
+
51
+ function ladderCandidates(modelMax, ceiling, floor) {
52
+ const hi = Math.min(
53
+ toPositiveInt(modelMax) || MODEL_FOLLOW_CTX_FALLBACK,
54
+ toPositiveInt(ceiling) || MODEL_FOLLOW_CTX_CEILING,
55
+ MODEL_FOLLOW_CTX_CEILING,
56
+ );
57
+ const lo = Math.max(toPositiveInt(floor) || MODEL_FOLLOW_CTX_FLOOR, 1024);
58
+ const picks = MODEL_FOLLOW_CTX_LADDER.filter(ctx => ctx <= hi && ctx >= lo);
59
+ if (picks.length === 0) picks.push(Math.max(lo, Math.min(hi, MODEL_FOLLOW_CTX_FALLBACK)));
60
+ return { picks, hi, lo };
61
+ }
62
+
63
+ export async function pickFittingLoadContext({
64
+ model,
65
+ quant = null,
66
+ modelMax = null,
67
+ ceiling = MODEL_FOLLOW_CTX_CEILING,
68
+ floor = MODEL_FOLLOW_CTX_FLOOR,
69
+ estimateFn = computeVramEstimate,
70
+ } = {}) {
71
+ const { picks, hi, lo } = ladderCandidates(modelMax, ceiling, floor);
72
+ let firstChecked = null;
73
+ for (const ctx of picks) {
74
+ let zone = null;
75
+ try {
76
+ const est = await estimateFn({ model, quant, contextLength: ctx, includeHardware: true });
77
+ zone = est?.classification?.zone || est?.predictiveClassification?.zone || null;
78
+ } catch {
79
+ zone = null;
80
+ }
81
+ if (!firstChecked) firstChecked = { ctx, zone };
82
+ if (zone && FIT_ZONES.has(String(zone).toUpperCase())) {
83
+ return { ctx, zone, cappedByCeiling: hi < (toPositiveInt(modelMax) || hi), modelMax: toPositiveInt(modelMax), vramGuarded: true };
84
+ }
85
+ if (!zone || String(zone).toUpperCase() === 'UNKNOWN') {
86
+ if (ctx <= MODEL_FOLLOW_CTX_FALLBACK) {
87
+ return { ctx, zone: zone || 'UNKNOWN', cappedByCeiling: false, modelMax: toPositiveInt(modelMax), vramGuarded: false };
88
+ }
89
+ }
90
+ }
91
+ const floorCtx = picks[picks.length - 1] ?? lo;
92
+ return { ctx: floorCtx, zone: firstChecked?.zone || 'UNKNOWN', cappedByCeiling: false, modelMax: toPositiveInt(modelMax), vramGuarded: false };
93
+ }
94
+
95
+ function sameModel(a, b) {
96
+ if (!a || !b) return false;
97
+ const norm = (v) => String(v).trim().toLowerCase();
98
+ return norm(a) === norm(b) || norm(a).includes(norm(b)) || norm(b).includes(norm(a));
99
+ }
100
+
101
+ export async function ensureModelContextFollow({
102
+ model,
103
+ quant = null,
104
+ requestedContext = null,
105
+ ceiling = MODEL_FOLLOW_CTX_CEILING,
106
+ floor = MODEL_FOLLOW_CTX_FLOOR,
107
+ probeFn,
108
+ loadFn,
109
+ unloadFn = null,
110
+ estimateFn = computeVramEstimate,
111
+ } = {}) {
112
+ if (typeof probeFn !== 'function' || typeof loadFn !== 'function') {
113
+ throw new Error('ensureModelContextFollow requires probeFn and loadFn');
114
+ }
115
+ const requested = toPositiveInt(requestedContext);
116
+
117
+ const probeCurrent = async () => {
118
+ try {
119
+ return await probeFn();
120
+ } catch {
121
+ return null;
122
+ }
123
+ };
124
+
125
+ let probe = await probeCurrent();
126
+ let probeMatches = Boolean(probe?.reachable && sameModel(probe?.loadedModel, model));
127
+ let modelMax = probeMatches ? toPositiveInt(probe?.modelMaxContextLength) : null;
128
+ let loaded = probeMatches ? toPositiveInt(probe?.loadedContextLength) : null;
129
+
130
+ let performedLoad = false;
131
+ if (!probeMatches) {
132
+ const loadRes = await loadFn({ contextLength: requested });
133
+ performedLoad = true;
134
+ if (!loadRes?.ok && !loadRes?.modelLoaded && !loadRes?.alreadyRunning) {
135
+ return { ok: false, reason: loadRes?.reason || 'Model load failed', performedLoad, reloadedForFollow: false };
136
+ }
137
+ probe = await probeCurrent();
138
+ probeMatches = Boolean(probe?.reachable && sameModel(probe?.loadedModel, model));
139
+ modelMax = probeMatches ? toPositiveInt(probe?.modelMaxContextLength) : modelMax;
140
+ loaded = probeMatches ? toPositiveInt(probe?.loadedContextLength) : loaded;
141
+ }
142
+
143
+ if (!modelMax && !loaded) {
144
+ const follow = resolveModelFollowContext({ loadedContext: requested, ceiling, floor });
145
+ return {
146
+ ok: true,
147
+ performedLoad,
148
+ reloadedForFollow: false,
149
+ loadedContextLength: loaded,
150
+ modelMaxContextLength: null,
151
+ followContext: requested || follow.target,
152
+ followReason: 'model-max-unknown',
153
+ };
154
+ }
155
+
156
+ const fit = await pickFittingLoadContext({ model, quant, modelMax, ceiling, floor, estimateFn });
157
+ let reloadedForFollow = false;
158
+ if (fit.ctx > (loaded || 0)) {
159
+ if (typeof unloadFn === 'function') {
160
+ try { await unloadFn(); } catch {}
161
+ }
162
+ const reloadRes = await loadFn({ contextLength: fit.ctx });
163
+ reloadedForFollow = true;
164
+ if (!reloadRes?.ok && !reloadRes?.modelLoaded && !reloadRes?.alreadyRunning) {
165
+ return {
166
+ ok: false,
167
+ reason: reloadRes?.reason || 'Follow-up reload failed',
168
+ performedLoad,
169
+ reloadedForFollow,
170
+ loadedContextLength: loaded,
171
+ modelMaxContextLength: modelMax,
172
+ followContext: fit.ctx,
173
+ followReason: 'reload-failed',
174
+ };
175
+ }
176
+ probe = await probeCurrent();
177
+ if (probe && sameModel(probe?.loadedModel, model)) {
178
+ loaded = toPositiveInt(probe?.loadedContextLength) || loaded;
179
+ modelMax = toPositiveInt(probe?.modelMaxContextLength) || modelMax;
180
+ }
181
+ }
182
+
183
+ return {
184
+ ok: true,
185
+ performedLoad,
186
+ reloadedForFollow,
187
+ loadedContextLength: loaded,
188
+ modelMaxContextLength: modelMax,
189
+ followContext: fit.ctx,
190
+ followZone: fit.zone || null,
191
+ followReason: reloadedForFollow ? 'reloaded-to-model-max' : 'already-optimal',
192
+ };
193
+ }