arcane-os 0.77.0 → 0.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/browser-runtime/ai/dbopfs-model-resources.mjs +1 -1
- package/browser-runtime/ai/speech-worker-runtime.mjs +79 -3
- package/docs/reference/ai/browser-speech.md +59 -2
- package/docs/reference/inventory/package-api.json +1 -1
- package/docs/reference/inventory/runtime-modules.json +9 -8
- package/docs/reference/runtime-modules.md +78 -11
- package/package.json +1 -1
- package/runtime/arcane/modules/AI.js +30 -7
- package/runtime/arcane/modules/DBOPFS.js +24 -6
- package/runtime/arcane/modules/PreparedSpeech.js +111 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.78.0
|
|
4
|
+
|
|
5
|
+
- Add `AI.readPreparedTTS()` and shared `readPreparedSpeech()` for complete
|
|
6
|
+
cache-only narration lookup. Return a ready playback handle or `null` without
|
|
7
|
+
loading a model, unmuting, synthesizing, writing storage or repairing partial
|
|
8
|
+
audio. Preserve complete semantic matching and genuine storage errors.
|
|
9
|
+
- Keep lookup cancellation independent from a successfully returned handle.
|
|
10
|
+
Replay uses the existing prepared-playback owner and can borrow the caller's
|
|
11
|
+
silent reusable audio output; no autoplay permission is implied.
|
|
12
|
+
- Add optional existing-only DBOPFS table/file reads while preserving ordinary
|
|
13
|
+
lazy-creation defaults. Cache-only lookup and later audio reads leave missing
|
|
14
|
+
tables absent.
|
|
15
|
+
- Give Kokoro voice caching a real Worker-lifetime response cache after runtime
|
|
16
|
+
import, while DBOPFS remains the durable model/resource owner. Preserve saved
|
|
17
|
+
native caches and clear transient responses on unload.
|
|
18
|
+
- Report a complete stored model resource as cached loading rather than a new
|
|
19
|
+
network download. This release changes no model selection, native engine or
|
|
20
|
+
physical-device routing and makes no live-model execution claim.
|
|
21
|
+
|
|
3
22
|
## 0.77.0
|
|
4
23
|
|
|
5
24
|
- Add `AI.prepareAudioOutput()` for synchronous, silent preparation of a reusable
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
20
20
|
share the theme, packaging, event, cancellation,
|
|
21
21
|
and browser run contracts while retaining their selected application layout.
|
|
22
22
|
|
|
23
|
-
This checkout defines the `0.
|
|
23
|
+
This checkout defines the `0.78.0` SDK contract. Development applications declare
|
|
24
24
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
25
25
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
26
26
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -96,7 +96,7 @@ export function createDbopfsResourceStore({dbopfs, tableName, fetchImpl = global
|
|
|
96
96
|
const file = await parts.read(entry.name, {signal});
|
|
97
97
|
if (file) {
|
|
98
98
|
const state = await parts.state(entry.name, {signal});
|
|
99
|
-
onProgress?.({phase: '
|
|
99
|
+
onProgress?.({phase: 'load', message: 'Reading stored model resource', completed: state.parts.length,
|
|
100
100
|
total: state.parts.length, unit: 'shards', cached: true, url: request.url});
|
|
101
101
|
return {...entry.metadata, file};
|
|
102
102
|
}
|
|
@@ -997,6 +997,7 @@ function createOrdinaryArtifactModuleRouter(
|
|
|
997
997
|
role,
|
|
998
998
|
reader,
|
|
999
999
|
originalWorker,
|
|
1000
|
+
responseCaches,
|
|
1000
1001
|
) {
|
|
1001
1002
|
const nestedWorkers = new Set();
|
|
1002
1003
|
const router = completeValue({
|
|
@@ -1015,8 +1016,8 @@ function createOrdinaryArtifactModuleRouter(
|
|
|
1015
1016
|
return reader.fetchResource(input, init, modulePath);
|
|
1016
1017
|
},
|
|
1017
1018
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1019
|
+
openCache(modulePath, name) {
|
|
1020
|
+
return responseCaches.open(name);
|
|
1020
1021
|
},
|
|
1021
1022
|
|
|
1022
1023
|
createWorker(modulePath, specifier, options = {}) {
|
|
@@ -1143,6 +1144,63 @@ function installOrdinaryArtifactModuleRouter(scope, configuration, role, send, s
|
|
|
1143
1144
|
const resourceClient = createModelResourceClient({ send });
|
|
1144
1145
|
let requestSignal = null;
|
|
1145
1146
|
let reportProgress = null;
|
|
1147
|
+
let cacheClosed = false;
|
|
1148
|
+
const responseCacheEntries = new Map();
|
|
1149
|
+
const responseCaches = {
|
|
1150
|
+
async open(name) {
|
|
1151
|
+
assertCacheActive(requestSignal);
|
|
1152
|
+
const cacheName = String(name);
|
|
1153
|
+
let cache = responseCacheEntries.get(cacheName);
|
|
1154
|
+
if (!cache) {
|
|
1155
|
+
const responses = new Map();
|
|
1156
|
+
cache = {
|
|
1157
|
+
responses,
|
|
1158
|
+
api: {
|
|
1159
|
+
async match(input) {
|
|
1160
|
+
assertCacheActive(requestSignal);
|
|
1161
|
+
const stored = responses.get(cacheRequestKey(input));
|
|
1162
|
+
if (!stored) return undefined;
|
|
1163
|
+
const response = new Response(stored.body, stored);
|
|
1164
|
+
Object.defineProperties(response, {
|
|
1165
|
+
url: { value: stored.url },
|
|
1166
|
+
redirected: { value: stored.redirected },
|
|
1167
|
+
type: { value: stored.type },
|
|
1168
|
+
});
|
|
1169
|
+
return response;
|
|
1170
|
+
},
|
|
1171
|
+
async put(input, response) {
|
|
1172
|
+
const signal = requestSignal;
|
|
1173
|
+
assertCacheActive(signal);
|
|
1174
|
+
const key = cacheRequestKey(input);
|
|
1175
|
+
const copy = response.clone();
|
|
1176
|
+
const body = await copy.blob();
|
|
1177
|
+
assertCacheActive(signal);
|
|
1178
|
+
responses.set(key, {
|
|
1179
|
+
body,
|
|
1180
|
+
status: response.status,
|
|
1181
|
+
statusText: response.statusText,
|
|
1182
|
+
headers: [...response.headers],
|
|
1183
|
+
url: response.url,
|
|
1184
|
+
redirected: response.redirected,
|
|
1185
|
+
type: response.type,
|
|
1186
|
+
});
|
|
1187
|
+
},
|
|
1188
|
+
},
|
|
1189
|
+
};
|
|
1190
|
+
responseCacheEntries.set(cacheName, cache);
|
|
1191
|
+
}
|
|
1192
|
+
return cache.api;
|
|
1193
|
+
},
|
|
1194
|
+
};
|
|
1195
|
+
function assertCacheActive(signal) {
|
|
1196
|
+
throwIfAborted(signal, 'speech-resource-cache-cancelled');
|
|
1197
|
+
if (cacheClosed) throw new Error('The speech Worker response cache was unloaded.');
|
|
1198
|
+
}
|
|
1199
|
+
function cacheRequestKey(input) {
|
|
1200
|
+
const RequestConstructor = scope.Request ?? Request;
|
|
1201
|
+
const request = new RequestConstructor(input);
|
|
1202
|
+
return JSON.stringify([request.method, request.url]);
|
|
1203
|
+
}
|
|
1146
1204
|
if (!is.function(originalFetch)) {
|
|
1147
1205
|
throw workerError(
|
|
1148
1206
|
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
@@ -1166,8 +1224,9 @@ function installOrdinaryArtifactModuleRouter(scope, configuration, role, send, s
|
|
|
1166
1224
|
role,
|
|
1167
1225
|
reader,
|
|
1168
1226
|
originalWorker,
|
|
1227
|
+
responseCaches,
|
|
1169
1228
|
);
|
|
1170
|
-
const originalProperties = ['fetch', 'Worker', MODULE_ROUTER_NAME].map(
|
|
1229
|
+
const originalProperties = ['fetch', 'Worker', 'caches', MODULE_ROUTER_NAME].map(
|
|
1171
1230
|
function originalWorkerProperty(name) {
|
|
1172
1231
|
return { name, descriptor: Object.getOwnPropertyDescriptor(scope, name) };
|
|
1173
1232
|
},
|
|
@@ -1240,7 +1299,22 @@ function installOrdinaryArtifactModuleRouter(scope, configuration, role, send, s
|
|
|
1240
1299
|
receive: resourceClient.receive,
|
|
1241
1300
|
setSignal(signal) { requestSignal = signal; },
|
|
1242
1301
|
setProgress(report) { reportProgress = report; },
|
|
1302
|
+
enableResponseCache() {
|
|
1303
|
+
// Transformers captures cache capability during import. Kokoro then uses
|
|
1304
|
+
// this real, Worker-lifetime match/put cache for voices; fetch remains the
|
|
1305
|
+
// sole route to durable DBOPFS resources. No native saved cache is touched.
|
|
1306
|
+
assertCacheActive(requestSignal);
|
|
1307
|
+
Object.defineProperty(scope, 'caches', {
|
|
1308
|
+
configurable: true,
|
|
1309
|
+
enumerable: true,
|
|
1310
|
+
writable: true,
|
|
1311
|
+
value: responseCaches,
|
|
1312
|
+
});
|
|
1313
|
+
},
|
|
1243
1314
|
async cleanup() {
|
|
1315
|
+
cacheClosed = true;
|
|
1316
|
+
for (const cache of responseCacheEntries.values()) cache.responses.clear();
|
|
1317
|
+
responseCacheEntries.clear();
|
|
1244
1318
|
resourceClient.close(new Error('The speech Worker resource owner was unloaded.'));
|
|
1245
1319
|
let cleanupFailure = null;
|
|
1246
1320
|
try {
|
|
@@ -1773,6 +1847,7 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
|
|
|
1773
1847
|
);
|
|
1774
1848
|
const namespace = await import(entry.moduleUrl);
|
|
1775
1849
|
throwIfAborted(signal, `${role}-load-cancelled`);
|
|
1850
|
+
environment.enableResponseCache();
|
|
1776
1851
|
restoreNamespace = configureRuntimeNamespace(
|
|
1777
1852
|
namespace,
|
|
1778
1853
|
configuration,
|
|
@@ -2071,6 +2146,7 @@ export function installBrowserSpeechArtifactModuleWorker(role, scope = globalThi
|
|
|
2071
2146
|
target?.sourceUrl ?? request.targetUrl,
|
|
2072
2147
|
);
|
|
2073
2148
|
await import(target?.moduleUrl ?? request.targetUrl);
|
|
2149
|
+
environment.enableResponseCache();
|
|
2074
2150
|
scope.removeEventListener("message", bootstrap);
|
|
2075
2151
|
await new Promise((resolve) => queueMicrotask(resolve));
|
|
2076
2152
|
for (const queuedEvent of queued.splice(0)) replayWorkerMessage(scope, queuedEvent);
|
|
@@ -727,6 +727,54 @@ pause delays the next queued audio on the existing `AudioContext` clock; it
|
|
|
727
727
|
does not delay the preceding promise after that passage's last buffer ends.
|
|
728
728
|
The promise is a playback result, not a listener acknowledgement.
|
|
729
729
|
|
|
730
|
+
## Read stored narration without starting generation
|
|
731
|
+
|
|
732
|
+
`await ai.readPreparedTTS({parts,textFormat,storage,identity,signal,onState})`
|
|
733
|
+
returns a complete prepared handle or `null`. It uses the same complete inputs,
|
|
734
|
+
selected speech configuration, formatting treatment, segmentation, storage key,
|
|
735
|
+
and semantic `identity` as `prepareTTS()`. Supply the same options used to
|
|
736
|
+
prepare the audio. A changed selection or input is a cache miss, not permission
|
|
737
|
+
to generate replacement audio.
|
|
738
|
+
|
|
739
|
+
This operation never activates or unmutes a provider, synthesizes speech,
|
|
740
|
+
creates stored records, repairs partial audio, or joins pending generation.
|
|
741
|
+
With `storage`, it reads the existing manifest and every ordered audio file.
|
|
742
|
+
Missing or incomplete audio returns `null`; unreadable metadata and other
|
|
743
|
+
storage errors reject. Without storage, it finds only already-complete audio
|
|
744
|
+
retained by this same AI instance. A miss leaves all existing data unchanged.
|
|
745
|
+
|
|
746
|
+
```javascript
|
|
747
|
+
async function readNarration(text, key, signal) {
|
|
748
|
+
return ai.readPreparedTTS({
|
|
749
|
+
parts: [{
|
|
750
|
+
input: text,
|
|
751
|
+
voice: speechSelection.model.defaultVoice,
|
|
752
|
+
speed: 1,
|
|
753
|
+
pauseAfterMs: 0
|
|
754
|
+
}],
|
|
755
|
+
storage: {db: dbopfs, table: 'saved_narration', key},
|
|
756
|
+
identity: {
|
|
757
|
+
model: speechSelection.model,
|
|
758
|
+
runtime: speechSelection.runtime
|
|
759
|
+
},
|
|
760
|
+
signal
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
A returned handle has the ordinary `segments`, `state`, `ready`, `getAudio(index)`,
|
|
766
|
+
and `cancel()` contract and can be passed to `ai.playPreparedTTS()` from the
|
|
767
|
+
application's playback action, including with its existing `audioOutput`.
|
|
768
|
+
The lookup signal governs only the read and rejects cancellation as
|
|
769
|
+
`AbortError`. After a successful return, aborting that signal does not invalidate
|
|
770
|
+
the retained handle; explicit handle cancellation and playback cancellation
|
|
771
|
+
remain separate. A successful read does not promise future autoplay permission
|
|
772
|
+
or prevent a later application-owned removal of stored files.
|
|
773
|
+
|
|
774
|
+
Applications decide which saved narration candidates to inspect. For example,
|
|
775
|
+
newest-first lookup can skip a newer partial preparation and then inspect an
|
|
776
|
+
older complete candidate without activating any model.
|
|
777
|
+
|
|
730
778
|
## Prepare narration once and replay stored audio
|
|
731
779
|
|
|
732
780
|
Use `ai.prepareTTS()` when preparation must continue independently of playback,
|
|
@@ -1631,8 +1679,13 @@ as graph runtimes:
|
|
|
1631
1679
|
Inside the dedicated speech Worker, native CacheStorage is temporarily
|
|
1632
1680
|
unavailable before runtime import, so the selected runtime takes its existing
|
|
1633
1681
|
fetch path. Transformers' public browser/custom/filesystem cache switches are
|
|
1634
|
-
disabled there.
|
|
1635
|
-
|
|
1682
|
+
disabled there. After the runtime import, Kokoro's voice `caches.open()` calls
|
|
1683
|
+
use a Worker-local response cache with actual `match`/`put` results. It retains
|
|
1684
|
+
complete response bodies and metadata in memory for that Worker only; a miss
|
|
1685
|
+
does not fetch, and ordinary fetch still owns durable DBOPFS persistence. This
|
|
1686
|
+
temporary response retention is separate from Kokoro's own in-memory voice
|
|
1687
|
+
arrays. Unload clears it, and an unfinished put cannot repopulate it afterward.
|
|
1688
|
+
The SDK neither clears nor changes saved native caches. The original Worker
|
|
1636
1689
|
environment is restored on unload or failed import. Existing explicit
|
|
1637
1690
|
`removeBrowserSpeechModelCache()` remains a separate caller-selected operation.
|
|
1638
1691
|
|
|
@@ -1646,6 +1699,10 @@ closed resource parts survive cancellation. Existing whole-file caches remain
|
|
|
1646
1699
|
readable without migration. HTTP resume is used only when the server returns
|
|
1647
1700
|
the requested range; otherwise the complete resource is downloaded anew.
|
|
1648
1701
|
Worker termination aborts and joins its parent-owned resource operations.
|
|
1702
|
+
An existing complete resource reports `phase:'load'`, `cached:true`, and
|
|
1703
|
+
`message:'Reading stored model resource'` with its complete shard progress.
|
|
1704
|
+
That event describes a DBOPFS read, not a network download or model-session
|
|
1705
|
+
reload. The resource owner does not fetch that stored response again.
|
|
1649
1706
|
|
|
1650
1707
|
Routing discovery is best effort and is not an admission gate. If the scanner
|
|
1651
1708
|
cannot interpret a module, that module is left unchanged and follows its native
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"artifactCount": 93,
|
|
12
12
|
"javascriptArtifactCount": 91,
|
|
13
|
-
"esmExportCount":
|
|
13
|
+
"esmExportCount": 375,
|
|
14
14
|
"artifacts": [
|
|
15
15
|
{
|
|
16
16
|
"file": "runtime/arcane/modules/AI.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"availability": "Browser + native bridge + TWiN Cloud",
|
|
32
32
|
"protocol": "arcane-ai-browser-speech-configuration/1, AIProviderRuntime arcane-ai-provider/2 routes, globalThis.arcaneEvents, TWiN Cloud HTTPS, Arcane.ollama, Arcane.speech, Android WebView bridge",
|
|
33
33
|
"normalization": "Complete mutable caller-owned browser speech authority, SDK-owned provider registration/replacement/disposal, explicit STT/TTS activation, normalized Whisper STT and Kokoro TTS execution with default capacities 1 and 4 respectively, explicit provider-neutral execution snapshots through status(role,{execution:true}), sticky readiness, canonical window.user readiness without compatibility-event object-identity admission or polling, exact ordered structural calls with required arguments.message, atomic nonblank all-ID tool-result sequencing, complete all-choice streaming/data validation, native Ollama structural adaptation, complete-response then all validated tool callbacks then completion ordering, observed async callbacks, Blob/File transcription, default repeated-formatting-mark removal from cloned outbound TTS input, explicit textFormat:'plain' preservation for complete synthesis/preparation while streaming cleanup remains automatic and caller/visible content stays exact, immediate exact-segment synthesis, per-call voice/speed capture, optional final-segment pause on the existing audio clock, optional per-call terminal playback completion, detached preparation with complete semantic DBOPFS audio reuse and same-owner pending request sharing; each preparation owns its pending activation interest, final-interest cancellation aborts that activation while other interests preserve it, complete cache hits start no model, and cancellation after readiness leaves the ready model loaded; independent prepared playback without a model load for cached audio, ordered audio-clock scheduling, playable audio, active-generation TTS operation-failure routing, mute, and cancellation are normalized; provider/model/runtime/voice policy remains caller-owned. Both speech roles default execution.device to auto: webnn-npu when navigator.ml.createContext is exposed, then webgpu when navigator.gpu is exposed, then wasm for CPU execution. Failed backend loads clean up the pool before trying fresh Workers with the same prepared model and dtype; explicit devices never fall back. STT capacity only accepts 1; TTS accepts 1 through 4. Execution snapshots expose requestedDevice, selectedDevice, maxConcurrentRequests, and activeRequestCount for both roles; selectedDevice is null while unloaded and names the successful upstream session backend request when loaded. It does not prove every operation used the physical NPU: WebNN unsupported operations may use WASM, and compatibility depends on the browser, driver, hardware, and graph.",
|
|
34
|
-
"surface": "Native fetchTTS(payload,signal) and fetchSTT(audio,signal) forward caller cancellation separately from their complete Core speech payloads, normalize aborts to AbortError and suppress late results; actual engine interruption remains service-owned. prepareTTSPlayback(payload,signal) silently returns a real Blob or native-speech descriptor; native controls expose finished/released/state/error/pause/resume/stop, and actual release owns the playback lane. fetchTTS and durable prepareTTS report ARCANE_AI_TTS_AUDIO_EXPORT_UNAVAILABLE for native voices. Request-local model overrides remain exact and never mutate the configured model. Browser-speech protocol/event/error/reason constants; default `AI`; read-only `providerRuntime`, `browserSpeechConfiguration`, and `browserSpeechDescriptor`; explicit `providerRuntime.status(role,{execution:true})` snapshots; `streamTTS(text,end,options={})` with optional voice, speed, pauseAfterMs, and waitForPlayback; default speech-input cleanup with SDK-internal preparation metadata and explicit complete-call textFormat:'plain' consumed before engine dispatch; `prepareTTS({parts,storage,identity,textFormat,signal,onState})` returning ordered segments, state, ready, getAudio(index), and cancel(); `prepareAudioOutput()` returning the persistent WebAudio handle {kind,state,contextState,error,subscribe,resume,dispose} without resume, unmute, model load, sources, sound, or gesture listeners; current-state replay and output-specific canonical `AI_AUDIO_OUTPUT_STATE_EVENT` (`ai-audio-output-state`), with ready only for an actually running context; explicit owner disposal observes context-bound cleanup and close; `playPreparedTTS(prepared,{signal,onState,audioOutput})` returning state, error, finished, pause(), resume(), and stop() with one playback lane per AI and observational state/error callbacks; explicit live same-AI audioOutput borrows its context until owner disposal, while omission or null preserves per-playback terminal close; configure/dispose speech, route lifecycle, declaration-validated chat/stream requests with exact ordered calls; optional streamRequest toolText:{name,field} and onToolText(text,call,displayId) for decoded selected argument text with actual call identity, distinct from ordinary prose and terminal tool execution; synthesis/transcription and playback controls; initializes from current canonical user readiness, installs `window.ai`, and projects `ai-ready` plus active-generation `ai-tts-failure`."
|
|
34
|
+
"surface": "Native fetchTTS(payload,signal) and fetchSTT(audio,signal) forward caller cancellation separately from their complete Core speech payloads, normalize aborts to AbortError and suppress late results; actual engine interruption remains service-owned. prepareTTSPlayback(payload,signal) silently returns a real Blob or native-speech descriptor; native controls expose finished/released/state/error/pause/resume/stop, and actual release owns the playback lane. fetchTTS and durable prepareTTS report ARCANE_AI_TTS_AUDIO_EXPORT_UNAVAILABLE for native voices. Request-local model overrides remain exact and never mutate the configured model. Browser-speech protocol/event/error/reason constants; default `AI`; read-only `providerRuntime`, `browserSpeechConfiguration`, and `browserSpeechDescriptor`; explicit `providerRuntime.status(role,{execution:true})` snapshots; `streamTTS(text,end,options={})` with optional voice, speed, pauseAfterMs, and waitForPlayback; default speech-input cleanup with SDK-internal preparation metadata and explicit complete-call textFormat:'plain' consumed before engine dispatch; `prepareTTS({parts,storage,identity,textFormat,signal,onState})` returning ordered segments, state, ready, getAudio(index), and cancel(); async `readPreparedTTS({parts,textFormat,storage,identity,signal,onState})` returning a complete ready handle or null through the same semantic matcher, with no activation, unmute, synthesis, playback, storage writes, or partial repair; manifest, audio availability, and returned-handle reads use DBOPFS createTable:false so absent tables remain absent; supplied adapters own that read-existing behavior; missing matches/audio return null, malformed metadata and storage failures reject, and omitted storage sees only completed matching same-AI in-memory audio; hits replay ready state, misses emit nothing, and the lookup signal detaches on settlement while returned-handle cancellation stays independent; `prepareAudioOutput()` returning the persistent WebAudio handle {kind,state,contextState,error,subscribe,resume,dispose} without resume, unmute, model load, sources, sound, or gesture listeners; current-state replay and output-specific canonical `AI_AUDIO_OUTPUT_STATE_EVENT` (`ai-audio-output-state`), with ready only for an actually running context; explicit owner disposal observes context-bound cleanup and close; `playPreparedTTS(prepared,{signal,onState,audioOutput})` returning state, error, finished, pause(), resume(), and stop() with one playback lane per AI and observational state/error callbacks; explicit live same-AI audioOutput borrows its context until owner disposal, while omission or null preserves per-playback terminal close; configure/dispose speech, route lifecycle, declaration-validated chat/stream requests with exact ordered calls; optional streamRequest toolText:{name,field} and onToolText(text,call,displayId) for decoded selected argument text with actual call identity, distinct from ordinary prose and terminal tool execution; synthesis/transcription and playback controls; initializes from current canonical user readiness, installs `window.ai`, and projects `ai-ready` plus active-generation `ai-tts-failure`."
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"file": "runtime/arcane/modules/AIModelSelectionController.js",
|
|
@@ -583,7 +583,7 @@
|
|
|
583
583
|
"availability": "Browser / native WebView",
|
|
584
584
|
"protocol": "OPFS, DBOPFSWorker, Compression Streams, per-realm globalThis.arcaneEvents authority, and native BroadcastChannel for live same-app/storage-partition committed changes",
|
|
585
585
|
"normalization": "App scope and recognized file parsing are normalized; nonblank unreadable JSONL rows remain complete raw strings, empty-table removal reports removed, absent, or not-empty after a create:false directory lookup and without scanning or recursively clearing a directory, same-named files are preserved, aliases and cached handles are invalidated only when the directory is gone, and unexpected DOM/storage errors are preserved.",
|
|
586
|
-
"surface": "default `DBOPFS`; installs `window.dbopfs`, emits `dbopfs-ready`; subscribeChanges(listener,{signal,once}) observes DBOPFS_EVENT_TYPES.change (dbopfs-change), returns idempotent unsubscribe/.dispose, and publishes metadata {applicationId,storagePath,tableName,directoryName,fileName,action,originId,sequence,changeId,remote} after successful storage commits only. Actions are write/append/delete/table-delete; batches preserve partial success, remote receipts invalidate targeted aliases before local publication and never rebroadcast, and pagehide/pageshow closes/reopens the native channel and clears memory caches without replay or fabricated changes. Local subscription/storage continue when BroadcastChannel is unavailable. Table/file/backup APIs include removeEmptyTable(tableName). createCompressedPNG({tableNames,additionalTables,signal}) returns a PNG Blob without download or record writes; omitted names select all, [] none, and supplied tables replace matching saved tables in the export. downloadCompressedPNG(name,options) reuses the same preparation and initiates one download; cancellation prevents it. restoreFromPNG(file,{selectTables}) closes the decoded image after pixel extraction, awaits the optional caller selection/projection before any writes, and rejects DBOPFS_RESTORE_WRITE_FAILED AggregateError after all selected table batches settle when records fail, preserving original errors and failures {tableName,fileName,reason}; successful writes remain. No-options restore and setMany retain their contracts."
|
|
586
|
+
"surface": "default `DBOPFS`; installs `window.dbopfs`, emits `dbopfs-ready`; subscribeChanges(listener,{signal,once}) observes DBOPFS_EVENT_TYPES.change (dbopfs-change), returns idempotent unsubscribe/.dispose, and publishes metadata {applicationId,storagePath,tableName,directoryName,fileName,action,originId,sequence,changeId,remote} after successful storage commits only. Actions are write/append/delete/table-delete; batches preserve partial success, remote receipts invalidate targeted aliases before local publication and never rebroadcast, and pagehide/pageshow closes/reopens the native channel and clears memory caches without replay or fabricated changes. Local subscription/storage continue when BroadcastChannel is unavailable. Table/file/backup APIs include getTableHandle(tableName='',{create=true}={}), readFile(tableName='',fileName='',{createTable=true}={}), get(tableName='',fileName='',force=false,{createTable=true}={}), and removeEmptyTable(tableName). Explicit create:false/createTable:false reads existing tables without creation or joining pending create work; default lazy creation remains unchanged. Low-level missing tables/files reject NotFoundError; get retains parsed caching/force semantics and resolves null only for NotFoundError, propagating other failures. Worker fallback reads already use existing app/table/file handles. createCompressedPNG({tableNames,additionalTables,signal}) returns a PNG Blob without download or record writes; omitted names select all, [] none, and supplied tables replace matching saved tables in the export. downloadCompressedPNG(name,options) reuses the same preparation and initiates one download; cancellation prevents it. restoreFromPNG(file,{selectTables}) closes the decoded image after pixel extraction, awaits the optional caller selection/projection before any writes, and rejects DBOPFS_RESTORE_WRITE_FAILED AggregateError after all selected table batches settle when records fail, preserving original errors and failures {tableName,fileName,reason}; successful writes remain. No-options restore and setMany retain their contracts."
|
|
587
587
|
},
|
|
588
588
|
{
|
|
589
589
|
"file": "runtime/arcane/modules/DBOPFSDocumentLibrary.js",
|
|
@@ -1092,13 +1092,14 @@
|
|
|
1092
1092
|
"name": "PreparedSpeech.js",
|
|
1093
1093
|
"kind": "esm",
|
|
1094
1094
|
"exports": [
|
|
1095
|
-
"prepareSpeech"
|
|
1095
|
+
"prepareSpeech",
|
|
1096
|
+
"readPreparedSpeech"
|
|
1096
1097
|
],
|
|
1097
|
-
"summary": "Owns detached ordered speech preparation, complete semantic audio reuse, and per-caller cancellation behind AI.prepareTTS.",
|
|
1098
|
-
"availability": "Browser or native WebView with Blob, AbortController, an injected synthesis callback
|
|
1098
|
+
"summary": "Owns detached ordered speech preparation, complete semantic audio reuse, cache-only lookup, and per-caller cancellation behind AI.prepareTTS and AI.readPreparedTTS.",
|
|
1099
|
+
"availability": "Browser or native WebView with Blob, AbortController, optional ready DBOPFS, and an injected synthesis callback for generation",
|
|
1099
1100
|
"protocol": "In-process preparation handle plus caller-owned DBOPFS raw audio and version-1 semantic manifest",
|
|
1100
|
-
"normalization": "Preserves complete original parts and JSON-compatible semantic context, shares pending matching work on the same owner/storage group, admits segments in order, stores complete Blobs and MIME metadata, retains successful audio after failure or cancellation, and separates generation readiness from playback.",
|
|
1101
|
-
"surface": "Named prepareSpeech({owner,parts,originalParts,selection,segmentation,storage,identity,signal,onState,synthesize}) returns {segments,state,ready,getAudio(index),cancel()};
|
|
1101
|
+
"normalization": "Preserves complete original parts and JSON-compatible semantic context, shares pending matching work on the same owner/storage group, admits segments in order, stores complete Blobs and MIME metadata, retains successful audio after failure or cancellation, and separates generation readiness from playback. Cache-only lookup uses the same semantic matcher, reads every stored audio reference without writes or repair, and returns null for missing matches/audio while preserving genuine read failures.",
|
|
1102
|
+
"surface": "Named prepareSpeech({owner,parts,originalParts,selection,segmentation,storage,identity,signal,onState,synthesize}) returns {segments,state,ready,getAudio(index),cancel()}. Named async readPreparedSpeech({owner,parts,originalParts=parts,selection=null,segmentation=null,storage=null,identity=null,signal=null,onState=null}) returns a complete ready handle or null without synthesis, activation, unmute, playback, or storage writes. Manifest, audio availability, and returned-handle reads pass DBOPFS createTable:false, leaving absent tables absent; supplied adapters own the same read-existing contract. Omitted storage reads only completed matching same-owner in-memory preparations; pending or failed work returns null. A hit replays ready state and a miss emits nothing. The lookup signal detaches on settlement; the returned handle keeps independent explicit cancellation and MIME-preserving getAudio, including later storage errors. Applications use AI.prepareTTS, AI.readPreparedTTS, and AI.playPreparedTTS so the shared AI owner retains segmentation, provider readiness, capacity, and playback."
|
|
1102
1103
|
},
|
|
1103
1104
|
{
|
|
1104
1105
|
"file": "runtime/arcane/modules/PrintView.js",
|
|
@@ -121,7 +121,7 @@ own asynchronous work, cancellation, and backpressure.
|
|
|
121
121
|
| [`OpenMeteoWeatherProvider.js`](#openmeteoweatherproviderjs) | esm | Searches and loads Open-Meteo data into complete mutable Arcane weather entities. | Browser / native WebView / server with fetch + cloud | Provider data normalized to mutable entities; transport errors mixed. |
|
|
122
122
|
| [`PersistentAIChatSession.js`](#persistentaichatsessionjs) | esm | Adds explicit retained-history/memory policy to complete configured chat without changing DBOPFS or ChatEntity semantics. | Browser / native WebView with DBOPFS and configured chat | Retained context commits atomically; `persist:false` turns are one-operation-only. |
|
|
123
123
|
| [`PreferenceStore.js`](#preferencestorejs) | esm | Loads and updates schema-defined app preferences through native storage with a narrow browser fallback. | Browser/native hybrid | Complete values and operation context retained; exact optional namespace absence or existing Android unsupported capability selects the existing local adapter; partial-service and genuine failures remain errors; an advertised rejected batch is never retried serially. |
|
|
124
|
-
| [`PreparedSpeech.js`](#preparedspeechjs) | esm | Owns detached ordered preparation, semantic audio reuse, and per-caller cancellation behind AI.prepareTTS. | Browser / native WebView with injected synthesis
|
|
124
|
+
| [`PreparedSpeech.js`](#preparedspeechjs) | esm | Owns detached ordered preparation, semantic audio reuse, cache-only lookup, and per-caller cancellation behind AI.prepareTTS and AI.readPreparedTTS. | Browser / native WebView with optional DBOPFS and injected synthesis for generation | Complete original inputs, ordered audio metadata, durable reuse, and observable preparation results. |
|
|
125
125
|
| [`PrintView.js`](#printviewjs) | esm | Prints current rendered content and title through one document-owned print lifecycle. | Browser / supported native WebView print implementation | Complete rendered snapshot, preparation cancellation, resource lifetime, and honest request result. |
|
|
126
126
|
| [`QRCode.min.js`](#qrcodeminjs) | classic-script | Vendored QRCode generator for DOM, canvas, SVG, and image output. | Browser vendor script | Vendor-native. |
|
|
127
127
|
| [`Questionnaire.js`](#questionnairejs) | esm | Evaluates whether a one-time questionnaire prompt is due without performing the prompt. | Cross-host | Normalized conservative boolean. |
|
|
@@ -171,6 +171,7 @@ default `AI`; read-only `providerRuntime`, `speechActivationPending`, `browserSp
|
|
|
171
171
|
read-only `ttsSegmentation`, `configureTTSSegmentation()`,
|
|
172
172
|
`streamTTS(text='',end=false,options={})`,
|
|
173
173
|
`prepareTTS({parts,textFormat,storage,identity,signal,onState})`,
|
|
174
|
+
`readPreparedTTS({parts,textFormat,storage,identity,signal,onState})`,
|
|
174
175
|
`prepareAudioOutput()`, `playPreparedTTS(prepared,{signal,onState,audioOutput})`,
|
|
175
176
|
`finishTTS()`, `prepareTTSPlayback()`, `fetchTTS()`, `fetchSTT()`, `stopAudio()`, `resumeAudio()`,
|
|
176
177
|
`playAudio()`; consumes `user-entity-loaded` and `arcane-ollama-ready`,
|
|
@@ -497,6 +498,34 @@ cancelling one handle does not cancel another active matching caller.
|
|
|
497
498
|
Preparation cancellation prevents later synthesis and releases its activation
|
|
498
499
|
interest. The provider's operation remains responsible for actual cleanup.
|
|
499
500
|
|
|
501
|
+
`await readPreparedTTS({parts,textFormat,storage,identity,signal,onState})`
|
|
502
|
+
returns a complete ready preparation handle or `null`. It uses the same original
|
|
503
|
+
parts, selected speech configuration, voice/speed/pause values, text treatment,
|
|
504
|
+
segmentation, and application identity as `prepareTTS()` for semantic matching.
|
|
505
|
+
It never activates or unmutes a provider, synthesizes, starts playback, writes
|
|
506
|
+
storage, or repairs a partial record. With storage, it reads the existing
|
|
507
|
+
manifest and every ordered audio reference through the existing per-key storage
|
|
508
|
+
queue. It passes `{createTable:false}` to DBOPFS manifest and audio reads,
|
|
509
|
+
including later handle reads, so missing tables stay absent. Supplied database
|
|
510
|
+
adapters are responsible for honoring that read-existing contract.
|
|
511
|
+
No matching record or any missing audio returns `null`; unreadable
|
|
512
|
+
metadata and other storage errors reject. Without storage, only a completed,
|
|
513
|
+
matching, same-AI retained in-memory preparation can be returned; pending or
|
|
514
|
+
failed work returns `null`.
|
|
515
|
+
|
|
516
|
+
A hit returns `{segments,state,ready,getAudio(index),cancel()}` with `state`
|
|
517
|
+
and every segment already `ready`; `onState` synchronously replays that state.
|
|
518
|
+
A miss emits no state callback. The lookup signal rejects cancellation as
|
|
519
|
+
`AbortError` and detaches when lookup settles; aborting it after success does
|
|
520
|
+
not cancel the returned handle. An already-started non-abortable database read
|
|
521
|
+
may finish after cancellation; observed cancellation prevents subsequent reads
|
|
522
|
+
or a returned handle. The handle's explicit `cancel()` retains its
|
|
523
|
+
own lifetime. Durable `getAudio(index)` reopens the stored file with its retained
|
|
524
|
+
MIME type, so a later file removal remains an observable error. Native speech
|
|
525
|
+
without audio export rejects with `ARCANE_AI_TTS_AUDIO_EXPORT_UNAVAILABLE`, as
|
|
526
|
+
`prepareTTS()` does. Pass a returned handle to `playPreparedTTS()` only when
|
|
527
|
+
playback is wanted.
|
|
528
|
+
|
|
500
529
|
`playPreparedTTS(prepared,{signal,onState,audioOutput})` can attach immediately. Its returned
|
|
501
530
|
`{state,error,finished,pause(),resume(),stop()}` handle schedules segments in
|
|
502
531
|
their original order using the existing AI audio clock. `finished` resolves `true` after
|
|
@@ -2254,6 +2283,23 @@ default `DBOPFS`; installs `window.dbopfs`, emits `dbopfs-ready`; table/file/bac
|
|
|
2254
2283
|
|
|
2255
2284
|
Exact exports: `DBOPFS_EVENT_TYPES`, `DBOPFS_REASONS`, `default`.
|
|
2256
2285
|
|
|
2286
|
+
`getTableHandle(tableName='', {create=true}={})` lazily opens or creates the
|
|
2287
|
+
requested table by default. `{create:false}` looks up only an existing table
|
|
2288
|
+
without joining a pending create request; a missing table rejects with the
|
|
2289
|
+
platform's `NotFoundError`. The existing `memories` / `memory` alias is retained.
|
|
2290
|
+
`readFile(tableName='', fileName='', {createTable=true}={})` returns the complete
|
|
2291
|
+
raw file. `{createTable:false}` also prevents table creation, and missing tables
|
|
2292
|
+
or files reject with `NotFoundError`. Its synchronous-access worker fallback
|
|
2293
|
+
already opens existing app/table/file handles without creating them.
|
|
2294
|
+
|
|
2295
|
+
`get(tableName='', fileName='', force=false, {createTable=true}={})` retains its
|
|
2296
|
+
existing parsed-value cache and `force` behavior, forwarding `createTable` when
|
|
2297
|
+
it reads a file. On a read, `NotFoundError` resolves `null`; other errors reject.
|
|
2298
|
+
Use `get(tableName,fileName,true,{createTable:false})` for an existing-storage
|
|
2299
|
+
lookup that does not return an earlier cached value. Omitted options preserve the previous
|
|
2300
|
+
lazy table-creation behavior. These options change neither write operations nor
|
|
2301
|
+
the owning app scope's initialization.
|
|
2302
|
+
|
|
2257
2303
|
`subscribeChanges(listener, {signal, once})` observes
|
|
2258
2304
|
`DBOPFS_EVENT_TYPES.change` (`dbopfs-change`) through the existing canonical
|
|
2259
2305
|
`arcane-os/event-manager` source. It returns an idempotent unsubscribe function
|
|
@@ -3649,10 +3695,11 @@ console.log(Object.keys(module));
|
|
|
3649
3695
|
|
|
3650
3696
|
### Overview
|
|
3651
3697
|
|
|
3652
|
-
Shared preparation mechanism used by `AI.prepareTTS()
|
|
3653
|
-
generation admission, same-owner request sharing, complete audio
|
|
3654
|
-
semantic reuse, and each caller's preparation
|
|
3655
|
-
an audio context, play speech, or select
|
|
3698
|
+
Shared preparation mechanism used by `AI.prepareTTS()` and `AI.readPreparedTTS()`.
|
|
3699
|
+
It owns ordered generation admission, same-owner request sharing, complete audio
|
|
3700
|
+
storage and semantic reuse, cache-only lookup, and each caller's preparation
|
|
3701
|
+
lifetime. It does not construct an audio context, play speech, or select
|
|
3702
|
+
application content.
|
|
3656
3703
|
|
|
3657
3704
|
### Public surface
|
|
3658
3705
|
|
|
@@ -3661,10 +3708,29 @@ segmentation=null,storage=null,identity=null,signal=null,onState,synthesize})`.
|
|
|
3661
3708
|
The owning AI supplies already segmented speech parts, complete original parts,
|
|
3662
3709
|
its selection snapshot, and its synthesis callback. The returned handle is
|
|
3663
3710
|
`{segments,state,ready,getAudio(index),cancel()}`. Applications use
|
|
3664
|
-
`AI.prepareTTS()` and `AI.playPreparedTTS()` so the existing
|
|
3665
|
-
automatic formatting cleanup, segmentation, provider readiness/capacity, and
|
|
3711
|
+
`AI.prepareTTS()`, `AI.readPreparedTTS()`, and `AI.playPreparedTTS()` so the existing
|
|
3712
|
+
AI owner retains automatic formatting cleanup, segmentation, provider readiness/capacity, and
|
|
3666
3713
|
ordered playback. See the [complete preparation contract](ai/browser-speech.md#prepare-narration-once-and-replay-stored-audio).
|
|
3667
3714
|
|
|
3715
|
+
Named async `readPreparedSpeech({owner,parts,originalParts=parts,selection=null,
|
|
3716
|
+
segmentation=null,storage=null,identity=null,signal=null,onState=null})` uses
|
|
3717
|
+
the same complete semantic matcher and resolves a ready preparation handle or
|
|
3718
|
+
`null`. It performs no synthesis, model activation, playback, storage writes,
|
|
3719
|
+
or repair. A durable lookup reads the existing manifest and every audio file;
|
|
3720
|
+
missing matching records or audio return `null`, while malformed metadata and
|
|
3721
|
+
other read failures reject. Manifest, audio availability, and returned-handle
|
|
3722
|
+
audio reads pass `{createTable:false}` to DBOPFS so a missing table is never
|
|
3723
|
+
created; supplied adapters own the same read-existing behavior. Without storage
|
|
3724
|
+
it reads only a completed matching same-owner in-memory preparation, never
|
|
3725
|
+
pending or failed work. A hit replays
|
|
3726
|
+
ready state through `onState`; a miss emits nothing. Lookup cancellation rejects
|
|
3727
|
+
with `AbortError` and is detached on settlement. After success the returned
|
|
3728
|
+
handle's explicit `cancel()` is independent of the lookup signal; its `ready`
|
|
3729
|
+
resolves the complete record and `getAudio(index)` preserves the existing MIME
|
|
3730
|
+
and later-read error behavior. Already-started non-abortable database reads may
|
|
3731
|
+
settle after cancellation; no further reads or handle are returned after the
|
|
3732
|
+
cancellation is observed.
|
|
3733
|
+
|
|
3668
3734
|
`storage:{db,table,key}` is optional; the ready DBOPFS instance supplies
|
|
3669
3735
|
`get`, `set`, `readFile`, and `writeFile`. JSON-compatible semantic inputs stay
|
|
3670
3736
|
complete in the version-1 manifest. Raw audio is persisted separately and its
|
|
@@ -3672,13 +3738,14 @@ MIME type is retained in metadata. Storage mutation serializes by database,
|
|
|
3672
3738
|
table, and key within the realm. Synthesis sharing is scoped to the same owner
|
|
3673
3739
|
and matching semantic inputs/storage; playback remains outside this module.
|
|
3674
3740
|
|
|
3675
|
-
Exact exports: `prepareSpeech`.
|
|
3741
|
+
Exact exports: `prepareSpeech`, `readPreparedSpeech`.
|
|
3676
3742
|
|
|
3677
3743
|
### Availability and normalization
|
|
3678
3744
|
|
|
3679
|
-
**Browser or native WebView with Blob, AbortController,
|
|
3680
|
-
|
|
3681
|
-
playback. Calling
|
|
3745
|
+
**Browser or native WebView with Blob, AbortController, optional ready DBOPFS,
|
|
3746
|
+
and an injected synthesis callback for generation.** Import creates no provider or
|
|
3747
|
+
playback. Calling `prepareSpeech` starts owned asynchronous generation/reuse;
|
|
3748
|
+
`readPreparedSpeech` only reads existing complete audio.
|
|
3682
3749
|
`ready` rejects complete synthesis/storage failures or `AbortError` after
|
|
3683
3750
|
cancellation; successful audio remains available for reuse. Malformed part,
|
|
3684
3751
|
storage, or semantic metadata inputs throw `TypeError`; an invalid segment
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
openAIResponseFormat
|
|
27
27
|
} from 'arcane-os/ai/twin-cloud';
|
|
28
28
|
import {MarkdownSpeech,stripSpeechFormatting} from 'arcane-os/speech-text';
|
|
29
|
-
import {prepareSpeech} from './PreparedSpeech.js';
|
|
29
|
+
import {prepareSpeech, readPreparedSpeech} from './PreparedSpeech.js';
|
|
30
30
|
import {createToolTextObserver} from 'arcane-os/ai/tool-text-stream';
|
|
31
31
|
|
|
32
32
|
const completeValue=(value)=>value;
|
|
@@ -6341,16 +6341,15 @@ class AI {
|
|
|
6341
6341
|
return result;
|
|
6342
6342
|
}
|
|
6343
6343
|
|
|
6344
|
-
|
|
6344
|
+
#preparedSpeechInput(parts, textFormat, operation) {
|
|
6345
6345
|
if(this.#providerSpeechModel()?.speech?.playback==='native'){
|
|
6346
6346
|
const error=new Error('Native browser speech has no audio file to prepare or store; use prepareTTSPlayback.');
|
|
6347
6347
|
error.code='ARCANE_AI_TTS_AUDIO_EXPORT_UNAVAILABLE';
|
|
6348
6348
|
throw error;
|
|
6349
6349
|
}
|
|
6350
6350
|
if(!is.array(parts)) {
|
|
6351
|
-
throw new TypeError(
|
|
6351
|
+
throw new TypeError(`${operation} parts must be an array.`);
|
|
6352
6352
|
}
|
|
6353
|
-
const runtime = this;
|
|
6354
6353
|
const selected = this.#providerRuntime.selection('tts');
|
|
6355
6354
|
const configuredSpeech = this.browserSpeechConfiguration?.tts;
|
|
6356
6355
|
const configuration = configuredSpeech
|
|
@@ -6367,12 +6366,9 @@ class AI {
|
|
|
6367
6366
|
responseFormat: selected ? this.#providerSpeechResponseFormat() : this.audioFormat,
|
|
6368
6367
|
configuration
|
|
6369
6368
|
};
|
|
6370
|
-
const browserGeneration = this.#browserSpeechGeneration;
|
|
6371
|
-
const controlGeneration = this.#speechControlGeneration;
|
|
6372
6369
|
const defaultVoice = selected
|
|
6373
6370
|
? this.#providerSpeechVoice()
|
|
6374
6371
|
: this.#builtInSpeechDefaultVoice('tts', this.ttsService);
|
|
6375
|
-
const responseFormat = selection.responseFormat;
|
|
6376
6372
|
const segmentation = this.ttsSegmentation;
|
|
6377
6373
|
const segments = [];
|
|
6378
6374
|
for(const part of parts) {
|
|
@@ -6398,6 +6394,33 @@ class AI {
|
|
|
6398
6394
|
);
|
|
6399
6395
|
}
|
|
6400
6396
|
}
|
|
6397
|
+
return {segments, selection, segmentation, selected};
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6400
|
+
async readPreparedTTS({parts, textFormat, storage = null, identity = null, signal = null, onState = null} = {}) {
|
|
6401
|
+
const {segments, selection, segmentation} = this.#preparedSpeechInput(parts, textFormat, 'AI.readPreparedTTS');
|
|
6402
|
+
this.#traceSpeech('readPreparedTTS.call', {parts, segments, selection, segmentation, identity, storage});
|
|
6403
|
+
return readPreparedSpeech(
|
|
6404
|
+
{
|
|
6405
|
+
owner: this,
|
|
6406
|
+
parts: segments,
|
|
6407
|
+
originalParts: parts,
|
|
6408
|
+
selection,
|
|
6409
|
+
segmentation,
|
|
6410
|
+
storage,
|
|
6411
|
+
identity,
|
|
6412
|
+
signal,
|
|
6413
|
+
onState
|
|
6414
|
+
}
|
|
6415
|
+
);
|
|
6416
|
+
}
|
|
6417
|
+
|
|
6418
|
+
prepareTTS({parts, textFormat, storage = null, identity = null, signal = null, onState = null} = {}) {
|
|
6419
|
+
const {segments, selection, segmentation, selected} = this.#preparedSpeechInput(parts, textFormat, 'AI.prepareTTS');
|
|
6420
|
+
const runtime = this;
|
|
6421
|
+
const browserGeneration = this.#browserSpeechGeneration;
|
|
6422
|
+
const controlGeneration = this.#speechControlGeneration;
|
|
6423
|
+
const responseFormat = selection.responseFormat;
|
|
6401
6424
|
this.#traceSpeech('prepareTTS.call', {parts, segments, selection, segmentation, identity, storage});
|
|
6402
6425
|
async function synthesizePreparedSpeech(part, requestSignal) {
|
|
6403
6426
|
function assertPreparedSpeechSelection() {
|
|
@@ -554,12 +554,13 @@ class DBOPFS {
|
|
|
554
554
|
|
|
555
555
|
/**
|
|
556
556
|
* Gets the directory handle for a table.
|
|
557
|
-
* Creates the table if it does not exist.
|
|
557
|
+
* Creates the table if it does not exist unless create is false.
|
|
558
558
|
*
|
|
559
559
|
* @param {string} tableName
|
|
560
|
+
* @param {{create?:boolean}} options A false create performs an existing-table lookup only.
|
|
560
561
|
* @returns {Promise<FileSystemDirectoryHandle>}
|
|
561
562
|
*/
|
|
562
|
-
async getTableHandle(tableName=''){
|
|
563
|
+
async getTableHandle(tableName='',{create=true}={}){
|
|
563
564
|
if(!this.ready){
|
|
564
565
|
await this.readyPromise;
|
|
565
566
|
}
|
|
@@ -582,6 +583,14 @@ class DBOPFS {
|
|
|
582
583
|
return existingHandle;
|
|
583
584
|
}
|
|
584
585
|
|
|
586
|
+
if(!create){
|
|
587
|
+
// A read-existing lookup must not wait for another caller to create the table.
|
|
588
|
+
return this.#db.getDirectoryHandle(
|
|
589
|
+
directoryName,
|
|
590
|
+
{create:false}
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
585
594
|
if(!this.#tableHandlePromises[registeredTableName]){
|
|
586
595
|
const handlePromise=this.#db.getDirectoryHandle(
|
|
587
596
|
directoryName,
|
|
@@ -736,10 +745,14 @@ class DBOPFS {
|
|
|
736
745
|
*
|
|
737
746
|
* @param {string} tableName
|
|
738
747
|
* @param {string} fileName
|
|
748
|
+
* @param {{createTable?:boolean}} options A false createTable preserves an absent table.
|
|
739
749
|
* @returns {Promise<File>}
|
|
740
750
|
*/
|
|
741
|
-
async readFile(tableName='',fileName=''){
|
|
742
|
-
const table=await this.getTableHandle(
|
|
751
|
+
async readFile(tableName='',fileName='',{createTable=true}={}){
|
|
752
|
+
const table=await this.getTableHandle(
|
|
753
|
+
tableName,
|
|
754
|
+
{create:createTable}
|
|
755
|
+
)
|
|
743
756
|
const handle=await table.getFileHandle(fileName,{create:false})
|
|
744
757
|
|
|
745
758
|
if(is.function(handle.getFile)){
|
|
@@ -818,14 +831,19 @@ class DBOPFS {
|
|
|
818
831
|
* @param {string} tableName
|
|
819
832
|
* @param {string} fileName
|
|
820
833
|
* @param {boolean} force
|
|
834
|
+
* @param {{createTable?:boolean}} options A false createTable preserves an absent table.
|
|
821
835
|
* @returns {Promise<*>}
|
|
822
836
|
*/
|
|
823
|
-
async get(tableName='',fileName='',force=false){
|
|
837
|
+
async get(tableName='',fileName='',force=false,{createTable=true}={}){
|
|
824
838
|
if(force||!this.#tables[tableName]?.[fileName]){
|
|
825
839
|
const cacheState=this.#fileCacheState(tableName,fileName);
|
|
826
840
|
const cacheRevision=cacheState.revision;
|
|
827
841
|
try{
|
|
828
|
-
const file=await this.readFile(
|
|
842
|
+
const file=await this.readFile(
|
|
843
|
+
tableName,
|
|
844
|
+
fileName,
|
|
845
|
+
{createTable}
|
|
846
|
+
)
|
|
829
847
|
const textContent=await file.text()
|
|
830
848
|
|
|
831
849
|
if(!this.#tables[tableName]){
|
|
@@ -111,8 +111,10 @@ function serializeStorage(storage,execute){
|
|
|
111
111
|
return operation;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
async function readManifest(storage){
|
|
115
|
-
const stored=
|
|
114
|
+
async function readManifest(storage,cacheOnly=false){
|
|
115
|
+
const stored=cacheOnly
|
|
116
|
+
?await storage.db.get(storage.table,storage.fileName,true,{createTable:false})
|
|
117
|
+
:await storage.db.get(storage.table,storage.fileName,true);
|
|
116
118
|
if(stored===null||stored===undefined)return {version:1,entries:[]};
|
|
117
119
|
const manifest=copyJSON(stored);
|
|
118
120
|
if(manifest.version!==1||!is.array(manifest.entries)){
|
|
@@ -349,9 +351,10 @@ function finishPreparation(job,error){
|
|
|
349
351
|
job.finished.resolve();
|
|
350
352
|
}
|
|
351
353
|
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
+
function createJobState(input,storage,synthesize=null,previous=null){
|
|
355
|
+
return {
|
|
354
356
|
input,storage,synthesize,previous,
|
|
357
|
+
cacheOnly:false,
|
|
355
358
|
controller:new AbortController(),
|
|
356
359
|
interests:new Set(),
|
|
357
360
|
state:'queued',
|
|
@@ -367,6 +370,10 @@ function createJob(ownerState,input,storage,synthesize,previous){
|
|
|
367
370
|
return {...part,index,state:'queued',audioFile:null,contentType:null,error:null};
|
|
368
371
|
})
|
|
369
372
|
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function createJob(ownerState,input,storage,synthesize,previous){
|
|
376
|
+
const job=createJobState(input,storage,synthesize,previous);
|
|
370
377
|
const admission=ownerState.admission.then(function admitOrderedPreparation(){
|
|
371
378
|
return admitPreparation(job);
|
|
372
379
|
});
|
|
@@ -416,7 +423,9 @@ function createHandle(job,signal,onState){
|
|
|
416
423
|
if(!interest.active)throw abortError();
|
|
417
424
|
if(!job.storage)return job.slots[index].blob;
|
|
418
425
|
const audio=job.record.segments[index];
|
|
419
|
-
const file=
|
|
426
|
+
const file=job.cacheOnly
|
|
427
|
+
?await job.storage.db.readFile(job.storage.table,audio.audioFile,{createTable:false})
|
|
428
|
+
:await job.storage.db.readFile(job.storage.table,audio.audioFile);
|
|
420
429
|
if(!interest.active)throw abortError();
|
|
421
430
|
return file.type===audio.contentType?file:new Blob([file],{type:audio.contentType});
|
|
422
431
|
},
|
|
@@ -482,4 +491,100 @@ function prepareSpeech({
|
|
|
482
491
|
return createHandle(job,signal,onState);
|
|
483
492
|
}
|
|
484
493
|
|
|
485
|
-
|
|
494
|
+
/** Read complete existing audio without synthesis, storage writes, or repair. */
|
|
495
|
+
async function readPreparedSpeech({
|
|
496
|
+
owner,
|
|
497
|
+
parts,
|
|
498
|
+
originalParts=parts,
|
|
499
|
+
selection=null,
|
|
500
|
+
segmentation=null,
|
|
501
|
+
storage=null,
|
|
502
|
+
identity=null,
|
|
503
|
+
signal=null,
|
|
504
|
+
onState=null
|
|
505
|
+
}={}){
|
|
506
|
+
if((!owner||!is.object(owner))&&!is.function(owner)){
|
|
507
|
+
throw new TypeError('Speech preparation requires its owning AI instance.');
|
|
508
|
+
}
|
|
509
|
+
if(!is.array(parts)||(onState!==null&&!is.function(onState))){
|
|
510
|
+
throw new TypeError('Cached speech requires ordered parts and an optional state callback.');
|
|
511
|
+
}
|
|
512
|
+
const input=copyJSON({parts,originalParts,selection,segmentation,identity});
|
|
513
|
+
const destination=normalizeStorage(storage);
|
|
514
|
+
if(signal?.aborted)throw abortError();
|
|
515
|
+
function completeCachedRead(job){
|
|
516
|
+
// The lookup signal owns only this read. Retained audio keeps its own
|
|
517
|
+
// explicit cancellation lifetime after a successful lookup returns.
|
|
518
|
+
const handle=createHandle(job,null,onState);
|
|
519
|
+
if(signal?.aborted){
|
|
520
|
+
handle.cancel();
|
|
521
|
+
throw abortError();
|
|
522
|
+
}
|
|
523
|
+
return handle;
|
|
524
|
+
}
|
|
525
|
+
if(!destination){
|
|
526
|
+
const jobs=owners.get(owner)?.jobs;
|
|
527
|
+
if(!jobs)return null;
|
|
528
|
+
for(const job of jobs){
|
|
529
|
+
if(job.storage||!job.done||job.failed||job.controller.signal.aborted
|
|
530
|
+
||!sameInput(job.input,input))continue;
|
|
531
|
+
if(!job.slots.every(function hasRetainedAudio(slot){
|
|
532
|
+
return slot.settled&&slot.blob instanceof Blob;
|
|
533
|
+
}))return null;
|
|
534
|
+
return completeCachedRead(job);
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const cancelled=deferred();
|
|
539
|
+
function cancelRead(){cancelled.reject(abortError());}
|
|
540
|
+
signal?.addEventListener('abort',cancelRead,{once:true});
|
|
541
|
+
try{
|
|
542
|
+
const reading=serializeStorage(destination,async function readCachedPreparation(){
|
|
543
|
+
if(signal?.aborted)throw abortError();
|
|
544
|
+
const manifest=await readManifest(destination,true);
|
|
545
|
+
if(signal?.aborted)throw abortError();
|
|
546
|
+
const record=manifest.entries.find(function findCachedPreparation(entry){
|
|
547
|
+
return sameInput(entry,input);
|
|
548
|
+
});
|
|
549
|
+
if(!record)return null;
|
|
550
|
+
if(!is.array(record.segments)||record.segments.length!==input.parts.length){
|
|
551
|
+
throw new TypeError('Stored speech preparation segments are unreadable.');
|
|
552
|
+
}
|
|
553
|
+
let complete=true;
|
|
554
|
+
for(const audio of record.segments){
|
|
555
|
+
if(!audio?.audioFile){
|
|
556
|
+
complete=false;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if(!is.string(audio.contentType)){
|
|
560
|
+
throw new TypeError('Stored speech audio content type is unreadable.');
|
|
561
|
+
}
|
|
562
|
+
if(signal?.aborted)throw abortError();
|
|
563
|
+
try{
|
|
564
|
+
const file=await destination.db.readFile(destination.table,audio.audioFile,{createTable:false});
|
|
565
|
+
if(!(file instanceof Blob)){
|
|
566
|
+
throw new TypeError('Stored speech audio must be a Blob.');
|
|
567
|
+
}
|
|
568
|
+
}catch(error){
|
|
569
|
+
if(error?.name!=='NotFoundError')throw error;
|
|
570
|
+
complete=false;
|
|
571
|
+
}
|
|
572
|
+
if(signal?.aborted)throw abortError();
|
|
573
|
+
}
|
|
574
|
+
return complete?record:null;
|
|
575
|
+
});
|
|
576
|
+
const record=await Promise.race([reading,cancelled.promise]);
|
|
577
|
+
if(signal?.aborted)throw abortError();
|
|
578
|
+
if(!record)return null;
|
|
579
|
+
const job=createJobState(input,destination);
|
|
580
|
+
job.cacheOnly=true;
|
|
581
|
+
job.record=record;
|
|
582
|
+
for(let index=0;index<job.slots.length;index+=1)settleSegment(job,index);
|
|
583
|
+
finishPreparation(job);
|
|
584
|
+
return completeCachedRead(job);
|
|
585
|
+
}finally{
|
|
586
|
+
signal?.removeEventListener('abort',cancelRead);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export {prepareSpeech,readPreparedSpeech};
|