arcane-os 0.40.0 → 0.41.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 +34 -0
- package/NOTICE +3 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-speech.mjs +4 -1
- package/browser-runtime/ai/digitalocean-speech.mjs +2 -468
- package/browser-runtime/ai/speech-worker-runtime.mjs +2 -3
- package/browser-runtime/ai/twin-cloud-speech.mjs +587 -0
- package/browser-runtime/dependencies/js-queue/licence.md +21 -0
- package/browser-runtime/dependencies/js-queue/package.json +100 -0
- package/browser-runtime/dependencies/js-queue/queue.js +78 -0
- package/docs/reference/ai/browser-speech.md +60 -14
- package/docs/reference/inventory/package-api.json +18 -4
- package/docs/reference/pwa.md +16 -1
- package/docs/reference/runtime-components.md +14 -4
- package/docs/reference/sdk-api.md +75 -17
- package/package.json +2 -1
- package/runtime/arcane/components/speech.html +17 -18
- package/src/dev-server.mjs +19 -11
- package/src/import-map.mjs +20 -1
- package/src/packager/core.mjs +8 -5
- package/src/toolchain.mjs +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.41.0
|
|
4
|
+
|
|
5
|
+
- Add `createTwinCloudTTSProvider` through `arcane-os/ai/browser-speech`,
|
|
6
|
+
retaining `createDigitalOceanFalTTSProvider` as the identical compatibility
|
|
7
|
+
function. Existing provider IDs, credentials, models, voices and endpoints
|
|
8
|
+
remain unchanged; no saved-data migration is performed.
|
|
9
|
+
- Submit cloud synthesis jobs immediately through the existing whole-job
|
|
10
|
+
capacity owner. Queue only follow-up status and audio GET operations through
|
|
11
|
+
one provider-local published `js-queue`, with independently configurable
|
|
12
|
+
`followUpQueue: {maxConcurrentRequests: 4, intervalMs: 250}` defaults.
|
|
13
|
+
Follow-up slots remain occupied through complete response-body consumption,
|
|
14
|
+
with spacing between actual dispatch starts and ordered same-job requeue.
|
|
15
|
+
- Preserve per-job Retry-After eligibility, bounded retries, cancellation and
|
|
16
|
+
unload without delaying eligible siblings or replaying ambiguous paid
|
|
17
|
+
submissions. Existing ordered playback, enabled speech intent and local
|
|
18
|
+
Whisper/Kokoro provider paths remain unchanged.
|
|
19
|
+
|
|
20
|
+
## 0.40.1
|
|
21
|
+
|
|
22
|
+
- Include complete selected local resource URL variants in generated static
|
|
23
|
+
PWA inventories. Reuse the source resource graph, resolve shared component
|
|
24
|
+
styles and modules against their runtime root, and retain speech worker mode
|
|
25
|
+
queries. Application selection, source content and output locations remain
|
|
26
|
+
unchanged; regenerate through the public import-map workflow to receive the
|
|
27
|
+
corrected offline inventory.
|
|
28
|
+
- Space DigitalOcean FAL inference request starts by one second while retaining
|
|
29
|
+
four concurrent jobs and ordered playback. Retry a readable rate-limit
|
|
30
|
+
rejection once, honoring longer readable Retry-After values, and retry a
|
|
31
|
+
same-job status network failure once. An ambiguous submission network failure
|
|
32
|
+
is surfaced without submitting another potentially paid job.
|
|
33
|
+
- Preserve the user's enabled speech intent after a synthesis, playback or
|
|
34
|
+
provider lifecycle failure. A failed job leaves unrelated queued jobs intact;
|
|
35
|
+
explicit Retry, Load, Mute and Cancel remain user-controlled.
|
|
36
|
+
|
|
3
37
|
## 0.40.0
|
|
4
38
|
|
|
5
39
|
- Add optional `arcane-package.json` `outputDirectory` to select the final
|
package/NOTICE
CHANGED
|
@@ -8,6 +8,9 @@ informational notice and is not itself a grant of commercial rights.
|
|
|
8
8
|
Third-party material retains its own terms:
|
|
9
9
|
|
|
10
10
|
- event-pubsub 6.1.0: MIT License; see its installed `licence` file.
|
|
11
|
+
- js-queue 3.1.1: MIT License; see browser-runtime/dependencies/js-queue/licence.md.
|
|
12
|
+
- js-queue's easy-stack 2.1.0 dependency: MIT License; see its installed
|
|
13
|
+
`licence` file. The browser Queue entry does not import this stack module.
|
|
11
14
|
- node-http-server 10.0.0: MIT License; see its installed `licence` file.
|
|
12
15
|
- strong-type 2.0.1: MIT License; see its installed `licence` file.
|
|
13
16
|
- vanilla-test 2.1.3: MIT License; see its installed `licence` file.
|
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.41.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
|
|
|
@@ -10,4 +10,7 @@ export {
|
|
|
10
10
|
createBrowserKokoroProvider,
|
|
11
11
|
createBrowserWhisperProvider,
|
|
12
12
|
} from "./browser-speech-providers.mjs";
|
|
13
|
-
export {
|
|
13
|
+
export {
|
|
14
|
+
createTwinCloudTTSProvider,
|
|
15
|
+
createTwinCloudTTSProvider as createDigitalOceanFalTTSProvider,
|
|
16
|
+
} from "./twin-cloud-speech.mjs";
|
|
@@ -1,468 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const is = new Is(false);
|
|
4
|
-
const INVOKE_URL = 'https://inference.do-ai.run/v1/async-invoke';
|
|
5
|
-
const POLL_INTERVAL_MS = 1000;
|
|
6
|
-
|
|
7
|
-
function speechError(message, code, cause) {
|
|
8
|
-
const error = cause === undefined
|
|
9
|
-
? new Error(message)
|
|
10
|
-
: new Error(
|
|
11
|
-
message,
|
|
12
|
-
{cause}
|
|
13
|
-
);
|
|
14
|
-
error.code = code;
|
|
15
|
-
return error;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function abortedSpeech(signal) {
|
|
19
|
-
const error = speechError(
|
|
20
|
-
'The cloud speech request was cancelled.',
|
|
21
|
-
'ARCANE_AI_REQUEST_ABORTED',
|
|
22
|
-
signal?.reason
|
|
23
|
-
);
|
|
24
|
-
error.name = 'AbortError';
|
|
25
|
-
return error;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function assertNotAborted(signal) {
|
|
29
|
-
if (signal?.aborted) throw abortedSpeech(signal);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function assertIdentifier(value, label) {
|
|
33
|
-
if (!is.string(value) || !value || value.trim() !== value) {
|
|
34
|
-
throw new TypeError(`${label} must be a nonempty string without surrounding whitespace.`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function waitForStatus(delay, signal) {
|
|
39
|
-
assertNotAborted(signal);
|
|
40
|
-
return new Promise(
|
|
41
|
-
function waitForCloudSpeechStatus(resolve, reject) {
|
|
42
|
-
const timer = setTimeout(finishStatusWait, delay);
|
|
43
|
-
function finishStatusWait() {
|
|
44
|
-
signal.removeEventListener('abort', cancelStatusWait);
|
|
45
|
-
resolve();
|
|
46
|
-
}
|
|
47
|
-
function cancelStatusWait() {
|
|
48
|
-
clearTimeout(timer);
|
|
49
|
-
signal.removeEventListener('abort', cancelStatusWait);
|
|
50
|
-
reject(abortedSpeech(signal));
|
|
51
|
-
}
|
|
52
|
-
signal.addEventListener(
|
|
53
|
-
'abort',
|
|
54
|
-
cancelStatusWait,
|
|
55
|
-
{once: true}
|
|
56
|
-
);
|
|
57
|
-
if (signal.aborted) cancelStatusWait();
|
|
58
|
-
}
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function retryAfterDelay(response) {
|
|
63
|
-
const value = response.headers.get('Retry-After');
|
|
64
|
-
if (!value?.trim()) return null;
|
|
65
|
-
const seconds = Number(value);
|
|
66
|
-
if (is.finite(seconds) && seconds >= 0) return seconds * 1000;
|
|
67
|
-
const date = Date.parse(value);
|
|
68
|
-
return is.finite(date) ? Math.max(0, date - Date.now()) : null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function requireSuccessfulResponse(response, signal, operation) {
|
|
72
|
-
assertNotAborted(signal);
|
|
73
|
-
if (response.ok) return response;
|
|
74
|
-
const detail = await response.text();
|
|
75
|
-
assertNotAborted(signal);
|
|
76
|
-
throw speechError(
|
|
77
|
-
`DigitalOcean speech ${operation} failed with HTTP ${response.status}${detail ? `: ${detail}` : '.'}`,
|
|
78
|
-
'ARCANE_AI_CLOUD_SPEECH_HTTP_ERROR'
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* A remote provider/2 TTS adapter. The application selects the model and voice,
|
|
84
|
-
* and getApiKey returns its current credential or the application's existing
|
|
85
|
-
* refresh promise. Credentials never enter status or configuration. The runtime
|
|
86
|
-
* owns the FIFO queue; cancellation stops only this adapter's credential wait.
|
|
87
|
-
*/
|
|
88
|
-
export function createDigitalOceanFalTTSProvider({
|
|
89
|
-
id,
|
|
90
|
-
model,
|
|
91
|
-
getApiKey,
|
|
92
|
-
maxConcurrentRequests = 4,
|
|
93
|
-
fetch: fetchImpl = globalThis.fetch
|
|
94
|
-
} = {}) {
|
|
95
|
-
assertIdentifier(id, 'DigitalOcean speech provider id');
|
|
96
|
-
assertIdentifier(model?.id, 'DigitalOcean speech model id');
|
|
97
|
-
assertIdentifier(model?.defaultVoice, 'DigitalOcean speech default voice');
|
|
98
|
-
if (!is.function(getApiKey)) {
|
|
99
|
-
throw new TypeError('DigitalOcean speech getApiKey must be a function.');
|
|
100
|
-
}
|
|
101
|
-
if (!is.function(fetchImpl)) {
|
|
102
|
-
throw new TypeError('DigitalOcean speech requires Fetch.');
|
|
103
|
-
}
|
|
104
|
-
if (!is.safeInteger(maxConcurrentRequests) || maxConcurrentRequests < 1) {
|
|
105
|
-
throw new RangeError('DigitalOcean speech maxConcurrentRequests must be a positive safe integer.');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const modelId = model.id;
|
|
109
|
-
const defaultVoice = model.defaultVoice;
|
|
110
|
-
const activeRequests = new Set();
|
|
111
|
-
const credentialWaits = new Set();
|
|
112
|
-
let state = 'unloaded';
|
|
113
|
-
let generation = 0;
|
|
114
|
-
let unloadOperation = null;
|
|
115
|
-
let disposeOperation = null;
|
|
116
|
-
|
|
117
|
-
function authority() {
|
|
118
|
-
return {
|
|
119
|
-
protocol: 'arcane-ai-model-authority/1',
|
|
120
|
-
providerId: id,
|
|
121
|
-
modelId,
|
|
122
|
-
role: 'tts',
|
|
123
|
-
localOnly: false
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function readApiKey(signal) {
|
|
128
|
-
assertNotAborted(signal);
|
|
129
|
-
return new Promise(
|
|
130
|
-
function waitForCloudSpeechCredential(resolve, reject) {
|
|
131
|
-
let settled = false;
|
|
132
|
-
function finishCredentialWait() {
|
|
133
|
-
settled = true;
|
|
134
|
-
credentialWaits.delete(cancelCredentialWait);
|
|
135
|
-
signal?.removeEventListener('abort', cancelCredentialWait);
|
|
136
|
-
}
|
|
137
|
-
function rejectCredential(error) {
|
|
138
|
-
if (settled) return;
|
|
139
|
-
finishCredentialWait();
|
|
140
|
-
reject(error);
|
|
141
|
-
}
|
|
142
|
-
function acceptCredential(apiKey) {
|
|
143
|
-
if (settled) return;
|
|
144
|
-
if (!is.string(apiKey) || !apiKey.trim()) {
|
|
145
|
-
rejectCredential(
|
|
146
|
-
speechError(
|
|
147
|
-
'A DigitalOcean inference access key is required for cloud speech.',
|
|
148
|
-
'ARCANE_AI_CLOUD_SPEECH_KEY_REQUIRED'
|
|
149
|
-
)
|
|
150
|
-
);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
finishCredentialWait();
|
|
154
|
-
resolve(apiKey);
|
|
155
|
-
}
|
|
156
|
-
function cancelCredentialWait() {
|
|
157
|
-
rejectCredential(abortedSpeech(signal));
|
|
158
|
-
}
|
|
159
|
-
credentialWaits.add(cancelCredentialWait);
|
|
160
|
-
signal?.addEventListener(
|
|
161
|
-
'abort',
|
|
162
|
-
cancelCredentialWait,
|
|
163
|
-
{once: true}
|
|
164
|
-
);
|
|
165
|
-
if (signal?.aborted) {
|
|
166
|
-
cancelCredentialWait();
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
try {
|
|
170
|
-
Promise.resolve(getApiKey()).then(acceptCredential, rejectCredential);
|
|
171
|
-
} catch (error) {
|
|
172
|
-
rejectCredential(error);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function assertSelection(selection) {
|
|
179
|
-
if (selection && (selection.providerId !== id
|
|
180
|
-
|| selection.modelId !== modelId
|
|
181
|
-
|| selection.localOnly === true
|
|
182
|
-
|| (selection.role !== undefined && selection.role !== 'tts'))) {
|
|
183
|
-
throw speechError(
|
|
184
|
-
'The selected cloud speech model does not match this provider.',
|
|
185
|
-
'ARCANE_AI_MODEL_AUTHORITY_REQUIRED'
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function assertContext(context) {
|
|
191
|
-
if (context.role !== undefined && context.role !== 'tts') {
|
|
192
|
-
throw speechError('This provider supports only TTS.', 'ARCANE_AI_INVALID_REQUEST');
|
|
193
|
-
}
|
|
194
|
-
assertSelection(context.selection);
|
|
195
|
-
assertNotAborted(context.signal);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function status() {
|
|
199
|
-
const loaded = state === 'ready';
|
|
200
|
-
return {
|
|
201
|
-
providerId: id,
|
|
202
|
-
role: 'tts',
|
|
203
|
-
modelId,
|
|
204
|
-
localOnly: false,
|
|
205
|
-
state,
|
|
206
|
-
loaded,
|
|
207
|
-
busy: activeRequests.size > 0,
|
|
208
|
-
execution: {
|
|
209
|
-
requestedDevice: 'remote',
|
|
210
|
-
selectedDevice: loaded ? 'remote' : null,
|
|
211
|
-
maxConcurrentRequests,
|
|
212
|
-
activeRequestCount: activeRequests.size
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function synthesize(payload, controller, requestGeneration) {
|
|
218
|
-
const signal = controller.signal;
|
|
219
|
-
const apiKey = await readApiKey(signal);
|
|
220
|
-
assertNotAborted(signal);
|
|
221
|
-
const headers = {Authorization: `Bearer ${apiKey}`};
|
|
222
|
-
const response = await fetchImpl(
|
|
223
|
-
INVOKE_URL,
|
|
224
|
-
{
|
|
225
|
-
method: 'POST',
|
|
226
|
-
headers: {...headers, 'Content-Type': 'application/json'},
|
|
227
|
-
body: JSON.stringify(
|
|
228
|
-
{
|
|
229
|
-
model_id: modelId,
|
|
230
|
-
input: {
|
|
231
|
-
text: payload.input,
|
|
232
|
-
voice: payload.voice ?? defaultVoice,
|
|
233
|
-
speed: payload.speed ?? 1
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
),
|
|
237
|
-
signal
|
|
238
|
-
}
|
|
239
|
-
);
|
|
240
|
-
await requireSuccessfulResponse(response, signal, 'submission');
|
|
241
|
-
let result = await response.json();
|
|
242
|
-
assertNotAborted(signal);
|
|
243
|
-
const requestId = result?.request_id;
|
|
244
|
-
while (result?.status === 'QUEUED' || result?.status === 'IN_PROGRESS') {
|
|
245
|
-
if (!is.string(requestId) || !requestId) {
|
|
246
|
-
throw speechError(
|
|
247
|
-
'DigitalOcean speech returned a pending job without a request_id.',
|
|
248
|
-
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
await waitForStatus(POLL_INTERVAL_MS, signal);
|
|
252
|
-
let statusResponse;
|
|
253
|
-
do {
|
|
254
|
-
statusResponse = await fetchImpl(
|
|
255
|
-
`${INVOKE_URL}/${encodeURIComponent(requestId)}`,
|
|
256
|
-
{method: 'GET', headers, signal}
|
|
257
|
-
);
|
|
258
|
-
assertNotAborted(signal);
|
|
259
|
-
const retryDelay = statusResponse.status === 429
|
|
260
|
-
? retryAfterDelay(statusResponse)
|
|
261
|
-
: null;
|
|
262
|
-
if (retryDelay === null) break;
|
|
263
|
-
// Only status reads may be retried; submitting twice can create two paid jobs.
|
|
264
|
-
await statusResponse.text();
|
|
265
|
-
await waitForStatus(retryDelay, signal);
|
|
266
|
-
} while (true);
|
|
267
|
-
await requireSuccessfulResponse(statusResponse, signal, 'status read');
|
|
268
|
-
result = await statusResponse.json();
|
|
269
|
-
assertNotAborted(signal);
|
|
270
|
-
}
|
|
271
|
-
if (result?.status === 'FAILED') {
|
|
272
|
-
throw speechError(
|
|
273
|
-
`DigitalOcean speech generation failed: ${JSON.stringify(result)}`,
|
|
274
|
-
'ARCANE_AI_CLOUD_SPEECH_GENERATION_FAILED'
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
if (result?.status !== 'COMPLETED' || !is.string(result.output?.audio?.url)
|
|
278
|
-
|| !result.output.audio.url) {
|
|
279
|
-
throw speechError(
|
|
280
|
-
`DigitalOcean speech returned an unsupported result: ${JSON.stringify(result)}`,
|
|
281
|
-
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
assertNotAborted(signal);
|
|
285
|
-
// The returned media URL has its own delivery authority; never forward the inference key.
|
|
286
|
-
const audioResponse = await fetchImpl(
|
|
287
|
-
result.output.audio.url,
|
|
288
|
-
{signal}
|
|
289
|
-
);
|
|
290
|
-
await requireSuccessfulResponse(audioResponse, signal, 'audio download');
|
|
291
|
-
const audio = await audioResponse.blob();
|
|
292
|
-
assertNotAborted(signal);
|
|
293
|
-
if (generation !== requestGeneration || state !== 'ready') {
|
|
294
|
-
throw speechError(
|
|
295
|
-
'The cloud speech result was superseded by a provider transition.',
|
|
296
|
-
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
return audio;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const provider = {
|
|
303
|
-
protocol: 'arcane-ai-provider/2',
|
|
304
|
-
role: 'tts',
|
|
305
|
-
id,
|
|
306
|
-
localOnly: false,
|
|
307
|
-
maxConcurrentRequests,
|
|
308
|
-
|
|
309
|
-
catalog() {
|
|
310
|
-
return [
|
|
311
|
-
{
|
|
312
|
-
id: modelId,
|
|
313
|
-
providerId: id,
|
|
314
|
-
role: 'tts',
|
|
315
|
-
localOnly: false,
|
|
316
|
-
defaultVoice,
|
|
317
|
-
speech: {responseFormats: ['provider-native'], defaultResponseFormat: 'provider-native'}
|
|
318
|
-
}
|
|
319
|
-
];
|
|
320
|
-
},
|
|
321
|
-
|
|
322
|
-
async inspect(selection, context = {}) {
|
|
323
|
-
assertContext(context);
|
|
324
|
-
if (state === 'disposed' || disposeOperation) {
|
|
325
|
-
return {
|
|
326
|
-
available: false,
|
|
327
|
-
code: 'ARCANE_AI_PROVIDER_DISPOSED',
|
|
328
|
-
message: 'The cloud speech provider is disposed.'
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
assertSelection(selection);
|
|
332
|
-
const inspectionGeneration = generation;
|
|
333
|
-
await readApiKey(context.signal);
|
|
334
|
-
assertNotAborted(context.signal);
|
|
335
|
-
if (generation !== inspectionGeneration) {
|
|
336
|
-
throw speechError('Cloud speech inspection was superseded.', 'ARCANE_AI_OPERATION_SUPERSEDED');
|
|
337
|
-
}
|
|
338
|
-
return {available: true, authority: authority()};
|
|
339
|
-
},
|
|
340
|
-
|
|
341
|
-
status,
|
|
342
|
-
|
|
343
|
-
async load(context = {}) {
|
|
344
|
-
assertContext(context);
|
|
345
|
-
if (state === 'disposed' || disposeOperation) {
|
|
346
|
-
throw speechError('The cloud speech provider is disposed.', 'ARCANE_AI_PROVIDER_DISPOSED');
|
|
347
|
-
}
|
|
348
|
-
if (unloadOperation) {
|
|
349
|
-
throw speechError('The cloud speech provider is unloading.', 'ARCANE_AI_OPERATION_SUPERSEDED');
|
|
350
|
-
}
|
|
351
|
-
const loadGeneration = generation;
|
|
352
|
-
if (state !== 'ready') state = 'loading';
|
|
353
|
-
try {
|
|
354
|
-
await readApiKey(context.signal);
|
|
355
|
-
assertNotAborted(context.signal);
|
|
356
|
-
if (generation !== loadGeneration) {
|
|
357
|
-
throw speechError('Cloud speech activation was superseded.', 'ARCANE_AI_OPERATION_SUPERSEDED');
|
|
358
|
-
}
|
|
359
|
-
} catch (error) {
|
|
360
|
-
if (generation === loadGeneration && state === 'loading') state = 'unloaded';
|
|
361
|
-
throw error;
|
|
362
|
-
}
|
|
363
|
-
state = 'ready';
|
|
364
|
-
return status();
|
|
365
|
-
},
|
|
366
|
-
|
|
367
|
-
async request(context = {}) {
|
|
368
|
-
assertContext(context);
|
|
369
|
-
if (context.role !== 'tts' || context.operation !== 'synthesize') {
|
|
370
|
-
throw speechError('Cloud speech supports only TTS synthesis.', 'ARCANE_AI_INVALID_REQUEST');
|
|
371
|
-
}
|
|
372
|
-
if (state !== 'ready' || unloadOperation || disposeOperation) {
|
|
373
|
-
throw speechError('The cloud speech provider is not ready.', 'ARCANE_AI_NOT_READY');
|
|
374
|
-
}
|
|
375
|
-
if (activeRequests.size >= maxConcurrentRequests) {
|
|
376
|
-
throw speechError('The cloud speech provider is at capacity.', 'ARCANE_AI_PROVIDER_BUSY');
|
|
377
|
-
}
|
|
378
|
-
const payload = context.payload;
|
|
379
|
-
if (!is.string(payload?.input) || !payload.input.trim()) {
|
|
380
|
-
throw new TypeError('Cloud speech input must be a nonempty string.');
|
|
381
|
-
}
|
|
382
|
-
if (payload.model !== undefined && payload.model !== modelId) {
|
|
383
|
-
throw speechError('The requested cloud speech model differs from the selection.', 'ARCANE_AI_MODEL_AUTHORITY_REQUIRED');
|
|
384
|
-
}
|
|
385
|
-
if (payload.voice !== undefined) assertIdentifier(payload.voice, 'Cloud speech voice');
|
|
386
|
-
if (payload.responseFormat !== undefined && payload.responseFormat !== 'provider-native') {
|
|
387
|
-
throw speechError('Cloud speech returns the provider-native audio format.', 'ARCANE_AI_UNSUPPORTED_RESPONSE_FORMAT');
|
|
388
|
-
}
|
|
389
|
-
const speed = payload.speed ?? 1;
|
|
390
|
-
if (!is.finite(speed) || speed < 0.7 || speed > 1.2) {
|
|
391
|
-
const error = new RangeError('Cloud speech speed must be from 0.7 through 1.2.');
|
|
392
|
-
error.code = 'ARCANE_AI_TTS_SPEED_INVALID';
|
|
393
|
-
throw error;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const controller = new AbortController();
|
|
397
|
-
function cancelCloudSpeechRequest() {
|
|
398
|
-
controller.abort(context.signal.reason);
|
|
399
|
-
}
|
|
400
|
-
context.signal?.addEventListener(
|
|
401
|
-
'abort',
|
|
402
|
-
cancelCloudSpeechRequest,
|
|
403
|
-
{once: true}
|
|
404
|
-
);
|
|
405
|
-
if (context.signal?.aborted) cancelCloudSpeechRequest();
|
|
406
|
-
const request = {controller, promise: null};
|
|
407
|
-
activeRequests.add(request);
|
|
408
|
-
request.promise = synthesize(payload, controller, generation);
|
|
409
|
-
try {
|
|
410
|
-
return await request.promise;
|
|
411
|
-
} catch (error) {
|
|
412
|
-
if (controller.signal.aborted) throw abortedSpeech(controller.signal);
|
|
413
|
-
throw error;
|
|
414
|
-
} finally {
|
|
415
|
-
context.signal?.removeEventListener('abort', cancelCloudSpeechRequest);
|
|
416
|
-
activeRequests.delete(request);
|
|
417
|
-
}
|
|
418
|
-
},
|
|
419
|
-
|
|
420
|
-
unload(context = {}) {
|
|
421
|
-
assertContext(context);
|
|
422
|
-
if (state === 'disposed') {
|
|
423
|
-
return Promise.resolve(
|
|
424
|
-
status()
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
if (unloadOperation) return unloadOperation;
|
|
428
|
-
state = 'unloading';
|
|
429
|
-
generation += 1;
|
|
430
|
-
for (const cancelCredentialWait of [...credentialWaits]) cancelCredentialWait();
|
|
431
|
-
const requests = [...activeRequests];
|
|
432
|
-
for (const request of requests) request.controller.abort('Cloud speech provider unloaded.');
|
|
433
|
-
unloadOperation = Promise.allSettled(
|
|
434
|
-
requests.map(
|
|
435
|
-
function pendingCloudSpeechRequest(request) {
|
|
436
|
-
return request.promise;
|
|
437
|
-
}
|
|
438
|
-
)
|
|
439
|
-
).then(
|
|
440
|
-
function finishCloudSpeechUnload() {
|
|
441
|
-
state = 'unloaded';
|
|
442
|
-
unloadOperation = null;
|
|
443
|
-
return status();
|
|
444
|
-
}
|
|
445
|
-
);
|
|
446
|
-
return unloadOperation;
|
|
447
|
-
},
|
|
448
|
-
|
|
449
|
-
dispose(context = {}) {
|
|
450
|
-
assertContext(context);
|
|
451
|
-
if (state === 'disposed') {
|
|
452
|
-
return Promise.resolve(
|
|
453
|
-
status()
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
if (disposeOperation) return disposeOperation;
|
|
457
|
-
disposeOperation = provider.unload(context).then(
|
|
458
|
-
function finishCloudSpeechDispose() {
|
|
459
|
-
state = 'disposed';
|
|
460
|
-
disposeOperation = null;
|
|
461
|
-
return status();
|
|
462
|
-
}
|
|
463
|
-
);
|
|
464
|
-
return disposeOperation;
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
return provider;
|
|
468
|
-
}
|
|
1
|
+
// Published compatibility name; the TWiN Cloud provider owns the implementation.
|
|
2
|
+
export {createTwinCloudTTSProvider as createDigitalOceanFalTTSProvider} from './twin-cloud-speech.mjs';
|
|
@@ -844,11 +844,10 @@ function validateConfiguration(configuration, role) {
|
|
|
844
844
|
function nestedWorkerUrl(role) {
|
|
845
845
|
const url = new URL(
|
|
846
846
|
role === "stt"
|
|
847
|
-
? "./browser-whisper-worker.mjs"
|
|
848
|
-
: "./browser-kokoro-worker.mjs",
|
|
847
|
+
? "./browser-whisper-worker.mjs?arcaneSpeechWorkerMode=artifact-module-worker"
|
|
848
|
+
: "./browser-kokoro-worker.mjs?arcaneSpeechWorkerMode=artifact-module-worker",
|
|
849
849
|
import.meta.url,
|
|
850
850
|
);
|
|
851
|
-
url.searchParams.set("arcaneSpeechWorkerMode", "artifact-module-worker");
|
|
852
851
|
return url;
|
|
853
852
|
}
|
|
854
853
|
|