arcane-os 0.7.0 → 0.7.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.
@@ -1,18 +1,46 @@
1
1
  <link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
2
2
  <style>
3
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,a{overflow-wrap:anywhere}
12
- button{white-space:normal}
13
- .flag-address{display:block;white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;margin-block:var(--arcane-space-2)}
14
- #gpuAvailability{white-space:normal;overflow-wrap:anywhere;line-height:1.4;max-width:100%;box-sizing:border-box}
15
- li{margin-block:var(--arcane-space-2)}
4
+ .flag-choice {
5
+ margin-block: var(--arcane-space-2);
6
+ }
7
+
8
+ .flag-choice h4 {
9
+ margin-block: var(--arcane-space-1);
10
+ }
11
+
12
+ p,
13
+ button,
14
+ a {
15
+ overflow-wrap: anywhere;
16
+ }
17
+
18
+ button {
19
+ white-space: normal;
20
+ }
21
+
22
+ .arcane-help:empty {
23
+ display: none;
24
+ }
25
+
26
+ .gpu-status {
27
+ box-sizing: border-box;
28
+ max-width: 100%;
29
+ line-height: 1.4;
30
+ white-space: normal;
31
+ overflow-wrap: anywhere;
32
+ }
33
+
34
+ .gpu-status[data-status="unknown"] {
35
+ background: var(--arcane-action);
36
+ border-color: var(--arcane-action);
37
+ color: var(--arcane-action-text);
38
+ }
39
+
40
+ summary:focus-visible {
41
+ outline: .18em solid var(--arcane-focus);
42
+ outline-offset: .18em;
43
+ }
16
44
  </style>
17
45
 
18
46
  <section class="arcane-card" aria-labelledby="setupTitle">
@@ -20,37 +48,27 @@
20
48
  <h2 id="setupTitle" class="arcane-section-heading">Browser AI acceleration</h2>
21
49
  </header>
22
50
  <div class="arcane-card__body">
23
- <p id="browserName"></p>
24
- <dl class="availability" aria-live="polite">
25
- <div><dt>WebNN API</dt><dd id="webnnAvailability"></dd></div>
26
- <div><dt>WebGPU API</dt><dd id="webgpuAvailability"></dd></div>
27
- </dl>
28
- <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>
29
- <h3>NPU setup</h3>
30
- <p id="browserInstructions"></p>
31
- <div id="flagAddresses"></div>
32
- <ol id="setupSteps"></ol>
33
- <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>
34
- <p><a href="https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html">ONNX Runtime WebNN setup documentation</a></p>
35
- <h3>GPU performance</h3>
36
- <p><span id="gpuAvailability" class="arcane-badge" role="status" aria-live="polite"></span></p>
51
+ <h3>NPU</h3>
52
+ <p id="webnnAvailability" role="status" aria-live="polite"></p>
53
+ <div id="npuSetup">
54
+ <p id="browserInstructions"></p>
55
+ <div id="flagAddresses"></div>
56
+ </div>
57
+ <h3>GPU</h3>
58
+ <p><span id="gpuAvailability" class="arcane-badge gpu-status" role="status" aria-live="polite"></span></p>
37
59
  <p id="gpuAdapterName" class="arcane-help"></p>
38
- <button id="detectGpu" class="arcane-button arcane-button--secondary" type="button">Detect GPU</button>
39
- <p id="gpuInstructions"></p>
40
- <div id="gpuFlagAddress">
41
- <a id="gpuFlagsLink" class="flag-address"></a>
42
- <button id="copyGpuAddress" class="arcane-button arcane-button--secondary" type="button">Copy GPU flags address</button>
43
- <ol>
44
- <li>Open the address above. If the browser blocks the link, copy the address and paste it into the address bar.</li>
45
- <li>Find “Force High Performance GPU” and choose Enabled.</li>
46
- <li>Save your work, then completely close and reopen the browser before loading the model again.</li>
47
- </ol>
60
+ <div id="gpuSetup" hidden>
61
+ <p>If you have a discrete GPU, enable “Force High Performance GPU”, save your work, and restart the browser.</p>
62
+ <button id="copyGpuAddress" class="arcane-button arcane-button--secondary" type="button">Copy GPU flag address</button>
48
63
  </div>
49
- <p class="arcane-help">GPU detection reports adapter availability. This page cannot read the flag's current setting or confirm that a model is using that adapter.</p>
64
+ <details>
65
+ <summary>Setup help</summary>
66
+ <p><a href="https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html">WebNN setup documentation</a></p>
67
+ </details>
50
68
  <p id="copyStatus" class="arcane-help" role="status" aria-live="polite"></p>
51
69
  </div>
52
70
  <footer class="arcane-card__footer">
53
- <button id="refreshAvailability" class="arcane-button arcane-button--secondary" type="button">Refresh browser availability</button>
71
+ <button id="refreshAvailability" class="arcane-button arcane-button--secondary" type="button">Refresh</button>
54
72
  <p id="setupStatus" class="arcane-help" role="status" aria-live="polite"></p>
55
73
  </footer>
56
74
  </section>
@@ -63,9 +81,8 @@
63
81
  const {
64
82
  getBrowserDeviceSettings,
65
83
  detectBrowserGpu,
66
- openBrowserDeviceSettings,
67
84
  WEBNN_BROWSER_SETTINGS
68
- }=await import('../sdk/ai/browser-device-settings.mjs');
85
+ } = await import('../sdk/ai/browser-device-settings.mjs');
69
86
  const {arcaneLogging}=await import('arcane-os/logging');
70
87
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
71
88
  const events=createArcaneEventSource(host,{
@@ -77,19 +94,14 @@
77
94
  const copyStatus=root.querySelector('#copyStatus');
78
95
  const gpuAvailability=root.querySelector('#gpuAvailability');
79
96
  const gpuAdapterName=root.querySelector('#gpuAdapterName');
80
- const detectGpuButton=root.querySelector('#detectGpu');
81
- const gpuFlagsLink=root.querySelector('#gpuFlagsLink');
97
+ const refreshButton = root.querySelector('#refreshAvailability');
82
98
  const copyGpuButton=root.querySelector('#copyGpuAddress');
83
99
  const copyButtons=[copyGpuButton];
84
100
  const choices=[];
85
- const setupSteps=[
86
- 'If the flags page does not open, copy the matching address into your browser\'s address bar and open it.',
87
- 'Find “Enables WebNN API” and choose Enabled.',
88
- 'Save your work, then use the browser\'s Relaunch control.',
89
- 'Return to this page and select Refresh browser availability.'
90
- ];
91
101
  let copyTask=null;
102
+ let copyingAddress = null;
92
103
  let gpuTask=null;
104
+ let gpuResult = null;
93
105
  let destroyed=false;
94
106
 
95
107
  host.refresh=refresh;
@@ -97,152 +109,164 @@
97
109
  host.open=open;
98
110
  host.destroy=destroy;
99
111
 
100
- function refresh(){
101
- if(destroyed)return false;
102
- const settings=getBrowserDeviceSettings();
103
- root.querySelector('#browserName').textContent=settings.browserId==='unknown'
104
- ?'Browser not identified.'
105
- :`Browser: ${settings.name}.`;
106
- root.querySelector('#webnnAvailability').textContent=settings.webnnAvailable
107
- ?'Exposed to this page.'
108
- :'Unavailable to this page.';
109
- root.querySelector('#webgpuAvailability').textContent=settings.webgpuAvailable
110
- ?'Exposed to this page.'
111
- :'Unavailable to this page.';
112
- root.querySelector('#browserInstructions').textContent=settings.webnnFlagsURL
113
- ?`Open the ${settings.name} WebNN flags link below to set up NPU access.`
114
- :'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.';
115
- for(const choice of choices){
116
- choice.row.hidden=Boolean(settings.webnnFlagsURL)
117
- &&choice.target.url!==settings.webnnFlagsURL;
118
- }
119
- const gpuTarget=settings.highPerformanceGpu;
120
- root.querySelector('#gpuFlagAddress').hidden=!gpuTarget;
121
- root.querySelector('#gpuInstructions').textContent=gpuTarget
122
- ?`If this computer has multiple GPUs, use the ${gpuTarget.name} flags link to request the high-performance GPU when available.`
123
- :'The Force High Performance GPU flag is documented here for desktop Windows Chromium browsers. No matching flags link is known for this browser and platform.';
124
- if(gpuTarget){
125
- gpuFlagsLink.href=gpuTarget.url;
126
- gpuFlagsLink.textContent=gpuTarget.url;
127
- gpuFlagsLink.setAttribute('aria-label',`${gpuTarget.name} Force High Performance GPU flags address: ${gpuTarget.url}`);
128
- }else{
129
- gpuFlagsLink.removeAttribute('href');
130
- gpuFlagsLink.textContent='';
112
+ function refresh() {
113
+ if (destroyed) return false;
114
+ const settings = getBrowserDeviceSettings();
115
+ root.querySelector('#webnnAvailability').textContent = settings.webnnAvailable
116
+ ? 'WebNN available.'
117
+ : 'WebNN unavailable.';
118
+ root.querySelector('#npuSetup').hidden = settings.webnnAvailable;
119
+ const unknownBrowser = settings.browserId === 'unknown';
120
+ root.querySelector('#browserInstructions').textContent = settings.webnnFlagsURL || unknownBrowser
121
+ ? 'Enable “Enables WebNN API”, save your work, and relaunch the browser.'
122
+ : 'See Setup help for this browser.';
123
+ for (const choice of choices) {
124
+ choice.row.hidden = !unknownBrowser && choice.target.url !== settings.webnnFlagsURL;
125
+ choice.heading.hidden = !unknownBrowser;
131
126
  }
127
+ renderGpuGuidance(settings);
132
128
  return settings;
133
129
  }
134
130
 
135
- function checkGpu(){
136
- if(destroyed)return Promise.resolve(false);
137
- if(gpuTask)return gpuTask;
138
- gpuAvailability.textContent='Detecting GPU...';
139
- gpuAvailability.dataset.status='pending';
140
- gpuAdapterName.textContent='';
141
- detectGpuButton.disabled=true;
142
- gpuTask=detect().finally(function finishGpuDetection(){
143
- gpuTask=null;
144
- if(!destroyed)detectGpuButton.disabled=false;
145
- });
131
+ function renderGpuGuidance(settings = getBrowserDeviceSettings()) {
132
+ const gpuTarget = settings.highPerformanceGpu;
133
+ const nonperformance = gpuResult?.available && (
134
+ gpuResult.performanceStatus === 'integrated'
135
+ || gpuResult.performanceStatus === 'fallback'
136
+ );
137
+ root.querySelector('#gpuSetup').hidden = !nonperformance || !gpuTarget;
138
+ }
139
+
140
+ function checkGpu() {
141
+ if (destroyed) return Promise.resolve(false);
142
+ if (gpuTask) return gpuTask;
143
+ gpuAvailability.textContent = 'Detecting GPU...';
144
+ gpuAvailability.dataset.status = 'pending';
145
+ gpuAdapterName.textContent = '';
146
+ refreshButton.disabled = true;
147
+ gpuTask = detect().finally(
148
+ function finishGpuDetection() {
149
+ gpuTask = null;
150
+ if (!destroyed) refreshButton.disabled = false;
151
+ }
152
+ );
146
153
  return gpuTask;
147
154
 
148
- async function detect(){
149
- try{
150
- const result=await detectBrowserGpu();
151
- if(destroyed)return false;
152
- gpuAvailability.textContent=result.available
153
- ?result.isFallbackAdapter===true
154
- ?'Fallback WebGPU adapter detected.'
155
- :'GPU detected — WebGPU adapter available.'
156
- :'No WebGPU adapter available to this page.';
157
- gpuAvailability.dataset.status=result.available&&result.isFallbackAdapter!==true
158
- ?'success':'warning';
159
- gpuAdapterName.textContent=result.available?result.name:'';
155
+ async function detect() {
156
+ try {
157
+ const result = await detectBrowserGpu();
158
+ if (destroyed) return false;
159
+ gpuResult = result;
160
+ if (!result.available) {
161
+ gpuAvailability.textContent = 'GPU unavailable.';
162
+ gpuAvailability.dataset.status = 'warning';
163
+ } else if (result.performanceStatus === 'discrete') {
164
+ gpuAvailability.textContent = 'Already using the performance GPU.';
165
+ gpuAvailability.dataset.status = 'success';
166
+ } else if (result.performanceStatus === 'integrated') {
167
+ gpuAvailability.textContent = 'Integrated GPU available.';
168
+ gpuAvailability.dataset.status = 'warning';
169
+ } else if (result.performanceStatus === 'fallback') {
170
+ gpuAvailability.textContent = 'Software adapter available.';
171
+ gpuAvailability.dataset.status = 'warning';
172
+ } else {
173
+ gpuAvailability.textContent = 'GPU available.';
174
+ gpuAvailability.dataset.status = 'unknown';
175
+ }
176
+ gpuAdapterName.textContent = result.available ? result.name : '';
177
+ renderGpuGuidance();
160
178
  return result;
161
- }catch(error){
162
- if(!destroyed){
163
- gpuAvailability.textContent='GPU detection failed. Select Detect GPU to try again.';
164
- gpuAvailability.dataset.status='error';
165
- arcaneLogging.error('Unable to detect the browser GPU adapter.',error);
179
+ } catch (error) {
180
+ if (!destroyed) {
181
+ gpuResult = null;
182
+ gpuAvailability.textContent = 'GPU detection failed. Select Refresh to retry.';
183
+ gpuAvailability.dataset.status = 'error';
184
+ renderGpuGuidance();
185
+ arcaneLogging.error('Unable to detect the browser GPU adapter.', error);
166
186
  }
167
187
  return false;
168
188
  }
169
189
  }
170
190
  }
171
191
 
172
- function open(browserId){
173
- if(destroyed)return false;
174
- const current=refresh();
175
- const selectedId=browserId??current.browserId;
176
- const target=WEBNN_BROWSER_SETTINGS.find(function matchSetupBrowser(browser){
177
- return browser.browserId===selectedId;
178
- });
179
- if(!target){
180
- status.textContent='Choose the Google Chrome or Microsoft Edge flags link that matches your browser.';
181
- return false;
182
- }
183
- const instructions=setupSteps.map(function numberSetupStep(step,index){
184
- return `${index+1}. ${step}`;
185
- }).join('\n');
186
- openBrowserDeviceSettings(
187
- target,
188
- `NPU setup for ${target.name}\n\n${instructions}\n\n${target.url}`
192
+ function open(browserId) {
193
+ if (destroyed) return Promise.resolve(false);
194
+ const current = refresh();
195
+ const selectedId = browserId ?? current.browserId;
196
+ const target = WEBNN_BROWSER_SETTINGS.find(
197
+ function matchSetupBrowser(browser) {
198
+ return browser.browserId === selectedId;
199
+ }
189
200
  );
190
- status.textContent='If the flags page did not open, copy its address above and paste it into your browser\'s address bar.';
201
+ if (!target) {
202
+ status.textContent = current.browserId === 'unknown'
203
+ ? 'Choose Chrome or Edge to copy its NPU flag address.'
204
+ : 'No NPU flag address for this browser.';
205
+ return Promise.resolve(false);
206
+ }
207
+ return copyAddress(target.url);
191
208
  }
192
209
 
193
- function copyAddress(address){
194
- if(destroyed)return Promise.resolve(false);
195
- if(copyTask)return copyTask;
196
- copyStatus.textContent='Copying address...';
197
- for(const button of copyButtons)button.disabled=true;
198
- copyTask=writeAddress().finally(function finishAddressCopy(){
199
- copyTask=null;
200
- if(!destroyed){
201
- for(const button of copyButtons)button.disabled=false;
210
+ function copyAddress(address) {
211
+ if (destroyed) return Promise.resolve(false);
212
+ if (copyTask) return copyingAddress === address ? copyTask : Promise.resolve(false);
213
+ copyStatus.textContent = 'Copying address...';
214
+ for (const button of copyButtons) {
215
+ button.disabled = true;
216
+ }
217
+ copyingAddress = address;
218
+ copyTask = writeAddress().finally(
219
+ function finishAddressCopy() {
220
+ copyTask = null;
221
+ copyingAddress = null;
222
+ if (!destroyed) {
223
+ for (const button of copyButtons) {
224
+ button.disabled = false;
225
+ }
226
+ }
202
227
  }
203
- });
228
+ );
204
229
  return copyTask;
205
230
 
206
- async function writeAddress(){
207
- try{
231
+ async function writeAddress() {
232
+ try {
208
233
  await globalThis.navigator.clipboard.writeText(address);
209
- if(destroyed)return false;
210
- copyStatus.textContent='Address copied. Paste it into your browser\'s address bar.';
234
+ if (destroyed) return false;
235
+ copyStatus.textContent = 'Address copied. Paste it into your browser\'s address bar.';
211
236
  return true;
212
- }catch(error){
213
- if(!destroyed){
214
- 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.';
215
- arcaneLogging.error('Unable to copy the browser flags address.',error);
237
+ } catch (error) {
238
+ if (!destroyed) {
239
+ copyStatus.textContent = `Copy failed. Paste this address into your browser's address bar: ${address}`;
240
+ arcaneLogging.error('Unable to copy the browser flags address.', error);
216
241
  }
217
242
  return false;
218
243
  }
219
244
  }
220
245
  }
221
246
 
222
- const addressList=root.querySelector('#flagAddresses');
223
- for(const target of WEBNN_BROWSER_SETTINGS){
224
- const row=document.createElement('section');
225
- const heading=document.createElement('h3');
226
- const address=document.createElement('a');
227
- const actions=document.createElement('div');
228
- const copyButton=document.createElement('button');
229
- row.className='flag-choice';
230
- heading.textContent=target.name;
231
- address.className='flag-address';
232
- address.href=target.url;
233
- address.textContent=target.url;
234
- address.setAttribute('aria-label',`${target.name} WebNN flags address: ${target.url}`);
235
- actions.className='actions';
236
- copyButton.type='button';
237
- copyButton.className='arcane-button arcane-button--secondary';
238
- copyButton.textContent=`Copy ${target.name} address`;
239
- copyButton.addEventListener('click',function copySelectedBrowserAddress(){
240
- return copyAddress(target.url);
241
- },{signal:lifecycleController.signal});
242
- actions.append(copyButton);
243
- row.append(heading,address,actions);
247
+ const addressList = root.querySelector('#flagAddresses');
248
+ for (const target of WEBNN_BROWSER_SETTINGS) {
249
+ const row = document.createElement('section');
250
+ const heading = document.createElement('h4');
251
+ const copyButton = document.createElement('button');
252
+ row.className = 'flag-choice';
253
+ heading.textContent = target.name;
254
+ copyButton.type = 'button';
255
+ copyButton.className = 'arcane-button arcane-button--secondary';
256
+ copyButton.textContent = 'Copy NPU flag address';
257
+ copyButton.setAttribute('aria-label', `Copy NPU flag address for ${target.name}`);
258
+ copyButton.addEventListener(
259
+ 'click',
260
+ function copySelectedBrowserAddress() {
261
+ return copyAddress(target.url);
262
+ },
263
+ {signal: lifecycleController.signal}
264
+ );
265
+ row.append(heading, copyButton);
244
266
  addressList.append(row);
245
- choices.push({row,target});
267
+ choices.push(
268
+ {row, heading, target}
269
+ );
246
270
  copyButtons.push(copyButton);
247
271
  }
248
272
 
@@ -250,19 +274,15 @@
250
274
  const target=getBrowserDeviceSettings().highPerformanceGpu;
251
275
  if(target)return copyAddress(target.url);
252
276
  },{signal:lifecycleController.signal});
253
- detectGpuButton.addEventListener('click',checkGpu,{signal:lifecycleController.signal});
254
- const stepList=root.querySelector('#setupSteps');
255
- for(const step of setupSteps){
256
- const item=document.createElement('li');
257
- item.textContent=step;
258
- stepList.append(item);
259
- }
260
-
261
- root.querySelector('#refreshAvailability').addEventListener('click',function refreshBrowserAvailability(){
262
- refresh();
263
- status.textContent='Browser API availability refreshed.';
264
- return checkGpu();
265
- },{signal:lifecycleController.signal});
277
+ refreshButton.addEventListener(
278
+ 'click',
279
+ function refreshBrowserAvailability() {
280
+ refresh();
281
+ status.textContent = '';
282
+ return checkGpu();
283
+ },
284
+ {signal: lifecycleController.signal}
285
+ );
266
286
  host.ownerDocument.defaultView.addEventListener('pagehide',function disposeBeforePageUnload(event){
267
287
  if(!event.persisted)destroy();
268
288
  },{
@@ -80,9 +80,12 @@
80
80
  display: none;
81
81
  }
82
82
 
83
- .speech_status {
83
+ .speech_feedback {
84
84
  grid-column: 2;
85
85
  min-width: 0;
86
+ }
87
+
88
+ .speech_status {
86
89
  margin: 0;
87
90
  color: var(--arcane-muted, var(--text-color));
88
91
  font-size: .86rem;
@@ -94,6 +97,18 @@
94
97
  color: var(--arcane-danger, var(--danger-color));
95
98
  }
96
99
 
100
+ .stt_activation_progress {
101
+ display: block;
102
+ width: 100%;
103
+ height: .6rem;
104
+ margin-block-start: .7rem;
105
+ accent-color: var(--arcane-action, var(--primary-color));
106
+ }
107
+
108
+ .stt_activation_progress[hidden] {
109
+ display: none;
110
+ }
111
+
97
112
  #muteButton {
98
113
  grid-column: 3;
99
114
  justify-self: end;
@@ -112,7 +127,7 @@
112
127
  grid-row: 1 / span 2;
113
128
  }
114
129
 
115
- .speech_status {
130
+ .speech_feedback {
116
131
  grid-row: 2;
117
132
  grid-column: 2 / span 2;
118
133
  }
@@ -128,7 +143,15 @@
128
143
  <button id="recordButton" type="button" aria-label="Transcription unavailable" disabled>Hold to talk</button>
129
144
  <button id="sttActivationButton" type="button" hidden disabled>Start transcription</button>
130
145
  <button id="stopButton" type="button" class="hidden" disabled>Send</button>
131
- <p id="speechStatus" class="speech_status" role="status" aria-live="polite"></p>
146
+ <div class="speech_feedback">
147
+ <p id="speechStatus" class="speech_status" role="status" aria-live="polite"></p>
148
+ <progress
149
+ id="sttActivationProgress"
150
+ class="stt_activation_progress"
151
+ aria-label="Transcription model activation progress"
152
+ hidden
153
+ ></progress>
154
+ </div>
132
155
  <button id="muteButton" type="button" class="mute" aria-pressed="true" aria-label="Voice unavailable" disabled>Voice unavailable</button>
133
156
 
134
157
  <script type="module">
@@ -179,6 +202,7 @@
179
202
  const shadowRoot = host.shadowRoot;
180
203
  const recordButton = shadowRoot.querySelector('#recordButton');
181
204
  const sttActivationButton = shadowRoot.querySelector('#sttActivationButton');
205
+ const sttActivationProgress = shadowRoot.querySelector('#sttActivationProgress');
182
206
  const stopButton = shadowRoot.querySelector('#stopButton');
183
207
  const muteButton = shadowRoot.querySelector('#muteButton');
184
208
  const speechStatus = shadowRoot.querySelector('#speechStatus');
@@ -319,6 +343,7 @@
319
343
  {
320
344
  host,
321
345
  button: sttActivationButton,
346
+ progress: sttActivationProgress,
322
347
  onChange: renderSTTActivationState,
323
348
  eventSource: events
324
349
  }
@@ -668,6 +693,12 @@
668
693
  if (localStatus?.message) {
669
694
  messages.push(localStatus.message);
670
695
  tone = localStatus.tone;
696
+ if (sttActivationController.pending
697
+ || ['loading', 'unloading'].includes(sttRole.state)) {
698
+ messages.push(
699
+ describeSTTRole()
700
+ );
701
+ }
671
702
  } else {
672
703
  messages.push(describeSTTRole());
673
704
  }
@@ -42,15 +42,40 @@
42
42
  }
43
43
 
44
44
  .description,
45
- .status{
46
- margin:0;
45
+ .status {
46
+ margin: 0;
47
+ overflow-wrap: anywhere;
48
+ }
49
+
50
+ .transcription-feedback {
51
+ min-width: 0;
52
+ }
53
+
54
+ .stt-activation-progress {
55
+ display: block;
56
+ width: 100%;
57
+ height: .6rem;
58
+ margin-block-start: .7rem;
59
+ accent-color: var(--arcane-action, var(--primary-color));
60
+ }
61
+
62
+ .stt-activation-progress[hidden] {
63
+ display: none;
47
64
  }
48
65
  </style>
49
66
 
50
67
  <section class="voice-transcription">
51
68
  <p id="description" class="description"></p>
52
69
  <section id="transcript" class="transcript"></section>
53
- <p id="status" class="status" role="status" aria-live="polite"></p>
70
+ <div class="transcription-feedback">
71
+ <p id="status" class="status" role="status" aria-live="polite"></p>
72
+ <progress
73
+ id="sttActivationProgress"
74
+ class="stt-activation-progress"
75
+ aria-label="Transcription model activation progress"
76
+ hidden
77
+ ></progress>
78
+ </div>
54
79
  <section class="controls">
55
80
  <button id="start" type="button" disabled></button>
56
81
  <button id="sttActivation" type="button" hidden disabled>Start transcription</button>
@@ -104,6 +129,7 @@
104
129
  const status=host.shadowRoot.querySelector('#status');
105
130
  const startButton=host.shadowRoot.querySelector('#start');
106
131
  const sttActivationButton=host.shadowRoot.querySelector('#sttActivation');
132
+ const sttActivationProgress = host.shadowRoot.querySelector('#sttActivationProgress');
107
133
  const stopButton=host.shadowRoot.querySelector('#stop');
108
134
  const completeButton=host.shadowRoot.querySelector('#complete');
109
135
  const runtimeStateAbortController=new AbortController();
@@ -225,6 +251,7 @@
225
251
  {
226
252
  host,
227
253
  button:sttActivationButton,
254
+ progress: sttActivationProgress,
228
255
  onChange:renderState,
229
256
  eventSource:events
230
257
  }