arcane-os 0.2.2 → 0.3.0
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 +29 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +27 -27
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +6 -3
- package/docs/reference/behavioral-testing.md +29 -6
- package/docs/reference/cli.md +117 -9
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +108 -44
- package/docs/reference/inventory/runtime-modules.json +131 -53
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +157 -43
- package/docs/reference/runtime-components.md +258 -83
- package/docs/reference/runtime-modules.md +613 -77
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +462 -384
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +639 -98
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +470 -0
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {createArcaneEventSource} from 'arcane-os/event-manager';
|
|
2
|
+
|
|
1
3
|
const MAX_SPEECH_INPUT=3900;
|
|
2
4
|
const MAX_SPEECH_CHUNKS=32;
|
|
3
5
|
const MAX_SPEECH_CHARACTERS=MAX_SPEECH_INPUT*MAX_SPEECH_CHUNKS;
|
|
@@ -15,6 +17,20 @@ const SPEECH_VOICE_OPTIONS=Object.freeze([
|
|
|
15
17
|
Object.freeze({value:'shimmer',label:'Shimmer'})
|
|
16
18
|
]);
|
|
17
19
|
const SUPERSEDED=Object.freeze({superseded:true});
|
|
20
|
+
const SPEECH_PLAYBACK_STATE_EVENT='speech-playback-state';
|
|
21
|
+
const SPEECH_PLAYBACK_FAILURE_REASONS=Object.freeze({
|
|
22
|
+
ARCANE_AI_OPERATION_SUPERSEDED:'speech-synthesis-superseded',
|
|
23
|
+
ARCANE_AI_REQUEST_ABORTED:'speech-synthesis-cancelled',
|
|
24
|
+
ARCANE_SPEECH_PLAYBACK_AUDIO_PLAYBACK_REJECTED:'audio-playback-rejected',
|
|
25
|
+
ARCANE_SPEECH_PLAYBACK_SYNTHESIZED_AUDIO_CONTRACT_MISMATCH:'synthesized-audio-contract-mismatch',
|
|
26
|
+
ARCANE_SPEECH_PLAYBACK_SYNTHESIZER_UNAVAILABLE:'speech-synthesizer-unavailable'
|
|
27
|
+
});
|
|
28
|
+
const WAV_AUDIO_CONTENT_TYPES=Object.freeze(new Set([
|
|
29
|
+
'audio/vnd.wave',
|
|
30
|
+
'audio/wav',
|
|
31
|
+
'audio/wave',
|
|
32
|
+
'audio/x-wav'
|
|
33
|
+
]));
|
|
18
34
|
const queuesBySpeechClient=new WeakMap();
|
|
19
35
|
|
|
20
36
|
class ReadonlyAliasSet{
|
|
@@ -103,7 +119,7 @@ function splitSpeechText(
|
|
|
103
119
|
return chunks;
|
|
104
120
|
}
|
|
105
121
|
|
|
106
|
-
function normalizeParts(parts,defaultVoice=
|
|
122
|
+
function normalizeParts(parts,defaultVoice=null,defaultSpeed=1){
|
|
107
123
|
if(!Array.isArray(parts)||!parts.length){
|
|
108
124
|
throw new TypeError('Narration text cannot be blank. The full visual content remains available.');
|
|
109
125
|
}
|
|
@@ -121,9 +137,14 @@ function normalizeParts(parts,defaultVoice='alloy',defaultSpeed=1){
|
|
|
121
137
|
if(!Number.isFinite(pauseAfterMs)||pauseAfterMs<0||pauseAfterMs>60_000){
|
|
122
138
|
throw new RangeError('Speech segment pauses must be between 0 and 60,000 milliseconds.');
|
|
123
139
|
}
|
|
124
|
-
const
|
|
140
|
+
const voiceValue=candidate?.voice??defaultVoice;
|
|
141
|
+
const voice=voiceValue===null||voiceValue===undefined
|
|
142
|
+
?null
|
|
143
|
+
:String(voiceValue).trim();
|
|
125
144
|
const speed=Number(candidate?.speed??defaultSpeed);
|
|
126
|
-
if(
|
|
145
|
+
if(voiceValue!==null&&voiceValue!==undefined&&!voice){
|
|
146
|
+
throw new TypeError('Speech segment voice cannot be blank.');
|
|
147
|
+
}
|
|
127
148
|
if(!Number.isFinite(speed)||speed<=0)throw new RangeError('Every speech segment requires a positive speech speed.');
|
|
128
149
|
return Object.freeze({
|
|
129
150
|
input,
|
|
@@ -142,6 +163,179 @@ function base64Bytes(value=''){
|
|
|
142
163
|
return bytes;
|
|
143
164
|
}
|
|
144
165
|
|
|
166
|
+
function synthesizedAudioContractError(cause=null){
|
|
167
|
+
const error=new TypeError('Arcane returned invalid synthesized audio.');
|
|
168
|
+
error.code='ARCANE_SPEECH_PLAYBACK_SYNTHESIZED_AUDIO_CONTRACT_MISMATCH';
|
|
169
|
+
if(cause!==null)error.cause=cause;
|
|
170
|
+
return error;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function normalizeAudioContentType(value,fallback=null){
|
|
174
|
+
const candidate=value===undefined||value===null?fallback:value;
|
|
175
|
+
if(typeof candidate!=='string'||!candidate.trim()){
|
|
176
|
+
throw synthesizedAudioContractError();
|
|
177
|
+
}
|
|
178
|
+
const contentType=candidate.split(';',1)[0].trim().toLowerCase();
|
|
179
|
+
if(!/^audio\/[a-z0-9][a-z0-9!#$&^_.+-]*$/u.test(contentType)){
|
|
180
|
+
throw synthesizedAudioContractError();
|
|
181
|
+
}
|
|
182
|
+
return contentType;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function audioContentTypesAgree(first,second){
|
|
186
|
+
return first===second
|
|
187
|
+
||(WAV_AUDIO_CONTENT_TYPES.has(first)&&WAV_AUDIO_CONTENT_TYPES.has(second));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function speechAudioBytes(value){
|
|
191
|
+
if(value instanceof ArrayBuffer){
|
|
192
|
+
return new Uint8Array(value.slice(0));
|
|
193
|
+
}
|
|
194
|
+
if(ArrayBuffer.isView(value)){
|
|
195
|
+
return new Uint8Array(
|
|
196
|
+
value.buffer.slice(
|
|
197
|
+
value.byteOffset,
|
|
198
|
+
value.byteOffset+value.byteLength
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
throw synthesizedAudioContractError();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function bytesMatchASCII(bytes,offset,value){
|
|
206
|
+
if(offset+value.length>bytes.byteLength)return false;
|
|
207
|
+
for(let index=0;index<value.length;index+=1){
|
|
208
|
+
if(bytes[offset+index]!==value.charCodeAt(index))return false;
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function assertPlayableWav(bytes){
|
|
214
|
+
if(bytes.byteLength<44
|
|
215
|
+
||!bytesMatchASCII(bytes,0,'RIFF')
|
|
216
|
+
||!bytesMatchASCII(bytes,8,'WAVE')){
|
|
217
|
+
throw synthesizedAudioContractError();
|
|
218
|
+
}
|
|
219
|
+
const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);
|
|
220
|
+
if(view.getUint32(4,true)+8!==bytes.byteLength){
|
|
221
|
+
throw synthesizedAudioContractError();
|
|
222
|
+
}
|
|
223
|
+
let offset=12;
|
|
224
|
+
let formatFound=false;
|
|
225
|
+
let audioFound=false;
|
|
226
|
+
while(offset+8<=bytes.byteLength){
|
|
227
|
+
const chunkBytes=view.getUint32(offset+4,true);
|
|
228
|
+
const chunkStart=offset+8;
|
|
229
|
+
const chunkEnd=chunkStart+chunkBytes;
|
|
230
|
+
if(chunkEnd>bytes.byteLength){
|
|
231
|
+
throw synthesizedAudioContractError();
|
|
232
|
+
}
|
|
233
|
+
if(bytesMatchASCII(bytes,offset,'fmt ')){
|
|
234
|
+
if(chunkBytes<16
|
|
235
|
+
||view.getUint16(chunkStart,true)===0
|
|
236
|
+
||view.getUint16(chunkStart+2,true)===0
|
|
237
|
+
||view.getUint32(chunkStart+4,true)===0
|
|
238
|
+
||view.getUint32(chunkStart+8,true)===0
|
|
239
|
+
||view.getUint16(chunkStart+12,true)===0
|
|
240
|
+
||view.getUint16(chunkStart+14,true)===0){
|
|
241
|
+
throw synthesizedAudioContractError();
|
|
242
|
+
}
|
|
243
|
+
formatFound=true;
|
|
244
|
+
}else if(bytesMatchASCII(bytes,offset,'data')){
|
|
245
|
+
if(chunkBytes===0)throw synthesizedAudioContractError();
|
|
246
|
+
audioFound=true;
|
|
247
|
+
}
|
|
248
|
+
offset=chunkEnd+(chunkBytes%2);
|
|
249
|
+
}
|
|
250
|
+
if(offset!==bytes.byteLength||!formatFound||!audioFound){
|
|
251
|
+
throw synthesizedAudioContractError();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function validatedSpeechBlob(blob,declaredContentType=null){
|
|
256
|
+
if(!(blob instanceof Blob)||blob.size===0){
|
|
257
|
+
throw synthesizedAudioContractError();
|
|
258
|
+
}
|
|
259
|
+
const blobContentType=blob.type
|
|
260
|
+
?normalizeAudioContentType(blob.type)
|
|
261
|
+
:null;
|
|
262
|
+
const declared=declaredContentType===undefined||declaredContentType===null
|
|
263
|
+
?null
|
|
264
|
+
:normalizeAudioContentType(declaredContentType);
|
|
265
|
+
if(blobContentType&&declared
|
|
266
|
+
&&!audioContentTypesAgree(blobContentType,declared)){
|
|
267
|
+
throw synthesizedAudioContractError();
|
|
268
|
+
}
|
|
269
|
+
const contentType=declared||blobContentType;
|
|
270
|
+
if(!contentType)throw synthesizedAudioContractError();
|
|
271
|
+
if(WAV_AUDIO_CONTENT_TYPES.has(contentType)){
|
|
272
|
+
assertPlayableWav(new Uint8Array(await blob.arrayBuffer()));
|
|
273
|
+
}
|
|
274
|
+
return blobContentType===contentType
|
|
275
|
+
?blob
|
|
276
|
+
:new Blob([blob],{type:contentType});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function validatedSpeechBytes(bytes,contentType='audio/wav'){
|
|
280
|
+
const normalizedContentType=normalizeAudioContentType(
|
|
281
|
+
contentType,
|
|
282
|
+
'audio/wav'
|
|
283
|
+
);
|
|
284
|
+
if(bytes.byteLength===0)throw synthesizedAudioContractError();
|
|
285
|
+
if(WAV_AUDIO_CONTENT_TYPES.has(normalizedContentType)){
|
|
286
|
+
assertPlayableWav(bytes);
|
|
287
|
+
}
|
|
288
|
+
return new Blob([bytes],{type:normalizedContentType});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function playableSpeechBlob(response){
|
|
292
|
+
try{
|
|
293
|
+
if(response instanceof Blob)return validatedSpeechBlob(response);
|
|
294
|
+
if(response instanceof ArrayBuffer||ArrayBuffer.isView(response)){
|
|
295
|
+
return validatedSpeechBytes(speechAudioBytes(response));
|
|
296
|
+
}
|
|
297
|
+
if(response&&typeof response==='object'){
|
|
298
|
+
if(response.audio instanceof Blob){
|
|
299
|
+
return validatedSpeechBlob(
|
|
300
|
+
response.audio,
|
|
301
|
+
response.contentType
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if(response.audio instanceof ArrayBuffer||ArrayBuffer.isView(response.audio)){
|
|
305
|
+
return validatedSpeechBytes(
|
|
306
|
+
speechAudioBytes(response.audio),
|
|
307
|
+
response.contentType
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if(typeof response.audioBase64==='string'&&response.audioBase64){
|
|
311
|
+
return validatedSpeechBytes(
|
|
312
|
+
base64Bytes(response.audioBase64),
|
|
313
|
+
response.contentType
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}catch(error){
|
|
318
|
+
if(error?.code
|
|
319
|
+
==='ARCANE_SPEECH_PLAYBACK_SYNTHESIZED_AUDIO_CONTRACT_MISMATCH'){
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
throw synthesizedAudioContractError(error);
|
|
323
|
+
}
|
|
324
|
+
throw synthesizedAudioContractError();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function speechPlaybackFailureReason(error){
|
|
328
|
+
if(error?.name==='AbortError')return 'speech-synthesis-cancelled';
|
|
329
|
+
if(typeof error?.code==='string'
|
|
330
|
+
&&Object.hasOwn(SPEECH_PLAYBACK_FAILURE_REASONS,error.code)){
|
|
331
|
+
return SPEECH_PLAYBACK_FAILURE_REASONS[error.code];
|
|
332
|
+
}
|
|
333
|
+
if(error instanceof TypeError||error instanceof RangeError){
|
|
334
|
+
return 'speech-playback-request-contract-mismatch';
|
|
335
|
+
}
|
|
336
|
+
return 'speech-synthesis-rejected';
|
|
337
|
+
}
|
|
338
|
+
|
|
145
339
|
class LatestSpeechQueue{
|
|
146
340
|
constructor(){
|
|
147
341
|
this.active=null;
|
|
@@ -224,6 +418,10 @@ class SpeechPlayback{
|
|
|
224
418
|
constructor({
|
|
225
419
|
audio,
|
|
226
420
|
speech=globalThis.Arcane?.speech,
|
|
421
|
+
model=null,
|
|
422
|
+
voice=null,
|
|
423
|
+
responseFormat=null,
|
|
424
|
+
speed=1,
|
|
227
425
|
onState=function noop(){},
|
|
228
426
|
createObjectURL,
|
|
229
427
|
revokeObjectURL,
|
|
@@ -233,13 +431,37 @@ class SpeechPlayback{
|
|
|
233
431
|
if(!audio||typeof audio.addEventListener!=='function'){
|
|
234
432
|
throw new TypeError('SpeechPlayback requires an audio element.');
|
|
235
433
|
}
|
|
434
|
+
const normalizedSpeed=Number(speed);
|
|
435
|
+
if(!Number.isFinite(normalizedSpeed)||normalizedSpeed<=0){
|
|
436
|
+
throw new RangeError('SpeechPlayback speed must be a positive number.');
|
|
437
|
+
}
|
|
236
438
|
this.audio=audio;
|
|
237
439
|
this.speech=speech;
|
|
440
|
+
this.events=createArcaneEventSource(
|
|
441
|
+
this,
|
|
442
|
+
{
|
|
443
|
+
source:'speech-playback',
|
|
444
|
+
eventTypes:Object.freeze([SPEECH_PLAYBACK_STATE_EVENT])
|
|
445
|
+
}
|
|
446
|
+
);
|
|
238
447
|
this.onState=onState;
|
|
239
448
|
this.createObjectURL=createObjectURL||function createAudioURL(blob){return URL.createObjectURL(blob);};
|
|
240
449
|
this.revokeObjectURL=revokeObjectURL||function revokeAudioURL(url){URL.revokeObjectURL(url);};
|
|
241
|
-
this.delay=delay||function playbackDelay(duration){
|
|
242
|
-
return new Promise(function wait(resolve){
|
|
450
|
+
this.delay=delay||function playbackDelay(duration,signal){
|
|
451
|
+
return new Promise(function wait(resolve){
|
|
452
|
+
let timer=null;
|
|
453
|
+
const finish=()=>{
|
|
454
|
+
globalThis.clearTimeout(timer);
|
|
455
|
+
signal?.removeEventListener('abort',finish);
|
|
456
|
+
resolve();
|
|
457
|
+
};
|
|
458
|
+
signal?.addEventListener('abort',finish,{once:true});
|
|
459
|
+
if(signal?.aborted){
|
|
460
|
+
finish();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
timer=globalThis.setTimeout(finish,duration);
|
|
464
|
+
});
|
|
243
465
|
};
|
|
244
466
|
this.messages=Object.freeze({...DEFAULT_MESSAGES,...messages});
|
|
245
467
|
this.urls=[];
|
|
@@ -249,10 +471,18 @@ class SpeechPlayback{
|
|
|
249
471
|
this.pendingGenerations=new Set();
|
|
250
472
|
this.lookahead=null;
|
|
251
473
|
this.lookaheadError=null;
|
|
252
|
-
this.
|
|
253
|
-
this.
|
|
474
|
+
this.model=model===null||model===undefined?null:String(model).trim()||null;
|
|
475
|
+
this.voice=voice===null||voice===undefined?null:String(voice).trim()||null;
|
|
476
|
+
this.responseFormat=responseFormat===null||responseFormat===undefined
|
|
477
|
+
?null
|
|
478
|
+
:String(responseFormat).trim()||null;
|
|
479
|
+
this.speed=normalizedSpeed;
|
|
254
480
|
this.key='';
|
|
255
481
|
this.state='idle';
|
|
482
|
+
this.operationSequence=0;
|
|
483
|
+
this.operationId=null;
|
|
484
|
+
this.abortControllers=new Set();
|
|
485
|
+
this.destroyed=false;
|
|
256
486
|
this.boundEnded=this.handleEnded.bind(this);
|
|
257
487
|
this.boundPlay=this.handlePlay.bind(this);
|
|
258
488
|
this.boundPause=this.handlePause.bind(this);
|
|
@@ -270,9 +500,19 @@ class SpeechPlayback{
|
|
|
270
500
|
return String(typeof value==='function'?value(details):value||'');
|
|
271
501
|
}
|
|
272
502
|
|
|
273
|
-
|
|
503
|
+
nextOperationId(){
|
|
504
|
+
if(this.operationSequence===Number.MAX_SAFE_INTEGER){
|
|
505
|
+
const error=new RangeError('SpeechPlayback operation sequence is exhausted.');
|
|
506
|
+
error.code='ARCANE_SPEECH_PLAYBACK_OPERATION_SEQUENCE_EXHAUSTED';
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
this.operationSequence+=1;
|
|
510
|
+
return `${this.events.descriptor.instanceId}:playback:${this.operationSequence.toString(36)}`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
emit(state,message,key=this.key,{code=null,reason=null}={}){
|
|
274
514
|
this.state=state;
|
|
275
|
-
|
|
515
|
+
const detail=Object.freeze({
|
|
276
516
|
state,
|
|
277
517
|
message,
|
|
278
518
|
key,
|
|
@@ -280,11 +520,49 @@ class SpeechPlayback{
|
|
|
280
520
|
total:this.parts.length||this.urls.filter(Boolean).length,
|
|
281
521
|
producing:this.synthesisInFlight,
|
|
282
522
|
buffered:this.urls.filter(Boolean).length,
|
|
283
|
-
hasAudio:this.hasAudio()
|
|
523
|
+
hasAudio:this.hasAudio(),
|
|
524
|
+
operationId:this.operationId,
|
|
525
|
+
code,
|
|
526
|
+
reason
|
|
284
527
|
});
|
|
528
|
+
if(!this.destroyed){
|
|
529
|
+
this.events.dispatch(
|
|
530
|
+
SPEECH_PLAYBACK_STATE_EVENT,
|
|
531
|
+
detail,
|
|
532
|
+
{
|
|
533
|
+
operationId:this.operationId,
|
|
534
|
+
publicDetail:{
|
|
535
|
+
state,
|
|
536
|
+
key,
|
|
537
|
+
index:detail.index,
|
|
538
|
+
total:detail.total,
|
|
539
|
+
producing:detail.producing,
|
|
540
|
+
buffered:detail.buffered,
|
|
541
|
+
hasAudio:detail.hasAudio,
|
|
542
|
+
code,
|
|
543
|
+
reason
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
try{
|
|
549
|
+
this.onState(detail);
|
|
550
|
+
}catch(error){
|
|
551
|
+
if(typeof globalThis.reportError==='function')globalThis.reportError(error);
|
|
552
|
+
else console.error(error);
|
|
553
|
+
}
|
|
554
|
+
return detail;
|
|
285
555
|
}
|
|
286
556
|
|
|
287
|
-
available(){
|
|
557
|
+
available(){
|
|
558
|
+
return Boolean(
|
|
559
|
+
this.speech
|
|
560
|
+
&&(
|
|
561
|
+
typeof this.speech.fetchTTS==='function'
|
|
562
|
+
||typeof this.speech.synthesize==='function'
|
|
563
|
+
)
|
|
564
|
+
);
|
|
565
|
+
}
|
|
288
566
|
|
|
289
567
|
hasAudio(key=this.key){return Boolean(this.urls.some(Boolean)&&(!key||key===this.key));}
|
|
290
568
|
|
|
@@ -300,9 +578,14 @@ class SpeechPlayback{
|
|
|
300
578
|
|
|
301
579
|
releaseUrls(){this.releaseURLs();}
|
|
302
580
|
|
|
303
|
-
cancel(
|
|
581
|
+
cancel(
|
|
582
|
+
message=this.message('idle'),
|
|
583
|
+
reason='speech-playback-cancelled'
|
|
584
|
+
){
|
|
304
585
|
const cancelledKey=this.key;
|
|
305
586
|
this.generation+=1;
|
|
587
|
+
for(const controller of this.abortControllers)controller.abort();
|
|
588
|
+
this.abortControllers.clear();
|
|
306
589
|
this.audio.pause();
|
|
307
590
|
this.audio.removeAttribute?.('src');
|
|
308
591
|
this.audio.load?.();
|
|
@@ -310,44 +593,68 @@ class SpeechPlayback{
|
|
|
310
593
|
this.releaseURLs();
|
|
311
594
|
this.index=0;
|
|
312
595
|
this.key='';
|
|
313
|
-
this.emit('idle',message,cancelledKey);
|
|
596
|
+
this.emit('idle',message,cancelledKey,{reason});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async requestSpeech(part,signal){
|
|
600
|
+
const payload={
|
|
601
|
+
input:part.input,
|
|
602
|
+
speed:part.speed,
|
|
603
|
+
...(this.model?{model:this.model}:{}),
|
|
604
|
+
...(part.voice?{voice:part.voice}:{}),
|
|
605
|
+
...(this.responseFormat?{responseFormat:this.responseFormat}:{})
|
|
606
|
+
};
|
|
607
|
+
if(typeof this.speech?.fetchTTS==='function'){
|
|
608
|
+
return playableSpeechBlob(
|
|
609
|
+
await this.speech.fetchTTS(payload,signal)
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
if(typeof this.speech?.synthesize==='function'){
|
|
613
|
+
return playableSpeechBlob(
|
|
614
|
+
await this.speech.synthesize(payload,{signal})
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
const error=new Error(this.message('unavailable'));
|
|
618
|
+
error.code='ARCANE_SPEECH_PLAYBACK_SYNTHESIZER_UNAVAILABLE';
|
|
619
|
+
throw error;
|
|
314
620
|
}
|
|
315
621
|
|
|
316
622
|
async synthesizeSegment(index,generation,announce=false){
|
|
317
623
|
const playback=this;
|
|
318
|
-
const
|
|
624
|
+
const controller=new AbortController();
|
|
625
|
+
const token=Object.freeze({generation,index,controller});
|
|
319
626
|
const queue=queueFor(this.speech);
|
|
320
627
|
this.pendingGenerations.add(token);
|
|
628
|
+
this.abortControllers.add(controller);
|
|
321
629
|
try{
|
|
322
630
|
return await queue.enqueue(async function synthesizeQueuedSegment(){
|
|
323
631
|
const part=playback.parts[index];
|
|
324
|
-
if(generation!==playback.generation||!part)
|
|
632
|
+
if(generation!==playback.generation||!part){
|
|
633
|
+
controller.abort();
|
|
634
|
+
return SUPERSEDED;
|
|
635
|
+
}
|
|
325
636
|
if(announce){
|
|
326
637
|
playback.emit('synthesizing',playback.message('preparing',{count:playback.parts.length}));
|
|
327
638
|
}
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if(generation!==playback.generation)return SUPERSEDED;
|
|
336
|
-
if(!response||typeof response.audioBase64!=='string'||!response.audioBase64){
|
|
337
|
-
throw new TypeError(playback.message('invalidAudio'));
|
|
639
|
+
const audio=await playback.requestSpeech(
|
|
640
|
+
part,
|
|
641
|
+
controller.signal
|
|
642
|
+
);
|
|
643
|
+
if(generation!==playback.generation){
|
|
644
|
+
controller.abort();
|
|
645
|
+
return SUPERSEDED;
|
|
338
646
|
}
|
|
339
|
-
const
|
|
340
|
-
?response.contentType
|
|
341
|
-
:'audio/ogg';
|
|
342
|
-
const url=playback.createObjectURL(new Blob([base64Bytes(response.audioBase64)],{type:contentType}));
|
|
647
|
+
const url=playback.createObjectURL(audio);
|
|
343
648
|
if(generation!==playback.generation){
|
|
344
649
|
playback.revokeObjectURL(url);
|
|
650
|
+
controller.abort();
|
|
345
651
|
return SUPERSEDED;
|
|
346
652
|
}
|
|
347
653
|
return {url};
|
|
348
654
|
},{speculative:!announce});
|
|
349
655
|
}finally{
|
|
350
656
|
this.pendingGenerations.delete(token);
|
|
657
|
+
this.abortControllers.delete(controller);
|
|
351
658
|
}
|
|
352
659
|
}
|
|
353
660
|
|
|
@@ -407,20 +714,70 @@ class SpeechPlayback{
|
|
|
407
714
|
return generation===this.generation&&result.ready===true&&Boolean(this.urls[index]);
|
|
408
715
|
}
|
|
409
716
|
|
|
410
|
-
async
|
|
411
|
-
|
|
717
|
+
async waitForPause(duration,generation){
|
|
718
|
+
const controller=new AbortController();
|
|
719
|
+
this.abortControllers.add(controller);
|
|
720
|
+
try{
|
|
721
|
+
await this.delay(duration,controller.signal);
|
|
722
|
+
return !controller.signal.aborted&&generation===this.generation;
|
|
723
|
+
}catch(error){
|
|
724
|
+
if(controller.signal.aborted||generation!==this.generation)return false;
|
|
725
|
+
throw error;
|
|
726
|
+
}finally{
|
|
727
|
+
this.abortControllers.delete(controller);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async prepare({
|
|
732
|
+
key,
|
|
733
|
+
parts,
|
|
734
|
+
model=this.model,
|
|
735
|
+
voice=this.voice,
|
|
736
|
+
responseFormat=this.responseFormat,
|
|
737
|
+
speed=this.speed,
|
|
738
|
+
autoplay=true
|
|
739
|
+
}={}){
|
|
740
|
+
if(this.destroyed){
|
|
741
|
+
const error=new Error('SpeechPlayback is destroyed.');
|
|
742
|
+
error.code='ARCANE_SPEECH_PLAYBACK_DESTROYED';
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
745
|
+
this.cancel(this.message('idle'),'playback-replaced');
|
|
412
746
|
const generation=this.generation;
|
|
747
|
+
this.operationId=this.nextOperationId();
|
|
413
748
|
this.key=String(key||'narration');
|
|
414
749
|
const requestKey=this.key;
|
|
415
750
|
let normalized;
|
|
416
751
|
try{
|
|
417
|
-
if(!this.available())
|
|
752
|
+
if(!this.available()){
|
|
753
|
+
const error=new Error(this.message('unavailable'));
|
|
754
|
+
error.code='ARCANE_SPEECH_PLAYBACK_SYNTHESIZER_UNAVAILABLE';
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
418
757
|
const numericSpeed=Number(speed);
|
|
419
758
|
if(!Number.isFinite(numericSpeed)||numericSpeed<=0)throw new RangeError('Speech speed must be a positive number.');
|
|
420
|
-
const
|
|
421
|
-
|
|
759
|
+
const selectedModel=model===null||model===undefined
|
|
760
|
+
?null
|
|
761
|
+
:String(model).trim();
|
|
762
|
+
const selectedVoice=voice===null||voice===undefined
|
|
763
|
+
?null
|
|
764
|
+
:String(voice).trim();
|
|
765
|
+
const selectedResponseFormat=responseFormat===null||responseFormat===undefined
|
|
766
|
+
?null
|
|
767
|
+
:String(responseFormat).trim();
|
|
768
|
+
if(model!==null&&model!==undefined&&!selectedModel){
|
|
769
|
+
throw new TypeError('Speech model cannot be blank.');
|
|
770
|
+
}
|
|
771
|
+
if(voice!==null&&voice!==undefined&&!selectedVoice){
|
|
772
|
+
throw new TypeError('Speech voice cannot be blank.');
|
|
773
|
+
}
|
|
774
|
+
if(responseFormat!==null&&responseFormat!==undefined&&!selectedResponseFormat){
|
|
775
|
+
throw new TypeError('Speech response format cannot be blank.');
|
|
776
|
+
}
|
|
422
777
|
this.speed=numericSpeed;
|
|
778
|
+
this.model=selectedModel;
|
|
423
779
|
this.voice=selectedVoice;
|
|
780
|
+
this.responseFormat=selectedResponseFormat;
|
|
424
781
|
normalized=normalizeParts(
|
|
425
782
|
typeof parts==='function'?parts():parts,
|
|
426
783
|
selectedVoice,
|
|
@@ -474,8 +831,13 @@ class SpeechPlayback{
|
|
|
474
831
|
try{
|
|
475
832
|
await this.audio.play();
|
|
476
833
|
return true;
|
|
477
|
-
}catch{
|
|
478
|
-
this.emit(
|
|
834
|
+
}catch(error){
|
|
835
|
+
this.emit(
|
|
836
|
+
'ready',
|
|
837
|
+
this.message('autoplayBlocked'),
|
|
838
|
+
this.key,
|
|
839
|
+
{reason:'audio-autoplay-rejected'}
|
|
840
|
+
);
|
|
479
841
|
return false;
|
|
480
842
|
}
|
|
481
843
|
}
|
|
@@ -507,7 +869,10 @@ class SpeechPlayback{
|
|
|
507
869
|
stop(){
|
|
508
870
|
const hasAudio=this.hasAudio();
|
|
509
871
|
if(!this.synthesisInFlight&&!hasAudio)return;
|
|
510
|
-
this.cancel(
|
|
872
|
+
this.cancel(
|
|
873
|
+
this.message(hasAudio?'stopped':'preparationStopped'),
|
|
874
|
+
'playback-stopped'
|
|
875
|
+
);
|
|
511
876
|
}
|
|
512
877
|
|
|
513
878
|
async advance(){
|
|
@@ -520,8 +885,9 @@ class SpeechPlayback{
|
|
|
520
885
|
const pauseAfterMs=this.parts[this.index]?.pauseAfterMs||0;
|
|
521
886
|
if(pauseAfterMs>0){
|
|
522
887
|
this.emit('pausing',this.message('pausing',{duration:pauseAfterMs}));
|
|
523
|
-
await this.
|
|
524
|
-
|
|
888
|
+
if(!await this.waitForPause(pauseAfterMs,generation)||!this.hasAudio()){
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
525
891
|
}
|
|
526
892
|
const nextIndex=this.index+1;
|
|
527
893
|
if(!this.urls[nextIndex]){
|
|
@@ -563,9 +929,39 @@ class SpeechPlayback{
|
|
|
563
929
|
}
|
|
564
930
|
}
|
|
565
931
|
|
|
566
|
-
handleError(){
|
|
932
|
+
handleError(){
|
|
933
|
+
const error=new Error(this.message('playbackError'));
|
|
934
|
+
error.code='ARCANE_SPEECH_PLAYBACK_AUDIO_PLAYBACK_REJECTED';
|
|
935
|
+
this.fail(error);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
fail(error){
|
|
939
|
+
const reason=speechPlaybackFailureReason(error);
|
|
940
|
+
const code=typeof error?.code==='string'&&error.code.trim()
|
|
941
|
+
?error.code.trim()
|
|
942
|
+
:reason==='speech-playback-request-contract-mismatch'
|
|
943
|
+
?'ARCANE_SPEECH_PLAYBACK_REQUEST_CONTRACT_MISMATCH'
|
|
944
|
+
:'ARCANE_SPEECH_PLAYBACK_SYNTHESIS_REQUEST_REJECTED';
|
|
945
|
+
return this.emit(
|
|
946
|
+
'error',
|
|
947
|
+
String(error?.message||this.message('fallbackError')),
|
|
948
|
+
this.key,
|
|
949
|
+
{code,reason}
|
|
950
|
+
);
|
|
951
|
+
}
|
|
567
952
|
|
|
568
|
-
|
|
953
|
+
destroy(){
|
|
954
|
+
if(this.destroyed)return false;
|
|
955
|
+
this.cancel(this.message('idle'),'playback-destroyed');
|
|
956
|
+
this.audio.removeEventListener('ended',this.boundEnded);
|
|
957
|
+
this.audio.removeEventListener('play',this.boundPlay);
|
|
958
|
+
this.audio.removeEventListener('pause',this.boundPause);
|
|
959
|
+
this.audio.removeEventListener('error',this.boundError);
|
|
960
|
+
this.destroyed=true;
|
|
961
|
+
this.events.dispose();
|
|
962
|
+
this.onState=function destroyedSpeechPlaybackStateObserver(){};
|
|
963
|
+
return true;
|
|
964
|
+
}
|
|
569
965
|
}
|
|
570
966
|
|
|
571
967
|
export {
|
|
@@ -575,6 +971,7 @@ export {
|
|
|
575
971
|
PREFERRED_STREAM_SEGMENT,
|
|
576
972
|
SPEECH_VOICE_ALIASES,
|
|
577
973
|
SPEECH_VOICE_OPTIONS,
|
|
974
|
+
SPEECH_PLAYBACK_STATE_EVENT,
|
|
578
975
|
SpeechPlayback,
|
|
579
976
|
splitSpeechText
|
|
580
977
|
};
|