arcane-os 0.5.16 → 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 +44 -0
- package/README.md +42 -19
- 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 +53 -46
- 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 +35 -0
- package/docs/architecture.md +1 -1
- package/docs/reference/README.md +8 -8
- package/docs/reference/ai/browser-speech.md +230 -16
- package/docs/reference/ai/twin-cloud.md +1 -1
- package/docs/reference/availability-and-normalization.md +42 -1
- package/docs/reference/cli.md +1 -1
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/inventory/package-api.json +60 -4
- package/docs/reference/inventory/runtime-components.json +2 -2
- package/docs/reference/inventory/runtime-modules.json +29 -31
- package/docs/reference/protocols.md +17 -13
- package/docs/reference/runtime-components.md +6 -5
- package/docs/reference/runtime-modules.md +212 -70
- package/docs/reference/sdk-api.md +197 -11
- 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 +4 -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 +118 -141
- 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 +503 -187
- package/runtime/arcane/modules/AIPreferenceRuntime.js +5 -2
- package/runtime/arcane/modules/AIPreferenceTuple.js +9 -6
- package/runtime/arcane/modules/AIProviderRuntime.js +117 -92
- 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/MarkdownSpeech.js +1 -50
- 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 +61 -28
- 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 +36 -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 {
|
|
@@ -13,7 +16,8 @@ import {
|
|
|
13
16
|
} from './AIProviderRuntime.js';
|
|
14
17
|
import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
|
|
15
18
|
import {arcaneLogging} from 'arcane-os/logging';
|
|
16
|
-
import {MarkdownSpeech} from '
|
|
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
|
);
|
|
@@ -2030,7 +2037,6 @@ class AI {
|
|
|
2030
2037
|
}
|
|
2031
2038
|
|
|
2032
2039
|
audioMessageChunks='';
|
|
2033
|
-
#speechTextFormat='plain';
|
|
2034
2040
|
#markdownSpeech=new MarkdownSpeech();
|
|
2035
2041
|
sourceNodes=[];
|
|
2036
2042
|
isSpeaking=false;
|
|
@@ -2053,7 +2059,7 @@ class AI {
|
|
|
2053
2059
|
if(value===undefined||value===null||value===''){
|
|
2054
2060
|
return current[index];
|
|
2055
2061
|
}
|
|
2056
|
-
if(
|
|
2062
|
+
if(!is.string(value)||value.trim()!==value||!value){
|
|
2057
2063
|
throw new TypeError('AI preferences must be nonempty trimmed strings.');
|
|
2058
2064
|
}
|
|
2059
2065
|
return value;
|
|
@@ -2488,7 +2494,7 @@ class AI {
|
|
|
2488
2494
|
|
|
2489
2495
|
#browserSpeechOperationId(action){
|
|
2490
2496
|
this.#browserSpeechOperationSequence=
|
|
2491
|
-
|
|
2497
|
+
is.bigint(this.#browserSpeechOperationSequence)
|
|
2492
2498
|
? this.#browserSpeechOperationSequence+1n
|
|
2493
2499
|
: this.#browserSpeechOperationSequence===Number.MAX_SAFE_INTEGER
|
|
2494
2500
|
? BigInt(this.#browserSpeechOperationSequence)+1n
|
|
@@ -2710,7 +2716,7 @@ class AI {
|
|
|
2710
2716
|
publicDetail:completeValue({
|
|
2711
2717
|
configurationId:normalized.id,
|
|
2712
2718
|
...(descriptor?{descriptor}:{}),
|
|
2713
|
-
...(
|
|
2719
|
+
...(is.string(error?.code)?{code:error.code}:{}),
|
|
2714
2720
|
reason
|
|
2715
2721
|
})
|
|
2716
2722
|
}
|
|
@@ -2738,7 +2744,7 @@ class AI {
|
|
|
2738
2744
|
continue;
|
|
2739
2745
|
}
|
|
2740
2746
|
const catalog=providers[role].catalog();
|
|
2741
|
-
if(catalog.length!==1
|
|
2747
|
+
if(catalog.length!==1||!is.string(catalog[0]?.id)){
|
|
2742
2748
|
throw aiBrowserSpeechError(
|
|
2743
2749
|
AI_BROWSER_SPEECH_ERROR_CODES.providerConstructionRejected,
|
|
2744
2750
|
AI_BROWSER_SPEECH_REASONS.providerConstructionRejected,
|
|
@@ -2952,7 +2958,7 @@ class AI {
|
|
|
2952
2958
|
for(const role of ['tts','stt'].filter(role=>roles.includes(role))){
|
|
2953
2959
|
if(record.registrationState?.[role]===false)continue;
|
|
2954
2960
|
const unregister=record.unregisters?.[role];
|
|
2955
|
-
if(
|
|
2961
|
+
if(!is.function(unregister)){
|
|
2956
2962
|
failures.push(aiBrowserSpeechError(
|
|
2957
2963
|
AI_BROWSER_SPEECH_ERROR_CODES.providerUnregistrationRejected,
|
|
2958
2964
|
AI_BROWSER_SPEECH_REASONS.providerUnregistrationRejected,
|
|
@@ -3746,7 +3752,7 @@ class AI {
|
|
|
3746
3752
|
|
|
3747
3753
|
async setSpeechMuted(muted){
|
|
3748
3754
|
const callId=this.#traceSpeech('setSpeechMuted.call',{requestedMuted:muted});
|
|
3749
|
-
if(
|
|
3755
|
+
if(!is.boolean(muted)){
|
|
3750
3756
|
throw new TypeError('AI speech muted state must be a boolean.');
|
|
3751
3757
|
}
|
|
3752
3758
|
const generation=++this.#speechControlGeneration;
|
|
@@ -3793,12 +3799,12 @@ class AI {
|
|
|
3793
3799
|
if(signal?.aborted){
|
|
3794
3800
|
throw normalizeAIRequestAbort(signal.reason);
|
|
3795
3801
|
}
|
|
3796
|
-
const message=
|
|
3802
|
+
const message=is.string(error)
|
|
3797
3803
|
?error
|
|
3798
3804
|
:error?.error?.message??error?.message;
|
|
3799
3805
|
if(
|
|
3800
3806
|
response.status!==429
|
|
3801
|
-
|
|
3807
|
+
||!is.string(message)
|
|
3802
3808
|
||!message.toLowerCase().includes('overload')
|
|
3803
3809
|
){
|
|
3804
3810
|
throw error;
|
|
@@ -3837,7 +3843,7 @@ class AI {
|
|
|
3837
3843
|
const client=globalThis.Arcane?.ollama;
|
|
3838
3844
|
|
|
3839
3845
|
return this.llmService==='OLLAMA'
|
|
3840
|
-
&&
|
|
3846
|
+
&&is.function(client?.chat)
|
|
3841
3847
|
?client
|
|
3842
3848
|
:null;
|
|
3843
3849
|
}
|
|
@@ -3848,10 +3854,10 @@ class AI {
|
|
|
3848
3854
|
if(service!=='LOCAL_SPEACH'){
|
|
3849
3855
|
return null;
|
|
3850
3856
|
}
|
|
3851
|
-
if(role==='tts'&&
|
|
3857
|
+
if(role==='tts'&&is.function(client?.synthesize)){
|
|
3852
3858
|
return client;
|
|
3853
3859
|
}
|
|
3854
|
-
if(role==='stt'&&
|
|
3860
|
+
if(role==='stt'&&is.function(client?.transcribe)){
|
|
3855
3861
|
return client;
|
|
3856
3862
|
}
|
|
3857
3863
|
return null;
|
|
@@ -3859,7 +3865,7 @@ class AI {
|
|
|
3859
3865
|
|
|
3860
3866
|
async #requestBuiltInSpeechTranscription(payload={},signal=null){
|
|
3861
3867
|
const audio=payload?.audio;
|
|
3862
|
-
if(!audio
|
|
3868
|
+
if(!audio||!is.function(audio.arrayBuffer)){
|
|
3863
3869
|
throw new TypeError('Speech transcription requires an audio Blob or File.');
|
|
3864
3870
|
}
|
|
3865
3871
|
if(signal?.aborted){
|
|
@@ -3881,7 +3887,7 @@ class AI {
|
|
|
3881
3887
|
if(signal?.aborted){
|
|
3882
3888
|
throw normalizeAIRequestAbort(signal.reason);
|
|
3883
3889
|
}
|
|
3884
|
-
if(!response
|
|
3890
|
+
if(!response||!is.string(response.text)){
|
|
3885
3891
|
throw new TypeError('Arcane returned an invalid local speech transcription.');
|
|
3886
3892
|
}
|
|
3887
3893
|
return response.text;
|
|
@@ -3909,7 +3915,7 @@ class AI {
|
|
|
3909
3915
|
}
|
|
3910
3916
|
|
|
3911
3917
|
async #requestBuiltInSpeechSynthesis(payload={},signal=null){
|
|
3912
|
-
const input=
|
|
3918
|
+
const input=is.string(payload?.input)?payload.input:'';
|
|
3913
3919
|
if(!input){
|
|
3914
3920
|
throw new TypeError('Speech synthesis requires nonempty input.');
|
|
3915
3921
|
}
|
|
@@ -3917,14 +3923,14 @@ class AI {
|
|
|
3917
3923
|
throw normalizeAIRequestAbort(signal.reason);
|
|
3918
3924
|
}
|
|
3919
3925
|
const model=String(payload.model||this.modelTTS);
|
|
3920
|
-
const voice=
|
|
3926
|
+
const voice=is.string(payload.voice)&&payload.voice.trim()
|
|
3921
3927
|
?payload.voice.trim()
|
|
3922
3928
|
:this.#builtInSpeechDefaultVoice('tts',this.ttsService);
|
|
3923
3929
|
if(!voice){
|
|
3924
3930
|
throw new TypeError('The selected speech provider requires a voice.');
|
|
3925
3931
|
}
|
|
3926
3932
|
const responseFormat=String(payload.responseFormat||this.audioFormat);
|
|
3927
|
-
const speed=
|
|
3933
|
+
const speed=is.finite(payload.speed)?payload.speed:this.voiceSpeed;
|
|
3928
3934
|
const nativeSpeech=this.#nativeSpeech(this.ttsService,'tts');
|
|
3929
3935
|
if(nativeSpeech){
|
|
3930
3936
|
const response=await nativeSpeech.synthesize({
|
|
@@ -3937,7 +3943,7 @@ class AI {
|
|
|
3937
3943
|
if(signal?.aborted){
|
|
3938
3944
|
throw normalizeAIRequestAbort(signal.reason);
|
|
3939
3945
|
}
|
|
3940
|
-
if(!response
|
|
3946
|
+
if(!response||!is.string(response.audioBase64)){
|
|
3941
3947
|
const error=new TypeError(
|
|
3942
3948
|
'The Arcane speech bridge returned invalid TTS audio.'
|
|
3943
3949
|
);
|
|
@@ -3947,7 +3953,7 @@ class AI {
|
|
|
3947
3953
|
return new Blob(
|
|
3948
3954
|
[this.#base64ToBytes(response.audioBase64)],
|
|
3949
3955
|
{
|
|
3950
|
-
type:
|
|
3956
|
+
type:is.string(response.contentType)
|
|
3951
3957
|
?response.contentType
|
|
3952
3958
|
:this.audioType
|
|
3953
3959
|
}
|
|
@@ -3996,7 +4002,7 @@ class AI {
|
|
|
3996
4002
|
}
|
|
3997
4003
|
|
|
3998
4004
|
async #androidNativeHost(){
|
|
3999
|
-
if(
|
|
4005
|
+
if(is.function(globalThis.arcaneAndroid?.postMessage)){
|
|
4000
4006
|
return true;
|
|
4001
4007
|
}
|
|
4002
4008
|
|
|
@@ -4038,7 +4044,7 @@ class AI {
|
|
|
4038
4044
|
}
|
|
4039
4045
|
|
|
4040
4046
|
#base64ToBytes(value){
|
|
4041
|
-
if(
|
|
4047
|
+
if(!is.string(value)||!value){
|
|
4042
4048
|
throw new TypeError('Arcane returned invalid local speech audio.');
|
|
4043
4049
|
}
|
|
4044
4050
|
|
|
@@ -4053,7 +4059,7 @@ class AI {
|
|
|
4053
4059
|
}
|
|
4054
4060
|
|
|
4055
4061
|
#ollamaTools(tools=[],toolChoice='auto'){
|
|
4056
|
-
if(!
|
|
4062
|
+
if(!is.array(tools)){
|
|
4057
4063
|
return [];
|
|
4058
4064
|
}
|
|
4059
4065
|
|
|
@@ -4077,12 +4083,12 @@ class AI {
|
|
|
4077
4083
|
const role=String(message?.role??'');
|
|
4078
4084
|
const content=String(message?.content??'');
|
|
4079
4085
|
|
|
4080
|
-
if(role==='assistant'&&
|
|
4086
|
+
if(role==='assistant'&&is.array(message?.tool_calls)){
|
|
4081
4087
|
const toolCalls=message.tool_calls.map(function sanitizeOllamaToolCall(call){
|
|
4082
4088
|
const name=String(call?.function?.name??'').trim();
|
|
4083
4089
|
const callId=String(call?.id??'').trim();
|
|
4084
4090
|
let argumentValue=call?.function?.arguments;
|
|
4085
|
-
if(
|
|
4091
|
+
if(is.string(argumentValue)){
|
|
4086
4092
|
try{
|
|
4087
4093
|
argumentValue=JSON.parse(argumentValue);
|
|
4088
4094
|
}catch(cause){
|
|
@@ -4096,8 +4102,8 @@ class AI {
|
|
|
4096
4102
|
}
|
|
4097
4103
|
if(
|
|
4098
4104
|
!argumentValue
|
|
4099
|
-
|
|
4100
|
-
||
|
|
4105
|
+
||!is.object(argumentValue)
|
|
4106
|
+
||is.array(argumentValue)
|
|
4101
4107
|
){
|
|
4102
4108
|
const error=new TypeError(
|
|
4103
4109
|
'Ollama tool call arguments must contain a JSON object.'
|
|
@@ -4173,8 +4179,8 @@ class AI {
|
|
|
4173
4179
|
return 'json';
|
|
4174
4180
|
}
|
|
4175
4181
|
if(
|
|
4176
|
-
|
|
4177
|
-
&&!
|
|
4182
|
+
is.object(value)
|
|
4183
|
+
&&!is.array(value)
|
|
4178
4184
|
&&(
|
|
4179
4185
|
Object.getPrototypeOf(value)===Object.prototype
|
|
4180
4186
|
||Object.getPrototypeOf(value)===null
|
|
@@ -4208,7 +4214,7 @@ class AI {
|
|
|
4208
4214
|
}
|
|
4209
4215
|
|
|
4210
4216
|
async #reportRequest(requestHandler,request,id,metadata){
|
|
4211
|
-
if(
|
|
4217
|
+
if(!is.function(requestHandler)){
|
|
4212
4218
|
throw new TypeError('AI onRequest callback must be a function.');
|
|
4213
4219
|
}
|
|
4214
4220
|
await requestHandler(request,id,metadata);
|
|
@@ -4216,31 +4222,31 @@ class AI {
|
|
|
4216
4222
|
|
|
4217
4223
|
#providerStreamEmissions(chunk,seeThinking){
|
|
4218
4224
|
const chunks=[];
|
|
4219
|
-
if(
|
|
4225
|
+
if(is.string(chunk)){
|
|
4220
4226
|
if(chunk) chunks.push({text:chunk,thinking:false});
|
|
4221
4227
|
return {chunks};
|
|
4222
4228
|
}
|
|
4223
|
-
const choices=
|
|
4229
|
+
const choices=is.array(chunk?.choices)?chunk.choices:[];
|
|
4224
4230
|
for(const choice of choices){
|
|
4225
4231
|
const delta=choice?.delta||{};
|
|
4226
4232
|
if(
|
|
4227
4233
|
seeThinking
|
|
4228
|
-
&&
|
|
4234
|
+
&&is.string(delta.reasoning_content)
|
|
4229
4235
|
&&delta.reasoning_content
|
|
4230
4236
|
){
|
|
4231
4237
|
chunks.push({text:delta.reasoning_content,thinking:true});
|
|
4232
4238
|
}
|
|
4233
|
-
if(
|
|
4239
|
+
if(is.string(delta.content)&&delta.content){
|
|
4234
4240
|
chunks.push({text:delta.content,thinking:false});
|
|
4235
4241
|
}
|
|
4236
4242
|
}
|
|
4237
4243
|
if(!choices.length){
|
|
4238
|
-
if(seeThinking&&
|
|
4244
|
+
if(seeThinking&&is.string(chunk?.thinking)){
|
|
4239
4245
|
chunks.push({text:chunk.thinking,thinking:true});
|
|
4240
4246
|
}
|
|
4241
|
-
const text=
|
|
4247
|
+
const text=is.string(chunk?.text)
|
|
4242
4248
|
?chunk.text
|
|
4243
|
-
:
|
|
4249
|
+
:is.string(chunk?.content)
|
|
4244
4250
|
?chunk.content
|
|
4245
4251
|
:'';
|
|
4246
4252
|
if(text){
|
|
@@ -4251,16 +4257,16 @@ class AI {
|
|
|
4251
4257
|
}
|
|
4252
4258
|
|
|
4253
4259
|
#providerCompletionOutput(completion){
|
|
4254
|
-
if(
|
|
4260
|
+
if(is.string(completion)){
|
|
4255
4261
|
return completion;
|
|
4256
4262
|
}
|
|
4257
|
-
if(
|
|
4263
|
+
if(is.array(completion?.choices)&&completion.choices.length>1){
|
|
4258
4264
|
return completion;
|
|
4259
4265
|
}
|
|
4260
4266
|
const structuralToolCalls=normalizeAICompletionToolCalls(completion);
|
|
4261
4267
|
if(structuralToolCalls.length)return structuralToolCalls;
|
|
4262
4268
|
const content=completion?.choices?.[0]?.message?.content;
|
|
4263
|
-
return
|
|
4269
|
+
return is.string(content)?content:completion;
|
|
4264
4270
|
}
|
|
4265
4271
|
|
|
4266
4272
|
#requestBuiltInLLMChat(payload={},signal=null){
|
|
@@ -4332,14 +4338,14 @@ class AI {
|
|
|
4332
4338
|
if(value===undefined){
|
|
4333
4339
|
return [];
|
|
4334
4340
|
}
|
|
4335
|
-
if(!
|
|
4341
|
+
if(!is.array(value)){
|
|
4336
4342
|
throw aiStructuralError(
|
|
4337
4343
|
'AI_CHAT_INVALID_TOOL_CALL',
|
|
4338
4344
|
'The native Ollama response contains invalid structural tool calls.'
|
|
4339
4345
|
);
|
|
4340
4346
|
}
|
|
4341
4347
|
return value.map(function adaptNativeOllamaToolCall(call,index){
|
|
4342
|
-
if(!call
|
|
4348
|
+
if(!call||!is.object(call)||is.array(call)){
|
|
4343
4349
|
throw aiStructuralError(
|
|
4344
4350
|
'AI_CHAT_INVALID_TOOL_CALL',
|
|
4345
4351
|
`The native Ollama structural tool call ${index+1} is invalid.`
|
|
@@ -4348,8 +4354,8 @@ class AI {
|
|
|
4348
4354
|
const nativeFunction=call.function;
|
|
4349
4355
|
if(
|
|
4350
4356
|
!nativeFunction
|
|
4351
|
-
|
|
4352
|
-
||
|
|
4357
|
+
||!is.object(nativeFunction)
|
|
4358
|
+
||is.array(nativeFunction)
|
|
4353
4359
|
){
|
|
4354
4360
|
throw aiStructuralError(
|
|
4355
4361
|
'AI_CHAT_INVALID_TOOL_CALL',
|
|
@@ -4359,8 +4365,8 @@ class AI {
|
|
|
4359
4365
|
let encodedArguments=nativeFunction.arguments;
|
|
4360
4366
|
if(
|
|
4361
4367
|
encodedArguments
|
|
4362
|
-
&&
|
|
4363
|
-
&&!
|
|
4368
|
+
&&is.object(encodedArguments)
|
|
4369
|
+
&&!is.array(encodedArguments)
|
|
4364
4370
|
){
|
|
4365
4371
|
try{
|
|
4366
4372
|
encodedArguments=JSON.stringify(encodedArguments);
|
|
@@ -4394,7 +4400,7 @@ class AI {
|
|
|
4394
4400
|
:{};
|
|
4395
4401
|
const responseFields={...responseRecord};
|
|
4396
4402
|
delete responseFields.message;
|
|
4397
|
-
const responseId=
|
|
4403
|
+
const responseId=is.string(response?.id)&&response.id
|
|
4398
4404
|
?response.id
|
|
4399
4405
|
:`ollama-${id}`;
|
|
4400
4406
|
const adaptedToolCalls=this.#openAICompatibleOllamaToolCalls(
|
|
@@ -4406,7 +4412,7 @@ class AI {
|
|
|
4406
4412
|
});
|
|
4407
4413
|
const messageRecord={
|
|
4408
4414
|
...message,
|
|
4409
|
-
role:
|
|
4415
|
+
role:is.string(message.role)&&message.role
|
|
4410
4416
|
?message.role
|
|
4411
4417
|
:'assistant',
|
|
4412
4418
|
content:Object.hasOwn(message,'content')?message.content:'',
|
|
@@ -4733,8 +4739,8 @@ class AI {
|
|
|
4733
4739
|
validateAIStructuralRequest(messages,tools,parallel_tool_calls);
|
|
4734
4740
|
this.#assertServiceConfigured(this.llmService);
|
|
4735
4741
|
if(signal&&(
|
|
4736
|
-
|
|
4737
|
-
|
|
4742
|
+
!is.boolean(signal.aborted)
|
|
4743
|
+
||!is.function(signal.addEventListener)
|
|
4738
4744
|
)){
|
|
4739
4745
|
throw new TypeError('AI request signal must be an AbortSignal.');
|
|
4740
4746
|
}
|
|
@@ -4972,7 +4978,7 @@ class AI {
|
|
|
4972
4978
|
const toolFunction=toolCall.function||{};
|
|
4973
4979
|
if(
|
|
4974
4980
|
toolCall.index!==undefined
|
|
4975
|
-
&&(!
|
|
4981
|
+
&&(!is.safeInteger(toolCall.index)||toolCall.index<0)
|
|
4976
4982
|
){
|
|
4977
4983
|
throw aiStructuralError(
|
|
4978
4984
|
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
|
|
@@ -4992,7 +4998,7 @@ class AI {
|
|
|
4992
4998
|
};
|
|
4993
4999
|
|
|
4994
5000
|
if(toolCall.id!==undefined){
|
|
4995
|
-
if(
|
|
5001
|
+
if(!is.string(toolCall.id)||!toolCall.id){
|
|
4996
5002
|
record.invalidIdentity=true;
|
|
4997
5003
|
}else if(record.id&&record.id!==toolCall.id){
|
|
4998
5004
|
record.invalidIdentity=true;
|
|
@@ -5002,7 +5008,7 @@ class AI {
|
|
|
5002
5008
|
}
|
|
5003
5009
|
|
|
5004
5010
|
if(toolCall.type!==undefined){
|
|
5005
|
-
if(
|
|
5011
|
+
if(!is.string(toolCall.type)||!toolCall.type){
|
|
5006
5012
|
record.invalidIdentity=true;
|
|
5007
5013
|
}else if(record.type&&record.type!==toolCall.type){
|
|
5008
5014
|
record.invalidIdentity=true;
|
|
@@ -5012,14 +5018,14 @@ class AI {
|
|
|
5012
5018
|
}
|
|
5013
5019
|
|
|
5014
5020
|
if(toolFunction.name!==undefined){
|
|
5015
|
-
if(
|
|
5021
|
+
if(!is.string(toolFunction.name)){
|
|
5016
5022
|
record.invalidName=true;
|
|
5017
5023
|
}else{
|
|
5018
5024
|
record.name+=toolFunction.name;
|
|
5019
5025
|
}
|
|
5020
5026
|
}
|
|
5021
5027
|
|
|
5022
|
-
if(
|
|
5028
|
+
if(is.string(toolFunction.arguments)){
|
|
5023
5029
|
record.arguments+=toolFunction.arguments;
|
|
5024
5030
|
}else if(toolFunction.arguments!==undefined){
|
|
5025
5031
|
record.invalidArguments=true;
|
|
@@ -5033,7 +5039,7 @@ class AI {
|
|
|
5033
5039
|
function receiveStreamedChoice(choice={},choicePosition=0){
|
|
5034
5040
|
if(
|
|
5035
5041
|
choice.index!==undefined
|
|
5036
|
-
&&(!
|
|
5042
|
+
&&(!is.safeInteger(choice.index)||choice.index<0)
|
|
5037
5043
|
){
|
|
5038
5044
|
throw aiStructuralError(
|
|
5039
5045
|
'AI_CHAT_INVALID_RESPONSE',
|
|
@@ -5065,8 +5071,8 @@ class AI {
|
|
|
5065
5071
|
if(key==='content'||key==='reasoning'||key==='reasoning_content'){
|
|
5066
5072
|
if(
|
|
5067
5073
|
!replaceText
|
|
5068
|
-
&&
|
|
5069
|
-
&&
|
|
5074
|
+
&&is.string(value)
|
|
5075
|
+
&&is.string(record.message[key])
|
|
5070
5076
|
){
|
|
5071
5077
|
record.message[key]+=value;
|
|
5072
5078
|
}else{
|
|
@@ -5077,7 +5083,7 @@ class AI {
|
|
|
5077
5083
|
record.message[key]=value;
|
|
5078
5084
|
}
|
|
5079
5085
|
}
|
|
5080
|
-
if(
|
|
5086
|
+
if(!is.string(record.message.role)||!record.message.role){
|
|
5081
5087
|
record.message.role='assistant';
|
|
5082
5088
|
}
|
|
5083
5089
|
if(!Object.hasOwn(record.message,'content')) record.message.content='';
|
|
@@ -5099,7 +5105,7 @@ class AI {
|
|
|
5099
5105
|
for(const [key,value] of Object.entries(streamedResponse)){
|
|
5100
5106
|
if(key!=='choices') streamMetadata[key]=value;
|
|
5101
5107
|
}
|
|
5102
|
-
const choices=
|
|
5108
|
+
const choices=is.array(streamedResponse.choices)
|
|
5103
5109
|
?streamedResponse.choices
|
|
5104
5110
|
:[];
|
|
5105
5111
|
for(let choicePosition=0;choicePosition<choices.length;choicePosition+=1){
|
|
@@ -5126,7 +5132,7 @@ class AI {
|
|
|
5126
5132
|
const choiceDelta=choice.delta??{};
|
|
5127
5133
|
const choiceMessage=choice.message??null;
|
|
5128
5134
|
if(Object.hasOwn(choiceDelta,'tool_calls')){
|
|
5129
|
-
if(!
|
|
5135
|
+
if(!is.array(choiceDelta.tool_calls)){
|
|
5130
5136
|
throw aiStructuralError(
|
|
5131
5137
|
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
|
|
5132
5138
|
`AI stream choice ${choicePosition+1} contains invalid structural tool-call data.`
|
|
@@ -5135,7 +5141,7 @@ class AI {
|
|
|
5135
5141
|
receiveStreamedToolCalls(choiceDelta.tool_calls,choiceIndex);
|
|
5136
5142
|
}
|
|
5137
5143
|
if(choiceMessage&&Object.hasOwn(choiceMessage,'tool_calls')){
|
|
5138
|
-
if(!
|
|
5144
|
+
if(!is.array(choiceMessage.tool_calls)){
|
|
5139
5145
|
throw aiStructuralError(
|
|
5140
5146
|
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
|
|
5141
5147
|
`AI stream choice ${choicePosition+1} contains invalid terminal structural tool-call data.`
|
|
@@ -5149,14 +5155,14 @@ class AI {
|
|
|
5149
5155
|
)
|
|
5150
5156
|
);
|
|
5151
5157
|
}
|
|
5152
|
-
const content=
|
|
5158
|
+
const content=is.string(choiceDelta.content)
|
|
5153
5159
|
?choiceDelta.content
|
|
5154
5160
|
:'';
|
|
5155
5161
|
let reasoning='';
|
|
5156
5162
|
if(seeThinking){
|
|
5157
|
-
reasoning=
|
|
5163
|
+
reasoning=is.string(choiceDelta.reasoning_content)
|
|
5158
5164
|
?choiceDelta.reasoning_content
|
|
5159
|
-
:
|
|
5165
|
+
:is.string(choiceDelta.reasoning)
|
|
5160
5166
|
?choiceDelta.reasoning
|
|
5161
5167
|
:'';
|
|
5162
5168
|
}
|
|
@@ -5344,7 +5350,7 @@ class AI {
|
|
|
5344
5350
|
const hasToolCalls=structuralToolCallsByChoice.has(choicePosition);
|
|
5345
5351
|
return {
|
|
5346
5352
|
...retained.choice,
|
|
5347
|
-
index:
|
|
5353
|
+
index:is.safeInteger(retained.choice.index)
|
|
5348
5354
|
&&retained.choice.index>=0
|
|
5349
5355
|
?retained.choice.index
|
|
5350
5356
|
:choicePosition,
|
|
@@ -5562,8 +5568,8 @@ class AI {
|
|
|
5562
5568
|
validateAIStructuralRequest(messages,tools,parallel_tool_calls);
|
|
5563
5569
|
this.#assertServiceConfigured(this.llmService);
|
|
5564
5570
|
if(signal&&(
|
|
5565
|
-
|
|
5566
|
-
|
|
5571
|
+
!is.boolean(signal.aborted)
|
|
5572
|
+
||!is.function(signal.addEventListener)
|
|
5567
5573
|
)){
|
|
5568
5574
|
throw new TypeError('AI request signal must be an AbortSignal.');
|
|
5569
5575
|
}
|
|
@@ -5725,16 +5731,291 @@ class AI {
|
|
|
5725
5731
|
return result;
|
|
5726
5732
|
}
|
|
5727
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
|
+
|
|
5728
6009
|
streamTTS(
|
|
5729
6010
|
text='',
|
|
5730
6011
|
end=false,
|
|
5731
6012
|
options={}
|
|
5732
6013
|
){
|
|
5733
6014
|
const callId=this.#traceSpeech('streamTTS.call',{text,end,options});
|
|
6015
|
+
for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
|
|
5734
6016
|
if(this.muted){
|
|
5735
6017
|
if(end){
|
|
5736
6018
|
this.audioMessageChunks='';
|
|
5737
|
-
this.#speechTextFormat='plain';
|
|
5738
6019
|
this.#markdownSpeech.reset();
|
|
5739
6020
|
}
|
|
5740
6021
|
this.#traceSpeech('streamTTS.result',{callId,result:false,reason:'muted'});
|
|
@@ -5743,28 +6024,16 @@ class AI {
|
|
|
5743
6024
|
|
|
5744
6025
|
const {voice,speed,waitForPlayback=false}=options;
|
|
5745
6026
|
const pauseAfterMs=Number(options.pauseAfterMs??0);
|
|
5746
|
-
if(!
|
|
6027
|
+
if(!is.finite(pauseAfterMs)||pauseAfterMs<0){
|
|
5747
6028
|
throw new RangeError('Speech pauses must be a nonnegative number of milliseconds.');
|
|
5748
6029
|
}
|
|
5749
6030
|
|
|
5750
|
-
const
|
|
5751
|
-
if(textFormat!=='plain'&&textFormat!=='markdown'){
|
|
5752
|
-
throw new TypeError('Speech textFormat must be plain or markdown.');
|
|
5753
|
-
}
|
|
5754
|
-
let speechText='';
|
|
5755
|
-
if(textFormat!==this.#speechTextFormat){
|
|
5756
|
-
speechText=this.#markdownSpeech.append('',true);
|
|
5757
|
-
}
|
|
5758
|
-
const incomingText=String(text||'');
|
|
5759
|
-
speechText+=textFormat==='markdown'
|
|
5760
|
-
?this.#markdownSpeech.append(incomingText,end)
|
|
5761
|
-
:incomingText;
|
|
5762
|
-
this.#speechTextFormat=end?'plain':textFormat;
|
|
6031
|
+
const speechText=this.#markdownSpeech.append(String(text||''),end);
|
|
5763
6032
|
this.audioMessageChunks+=speechText;
|
|
5764
6033
|
const outputs=this.#extractSpeechSegments(end);
|
|
5765
6034
|
this.#traceSpeech('streamTTS.segments',{
|
|
5766
6035
|
callId,segments:outputs,remainder:this.audioMessageChunks,
|
|
5767
|
-
segmentation:this.ttsSegmentation,
|
|
6036
|
+
segmentation:this.ttsSegmentation,speechText
|
|
5768
6037
|
});
|
|
5769
6038
|
|
|
5770
6039
|
if(!outputs.length){
|
|
@@ -5814,15 +6083,22 @@ class AI {
|
|
|
5814
6083
|
}
|
|
5815
6084
|
|
|
5816
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) {
|
|
5817
6092
|
const segments=[];
|
|
5818
|
-
let remainder=
|
|
6093
|
+
let remainder=text;
|
|
5819
6094
|
|
|
5820
6095
|
while(remainder.length>0){
|
|
5821
6096
|
const punctuationBoundary=this.#findSpeechPunctuationBoundary(
|
|
5822
6097
|
remainder,
|
|
5823
|
-
end
|
|
6098
|
+
end,
|
|
6099
|
+
segmentation
|
|
5824
6100
|
);
|
|
5825
|
-
const cadenceBoundary=this.#findSpeechCadenceBoundary(remainder,end);
|
|
6101
|
+
const cadenceBoundary=this.#findSpeechCadenceBoundary(remainder,end,segmentation);
|
|
5826
6102
|
let boundary=punctuationBoundary;
|
|
5827
6103
|
|
|
5828
6104
|
if(boundary<0||(cadenceBoundary>0&&cadenceBoundary<boundary)){
|
|
@@ -5845,15 +6121,14 @@ class AI {
|
|
|
5845
6121
|
}
|
|
5846
6122
|
}
|
|
5847
6123
|
|
|
5848
|
-
|
|
5849
|
-
return segments;
|
|
6124
|
+
return {segments, remainder};
|
|
5850
6125
|
}
|
|
5851
6126
|
|
|
5852
|
-
#findSpeechPunctuationBoundary(text,end=false){
|
|
5853
|
-
if(
|
|
6127
|
+
#findSpeechPunctuationBoundary(text,end=false,segmentation=this.#ttsSegmentation){
|
|
6128
|
+
if(segmentation.punctuation==='none'){
|
|
5854
6129
|
return -1;
|
|
5855
6130
|
}
|
|
5856
|
-
if(
|
|
6131
|
+
if(segmentation.punctuation==='any'){
|
|
5857
6132
|
return this.#findAnySpeechPunctuationBoundary(text,end);
|
|
5858
6133
|
}
|
|
5859
6134
|
|
|
@@ -5898,8 +6173,8 @@ class AI {
|
|
|
5898
6173
|
return -1;
|
|
5899
6174
|
}
|
|
5900
6175
|
|
|
5901
|
-
#findSpeechCadenceBoundary(text,end=false){
|
|
5902
|
-
const wordCadence=
|
|
6176
|
+
#findSpeechCadenceBoundary(text,end=false,segmentation=this.#ttsSegmentation){
|
|
6177
|
+
const wordCadence=segmentation.wordCadence;
|
|
5903
6178
|
if(wordCadence===null){
|
|
5904
6179
|
return -1;
|
|
5905
6180
|
}
|
|
@@ -6040,7 +6315,8 @@ class AI {
|
|
|
6040
6315
|
this.#traceSpeechJob('generation.request',job,{payload,selection});
|
|
6041
6316
|
const response=await this.fetchTTS(
|
|
6042
6317
|
payload,
|
|
6043
|
-
job.abortController.signal
|
|
6318
|
+
job.abortController.signal,
|
|
6319
|
+
{speechInputPrepared:true}
|
|
6044
6320
|
);
|
|
6045
6321
|
return this.#normalizeProviderSpeechAudio(response);
|
|
6046
6322
|
}
|
|
@@ -6054,7 +6330,7 @@ class AI {
|
|
|
6054
6330
|
entry=>entry.providerId===selection.providerId
|
|
6055
6331
|
);
|
|
6056
6332
|
const model=provider?.models.find(entry=>entry?.id===selection.modelId);
|
|
6057
|
-
return
|
|
6333
|
+
return is.string(model?.defaultVoice)&&model.defaultVoice.trim()
|
|
6058
6334
|
?model.defaultVoice.trim()
|
|
6059
6335
|
:null;
|
|
6060
6336
|
}
|
|
@@ -6072,7 +6348,7 @@ class AI {
|
|
|
6072
6348
|
if(speech===undefined){
|
|
6073
6349
|
return this.audioFormat;
|
|
6074
6350
|
}
|
|
6075
|
-
const prototype=speech&&
|
|
6351
|
+
const prototype=speech&&is.object(speech)
|
|
6076
6352
|
?Object.getPrototypeOf(speech)
|
|
6077
6353
|
:null;
|
|
6078
6354
|
const descriptors=prototype===Object.prototype||prototype===null
|
|
@@ -6080,10 +6356,10 @@ class AI {
|
|
|
6080
6356
|
:null;
|
|
6081
6357
|
const formats=descriptors?.responseFormats?.value;
|
|
6082
6358
|
const defaultFormat=descriptors?.defaultResponseFormat?.value;
|
|
6083
|
-
if(!
|
|
6359
|
+
if(!is.array(formats)
|
|
6084
6360
|
||formats.length<1
|
|
6085
|
-
||!formats.every(format=>
|
|
6086
|
-
|
|
6361
|
+
||!formats.every(format=>is.string(format)&&format.trim()===format&&format)
|
|
6362
|
+
||!is.string(defaultFormat)
|
|
6087
6363
|
||!formats.includes(defaultFormat)){
|
|
6088
6364
|
throw aiProviderError(
|
|
6089
6365
|
'The selected TTS provider returned an invalid speech format catalog.',
|
|
@@ -6110,7 +6386,7 @@ class AI {
|
|
|
6110
6386
|
};
|
|
6111
6387
|
}
|
|
6112
6388
|
|
|
6113
|
-
if(response instanceof ArrayBuffer||
|
|
6389
|
+
if(response instanceof ArrayBuffer||is.arrayBufferView(response)){
|
|
6114
6390
|
const bytes=response instanceof ArrayBuffer
|
|
6115
6391
|
?new Uint8Array(response)
|
|
6116
6392
|
:new Uint8Array(
|
|
@@ -6124,11 +6400,11 @@ class AI {
|
|
|
6124
6400
|
};
|
|
6125
6401
|
}
|
|
6126
6402
|
|
|
6127
|
-
if(response&&
|
|
6128
|
-
if(
|
|
6403
|
+
if(response&&is.object(response)){
|
|
6404
|
+
if(is.string(response.audioBase64)){
|
|
6129
6405
|
return {
|
|
6130
6406
|
chunks:[this.#base64ToBytes(response.audioBase64)],
|
|
6131
|
-
type:
|
|
6407
|
+
type:is.string(response.contentType)
|
|
6132
6408
|
?response.contentType
|
|
6133
6409
|
:this.audioType
|
|
6134
6410
|
};
|
|
@@ -6142,7 +6418,7 @@ class AI {
|
|
|
6142
6418
|
};
|
|
6143
6419
|
}
|
|
6144
6420
|
if(response.audio instanceof ArrayBuffer
|
|
6145
|
-
||
|
|
6421
|
+
||is.arrayBufferView(response.audio)){
|
|
6146
6422
|
const bytes=response.audio instanceof ArrayBuffer
|
|
6147
6423
|
?new Uint8Array(response.audio)
|
|
6148
6424
|
:new Uint8Array(
|
|
@@ -6152,7 +6428,7 @@ class AI {
|
|
|
6152
6428
|
);
|
|
6153
6429
|
return {
|
|
6154
6430
|
chunks:[bytes],
|
|
6155
|
-
type:
|
|
6431
|
+
type:is.string(response.contentType)
|
|
6156
6432
|
?response.contentType
|
|
6157
6433
|
:this.audioType
|
|
6158
6434
|
};
|
|
@@ -6181,7 +6457,7 @@ class AI {
|
|
|
6181
6457
|
|
|
6182
6458
|
const AudioContext=window.AudioContext||window.webkitAudioContext;
|
|
6183
6459
|
|
|
6184
|
-
if(
|
|
6460
|
+
if(!is.function(AudioContext)){
|
|
6185
6461
|
throw new TypeError('Audio playback is unavailable in this browser.');
|
|
6186
6462
|
}
|
|
6187
6463
|
|
|
@@ -6189,13 +6465,13 @@ class AI {
|
|
|
6189
6465
|
return this.audioContext;
|
|
6190
6466
|
}
|
|
6191
6467
|
|
|
6192
|
-
async fetchTTS(payload={},signal=null){
|
|
6468
|
+
async fetchTTS(payload={},signal=null,{speechInputPrepared=false}={}){
|
|
6193
6469
|
const callId=this.#traceSpeech('fetchTTS.call',{payload,aborted:signal?.aborted});
|
|
6194
6470
|
try{
|
|
6195
6471
|
this.#assertServiceConfigured(this.ttsService,'tts');
|
|
6196
6472
|
if(!payload
|
|
6197
|
-
|
|
6198
|
-
||
|
|
6473
|
+
||!is.object(payload)
|
|
6474
|
+
||is.array(payload)
|
|
6199
6475
|
||![Object.prototype,null].includes(Object.getPrototypeOf(payload))){
|
|
6200
6476
|
const error=new TypeError('AI.fetchTTS requires a speech request object.');
|
|
6201
6477
|
error.code='ARCANE_AI_TTS_REQUEST_INVALID';
|
|
@@ -6210,7 +6486,7 @@ class AI {
|
|
|
6210
6486
|
'speed'
|
|
6211
6487
|
]);
|
|
6212
6488
|
for(const key of Reflect.ownKeys(descriptors)){
|
|
6213
|
-
if(
|
|
6489
|
+
if(is.symbol(key)
|
|
6214
6490
|
||!acceptedKeys.has(key)
|
|
6215
6491
|
||!Object.hasOwn(descriptors[key],'value')){
|
|
6216
6492
|
const error=new TypeError(
|
|
@@ -6221,9 +6497,9 @@ class AI {
|
|
|
6221
6497
|
}
|
|
6222
6498
|
}
|
|
6223
6499
|
if(signal&&(
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6500
|
+
!is.boolean(signal.aborted)
|
|
6501
|
+
||!is.function(signal.addEventListener)
|
|
6502
|
+
||!is.function(signal.removeEventListener)
|
|
6227
6503
|
)){
|
|
6228
6504
|
const error=new TypeError('AI.fetchTTS signal must be an AbortSignal.');
|
|
6229
6505
|
error.code='ARCANE_AI_TTS_SIGNAL_INVALID';
|
|
@@ -6233,16 +6509,19 @@ class AI {
|
|
|
6233
6509
|
throw normalizeAIRequestAbort(signal.reason);
|
|
6234
6510
|
}
|
|
6235
6511
|
|
|
6236
|
-
const
|
|
6237
|
-
if(
|
|
6512
|
+
const suppliedInput=descriptors.input?.value;
|
|
6513
|
+
if(!is.string(suppliedInput)||!suppliedInput.trim()){
|
|
6238
6514
|
const error=new TypeError('AI.fetchTTS input must be nonempty text.');
|
|
6239
6515
|
error.code='ARCANE_AI_TTS_INPUT_INVALID';
|
|
6240
6516
|
throw error;
|
|
6241
6517
|
}
|
|
6518
|
+
const input=speechInputPrepared===true
|
|
6519
|
+
?suppliedInput
|
|
6520
|
+
:stripSpeechFormatting(suppliedInput);
|
|
6242
6521
|
const selection=this.#providerRuntime.selection('tts');
|
|
6243
6522
|
const requestedModel=descriptors.model?.value;
|
|
6244
6523
|
if(requestedModel!==undefined
|
|
6245
|
-
&&(
|
|
6524
|
+
&&(!is.string(requestedModel)
|
|
6246
6525
|
||requestedModel.trim()!==requestedModel
|
|
6247
6526
|
||!requestedModel)){
|
|
6248
6527
|
const error=new TypeError(
|
|
@@ -6269,7 +6548,7 @@ class AI {
|
|
|
6269
6548
|
}
|
|
6270
6549
|
const requestedVoice=descriptors.voice?.value;
|
|
6271
6550
|
if(requestedVoice!==undefined
|
|
6272
|
-
&&(
|
|
6551
|
+
&&(!is.string(requestedVoice)
|
|
6273
6552
|
||requestedVoice.trim()!==requestedVoice
|
|
6274
6553
|
||!requestedVoice)){
|
|
6275
6554
|
const error=new TypeError(
|
|
@@ -6292,7 +6571,7 @@ class AI {
|
|
|
6292
6571
|
}
|
|
6293
6572
|
const requestedResponseFormat=descriptors.responseFormat?.value;
|
|
6294
6573
|
if(requestedResponseFormat!==undefined
|
|
6295
|
-
&&(
|
|
6574
|
+
&&(!is.string(requestedResponseFormat)
|
|
6296
6575
|
||requestedResponseFormat.trim()!==requestedResponseFormat
|
|
6297
6576
|
||!requestedResponseFormat)){
|
|
6298
6577
|
const error=new TypeError(
|
|
@@ -6312,7 +6591,7 @@ class AI {
|
|
|
6312
6591
|
const speed=descriptors.speed
|
|
6313
6592
|
?Number(descriptors.speed.value)
|
|
6314
6593
|
:this.voiceSpeed;
|
|
6315
|
-
if(!
|
|
6594
|
+
if(!is.finite(speed)||speed<=0){
|
|
6316
6595
|
const error=new RangeError('AI.fetchTTS speed must be a positive number.');
|
|
6317
6596
|
error.code='ARCANE_AI_TTS_SPEED_INVALID';
|
|
6318
6597
|
throw error;
|
|
@@ -6329,7 +6608,8 @@ class AI {
|
|
|
6329
6608
|
payload:{model,voice,input,responseFormat,speed},
|
|
6330
6609
|
localOnly:false,
|
|
6331
6610
|
signal
|
|
6332
|
-
}
|
|
6611
|
+
},
|
|
6612
|
+
{speechInputPrepared:true}
|
|
6333
6613
|
);
|
|
6334
6614
|
this.#traceSpeech('fetchTTS.providerResult',{callId,response});
|
|
6335
6615
|
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
@@ -6359,9 +6639,9 @@ class AI {
|
|
|
6359
6639
|
try{
|
|
6360
6640
|
this.#assertServiceConfigured(this.sttService,'stt');
|
|
6361
6641
|
if(signal&&(
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6642
|
+
!is.boolean(signal.aborted)
|
|
6643
|
+
||!is.function(signal.addEventListener)
|
|
6644
|
+
||!is.function(signal.removeEventListener)
|
|
6365
6645
|
)){
|
|
6366
6646
|
const error=new TypeError('AI.fetchSTT signal must be an AbortSignal.');
|
|
6367
6647
|
error.code='ARCANE_AI_STT_SIGNAL_INVALID';
|
|
@@ -6378,7 +6658,7 @@ class AI {
|
|
|
6378
6658
|
operation:'transcribe',
|
|
6379
6659
|
payload:{
|
|
6380
6660
|
audio:audioFile,
|
|
6381
|
-
mimeType:
|
|
6661
|
+
mimeType:is.function(globalThis.Blob)
|
|
6382
6662
|
&&audioFile instanceof Blob
|
|
6383
6663
|
?String(audioFile.type||'audio/webm')
|
|
6384
6664
|
:'audio/webm',
|
|
@@ -6389,10 +6669,10 @@ class AI {
|
|
|
6389
6669
|
}
|
|
6390
6670
|
);
|
|
6391
6671
|
this.#traceSpeech('fetchSTT.providerResult',{callId,response});
|
|
6392
|
-
const text=
|
|
6672
|
+
const text=is.string(response)
|
|
6393
6673
|
?response
|
|
6394
6674
|
:response?.text;
|
|
6395
|
-
if(
|
|
6675
|
+
if(!is.string(text)){
|
|
6396
6676
|
const error=new TypeError(
|
|
6397
6677
|
'Arcane returned an invalid provider speech transcription.'
|
|
6398
6678
|
);
|
|
@@ -6425,6 +6705,7 @@ class AI {
|
|
|
6425
6705
|
const callId=this.#traceSpeech('stopAudio.call',{
|
|
6426
6706
|
remainder:this.audioMessageChunks,queuedJobs:this.speechJobs.length
|
|
6427
6707
|
});
|
|
6708
|
+
for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
|
|
6428
6709
|
this.speechGeneration+=1;
|
|
6429
6710
|
this.speechResumeAttempt+=1;
|
|
6430
6711
|
this.speechResumePending=false;
|
|
@@ -6432,7 +6713,6 @@ class AI {
|
|
|
6432
6713
|
this.speechScheduleContext=null;
|
|
6433
6714
|
this.speechScheduleTime=0;
|
|
6434
6715
|
this.audioMessageChunks='';
|
|
6435
|
-
this.#speechTextFormat='plain';
|
|
6436
6716
|
this.#markdownSpeech.reset();
|
|
6437
6717
|
this.#clearSpeechUnlock();
|
|
6438
6718
|
|
|
@@ -6485,9 +6765,14 @@ class AI {
|
|
|
6485
6765
|
|
|
6486
6766
|
let attempt=0;
|
|
6487
6767
|
let context;
|
|
6768
|
+
let preparedGroup = null;
|
|
6488
6769
|
|
|
6489
6770
|
try{
|
|
6490
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;
|
|
6491
6776
|
this.#traceSpeech('resumeAudio.context',{
|
|
6492
6777
|
callId,state:context.state,audioTime:context.currentTime,
|
|
6493
6778
|
sampleRate:context.sampleRate
|
|
@@ -6500,7 +6785,7 @@ class AI {
|
|
|
6500
6785
|
return true;
|
|
6501
6786
|
}
|
|
6502
6787
|
|
|
6503
|
-
if(
|
|
6788
|
+
if(!is.function(context.resume)){
|
|
6504
6789
|
this.#waitForSpeechGesture(null,context);
|
|
6505
6790
|
this.#traceSpeech('resumeAudio.result',{
|
|
6506
6791
|
callId,result:false,reason:'resume-unavailable'
|
|
@@ -6511,6 +6796,12 @@ class AI {
|
|
|
6511
6796
|
attempt=++this.speechResumeAttempt;
|
|
6512
6797
|
this.speechResumePending=true;
|
|
6513
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
|
+
}
|
|
6514
6805
|
this.#traceSpeech('resumeAudio.resumed',{
|
|
6515
6806
|
callId,state:context.state,audioTime:context.currentTime
|
|
6516
6807
|
});
|
|
@@ -6532,6 +6823,7 @@ class AI {
|
|
|
6532
6823
|
}
|
|
6533
6824
|
}catch(error){
|
|
6534
6825
|
this.#traceSpeech('resumeAudio.error',{callId,error,state:context?.state});
|
|
6826
|
+
if(preparedGroup?.stopped || preparedGroup?.paused) return false;
|
|
6535
6827
|
if(attempt&&attempt!==this.speechResumeAttempt){
|
|
6536
6828
|
return context?.state==='running';
|
|
6537
6829
|
}
|
|
@@ -6539,6 +6831,10 @@ class AI {
|
|
|
6539
6831
|
if(attempt===this.speechResumeAttempt){
|
|
6540
6832
|
this.speechResumePending=false;
|
|
6541
6833
|
}
|
|
6834
|
+
if(preparedGroup && error?.name !== 'NotAllowedError') {
|
|
6835
|
+
for(const job of preparedGroup.jobs) this.#failSpeechJob(job, error, 'playback-resume');
|
|
6836
|
+
return false;
|
|
6837
|
+
}
|
|
6542
6838
|
if(context?.state==='closed'){
|
|
6543
6839
|
this.#clearSpeechUnlock();
|
|
6544
6840
|
const jobs=this.speechJobs.filter(function usesClosedSpeechContext(job){
|
|
@@ -6599,7 +6895,8 @@ class AI {
|
|
|
6599
6895
|
this.speechJobs.push(job);
|
|
6600
6896
|
}
|
|
6601
6897
|
|
|
6602
|
-
if(this.muted||job.generation!==this.speechGeneration
|
|
6898
|
+
if(this.muted||job.generation!==this.speechGeneration
|
|
6899
|
+
||job.preparedPlayback?.stopped||['cancelled','failed'].includes(job.state)){
|
|
6603
6900
|
return this.#cancelSpeechJob(job);
|
|
6604
6901
|
}
|
|
6605
6902
|
|
|
@@ -6616,7 +6913,8 @@ class AI {
|
|
|
6616
6913
|
channels:audioBuffer.numberOfChannels,audioBuffer
|
|
6617
6914
|
});
|
|
6618
6915
|
|
|
6619
|
-
if(this.muted||job.generation!==this.speechGeneration
|
|
6916
|
+
if(this.muted||job.generation!==this.speechGeneration
|
|
6917
|
+
||job.preparedPlayback?.stopped||['cancelled','failed'].includes(job.state)){
|
|
6620
6918
|
return this.#cancelSpeechJob(job);
|
|
6621
6919
|
}
|
|
6622
6920
|
|
|
@@ -6694,6 +6992,8 @@ class AI {
|
|
|
6694
6992
|
|
|
6695
6993
|
const audioContext=job.sourceNode.context||job.audioContext;
|
|
6696
6994
|
|
|
6995
|
+
if(job.preparedPlayback?.paused) break;
|
|
6996
|
+
|
|
6697
6997
|
if(audioContext.state!=='running'){
|
|
6698
6998
|
this.#waitForSpeechGesture(null,audioContext);
|
|
6699
6999
|
|
|
@@ -6722,7 +7022,7 @@ class AI {
|
|
|
6722
7022
|
|
|
6723
7023
|
try{
|
|
6724
7024
|
const duration=Number(job.audioBuffer?.duration);
|
|
6725
|
-
const hasKnownDuration=
|
|
7025
|
+
const hasKnownDuration=is.finite(duration)&&duration>0;
|
|
6726
7026
|
const currentTime=Number(audioContext.currentTime)||0;
|
|
6727
7027
|
const previousContext=this.speechScheduleContext;
|
|
6728
7028
|
const remainingDelay=previousContext
|
|
@@ -6749,6 +7049,9 @@ class AI {
|
|
|
6749
7049
|
}
|
|
6750
7050
|
this.isSpeaking=true;
|
|
6751
7051
|
job.sourceNode.start(scheduledStart);
|
|
7052
|
+
if(job.preparedPlayback && !job.preparedPlayback.error) {
|
|
7053
|
+
job.preparedPlayback.publish('scheduled');
|
|
7054
|
+
}
|
|
6752
7055
|
this.#traceSpeechJob('playback.scheduled',job,{
|
|
6753
7056
|
audioEnd:hasKnownDuration?scheduledStart+duration:null,
|
|
6754
7057
|
previousScheduledEnd:this.speechScheduleTime,
|
|
@@ -6783,6 +7086,9 @@ class AI {
|
|
|
6783
7086
|
!this.muted
|
|
6784
7087
|
&&!this.speechAwaitingGesture
|
|
6785
7088
|
&&!this.speechResumePending
|
|
7089
|
+
&&!this.speechJobs.some(function hasPausedPreparedPlayback(job) {
|
|
7090
|
+
return job.preparedPlayback?.paused;
|
|
7091
|
+
})
|
|
6786
7092
|
&&!this.speechJobs.some(
|
|
6787
7093
|
function hasScheduledSpeechWithoutDuration(job){
|
|
6788
7094
|
return job.state==='scheduled'
|
|
@@ -6887,7 +7193,7 @@ class AI {
|
|
|
6887
7193
|
operationId,
|
|
6888
7194
|
publicDetail:completeValue({
|
|
6889
7195
|
boundary:normalizedBoundary,
|
|
6890
|
-
...(
|
|
7196
|
+
...(is.string(error?.code)?{code:error.code}:{}),
|
|
6891
7197
|
generation,
|
|
6892
7198
|
reason
|
|
6893
7199
|
})
|
|
@@ -6910,6 +7216,7 @@ class AI {
|
|
|
6910
7216
|
}
|
|
6911
7217
|
|
|
6912
7218
|
job.state='failed';
|
|
7219
|
+
job.preparedPlayback?.fail(error);
|
|
6913
7220
|
this.#traceSpeechJob('queue.failed',job,{boundary,error});
|
|
6914
7221
|
|
|
6915
7222
|
this.#publishTTSFailure(error,{
|
|
@@ -6970,7 +7277,15 @@ class AI {
|
|
|
6970
7277
|
const target=window;
|
|
6971
7278
|
|
|
6972
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
|
+
}
|
|
6973
7287
|
this.speechUnlockHandler=function unlockSpeechFromUserGesture(){
|
|
7288
|
+
if(preparedJob?.preparedPlayback.paused || preparedJob?.preparedPlayback.stopped) return false;
|
|
6974
7289
|
runtime.#clearSpeechUnlock();
|
|
6975
7290
|
return runtime.resumeAudio(audioContext);
|
|
6976
7291
|
};
|
|
@@ -7008,6 +7323,7 @@ class AI {
|
|
|
7008
7323
|
}
|
|
7009
7324
|
|
|
7010
7325
|
this.speechUnlockHandler=null;
|
|
7326
|
+
this.#speechUnlockContext=null;
|
|
7011
7327
|
this.speechAwaitingGesture=false;
|
|
7012
7328
|
}
|
|
7013
7329
|
}
|
|
@@ -7029,7 +7345,7 @@ function installAIUserReadyRegistration(){
|
|
|
7029
7345
|
?existingDescriptor.value
|
|
7030
7346
|
:null;
|
|
7031
7347
|
if(existing?.protocol!==AI_USER_READY_REGISTRATION_PROTOCOL
|
|
7032
|
-
|
|
7348
|
+
||!is.function(existing.dispose)){
|
|
7033
7349
|
throw aiInitializationError(
|
|
7034
7350
|
AI_INITIALIZATION_ERROR_CODES.userReadyRegistrationCollision,
|
|
7035
7351
|
AI_INITIALIZATION_REASONS.userReadyRegistrationCollision,
|