arcane-os 0.46.0 → 0.48.0

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 CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.48.0
4
+
5
+ - Add opt-in Chat `transcriptionMode='draft'` and public `appendDraft(text)`.
6
+ Native recognition appears immediately in the editable composer; interim
7
+ replacement preserves typed text and user edits, and completion leaves manual
8
+ Send to the user. Existing consumers retain default completed-transcription
9
+ submission. Conversation language remains separate from recognition language.
10
+ - Add native BCP47 `recognitionLanguage` in the visible shared speech status
11
+ area and cumulative `speech-transcription-progress` events. Preserve model
12
+ transcription wording, TTS controls, gesture-bound capture, final draining,
13
+ cancellation and provider ownership. Continuous voice shows canonical text,
14
+ queued finals and transient interim text together in its existing output;
15
+ durable text and ordered save/retry payloads remain unchanged.
16
+ - Centralize direct-page and rendered-snapshot print styles in `print.css`,
17
+ with a light paper palette and one-inch page margins. Shared screen shells
18
+ expand into document flow only for print. Preserve rendered fonts, complete
19
+ images and SVG artwork, resource readiness and snapshot ownership through
20
+ `afterprint`.
21
+ - Add public Chat `print()` and `printTitle`, using shared PrintView for the
22
+ complete rendered conversation without composer controls, transient thinking
23
+ or tool-inspection details. Preserve human-facing tool messages. A successful
24
+ method result means the browser print request began, not that paper or PDF
25
+ output completed. No saved-data migration or model/provider/default change.
26
+
27
+ ## 0.47.0
28
+
29
+ - Add explicitly selected native browser speech recognition through
30
+ `createBrowserSpeechRecognitionProvider` in `arcane-os/ai/browser-speech`.
31
+ The provider uses the browser's SpeechRecognition capability without an SDK
32
+ model download or storage. It declares non-local behavior because the browser
33
+ may use its own remote recognition service. Applications own Profile selection
34
+ and language; Whisper, TTS, typing and existing defaults remain unchanged.
35
+ - Own live capture through the existing independent STT runtime. Start stays in
36
+ the initiating user gesture; Stop drains final recognition, while cancellation,
37
+ provider replacement and cleanup suppress late results. Unsupported browsers
38
+ retain provider construction/catalog and report unavailability on activation.
39
+ - Display native interim recognition immediately in transient shared controls.
40
+ Chat receives one complete combined final transcription at capture end;
41
+ continuous voice appends and saves confirmed segments in order through its
42
+ existing retryable save queue. Interim hypotheses are neither saved nor
43
+ submitted, and existing saved data is unchanged.
44
+
3
45
  ## 0.46.0
4
46
 
5
47
  - Add public `decodeMarkdownMediaRecord` for complete stored image records or
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
20
20
  and browser run contracts while retaining their selected application layout.
21
21
 
22
- This checkout defines the `0.46.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.48.0` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -0,0 +1,495 @@
1
+ import Is from '../dependencies/strong-type/index.js';
2
+ import {arcaneLogging} from '../logging.mjs';
3
+
4
+ const is = new Is(false);
5
+
6
+ function recognitionError(message, code, cause) {
7
+ const error = cause === undefined
8
+ ? new Error(message)
9
+ : new Error(
10
+ message,
11
+ {cause}
12
+ );
13
+ error.code = code;
14
+ return error;
15
+ }
16
+
17
+ function recognitionConstructor() {
18
+ if (is.function(globalThis.SpeechRecognition)) return globalThis.SpeechRecognition;
19
+ if (is.function(globalThis.webkitSpeechRecognition)) return globalThis.webkitSpeechRecognition;
20
+ return null;
21
+ }
22
+
23
+ function unavailableRecognition() {
24
+ return recognitionError(
25
+ 'Native browser speech recognition is unavailable in this browser.',
26
+ 'ARCANE_AI_SPEECH_RECOGNITION_UNAVAILABLE'
27
+ );
28
+ }
29
+
30
+ function assertIdentifier(value, label) {
31
+ if (!is.string(value) || !value || value.trim() !== value) {
32
+ throw new TypeError(`${label} must be a nonempty string without surrounding whitespace.`);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Browser-owned live recognition, which may use a remote recognition service.
38
+ * Loading selects the capability; only capture.start() opens a native session.
39
+ */
40
+ export function createBrowserSpeechRecognitionProvider(
41
+ {id, model, language} = {}
42
+ ) {
43
+ assertIdentifier(id, 'Browser speech recognition provider id');
44
+ assertIdentifier(model?.id, 'Browser speech recognition model id');
45
+ if (language !== undefined && !is.string(language)) {
46
+ throw new TypeError('Browser speech recognition language must be a string.');
47
+ }
48
+
49
+ const modelId = model.id;
50
+ let state = 'unloaded';
51
+ let generation = 0;
52
+ let activeCapture = null;
53
+ let unloadOperation = null;
54
+ let disposeOperation = null;
55
+
56
+ function authority() {
57
+ return {
58
+ protocol: 'arcane-ai-model-authority/1',
59
+ providerId: id,
60
+ modelId,
61
+ role: 'stt',
62
+ localOnly: false
63
+ };
64
+ }
65
+
66
+ function assertSelection(selection) {
67
+ if (selection && (selection.providerId !== id
68
+ || selection.modelId !== modelId
69
+ || selection.localOnly === true
70
+ || (selection.role !== undefined && selection.role !== 'stt'))) {
71
+ throw recognitionError(
72
+ 'The selected browser speech model does not match this provider.',
73
+ 'ARCANE_AI_MODEL_AUTHORITY_REQUIRED'
74
+ );
75
+ }
76
+ }
77
+
78
+ function assertContext(context) {
79
+ if (context.role !== undefined && context.role !== 'stt') {
80
+ throw recognitionError('This provider supports only STT.', 'ARCANE_AI_INVALID_REQUEST');
81
+ }
82
+ assertSelection(context.selection);
83
+ if (context.signal?.aborted) {
84
+ const error = recognitionError(
85
+ 'The browser speech operation was cancelled.',
86
+ 'ARCANE_AI_REQUEST_ABORTED',
87
+ context.signal.reason
88
+ );
89
+ error.name = 'AbortError';
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ function status() {
95
+ const loaded = state === 'ready';
96
+ return {
97
+ providerId: id,
98
+ role: 'stt',
99
+ modelId,
100
+ localOnly: false,
101
+ state,
102
+ loaded,
103
+ busy: activeCapture !== null,
104
+ execution: {
105
+ requestedDevice: 'browser-native',
106
+ selectedDevice: loaded ? 'browser-native' : null,
107
+ maxConcurrentRequests: 1,
108
+ activeRequestCount: activeCapture === null ? 0 : 1
109
+ }
110
+ };
111
+ }
112
+
113
+ function createCapture(
114
+ {
115
+ onSegment,
116
+ onInterim,
117
+ onState,
118
+ onError,
119
+ language: captureLanguage = language,
120
+ continuous = true
121
+ } = {}
122
+ ) {
123
+ const captureGeneration = generation;
124
+ let recognizer = null;
125
+ let signal = null;
126
+ let started = false;
127
+ let nativeStarted = false;
128
+ let receivedStart = false;
129
+ let stopping = false;
130
+ let cancelled = false;
131
+ let failed = false;
132
+ let settled = false;
133
+ let captureState = null;
134
+ let lastFinalIndex = -1;
135
+ let sequence = 0;
136
+ let resolveStarted = null;
137
+ let resolveDone;
138
+ const done = new Promise(
139
+ function waitForRecognitionEnd(resolve) {
140
+ resolveDone = resolve;
141
+ }
142
+ );
143
+
144
+ function reportError(error) {
145
+ if (!is.function(onError)) {
146
+ arcaneLogging.error('Browser speech recognition failed:', error);
147
+ return;
148
+ }
149
+ try {
150
+ const operation = onError(error);
151
+ Promise.resolve(operation).catch(reportErrorCallbackFailure);
152
+ } catch (callbackError) {
153
+ reportErrorCallbackFailure(callbackError);
154
+ }
155
+ }
156
+
157
+ function reportErrorCallbackFailure(error) {
158
+ arcaneLogging.error('Browser speech recognition error handler failed:', error);
159
+ }
160
+
161
+ function observeCallbackFailure(error) {
162
+ const callbackError = recognitionError(
163
+ 'A browser speech recognition callback failed.',
164
+ 'ARCANE_AI_SPEECH_CALLBACK_FAILED',
165
+ error
166
+ );
167
+ if (settled || cancelled || failed) {
168
+ reportError(callbackError);
169
+ return;
170
+ }
171
+ interrupt(callbackError);
172
+ abortNative();
173
+ }
174
+
175
+ function notify(callback, value) {
176
+ if (!is.function(callback)) return;
177
+ try {
178
+ const operation = callback(value);
179
+ Promise.resolve(operation).catch(observeCallbackFailure);
180
+ } catch (error) {
181
+ observeCallbackFailure(error);
182
+ }
183
+ }
184
+
185
+ function publishState(nextState) {
186
+ if (captureState === nextState) return;
187
+ captureState = nextState;
188
+ notify(onState, nextState);
189
+ }
190
+
191
+ function settleStarted(success) {
192
+ if (!resolveStarted) return;
193
+ const resolve = resolveStarted;
194
+ resolveStarted = null;
195
+ resolve(success);
196
+ }
197
+
198
+ function finish(success) {
199
+ if (settled) return;
200
+ settled = true;
201
+ nativeStarted = false;
202
+ signal?.removeEventListener('abort', cancel);
203
+ signal = null;
204
+ if (recognizer) {
205
+ recognizer.onstart = null;
206
+ recognizer.onresult = null;
207
+ recognizer.onerror = null;
208
+ recognizer.onend = null;
209
+ recognizer = null;
210
+ }
211
+ if (activeCapture === capture) activeCapture = null;
212
+ settleStarted(false);
213
+ publishState(failed ? 'interrupted' : 'stopped');
214
+ resolveDone(success);
215
+ }
216
+
217
+ function interrupt(error) {
218
+ if (failed || settled) return;
219
+ failed = true;
220
+ settleStarted(false);
221
+ reportError(error);
222
+ publishState('interrupted');
223
+ }
224
+
225
+ function abortNative() {
226
+ if (settled) return;
227
+ if (!nativeStarted) {
228
+ finish(false);
229
+ return;
230
+ }
231
+ try {
232
+ recognizer.abort();
233
+ } catch (error) {
234
+ const abortError = recognitionError(
235
+ 'Native browser speech recognition could not be cancelled.',
236
+ 'ARCANE_AI_SPEECH_RECOGNITION_ABORT_FAILED',
237
+ error
238
+ );
239
+ if (failed) reportError(abortError);
240
+ else interrupt(abortError);
241
+ // An unsuccessful abort is not evidence of native end. Keep
242
+ // the session owned until the browser reports disconnection.
243
+ }
244
+ }
245
+
246
+ function cancel() {
247
+ if (settled || cancelled) return done;
248
+ cancelled = true;
249
+ settleStarted(false);
250
+ abortNative();
251
+ return done;
252
+ }
253
+
254
+ function recognitionStarted() {
255
+ if (settled || cancelled || failed) return;
256
+ receivedStart = true;
257
+ if (!stopping) publishState('listening');
258
+ settleStarted(!cancelled && !failed && !stopping);
259
+ }
260
+
261
+ function recognitionResults(event) {
262
+ if (settled || cancelled || failed) return;
263
+ try {
264
+ for (let index = event.resultIndex; index < event.results.length; index += 1) {
265
+ if (settled || cancelled || failed) return;
266
+ const result = event.results[index];
267
+ if (!result.isFinal || index <= lastFinalIndex) continue;
268
+ lastFinalIndex = index;
269
+ sequence += 1;
270
+ notify(
271
+ onSegment,
272
+ {text: result[0].transcript, sequence}
273
+ );
274
+ }
275
+ if (settled || cancelled || failed) return;
276
+ let text = '';
277
+ // Native results retain their final prefix and replace only
278
+ // interim entries. Do not revisit that growing final prefix.
279
+ for (let index = lastFinalIndex + 1; index < event.results.length; index += 1) {
280
+ text += event.results[index][0].transcript;
281
+ }
282
+ notify(
283
+ onInterim,
284
+ {text}
285
+ );
286
+ } catch (error) {
287
+ interrupt(
288
+ recognitionError(
289
+ 'The browser speech recognition result could not be read.',
290
+ 'ARCANE_AI_INVALID_PROVIDER_RESULT',
291
+ error
292
+ )
293
+ );
294
+ abortNative();
295
+ }
296
+ }
297
+
298
+ function recognitionFailed(event) {
299
+ if (settled || cancelled || failed) return;
300
+ const error = recognitionError(
301
+ event.message || `Native browser speech recognition failed: ${event.error}.`,
302
+ 'ARCANE_AI_SPEECH_RECOGNITION_FAILED',
303
+ event
304
+ );
305
+ error.reason = event.error;
306
+ interrupt(error);
307
+ }
308
+
309
+ function recognitionEnded() {
310
+ finish(receivedStart && !cancelled && !failed);
311
+ }
312
+
313
+ function start(
314
+ {signal: captureSignal} = {}
315
+ ) {
316
+ if (started || settled) return Promise.resolve(false);
317
+ started = true;
318
+ const starting = new Promise(
319
+ function waitForRecognitionStart(resolve) {
320
+ resolveStarted = resolve;
321
+ }
322
+ );
323
+ signal = captureSignal ?? null;
324
+ if (signal?.aborted) {
325
+ cancel();
326
+ return starting;
327
+ }
328
+ try {
329
+ if (state === 'disposed' || disposeOperation) {
330
+ throw recognitionError('The browser speech provider is disposed.', 'ARCANE_AI_PROVIDER_DISPOSED');
331
+ }
332
+ if (state !== 'ready' || unloadOperation || generation !== captureGeneration) {
333
+ throw recognitionError('The browser speech provider is not ready.', 'ARCANE_AI_NOT_READY');
334
+ }
335
+ if (activeCapture !== null) {
336
+ throw recognitionError('The browser speech provider is at capacity.', 'ARCANE_AI_PROVIDER_BUSY');
337
+ }
338
+ const Recognition = recognitionConstructor();
339
+ if (!Recognition) throw unavailableRecognition();
340
+ activeCapture = capture;
341
+ recognizer = new Recognition();
342
+ if (captureLanguage !== undefined) recognizer.lang = captureLanguage;
343
+ recognizer.continuous = continuous;
344
+ recognizer.interimResults = true;
345
+ recognizer.maxAlternatives = 1;
346
+ recognizer.onstart = recognitionStarted;
347
+ recognizer.onresult = recognitionResults;
348
+ recognizer.onerror = recognitionFailed;
349
+ recognizer.onend = recognitionEnded;
350
+ signal?.addEventListener(
351
+ 'abort',
352
+ cancel,
353
+ {once: true}
354
+ );
355
+ publishState('starting');
356
+ if (settled || cancelled || failed) return starting;
357
+ nativeStarted = true;
358
+ // This call stays in the initiating user gesture. No model
359
+ // loading, permission preflight, or promise precedes it.
360
+ recognizer.start();
361
+ } catch (error) {
362
+ interrupt(
363
+ recognitionError(
364
+ 'Native browser speech recognition could not start.',
365
+ is.string(error?.code) ? error.code : 'ARCANE_AI_SPEECH_RECOGNITION_START_FAILED',
366
+ error
367
+ )
368
+ );
369
+ finish(false);
370
+ }
371
+ return starting;
372
+ }
373
+
374
+ function stop() {
375
+ if (settled || stopping || cancelled || failed) return done;
376
+ if (!started || !nativeStarted) return cancel();
377
+ stopping = true;
378
+ publishState('stopping');
379
+ if (settled || cancelled || failed) return done;
380
+ try {
381
+ recognizer.stop();
382
+ } catch (error) {
383
+ interrupt(
384
+ recognitionError(
385
+ 'Native browser speech recognition could not stop.',
386
+ 'ARCANE_AI_SPEECH_RECOGNITION_STOP_FAILED',
387
+ error
388
+ )
389
+ );
390
+ abortNative();
391
+ }
392
+ return done;
393
+ }
394
+
395
+ const capture = {start, stop, cancel, destroy: cancel, done};
396
+ return capture;
397
+ }
398
+
399
+ const provider = {
400
+ protocol: 'arcane-ai-provider/2',
401
+ role: 'stt',
402
+ id,
403
+ localOnly: false,
404
+ maxConcurrentRequests: 1,
405
+
406
+ catalog() {
407
+ return [{id: modelId, providerId: id, role: 'stt', localOnly: false}];
408
+ },
409
+
410
+ async inspect(selection, context = {}) {
411
+ assertContext(context);
412
+ assertSelection(selection);
413
+ if (state === 'disposed' || disposeOperation) {
414
+ return {
415
+ available: false,
416
+ code: 'ARCANE_AI_PROVIDER_DISPOSED',
417
+ message: 'The browser speech provider is disposed.'
418
+ };
419
+ }
420
+ if (!recognitionConstructor()) {
421
+ const error = unavailableRecognition();
422
+ return {available: false, code: error.code, message: error.message};
423
+ }
424
+ return {available: true, authority: authority()};
425
+ },
426
+
427
+ status,
428
+ createCapture,
429
+
430
+ async load(context = {}) {
431
+ assertContext(context);
432
+ if (state === 'disposed' || disposeOperation) {
433
+ throw recognitionError('The browser speech provider is disposed.', 'ARCANE_AI_PROVIDER_DISPOSED');
434
+ }
435
+ if (unloadOperation) {
436
+ throw recognitionError('The browser speech provider is unloading.', 'ARCANE_AI_OPERATION_SUPERSEDED');
437
+ }
438
+ if (!recognitionConstructor()) {
439
+ state = 'error';
440
+ throw unavailableRecognition();
441
+ }
442
+ state = 'ready';
443
+ return status();
444
+ },
445
+
446
+ async request(context = {}) {
447
+ assertContext(context);
448
+ throw recognitionError(
449
+ 'Native browser speech recognition accepts live microphone capture only; arbitrary audio files cannot be transcribed.',
450
+ 'ARCANE_AI_SPEECH_INPUT_UNSUPPORTED'
451
+ );
452
+ },
453
+
454
+ unload(context = {}) {
455
+ assertContext(context);
456
+ if (state === 'disposed') {
457
+ return Promise.resolve(
458
+ status()
459
+ );
460
+ }
461
+ if (unloadOperation) return unloadOperation;
462
+ state = 'unloading';
463
+ generation += 1;
464
+ const capture = activeCapture;
465
+ unloadOperation = Promise.resolve(capture?.done).then(
466
+ function finishRecognitionUnload() {
467
+ state = 'unloaded';
468
+ unloadOperation = null;
469
+ return status();
470
+ }
471
+ );
472
+ capture?.cancel();
473
+ return unloadOperation;
474
+ },
475
+
476
+ dispose(context = {}) {
477
+ assertContext(context);
478
+ if (state === 'disposed') {
479
+ return Promise.resolve(
480
+ status()
481
+ );
482
+ }
483
+ if (disposeOperation) return disposeOperation;
484
+ disposeOperation = provider.unload(context).then(
485
+ function finishRecognitionDispose() {
486
+ state = 'disposed';
487
+ disposeOperation = null;
488
+ return status();
489
+ }
490
+ );
491
+ return disposeOperation;
492
+ }
493
+ };
494
+ return provider;
495
+ }
@@ -10,6 +10,7 @@ export {
10
10
  createBrowserKokoroProvider,
11
11
  createBrowserWhisperProvider,
12
12
  } from "./browser-speech-providers.mjs";
13
+ export {createBrowserSpeechRecognitionProvider} from './browser-speech-recognition.mjs';
13
14
  export {
14
15
  createTwinCloudTTSProvider,
15
16
  createTwinCloudTTSProvider as createDigitalOceanFalTTSProvider,
@@ -1,11 +1,87 @@
1
1
  # Browser speech providers
2
2
 
3
- `arcane-os/ai/browser-speech` supplies caller-selected Whisper speech-to-text,
4
- Kokoro text-to-speech, and TWiN Cloud text-to-speech providers. Its local path
3
+ `arcane-os/ai/browser-speech` supplies caller-selected Whisper or native Web
4
+ Speech recognition, Kokoro text-to-speech, and TWiN Cloud text-to-speech providers. Its local path
5
5
  provides artifact storage, live module routing, role Workers, provider/2
6
6
  adapters, bounded parallel TTS synthesis, audio normalization, cancellation,
7
7
  and cleanup.
8
8
 
9
+ ## Native browser speech recognition
10
+
11
+ Select the browser's `SpeechRecognition` or `webkitSpeechRecognition` service
12
+ explicitly. It may use a remote browser service, so this provider declares
13
+ `localOnly:false`; it is never substituted for Whisper automatically. The
14
+ application owns the provider/model identifiers and saved Profile selection.
15
+
16
+ ```javascript
17
+ import {createBrowserSpeechRecognitionProvider} from 'arcane-os/ai/browser-speech';
18
+
19
+ async function selectBrowserRecognition(ai, signal) {
20
+ const provider = createBrowserSpeechRecognitionProvider(
21
+ {id: 'WEB_SPEECH', model: {id: 'WEB_SPEECH'}}
22
+ );
23
+ await ai.configureSpeechProvider('stt', provider, {modelId: 'WEB_SPEECH', signal});
24
+ }
25
+ ```
26
+
27
+ Configuration and activation use the existing independent STT lifecycle.
28
+ Construction and `catalog()` succeed when the native API is absent;
29
+ `inspect()` reports unavailable and `load()` reports an unsupported error.
30
+ Neither factory, configuration nor load opens a microphone, downloads a
31
+ model, accesses storage, or changes TTS, typing, existing defaults, or saved
32
+ data. Browser/OS availability, microphone permission, language support and
33
+ service connectivity remain native platform requirements across Windows,
34
+ Linux, macOS and Android hosts.
35
+
36
+ After STT activation, the existing `speech.html` and `voice-transcription.html`
37
+ recording controls select live capture automatically. `speech.html` retains
38
+ every final result and emits one complete `speech-transcription-complete`
39
+ message when capture ends. Its live display retains finalized words alongside
40
+ the current interim text while speaking. The voice component appends each final result
41
+ through its existing ordered save queue. Interim text appears only in a
42
+ transient surface and never enters a save or completion event. A custom voice
43
+ component `transcribe(file,context)` callback retains the existing clip path.
44
+ Final segments waiting behind an application save remain visible in that
45
+ transient surface until the ordered queue appends them to the main transcript.
46
+
47
+ For direct use, `ai.supportsTranscriptionCapture()` detects the selected
48
+ provider's capture capability. `ai.createTranscriptionCapture(options)` returns
49
+ `null` for a file-only provider or a single-use capture handle with
50
+ `start({signal})`, `stop()`, `cancel()`, `destroy()` and `done`.
51
+ Options are `{language,continuous,onSegment,onInterim,onState,onError}`;
52
+ `onSegment({text,sequence})` receives each complete final result once, and
53
+ `onInterim({text})` receives the current mutable hypothesis. Callbacks are
54
+ observational and never delay native capture. The voice component, rather than
55
+ the capture callback, owns asynchronous ordered application saves.
56
+
57
+ Call `start()` directly from the initiating user gesture. Its promise resolves
58
+ to whether native capture actually started. `stop()` asks the browser to stop
59
+ listening and finish recognition, retaining final results through native `end`;
60
+ `stop()` and `done` resolve `true` for normal native completion and `false` for
61
+ cancellation or failure after capture settlement. Runtime cleanup failure
62
+ rejects the runtime handle's promises. `cancel()`, an aborted signal,
63
+ provider replacement, unload, and disposal request native `abort()` and suppress
64
+ late text. Capture is an owned busy STT operation; it is never delayed into a
65
+ request queue that would lose the gesture. Natural native `end` stops the
66
+ session without automatic restart. The user can start another session.
67
+ If native `abort()` itself throws, the complete failure remains observable and
68
+ the capture stays owned until native `end`; requesting abort is not evidence
69
+ that the browser microphone has disconnected.
70
+
71
+ Missing API reports `ARCANE_AI_SPEECH_RECOGNITION_UNAVAILABLE`; file input
72
+ reports `ARCANE_AI_SPEECH_INPUT_UNSUPPORTED`. Native service errors use
73
+ `ARCANE_AI_SPEECH_RECOGNITION_FAILED`, retain the browser reason and complete
74
+ event as their cause, and remain available in developer diagnostics. Shared
75
+ ordinary controls show concise failure status. Confirmed final words received
76
+ before a service failure remain deliverable; explicit cancellation or
77
+ supersession suppresses the pending chat submission.
78
+
79
+ This API captures live microphone speech. `AI.fetchSTT(file)` remains the
80
+ Whisper/file-provider contract; native recognition rejects file transcription
81
+ honestly rather than recording again or silently switching providers. See the
82
+ [Web Speech specification](https://webaudio.github.io/web-speech-api/#speechreco-methods)
83
+ for native stop/abort behavior.
84
+
9
85
  <a id="digitalocean-fal-text-to-speech"></a>
10
86
 
11
87
  ## TWiN Cloud text-to-speech
@@ -971,6 +1047,7 @@ records; this contract does not freeze them or shorten their content.
971
1047
  | Native WebView | Conditional | Available when the WebView exposes the browser APIs above. It does not invoke Core speech. |
972
1048
  | Node | Remote adapter available; local execution unavailable | TWiN Cloud uses Fetch and Blob. The local providers have no SDK Node speech-storage, Worker, or audio-decoder host. |
973
1049
  | Remote TTS | Explicitly selected | TWiN Cloud uses standard Fetch and Blob with an application-owned key; no Worker, OPFS, or local model is needed for this adapter. |
1050
+ | Native browser STT | Explicitly selected, conditional | Requires `SpeechRecognition` or `webkitSpeechRecognition` and native microphone/service availability. No Worker, OPFS, or SDK model download; locality is not promised. |
974
1051
 
975
1052
  STT and TTS own independent provider lifecycles. A failure or cancellation in
976
1053
  one role does not disable the other role or authorize a fallback provider.
@@ -982,6 +1059,7 @@ import {
982
1059
  BROWSER_SPEECH_ARTIFACT_GRAPH_PROTOCOL,
983
1060
  BROWSER_SPEECH_ARTIFACT_PROTOCOL,
984
1061
  createBrowserKokoroProvider,
1062
+ createBrowserSpeechRecognitionProvider,
985
1063
  createBrowserSpeechArtifactGraph,
986
1064
  createBrowserSpeechAuthority,
987
1065
  createBrowserWhisperProvider,