arcane-os 0.2.0 → 0.2.2
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/CHANGELOG.md +35 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +5 -5
- package/browser-runtime/ai/browser-speech-providers.mjs +331 -25
- package/docs/architecture.md +2 -2
- package/docs/reference/README.md +79 -13
- package/docs/reference/ai/browser-speech.md +336 -0
- package/docs/reference/ai/browser-wasm.md +207 -82
- package/docs/reference/availability-and-normalization.md +30 -4
- package/docs/reference/behavioral-testing.md +4 -1
- package/docs/reference/cli.md +29 -10
- package/docs/reference/core/arcane-ai-contracts.md +43 -9
- package/docs/reference/inventory/package-api.json +110 -14
- package/docs/reference/inventory/runtime-components.json +19 -6
- package/docs/reference/inventory/runtime-modules.json +113 -9
- package/docs/reference/protocols.md +260 -38
- package/docs/reference/runtime-components.md +108 -15
- package/docs/reference/runtime-modules.md +422 -8
- package/docs/reference/sdk-api.md +626 -85
- package/package.json +1 -1
- package/runtime/ARCANE_RUNTIME_RELEASE.json +19 -19
- package/runtime/arcane/components/chat.html +288 -52
- package/runtime/arcane/components/speech.html +339 -15
- package/runtime/arcane/modules/AI.js +1245 -136
- package/runtime/arcane/modules/AIProviderRuntime.js +299 -30
- package/runtime/arcane/modules/AIRuntimeState.js +23 -4
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +93 -8
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +448 -24
- package/runtime/arcane/modules/LocalAIReadinessController.js +1 -1
- package/schemas/arcane-lock.schema.json +6 -4
- package/src/dev-server.mjs +29 -13
- package/src/doctor.mjs +1 -3
- package/src/import-map.mjs +134 -83
- package/src/packager/core.mjs +311 -39
- package/src/scaffold.mjs +45 -17
- package/src/templates/workspace-template.mjs +23 -4
- package/src/toolchain.mjs +10 -2
- package/src/workspace.mjs +177 -24
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import './DBOPFS.js';
|
|
2
2
|
import UserEntity from '../entities/User.js';
|
|
3
3
|
import {getAIPreferencesForRuntime} from './AIPreferenceRuntime.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
AI_MODEL_AUTHORITY_PROTOCOL,
|
|
6
|
+
AI_PROVIDER_PROTOCOL,
|
|
7
|
+
getAIProviderRuntime
|
|
8
|
+
} from './AIProviderRuntime.js';
|
|
5
9
|
import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
|
|
6
10
|
|
|
7
11
|
let credentials='include';
|
|
12
|
+
const LEGACY_TTS_RESPONSE_FORMAT='opus';
|
|
8
13
|
credentials='omit';
|
|
9
14
|
|
|
10
15
|
const LEGACY_AI_SERVICES=new Set(['OPENAI','OLLAMA','LOCAL_SPEACH']);
|
|
@@ -27,9 +32,155 @@ function normalizeAIRequestAbort(error){
|
|
|
27
32
|
return normalized;
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
function legacyAIProviderError(message,code,cause){
|
|
36
|
+
const error=cause===undefined
|
|
37
|
+
?new Error(message)
|
|
38
|
+
:new Error(message,{cause});
|
|
39
|
+
error.code=code;
|
|
40
|
+
return error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createLegacyAIStreamBridge(execute,sourceSignal){
|
|
44
|
+
const controller=new AbortController();
|
|
45
|
+
const queue=[];
|
|
46
|
+
const waiters=[];
|
|
47
|
+
let complete=false;
|
|
48
|
+
let failure=null;
|
|
49
|
+
let detached=false;
|
|
50
|
+
|
|
51
|
+
function forwardLegacyAIStreamAbort(){
|
|
52
|
+
if(!controller.signal.aborted){
|
|
53
|
+
controller.abort(sourceSignal?.reason);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function detachLegacyAIStreamAbort(){
|
|
58
|
+
if(detached){
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
detached=true;
|
|
62
|
+
sourceSignal?.removeEventListener?.(
|
|
63
|
+
'abort',
|
|
64
|
+
forwardLegacyAIStreamAbort
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if(sourceSignal?.aborted){
|
|
69
|
+
forwardLegacyAIStreamAbort();
|
|
70
|
+
}else{
|
|
71
|
+
sourceSignal?.addEventListener?.(
|
|
72
|
+
'abort',
|
|
73
|
+
forwardLegacyAIStreamAbort,
|
|
74
|
+
{once:true}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function emitLegacyAIStreamChunk(chunk){
|
|
79
|
+
if(complete){
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const waiter=waiters.shift();
|
|
83
|
+
if(waiter){
|
|
84
|
+
waiter.resolve({value:chunk,done:false});
|
|
85
|
+
}else{
|
|
86
|
+
queue.push(chunk);
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function finishLegacyAIStream(error){
|
|
92
|
+
if(complete){
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
complete=true;
|
|
96
|
+
failure=error||null;
|
|
97
|
+
detachLegacyAIStreamAbort();
|
|
98
|
+
while(waiters.length){
|
|
99
|
+
const waiter=waiters.shift();
|
|
100
|
+
if(failure){
|
|
101
|
+
waiter.reject(failure);
|
|
102
|
+
}else{
|
|
103
|
+
waiter.resolve({value:undefined,done:true});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result=Promise.resolve().then(
|
|
109
|
+
function executeLegacyAIStream(){
|
|
110
|
+
if(controller.signal.aborted){
|
|
111
|
+
throw normalizeAIRequestAbort(controller.signal.reason);
|
|
112
|
+
}
|
|
113
|
+
return execute({
|
|
114
|
+
emit:emitLegacyAIStreamChunk,
|
|
115
|
+
signal:controller.signal
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
).then(
|
|
119
|
+
function acceptLegacyAIStreamResult(value){
|
|
120
|
+
finishLegacyAIStream(null);
|
|
121
|
+
return value;
|
|
122
|
+
},
|
|
123
|
+
function rejectLegacyAIStreamResult(error){
|
|
124
|
+
const normalized=isAIRequestAbort(error,controller.signal)
|
|
125
|
+
?normalizeAIRequestAbort(error)
|
|
126
|
+
:error;
|
|
127
|
+
finishLegacyAIStream(normalized);
|
|
128
|
+
throw normalized;
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
result.catch(function retainLegacyAIStreamFailure() {});
|
|
132
|
+
|
|
133
|
+
async function cancelLegacyAIStream(reason){
|
|
134
|
+
if(!controller.signal.aborted){
|
|
135
|
+
controller.abort(reason);
|
|
136
|
+
}
|
|
137
|
+
await result.catch(function retainCancelledLegacyAIStream() {});
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handle={
|
|
142
|
+
result,
|
|
143
|
+
cancel:cancelLegacyAIStream,
|
|
144
|
+
next:function readLegacyAIStreamChunk(){
|
|
145
|
+
if(queue.length){
|
|
146
|
+
return Promise.resolve({value:queue.shift(),done:false});
|
|
147
|
+
}
|
|
148
|
+
if(complete){
|
|
149
|
+
return failure
|
|
150
|
+
?Promise.reject(failure)
|
|
151
|
+
:Promise.resolve({value:undefined,done:true});
|
|
152
|
+
}
|
|
153
|
+
return new Promise(function waitForLegacyAIStreamChunk(resolve,reject){
|
|
154
|
+
waiters.push({resolve,reject});
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
return:async function returnLegacyAIStream(value){
|
|
158
|
+
await cancelLegacyAIStream(
|
|
159
|
+
legacyAIProviderError(
|
|
160
|
+
'The legacy AI stream consumer stopped before completion.',
|
|
161
|
+
'ARCANE_AI_REQUEST_ABORTED'
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
return {value,done:true};
|
|
165
|
+
},
|
|
166
|
+
throw:async function throwLegacyAIStream(error){
|
|
167
|
+
await cancelLegacyAIStream(error);
|
|
168
|
+
throw error;
|
|
169
|
+
},
|
|
170
|
+
[Symbol.asyncIterator]:function iterateLegacyAIStream(){
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
return Object.freeze(handle);
|
|
175
|
+
}
|
|
176
|
+
|
|
30
177
|
function normalizeAIStartupOptions(options){
|
|
31
178
|
if(options===undefined){
|
|
32
|
-
return Object.freeze({
|
|
179
|
+
return Object.freeze({
|
|
180
|
+
startMuted:true,
|
|
181
|
+
startTranscription:false,
|
|
182
|
+
signal:null
|
|
183
|
+
});
|
|
33
184
|
}
|
|
34
185
|
if(!options||typeof options!=='object'||Array.isArray(options)){
|
|
35
186
|
throw new TypeError('AI startup options must be a plain object.');
|
|
@@ -40,7 +191,11 @@ function normalizeAIStartupOptions(options){
|
|
|
40
191
|
}
|
|
41
192
|
const descriptors=Object.getOwnPropertyDescriptors(options);
|
|
42
193
|
for(const key of Reflect.ownKeys(descriptors)){
|
|
43
|
-
if(typeof key==='symbol'||(
|
|
194
|
+
if(typeof key==='symbol'||(
|
|
195
|
+
key!=='startMuted'
|
|
196
|
+
&&key!=='startTranscription'
|
|
197
|
+
&&key!=='signal'
|
|
198
|
+
)){
|
|
44
199
|
throw new TypeError('AI startup options contain an unknown option.');
|
|
45
200
|
}
|
|
46
201
|
if(!Object.hasOwn(descriptors[key],'value')){
|
|
@@ -50,12 +205,18 @@ function normalizeAIStartupOptions(options){
|
|
|
50
205
|
const startMuted=Object.hasOwn(descriptors,'startMuted')
|
|
51
206
|
?descriptors.startMuted.value
|
|
52
207
|
:true;
|
|
208
|
+
const startTranscription=Object.hasOwn(descriptors,'startTranscription')
|
|
209
|
+
?descriptors.startTranscription.value
|
|
210
|
+
:false;
|
|
53
211
|
const signal=Object.hasOwn(descriptors,'signal')
|
|
54
212
|
?descriptors.signal.value
|
|
55
213
|
:null;
|
|
56
214
|
if(typeof startMuted!=='boolean'){
|
|
57
215
|
throw new TypeError('AI startup startMuted must be a boolean.');
|
|
58
216
|
}
|
|
217
|
+
if(typeof startTranscription!=='boolean'){
|
|
218
|
+
throw new TypeError('AI startup startTranscription must be a boolean.');
|
|
219
|
+
}
|
|
59
220
|
if(signal!==null&&signal!==undefined&&(
|
|
60
221
|
typeof signal!=='object'
|
|
61
222
|
||typeof signal.aborted!=='boolean'
|
|
@@ -64,7 +225,7 @@ function normalizeAIStartupOptions(options){
|
|
|
64
225
|
)){
|
|
65
226
|
throw new TypeError('AI startup signal must be an AbortSignal.');
|
|
66
227
|
}
|
|
67
|
-
return Object.freeze({startMuted,signal});
|
|
228
|
+
return Object.freeze({startMuted,startTranscription,signal});
|
|
68
229
|
}
|
|
69
230
|
|
|
70
231
|
class AI {
|
|
@@ -190,9 +351,23 @@ class AI {
|
|
|
190
351
|
this.setAI(
|
|
191
352
|
...preferences
|
|
192
353
|
);
|
|
354
|
+
|
|
355
|
+
const runtime=this;
|
|
356
|
+
globalThis.addEventListener?.(
|
|
357
|
+
'arcane-ollama-ready',
|
|
358
|
+
function reconcileLegacyOllamaReadiness(){
|
|
359
|
+
runtime.#retainLegacyLLMReadiness(
|
|
360
|
+
runtime.#reconcileLegacyLLMReadiness()
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
);
|
|
193
364
|
}
|
|
194
365
|
|
|
195
366
|
#providerRuntime=getAIProviderRuntime();
|
|
367
|
+
#legacyLLMProviders=new Map();
|
|
368
|
+
#legacyLLMReadiness=Promise.resolve(null);
|
|
369
|
+
#legacySpeechProviders=new Map();
|
|
370
|
+
#legacySpeechReadiness=Promise.resolve(null);
|
|
196
371
|
#preferenceTuple=Object.freeze([
|
|
197
372
|
'OPENAI',
|
|
198
373
|
'OPENAI',
|
|
@@ -240,11 +415,642 @@ class AI {
|
|
|
240
415
|
|
|
241
416
|
set license(value){
|
|
242
417
|
this.#license=typeof value==='string' ? value.trim():'';
|
|
418
|
+
this.#retainLegacyLLMReadiness(
|
|
419
|
+
this.#reconcileLegacyLLMReadiness()
|
|
420
|
+
);
|
|
421
|
+
this.#retainLegacySpeechReadiness(
|
|
422
|
+
this.#reconcileLegacySpeechReadiness()
|
|
423
|
+
);
|
|
243
424
|
return this.#license;
|
|
244
425
|
}
|
|
245
426
|
|
|
427
|
+
#legacyLLMCapability(providerId){
|
|
428
|
+
if(providerId==='OPENAI'){
|
|
429
|
+
return this.llmService==='OPENAI'
|
|
430
|
+
&&Boolean(this.model)
|
|
431
|
+
&&Boolean(this.license)
|
|
432
|
+
&&typeof globalThis.fetch==='function';
|
|
433
|
+
}
|
|
434
|
+
if(providerId==='OLLAMA'){
|
|
435
|
+
return this.llmService==='OLLAMA'
|
|
436
|
+
&&Boolean(this.model)
|
|
437
|
+
&&Boolean(this.#nativeOllama());
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
#legacyLLMInspection(providerId,selection){
|
|
443
|
+
const localOnly=providerId==='OLLAMA';
|
|
444
|
+
if(!selection
|
|
445
|
+
||selection.providerId!==providerId
|
|
446
|
+
||selection.modelId!==this.model
|
|
447
|
+
||selection.localOnly!==localOnly
|
|
448
|
+
||this.llmService!==providerId){
|
|
449
|
+
return Object.freeze({
|
|
450
|
+
available:false,
|
|
451
|
+
code:'ARCANE_AI_MODEL_AUTHORITY_REQUIRED',
|
|
452
|
+
message:'The selected legacy LLM route does not match the active AI configuration.'
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if(!this.#legacyLLMCapability(providerId)){
|
|
456
|
+
return Object.freeze({
|
|
457
|
+
available:false,
|
|
458
|
+
code:providerId==='OLLAMA'
|
|
459
|
+
?'AI_NATIVE_LOCAL_REQUIRED'
|
|
460
|
+
:'AI_PROVIDER_NOT_CONFIGURED',
|
|
461
|
+
message:providerId==='OLLAMA'
|
|
462
|
+
?'Local AI requires the capability-gated Arcane API.'
|
|
463
|
+
:'AI provider is not configured.'
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return Object.freeze({
|
|
467
|
+
available:true,
|
|
468
|
+
authority:Object.freeze({
|
|
469
|
+
protocol:AI_MODEL_AUTHORITY_PROTOCOL,
|
|
470
|
+
providerId,
|
|
471
|
+
modelId:selection.modelId,
|
|
472
|
+
admitted:true
|
|
473
|
+
})
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
#createLegacyLLMProvider(providerId){
|
|
478
|
+
const runtime=this;
|
|
479
|
+
const localOnly=providerId==='OLLAMA';
|
|
480
|
+
let state='unloaded';
|
|
481
|
+
let busy=false;
|
|
482
|
+
|
|
483
|
+
function statusLegacyLLMProvider(){
|
|
484
|
+
if(state==='ready'
|
|
485
|
+
&&!busy
|
|
486
|
+
&&!runtime.#legacyLLMCapability(providerId)){
|
|
487
|
+
state='unloaded';
|
|
488
|
+
}
|
|
489
|
+
return Object.freeze({
|
|
490
|
+
state,
|
|
491
|
+
loaded:state==='ready',
|
|
492
|
+
busy
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function assertLegacyLLMSelection(selection){
|
|
497
|
+
const inspection=runtime.#legacyLLMInspection(
|
|
498
|
+
providerId,
|
|
499
|
+
selection
|
|
500
|
+
);
|
|
501
|
+
if(!inspection.available){
|
|
502
|
+
throw legacyAIProviderError(
|
|
503
|
+
inspection.message,
|
|
504
|
+
inspection.code
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
return inspection;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function releaseLegacyLLMRequest(){
|
|
511
|
+
busy=false;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return Object.freeze({
|
|
515
|
+
protocol:AI_PROVIDER_PROTOCOL,
|
|
516
|
+
role:'llm',
|
|
517
|
+
id:providerId,
|
|
518
|
+
localOnly,
|
|
519
|
+
catalog:function catalogLegacyLLMProvider(){
|
|
520
|
+
if(runtime.llmService!==providerId||!runtime.model){
|
|
521
|
+
return Object.freeze([]);
|
|
522
|
+
}
|
|
523
|
+
return Object.freeze([
|
|
524
|
+
Object.freeze({id:runtime.model})
|
|
525
|
+
]);
|
|
526
|
+
},
|
|
527
|
+
inspect:function inspectLegacyLLMProvider(selection,{signal}={}){
|
|
528
|
+
if(signal?.aborted){
|
|
529
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
530
|
+
}
|
|
531
|
+
return runtime.#legacyLLMInspection(providerId,selection);
|
|
532
|
+
},
|
|
533
|
+
status:statusLegacyLLMProvider,
|
|
534
|
+
load:function loadLegacyLLMProvider(context={}){
|
|
535
|
+
if(context.signal?.aborted){
|
|
536
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
537
|
+
}
|
|
538
|
+
if(state==='disposed'){
|
|
539
|
+
throw legacyAIProviderError(
|
|
540
|
+
'The legacy LLM provider is disposed.',
|
|
541
|
+
'ARCANE_AI_PROVIDER_DISPOSED'
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if(busy){
|
|
545
|
+
throw legacyAIProviderError(
|
|
546
|
+
'The legacy LLM provider owns an active request.',
|
|
547
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if(typeof context.progress!=='function'){
|
|
551
|
+
throw new TypeError(
|
|
552
|
+
'Legacy LLM provider load progress must be a function.'
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
const inspection=assertLegacyLLMSelection(context.selection);
|
|
556
|
+
state='loading';
|
|
557
|
+
context.progress({
|
|
558
|
+
phase:'capability',
|
|
559
|
+
completed:0,
|
|
560
|
+
total:1,
|
|
561
|
+
unit:'items',
|
|
562
|
+
heartbeat:false
|
|
563
|
+
});
|
|
564
|
+
if(context.signal?.aborted){
|
|
565
|
+
state='unloaded';
|
|
566
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
567
|
+
}
|
|
568
|
+
state='ready';
|
|
569
|
+
context.progress({
|
|
570
|
+
phase:'capability',
|
|
571
|
+
completed:1,
|
|
572
|
+
total:1,
|
|
573
|
+
unit:'items',
|
|
574
|
+
heartbeat:false
|
|
575
|
+
});
|
|
576
|
+
return Object.freeze({
|
|
577
|
+
authority:inspection.authority,
|
|
578
|
+
status:statusLegacyLLMProvider()
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
request:function requestLegacyLLMProvider(context={}){
|
|
582
|
+
if(context.signal?.aborted){
|
|
583
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
584
|
+
}
|
|
585
|
+
assertLegacyLLMSelection(context.selection);
|
|
586
|
+
const current=statusLegacyLLMProvider();
|
|
587
|
+
if(current.state!=='ready'||!current.loaded){
|
|
588
|
+
throw legacyAIProviderError(
|
|
589
|
+
'The legacy LLM provider is not ready.',
|
|
590
|
+
'ARCANE_AI_ROLE_NOT_READY'
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
if(busy){
|
|
594
|
+
throw legacyAIProviderError(
|
|
595
|
+
'The legacy LLM provider owns an active request.',
|
|
596
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
busy=true;
|
|
600
|
+
if(context.operation==='chat'){
|
|
601
|
+
return Promise.resolve(
|
|
602
|
+
runtime.#requestLegacyLLMChat(
|
|
603
|
+
context.payload,
|
|
604
|
+
context.signal
|
|
605
|
+
)
|
|
606
|
+
).finally(releaseLegacyLLMRequest);
|
|
607
|
+
}
|
|
608
|
+
if(context.operation==='stream'){
|
|
609
|
+
const handle=createLegacyAIStreamBridge(
|
|
610
|
+
function executeLegacyLLMProviderStream(bridge){
|
|
611
|
+
return runtime.#requestLegacyLLMStream(
|
|
612
|
+
context.payload,
|
|
613
|
+
bridge
|
|
614
|
+
);
|
|
615
|
+
},
|
|
616
|
+
context.signal
|
|
617
|
+
);
|
|
618
|
+
handle.result.then(
|
|
619
|
+
releaseLegacyLLMRequest,
|
|
620
|
+
releaseLegacyLLMRequest
|
|
621
|
+
);
|
|
622
|
+
return handle;
|
|
623
|
+
}
|
|
624
|
+
busy=false;
|
|
625
|
+
throw legacyAIProviderError(
|
|
626
|
+
'The legacy LLM provider operation is unsupported.',
|
|
627
|
+
'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
|
|
628
|
+
);
|
|
629
|
+
},
|
|
630
|
+
unload:function unloadLegacyLLMProvider(context={}){
|
|
631
|
+
if(context.signal?.aborted){
|
|
632
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
633
|
+
}
|
|
634
|
+
state='unloaded';
|
|
635
|
+
busy=false;
|
|
636
|
+
return statusLegacyLLMProvider();
|
|
637
|
+
},
|
|
638
|
+
dispose:function disposeLegacyLLMProvider(context={}){
|
|
639
|
+
if(context.signal?.aborted){
|
|
640
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
641
|
+
}
|
|
642
|
+
state='disposed';
|
|
643
|
+
busy=false;
|
|
644
|
+
return statusLegacyLLMProvider();
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
#legacySpeechService(role){
|
|
650
|
+
return role==='stt'?this.sttService:this.ttsService;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
#legacySpeechModel(role){
|
|
654
|
+
return role==='stt'?this.modelSTT:this.modelTTS;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#legacySpeechProviderKey(role,providerId){
|
|
658
|
+
return `${role}:${providerId}`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
#legacySpeechDefaultVoice(role,providerId){
|
|
662
|
+
if(role!=='tts'){
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
if(providerId==='OPENAI'){
|
|
666
|
+
const selected=typeof globalThis.window?.user?.AI_voice==='string'
|
|
667
|
+
?globalThis.window.user.AI_voice.trim()
|
|
668
|
+
:'';
|
|
669
|
+
return selected||'alloy';
|
|
670
|
+
}
|
|
671
|
+
if(providerId==='LOCAL_SPEACH'){
|
|
672
|
+
return 'af_heart';
|
|
673
|
+
}
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
#legacySpeechCapability(role,providerId){
|
|
678
|
+
const service=this.#legacySpeechService(role);
|
|
679
|
+
const model=this.#legacySpeechModel(role);
|
|
680
|
+
if(service!==providerId||!model){
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
if(providerId==='OPENAI'){
|
|
684
|
+
return Boolean(this.license)&&typeof globalThis.fetch==='function';
|
|
685
|
+
}
|
|
686
|
+
if(providerId==='LOCAL_SPEACH'){
|
|
687
|
+
return Boolean(this.#nativeSpeech(service,role));
|
|
688
|
+
}
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
#legacySpeechInspection(role,providerId,selection){
|
|
693
|
+
const localOnly=providerId==='LOCAL_SPEACH';
|
|
694
|
+
if(!selection
|
|
695
|
+
||selection.providerId!==providerId
|
|
696
|
+
||selection.modelId!==this.#legacySpeechModel(role)
|
|
697
|
+
||selection.localOnly!==localOnly
|
|
698
|
+
||this.#legacySpeechService(role)!==providerId){
|
|
699
|
+
return Object.freeze({
|
|
700
|
+
available:false,
|
|
701
|
+
code:'ARCANE_AI_MODEL_AUTHORITY_REQUIRED',
|
|
702
|
+
message:`The selected legacy ${role.toUpperCase()} route does not match the active AI configuration.`
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
if(!this.#legacySpeechCapability(role,providerId)){
|
|
706
|
+
return Object.freeze({
|
|
707
|
+
available:false,
|
|
708
|
+
code:providerId==='LOCAL_SPEACH'
|
|
709
|
+
?'AI_NATIVE_LOCAL_REQUIRED'
|
|
710
|
+
:'AI_PROVIDER_NOT_CONFIGURED',
|
|
711
|
+
message:providerId==='LOCAL_SPEACH'
|
|
712
|
+
?`Local ${role.toUpperCase()} requires the capability-gated Arcane API.`
|
|
713
|
+
:'AI provider is not configured.'
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return Object.freeze({
|
|
717
|
+
available:true,
|
|
718
|
+
authority:Object.freeze({
|
|
719
|
+
protocol:AI_MODEL_AUTHORITY_PROTOCOL,
|
|
720
|
+
providerId,
|
|
721
|
+
modelId:selection.modelId,
|
|
722
|
+
admitted:true
|
|
723
|
+
})
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
#createLegacySpeechProvider(role,providerId){
|
|
728
|
+
const runtime=this;
|
|
729
|
+
const localOnly=providerId==='LOCAL_SPEACH';
|
|
730
|
+
const expectedOperation=role==='stt'?'transcribe':'synthesize';
|
|
731
|
+
let state='unloaded';
|
|
732
|
+
let busy=false;
|
|
733
|
+
|
|
734
|
+
function statusLegacySpeechProvider(){
|
|
735
|
+
if(state==='ready'
|
|
736
|
+
&&!busy
|
|
737
|
+
&&!runtime.#legacySpeechCapability(role,providerId)){
|
|
738
|
+
state='unloaded';
|
|
739
|
+
}
|
|
740
|
+
return Object.freeze({
|
|
741
|
+
state,
|
|
742
|
+
loaded:state==='ready',
|
|
743
|
+
busy
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function assertLegacySpeechSelection(selection){
|
|
748
|
+
const inspection=runtime.#legacySpeechInspection(
|
|
749
|
+
role,
|
|
750
|
+
providerId,
|
|
751
|
+
selection
|
|
752
|
+
);
|
|
753
|
+
if(!inspection.available){
|
|
754
|
+
throw legacyAIProviderError(
|
|
755
|
+
inspection.message,
|
|
756
|
+
inspection.code
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
return inspection;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function releaseLegacySpeechRequest(){
|
|
763
|
+
busy=false;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return Object.freeze({
|
|
767
|
+
protocol:AI_PROVIDER_PROTOCOL,
|
|
768
|
+
role,
|
|
769
|
+
id:providerId,
|
|
770
|
+
localOnly,
|
|
771
|
+
catalog:function catalogLegacySpeechProvider(){
|
|
772
|
+
const model=runtime.#legacySpeechModel(role);
|
|
773
|
+
if(runtime.#legacySpeechService(role)!==providerId||!model){
|
|
774
|
+
return Object.freeze([]);
|
|
775
|
+
}
|
|
776
|
+
const defaultVoice=runtime.#legacySpeechDefaultVoice(
|
|
777
|
+
role,
|
|
778
|
+
providerId
|
|
779
|
+
);
|
|
780
|
+
return Object.freeze([
|
|
781
|
+
Object.freeze({
|
|
782
|
+
id:model,
|
|
783
|
+
...(defaultVoice?{defaultVoice}:{})
|
|
784
|
+
})
|
|
785
|
+
]);
|
|
786
|
+
},
|
|
787
|
+
inspect:function inspectLegacySpeechProvider(selection,{signal}={}){
|
|
788
|
+
if(signal?.aborted){
|
|
789
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
790
|
+
}
|
|
791
|
+
return runtime.#legacySpeechInspection(role,providerId,selection);
|
|
792
|
+
},
|
|
793
|
+
status:statusLegacySpeechProvider,
|
|
794
|
+
load:function loadLegacySpeechProvider(context={}){
|
|
795
|
+
if(context.signal?.aborted){
|
|
796
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
797
|
+
}
|
|
798
|
+
if(state==='disposed'){
|
|
799
|
+
throw legacyAIProviderError(
|
|
800
|
+
`The legacy ${role.toUpperCase()} provider is disposed.`,
|
|
801
|
+
'ARCANE_AI_PROVIDER_DISPOSED'
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
if(busy){
|
|
805
|
+
throw legacyAIProviderError(
|
|
806
|
+
`The legacy ${role.toUpperCase()} provider owns an active request.`,
|
|
807
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
if(typeof context.progress!=='function'){
|
|
811
|
+
throw new TypeError(
|
|
812
|
+
`Legacy ${role.toUpperCase()} provider load progress must be a function.`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const inspection=assertLegacySpeechSelection(context.selection);
|
|
816
|
+
state='loading';
|
|
817
|
+
context.progress({
|
|
818
|
+
phase:'capability',
|
|
819
|
+
completed:0,
|
|
820
|
+
total:1,
|
|
821
|
+
unit:'items',
|
|
822
|
+
heartbeat:false
|
|
823
|
+
});
|
|
824
|
+
if(context.signal?.aborted){
|
|
825
|
+
state='unloaded';
|
|
826
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
827
|
+
}
|
|
828
|
+
state='ready';
|
|
829
|
+
context.progress({
|
|
830
|
+
phase:'capability',
|
|
831
|
+
completed:1,
|
|
832
|
+
total:1,
|
|
833
|
+
unit:'items',
|
|
834
|
+
heartbeat:false
|
|
835
|
+
});
|
|
836
|
+
return Object.freeze({
|
|
837
|
+
authority:inspection.authority,
|
|
838
|
+
status:statusLegacySpeechProvider()
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
request:function requestLegacySpeechProvider(context={}){
|
|
842
|
+
if(context.signal?.aborted){
|
|
843
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
844
|
+
}
|
|
845
|
+
assertLegacySpeechSelection(context.selection);
|
|
846
|
+
const current=statusLegacySpeechProvider();
|
|
847
|
+
if(current.state!=='ready'||!current.loaded){
|
|
848
|
+
throw legacyAIProviderError(
|
|
849
|
+
`The legacy ${role.toUpperCase()} provider is not ready.`,
|
|
850
|
+
'ARCANE_AI_ROLE_NOT_READY'
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
if(busy){
|
|
854
|
+
throw legacyAIProviderError(
|
|
855
|
+
`The legacy ${role.toUpperCase()} provider owns an active request.`,
|
|
856
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
if(context.operation!==expectedOperation){
|
|
860
|
+
throw legacyAIProviderError(
|
|
861
|
+
`The legacy ${role.toUpperCase()} provider operation is unsupported.`,
|
|
862
|
+
'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
busy=true;
|
|
866
|
+
const request=role==='stt'
|
|
867
|
+
?runtime.#requestLegacySpeechTranscription(
|
|
868
|
+
context.payload,
|
|
869
|
+
context.signal
|
|
870
|
+
)
|
|
871
|
+
:runtime.#requestLegacySpeechSynthesis(
|
|
872
|
+
context.payload,
|
|
873
|
+
context.signal
|
|
874
|
+
);
|
|
875
|
+
return Promise.resolve(request).finally(releaseLegacySpeechRequest);
|
|
876
|
+
},
|
|
877
|
+
unload:function unloadLegacySpeechProvider(context={}){
|
|
878
|
+
if(context.signal?.aborted){
|
|
879
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
880
|
+
}
|
|
881
|
+
if(busy){
|
|
882
|
+
throw legacyAIProviderError(
|
|
883
|
+
`The legacy ${role.toUpperCase()} provider still owns an active request.`,
|
|
884
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
state='unloaded';
|
|
888
|
+
return statusLegacySpeechProvider();
|
|
889
|
+
},
|
|
890
|
+
dispose:function disposeLegacySpeechProvider(context={}){
|
|
891
|
+
if(context.signal?.aborted){
|
|
892
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
893
|
+
}
|
|
894
|
+
if(busy){
|
|
895
|
+
throw legacyAIProviderError(
|
|
896
|
+
`The legacy ${role.toUpperCase()} provider still owns an active request.`,
|
|
897
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
state='disposed';
|
|
901
|
+
return statusLegacySpeechProvider();
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
#ensureLegacyLLMProvider(providerId){
|
|
907
|
+
if(providerId!=='OPENAI'&&providerId!=='OLLAMA'){
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
if(this.#providerRuntime.hasProvider('llm',providerId)){
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
const provider=this.#createLegacyLLMProvider(providerId);
|
|
914
|
+
const unregister=this.#providerRuntime.register(provider);
|
|
915
|
+
this.#legacyLLMProviders.set(
|
|
916
|
+
providerId,
|
|
917
|
+
Object.freeze({provider,unregister})
|
|
918
|
+
);
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
#ensureLegacySpeechProvider(role,providerId){
|
|
923
|
+
if(!['stt','tts'].includes(role)
|
|
924
|
+
||!['OPENAI','LOCAL_SPEACH'].includes(providerId)){
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
if(this.#providerRuntime.hasProvider(role,providerId)){
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
const provider=this.#createLegacySpeechProvider(role,providerId);
|
|
931
|
+
const unregister=this.#providerRuntime.register(provider);
|
|
932
|
+
this.#legacySpeechProviders.set(
|
|
933
|
+
this.#legacySpeechProviderKey(role,providerId),
|
|
934
|
+
Object.freeze({role,providerId,provider,unregister})
|
|
935
|
+
);
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
#releaseInactiveLegacyLLMProviders(activeProviderId){
|
|
940
|
+
for(const [providerId,record] of this.#legacyLLMProviders){
|
|
941
|
+
if(providerId===activeProviderId){
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
if(record.unregister()){
|
|
945
|
+
this.#legacyLLMProviders.delete(providerId);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
#releaseInactiveLegacySpeechProviders(activeProviders){
|
|
951
|
+
for(const [key,record] of this.#legacySpeechProviders){
|
|
952
|
+
if(activeProviders[record.role]===record.providerId){
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if(record.unregister()){
|
|
956
|
+
this.#legacySpeechProviders.delete(key);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
#internalLegacyLLMSelection(localOnly=false){
|
|
962
|
+
const selection=this.#providerRuntime.selection(
|
|
963
|
+
'llm',
|
|
964
|
+
{localOnly}
|
|
965
|
+
);
|
|
966
|
+
if(!selection
|
|
967
|
+
||!this.#legacyLLMProviders.has(selection.providerId)
|
|
968
|
+
||selection.providerId!==this.llmService
|
|
969
|
+
||selection.modelId!==this.model){
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
return selection;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
#internalLegacySpeechSelection(role,localOnly=false){
|
|
976
|
+
const selection=this.#providerRuntime.selection(role,{localOnly});
|
|
977
|
+
if(!selection
|
|
978
|
+
||!this.#legacySpeechProviders.has(
|
|
979
|
+
this.#legacySpeechProviderKey(role,selection.providerId)
|
|
980
|
+
)
|
|
981
|
+
||selection.providerId!==this.#legacySpeechService(role)
|
|
982
|
+
||selection.modelId!==this.#legacySpeechModel(role)){
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
return selection;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
#retainLegacyLLMReadiness(operation){
|
|
989
|
+
this.#legacyLLMReadiness=Promise.resolve(operation).catch(
|
|
990
|
+
function retainLegacyLLMReadinessFailure(){
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
);
|
|
994
|
+
return this.#legacyLLMReadiness;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
#retainLegacySpeechReadiness(operation){
|
|
998
|
+
this.#legacySpeechReadiness=Promise.resolve(operation).catch(
|
|
999
|
+
function retainLegacySpeechReadinessFailure(){
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
1003
|
+
return this.#legacySpeechReadiness;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
#reconcileLegacyLLMReadiness(){
|
|
1007
|
+
const selection=this.#internalLegacyLLMSelection(false);
|
|
1008
|
+
if(!selection){
|
|
1009
|
+
return Promise.resolve(this.#providerRuntime.status('llm'));
|
|
1010
|
+
}
|
|
1011
|
+
const status=this.#providerRuntime.status('llm');
|
|
1012
|
+
if(this.#legacyLLMCapability(selection.providerId)){
|
|
1013
|
+
if(status.state==='ready'&&status.loaded===true){
|
|
1014
|
+
return Promise.resolve(status);
|
|
1015
|
+
}
|
|
1016
|
+
return this.#providerRuntime.load('llm');
|
|
1017
|
+
}
|
|
1018
|
+
if(status.loaded===true
|
|
1019
|
+
||status.busy===true
|
|
1020
|
+
||status.state==='loading'
|
|
1021
|
+
||status.state==='unloading'){
|
|
1022
|
+
return this.#providerRuntime.unload('llm');
|
|
1023
|
+
}
|
|
1024
|
+
return Promise.resolve(status);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
#reconcileLegacySpeechReadiness(){
|
|
1028
|
+
const runtime=this;
|
|
1029
|
+
return Promise.all(['stt','tts'].map(function reconcileLegacySpeechRole(role){
|
|
1030
|
+
const selection=runtime.#internalLegacySpeechSelection(role,false);
|
|
1031
|
+
if(!selection){
|
|
1032
|
+
return runtime.#providerRuntime.status(role);
|
|
1033
|
+
}
|
|
1034
|
+
const status=runtime.#providerRuntime.status(role);
|
|
1035
|
+
if(runtime.#legacySpeechCapability(role,selection.providerId)){
|
|
1036
|
+
return status;
|
|
1037
|
+
}
|
|
1038
|
+
if(status.loaded===true
|
|
1039
|
+
||status.busy===true
|
|
1040
|
+
||status.state==='loading'
|
|
1041
|
+
||status.state==='unloading'){
|
|
1042
|
+
return runtime.#providerRuntime.unload(role);
|
|
1043
|
+
}
|
|
1044
|
+
return status;
|
|
1045
|
+
}));
|
|
1046
|
+
}
|
|
1047
|
+
|
|
246
1048
|
get configured(){
|
|
247
1049
|
if(this.#usesProviderRuntime('llm',this.llmService)){
|
|
1050
|
+
if(this.#internalLegacyLLMSelection(false)
|
|
1051
|
+
&&!this.#legacyLLMCapability(this.llmService)){
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
248
1054
|
const state=this.#providerRuntime.status('llm');
|
|
249
1055
|
return state.state==='ready'&&state.loaded===true;
|
|
250
1056
|
}
|
|
@@ -259,6 +1065,26 @@ class AI {
|
|
|
259
1065
|
|
|
260
1066
|
#assertServiceConfigured(service=this.llmService,role='llm'){
|
|
261
1067
|
if(this.#usesProviderRuntime(role,service)){
|
|
1068
|
+
const internal=role==='llm'
|
|
1069
|
+
?this.#internalLegacyLLMSelection(false)
|
|
1070
|
+
:this.#internalLegacySpeechSelection(role,false);
|
|
1071
|
+
const internalAvailable=!internal
|
|
1072
|
+
||(role==='llm'
|
|
1073
|
+
?this.#legacyLLMCapability(internal.providerId)
|
|
1074
|
+
:this.#legacySpeechCapability(role,internal.providerId));
|
|
1075
|
+
if(!internalAvailable){
|
|
1076
|
+
const inspection=role==='llm'
|
|
1077
|
+
?this.#legacyLLMInspection(internal.providerId,internal)
|
|
1078
|
+
:this.#legacySpeechInspection(
|
|
1079
|
+
role,
|
|
1080
|
+
internal.providerId,
|
|
1081
|
+
internal
|
|
1082
|
+
);
|
|
1083
|
+
throw legacyAIProviderError(
|
|
1084
|
+
inspection.message,
|
|
1085
|
+
inspection.code
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
262
1088
|
return true;
|
|
263
1089
|
}
|
|
264
1090
|
if(service==='OLLAMA'){
|
|
@@ -298,6 +1124,11 @@ class AI {
|
|
|
298
1124
|
}
|
|
299
1125
|
|
|
300
1126
|
#shouldUseProviderRuntime(role,service,localOnly=false){
|
|
1127
|
+
// Legacy adapters publish lifecycle without replacing established
|
|
1128
|
+
// public transport callbacks or their cancellation behavior.
|
|
1129
|
+
if(role==='llm'&&this.#internalLegacyLLMSelection(localOnly)){
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
301
1132
|
if(!localOnly){
|
|
302
1133
|
return this.#usesProviderRuntime(role,service);
|
|
303
1134
|
}
|
|
@@ -361,6 +1192,19 @@ class AI {
|
|
|
361
1192
|
}
|
|
362
1193
|
}
|
|
363
1194
|
|
|
1195
|
+
#normalizedLLMModel(service,model){
|
|
1196
|
+
if(service==='OLLAMA'){
|
|
1197
|
+
const mappedModel=model==='OPENAI'?null:this.#models[model];
|
|
1198
|
+
return mappedModel
|
|
1199
|
+
||normalizeOllamaModelIdentifier(model)
|
|
1200
|
+
||model;
|
|
1201
|
+
}
|
|
1202
|
+
if(service==='OPENAI'){
|
|
1203
|
+
return this.#models.OPENAI;
|
|
1204
|
+
}
|
|
1205
|
+
return model;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
364
1208
|
#applyPreferenceTuple(tuple){
|
|
365
1209
|
const [
|
|
366
1210
|
llmService,
|
|
@@ -369,16 +1213,11 @@ class AI {
|
|
|
369
1213
|
model,
|
|
370
1214
|
modelTTS,
|
|
371
1215
|
modelSTT
|
|
372
|
-
]=tuple;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
||normalizeOllamaModelIdentifier(model)
|
|
378
|
-
||model;
|
|
379
|
-
}else if(llmService==='OPENAI'){
|
|
380
|
-
normalizedLLMModel=this.#models.OPENAI;
|
|
381
|
-
}
|
|
1216
|
+
]=tuple;
|
|
1217
|
+
const normalizedLLMModel=this.#normalizedLLMModel(
|
|
1218
|
+
llmService,
|
|
1219
|
+
model
|
|
1220
|
+
);
|
|
382
1221
|
this.llmService=llmService;
|
|
383
1222
|
this.sttService=sttService;
|
|
384
1223
|
this.ttsService=ttsService;
|
|
@@ -405,9 +1244,12 @@ class AI {
|
|
|
405
1244
|
|
|
406
1245
|
#routesFromPreferenceTuple(tuple){
|
|
407
1246
|
const roles={
|
|
408
|
-
llm:[
|
|
409
|
-
|
|
410
|
-
|
|
1247
|
+
llm:[
|
|
1248
|
+
tuple[0],
|
|
1249
|
+
this.#normalizedLLMModel(tuple[0],tuple[3])
|
|
1250
|
+
],
|
|
1251
|
+
stt:[tuple[1],this.#sttModels[tuple[5]]||tuple[5]],
|
|
1252
|
+
tts:[tuple[2],this.#ttsModels[tuple[4]]||tuple[4]]
|
|
411
1253
|
};
|
|
412
1254
|
const selections={};
|
|
413
1255
|
for(const role of ['llm','stt','tts']){
|
|
@@ -501,15 +1343,54 @@ class AI {
|
|
|
501
1343
|
modelSTT
|
|
502
1344
|
]);
|
|
503
1345
|
this.#assertValidProviderTuple(tuple);
|
|
1346
|
+
this.#ensureLegacyLLMProvider(tuple[0]);
|
|
1347
|
+
this.#ensureLegacySpeechProvider('stt',tuple[1]);
|
|
1348
|
+
this.#ensureLegacySpeechProvider('tts',tuple[2]);
|
|
504
1349
|
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
505
1350
|
this.#applyPreferenceTuple(tuple);
|
|
1351
|
+
this.#releaseInactiveLegacyLLMProviders(tuple[0]);
|
|
1352
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1353
|
+
stt:tuple[1],
|
|
1354
|
+
tts:tuple[2]
|
|
1355
|
+
});
|
|
1356
|
+
this.#retainLegacyLLMReadiness(
|
|
1357
|
+
this.#reconcileLegacyLLMReadiness()
|
|
1358
|
+
);
|
|
1359
|
+
this.#retainLegacySpeechReadiness(
|
|
1360
|
+
this.#reconcileLegacySpeechReadiness()
|
|
1361
|
+
);
|
|
506
1362
|
return true;
|
|
507
1363
|
}
|
|
508
1364
|
|
|
509
1365
|
configureProviders(selections){
|
|
510
|
-
this.#
|
|
511
|
-
|
|
1366
|
+
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
1367
|
+
this.#ensureLegacyLLMProvider(
|
|
1368
|
+
prepared.llm.default?.providerId
|
|
1369
|
+
);
|
|
1370
|
+
this.#ensureLegacySpeechProvider(
|
|
1371
|
+
'stt',
|
|
1372
|
+
prepared.stt.default?.providerId
|
|
1373
|
+
);
|
|
1374
|
+
this.#ensureLegacySpeechProvider(
|
|
1375
|
+
'tts',
|
|
1376
|
+
prepared.tts.default?.providerId
|
|
1377
|
+
);
|
|
1378
|
+
this.#assertRegisteredLegacyRoutes(prepared);
|
|
1379
|
+
const configured=this.#providerRuntime.configure(prepared);
|
|
512
1380
|
this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
|
|
1381
|
+
this.#releaseInactiveLegacyLLMProviders(
|
|
1382
|
+
configured.llm.default?.providerId
|
|
1383
|
+
);
|
|
1384
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1385
|
+
stt:configured.stt.default?.providerId,
|
|
1386
|
+
tts:configured.tts.default?.providerId
|
|
1387
|
+
});
|
|
1388
|
+
this.#retainLegacyLLMReadiness(
|
|
1389
|
+
this.#reconcileLegacyLLMReadiness()
|
|
1390
|
+
);
|
|
1391
|
+
this.#retainLegacySpeechReadiness(
|
|
1392
|
+
this.#reconcileLegacySpeechReadiness()
|
|
1393
|
+
);
|
|
513
1394
|
return configured;
|
|
514
1395
|
}
|
|
515
1396
|
|
|
@@ -532,18 +1413,48 @@ class AI {
|
|
|
532
1413
|
this.#assertValidProviderTuple(tuple);
|
|
533
1414
|
this.stopAudio();
|
|
534
1415
|
await this.#unloadProviderRolesForTransition();
|
|
1416
|
+
this.#ensureLegacyLLMProvider(tuple[0]);
|
|
1417
|
+
this.#ensureLegacySpeechProvider('stt',tuple[1]);
|
|
1418
|
+
this.#ensureLegacySpeechProvider('tts',tuple[2]);
|
|
535
1419
|
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
536
1420
|
this.#applyPreferenceTuple(tuple);
|
|
1421
|
+
this.#releaseInactiveLegacyLLMProviders(tuple[0]);
|
|
1422
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1423
|
+
stt:tuple[1],
|
|
1424
|
+
tts:tuple[2]
|
|
1425
|
+
});
|
|
1426
|
+
await this.#reconcileLegacyLLMReadiness();
|
|
1427
|
+
await this.#reconcileLegacySpeechReadiness();
|
|
537
1428
|
return this.#providerRuntime.status();
|
|
538
1429
|
}
|
|
539
1430
|
|
|
540
1431
|
async transitionProviders(selections){
|
|
541
|
-
this.#assertRegisteredLegacyRoutes(selections);
|
|
542
1432
|
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
1433
|
+
this.#ensureLegacyLLMProvider(
|
|
1434
|
+
prepared.llm.default?.providerId
|
|
1435
|
+
);
|
|
1436
|
+
this.#ensureLegacySpeechProvider(
|
|
1437
|
+
'stt',
|
|
1438
|
+
prepared.stt.default?.providerId
|
|
1439
|
+
);
|
|
1440
|
+
this.#ensureLegacySpeechProvider(
|
|
1441
|
+
'tts',
|
|
1442
|
+
prepared.tts.default?.providerId
|
|
1443
|
+
);
|
|
1444
|
+
this.#assertRegisteredLegacyRoutes(prepared);
|
|
543
1445
|
this.stopAudio();
|
|
544
1446
|
await this.#unloadProviderRolesForTransition();
|
|
545
1447
|
const configured=this.#providerRuntime.configure(prepared);
|
|
546
1448
|
this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
|
|
1449
|
+
this.#releaseInactiveLegacyLLMProviders(
|
|
1450
|
+
configured.llm.default?.providerId
|
|
1451
|
+
);
|
|
1452
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1453
|
+
stt:configured.stt.default?.providerId,
|
|
1454
|
+
tts:configured.tts.default?.providerId
|
|
1455
|
+
});
|
|
1456
|
+
await this.#reconcileLegacyLLMReadiness();
|
|
1457
|
+
await this.#reconcileLegacySpeechReadiness();
|
|
547
1458
|
return configured;
|
|
548
1459
|
}
|
|
549
1460
|
|
|
@@ -632,6 +1543,127 @@ class AI {
|
|
|
632
1543
|
return null;
|
|
633
1544
|
}
|
|
634
1545
|
|
|
1546
|
+
async #requestLegacySpeechTranscription(payload={},signal=null){
|
|
1547
|
+
const audio=payload?.audio;
|
|
1548
|
+
if(!audio||typeof audio.arrayBuffer!=='function'){
|
|
1549
|
+
throw new TypeError('Speech transcription requires an audio Blob or File.');
|
|
1550
|
+
}
|
|
1551
|
+
if(signal?.aborted){
|
|
1552
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1553
|
+
}
|
|
1554
|
+
const mimeType=String(payload.mimeType||audio.type||'audio/webm');
|
|
1555
|
+
const model=String(payload.model||this.modelSTT);
|
|
1556
|
+
const nativeSpeech=this.#nativeSpeech(this.sttService,'stt');
|
|
1557
|
+
if(nativeSpeech){
|
|
1558
|
+
const audioBytes=await audio.arrayBuffer();
|
|
1559
|
+
if(signal?.aborted){
|
|
1560
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1561
|
+
}
|
|
1562
|
+
const response=await nativeSpeech.transcribe({
|
|
1563
|
+
audioBase64:this.#arrayBufferToBase64(audioBytes),
|
|
1564
|
+
mimeType,
|
|
1565
|
+
model
|
|
1566
|
+
});
|
|
1567
|
+
if(signal?.aborted){
|
|
1568
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1569
|
+
}
|
|
1570
|
+
if(!response||typeof response.text!=='string'){
|
|
1571
|
+
throw new TypeError('Arcane returned an invalid local speech transcription.');
|
|
1572
|
+
}
|
|
1573
|
+
return response.text;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
await this.#assertAndroidSpeechBridge(this.sttService);
|
|
1577
|
+
const formData=new FormData();
|
|
1578
|
+
formData.append('file',audio);
|
|
1579
|
+
formData.append('model',model);
|
|
1580
|
+
formData.append('response_format','text');
|
|
1581
|
+
const response=await fetch(
|
|
1582
|
+
this.urlSTT,
|
|
1583
|
+
{
|
|
1584
|
+
method:'POST',
|
|
1585
|
+
credentials,
|
|
1586
|
+
headers:this.#sttHeaders[this.sttService],
|
|
1587
|
+
body:formData,
|
|
1588
|
+
signal
|
|
1589
|
+
}
|
|
1590
|
+
);
|
|
1591
|
+
if(!response.ok){
|
|
1592
|
+
throw new Error(`Speech transcription failed with status ${response.status}.`);
|
|
1593
|
+
}
|
|
1594
|
+
return response.text();
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
async #requestLegacySpeechSynthesis(payload={},signal=null){
|
|
1598
|
+
const input=typeof payload?.input==='string'?payload.input:'';
|
|
1599
|
+
if(!input){
|
|
1600
|
+
throw new TypeError('Speech synthesis requires nonempty input.');
|
|
1601
|
+
}
|
|
1602
|
+
if(signal?.aborted){
|
|
1603
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1604
|
+
}
|
|
1605
|
+
const model=String(payload.model||this.modelTTS);
|
|
1606
|
+
const voice=typeof payload.voice==='string'&&payload.voice.trim()
|
|
1607
|
+
?payload.voice.trim()
|
|
1608
|
+
:this.#legacySpeechDefaultVoice('tts',this.ttsService);
|
|
1609
|
+
if(!voice){
|
|
1610
|
+
throw new TypeError('The selected speech provider requires a voice.');
|
|
1611
|
+
}
|
|
1612
|
+
const responseFormat=String(payload.responseFormat||this.audioFormat);
|
|
1613
|
+
const speed=Number.isFinite(payload.speed)?payload.speed:this.voiceSpeed;
|
|
1614
|
+
const nativeSpeech=this.#nativeSpeech(this.ttsService,'tts');
|
|
1615
|
+
if(nativeSpeech){
|
|
1616
|
+
const response=await nativeSpeech.synthesize({
|
|
1617
|
+
model,
|
|
1618
|
+
voice,
|
|
1619
|
+
input,
|
|
1620
|
+
responseFormat,
|
|
1621
|
+
speed
|
|
1622
|
+
});
|
|
1623
|
+
if(signal?.aborted){
|
|
1624
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1625
|
+
}
|
|
1626
|
+
if(!response||typeof response.audioBase64!=='string'){
|
|
1627
|
+
throw new TypeError('Arcane returned an invalid local speech response.');
|
|
1628
|
+
}
|
|
1629
|
+
return new Blob(
|
|
1630
|
+
[this.#base64ToBytes(response.audioBase64)],
|
|
1631
|
+
{
|
|
1632
|
+
type:typeof response.contentType==='string'
|
|
1633
|
+
?response.contentType
|
|
1634
|
+
:this.audioType
|
|
1635
|
+
}
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
await this.#assertAndroidSpeechBridge(this.ttsService);
|
|
1640
|
+
const personality=await window.user?.personality
|
|
1641
|
+
||'A behavioral health technician with a slight veteran feel on occasion.';
|
|
1642
|
+
const religion=await window.user?.religion||'caring';
|
|
1643
|
+
const response=await fetch(
|
|
1644
|
+
this.urlTTS,
|
|
1645
|
+
{
|
|
1646
|
+
method:'POST',
|
|
1647
|
+
credentials,
|
|
1648
|
+
headers:this.#ttsHeaders[this.ttsService],
|
|
1649
|
+
body:JSON.stringify({
|
|
1650
|
+
model,
|
|
1651
|
+
voice,
|
|
1652
|
+
input,
|
|
1653
|
+
speed,
|
|
1654
|
+
instructions:`${personality} and sounding a bit ${religion}`,
|
|
1655
|
+
response_format:responseFormat
|
|
1656
|
+
}),
|
|
1657
|
+
signal
|
|
1658
|
+
}
|
|
1659
|
+
);
|
|
1660
|
+
if(!response.ok){
|
|
1661
|
+
throw new Error(`Speech synthesis failed with status ${response.status}.`);
|
|
1662
|
+
}
|
|
1663
|
+
const contentType=response.headers.get('content-type')||this.audioType;
|
|
1664
|
+
return new Blob([await response.arrayBuffer()],{type:contentType});
|
|
1665
|
+
}
|
|
1666
|
+
|
|
635
1667
|
async #androidNativeHost(){
|
|
636
1668
|
if(typeof globalThis.arcaneAndroid?.postMessage==='function'){
|
|
637
1669
|
return true;
|
|
@@ -912,6 +1944,55 @@ class AI {
|
|
|
912
1944
|
return typeof content==='string'?content:completion;
|
|
913
1945
|
}
|
|
914
1946
|
|
|
1947
|
+
#requestLegacyLLMChat(payload={},signal=null){
|
|
1948
|
+
return this.#fetchLegacy(
|
|
1949
|
+
payload.messages??[],
|
|
1950
|
+
function ignoreLegacyLLMProviderResponse(){},
|
|
1951
|
+
payload.structuredOutput??false,
|
|
1952
|
+
payload.tools??[],
|
|
1953
|
+
payload.toolChoice??'auto',
|
|
1954
|
+
payload.parallelToolCalls??true,
|
|
1955
|
+
payload.id??Date.now(),
|
|
1956
|
+
function ignoreLegacyLLMProviderRequest(){},
|
|
1957
|
+
signal
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
#requestLegacyLLMStream(payload={},bridge){
|
|
1962
|
+
function emitLegacyLLMStreamText(text,id,thinking){
|
|
1963
|
+
if(typeof text!=='string'||!text){
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
bridge.emit(
|
|
1967
|
+
thinking
|
|
1968
|
+
?{thinking:text}
|
|
1969
|
+
:{content:text}
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
function emitLegacyLLMStreamTool(name){
|
|
1974
|
+
if(typeof name==='string'&&name){
|
|
1975
|
+
bridge.emit({toolCalls:[{name}]});
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
return this.#streamLegacyMessage(
|
|
1980
|
+
payload.messages??[],
|
|
1981
|
+
emitLegacyLLMStreamText,
|
|
1982
|
+
function ignoreLegacyLLMProviderCompletion(){},
|
|
1983
|
+
payload.tools??[],
|
|
1984
|
+
payload.toolChoice??'auto',
|
|
1985
|
+
emitLegacyLLMStreamTool,
|
|
1986
|
+
payload.parallelToolCalls??true,
|
|
1987
|
+
payload.id??Date.now(),
|
|
1988
|
+
payload.seeThinking??false,
|
|
1989
|
+
bridge.signal,
|
|
1990
|
+
function ignoreLegacyLLMProviderRequest(){},
|
|
1991
|
+
payload.structuredOutput??false,
|
|
1992
|
+
false
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
915
1996
|
#assertRequiredOllamaToolCall(toolCalls=[],toolChoice='auto'){
|
|
916
1997
|
const requiredName=toolChoice?.function?.name;
|
|
917
1998
|
|
|
@@ -1152,6 +2233,38 @@ class AI {
|
|
|
1152
2233
|
signal
|
|
1153
2234
|
});
|
|
1154
2235
|
}
|
|
2236
|
+
|
|
2237
|
+
return this.#streamLegacyMessage(
|
|
2238
|
+
messages,
|
|
2239
|
+
streamHandler,
|
|
2240
|
+
streamComplete,
|
|
2241
|
+
tools,
|
|
2242
|
+
tool_choice,
|
|
2243
|
+
earlyFunctionTrigger,
|
|
2244
|
+
parallel_tool_calls,
|
|
2245
|
+
id,
|
|
2246
|
+
seeThinking,
|
|
2247
|
+
signal,
|
|
2248
|
+
requestHandler,
|
|
2249
|
+
structuredOutput
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
async #streamLegacyMessage(
|
|
2254
|
+
messages=[],
|
|
2255
|
+
streamHandler=function ignoreStreamChunk(){},
|
|
2256
|
+
streamComplete=function finishIgnoredStream(){},
|
|
2257
|
+
tools=[],
|
|
2258
|
+
tool_choice='auto',
|
|
2259
|
+
earlyFunctionTrigger=function ignoreEarlyFunction(){},
|
|
2260
|
+
parallel_tool_calls=true,
|
|
2261
|
+
id=Date.now(),
|
|
2262
|
+
seeThinking=false,
|
|
2263
|
+
signal=null,
|
|
2264
|
+
requestHandler=function ignoreStreamRequest(){},
|
|
2265
|
+
structuredOutput=false,
|
|
2266
|
+
finishSpeech=true
|
|
2267
|
+
){
|
|
1155
2268
|
let speechTurnCompleted=false;
|
|
1156
2269
|
|
|
1157
2270
|
try{
|
|
@@ -1198,6 +2311,13 @@ class AI {
|
|
|
1198
2311
|
|
|
1199
2312
|
const nativeOllama=this.#nativeOllama();
|
|
1200
2313
|
|
|
2314
|
+
if(this.llmService==='OLLAMA'&&!nativeOllama){
|
|
2315
|
+
throw legacyAIProviderError(
|
|
2316
|
+
'Local AI requires the capability-gated Arcane API.',
|
|
2317
|
+
'AI_NATIVE_LOCAL_REQUIRED'
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
|
|
1201
2321
|
if(nativeOllama){
|
|
1202
2322
|
let nativeContent='';
|
|
1203
2323
|
const nativeToolCalls={};
|
|
@@ -1294,7 +2414,9 @@ class AI {
|
|
|
1294
2414
|
if(Object.keys(nativeToolCalls).length&&!nativeContent){
|
|
1295
2415
|
streamHandler('',`M-${id}`,false);
|
|
1296
2416
|
}
|
|
1297
|
-
|
|
2417
|
+
if(finishSpeech){
|
|
2418
|
+
this.finishTTS();
|
|
2419
|
+
}
|
|
1298
2420
|
await streamComplete(nativeResult,`M-${id}`,isThinking);
|
|
1299
2421
|
speechTurnCompleted=true;
|
|
1300
2422
|
return nativeResult;
|
|
@@ -1496,7 +2618,9 @@ class AI {
|
|
|
1496
2618
|
if(Object.keys(tool_funcs).length&&!chunkString){
|
|
1497
2619
|
streamHandler('',`M-${id}`,false);
|
|
1498
2620
|
}
|
|
1499
|
-
|
|
2621
|
+
if(finishSpeech){
|
|
2622
|
+
this.finishTTS();
|
|
2623
|
+
}
|
|
1500
2624
|
await streamComplete(streamResult, `M-${id}`,isThinking);
|
|
1501
2625
|
|
|
1502
2626
|
//sync
|
|
@@ -1609,6 +2733,31 @@ class AI {
|
|
|
1609
2733
|
onResponse:responseHandler
|
|
1610
2734
|
});
|
|
1611
2735
|
}
|
|
2736
|
+
|
|
2737
|
+
return this.#fetchLegacy(
|
|
2738
|
+
messages,
|
|
2739
|
+
responseHandler,
|
|
2740
|
+
structuredOutput,
|
|
2741
|
+
tools,
|
|
2742
|
+
tool_choice,
|
|
2743
|
+
parallel_tool_calls,
|
|
2744
|
+
id,
|
|
2745
|
+
requestHandler,
|
|
2746
|
+
signal
|
|
2747
|
+
);
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
async #fetchLegacy(
|
|
2751
|
+
messages=[],
|
|
2752
|
+
responseHandler=function ignoreFetchResponse(){},
|
|
2753
|
+
structuredOutput=false,
|
|
2754
|
+
tools=[],
|
|
2755
|
+
tool_choice='auto',
|
|
2756
|
+
parallel_tool_calls=true,
|
|
2757
|
+
id=Date.now(),
|
|
2758
|
+
requestHandler=function ignoreFetchRequest(){},
|
|
2759
|
+
signal=null,
|
|
2760
|
+
){
|
|
1612
2761
|
this.#assertServiceConfigured(this.llmService);
|
|
1613
2762
|
if(signal&&(
|
|
1614
2763
|
typeof signal.aborted!=='boolean'
|
|
@@ -1645,6 +2794,13 @@ class AI {
|
|
|
1645
2794
|
|
|
1646
2795
|
const nativeOllama=this.#nativeOllama();
|
|
1647
2796
|
|
|
2797
|
+
if(this.llmService==='OLLAMA'&&!nativeOllama){
|
|
2798
|
+
throw legacyAIProviderError(
|
|
2799
|
+
'Local AI requires the capability-gated Arcane API.',
|
|
2800
|
+
'AI_NATIVE_LOCAL_REQUIRED'
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
|
|
1648
2804
|
if(nativeOllama){
|
|
1649
2805
|
const ollamaTools=this.#ollamaTools(tools,tool_choice);
|
|
1650
2806
|
const ollamaMessages=this.#ollamaMessages(messages,tool_choice);
|
|
@@ -1931,15 +3087,17 @@ class AI {
|
|
|
1931
3087
|
async #requestSpeechAudio(job){
|
|
1932
3088
|
if(this.#usesProviderRuntime('tts',this.ttsService)){
|
|
1933
3089
|
job.abortController=new AbortController();
|
|
3090
|
+
const responseFormat=this.#providerSpeechResponseFormat();
|
|
3091
|
+
const voice=this.#providerSpeechVoice();
|
|
1934
3092
|
const response=await this.#providerRuntime.request(
|
|
1935
3093
|
'tts',
|
|
1936
3094
|
{
|
|
1937
3095
|
operation:'synthesize',
|
|
1938
3096
|
payload:{
|
|
1939
3097
|
model:this.#providerRuntime.selection('tts')?.modelId,
|
|
1940
|
-
voice:String(window.user?.AI_voice||'af_heart'),
|
|
1941
3098
|
input:job.text,
|
|
1942
|
-
responseFormat
|
|
3099
|
+
responseFormat,
|
|
3100
|
+
...(voice?{voice}:{}),
|
|
1943
3101
|
speed:this.voiceSpeed
|
|
1944
3102
|
},
|
|
1945
3103
|
localOnly:false,
|
|
@@ -1949,83 +3107,74 @@ class AI {
|
|
|
1949
3107
|
return this.#normalizeProviderSpeechAudio(response);
|
|
1950
3108
|
}
|
|
1951
3109
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
const response=await nativeSpeech.synthesize({
|
|
3110
|
+
job.abortController=new AbortController();
|
|
3111
|
+
const response=await this.#requestLegacySpeechSynthesis(
|
|
3112
|
+
{
|
|
1956
3113
|
model:this.modelTTS,
|
|
1957
|
-
voice:String(window.user?.AI_voice||'af_heart'),
|
|
1958
3114
|
input:job.text,
|
|
1959
3115
|
responseFormat:this.audioFormat,
|
|
1960
3116
|
speed:this.voiceSpeed
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
3117
|
+
},
|
|
3118
|
+
job.abortController.signal
|
|
3119
|
+
);
|
|
3120
|
+
return this.#normalizeProviderSpeechAudio(response);
|
|
3121
|
+
}
|
|
1966
3122
|
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
:this.audioType
|
|
1972
|
-
};
|
|
3123
|
+
#providerSpeechVoice(){
|
|
3124
|
+
const selection=this.#providerRuntime.selection('tts');
|
|
3125
|
+
if(!selection){
|
|
3126
|
+
return null;
|
|
1973
3127
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
job.abortController=new AbortController();
|
|
1978
|
-
const personality=await window.user?.personality
|
|
1979
|
-
||'A behavioral health technician with a slight veteran feel on occasion.';
|
|
1980
|
-
const religion=await window.user?.religion||'caring';
|
|
1981
|
-
const request={
|
|
1982
|
-
model:this.modelTTS,
|
|
1983
|
-
voice:window.user?.AI_voice,
|
|
1984
|
-
input:job.text,
|
|
1985
|
-
speed:this.voiceSpeed,
|
|
1986
|
-
instructions:`${personality} and sounding a bit ${religion}`,
|
|
1987
|
-
response_format:this.audioFormat
|
|
1988
|
-
};
|
|
1989
|
-
const response=await fetch(
|
|
1990
|
-
this.urlTTS,
|
|
1991
|
-
{
|
|
1992
|
-
method:'POST',
|
|
1993
|
-
credentials,
|
|
1994
|
-
headers:this.#ttsHeaders[this.ttsService],
|
|
1995
|
-
body:JSON.stringify(request),
|
|
1996
|
-
signal:job.abortController.signal
|
|
1997
|
-
}
|
|
3128
|
+
const provider=this.#providerRuntime.catalog('tts').find(
|
|
3129
|
+
entry=>entry.providerId===selection.providerId
|
|
1998
3130
|
);
|
|
3131
|
+
const model=provider?.models.find(entry=>entry?.id===selection.modelId);
|
|
3132
|
+
return typeof model?.defaultVoice==='string'&&model.defaultVoice.trim()
|
|
3133
|
+
?model.defaultVoice.trim()
|
|
3134
|
+
:null;
|
|
3135
|
+
}
|
|
1999
3136
|
|
|
2000
|
-
|
|
2001
|
-
|
|
3137
|
+
#providerSpeechResponseFormat(){
|
|
3138
|
+
const selection=this.#providerRuntime.selection('tts');
|
|
3139
|
+
if(!selection){
|
|
3140
|
+
return this.audioFormat;
|
|
2002
3141
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
3142
|
+
const provider=this.#providerRuntime.catalog('tts').find(
|
|
3143
|
+
entry=>entry.providerId===selection.providerId
|
|
3144
|
+
);
|
|
3145
|
+
const model=provider?.models.find(entry=>entry?.id===selection.modelId);
|
|
3146
|
+
const speech=model?.speech;
|
|
3147
|
+
if(speech===undefined){
|
|
3148
|
+
return this.audioFormat;
|
|
2008
3149
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
3150
|
+
const prototype=speech&&typeof speech==='object'
|
|
3151
|
+
?Object.getPrototypeOf(speech)
|
|
3152
|
+
:null;
|
|
3153
|
+
const descriptors=prototype===Object.prototype||prototype===null
|
|
3154
|
+
?Object.getOwnPropertyDescriptors(speech)
|
|
3155
|
+
:null;
|
|
3156
|
+
const formats=descriptors?.responseFormats?.value;
|
|
3157
|
+
const defaultFormat=descriptors?.defaultResponseFormat?.value;
|
|
3158
|
+
if(!Array.isArray(formats)
|
|
3159
|
+
||formats.length<1
|
|
3160
|
+
||!formats.every(format=>typeof format==='string'&&format.trim()===format&&format)
|
|
3161
|
+
||typeof defaultFormat!=='string'
|
|
3162
|
+
||!formats.includes(defaultFormat)){
|
|
3163
|
+
throw legacyAIProviderError(
|
|
3164
|
+
'The selected TTS provider returned an invalid speech format catalog.',
|
|
3165
|
+
'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
|
|
3166
|
+
);
|
|
2026
3167
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
3168
|
+
if(formats.includes(this.audioFormat)){
|
|
3169
|
+
return this.audioFormat;
|
|
3170
|
+
}
|
|
3171
|
+
if(this.audioFormat===LEGACY_TTS_RESPONSE_FORMAT){
|
|
3172
|
+
return defaultFormat;
|
|
3173
|
+
}
|
|
3174
|
+
throw legacyAIProviderError(
|
|
3175
|
+
`The selected TTS provider does not support ${this.audioFormat}.`,
|
|
3176
|
+
'ARCANE_AI_UNSUPPORTED_RESPONSE_FORMAT'
|
|
3177
|
+
);
|
|
2029
3178
|
}
|
|
2030
3179
|
|
|
2031
3180
|
async #normalizeProviderSpeechAudio(response){
|
|
@@ -2148,55 +3297,15 @@ class AI {
|
|
|
2148
3297
|
return text;
|
|
2149
3298
|
}
|
|
2150
3299
|
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
if(nativeSpeech){
|
|
2154
|
-
if(!audioFile||typeof audioFile.arrayBuffer!=='function'){
|
|
2155
|
-
throw new TypeError('Speech transcription requires an audio Blob or File.');
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
const response=await nativeSpeech.transcribe({
|
|
2159
|
-
audioBase64:this.#arrayBufferToBase64(await audioFile.arrayBuffer()),
|
|
2160
|
-
mimeType:String(audioFile.type||'audio/webm'),
|
|
2161
|
-
model:this.modelSTT
|
|
2162
|
-
});
|
|
2163
|
-
|
|
2164
|
-
if(!response||typeof response.text!=='string'){
|
|
2165
|
-
throw new TypeError('Arcane returned an invalid local speech transcription.');
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
await responseHandler(response.text);
|
|
2169
|
-
return response.text;
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
await this.#assertAndroidSpeechBridge(this.sttService);
|
|
2173
|
-
|
|
2174
|
-
const formData = new FormData();
|
|
2175
|
-
formData.append('file', audioFile);
|
|
2176
|
-
formData.append('model', this.modelSTT);
|
|
2177
|
-
formData.append('response_format', 'text');
|
|
2178
|
-
|
|
2179
|
-
const response = await fetch(
|
|
2180
|
-
this.urlSTT,
|
|
3300
|
+
const text=await this.#requestLegacySpeechTranscription(
|
|
2181
3301
|
{
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
}
|
|
3302
|
+
audio:audioFile,
|
|
3303
|
+
mimeType:String(audioFile?.type||'audio/webm'),
|
|
3304
|
+
model:this.modelSTT
|
|
3305
|
+
},
|
|
3306
|
+
signal
|
|
2188
3307
|
);
|
|
2189
|
-
|
|
2190
|
-
if(!response.ok){
|
|
2191
|
-
throw new Error(`Speech transcription failed with status ${response.status}.`);
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
const text = await response.text();
|
|
2195
|
-
|
|
2196
|
-
//async
|
|
2197
3308
|
await responseHandler(text);
|
|
2198
|
-
|
|
2199
|
-
//sync
|
|
2200
3309
|
return text;
|
|
2201
3310
|
}
|
|
2202
3311
|
|