arcane-os 0.7.0 → 0.7.2

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,28 @@
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>“Force High Performance GPU” prefers your faster GPU.</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
+ <p>Paste a copied address into your browser's address bar. Enable the flag, save your work, and relaunch.</p>
65
+ <details>
66
+ <summary>Setup help</summary>
67
+ <p><a href="https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html">WebNN setup documentation</a></p>
68
+ </details>
50
69
  <p id="copyStatus" class="arcane-help" role="status" aria-live="polite"></p>
51
70
  </div>
52
71
  <footer class="arcane-card__footer">
53
- <button id="refreshAvailability" class="arcane-button arcane-button--secondary" type="button">Refresh browser availability</button>
72
+ <button id="refreshAvailability" class="arcane-button arcane-button--secondary" type="button">Refresh</button>
54
73
  <p id="setupStatus" class="arcane-help" role="status" aria-live="polite"></p>
55
74
  </footer>
56
75
  </section>
@@ -63,9 +82,8 @@
63
82
  const {
64
83
  getBrowserDeviceSettings,
65
84
  detectBrowserGpu,
66
- openBrowserDeviceSettings,
67
85
  WEBNN_BROWSER_SETTINGS
68
- }=await import('../sdk/ai/browser-device-settings.mjs');
86
+ } = await import('../sdk/ai/browser-device-settings.mjs');
69
87
  const {arcaneLogging}=await import('arcane-os/logging');
70
88
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
71
89
  const events=createArcaneEventSource(host,{
@@ -77,18 +95,12 @@
77
95
  const copyStatus=root.querySelector('#copyStatus');
78
96
  const gpuAvailability=root.querySelector('#gpuAvailability');
79
97
  const gpuAdapterName=root.querySelector('#gpuAdapterName');
80
- const detectGpuButton=root.querySelector('#detectGpu');
81
- const gpuFlagsLink=root.querySelector('#gpuFlagsLink');
98
+ const refreshButton = root.querySelector('#refreshAvailability');
82
99
  const copyGpuButton=root.querySelector('#copyGpuAddress');
83
100
  const copyButtons=[copyGpuButton];
84
101
  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
102
  let copyTask=null;
103
+ let copyingAddress = null;
92
104
  let gpuTask=null;
93
105
  let destroyed=false;
94
106
 
@@ -97,152 +109,154 @@
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
+ const unknownBrowser = settings.browserId === 'unknown';
119
+ root.querySelector('#browserInstructions').textContent = settings.webnnFlagsURL || unknownBrowser
120
+ ? '“Enables WebNN API” allows NPU use when supported.'
121
+ : 'See Setup help for this browser.';
122
+ for (const choice of choices) {
123
+ choice.row.hidden = !unknownBrowser && choice.target.url !== settings.webnnFlagsURL;
124
+ choice.heading.hidden = !unknownBrowser;
131
125
  }
126
+ renderGpuGuidance(settings);
132
127
  return settings;
133
128
  }
134
129
 
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
- });
130
+ function renderGpuGuidance(settings = getBrowserDeviceSettings()) {
131
+ root.querySelector('#gpuSetup').hidden = !settings.highPerformanceGpu;
132
+ }
133
+
134
+ function checkGpu() {
135
+ if (destroyed) return Promise.resolve(false);
136
+ if (gpuTask) return gpuTask;
137
+ gpuAvailability.textContent = 'Detecting GPU...';
138
+ gpuAvailability.dataset.status = 'pending';
139
+ gpuAdapterName.textContent = '';
140
+ refreshButton.disabled = true;
141
+ gpuTask = detect().finally(
142
+ function finishGpuDetection() {
143
+ gpuTask = null;
144
+ if (!destroyed) refreshButton.disabled = false;
145
+ }
146
+ );
146
147
  return gpuTask;
147
148
 
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:'';
149
+ async function detect() {
150
+ try {
151
+ const result = await detectBrowserGpu();
152
+ if (destroyed) return false;
153
+ if (!result.available) {
154
+ gpuAvailability.textContent = 'GPU unavailable.';
155
+ gpuAvailability.dataset.status = 'warning';
156
+ } else if (result.performanceStatus === 'discrete') {
157
+ gpuAvailability.textContent = 'Already using the performance GPU.';
158
+ gpuAvailability.dataset.status = 'success';
159
+ } else if (result.performanceStatus === 'integrated') {
160
+ gpuAvailability.textContent = 'Integrated GPU available.';
161
+ gpuAvailability.dataset.status = 'warning';
162
+ } else if (result.performanceStatus === 'fallback') {
163
+ gpuAvailability.textContent = 'Software adapter available.';
164
+ gpuAvailability.dataset.status = 'warning';
165
+ } else {
166
+ gpuAvailability.textContent = 'GPU available.';
167
+ gpuAvailability.dataset.status = 'unknown';
168
+ }
169
+ gpuAdapterName.textContent = result.available ? result.name : '';
160
170
  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);
171
+ } catch (error) {
172
+ if (!destroyed) {
173
+ gpuAvailability.textContent = 'GPU detection failed. Select Refresh to retry.';
174
+ gpuAvailability.dataset.status = 'error';
175
+ arcaneLogging.error('Unable to detect the browser GPU adapter.', error);
166
176
  }
167
177
  return false;
168
178
  }
169
179
  }
170
180
  }
171
181
 
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}`
182
+ function open(browserId) {
183
+ if (destroyed) return Promise.resolve(false);
184
+ const current = refresh();
185
+ const selectedId = browserId ?? current.browserId;
186
+ const target = WEBNN_BROWSER_SETTINGS.find(
187
+ function matchSetupBrowser(browser) {
188
+ return browser.browserId === selectedId;
189
+ }
189
190
  );
190
- status.textContent='If the flags page did not open, copy its address above and paste it into your browser\'s address bar.';
191
+ if (!target) {
192
+ status.textContent = current.browserId === 'unknown'
193
+ ? 'Choose Chrome or Edge to copy its NPU flag address.'
194
+ : 'No NPU flag address for this browser.';
195
+ return Promise.resolve(false);
196
+ }
197
+ return copyAddress(target.url);
191
198
  }
192
199
 
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;
200
+ function copyAddress(address) {
201
+ if (destroyed) return Promise.resolve(false);
202
+ if (copyTask) return copyingAddress === address ? copyTask : Promise.resolve(false);
203
+ copyStatus.textContent = 'Copying address...';
204
+ for (const button of copyButtons) {
205
+ button.disabled = true;
206
+ }
207
+ copyingAddress = address;
208
+ copyTask = writeAddress().finally(
209
+ function finishAddressCopy() {
210
+ copyTask = null;
211
+ copyingAddress = null;
212
+ if (!destroyed) {
213
+ for (const button of copyButtons) {
214
+ button.disabled = false;
215
+ }
216
+ }
202
217
  }
203
- });
218
+ );
204
219
  return copyTask;
205
220
 
206
- async function writeAddress(){
207
- try{
221
+ async function writeAddress() {
222
+ try {
208
223
  await globalThis.navigator.clipboard.writeText(address);
209
- if(destroyed)return false;
210
- copyStatus.textContent='Address copied. Paste it into your browser\'s address bar.';
224
+ if (destroyed) return false;
225
+ copyStatus.textContent = 'Address copied. Paste it into your browser\'s address bar.';
211
226
  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);
227
+ } catch (error) {
228
+ if (!destroyed) {
229
+ copyStatus.textContent = `Copy failed. Paste this address into your browser's address bar: ${address}`;
230
+ arcaneLogging.error('Unable to copy the browser flags address.', error);
216
231
  }
217
232
  return false;
218
233
  }
219
234
  }
220
235
  }
221
236
 
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);
237
+ const addressList = root.querySelector('#flagAddresses');
238
+ for (const target of WEBNN_BROWSER_SETTINGS) {
239
+ const row = document.createElement('section');
240
+ const heading = document.createElement('h4');
241
+ const copyButton = document.createElement('button');
242
+ row.className = 'flag-choice';
243
+ heading.textContent = target.name;
244
+ copyButton.type = 'button';
245
+ copyButton.className = 'arcane-button arcane-button--secondary';
246
+ copyButton.textContent = 'Copy NPU flag address';
247
+ copyButton.setAttribute('aria-label', `Copy NPU flag address for ${target.name}`);
248
+ copyButton.addEventListener(
249
+ 'click',
250
+ function copySelectedBrowserAddress() {
251
+ return copyAddress(target.url);
252
+ },
253
+ {signal: lifecycleController.signal}
254
+ );
255
+ row.append(heading, copyButton);
244
256
  addressList.append(row);
245
- choices.push({row,target});
257
+ choices.push(
258
+ {row, heading, target}
259
+ );
246
260
  copyButtons.push(copyButton);
247
261
  }
248
262
 
@@ -250,19 +264,15 @@
250
264
  const target=getBrowserDeviceSettings().highPerformanceGpu;
251
265
  if(target)return copyAddress(target.url);
252
266
  },{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});
267
+ refreshButton.addEventListener(
268
+ 'click',
269
+ function refreshBrowserAvailability() {
270
+ refresh();
271
+ status.textContent = '';
272
+ return checkGpu();
273
+ },
274
+ {signal: lifecycleController.signal}
275
+ );
266
276
  host.ownerDocument.defaultView.addEventListener('pagehide',function disposeBeforePageUnload(event){
267
277
  if(!event.persisted)destroy();
268
278
  },{
@@ -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
  }