arcane-os 0.5.17 → 0.5.18
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 +24 -0
- package/README.md +9 -9
- package/bin/arcane-test.mjs +16 -13
- package/browser-runtime/ai/browser-speech-artifacts.mjs +41 -38
- package/browser-runtime/ai/browser-speech-providers.mjs +46 -43
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +127 -124
- package/browser-runtime/ai/browser-wasm.mjs +6 -3
- package/browser-runtime/ai/browser-wllama-runtime.mjs +26 -23
- package/browser-runtime/ai/model-controller.mjs +94 -91
- package/browser-runtime/ai/speech-worker-client.mjs +12 -9
- package/browser-runtime/ai/speech-worker-runtime.mjs +56 -53
- package/browser-runtime/dependencies/strong-type/index.js +207 -82
- package/browser-runtime/dependencies/strong-type/package.json +15 -6
- package/browser-runtime/dom-event-instrumentation.mjs +24 -20
- package/browser-runtime/event-manager.mjs +111 -108
- package/browser-runtime/speech-text.mjs +5 -1
- package/docs/reference/ai/browser-speech.md +198 -5
- package/docs/reference/availability-and-normalization.md +40 -0
- package/docs/reference/inventory/package-api.json +1 -1
- package/docs/reference/inventory/runtime-components.json +1 -1
- package/docs/reference/inventory/runtime-modules.json +20 -24
- package/docs/reference/protocols.md +1 -1
- package/docs/reference/runtime-modules.md +125 -35
- package/docs/reference/sdk-api.md +23 -7
- package/examples/wasm-ai-demo/index.html +1 -0
- package/node_modules/event-pubsub/node_modules/strong-type/README.md +408 -0
- package/node_modules/event-pubsub/node_modules/strong-type/assets/strong-type-header.png +0 -0
- package/node_modules/event-pubsub/node_modules/strong-type/index.js +1151 -0
- package/node_modules/event-pubsub/node_modules/strong-type/licence +21 -0
- package/node_modules/event-pubsub/node_modules/strong-type/node.js +125 -0
- package/node_modules/event-pubsub/node_modules/strong-type/package.json +61 -0
- package/node_modules/strong-type/README.md +42 -24
- package/node_modules/strong-type/benchmark/core.js +324 -0
- package/node_modules/strong-type/index.js +207 -82
- package/node_modules/strong-type/package.json +14 -6
- package/package.json +3 -3
- package/runtime/arcane/components/app-bar.html +5 -2
- package/runtime/arcane/components/assistant-panel.html +5 -2
- package/runtime/arcane/components/chart.html +15 -12
- package/runtime/arcane/components/chat.html +117 -138
- package/runtime/arcane/components/dashboard-config.html +6 -3
- package/runtime/arcane/components/data-view.html +4 -1
- package/runtime/arcane/components/directory-picker.html +5 -2
- package/runtime/arcane/components/document-inspector.html +6 -3
- package/runtime/arcane/components/file-drop.html +5 -2
- package/runtime/arcane/components/file-inspector.html +9 -6
- package/runtime/arcane/components/file-manager.html +16 -13
- package/runtime/arcane/components/local-ai-status.html +5 -2
- package/runtime/arcane/components/markdown-document.html +14 -11
- package/runtime/arcane/components/markdown-editor.html +4 -1
- package/runtime/arcane/components/modal.html +4 -1
- package/runtime/arcane/components/output-panel.html +6 -3
- package/runtime/arcane/components/preferences-form.html +4 -1
- package/runtime/arcane/components/record-timeline.html +8 -5
- package/runtime/arcane/components/relationship-board.html +9 -6
- package/runtime/arcane/components/source-code-viewer.html +12 -9
- package/runtime/arcane/components/source-explanation.html +8 -5
- package/runtime/arcane/components/speech.html +24 -21
- package/runtime/arcane/components/summary-strip.html +4 -1
- package/runtime/arcane/components/task-progress.html +6 -3
- package/runtime/arcane/components/terminal-workspace.html +4 -1
- package/runtime/arcane/components/voice-transcription.html +11 -8
- package/runtime/arcane/components/web-navigator.html +5 -2
- package/runtime/arcane/entities/ApiModelRecord.js +5 -2
- package/runtime/arcane/entities/Calculation.js +5 -2
- package/runtime/arcane/entities/Chat.js +33 -33
- package/runtime/arcane/entities/CommunicationMessage.js +4 -1
- package/runtime/arcane/entities/CommunicationThread.js +4 -1
- package/runtime/arcane/entities/File.js +1 -1
- package/runtime/arcane/entities/Image.js +8 -5
- package/runtime/arcane/entities/IntentEnvelope.js +20 -17
- package/runtime/arcane/entities/Preference.js +9 -6
- package/runtime/arcane/entities/Theme.js +5 -2
- package/runtime/arcane/entities/User.js +11 -11
- package/runtime/arcane/entities/Weather.js +5 -2
- package/runtime/arcane/modules/AI.js +491 -165
- package/runtime/arcane/modules/AIPreferenceRuntime.js +5 -2
- package/runtime/arcane/modules/AIPreferenceTuple.js +9 -6
- package/runtime/arcane/modules/AIProviderRuntime.js +88 -85
- package/runtime/arcane/modules/AIResponseURLPolicy.js +148 -62
- package/runtime/arcane/modules/AIRuntimeState.js +29 -26
- package/runtime/arcane/modules/AnsiText.js +4 -1
- package/runtime/arcane/modules/ApiModelDatabase.js +22 -19
- package/runtime/arcane/modules/AppDataScope.js +8 -5
- package/runtime/arcane/modules/ArcaneCommunicationBridge.js +4 -1
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +8 -5
- package/runtime/arcane/modules/ArcaneNetworkPolicy.js +18 -15
- package/runtime/arcane/modules/AsyncBoundary.js +13 -10
- package/runtime/arcane/modules/BrowserTestSuite.js +19 -16
- package/runtime/arcane/modules/CalculatorEngine.js +5 -2
- package/runtime/arcane/modules/ChatRecords.js +18 -15
- package/runtime/arcane/modules/CommunicationAppController.js +9 -6
- package/runtime/arcane/modules/CommunicationHub.js +9 -6
- package/runtime/arcane/modules/CommunicationPreferences.js +4 -1
- package/runtime/arcane/modules/CommunicationProviderRegistry.js +4 -1
- package/runtime/arcane/modules/ComponentContracts.js +56 -53
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +30 -27
- package/runtime/arcane/modules/ConversationActionItems.js +15 -12
- package/runtime/arcane/modules/ConversationClosingReport.js +11 -8
- package/runtime/arcane/modules/ConversationTimebox.js +28 -25
- package/runtime/arcane/modules/CoreLocalModelCatalog.js +17 -14
- package/runtime/arcane/modules/DBLS.js +7 -4
- package/runtime/arcane/modules/DBOPFS.js +7 -7
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +34 -31
- package/runtime/arcane/modules/DevelopmentWorkspace.js +4 -1
- package/runtime/arcane/modules/DirectoryPicker.js +8 -5
- package/runtime/arcane/modules/DocumentLexicalSearch.js +11 -8
- package/runtime/arcane/modules/DocumentNavigation.js +7 -4
- package/runtime/arcane/modules/Errors.js +28 -25
- package/runtime/arcane/modules/HTMLImport.js +7 -4
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +15 -12
- package/runtime/arcane/modules/LocalAIReadiness.js +21 -18
- package/runtime/arcane/modules/LocalAIReadinessController.js +6 -3
- package/runtime/arcane/modules/MD.js +1 -1
- package/runtime/arcane/modules/Mail.js +46 -43
- package/runtime/arcane/modules/MailOutbox.mjs +48 -45
- package/runtime/arcane/modules/MailTransport.mjs +29 -26
- package/runtime/arcane/modules/MemoryRecords.js +7 -4
- package/runtime/arcane/modules/MessageAdvisory.js +6 -3
- package/runtime/arcane/modules/ModelDefinition.js +7 -4
- package/runtime/arcane/modules/Ollama.js +6 -3
- package/runtime/arcane/modules/OllamaModelIdentifier.js +4 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +18 -15
- package/runtime/arcane/modules/PersistentAIChatSession.js +33 -30
- package/runtime/arcane/modules/PreferenceStore.js +20 -17
- package/runtime/arcane/modules/PreparedSpeech.js +485 -0
- package/runtime/arcane/modules/Questionnaire.js +6 -3
- package/runtime/arcane/modules/RecordLinkIndex.js +4 -1
- package/runtime/arcane/modules/RecordPassageIndex.js +9 -6
- package/runtime/arcane/modules/RecordReviewStore.js +12 -9
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +4 -1
- package/runtime/arcane/modules/ScopedOPFSCache.js +6 -3
- package/runtime/arcane/modules/ScreenCapture.js +20 -17
- package/runtime/arcane/modules/SpeechPlayback.js +29 -26
- package/runtime/arcane/modules/StaticDocumentCatalog.js +33 -30
- package/runtime/arcane/modules/SystemAppearance.js +5 -2
- package/runtime/arcane/modules/SystemToolRegistry.js +6 -3
- package/runtime/arcane/modules/TerminalClient.js +14 -11
- package/runtime/arcane/modules/TerminalCommandRegistry.js +4 -1
- package/runtime/arcane/modules/ThemeBootstrap.js +9 -6
- package/runtime/arcane/modules/ThemeManager.js +5 -2
- package/runtime/arcane/modules/TimeGuard.js +1 -1
- package/runtime/arcane/modules/ToolCallRouter.js +11 -8
- package/runtime/arcane/modules/WaitForComponent.js +19 -16
- package/runtime/arcane/modules/YouTubeMedia.js +4 -1
- package/runtime/strong-type/index.js +1276 -352
- package/runtime/strong-type/package.json +69 -45
- package/src/app-descriptor.mjs +17 -14
- package/src/application-tests.mjs +4 -1
- package/src/cli/main.mjs +8 -5
- package/src/constants.mjs +4 -1
- package/src/dev-server.mjs +8 -5
- package/src/doctor.mjs +5 -2
- package/src/dom-event-instrumentation.mjs +24 -20
- package/src/errors.mjs +7 -3
- package/src/event-manager.mjs +111 -108
- package/src/event-queue.mjs +8 -5
- package/src/events.mjs +12 -9
- package/src/import-map.mjs +35 -27
- package/src/installed-sdk-runtime.mjs +4 -1
- package/src/integrated-provider-loader.mjs +9 -6
- package/src/mail-credentials.mjs +16 -13
- package/src/mail-server.mjs +53 -50
- package/src/mail.mjs +18 -15
- package/src/native-plan.mjs +12 -9
- package/src/native-provider-loader.mjs +7 -4
- package/src/packager/core.mjs +18 -15
- package/src/process.mjs +6 -3
- package/src/release-bundle.mjs +14 -11
- package/src/runtime.mjs +4 -1
- package/src/scaffold.mjs +9 -6
- package/src/sdk-browser-runtime.mjs +4 -1
- package/src/source-server.mjs +17 -14
- package/src/targets/index.mjs +5 -2
- package/src/templates/workspace-template.mjs +13 -6
- package/src/testing-loader.mjs +7 -4
- package/src/testing.mjs +10 -7
- package/src/toolchain.mjs +13 -10
- package/src/update-check.mjs +9 -6
- package/src/workspace-operation-lock.mjs +14 -11
- package/src/workspace-runtime.mjs +4 -1
- package/src/workspace.mjs +17 -14
- package/runtime/arcane/modules/AIResponseLength.js +0 -32
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
const is=new Is(false);
|
|
3
|
+
|
|
1
4
|
import './DBOPFS.js';
|
|
2
5
|
import UserEntity from '../entities/User.js';
|
|
3
6
|
import {
|
|
@@ -14,6 +17,7 @@ import {
|
|
|
14
17
|
import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
|
|
15
18
|
import {arcaneLogging} from 'arcane-os/logging';
|
|
16
19
|
import {MarkdownSpeech,stripSpeechFormatting} from 'arcane-os/speech-text';
|
|
20
|
+
import {prepareSpeech} from './PreparedSpeech.js';
|
|
17
21
|
|
|
18
22
|
const completeValue=(value)=>value;
|
|
19
23
|
|
|
@@ -141,7 +145,7 @@ function normalizeAIReasoningEffort(value){
|
|
|
141
145
|
if(value===undefined||value===null||value===''){
|
|
142
146
|
return '';
|
|
143
147
|
}
|
|
144
|
-
if(
|
|
148
|
+
if(!is.string(value)||!AI_REASONING_EFFORTS.has(value)){
|
|
145
149
|
const error=new TypeError(
|
|
146
150
|
'AI reasoningEffort must be none, low, medium, high, or max.'
|
|
147
151
|
);
|
|
@@ -168,7 +172,7 @@ function aiStructuralError(code,message,cause){
|
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
function isPlainAIRecord(value){
|
|
171
|
-
if(!value
|
|
175
|
+
if(!value||!is.object(value)||is.array(value)){
|
|
172
176
|
return false;
|
|
173
177
|
}
|
|
174
178
|
const prototype=Object.getPrototypeOf(value);
|
|
@@ -197,7 +201,7 @@ function normalizeTTSSegmentation(value,current=DEFAULT_TTS_SEGMENTATION){
|
|
|
197
201
|
:value.wordCadence;
|
|
198
202
|
if(
|
|
199
203
|
wordCadence!==null
|
|
200
|
-
&&(!
|
|
204
|
+
&&(!is.safeInteger(wordCadence)||wordCadence<1)
|
|
201
205
|
){
|
|
202
206
|
throw new RangeError(
|
|
203
207
|
'TTS segmentation wordCadence must be null or a positive integer.'
|
|
@@ -208,7 +212,7 @@ function normalizeTTSSegmentation(value,current=DEFAULT_TTS_SEGMENTATION){
|
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
function requireAIToolMessageSchemas(value,label='AI tools'){
|
|
211
|
-
if(!
|
|
215
|
+
if(!is.array(value)){
|
|
212
216
|
throw aiStructuralError(
|
|
213
217
|
'AI_CHAT_INVALID_TOOL_CALL',
|
|
214
218
|
`${label} must be an array.`
|
|
@@ -227,9 +231,9 @@ function requireAIToolMessageSchemas(value,label='AI tools'){
|
|
|
227
231
|
||!isPlainAIRecord(parameters.properties)
|
|
228
232
|
||!isPlainAIRecord(messageSchema)
|
|
229
233
|
||messageSchema.type!=='string'
|
|
230
|
-
||!
|
|
234
|
+
||!is.integer(messageSchema.minLength)
|
|
231
235
|
||messageSchema.minLength<1
|
|
232
|
-
||!
|
|
236
|
+
||!is.array(parameters.required)
|
|
233
237
|
||!parameters.required.includes('message')
|
|
234
238
|
){
|
|
235
239
|
throw aiStructuralError(
|
|
@@ -243,13 +247,13 @@ function requireAIToolMessageSchemas(value,label='AI tools'){
|
|
|
243
247
|
function normalizeAIStructuralToolCall(call,label='Structural tool call'){
|
|
244
248
|
if(
|
|
245
249
|
!isPlainAIRecord(call)
|
|
246
|
-
|
|
250
|
+
||!is.string(call.id)
|
|
247
251
|
||!call.id.trim()
|
|
248
252
|
||call.type!=='function'
|
|
249
253
|
||!isPlainAIRecord(call.function)
|
|
250
|
-
|
|
254
|
+
||!is.string(call.function.name)
|
|
251
255
|
||!call.function.name.trim()
|
|
252
|
-
|
|
256
|
+
||!is.string(call.function.arguments)
|
|
253
257
|
){
|
|
254
258
|
throw aiStructuralError(
|
|
255
259
|
'AI_CHAT_INVALID_TOOL_CALL',
|
|
@@ -272,7 +276,7 @@ function normalizeAIStructuralToolCall(call,label='Structural tool call'){
|
|
|
272
276
|
`${label} arguments must encode a JSON object.`
|
|
273
277
|
);
|
|
274
278
|
}
|
|
275
|
-
if(
|
|
279
|
+
if(!is.string(argumentsRecord.message)||!argumentsRecord.message.trim()){
|
|
276
280
|
throw aiStructuralError(
|
|
277
281
|
'AI_CHAT_TOOL_MESSAGE_REQUIRED',
|
|
278
282
|
`${label} arguments must include a nonempty user-facing message.`
|
|
@@ -291,7 +295,7 @@ function normalizeAIStructuralToolCall(call,label='Structural tool call'){
|
|
|
291
295
|
}
|
|
292
296
|
|
|
293
297
|
function validateAIRequestMessages(messages=[]){
|
|
294
|
-
if(!
|
|
298
|
+
if(!is.array(messages)){
|
|
295
299
|
throw new TypeError('AI messages must be an array.');
|
|
296
300
|
}
|
|
297
301
|
const pendingToolCallIds=new Set();
|
|
@@ -300,7 +304,7 @@ function validateAIRequestMessages(messages=[]){
|
|
|
300
304
|
const calls=message?.tool_calls;
|
|
301
305
|
let openedToolCall=false;
|
|
302
306
|
if(calls!==undefined){
|
|
303
|
-
if(message?.role!=='assistant'||!
|
|
307
|
+
if(message?.role!=='assistant'||!is.array(calls)){
|
|
304
308
|
throw aiStructuralError(
|
|
305
309
|
'AI_CHAT_INVALID_TOOL_CALL',
|
|
306
310
|
`AI messages[${messageIndex}].tool_calls is invalid.`
|
|
@@ -330,7 +334,7 @@ function validateAIRequestMessages(messages=[]){
|
|
|
330
334
|
}
|
|
331
335
|
}
|
|
332
336
|
if(message?.role==='tool'){
|
|
333
|
-
if(
|
|
337
|
+
if(!is.string(message.content)||!message.content.trim()){
|
|
334
338
|
throw aiStructuralError(
|
|
335
339
|
'AI_CHAT_INVALID_TOOL_MESSAGE',
|
|
336
340
|
`AI messages[${messageIndex}] must contain a nonblank user-facing tool result.`
|
|
@@ -338,7 +342,7 @@ function validateAIRequestMessages(messages=[]){
|
|
|
338
342
|
}
|
|
339
343
|
if(
|
|
340
344
|
!pendingToolCallIds.size
|
|
341
|
-
|
|
345
|
+
||!is.string(message.tool_call_id)
|
|
342
346
|
||!pendingToolCallIds.has(message.tool_call_id)
|
|
343
347
|
){
|
|
344
348
|
throw aiStructuralError(
|
|
@@ -365,7 +369,7 @@ function validateAIRequestMessages(messages=[]){
|
|
|
365
369
|
function validateAIStructuralRequest(messages,tools,parallelToolCalls){
|
|
366
370
|
validateAIRequestMessages(messages);
|
|
367
371
|
requireAIToolMessageSchemas(tools);
|
|
368
|
-
if(parallelToolCalls!==undefined
|
|
372
|
+
if(parallelToolCalls!==undefined&&!is.boolean(parallelToolCalls)){
|
|
369
373
|
throw new TypeError('AI parallelToolCalls must be a boolean when provided.');
|
|
370
374
|
}
|
|
371
375
|
}
|
|
@@ -374,12 +378,12 @@ function normalizeAICompletionToolCalls(completion){
|
|
|
374
378
|
const calls=[];
|
|
375
379
|
const hasMessage=Boolean(
|
|
376
380
|
completion
|
|
377
|
-
&&
|
|
381
|
+
&&is.object(completion)
|
|
378
382
|
&&Object.hasOwn(completion,'message')
|
|
379
383
|
);
|
|
380
384
|
const hasChoices=Boolean(
|
|
381
385
|
completion
|
|
382
|
-
&&
|
|
386
|
+
&&is.object(completion)
|
|
383
387
|
&&Object.hasOwn(completion,'choices')
|
|
384
388
|
);
|
|
385
389
|
if(hasMessage&&hasChoices){
|
|
@@ -388,7 +392,7 @@ function normalizeAICompletionToolCalls(completion){
|
|
|
388
392
|
'The AI completion must not mix message and choices envelopes.'
|
|
389
393
|
);
|
|
390
394
|
}
|
|
391
|
-
if(hasChoices&&!
|
|
395
|
+
if(hasChoices&&!is.array(completion.choices)){
|
|
392
396
|
throw aiStructuralError(
|
|
393
397
|
'AI_CHAT_INVALID_RESPONSE',
|
|
394
398
|
'The AI completion choices envelope must be an array.'
|
|
@@ -401,7 +405,7 @@ function normalizeAICompletionToolCalls(completion){
|
|
|
401
405
|
:[];
|
|
402
406
|
for(let messageIndex=0;messageIndex<messages.length;messageIndex+=1){
|
|
403
407
|
const toolCalls=messages[messageIndex]?.tool_calls;
|
|
404
|
-
if(toolCalls!==undefined&&!
|
|
408
|
+
if(toolCalls!==undefined&&!is.array(toolCalls)){
|
|
405
409
|
throw aiStructuralError(
|
|
406
410
|
'AI_CHAT_INVALID_TOOL_CALL',
|
|
407
411
|
`AI response message ${messageIndex+1} contains invalid structural tool calls.`
|
|
@@ -428,12 +432,12 @@ function normalizeAICompletionToolCalls(completion){
|
|
|
428
432
|
|
|
429
433
|
function sameAIDataValue(left,right,seen=new Map()){
|
|
430
434
|
if(Object.is(left,right))return true;
|
|
431
|
-
if(!left||!right
|
|
432
|
-
if(
|
|
435
|
+
if(!left||!right||!is.object(left)||!is.object(right))return false;
|
|
436
|
+
if(is.array(left)!==is.array(right))return false;
|
|
433
437
|
const matched=seen.get(left);
|
|
434
438
|
if(matched!==undefined)return matched===right;
|
|
435
439
|
seen.set(left,right);
|
|
436
|
-
if(
|
|
440
|
+
if(is.array(left)){
|
|
437
441
|
return left.length===right.length
|
|
438
442
|
&&left.every((value,index)=>sameAIDataValue(value,right[index],seen));
|
|
439
443
|
}
|
|
@@ -507,11 +511,11 @@ function isAIStreamStructuralKey(key){
|
|
|
507
511
|
const OMITTED_AI_STREAM_DATA=Symbol('omitted-ai-stream-data');
|
|
508
512
|
|
|
509
513
|
function projectAIStreamData(value,seen=new Map()){
|
|
510
|
-
if(value===null||value===undefined
|
|
514
|
+
if(value===null||value===undefined||!is.object(value)){
|
|
511
515
|
return value;
|
|
512
516
|
}
|
|
513
517
|
if(seen.has(value)) return seen.get(value);
|
|
514
|
-
if(
|
|
518
|
+
if(is.array(value)){
|
|
515
519
|
const result=[];
|
|
516
520
|
seen.set(value,result);
|
|
517
521
|
for(const item of value){
|
|
@@ -688,7 +692,7 @@ function normalizeAIStartupOptions(options){
|
|
|
688
692
|
signal:null
|
|
689
693
|
});
|
|
690
694
|
}
|
|
691
|
-
if(!options
|
|
695
|
+
if(!options||!is.object(options)||is.array(options)){
|
|
692
696
|
throw new TypeError('AI startup options must be a plain object.');
|
|
693
697
|
}
|
|
694
698
|
const prototype=Object.getPrototypeOf(options);
|
|
@@ -697,7 +701,7 @@ function normalizeAIStartupOptions(options){
|
|
|
697
701
|
}
|
|
698
702
|
const descriptors=Object.getOwnPropertyDescriptors(options);
|
|
699
703
|
for(const key of Reflect.ownKeys(descriptors)){
|
|
700
|
-
if(
|
|
704
|
+
if(is.symbol(key)||(
|
|
701
705
|
key!=='startLanguageModel'
|
|
702
706
|
&&key!=='startMuted'
|
|
703
707
|
&&key!=='startTranscription'
|
|
@@ -721,20 +725,20 @@ function normalizeAIStartupOptions(options){
|
|
|
721
725
|
const signal=Object.hasOwn(descriptors,'signal')
|
|
722
726
|
?descriptors.signal.value
|
|
723
727
|
:null;
|
|
724
|
-
if(
|
|
728
|
+
if(!is.boolean(startLanguageModel)){
|
|
725
729
|
throw new TypeError('AI startup startLanguageModel must be a boolean.');
|
|
726
730
|
}
|
|
727
|
-
if(
|
|
731
|
+
if(!is.boolean(startMuted)){
|
|
728
732
|
throw new TypeError('AI startup startMuted must be a boolean.');
|
|
729
733
|
}
|
|
730
|
-
if(
|
|
734
|
+
if(!is.boolean(startTranscription)){
|
|
731
735
|
throw new TypeError('AI startup startTranscription must be a boolean.');
|
|
732
736
|
}
|
|
733
737
|
if(signal!==null&&signal!==undefined&&(
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
+
!is.object(signal)
|
|
739
|
+
||!is.boolean(signal.aborted)
|
|
740
|
+
||!is.function(signal.addEventListener)
|
|
741
|
+
||!is.function(signal.removeEventListener)
|
|
738
742
|
)){
|
|
739
743
|
throw new TypeError('AI startup signal must be an AbortSignal.');
|
|
740
744
|
}
|
|
@@ -759,16 +763,16 @@ function aiBrowserSpeechError(code,reason,message,cause,{committed=false,name='E
|
|
|
759
763
|
|
|
760
764
|
function isAbortSignal(value){
|
|
761
765
|
return Boolean(value)
|
|
762
|
-
&&
|
|
763
|
-
&&
|
|
764
|
-
&&
|
|
765
|
-
&&
|
|
766
|
+
&&is.object(value)
|
|
767
|
+
&&is.boolean(value.aborted)
|
|
768
|
+
&&is.function(value.addEventListener)
|
|
769
|
+
&&is.function(value.removeEventListener);
|
|
766
770
|
}
|
|
767
771
|
|
|
768
772
|
function closedRecord(value,keys,required,label){
|
|
769
773
|
if(!value
|
|
770
|
-
|
|
771
|
-
||
|
|
774
|
+
||!is.object(value)
|
|
775
|
+
||is.array(value)
|
|
772
776
|
||![Object.prototype,null].includes(Object.getPrototypeOf(value))){
|
|
773
777
|
throw aiBrowserSpeechError(
|
|
774
778
|
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
@@ -778,7 +782,7 @@ function closedRecord(value,keys,required,label){
|
|
|
778
782
|
}
|
|
779
783
|
const descriptors=Object.getOwnPropertyDescriptors(value);
|
|
780
784
|
for(const key of Reflect.ownKeys(descriptors)){
|
|
781
|
-
if(
|
|
785
|
+
if(!is.string(key)
|
|
782
786
|
||!keys.includes(key)
|
|
783
787
|
||!Object.hasOwn(descriptors[key],'value')){
|
|
784
788
|
throw aiBrowserSpeechError(
|
|
@@ -801,7 +805,7 @@ function closedRecord(value,keys,required,label){
|
|
|
801
805
|
}
|
|
802
806
|
|
|
803
807
|
function browserSpeechIdentifier(value,label){
|
|
804
|
-
if(
|
|
808
|
+
if(!is.string(value)
|
|
805
809
|
||value.trim()!==value
|
|
806
810
|
||value.length<1){
|
|
807
811
|
throw aiBrowserSpeechError(
|
|
@@ -829,14 +833,14 @@ function normalizeBrowserTTSExecution(value){
|
|
|
829
833
|
const maxConcurrentRequests=descriptors.maxConcurrentRequests
|
|
830
834
|
?descriptors.maxConcurrentRequests.value
|
|
831
835
|
:DEFAULT_BROWSER_TTS_EXECUTION.maxConcurrentRequests;
|
|
832
|
-
if(
|
|
836
|
+
if(!is.string(device)||!BROWSER_TTS_EXECUTION_DEVICES.has(device)){
|
|
833
837
|
throw aiBrowserSpeechError(
|
|
834
838
|
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
835
839
|
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
836
840
|
'AI browser speech tts.execution.device must be auto, webgpu, or wasm.'
|
|
837
841
|
);
|
|
838
842
|
}
|
|
839
|
-
if(!
|
|
843
|
+
if(!is.safeInteger(maxConcurrentRequests)
|
|
840
844
|
||maxConcurrentRequests<1
|
|
841
845
|
||maxConcurrentRequests>MAX_BROWSER_TTS_CONCURRENT_REQUESTS){
|
|
842
846
|
throw aiBrowserSpeechError(
|
|
@@ -881,7 +885,7 @@ function normalizeBrowserSpeechRole(value,role){
|
|
|
881
885
|
}
|
|
882
886
|
if(hasGraph){
|
|
883
887
|
const graph=descriptors.graph.value;
|
|
884
|
-
if(!graph
|
|
888
|
+
if(!graph||!is.object(graph)){
|
|
885
889
|
throw aiBrowserSpeechError(
|
|
886
890
|
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
887
891
|
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
@@ -891,7 +895,7 @@ function normalizeBrowserSpeechRole(value,role){
|
|
|
891
895
|
}else{
|
|
892
896
|
for(const key of ['model','runtime']){
|
|
893
897
|
const descriptor=descriptors[key].value;
|
|
894
|
-
if(!descriptor
|
|
898
|
+
if(!descriptor||!is.object(descriptor)||is.array(descriptor)){
|
|
895
899
|
throw aiBrowserSpeechError(
|
|
896
900
|
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
897
901
|
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
@@ -906,7 +910,7 @@ function normalizeBrowserSpeechRole(value,role){
|
|
|
906
910
|
// an implementation may be enabled.
|
|
907
911
|
secure=descriptors.security.value?.secure===true;
|
|
908
912
|
}
|
|
909
|
-
if(
|
|
913
|
+
if(!is.boolean(descriptors.offline.value)){
|
|
910
914
|
throw aiBrowserSpeechError(
|
|
911
915
|
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
912
916
|
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
@@ -960,7 +964,7 @@ function normalizeBrowserSpeechConfiguration(value){
|
|
|
960
964
|
'AI browser speech configuration.id'
|
|
961
965
|
);
|
|
962
966
|
const dbopfs=descriptors.dbopfs.value;
|
|
963
|
-
if(!dbopfs
|
|
967
|
+
if(!dbopfs||!is.object(dbopfs)){
|
|
964
968
|
throw aiBrowserSpeechError(
|
|
965
969
|
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
966
970
|
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
@@ -997,8 +1001,8 @@ function normalizeBrowserSpeechConfiguration(value){
|
|
|
997
1001
|
function normalizeBrowserSpeechOperationOptions(value,label){
|
|
998
1002
|
if(value===undefined)return completeValue({signal:null});
|
|
999
1003
|
if(!value
|
|
1000
|
-
|
|
1001
|
-
||
|
|
1004
|
+
||!is.object(value)
|
|
1005
|
+
||is.array(value)
|
|
1002
1006
|
||![Object.prototype,null].includes(Object.getPrototypeOf(value))){
|
|
1003
1007
|
throw aiBrowserSpeechError(
|
|
1004
1008
|
AI_BROWSER_SPEECH_ERROR_CODES.operationOptionsContractMismatch,
|
|
@@ -1185,6 +1189,9 @@ class AI {
|
|
|
1185
1189
|
#speechJobSequence=0;
|
|
1186
1190
|
#stopOllamaReady=null;
|
|
1187
1191
|
#ttsSegmentation={...DEFAULT_TTS_SEGMENTATION};
|
|
1192
|
+
#preparedSpeechReadiness = null;
|
|
1193
|
+
#preparedSpeechPlaybacks = new Set();
|
|
1194
|
+
#speechUnlockContext = null;
|
|
1188
1195
|
|
|
1189
1196
|
#traceSpeech(phase,detail={}){
|
|
1190
1197
|
if(!arcaneLogging.enabled)return null;
|
|
@@ -1294,7 +1301,7 @@ class AI {
|
|
|
1294
1301
|
}
|
|
1295
1302
|
|
|
1296
1303
|
set twinKey(value){
|
|
1297
|
-
this.#license=
|
|
1304
|
+
this.#license=is.string(value) ? value.trim():'';
|
|
1298
1305
|
this.#retainBuiltInLLMReadiness(
|
|
1299
1306
|
this.#reconcileBuiltInLLMReadiness()
|
|
1300
1307
|
);
|
|
@@ -1317,7 +1324,7 @@ class AI {
|
|
|
1317
1324
|
return this.llmService==='TWIN'
|
|
1318
1325
|
&&Boolean(this.model)
|
|
1319
1326
|
&&Boolean(this.license)
|
|
1320
|
-
&&
|
|
1327
|
+
&&is.function(globalThis.fetch);
|
|
1321
1328
|
}
|
|
1322
1329
|
if(providerId==='OLLAMA'){
|
|
1323
1330
|
return this.llmService==='OLLAMA'
|
|
@@ -1434,7 +1441,7 @@ class AI {
|
|
|
1434
1441
|
'ARCANE_AI_ROLE_BUSY'
|
|
1435
1442
|
);
|
|
1436
1443
|
}
|
|
1437
|
-
if(
|
|
1444
|
+
if(!is.function(context.progress)){
|
|
1438
1445
|
throw new TypeError(
|
|
1439
1446
|
'Built-in LLM provider load progress must be a function.'
|
|
1440
1447
|
);
|
|
@@ -1684,7 +1691,7 @@ class AI {
|
|
|
1684
1691
|
'ARCANE_AI_ROLE_BUSY'
|
|
1685
1692
|
);
|
|
1686
1693
|
}
|
|
1687
|
-
if(
|
|
1694
|
+
if(!is.function(context.progress)){
|
|
1688
1695
|
throw new TypeError(
|
|
1689
1696
|
`Built-in ${role.toUpperCase()} provider load progress must be a function.`
|
|
1690
1697
|
);
|
|
@@ -2052,7 +2059,7 @@ class AI {
|
|
|
2052
2059
|
if(value===undefined||value===null||value===''){
|
|
2053
2060
|
return current[index];
|
|
2054
2061
|
}
|
|
2055
|
-
if(
|
|
2062
|
+
if(!is.string(value)||value.trim()!==value||!value){
|
|
2056
2063
|
throw new TypeError('AI preferences must be nonempty trimmed strings.');
|
|
2057
2064
|
}
|
|
2058
2065
|
return value;
|
|
@@ -2487,7 +2494,7 @@ class AI {
|
|
|
2487
2494
|
|
|
2488
2495
|
#browserSpeechOperationId(action){
|
|
2489
2496
|
this.#browserSpeechOperationSequence=
|
|
2490
|
-
|
|
2497
|
+
is.bigint(this.#browserSpeechOperationSequence)
|
|
2491
2498
|
? this.#browserSpeechOperationSequence+1n
|
|
2492
2499
|
: this.#browserSpeechOperationSequence===Number.MAX_SAFE_INTEGER
|
|
2493
2500
|
? BigInt(this.#browserSpeechOperationSequence)+1n
|
|
@@ -2709,7 +2716,7 @@ class AI {
|
|
|
2709
2716
|
publicDetail:completeValue({
|
|
2710
2717
|
configurationId:normalized.id,
|
|
2711
2718
|
...(descriptor?{descriptor}:{}),
|
|
2712
|
-
...(
|
|
2719
|
+
...(is.string(error?.code)?{code:error.code}:{}),
|
|
2713
2720
|
reason
|
|
2714
2721
|
})
|
|
2715
2722
|
}
|
|
@@ -2737,7 +2744,7 @@ class AI {
|
|
|
2737
2744
|
continue;
|
|
2738
2745
|
}
|
|
2739
2746
|
const catalog=providers[role].catalog();
|
|
2740
|
-
if(catalog.length!==1
|
|
2747
|
+
if(catalog.length!==1||!is.string(catalog[0]?.id)){
|
|
2741
2748
|
throw aiBrowserSpeechError(
|
|
2742
2749
|
AI_BROWSER_SPEECH_ERROR_CODES.providerConstructionRejected,
|
|
2743
2750
|
AI_BROWSER_SPEECH_REASONS.providerConstructionRejected,
|
|
@@ -2951,7 +2958,7 @@ class AI {
|
|
|
2951
2958
|
for(const role of ['tts','stt'].filter(role=>roles.includes(role))){
|
|
2952
2959
|
if(record.registrationState?.[role]===false)continue;
|
|
2953
2960
|
const unregister=record.unregisters?.[role];
|
|
2954
|
-
if(
|
|
2961
|
+
if(!is.function(unregister)){
|
|
2955
2962
|
failures.push(aiBrowserSpeechError(
|
|
2956
2963
|
AI_BROWSER_SPEECH_ERROR_CODES.providerUnregistrationRejected,
|
|
2957
2964
|
AI_BROWSER_SPEECH_REASONS.providerUnregistrationRejected,
|
|
@@ -3745,7 +3752,7 @@ class AI {
|
|
|
3745
3752
|
|
|
3746
3753
|
async setSpeechMuted(muted){
|
|
3747
3754
|
const callId=this.#traceSpeech('setSpeechMuted.call',{requestedMuted:muted});
|
|
3748
|
-
if(
|
|
3755
|
+
if(!is.boolean(muted)){
|
|
3749
3756
|
throw new TypeError('AI speech muted state must be a boolean.');
|
|
3750
3757
|
}
|
|
3751
3758
|
const generation=++this.#speechControlGeneration;
|
|
@@ -3792,12 +3799,12 @@ class AI {
|
|
|
3792
3799
|
if(signal?.aborted){
|
|
3793
3800
|
throw normalizeAIRequestAbort(signal.reason);
|
|
3794
3801
|
}
|
|
3795
|
-
const message=
|
|
3802
|
+
const message=is.string(error)
|
|
3796
3803
|
?error
|
|
3797
3804
|
:error?.error?.message??error?.message;
|
|
3798
3805
|
if(
|
|
3799
3806
|
response.status!==429
|
|
3800
|
-
|
|
3807
|
+
||!is.string(message)
|
|
3801
3808
|
||!message.toLowerCase().includes('overload')
|
|
3802
3809
|
){
|
|
3803
3810
|
throw error;
|
|
@@ -3836,7 +3843,7 @@ class AI {
|
|
|
3836
3843
|
const client=globalThis.Arcane?.ollama;
|
|
3837
3844
|
|
|
3838
3845
|
return this.llmService==='OLLAMA'
|
|
3839
|
-
&&
|
|
3846
|
+
&&is.function(client?.chat)
|
|
3840
3847
|
?client
|
|
3841
3848
|
:null;
|
|
3842
3849
|
}
|
|
@@ -3847,10 +3854,10 @@ class AI {
|
|
|
3847
3854
|
if(service!=='LOCAL_SPEACH'){
|
|
3848
3855
|
return null;
|
|
3849
3856
|
}
|
|
3850
|
-
if(role==='tts'&&
|
|
3857
|
+
if(role==='tts'&&is.function(client?.synthesize)){
|
|
3851
3858
|
return client;
|
|
3852
3859
|
}
|
|
3853
|
-
if(role==='stt'&&
|
|
3860
|
+
if(role==='stt'&&is.function(client?.transcribe)){
|
|
3854
3861
|
return client;
|
|
3855
3862
|
}
|
|
3856
3863
|
return null;
|
|
@@ -3858,7 +3865,7 @@ class AI {
|
|
|
3858
3865
|
|
|
3859
3866
|
async #requestBuiltInSpeechTranscription(payload={},signal=null){
|
|
3860
3867
|
const audio=payload?.audio;
|
|
3861
|
-
if(!audio
|
|
3868
|
+
if(!audio||!is.function(audio.arrayBuffer)){
|
|
3862
3869
|
throw new TypeError('Speech transcription requires an audio Blob or File.');
|
|
3863
3870
|
}
|
|
3864
3871
|
if(signal?.aborted){
|
|
@@ -3880,7 +3887,7 @@ class AI {
|
|
|
3880
3887
|
if(signal?.aborted){
|
|
3881
3888
|
throw normalizeAIRequestAbort(signal.reason);
|
|
3882
3889
|
}
|
|
3883
|
-
if(!response
|
|
3890
|
+
if(!response||!is.string(response.text)){
|
|
3884
3891
|
throw new TypeError('Arcane returned an invalid local speech transcription.');
|
|
3885
3892
|
}
|
|
3886
3893
|
return response.text;
|
|
@@ -3908,7 +3915,7 @@ class AI {
|
|
|
3908
3915
|
}
|
|
3909
3916
|
|
|
3910
3917
|
async #requestBuiltInSpeechSynthesis(payload={},signal=null){
|
|
3911
|
-
const input=
|
|
3918
|
+
const input=is.string(payload?.input)?payload.input:'';
|
|
3912
3919
|
if(!input){
|
|
3913
3920
|
throw new TypeError('Speech synthesis requires nonempty input.');
|
|
3914
3921
|
}
|
|
@@ -3916,14 +3923,14 @@ class AI {
|
|
|
3916
3923
|
throw normalizeAIRequestAbort(signal.reason);
|
|
3917
3924
|
}
|
|
3918
3925
|
const model=String(payload.model||this.modelTTS);
|
|
3919
|
-
const voice=
|
|
3926
|
+
const voice=is.string(payload.voice)&&payload.voice.trim()
|
|
3920
3927
|
?payload.voice.trim()
|
|
3921
3928
|
:this.#builtInSpeechDefaultVoice('tts',this.ttsService);
|
|
3922
3929
|
if(!voice){
|
|
3923
3930
|
throw new TypeError('The selected speech provider requires a voice.');
|
|
3924
3931
|
}
|
|
3925
3932
|
const responseFormat=String(payload.responseFormat||this.audioFormat);
|
|
3926
|
-
const speed=
|
|
3933
|
+
const speed=is.finite(payload.speed)?payload.speed:this.voiceSpeed;
|
|
3927
3934
|
const nativeSpeech=this.#nativeSpeech(this.ttsService,'tts');
|
|
3928
3935
|
if(nativeSpeech){
|
|
3929
3936
|
const response=await nativeSpeech.synthesize({
|
|
@@ -3936,7 +3943,7 @@ class AI {
|
|
|
3936
3943
|
if(signal?.aborted){
|
|
3937
3944
|
throw normalizeAIRequestAbort(signal.reason);
|
|
3938
3945
|
}
|
|
3939
|
-
if(!response
|
|
3946
|
+
if(!response||!is.string(response.audioBase64)){
|
|
3940
3947
|
const error=new TypeError(
|
|
3941
3948
|
'The Arcane speech bridge returned invalid TTS audio.'
|
|
3942
3949
|
);
|
|
@@ -3946,7 +3953,7 @@ class AI {
|
|
|
3946
3953
|
return new Blob(
|
|
3947
3954
|
[this.#base64ToBytes(response.audioBase64)],
|
|
3948
3955
|
{
|
|
3949
|
-
type:
|
|
3956
|
+
type:is.string(response.contentType)
|
|
3950
3957
|
?response.contentType
|
|
3951
3958
|
:this.audioType
|
|
3952
3959
|
}
|
|
@@ -3995,7 +4002,7 @@ class AI {
|
|
|
3995
4002
|
}
|
|
3996
4003
|
|
|
3997
4004
|
async #androidNativeHost(){
|
|
3998
|
-
if(
|
|
4005
|
+
if(is.function(globalThis.arcaneAndroid?.postMessage)){
|
|
3999
4006
|
return true;
|
|
4000
4007
|
}
|
|
4001
4008
|
|
|
@@ -4037,7 +4044,7 @@ class AI {
|
|
|
4037
4044
|
}
|
|
4038
4045
|
|
|
4039
4046
|
#base64ToBytes(value){
|
|
4040
|
-
if(
|
|
4047
|
+
if(!is.string(value)||!value){
|
|
4041
4048
|
throw new TypeError('Arcane returned invalid local speech audio.');
|
|
4042
4049
|
}
|
|
4043
4050
|
|
|
@@ -4052,7 +4059,7 @@ class AI {
|
|
|
4052
4059
|
}
|
|
4053
4060
|
|
|
4054
4061
|
#ollamaTools(tools=[],toolChoice='auto'){
|
|
4055
|
-
if(!
|
|
4062
|
+
if(!is.array(tools)){
|
|
4056
4063
|
return [];
|
|
4057
4064
|
}
|
|
4058
4065
|
|
|
@@ -4076,12 +4083,12 @@ class AI {
|
|
|
4076
4083
|
const role=String(message?.role??'');
|
|
4077
4084
|
const content=String(message?.content??'');
|
|
4078
4085
|
|
|
4079
|
-
if(role==='assistant'&&
|
|
4086
|
+
if(role==='assistant'&&is.array(message?.tool_calls)){
|
|
4080
4087
|
const toolCalls=message.tool_calls.map(function sanitizeOllamaToolCall(call){
|
|
4081
4088
|
const name=String(call?.function?.name??'').trim();
|
|
4082
4089
|
const callId=String(call?.id??'').trim();
|
|
4083
4090
|
let argumentValue=call?.function?.arguments;
|
|
4084
|
-
if(
|
|
4091
|
+
if(is.string(argumentValue)){
|
|
4085
4092
|
try{
|
|
4086
4093
|
argumentValue=JSON.parse(argumentValue);
|
|
4087
4094
|
}catch(cause){
|
|
@@ -4095,8 +4102,8 @@ class AI {
|
|
|
4095
4102
|
}
|
|
4096
4103
|
if(
|
|
4097
4104
|
!argumentValue
|
|
4098
|
-
|
|
4099
|
-
||
|
|
4105
|
+
||!is.object(argumentValue)
|
|
4106
|
+
||is.array(argumentValue)
|
|
4100
4107
|
){
|
|
4101
4108
|
const error=new TypeError(
|
|
4102
4109
|
'Ollama tool call arguments must contain a JSON object.'
|
|
@@ -4172,8 +4179,8 @@ class AI {
|
|
|
4172
4179
|
return 'json';
|
|
4173
4180
|
}
|
|
4174
4181
|
if(
|
|
4175
|
-
|
|
4176
|
-
&&!
|
|
4182
|
+
is.object(value)
|
|
4183
|
+
&&!is.array(value)
|
|
4177
4184
|
&&(
|
|
4178
4185
|
Object.getPrototypeOf(value)===Object.prototype
|
|
4179
4186
|
||Object.getPrototypeOf(value)===null
|
|
@@ -4207,7 +4214,7 @@ class AI {
|
|
|
4207
4214
|
}
|
|
4208
4215
|
|
|
4209
4216
|
async #reportRequest(requestHandler,request,id,metadata){
|
|
4210
|
-
if(
|
|
4217
|
+
if(!is.function(requestHandler)){
|
|
4211
4218
|
throw new TypeError('AI onRequest callback must be a function.');
|
|
4212
4219
|
}
|
|
4213
4220
|
await requestHandler(request,id,metadata);
|
|
@@ -4215,31 +4222,31 @@ class AI {
|
|
|
4215
4222
|
|
|
4216
4223
|
#providerStreamEmissions(chunk,seeThinking){
|
|
4217
4224
|
const chunks=[];
|
|
4218
|
-
if(
|
|
4225
|
+
if(is.string(chunk)){
|
|
4219
4226
|
if(chunk) chunks.push({text:chunk,thinking:false});
|
|
4220
4227
|
return {chunks};
|
|
4221
4228
|
}
|
|
4222
|
-
const choices=
|
|
4229
|
+
const choices=is.array(chunk?.choices)?chunk.choices:[];
|
|
4223
4230
|
for(const choice of choices){
|
|
4224
4231
|
const delta=choice?.delta||{};
|
|
4225
4232
|
if(
|
|
4226
4233
|
seeThinking
|
|
4227
|
-
&&
|
|
4234
|
+
&&is.string(delta.reasoning_content)
|
|
4228
4235
|
&&delta.reasoning_content
|
|
4229
4236
|
){
|
|
4230
4237
|
chunks.push({text:delta.reasoning_content,thinking:true});
|
|
4231
4238
|
}
|
|
4232
|
-
if(
|
|
4239
|
+
if(is.string(delta.content)&&delta.content){
|
|
4233
4240
|
chunks.push({text:delta.content,thinking:false});
|
|
4234
4241
|
}
|
|
4235
4242
|
}
|
|
4236
4243
|
if(!choices.length){
|
|
4237
|
-
if(seeThinking&&
|
|
4244
|
+
if(seeThinking&&is.string(chunk?.thinking)){
|
|
4238
4245
|
chunks.push({text:chunk.thinking,thinking:true});
|
|
4239
4246
|
}
|
|
4240
|
-
const text=
|
|
4247
|
+
const text=is.string(chunk?.text)
|
|
4241
4248
|
?chunk.text
|
|
4242
|
-
:
|
|
4249
|
+
:is.string(chunk?.content)
|
|
4243
4250
|
?chunk.content
|
|
4244
4251
|
:'';
|
|
4245
4252
|
if(text){
|
|
@@ -4250,16 +4257,16 @@ class AI {
|
|
|
4250
4257
|
}
|
|
4251
4258
|
|
|
4252
4259
|
#providerCompletionOutput(completion){
|
|
4253
|
-
if(
|
|
4260
|
+
if(is.string(completion)){
|
|
4254
4261
|
return completion;
|
|
4255
4262
|
}
|
|
4256
|
-
if(
|
|
4263
|
+
if(is.array(completion?.choices)&&completion.choices.length>1){
|
|
4257
4264
|
return completion;
|
|
4258
4265
|
}
|
|
4259
4266
|
const structuralToolCalls=normalizeAICompletionToolCalls(completion);
|
|
4260
4267
|
if(structuralToolCalls.length)return structuralToolCalls;
|
|
4261
4268
|
const content=completion?.choices?.[0]?.message?.content;
|
|
4262
|
-
return
|
|
4269
|
+
return is.string(content)?content:completion;
|
|
4263
4270
|
}
|
|
4264
4271
|
|
|
4265
4272
|
#requestBuiltInLLMChat(payload={},signal=null){
|
|
@@ -4331,14 +4338,14 @@ class AI {
|
|
|
4331
4338
|
if(value===undefined){
|
|
4332
4339
|
return [];
|
|
4333
4340
|
}
|
|
4334
|
-
if(!
|
|
4341
|
+
if(!is.array(value)){
|
|
4335
4342
|
throw aiStructuralError(
|
|
4336
4343
|
'AI_CHAT_INVALID_TOOL_CALL',
|
|
4337
4344
|
'The native Ollama response contains invalid structural tool calls.'
|
|
4338
4345
|
);
|
|
4339
4346
|
}
|
|
4340
4347
|
return value.map(function adaptNativeOllamaToolCall(call,index){
|
|
4341
|
-
if(!call
|
|
4348
|
+
if(!call||!is.object(call)||is.array(call)){
|
|
4342
4349
|
throw aiStructuralError(
|
|
4343
4350
|
'AI_CHAT_INVALID_TOOL_CALL',
|
|
4344
4351
|
`The native Ollama structural tool call ${index+1} is invalid.`
|
|
@@ -4347,8 +4354,8 @@ class AI {
|
|
|
4347
4354
|
const nativeFunction=call.function;
|
|
4348
4355
|
if(
|
|
4349
4356
|
!nativeFunction
|
|
4350
|
-
|
|
4351
|
-
||
|
|
4357
|
+
||!is.object(nativeFunction)
|
|
4358
|
+
||is.array(nativeFunction)
|
|
4352
4359
|
){
|
|
4353
4360
|
throw aiStructuralError(
|
|
4354
4361
|
'AI_CHAT_INVALID_TOOL_CALL',
|
|
@@ -4358,8 +4365,8 @@ class AI {
|
|
|
4358
4365
|
let encodedArguments=nativeFunction.arguments;
|
|
4359
4366
|
if(
|
|
4360
4367
|
encodedArguments
|
|
4361
|
-
&&
|
|
4362
|
-
&&!
|
|
4368
|
+
&&is.object(encodedArguments)
|
|
4369
|
+
&&!is.array(encodedArguments)
|
|
4363
4370
|
){
|
|
4364
4371
|
try{
|
|
4365
4372
|
encodedArguments=JSON.stringify(encodedArguments);
|
|
@@ -4393,7 +4400,7 @@ class AI {
|
|
|
4393
4400
|
:{};
|
|
4394
4401
|
const responseFields={...responseRecord};
|
|
4395
4402
|
delete responseFields.message;
|
|
4396
|
-
const responseId=
|
|
4403
|
+
const responseId=is.string(response?.id)&&response.id
|
|
4397
4404
|
?response.id
|
|
4398
4405
|
:`ollama-${id}`;
|
|
4399
4406
|
const adaptedToolCalls=this.#openAICompatibleOllamaToolCalls(
|
|
@@ -4405,7 +4412,7 @@ class AI {
|
|
|
4405
4412
|
});
|
|
4406
4413
|
const messageRecord={
|
|
4407
4414
|
...message,
|
|
4408
|
-
role:
|
|
4415
|
+
role:is.string(message.role)&&message.role
|
|
4409
4416
|
?message.role
|
|
4410
4417
|
:'assistant',
|
|
4411
4418
|
content:Object.hasOwn(message,'content')?message.content:'',
|
|
@@ -4732,8 +4739,8 @@ class AI {
|
|
|
4732
4739
|
validateAIStructuralRequest(messages,tools,parallel_tool_calls);
|
|
4733
4740
|
this.#assertServiceConfigured(this.llmService);
|
|
4734
4741
|
if(signal&&(
|
|
4735
|
-
|
|
4736
|
-
|
|
4742
|
+
!is.boolean(signal.aborted)
|
|
4743
|
+
||!is.function(signal.addEventListener)
|
|
4737
4744
|
)){
|
|
4738
4745
|
throw new TypeError('AI request signal must be an AbortSignal.');
|
|
4739
4746
|
}
|
|
@@ -4971,7 +4978,7 @@ class AI {
|
|
|
4971
4978
|
const toolFunction=toolCall.function||{};
|
|
4972
4979
|
if(
|
|
4973
4980
|
toolCall.index!==undefined
|
|
4974
|
-
&&(!
|
|
4981
|
+
&&(!is.safeInteger(toolCall.index)||toolCall.index<0)
|
|
4975
4982
|
){
|
|
4976
4983
|
throw aiStructuralError(
|
|
4977
4984
|
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
|
|
@@ -4991,7 +4998,7 @@ class AI {
|
|
|
4991
4998
|
};
|
|
4992
4999
|
|
|
4993
5000
|
if(toolCall.id!==undefined){
|
|
4994
|
-
if(
|
|
5001
|
+
if(!is.string(toolCall.id)||!toolCall.id){
|
|
4995
5002
|
record.invalidIdentity=true;
|
|
4996
5003
|
}else if(record.id&&record.id!==toolCall.id){
|
|
4997
5004
|
record.invalidIdentity=true;
|
|
@@ -5001,7 +5008,7 @@ class AI {
|
|
|
5001
5008
|
}
|
|
5002
5009
|
|
|
5003
5010
|
if(toolCall.type!==undefined){
|
|
5004
|
-
if(
|
|
5011
|
+
if(!is.string(toolCall.type)||!toolCall.type){
|
|
5005
5012
|
record.invalidIdentity=true;
|
|
5006
5013
|
}else if(record.type&&record.type!==toolCall.type){
|
|
5007
5014
|
record.invalidIdentity=true;
|
|
@@ -5011,14 +5018,14 @@ class AI {
|
|
|
5011
5018
|
}
|
|
5012
5019
|
|
|
5013
5020
|
if(toolFunction.name!==undefined){
|
|
5014
|
-
if(
|
|
5021
|
+
if(!is.string(toolFunction.name)){
|
|
5015
5022
|
record.invalidName=true;
|
|
5016
5023
|
}else{
|
|
5017
5024
|
record.name+=toolFunction.name;
|
|
5018
5025
|
}
|
|
5019
5026
|
}
|
|
5020
5027
|
|
|
5021
|
-
if(
|
|
5028
|
+
if(is.string(toolFunction.arguments)){
|
|
5022
5029
|
record.arguments+=toolFunction.arguments;
|
|
5023
5030
|
}else if(toolFunction.arguments!==undefined){
|
|
5024
5031
|
record.invalidArguments=true;
|
|
@@ -5032,7 +5039,7 @@ class AI {
|
|
|
5032
5039
|
function receiveStreamedChoice(choice={},choicePosition=0){
|
|
5033
5040
|
if(
|
|
5034
5041
|
choice.index!==undefined
|
|
5035
|
-
&&(!
|
|
5042
|
+
&&(!is.safeInteger(choice.index)||choice.index<0)
|
|
5036
5043
|
){
|
|
5037
5044
|
throw aiStructuralError(
|
|
5038
5045
|
'AI_CHAT_INVALID_RESPONSE',
|
|
@@ -5064,8 +5071,8 @@ class AI {
|
|
|
5064
5071
|
if(key==='content'||key==='reasoning'||key==='reasoning_content'){
|
|
5065
5072
|
if(
|
|
5066
5073
|
!replaceText
|
|
5067
|
-
&&
|
|
5068
|
-
&&
|
|
5074
|
+
&&is.string(value)
|
|
5075
|
+
&&is.string(record.message[key])
|
|
5069
5076
|
){
|
|
5070
5077
|
record.message[key]+=value;
|
|
5071
5078
|
}else{
|
|
@@ -5076,7 +5083,7 @@ class AI {
|
|
|
5076
5083
|
record.message[key]=value;
|
|
5077
5084
|
}
|
|
5078
5085
|
}
|
|
5079
|
-
if(
|
|
5086
|
+
if(!is.string(record.message.role)||!record.message.role){
|
|
5080
5087
|
record.message.role='assistant';
|
|
5081
5088
|
}
|
|
5082
5089
|
if(!Object.hasOwn(record.message,'content')) record.message.content='';
|
|
@@ -5098,7 +5105,7 @@ class AI {
|
|
|
5098
5105
|
for(const [key,value] of Object.entries(streamedResponse)){
|
|
5099
5106
|
if(key!=='choices') streamMetadata[key]=value;
|
|
5100
5107
|
}
|
|
5101
|
-
const choices=
|
|
5108
|
+
const choices=is.array(streamedResponse.choices)
|
|
5102
5109
|
?streamedResponse.choices
|
|
5103
5110
|
:[];
|
|
5104
5111
|
for(let choicePosition=0;choicePosition<choices.length;choicePosition+=1){
|
|
@@ -5125,7 +5132,7 @@ class AI {
|
|
|
5125
5132
|
const choiceDelta=choice.delta??{};
|
|
5126
5133
|
const choiceMessage=choice.message??null;
|
|
5127
5134
|
if(Object.hasOwn(choiceDelta,'tool_calls')){
|
|
5128
|
-
if(!
|
|
5135
|
+
if(!is.array(choiceDelta.tool_calls)){
|
|
5129
5136
|
throw aiStructuralError(
|
|
5130
5137
|
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
|
|
5131
5138
|
`AI stream choice ${choicePosition+1} contains invalid structural tool-call data.`
|
|
@@ -5134,7 +5141,7 @@ class AI {
|
|
|
5134
5141
|
receiveStreamedToolCalls(choiceDelta.tool_calls,choiceIndex);
|
|
5135
5142
|
}
|
|
5136
5143
|
if(choiceMessage&&Object.hasOwn(choiceMessage,'tool_calls')){
|
|
5137
|
-
if(!
|
|
5144
|
+
if(!is.array(choiceMessage.tool_calls)){
|
|
5138
5145
|
throw aiStructuralError(
|
|
5139
5146
|
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
|
|
5140
5147
|
`AI stream choice ${choicePosition+1} contains invalid terminal structural tool-call data.`
|
|
@@ -5148,14 +5155,14 @@ class AI {
|
|
|
5148
5155
|
)
|
|
5149
5156
|
);
|
|
5150
5157
|
}
|
|
5151
|
-
const content=
|
|
5158
|
+
const content=is.string(choiceDelta.content)
|
|
5152
5159
|
?choiceDelta.content
|
|
5153
5160
|
:'';
|
|
5154
5161
|
let reasoning='';
|
|
5155
5162
|
if(seeThinking){
|
|
5156
|
-
reasoning=
|
|
5163
|
+
reasoning=is.string(choiceDelta.reasoning_content)
|
|
5157
5164
|
?choiceDelta.reasoning_content
|
|
5158
|
-
:
|
|
5165
|
+
:is.string(choiceDelta.reasoning)
|
|
5159
5166
|
?choiceDelta.reasoning
|
|
5160
5167
|
:'';
|
|
5161
5168
|
}
|
|
@@ -5343,7 +5350,7 @@ class AI {
|
|
|
5343
5350
|
const hasToolCalls=structuralToolCallsByChoice.has(choicePosition);
|
|
5344
5351
|
return {
|
|
5345
5352
|
...retained.choice,
|
|
5346
|
-
index:
|
|
5353
|
+
index:is.safeInteger(retained.choice.index)
|
|
5347
5354
|
&&retained.choice.index>=0
|
|
5348
5355
|
?retained.choice.index
|
|
5349
5356
|
:choicePosition,
|
|
@@ -5561,8 +5568,8 @@ class AI {
|
|
|
5561
5568
|
validateAIStructuralRequest(messages,tools,parallel_tool_calls);
|
|
5562
5569
|
this.#assertServiceConfigured(this.llmService);
|
|
5563
5570
|
if(signal&&(
|
|
5564
|
-
|
|
5565
|
-
|
|
5571
|
+
!is.boolean(signal.aborted)
|
|
5572
|
+
||!is.function(signal.addEventListener)
|
|
5566
5573
|
)){
|
|
5567
5574
|
throw new TypeError('AI request signal must be an AbortSignal.');
|
|
5568
5575
|
}
|
|
@@ -5724,12 +5731,288 @@ class AI {
|
|
|
5724
5731
|
return result;
|
|
5725
5732
|
}
|
|
5726
5733
|
|
|
5734
|
+
prepareTTS({parts, storage = null, identity = null, signal = null, onState = null} = {}) {
|
|
5735
|
+
if(!is.array(parts)) {
|
|
5736
|
+
throw new TypeError('AI.prepareTTS parts must be an array.');
|
|
5737
|
+
}
|
|
5738
|
+
const runtime = this;
|
|
5739
|
+
const selected = this.#providerRuntime.selection('tts');
|
|
5740
|
+
const configuredSpeech = this.browserSpeechConfiguration?.tts;
|
|
5741
|
+
const configuration = configuredSpeech
|
|
5742
|
+
? {
|
|
5743
|
+
model: configuredSpeech.model ?? null,
|
|
5744
|
+
runtime: configuredSpeech.runtime ?? null,
|
|
5745
|
+
graph: configuredSpeech.graph ?? null,
|
|
5746
|
+
device: configuredSpeech.execution?.device ?? 'auto'
|
|
5747
|
+
}
|
|
5748
|
+
: null;
|
|
5749
|
+
const selection = {
|
|
5750
|
+
providerId: selected?.providerId ?? this.ttsService,
|
|
5751
|
+
modelId: selected?.modelId ?? this.modelTTS,
|
|
5752
|
+
responseFormat: selected ? this.#providerSpeechResponseFormat() : this.audioFormat,
|
|
5753
|
+
configuration
|
|
5754
|
+
};
|
|
5755
|
+
const browserGeneration = this.#browserSpeechGeneration;
|
|
5756
|
+
const controlGeneration = this.#speechControlGeneration;
|
|
5757
|
+
const defaultVoice = selected
|
|
5758
|
+
? this.#providerSpeechVoice()
|
|
5759
|
+
: this.#builtInSpeechDefaultVoice('tts', this.ttsService);
|
|
5760
|
+
const responseFormat = selection.responseFormat;
|
|
5761
|
+
const segmentation = this.ttsSegmentation;
|
|
5762
|
+
const segments = [];
|
|
5763
|
+
for(const part of parts) {
|
|
5764
|
+
const source = is.string(part) ? {input: part} : part;
|
|
5765
|
+
if(!is.string(source?.input)) {
|
|
5766
|
+
throw new TypeError('Each prepared speech part requires input text.');
|
|
5767
|
+
}
|
|
5768
|
+
const voice = source.voice ?? defaultVoice;
|
|
5769
|
+
const speed = Number(source.speed ?? this.voiceSpeed);
|
|
5770
|
+
const pauseAfterMs = Number(source.pauseAfterMs ?? 0);
|
|
5771
|
+
if(!is.finite(speed) || speed <= 0) {
|
|
5772
|
+
throw new RangeError('Prepared speech speed must be positive.');
|
|
5773
|
+
}
|
|
5774
|
+
if(!is.finite(pauseAfterMs) || pauseAfterMs < 0) {
|
|
5775
|
+
throw new RangeError('Prepared speech pauses must be nonnegative milliseconds.');
|
|
5776
|
+
}
|
|
5777
|
+
const cleaned = stripSpeechFormatting(source.input);
|
|
5778
|
+
const {segments: outputs} = this.#segmentSpeechText(cleaned, true, segmentation);
|
|
5779
|
+
for(const [index, input] of outputs.entries()) {
|
|
5780
|
+
if(!input.trim()) continue;
|
|
5781
|
+
segments.push(
|
|
5782
|
+
{input, voice, speed, pauseAfterMs: index === outputs.length - 1 ? pauseAfterMs : 0}
|
|
5783
|
+
);
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
this.#traceSpeech('prepareTTS.call', {parts, segments, selection, segmentation, identity, storage});
|
|
5787
|
+
async function synthesizePreparedSpeech(part, requestSignal) {
|
|
5788
|
+
function assertPreparedSpeechSelection() {
|
|
5789
|
+
const current = runtime.#providerRuntime.selection('tts');
|
|
5790
|
+
if(requestSignal?.aborted) throw normalizeAIRequestAbort(requestSignal.reason);
|
|
5791
|
+
if(browserGeneration !== runtime.#browserSpeechGeneration
|
|
5792
|
+
|| controlGeneration !== runtime.#speechControlGeneration
|
|
5793
|
+
|| selection.providerId !== (current?.providerId ?? runtime.ttsService)
|
|
5794
|
+
|| selection.modelId !== (current?.modelId ?? runtime.modelTTS)) {
|
|
5795
|
+
throw normalizeAIRequestAbort('The prepared speech selection changed.');
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
assertPreparedSpeechSelection();
|
|
5799
|
+
if(selected) {
|
|
5800
|
+
const status = runtime.#providerRuntime.status('tts');
|
|
5801
|
+
if(status.state !== 'ready' || status.loaded !== true) {
|
|
5802
|
+
if(!runtime.#preparedSpeechReadiness) {
|
|
5803
|
+
const readiness = runtime.#providerRuntime.setSpeechMuted(false);
|
|
5804
|
+
runtime.#preparedSpeechReadiness = readiness;
|
|
5805
|
+
readiness.finally(
|
|
5806
|
+
function releasePreparedSpeechReadiness() {
|
|
5807
|
+
if(runtime.#preparedSpeechReadiness === readiness) {
|
|
5808
|
+
runtime.#preparedSpeechReadiness = null;
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
).catch(
|
|
5812
|
+
function reportPreparedSpeechReadinessFailure(error) {
|
|
5813
|
+
runtime.#traceSpeech('prepareTTS.readiness.error', {error});
|
|
5814
|
+
}
|
|
5815
|
+
);
|
|
5816
|
+
}
|
|
5817
|
+
await runtime.#preparedSpeechReadiness;
|
|
5818
|
+
assertPreparedSpeechSelection();
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
return runtime.fetchTTS(
|
|
5822
|
+
{model: selection.modelId, input: part.input, voice: part.voice, speed: part.speed, responseFormat},
|
|
5823
|
+
requestSignal,
|
|
5824
|
+
{speechInputPrepared: true}
|
|
5825
|
+
);
|
|
5826
|
+
}
|
|
5827
|
+
return prepareSpeech(
|
|
5828
|
+
{
|
|
5829
|
+
owner: this,
|
|
5830
|
+
parts: segments,
|
|
5831
|
+
originalParts: parts,
|
|
5832
|
+
selection,
|
|
5833
|
+
segmentation,
|
|
5834
|
+
storage,
|
|
5835
|
+
identity,
|
|
5836
|
+
signal,
|
|
5837
|
+
onState,
|
|
5838
|
+
synthesize: synthesizePreparedSpeech
|
|
5839
|
+
}
|
|
5840
|
+
);
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
playPreparedTTS(prepared, {signal = null, onState = null} = {}) {
|
|
5844
|
+
if(!is.array(prepared?.segments) || !is.function(prepared?.getAudio)) {
|
|
5845
|
+
throw new TypeError('AI.playPreparedTTS requires a speech preparation handle.');
|
|
5846
|
+
}
|
|
5847
|
+
const runtime = this;
|
|
5848
|
+
if(onState !== null && !is.function(onState)) {
|
|
5849
|
+
throw new TypeError('Prepared playback onState must be a function when provided.');
|
|
5850
|
+
}
|
|
5851
|
+
// One playback lane per AI; detached preparation has its own lifetime.
|
|
5852
|
+
this.stopAudio();
|
|
5853
|
+
const group = {jobs: [], context: null, paused: false, stopped: false, state: 'waiting', error: null};
|
|
5854
|
+
const completions = [];
|
|
5855
|
+
const generation = this.speechGeneration;
|
|
5856
|
+
this.#traceSpeech('playPreparedTTS.call', {segments: prepared.segments});
|
|
5857
|
+
function publishPreparedPlaybackState(state, error = group.error) {
|
|
5858
|
+
group.state = state;
|
|
5859
|
+
group.error = error;
|
|
5860
|
+
try {
|
|
5861
|
+
onState?.({state, error});
|
|
5862
|
+
} catch(observerError) {
|
|
5863
|
+
arcaneLogging.error('Prepared playback state observer failed.', observerError);
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
5866
|
+
group.publish = publishPreparedPlaybackState;
|
|
5867
|
+
group.fail = function reportPreparedPlaybackFailure(error) {
|
|
5868
|
+
publishPreparedPlaybackState('error', error);
|
|
5869
|
+
};
|
|
5870
|
+
function stopPreparedPlayback() {
|
|
5871
|
+
if(group.stopped) return false;
|
|
5872
|
+
group.stopped = true;
|
|
5873
|
+
runtime.speechResumeAttempt += 1;
|
|
5874
|
+
runtime.speechResumePending = false;
|
|
5875
|
+
publishPreparedPlaybackState('stopped');
|
|
5876
|
+
for(const job of group.jobs) {
|
|
5877
|
+
if(job.state === 'complete') continue;
|
|
5878
|
+
if(job.sourceNode) {
|
|
5879
|
+
job.sourceNode.onended = null;
|
|
5880
|
+
if(job.sourceNode.__arcaneStarted) {
|
|
5881
|
+
try {
|
|
5882
|
+
job.sourceNode.stop();
|
|
5883
|
+
} catch(error) {
|
|
5884
|
+
arcaneLogging.warn('Prepared speech could not be stopped.', error);
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5888
|
+
runtime.#cancelSpeechJob(job);
|
|
5889
|
+
}
|
|
5890
|
+
if(runtime.speechScheduleContext === group.context) {
|
|
5891
|
+
runtime.speechScheduleContext = null;
|
|
5892
|
+
runtime.speechScheduleTime = 0;
|
|
5893
|
+
}
|
|
5894
|
+
runtime.#requestSpeechPlayback();
|
|
5895
|
+
return true;
|
|
5896
|
+
}
|
|
5897
|
+
const control = {
|
|
5898
|
+
finished: null,
|
|
5899
|
+
get state() { return group.state; },
|
|
5900
|
+
get error() { return group.error; },
|
|
5901
|
+
async pause() {
|
|
5902
|
+
if(group.stopped || !group.context || group.context.state === 'closed') return false;
|
|
5903
|
+
group.paused = true;
|
|
5904
|
+
if(runtime.#speechUnlockContext === group.context) runtime.#clearSpeechUnlock();
|
|
5905
|
+
await group.context.suspend();
|
|
5906
|
+
if(!group.stopped) publishPreparedPlaybackState('paused');
|
|
5907
|
+
return true;
|
|
5908
|
+
},
|
|
5909
|
+
async resume() {
|
|
5910
|
+
if(group.stopped || !group.context || group.context.state === 'closed') return false;
|
|
5911
|
+
group.paused = false;
|
|
5912
|
+
const resumed = await runtime.resumeAudio(group.context, true);
|
|
5913
|
+
if(!group.stopped && !group.paused && !group.error) {
|
|
5914
|
+
publishPreparedPlaybackState(resumed
|
|
5915
|
+
? group.jobs.some(function hasScheduledPreparedAudio(job) { return job.state === 'scheduled'; })
|
|
5916
|
+
? 'scheduled' : 'waiting'
|
|
5917
|
+
: 'waiting-for-gesture');
|
|
5918
|
+
}
|
|
5919
|
+
return resumed;
|
|
5920
|
+
},
|
|
5921
|
+
stop: stopPreparedPlayback
|
|
5922
|
+
};
|
|
5923
|
+
this.#preparedSpeechPlaybacks.add(control);
|
|
5924
|
+
for(const [index, segment] of prepared.segments.entries()) {
|
|
5925
|
+
const job = {
|
|
5926
|
+
diagnosticId: ++this.#speechJobSequence,
|
|
5927
|
+
abortController: null,
|
|
5928
|
+
audioBuffer: null,
|
|
5929
|
+
audioContext: null,
|
|
5930
|
+
generation,
|
|
5931
|
+
scheduledEnd: null,
|
|
5932
|
+
scheduledStart: null,
|
|
5933
|
+
sourceNode: null,
|
|
5934
|
+
state: 'preparing',
|
|
5935
|
+
text: segment.input,
|
|
5936
|
+
pauseAfterMs: segment.pauseAfterMs ?? 0,
|
|
5937
|
+
preparedPlayback: group,
|
|
5938
|
+
resolvePlayback: null
|
|
5939
|
+
};
|
|
5940
|
+
completions.push(
|
|
5941
|
+
new Promise(
|
|
5942
|
+
function capturePreparedPlaybackCompletion(resolve) {
|
|
5943
|
+
job.resolvePlayback = resolve;
|
|
5944
|
+
}
|
|
5945
|
+
)
|
|
5946
|
+
);
|
|
5947
|
+
group.jobs.push(job);
|
|
5948
|
+
this.speechJobs.push(job);
|
|
5949
|
+
Promise.resolve().then(
|
|
5950
|
+
async function decodePreparedSpeechSegment() {
|
|
5951
|
+
if(group.stopped) return false;
|
|
5952
|
+
const audio = await prepared.getAudio(index);
|
|
5953
|
+
if(group.stopped || generation !== runtime.speechGeneration) return false;
|
|
5954
|
+
return runtime.playAudio([audio], group.context, null, audio.type, job);
|
|
5955
|
+
}
|
|
5956
|
+
).catch(
|
|
5957
|
+
function failPreparedSpeechSegment(error) {
|
|
5958
|
+
if(group.stopped) return false;
|
|
5959
|
+
return runtime.#failSpeechJob(job, error, 'prepared-audio');
|
|
5960
|
+
}
|
|
5961
|
+
);
|
|
5962
|
+
}
|
|
5963
|
+
control.finished = Promise.all(completions).then(
|
|
5964
|
+
async function finishPreparedPlayback(results) {
|
|
5965
|
+
const completed = !group.stopped && results.every(Boolean);
|
|
5966
|
+
group.stopped = true;
|
|
5967
|
+
publishPreparedPlaybackState(completed ? 'complete' : group.error ? 'error' : 'stopped');
|
|
5968
|
+
signal?.removeEventListener('abort', stopPreparedPlayback);
|
|
5969
|
+
runtime.#preparedSpeechPlaybacks.delete(control);
|
|
5970
|
+
if(runtime.#speechUnlockContext === group.context) runtime.#clearSpeechUnlock();
|
|
5971
|
+
if(runtime.speechScheduleContext === group.context) {
|
|
5972
|
+
runtime.speechScheduleContext = null;
|
|
5973
|
+
runtime.speechScheduleTime = 0;
|
|
5974
|
+
}
|
|
5975
|
+
if(group.context && group.context.state !== 'closed') {
|
|
5976
|
+
try {
|
|
5977
|
+
await group.context.close();
|
|
5978
|
+
} catch(error) {
|
|
5979
|
+
arcaneLogging.warn('Prepared speech audio context could not be closed.', error);
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5982
|
+
runtime.#traceSpeech('playPreparedTTS.result', {completed});
|
|
5983
|
+
return completed;
|
|
5984
|
+
}
|
|
5985
|
+
);
|
|
5986
|
+
signal?.addEventListener('abort', stopPreparedPlayback, {once: true});
|
|
5987
|
+
publishPreparedPlaybackState('waiting');
|
|
5988
|
+
if(signal?.aborted) {
|
|
5989
|
+
stopPreparedPlayback();
|
|
5990
|
+
} else if(group.jobs.length) {
|
|
5991
|
+
// Playing saved audio does not require activating a synthesis provider.
|
|
5992
|
+
this.muted = false;
|
|
5993
|
+
try {
|
|
5994
|
+
const AudioContext = globalThis.AudioContext || globalThis.webkitAudioContext;
|
|
5995
|
+
if(!is.function(AudioContext)) throw new TypeError('Audio playback is unavailable in this browser.');
|
|
5996
|
+
group.context = new AudioContext();
|
|
5997
|
+
this.resumeAudio(group.context, true).catch(
|
|
5998
|
+
function failPreparedSpeechUnlock(error) {
|
|
5999
|
+
for(const job of group.jobs) runtime.#failSpeechJob(job, error, 'playback-resume');
|
|
6000
|
+
}
|
|
6001
|
+
);
|
|
6002
|
+
} catch(error) {
|
|
6003
|
+
for(const job of group.jobs) this.#failSpeechJob(job, error, 'playback-start');
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
return control;
|
|
6007
|
+
}
|
|
6008
|
+
|
|
5727
6009
|
streamTTS(
|
|
5728
6010
|
text='',
|
|
5729
6011
|
end=false,
|
|
5730
6012
|
options={}
|
|
5731
6013
|
){
|
|
5732
6014
|
const callId=this.#traceSpeech('streamTTS.call',{text,end,options});
|
|
6015
|
+
for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
|
|
5733
6016
|
if(this.muted){
|
|
5734
6017
|
if(end){
|
|
5735
6018
|
this.audioMessageChunks='';
|
|
@@ -5741,7 +6024,7 @@ class AI {
|
|
|
5741
6024
|
|
|
5742
6025
|
const {voice,speed,waitForPlayback=false}=options;
|
|
5743
6026
|
const pauseAfterMs=Number(options.pauseAfterMs??0);
|
|
5744
|
-
if(!
|
|
6027
|
+
if(!is.finite(pauseAfterMs)||pauseAfterMs<0){
|
|
5745
6028
|
throw new RangeError('Speech pauses must be a nonnegative number of milliseconds.');
|
|
5746
6029
|
}
|
|
5747
6030
|
|
|
@@ -5800,15 +6083,22 @@ class AI {
|
|
|
5800
6083
|
}
|
|
5801
6084
|
|
|
5802
6085
|
#extractSpeechSegments(end=false){
|
|
6086
|
+
const {segments, remainder} = this.#segmentSpeechText(this.audioMessageChunks, end, this.#ttsSegmentation);
|
|
6087
|
+
this.audioMessageChunks = remainder;
|
|
6088
|
+
return segments;
|
|
6089
|
+
}
|
|
6090
|
+
|
|
6091
|
+
#segmentSpeechText(text, end, segmentation) {
|
|
5803
6092
|
const segments=[];
|
|
5804
|
-
let remainder=
|
|
6093
|
+
let remainder=text;
|
|
5805
6094
|
|
|
5806
6095
|
while(remainder.length>0){
|
|
5807
6096
|
const punctuationBoundary=this.#findSpeechPunctuationBoundary(
|
|
5808
6097
|
remainder,
|
|
5809
|
-
end
|
|
6098
|
+
end,
|
|
6099
|
+
segmentation
|
|
5810
6100
|
);
|
|
5811
|
-
const cadenceBoundary=this.#findSpeechCadenceBoundary(remainder,end);
|
|
6101
|
+
const cadenceBoundary=this.#findSpeechCadenceBoundary(remainder,end,segmentation);
|
|
5812
6102
|
let boundary=punctuationBoundary;
|
|
5813
6103
|
|
|
5814
6104
|
if(boundary<0||(cadenceBoundary>0&&cadenceBoundary<boundary)){
|
|
@@ -5831,15 +6121,14 @@ class AI {
|
|
|
5831
6121
|
}
|
|
5832
6122
|
}
|
|
5833
6123
|
|
|
5834
|
-
|
|
5835
|
-
return segments;
|
|
6124
|
+
return {segments, remainder};
|
|
5836
6125
|
}
|
|
5837
6126
|
|
|
5838
|
-
#findSpeechPunctuationBoundary(text,end=false){
|
|
5839
|
-
if(
|
|
6127
|
+
#findSpeechPunctuationBoundary(text,end=false,segmentation=this.#ttsSegmentation){
|
|
6128
|
+
if(segmentation.punctuation==='none'){
|
|
5840
6129
|
return -1;
|
|
5841
6130
|
}
|
|
5842
|
-
if(
|
|
6131
|
+
if(segmentation.punctuation==='any'){
|
|
5843
6132
|
return this.#findAnySpeechPunctuationBoundary(text,end);
|
|
5844
6133
|
}
|
|
5845
6134
|
|
|
@@ -5884,8 +6173,8 @@ class AI {
|
|
|
5884
6173
|
return -1;
|
|
5885
6174
|
}
|
|
5886
6175
|
|
|
5887
|
-
#findSpeechCadenceBoundary(text,end=false){
|
|
5888
|
-
const wordCadence=
|
|
6176
|
+
#findSpeechCadenceBoundary(text,end=false,segmentation=this.#ttsSegmentation){
|
|
6177
|
+
const wordCadence=segmentation.wordCadence;
|
|
5889
6178
|
if(wordCadence===null){
|
|
5890
6179
|
return -1;
|
|
5891
6180
|
}
|
|
@@ -6041,7 +6330,7 @@ class AI {
|
|
|
6041
6330
|
entry=>entry.providerId===selection.providerId
|
|
6042
6331
|
);
|
|
6043
6332
|
const model=provider?.models.find(entry=>entry?.id===selection.modelId);
|
|
6044
|
-
return
|
|
6333
|
+
return is.string(model?.defaultVoice)&&model.defaultVoice.trim()
|
|
6045
6334
|
?model.defaultVoice.trim()
|
|
6046
6335
|
:null;
|
|
6047
6336
|
}
|
|
@@ -6059,7 +6348,7 @@ class AI {
|
|
|
6059
6348
|
if(speech===undefined){
|
|
6060
6349
|
return this.audioFormat;
|
|
6061
6350
|
}
|
|
6062
|
-
const prototype=speech&&
|
|
6351
|
+
const prototype=speech&&is.object(speech)
|
|
6063
6352
|
?Object.getPrototypeOf(speech)
|
|
6064
6353
|
:null;
|
|
6065
6354
|
const descriptors=prototype===Object.prototype||prototype===null
|
|
@@ -6067,10 +6356,10 @@ class AI {
|
|
|
6067
6356
|
:null;
|
|
6068
6357
|
const formats=descriptors?.responseFormats?.value;
|
|
6069
6358
|
const defaultFormat=descriptors?.defaultResponseFormat?.value;
|
|
6070
|
-
if(!
|
|
6359
|
+
if(!is.array(formats)
|
|
6071
6360
|
||formats.length<1
|
|
6072
|
-
||!formats.every(format=>
|
|
6073
|
-
|
|
6361
|
+
||!formats.every(format=>is.string(format)&&format.trim()===format&&format)
|
|
6362
|
+
||!is.string(defaultFormat)
|
|
6074
6363
|
||!formats.includes(defaultFormat)){
|
|
6075
6364
|
throw aiProviderError(
|
|
6076
6365
|
'The selected TTS provider returned an invalid speech format catalog.',
|
|
@@ -6097,7 +6386,7 @@ class AI {
|
|
|
6097
6386
|
};
|
|
6098
6387
|
}
|
|
6099
6388
|
|
|
6100
|
-
if(response instanceof ArrayBuffer||
|
|
6389
|
+
if(response instanceof ArrayBuffer||is.arrayBufferView(response)){
|
|
6101
6390
|
const bytes=response instanceof ArrayBuffer
|
|
6102
6391
|
?new Uint8Array(response)
|
|
6103
6392
|
:new Uint8Array(
|
|
@@ -6111,11 +6400,11 @@ class AI {
|
|
|
6111
6400
|
};
|
|
6112
6401
|
}
|
|
6113
6402
|
|
|
6114
|
-
if(response&&
|
|
6115
|
-
if(
|
|
6403
|
+
if(response&&is.object(response)){
|
|
6404
|
+
if(is.string(response.audioBase64)){
|
|
6116
6405
|
return {
|
|
6117
6406
|
chunks:[this.#base64ToBytes(response.audioBase64)],
|
|
6118
|
-
type:
|
|
6407
|
+
type:is.string(response.contentType)
|
|
6119
6408
|
?response.contentType
|
|
6120
6409
|
:this.audioType
|
|
6121
6410
|
};
|
|
@@ -6129,7 +6418,7 @@ class AI {
|
|
|
6129
6418
|
};
|
|
6130
6419
|
}
|
|
6131
6420
|
if(response.audio instanceof ArrayBuffer
|
|
6132
|
-
||
|
|
6421
|
+
||is.arrayBufferView(response.audio)){
|
|
6133
6422
|
const bytes=response.audio instanceof ArrayBuffer
|
|
6134
6423
|
?new Uint8Array(response.audio)
|
|
6135
6424
|
:new Uint8Array(
|
|
@@ -6139,7 +6428,7 @@ class AI {
|
|
|
6139
6428
|
);
|
|
6140
6429
|
return {
|
|
6141
6430
|
chunks:[bytes],
|
|
6142
|
-
type:
|
|
6431
|
+
type:is.string(response.contentType)
|
|
6143
6432
|
?response.contentType
|
|
6144
6433
|
:this.audioType
|
|
6145
6434
|
};
|
|
@@ -6168,7 +6457,7 @@ class AI {
|
|
|
6168
6457
|
|
|
6169
6458
|
const AudioContext=window.AudioContext||window.webkitAudioContext;
|
|
6170
6459
|
|
|
6171
|
-
if(
|
|
6460
|
+
if(!is.function(AudioContext)){
|
|
6172
6461
|
throw new TypeError('Audio playback is unavailable in this browser.');
|
|
6173
6462
|
}
|
|
6174
6463
|
|
|
@@ -6181,8 +6470,8 @@ class AI {
|
|
|
6181
6470
|
try{
|
|
6182
6471
|
this.#assertServiceConfigured(this.ttsService,'tts');
|
|
6183
6472
|
if(!payload
|
|
6184
|
-
|
|
6185
|
-
||
|
|
6473
|
+
||!is.object(payload)
|
|
6474
|
+
||is.array(payload)
|
|
6186
6475
|
||![Object.prototype,null].includes(Object.getPrototypeOf(payload))){
|
|
6187
6476
|
const error=new TypeError('AI.fetchTTS requires a speech request object.');
|
|
6188
6477
|
error.code='ARCANE_AI_TTS_REQUEST_INVALID';
|
|
@@ -6197,7 +6486,7 @@ class AI {
|
|
|
6197
6486
|
'speed'
|
|
6198
6487
|
]);
|
|
6199
6488
|
for(const key of Reflect.ownKeys(descriptors)){
|
|
6200
|
-
if(
|
|
6489
|
+
if(is.symbol(key)
|
|
6201
6490
|
||!acceptedKeys.has(key)
|
|
6202
6491
|
||!Object.hasOwn(descriptors[key],'value')){
|
|
6203
6492
|
const error=new TypeError(
|
|
@@ -6208,9 +6497,9 @@ class AI {
|
|
|
6208
6497
|
}
|
|
6209
6498
|
}
|
|
6210
6499
|
if(signal&&(
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6500
|
+
!is.boolean(signal.aborted)
|
|
6501
|
+
||!is.function(signal.addEventListener)
|
|
6502
|
+
||!is.function(signal.removeEventListener)
|
|
6214
6503
|
)){
|
|
6215
6504
|
const error=new TypeError('AI.fetchTTS signal must be an AbortSignal.');
|
|
6216
6505
|
error.code='ARCANE_AI_TTS_SIGNAL_INVALID';
|
|
@@ -6221,7 +6510,7 @@ class AI {
|
|
|
6221
6510
|
}
|
|
6222
6511
|
|
|
6223
6512
|
const suppliedInput=descriptors.input?.value;
|
|
6224
|
-
if(
|
|
6513
|
+
if(!is.string(suppliedInput)||!suppliedInput.trim()){
|
|
6225
6514
|
const error=new TypeError('AI.fetchTTS input must be nonempty text.');
|
|
6226
6515
|
error.code='ARCANE_AI_TTS_INPUT_INVALID';
|
|
6227
6516
|
throw error;
|
|
@@ -6232,7 +6521,7 @@ class AI {
|
|
|
6232
6521
|
const selection=this.#providerRuntime.selection('tts');
|
|
6233
6522
|
const requestedModel=descriptors.model?.value;
|
|
6234
6523
|
if(requestedModel!==undefined
|
|
6235
|
-
&&(
|
|
6524
|
+
&&(!is.string(requestedModel)
|
|
6236
6525
|
||requestedModel.trim()!==requestedModel
|
|
6237
6526
|
||!requestedModel)){
|
|
6238
6527
|
const error=new TypeError(
|
|
@@ -6259,7 +6548,7 @@ class AI {
|
|
|
6259
6548
|
}
|
|
6260
6549
|
const requestedVoice=descriptors.voice?.value;
|
|
6261
6550
|
if(requestedVoice!==undefined
|
|
6262
|
-
&&(
|
|
6551
|
+
&&(!is.string(requestedVoice)
|
|
6263
6552
|
||requestedVoice.trim()!==requestedVoice
|
|
6264
6553
|
||!requestedVoice)){
|
|
6265
6554
|
const error=new TypeError(
|
|
@@ -6282,7 +6571,7 @@ class AI {
|
|
|
6282
6571
|
}
|
|
6283
6572
|
const requestedResponseFormat=descriptors.responseFormat?.value;
|
|
6284
6573
|
if(requestedResponseFormat!==undefined
|
|
6285
|
-
&&(
|
|
6574
|
+
&&(!is.string(requestedResponseFormat)
|
|
6286
6575
|
||requestedResponseFormat.trim()!==requestedResponseFormat
|
|
6287
6576
|
||!requestedResponseFormat)){
|
|
6288
6577
|
const error=new TypeError(
|
|
@@ -6302,7 +6591,7 @@ class AI {
|
|
|
6302
6591
|
const speed=descriptors.speed
|
|
6303
6592
|
?Number(descriptors.speed.value)
|
|
6304
6593
|
:this.voiceSpeed;
|
|
6305
|
-
if(!
|
|
6594
|
+
if(!is.finite(speed)||speed<=0){
|
|
6306
6595
|
const error=new RangeError('AI.fetchTTS speed must be a positive number.');
|
|
6307
6596
|
error.code='ARCANE_AI_TTS_SPEED_INVALID';
|
|
6308
6597
|
throw error;
|
|
@@ -6350,9 +6639,9 @@ class AI {
|
|
|
6350
6639
|
try{
|
|
6351
6640
|
this.#assertServiceConfigured(this.sttService,'stt');
|
|
6352
6641
|
if(signal&&(
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6642
|
+
!is.boolean(signal.aborted)
|
|
6643
|
+
||!is.function(signal.addEventListener)
|
|
6644
|
+
||!is.function(signal.removeEventListener)
|
|
6356
6645
|
)){
|
|
6357
6646
|
const error=new TypeError('AI.fetchSTT signal must be an AbortSignal.');
|
|
6358
6647
|
error.code='ARCANE_AI_STT_SIGNAL_INVALID';
|
|
@@ -6369,7 +6658,7 @@ class AI {
|
|
|
6369
6658
|
operation:'transcribe',
|
|
6370
6659
|
payload:{
|
|
6371
6660
|
audio:audioFile,
|
|
6372
|
-
mimeType:
|
|
6661
|
+
mimeType:is.function(globalThis.Blob)
|
|
6373
6662
|
&&audioFile instanceof Blob
|
|
6374
6663
|
?String(audioFile.type||'audio/webm')
|
|
6375
6664
|
:'audio/webm',
|
|
@@ -6380,10 +6669,10 @@ class AI {
|
|
|
6380
6669
|
}
|
|
6381
6670
|
);
|
|
6382
6671
|
this.#traceSpeech('fetchSTT.providerResult',{callId,response});
|
|
6383
|
-
const text=
|
|
6672
|
+
const text=is.string(response)
|
|
6384
6673
|
?response
|
|
6385
6674
|
:response?.text;
|
|
6386
|
-
if(
|
|
6675
|
+
if(!is.string(text)){
|
|
6387
6676
|
const error=new TypeError(
|
|
6388
6677
|
'Arcane returned an invalid provider speech transcription.'
|
|
6389
6678
|
);
|
|
@@ -6416,6 +6705,7 @@ class AI {
|
|
|
6416
6705
|
const callId=this.#traceSpeech('stopAudio.call',{
|
|
6417
6706
|
remainder:this.audioMessageChunks,queuedJobs:this.speechJobs.length
|
|
6418
6707
|
});
|
|
6708
|
+
for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
|
|
6419
6709
|
this.speechGeneration+=1;
|
|
6420
6710
|
this.speechResumeAttempt+=1;
|
|
6421
6711
|
this.speechResumePending=false;
|
|
@@ -6475,9 +6765,14 @@ class AI {
|
|
|
6475
6765
|
|
|
6476
6766
|
let attempt=0;
|
|
6477
6767
|
let context;
|
|
6768
|
+
let preparedGroup = null;
|
|
6478
6769
|
|
|
6479
6770
|
try{
|
|
6480
6771
|
context=audioContext||this.#getSpeechAudioContext();
|
|
6772
|
+
preparedGroup = this.speechJobs.find(function findPreparedResumeOwner(job) {
|
|
6773
|
+
return job.preparedPlayback?.context === context;
|
|
6774
|
+
})?.preparedPlayback ?? null;
|
|
6775
|
+
if(preparedGroup?.paused || preparedGroup?.stopped) return false;
|
|
6481
6776
|
this.#traceSpeech('resumeAudio.context',{
|
|
6482
6777
|
callId,state:context.state,audioTime:context.currentTime,
|
|
6483
6778
|
sampleRate:context.sampleRate
|
|
@@ -6490,7 +6785,7 @@ class AI {
|
|
|
6490
6785
|
return true;
|
|
6491
6786
|
}
|
|
6492
6787
|
|
|
6493
|
-
if(
|
|
6788
|
+
if(!is.function(context.resume)){
|
|
6494
6789
|
this.#waitForSpeechGesture(null,context);
|
|
6495
6790
|
this.#traceSpeech('resumeAudio.result',{
|
|
6496
6791
|
callId,result:false,reason:'resume-unavailable'
|
|
@@ -6501,6 +6796,12 @@ class AI {
|
|
|
6501
6796
|
attempt=++this.speechResumeAttempt;
|
|
6502
6797
|
this.speechResumePending=true;
|
|
6503
6798
|
await context.resume();
|
|
6799
|
+
if(preparedGroup?.stopped) return false;
|
|
6800
|
+
if(preparedGroup?.paused) {
|
|
6801
|
+
await context.suspend();
|
|
6802
|
+
this.speechResumePending = false;
|
|
6803
|
+
return false;
|
|
6804
|
+
}
|
|
6504
6805
|
this.#traceSpeech('resumeAudio.resumed',{
|
|
6505
6806
|
callId,state:context.state,audioTime:context.currentTime
|
|
6506
6807
|
});
|
|
@@ -6522,6 +6823,7 @@ class AI {
|
|
|
6522
6823
|
}
|
|
6523
6824
|
}catch(error){
|
|
6524
6825
|
this.#traceSpeech('resumeAudio.error',{callId,error,state:context?.state});
|
|
6826
|
+
if(preparedGroup?.stopped || preparedGroup?.paused) return false;
|
|
6525
6827
|
if(attempt&&attempt!==this.speechResumeAttempt){
|
|
6526
6828
|
return context?.state==='running';
|
|
6527
6829
|
}
|
|
@@ -6529,6 +6831,10 @@ class AI {
|
|
|
6529
6831
|
if(attempt===this.speechResumeAttempt){
|
|
6530
6832
|
this.speechResumePending=false;
|
|
6531
6833
|
}
|
|
6834
|
+
if(preparedGroup && error?.name !== 'NotAllowedError') {
|
|
6835
|
+
for(const job of preparedGroup.jobs) this.#failSpeechJob(job, error, 'playback-resume');
|
|
6836
|
+
return false;
|
|
6837
|
+
}
|
|
6532
6838
|
if(context?.state==='closed'){
|
|
6533
6839
|
this.#clearSpeechUnlock();
|
|
6534
6840
|
const jobs=this.speechJobs.filter(function usesClosedSpeechContext(job){
|
|
@@ -6589,7 +6895,8 @@ class AI {
|
|
|
6589
6895
|
this.speechJobs.push(job);
|
|
6590
6896
|
}
|
|
6591
6897
|
|
|
6592
|
-
if(this.muted||job.generation!==this.speechGeneration
|
|
6898
|
+
if(this.muted||job.generation!==this.speechGeneration
|
|
6899
|
+
||job.preparedPlayback?.stopped||['cancelled','failed'].includes(job.state)){
|
|
6593
6900
|
return this.#cancelSpeechJob(job);
|
|
6594
6901
|
}
|
|
6595
6902
|
|
|
@@ -6606,7 +6913,8 @@ class AI {
|
|
|
6606
6913
|
channels:audioBuffer.numberOfChannels,audioBuffer
|
|
6607
6914
|
});
|
|
6608
6915
|
|
|
6609
|
-
if(this.muted||job.generation!==this.speechGeneration
|
|
6916
|
+
if(this.muted||job.generation!==this.speechGeneration
|
|
6917
|
+
||job.preparedPlayback?.stopped||['cancelled','failed'].includes(job.state)){
|
|
6610
6918
|
return this.#cancelSpeechJob(job);
|
|
6611
6919
|
}
|
|
6612
6920
|
|
|
@@ -6684,6 +6992,8 @@ class AI {
|
|
|
6684
6992
|
|
|
6685
6993
|
const audioContext=job.sourceNode.context||job.audioContext;
|
|
6686
6994
|
|
|
6995
|
+
if(job.preparedPlayback?.paused) break;
|
|
6996
|
+
|
|
6687
6997
|
if(audioContext.state!=='running'){
|
|
6688
6998
|
this.#waitForSpeechGesture(null,audioContext);
|
|
6689
6999
|
|
|
@@ -6712,7 +7022,7 @@ class AI {
|
|
|
6712
7022
|
|
|
6713
7023
|
try{
|
|
6714
7024
|
const duration=Number(job.audioBuffer?.duration);
|
|
6715
|
-
const hasKnownDuration=
|
|
7025
|
+
const hasKnownDuration=is.finite(duration)&&duration>0;
|
|
6716
7026
|
const currentTime=Number(audioContext.currentTime)||0;
|
|
6717
7027
|
const previousContext=this.speechScheduleContext;
|
|
6718
7028
|
const remainingDelay=previousContext
|
|
@@ -6739,6 +7049,9 @@ class AI {
|
|
|
6739
7049
|
}
|
|
6740
7050
|
this.isSpeaking=true;
|
|
6741
7051
|
job.sourceNode.start(scheduledStart);
|
|
7052
|
+
if(job.preparedPlayback && !job.preparedPlayback.error) {
|
|
7053
|
+
job.preparedPlayback.publish('scheduled');
|
|
7054
|
+
}
|
|
6742
7055
|
this.#traceSpeechJob('playback.scheduled',job,{
|
|
6743
7056
|
audioEnd:hasKnownDuration?scheduledStart+duration:null,
|
|
6744
7057
|
previousScheduledEnd:this.speechScheduleTime,
|
|
@@ -6773,6 +7086,9 @@ class AI {
|
|
|
6773
7086
|
!this.muted
|
|
6774
7087
|
&&!this.speechAwaitingGesture
|
|
6775
7088
|
&&!this.speechResumePending
|
|
7089
|
+
&&!this.speechJobs.some(function hasPausedPreparedPlayback(job) {
|
|
7090
|
+
return job.preparedPlayback?.paused;
|
|
7091
|
+
})
|
|
6776
7092
|
&&!this.speechJobs.some(
|
|
6777
7093
|
function hasScheduledSpeechWithoutDuration(job){
|
|
6778
7094
|
return job.state==='scheduled'
|
|
@@ -6877,7 +7193,7 @@ class AI {
|
|
|
6877
7193
|
operationId,
|
|
6878
7194
|
publicDetail:completeValue({
|
|
6879
7195
|
boundary:normalizedBoundary,
|
|
6880
|
-
...(
|
|
7196
|
+
...(is.string(error?.code)?{code:error.code}:{}),
|
|
6881
7197
|
generation,
|
|
6882
7198
|
reason
|
|
6883
7199
|
})
|
|
@@ -6900,6 +7216,7 @@ class AI {
|
|
|
6900
7216
|
}
|
|
6901
7217
|
|
|
6902
7218
|
job.state='failed';
|
|
7219
|
+
job.preparedPlayback?.fail(error);
|
|
6903
7220
|
this.#traceSpeechJob('queue.failed',job,{boundary,error});
|
|
6904
7221
|
|
|
6905
7222
|
this.#publishTTSFailure(error,{
|
|
@@ -6960,7 +7277,15 @@ class AI {
|
|
|
6960
7277
|
const target=window;
|
|
6961
7278
|
|
|
6962
7279
|
this.speechAwaitingGesture=true;
|
|
7280
|
+
this.#speechUnlockContext=audioContext;
|
|
7281
|
+
const preparedJob = this.speechJobs.find(function preparedPlaybackForGesture(job) {
|
|
7282
|
+
return job.preparedPlayback?.context === audioContext;
|
|
7283
|
+
});
|
|
7284
|
+
if(preparedJob && !preparedJob.preparedPlayback.paused && !preparedJob.preparedPlayback.error) {
|
|
7285
|
+
preparedJob.preparedPlayback.publish('waiting-for-gesture');
|
|
7286
|
+
}
|
|
6963
7287
|
this.speechUnlockHandler=function unlockSpeechFromUserGesture(){
|
|
7288
|
+
if(preparedJob?.preparedPlayback.paused || preparedJob?.preparedPlayback.stopped) return false;
|
|
6964
7289
|
runtime.#clearSpeechUnlock();
|
|
6965
7290
|
return runtime.resumeAudio(audioContext);
|
|
6966
7291
|
};
|
|
@@ -6998,6 +7323,7 @@ class AI {
|
|
|
6998
7323
|
}
|
|
6999
7324
|
|
|
7000
7325
|
this.speechUnlockHandler=null;
|
|
7326
|
+
this.#speechUnlockContext=null;
|
|
7001
7327
|
this.speechAwaitingGesture=false;
|
|
7002
7328
|
}
|
|
7003
7329
|
}
|
|
@@ -7019,7 +7345,7 @@ function installAIUserReadyRegistration(){
|
|
|
7019
7345
|
?existingDescriptor.value
|
|
7020
7346
|
:null;
|
|
7021
7347
|
if(existing?.protocol!==AI_USER_READY_REGISTRATION_PROTOCOL
|
|
7022
|
-
|
|
7348
|
+
||!is.function(existing.dispose)){
|
|
7023
7349
|
throw aiInitializationError(
|
|
7024
7350
|
AI_INITIALIZATION_ERROR_CODES.userReadyRegistrationCollision,
|
|
7025
7351
|
AI_INITIALIZATION_REASONS.userReadyRegistrationCollision,
|