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.
- package/README.md +69 -12
- package/package.json +5 -12
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/AGENTS.md +228 -0
- package/server/agent/eval/analyze.py +684 -0
- package/server/agent/eval/gates.json +15 -0
- package/server/agent/eval/modules/analysis/compare.js +198 -0
- package/server/agent/eval/modules/analysis/gates.js +72 -0
- package/server/agent/eval/modules/analysis/judge.js +70 -0
- package/server/agent/eval/modules/core/cli.js +453 -0
- package/server/agent/eval/modules/core/config.js +60 -0
- package/server/agent/eval/modules/core/store.js +236 -0
- package/server/agent/eval/modules/evaluation/cases.js +689 -0
- package/server/agent/eval/modules/evaluation/runner.js +288 -0
- package/server/agent/eval/modules/evaluation/scoring.js +363 -0
- package/server/agent/eval/modules/evaluation/worker.js +122 -0
- package/server/agent/eval/modules/reporting/benchmark.js +265 -0
- package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
- package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
- package/server/agent/eval/modules/server/evalApi.js +1099 -0
- package/server/agent/eval/modules/ui/evalUi.js +4799 -0
- package/server/agent/eval/modules/utils/smokes.js +119 -0
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentBubbleSystem.test.js +65 -0
- package/server/agent/tests/agentFollowUps.test.js +253 -0
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +108 -0
- package/server/agent/tests/architectureFixes.test.js +81 -0
- package/server/agent/tests/architectureRefactoring.test.js +194 -0
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/autoResearch.test.js +64 -0
- package/server/agent/tests/bilibiliEmbed.test.js +296 -0
- package/server/agent/tests/catalogSync.test.js +130 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/citations.test.js +180 -0
- package/server/agent/tests/clarificationAnalytics.test.js +219 -0
- package/server/agent/tests/clarifyingQuestions.test.js +627 -0
- package/server/agent/tests/cliAutomation.test.js +158 -0
- package/server/agent/tests/cloudProviders.test.js +322 -0
- package/server/agent/tests/commandInput.test.js +24 -0
- package/server/agent/tests/commands.test.js +506 -0
- package/server/agent/tests/contextIntelligence.test.js +209 -0
- package/server/agent/tests/conversationSafety.test.js +53 -0
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +167 -0
- package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
- package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
- package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +131 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/entertainmentMetrics.test.js +126 -0
- package/server/agent/tests/envBootstrap.test.js +87 -0
- package/server/agent/tests/envCommand.test.js +284 -0
- package/server/agent/tests/errorHandling.test.js +80 -0
- package/server/agent/tests/errors.test.js +13 -0
- package/server/agent/tests/evalAnalyze.test.js +54 -0
- package/server/agent/tests/evalCommand.test.js +54 -0
- package/server/agent/tests/evalCorpus.test.js +126 -0
- package/server/agent/tests/evalHarness.test.js +62 -0
- package/server/agent/tests/evalJudgeContext.test.js +38 -0
- package/server/agent/tests/evalMetrics.test.js +320 -0
- package/server/agent/tests/evalStore.test.js +190 -0
- package/server/agent/tests/evalStudio.test.js +179 -0
- package/server/agent/tests/feedbackLearning.test.js +186 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/firstRun.test.js +44 -0
- package/server/agent/tests/gemma4eOptimization.test.js +95 -0
- package/server/agent/tests/hardwareTiers.test.js +206 -0
- package/server/agent/tests/hybridWebSearch.test.js +194 -0
- package/server/agent/tests/judgeService.test.js +251 -0
- package/server/agent/tests/lanDiscovery.test.js +324 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/mcpClient.test.js +201 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +141 -0
- package/server/agent/tests/multilingualScenarios.test.js +69 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/newCommands.test.js +347 -0
- package/server/agent/tests/newCommandsTui.test.js +141 -0
- package/server/agent/tests/newTools.test.js +113 -0
- package/server/agent/tests/newsIntelligence.test.js +178 -0
- package/server/agent/tests/nextGenArchitecture.test.js +101 -0
- package/server/agent/tests/offlineInput.test.js +217 -0
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/packagingGuard.test.js +57 -0
- package/server/agent/tests/presentation.test.js +61 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +44 -0
- package/server/agent/tests/queryBenchmarks.test.js +83 -0
- package/server/agent/tests/queryDetails.test.js +126 -0
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/realWorldValidation.test.js +391 -0
- package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
- package/server/agent/tests/runtimeWatchdog.test.js +32 -0
- package/server/agent/tests/sanitizer.test.js +37 -0
- package/server/agent/tests/scenariosData.js +378 -0
- package/server/agent/tests/schemaRouting.test.js +54 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +221 -0
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +146 -0
- package/server/agent/tests/securitySanitizer.test.js +38 -0
- package/server/agent/tests/sessionAndCopy.test.js +267 -0
- package/server/agent/tests/sidebarControls.test.js +135 -0
- package/server/agent/tests/statefulMemory.test.js +277 -0
- package/server/agent/tests/subagent.test.js +216 -0
- package/server/agent/tests/subagentProgress.test.js +109 -0
- package/server/agent/tests/subagentTextFallback.test.js +115 -0
- package/server/agent/tests/suggestions.test.js +199 -0
- package/server/agent/tests/suggestionsTui.test.js +251 -0
- package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
- package/server/agent/tests/terminalImage.test.js +80 -0
- package/server/agent/tests/terminalMarkdown.test.js +166 -0
- package/server/agent/tests/terminalText.test.js +133 -0
- package/server/agent/tests/textProtocolLoop.test.js +197 -0
- package/server/agent/tests/textToolParsing.test.js +187 -0
- package/server/agent/tests/theme.test.js +26 -0
- package/server/agent/tests/toolRegistryParity.test.js +53 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +160 -0
- package/server/agent/tests/tools.test.js +259 -0
- package/server/agent/tests/unifiedEndpoints.test.js +159 -0
- package/server/agent/tests/v110Features.test.js +262 -0
- package/server/agent/tests/versionBump.test.js +69 -0
- package/server/agent/tests/vramEstimator.test.js +595 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +551 -0
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webChatMemory.test.js +112 -0
- package/server/agent/tests/webChatSecurity.test.js +169 -0
- package/server/agent/tests/webExtract.test.js +284 -0
- package/server/agent/tests/webSources.test.js +281 -0
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/eval/judgeRubrics.js +110 -0
- package/server/eval/judgeService.js +290 -0
- package/server/eval/runBenchmark.js +182 -0
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/examples/librechat.example.yaml +16 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +162 -0
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +63 -0
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import {
|
|
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 => {
|
|
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) {
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|