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.
@@ -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 DEFAULT_BROWSER_TTS_EXECUTION={
31
- device:'auto',
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 normalizeBrowserTTSExecution(value){
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({...DEFAULT_BROWSER_TTS_EXECUTION});
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
- 'AI browser speech tts.execution'
832
+ label
829
833
  );
830
834
  const device=descriptors.device
831
835
  ?descriptors.device.value
832
- :DEFAULT_BROWSER_TTS_EXECUTION.device;
836
+ :DEFAULT_BROWSER_SPEECH_EXECUTION_DEVICE;
833
837
  const maxConcurrentRequests=descriptors.maxConcurrentRequests
834
838
  ?descriptors.maxConcurrentRequests.value
835
- :DEFAULT_BROWSER_TTS_EXECUTION.maxConcurrentRequests;
836
- if(!is.string(device)||!BROWSER_TTS_EXECUTION_DEVICES.has(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
- 'AI browser speech tts.execution.device must be auto, webgpu, or wasm.'
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>MAX_BROWSER_TTS_CONCURRENT_REQUESTS){
849
+ ||maxConcurrentRequests>maximumConcurrentRequests){
846
850
  throw aiBrowserSpeechError(
847
851
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
848
852
  AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
849
- `AI browser speech tts.execution.maxConcurrentRequests must be an integer from 1 through ${MAX_BROWSER_TTS_CONCURRENT_REQUESTS}.`
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
- ...(role==='tts'
941
- ?{execution:normalizeBrowserTTSExecution(
942
- descriptors.execution?.value
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
- ...(role==='tts'?{execution:configured.execution}:{})
3146
+ execution:configured.execution
3149
3147
  });
3150
3148
  }
3151
3149
  providers=completeValue({...candidateProviders});