arcane-os 0.47.0 → 0.49.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 +42 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-speech-artifacts.mjs +4 -0
- package/browser-runtime/ai/browser-speech-providers.mjs +1 -1
- package/browser-runtime/ai/twin-cloud-speech.mjs +148 -55
- package/docs/reference/ai/browser-speech.md +24 -11
- package/docs/reference/runtime-components.md +72 -20
- package/docs/reference/runtime-modules.md +25 -10
- package/package.json +1 -1
- package/runtime/arcane/components/chat.html +223 -2
- package/runtime/arcane/components/speech.html +123 -4
- package/runtime/arcane/components/voice-transcription.html +21 -23
- package/runtime/arcane/css/document-site.css +2 -35
- package/runtime/arcane/css/print.css +235 -0
- package/runtime/arcane/css/theme.css +2 -40
- package/runtime/arcane/modules/PrintView.js +19 -55
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.49.0
|
|
4
|
+
|
|
5
|
+
- Queue complete TWiN Cloud/FAL synthesis jobs through credential readiness,
|
|
6
|
+
submission, remote completion and the complete audio response. Excess jobs
|
|
7
|
+
wait in order; cancellation removes waiting work before submission. Preserve
|
|
8
|
+
independent follow-up HTTP concurrency and dispatch cadence, full input,
|
|
9
|
+
caller cancellation and original-order playback.
|
|
10
|
+
- Queue initial POST attempts and eligible retries with a shared HTTP 429
|
|
11
|
+
cooldown. Preserve the bounded readable-rejection retry and never replay an
|
|
12
|
+
ambiguous POST network, JSON or error-body failure. Existing provider factory,
|
|
13
|
+
compatibility alias and configuration remain supported.
|
|
14
|
+
- Expose optional ordinary Kokoro `model.voices` display records through the
|
|
15
|
+
synchronous provider catalog without loading or fetching a model. Applications
|
|
16
|
+
own their selected inventory and previews; metadata does not limit synthesis
|
|
17
|
+
voices or claim playback availability. Existing artifact-graph voice paths
|
|
18
|
+
retain their meaning. No new dependency, saved-data migration or default
|
|
19
|
+
provider change; native browser TTS is a separate subsequent increment.
|
|
20
|
+
|
|
21
|
+
## 0.48.0
|
|
22
|
+
|
|
23
|
+
- Add opt-in Chat `transcriptionMode='draft'` and public `appendDraft(text)`.
|
|
24
|
+
Native recognition appears immediately in the editable composer; interim
|
|
25
|
+
replacement preserves typed text and user edits, and completion leaves manual
|
|
26
|
+
Send to the user. Existing consumers retain default completed-transcription
|
|
27
|
+
submission. Conversation language remains separate from recognition language.
|
|
28
|
+
- Add native BCP47 `recognitionLanguage` in the visible shared speech status
|
|
29
|
+
area and cumulative `speech-transcription-progress` events. Preserve model
|
|
30
|
+
transcription wording, TTS controls, gesture-bound capture, final draining,
|
|
31
|
+
cancellation and provider ownership. Continuous voice shows canonical text,
|
|
32
|
+
queued finals and transient interim text together in its existing output;
|
|
33
|
+
durable text and ordered save/retry payloads remain unchanged.
|
|
34
|
+
- Centralize direct-page and rendered-snapshot print styles in `print.css`,
|
|
35
|
+
with a light paper palette and one-inch page margins. Shared screen shells
|
|
36
|
+
expand into document flow only for print. Preserve rendered fonts, complete
|
|
37
|
+
images and SVG artwork, resource readiness and snapshot ownership through
|
|
38
|
+
`afterprint`.
|
|
39
|
+
- Add public Chat `print()` and `printTitle`, using shared PrintView for the
|
|
40
|
+
complete rendered conversation without composer controls, transient thinking
|
|
41
|
+
or tool-inspection details. Preserve human-facing tool messages. A successful
|
|
42
|
+
method result means the browser print request began, not that paper or PDF
|
|
43
|
+
output completed. No saved-data migration or model/provider/default change.
|
|
44
|
+
|
|
3
45
|
## 0.47.0
|
|
4
46
|
|
|
5
47
|
- Add explicitly selected native browser speech recognition through
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
|
|
20
20
|
and browser run contracts while retaining their selected application layout.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.
|
|
22
|
+
This checkout defines the `0.49.0` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -899,6 +899,9 @@ export function createBrowserSpeechAuthority({
|
|
|
899
899
|
const dtype = model.dtype === undefined
|
|
900
900
|
? null
|
|
901
901
|
: identifier(model.dtype, "Browser speech model dtype");
|
|
902
|
+
if (role === "tts" && model.voices !== undefined && !is.array(model.voices)) {
|
|
903
|
+
throw new TypeError("Browser Kokoro model voices must be an array.");
|
|
904
|
+
}
|
|
902
905
|
const modelFiles = uniqueFiles(
|
|
903
906
|
model.files ?? [],
|
|
904
907
|
"Browser speech model",
|
|
@@ -967,6 +970,7 @@ export function createBrowserSpeechAuthority({
|
|
|
967
970
|
revision: modelRevision,
|
|
968
971
|
...(dtype === null ? {} : { dtype }),
|
|
969
972
|
defaultVoice: normalizedModel.defaultVoice,
|
|
973
|
+
...(role === "tts" && model.voices !== undefined ? { voices: model.voices } : {}),
|
|
970
974
|
runtime: completeValue({
|
|
971
975
|
adapter: normalizedRuntime.adapter,
|
|
972
976
|
version: normalizedRuntime.version,
|
|
@@ -1151,7 +1151,7 @@ function createBrowserSpeechProvider({
|
|
|
1151
1151
|
...(authority.dtype ? { dtype: authority.dtype } : {}),
|
|
1152
1152
|
runtime: authority.runtime,
|
|
1153
1153
|
files: authority.files,
|
|
1154
|
-
...(authority.graph ? {
|
|
1154
|
+
...(authority.graph || authority.voices !== undefined ? {
|
|
1155
1155
|
voices: authority.voices,
|
|
1156
1156
|
} : {}),
|
|
1157
1157
|
speech,
|
|
@@ -3,7 +3,7 @@ import Queue from '../dependencies/js-queue/queue.js';
|
|
|
3
3
|
|
|
4
4
|
const is = new Is(false);
|
|
5
5
|
const INVOKE_URL = 'https://inference.do-ai.run/v1/async-invoke';
|
|
6
|
-
const
|
|
6
|
+
const RATE_LIMIT_RETRY_DELAY_MS = 1000;
|
|
7
7
|
|
|
8
8
|
function speechError(message, code, cause) {
|
|
9
9
|
const error = cause === undefined
|
|
@@ -36,30 +36,6 @@ function assertIdentifier(value, label) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function waitForStatus(delay, signal) {
|
|
40
|
-
assertNotAborted(signal);
|
|
41
|
-
return new Promise(
|
|
42
|
-
function waitForCloudSpeechStatus(resolve, reject) {
|
|
43
|
-
const timer = setTimeout(finishStatusWait, delay);
|
|
44
|
-
function finishStatusWait() {
|
|
45
|
-
signal.removeEventListener('abort', cancelStatusWait);
|
|
46
|
-
resolve();
|
|
47
|
-
}
|
|
48
|
-
function cancelStatusWait() {
|
|
49
|
-
clearTimeout(timer);
|
|
50
|
-
signal.removeEventListener('abort', cancelStatusWait);
|
|
51
|
-
reject(abortedSpeech(signal));
|
|
52
|
-
}
|
|
53
|
-
signal.addEventListener(
|
|
54
|
-
'abort',
|
|
55
|
-
cancelStatusWait,
|
|
56
|
-
{once: true}
|
|
57
|
-
);
|
|
58
|
-
if (signal.aborted) cancelStatusWait();
|
|
59
|
-
}
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
39
|
function retryAfterDelay(response) {
|
|
64
40
|
const value = response.headers?.get?.('Retry-After');
|
|
65
41
|
if (!value?.trim()) return null;
|
|
@@ -69,11 +45,11 @@ function retryAfterDelay(response) {
|
|
|
69
45
|
return is.finite(date) ? Math.max(0, date - Date.now()) : null;
|
|
70
46
|
}
|
|
71
47
|
|
|
72
|
-
async function
|
|
73
|
-
if (response.ok) return
|
|
48
|
+
async function readResponseError(response, signal, operation) {
|
|
49
|
+
if (response.ok) return null;
|
|
74
50
|
const detail = await response.text();
|
|
75
51
|
assertNotAborted(signal);
|
|
76
|
-
|
|
52
|
+
return speechError(
|
|
77
53
|
`TWiN Cloud speech ${operation} failed with HTTP ${response.status}${detail ? `: ${detail}` : '.'}`,
|
|
78
54
|
'ARCANE_AI_CLOUD_SPEECH_HTTP_ERROR'
|
|
79
55
|
);
|
|
@@ -83,8 +59,8 @@ async function requireSuccessfulResponse(response, signal, operation) {
|
|
|
83
59
|
* A remote provider/2 TTS adapter. The application selects the model and voice,
|
|
84
60
|
* and getApiKey returns its current credential or the application's existing
|
|
85
61
|
* refresh promise. Credentials never enter status or configuration. The runtime
|
|
86
|
-
* owns
|
|
87
|
-
*
|
|
62
|
+
* owns playback order. This provider queues whole synthesis jobs, submission
|
|
63
|
+
* attempts, and follow-up HTTP/body operations under one shared 429 cooldown.
|
|
88
64
|
*/
|
|
89
65
|
export function createTwinCloudTTSProvider({
|
|
90
66
|
id,
|
|
@@ -115,18 +91,132 @@ export function createTwinCloudTTSProvider({
|
|
|
115
91
|
|
|
116
92
|
const modelId = model.id;
|
|
117
93
|
const defaultVoice = model.defaultVoice;
|
|
94
|
+
const requests = new Set();
|
|
118
95
|
const activeRequests = new Set();
|
|
119
96
|
const credentialWaits = new Set();
|
|
120
97
|
let state = 'unloaded';
|
|
121
98
|
let generation = 0;
|
|
122
99
|
let unloadOperation = null;
|
|
123
100
|
let disposeOperation = null;
|
|
101
|
+
const jobs = new Queue();
|
|
102
|
+
jobs.autoRun = false;
|
|
103
|
+
const submissions = new Queue();
|
|
104
|
+
submissions.autoRun = false;
|
|
124
105
|
const followUps = new Queue();
|
|
125
106
|
followUps.autoRun = false;
|
|
107
|
+
let cooldownUntil = 0;
|
|
108
|
+
let submissionTimer = null;
|
|
126
109
|
let activeFollowUps = 0;
|
|
127
110
|
let nextFollowUpStart = 0;
|
|
128
111
|
let followUpTimer = null;
|
|
129
112
|
|
|
113
|
+
function dispatchJobs() {
|
|
114
|
+
while (state === 'ready' && activeRequests.size < maxConcurrentRequests && jobs.size) {
|
|
115
|
+
jobs.next();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function queueSynthesis(request, payload, requestGeneration) {
|
|
120
|
+
const signal = request.controller.signal;
|
|
121
|
+
assertNotAborted(signal);
|
|
122
|
+
return new Promise(
|
|
123
|
+
function enqueueCloudSpeechJob(resolve, reject) {
|
|
124
|
+
function cancelQueuedJob() {
|
|
125
|
+
jobs.contents = jobs.contents.filter(
|
|
126
|
+
function keepOtherJob(task) {return task !== startJob;}
|
|
127
|
+
);
|
|
128
|
+
signal.removeEventListener('abort', cancelQueuedJob);
|
|
129
|
+
reject(abortedSpeech(signal));
|
|
130
|
+
dispatchJobs();
|
|
131
|
+
}
|
|
132
|
+
function finishJob() {
|
|
133
|
+
activeRequests.delete(request);
|
|
134
|
+
dispatchJobs();
|
|
135
|
+
}
|
|
136
|
+
function startJob() {
|
|
137
|
+
signal.removeEventListener('abort', cancelQueuedJob);
|
|
138
|
+
activeRequests.add(request);
|
|
139
|
+
// A slot belongs to the entire synthesis, including its
|
|
140
|
+
// accepted remote job, every read, and the complete audio.
|
|
141
|
+
synthesize(payload, request.controller, requestGeneration).then(
|
|
142
|
+
function acceptCloudSpeechJob(audio) {
|
|
143
|
+
finishJob();
|
|
144
|
+
resolve(audio);
|
|
145
|
+
},
|
|
146
|
+
function rejectCloudSpeechJob(error) {
|
|
147
|
+
finishJob();
|
|
148
|
+
reject(error);
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
jobs.add(startJob);
|
|
153
|
+
signal.addEventListener(
|
|
154
|
+
'abort',
|
|
155
|
+
cancelQueuedJob,
|
|
156
|
+
{once: true}
|
|
157
|
+
);
|
|
158
|
+
if (signal.aborted) cancelQueuedJob();
|
|
159
|
+
else dispatchJobs();
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function dispatchSubmissions() {
|
|
165
|
+
clearTimeout(submissionTimer);
|
|
166
|
+
submissionTimer = null;
|
|
167
|
+
if (state !== 'ready' || !submissions.size) return;
|
|
168
|
+
const delay = cooldownUntil - Date.now();
|
|
169
|
+
if (delay > 0) {
|
|
170
|
+
submissionTimer = setTimeout(dispatchSubmissions, Math.min(2147483647, delay));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Each admitted job awaits its current attempt before requeueing, so
|
|
174
|
+
// whole-job capacity also bounds concurrent submission attempts.
|
|
175
|
+
while (state === 'ready' && submissions.size) submissions.next();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function queueSubmission(readResponse, signal) {
|
|
179
|
+
assertNotAborted(signal);
|
|
180
|
+
return new Promise(
|
|
181
|
+
function enqueueCloudSpeechSubmission(resolve, reject) {
|
|
182
|
+
function cancelQueuedSubmission() {
|
|
183
|
+
submissions.contents = submissions.contents.filter(
|
|
184
|
+
function keepOtherSubmission(task) {return task !== startSubmission;}
|
|
185
|
+
);
|
|
186
|
+
signal.removeEventListener('abort', cancelQueuedSubmission);
|
|
187
|
+
reject(abortedSpeech(signal));
|
|
188
|
+
dispatchSubmissions();
|
|
189
|
+
}
|
|
190
|
+
async function consumeSubmission() {
|
|
191
|
+
assertNotAborted(signal);
|
|
192
|
+
return await readResponse();
|
|
193
|
+
}
|
|
194
|
+
function startSubmission() {
|
|
195
|
+
signal.removeEventListener('abort', cancelQueuedSubmission);
|
|
196
|
+
consumeSubmission().then(resolve, reject);
|
|
197
|
+
}
|
|
198
|
+
submissions.add(startSubmission);
|
|
199
|
+
signal.addEventListener(
|
|
200
|
+
'abort',
|
|
201
|
+
cancelQueuedSubmission,
|
|
202
|
+
{once: true}
|
|
203
|
+
);
|
|
204
|
+
if (signal.aborted) cancelQueuedSubmission();
|
|
205
|
+
else dispatchSubmissions();
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function observeRateLimit(response) {
|
|
211
|
+
if (response.status !== 429) return;
|
|
212
|
+
cooldownUntil = Math.max(
|
|
213
|
+
cooldownUntil,
|
|
214
|
+
Date.now() + Math.max(RATE_LIMIT_RETRY_DELAY_MS, retryAfterDelay(response) ?? 0)
|
|
215
|
+
);
|
|
216
|
+
dispatchSubmissions();
|
|
217
|
+
dispatchFollowUps();
|
|
218
|
+
}
|
|
219
|
+
|
|
130
220
|
function dispatchFollowUps() {
|
|
131
221
|
clearTimeout(followUpTimer);
|
|
132
222
|
followUpTimer = null;
|
|
@@ -136,18 +226,20 @@ export function createTwinCloudTTSProvider({
|
|
|
136
226
|
const eligibleIndex = followUps.contents.findIndex(
|
|
137
227
|
function eligibleFollowUp(task) {return task.eligibleAt <= now;}
|
|
138
228
|
);
|
|
139
|
-
|
|
229
|
+
const nextStart = Math.max(nextFollowUpStart, cooldownUntil);
|
|
230
|
+
if (now < nextStart || eligibleIndex === -1) {
|
|
140
231
|
const firstEligibility = followUps.contents.reduce(
|
|
141
232
|
function earliestFollowUp(time, task) {return Math.min(time, task.eligibleAt);},
|
|
142
233
|
Infinity
|
|
143
234
|
);
|
|
144
235
|
// Long Retry-After dates may exceed the platform timer's range.
|
|
145
236
|
// Revisit at that boundary without shortening the job's eligibility.
|
|
146
|
-
followUpTimer = setTimeout(dispatchFollowUps, Math.min(2147483647, Math.max(
|
|
237
|
+
followUpTimer = setTimeout(dispatchFollowUps, Math.min(2147483647, Math.max(nextStart, firstEligibility) - now));
|
|
147
238
|
return;
|
|
148
239
|
}
|
|
149
|
-
//
|
|
150
|
-
//
|
|
240
|
+
// Successful pending responses retain job-local Retry-After timing.
|
|
241
|
+
// HTTP 429 also applies the shared cooldown before any queued start.
|
|
242
|
+
// Queue owns pending tasks in FIFO order among eligible entries.
|
|
151
243
|
if (eligibleIndex > 0) {
|
|
152
244
|
const [eligibleTask] = followUps.contents.splice(eligibleIndex, 1);
|
|
153
245
|
followUps.contents.unshift(eligibleTask);
|
|
@@ -287,7 +379,7 @@ export function createTwinCloudTTSProvider({
|
|
|
287
379
|
localOnly: false,
|
|
288
380
|
state,
|
|
289
381
|
loaded,
|
|
290
|
-
busy:
|
|
382
|
+
busy: requests.size > 0,
|
|
291
383
|
execution: {
|
|
292
384
|
requestedDevice: 'remote',
|
|
293
385
|
selectedDevice: loaded ? 'remote' : null,
|
|
@@ -310,11 +402,12 @@ export function createTwinCloudTTSProvider({
|
|
|
310
402
|
// an identified job's status read can retry a network failure.
|
|
311
403
|
return {error, retryable: options.method === 'GET' && error instanceof TypeError};
|
|
312
404
|
}
|
|
405
|
+
assertNotAborted(signal);
|
|
406
|
+
observeRateLimit(response);
|
|
313
407
|
const nextEligibility = Date.now() + (retryAfterDelay(response) ?? 0);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
assertNotAborted(signal);
|
|
408
|
+
const error = await readResponseError(response, signal, operation);
|
|
409
|
+
assertNotAborted(signal);
|
|
410
|
+
if (error) {
|
|
318
411
|
return {error, retryable: response.status === 429, eligibleAt: nextEligibility};
|
|
319
412
|
}
|
|
320
413
|
// Parsing errors are not Fetch failures and must not replay a job.
|
|
@@ -325,7 +418,7 @@ export function createTwinCloudTTSProvider({
|
|
|
325
418
|
while (true) {
|
|
326
419
|
assertNotAborted(signal);
|
|
327
420
|
const outcome = options.method === 'POST'
|
|
328
|
-
? await readInferenceResponse
|
|
421
|
+
? await queueSubmission(readInferenceResponse, signal)
|
|
329
422
|
: await queueFollowUp(readInferenceResponse, signal, eligibleAt);
|
|
330
423
|
assertNotAborted(signal);
|
|
331
424
|
if (!outcome.error) return outcome;
|
|
@@ -333,11 +426,6 @@ export function createTwinCloudTTSProvider({
|
|
|
333
426
|
retryError = outcome.error;
|
|
334
427
|
console.warn(`Retrying the cloud speech ${operation} once.`, retryError);
|
|
335
428
|
eligibleAt = outcome.eligibleAt ?? 0;
|
|
336
|
-
if (options.method === 'POST') {
|
|
337
|
-
// A readable submission rejection retains its own bounded retry
|
|
338
|
-
// delay. It never delays another job or enters the GET queue.
|
|
339
|
-
await waitForStatus(Math.max(SUBMISSION_RETRY_DELAY_MS, eligibleAt - Date.now()), signal);
|
|
340
|
-
}
|
|
341
429
|
}
|
|
342
430
|
}
|
|
343
431
|
|
|
@@ -402,7 +490,11 @@ export function createTwinCloudTTSProvider({
|
|
|
402
490
|
const audio = await queueFollowUp(
|
|
403
491
|
async function downloadCloudSpeechAudio() {
|
|
404
492
|
const audioResponse = await fetchImpl(result.output.audio.url, {signal});
|
|
405
|
-
|
|
493
|
+
assertNotAborted(signal);
|
|
494
|
+
observeRateLimit(audioResponse);
|
|
495
|
+
const error = await readResponseError(audioResponse, signal, 'audio download');
|
|
496
|
+
assertNotAborted(signal);
|
|
497
|
+
if (error) throw error;
|
|
406
498
|
return await audioResponse.blob();
|
|
407
499
|
},
|
|
408
500
|
signal,
|
|
@@ -491,9 +583,6 @@ export function createTwinCloudTTSProvider({
|
|
|
491
583
|
if (state !== 'ready' || unloadOperation || disposeOperation) {
|
|
492
584
|
throw speechError('The cloud speech provider is not ready.', 'ARCANE_AI_NOT_READY');
|
|
493
585
|
}
|
|
494
|
-
if (activeRequests.size >= maxConcurrentRequests) {
|
|
495
|
-
throw speechError('The cloud speech provider is at capacity.', 'ARCANE_AI_PROVIDER_BUSY');
|
|
496
|
-
}
|
|
497
586
|
const payload = context.payload;
|
|
498
587
|
if (!is.string(payload?.input) || !payload.input.trim()) {
|
|
499
588
|
throw new TypeError('Cloud speech input must be a nonempty string.');
|
|
@@ -523,16 +612,16 @@ export function createTwinCloudTTSProvider({
|
|
|
523
612
|
);
|
|
524
613
|
if (context.signal?.aborted) cancelCloudSpeechRequest();
|
|
525
614
|
const request = {controller, promise: null};
|
|
526
|
-
|
|
527
|
-
request.promise = synthesize(payload, controller, generation);
|
|
615
|
+
requests.add(request);
|
|
528
616
|
try {
|
|
617
|
+
request.promise = queueSynthesis(request, payload, generation);
|
|
529
618
|
return await request.promise;
|
|
530
619
|
} catch (error) {
|
|
531
620
|
if (controller.signal.aborted) throw abortedSpeech(controller.signal);
|
|
532
621
|
throw error;
|
|
533
622
|
} finally {
|
|
534
623
|
context.signal?.removeEventListener('abort', cancelCloudSpeechRequest);
|
|
535
|
-
|
|
624
|
+
requests.delete(request);
|
|
536
625
|
}
|
|
537
626
|
},
|
|
538
627
|
|
|
@@ -546,11 +635,15 @@ export function createTwinCloudTTSProvider({
|
|
|
546
635
|
if (unloadOperation) return unloadOperation;
|
|
547
636
|
state = 'unloading';
|
|
548
637
|
generation += 1;
|
|
638
|
+
clearTimeout(submissionTimer);
|
|
639
|
+
submissionTimer = null;
|
|
640
|
+
clearTimeout(followUpTimer);
|
|
641
|
+
followUpTimer = null;
|
|
549
642
|
for (const cancelCredentialWait of [...credentialWaits]) cancelCredentialWait();
|
|
550
|
-
const
|
|
551
|
-
for (const request of
|
|
643
|
+
const pendingRequests = [...requests];
|
|
644
|
+
for (const request of pendingRequests) request.controller.abort('Cloud speech provider unloaded.');
|
|
552
645
|
unloadOperation = Promise.allSettled(
|
|
553
|
-
|
|
646
|
+
pendingRequests.map(
|
|
554
647
|
function pendingCloudSpeechRequest(request) {
|
|
555
648
|
return request.promise;
|
|
556
649
|
}
|
|
@@ -138,11 +138,13 @@ playback remain the only owners. Use
|
|
|
138
138
|
punctuation boundary should submit promptly. This is incremental playback of
|
|
139
139
|
completed synthesis jobs, not a live audio stream from TWiN Cloud.
|
|
140
140
|
|
|
141
|
-
The
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
The provider queues each complete synthesis job, including initial
|
|
142
|
+
`POST https://inference.do-ai.run/v1/async-invoke` and any eligible submission
|
|
143
|
+
retry. `maxConcurrentRequests` defaults to four whole jobs. Each slot remains
|
|
144
|
+
occupied through credential readiness, submission, queued/running status, and
|
|
145
|
+
the complete final audio response. Additional requests wait in order; cancelling
|
|
146
|
+
a waiting job removes it before credential reading or submission. This capacity
|
|
147
|
+
is separate from `followUpQueue.maxConcurrentRequests`.
|
|
146
148
|
|
|
147
149
|
One provider-local `js-queue` owns only follow-up status/result GETs and the final
|
|
148
150
|
audio GET. `followUpQueue` defaults to `{maxConcurrentRequests:4,intervalMs:250}`;
|
|
@@ -153,20 +155,31 @@ job awaits its current response and body before immediately enqueueing another
|
|
|
153
155
|
request only when incomplete or eligible for a retry. There is no additional
|
|
154
156
|
per-job polling delay.
|
|
155
157
|
|
|
156
|
-
A
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
A successful pending response's `Retry-After` changes that job's next eligibility.
|
|
159
|
+
A `429` applies a shared cooldown to all queued submission and follow-up starts,
|
|
160
|
+
using a one-second minimum and any longer readable `Retry-After`. A completely
|
|
161
|
+
readable `429` permits one retry of that submission or status read, within the
|
|
162
|
+
same whole-job slot. Already dispatched operations retain their response ownership.
|
|
160
163
|
An accepted job's status Fetch network failure may retry once for the same job;
|
|
161
|
-
|
|
164
|
+
Ambiguous POST network, JSON parsing, and error-body read failures never replay
|
|
165
|
+
the submission. Repeated failures surface their
|
|
162
166
|
complete diagnostics without an endless retry loop; a failed job does not cancel
|
|
163
167
|
siblings. Retry diagnostics remain in the developer console, outside conversation
|
|
164
168
|
history.
|
|
165
169
|
|
|
166
|
-
The
|
|
170
|
+
The provider-local queues are created when this cloud factory is called, not when the browser
|
|
167
171
|
speech entrypoint is imported. Local Kokoro and Whisper do not use this queue;
|
|
168
172
|
their setup, Workers, synthesis/transcription, and lifecycle remain independent.
|
|
169
173
|
|
|
174
|
+
Ordinary Kokoro descriptors may supply `model.voices` as complete display records,
|
|
175
|
+
for example `{id:'af_heart',name:'Heart',lang:'en-US'}`. The synchronous
|
|
176
|
+
`provider.catalog()[0].voices` preserves that array and its metadata without
|
|
177
|
+
loading the model or fetching a catalog. The application supplies the inventory
|
|
178
|
+
for its selected runtime; these records neither create a synthesis allow-list
|
|
179
|
+
nor establish that a voice has loaded or played. Ordinary Worker configuration
|
|
180
|
+
does not receive this display inventory. Existing artifact-graph voice paths
|
|
181
|
+
retain their separate meaning.
|
|
182
|
+
|
|
170
183
|
The SDK pins published `js-queue` through npm. Its browser Queue entry, original
|
|
171
184
|
package metadata, and license are projected unchanged by
|
|
172
185
|
`node tools/sync-js-queue.mjs` after an SDK dependency update. The existing runtime
|