@pie-players/tts-client-server 0.3.73 → 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 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
- voiceId: 'Joanna',
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` is required when using `ServerTTSProvider` directly. Toolkit-level
61
- `tools.providers.textToSpeech` integration may provide a default endpoint for
62
- server-backed backends, but direct client usage should pass this explicitly.
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
- organizationId?: string; // For multi-tenant setups
184
+ credentials?: 'omit' | 'same-origin' | 'include'; // Fetch credentials mode
188
185
  headers?: Record<string, string>; // Custom headers
189
- voiceId?: string; // Voice ID
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
- * Include auth headers when fetching custom-transport speech marks and audio URLs.
48
- * Defaults to false for compatibility.
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
- const resolveVoicesValidationUrl = (config) => {
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 effectiveMarksHeaders = isOriginTrustedForAuth(marksUrl, config)
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 effectiveAssetHeaders = isOriginTrustedForAuth(parsedAssetUrl, this.config)
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 validationUrl = mode === "voices"
871
- ? resolveVoicesValidationUrl(this.config)
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
- const response = await fetch(validationUrl, {
876
- method,
877
- headers,
878
- signal: controller.signal,
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
- // Some endpoints may not accept OPTIONS; treat 405 as reachable.
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.73",
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.73",
42
- "@pie-players/pie-tts": "0.3.73",
43
- "@pie-players/tts-server-core": "0.3.73"
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": "^4.1.11"
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",