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
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
import {arcaneLogging} from 'arcane-os/logging';
|
|
3
|
+
|
|
4
|
+
const is=new Is(false);
|
|
5
|
+
const owners=new WeakMap();
|
|
6
|
+
const storageQueues=new WeakMap();
|
|
7
|
+
|
|
8
|
+
function deferred(){
|
|
9
|
+
let resolve;
|
|
10
|
+
let reject;
|
|
11
|
+
const promise=new Promise(function captureSettlement(accept,decline){
|
|
12
|
+
resolve=accept;
|
|
13
|
+
reject=decline;
|
|
14
|
+
});
|
|
15
|
+
// The preparation owns failures even before a caller requests a segment.
|
|
16
|
+
promise.catch(function observeDeferredFailure(){});
|
|
17
|
+
return {promise,resolve,reject};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function abortError(){
|
|
21
|
+
const error=new Error('Speech preparation was cancelled.');
|
|
22
|
+
error.name='AbortError';
|
|
23
|
+
error.code='ARCANE_AI_REQUEST_ABORTED';
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function copyJSON(value,ancestors=new Set()){
|
|
28
|
+
if(value===null||is.string(value)||is.boolean(value))return value;
|
|
29
|
+
if(is.finite(value))return value;
|
|
30
|
+
if(!value||!is.object(value)||ancestors.has(value)){
|
|
31
|
+
throw new TypeError('Speech preparation metadata must contain JSON-compatible values.');
|
|
32
|
+
}
|
|
33
|
+
const prototype=Object.getPrototypeOf(value);
|
|
34
|
+
if(!is.array(value)&&prototype!==Object.prototype&&prototype!==null){
|
|
35
|
+
throw new TypeError('Speech preparation metadata must contain JSON-compatible values.');
|
|
36
|
+
}
|
|
37
|
+
ancestors.add(value);
|
|
38
|
+
const result=is.array(value)?[]:{};
|
|
39
|
+
const keys=is.array(value)
|
|
40
|
+
?Array.from({length:value.length},function arrayIndex(unused,index){return String(index);})
|
|
41
|
+
:Object.keys(value);
|
|
42
|
+
for(const key of keys){
|
|
43
|
+
Object.defineProperty(result,key,{
|
|
44
|
+
value:copyJSON(value[key],ancestors),
|
|
45
|
+
enumerable:true,
|
|
46
|
+
configurable:true,
|
|
47
|
+
writable:true
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
ancestors.delete(value);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sameJSON(first,second){
|
|
55
|
+
if(Object.is(first,second))return true;
|
|
56
|
+
if(!first||!second||!is.object(first)||!is.object(second))return false;
|
|
57
|
+
if(is.array(first)!==is.array(second))return false;
|
|
58
|
+
const keys=Object.keys(first);
|
|
59
|
+
if(keys.length!==Object.keys(second).length)return false;
|
|
60
|
+
for(const key of keys){
|
|
61
|
+
if(!Object.hasOwn(second,key)||!sameJSON(first[key],second[key]))return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sameInput(first,second){
|
|
67
|
+
return sameJSON(first.parts,second.parts)
|
|
68
|
+
&&sameJSON(first.originalParts,second.originalParts)
|
|
69
|
+
&&sameJSON(first.selection,second.selection)
|
|
70
|
+
&&sameJSON(first.segmentation,second.segmentation)
|
|
71
|
+
&&sameJSON(first.identity,second.identity);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeStorage(storage){
|
|
75
|
+
if(storage===null||storage===undefined)return null;
|
|
76
|
+
const {db,table,key}=storage;
|
|
77
|
+
if(!db||!is.string(table)||!table||!is.string(key)||!key){
|
|
78
|
+
throw new TypeError('Speech storage requires an existing database, table, and key.');
|
|
79
|
+
}
|
|
80
|
+
for(const method of ['get','set','readFile','writeFile']){
|
|
81
|
+
if(!is.function(db[method])){
|
|
82
|
+
throw new TypeError(`Speech storage requires db.${method}().`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {db,table,key,fileName:`${encodeURIComponent(key)}.json`};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sameStorage(first,second){
|
|
89
|
+
return first===second||Boolean(first&&second
|
|
90
|
+
&&first.db===second.db&&first.table===second.table&&first.key===second.key);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function serializeStorage(storage,execute){
|
|
94
|
+
let tables=storageQueues.get(storage.db);
|
|
95
|
+
if(!tables){
|
|
96
|
+
tables=new Map();
|
|
97
|
+
storageQueues.set(storage.db,tables);
|
|
98
|
+
}
|
|
99
|
+
let keys=tables.get(storage.table);
|
|
100
|
+
if(!keys){
|
|
101
|
+
keys=new Map();
|
|
102
|
+
tables.set(storage.table,keys);
|
|
103
|
+
}
|
|
104
|
+
const previous=keys.get(storage.key)||Promise.resolve();
|
|
105
|
+
const operation=previous.then(execute,execute);
|
|
106
|
+
keys.set(storage.key,operation);
|
|
107
|
+
function releaseStorageQueue(){
|
|
108
|
+
if(keys.get(storage.key)===operation)keys.delete(storage.key);
|
|
109
|
+
}
|
|
110
|
+
operation.then(releaseStorageQueue,releaseStorageQueue);
|
|
111
|
+
return operation;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function readManifest(storage){
|
|
115
|
+
const stored=await storage.db.get(storage.table,storage.fileName,true);
|
|
116
|
+
if(stored===null||stored===undefined)return {version:1,entries:[]};
|
|
117
|
+
const manifest=copyJSON(stored);
|
|
118
|
+
if(manifest.version!==1||!is.array(manifest.entries)){
|
|
119
|
+
throw new TypeError('Stored speech preparation metadata is unreadable.');
|
|
120
|
+
}
|
|
121
|
+
return manifest;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createRecord(input){
|
|
125
|
+
return {
|
|
126
|
+
id:globalThis.crypto.randomUUID(),
|
|
127
|
+
...copyJSON(input),
|
|
128
|
+
segments:input.parts.map(function emptyAudioRecord(){
|
|
129
|
+
return {audioFile:null,contentType:null};
|
|
130
|
+
})
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function publish(interest,error=null){
|
|
135
|
+
const job=interest.job;
|
|
136
|
+
let completed=0;
|
|
137
|
+
for(const segment of job.segments){
|
|
138
|
+
if(segment.state==='ready'||segment.state==='error')completed+=1;
|
|
139
|
+
}
|
|
140
|
+
try{
|
|
141
|
+
interest.onState?.({
|
|
142
|
+
state:interest.state,
|
|
143
|
+
completed,
|
|
144
|
+
total:job.segments.length,
|
|
145
|
+
segments:job.segments,
|
|
146
|
+
error
|
|
147
|
+
});
|
|
148
|
+
}catch(observerError){
|
|
149
|
+
arcaneLogging.error('Speech preparation state observer failed.',observerError);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function publishJob(job,error=null){
|
|
154
|
+
for(const interest of Array.from(job.interests)){
|
|
155
|
+
if(!interest.active)continue;
|
|
156
|
+
interest.state=job.state;
|
|
157
|
+
publish(interest,error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function settleSegment(job,index,error){
|
|
162
|
+
const slot=job.slots[index];
|
|
163
|
+
if(slot.settled)return;
|
|
164
|
+
slot.settled=true;
|
|
165
|
+
const segment=job.segments[index];
|
|
166
|
+
if(arguments.length===3){
|
|
167
|
+
segment.state='error';
|
|
168
|
+
segment.error=error;
|
|
169
|
+
slot.ready.reject(error);
|
|
170
|
+
if(error?.name!=='AbortError'){
|
|
171
|
+
arcaneLogging.error('Speech preparation segment failed.',{index,error});
|
|
172
|
+
}
|
|
173
|
+
}else{
|
|
174
|
+
const saved=job.record.segments[index];
|
|
175
|
+
segment.state='ready';
|
|
176
|
+
segment.audioFile=saved.audioFile;
|
|
177
|
+
segment.contentType=saved.contentType;
|
|
178
|
+
slot.ready.resolve();
|
|
179
|
+
}
|
|
180
|
+
publishJob(job);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function saveAudio(job,index,blob){
|
|
184
|
+
const contentType=blob.type;
|
|
185
|
+
if(!job.storage){
|
|
186
|
+
job.record.segments[index]={audioFile:null,contentType};
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const storage=job.storage;
|
|
190
|
+
const audioFile=`${job.record.id}.${index}.audio`;
|
|
191
|
+
await serializeStorage(storage,async function writePreparedAudio(){
|
|
192
|
+
if(job.controller.signal.aborted)throw abortError();
|
|
193
|
+
const manifest=await readManifest(storage);
|
|
194
|
+
let entry=manifest.entries.find(function findPreparedEntry(candidate){
|
|
195
|
+
return candidate.id===job.record.id;
|
|
196
|
+
});
|
|
197
|
+
if(!entry){
|
|
198
|
+
entry=copyJSON(job.record);
|
|
199
|
+
manifest.entries.push(entry);
|
|
200
|
+
}
|
|
201
|
+
if(job.controller.signal.aborted)throw abortError();
|
|
202
|
+
// DB writes cannot be aborted; finish this started write and its metadata
|
|
203
|
+
// together so a cancelled interest still leaves successful audio reusable.
|
|
204
|
+
await storage.db.writeFile(storage.table,audioFile,blob,false);
|
|
205
|
+
entry.segments[index]={audioFile,contentType};
|
|
206
|
+
await storage.db.set(storage.table,storage.fileName,manifest);
|
|
207
|
+
job.record.segments[index]={audioFile,contentType};
|
|
208
|
+
// Durable audio is reopened on demand rather than retained in memory.
|
|
209
|
+
job.slots[index].blob=null;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function prepareSegment(job,index,request){
|
|
214
|
+
const slot=job.slots[index];
|
|
215
|
+
try{
|
|
216
|
+
const blob=slot.blob||await request;
|
|
217
|
+
if(!(blob instanceof Blob)){
|
|
218
|
+
throw new TypeError('Speech synthesis must return a Blob.');
|
|
219
|
+
}
|
|
220
|
+
slot.blob=blob;
|
|
221
|
+
if(job.controller.signal.aborted)throw abortError();
|
|
222
|
+
await saveAudio(job,index,blob);
|
|
223
|
+
settleSegment(job,index);
|
|
224
|
+
}catch(error){
|
|
225
|
+
settleSegment(job,index,error);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function loadRecord(job){
|
|
230
|
+
if(job.previous){
|
|
231
|
+
await job.previous.finished.promise;
|
|
232
|
+
for(let index=0;index<job.slots.length;index+=1){
|
|
233
|
+
job.slots[index].blob=job.previous.slots[index].blob;
|
|
234
|
+
}
|
|
235
|
+
if(!job.storage&&job.previous.record){
|
|
236
|
+
job.record=copyJSON(job.previous.record);
|
|
237
|
+
}
|
|
238
|
+
job.previous=null;
|
|
239
|
+
}
|
|
240
|
+
if(job.controller.signal.aborted)throw abortError();
|
|
241
|
+
if(job.storage){
|
|
242
|
+
const storage=job.storage;
|
|
243
|
+
job.record=await serializeStorage(storage,async function findStoredPreparation(){
|
|
244
|
+
const manifest=await readManifest(storage);
|
|
245
|
+
const existing=manifest.entries.find(function findMatchingPreparation(entry){
|
|
246
|
+
return sameInput(entry,job.input);
|
|
247
|
+
});
|
|
248
|
+
if(existing){
|
|
249
|
+
if(!is.array(existing.segments)||existing.segments.length!==job.slots.length){
|
|
250
|
+
throw new TypeError('Stored speech preparation segments are unreadable.');
|
|
251
|
+
}
|
|
252
|
+
return copyJSON(existing);
|
|
253
|
+
}
|
|
254
|
+
const record=createRecord(job.input);
|
|
255
|
+
manifest.entries.push(record);
|
|
256
|
+
if(job.controller.signal.aborted)throw abortError();
|
|
257
|
+
await storage.db.set(storage.table,storage.fileName,manifest);
|
|
258
|
+
return record;
|
|
259
|
+
});
|
|
260
|
+
}else if(!job.record){
|
|
261
|
+
job.record=createRecord(job.input);
|
|
262
|
+
}
|
|
263
|
+
for(let index=0;index<job.slots.length;index+=1){
|
|
264
|
+
if(job.controller.signal.aborted)throw abortError();
|
|
265
|
+
const audio=job.record.segments[index];
|
|
266
|
+
if(!job.storage){
|
|
267
|
+
if(job.slots[index].blob){
|
|
268
|
+
job.record.segments[index]={
|
|
269
|
+
audioFile:null,
|
|
270
|
+
contentType:job.slots[index].blob.type
|
|
271
|
+
};
|
|
272
|
+
settleSegment(job,index);
|
|
273
|
+
}
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if(!audio?.audioFile)continue;
|
|
277
|
+
try{
|
|
278
|
+
if(!is.string(audio.contentType)){
|
|
279
|
+
throw new TypeError('Stored speech audio content type is unreadable.');
|
|
280
|
+
}
|
|
281
|
+
await job.storage.db.readFile(job.storage.table,audio.audioFile);
|
|
282
|
+
job.slots[index].blob=null;
|
|
283
|
+
settleSegment(job,index);
|
|
284
|
+
}catch(error){
|
|
285
|
+
if(error?.name==='NotFoundError'){
|
|
286
|
+
job.record.segments[index]={audioFile:null,contentType:null};
|
|
287
|
+
}else{
|
|
288
|
+
settleSegment(job,index,error);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function admitPreparation(job){
|
|
295
|
+
await loadRecord(job);
|
|
296
|
+
if(job.controller.signal.aborted)throw abortError();
|
|
297
|
+
job.state='preparing';
|
|
298
|
+
publishJob(job);
|
|
299
|
+
const pending=[];
|
|
300
|
+
// Calls enter the existing provider queue in order, before awaiting results.
|
|
301
|
+
for(let index=0;index<job.slots.length;index+=1){
|
|
302
|
+
const slot=job.slots[index];
|
|
303
|
+
if(slot.settled)continue;
|
|
304
|
+
if(job.controller.signal.aborted){
|
|
305
|
+
settleSegment(job,index,abortError());
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
job.segments[index].state='preparing';
|
|
309
|
+
let request;
|
|
310
|
+
if(!slot.blob){
|
|
311
|
+
try{
|
|
312
|
+
request=job.synthesize(copyJSON(job.input.parts[index]),job.controller.signal);
|
|
313
|
+
}catch(error){
|
|
314
|
+
request=Promise.reject(error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
pending.push(prepareSegment(job,index,request));
|
|
318
|
+
}
|
|
319
|
+
publishJob(job);
|
|
320
|
+
return pending;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function finishPreparation(job,error){
|
|
324
|
+
if(arguments.length===2){
|
|
325
|
+
for(let index=0;index<job.slots.length;index+=1){
|
|
326
|
+
if(!job.slots[index].settled)settleSegment(job,index,error);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const errors=[];
|
|
330
|
+
for(const segment of job.segments){
|
|
331
|
+
if(segment.state==='error')errors.push(segment.error);
|
|
332
|
+
}
|
|
333
|
+
if(arguments.length===2&&errors.length===0)errors.push(error);
|
|
334
|
+
job.done=true;
|
|
335
|
+
job.failed=errors.length>0;
|
|
336
|
+
if(job.failed){
|
|
337
|
+
const failure=errors.length===1?errors[0]:new AggregateError(
|
|
338
|
+
errors,
|
|
339
|
+
'Some speech segments could not be prepared.'
|
|
340
|
+
);
|
|
341
|
+
job.state=job.controller.signal.aborted?'cancelled':'error';
|
|
342
|
+
publishJob(job,failure);
|
|
343
|
+
job.ready.reject(failure);
|
|
344
|
+
}else{
|
|
345
|
+
job.state='ready';
|
|
346
|
+
publishJob(job);
|
|
347
|
+
job.ready.resolve(copyJSON(job.record));
|
|
348
|
+
}
|
|
349
|
+
job.finished.resolve();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function createJob(ownerState,input,storage,synthesize,previous){
|
|
353
|
+
const job={
|
|
354
|
+
input,storage,synthesize,previous,
|
|
355
|
+
controller:new AbortController(),
|
|
356
|
+
interests:new Set(),
|
|
357
|
+
state:'queued',
|
|
358
|
+
done:false,
|
|
359
|
+
failed:false,
|
|
360
|
+
record:null,
|
|
361
|
+
ready:deferred(),
|
|
362
|
+
finished:deferred(),
|
|
363
|
+
slots:input.parts.map(function createSegmentSlot(){
|
|
364
|
+
return {ready:deferred(),blob:null,settled:false};
|
|
365
|
+
}),
|
|
366
|
+
segments:input.parts.map(function createPublicSegment(part,index){
|
|
367
|
+
return {...part,index,state:'queued',audioFile:null,contentType:null,error:null};
|
|
368
|
+
})
|
|
369
|
+
};
|
|
370
|
+
const admission=ownerState.admission.then(function admitOrderedPreparation(){
|
|
371
|
+
return admitPreparation(job);
|
|
372
|
+
});
|
|
373
|
+
ownerState.admission=admission.then(
|
|
374
|
+
function preparationAdmitted(){},
|
|
375
|
+
function preparationAdmissionFailed(){}
|
|
376
|
+
);
|
|
377
|
+
admission.then(async function settleAdmittedPreparation(pending){
|
|
378
|
+
await Promise.allSettled(pending);
|
|
379
|
+
finishPreparation(job);
|
|
380
|
+
},function failPreparationAdmission(error){
|
|
381
|
+
finishPreparation(job,error);
|
|
382
|
+
}).catch(function reportPreparationSettlementFailure(error){
|
|
383
|
+
finishPreparation(job,error);
|
|
384
|
+
});
|
|
385
|
+
return job;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function createHandle(job,signal,onState){
|
|
389
|
+
const ready=deferred();
|
|
390
|
+
const cancelled=deferred();
|
|
391
|
+
const interest={job,onState,active:true,state:job.state};
|
|
392
|
+
function removeSignal(){signal?.removeEventListener('abort',cancel);}
|
|
393
|
+
function cancel(){
|
|
394
|
+
if(!interest.active)return false;
|
|
395
|
+
interest.active=false;
|
|
396
|
+
interest.state='cancelled';
|
|
397
|
+
removeSignal();
|
|
398
|
+
job.interests.delete(interest);
|
|
399
|
+
const error=abortError();
|
|
400
|
+
ready.reject(error);
|
|
401
|
+
cancelled.reject(error);
|
|
402
|
+
publish(interest,error);
|
|
403
|
+
if(!job.done&&job.interests.size===0)job.controller.abort();
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
const handle={
|
|
407
|
+
segments:job.segments,
|
|
408
|
+
get state(){return interest.state;},
|
|
409
|
+
ready:ready.promise,
|
|
410
|
+
async getAudio(index){
|
|
411
|
+
if(!interest.active)throw abortError();
|
|
412
|
+
if(!is.safeInteger(index)||index<0||index>=job.slots.length){
|
|
413
|
+
throw new RangeError('Speech segment index is outside this preparation.');
|
|
414
|
+
}
|
|
415
|
+
await Promise.race([job.slots[index].ready.promise,cancelled.promise]);
|
|
416
|
+
if(!interest.active)throw abortError();
|
|
417
|
+
if(!job.storage)return job.slots[index].blob;
|
|
418
|
+
const audio=job.record.segments[index];
|
|
419
|
+
const file=await job.storage.db.readFile(job.storage.table,audio.audioFile);
|
|
420
|
+
if(!interest.active)throw abortError();
|
|
421
|
+
return file.type===audio.contentType?file:new Blob([file],{type:audio.contentType});
|
|
422
|
+
},
|
|
423
|
+
cancel
|
|
424
|
+
};
|
|
425
|
+
job.interests.add(interest);
|
|
426
|
+
job.ready.promise.then(function preparationReady(record){
|
|
427
|
+
if(interest.active)ready.resolve(copyJSON(record));
|
|
428
|
+
removeSignal();
|
|
429
|
+
job.interests.delete(interest);
|
|
430
|
+
},function preparationFailed(error){
|
|
431
|
+
if(interest.active)ready.reject(error);
|
|
432
|
+
removeSignal();
|
|
433
|
+
job.interests.delete(interest);
|
|
434
|
+
});
|
|
435
|
+
signal?.addEventListener('abort',cancel,{once:true});
|
|
436
|
+
if(signal?.aborted)cancel();
|
|
437
|
+
else publish(interest);
|
|
438
|
+
return handle;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Prepare complete ordered speech without constructing or controlling playback. */
|
|
442
|
+
function prepareSpeech({
|
|
443
|
+
owner,
|
|
444
|
+
parts,
|
|
445
|
+
originalParts=parts,
|
|
446
|
+
selection=null,
|
|
447
|
+
segmentation=null,
|
|
448
|
+
storage=null,
|
|
449
|
+
identity=null,
|
|
450
|
+
signal=null,
|
|
451
|
+
onState=null,
|
|
452
|
+
synthesize
|
|
453
|
+
}={}){
|
|
454
|
+
if((!owner||!is.object(owner))&&!is.function(owner)){
|
|
455
|
+
throw new TypeError('Speech preparation requires its owning AI instance.');
|
|
456
|
+
}
|
|
457
|
+
if(!is.array(parts)||!is.function(synthesize)
|
|
458
|
+
||(onState!==null&&!is.function(onState))){
|
|
459
|
+
throw new TypeError('Speech preparation requires ordered parts and synthesis/state callbacks.');
|
|
460
|
+
}
|
|
461
|
+
const input=copyJSON({parts,originalParts,selection,segmentation,identity});
|
|
462
|
+
const destination=normalizeStorage(storage);
|
|
463
|
+
let ownerState=owners.get(owner);
|
|
464
|
+
if(!ownerState){
|
|
465
|
+
ownerState={admission:Promise.resolve(),jobs:new Set()};
|
|
466
|
+
owners.set(owner,ownerState);
|
|
467
|
+
}
|
|
468
|
+
let previous=null;
|
|
469
|
+
for(const candidate of ownerState.jobs){
|
|
470
|
+
if(sameStorage(candidate.storage,destination)&&sameInput(candidate.input,input)){
|
|
471
|
+
previous=candidate;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if(previous&&!previous.failed&&!previous.controller.signal.aborted
|
|
476
|
+
&&(!previous.done||!destination)){
|
|
477
|
+
return createHandle(previous,signal,onState);
|
|
478
|
+
}
|
|
479
|
+
const job=createJob(ownerState,input,destination,synthesize,previous);
|
|
480
|
+
if(previous)ownerState.jobs.delete(previous);
|
|
481
|
+
ownerState.jobs.add(job);
|
|
482
|
+
return createHandle(job,signal,onState);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export {prepareSpeech};
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
const is=new Is(false);
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Default delay of exactly seven 24-hour periods in milliseconds.
|
|
3
6
|
*
|
|
@@ -23,7 +26,7 @@ export class Questionnaire{
|
|
|
23
26
|
* @throws {RangeError} When the delay is not a positive finite number.
|
|
24
27
|
*/
|
|
25
28
|
setNotificationTime(notificationTime){
|
|
26
|
-
if(!
|
|
29
|
+
if(!is.finite(notificationTime)||notificationTime<=0){
|
|
27
30
|
throw new RangeError('notificationTime must be a positive finite number');
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -48,11 +51,11 @@ export class Questionnaire{
|
|
|
48
51
|
return false;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
if(!
|
|
54
|
+
if(!is.safeInteger(firstBootUp)||firstBootUp<=0){
|
|
52
55
|
return false;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
if(!
|
|
58
|
+
if(!is.finite(now)||now<firstBootUp){
|
|
56
59
|
return false;
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
const is=new Is(false);
|
|
3
|
+
|
|
1
4
|
function parseRecordLinks(value='',{pattern=/\b[A-Z]\d{4}\b/gi}={}){
|
|
2
|
-
const text=
|
|
5
|
+
const text=is.array(value)?value.join(' '):String(value||'');
|
|
3
6
|
return [...new Set([...text.matchAll(pattern)].map(match=>match[0].toUpperCase()))];
|
|
4
7
|
}
|
|
5
8
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
const is=new Is(false);
|
|
3
|
+
|
|
1
4
|
const MONTH_NUMBER={
|
|
2
5
|
january:1,february:2,march:3,april:4,may:5,june:6,
|
|
3
6
|
july:7,august:8,september:9,october:10,november:11,december:12
|
|
@@ -7,7 +10,7 @@ const MONTH_TOKEN='January|February|March|April|May|June|July|August|September|O
|
|
|
7
10
|
|
|
8
11
|
function normalizedInteger(value,{minimum=0,fallback=0}={}){
|
|
9
12
|
const number=Number(value);
|
|
10
|
-
return
|
|
13
|
+
return is.integer(number)&&number>=minimum?number:fallback;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
function textLines(value=''){
|
|
@@ -86,9 +89,9 @@ function findRulePassages(text='',rules=[],{
|
|
|
86
89
|
const findings=[];
|
|
87
90
|
const seen=new Set();
|
|
88
91
|
|
|
89
|
-
for(const definition of
|
|
92
|
+
for(const definition of is.array(rules)?rules:[]){
|
|
90
93
|
if(!definition?.id) continue;
|
|
91
|
-
const patterns=
|
|
94
|
+
const patterns=is.array(definition.patterns)?definition.patterns:[definition.pattern];
|
|
92
95
|
let ruleCount=0;
|
|
93
96
|
for(const supplied of patterns.filter(Boolean)){
|
|
94
97
|
const pattern=globalPattern(supplied);
|
|
@@ -111,9 +114,9 @@ function findRulePassages(text='',rules=[],{
|
|
|
111
114
|
lineEnd:lineEnd+1,
|
|
112
115
|
page:pageAtLine(markers,line),
|
|
113
116
|
excerpt,
|
|
114
|
-
metadata:definition.metadata&&
|
|
117
|
+
metadata:definition.metadata&&is.object(definition.metadata)?{...definition.metadata}:{}
|
|
115
118
|
};
|
|
116
|
-
if(
|
|
119
|
+
if(is.function(definition.accept)&&!definition.accept(candidate,{match,source,lines,line})) continue;
|
|
117
120
|
const key=passageKey(candidate);
|
|
118
121
|
if(seen.has(key)) continue;
|
|
119
122
|
seen.add(key);
|
|
@@ -127,7 +130,7 @@ function findRulePassages(text='',rules=[],{
|
|
|
127
130
|
|
|
128
131
|
function validIsoDate(year,month=1,day=1){
|
|
129
132
|
const y=Number(year); const m=Number(month); const d=Number(day);
|
|
130
|
-
if(!
|
|
133
|
+
if(!is.integer(y)||y<1900||y>2100||!is.integer(m)||m<1||m>12||!is.integer(d)||d<1||d>31) return null;
|
|
131
134
|
const date=new Date(Date.UTC(y,m-1,d));
|
|
132
135
|
if(date.getUTCFullYear()!==y||date.getUTCMonth()!==m-1||date.getUTCDate()!==d) return null;
|
|
133
136
|
return `${String(y).padStart(4,'0')}-${String(m).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
const is=new Is(false);
|
|
3
|
+
|
|
1
4
|
import {createArcaneEventSource} from 'arcane-os/event-manager';
|
|
2
5
|
import {resolveApplicationLocalStorageKey} from './AppDataScope.js';
|
|
3
6
|
|
|
@@ -43,17 +46,17 @@ function operationAbortedError(reason){
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
function isPlainRecord(value){
|
|
46
|
-
if(!value
|
|
49
|
+
if(!value||!is.object(value)||is.array(value)) return false;
|
|
47
50
|
const prototype=Object.getPrototypeOf(value);
|
|
48
51
|
return prototype===Object.prototype||prototype===null;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
function isAbortSignal(value){
|
|
52
55
|
return Boolean(value)
|
|
53
|
-
&&
|
|
54
|
-
&&
|
|
55
|
-
&&
|
|
56
|
-
&&
|
|
56
|
+
&&is.object(value)
|
|
57
|
+
&&is.boolean(value.aborted)
|
|
58
|
+
&&is.function(value.addEventListener)
|
|
59
|
+
&&is.function(value.removeEventListener);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
function normalizeOperationOptions(value={}){
|
|
@@ -110,15 +113,15 @@ function normalizeRecordId(value=''){
|
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
function normalizeReview(value={}){
|
|
113
|
-
const source=value&&
|
|
116
|
+
const source=value&&is.object(value)?value:{};
|
|
114
117
|
const attributes={};
|
|
115
|
-
if(source.attributes&&
|
|
118
|
+
if(source.attributes&&is.object(source.attributes)&&!is.array(source.attributes)){
|
|
116
119
|
for(const [key,value] of Object.entries(source.attributes)){
|
|
117
120
|
const normalizedKey=String(key);
|
|
118
121
|
setDataProperty(
|
|
119
122
|
attributes,
|
|
120
123
|
normalizedKey,
|
|
121
|
-
|
|
124
|
+
is.array(value)
|
|
122
125
|
?value.map(function normalizeReviewAttributeItem(item){
|
|
123
126
|
return String(item);
|
|
124
127
|
})
|
|
@@ -184,7 +187,7 @@ function normalizedStoredRecords(value){
|
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
function validateAdapter(adapter){
|
|
187
|
-
if(!adapter
|
|
190
|
+
if(!adapter||!is.function(adapter.get)||!is.function(adapter.set)){
|
|
188
191
|
throw recordReviewStoreError(
|
|
189
192
|
RECORD_REVIEW_STORE_ERROR_CODES.adapterInvalid,
|
|
190
193
|
'record-review-storage-adapter-invalid',
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
const is=new Is(false);
|
|
3
|
+
|
|
1
4
|
const DEFAULT_LEVELS=[
|
|
2
5
|
{minimum:70,id:'critical'},
|
|
3
6
|
{minimum:40,id:'high'},
|
|
@@ -15,7 +18,7 @@ export function analyzeRiskSignals(input,{signals=[],levels=DEFAULT_LEVELS}={}){
|
|
|
15
18
|
let score=0;
|
|
16
19
|
|
|
17
20
|
for(const signal of signals){
|
|
18
|
-
if(!signal
|
|
21
|
+
if(!signal||!is.string(signal.id)||!(signal.pattern instanceof RegExp))continue;
|
|
19
22
|
signal.pattern.lastIndex=0;
|
|
20
23
|
if(!signal.pattern.test(text))continue;
|
|
21
24
|
const weight=Math.max(0,Number(signal.weight)||0);
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
const is=new Is(false);
|
|
3
|
+
|
|
1
4
|
import {
|
|
2
5
|
canonicalApplicationId,
|
|
3
6
|
openApplicationDataDirectory
|
|
4
7
|
} from './AppDataScope.js';
|
|
5
8
|
|
|
6
9
|
function safeSegment(value,label){
|
|
7
|
-
if(
|
|
10
|
+
if(!is.string(value)){
|
|
8
11
|
throw new TypeError(`${label} must be a string.`);
|
|
9
12
|
}
|
|
10
13
|
const normalized=value.trim();
|
|
@@ -49,7 +52,7 @@ export default class ScopedOPFSCache{
|
|
|
49
52
|
?null
|
|
50
53
|
:canonicalApplicationId(applicationId);
|
|
51
54
|
this.#namespace=safeSegment(namespace,'namespace');
|
|
52
|
-
if(!storage
|
|
55
|
+
if(!storage||!is.function(storage.getDirectory)){
|
|
53
56
|
throw unavailable();
|
|
54
57
|
}
|
|
55
58
|
this.#storage=storage;
|
|
@@ -58,7 +61,7 @@ export default class ScopedOPFSCache{
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
static supported(storage=globalThis.navigator?.storage){
|
|
61
|
-
return Boolean(storage&&
|
|
64
|
+
return Boolean(storage&&is.function(storage.getDirectory));
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
get namespace(){
|