On this page
arcane-os/ai/browser-speech supplies caller-selected Whisper transcription,
native browser recognition and synthesis, Kokoro text-to-speech, and TWiN Cloud
text-to-speech providers. Its local model path
provides artifact storage, live module routing, role Workers, provider/2
adapters, bounded parallel TTS synthesis, audio normalization, cancellation,
and cleanup.
Choose a speech provider
| Need | Factory | Input or output | Runtime and locality |
|---|---|---|---|
| Transcribe a recorded clip | createBrowserWhisperProvider |
Audio input, complete transcript output | Caller-selected local model and Worker runtime |
| Recognize live microphone speech | createBrowserSpeechRecognitionProvider |
Final segments and transient interim text | Native browser service; may use a remote service |
| Speak with installed browser voices | createBrowserSpeechSynthesisProvider |
Silent native playback descriptor, then explicit playback | Native voice inventory; individual voices may be remote |
| Generate reusable audio locally | createBrowserKokoroProvider |
Synthesized audio | Caller-selected local model, voice and Worker runtime |
| Generate audio through TWiN Cloud | createTwinCloudTTSProvider |
Completed remote synthesis returned as audio | Application-owned key reader and selected FAL model |
STT and TTS have independent selections and lifecycles. Applications choose their defaults, recognition language, voice, and activation controls. Native browser providers need no SDK model download or DBOPFS artifact store.
Native browser speech recognition
Select the browser's SpeechRecognition or webkitSpeechRecognition service
explicitly. It may use a remote browser service, so this provider declares
localOnly:false; it is never substituted for Whisper automatically. The
application owns the provider/model identifiers and saved Profile selection.
import {createBrowserSpeechRecognitionProvider} from 'arcane-os/ai/browser-speech';
async function selectBrowserRecognition(ai, signal) {
const provider = createBrowserSpeechRecognitionProvider(
{id: 'WEB_SPEECH', model: {id: 'WEB_SPEECH'}}
);
await ai.configureSpeechProvider('stt', provider, {modelId: 'WEB_SPEECH', signal});
}
Configuration and activation use the existing independent STT lifecycle.
Construction and catalog() succeed when the native API is absent;
inspect() reports unavailable and load() reports an unsupported error.
Neither factory, configuration nor load opens a microphone, downloads a
model, accesses storage, or changes TTS, typing, existing defaults, or saved
data. Browser/OS availability, microphone permission, language support and
service connectivity remain native platform requirements across Windows,
Linux, macOS and Android hosts.
After STT activation, the existing speech.html and voice-transcription.html
recording controls select live capture automatically. speech.html retains
every final result and emits one complete speech-transcription-complete
message when capture ends. Its live display retains finalized words alongside
the current interim text while speaking. The voice component appends each final result
through its existing ordered save queue. Interim text appears only in a
transient surface and never enters a save or completion event. A custom voice
component transcribe(file,context) callback retains the existing clip path.
Final segments waiting behind an application save remain visible in that
transient surface until the ordered queue appends them to the main transcript.
Recognition language and editable Chat drafts
The shared speech control exposes recognitionLanguage as an editable BCP47
tag. Its visible picker is available before live capture, shows every suggested
language when opened, and filters after the user changes its text. Suggestions
retain native language and region labels; callers can also supply an exact
custom tag. The picker is disabled during capture and outside live-recognition
mode. Recognition language is independent of Chat's conversation language.
For an initialized shared Chat component, choose whether the completed transcription submits immediately or remains editable:
// chat is the application's ready shared Chat component.
chat.transcriptionMode = 'draft';
chat.recognitionLanguage = 'es-MX';
chat.appendDraft('Captain, the moon-powered toaster reports: ');
'submit' is the existing default. In 'draft' mode, final and interim native
recognition appear immediately in the composer alongside typed text. Completion
leaves Send to the user. Interim replacement preserves user edits; cancellation
retires only the unedited interim range and retains confirmed text. The shared
speech component emits cumulative speech-transcription-progress detail with
text, interim, and operationId; confirmed completion keeps the existing
speech-transcription-complete event. Interim text remains transient and never
enters a completion payload or durable history.
Direct capture
For direct use, ai.supportsTranscriptionCapture() detects the selected
provider's capture capability. ai.createTranscriptionCapture(options) returns
null for a file-only provider or a single-use capture handle with
start({signal}), stop(), cancel(), destroy() and done.
Options are {language,continuous,onSegment,onInterim,onState,onError};
onSegment({text,sequence}) receives each complete final result once, and
onInterim({text}) receives the current mutable hypothesis. Callbacks are
observational and never delay native capture. The voice component, rather than
the capture callback, owns asynchronous ordered application saves.
Call start() directly from the initiating user gesture. Its promise resolves
to whether native capture actually started. stop() asks the browser to stop
listening and finish recognition, retaining final results through native end;
stop() and done resolve true for normal native completion and false for
cancellation or failure after capture settlement. Runtime cleanup failure
rejects the runtime handle's promises. cancel(), an aborted signal,
provider replacement, unload, and disposal request native abort() and suppress
late text. Capture is an owned busy STT operation; it is never delayed into a
request queue that would lose the gesture. Natural native end stops the
session without automatic restart. The user can start another session.
If native abort() itself throws, the complete failure remains observable and
the capture stays owned until native end; requesting abort is not evidence
that the browser microphone has disconnected.
Missing API reports ARCANE_AI_SPEECH_RECOGNITION_UNAVAILABLE; file input
reports ARCANE_AI_SPEECH_INPUT_UNSUPPORTED. Native service errors use
ARCANE_AI_SPEECH_RECOGNITION_FAILED, retain the browser reason and complete
event as their cause, and remain available in developer diagnostics. Shared
ordinary controls show concise failure status. Confirmed final words received
before a service failure remain deliverable; explicit cancellation or
supersession suppresses the pending chat submission.
This API captures live microphone speech. AI.fetchSTT(file) remains the
Whisper/file-provider contract; native recognition rejects file transcription
honestly rather than recording again or silently switching providers. See the
Web Speech specification
for native stop/abort behavior.
Native browser text-to-speech and voice previews
createBrowserSpeechSynthesisProvider({id,model:{id,name?,defaultVoice?},language?})
uses the host browser's speechSynthesis and SpeechSynthesisUtterance.
The application selects this provider as its default when desired; constructing
it does not change global preferences, activate Chat speech, or play audio.
The provider declares localOnly:false because a browser voice may use a remote
service. Individual catalog records expose the browser's localService value.
import {createBrowserSpeechSynthesisProvider} from 'arcane-os/ai/browser-speech';
const voicePreview = createBrowserSpeechSynthesisProvider({
id: 'browser-preview',
model: {id: 'browser-voices', name: 'Browser voices'}
});
const unsubscribe = voicePreview.subscribeCatalog(function showVoices(models) {
// Pass these real records to the application's language/voice selector.
console.log(models[0].voices);
});
// Call only from the application's explicit Play action.
function playIntroduction(input, voice) {
return voicePreview.play({input, voice});
}
// voiceURI comes from the real catalog or the application's saved selection.
function selectDefaultVoice(voiceURI) {
return voicePreview.setDefaultVoice(voiceURI);
}
// The owning view unsubscribes and disposes voicePreview when it closes.
catalog() synchronously returns an array of model records. Each model has
speech:{playback:'native'} and voices:[{id,name,lang,default,localService}];
id is the exact native voiceURI. subscribeCatalog(callback) immediately
replays that same shape and updates it on voiceschanged or a changed default,
returning an unsubscribe function. An initially empty inventory is ordinary pending browser
state. There is no polling, discovery utterance, model download, or page-load
barrier. Language selectors derive their choices from this actual inventory.
provider.setDefaultVoice(voiceURI) updates the provider's current
catalog()[0].defaultVoice synchronously and returns that current value.
The application may call it from its catalog subscription after asynchronous
voice discovery or when its saved selection changes. The SDK supplies no
voice-name preference: preserve an explicit saved choice in the application
and pass the exact native voiceURI. Repeating the current value is a no-op,
so a catalog subscription can apply the same choice without notification loops.
null, an empty string, or an omitted argument clears the configured default
and restores the existing browser-default behavior. A non-string, non-null
value throws TypeError; an unloading or disposed provider retains its
existing lifecycle error. A voice need not be present when setting the default:
the existing preparation/playback availability behavior below still applies.
Only future preparations use the new default. Explicit request voices and already-prepared, queued, or active speech keep their selected voice. Updating the default does not load, unload, replace, cancel, pause, resume, or speak. AI's ordinary speech path reads this current catalog for new requests, so no provider reconfiguration is necessary.
prepare({input,voice?,language?,speed?},{signal?}) returns an inert
{kind:'native-speech',input,voice,language,speed,play(options)} descriptor.
play(payload,{signal,onState}) is the explicit-preview convenience. Both
preserve complete input. Omitted voice uses the current default, initially
model.defaultVoice, or the actual browser default; a supplied voice is never replaced with a named
SDK alias. Language comes from that voice unless explicitly provided. Neither
API reads the conversation's language preference. A saved voice waits for
voiceschanged if the inventory is empty; a nonempty inventory missing it
reports ARCANE_AI_SPEECH_VOICE_UNAVAILABLE.
Playback returns {finished,released,state,error,pause(),resume(),stop()}.
finished resolves true on actual native end, false on stop/abort, and
rejects genuine native failures. released resolves when native resource
ownership actually ends: a failed pause/resume/cancel can report an error before
the utterance terminates. onState({state,error}) observes queued,
waiting-for-voices, playing, paused, complete, stopped, or error. Keep complete
errors in diagnostics and ordinary status in the interface.
Preview does not depend on AI mute or model activation. Use a separate provider instance when the preview should have a lifetime independent of Chat's selected provider. Provider unload/dispose stop their own playback, invalidate prepared descriptors, and disposal removes catalog listeners. Web Speech pause/resume/cancel operate on the native synthesis object globally; the SDK serializes its own utterances and does not claim control isolation from unrelated direct native callers. See the native synthesis contract.
For Chat, configure this provider with ai.configureSpeechProvider('tts',provider)
and retain the existing explicit mute/unmute lifecycle. AI.streamTTS() and
SpeechPlayback use actual utterance completion and cancellation in original
order. AI.prepareTTSPlayback(payload,signal) returns either real Blob audio or
the native descriptor without speaking; its signal remains attached by default
when the returned descriptor is played. AI.fetchTTS() and durable
AI.prepareTTS() remain audio-file APIs and report
ARCANE_AI_TTS_AUDIO_EXPORT_UNAVAILABLE for native synthesis, which exposes no
downloadable audio file.
Choose preparation according to the result the application needs:
| API | Result | Playback and retention |
|---|---|---|
provider.prepare(payload,{signal}) |
Native speech descriptor | Silent until its explicit play() call; keeps the selected voice |
ai.prepareTTSPlayback(payload,signal) |
Promise resolving a native descriptor or real audio Blob | Silent, provider-neutral preparation; no durable storage |
ai.streamTTS(text,end,options) |
Promise resolving a preparation or playback boolean | Shared segmentation and playback in original order |
ai.fetchTTS(payload,signal) |
Promise resolving a real audio Blob | Audio-file providers only; does not play |
ai.prepareTTS(options) |
Preparation handle with ready and getAudio(index) |
Audio-file providers only; optional caller-owned durable storage |
Native Core streaming speech
The shared AI controls also consume an explicitly selected native Core speech service. Nemotron supplies retained streaming recognition and optional diarization; Magpie supplies streamed PCM and complete WAV/PCM audio. These are separate from browser Web Speech voices: Magpie can export audio, whereas the browser-native synthesis provider exposes playback only. No selected route silently falls back to another engine.
For speech-only construction, pass null for both LLM arguments:
new AI(null, 'LOCAL_SPEACH', 'LOCAL_SPEACH', null, ttsModel, sttModel).
This leaves the LLM unselected; omitted or empty arguments preserve defaults.
The existing per-realm AI singleton is reused if already present. Import and
construction do not prove native readiness.
After the selected STT role is ready, ai.supportsTranscriptionCapture()
reports its capture capability. ai.createTranscriptionCapture(options)
returns null without a provider capture implementation, otherwise an owned
{start, stop, cancel, destroy, done, forceEndpoint?} handle. Start from the
user's capture action with {signal, mediaConstraints}. Native options include
sampleRate (default 16000), pcmChunkMs (default 100),
activityThreshold (default 0.02), streamOptions, onUpdate,
onInterim, onSegment, onActivity, onState, onError,
onProgress, and onRequest. The actual capture rate must match the native
stream; every PCM contribution is delivered in order.
Native recognition callbacks receive (result, {requestStop}). Keep full
snapshots: interim results replace, and final segments retain their native
boundaries, words, timestamps and speaker scope. A callback can call
requestStop() without awaiting it and return; the external owner awaits
stop() or done. Never await those joins or forceEndpoint() from their
own callback. External forceEndpoint() flushes pending capture PCM and
recognition callbacks while leaving the microphone and native stream open.
stop() closes capture and drains recognition; cancel() and destroy()
join cleanup without claiming a complete transcript. No control sends chat.
For a streaming TTS catalog, ai.prepareTTSPlayback({input, voice, languageCode, speed}, signal)
returns an inert descriptor. Its explicit play() uses the shared provider
lane and Web Audio output. The returned control exposes finished,
released, state, error, pause(), resume(), and stop().
finished describes playback completion, while released joins native
cleanup. Stop targets that utterance, independently of capture.
fetchTTS({input, languageCode, ...}, signal) still returns complete audio
when the selected engine supports it. Language is caller-selected control data,
separate from the text; Magpie supports natural speed 1 only. See
shared native capture/playback.
<a id="digitalocean-fal-text-to-speech"></a>
TWiN Cloud text-to-speech
Use the same AI speech queue with an explicitly selected remote provider.
The application supplies its current DigitalOcean inference key through a
function returning a string or promise; never place that key in source, logs, or model records.
This example receives an existing ai and application-owned key reader:
import {createTwinCloudTTSProvider} from 'arcane-os/ai/browser-speech';
async function selectCloudVoice(ai, getApiKey) {
const provider = createTwinCloudTTSProvider({
id: 'my-cloud-voice',
model: {
id: 'fal-ai/elevenlabs/tts/multilingual-v2',
defaultVoice: 'Rachel'
},
getApiKey,
maxConcurrentRequests: 4,
followUpQueue: {maxConcurrentRequests: 4, intervalMs: 250}
});
await ai.configureSpeechProvider('tts', provider, {
modelId: provider.catalog()[0].id
});
return provider;
}
createDigitalOceanFalTTSProvider remains a compatibility export of the same
factory. Existing provider IDs and credentials remain valid; the TWiN Cloud
name does not change the underlying DigitalOcean FAL endpoint or protocol.
Construction and configuration make no network request. Explicit
ai.setSpeechMuted(false) awaits the key reader, checks credential presence, and activates
the adapter; its ready state does not prove remote credential validity.
Only synthesis submits text. Keep local STT configured independently, before
or after this TTS selection. Existing shared Chat/Speech controls own user
mute/unmute intent; do not reapply a saved unmute over a later manual mute.
An asynchronous key reader may await the application's already-running key
refresh at this activation boundary; it does not delay provider configuration
or page rendering. Cancellation releases this request's wait without cancelling
the application's shared refresh, and no late key result starts synthesis.
Once explicit activation is requested, feed each arriving text chunk immediately through
ai.streamTTS(chunk) and observe each promise without awaiting it ahead of
the next chunk. Pending activation retains these segments in the existing queue
until the selected provider is ready; text does not initiate activation itself.
ai.finishTTS() submits the final remainder. The existing
punctuation segmentation, concurrent provider queue, and original-order
playback remain the only owners. Use
ai.configureTTSSegmentation({punctuation:'any',wordCadence:null}) when each
punctuation boundary should submit promptly. This is incremental playback of
completed synthesis jobs, not a live audio stream from TWiN Cloud.
The provider queues each complete synthesis job, including initial
POST https://inference.do-ai.run/v1/async-invoke and any eligible submission
retry. maxConcurrentRequests defaults to four whole jobs. Each slot remains
occupied through credential readiness, submission, queued/running status, and
the complete final audio response. Additional requests wait in order; cancelling
a waiting job removes it before credential reading or submission. This capacity
is separate from followUpQueue.maxConcurrentRequests.
One provider-local js-queue owns only follow-up status/result GETs and the final
audio GET. followUpQueue defaults to {maxConcurrentRequests:4,intervalMs:250};
the application may supply those settings explicitly. A follow-up slot remains
occupied through the complete JSON, audio Blob, or error response body. Dispatch
starts observe intervalMs, with at most the configured number in flight. Each
job awaits its current response and body before immediately enqueueing another
request only when incomplete or eligible for a retry. There is no additional
per-job polling delay.
A successful pending response's Retry-After changes that job's next eligibility.
A 429 applies a shared cooldown to all queued submission and follow-up starts,
using a one-second minimum and any longer readable Retry-After. A completely
readable 429 permits one retry of that submission or status read, within the
same whole-job slot. Already dispatched operations retain their response ownership.
An accepted job's status Fetch network failure may retry once for the same job;
Ambiguous POST network, JSON parsing, and error-body read failures never replay
the submission. Repeated failures surface their
complete diagnostics without an endless retry loop; a failed job does not cancel
siblings. Retry diagnostics remain in the developer console, outside conversation
history.
The provider-local queues are created when this cloud factory is called, not when the browser speech entrypoint is imported. Local Kokoro and Whisper do not use this queue; their setup, Workers, synthesis/transcription, and lifecycle remain independent.
Ordinary Kokoro descriptors may supply model.voices as complete display records,
for example {id:'af_heart',name:'Heart',lang:'en-US'}. The synchronous
provider.catalog()[0].voices preserves that array and its metadata without
loading the model or fetching a catalog. The application supplies the inventory
for its selected runtime; these records neither create a synthesis allow-list
nor establish that a voice has loaded or played. Ordinary Worker configuration
does not receive this display inventory. Existing artifact-graph voice paths
retain their separate meaning.
The SDK pins published js-queue through npm. Its browser Queue entry, original
package metadata, and license are projected unchanged by
node tools/sync-js-queue.mjs after an SDK dependency update. The existing runtime
materializer serves that same entry; applications do not need another import-map
setting. easy-stack remains an npm dependency and is not imported by the Queue
entry. Do not hand-edit this upstream projection.
An ambiguous submission network failure is not automatically resubmitted: a
browser cannot distinguish a rejected preflight from an accepted paid job whose
response was lost. This contract has no submission idempotency mechanism, and
unreadable response headers cannot supply a retry delay. Follow-up pacing does
not claim to free upstream concurrency slots or fix CORS.
The audio download never receives the inference Authorization header.
The selected multilingual-v2 service supports speed 0.7 through 1.2;
unsupported speeds reject rather than being silently clamped. Complete
input remains intact except for the existing explicitly owned speech-only
formatting cleanup described below. A cloud failure never selects local TTS.
ai.stopAudio() cancels the current local request/poll/download/playback
lifetime. Mute additionally unloads TTS. DigitalOcean exposes no documented
job cancellation operation through this contract: a submitted remote job may
continue after local cancellation. Browser use also depends on the inference
and returned media endpoints allowing the requesting origin.
Release this exact configured provider with:
// Keep the provider returned by selectCloudVoice(ai, getApiKey).
await ai.configureSpeechProvider('tts', null, {expectedProvider: provider});
The identity is rechecked inside the SDK's serialized configuration operation,
so a later selected provider remains untouched. Returning to local TTS uses
the normal TTS-only configureBrowserSpeech() configuration. Neither action
unloads STT or changes its stored artifacts.
Transport references: DigitalOcean asynchronous inference, DigitalOcean FAL guide, and multilingual-v2 model schema.
Quick start: say one sentence
Use this in a browser application served by arcane dev, where the generated
import map resolves arcane-os/ai and arcane-os/modules/DBOPFS.js. These
browser modules require a browser host. To create an application:
npx arcane-os@latest new hello-speech --path ./hello-speech --target browser
cd hello-speech
npm install
npm run dev
Keep the generated page's Arcane theme and import map. The examples below go in
modules/App.js at the application root and the adjacent speech-selection.js.
Follow the development server's printed URL. Installing the SDK supplies its
provider, storage, and Worker code; it does not install a speech model or choose
an upstream speech runtime for your application.
First create speech-selection.js, the one application-owned configuration
file used throughout this guide. This concrete selection is also used by the
maintained WASM voice-chat example.
Your application owns these runtime/model versions, URLs, dtype, and voice.
Loading this selection uses those upstream publishers' downloads and caches.
export const speechSelection = {
model: {
id: 'onnx-community/Kokoro-82M-v1.0-ONNX',
repository: 'onnx-community/Kokoro-82M-v1.0-ONNX',
revision: '1939ad2a8e416c0acfeecc08a694d14ef25f2231',
dtype: 'fp32',
defaultVoice: 'af_heart'
},
runtime: {
adapter: 'kokoro-js',
version: '1.2.1',
revision: '664c76a704021239ba59c84dcbaa4d3dece01fe9',
entry: 'kokoro.web.js',
wasmPaths: 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.1/dist/',
files: [{
path: 'kokoro.web.js',
url: 'https://cdn.jsdelivr.net/npm/kokoro-js@1.2.1/dist/kokoro.web.js',
mediaType: 'text/javascript'
}]
}
};
The omitted execution record below uses the SDK's NPU, GPU, then CPU automatic
selection. This basic configuration uses fp32 because
Kokoro.js recommends fp32 when using WebGPU.
Automatic fallback carries this same selected model and dtype between devices;
the SDK does not rewrite the application selection. If you intentionally choose
another dtype, evaluate that exact model, browser, and execution route.
selectedDevice reports routing after load, not pronunciation, text fidelity,
or audio quality.
Then use this App.js. The application creates and owns the DBOPFS
instance. Configuration selects the provider without loading it; the button
explicitly loads and unmutes TTS before requesting speech.
import arcaneThemeReady from 'arcane-os/modules/ThemeBootstrap.js';
import AI, { AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL } from 'arcane-os/ai';
import DBOPFS from 'arcane-os/modules/DBOPFS.js';
import { speechSelection } from './speech-selection.js';
await arcaneThemeReady;
const dbopfs = new DBOPFS();
await dbopfs.readyPromise;
const ai = new AI();
await ai.configureBrowserSpeech({
protocol: AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL,
id: 'hello-speech',
dbopfs,
tts: {
providerId: 'hello-kokoro',
model: speechSelection.model,
runtime: speechSelection.runtime,
offline: false
}
});
const speakButton = document.createElement('button');
speakButton.textContent = 'Load voice and say hello';
document.body.append(speakButton);
speakButton.addEventListener('click', async function sayHello() {
speakButton.disabled = true;
try {
await ai.setSpeechMuted(false); // Loads the selected TTS provider.
const prepared = await ai.streamTTS('Hello from Arcane. ', true);
console.log('Speech preparation completed:', prepared);
} catch (error) {
console.error(error.code, error.message);
} finally {
speakButton.disabled = false;
}
});
The first user action may download the selected runtime, model, and voice.
The browser may require another audio-unlock gesture after a long load; the SDK
retains prepared audio for that gesture. The two-argument streamTTS() call
resolves after preparing audio for scheduling; it does not wait for playback
to end. It returns false when that preparation is muted, stopped, or fails.
Later playback failures still reach the SDK's complete console diagnostics
and ai-tts-failure event. Use the optional playback mode below when you need
to wait for the submitted audio to end. Neither mode proves a listener heard it.
To display complete high-level playback errors in this page, observe its
existing event. The listener belongs to this example's one ai instance:
const speechEvents = new AbortController();
window.addEventListener('ai-tts-failure', function reportSpeechFailure(event) {
if (event.detail.ai !== ai) return;
console.error(event.detail.error.code, event.detail.error.message);
}, { signal: speechEvents.signal });
Call speechEvents.abort() when disposing that interface to remove the listener.
Browser NPU and GPU setup
Applications can place the shared browser-ai-setup.html component in their
profile or settings page. It reports whether this page exposes WebNN and
WebGPU, then requests one WebGPU adapter to display its availability and reported
name. It does not load a model or create a GPU device or WebNN context:
<html-import
id="browserAISetup"
href="/node_modules/arcane-os/runtime/arcane/components/browser-ai-setup.html">
</html-import>
NPU setup provides a Copy NPU flag address button regardless of WebNN
availability, with one short explanation. Chrome uses
chrome://flags/#web-machine-learning-neural-network; Edge uses
edge://flags/#web-machine-learning-neural-network. Unrecognized browsers
receive explicit Chrome and Edge choices. A failed copy shows the complete
address as selectable text; the component does not open internal browser pages.
The ONNX Runtime WebNN guide
documents the Enables WebNN API flag and model/operator requirements.
The GPU performance section shows a short adapter status and a Copy GPU
flag address button on desktop Windows Chromium browsers. The button remains
available for every adapter class, unavailable API, or detection failure. One
shared instruction explains pasting, enabling, saving work, then closing the
browser and reopening it, with the final step underlined and emphasized. Identified
Chrome uses chrome://flags/#force-high-performance-gpu; other recognized
Chromium browsers use their corresponding internal scheme. Refresh
requests a new availability result; concurrent
requests share the pending operation. The public checkGpu() method provides
the same promise, while refresh() remains a synchronous API-presence and
settings update. Component readiness does not wait for adapter detection.
Adapter selection uses powerPreference: "high-performance" as a hint, not proof
of the selected GPU's performance or the flag's current state. When the browser
explicitly reports a discrete GPU, Profile says Already using the performance
GPU. Explicit integrated or software/fallback
metadata is reported as such. Missing type metadata shows GPU available.
and leaves performance selection unconfirmed; a vendor name is not a GPU
classification. The component cannot read the browser flag or prove that a
model is executing on the adapter. See the
component contract for result
fields, failure handling, and disposal behavior.
This control does not save an execution preference, change browser settings, restart the browser, or report that the NPU is active. A WebNN API presence result is not proof of NPU hardware, a compatible model, or physical execution. The profile also cannot report a different chat page's loaded runtime. The existing automatic speech route remains NPU, then GPU, then CPU; upstream sessions can still place unsupported operators on CPU. Use the owning model runtime's evidence to determine actual accelerator execution.
Developer diagnostics
Arcane uses the existing shared user.developer preference for diagnostic
logging. Enable developer mode in the application's profile settings; the
logger reads that preference on every emission after the shared user is ready.
There is no separate speech verbosity setting. Ordinary warnings and errors
remain visible with developer mode disabled.
Applications can use the same owner for their complete AI requests and parsed responses:
import { arcaneLogging } from 'arcane-os/logging';
arcaneLogging.info('AI request', request);
arcaneLogging.info('AI response', response);
arcaneLogging.log, .info, and .debug use the developer preference and
appear at the browser console's normal Info level. .warn, .error, and
failure .trace calls remain visible in either mode. The same logger is
available as globalThis.arcaneLogging; it stores no diagnostic history.
Speech diagnostics include complete API inputs and results, selected provider and model, exact segment text, voice and speed, generation queue state, Worker request IDs and responses, and decoding/playback events. Worker requests are copied only for developer diagnostics before native transfer detaches their audio buffers; the original request still goes to the Worker unchanged.
Follow a speech jobId through queue.add, generation.request,
generation.result, decode.result, playback.scheduled, playback.ended,
and queue.complete. Cancellation and failure appear as queue.cancelled
and queue.failed. Each playback record includes the audio clock,
sample rate, duration, playback rate, and scheduled start/end when available.
For ready adjacent buffers, gapSeconds is zero on the same audio clock;
audioEnd marks the end of audio and scheduledEnd includes the caller's
requested pause. A positive gap can expose generation arriving too late to
fill the audio clock continuously. A scheduled event alone does not establish
that the buffer finished; use its playback.ended event.
Diagnostics do not change text, voice, speed, language selection, segmentation, generation capacity, or playback scheduling. They stay outside chat history.
<a id="four-synthesis-slots-and-exact-order-playback"></a>
Local synthesis capacity and exact-order playback
The default Kokoro capacity of 4 means up to four segments synthesize at once. Segment 5 and later wait in the SDK's FIFO queue; they are not dropped. Synthesis may finish out of order, but playback waits for earlier segments and plays exact input order. Each slot owns a Worker/model session, so raising capacity trades memory for latency.
The high-level AI route owns the FIFO queue. Calling a low-level Kokoro
provider directly beyond its capacity returns ARCANE_AI_PROVIDER_BUSY.
Ready adjacent audio buffers use contiguous AudioContext scheduling. Browser
audio scheduling and selected WebGPU status do not prove physical GPU kernel
overlap or audio quality. LLM and Whisper/STT capacity remains one.
Play a complete array with SpeechPlayback
Use this when your application already has complete segments in an array. The
configured ai below is the same instance created in the quick start.
import SpeechPlayback from 'arcane-os/speech-playback';
const audio = document.body.appendChild(document.createElement('audio'));
audio.controls = true;
const narration = new SpeechPlayback({audio, speech: ai});
const speakAll = document.body.appendChild(document.createElement('button'));
speakAll.textContent = 'Speak all segments';
speakAll.addEventListener('click', async function speakAllSegments() {
await ai.setSpeechMuted(false);
await narration.prepare({
parts: [
'First complete segment.',
'Second complete segment.',
'Third complete segment.'
],
autoplay: true
});
});
prepare() submits all three parts immediately because this ai exposes
prepareTTSPlayback and advertises TTS execution capacity. With the Kokoro
configuration above, the provider admits up to four synthesis requests and keeps later requests in
its FIFO queue. Completed audio remains indexed, so playback is always first,
second, third even if the third synthesis finishes first.
SpeechPlayback prefers prepareTTSPlayback() when present and also supports
existing audio-only fetchTTS() clients. Native browser descriptors and real
audio Blobs remain in the same ordered sequence, including requested pauses.
Native segments advance only after actual utterance completion and resource
release; they are never converted into placeholder audio files.
This eager path is capability-driven. A native Arcane.speech.synthesize
client, or a custom client without a positive advertised TTS execution capacity,
retains serialized synthesis with one lookahead segment. togglePause() pauses
and resumes the active native utterance or the same audio element.
stop() cancels all requests owned by the playback. replay() keeps completed
and pending provider segments and retries
only failed missing segments.
The default {device:'auto',maxConcurrentRequests:4} attempts the full ONNX
Worker/session pool on WebNN NPU, then WebGPU, then CPU through WASM. It skips
an accelerator when its browser API is absent and replaces a failed candidate
with a fresh pool before trying the next device. The basic configuration above
keeps fp32 throughout that sequence. Use the status example below to read
selectedDevice; console node-assignment warnings alone do not identify the
selected execution device or assess the generated audio.
Queue complete passages and wait for playback
For a complete page or passage, call
ai.streamTTS(text, true, {voice, speed, languageCode, pauseAfterMs, waitForPlayback:true}).
The existing AI queue owns segmentation, concurrent synthesis, ordered
playback, and cancellation. Supply the exact text; there is no need for an
application sentence queue, audio cache, or playback scheduler.
This function uses the configured ai above. Its application-supplied
passages argument is an ordered array of {text, voice?, speed?, languageCode?, pauseAfterMs?}
records. An omitted voice uses the selected model's default voice, an omitted
speed uses ai.voiceSpeed, and an omitted pause is zero. Each supplied voice
must be supported by the selected model; speed must be positive. A pause is
finite, nonnegative milliseconds and applies only after that passage's final
extracted segment. These options do not change the instance defaults.
async function speakPassages(passages) {
await ai.setSpeechMuted(false);
const pending = passages.map(
function queuePassage(passage) {
return ai.streamTTS(
passage.text,
true,
{
voice: passage.voice,
speed: passage.speed,
languageCode: passage.languageCode,
pauseAfterMs: passage.pauseAfterMs,
waitForPlayback: true
}
);
}
);
return Promise.all(pending);
}
Call speakPassages(...) from your owned user action and handle errors with
the earlier error.code / error.message pattern. The map submits every
passage synchronously before Promise.all waits, so synthesis can use the
provider's available capacity. The returned array has one boolean per passage
in input order: true after all its extracted audio buffers naturally end,
or false after terminal cancellation or failure. ai.stopAudio() cancels
streamed speech and prepared playback owned by that AI instance and settles
pending playback results false. Detached preparation described below keeps
its independent lifetime.
Voice, speed and languageCode remain associated with an unfinished end:false
utterance and its later finishTTS() flush; a supplied option replaces its retained
selection. An explicitly changed language first queues the preceding remainder
with its old selection, then accepts the new text. Language is separate control
information and does not rewrite the text. End, mute and stop clear the retained
selection. Pause and waitForPlayback remain per-call options. Finish a producer
before starting an independent passage; the automatic speech-input cleanup
still retains a pending single formatting mark until final flush.
A call extracting no segments returns true without waiting for earlier jobs,
except that a language-change call also joins its preceding remainder preparation.
An already muted call returns false.
Autoplay permission waiting and recoverable audio-resume attempts leave the
playback promise pending until playback completes or is stopped. A failed
resume of a closed AudioContext terminates the affected jobs and settles
their playback results false. A trailing
pause delays the next queued audio on the existing AudioContext clock; it
does not delay the preceding promise after that passage's last buffer ends.
The promise is a playback result, not a listener acknowledgement.
Read stored narration without starting generation
await ai.readPreparedTTS({parts,textFormat,storage,identity,signal,onState})
returns a complete prepared handle or null. It uses the same complete inputs,
selected speech configuration, formatting treatment, segmentation, storage key,
and semantic identity as prepareTTS(). Supply the same options used to
prepare the audio. A changed selection or input is a cache miss, not permission
to generate replacement audio.
This operation never activates or unmutes a provider, synthesizes speech,
creates stored records, repairs partial audio, or joins pending generation.
With storage, it reads the existing manifest and every ordered audio file.
Missing or incomplete audio returns null; unreadable metadata and other
storage errors reject. Without storage, it finds only already-complete audio
retained by this same AI instance. A miss leaves all existing data unchanged.
async function readNarration(text, key, signal) {
return ai.readPreparedTTS({
parts: [{
input: text,
voice: speechSelection.model.defaultVoice,
speed: 1,
pauseAfterMs: 0
}],
storage: {db: dbopfs, table: 'saved_narration', key},
identity: {
model: speechSelection.model,
runtime: speechSelection.runtime
},
signal
});
}
A returned handle has the ordinary segments, state, ready, getAudio(index),
and cancel() contract and can be passed to ai.playPreparedTTS() from the
application's playback action, including with its existing audioOutput.
The lookup signal governs only the read and rejects cancellation as
AbortError. After a successful return, aborting that signal does not invalidate
the retained handle; explicit handle cancellation and playback cancellation
remain separate. A successful read does not promise future autoplay permission
or prevent a later application-owned removal of stored files.
Applications decide which saved narration candidates to inspect. For example, newest-first lookup can skip a newer partial preparation and then inspect an older complete candidate without activating any model.
Prepare narration once and replay stored audio
Use ai.prepareTTS() when preparation must continue independently of playback,
or when a later visit should reuse generated audio. It returns a handle
synchronously and starts preparing the supplied complete parts without speaking
them. Attach ai.playPreparedTTS() immediately to play those parts as their
audio becomes available, or omit playback to prepare them silently.
Use the configured ai and ready, application-owned dbopfs from the quick
start. Configuration itself does not load Kokoro. This path reads stored audio
first: a complete match can replay while the AI starts muted, with no model
load. Only missing audio requests the selected provider's shared load/unmute
path. Do not eagerly call setSpeechMuted(false) before this path if fully
cached playback should avoid loading the model.
ai.configureTTSSegmentation(
{
punctuation: 'any',
wordCadence: null
}
);
function prepareNarration(text, key) {
return ai.prepareTTS(
{
parts: [
{
input: text,
voice: speechSelection.model.defaultVoice,
speed: 1,
pauseAfterMs: 0
}
],
storage: {
db: dbopfs,
table: 'saved_narration',
key
},
identity: {
model: speechSelection.model,
runtime: speechSelection.runtime
},
onState: reportNarrationPreparation
}
);
}
function reportNarrationPreparation(state) {
console.log('Narration preparation:', state);
}
function reportNarrationPlayback(state) {
console.log('Narration playback:', state);
}
async function readNarration(text, key) {
const prepared = prepareNarration(text, key);
const playback = ai.playPreparedTTS(
prepared,
{onState: reportNarrationPlayback}
);
const [record, ended] = await Promise.all(
[prepared.ready, playback.finished]
);
return {record, ended};
}
Call readNarration('**Hello**. Welcome back.', 'welcome') from an owned user
action and handle its rejection. Calling only prepareNarration(...) prepares
silently; observe that handle's ready promise to receive its complete record
or error. A storage key groups application content. The application owns its
keys, preparation priority, semantic identity, and retention policy; the SDK
does not select another page or speak background work.
Preparation inputs and reuse
ai.prepareTTS({parts,textFormat,storage,identity,signal,onState}) accepts an ordered array
of strings or {input,voice?,speed?,pauseAfterMs?} records. An omitted voice
uses the selected model default; omitted speed uses ai.voiceSpeed; omitted
pause is zero. The SDK snapshots the selected speech configuration and current
punctuation/word-cadence options for that preparation. A part's pause applies
after its final extracted segment. Complete original input remains available
for semantic comparison; default Markdown cleanup affects only the speech
copy and occurs once before segmentation. Set textFormat:'plain' to preserve
each supplied input exactly through that segmentation. The prepared segment
text participates in semantic reuse, so a changed synthesis input does not
reuse audio produced from the cleaned version.
storage is optional. When supplied, {db,table,key} names the caller's ready
DBOPFS instance and its application-owned table/key. The SDK stores complete
generated audio as raw files and retains MIME metadata separately so subsequent
Blob playback preserves its content type. Reuse compares complete semantic
inputs, including parts, selection, segmentation, and separate application
identity. A changed voice, speed, source text, or other semantic selection
prepares the changed request. An SDK patch version alone does not invalidate
stored speech.
The storage key is encoded as encodeURIComponent(key) + '.json' for its
manifest. That version-1 manifest keeps an entries array of semantic variants
under the key. Each entry records
{id,parts,originalParts,selection,segmentation,identity,segments}; the ordered
stored segment entries name {audioFile,contentType}. originalParts retains
the complete source text, including whitespace. These persistent semantic
inputs and identity must be JSON-compatible. Raw audio filenames contain the
generated record ID and segment index. Storage writes preserve existing
variants; the application chooses when its records should be removed.
Matching pending requests on the same AI instance and the same storage
db/table/key share synthesis. Each caller receives its own handle and
cancellation signal. Cancelling one handle detaches that caller; the shared
preparation is aborted only when its last pending caller cancels. Different AI
instances do not share an in-flight synthesis operation. Complete stored
results remain reusable through the same application storage.
Missing audio participates in the provider runtime's shared TTS activation with the preparation's cancellation signal. Cancelling the final pending interest aborts that activation; another preparation, explicit playback activation, or direct TTS load joining it keeps the load alive. Cancellation does not unload a model whose activation has already completed. A complete audio-cache hit never requests model activation. Provider cancellation and cleanup remain cooperative; an aborted caller wait alone is not evidence that native work has stopped. After a durable preparation completes, a later preparation call rereads its manifest and saved-file presence, regenerating only missing segments. A completed preparation without storage reuses its retained Blobs.
Preparation requests retain call order for synthesis admission. Within each request, punctuation segments use the existing bounded provider queue: the default Kokoro capacity admits up to four at once, with later segments waiting in FIFO order. Completion may be out of order; segment metadata and playback remain in input order. Preparing another request does not attach it to playback.
Preparation handle and progress
| Member | Contract |
|---|---|
state |
Current string: queued, preparing, ready, error, or cancelled. |
segments |
Ordered segment metadata with input, voice, speed, pauseAfterMs, index, state, audioFile, contentType, and error. |
ready |
Promise resolving the complete ordered record when every segment is generated or reused and, when storage is supplied, durably saved. Rejects with the complete error or AbortError if cancelled while pending. |
getAudio(index) |
Promise waiting for that ordered segment and returning its complete Blob with the recorded MIME type. |
cancel() |
Cancels this preparation handle without erasing successful stored segments. |
The optional synchronous onState callback receives
{state,completed,total,segments,error}. It observes preparation only; it is
not a playback-state callback. completed counts settled segments, including
failed segments; inspect state, segment errors, and ready for the outcome.
total is the number of ordered segments.
Without storage, the handle retains generated Blobs for its lifetime; the
same result shape has audioFile:null and the actual contentType.
The supplied signal observes preparation until ready settles. Manual
cancel() remains available afterward to stop further reads through that
handle. If a raw audio write has already begun when cancellation arrives,
its metadata transaction finishes so that successful audio can be reused;
no later queued write starts for that cancelled preparation.
Cancelling preparation prevents later synthesis and releases its pending
activation interest. Other pending interests retain the shared load; cancelling
the final interest forwards abort to its provider-owned activation. Actual
provider cleanup remains observable through that operation's settlement.
Playback controls and independent lifetimes
ai.playPreparedTTS(prepared,{signal,onState,audioOutput}) returns
{state,error,finished,pause(),resume(),stop()} immediately. It uses the
existing AI audio-clock scheduler and waits for each earlier segment before scheduling
later audio. Ready adjacent buffers retain contiguous scheduling. Requested
pauses separate adjacent parts; a final trailing pause does not delay
finished, which resolves after the final audio buffer ends.
state and error are current-state getters. The optional synchronous
onState callback receives {state,error} and is observational. Playback
states are waiting, waiting-for-gesture, scheduled, paused, complete,
stopped, and error. Use waiting-for-gesture to present an audio-unlock
control. A false result from resume() is not a first-segment-ready signal;
inspect state and error to distinguish a stopped or unavailable context
from browser gesture waiting. With audioOutput omitted or null, playback
creates its own audio context immediately, before the first segment is ready,
and closes that context after completion, stop, cancellation, or failure.
finished resolves true after natural playback completion and false after
stop, playback cancellation, or failure. Real failures also reach the existing
ai-tts-failure event and complete SDK diagnostics. pause()
and resume() control only this handle's audio context, and stop() stops
that playback handle. The controls return booleans; pause and resume are
asynchronous. Each AI has one playback lane: a new playPreparedTTS() call
stops its preceding streamed or prepared playback, and streamTTS() interrupts
active prepared playback. This replacement preserves detached preparation.
None of these playback controls cancels preparation or deletes saved audio.
Use the preparation handle's cancel() or its separate signal when the
application also wants to stop preparation. Successful saved segments survive
cancellation or failure for later reuse.
ai.stopAudio() stops streamed speech and all prepared playback on that AI,
while detached preparation continues. ai.setSpeechMuted(true) additionally
cancels provider TTS work and unloads the provider. Replacing the speech
configuration cancels missing generation tied to the earlier selection.
Neither action deletes completed stored audio. Apply those broader lifecycle
controls deliberately when the application intends to stop provider work.
Preparation ready and playback finished are separate promises. Natural
playback completion must not be used as a signal to cancel other background
preparations. A page that starts several preparations owns and observes every
ready promise, even when it plays only one handle. Browser autoplay permission
still applies; neither promise proves that a person heard the audio.
Prepare a silent reusable WebAudio output
ai.prepareAudioOutput() synchronously returns an AI-owned output handle.
It creates or reuses that AI's persistent WebAudio context and observes its
actual state. Preparation stays silent: it does not resume the context, unmute
speech, load a model, create an audio source, play a test sound, or install
gesture listeners. It does not wait for speech preparation or provider readiness.
The handle exposes kind:'web-audio', live state, contextState, and error
getters, subscribe(listener), resume(), and dispose(). Repeated preparation
returns the same handle until its output is disposed or its context closes.
contextState is the platform context's current state, or null when unavailable;
error retains the complete actual error, or null.
| Output state | Meaning |
|---|---|
ready |
This WebAudio context is currently running. |
suspended, interrupted, or another platform state |
The context reports that state; suspension alone does not establish a gesture requirement. |
gesture-required |
A resume operation actually reported NotAllowedError while the context was not running. |
unavailable |
Context creation failed; error contains the actual failure. |
error |
An output operation failed; inspect the complete error. |
disposing, disposed |
Owner disposal is in progress or has completed. |
closed |
The underlying context has closed outside completed owner disposal. |
subscribe(listener) immediately replays {kind,state,contextState,error} and
then delivers state changes through the canonical AI event owner. It returns
an idempotent unsubscribe function. The named export
AI_AUDIO_OUTPUT_STATE_EVENT from AI.js is 'ai-audio-output-state'; its
canonical occurrences carry the same snapshot in detail and an output-specific
operationId. Handle subscriptions select only their own output. Disposal or
context closure releases those subscriptions after the terminal notification;
a later handle subscription still receives the current snapshot immediately.
resume() is an explicit asynchronous action for the application's user-gesture
flow. It resolves true only when the context is actually running, and false
when unavailable, disposed, superseded, or held by a paused prepared playback.
A platform rejection rejects with its actual error and retains that failure in
the output state. It does not unmute speech or load a model. Output readiness
does not establish HTML audio readiness, native-speech readiness, future autoplay
permission, available speech audio, or audible delivery.
Pass the handle explicitly to borrow that exact output for prepared playback:
// ai and prepared are the same AI owner and preparation used above.
const audioOutput = ai.prepareAudioOutput();
function observeNarrationOutput(listener) {
return audioOutput.subscribe(listener);
}
function resumeNarrationOutput() {
// Call from the application's user action and observe the returned promise.
return audioOutput.resume();
}
function playNarration() {
return ai.playPreparedTTS(prepared, {audioOutput});
}
function disposeNarrationOutput() {
// Call when this owner no longer needs the output; observe its promise.
return audioOutput.dispose();
}
The selected handle must be live and belong to the same AI instance; another
owner's, closed, or disposing output throws TypeError. Borrowed playback
retains the same ordered scheduling, pause/resume, cancellation, and single-lane
replacement behavior. Completion or stopping playback leaves the output open
for reuse. Merely preparing an output does not change the omitted-option path.
The application owns the output lifetime and calls dispose() when finished
with it. Disposal stops playback bound to that context and any following work
waiting on its clock, observes cleanup, and awaits the actual context close.
It preserves detached preparation, saved audio, unrelated contexts, and model
selection. Concurrent calls share the disposal promise. Actual cleanup or close
errors reject and remain observable; a failed context close permits a later
disposal retry. Stop/mute controls retain their existing playback/provider scope
and do not replace this explicit output disposal.
Stream chunks as they arrive
Use the configured ai created above. Run this snippet from an owned user
action, such as your Speak button, and catch errors with the earlier
error.code / error.message pattern. First load/unmute, then accept chunks.
Call streamTTS(chunk) inside the producer's chunk callback immediately;
waiting for each speech promise there would serialize synthesis. This tiny
example uses three arriving chunks. Replace the three onTextChunk(...) calls
with your actual text stream callback, and flush after that producer ends.
await ai.setSpeechMuted(false);
const pendingSpeech = [];
function onTextChunk(chunk) {
const pending = ai.streamTTS(chunk);
// Attach both handlers immediately, so later failure cannot be unhandled.
pendingSpeech.push(pending.then(
function speechPrepared(value) { return { status: 'fulfilled', value }; },
function speechRejected(reason) { return { status: 'rejected', reason }; }
));
}
onTextChunk('First sentence. ');
onTextChunk('Second sentence. ');
onTextChunk('Third sentence.');
// After the text producer ends, flush trailing text and settle every call.
const finalPrepared = await ai.finishTTS();
const outcomes = await Promise.all(pendingSpeech);
for (const outcome of outcomes) {
if (outcome.status === 'rejected') {
console.error(outcome.reason.code, outcome.reason.message);
} else if (outcome.value === false) {
console.log('Speech was muted, stopped, or failed; inspect SDK diagnostics.');
}
}
if (finalPrepared === false) {
console.log('Final speech was muted, stopped, or failed; inspect SDK diagnostics.');
}
The segmentation default uses sentence punctuation. To submit smaller complete
segments, call ai.configureTTSSegmentation({punctuation:'any',wordCadence:null})
before feeding the stream. Chunk boundaries themselves do not force a sentence
boundary; finishTTS() flushes any remaining text. It is not a playback-ended
notification. Do not mute or dispose immediately after it if playback should
continue.
Automatic model-stream completion reads the AI instance's current mute state.
While muted, it clears pending speech text and formatting state and stops
prepared playback without invoking finishTTS() or streamTTS(). Unmuted
completion keeps its ordinary speech flush. Explicit calls to either public
speech method retain their existing behavior.
For a text-only request, pass speech:false to the object-form
ai.streamRequest({messages, speech:false, onChunk, signal}). Its completion,
failure, and cancellation leave buffered speech, prepared playback, and native
playback alone. The option controls only the request's automatic speech
finalization and stopping; it is not sent to the language provider and does not
mute or unload TTS. Explicit speech calls made by your callbacks still run with
their ordinary behavior. Omitted speech or speech:true preserves the
automatic lifecycle above, and positional streamMessage(...) is unchanged.
Explicit streamTTS(), finishTTS(), and global stopAudio() keep their existing
scope.
Automatic speech-input formatting cleanup
The shared streaming, audio-file, and SpeechPlayback paths remove repeated
same formatting marks from the outbound speech-input copy by default:
ai.streamTTS('## Heading\n**Hello');
ai.streamTTS('**. Next sentence.');
await ai.finishTTS();
The SDK omits runs of two or more of the same *, #, _, backtick, or ~
before speech segmentation. A candidate run split across chunks is still
recognized. Single marks, ellipses, quoted endings, and all other text are
preserved. This is a small narration filter, not a full Markdown parser.
Ordinary prose is forwarded immediately; only a trailing formatting candidate
waits for its next character or the final flush.
end:true, finishTTS(), or cancellation clears pending streaming formatting
state. fetchTTS(), provider-runtime TTS requests, direct Kokoro provider
requests, and SpeechPlayback apply the same default cleanup to complete input.
Complete-input APIs can explicitly select textFormat:'plain' as described
below. SpeechPlayback accepts textFormat:'plain' in its constructor or
prepare() options and forwards each prepared part's exact input through every
supported speech client. Omitted options reuse its current selection; explicit
null restores default cleanup. The streaming streamTTS() options still ignore
textFormat and streaming narration keeps its existing cleanup.
SDK-internal delegation carries {speechInputPrepared:true} outside the speech
payload after its owner selects text treatment, preventing a second
non-idempotent cleanup pass or cleanup of explicitly plain input.
Applications omit that internal metadata. Displayed messages, saved history,
model input, caller payload objects, language, voice, synthesis capacity, and
playback timing are not changed by the filter.
Direct native provider.prepare() and provider.play() preserve their complete
input as supplied. The native route of ai.prepareTTSPlayback() also preserves
that input; shared streaming and default SpeechPlayback perform cleanup before
calling it. Plain SpeechPlayback preserves the complete input instead.
Choose the high-level narration path when that speech-only
formatting cleanup is desired.
The shared helper is public for code that needs the speech-only transformation directly:
import {
MarkdownSpeech,
stripSpeechFormatting
} from 'arcane-os/speech-text';
console.log(stripSpeechFormatting('**Hello**')); // Hello
const speechText = new MarkdownSpeech();
console.log(speechText.append('## Head', false)); // ' Head'
console.log(speechText.append('ing', true)); // ing
Exact complete synthesis input
Use the public textFormat:'plain' selection for already-authored speech or an
exact generated preview. It belongs in prepareTTS options, or alongside the
input/text field of a complete synthesis payload:
function preparePreview(ai, preview, signal) {
return ai.prepareTTS({parts: [preview], textFormat: 'plain', signal});
}
function fetchPreview(ai, preview, signal) {
return ai.fetchTTS({input: preview, textFormat: 'plain'}, signal);
}
The same payload field is supported by AI.prepareTTSPlayback() for audio-file
providers, AIProviderRuntime.request('tts', {operation:'synthesize', payload, localOnly, signal}), its synthesize(payload, options) alias, and direct
Kokoro request() calls using either {text,...} or {model,input,...}.
The SDK consumes textFormat before engine/host dispatch. The original nonblank input,
including whitespace and repeated formatting marks, remains unchanged; voice,
speed, selected model, cancellation and audio ordering retain their existing
owners. Only 'plain' selects exact input; omission and other values retain
the existing narration cleanup. Applications never supply the internal
speechInputPrepared argument.
This selection governs SDK text treatment, not pronunciation. The browser Kokoro
adapter preserves the selected runtime's own text normalization and phonemization,
then uses a genuine tokenizer subclass with truncation disabled. Its Kokoro
subclass sends every resulting content token through the public
generate_from_ids() method. Short inputs retain one inference; longer inputs
use ordered segments within the selected engine's inference capacity and return
all audio in order. This is token-based engine transport, not a character limit,
text estimate, summary, or input discard.
The selected Kokoro model
accepts 510 content tokens plus its two outer padding positions per inference.
Each segment receives that padding independently. Upstream voice-style selection,
speed, waveform generation and sample rate remain upstream-owned. The SDK reuses
one loaded model; it does not copy the private phonemizer, replace instance methods,
load a second model, or import another Transformers runtime. Its tokenizer
reconstruction uses the same effective selection as the
Kokoro 1.2.1 factory.
That factory ignores a revision option; this change does not establish revision
pinning that the upstream factory does not implement.
Cancellation is request-local and checked between inference segments and before returning audio. As with the existing Worker contract, cancellation prevents later work and output while an already-running upstream inference may finish. Unload and disposal retain their existing Worker-termination boundary. Source and controlled-fixture coverage of token/audio preservation are distinct from speech quality or execution evidence for a particular model, browser and device.
Choose a device or reduce memory use
Local Whisper and Kokoro roles accept
execution:{device,maxConcurrentRequests}. Omitting stt.execution selects
{device:'auto',maxConcurrentRequests:1}; omitting
tts.execution selects {device:'auto',maxConcurrentRequests:4}. Whisper keeps
one transcription slot. Kokoro accepts capacities 1 through 4.
Automatic selection tries webnn-npu when navigator.ml.createContext is
exposed, then webgpu when navigator.gpu is exposed, then CPU through wasm.
API exposure only determines which upstream backend to attempt; it
does not establish compatible hardware, operators, or model shapes. A failed
candidate is fully cleaned up before the next candidate uses fresh Workers
with the same prepared model and dtype. An explicit webnn-npu, webgpu, or
wasm selection attempts only that backend and reports its failure.
These are four alternative TTS configurations:
async function selectSpeechExecution(execution) {
await ai.configureBrowserSpeech({
protocol: AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL,
id: 'hello-speech',
dbopfs,
tts: {
providerId: 'hello-kokoro',
model: speechSelection.model,
runtime: speechSelection.runtime,
offline: false,
execution
}
});
}
// Choose and call one from your application settings action:
// await selectSpeechExecution({ device: 'auto' });
// await selectSpeechExecution({ device: 'webnn-npu' });
// await selectSpeechExecution({ device: 'webgpu' });
// await selectSpeechExecution({ device: 'wasm', maxConcurrentRequests: 1 });
The TTS capacity override accepts integers 1, 2, 3, or 4; an STT capacity
override accepts only 1. Apply the same device choices to the configured
stt.execution record. A configuration change leaves TTS muted; explicitly
load/unmute again.
The selected upstream versions expose WebNN NPU through Transformers.js device selection and Kokoro.js device forwarding. WebNN compatibility depends on the model's shapes and operations, browser, drivers, and hardware. Unsupported operations may run through WASM even after an NPU session loads; see the ONNX Runtime WebNN contract.
Inspect the requested and selected device
Request the execution projection on the existing public runtime status after
loading. This explicitly reads the selected provider's current report; ordinary
status() retains its existing sticky snapshot and identity. There is no
separate execution-state event subscription.
function printSpeechStatus(role = 'tts') {
const status = ai.providerRuntime.status(role, { execution: true });
const execution = status.execution;
console.log('Speech role and state:', role, status.state);
if (execution) {
console.log('Requested device:', execution.requestedDevice);
console.log('Selected device:', execution.selectedDevice);
console.log('Capacity:', execution.maxConcurrentRequests);
console.log('Active requests:', execution.activeRequestCount);
console.log('Automatic WASM fallback:',
execution.requestedDevice === 'auto' && execution.selectedDevice === 'wasm');
}
}
Call printSpeechStatus() after the load in sayHello() or from your status
button. The same projection is at
ai.providerRuntime.status(null, {execution:true}).roles.tts.execution.
For configured Whisper, call printSpeechStatus('stt') or read the corresponding
roles.stt.execution projection.
selectedDevice is null
until a pool is selected and returns to null on unload. Providers without an
execution report omit execution; do not infer a device from navigator.gpu
or a configured preference alone. An explicit inspection can throw a provider
status error; handle it with the same error.code / error.message pattern.
selectedDevice names the backend requested by the successful upstream session
load. It does not prove that every operation ran on a physical NPU or GPU, or
establish transcription correctness, pronunciation, or audio quality. Evaluate
actual speech output for the model, dtype, browser, and device combinations your
application supports.
Stop, mute, cancel, and release
These are actions for your own controls, using the same ai instance:
function stopSpeech() {
ai.stopAudio(); // Stops streamed speech and all prepared playback.
}
async function muteSpeech() {
await ai.setSpeechMuted(true); // Stops audio and unloads TTS.
}
async function unmuteSpeech() {
await ai.setSpeechMuted(false); // Loads TTS and permits playback.
}
async function unloadSpeech() {
ai.stopAudio();
await ai.providerRuntime.unload('tts'); // Keeps the provider configured.
}
async function disposeSpeech() {
await ai.disposeBrowserSpeech(); // Releases SDK-owned speech providers.
}
STT has its own ai.providerRuntime.load('stt') and unload('stt') lifecycle
when configured; muting TTS does not unload STT or the LLM. A directly created
provider similarly exposes load(), unload(), and final dispose().
disposeBrowserSpeech() leaves application-owned DBOPFS and stored artifacts
in place; it does not erase the application's data.
For a cancellable individual synthesis, unmute first. A fresh browser speech
configuration is muted, so calling providerRuntime.load('tts') directly at
that point rejects with ARCANE_AI_TTS_MUTED. fetchTTS() accepts an
AbortSignal as its second argument, cleans repeated formatting marks from the
outbound input copy, and returns a WAV Blob without playing it:
const synthesisController = new AbortController();
async function synthesizeOneSentence() {
try {
await ai.setSpeechMuted(false);
const result = await ai.fetchTTS({
input: 'This request can be cancelled.',
responseFormat: 'wav'
}, synthesisController.signal);
console.log(result); // The complete WAV Blob, with type 'audio/wav'.
return result;
} catch (error) {
console.error(error.code, error.message); // Keep the complete message.
throw error;
}
}
function cancelSynthesis() {
synthesisController.abort();
}
Call synthesizeOneSentence() from an owned UI action and catch its rejection;
wire cancelSynthesis() to its Cancel control. The controller cancels the
individual fetchTTS() request; it does not control the preceding
setSpeechMuted(false) load/unmute lifecycle. Use a fresh controller for each
new operation. Configuration accepts configureBrowserSpeech(configuration, {signal}); disposal accepts disposeBrowserSpeech({signal}). The streaming
playback methods do not accept a caller signal; wire
your stream's abort action to ai.stopAudio() as well as aborting its producer.
Cancellation suppresses late results, but upstream Kokoro may finish active
engine work before the affected slot is reusable.
const textStreamController = new AbortController();
textStreamController.signal.addEventListener('abort', function stopStreamAudio() {
ai.stopAudio();
}, { once: true });
function cancelTextAndSpeech() {
textStreamController.abort();
}
Pass that same textStreamController.signal to your text producer's supported
signal option. Connect cancelTextAndSpeech() to Cancel; use a new controller
for the next stream.
Advanced provider and artifact reference
The SDK does not choose a runtime, model, voice, catalog, prompt, or product
policy. Applications keep those choices. Nothing is downloaded or activated
until the application explicitly calls load().
Ordinary speech operation is the complete functional path. It uses the selected upstream Transformers or Kokoro package and the browser's normal networking, Worker, and Cache APIs. The records returned by this entrypoint are ordinary JavaScript objects and arrays. Callers may copy, extend, and present complete records; this contract does not freeze them or shorten their content.
Availability
| Host | Availability | Notes |
|---|---|---|
| Local browser models | Shipped | Require Workers, Fetch, Blob/File, object URLs, DBOPFS/OPFS, and Web Locks. Blob/File STT requests also require the browser audio decoder. |
| Native WebView | Conditional | Available when the WebView exposes the browser APIs above. It does not invoke Core speech. |
| 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. |
| 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. |
| 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. |
| Native browser TTS | Explicitly selected, conditional | Requires speechSynthesis and SpeechSynthesisUtterance. Uses the native voice catalog and playback lifecycle; no Worker, OPFS, or SDK model download. Native voices expose no audio export. |
STT and TTS own independent provider lifecycles. A failure or cancellation in one role does not disable the other role or authorize a fallback provider.
Public exports
import {
BROWSER_SPEECH_ARTIFACT_GRAPH_PROTOCOL,
BROWSER_SPEECH_ARTIFACT_PROTOCOL,
createBrowserKokoroProvider,
createBrowserSpeechRecognitionProvider,
createBrowserSpeechSynthesisProvider,
createBrowserSpeechArtifactGraph,
createBrowserSpeechAuthority,
createBrowserWhisperProvider,
createDbopfsSpeechArtifactStore,
createDigitalOceanFalTTSProvider,
createTwinCloudTTSProvider,
removeBrowserSpeechModelCache
} from 'arcane-os/ai/browser-speech';
Importing this entrypoint downloads nothing, opens no cache, creates no Worker, and publishes no event.
Protocol identifiers
These exact strings identify the current public contracts:
| Subject | Exact value |
|---|---|
| Artifact-store protocol | arcane-ai-browser-speech-artifacts/1 |
| Artifact-graph protocol | arcane-ai-browser-speech-artifact-graph/1 |
Graph kind and prepared runtime.moduleGraph |
browser-speech-authenticated-artifact-graph |
Single-module runtime.moduleGraph |
self-contained |
| Model authority | arcane-ai-model-authority/1 |
| Provider | arcane-ai-provider/2 |
| Worker | arcane-ai-speech-worker/1 |
| Worker error envelope | arcane-ai-speech-worker-error/1 |
| Nested module Worker | arcane-ai-browser-speech-artifact-module-worker/1 |
The word authenticated in the graph discriminator does not activate an
authentication, admission, or isolation stage. It is the current protocol
value.
createBrowserSpeechArtifactGraph()
An artifact graph describes the caller-selected runtime, model, voice, and supporting files that the SDK stores and materializes. It is a routing and selection record, not an execution permission list.
const graph = createBrowserSpeechArtifactGraph({
providerId: 'my-whisper',
role: 'stt',
model: {
id: 'whisper-small',
repository: 'publisher/whisper-small',
revision: 'selected-model-revision',
dtype: 'q8',
inputSampleRate: 16000
},
runtime: {
adapter: 'transformers-whisper',
version: 'selected-runtime-version',
revision: 'selected-runtime-revision',
entrypoint: 'runtime/transformers.js',
onnxWasm: {
namespace: 'transformers-env-backends-onnx-wasm',
mjsPath: 'runtime/ort-wasm.mjs',
wasmPath: 'runtime/ort-wasm.wasm'
}
},
files: [
{
kind: 'runtime-entrypoint-javascript',
path: 'runtime/transformers.js',
sourceUrl: 'https://publisher.example/transformers.js',
revision: 'selected-runtime-revision',
mediaType: 'text/javascript'
},
{
kind: 'runtime-auxiliary-javascript',
path: 'runtime/ort-wasm.mjs',
sourceUrl: 'https://publisher.example/ort-wasm.mjs',
revision: 'selected-runtime-revision',
mediaType: 'text/javascript'
},
{
kind: 'runtime-wasm-binary',
path: 'runtime/ort-wasm.wasm',
sourceUrl: 'https://publisher.example/ort-wasm.wasm',
revision: 'selected-runtime-revision',
mediaType: 'application/wasm'
},
{
kind: 'model-onnx-binary',
path: 'model/encoder.onnx',
sourceUrl: 'https://publisher.example/encoder.onnx',
revision: 'selected-model-revision',
mediaType: 'application/octet-stream',
runtimeRequestUrls: [
'https://publisher.example/model/encoder.onnx'
]
}
]
});
Model and runtime selection
role is stt or tts. The runtime adapter is
transformers-whisper for STT and kokoro-js for TTS. The caller supplies the
model id, repository, revision, dtype, sample rate, and runtime version and
revision.
STT requires model.inputSampleRate. TTS requires
model.outputSampleRate, model.defaultVoice, and a nonempty
model.voices array of {id,path} records. Each voice path names a declared
voice-style-binary file. runtime.onnxWasm names the selected ONNX module and
WASM files. numThreads is optional for Transformers and is not inferred from
hardware. Kokoro does not expose that field.
File records
Each file record uses:
{
kind,
path,
sourceUrl,
revision,
license?,
mediaType,
sourceMediaType?,
runtimeRequestUrls?
}
path is a normalized relative path. sourceUrl is the caller-selected source
used for installation. runtimeRequestUrls lists aliases used by upstream
runtime code for the same stored file. mediaType becomes the materialized
Blob type; sourceMediaType may describe a different upstream response type.
The SDK does not require or interpret legal metadata at runtime. If the caller
includes license, the graph preserves that complete value as inert metadata;
runtime materialization never treats it as capability or admission data.
Runtime file kinds are runtime-entrypoint-javascript,
runtime-auxiliary-javascript, runtime-wasm-binary, and
runtime-opaque-data. Model/data kinds are model-configuration-json,
model-generation-configuration-json, model-onnx-binary,
model-onnx-external-data, model-preprocessor-json,
model-tokenizer-json, model-opaque-data, and voice-style-binary.
Graph construction requires paths and route aliases to be unambiguous so one known URL maps to at most one stored file. The runtime router is independently permissive: if ambiguous routing metadata nevertheless reaches it, that URL is left unmapped and uses the native browser operation.
createDbopfsSpeechArtifactStore()
const store = createDbopfsSpeechArtifactStore({
dbopfs,
tableName: 'arcane_ai_browser_speech'
});
The store exposes {protocol,tableName,prepare,remove,fetchResource}. It serializes updates
to one selected authority with Web Locks, downloads each caller-selected file
after explicit provider activation, stores it in DBOPFS, and reopens the stored
file before materialization. Missing storage, an unreadable response, a failed
HTTP request, a missing stored file, or cancellation rejects honestly.
The store writes ordinary mutable selection metadata before the selected files. On a later load, a changed file inventory or source mapping is a cache miss and is downloaded again. The selection metadata is not a completion, integrity, or publication receipt and never blocks ordinary loading.
prepare(authority,{signal,onProgress,offline=false,security}) accepts an
SDK-created artifact graph or upstream-package authority. offline:true uses
only existing DBOPFS state. Preparation returns the selected runtime/model
configuration, cache as installed or cached, object URLs, and a release()
function that revokes the materialized URLs.
security records caller intent only. Ordinary preparation does not
forward a security payload to the Worker and performs no security work. Passing
{secure:true} does not activate hardening. Any future hardening stage requires
a separate user review and an explicit implementation change before it may execute.
removeBrowserSpeechModelCache()
const removal = await removeBrowserSpeechModelCache(
{repository: 'Xenova/whisper-small', signal}
);
Exported by arcane-os/ai/browser-speech. This explicit operation removes all
cached revisions of the exact selected Hugging Face model from the current
origin's existing transformers-cache. It returns {repository,removed},
where removed contains the complete URLs actually deleted. An absent cache or
model returns an empty list. Other models, runtime files, the Kokoro voice
cache, DBOPFS files, and preferences remain untouched. No model is loaded or
downloaded and no inference is started.
The caller owns model retirement policy and must stop using the retired model
before removal so a live provider cannot download it again. This operation is
separate from store.remove(authority), which removes declared DBOPFS files.
cacheStorage may explicitly supply a CacheStorage implementation; it defaults
to globalThis.caches. Missing storage rejects with
ARCANE_AI_STORAGE_UNAVAILABLE; cache failures reject with
ARCANE_AI_STORAGE_DELETE_FAILED. An aborted signal stops further removals
with AbortError. Deletions already completed are retained in error.removed;
removal is repeatable and does not claim transactional rollback. The operation
enumerates one cache once and awaits each matching deletion in order.
Ordinary module routing
Artifact-graph preparation reads each stored JavaScript module, discovers
ordinary module operations, and materializes the complete stored files as
object URLs. The prepared runtime retains the existing
browser-speech-authenticated-artifact-graph discriminator.
The Worker installs one private module router before importing the entrypoint:
- a static import whose target is a known stored file uses that file's materialized URL;
- a dynamic import whose target is known imports the materialized URL;
- a fetch whose target is known reads the materialized URL;
- a Worker whose target is known starts the SDK role Worker and imports the materialized target there.
Resource ownership extends to ordinary self-contained speech runtimes as well as graph runtimes:
- an unmapped relative or URL-like import resolves against the calling module's original source URL, while a bare specifier remains unchanged for native import-map resolution;
- an unmapped HTTP fetch preserves the caller's request options and reaches the parent DBOPFS resource store; Blob/data resources remain local;
- an unmapped module Worker starts through the SDK role Worker and forwards its resource fetches to that same store; and
- an unmapped classic Worker retains native execution. Its internal fetches are outside this module-Worker routing contract.
Inside the dedicated speech Worker, native CacheStorage is temporarily
unavailable before runtime import, so the selected runtime takes its existing
fetch path. Transformers' public browser/custom/filesystem cache switches are
disabled there. After the runtime import, Kokoro's voice caches.open() calls
use a Worker-local response cache with actual match/put results. It retains
complete response bodies and metadata in memory for that Worker only; a miss
does not fetch, and ordinary fetch still owns durable DBOPFS persistence. This
temporary response retention is separate from Kokoro's own in-memory voice
arrays. Unload clears it, and an unfinished put cannot repopulate it afterward.
The SDK neither clears nor changes saved native caches. The original Worker
environment is restored on unload or failed import. Existing explicit
removeBrowserSpeechModelCache() remains a separate caller-selected operation.
The existing speech store exposes fetchResource(input, options) for its
Worker loaders, returning {file,status,statusText,headers,url,redirected}.
New declared files and fetched model, voice, configuration, and WASM resources
stream into ordered DBOPFS parts even without a response length. Closed-part
progress uses unit:'shards'; declared-file progress retains unit:'files'
with the current shard record in resource. Complete declared peer files and
closed resource parts survive cancellation. Existing whole-file caches remain
readable without migration. HTTP resume is used only when the server returns
the requested range; otherwise the complete resource is downloaded anew.
Worker termination aborts and joins its parent-owned resource operations.
An existing complete resource reports phase:'load', cached:true, and
message:'Reading stored model resource' with its complete shard progress.
That event describes a DBOPFS read, not a network download or model-session
reload. The resource owner does not fetch that stored response again.
Routing discovery is best effort and is not an admission gate. If the scanner cannot interpret a module, that module is left unchanged and follows its native URLs. A static-import cycle may likewise retain an original source URL where a target has not yet been materialized. These fallbacks preserve functionality; they do not silently convert into a rejection policy.
ONNX runtime configuration
The Worker applies only the selected runtime settings needed to run the chosen provider:
- Kokoro forwards the pool's selected
webnn-npu,webgpu, orwasmdevice toKokoroTTS.from_pretrained(). Its configured dtype remains exactly the caller-selected dtype on every path. The WASM path also usesnamespace.env.wasmPaths = {mjs,wasm}. - Transformers forwards the selected device to its speech-recognition pipeline,
preserves the caller-selected dtype, and uses
namespace.env.backends.onnx.wasm.wasmPaths = {mjs,wasm}, keeps remote model loading enabled, and applies caller-selectednumThreadswhen present.
Transformers and Kokoro use their normal provider request semantics through
the selected DBOPFS resource owner. Kokoro voice aliases and Transformers
model aliases may be listed in runtimeRequestUrls so an upstream request for a
known file resolves to the already materialized local file. Model, voice, and
runtime selection remains with the application and upstream publisher.
Native ESM imports remain platform-owned; only declared runtime modules are
materialized by artifact preparation. Custom classic Worker internals are not
claimed as stored by this contract.
Providers
const whisper = createBrowserWhisperProvider({
id: 'my-whisper',
graph,
store
});
const kokoro = createBrowserKokoroProvider({
id: 'my-kokoro',
graph: kokoroGraph,
store,
execution: {
device: 'auto',
maxConcurrentRequests: 4
}
});
The constructors also accept ordinary model and runtime descriptors instead
of graph; the two forms are mutually exclusive. Both forms require an
SDK-created DBOPFS speech artifact store. localOnly remains true.
Both constructors accept the exact execution record
{device,maxConcurrentRequests}. device is auto, webnn-npu, webgpu, or
wasm. Whisper permits capacity 1 and defaults to
{device:'auto',maxConcurrentRequests:1}. Kokoro permits integer capacities 1
through 4 and defaults to {device:'auto',maxConcurrentRequests:4}. auto
tries WebNN NPU when navigator.ml.createContext is exposed, then WebGPU when
navigator.gpu is exposed, then WASM. Before advancing after a failed load,
the SDK tears down the candidate and creates fresh Workers using the same
prepared model and dtype. Explicit device selections never fall back.
Each constructor returns an arcane-ai-provider/2 object with:
{
protocol,
role,
id,
localOnly,
maxConcurrentRequests,
catalog,
inspect,
status,
load,
request,
unload,
dispose
}
catalog(), inspect(), and status() do not activate a provider. load() is
the explicit activation boundary. The caller supplies model/profile policy and
may display the provider's lifecycle status. unload() releases the Worker and
materialized URLs. dispose() performs final teardown and prevents later use.
Upstream-package authority
createBrowserSpeechAuthority({providerId,role,model,runtime,security}) creates
the ordinary single-entrypoint authority. The model descriptor is
{id,repository,revision,dtype?,defaultVoice?,voices?,files?}. The runtime descriptor is
{adapter,version,revision,entry,wasmPaths?,files}. A file is
{path,url,mediaType?}. Model files may be omitted so the selected upstream
provider performs its normal model and voice downloads after explicit use.
The authority record is a mutable complete record. An omitted ordinary security
option produces no security field. A present {secure:true} value records only
the caller's future intent and does not change loading or routing behavior.
Whisper STT
const result = await whisper.request({
role: 'stt',
operation: 'transcribe',
signal,
payload: {
audio: pcmFloat32,
sampleRate: 16000
}
});
The provider-native payload is mono Float32Array PCM at the selected input
sample rate, and the result is {text}. The shared AI form accepts
{audio:Blob|File,mimeType,model} and uses the browser decoder to produce the
same mono input. The SDK preserves the complete returned transcript.
Kokoro TTS
const result = await kokoro.request({
role: 'tts',
operation: 'synthesize',
signal,
payload: {
text: 'Hello from Arcane.',
voice: 'caller-voice-id',
speed: 1
}
});
The voice belongs to the caller-selected inventory; omission uses that model's
default voice. The provider-native result is
{audio:Float32Array,sampleRate,voice}. The provider/2 shared request form accepts
{model,input,textFormat?,responseFormat:'wav',voice?,speed?} and returns
{audio:Uint8Array,contentType:'audio/wav'}. High-level AI.fetchTTS() wraps
that provider result in a WAV Blob. Returned provider records remain ordinary
mutable values. Both payload forms accept textFormat:'plain' to retain exact
input; omission keeps the default speech-formatting cleanup.
Lifecycle and cancellation
Provider states are unloaded, loading, ready, unloading, error, and
disposed. status() includes role, provider/model ids, state, lifecycle
status and reason, active operation, loaded/busy flags, generation, error code,
cache state, and warnings. Both speech roles include an execution record
with requested and selected device, request limit, and active request count. A
successful selectedDevice reports the backend requested by the upstream model
load. It does not prove that every operation ran on a physical NPU or GPU,
that accelerator kernels overlap, or that generated speech is correct. WebNN
may execute unsupported operations through WASM. A security field is absent
in ordinary mode.
The provider/2 load context accepts an optional progress callback. Speech
artifact preparation and upstream model loading publish records through that
callback and the shared sticky AI runtime state. Records include phase,
stage, message, the current file when available, and elapsedMs.
Known artifact inventories report completed, total, and unit:'files'.
Upstream model files are discovered during loading, so their total remains
null while completed files are counted. Generic upstream model callbacks use
phase:'load'; upstream readiness or completion of the current file set uses
phase:'initialize'. These callbacks do not establish a network transfer or a
cache miss. Resource-fetch progress owns phase:'download', while an existing
DBOPFS resource reports phase:'load' with cached:true. A later generic model
callback reports model loading without reclassifying that stored read as a
download. Remaining TTS pool initialization
reports completed model sessions. Consumers use an indeterminate progress bar
when no final total is known; transfer quantities do not determine this display.
The Worker sends intermediate {protocol,id,type:'progress',progress} messages
for its active load request. They do not settle the request; the ordinary final
response still owns completion or failure. Complete upstream callback content
remains in the adjacent detail field for diagnostics. Compatible observers
receive the latest progress when joining an active load, and cancelled,
superseded, or settled loads stop publishing progress.
Compatible concurrent loads share one underlying preparation and pool load while each caller retains its own cancellation signal. One observer cannot cancel another still-active observer; cancellation of the final observer stops the shared load.
Whisper retains one active role request. Kokoro admits synthesis requests up to
its declared capacity and rejects a direct over-capacity call with
ARCANE_AI_PROVIDER_BUSY; the provider-neutral runtime keeps overflow in FIFO
order. Each Kokoro slot owns a distinct Worker and loaded model session because
the selected browser adapter serializes inference inside one JavaScript
isolate. The SDK prepares the artifact URLs once and shares that same prepared
selection across the bounded pool. Pool activation completes the first model
session before it starts the remaining Workers, then loads those remaining
sessions concurrently. This avoids multiplying simultaneous cold artifact
acquisition while still making the configured synthesis capacity ready in
parallel.
Cancellation of one active Kokoro synthesis suppresses only that request and sends the Worker's targeted cancel control. The selected upstream Kokoro version may finish already-running engine work before that slot can run its next request; the SDK does not claim stronger per-call preemption. Whisper cancellation and role unload/dispose retain destructive Worker teardown. Kokoro unload/dispose abort every active request, terminates every pool Worker, and releases materialized URLs once. Late results cannot settle a cancelled or superseded operation.
The provider owns no event bus. Applications may project promises and status
into the SDK's shared event/state owner. Mute and unmute are likewise
application state: mute may await unload(), and unmute may explicitly call
load().
Errors
Errors retain the normal code, message, reason, and cause fields used by
the browser AI runtime.
The Worker error envelope carries cause as an optional mutable diagnostic
record. It preserves complete nested messages, stacks, codes, reasons, details,
own properties, and cycles without a depth or content cap. Worker and client
sources must come from the same SDK revision so their /1 envelope shape is
updated atomically; a current client still accepts the cause-free four-field form.
If the platform cannot clone an exotic diagnostic value, the Worker keeps the
complete raw failure in its console diagnostics and retries the response with
that cause-free four-field envelope so the caller still receives an error.
Representative stable codes include:
ARCANE_AI_INVALID_REQUESTARCANE_AI_MODEL_AUTHORITY_REQUIREDARCANE_AI_PROVIDER_UNAVAILABLEARCANE_AI_PROVIDER_BUSYARCANE_AI_PROVIDER_DISPOSEDARCANE_AI_REQUEST_ABORTEDARCANE_AI_OPERATION_SUPERSEDEDARCANE_AI_ARTIFACT_DOWNLOAD_FAILEDARCANE_AI_ARTIFACT_OFFLINE_MISSARCANE_AI_STORAGE_BUSYARCANE_AI_STORAGE_UNAVAILABLEARCANE_AI_WORKER_MESSAGE_ERROR
Malformed selected descriptors, missing required files, unreadable responses, unsupported provider namespace shapes, and unavailable browser APIs reject at their functional owner. An unmapped runtime route retains the native browser operation.
Ownership
- Applications own model, runtime, dtype, sample-rate, voice, profile, prompt, catalog, activation, optional STT/TTS execution override, and presentation policy.
- Upstream publishers own their runtime, model, voice, and license delivery.
- The SDK owns storage, materialization, routing, Worker lifecycle, normalized provider contracts, cancellation, and cleanup.
- The SDK redistributes no third-party runtime, model, or voice package through this entrypoint.