@pie-players/tts-client-server 0.3.74 → 0.3.75
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 +16 -11
- package/dist/ServerTTSProvider.d.ts +26 -2
- package/dist/ServerTTSProvider.js +125 -77
- package/dist/index.d.ts +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -30,18 +30,22 @@ npm install @pie-players/tts-client-server
|
|
|
30
30
|
### Basic Setup
|
|
31
31
|
|
|
32
32
|
```typescript
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
ServerTTSProvider,
|
|
35
|
+
type ServerTTSProviderConfig,
|
|
36
|
+
} from '@pie-players/tts-client-server';
|
|
34
37
|
import { TTSService } from '@pie-players/pie-assessment-toolkit';
|
|
35
38
|
|
|
36
39
|
const provider = new ServerTTSProvider();
|
|
37
|
-
|
|
38
|
-
const ttsService = new TTSService();
|
|
39
|
-
await ttsService.initialize(provider, {
|
|
40
|
+
const config: ServerTTSProviderConfig = {
|
|
40
41
|
apiEndpoint: '/api/tts', // Your SvelteKit API route
|
|
41
42
|
provider: 'polly', // Server-side provider to use
|
|
42
43
|
voice: 'Joanna',
|
|
43
44
|
language: 'en-US',
|
|
44
|
-
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const ttsService = new TTSService();
|
|
48
|
+
await ttsService.initialize(provider, config);
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
`apiEndpoint`, `provider`, `voice` and `language` are top-level fields; the
|
|
@@ -68,9 +72,7 @@ await ttsService.initialize(provider, {
|
|
|
68
72
|
|
|
69
73
|
```typescript
|
|
70
74
|
// The provider automatically coordinates word highlighting
|
|
71
|
-
await ttsService.speak(
|
|
72
|
-
contentElement: document.getElementById('content'),
|
|
73
|
-
});
|
|
75
|
+
await ttsService.speak(document.getElementById('content'));
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
## Transport Modes
|
|
@@ -138,7 +140,10 @@ await ttsService.speak('Hello world, this is a test.', {
|
|
|
138
140
|
```
|
|
139
141
|
|
|
140
142
|
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.
|
|
143
|
+
JSON in camelCase reads it without a mapping of its own. It is the host's
|
|
144
|
+
`providerOptions.lang_id`, else the language a speak names, else `language`,
|
|
145
|
+
else `en-US`. In PIE mode, `language` is the language a speak names, else the
|
|
146
|
+
configured one.
|
|
142
147
|
|
|
143
148
|
### Custom mode response
|
|
144
149
|
|
|
@@ -231,7 +236,7 @@ for the host-wide contract this provider fits into.
|
|
|
231
236
|
|
|
232
237
|
## How It Works
|
|
233
238
|
|
|
234
|
-
1. **Client calls** `speak(
|
|
239
|
+
1. **Client calls** `speak(element)`
|
|
235
240
|
2. **Adapter builds** backend-specific request payload
|
|
236
241
|
3. **Provider POSTs** to resolved synthesis endpoint (`/synthesize` or root POST)
|
|
237
242
|
4. **Adapter normalizes** response into audio + speech marks
|
|
@@ -272,7 +277,7 @@ The provider automatically manages Blob URLs:
|
|
|
272
277
|
|
|
273
278
|
```typescript
|
|
274
279
|
try {
|
|
275
|
-
await ttsService.speak('
|
|
280
|
+
await ttsService.speak(document.getElementById('content'));
|
|
276
281
|
} catch (error) {
|
|
277
282
|
console.error('TTS failed:', error.message);
|
|
278
283
|
// Fallback to browser TTS or show error
|
|
@@ -5,11 +5,36 @@
|
|
|
5
5
|
* providers like AWS Polly, Google Cloud TTS, etc.
|
|
6
6
|
* Returns audio with precise word-level timing (speech marks).
|
|
7
7
|
*/
|
|
8
|
-
import type { ITTSProvider, ITTSProviderImplementation, TTSConfig,
|
|
8
|
+
import type { ITTSProvider, ITTSProviderImplementation, TTSConfig, TTSProviderCapabilities, TTSProviderOptions } from "@pie-players/pie-tts";
|
|
9
|
+
/**
|
|
10
|
+
* The provider options the server provider reads. `contentLanguage`, set per
|
|
11
|
+
* speak, names the language synthesized: it wins over `language`, and a
|
|
12
|
+
* `lang_id` the host sets wins over it.
|
|
13
|
+
*/
|
|
14
|
+
export interface ServerTTSProviderOptions extends TTSProviderOptions {
|
|
15
|
+
/** Polly engine, on the pie transport, when `engine` is not set. */
|
|
16
|
+
engine?: "standard" | "neural";
|
|
17
|
+
/** Output sample rate, on the pie transport. */
|
|
18
|
+
sampleRate?: number;
|
|
19
|
+
/** Audio format, on the pie transport. */
|
|
20
|
+
format?: "mp3" | "ogg" | "pcm";
|
|
21
|
+
/** Speech mark types requested, on the pie transport. */
|
|
22
|
+
speechMarkTypes?: Array<"word" | "sentence" | "ssml">;
|
|
23
|
+
/** Speed bucket, on the custom transport; derived from `rate` when unset. */
|
|
24
|
+
speedRate?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Language id, on the custom transport. Without it, the language a speak
|
|
27
|
+
* names, then `language`, then en-US.
|
|
28
|
+
*/
|
|
29
|
+
lang_id?: string;
|
|
30
|
+
/** Server-side caching, on the custom transport. Defaults to true. */
|
|
31
|
+
cache?: boolean;
|
|
32
|
+
}
|
|
9
33
|
/**
|
|
10
34
|
* Configuration for ServerTTSProvider
|
|
11
35
|
*/
|
|
12
36
|
export interface ServerTTSProviderConfig extends TTSConfig {
|
|
37
|
+
providerOptions?: ServerTTSProviderOptions;
|
|
13
38
|
/** API endpoint base URL (e.g., '/api/tts' or 'https://api.example.com/tts') */
|
|
14
39
|
apiEndpoint: string;
|
|
15
40
|
/** Provider to use on server ('polly', 'google', 'elevenlabs', etc.) */
|
|
@@ -106,7 +131,6 @@ export declare class ServerTTSProvider implements ITTSProvider {
|
|
|
106
131
|
* @performance 100-500ms depending on network
|
|
107
132
|
*/
|
|
108
133
|
private testAPIAvailability;
|
|
109
|
-
supportsFeature(feature: TTSFeature): boolean;
|
|
110
134
|
getCapabilities(): TTSProviderCapabilities;
|
|
111
135
|
destroy(): void;
|
|
112
136
|
}
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* providers like AWS Polly, Google Cloud TTS, etc.
|
|
6
6
|
* Returns audio with precise word-level timing (speech marks).
|
|
7
7
|
*/
|
|
8
|
-
import { createPieLogger,
|
|
8
|
+
import { createPieLogger, isTtsDebugEnabled, } from "@pie-players/pie-players-shared/pie";
|
|
9
9
|
import { normalizeSpeechMarks, resolveSpeedRateBucket, } from "@pie-players/tts-server-core";
|
|
10
|
-
|
|
10
|
+
// Debug lines need PIE_TTS_DEBUG=1 or globalThis.__PIE_TTS_DEBUG__ = true.
|
|
11
|
+
const logger = createPieLogger("server-tts-provider", isTtsDebugEnabled);
|
|
12
|
+
// Telemetry names the tool by its canonical id, as the toolkit does.
|
|
13
|
+
const TOOL_ID = "textToSpeech";
|
|
11
14
|
/**
|
|
12
15
|
* Parse a TTS asset URL (speech marks or audio). Rejects anything that is
|
|
13
16
|
* not an absolute http(s) URL; relative URLs are resolved against
|
|
@@ -113,13 +116,8 @@ function assetFetchHeaders(requestHeaders, config) {
|
|
|
113
116
|
return assetHeaders;
|
|
114
117
|
}
|
|
115
118
|
const getTelemetryReporter = (config) => {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
: {};
|
|
119
|
-
const reporter = providerOptions.__pieTelemetry;
|
|
120
|
-
return typeof reporter === "function"
|
|
121
|
-
? reporter
|
|
122
|
-
: undefined;
|
|
119
|
+
const reporter = config.providerOptions?.__pieTelemetry;
|
|
120
|
+
return typeof reporter === "function" ? reporter : undefined;
|
|
123
121
|
};
|
|
124
122
|
const MAX_TEXT_LENGTH_BY_MODE = {
|
|
125
123
|
pie: 3000,
|
|
@@ -172,12 +170,18 @@ const resolveValidationMode = (config, mode) => {
|
|
|
172
170
|
return mode === "custom" ? "none" : "voices";
|
|
173
171
|
};
|
|
174
172
|
const resolveSpeedRate = (config) => {
|
|
175
|
-
const
|
|
176
|
-
if (typeof
|
|
177
|
-
return
|
|
178
|
-
}
|
|
173
|
+
const speedRate = config.providerOptions?.speedRate;
|
|
174
|
+
if (typeof speedRate === "string")
|
|
175
|
+
return speedRate;
|
|
179
176
|
return resolveSpeedRateBucket(config.rate);
|
|
180
177
|
};
|
|
178
|
+
/** The language a speak named for its content, if any. */
|
|
179
|
+
const contentLanguageOf = (config) => {
|
|
180
|
+
const language = config.providerOptions?.contentLanguage;
|
|
181
|
+
return typeof language === "string" && language.trim()
|
|
182
|
+
? language.trim()
|
|
183
|
+
: undefined;
|
|
184
|
+
};
|
|
181
185
|
const parseInlineSpeechMarks = (input) => {
|
|
182
186
|
if (!Array.isArray(input))
|
|
183
187
|
return [];
|
|
@@ -218,7 +222,7 @@ const pieAdapter = {
|
|
|
218
222
|
return endpointMode === "rootPost" ? base : `${base}/synthesize`;
|
|
219
223
|
},
|
|
220
224
|
buildRequestBody: (text, config) => {
|
|
221
|
-
const providerOptions =
|
|
225
|
+
const providerOptions = config.providerOptions || {};
|
|
222
226
|
const engine = typeof config.engine === "string"
|
|
223
227
|
? config.engine
|
|
224
228
|
: typeof providerOptions.engine === "string"
|
|
@@ -240,7 +244,7 @@ const pieAdapter = {
|
|
|
240
244
|
text,
|
|
241
245
|
provider: config.provider || "polly",
|
|
242
246
|
voice: config.voice,
|
|
243
|
-
language: config.language,
|
|
247
|
+
language: contentLanguageOf(config) ?? config.language,
|
|
244
248
|
rate: config.rate,
|
|
245
249
|
engine,
|
|
246
250
|
sampleRate,
|
|
@@ -269,10 +273,14 @@ const customAdapter = {
|
|
|
269
273
|
return endpointMode === "synthesizePath" ? `${base}/synthesize` : base;
|
|
270
274
|
},
|
|
271
275
|
buildRequestBody: (text, config) => {
|
|
272
|
-
const providerOptions =
|
|
273
|
-
|
|
276
|
+
const providerOptions = config.providerOptions || {};
|
|
277
|
+
// A host's `lang_id` names a locale from the service's own roster, which a
|
|
278
|
+
// content tag such as `es` is not: SchoolCity reads one it lacks as en-US.
|
|
279
|
+
const pinned = typeof providerOptions.lang_id === "string" &&
|
|
280
|
+
providerOptions.lang_id.trim()
|
|
274
281
|
? providerOptions.lang_id
|
|
275
|
-
:
|
|
282
|
+
: undefined;
|
|
283
|
+
const langId = pinned ?? contentLanguageOf(config) ?? (config.language || "en-US");
|
|
276
284
|
const cache = typeof providerOptions.cache === "boolean" ? providerOptions.cache : true;
|
|
277
285
|
return {
|
|
278
286
|
text,
|
|
@@ -347,7 +355,11 @@ class ServerTTSProviderImpl {
|
|
|
347
355
|
*/
|
|
348
356
|
highlightCursor = -1;
|
|
349
357
|
intentionallyStopped = false;
|
|
358
|
+
// Set while a synthesis request is in flight.
|
|
350
359
|
activeSynthesisController = null;
|
|
360
|
+
// Starts the current audio element; a pause before it sounds leaves the
|
|
361
|
+
// start to resume.
|
|
362
|
+
startCurrentAudio = null;
|
|
351
363
|
synthesisRunId = 0;
|
|
352
364
|
telemetryReporter;
|
|
353
365
|
onPlaybackStart;
|
|
@@ -374,7 +386,23 @@ class ServerTTSProviderImpl {
|
|
|
374
386
|
const synthesisController = new AbortController();
|
|
375
387
|
this.activeSynthesisController = synthesisController;
|
|
376
388
|
// Call server API to synthesize speech
|
|
377
|
-
|
|
389
|
+
let synthesized;
|
|
390
|
+
try {
|
|
391
|
+
synthesized = await this.synthesizeSpeech(text, synthesisController.signal, runId);
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
// A stop or a newer speak aborted this synthesis: not a failure.
|
|
395
|
+
if (runId !== this.synthesisRunId || synthesisController.signal.aborted) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
if (this.activeSynthesisController === synthesisController) {
|
|
402
|
+
this.activeSynthesisController = null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const { audioUrl, wordTimings } = synthesized;
|
|
378
406
|
if (runId !== this.synthesisRunId) {
|
|
379
407
|
URL.revokeObjectURL(audioUrl);
|
|
380
408
|
return;
|
|
@@ -389,11 +417,14 @@ class ServerTTSProviderImpl {
|
|
|
389
417
|
if (this.config.volume !== undefined) {
|
|
390
418
|
audio.volume = Math.max(0, Math.min(1, this.config.volume));
|
|
391
419
|
}
|
|
392
|
-
//
|
|
393
|
-
audio
|
|
394
|
-
|
|
420
|
+
// A stop or a newer speak supersedes this audio: its late events must
|
|
421
|
+
// neither clear the current audio's state nor fail the current speak.
|
|
422
|
+
const superseded = () => runId !== this.synthesisRunId || this.currentAudio !== audio;
|
|
423
|
+
// `playing`, not `play`: `play` fires when playback is requested, before
|
|
424
|
+
// the audio has buffered and sounds.
|
|
425
|
+
audio.onplaying = () => {
|
|
426
|
+
if (superseded())
|
|
395
427
|
return;
|
|
396
|
-
}
|
|
397
428
|
this.pausedState = false;
|
|
398
429
|
try {
|
|
399
430
|
this.onPlaybackStart?.();
|
|
@@ -407,35 +438,40 @@ class ServerTTSProviderImpl {
|
|
|
407
438
|
}
|
|
408
439
|
};
|
|
409
440
|
audio.onended = () => {
|
|
410
|
-
this.stopWordHighlighting();
|
|
411
441
|
URL.revokeObjectURL(audioUrl);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
this.highlightCursor = -1;
|
|
442
|
+
if (!superseded())
|
|
443
|
+
this.clearCurrentAudio();
|
|
415
444
|
resolve();
|
|
416
445
|
};
|
|
417
|
-
audio.onerror = (
|
|
418
|
-
this.stopWordHighlighting();
|
|
446
|
+
audio.onerror = () => {
|
|
419
447
|
URL.revokeObjectURL(audioUrl);
|
|
420
|
-
this.
|
|
421
|
-
this.wordTimings = [];
|
|
422
|
-
this.highlightCursor = -1;
|
|
423
|
-
void event;
|
|
424
|
-
// Only reject if this wasn't an intentional stop
|
|
425
|
-
if (!this.intentionallyStopped) {
|
|
426
|
-
reject(new Error("Failed to play audio from server"));
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
// Intentional stop, resolve normally
|
|
448
|
+
if (superseded() || this.intentionallyStopped) {
|
|
430
449
|
resolve();
|
|
450
|
+
return;
|
|
431
451
|
}
|
|
452
|
+
this.clearCurrentAudio();
|
|
453
|
+
reject(new Error("Failed to play audio from server"));
|
|
432
454
|
};
|
|
433
455
|
audio.onpause = () => {
|
|
456
|
+
if (superseded())
|
|
457
|
+
return;
|
|
434
458
|
this.stopWordHighlighting();
|
|
435
459
|
this.pausedState = true;
|
|
436
460
|
};
|
|
437
|
-
|
|
438
|
-
|
|
461
|
+
const start = () => {
|
|
462
|
+
audio.play().catch((error) => {
|
|
463
|
+
// A pause before the audio sounds aborts its play; resume starts it.
|
|
464
|
+
const aborted = error?.name === "AbortError";
|
|
465
|
+
if (aborted && this.pausedState && this.currentAudio === audio) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
reject(error);
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
this.startCurrentAudio = start;
|
|
472
|
+
// A pause during synthesis holds the audio until resume.
|
|
473
|
+
if (!this.pausedState)
|
|
474
|
+
start();
|
|
439
475
|
});
|
|
440
476
|
}
|
|
441
477
|
/**
|
|
@@ -444,7 +480,7 @@ class ServerTTSProviderImpl {
|
|
|
444
480
|
async synthesizeSpeech(text, signal, runId) {
|
|
445
481
|
const synthStartedAt = Date.now();
|
|
446
482
|
await this.emitTelemetry("pie-tool-backend-call-start", {
|
|
447
|
-
toolId:
|
|
483
|
+
toolId: TOOL_ID,
|
|
448
484
|
backend: this.config.provider || "server",
|
|
449
485
|
operation: "synthesize-speech",
|
|
450
486
|
});
|
|
@@ -469,8 +505,10 @@ class ServerTTSProviderImpl {
|
|
|
469
505
|
});
|
|
470
506
|
}
|
|
471
507
|
catch (error) {
|
|
508
|
+
if (signal.aborted)
|
|
509
|
+
throw error;
|
|
472
510
|
await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
473
|
-
toolId:
|
|
511
|
+
toolId: TOOL_ID,
|
|
474
512
|
backend: this.config.provider || "server",
|
|
475
513
|
operation: "synthesize-speech",
|
|
476
514
|
duration: Date.now() - synthStartedAt,
|
|
@@ -510,7 +548,7 @@ class ServerTTSProviderImpl {
|
|
|
510
548
|
: undefined) ||
|
|
511
549
|
`Server returned ${response.status}`;
|
|
512
550
|
await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
513
|
-
toolId:
|
|
551
|
+
toolId: TOOL_ID,
|
|
514
552
|
backend: this.config.provider || "server",
|
|
515
553
|
operation: "synthesize-speech",
|
|
516
554
|
duration: Date.now() - synthStartedAt,
|
|
@@ -548,7 +586,7 @@ class ServerTTSProviderImpl {
|
|
|
548
586
|
const audioAssetUrl = normalized.audio.url;
|
|
549
587
|
const assetFetchStartedAt = Date.now();
|
|
550
588
|
await this.emitTelemetry("pie-tool-backend-call-start", {
|
|
551
|
-
toolId:
|
|
589
|
+
toolId: TOOL_ID,
|
|
552
590
|
backend: this.config.provider || "server",
|
|
553
591
|
operation: "fetch-synthesized-audio-asset",
|
|
554
592
|
});
|
|
@@ -556,7 +594,7 @@ class ServerTTSProviderImpl {
|
|
|
556
594
|
const parsedAssetUrl = parseAssetUrl(audioAssetUrl, this.config);
|
|
557
595
|
if (parsedAssetUrl === null) {
|
|
558
596
|
await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
559
|
-
toolId:
|
|
597
|
+
toolId: TOOL_ID,
|
|
560
598
|
backend: this.config.provider || "server",
|
|
561
599
|
operation: "fetch-synthesized-audio-asset",
|
|
562
600
|
duration: Date.now() - assetFetchStartedAt,
|
|
@@ -579,8 +617,10 @@ class ServerTTSProviderImpl {
|
|
|
579
617
|
});
|
|
580
618
|
}
|
|
581
619
|
catch (error) {
|
|
620
|
+
if (signal.aborted)
|
|
621
|
+
throw error;
|
|
582
622
|
await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
583
|
-
toolId:
|
|
623
|
+
toolId: TOOL_ID,
|
|
584
624
|
backend: this.config.provider || "server",
|
|
585
625
|
operation: "fetch-synthesized-audio-asset",
|
|
586
626
|
duration: Date.now() - assetFetchStartedAt,
|
|
@@ -592,7 +632,7 @@ class ServerTTSProviderImpl {
|
|
|
592
632
|
})();
|
|
593
633
|
if (!audioResponse.ok) {
|
|
594
634
|
await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
595
|
-
toolId:
|
|
635
|
+
toolId: TOOL_ID,
|
|
596
636
|
backend: this.config.provider || "server",
|
|
597
637
|
operation: "fetch-synthesized-audio-asset",
|
|
598
638
|
duration: Date.now() - assetFetchStartedAt,
|
|
@@ -604,7 +644,7 @@ class ServerTTSProviderImpl {
|
|
|
604
644
|
}
|
|
605
645
|
audioBlob = await audioResponse.blob();
|
|
606
646
|
await this.emitTelemetry("pie-tool-backend-call-success", {
|
|
607
|
-
toolId:
|
|
647
|
+
toolId: TOOL_ID,
|
|
608
648
|
backend: this.config.provider || "server",
|
|
609
649
|
operation: "fetch-synthesized-audio-asset",
|
|
610
650
|
duration: Date.now() - assetFetchStartedAt,
|
|
@@ -614,7 +654,7 @@ class ServerTTSProviderImpl {
|
|
|
614
654
|
// Convert speech marks to word timings
|
|
615
655
|
const wordTimings = this.parseSpeechMarks(normalized.speechMarks);
|
|
616
656
|
await this.emitTelemetry("pie-tool-backend-call-success", {
|
|
617
|
-
toolId:
|
|
657
|
+
toolId: TOOL_ID,
|
|
618
658
|
backend: this.config.provider || "server",
|
|
619
659
|
operation: "synthesize-speech",
|
|
620
660
|
duration: Date.now() - synthStartedAt,
|
|
@@ -734,25 +774,39 @@ class ServerTTSProviderImpl {
|
|
|
734
774
|
this.highlightInterval = null;
|
|
735
775
|
}
|
|
736
776
|
}
|
|
777
|
+
/** Pause playback, or, while synthesis is in flight, hold its audio. */
|
|
737
778
|
pause() {
|
|
738
|
-
if (this.
|
|
779
|
+
if (this.pausedState)
|
|
780
|
+
return;
|
|
781
|
+
if (this.currentAudio) {
|
|
739
782
|
this.currentAudio.pause();
|
|
740
783
|
this.stopWordHighlighting();
|
|
741
|
-
this.pausedState = true;
|
|
742
784
|
}
|
|
785
|
+
else if (!this.activeSynthesisController) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
this.pausedState = true;
|
|
743
789
|
}
|
|
744
790
|
resume() {
|
|
745
|
-
if (this.
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
791
|
+
if (!this.pausedState)
|
|
792
|
+
return;
|
|
793
|
+
this.pausedState = false;
|
|
794
|
+
if (!this.currentAudio)
|
|
795
|
+
return;
|
|
796
|
+
this.startCurrentAudio?.();
|
|
797
|
+
if (this.onWordBoundary && this.wordTimings.length > 0) {
|
|
798
|
+
this.startWordHighlighting();
|
|
752
799
|
}
|
|
753
800
|
}
|
|
801
|
+
clearCurrentAudio() {
|
|
802
|
+
this.stopWordHighlighting();
|
|
803
|
+
this.currentAudio = null;
|
|
804
|
+
this.wordTimings = [];
|
|
805
|
+
this.highlightCursor = -1;
|
|
806
|
+
}
|
|
754
807
|
stop() {
|
|
755
808
|
this.synthesisRunId += 1;
|
|
809
|
+
this.startCurrentAudio = null;
|
|
756
810
|
if (this.activeSynthesisController) {
|
|
757
811
|
this.activeSynthesisController.abort();
|
|
758
812
|
this.activeSynthesisController = null;
|
|
@@ -801,6 +855,15 @@ class ServerTTSProviderImpl {
|
|
|
801
855
|
// Voice change requires resynthesis, affects next speak()
|
|
802
856
|
this.config.voice = settings.voice;
|
|
803
857
|
}
|
|
858
|
+
// Only the per-speak language: the toolkit's other options would replace
|
|
859
|
+
// the speed bucket a rate change set above.
|
|
860
|
+
if (settings.providerOptions &&
|
|
861
|
+
"contentLanguage" in settings.providerOptions) {
|
|
862
|
+
this.config.providerOptions = {
|
|
863
|
+
...this.config.providerOptions,
|
|
864
|
+
contentLanguage: settings.providerOptions.contentLanguage,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
804
867
|
}
|
|
805
868
|
}
|
|
806
869
|
/**
|
|
@@ -845,14 +908,14 @@ export class ServerTTSProvider {
|
|
|
845
908
|
if (serverConfig.validateEndpoint) {
|
|
846
909
|
const validationStartedAt = Date.now();
|
|
847
910
|
await this.emitTelemetry("pie-tool-backend-call-start", {
|
|
848
|
-
toolId:
|
|
911
|
+
toolId: TOOL_ID,
|
|
849
912
|
backend: serverConfig.provider || "server",
|
|
850
913
|
operation: "validate-endpoint",
|
|
851
914
|
});
|
|
852
915
|
const available = await this.testAPIAvailability();
|
|
853
916
|
if (!available) {
|
|
854
917
|
await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
855
|
-
toolId:
|
|
918
|
+
toolId: TOOL_ID,
|
|
856
919
|
backend: serverConfig.provider || "server",
|
|
857
920
|
operation: "validate-endpoint",
|
|
858
921
|
duration: Date.now() - validationStartedAt,
|
|
@@ -862,7 +925,7 @@ export class ServerTTSProvider {
|
|
|
862
925
|
throw new Error(`Server TTS API not available at ${serverConfig.apiEndpoint}`);
|
|
863
926
|
}
|
|
864
927
|
await this.emitTelemetry("pie-tool-backend-call-success", {
|
|
865
|
-
toolId:
|
|
928
|
+
toolId: TOOL_ID,
|
|
866
929
|
backend: serverConfig.provider || "server",
|
|
867
930
|
operation: "validate-endpoint",
|
|
868
931
|
duration: Date.now() - validationStartedAt,
|
|
@@ -921,21 +984,6 @@ export class ServerTTSProvider {
|
|
|
921
984
|
return false;
|
|
922
985
|
}
|
|
923
986
|
}
|
|
924
|
-
supportsFeature(feature) {
|
|
925
|
-
switch (feature) {
|
|
926
|
-
case "pause":
|
|
927
|
-
case "resume":
|
|
928
|
-
case "wordBoundary":
|
|
929
|
-
case "voiceSelection":
|
|
930
|
-
case "rateControl":
|
|
931
|
-
return true;
|
|
932
|
-
case "pitchControl":
|
|
933
|
-
// Depends on server provider, assume no for safety
|
|
934
|
-
return false;
|
|
935
|
-
default:
|
|
936
|
-
return false;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
987
|
getCapabilities() {
|
|
940
988
|
const mode = this.config ? resolveTransportMode(this.config) : "pie";
|
|
941
989
|
return {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Client-side TTS provider for calling server APIs
|
|
3
3
|
* @module @pie-players/tts-client-server
|
|
4
4
|
*/
|
|
5
|
-
export type { ServerTTSProviderConfig } from "./ServerTTSProvider.js";
|
|
5
|
+
export type { ServerTTSProviderConfig, ServerTTSProviderOptions, } from "./ServerTTSProvider.js";
|
|
6
6
|
export { ServerTTSProvider } from "./ServerTTSProvider.js";
|
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.75",
|
|
4
4
|
"description": "Client-side TTS provider that calls server API for synthesis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,9 +38,9 @@
|
|
|
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.75",
|
|
42
|
+
"@pie-players/pie-tts": "0.3.75",
|
|
43
|
+
"@pie-players/tts-server-core": "0.3.75"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"typescript": "^5.9.3",
|