@pie-players/tts-client-server 1.0.0-beta.0 → 1.0.0-beta.10
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/LICENSE +21 -0
- package/dist/ServerTTSProvider.d.ts +0 -2
- package/dist/ServerTTSProvider.js +133 -98
- package/package.json +8 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PIE Framework contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -57,8 +57,6 @@ export interface ServerTTSProviderConfig extends TTSConfig {
|
|
|
57
57
|
language?: string;
|
|
58
58
|
/** Polly engine selection when provider is AWS Polly */
|
|
59
59
|
engine?: "standard" | "neural";
|
|
60
|
-
/** Volume level 0-1 */
|
|
61
|
-
volume?: number;
|
|
62
60
|
/**
|
|
63
61
|
* Transport mode determines request/response translation strategy.
|
|
64
62
|
* - pie: POST {apiEndpoint}/synthesize, expects inline base64 audio + speech marks
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* `ServerTTSProvider`, which picks the adapter, runs the optional endpoint probe
|
|
13
13
|
* and hands out implementations.
|
|
14
14
|
*/
|
|
15
|
+
import { anchorSpeechMarks, normalizeSpeechMarks, resolveSpeedRateBucket, } from "@pie-players/pie-tts";
|
|
16
|
+
import { languageTagWithoutVariants } from "@pie-players/pie-players-shared/i18n/language-tags";
|
|
15
17
|
import { createPieLogger, isTtsDebugEnabled, } from "@pie-players/pie-players-shared/pie";
|
|
16
|
-
import { anchorSpeechMarks, normalizeSpeechMarks, resolveSpeedRateBucket, } from "@pie-players/tts-server-core";
|
|
17
18
|
// Debug lines need PIE_TTS_DEBUG=1 or globalThis.__PIE_TTS_DEBUG__ = true.
|
|
18
19
|
const logger = createPieLogger("server-tts-provider", isTtsDebugEnabled);
|
|
19
20
|
// Telemetry names the tool by its canonical id, as the toolkit does.
|
|
@@ -22,9 +23,6 @@ const TOOL_ID = "textToSpeech";
|
|
|
22
23
|
const HIGHLIGHT_INTERVAL_MS = 50;
|
|
23
24
|
// How long the endpoint probe waits before reporting the server unreachable.
|
|
24
25
|
const ENDPOINT_PROBE_TIMEOUT_MS = 5000;
|
|
25
|
-
// Debug-log previews of an error body and of the text under synthesis.
|
|
26
|
-
const RAW_BODY_PREVIEW_LENGTH = 500;
|
|
27
|
-
const TEXT_PREVIEW_LENGTH = 120;
|
|
28
26
|
/**
|
|
29
27
|
* Parse a TTS asset URL (speech marks or audio). Rejects anything that is
|
|
30
28
|
* not an absolute http(s) URL; relative URLs are resolved against
|
|
@@ -200,11 +198,15 @@ const resolveSpeedRate = (config) => {
|
|
|
200
198
|
return speedRate;
|
|
201
199
|
return resolveSpeedRateBucket(config.rate);
|
|
202
200
|
};
|
|
203
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* The language a speak named for its content, if any, without variant or
|
|
203
|
+
* private-use subtags: voices are keyed by language and region, so
|
|
204
|
+
* `en-GB-x-gulf` is read by an `en-GB` voice.
|
|
205
|
+
*/
|
|
204
206
|
const contentLanguageOf = (config) => {
|
|
205
207
|
const language = config.providerOptions?.contentLanguage;
|
|
206
208
|
return typeof language === "string" && language.trim()
|
|
207
|
-
? language
|
|
209
|
+
? languageTagWithoutVariants(language)
|
|
208
210
|
: undefined;
|
|
209
211
|
};
|
|
210
212
|
const parseInlineSpeechMarks = (input) => {
|
|
@@ -271,6 +273,7 @@ const pieAdapter = {
|
|
|
271
273
|
voice: config.voice,
|
|
272
274
|
language: contentLanguageOf(config) ?? config.language,
|
|
273
275
|
rate: config.rate,
|
|
276
|
+
...(config.pitch !== undefined ? { pitch: config.pitch } : {}),
|
|
274
277
|
engine,
|
|
275
278
|
sampleRate,
|
|
276
279
|
format,
|
|
@@ -365,6 +368,28 @@ const ADAPTERS = {
|
|
|
365
368
|
pie: pieAdapter,
|
|
366
369
|
custom: customAdapter,
|
|
367
370
|
};
|
|
371
|
+
/** Release a rejected response without waiting for its underlying stream. */
|
|
372
|
+
function cancelResponseBody(response) {
|
|
373
|
+
try {
|
|
374
|
+
void response.body?.cancel().catch(() => { });
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// Cleanup must not replace the status-only failure.
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* A fixed-message error that keeps the original's standard name, such as
|
|
382
|
+
* `NotAllowedError` for blocked autoplay, so hosts can still branch on it. The
|
|
383
|
+
* original message and cause are dropped: they can carry signed URLs.
|
|
384
|
+
*/
|
|
385
|
+
function redactedError(message, original) {
|
|
386
|
+
const error = new Error(message);
|
|
387
|
+
const name = original?.name;
|
|
388
|
+
if (typeof name === "string" && /^[A-Z][A-Za-z]*Error$/.test(name)) {
|
|
389
|
+
error.name = name;
|
|
390
|
+
}
|
|
391
|
+
return error;
|
|
392
|
+
}
|
|
368
393
|
/**
|
|
369
394
|
* Provider implementation that handles audio playback
|
|
370
395
|
*/
|
|
@@ -381,12 +406,12 @@ class ServerTTSProviderImpl {
|
|
|
381
406
|
* spoken position instead of replaying the passage from its first word.
|
|
382
407
|
*/
|
|
383
408
|
highlightCursor = -1;
|
|
384
|
-
intentionallyStopped = false;
|
|
385
409
|
// Set while a synthesis request is in flight.
|
|
386
410
|
activeSynthesisController = null;
|
|
387
411
|
// Starts the current audio element; a pause before it sounds leaves the
|
|
388
412
|
// start to resume.
|
|
389
413
|
startCurrentAudio = null;
|
|
414
|
+
settleActivePlayback = null;
|
|
390
415
|
synthesisRunId = 0;
|
|
391
416
|
telemetryReporter;
|
|
392
417
|
onPlaybackStart;
|
|
@@ -400,13 +425,17 @@ class ServerTTSProviderImpl {
|
|
|
400
425
|
try {
|
|
401
426
|
await this.telemetryReporter?.(eventName, payload);
|
|
402
427
|
}
|
|
403
|
-
catch
|
|
404
|
-
logger.warn("telemetry callback failed
|
|
428
|
+
catch {
|
|
429
|
+
logger.warn("telemetry callback failed");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
assertSynthesisActive(signal, runId) {
|
|
433
|
+
if (signal.aborted || runId !== this.synthesisRunId) {
|
|
434
|
+
throw new DOMException("TTS request was aborted", "AbortError");
|
|
405
435
|
}
|
|
406
436
|
}
|
|
407
437
|
async speak(text) {
|
|
408
438
|
this.stop();
|
|
409
|
-
this.intentionallyStopped = false;
|
|
410
439
|
const runId = ++this.synthesisRunId;
|
|
411
440
|
const synthesisController = new AbortController();
|
|
412
441
|
this.activeSynthesisController = synthesisController;
|
|
@@ -436,6 +465,32 @@ class ServerTTSProviderImpl {
|
|
|
436
465
|
return new Promise((resolve, reject) => {
|
|
437
466
|
const audio = new Audio(audioUrl);
|
|
438
467
|
this.currentAudio = audio;
|
|
468
|
+
let settled = false;
|
|
469
|
+
const settle = (error) => {
|
|
470
|
+
if (settled)
|
|
471
|
+
return;
|
|
472
|
+
settled = true;
|
|
473
|
+
audio.onplaying = null;
|
|
474
|
+
audio.onended = null;
|
|
475
|
+
audio.onerror = null;
|
|
476
|
+
audio.onpause = null;
|
|
477
|
+
audio.pause();
|
|
478
|
+
audio.src = "";
|
|
479
|
+
URL.revokeObjectURL(audioUrl);
|
|
480
|
+
if (this.currentAudio === audio) {
|
|
481
|
+
this.clearCurrentAudio();
|
|
482
|
+
this.pausedState = false;
|
|
483
|
+
this.startCurrentAudio = null;
|
|
484
|
+
}
|
|
485
|
+
if (this.settleActivePlayback === settle) {
|
|
486
|
+
this.settleActivePlayback = null;
|
|
487
|
+
}
|
|
488
|
+
if (error)
|
|
489
|
+
reject(error);
|
|
490
|
+
else
|
|
491
|
+
resolve();
|
|
492
|
+
};
|
|
493
|
+
this.settleActivePlayback = settle;
|
|
439
494
|
if (this.config.volume !== undefined) {
|
|
440
495
|
audio.volume = Math.max(0, Math.min(1, this.config.volume));
|
|
441
496
|
}
|
|
@@ -453,27 +508,18 @@ class ServerTTSProviderImpl {
|
|
|
453
508
|
wordBoundaries: this.wordTimings.length > 0,
|
|
454
509
|
});
|
|
455
510
|
}
|
|
456
|
-
catch
|
|
457
|
-
logger.warn("playback-start callback failed
|
|
511
|
+
catch {
|
|
512
|
+
logger.warn("playback-start callback failed");
|
|
458
513
|
}
|
|
459
514
|
if (this.onWordBoundary && this.wordTimings.length > 0) {
|
|
460
515
|
this.startWordHighlighting();
|
|
461
516
|
}
|
|
462
517
|
};
|
|
463
|
-
audio.onended = () =>
|
|
464
|
-
URL.revokeObjectURL(audioUrl);
|
|
465
|
-
if (!superseded())
|
|
466
|
-
this.clearCurrentAudio();
|
|
467
|
-
resolve();
|
|
468
|
-
};
|
|
518
|
+
audio.onended = () => settle();
|
|
469
519
|
audio.onerror = () => {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
this.clearCurrentAudio();
|
|
476
|
-
reject(new Error("Failed to play audio from server"));
|
|
520
|
+
settle(superseded()
|
|
521
|
+
? undefined
|
|
522
|
+
: new Error("Failed to play audio from server"));
|
|
477
523
|
};
|
|
478
524
|
audio.onpause = () => {
|
|
479
525
|
if (superseded())
|
|
@@ -488,7 +534,9 @@ class ServerTTSProviderImpl {
|
|
|
488
534
|
if (aborted && this.pausedState && this.currentAudio === audio) {
|
|
489
535
|
return;
|
|
490
536
|
}
|
|
491
|
-
|
|
537
|
+
settle(superseded()
|
|
538
|
+
? undefined
|
|
539
|
+
: redactedError("Failed to start audio playback", error));
|
|
492
540
|
});
|
|
493
541
|
};
|
|
494
542
|
this.startCurrentAudio = start;
|
|
@@ -503,6 +551,7 @@ class ServerTTSProviderImpl {
|
|
|
503
551
|
async synthesizeSpeech(text, signal, runId) {
|
|
504
552
|
const synthStartedAt = Date.now();
|
|
505
553
|
await this.emitTelemetry("pie-tool-backend-call-start", backendTelemetry(this.config, "synthesize-speech"));
|
|
554
|
+
this.assertSynthesisActive(signal, runId);
|
|
506
555
|
const headers = {
|
|
507
556
|
"Content-Type": "application/json",
|
|
508
557
|
...this.config.headers,
|
|
@@ -523,76 +572,51 @@ class ServerTTSProviderImpl {
|
|
|
523
572
|
});
|
|
524
573
|
}
|
|
525
574
|
catch (error) {
|
|
526
|
-
|
|
527
|
-
throw error;
|
|
575
|
+
this.assertSynthesisActive(signal, runId);
|
|
528
576
|
await this.emitTelemetry("pie-tool-backend-call-error", backendTelemetry(this.config, "synthesize-speech", {
|
|
529
577
|
duration: Date.now() - synthStartedAt,
|
|
530
578
|
errorType: "TTSBackendNetworkError",
|
|
531
|
-
message:
|
|
579
|
+
message: "TTS synthesis network request failed",
|
|
532
580
|
}));
|
|
533
|
-
|
|
581
|
+
this.assertSynthesisActive(signal, runId);
|
|
582
|
+
throw redactedError("TTS synthesis network request failed", error);
|
|
534
583
|
}
|
|
535
584
|
})();
|
|
536
585
|
if (!response.ok) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
errorData = JSON.parse(rawBody);
|
|
542
|
-
}
|
|
543
|
-
catch (parseError) {
|
|
544
|
-
logger.warn("non-OK response body was not JSON", {
|
|
545
|
-
status: response.status,
|
|
546
|
-
url: synthUrl,
|
|
547
|
-
bodyLength: rawBody.length,
|
|
548
|
-
parseError: parseError instanceof Error
|
|
549
|
-
? parseError.message
|
|
550
|
-
: String(parseError),
|
|
551
|
-
});
|
|
552
|
-
// A provider's error body can echo the SSML it rejected, so the body
|
|
553
|
-
// itself stays behind the debug gate; the length above is what says
|
|
554
|
-
// a body arrived and was not JSON.
|
|
555
|
-
logger.debug("non-OK response body", {
|
|
556
|
-
rawBodyPreview: rawBody.slice(0, RAW_BODY_PREVIEW_LENGTH),
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
const errorMessage = (typeof errorData.message === "string" && errorData.message) ||
|
|
561
|
-
(typeof errorData.error?.message === "string"
|
|
562
|
-
? errorData.error.message
|
|
563
|
-
: undefined) ||
|
|
564
|
-
`Server returned ${response.status}`;
|
|
586
|
+
// A provider can echo request text or signed URLs in its body. Do not
|
|
587
|
+
// read it, or wait for a stalled stream before reporting the status.
|
|
588
|
+
cancelResponseBody(response);
|
|
589
|
+
const errorMessage = `Server returned ${response.status}`;
|
|
565
590
|
await this.emitTelemetry("pie-tool-backend-call-error", backendTelemetry(this.config, "synthesize-speech", {
|
|
566
591
|
duration: Date.now() - synthStartedAt,
|
|
567
592
|
statusCode: response.status,
|
|
568
593
|
errorType: "TTSBackendRequestError",
|
|
569
594
|
message: errorMessage,
|
|
570
595
|
}));
|
|
596
|
+
this.assertSynthesisActive(signal, runId);
|
|
571
597
|
logger.error("synthesize request failed", {
|
|
572
598
|
status: response.status,
|
|
573
|
-
url: synthUrl,
|
|
574
599
|
backend: this.config.provider || "server",
|
|
575
|
-
message: errorMessage,
|
|
576
600
|
textLength: text.length,
|
|
577
601
|
});
|
|
578
|
-
// The text under synthesis is item content and the provider's error body
|
|
579
|
-
// can echo it, so both stay behind the debug gate. `errorMessage` above is
|
|
580
|
-
// thrown to the caller regardless, and `textLength` is what correlates a
|
|
581
|
-
// failure with a payload without publishing what the learner is reading.
|
|
582
|
-
const textPreview = text
|
|
583
|
-
.replace(/\s+/g, " ")
|
|
584
|
-
.trim()
|
|
585
|
-
.slice(0, TEXT_PREVIEW_LENGTH);
|
|
586
|
-
logger.debug("synthesize request failed for text", {
|
|
587
|
-
responseBody: errorData,
|
|
588
|
-
textPreview: textPreview.length < text.length ? `${textPreview}…` : textPreview,
|
|
589
|
-
});
|
|
590
602
|
throw new Error(errorMessage);
|
|
591
603
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
604
|
+
let normalized;
|
|
605
|
+
try {
|
|
606
|
+
normalized = await this.adapter.parseResponse(response, this.config, headers, signal, text);
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
this.assertSynthesisActive(signal, runId);
|
|
610
|
+
const message = "TTS synthesis response could not be processed";
|
|
611
|
+
await this.emitTelemetry("pie-tool-backend-call-error", backendTelemetry(this.config, "synthesize-speech", {
|
|
612
|
+
duration: Date.now() - synthStartedAt,
|
|
613
|
+
errorType: "TTSBackendResponseError",
|
|
614
|
+
message,
|
|
615
|
+
}));
|
|
616
|
+
this.assertSynthesisActive(signal, runId);
|
|
617
|
+
throw new Error(message);
|
|
595
618
|
}
|
|
619
|
+
this.assertSynthesisActive(signal, runId);
|
|
596
620
|
let audioBlob;
|
|
597
621
|
if (normalized.audio.kind === "base64") {
|
|
598
622
|
audioBlob = this.base64ToBlob(normalized.audio.data, normalized.audio.contentType);
|
|
@@ -601,6 +625,7 @@ class ServerTTSProviderImpl {
|
|
|
601
625
|
const audioAssetUrl = normalized.audio.url;
|
|
602
626
|
const assetFetchStartedAt = Date.now();
|
|
603
627
|
await this.emitTelemetry("pie-tool-backend-call-start", backendTelemetry(this.config, "fetch-synthesized-audio-asset"));
|
|
628
|
+
this.assertSynthesisActive(signal, runId);
|
|
604
629
|
const assetHeaders = assetFetchHeaders(headers, this.config);
|
|
605
630
|
const parsedAssetUrl = parseAssetUrl(audioAssetUrl, this.config);
|
|
606
631
|
if (parsedAssetUrl === null) {
|
|
@@ -625,17 +650,18 @@ class ServerTTSProviderImpl {
|
|
|
625
650
|
});
|
|
626
651
|
}
|
|
627
652
|
catch (error) {
|
|
628
|
-
|
|
629
|
-
throw error;
|
|
653
|
+
this.assertSynthesisActive(signal, runId);
|
|
630
654
|
await this.emitTelemetry("pie-tool-backend-call-error", backendTelemetry(this.config, "fetch-synthesized-audio-asset", {
|
|
631
655
|
duration: Date.now() - assetFetchStartedAt,
|
|
632
656
|
errorType: "TTSAssetNetworkError",
|
|
633
|
-
message:
|
|
657
|
+
message: "TTS synthesized audio request failed",
|
|
634
658
|
}));
|
|
635
|
-
|
|
659
|
+
this.assertSynthesisActive(signal, runId);
|
|
660
|
+
throw redactedError("TTS synthesized audio request failed", error);
|
|
636
661
|
}
|
|
637
662
|
})();
|
|
638
663
|
if (!audioResponse.ok) {
|
|
664
|
+
cancelResponseBody(audioResponse);
|
|
639
665
|
await this.emitTelemetry("pie-tool-backend-call-error", backendTelemetry(this.config, "fetch-synthesized-audio-asset", {
|
|
640
666
|
duration: Date.now() - assetFetchStartedAt,
|
|
641
667
|
statusCode: audioResponse.status,
|
|
@@ -644,16 +670,30 @@ class ServerTTSProviderImpl {
|
|
|
644
670
|
}));
|
|
645
671
|
throw new Error(`Failed to download synthesized audio (${audioResponse.status})`);
|
|
646
672
|
}
|
|
647
|
-
|
|
673
|
+
try {
|
|
674
|
+
audioBlob = await audioResponse.blob();
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
this.assertSynthesisActive(signal, runId);
|
|
678
|
+
const message = "TTS synthesized audio response could not be read";
|
|
679
|
+
await this.emitTelemetry("pie-tool-backend-call-error", backendTelemetry(this.config, "fetch-synthesized-audio-asset", {
|
|
680
|
+
duration: Date.now() - assetFetchStartedAt,
|
|
681
|
+
errorType: "TTSAssetResponseError",
|
|
682
|
+
message,
|
|
683
|
+
}));
|
|
684
|
+
this.assertSynthesisActive(signal, runId);
|
|
685
|
+
throw new Error(message);
|
|
686
|
+
}
|
|
648
687
|
await this.emitTelemetry("pie-tool-backend-call-success", backendTelemetry(this.config, "fetch-synthesized-audio-asset", {
|
|
649
688
|
duration: Date.now() - assetFetchStartedAt,
|
|
650
689
|
}));
|
|
651
690
|
}
|
|
652
|
-
const audioUrl = URL.createObjectURL(audioBlob);
|
|
653
691
|
const wordTimings = this.parseSpeechMarks(normalized.speechMarks);
|
|
654
692
|
await this.emitTelemetry("pie-tool-backend-call-success", backendTelemetry(this.config, "synthesize-speech", {
|
|
655
693
|
duration: Date.now() - synthStartedAt,
|
|
656
694
|
}));
|
|
695
|
+
this.assertSynthesisActive(signal, runId);
|
|
696
|
+
const audioUrl = URL.createObjectURL(audioBlob);
|
|
657
697
|
return { audioUrl, wordTimings };
|
|
658
698
|
}
|
|
659
699
|
/**
|
|
@@ -807,25 +847,17 @@ class ServerTTSProviderImpl {
|
|
|
807
847
|
this.activeSynthesisController = null;
|
|
808
848
|
}
|
|
809
849
|
this.stopWordHighlighting();
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
this.intentionallyStopped = true;
|
|
813
|
-
this.currentAudio.pause();
|
|
814
|
-
if (this.currentAudio.src) {
|
|
815
|
-
URL.revokeObjectURL(this.currentAudio.src);
|
|
816
|
-
}
|
|
817
|
-
this.currentAudio.src = "";
|
|
818
|
-
this.currentAudio = null;
|
|
819
|
-
}
|
|
850
|
+
this.settleActivePlayback?.();
|
|
851
|
+
this.clearCurrentAudio();
|
|
820
852
|
this.pausedState = false;
|
|
821
853
|
this.wordTimings = [];
|
|
822
854
|
this.highlightCursor = -1;
|
|
823
855
|
}
|
|
824
856
|
/**
|
|
825
857
|
* Store rate, pitch, voice and the per-speak language for the next
|
|
826
|
-
* synthesis. Server-side synthesis bakes
|
|
827
|
-
* marks, so an active speed change
|
|
828
|
-
* current segment.
|
|
858
|
+
* synthesis, and volume for the next playback. Server-side synthesis bakes
|
|
859
|
+
* the first four into the audio and speech marks, so an active speed change
|
|
860
|
+
* is TTSService's job: it replays from the current segment.
|
|
829
861
|
*/
|
|
830
862
|
updateSettings(settings) {
|
|
831
863
|
if (settings.rate !== undefined) {
|
|
@@ -838,6 +870,9 @@ class ServerTTSProviderImpl {
|
|
|
838
870
|
if (settings.pitch !== undefined) {
|
|
839
871
|
this.config.pitch = settings.pitch;
|
|
840
872
|
}
|
|
873
|
+
if (settings.volume !== undefined) {
|
|
874
|
+
this.config.volume = settings.volume;
|
|
875
|
+
}
|
|
841
876
|
if (settings.voice !== undefined) {
|
|
842
877
|
this.config.voice = settings.voice;
|
|
843
878
|
}
|
|
@@ -867,8 +902,8 @@ export class ServerTTSProvider {
|
|
|
867
902
|
try {
|
|
868
903
|
await this.telemetryReporter?.(eventName, payload);
|
|
869
904
|
}
|
|
870
|
-
catch
|
|
871
|
-
logger.warn("telemetry callback failed
|
|
905
|
+
catch {
|
|
906
|
+
logger.warn("telemetry callback failed");
|
|
872
907
|
}
|
|
873
908
|
}
|
|
874
909
|
/**
|
|
@@ -895,9 +930,9 @@ export class ServerTTSProvider {
|
|
|
895
930
|
await this.emitTelemetry("pie-tool-backend-call-error", backendTelemetry(serverConfig, "validate-endpoint", {
|
|
896
931
|
duration: Date.now() - validationStartedAt,
|
|
897
932
|
errorType: "TTSEndpointValidationError",
|
|
898
|
-
message:
|
|
933
|
+
message: "Server TTS API is not available",
|
|
899
934
|
}));
|
|
900
|
-
throw new Error(
|
|
935
|
+
throw new Error("Server TTS API is not available");
|
|
901
936
|
}
|
|
902
937
|
await this.emitTelemetry("pie-tool-backend-call-success", backendTelemetry(serverConfig, "validate-endpoint", {
|
|
903
938
|
duration: Date.now() - validationStartedAt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/tts-client-server",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.10",
|
|
4
4
|
"description": "Client-side TTS provider that calls server API for synthesis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
12
|
-
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
13
15
|
},
|
|
14
16
|
"files": [
|
|
15
17
|
"dist"
|
|
@@ -37,13 +39,12 @@
|
|
|
37
39
|
"access": "public"
|
|
38
40
|
},
|
|
39
41
|
"dependencies": {
|
|
40
|
-
"@pie-players/pie-players-shared": "1.0.0-beta.
|
|
41
|
-
"@pie-players/pie-tts": "1.0.0-beta.
|
|
42
|
-
"@pie-players/tts-server-core": "1.0.0-beta.0"
|
|
42
|
+
"@pie-players/pie-players-shared": "1.0.0-beta.10",
|
|
43
|
+
"@pie-players/pie-tts": "1.0.0-beta.10"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"typescript": "^5.9.3",
|
|
46
|
-
"vitest": "^5.0.
|
|
47
|
+
"vitest": "^5.0.5"
|
|
47
48
|
},
|
|
48
49
|
"sideEffects": false,
|
|
49
50
|
"homepage": "https://github.com/pie-framework/pie-players/tree/master/packages/tts-client-server#readme",
|