arcane-os 0.5.13 → 0.5.15

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +33 -8
  3. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +11 -10
  4. package/browser-runtime/ai/browser-wllama-runtime.mjs +2 -1
  5. package/browser-runtime/ai/model-controller.mjs +6 -5
  6. package/browser-runtime/ai/speech-worker-client.mjs +45 -3
  7. package/browser-runtime/event-manager.mjs +2 -1
  8. package/browser-runtime/logging.mjs +58 -0
  9. package/docs/architecture.md +1 -1
  10. package/docs/reference/README.md +9 -7
  11. package/docs/reference/ai/browser-speech.md +90 -1
  12. package/docs/reference/ai/twin-cloud.md +1 -1
  13. package/docs/reference/cli.md +1 -1
  14. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  15. package/docs/reference/inventory/package-api.json +34 -6
  16. package/docs/reference/inventory/runtime-components.json +1 -1
  17. package/docs/reference/inventory/runtime-modules.json +4 -4
  18. package/docs/reference/protocols.md +5 -5
  19. package/docs/reference/runtime-modules.md +70 -15
  20. package/docs/reference/sdk-api.md +120 -11
  21. package/package.json +4 -3
  22. package/runtime/arcane/components/app-bar.html +3 -1
  23. package/runtime/arcane/components/chat.html +22 -20
  24. package/runtime/arcane/components/dashboard-config.html +3 -1
  25. package/runtime/arcane/components/data-maintenance.html +4 -2
  26. package/runtime/arcane/components/data-view.html +3 -1
  27. package/runtime/arcane/components/directory-picker.html +3 -1
  28. package/runtime/arcane/components/file-manager.html +11 -9
  29. package/runtime/arcane/components/header.html +5 -3
  30. package/runtime/arcane/components/markdown-document.html +3 -1
  31. package/runtime/arcane/components/markdown-editor.html +4 -2
  32. package/runtime/arcane/components/modal.html +3 -1
  33. package/runtime/arcane/components/screen-capture.html +4 -2
  34. package/runtime/arcane/components/speech.html +10 -8
  35. package/runtime/arcane/components/table.html +3 -1
  36. package/runtime/arcane/components/voice-transcription.html +8 -6
  37. package/runtime/arcane/entities/Chat.js +5 -4
  38. package/runtime/arcane/entities/User.js +2 -1
  39. package/runtime/arcane/modules/AI.js +442 -292
  40. package/runtime/arcane/modules/AIProviderRuntime.js +359 -82
  41. package/runtime/arcane/modules/CommunicationAppController.js +2 -1
  42. package/runtime/arcane/modules/ComponentContracts.js +3 -2
  43. package/runtime/arcane/modules/ConversationTimebox.js +2 -1
  44. package/runtime/arcane/modules/DBOPFS.js +5 -4
  45. package/runtime/arcane/modules/Errors.js +3 -14
  46. package/runtime/arcane/modules/HTMLImport.js +4 -3
  47. package/runtime/arcane/modules/LocalAIReadinessController.js +2 -1
  48. package/runtime/arcane/modules/MD.js +3 -2
  49. package/runtime/arcane/modules/MailOutbox.mjs +2 -1
  50. package/runtime/arcane/modules/PersistentAIChatSession.js +2 -1
  51. package/runtime/arcane/modules/ScreenCapture.js +2 -1
  52. package/runtime/arcane/modules/SpeechPlayback.js +206 -40
  53. package/runtime/arcane/modules/ThemeBootstrap.js +3 -2
  54. package/runtime/arcane/modules/ToolCallRouter.js +2 -1
  55. package/src/import-map.mjs +65 -10
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import ArcaneCommunicationBridge from './ArcaneCommunicationBridge.js';
2
3
  import CommunicationHub from './CommunicationHub.js?v=2';
3
4
  import CommunicationPreferences from './CommunicationPreferences.js';
@@ -360,7 +361,7 @@ export default class CommunicationAppController{
360
361
  hub.dispose();
361
362
  }
362
363
  }catch(error){
363
- console.error('The communication hub could not be disposed cleanly.',error);
364
+ arcaneLogging.error('The communication hub could not be disposed cleanly.',error);
364
365
  }
365
366
  }
366
367
 
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {
2
3
  createArcaneEventSource,
3
4
  projectArcaneDOMEvent
@@ -713,7 +714,7 @@ function createSTTActivationController({
713
714
  try{
714
715
  onChange();
715
716
  }catch(error){
716
- console.error('Unable to render STT activation state:',error);
717
+ arcaneLogging.error('Unable to render STT activation state:',error);
717
718
  }
718
719
  }
719
720
  }
@@ -724,7 +725,7 @@ function createSTTActivationController({
724
725
  if(nextAction){
725
726
  void request(nextAction).catch(
726
727
  function reportSTTActivationRequestFailure(error){
727
- console.error('The STT activation request could not settle.',error);
728
+ arcaneLogging.error('The STT activation request could not settle.',error);
728
729
  }
729
730
  );
730
731
  }
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {createArcaneEventSource} from 'arcane-os/event-manager';
2
3
 
3
4
  const CONVERSATION_TIMEBOX_TOOL_NAME='conversation_timebox';
@@ -168,7 +169,7 @@ function conversationTimeboxSubmissionKey(context={}){
168
169
  }
169
170
 
170
171
  function reportConversationTimeboxListenerError(error){
171
- console.error('A conversation timebox listener failed.',error);
172
+ arcaneLogging.error('A conversation timebox listener failed.',error);
172
173
  }
173
174
 
174
175
  function conversationTimeboxError(message,code,reason,ErrorType=Error){
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import Is from "../../node_modules/strong-type/index.js";
2
3
  import {openApplicationDataDirectory} from './AppDataScope.js';
3
4
  import {
@@ -458,7 +459,7 @@ class DBOPFS {
458
459
  );
459
460
  }
460
461
  }catch(error){
461
- console.error(`Error writing file '${fileName}' to table '${tableName}':`,error)
462
+ arcaneLogging.error(`Error writing file '${fileName}' to table '${tableName}':`,error)
462
463
  throw error
463
464
  }
464
465
 
@@ -603,7 +604,7 @@ class DBOPFS {
603
604
  const fileName=entries[index][0]
604
605
 
605
606
  if(result.status!=='fulfilled'){
606
- console.error(`Failed to set file '${fileName}':`,result.reason)
607
+ arcaneLogging.error(`Failed to set file '${fileName}':`,result.reason)
607
608
  }
608
609
  }
609
610
  );
@@ -742,7 +743,7 @@ class DBOPFS {
742
743
  await table.removeEntry(fileName)
743
744
  }catch(error){
744
745
  if(error.name!=='NotFoundError'){
745
- console.error(error)
746
+ arcaneLogging.error(error)
746
747
  throw error
747
748
  }
748
749
  }
@@ -784,7 +785,7 @@ class DBOPFS {
784
785
  delete this.#tables[tableName]
785
786
  delete this.#tableHandles[tableName]
786
787
  }catch(error){
787
- console.error(error)
788
+ arcaneLogging.error(error)
788
789
  }
789
790
 
790
791
  return true
@@ -1,4 +1,5 @@
1
1
  import waitForComponent from './WaitForComponent.js';
2
+ import { arcaneLogging,readArcaneDeveloperMode } from 'arcane-os/logging';
2
3
  import {
3
4
  arcaneEvents,
4
5
  createArcaneEventSource
@@ -154,18 +155,6 @@ async function sendWithWindowMail(target,...args){
154
155
  return target.mail.send(...args);
155
156
  }
156
157
 
157
- function defaultDeveloperModeStatus(target){
158
- try{
159
- if(target?.user?.ready!==true){
160
- return null;
161
- }
162
-
163
- return target.user.developer===true;
164
- }catch{
165
- return false;
166
- }
167
- }
168
-
169
158
  function appendDeveloperDetail(document,list,label,value){
170
159
  if(value===null||value===undefined||value===''){
171
160
  return;
@@ -340,7 +329,7 @@ class Errors {
340
329
  this.now=options.now||Date.now;
341
330
  this.schedule=options.schedule||globalThis.setTimeout.bind(globalThis);
342
331
  this.cancel=options.cancel||globalThis.clearTimeout.bind(globalThis);
343
- this.logger=options.logger||globalThis.console;
332
+ this.logger=options.logger||arcaneLogging;
344
333
  this.storage=options.storage===undefined
345
334
  ? defaultStorage(target)
346
335
  : options.storage;
@@ -352,7 +341,7 @@ class Errors {
352
341
  || sendWithWindowMail.bind(null,target);
353
342
  this.isDeveloperMode=typeof options.isDeveloperMode==='function'
354
343
  ? options.isDeveloperMode
355
- : defaultDeveloperModeStatus.bind(null,target);
344
+ : readArcaneDeveloperMode.bind(null,target);
356
345
  this.presentDeveloperIncident=typeof options.presentDeveloperIncident==='function'
357
346
  ? options.presentDeveloperIncident
358
347
  : presentDeveloperIncidentModal.bind(null,target);
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {
2
3
  createArcaneEventSource,
3
4
  projectArcaneDOMEvent
@@ -299,7 +300,7 @@ class HTMLImport extends HTMLElement {
299
300
  let resolvedHref='';
300
301
  const operationId=`html-import-load-${this.#events.instanceId}-${generation}`;
301
302
  if(!href){
302
- console.log('no href provided for html-import tag',this);
303
+ arcaneLogging.log('no href provided for html-import tag',this);
303
304
  this.#loadController=null;
304
305
  return;
305
306
  }
@@ -319,7 +320,7 @@ class HTMLImport extends HTMLElement {
319
320
  await this.#destroyImportedHost();
320
321
  return;
321
322
  }
322
- console.error('Error loading HTML component:',err);
323
+ arcaneLogging.error('Error loading HTML component:',err);
323
324
  const detail={
324
325
  code:'HTML_IMPORT_FAILED',
325
326
  href,
@@ -369,7 +370,7 @@ class HTMLImport extends HTMLElement {
369
370
  }
370
371
 
371
372
  #reportImportedDestroyError(error,{events,href,operationId}){
372
- console.error('Error destroying imported HTML component:',error);
373
+ arcaneLogging.error('Error destroying imported HTML component:',error);
373
374
  if(!events||events.disposed)return;
374
375
  const detail={
375
376
  code:'HTML_IMPORT_IMPORTED_COMPONENT_DESTROY_REJECTED',
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {
2
3
  checkLocalAIReadiness,
3
4
  deriveLocalAIRequirements
@@ -287,7 +288,7 @@ export function createLocalAIReadinessController({
287
288
  function retry(){
288
289
  void check().catch(function ignoreDestroyedLocalAIRetry(error){
289
290
  if(error?.name!=='AbortError'){
290
- console.error('Local AI readiness retry failed.',error);
291
+ arcaneLogging.error('Local AI readiness retry failed.',error);
291
292
  }
292
293
  });
293
294
  }
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import { marked } from './Marked.min.js';
2
3
  import Is from '../../node_modules/strong-type/index.js';
3
4
 
@@ -45,7 +46,7 @@ class MD {
45
46
 
46
47
  set raw(value=''){
47
48
  if(!is.string(value)){
48
- console.trace('MD.raw must be a string.');
49
+ arcaneLogging.trace('MD.raw must be a string.');
49
50
  return this.#raw;
50
51
  }
51
52
  this.#raw = value;
@@ -55,7 +56,7 @@ class MD {
55
56
 
56
57
  append(value=''){
57
58
  if(!is.string(value)){
58
- console.trace('MD.append must be a string.');
59
+ arcaneLogging.trace('MD.append must be a string.');
59
60
  return this.#raw;
60
61
  }
61
62
  this.#raw += value;
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  export const MAIL_OUTBOX_PROTOCOL='arcane-mail-outbox/1';
2
3
  export const MAIL_OUTBOX_TABLE='mail_outbox';
3
4
  export const MAIL_OUTBOX_IDEMPOTENCY_WINDOW_MS=24*60*60*1000;
@@ -34,7 +35,7 @@ function reportMailOutboxObserverError(error){
34
35
  return;
35
36
  }
36
37
  }catch{}
37
- try{globalThis.console?.error?.(error);}catch{}
38
+ try{arcaneLogging.error?.(error);}catch{}
38
39
  }
39
40
 
40
41
  function fail(message,code='MAIL_OUTBOX_INVALID',ErrorType=Error,cause){
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import ChatEntity from '../entities/Chat.js';
2
3
  import ConfiguredAIChatSession,{
3
4
  normalizeStructuralToolCall
@@ -475,7 +476,7 @@ class PersistentAIChatSession{
475
476
  error instanceof Error?error:new Error('The saved chat history is invalid.'),
476
477
  'AI_CHAT_INCOHERENT_PERSISTENCE',
477
478
  );
478
- console.error('Arcane saved chat history is readable but not actionable.',error);
479
+ arcaneLogging.error('Arcane saved chat history is readable but not actionable.',error);
479
480
  }
480
481
  const storedSystem=storedMessages.find(message=>message.role==='system');
481
482
  const initialMessages=storedMessages
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {createArcaneEventSource} from 'arcane-os/event-manager';
2
3
  import GifEncoder from './GifEncoder.js';
3
4
 
@@ -252,7 +253,7 @@ function supportedRecorderType(Recorder){
252
253
 
253
254
  function reportDetachedError(error){
254
255
  if(typeof globalThis.reportError==='function')globalThis.reportError(error);
255
- else globalThis.console?.error?.(error);
256
+ else arcaneLogging.error?.(error);
256
257
  }
257
258
 
258
259
  function monotonicNow(){
@@ -289,6 +289,90 @@ function queueFor(speech){
289
289
  return queue;
290
290
  }
291
291
 
292
+ function providerSpeechCapacity(speech){
293
+ if(typeof speech?.fetchTTS!=='function')return null;
294
+ try{
295
+ const providerRuntime=speech.providerRuntime;
296
+ if(!providerRuntime||typeof providerRuntime.status!=='function')return null;
297
+ const capacity=providerRuntime.status(
298
+ 'tts',
299
+ {execution:true}
300
+ )?.execution?.maxConcurrentRequests;
301
+ return Number.isSafeInteger(capacity)&&capacity>0?capacity:null;
302
+ }catch{
303
+ // Clients without inspectable provider capacity retain serialized admission.
304
+ return null;
305
+ }
306
+ }
307
+
308
+ function startProviderSegment(playback,index,providerSynthesisBatch){
309
+ if(
310
+ providerSynthesisBatch!==playback.providerSynthesisBatch
311
+ ||index<0
312
+ ||index>=playback.parts.length
313
+ ||playback.urls[index]
314
+ )return null;
315
+ const existing=playback.segmentRequests.get(index);
316
+ if(existing?.providerSynthesisBatch===providerSynthesisBatch){
317
+ return existing;
318
+ }
319
+
320
+ playback.segmentErrors.delete(index);
321
+ const record={providerSynthesisBatch,index,promise:null};
322
+ record.promise=playback.synthesizeSegment(
323
+ index,
324
+ playback.generation,
325
+ index===0,
326
+ providerSynthesisBatch
327
+ ).then(
328
+ function storeProviderSynthesizedSegment(outcome){
329
+ if(outcome===SUPERSEDED)return {ready:false,superseded:true};
330
+ if(providerSynthesisBatch!==playback.providerSynthesisBatch){
331
+ playback.revokeObjectURL(outcome.url);
332
+ return {ready:false,superseded:true};
333
+ }
334
+ playback.urls[index]=outcome.url;
335
+ return {ready:true};
336
+ },
337
+ function handleProviderSegmentFailure(error){
338
+ if(providerSynthesisBatch===playback.providerSynthesisBatch){
339
+ playback.segmentErrors.set(
340
+ index,
341
+ {providerSynthesisBatch,index,error}
342
+ );
343
+ }
344
+ return {ready:false,error};
345
+ }
346
+ ).finally(function clearSettledProviderSegment(){
347
+ if(playback.segmentRequests.get(index)===record){
348
+ playback.segmentRequests.delete(index);
349
+ }
350
+ }).catch(function observeProviderSegmentHandlingFailure(error){
351
+ if(providerSynthesisBatch===playback.providerSynthesisBatch){
352
+ playback.segmentErrors.set(
353
+ index,
354
+ {providerSynthesisBatch,index,error}
355
+ );
356
+ }
357
+ return {ready:false,error};
358
+ });
359
+ playback.segmentRequests.set(index,record);
360
+ return record;
361
+ }
362
+
363
+ function startProviderSynthesis(playback,providerSynthesisBatch){
364
+ let first=null;
365
+ for(let index=0;index<playback.parts.length;index+=1){
366
+ const record=startProviderSegment(
367
+ playback,
368
+ index,
369
+ providerSynthesisBatch
370
+ );
371
+ if(index===0)first=record;
372
+ }
373
+ return first;
374
+ }
375
+
292
376
  const DEFAULT_MESSAGES={
293
377
  idle:'Speech is ready when visual content is available.',
294
378
  queued:'Waiting for the current local speech request. The latest narration will begin next.',
@@ -364,6 +448,9 @@ class SpeechPlayback{
364
448
  this.pendingGenerations=new Set();
365
449
  this.lookahead=null;
366
450
  this.lookaheadError=null;
451
+ this.providerSynthesisBatch=null;
452
+ this.segmentRequests=new Map();
453
+ this.segmentErrors=new Map();
367
454
  this.model=model===null||model===undefined?null:String(model);
368
455
  this.voice=voice===null||voice===undefined?null:String(voice);
369
456
  this.responseFormat=responseFormat===null||responseFormat===undefined
@@ -441,7 +528,12 @@ class SpeechPlayback{
441
528
  );
442
529
  }
443
530
 
444
- hasAudio(key=this.key){return Boolean(this.urls.some(Boolean)&&(!key||key===this.key));}
531
+ hasAudio(key=this.key){
532
+ const playable=this.providerSynthesisBatch
533
+ ?Boolean(this.urls[0])
534
+ :this.urls.some(Boolean);
535
+ return Boolean(playable&&(!key||key===this.key));
536
+ }
445
537
 
446
538
  releaseURLs(){
447
539
  for(const url of this.urls){
@@ -451,6 +543,9 @@ class SpeechPlayback{
451
543
  this.parts=[];
452
544
  this.lookahead=null;
453
545
  this.lookaheadError=null;
546
+ this.providerSynthesisBatch=null;
547
+ this.segmentRequests.clear();
548
+ this.segmentErrors.clear();
454
549
  }
455
550
 
456
551
  releaseUrls(){this.releaseURLs();}
@@ -496,39 +591,55 @@ class SpeechPlayback{
496
591
  throw error;
497
592
  }
498
593
 
499
- async synthesizeSegment(index,generation,announce=false){
594
+ async synthesizeSegment(
595
+ index,
596
+ generation,
597
+ announce=false,
598
+ providerSynthesisBatch=null
599
+ ){
500
600
  const playback=this;
501
601
  const controller=new AbortController();
502
602
  const token={generation,index,controller};
503
- const queue=queueFor(this.speech);
603
+ const queue=providerSynthesisBatch?null:queueFor(this.speech);
504
604
  this.pendingGenerations.add(token);
505
605
  this.abortControllers.add(controller);
606
+ function segmentRequestIsCurrent(){
607
+ return providerSynthesisBatch
608
+ ?providerSynthesisBatch===playback.providerSynthesisBatch
609
+ :generation===playback.generation;
610
+ }
611
+ async function synthesizeQueuedSegment(){
612
+ const part=playback.parts[index];
613
+ if(!segmentRequestIsCurrent()||!part){
614
+ controller.abort();
615
+ return SUPERSEDED;
616
+ }
617
+ if(announce){
618
+ playback.emit('synthesizing',playback.message('preparing',{count:playback.parts.length}));
619
+ }
620
+ const audio=await playback.requestSpeech(
621
+ part,
622
+ controller.signal
623
+ );
624
+ if(!segmentRequestIsCurrent()){
625
+ controller.abort();
626
+ return SUPERSEDED;
627
+ }
628
+ const url=playback.createObjectURL(audio);
629
+ if(!segmentRequestIsCurrent()){
630
+ playback.revokeObjectURL(url);
631
+ controller.abort();
632
+ return SUPERSEDED;
633
+ }
634
+ return {url};
635
+ }
506
636
  try{
507
- return await queue.enqueue(async function synthesizeQueuedSegment(){
508
- const part=playback.parts[index];
509
- if(generation!==playback.generation||!part){
510
- controller.abort();
511
- return SUPERSEDED;
512
- }
513
- if(announce){
514
- playback.emit('synthesizing',playback.message('preparing',{count:playback.parts.length}));
515
- }
516
- const audio=await playback.requestSpeech(
517
- part,
518
- controller.signal
637
+ return providerSynthesisBatch
638
+ ?await Promise.resolve().then(synthesizeQueuedSegment)
639
+ :await queue.enqueue(
640
+ synthesizeQueuedSegment,
641
+ {speculative:!announce}
519
642
  );
520
- if(generation!==playback.generation){
521
- controller.abort();
522
- return SUPERSEDED;
523
- }
524
- const url=playback.createObjectURL(audio);
525
- if(generation!==playback.generation){
526
- playback.revokeObjectURL(url);
527
- controller.abort();
528
- return SUPERSEDED;
529
- }
530
- return {url};
531
- },{speculative:!announce});
532
643
  }finally{
533
644
  this.pendingGenerations.delete(token);
534
645
  this.abortControllers.delete(controller);
@@ -576,6 +687,19 @@ class SpeechPlayback{
576
687
  async waitForSegment(index,generation){
577
688
  if(generation!==this.generation)return false;
578
689
  if(this.urls[index])return true;
690
+ if(this.providerSynthesisBatch){
691
+ const segmentError=this.segmentErrors.get(index);
692
+ if(segmentError?.providerSynthesisBatch===this.providerSynthesisBatch){
693
+ return false;
694
+ }
695
+ const record=this.segmentRequests.get(index);
696
+ if(!record)return Boolean(this.urls[index]);
697
+ const result=await record.promise;
698
+ return generation===this.generation
699
+ &&record.providerSynthesisBatch===this.providerSynthesisBatch
700
+ &&result.ready===true
701
+ &&Boolean(this.urls[index]);
702
+ }
579
703
  if(
580
704
  this.lookaheadError
581
705
  &&this.lookaheadError.generation===generation
@@ -666,13 +790,28 @@ class SpeechPlayback{
666
790
  throw error;
667
791
  }
668
792
 
669
- const queue=queueFor(this.speech);
670
- if(queue.busy)this.emit('synthesizing',this.message('queued',{count:normalized.length}));
671
793
  let outcome;
672
794
  try{
673
- outcome=await this.synthesizeSegment(0,generation,true);
795
+ const providerCapacity=providerSpeechCapacity(this.speech);
796
+ if(providerCapacity===null){
797
+ const queue=queueFor(this.speech);
798
+ if(queue.busy)this.emit('synthesizing',this.message('queued',{count:normalized.length}));
799
+ outcome=await this.synthesizeSegment(0,generation,true);
800
+ }else{
801
+ const providerSynthesisBatch={capacity:providerCapacity};
802
+ this.providerSynthesisBatch=providerSynthesisBatch;
803
+ const first=startProviderSynthesis(
804
+ this,
805
+ providerSynthesisBatch
806
+ );
807
+ const result=await first.promise;
808
+ if(Object.hasOwn(result,'error'))throw result.error;
809
+ outcome=result.ready?{url:this.urls[0]}:SUPERSEDED;
810
+ }
674
811
  }catch(error){
675
812
  if(generation!==this.generation)return {ready:false,played:false};
813
+ for(const controller of this.abortControllers)controller.abort();
814
+ this.abortControllers.clear();
676
815
  this.releaseURLs();
677
816
  this.fail(error);
678
817
  throw error;
@@ -693,7 +832,9 @@ class SpeechPlayback{
693
832
  this.loadCurrent();
694
833
  this.emit('ready',this.message('ready'));
695
834
  const played=autoplay?await this.play():false;
696
- if(generation===this.generation)this.startLookahead(1,generation);
835
+ if(generation===this.generation&&!this.providerSynthesisBatch){
836
+ this.startLookahead(1,generation);
837
+ }
697
838
  return {ready:true,played};
698
839
  }
699
840
 
@@ -723,14 +864,34 @@ class SpeechPlayback{
723
864
  if(!this.hasAudio())return false;
724
865
  this.generation+=1;
725
866
  const generation=this.generation;
726
- this.lookahead=null;
727
- this.lookaheadError=null;
867
+ if(this.providerSynthesisBatch){
868
+ const providerSynthesisBatch=this.providerSynthesisBatch;
869
+ for(const [index,segmentError] of this.segmentErrors){
870
+ if(
871
+ segmentError?.providerSynthesisBatch
872
+ ===providerSynthesisBatch
873
+ &&!this.urls[index]
874
+ &&!this.segmentRequests.has(index)
875
+ ){
876
+ startProviderSegment(
877
+ this,
878
+ index,
879
+ providerSynthesisBatch
880
+ );
881
+ }
882
+ }
883
+ }else{
884
+ this.lookahead=null;
885
+ this.lookaheadError=null;
886
+ }
728
887
  this.audio.pause();
729
888
  this.index=0;
730
889
  this.loadCurrent();
731
890
  this.audio.currentTime=0;
732
891
  const played=await this.play();
733
- if(generation===this.generation)this.startLookahead(1,generation);
892
+ if(generation===this.generation&&!this.providerSynthesisBatch){
893
+ this.startLookahead(1,generation);
894
+ }
734
895
  return played;
735
896
  }
736
897
 
@@ -772,12 +933,15 @@ class SpeechPlayback{
772
933
  const ready=await this.waitForSegment(nextIndex,generation);
773
934
  if(!ready||generation!==this.generation){
774
935
  if(generation===this.generation&&this.state==='buffering'){
775
- const bufferedError=(
776
- this.lookaheadError
936
+ const providerError=this.segmentErrors.get(nextIndex);
937
+ const providerFailed=this.providerSynthesisBatch
938
+ &&providerError?.providerSynthesisBatch
939
+ ===this.providerSynthesisBatch;
940
+ const lookaheadFailed=this.lookaheadError
777
941
  &&this.lookaheadError.generation===generation
778
- &&this.lookaheadError.index===nextIndex
779
- )?this.lookaheadError.error:null;
780
- if(bufferedError)this.fail(bufferedError);
942
+ &&this.lookaheadError.index===nextIndex;
943
+ if(providerFailed)this.fail(providerError.error);
944
+ else if(lookaheadFailed)this.fail(this.lookaheadError.error);
781
945
  else this.emit('ready',this.message('ready'));
782
946
  }
783
947
  return false;
@@ -786,7 +950,9 @@ class SpeechPlayback{
786
950
  this.index=nextIndex;
787
951
  this.loadCurrent();
788
952
  const played=await this.play();
789
- if(generation===this.generation)this.startLookahead(nextIndex+1,generation);
953
+ if(generation===this.generation&&!this.providerSynthesisBatch){
954
+ this.startLookahead(nextIndex+1,generation);
955
+ }
790
956
  return played;
791
957
  }
792
958
 
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {loadAndApplyTheme} from './ThemeManager.js';
2
3
  import {createArcaneEventSource} from 'arcane-os/event-manager';
3
4
 
@@ -43,7 +44,7 @@ function installAppearanceListener(ready){
43
44
  Promise.resolve(ready).then(function reloadTheme(result){
44
45
  return result?.manager?.load?.();
45
46
  }).catch(function reportThemeReloadFailure(error){
46
- console.warn(
47
+ arcaneLogging.warn(
47
48
  '[Arcane theme] Unable to apply the changed host appearance.',
48
49
  error
49
50
  );
@@ -80,7 +81,7 @@ export function bootstrapArcaneTheme(options={}){
80
81
  }
81
82
 
82
83
  const ready=loadAndApplyTheme(options).catch(error=>{
83
- console.warn('[Arcane theme] Unable to load the saved appearance; using the system theme.',error);
84
+ arcaneLogging.warn('[Arcane theme] Unable to load the saved appearance; using the system theme.',error);
84
85
  return {manager:null,state:null,error};
85
86
  });
86
87
 
@@ -1,10 +1,11 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  function parseArguments(value,name=''){
2
3
  let parsed=value;
3
4
  if(typeof parsed==='string'){
4
5
  try{
5
6
  parsed=JSON.parse(parsed);
6
7
  }catch(error){
7
- console.warn(error);
8
+ arcaneLogging.warn(error);
8
9
  throw new Error(`Tool ${name} returned invalid JSON arguments.`);
9
10
  }
10
11
  }