arcane-os 0.76.0 → 0.78.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 +36 -0
- package/README.md +1 -1
- package/browser-runtime/ai/dbopfs-model-resources.mjs +1 -1
- package/browser-runtime/ai/speech-worker-runtime.mjs +79 -3
- package/docs/reference/ai/browser-speech.md +144 -5
- package/docs/reference/inventory/package-api.json +1 -1
- package/docs/reference/inventory/runtime-modules.json +10 -8
- package/docs/reference/runtime-modules.md +114 -14
- package/package.json +2 -2
- package/runtime/arcane/modules/AI.js +346 -31
- package/runtime/arcane/modules/DBOPFS.js +24 -6
- package/runtime/arcane/modules/PreparedSpeech.js +111 -6
|
@@ -111,8 +111,10 @@ function serializeStorage(storage,execute){
|
|
|
111
111
|
return operation;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
async function readManifest(storage){
|
|
115
|
-
const stored=
|
|
114
|
+
async function readManifest(storage,cacheOnly=false){
|
|
115
|
+
const stored=cacheOnly
|
|
116
|
+
?await storage.db.get(storage.table,storage.fileName,true,{createTable:false})
|
|
117
|
+
:await storage.db.get(storage.table,storage.fileName,true);
|
|
116
118
|
if(stored===null||stored===undefined)return {version:1,entries:[]};
|
|
117
119
|
const manifest=copyJSON(stored);
|
|
118
120
|
if(manifest.version!==1||!is.array(manifest.entries)){
|
|
@@ -349,9 +351,10 @@ function finishPreparation(job,error){
|
|
|
349
351
|
job.finished.resolve();
|
|
350
352
|
}
|
|
351
353
|
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
+
function createJobState(input,storage,synthesize=null,previous=null){
|
|
355
|
+
return {
|
|
354
356
|
input,storage,synthesize,previous,
|
|
357
|
+
cacheOnly:false,
|
|
355
358
|
controller:new AbortController(),
|
|
356
359
|
interests:new Set(),
|
|
357
360
|
state:'queued',
|
|
@@ -367,6 +370,10 @@ function createJob(ownerState,input,storage,synthesize,previous){
|
|
|
367
370
|
return {...part,index,state:'queued',audioFile:null,contentType:null,error:null};
|
|
368
371
|
})
|
|
369
372
|
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function createJob(ownerState,input,storage,synthesize,previous){
|
|
376
|
+
const job=createJobState(input,storage,synthesize,previous);
|
|
370
377
|
const admission=ownerState.admission.then(function admitOrderedPreparation(){
|
|
371
378
|
return admitPreparation(job);
|
|
372
379
|
});
|
|
@@ -416,7 +423,9 @@ function createHandle(job,signal,onState){
|
|
|
416
423
|
if(!interest.active)throw abortError();
|
|
417
424
|
if(!job.storage)return job.slots[index].blob;
|
|
418
425
|
const audio=job.record.segments[index];
|
|
419
|
-
const file=
|
|
426
|
+
const file=job.cacheOnly
|
|
427
|
+
?await job.storage.db.readFile(job.storage.table,audio.audioFile,{createTable:false})
|
|
428
|
+
:await job.storage.db.readFile(job.storage.table,audio.audioFile);
|
|
420
429
|
if(!interest.active)throw abortError();
|
|
421
430
|
return file.type===audio.contentType?file:new Blob([file],{type:audio.contentType});
|
|
422
431
|
},
|
|
@@ -482,4 +491,100 @@ function prepareSpeech({
|
|
|
482
491
|
return createHandle(job,signal,onState);
|
|
483
492
|
}
|
|
484
493
|
|
|
485
|
-
|
|
494
|
+
/** Read complete existing audio without synthesis, storage writes, or repair. */
|
|
495
|
+
async function readPreparedSpeech({
|
|
496
|
+
owner,
|
|
497
|
+
parts,
|
|
498
|
+
originalParts=parts,
|
|
499
|
+
selection=null,
|
|
500
|
+
segmentation=null,
|
|
501
|
+
storage=null,
|
|
502
|
+
identity=null,
|
|
503
|
+
signal=null,
|
|
504
|
+
onState=null
|
|
505
|
+
}={}){
|
|
506
|
+
if((!owner||!is.object(owner))&&!is.function(owner)){
|
|
507
|
+
throw new TypeError('Speech preparation requires its owning AI instance.');
|
|
508
|
+
}
|
|
509
|
+
if(!is.array(parts)||(onState!==null&&!is.function(onState))){
|
|
510
|
+
throw new TypeError('Cached speech requires ordered parts and an optional state callback.');
|
|
511
|
+
}
|
|
512
|
+
const input=copyJSON({parts,originalParts,selection,segmentation,identity});
|
|
513
|
+
const destination=normalizeStorage(storage);
|
|
514
|
+
if(signal?.aborted)throw abortError();
|
|
515
|
+
function completeCachedRead(job){
|
|
516
|
+
// The lookup signal owns only this read. Retained audio keeps its own
|
|
517
|
+
// explicit cancellation lifetime after a successful lookup returns.
|
|
518
|
+
const handle=createHandle(job,null,onState);
|
|
519
|
+
if(signal?.aborted){
|
|
520
|
+
handle.cancel();
|
|
521
|
+
throw abortError();
|
|
522
|
+
}
|
|
523
|
+
return handle;
|
|
524
|
+
}
|
|
525
|
+
if(!destination){
|
|
526
|
+
const jobs=owners.get(owner)?.jobs;
|
|
527
|
+
if(!jobs)return null;
|
|
528
|
+
for(const job of jobs){
|
|
529
|
+
if(job.storage||!job.done||job.failed||job.controller.signal.aborted
|
|
530
|
+
||!sameInput(job.input,input))continue;
|
|
531
|
+
if(!job.slots.every(function hasRetainedAudio(slot){
|
|
532
|
+
return slot.settled&&slot.blob instanceof Blob;
|
|
533
|
+
}))return null;
|
|
534
|
+
return completeCachedRead(job);
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const cancelled=deferred();
|
|
539
|
+
function cancelRead(){cancelled.reject(abortError());}
|
|
540
|
+
signal?.addEventListener('abort',cancelRead,{once:true});
|
|
541
|
+
try{
|
|
542
|
+
const reading=serializeStorage(destination,async function readCachedPreparation(){
|
|
543
|
+
if(signal?.aborted)throw abortError();
|
|
544
|
+
const manifest=await readManifest(destination,true);
|
|
545
|
+
if(signal?.aborted)throw abortError();
|
|
546
|
+
const record=manifest.entries.find(function findCachedPreparation(entry){
|
|
547
|
+
return sameInput(entry,input);
|
|
548
|
+
});
|
|
549
|
+
if(!record)return null;
|
|
550
|
+
if(!is.array(record.segments)||record.segments.length!==input.parts.length){
|
|
551
|
+
throw new TypeError('Stored speech preparation segments are unreadable.');
|
|
552
|
+
}
|
|
553
|
+
let complete=true;
|
|
554
|
+
for(const audio of record.segments){
|
|
555
|
+
if(!audio?.audioFile){
|
|
556
|
+
complete=false;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if(!is.string(audio.contentType)){
|
|
560
|
+
throw new TypeError('Stored speech audio content type is unreadable.');
|
|
561
|
+
}
|
|
562
|
+
if(signal?.aborted)throw abortError();
|
|
563
|
+
try{
|
|
564
|
+
const file=await destination.db.readFile(destination.table,audio.audioFile,{createTable:false});
|
|
565
|
+
if(!(file instanceof Blob)){
|
|
566
|
+
throw new TypeError('Stored speech audio must be a Blob.');
|
|
567
|
+
}
|
|
568
|
+
}catch(error){
|
|
569
|
+
if(error?.name!=='NotFoundError')throw error;
|
|
570
|
+
complete=false;
|
|
571
|
+
}
|
|
572
|
+
if(signal?.aborted)throw abortError();
|
|
573
|
+
}
|
|
574
|
+
return complete?record:null;
|
|
575
|
+
});
|
|
576
|
+
const record=await Promise.race([reading,cancelled.promise]);
|
|
577
|
+
if(signal?.aborted)throw abortError();
|
|
578
|
+
if(!record)return null;
|
|
579
|
+
const job=createJobState(input,destination);
|
|
580
|
+
job.cacheOnly=true;
|
|
581
|
+
job.record=record;
|
|
582
|
+
for(let index=0;index<job.slots.length;index+=1)settleSegment(job,index);
|
|
583
|
+
finishPreparation(job);
|
|
584
|
+
return completeCachedRead(job);
|
|
585
|
+
}finally{
|
|
586
|
+
signal?.removeEventListener('abort',cancelRead);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export {prepareSpeech,readPreparedSpeech};
|