arcane-os 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -72,6 +72,7 @@ version; WebKitGTK availability must not be generalized to macOS.
72
72
  | Select and observe independent LLM/STT/TTS roles | `/arcane/modules/AIProviderRuntime.js` and `AIRuntimeState.js` | **Cross-host** controller/state; registered providers retain their own host requirements | Required/projected provider members, route/configuration records, and status fields; per-role lifecycle, cancellation, stream cleanup, sticky state, and startup barriers are normalized. `localOnly` creates no fallback. |
73
73
  | Run a caller-selected local LLM entirely in a browser renderer | `arcane-os/ai/browser-wasm` through `createArcaneAI()` | **Browser** only; secure context, WebAssembly, and OPFS/DBOPFS; full WebGPU offload by default or explicit CPU with `gpuLayers:0` | The public AI API module normalizes multi-model lifecycle, status, complete all-choice streaming, cancellation, exact ordered structural tool-call visibility, and session persistence. Model sources are canonical ordered file descriptors; licenses and model choice remain application policy. |
74
74
  | Run caller-selected Whisper or Kokoro in a browser renderer | `arcane-os/ai/browser-speech` registered with `AIProviderRuntime` | **Browser** only; DBOPFS, Web Locks, Workers, Fetch/object URLs, and a caller-supplied self-contained runtime/model closure are required | STT/TTS use independent provider/2 lifecycle and status. Kokoro adds bounded Worker/session concurrency and explicit `auto`, `webgpu`, or `wasm` execution. Complete model/runtime selection, offline behavior, cancellation, Worker teardown, and request/result shapes are normalized. No runtime/model content or cloud fallback is supplied. |
75
+ | Capture speech with the explicitly selected native browser service | `createBrowserSpeechRecognitionProvider` from `arcane-os/ai/browser-speech`, configured as STT | **Browser / native WebView**, conditional on `SpeechRecognition` or `webkitSpeechRecognition`; no SDK model, Worker, or storage required | `localOnly:false` because the browser may use a remote service. Factory/catalog remain usable without native support; inspect/load report availability. User-gesture start, exact final results, transient interim text, stop/drain and abort are owned by the provider/runtime and shared controls. No provider fallback. |
75
76
  | Prepare ordered speech playback or reuse the speech-input formatting filter | `arcane-os/speech-playback` and `arcane-os/speech-text` | **Node** with injected media adapters, or **Browser / Native WebView** media; the text filter itself is **Cross-host** | Stored and caller-owned text stays exact. Only the outbound synthesis copy automatically loses repeated same formatting marks. A capacity-advertising provider receives complete segments immediately while retaining indexed playback; native/custom synthesis stays serialized. |
76
77
  | Preserve complete chat history and memory | `/arcane/modules/PersistentAIChatSession.js` | **Browser / native WebView** with ChatEntity/DBOPFS and a configured chat function | Existing DBOPFS names and memory semantics are preserved. Live-context commit is atomic; durable persistence is explicit and coherent across user/assistant turns and atomic all-ID tool-result batches. |
77
78
  | Search an app-owned document corpus for explicit chat context | `/arcane/modules/DBOPFSDocumentLibrary.js` | **Browser** or compatible injected DBOPFS adapter | Generation/manifest completion, complete lexical search, partial read failures, and untrusted context labels are normalized. Construction does not search; an explicitly wired context builder performs retrieval for each prepared chat send. |
@@ -79,7 +80,7 @@ version; WebKitGTK availability must not be generalized to macOS.
79
80
  | Use local AI without coupling app code to Ollama HTTP | `Arcane.localAI`, `Arcane.ai`, or `/arcane/modules/Ollama.js` | Primarily **Native**; Android exposes a narrower admitted inference projection | Admission, errors, and managed-operation events are normalized. Direct Ollama response envelopes remain **Provider-native**. |
80
81
  | Use TWiN Cloud from the renderer profile | `/arcane/modules/AI.js` | **Cloud** from an allowed browser/native renderer | High-level chat behavior is normalized by the module. The TWiN access key authenticates remote LLM chat; raw provider diagnostics remain provider-specific. No automatic cloud fallback is inferred from local failure. |
81
82
  | Send one TWiN Cloud request with an explicit key and model | `fetchRequest` from `arcane-os/ai/twin-cloud` | **Node** and **Browser**, using standard Fetch and a remote HTTPS provider | Keeps complete messages and returns the full parsed provider JSON. Rejected Fetch and HTTP 529 share three retries after 3000 ms; HTTP 429 overload retains unlimited three-second retries. Optional `onRetry` reports waiting/requesting phases without blocking recovery. Shared structured-output mapping and cancellation match browser TWiN transport. No browser profile, AI/user singleton, DBOPFS, or retained request history is created. |
82
- | Use speech through one application helper | `/arcane/modules/AI.js` and `Arcane.speech` | **Browser** or **Native** | The helper keeps audio on device: Whisper owns STT and Kokoro owns TTS. It automatically cleans only the outbound speech-input copy and normalizes application-facing audio/text behavior while browser and native request/response plumbing differs below that boundary. |
83
+ | Use speech through one application helper | `/arcane/modules/AI.js` and `Arcane.speech` | **Browser** or **Native** | Whisper and Kokoro keep their on-device paths; explicitly selected Web Speech STT and TWiN Cloud TTS declare their non-local behavior. The helper automatically cleans only the outbound synthesis-input copy and normalizes application-facing audio/text behavior while request/response plumbing differs below that boundary. |
83
84
  | Inspect or manage raw Ollama models | `Arcane.ollama` or `/arcane/modules/Ollama.js` | **Native** desktop Core for management; narrower Android inference only | Wrapper method names, errors, streaming correlation, and admission are Arcane-controlled. Direct Ollama success envelopes are intentionally provider-native. |
84
85
  | Use native terminal, installation, user, provisioning, or machine controls | matching `Arcane.*` namespace | **Native** and app/capability restricted | Calls and errors use the common bridge contract. Platform results can be host-specific and are marked in the method guide. |
85
86
 
@@ -3134,6 +3134,20 @@
3134
3134
  "protocol": "arcane-ai-provider/2 tts/synthesize",
3135
3135
  "normalization": "Construction and activation make no adapter network requests. SDK initial POSTs bypass the provider-local js-queue; follow-up status/result/audio GETs use followUpQueue capacity and cadence through complete body consumption. Retry-After affects only its job. Whole-job maxConcurrentRequests remains independent. Local Kokoro and Whisper paths are unchanged. Applications own model, voice, credentials, activation, and queue settings."
3136
3136
  },
3137
+ {
3138
+ "id": "browser-speech:createBrowserSpeechRecognitionProvider",
3139
+ "name": "createBrowserSpeechRecognitionProvider",
3140
+ "displayName": "createBrowserSpeechRecognitionProvider()",
3141
+ "kind": "function",
3142
+ "signature": "createBrowserSpeechRecognitionProvider({id,model,language}={})",
3143
+ "entrypoints": ["arcane-os/ai/browser-speech"],
3144
+ "primaryImport": "arcane-os/ai/browser-speech",
3145
+ "group": "Browser speech providers",
3146
+ "summary": "Creates an explicitly selected native browser speech recognition provider with gesture-started live capture.",
3147
+ "availability": "Browser or WebView exposing SpeechRecognition or webkitSpeechRecognition; construction remains available on unsupported hosts",
3148
+ "protocol": "arcane-ai-provider/2 stt with optional createCapture",
3149
+ "normalization": "Non-local service declaration, independent STT lifecycle, exact final segments, transient interim results, graceful stop/drain and native abort. No model download, storage, microphone on activation, file-transcription emulation, or automatic fallback."
3150
+ },
3137
3151
  {
3138
3152
  "id": "browser-speech:createBrowserKokoroProvider",
3139
3153
  "name": "createBrowserKokoroProvider",
@@ -54,6 +54,16 @@ imported component's `destroy()` through `HTMLImport`; callers that invoke
54
54
  `destroy()` directly retain responsibility for removing the element when
55
55
  appropriate.
56
56
 
57
+ ## Shared print presentation
58
+
59
+ Applications loading the shared `theme.css` receive a print-only base light
60
+ palette with a white paper surface and `15mm` page margins. Root, body, and
61
+ scoped `data-arcane-palette` tokens override screen/custom colors during print;
62
+ root/body margins are reset. Saved preferences, screen presentation, scrolling,
63
+ media, and other layout rules remain unchanged. Registered rendered-print
64
+ views use the separate [`PrintView.js`](runtime-modules.md#printviewjs) snapshot
65
+ owner described with the editor and file preview below.
66
+
57
67
  ## Canonical inventory
58
68
 
59
69
  | Component | Capability | Principal methods | Events | Normalization |
@@ -919,7 +929,16 @@ extension. Routing compares the lowercase MIME essence without its parameters;
919
929
  the callback's `mimeType` and the file content remain unchanged.
920
930
  HTML uses its complete original string in `srcdoc` without an
921
931
  added sandbox policy; Markdown reads the complete original text before
922
- rendering. Untransformed JSONL/NDJSON shows its complete original text,
932
+ rendering. With no descriptor or `previewTransform`, JSON, JSONL, and NDJSON
933
+ files are read in full and recognized through
934
+ [`decodeMarkdownMediaRecord()`](runtime-modules.md#markdownmediajs). A complete
935
+ stored image record, including nested single-record arrays, opens as an image
936
+ when its decoded Blob has an `image/` media type. Untyped/non-image records,
937
+ multiple records, and unrecognized content keep their ordinary JSON-family
938
+ presentation; malformed recognized image encoding follows the existing
939
+ file-open failure path.
940
+ This recognition leaves saved content unchanged and performs no migration.
941
+ Untransformed JSONL/NDJSON shows its complete original text,
923
942
  including lines that are not JSON records. Other binary files offer a download
924
943
  of the original `File`.
925
944
  The larger file-preview dialog reuses `--modal-width`, `--modal-max-height`,
@@ -931,6 +950,8 @@ preview ahead of the editor. Printing uses the shared
931
950
  [`PrintView.js`](runtime-modules.md#printviewjs) owner; accessible HTML frames
932
951
  are copied from their current rendered document, never substituted with source
933
952
  text. Explicit printing waits for images/fonts and HTML frame readiness.
953
+ The shared print surface uses a light paper palette and `15mm` page margins
954
+ while preserving image/SVG colors and leaving the screen theme unchanged.
934
955
  Native PDF and inaccessible embedded viewers require their own print command.
935
956
  `true` means the browser print dialog was requested, not that paper or a PDF
936
957
  was produced. Closed, destroyed, or unavailable previews return `false`;
@@ -945,6 +966,9 @@ pending media because it cannot await them. Closing or replacing a preview
945
966
  cancels display readiness without waiting for storage; URLs already retained
946
967
  by a requested print remain until `afterprint`. Original Markdown and ordinary
947
968
  external image URLs remain unchanged.
969
+ Standalone IMG blocks with multiline quoted descriptions use the shared
970
+ [`MD.js`](runtime-modules.md#mdjs) tokenizer in both ordinary and source-mapped
971
+ rendering; the complete description and stored Markdown remain intact.
948
972
 
949
973
  `await close()` cancels pending presentation work and closes delete, file, and
950
974
  directory dialogs in that order. A settled preview keeps its rendered content,
@@ -957,7 +981,7 @@ writes complete through the storage owner without reopening closed dialogs.
957
981
 
958
982
  Events: `file-manager-ready`, `file-manager-select`, `file-manager-open`, `file-manager-action`.
959
983
 
960
- Shared dependencies: [`DBOPFS.js`](runtime-modules.md#dbopfsjs), [`File.js`](runtime-entities.md#filejs), [`MD.js`](runtime-modules.md#mdjs), [`WaitForComponent.js`](runtime-modules.md#waitforcomponentjs).
984
+ Shared dependencies: [`DBOPFS.js`](runtime-modules.md#dbopfsjs), [`File.js`](runtime-entities.md#filejs), [`MD.js`](runtime-modules.md#mdjs), [`MarkdownMedia.js`](runtime-modules.md#markdownmediajs), [`PrintView.js`](runtime-modules.md#printviewjs), [`WaitForComponent.js`](runtime-modules.md#waitforcomponentjs).
961
985
 
962
986
  ### Availability and normalization
963
987
 
@@ -1082,6 +1106,9 @@ diagnostics remain in the developer console; successful images and document
1082
1106
  text remain visible. Replacing, clearing, failing, or destroying the document
1083
1107
  releases its display resources without deleting stored media. Markdown source
1084
1108
  and ordinary external image URLs remain unchanged.
1109
+ Complete standalone IMG blocks with quoted multiline descriptions render
1110
+ through the shared [`MD.js`](runtime-modules.md#mdjs) tokenizer without
1111
+ rewriting saved Markdown.
1085
1112
 
1086
1113
  Shared dependencies: [`MD.js`](runtime-modules.md#mdjs), [`MarkdownMedia.js`](runtime-modules.md#markdownmediajs).
1087
1114
 
@@ -1125,8 +1152,10 @@ The visible **Print** control and `await print()` print the complete rendered
1125
1152
  Markdown with `entryTitle`, independently of saving. Native browser Print uses
1126
1153
  that same registered view unless an open file preview takes precedence.
1127
1154
  The SDK copies current rendered DOM/styles into a temporary print-only view,
1128
- leaving the editor's source, controls, and screen layout in place. Explicit
1129
- printing waits for resource readiness; native `beforeprint` is synchronous
1155
+ leaving the editor's source, controls, and screen layout in place. Both explicit
1156
+ and native print use the shared light paper palette and `15mm` page margins;
1157
+ image pixels and SVG artwork keep their colors. Explicit printing waits for
1158
+ resource readiness; native `beforeprint` is synchronous
1130
1159
  and uses the currently rendered content. `print()` returns `false` when the
1131
1160
  view is unavailable, rejects preparation failures, and returns `true` only
1132
1161
  when the browser dialog was requested. Completion of physical printing or PDF
@@ -1142,6 +1171,9 @@ waits for the current image reads/decoding, follows a replacement preview, and
1142
1171
  rechecks readiness before capture. Native Print reports still-pending media
1143
1172
  synchronously; it cannot wait or cancel the native dialog. Print-owned URLs
1144
1173
  outlive a changed or destroyed editor until `afterprint`.
1174
+ The shared [`MD.js`](runtime-modules.md#mdjs) tokenizer preserves complete
1175
+ standalone IMG blocks with quoted multiline descriptions in ordinary and
1176
+ source-mapped rendering. Existing saved markup needs no rewriting.
1145
1177
 
1146
1178
  `configure({fit:true,followPreview:true})` opts into the two independent
1147
1179
  behaviors. Both options default to `false`; omitted options retain their
@@ -1597,6 +1629,16 @@ Events: `source-explanation-ready`, `source-explanation-save`, `source-explanati
1597
1629
 
1598
1630
  ## speech.html
1599
1631
 
1632
+ An explicitly selected native Web Speech provider uses gesture-started live
1633
+ capture instead of MediaRecorder. The shared control retains every final
1634
+ result and emits its existing completion event once with the exact combined
1635
+ text when native capture ends. The live display retains final words alongside
1636
+ interim hypotheses while speaking; interim hypotheses stay transient. Release/Stop
1637
+ finishes native recognition; cancellation, replacement and destruction abort
1638
+ it and suppress late submission. A persisted page hide stops live native capture
1639
+ without restarting it when the page returns. Existing Whisper and TTS paths
1640
+ remain unchanged.
1641
+
1600
1642
  ### Overview
1601
1643
 
1602
1644
  Coordinates explicit speech-to-text activation, speech controls, transcription
@@ -2051,6 +2093,19 @@ Native methods remain subject to the bound app's capabilities. [Deep protocol de
2051
2093
 
2052
2094
  ### Continuous capture and controls
2053
2095
 
2096
+ An explicitly selected native Web Speech provider uses the same ordered
2097
+ transcript/save queue in either capture mode. Each complete final text segment
2098
+ skips file transcription and enters the existing append/save stages; interim
2099
+ text stays in the transient `interim` part and never reaches a save or success
2100
+ event. Final segments waiting behind a save stay visible there until appended
2101
+ to the main transcript. Native `stopRecording()` returns the drain promise, including final
2102
+ results emitted during native stop. Browser `end` stops capture without an
2103
+ automatic restart. Provider replacement stops native capture while already
2104
+ accepted final segments retain their ordered saves. Explicit Cancel, transcript
2105
+ replacement and destruction retain the existing supersession behavior.
2106
+ This path does not require AudioWorklet or MediaRecorder. A supplied custom
2107
+ `transcribe(file,context)` callback keeps the existing audio-clip route.
2108
+
2054
2109
  `configure({capture:{mode:'continuous'}})` opts into the SDK's
2055
2110
  [`ContinuousVoiceCapture`](runtime-modules.md#continuousvoicecapturejs) owner.
2056
2111
  The default capture settings are `preRollMs:1500`, `quietMs:2000`,
@@ -96,7 +96,7 @@ own asynchronous work, cancellation, and backpressure.
96
96
  | [`Mail.js`](#mailjs) | esm | Builds complete reports and prefers the native mail capability with an explicit HTTP transport fallback. | Browser/native hybrid + cloud | Mail inputs/results normalized; transport failures mixed. |
97
97
  | [`MailOutbox.mjs`](#mailoutboxmjs) | esm | Persists complete mail reports before delivery and normalizes idempotent enqueue, retry, reconciliation, and invalid-record maintenance. | Browser/native WebView or compatible injected host | Complete records, full work, cancellation, and lifecycle states normalized; storage, lock, and delivery failures coded. |
98
98
  | [`MailTransport.mjs`](#mailtransportmjs) | esm | Sends one complete mail report to a normalized HTTP(S) endpoint. | Browser/server with fetch + cloud | Normalized endpoint and transport errors; remote detail preserved. |
99
- | [`MarkdownMedia.js`](#markdownmediajs) | esm | Saves local Markdown images separately and resolves their stable references in rendered views. | Browser / supported native WebView | Complete image records, concurrent display reads, cancellation, and print-owned URL lifetime. |
99
+ | [`MarkdownMedia.js`](#markdownmediajs) | esm | Saves local Markdown images separately, decodes complete stored records, and resolves stable references in rendered views. | Browser / supported native WebView | Complete image records, concurrent display reads, cancellation, and print-owned URL lifetime. |
100
100
  | [`MarkdownSpeech.js`](#markdownspeechjs) | esm | Removes repeated Markdown formatting marks from streamed narration. | Cross-host | Speech-only filtering; single marks and ordinary punctuation remain literal. |
101
101
  | [`Marked.min.js`](#markedminjs) | esm | Vendored Marked 18.0.5 Markdown lexer, parser, renderer, extension, and walk-token API. | Cross-host vendor module | Vendor-native Marked contract. |
102
102
  | [`MD.js`](#mdjs) | esm | Renders complete Markdown with Marked and optionally maps source blocks to rendered comment anchors. | Browser / native WebView | Complete raw and rendered Marked values; opt-in original-source offsets; parse errors vendor-native. |
@@ -275,8 +275,9 @@ tool declarations, emitted tool calls, or callback ordering.
275
275
  `configureSpeechProviders({stt,tts})` commits only the two speech routes and
276
276
  leaves the current LLM route and sticky lifecycle record unchanged. Both speech
277
277
  roles must be unloaded and own no request, load, unload, or dispose operation.
278
- STT remains local-only (`AI_STT_DEVICE_ONLY` for a remote selection); an
279
- explicit TTS provider may declare `localOnly:false`.
278
+ File-based STT remains local-only (`AI_STT_DEVICE_ONLY` for an unsupported
279
+ remote file selection). An explicitly selected native live-capture STT provider
280
+ or TTS provider may declare `localOnly:false`.
280
281
  `transitionSpeechProviders({stt,tts})` stops queued audio, explicitly unloads
281
282
  only STT and TTS, then commits that same closed speech route record. Neither
282
283
  method loads a model, selects a fallback, or changes caller-owned model or voice
@@ -302,6 +303,13 @@ cancels that pending unmute. The application keeps model, voice, and credential
302
303
  selection. See [DigitalOcean FAL speech](ai/browser-speech.md#digitalocean-fal-text-to-speech)
303
304
  for the public remote adapter and completed-job audio transport.
304
305
 
306
+ `supportsTranscriptionCapture()` reports whether the selected STT provider
307
+ offers native live capture. `createTranscriptionCapture(options={})` delegates
308
+ to the provider runtime's owned capture boundary below and returns `null` for
309
+ file-only providers. Shared speech controls select it automatically; apps keep
310
+ using their existing controls and completion/save callbacks. See
311
+ [native browser recognition](ai/browser-speech.md#native-browser-speech-recognition).
312
+
305
313
  `startProviders({startLanguageModel=true,startMuted=true,startTranscription=false,signal=null}={})`
306
314
  starts provider-owned text chat without requesting an STT load by default.
307
315
  Callers selecting a browser-WASM LLM pass `startLanguageModel:false` so it
@@ -961,6 +969,8 @@ The singleton exposes read-only `protocol`, `configured`, and `speechMuted`;
961
969
  `request(role,options={},preparation={})`;
962
970
  `chat(payload,options={})`; `stream(payload,options={})`;
963
971
  `transcribe(payload,options={})`;
972
+ `supportsTranscriptionCapture(providerId=null)`;
973
+ `createTranscriptionCapture(options={})`;
964
974
  `synthesize(payload,options={},preparation={})`; and
965
975
  `setSpeechMuted(muted)`. Provider payloads must be data-only; callbacks,
966
976
  accessors, symbols, and cycles are rejected at the provider boundary.
@@ -1099,6 +1109,24 @@ cleanup are normalized, while the
1099
1109
  selected provider retains its own capability, permission, download, and model
1100
1110
  requirements. [Deep protocol details](protocols.md#portable-ai-provider-runtime).
1101
1111
 
1112
+ STT providers may additionally implement `createCapture(options)`. Registration
1113
+ retains that optional method without changing the required provider/2 methods.
1114
+ `supportsTranscriptionCapture(providerId=null)` checks the current selection by
1115
+ default or a specified registered STT provider without starting it.
1116
+ `createTranscriptionCapture({language,continuous,onSegment,onInterim,onState,onError})`
1117
+ returns `null` for a provider without that method; otherwise it returns
1118
+ `{start,stop,cancel,destroy,done}`. Creation has no microphone side effect.
1119
+ `start({signal})` requires the same selected ready provider with an idle STT
1120
+ lane and invokes its capture start synchronously in the caller's gesture.
1121
+ Capture occupies one active STT request until its `done` promise settles;
1122
+ ordinary file requests remain queued, and unload/dispose/cancel use the same
1123
+ owned request cancellation lifecycle. Stop drains native final recognition;
1124
+ cancel suppresses late callbacks. Provider callbacks carry exact final
1125
+ `{text,sequence}` segments, transient `{text}` interim results, capture state,
1126
+ or complete errors. The browser provider's `done` resolves a success boolean;
1127
+ runtime cleanup errors remain observable as rejection. No callback is inserted
1128
+ into a data-only transcription request payload.
1129
+
1102
1130
  ### Example
1103
1131
 
1104
1132
  ```javascript
@@ -2816,9 +2844,18 @@ Public package import: `arcane-os/modules/MarkdownMedia.js`.
2816
2844
  - `parseMarkdownMediaReference(reference)` returns `{tableName,fileName}` for
2817
2845
  a local reference, or `null` for an ordinary URL. Malformed local addresses
2818
2846
  report their actual parsing error.
2847
+ - `decodeMarkdownMediaRecord(value)` synchronously decodes a complete
2848
+ `{dataUrl,mediaType?}` record or its JSON text into a `Blob`, without storage
2849
+ or network access. Nested single-record arrays are unwrapped; arrays with
2850
+ multiple records are never reduced to one image. Unrecognized content,
2851
+ including text that is not complete JSON, returns `null`. A recognized
2852
+ record with unreadable base64 data-URL content throws its decoding error.
2853
+ Omitted `mediaType` preserves the Blob's empty type. Input remains unchanged.
2819
2854
  - `await readMarkdownMedia(reference)` returns the complete `Blob`, using
2820
2855
  only local storage and data-URL decoding; stored strings are never fetched
2821
- as network addresses.
2856
+ as network addresses. It uses the same record decoder after loading, while
2857
+ preserving JSON parse errors for unreadable stored JSON strings and reporting
2858
+ an unreadable record as an error rather than `null`.
2822
2859
  - `hydrateMarkdownMedia(root,{signal}={})` returns `{ready,destroy,retain}`
2823
2860
  immediately. It includes an IMG root and descendant IMG elements, removes
2824
2861
  local-reference `src` values synchronously, starts independent reads
@@ -2841,6 +2878,9 @@ records their existing JSON backup/export includes. Reusing the same filename
2841
2878
  supports an application-selected save-only retry without another generation
2842
2879
  request. The helper performs no existing-data migration.
2843
2880
 
2881
+ Exact exports: `saveMarkdownMedia`, `parseMarkdownMediaReference`,
2882
+ `decodeMarkdownMediaRecord`, `readMarkdownMedia`, `hydrateMarkdownMedia`.
2883
+
2844
2884
  ### Availability and normalization
2845
2885
 
2846
2886
  Browser or native WebView with the existing app-scoped DBOPFS owner, Blob,
@@ -2896,16 +2936,29 @@ mapping, not exact rendered glyph or caret geometry.
2896
2936
 
2897
2937
  Assigning `raw` or calling `append(string)` rebuilds both the complete rendered
2898
2938
  result and its current mapping. Without `sourceMap:true`, `sourceMap` is empty
2899
- and the ordinary rendered output is unchanged. Existing raw content, link
2939
+ and the same parsing runs without comment anchors. Existing raw content, link
2900
2940
  behavior, no-op `rendered` setter, and complete `append()` result remain.
2901
2941
 
2942
+ Both rendering paths first use Marked's native HTML block tokenizer. When it
2943
+ does not recognize a complete standalone `<img>` tag with a newline inside a
2944
+ quoted attribute value, MD keeps that complete tag together as one HTML block.
2945
+ This includes blank lines in an image's full `alt` description. The tag must
2946
+ start at an existing Markdown block boundary with at most three leading spaces
2947
+ and end on its own line or at the end of the source. Separate inserted image
2948
+ blocks from surrounding Markdown with blank lines. Native HTML handling,
2949
+ inline prose, fenced/indented/inline code, escaped examples, and incomplete
2950
+ tags retain their existing parsing behavior. No source is rewritten or saved
2951
+ data migrated. Marked's existing rendered line-ending normalization remains;
2952
+ mapped offsets still address the exact original source, including CRLF or CR.
2953
+
2902
2954
  Exact exports: `default`.
2903
2955
 
2904
2956
  ### Availability and normalization
2905
2957
 
2906
- **Browser / native WebView.** Raw Marked behavior is preserved; parse errors are
2907
- vendor-native. Rendering and the getters are DOM-independent. DOM lookup and
2908
- scrolling belong to the consuming component;
2958
+ **Browser / native WebView.** Marked parsing, including the shared standalone
2959
+ image-token correction above; parse errors remain vendor-native. Rendering and
2960
+ the getters are DOM-independent. DOM lookup and scrolling belong to the
2961
+ consuming component;
2909
2962
  mapping performs no persistence or viewport movement. Transport: Marked.
2910
2963
  [Deep protocol details](protocols.md).
2911
2964
 
@@ -3345,6 +3398,13 @@ Shared rendered-print owner used by Markdown Editor and File Manager. It
3345
3398
  captures the current DOM and computed styles, including accessible rendered
3346
3399
  HTML frames, complete text, selected images, and the current title. A temporary
3347
3400
  print-only surface expands scrollable content without editing the live screen.
3401
+ That surface uses the shared Arcane light-theme text, paper, and border colors,
3402
+ with `15mm` page margins. It retains rendered fonts, emphasis, and complete
3403
+ content while removing screen-theme text/shadow paint from ordinary HTML and
3404
+ pseudo-elements. Links are underlined and table cells receive visible borders.
3405
+ Image pixels, canvas snapshots, and SVG artwork keep their own colors. These
3406
+ print rules leave the live screen theme unchanged; browser print settings still
3407
+ control the eventual page output.
3348
3408
 
3349
3409
  ### Public surface
3350
3410
 
@@ -4022,12 +4082,20 @@ is applied once the body is available.
4022
4082
 
4023
4083
  The shared `theme.css` owns the default, warm, curious, hopeful, harmony, and
4024
4084
  warrior palettes. Each works with explicit light/dark or system appearance.
4025
- `layout.css` imports that same owner. Explicit custom themes retain precedence
4026
- through `data-arcane-skin="custom"` and their inline tokens.
4085
+ `layout.css` imports that same owner. On screen, explicit custom themes retain
4086
+ precedence through `data-arcane-skin="custom"` and their inline tokens.
4027
4087
  The bare root retains the generic neutral light/dark baseline; the prior
4028
4088
  layout default palette is selected explicitly through `data-user-skin="default"`,
4029
4089
  `body.default` compatibility, or the default swatch.
4030
4090
 
4091
+ During printing, `theme.css` applies the existing base light palette, with a
4092
+ white paper surface and `15mm` page margins, to the root, body, and scoped
4093
+ `data-arcane-palette` elements. These print-only tokens override screen/custom
4094
+ palette tokens. It resets the root/body margin without changing saved
4095
+ preferences, screen presentation, scrolling, media, or other layout rules.
4096
+ The separate [`PrintView.js`](#printviewjs) owner uses the same light palette
4097
+ and margins for its expanded rendered-content snapshot.
4098
+
4031
4099
  A scoped `data-arcane-palette="warm"` element exposes the same palette variables
4032
4100
  for a chooser preview without changing the page's selection. Use any of the
4033
4101
  six named palettes and paint the preview with ordinary shared variables such
@@ -169,6 +169,7 @@ browser map are cataloged separately in [Runtime modules](runtime-modules.md).
169
169
  | `createBrowserSpeechAuthority()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser descriptor construction; use requires the selected storage and provider Web APIs |
170
170
  | `createBrowserWasmLlmProvider()` | function | `arcane-os/ai/browser-wasm` | Browser-WASM local AI | Browser context with WebAssembly and OPFS/DBOPFS; WebGPU by default or explicit CPU with gpuLayers:0 |
171
171
  | `createBrowserWhisperProvider()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser with Workers, object URLs, caller-selected Whisper runtime/model artifacts, and an SDK DBOPFS speech store |
172
+ | `createBrowserSpeechRecognitionProvider()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser or WebView with native SpeechRecognition; construction and catalog remain available on unsupported hosts |
172
173
  | `createCanonicalUstarHeader()` | function | `arcane-os` | Packaging and release bundles | Node |
173
174
  | `createDbopfsModelStore()` | function | `arcane-os/ai/browser-wasm` | Browser-WASM local AI | Browser with a ready DBOPFS instance and OPFS |
174
175
  | `getBrowserDeviceClass()` | function | `arcane-os/browser-device` | Browser device settings | Node and Browser; synchronous identity hint with no model, storage, or GPU operation |
@@ -7185,6 +7186,62 @@ async function transcribeAfterUserChoice(audioBlob) {
7185
7186
  }
7186
7187
  ```
7187
7188
 
7189
+ ## createBrowserSpeechRecognitionProvider()
7190
+
7191
+ ### Overview
7192
+
7193
+ Creates an explicitly selected provider/2 STT adapter for the browser's native
7194
+ `SpeechRecognition` or `webkitSpeechRecognition` service. It declares
7195
+ `localOnly:false` because recognition may use a remote browser service. It does
7196
+ not change Whisper, TTS, typing, stored preferences, or any default selection.
7197
+
7198
+ ### Signature and result
7199
+
7200
+ ```text
7201
+ createBrowserSpeechRecognitionProvider({id,model,language}={})
7202
+ ```
7203
+
7204
+ The application supplies its provider `id` and `model:{id}`; optional `language`
7205
+ is passed to native recognition without changing transcript text. The provider
7206
+ implements the required `catalog`, `inspect`, `status`, `load`, `request`,
7207
+ `unload`, and `dispose` methods plus `createCapture(options)`. There is no SDK
7208
+ model download or storage. Construction/catalog do not require native support;
7209
+ inspect/load report actual browser availability without opening a microphone.
7210
+ File transcription requests are unsupported for this provider.
7211
+
7212
+ `createCapture({language,continuous,onSegment,onInterim,onState,onError})`
7213
+ returns a single-use `{start,stop,cancel,destroy,done}` handle. Start must be
7214
+ called directly from a user gesture and returns a promise for startup success.
7215
+ Final callbacks carry exact `{text,sequence}` values once per final result;
7216
+ interim `{text}` stays transient. Stop preserves pending native final results
7217
+ through `end`; cancellation aborts recognition and suppresses late text.
7218
+ `done` resolves `true` for normal native end and `false` for cancellation or
7219
+ failure. Unload/dispose cancel owned capture.
7220
+ The shared runtime's `createTranscriptionCapture()` additionally owns STT busy
7221
+ state and lifecycle replacement. See the
7222
+ [complete native capture contract](ai/browser-speech.md#native-browser-speech-recognition).
7223
+
7224
+ ### Availability and normalization
7225
+
7226
+ Conditional browser/native-WebView support on Windows, Linux, macOS and
7227
+ Android, determined by the actual browser API and permission/service outcome,
7228
+ not by operating-system name. Missing API, denied microphone permission and
7229
+ service errors remain honest STT failures without disabling TTS or typing.
7230
+ No Core, Whisper, or cloud-provider fallback is inferred.
7231
+
7232
+ ### Example
7233
+
7234
+ ```javascript
7235
+ import {createBrowserSpeechRecognitionProvider} from 'arcane-os/ai/browser-speech';
7236
+
7237
+ async function selectNativeRecognition(ai, signal) {
7238
+ const provider = createBrowserSpeechRecognitionProvider(
7239
+ {id: 'WEB_SPEECH', model: {id: 'WEB_SPEECH'}}
7240
+ );
7241
+ return ai.configureSpeechProvider('stt', provider, {modelId: 'WEB_SPEECH', signal});
7242
+ }
7243
+ ```
7244
+
7188
7245
  ## createTwinCloudTTSProvider()
7189
7246
 
7190
7247
  ### Overview
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -86,7 +86,7 @@
86
86
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
87
87
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
88
88
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
89
- "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
89
+ "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
90
90
  "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
91
91
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
92
92
  "check": "node tools/check-source.mjs && npm test",
@@ -2142,8 +2142,9 @@
2142
2142
  const rendered=document.createElement('div');
2143
2143
  rendered.className='file-preview';
2144
2144
  let value=fileRef.parsed;
2145
- const transformed=['json','jsonl','ndjson'].includes(fileRef.ext)
2146
- &&is.function(host.previewTransform);
2145
+ const jsonFile=['json','jsonl','ndjson'].includes(fileRef.ext);
2146
+ const transformed=jsonFile&&is.function(host.previewTransform);
2147
+ let completeJSON;
2147
2148
 
2148
2149
  if(transformed){
2149
2150
  value=await host.previewTransform(
@@ -2155,6 +2156,37 @@
2155
2156
  }
2156
2157
  );
2157
2158
  preview.controller.signal.throwIfAborted();
2159
+ }else if(jsonFile){
2160
+ // Recognition needs the complete file: FileEntity's parsed JSONL
2161
+ // rows do not include every unparseable line in the original.
2162
+ const [{decodeMarkdownMediaRecord},text]=await Promise.all([
2163
+ import('../modules/MarkdownMedia.js'),
2164
+ fileRef.text()
2165
+ ]);
2166
+ preview.controller.signal.throwIfAborted();
2167
+ completeJSON=text;
2168
+ let imageRecord;
2169
+ try {
2170
+ imageRecord = JSON.parse(completeJSON);
2171
+ } catch {
2172
+ // Mixed JSONL and ordinary text keep their complete presentation.
2173
+ imageRecord = null;
2174
+ }
2175
+ while (is.array(imageRecord) && imageRecord.length === 1) {
2176
+ imageRecord = imageRecord[0];
2177
+ }
2178
+ // A dataUrl property alone does not identify an image record.
2179
+ // Recognize its declared media type before attempting any decoding.
2180
+ if (is.string(imageRecord?.mediaType)
2181
+ && imageRecord.mediaType.toLowerCase().startsWith('image/')) {
2182
+ const image = decodeMarkdownMediaRecord(imageRecord);
2183
+ if (image?.type.startsWith('image/')) {
2184
+ return renderDescriptor(
2185
+ {kind: 'image', content: image, alt: fileRef.name},
2186
+ preview
2187
+ );
2188
+ }
2189
+ }
2158
2190
  }
2159
2191
 
2160
2192
  switch(fileRef.ext){
@@ -2168,7 +2200,7 @@
2168
2200
  case 'jsonl':
2169
2201
  case 'ndjson':{
2170
2202
  if(!transformed){
2171
- rendered.append(createPreformatted(await fileRef.text()));
2203
+ rendered.append(createPreformatted(completeJSON));
2172
2204
  break;
2173
2205
  }
2174
2206
  const records=is.array(value)?value:[];