arcane-os 0.5.19 → 0.6.1
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 +26 -0
- package/README.md +18 -12
- package/browser-runtime/ai/browser-device-settings.mjs +83 -0
- package/browser-runtime/ai/browser-speech-providers.mjs +64 -60
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +4 -48
- package/browser-runtime/ai/speech-worker-runtime.mjs +4 -5
- package/docs/architecture.md +1 -1
- package/docs/reference/ai/browser-speech.md +95 -38
- package/docs/reference/behavioral-testing.md +1 -1
- package/docs/reference/inventory/runtime-modules.json +2 -2
- package/docs/reference/runtime-components.md +53 -0
- package/docs/reference/runtime-modules.md +32 -17
- package/docs/reference/sdk-api.md +32 -11
- package/examples/wasm-ai-demo/README.md +5 -3
- package/examples/wasm-ai-demo/app.js +1 -1
- package/package.json +1 -1
- package/runtime/arcane/components/browser-ai-setup.html +212 -0
- package/runtime/arcane/modules/AI.js +27 -29
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
|
|
2
|
+
<style>
|
|
3
|
+
:host{display:block;min-width:0}
|
|
4
|
+
.availability{display:grid;gap:var(--arcane-space-2);margin:0}
|
|
5
|
+
.availability div{display:grid;gap:var(--arcane-space-1)}
|
|
6
|
+
.availability dt{font-weight:700}
|
|
7
|
+
.availability dd{margin:0}
|
|
8
|
+
.flag-choice{margin-block:var(--arcane-space-4)}
|
|
9
|
+
.flag-choice h3{margin-block:var(--arcane-space-2)}
|
|
10
|
+
.actions{display:flex;flex-wrap:wrap;gap:var(--arcane-space-2)}
|
|
11
|
+
p,dt,dd,li,button{overflow-wrap:anywhere}
|
|
12
|
+
button{white-space:normal}
|
|
13
|
+
code{display:block;white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;margin-block:var(--arcane-space-2)}
|
|
14
|
+
li{margin-block:var(--arcane-space-2)}
|
|
15
|
+
</style>
|
|
16
|
+
|
|
17
|
+
<section class="arcane-card" aria-labelledby="setupTitle">
|
|
18
|
+
<header class="arcane-card__header">
|
|
19
|
+
<h2 id="setupTitle" class="arcane-section-heading">Browser AI acceleration</h2>
|
|
20
|
+
</header>
|
|
21
|
+
<div class="arcane-card__body">
|
|
22
|
+
<p id="browserName"></p>
|
|
23
|
+
<dl class="availability" aria-live="polite">
|
|
24
|
+
<div><dt>WebNN API</dt><dd id="webnnAvailability"></dd></div>
|
|
25
|
+
<div><dt>WebGPU API</dt><dd id="webgpuAvailability"></dd></div>
|
|
26
|
+
</dl>
|
|
27
|
+
<p class="arcane-help">These checks show browser APIs only. They do not report whether a model is running on an NPU or GPU.</p>
|
|
28
|
+
<h3>NPU setup</h3>
|
|
29
|
+
<p id="browserInstructions"></p>
|
|
30
|
+
<div id="flagAddresses"></div>
|
|
31
|
+
<ol id="setupSteps"></ol>
|
|
32
|
+
<p>Browser API availability does not confirm that an NPU or a particular model is supported. You choose whether to change the browser setting.</p>
|
|
33
|
+
<p><a href="https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html">ONNX Runtime WebNN setup documentation</a></p>
|
|
34
|
+
<p id="copyStatus" class="arcane-help" role="status" aria-live="polite"></p>
|
|
35
|
+
</div>
|
|
36
|
+
<footer class="arcane-card__footer">
|
|
37
|
+
<button id="refreshAvailability" class="arcane-button arcane-button--secondary" type="button">Refresh browser availability</button>
|
|
38
|
+
<p id="setupStatus" class="arcane-help" role="status" aria-live="polite"></p>
|
|
39
|
+
</footer>
|
|
40
|
+
</section>
|
|
41
|
+
|
|
42
|
+
<script type="module">
|
|
43
|
+
const host=this;
|
|
44
|
+
const root=host.shadowRoot;
|
|
45
|
+
host.ready=false;
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
getBrowserDeviceSettings,
|
|
49
|
+
openBrowserDeviceSettings,
|
|
50
|
+
WEBNN_BROWSER_SETTINGS
|
|
51
|
+
}=await import('../sdk/ai/browser-device-settings.mjs');
|
|
52
|
+
const {arcaneLogging}=await import('arcane-os/logging');
|
|
53
|
+
const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
|
|
54
|
+
const events=createArcaneEventSource(host,{
|
|
55
|
+
source:'arcane.component.browser-ai-setup',
|
|
56
|
+
eventTypes:['browser-ai-setup-ready']
|
|
57
|
+
});
|
|
58
|
+
const lifecycleController=new AbortController();
|
|
59
|
+
const status=root.querySelector('#setupStatus');
|
|
60
|
+
const copyStatus=root.querySelector('#copyStatus');
|
|
61
|
+
const choices=[];
|
|
62
|
+
const setupSteps=[
|
|
63
|
+
'If the flags page does not open, copy the matching address into your browser\'s address bar and open it.',
|
|
64
|
+
'Find “Enables WebNN API” and choose Enabled.',
|
|
65
|
+
'Save your work, then use the browser\'s Relaunch control.',
|
|
66
|
+
'Return to this page and select Refresh browser availability.'
|
|
67
|
+
];
|
|
68
|
+
let copyTask=null;
|
|
69
|
+
let destroyed=false;
|
|
70
|
+
|
|
71
|
+
host.refresh=refresh;
|
|
72
|
+
host.open=open;
|
|
73
|
+
host.destroy=destroy;
|
|
74
|
+
|
|
75
|
+
function refresh(){
|
|
76
|
+
if(destroyed)return false;
|
|
77
|
+
const settings=getBrowserDeviceSettings();
|
|
78
|
+
root.querySelector('#browserName').textContent=settings.browserId==='unknown'
|
|
79
|
+
?'Browser not identified.'
|
|
80
|
+
:`Browser: ${settings.name}.`;
|
|
81
|
+
root.querySelector('#webnnAvailability').textContent=settings.webnnAvailable
|
|
82
|
+
?'Exposed to this page.'
|
|
83
|
+
:'Unavailable to this page.';
|
|
84
|
+
root.querySelector('#webgpuAvailability').textContent=settings.webgpuAvailable
|
|
85
|
+
?'Exposed to this page.'
|
|
86
|
+
:'Unavailable to this page.';
|
|
87
|
+
root.querySelector('#browserInstructions').textContent=settings.webnnFlagsURL
|
|
88
|
+
?`Set up NPU attempts to open the WebNN flags page in ${settings.name} and shows the full instructions.`
|
|
89
|
+
:'A matching WebNN flags address is not known for this browser. Choose Google Chrome or Microsoft Edge below if that is the browser you are using.';
|
|
90
|
+
for(const choice of choices){
|
|
91
|
+
choice.row.hidden=Boolean(settings.webnnFlagsURL)
|
|
92
|
+
&&choice.target.url!==settings.webnnFlagsURL;
|
|
93
|
+
choice.setupButton.textContent=settings.webnnFlagsURL
|
|
94
|
+
?'Set up NPU'
|
|
95
|
+
:`Set up NPU in ${choice.target.name}`;
|
|
96
|
+
}
|
|
97
|
+
return settings;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function open(browserId){
|
|
101
|
+
if(destroyed)return false;
|
|
102
|
+
const current=refresh();
|
|
103
|
+
const selectedId=browserId??current.browserId;
|
|
104
|
+
const target=WEBNN_BROWSER_SETTINGS.find(function matchSetupBrowser(browser){
|
|
105
|
+
return browser.browserId===selectedId;
|
|
106
|
+
});
|
|
107
|
+
if(!target){
|
|
108
|
+
status.textContent='Choose the Google Chrome or Microsoft Edge setup button that matches your browser.';
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const instructions=setupSteps.map(function numberSetupStep(step,index){
|
|
112
|
+
return `${index+1}. ${step}`;
|
|
113
|
+
}).join('\n');
|
|
114
|
+
openBrowserDeviceSettings(
|
|
115
|
+
target,
|
|
116
|
+
`NPU setup for ${target.name}\n\n${instructions}\n\n${target.url}`
|
|
117
|
+
);
|
|
118
|
+
status.textContent='If the flags page did not open, copy its address above and paste it into your browser\'s address bar.';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function copyAddress(address){
|
|
122
|
+
if(destroyed)return Promise.resolve(false);
|
|
123
|
+
if(copyTask)return copyTask;
|
|
124
|
+
copyStatus.textContent='Copying address...';
|
|
125
|
+
for(const choice of choices)choice.copyButton.disabled=true;
|
|
126
|
+
copyTask=writeAddress().finally(function finishAddressCopy(){
|
|
127
|
+
copyTask=null;
|
|
128
|
+
if(!destroyed){
|
|
129
|
+
for(const choice of choices)choice.copyButton.disabled=false;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return copyTask;
|
|
133
|
+
|
|
134
|
+
async function writeAddress(){
|
|
135
|
+
try{
|
|
136
|
+
await globalThis.navigator.clipboard.writeText(address);
|
|
137
|
+
if(destroyed)return false;
|
|
138
|
+
copyStatus.textContent='Address copied. Paste it into your browser\'s address bar.';
|
|
139
|
+
return true;
|
|
140
|
+
}catch(error){
|
|
141
|
+
if(!destroyed){
|
|
142
|
+
copyStatus.textContent='The address could not be copied automatically. Select the complete address above, copy it manually, and paste it into your browser\'s address bar.';
|
|
143
|
+
arcaneLogging.error('Unable to copy the browser WebNN flags address.',error);
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const addressList=root.querySelector('#flagAddresses');
|
|
151
|
+
for(const target of WEBNN_BROWSER_SETTINGS){
|
|
152
|
+
const row=document.createElement('section');
|
|
153
|
+
const heading=document.createElement('h3');
|
|
154
|
+
const address=document.createElement('code');
|
|
155
|
+
const actions=document.createElement('div');
|
|
156
|
+
const setupButton=document.createElement('button');
|
|
157
|
+
const copyButton=document.createElement('button');
|
|
158
|
+
row.className='flag-choice';
|
|
159
|
+
heading.textContent=target.name;
|
|
160
|
+
address.textContent=target.url;
|
|
161
|
+
address.tabIndex=0;
|
|
162
|
+
address.setAttribute('aria-label',`${target.name} flags address`);
|
|
163
|
+
actions.className='actions';
|
|
164
|
+
setupButton.type='button';
|
|
165
|
+
setupButton.className='arcane-button';
|
|
166
|
+
setupButton.addEventListener('click',function openSelectedBrowserSettings(){
|
|
167
|
+
return open(target.browserId);
|
|
168
|
+
},{signal:lifecycleController.signal});
|
|
169
|
+
copyButton.type='button';
|
|
170
|
+
copyButton.className='arcane-button arcane-button--secondary';
|
|
171
|
+
copyButton.textContent=`Copy ${target.name} address`;
|
|
172
|
+
copyButton.addEventListener('click',function copySelectedBrowserAddress(){
|
|
173
|
+
return copyAddress(target.url);
|
|
174
|
+
},{signal:lifecycleController.signal});
|
|
175
|
+
actions.append(setupButton,copyButton);
|
|
176
|
+
row.append(heading,address,actions);
|
|
177
|
+
addressList.append(row);
|
|
178
|
+
choices.push({row,target,setupButton,copyButton});
|
|
179
|
+
}
|
|
180
|
+
const stepList=root.querySelector('#setupSteps');
|
|
181
|
+
for(const step of setupSteps){
|
|
182
|
+
const item=document.createElement('li');
|
|
183
|
+
item.textContent=step;
|
|
184
|
+
stepList.append(item);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
root.querySelector('#refreshAvailability').addEventListener('click',function refreshBrowserAvailability(){
|
|
188
|
+
refresh();
|
|
189
|
+
status.textContent='Browser API availability refreshed.';
|
|
190
|
+
},{signal:lifecycleController.signal});
|
|
191
|
+
host.ownerDocument.defaultView.addEventListener('pagehide',function disposeBeforePageUnload(event){
|
|
192
|
+
if(!event.persisted)destroy();
|
|
193
|
+
},{
|
|
194
|
+
signal:lifecycleController.signal
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
function destroy(){
|
|
198
|
+
if(destroyed)return false;
|
|
199
|
+
destroyed=true;
|
|
200
|
+
host.ready=false;
|
|
201
|
+
lifecycleController.abort('browser-ai-setup-destroyed');
|
|
202
|
+
events.dispose();
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
refresh();
|
|
207
|
+
host.ready=true;
|
|
208
|
+
const publication=events.dispatch('browser-ai-setup-ready',{}, {
|
|
209
|
+
publicDetail:{ready:true}
|
|
210
|
+
});
|
|
211
|
+
projectArcaneDOMEvent(host,publication.occurrence);
|
|
212
|
+
</script>
|
|
@@ -27,11 +27,8 @@ const DEFAULT_TTS_SEGMENTATION={
|
|
|
27
27
|
punctuation:'sentence',
|
|
28
28
|
wordCadence:null
|
|
29
29
|
};
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
maxConcurrentRequests:4
|
|
33
|
-
};
|
|
34
|
-
const BROWSER_TTS_EXECUTION_DEVICES=new Set(['auto','webgpu','wasm']);
|
|
30
|
+
const DEFAULT_BROWSER_SPEECH_EXECUTION_DEVICE='auto';
|
|
31
|
+
const BROWSER_SPEECH_EXECUTION_DEVICES=new Set(['auto','webnn-npu','webgpu','wasm']);
|
|
35
32
|
const MAX_BROWSER_TTS_CONCURRENT_REQUESTS=4;
|
|
36
33
|
const TTS_PUNCTUATION_MODES=new Set(['sentence','any','none']);
|
|
37
34
|
// A complete punctuation run is a boundary unless the whole run consists of
|
|
@@ -817,36 +814,45 @@ function browserSpeechIdentifier(value,label){
|
|
|
817
814
|
return value;
|
|
818
815
|
}
|
|
819
816
|
|
|
820
|
-
function
|
|
817
|
+
function normalizeBrowserSpeechExecution(role,value){
|
|
818
|
+
const label=`AI browser speech ${role}.execution`;
|
|
819
|
+
const maximumConcurrentRequests=role==='stt'
|
|
820
|
+
?1
|
|
821
|
+
:MAX_BROWSER_TTS_CONCURRENT_REQUESTS;
|
|
821
822
|
if(value===undefined){
|
|
822
|
-
return completeValue({
|
|
823
|
+
return completeValue({
|
|
824
|
+
device:DEFAULT_BROWSER_SPEECH_EXECUTION_DEVICE,
|
|
825
|
+
maxConcurrentRequests:maximumConcurrentRequests
|
|
826
|
+
});
|
|
823
827
|
}
|
|
824
828
|
const descriptors=closedRecord(
|
|
825
829
|
value,
|
|
826
830
|
['device','maxConcurrentRequests'],
|
|
827
831
|
[],
|
|
828
|
-
|
|
832
|
+
label
|
|
829
833
|
);
|
|
830
834
|
const device=descriptors.device
|
|
831
835
|
?descriptors.device.value
|
|
832
|
-
:
|
|
836
|
+
:DEFAULT_BROWSER_SPEECH_EXECUTION_DEVICE;
|
|
833
837
|
const maxConcurrentRequests=descriptors.maxConcurrentRequests
|
|
834
838
|
?descriptors.maxConcurrentRequests.value
|
|
835
|
-
:
|
|
836
|
-
if(!is.string(device)||!
|
|
839
|
+
:maximumConcurrentRequests;
|
|
840
|
+
if(!is.string(device)||!BROWSER_SPEECH_EXECUTION_DEVICES.has(device)){
|
|
837
841
|
throw aiBrowserSpeechError(
|
|
838
842
|
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
839
843
|
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
840
|
-
|
|
844
|
+
`${label}.device must be auto, webnn-npu, webgpu, or wasm.`
|
|
841
845
|
);
|
|
842
846
|
}
|
|
843
847
|
if(!is.safeInteger(maxConcurrentRequests)
|
|
844
848
|
||maxConcurrentRequests<1
|
|
845
|
-
||maxConcurrentRequests>
|
|
849
|
+
||maxConcurrentRequests>maximumConcurrentRequests){
|
|
846
850
|
throw aiBrowserSpeechError(
|
|
847
851
|
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
848
852
|
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
849
|
-
|
|
853
|
+
role==='stt'
|
|
854
|
+
?`${label}.maxConcurrentRequests must be 1.`
|
|
855
|
+
:`${label}.maxConcurrentRequests must be an integer from 1 through ${maximumConcurrentRequests}.`
|
|
850
856
|
);
|
|
851
857
|
}
|
|
852
858
|
return completeValue({device,maxConcurrentRequests});
|
|
@@ -917,13 +923,6 @@ function normalizeBrowserSpeechRole(value,role){
|
|
|
917
923
|
`${label}.offline must be a boolean.`
|
|
918
924
|
);
|
|
919
925
|
}
|
|
920
|
-
if(role==='stt'&&descriptors.execution){
|
|
921
|
-
throw aiBrowserSpeechError(
|
|
922
|
-
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
923
|
-
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
924
|
-
'AI browser speech execution policy is available only for TTS.'
|
|
925
|
-
);
|
|
926
|
-
}
|
|
927
926
|
return completeValue({
|
|
928
927
|
providerId,
|
|
929
928
|
...(hasGraph
|
|
@@ -937,11 +936,10 @@ function normalizeBrowserSpeechRole(value,role){
|
|
|
937
936
|
...(secure?{security:{secure:true}}:{})
|
|
938
937
|
}),
|
|
939
938
|
offline:descriptors.offline.value,
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
:{})
|
|
939
|
+
execution:normalizeBrowserSpeechExecution(
|
|
940
|
+
role,
|
|
941
|
+
descriptors.execution?.value
|
|
942
|
+
)
|
|
945
943
|
});
|
|
946
944
|
}
|
|
947
945
|
|
|
@@ -2770,10 +2768,10 @@ class AI {
|
|
|
2770
2768
|
?{artifactGraphId:catalog.artifactGraphId}
|
|
2771
2769
|
:{}),
|
|
2772
2770
|
offline:configured.offline,
|
|
2771
|
+
execution:configured.execution,
|
|
2773
2772
|
...(role==='tts'
|
|
2774
2773
|
?{
|
|
2775
|
-
defaultVoice:catalog.defaultVoice
|
|
2776
|
-
execution:configured.execution
|
|
2774
|
+
defaultVoice:catalog.defaultVoice
|
|
2777
2775
|
}
|
|
2778
2776
|
:{})
|
|
2779
2777
|
});
|
|
@@ -3145,7 +3143,7 @@ class AI {
|
|
|
3145
3143
|
}),
|
|
3146
3144
|
store,
|
|
3147
3145
|
offline:configured.offline,
|
|
3148
|
-
|
|
3146
|
+
execution:configured.execution
|
|
3149
3147
|
});
|
|
3150
3148
|
}
|
|
3151
3149
|
providers=completeValue({...candidateProviders});
|