@pie-players/tts-client-server 0.3.72 → 0.3.74
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/README.md +28 -22
- package/dist/ServerTTSProvider.d.ts +11 -2
- package/dist/ServerTTSProvider.js +60 -29
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -39,27 +39,20 @@ const ttsService = new TTSService();
|
|
|
39
39
|
await ttsService.initialize(provider, {
|
|
40
40
|
apiEndpoint: '/api/tts', // Your SvelteKit API route
|
|
41
41
|
provider: 'polly', // Server-side provider to use
|
|
42
|
-
|
|
42
|
+
voice: 'Joanna',
|
|
43
43
|
language: 'en-US',
|
|
44
44
|
});
|
|
45
|
-
|
|
46
|
-
// Note: `apiEndpoint`, `provider`, and `voiceId` are server-provider-specific
|
|
47
|
-
// options. When using TTSConfig, pass these inside `providerOptions` rather
|
|
48
|
-
// than as top-level fields:
|
|
49
|
-
//
|
|
50
|
-
// const config: TTSConfig = {
|
|
51
|
-
// providerOptions: {
|
|
52
|
-
// apiEndpoint: '/api/tts',
|
|
53
|
-
// provider: 'polly',
|
|
54
|
-
// voiceId: 'Joanna',
|
|
55
|
-
// },
|
|
56
|
-
// language: 'en-US',
|
|
57
|
-
// };
|
|
58
45
|
```
|
|
59
46
|
|
|
60
|
-
`apiEndpoint`
|
|
61
|
-
|
|
62
|
-
|
|
47
|
+
`apiEndpoint`, `provider`, `voice` and `language` are top-level fields; the
|
|
48
|
+
provider reads none of them from `providerOptions`. `providerOptions` carries
|
|
49
|
+
request extras: the PIE transport forwards its `engine` (when no top-level
|
|
50
|
+
`engine` is set), `sampleRate`, `format` and `speechMarkTypes`, and the custom
|
|
51
|
+
transport reads its `speedRate`.
|
|
52
|
+
|
|
53
|
+
`apiEndpoint` is required when using `ServerTTSProvider` directly, and
|
|
54
|
+
`initialize` throws without it. Toolkit-level `tools.providers.textToSpeech`
|
|
55
|
+
integration defaults it to `/api/tts` for server-backed backends.
|
|
63
56
|
|
|
64
57
|
### With Authentication
|
|
65
58
|
|
|
@@ -67,8 +60,7 @@ server-backed backends, but direct client usage should pass this explicitly.
|
|
|
67
60
|
await ttsService.initialize(provider, {
|
|
68
61
|
apiEndpoint: '/api/tts',
|
|
69
62
|
provider: 'polly',
|
|
70
|
-
authToken: 'your-jwt-token',
|
|
71
|
-
organizationId: 'org-123',
|
|
63
|
+
authToken: 'your-jwt-token', // sent as `Authorization: Bearer <token>`
|
|
72
64
|
});
|
|
73
65
|
```
|
|
74
66
|
|
|
@@ -140,10 +132,14 @@ await ttsService.speak('Hello world, this is a test.', {
|
|
|
140
132
|
"text": "Hello world",
|
|
141
133
|
"speedRate": "medium",
|
|
142
134
|
"lang_id": "en-US",
|
|
135
|
+
"langId": "en-US",
|
|
143
136
|
"cache": true
|
|
144
137
|
}
|
|
145
138
|
```
|
|
146
139
|
|
|
140
|
+
The language travels under both `lang_id` and `langId`, so a server that binds
|
|
141
|
+
JSON in camelCase reads it without a mapping of its own.
|
|
142
|
+
|
|
147
143
|
### Custom mode response
|
|
148
144
|
|
|
149
145
|
```json
|
|
@@ -157,7 +153,7 @@ Speech marks are fetched from the `word` URL and parsed as JSONL.
|
|
|
157
153
|
|
|
158
154
|
### Optional voices endpoint
|
|
159
155
|
|
|
160
|
-
`GET ${apiEndpoint}/voices` remains optional and is only used when endpoint validation is configured with `endpointValidationMode: "voices"`.
|
|
156
|
+
`GET ${apiEndpoint}/voices` remains optional and is only used when endpoint validation is configured with `endpointValidationMode: "voices"`. With `provider: "polly"` or `"google"` the probe tries `${apiEndpoint}/<provider>/voices` first and falls back to `${apiEndpoint}/voices` when that route returns 404. A failed probe makes the toolkit switch to browser TTS.
|
|
161
157
|
|
|
162
158
|
## SvelteKit Implementation Example
|
|
163
159
|
|
|
@@ -181,15 +177,18 @@ interface ServerTTSProviderConfig {
|
|
|
181
177
|
transportMode?: 'pie' | 'custom';
|
|
182
178
|
endpointMode?: 'synthesizePath' | 'rootPost';
|
|
183
179
|
endpointValidationMode?: 'voices' | 'endpoint' | 'none';
|
|
180
|
+
validateEndpoint?: boolean; // Probe the endpoint during initialize()
|
|
184
181
|
authToken?: string; // JWT or API key
|
|
185
182
|
includeAuthOnAssetFetch?: boolean;
|
|
186
183
|
assetOrigins?: string[]; // Trusted origins for Authorization header
|
|
187
|
-
|
|
184
|
+
credentials?: 'omit' | 'same-origin' | 'include'; // Fetch credentials mode
|
|
188
185
|
headers?: Record<string, string>; // Custom headers
|
|
189
|
-
|
|
186
|
+
voice?: string; // Voice ID, e.g. 'Joanna'
|
|
187
|
+
engine?: 'standard' | 'neural'; // Polly engine
|
|
190
188
|
language?: string; // Language code
|
|
191
189
|
rate?: number; // Speech rate (0.25-4.0)
|
|
192
190
|
volume?: number; // Volume (0-1)
|
|
191
|
+
providerOptions?: Record<string, unknown>; // Request extras (see Basic Setup)
|
|
193
192
|
}
|
|
194
193
|
```
|
|
195
194
|
|
|
@@ -218,6 +217,13 @@ server that fall outside a trusted origin set.
|
|
|
218
217
|
origins in `assetOrigins`; off-allow-list origins are fetched without
|
|
219
218
|
auth. Leave at the default unless your CDN / storage backend actually
|
|
220
219
|
requires the bearer token to read assets.
|
|
220
|
+
- **`credentials`** — the fetch `credentials` mode, unset by default, which
|
|
221
|
+
leaves the browser default (`"same-origin"`). Set `"include"` when the TTS
|
|
222
|
+
server sits on another origin and authenticates by cookie; the server must
|
|
223
|
+
answer CORS with `Access-Control-Allow-Credentials: true` and an explicit
|
|
224
|
+
origin. Speech-mark and audio fetches to an origin outside `assetOrigins`
|
|
225
|
+
keep the browser default, so cookies follow the same allow-list as the
|
|
226
|
+
bearer token.
|
|
221
227
|
|
|
222
228
|
See
|
|
223
229
|
[`docs/tools-and-accomodations/tool_host_contract.md#backend-endpoints-for-tool-providers`](../../docs/tools-and-accomodations/tool_host_contract.md#backend-endpoints-for-tool-providers)
|
|
@@ -44,8 +44,9 @@ export interface ServerTTSProviderConfig extends TTSConfig {
|
|
|
44
44
|
*/
|
|
45
45
|
endpointValidationMode?: "voices" | "endpoint" | "none";
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* Send the synthesis request's `Authorization` header, whether it comes from
|
|
48
|
+
* `headers` or `authToken`, with the custom-transport speech-mark and audio
|
|
49
|
+
* fetches. Defaults to false for compatibility.
|
|
49
50
|
*/
|
|
50
51
|
includeAuthOnAssetFetch?: boolean;
|
|
51
52
|
/**
|
|
@@ -59,6 +60,14 @@ export interface ServerTTSProviderConfig extends TTSConfig {
|
|
|
59
60
|
* responses keep working without explicit configuration.
|
|
60
61
|
*/
|
|
61
62
|
assetOrigins?: string[];
|
|
63
|
+
/**
|
|
64
|
+
* Fetch `credentials` mode for requests to the TTS server, so a
|
|
65
|
+
* cookie-authenticated endpoint on another origin receives its cookies.
|
|
66
|
+
* Speech-mark and audio fetches use it only for an origin trusted with auth
|
|
67
|
+
* (see `assetOrigins`); others keep the browser default. Unset leaves every
|
|
68
|
+
* fetch on the browser default, `"same-origin"`.
|
|
69
|
+
*/
|
|
70
|
+
credentials?: "omit" | "same-origin" | "include";
|
|
62
71
|
/**
|
|
63
72
|
* Validate API endpoint availability during initialization (slower but safer)
|
|
64
73
|
*
|
|
@@ -85,6 +85,33 @@ function scrubAuthHeaders(headers) {
|
|
|
85
85
|
}
|
|
86
86
|
return cleaned;
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The `credentials` init for a fetch, spread into it. A cookie leaves the page
|
|
90
|
+
* for an untrusted asset origin only as the browser default would send it.
|
|
91
|
+
*/
|
|
92
|
+
function fetchCredentials(config, trustedForAuth = true) {
|
|
93
|
+
if (!config.credentials)
|
|
94
|
+
return {};
|
|
95
|
+
if (!trustedForAuth && config.credentials === "include")
|
|
96
|
+
return {};
|
|
97
|
+
return { credentials: config.credentials };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The headers a custom-transport speech-mark or audio fetch starts from: the
|
|
101
|
+
* synthesis request's `Authorization` header under `includeAuthOnAssetFetch`,
|
|
102
|
+
* nothing otherwise.
|
|
103
|
+
*/
|
|
104
|
+
function assetFetchHeaders(requestHeaders, config) {
|
|
105
|
+
const assetHeaders = {};
|
|
106
|
+
if (!config.includeAuthOnAssetFetch)
|
|
107
|
+
return assetHeaders;
|
|
108
|
+
for (const [key, value] of Object.entries(requestHeaders)) {
|
|
109
|
+
if (key.toLowerCase() === "authorization") {
|
|
110
|
+
assetHeaders[key] = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return assetHeaders;
|
|
114
|
+
}
|
|
88
115
|
const getTelemetryReporter = (config) => {
|
|
89
116
|
const providerOptions = config.providerOptions && typeof config.providerOptions === "object"
|
|
90
117
|
? config.providerOptions
|
|
@@ -117,13 +144,15 @@ const resolveSupportsSSML = (config) => {
|
|
|
117
144
|
return SSML_CAPABLE_PIE_PROVIDERS.has(provider);
|
|
118
145
|
};
|
|
119
146
|
const trimTrailingSlash = (value) => value.replace(/\/+$/, "");
|
|
120
|
-
|
|
147
|
+
// Polly and Google probe their provider route first, then the generic
|
|
148
|
+
// `${apiEndpoint}/voices` the integration guides have hosts create.
|
|
149
|
+
const resolveVoicesValidationUrls = (config) => {
|
|
121
150
|
const base = trimTrailingSlash(config.apiEndpoint);
|
|
122
151
|
const provider = (config.provider || "").toLowerCase();
|
|
123
152
|
if (provider === "polly" || provider === "google") {
|
|
124
|
-
return `${base}/${provider}/voices
|
|
153
|
+
return [`${base}/${provider}/voices`, `${base}/voices`];
|
|
125
154
|
}
|
|
126
|
-
return `${base}/voices
|
|
155
|
+
return [`${base}/voices`];
|
|
127
156
|
};
|
|
128
157
|
const resolveTransportMode = (config) => {
|
|
129
158
|
if (config.transportMode === "custom")
|
|
@@ -248,20 +277,15 @@ const customAdapter = {
|
|
|
248
277
|
return {
|
|
249
278
|
text,
|
|
250
279
|
speedRate: resolveSpeedRate(config),
|
|
280
|
+
// Both spellings: servers that bind JSON camelCase never see `lang_id`.
|
|
251
281
|
lang_id: langId,
|
|
282
|
+
langId,
|
|
252
283
|
cache,
|
|
253
284
|
};
|
|
254
285
|
},
|
|
255
286
|
parseResponse: async (response, config, headers, signal, text) => {
|
|
256
287
|
const data = await response.json();
|
|
257
|
-
const marksHeaders =
|
|
258
|
-
if (config.includeAuthOnAssetFetch) {
|
|
259
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
260
|
-
if (key.toLowerCase() === "authorization") {
|
|
261
|
-
marksHeaders[key] = value;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
288
|
+
const marksHeaders = assetFetchHeaders(headers, config);
|
|
265
289
|
let speechMarks = [];
|
|
266
290
|
const inlineSpeechMarks = parseInlineSpeechMarks(data.speechMarks);
|
|
267
291
|
if (inlineSpeechMarks.length > 0) {
|
|
@@ -270,11 +294,13 @@ const customAdapter = {
|
|
|
270
294
|
else if (typeof data.word === "string" && data.word.length > 0) {
|
|
271
295
|
const marksUrl = parseAssetUrl(data.word, config);
|
|
272
296
|
if (marksUrl !== null) {
|
|
273
|
-
const
|
|
297
|
+
const marksTrusted = isOriginTrustedForAuth(marksUrl, config);
|
|
298
|
+
const effectiveMarksHeaders = marksTrusted
|
|
274
299
|
? marksHeaders
|
|
275
300
|
: scrubAuthHeaders(marksHeaders);
|
|
276
301
|
const marksResponse = await fetch(marksUrl.toString(), {
|
|
277
302
|
headers: effectiveMarksHeaders,
|
|
303
|
+
...fetchCredentials(config, marksTrusted),
|
|
278
304
|
signal,
|
|
279
305
|
// Fail loud on redirects; the origin allow-list decision
|
|
280
306
|
// is made pre-redirect and a silent hop could leak auth
|
|
@@ -439,6 +465,7 @@ class ServerTTSProviderImpl {
|
|
|
439
465
|
headers,
|
|
440
466
|
body: JSON.stringify(requestBody),
|
|
441
467
|
signal,
|
|
468
|
+
...fetchCredentials(this.config),
|
|
442
469
|
});
|
|
443
470
|
}
|
|
444
471
|
catch (error) {
|
|
@@ -525,12 +552,7 @@ class ServerTTSProviderImpl {
|
|
|
525
552
|
backend: this.config.provider || "server",
|
|
526
553
|
operation: "fetch-synthesized-audio-asset",
|
|
527
554
|
});
|
|
528
|
-
const assetHeaders =
|
|
529
|
-
if (this.config.includeAuthOnAssetFetch) {
|
|
530
|
-
if (this.config.authToken) {
|
|
531
|
-
assetHeaders["Authorization"] = `Bearer ${this.config.authToken}`;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
555
|
+
const assetHeaders = assetFetchHeaders(headers, this.config);
|
|
534
556
|
const parsedAssetUrl = parseAssetUrl(audioAssetUrl, this.config);
|
|
535
557
|
if (parsedAssetUrl === null) {
|
|
536
558
|
await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
@@ -543,7 +565,8 @@ class ServerTTSProviderImpl {
|
|
|
543
565
|
});
|
|
544
566
|
throw new Error("TTS asset URL rejected (non-http(s) or malformed)");
|
|
545
567
|
}
|
|
546
|
-
const
|
|
568
|
+
const assetTrusted = isOriginTrustedForAuth(parsedAssetUrl, this.config);
|
|
569
|
+
const effectiveAssetHeaders = assetTrusted
|
|
547
570
|
? assetHeaders
|
|
548
571
|
: scrubAuthHeaders(assetHeaders);
|
|
549
572
|
const audioResponse = await (async () => {
|
|
@@ -551,6 +574,7 @@ class ServerTTSProviderImpl {
|
|
|
551
574
|
return await fetch(parsedAssetUrl.toString(), {
|
|
552
575
|
headers: effectiveAssetHeaders,
|
|
553
576
|
signal,
|
|
577
|
+
...fetchCredentials(this.config, assetTrusted),
|
|
554
578
|
redirect: "error",
|
|
555
579
|
});
|
|
556
580
|
}
|
|
@@ -867,19 +891,26 @@ export class ServerTTSProvider {
|
|
|
867
891
|
clearTimeout(timeoutId);
|
|
868
892
|
return true;
|
|
869
893
|
}
|
|
870
|
-
const
|
|
871
|
-
?
|
|
872
|
-
: this.adapter.resolveSynthesisUrl(this.config);
|
|
894
|
+
const validationUrls = mode === "voices"
|
|
895
|
+
? resolveVoicesValidationUrls(this.config)
|
|
896
|
+
: [this.adapter.resolveSynthesisUrl(this.config)];
|
|
873
897
|
const method = mode === "voices" ? "GET" : "OPTIONS";
|
|
874
898
|
try {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
899
|
+
let reachable = false;
|
|
900
|
+
for (const validationUrl of validationUrls) {
|
|
901
|
+
const response = await fetch(validationUrl, {
|
|
902
|
+
method,
|
|
903
|
+
headers,
|
|
904
|
+
signal: controller.signal,
|
|
905
|
+
...fetchCredentials(this.config),
|
|
906
|
+
});
|
|
907
|
+
// Some endpoints may not accept OPTIONS; treat 405 as reachable.
|
|
908
|
+
reachable = response.ok || response.status === 405;
|
|
909
|
+
if (reachable || response.status !== 404)
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
880
912
|
clearTimeout(timeoutId);
|
|
881
|
-
|
|
882
|
-
return response.ok || response.status === 405;
|
|
913
|
+
return reachable;
|
|
883
914
|
}
|
|
884
915
|
catch {
|
|
885
916
|
clearTimeout(timeoutId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/tts-client-server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.74",
|
|
4
4
|
"description": "Client-side TTS provider that calls server API for synthesis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@pie-players/pie-players-shared": "0.3.
|
|
42
|
-
"@pie-players/pie-tts": "0.3.
|
|
43
|
-
"@pie-players/tts-server-core": "0.3.
|
|
41
|
+
"@pie-players/pie-players-shared": "0.3.74",
|
|
42
|
+
"@pie-players/pie-tts": "0.3.74",
|
|
43
|
+
"@pie-players/tts-server-core": "0.3.74"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"typescript": "^5.9.3",
|
|
47
|
-
"vitest": "^
|
|
47
|
+
"vitest": "^5.0.3"
|
|
48
48
|
},
|
|
49
49
|
"sideEffects": false,
|
|
50
50
|
"homepage": "https://github.com/pie-framework/pie-players/tree/master/packages/tts-client-server#readme",
|