@pie-players/pie-section-player-tools-tts-settings 0.3.26 → 0.3.28
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/TtsSettingsPanel.svelte +105 -5
- package/dist/defineProperty-CyepwRr5.js +33 -0
- package/dist/dist-Bud4G4lv.js +411 -0
- package/dist/dist-DwP27yIs.js +201 -0
- package/dist/section-player-tools-tts-settings.js +3955 -2333
- package/package.json +3 -2
package/TtsSettingsPanel.svelte
CHANGED
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
<script lang="ts">
|
|
16
16
|
import "@pie-players/pie-theme/components.css";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_TTS_SPEED_OPTIONS,
|
|
19
|
+
formatTTSSpeedOptionsAsText,
|
|
20
|
+
normalizeTTSSpeedOptions,
|
|
21
|
+
parseTTSSpeedOptionsFromText,
|
|
22
|
+
resolveTTSRuntimeSettings,
|
|
23
|
+
type TTSLayoutMode,
|
|
24
|
+
} from "@pie-players/pie-assessment-toolkit";
|
|
17
25
|
import { createEventDispatcher, onDestroy, onMount, untrack } from "svelte";
|
|
18
26
|
|
|
19
27
|
type BuiltInBackendTab = "browser" | "polly" | "google";
|
|
@@ -164,6 +172,9 @@ type PreviewSpeechMark = { time: number; start: number; end: number; value?: str
|
|
|
164
172
|
googleVoiceType?: string;
|
|
165
173
|
googleGender?: string;
|
|
166
174
|
providerOptions?: Record<string, unknown>;
|
|
175
|
+
layoutMode?: TTSLayoutMode;
|
|
176
|
+
/** Inline toolbar speed multipliers; `[]` hides speed buttons. */
|
|
177
|
+
speedOptions?: number[];
|
|
167
178
|
[key: string]: unknown;
|
|
168
179
|
};
|
|
169
180
|
|
|
@@ -186,6 +197,8 @@ type PreviewSpeechMark = { time: number; start: number; end: number; value?: str
|
|
|
186
197
|
let browserVoice = $state("");
|
|
187
198
|
let browserRate = $state(1);
|
|
188
199
|
let browserPitch = $state(1);
|
|
200
|
+
let layoutMode = $state<TTSLayoutMode>("left-aligned");
|
|
201
|
+
let speedOptionsText = $state("");
|
|
189
202
|
|
|
190
203
|
let pollyApiEndpoint = $state("");
|
|
191
204
|
let pollyLanguage = $state("en-US");
|
|
@@ -291,6 +304,12 @@ type PreviewSpeechMark = { time: number; start: number; end: number; value?: str
|
|
|
291
304
|
};
|
|
292
305
|
const BUILT_IN_TABS: BuiltInBackendTab[] = ["browser", "polly", "google"];
|
|
293
306
|
const PREVIEW_DEBUG_PREFIX = "[pie-tts-preview]";
|
|
307
|
+
const TTS_LAYOUT_MODES: readonly TTSLayoutMode[] = [
|
|
308
|
+
"reserved-row",
|
|
309
|
+
"expanding-row",
|
|
310
|
+
"floating-overlay",
|
|
311
|
+
"left-aligned",
|
|
312
|
+
];
|
|
294
313
|
|
|
295
314
|
function debugPreview(event: string, payload?: Record<string, unknown>): void {
|
|
296
315
|
if (typeof console === "undefined") return;
|
|
@@ -301,6 +320,16 @@ function debugPreview(event: string, payload?: Record<string, unknown>): void {
|
|
|
301
320
|
console.debug(`${PREVIEW_DEBUG_PREFIX} ${event}`);
|
|
302
321
|
}
|
|
303
322
|
|
|
323
|
+
function normalizeLayoutMode(value: unknown): TTSLayoutMode {
|
|
324
|
+
return TTS_LAYOUT_MODES.includes(value as TTSLayoutMode)
|
|
325
|
+
? (value as TTSLayoutMode)
|
|
326
|
+
: "left-aligned";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function resetInlineSpeedOptionsToDefaults(): void {
|
|
330
|
+
speedOptionsText = formatTTSSpeedOptionsAsText([...DEFAULT_TTS_SPEED_OPTIONS]);
|
|
331
|
+
}
|
|
332
|
+
|
|
304
333
|
const normalizedCustomProviders = $derived.by(() => {
|
|
305
334
|
const reserved = new Set<string>(BUILT_IN_TABS);
|
|
306
335
|
const deduped: CustomProviderDescriptor[] = [];
|
|
@@ -512,6 +541,22 @@ function debugPreview(event: string, payload?: Record<string, unknown>): void {
|
|
|
512
541
|
? "standard"
|
|
513
542
|
: "neural";
|
|
514
543
|
const sourceProviderOptions = (source?.providerOptions || {}) as Record<string, unknown>;
|
|
544
|
+
layoutMode = normalizeLayoutMode(source?.layoutMode);
|
|
545
|
+
const runtimeForSpeed = resolveTTSRuntimeSettings(
|
|
546
|
+
source && typeof source === "object" ? (source as Record<string, unknown>) : undefined,
|
|
547
|
+
);
|
|
548
|
+
if (runtimeForSpeed.speedOptions === undefined) {
|
|
549
|
+
speedOptionsText = formatTTSSpeedOptionsAsText([...DEFAULT_TTS_SPEED_OPTIONS]);
|
|
550
|
+
} else if (
|
|
551
|
+
Array.isArray(runtimeForSpeed.speedOptions) &&
|
|
552
|
+
runtimeForSpeed.speedOptions.length === 0
|
|
553
|
+
) {
|
|
554
|
+
speedOptionsText = "";
|
|
555
|
+
} else {
|
|
556
|
+
speedOptionsText = formatTTSSpeedOptionsAsText(
|
|
557
|
+
normalizeTTSSpeedOptions(runtimeForSpeed.speedOptions),
|
|
558
|
+
);
|
|
559
|
+
}
|
|
515
560
|
const defaultSampleRate = normalizePollySampleRate(
|
|
516
561
|
Number(source?.sampleRate ?? sourceProviderOptions.sampleRate ?? 24000)
|
|
517
562
|
);
|
|
@@ -570,6 +615,8 @@ function debugPreview(event: string, payload?: Record<string, unknown>): void {
|
|
|
570
615
|
setPreviewTextForCurrentTab();
|
|
571
616
|
}
|
|
572
617
|
|
|
618
|
+
const layoutModeReservesRow = $derived(layoutMode === "reserved-row");
|
|
619
|
+
|
|
573
620
|
function sameRecordEntries<T>(left: Record<string, T>, right: Record<string, T>): boolean {
|
|
574
621
|
const leftKeys = Object.keys(left);
|
|
575
622
|
const rightKeys = Object.keys(right);
|
|
@@ -1422,6 +1469,7 @@ function normalizePreviewSpeechMarkOffsets(
|
|
|
1422
1469
|
|
|
1423
1470
|
isApplying = true;
|
|
1424
1471
|
try {
|
|
1472
|
+
const appliedSpeedOptions = parseTTSSpeedOptionsFromText(speedOptionsText);
|
|
1425
1473
|
if (!isBuiltInTab(activeTab)) {
|
|
1426
1474
|
const provider = getCustomProviderOrThrow(activeTab);
|
|
1427
1475
|
let next: ProviderApplyResult | undefined;
|
|
@@ -1438,11 +1486,15 @@ function normalizePreviewSpeechMarkOffsets(
|
|
|
1438
1486
|
}
|
|
1439
1487
|
toolkitCoordinator.updateToolConfig("textToSpeech", {
|
|
1440
1488
|
enabled: true,
|
|
1441
|
-
...next.config
|
|
1489
|
+
...next.config,
|
|
1490
|
+
layoutMode,
|
|
1491
|
+
speedOptions: appliedSpeedOptions,
|
|
1442
1492
|
});
|
|
1443
1493
|
persistSettings({
|
|
1444
1494
|
backend: provider.id,
|
|
1445
|
-
...(next.config || {})
|
|
1495
|
+
...(next.config || {}),
|
|
1496
|
+
layoutMode,
|
|
1497
|
+
speedOptions: appliedSpeedOptions,
|
|
1446
1498
|
});
|
|
1447
1499
|
applyMessage = next.message || `Applied ${provider.label} TTS settings.`;
|
|
1448
1500
|
} else if (activeTab === "browser") {
|
|
@@ -1453,7 +1505,9 @@ function normalizePreviewSpeechMarkOffsets(
|
|
|
1453
1505
|
defaultVoice: resolveVoiceForBackend("browser"),
|
|
1454
1506
|
rate: normalizeRate(browserRate),
|
|
1455
1507
|
pitch: normalizePitch(browserPitch),
|
|
1456
|
-
transportMode: "pie" as const
|
|
1508
|
+
transportMode: "pie" as const,
|
|
1509
|
+
layoutMode,
|
|
1510
|
+
speedOptions: appliedSpeedOptions,
|
|
1457
1511
|
};
|
|
1458
1512
|
toolkitCoordinator.updateToolConfig("textToSpeech", {
|
|
1459
1513
|
enabled: true,
|
|
@@ -1480,7 +1534,9 @@ function normalizePreviewSpeechMarkOffsets(
|
|
|
1480
1534
|
sampleRate: normalizePollySampleRate(pollySampleRate),
|
|
1481
1535
|
format: pollyFormat,
|
|
1482
1536
|
speechMarkTypes: getPollySpeechMarkTypes()
|
|
1483
|
-
}
|
|
1537
|
+
},
|
|
1538
|
+
layoutMode,
|
|
1539
|
+
speedOptions: appliedSpeedOptions,
|
|
1484
1540
|
};
|
|
1485
1541
|
toolkitCoordinator.updateToolConfig("textToSpeech", {
|
|
1486
1542
|
enabled: true,
|
|
@@ -1499,7 +1555,9 @@ function normalizePreviewSpeechMarkOffsets(
|
|
|
1499
1555
|
rate: normalizeRate(googleRate),
|
|
1500
1556
|
language: googleLanguage || undefined,
|
|
1501
1557
|
googleVoiceType,
|
|
1502
|
-
googleGender
|
|
1558
|
+
googleGender,
|
|
1559
|
+
layoutMode,
|
|
1560
|
+
speedOptions: appliedSpeedOptions,
|
|
1503
1561
|
};
|
|
1504
1562
|
toolkitCoordinator.updateToolConfig("textToSpeech", {
|
|
1505
1563
|
enabled: true,
|
|
@@ -1602,6 +1660,48 @@ function normalizePreviewSpeechMarkOffsets(
|
|
|
1602
1660
|
</button>
|
|
1603
1661
|
</div>
|
|
1604
1662
|
|
|
1663
|
+
<div class="pie-tts-fieldset fieldset bg-base-200 border border-base-300 rounded-box">
|
|
1664
|
+
<div class="pie-tts-field">
|
|
1665
|
+
<label class="pie-tts-label" for="tts-layout-mode">Toolbar layout mode</label>
|
|
1666
|
+
<select
|
|
1667
|
+
id="tts-layout-mode"
|
|
1668
|
+
class="select select-sm select-bordered w-full"
|
|
1669
|
+
bind:value={layoutMode}
|
|
1670
|
+
>
|
|
1671
|
+
<option value="reserved-row">Reserved row</option>
|
|
1672
|
+
<option value="expanding-row">Expanding row</option>
|
|
1673
|
+
<option value="floating-overlay">Floating overlay</option>
|
|
1674
|
+
<option value="left-aligned">Left-aligned controls</option>
|
|
1675
|
+
</select>
|
|
1676
|
+
<div class="text-xs opacity-75">
|
|
1677
|
+
Item header row reservation: {layoutModeReservesRow ? "Enabled" : "Disabled"}
|
|
1678
|
+
</div>
|
|
1679
|
+
</div>
|
|
1680
|
+
<div class="pie-tts-field">
|
|
1681
|
+
<label class="pie-tts-label" for="tts-inline-speed-options">Inline speed buttons</label>
|
|
1682
|
+
<input
|
|
1683
|
+
id="tts-inline-speed-options"
|
|
1684
|
+
class="input input-sm input-bordered w-full"
|
|
1685
|
+
bind:value={speedOptionsText}
|
|
1686
|
+
placeholder="0.8, 1.25"
|
|
1687
|
+
autocomplete="off"
|
|
1688
|
+
/>
|
|
1689
|
+
<div class="mt-1 flex flex-wrap items-center gap-2">
|
|
1690
|
+
<span class="text-xs opacity-75">
|
|
1691
|
+
Comma or semicolon-separated multipliers (1× is not shown as a button). Leave empty to
|
|
1692
|
+
hide speed buttons.
|
|
1693
|
+
</span>
|
|
1694
|
+
<button
|
|
1695
|
+
type="button"
|
|
1696
|
+
class="btn btn-xs btn-ghost"
|
|
1697
|
+
onclick={resetInlineSpeedOptionsToDefaults}
|
|
1698
|
+
>
|
|
1699
|
+
Reset to defaults
|
|
1700
|
+
</button>
|
|
1701
|
+
</div>
|
|
1702
|
+
</div>
|
|
1703
|
+
</div>
|
|
1704
|
+
|
|
1605
1705
|
<div class="join pie-tts-tabs">
|
|
1606
1706
|
{#each providerTabs as provider}
|
|
1607
1707
|
<button
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function o(e) {
|
|
2
|
+
"@babel/helpers - typeof";
|
|
3
|
+
return o = typeof Symbol == "function" && typeof Symbol.iterator == "symbol" ? function(t) {
|
|
4
|
+
return typeof t;
|
|
5
|
+
} : function(t) {
|
|
6
|
+
return t && typeof Symbol == "function" && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t;
|
|
7
|
+
}, o(e);
|
|
8
|
+
}
|
|
9
|
+
function i(e, t) {
|
|
10
|
+
if (o(e) != "object" || !e) return e;
|
|
11
|
+
var r = e[Symbol.toPrimitive];
|
|
12
|
+
if (r !== void 0) {
|
|
13
|
+
var n = r.call(e, t || "default");
|
|
14
|
+
if (o(n) != "object") return n;
|
|
15
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
16
|
+
}
|
|
17
|
+
return (t === "string" ? String : Number)(e);
|
|
18
|
+
}
|
|
19
|
+
function u(e) {
|
|
20
|
+
var t = i(e, "string");
|
|
21
|
+
return o(t) == "symbol" ? t : t + "";
|
|
22
|
+
}
|
|
23
|
+
function f(e, t, r) {
|
|
24
|
+
return (t = u(t)) in e ? Object.defineProperty(e, t, {
|
|
25
|
+
value: r,
|
|
26
|
+
enumerable: !0,
|
|
27
|
+
configurable: !0,
|
|
28
|
+
writable: !0
|
|
29
|
+
}) : e[t] = r, e;
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
f as t
|
|
33
|
+
};
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { t as h } from "./defineProperty-CyepwRr5.js";
|
|
2
|
+
var b = (t) => {
|
|
3
|
+
const e = (t.providerOptions && typeof t.providerOptions == "object" ? t.providerOptions : {}).__pieTelemetry;
|
|
4
|
+
return typeof e == "function" ? e : void 0;
|
|
5
|
+
}, S = {
|
|
6
|
+
pie: 3e3,
|
|
7
|
+
custom: 3e3
|
|
8
|
+
}, v = (t) => t.replace(/\/+$/, ""), k = (t) => {
|
|
9
|
+
const e = v(t.apiEndpoint), r = (t.provider || "").toLowerCase();
|
|
10
|
+
return r === "polly" || r === "google" ? `${e}/${r}/voices` : `${e}/voices`;
|
|
11
|
+
}, T = (t) => t.transportMode === "custom" ? "custom" : t.transportMode === "pie" ? "pie" : t.provider === "custom" ? "custom" : "pie", w = (t, e) => t.endpointMode ? t.endpointMode : e === "custom" ? "rootPost" : "synthesizePath", A = (t, e) => t.endpointValidationMode ? t.endpointValidationMode : e === "custom" ? "none" : "voices", R = (t) => {
|
|
12
|
+
const e = t.providerOptions || {};
|
|
13
|
+
if (typeof e.speedRate == "string") return e.speedRate;
|
|
14
|
+
const r = Number(t.rate ?? 1);
|
|
15
|
+
return !Number.isFinite(r) || r <= 0.95 ? "slow" : r >= 1.5 ? "fast" : "medium";
|
|
16
|
+
}, I = (t) => {
|
|
17
|
+
const e = [];
|
|
18
|
+
let r = 0;
|
|
19
|
+
const i = t.split(`
|
|
20
|
+
`).map((o) => o.trim()).filter(Boolean);
|
|
21
|
+
for (const o of i) try {
|
|
22
|
+
const s = JSON.parse(o), a = typeof s.type == "string" ? s.type : "word", d = typeof s.time == "number" && Number.isFinite(s.time) ? s.time : 0, n = typeof s.value == "string" ? s.value : "", c = typeof s.start == "number" && Number.isFinite(s.start) ? s.start : null, u = typeof s.end == "number" && Number.isFinite(s.end) ? s.end : null, y = c ?? r, l = u ?? y + Math.max(1, n.length || String(s.value || "").length);
|
|
23
|
+
r = Math.max(l + 1, r), e.push({
|
|
24
|
+
time: d,
|
|
25
|
+
type: a,
|
|
26
|
+
start: y,
|
|
27
|
+
end: l,
|
|
28
|
+
value: n
|
|
29
|
+
});
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
return e;
|
|
33
|
+
}, M = (t) => {
|
|
34
|
+
if (!Array.isArray(t)) return [];
|
|
35
|
+
const e = [];
|
|
36
|
+
let r = 0;
|
|
37
|
+
for (const i of t) {
|
|
38
|
+
if (!i || typeof i != "object") continue;
|
|
39
|
+
const o = typeof i.type == "string" ? i.type : "word", s = typeof i.time == "number" && Number.isFinite(i.time) ? i.time : 0, a = typeof i.value == "string" ? i.value : "", d = typeof i.start == "number" && Number.isFinite(i.start) ? i.start : null, n = typeof i.end == "number" && Number.isFinite(i.end) ? i.end : null, c = d ?? r, u = n ?? c + Math.max(1, a.length || 1);
|
|
40
|
+
r = Math.max(r, u + 1), e.push({
|
|
41
|
+
time: s,
|
|
42
|
+
type: o,
|
|
43
|
+
start: c,
|
|
44
|
+
end: u,
|
|
45
|
+
value: a
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return e.sort((i, o) => i.time !== o.time ? i.time - o.time : i.start !== o.start ? i.start - o.start : i.end - o.end);
|
|
49
|
+
}, E = {
|
|
50
|
+
pie: {
|
|
51
|
+
id: "pie",
|
|
52
|
+
resolveSynthesisUrl: (t) => {
|
|
53
|
+
const e = w(t, "pie"), r = v(t.apiEndpoint);
|
|
54
|
+
return e === "rootPost" ? r : `${r}/synthesize`;
|
|
55
|
+
},
|
|
56
|
+
buildRequestBody: (t, e) => {
|
|
57
|
+
const r = e.providerOptions || {}, i = typeof e.engine == "string" ? e.engine : typeof r.engine == "string" ? r.engine : void 0, o = typeof r.sampleRate == "number" && Number.isFinite(r.sampleRate) ? r.sampleRate : void 0, s = r.format === "mp3" || r.format === "ogg" || r.format === "pcm" ? r.format : void 0, a = Array.isArray(r.speechMarkTypes) ? r.speechMarkTypes.filter((d) => d === "word" || d === "sentence" || d === "ssml") : void 0;
|
|
58
|
+
return {
|
|
59
|
+
text: t,
|
|
60
|
+
provider: e.provider || "polly",
|
|
61
|
+
voice: e.voice,
|
|
62
|
+
language: e.language,
|
|
63
|
+
rate: e.rate,
|
|
64
|
+
engine: i,
|
|
65
|
+
sampleRate: o,
|
|
66
|
+
format: s,
|
|
67
|
+
speechMarkTypes: a,
|
|
68
|
+
includeSpeechMarks: !0
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
parseResponse: async (t) => {
|
|
72
|
+
const e = await t.json();
|
|
73
|
+
return {
|
|
74
|
+
audio: {
|
|
75
|
+
kind: "base64",
|
|
76
|
+
data: e.audio,
|
|
77
|
+
contentType: e.contentType
|
|
78
|
+
},
|
|
79
|
+
speechMarks: Array.isArray(e.speechMarks) ? e.speechMarks : []
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
custom: {
|
|
84
|
+
id: "custom",
|
|
85
|
+
resolveSynthesisUrl: (t) => {
|
|
86
|
+
const e = w(t, "custom"), r = v(t.apiEndpoint);
|
|
87
|
+
return e === "synthesizePath" ? `${r}/synthesize` : r;
|
|
88
|
+
},
|
|
89
|
+
buildRequestBody: (t, e) => {
|
|
90
|
+
const r = e.providerOptions || {}, i = typeof r.lang_id == "string" ? r.lang_id : e.language || "en-US", o = typeof r.cache == "boolean" ? r.cache : !0;
|
|
91
|
+
return {
|
|
92
|
+
text: t,
|
|
93
|
+
speedRate: R(e),
|
|
94
|
+
lang_id: i,
|
|
95
|
+
cache: o
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
parseResponse: async (t, e, r, i) => {
|
|
99
|
+
const o = await t.json(), s = {};
|
|
100
|
+
if (e.includeAuthOnAssetFetch)
|
|
101
|
+
for (const [n, c] of Object.entries(r)) n.toLowerCase() === "authorization" && (s[n] = c);
|
|
102
|
+
let a = [];
|
|
103
|
+
const d = M(o.speechMarks);
|
|
104
|
+
if (d.length > 0) a = d;
|
|
105
|
+
else if (typeof o.word == "string" && o.word.length > 0) {
|
|
106
|
+
const n = await fetch(o.word, {
|
|
107
|
+
headers: s,
|
|
108
|
+
signal: i
|
|
109
|
+
});
|
|
110
|
+
n.ok && (a = I(await n.text()));
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
audio: {
|
|
114
|
+
kind: "url",
|
|
115
|
+
url: o.audioContent
|
|
116
|
+
},
|
|
117
|
+
speechMarks: a
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}, P = class {
|
|
122
|
+
constructor(t, e) {
|
|
123
|
+
h(this, "config", void 0), h(this, "adapter", void 0), h(this, "currentAudio", null), h(this, "pausedState", !1), h(this, "wordTimings", []), h(this, "highlightInterval", null), h(this, "intentionallyStopped", !1), h(this, "activeSynthesisController", null), h(this, "synthesisRunId", 0), h(this, "telemetryReporter", void 0), h(this, "onWordBoundary", void 0), this.config = t, this.adapter = e, this.telemetryReporter = b(t);
|
|
124
|
+
}
|
|
125
|
+
async emitTelemetry(t, e) {
|
|
126
|
+
try {
|
|
127
|
+
await this.telemetryReporter?.(t, e);
|
|
128
|
+
} catch (r) {
|
|
129
|
+
console.warn("[ServerTTSProvider] telemetry callback failed:", r);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async speak(t) {
|
|
133
|
+
this.stop(), this.intentionallyStopped = !1;
|
|
134
|
+
const e = ++this.synthesisRunId, r = new AbortController();
|
|
135
|
+
this.activeSynthesisController = r;
|
|
136
|
+
const { audioUrl: i, wordTimings: o } = await this.synthesizeSpeech(t, r.signal, e);
|
|
137
|
+
if (e !== this.synthesisRunId) {
|
|
138
|
+
URL.revokeObjectURL(i);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const s = this.config.rate || 1;
|
|
142
|
+
return this.wordTimings = o.map((a) => ({
|
|
143
|
+
...a,
|
|
144
|
+
time: a.time / s
|
|
145
|
+
})), new Promise((a, d) => {
|
|
146
|
+
const n = new Audio(i);
|
|
147
|
+
this.currentAudio = n, this.config.rate && (n.playbackRate = Math.max(0.25, Math.min(4, this.config.rate))), this.config.volume !== void 0 && (n.volume = Math.max(0, Math.min(1, this.config.volume))), n.onplay = () => {
|
|
148
|
+
this.pausedState = !1, this.onWordBoundary && this.wordTimings.length > 0 && this.startWordHighlighting();
|
|
149
|
+
}, n.onended = () => {
|
|
150
|
+
this.stopWordHighlighting(), URL.revokeObjectURL(i), this.currentAudio = null, this.wordTimings = [], a();
|
|
151
|
+
}, n.onerror = (c) => {
|
|
152
|
+
this.stopWordHighlighting(), URL.revokeObjectURL(i), this.currentAudio = null, this.wordTimings = [], this.intentionallyStopped ? a() : d(/* @__PURE__ */ new Error("Failed to play audio from server"));
|
|
153
|
+
}, n.onpause = () => {
|
|
154
|
+
this.stopWordHighlighting(), this.pausedState = !0;
|
|
155
|
+
}, n.play().catch(d);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async synthesizeSpeech(t, e, r) {
|
|
159
|
+
const i = Date.now();
|
|
160
|
+
await this.emitTelemetry("pie-tool-backend-call-start", {
|
|
161
|
+
toolId: "tts",
|
|
162
|
+
backend: this.config.provider || "server",
|
|
163
|
+
operation: "synthesize-speech"
|
|
164
|
+
});
|
|
165
|
+
const o = {
|
|
166
|
+
"Content-Type": "application/json",
|
|
167
|
+
...this.config.headers
|
|
168
|
+
};
|
|
169
|
+
this.config.authToken && (o.Authorization = `Bearer ${this.config.authToken}`);
|
|
170
|
+
const s = this.adapter.resolveSynthesisUrl(this.config), a = this.adapter.buildRequestBody(t, this.config), d = await (async () => {
|
|
171
|
+
try {
|
|
172
|
+
return await fetch(s, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: o,
|
|
175
|
+
body: JSON.stringify(a),
|
|
176
|
+
signal: e
|
|
177
|
+
});
|
|
178
|
+
} catch (l) {
|
|
179
|
+
throw await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
180
|
+
toolId: "tts",
|
|
181
|
+
backend: this.config.provider || "server",
|
|
182
|
+
operation: "synthesize-speech",
|
|
183
|
+
duration: Date.now() - i,
|
|
184
|
+
errorType: "TTSBackendNetworkError",
|
|
185
|
+
message: l instanceof Error ? l.message : String(l)
|
|
186
|
+
}), l;
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
if (!d.ok) {
|
|
190
|
+
const l = await d.json().catch(() => ({})), p = l.message || l.error?.message || `Server returned ${d.status}`;
|
|
191
|
+
throw await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
192
|
+
toolId: "tts",
|
|
193
|
+
backend: this.config.provider || "server",
|
|
194
|
+
operation: "synthesize-speech",
|
|
195
|
+
duration: Date.now() - i,
|
|
196
|
+
statusCode: d.status,
|
|
197
|
+
errorType: "TTSBackendRequestError",
|
|
198
|
+
message: p
|
|
199
|
+
}), new Error(p);
|
|
200
|
+
}
|
|
201
|
+
const n = await this.adapter.parseResponse(d, this.config, o, e);
|
|
202
|
+
if (r !== this.synthesisRunId || e.aborted) throw new Error("Synthesis superseded by a newer request");
|
|
203
|
+
let c;
|
|
204
|
+
if (n.audio.kind === "base64") c = this.base64ToBlob(n.audio.data, n.audio.contentType);
|
|
205
|
+
else {
|
|
206
|
+
const l = n.audio.url, p = Date.now();
|
|
207
|
+
await this.emitTelemetry("pie-tool-backend-call-start", {
|
|
208
|
+
toolId: "tts",
|
|
209
|
+
backend: this.config.provider || "server",
|
|
210
|
+
operation: "fetch-synthesized-audio-asset"
|
|
211
|
+
});
|
|
212
|
+
const f = {};
|
|
213
|
+
this.config.includeAuthOnAssetFetch && this.config.authToken && (f.Authorization = `Bearer ${this.config.authToken}`);
|
|
214
|
+
const m = await (async () => {
|
|
215
|
+
try {
|
|
216
|
+
return await fetch(l, {
|
|
217
|
+
headers: f,
|
|
218
|
+
signal: e
|
|
219
|
+
});
|
|
220
|
+
} catch (g) {
|
|
221
|
+
throw await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
222
|
+
toolId: "tts",
|
|
223
|
+
backend: this.config.provider || "server",
|
|
224
|
+
operation: "fetch-synthesized-audio-asset",
|
|
225
|
+
duration: Date.now() - p,
|
|
226
|
+
errorType: "TTSAssetNetworkError",
|
|
227
|
+
message: g instanceof Error ? g.message : String(g)
|
|
228
|
+
}), g;
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
231
|
+
if (!m.ok)
|
|
232
|
+
throw await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
233
|
+
toolId: "tts",
|
|
234
|
+
backend: this.config.provider || "server",
|
|
235
|
+
operation: "fetch-synthesized-audio-asset",
|
|
236
|
+
duration: Date.now() - p,
|
|
237
|
+
statusCode: m.status,
|
|
238
|
+
errorType: "TTSAssetFetchError",
|
|
239
|
+
message: `Failed to download synthesized audio (${m.status})`
|
|
240
|
+
}), new Error(`Failed to download synthesized audio (${m.status})`);
|
|
241
|
+
c = await m.blob(), await this.emitTelemetry("pie-tool-backend-call-success", {
|
|
242
|
+
toolId: "tts",
|
|
243
|
+
backend: this.config.provider || "server",
|
|
244
|
+
operation: "fetch-synthesized-audio-asset",
|
|
245
|
+
duration: Date.now() - p
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const u = URL.createObjectURL(c), y = this.parseSpeechMarks(n.speechMarks);
|
|
249
|
+
return await this.emitTelemetry("pie-tool-backend-call-success", {
|
|
250
|
+
toolId: "tts",
|
|
251
|
+
backend: this.config.provider || "server",
|
|
252
|
+
operation: "synthesize-speech",
|
|
253
|
+
duration: Date.now() - i
|
|
254
|
+
}), {
|
|
255
|
+
audioUrl: u,
|
|
256
|
+
wordTimings: y
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
base64ToBlob(t, e) {
|
|
260
|
+
const r = atob(t), i = new Array(r.length);
|
|
261
|
+
for (let o = 0; o < r.length; o++) i[o] = r.charCodeAt(o);
|
|
262
|
+
return new Blob([new Uint8Array(i)], { type: e });
|
|
263
|
+
}
|
|
264
|
+
parseSpeechMarks(t) {
|
|
265
|
+
return t.filter((e) => e.type === "word").map((e, r) => ({
|
|
266
|
+
time: e.time,
|
|
267
|
+
wordIndex: r,
|
|
268
|
+
charIndex: e.start,
|
|
269
|
+
length: e.end - e.start
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
startWordHighlighting() {
|
|
273
|
+
if (this.stopWordHighlighting(), !this.currentAudio || !this.onWordBoundary || this.wordTimings.length === 0) {
|
|
274
|
+
console.log("[ServerTTSProvider] Cannot start highlighting:", {
|
|
275
|
+
hasAudio: !!this.currentAudio,
|
|
276
|
+
hasCallback: !!this.onWordBoundary,
|
|
277
|
+
wordTimingsCount: this.wordTimings.length
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
console.log("[ServerTTSProvider] Starting word highlighting with", this.wordTimings.length, "word timings"), console.log("[ServerTTSProvider] Playback rate:", this.currentAudio.playbackRate), console.log("[ServerTTSProvider] First 3 timings:", this.wordTimings.slice(0, 3));
|
|
282
|
+
let t = -1;
|
|
283
|
+
this.highlightInterval = window.setInterval(() => {
|
|
284
|
+
if (!this.currentAudio) {
|
|
285
|
+
this.stopWordHighlighting();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const e = this.currentAudio.currentTime * 1e3;
|
|
289
|
+
for (let r = 0; r < this.wordTimings.length; r++) {
|
|
290
|
+
const i = this.wordTimings[r];
|
|
291
|
+
if (e >= i.time && r > t) {
|
|
292
|
+
this.onWordBoundary && (console.log("[ServerTTSProvider] Highlighting word at charIndex:", i.charIndex, "length:", i.length, "time:", i.time, "currentTime:", e), this.onWordBoundary("", i.charIndex, i.length)), t = r;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}, 50);
|
|
297
|
+
}
|
|
298
|
+
stopWordHighlighting() {
|
|
299
|
+
this.highlightInterval !== null && (clearInterval(this.highlightInterval), this.highlightInterval = null);
|
|
300
|
+
}
|
|
301
|
+
pause() {
|
|
302
|
+
this.currentAudio && !this.pausedState && (this.currentAudio.pause(), this.stopWordHighlighting(), this.pausedState = !0);
|
|
303
|
+
}
|
|
304
|
+
resume() {
|
|
305
|
+
this.currentAudio && this.pausedState && (this.currentAudio.play(), this.pausedState = !1, this.onWordBoundary && this.wordTimings.length > 0 && this.startWordHighlighting());
|
|
306
|
+
}
|
|
307
|
+
stop() {
|
|
308
|
+
this.synthesisRunId += 1, this.activeSynthesisController && (this.activeSynthesisController.abort(), this.activeSynthesisController = null), this.stopWordHighlighting(), this.currentAudio && (this.intentionallyStopped = !0, this.currentAudio.pause(), this.currentAudio.src && URL.revokeObjectURL(this.currentAudio.src), this.currentAudio.src = "", this.currentAudio = null), this.pausedState = !1, this.wordTimings = [];
|
|
309
|
+
}
|
|
310
|
+
isPlaying() {
|
|
311
|
+
return this.currentAudio !== null && !this.pausedState;
|
|
312
|
+
}
|
|
313
|
+
isPaused() {
|
|
314
|
+
return this.pausedState;
|
|
315
|
+
}
|
|
316
|
+
updateSettings(t) {
|
|
317
|
+
t.rate !== void 0 && (this.config.rate = t.rate, this.currentAudio && (this.currentAudio.playbackRate = Math.max(0.25, Math.min(4, t.rate)))), t.pitch !== void 0 && (this.config.pitch = t.pitch), t.voice !== void 0 && (this.config.voice = t.voice);
|
|
318
|
+
}
|
|
319
|
+
}, B = class {
|
|
320
|
+
constructor() {
|
|
321
|
+
h(this, "providerId", "server-tts"), h(this, "providerName", "Server TTS"), h(this, "version", "1.0.0"), h(this, "config", null), h(this, "adapter", null), h(this, "telemetryReporter", void 0);
|
|
322
|
+
}
|
|
323
|
+
async emitTelemetry(t, e) {
|
|
324
|
+
try {
|
|
325
|
+
await this.telemetryReporter?.(t, e);
|
|
326
|
+
} catch (r) {
|
|
327
|
+
console.warn("[ServerTTSProvider] telemetry callback failed:", r);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async initialize(t) {
|
|
331
|
+
const e = t;
|
|
332
|
+
if (!e.apiEndpoint) throw new Error("apiEndpoint is required for ServerTTSProvider");
|
|
333
|
+
if (this.config = e, this.telemetryReporter = b(e), this.adapter = E[T(e)], e.validateEndpoint) {
|
|
334
|
+
const r = Date.now();
|
|
335
|
+
if (await this.emitTelemetry("pie-tool-backend-call-start", {
|
|
336
|
+
toolId: "tts",
|
|
337
|
+
backend: e.provider || "server",
|
|
338
|
+
operation: "validate-endpoint"
|
|
339
|
+
}), !await this.testAPIAvailability())
|
|
340
|
+
throw await this.emitTelemetry("pie-tool-backend-call-error", {
|
|
341
|
+
toolId: "tts",
|
|
342
|
+
backend: e.provider || "server",
|
|
343
|
+
operation: "validate-endpoint",
|
|
344
|
+
duration: Date.now() - r,
|
|
345
|
+
errorType: "TTSEndpointValidationError",
|
|
346
|
+
message: `Server TTS API not available at ${e.apiEndpoint}`
|
|
347
|
+
}), new Error(`Server TTS API not available at ${e.apiEndpoint}`);
|
|
348
|
+
await this.emitTelemetry("pie-tool-backend-call-success", {
|
|
349
|
+
toolId: "tts",
|
|
350
|
+
backend: e.provider || "server",
|
|
351
|
+
operation: "validate-endpoint",
|
|
352
|
+
duration: Date.now() - r
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return new P(e, this.adapter);
|
|
356
|
+
}
|
|
357
|
+
async testAPIAvailability() {
|
|
358
|
+
if (!this.config || !this.adapter) return !1;
|
|
359
|
+
try {
|
|
360
|
+
const t = { ...this.config.headers };
|
|
361
|
+
this.config.authToken && (t.Authorization = `Bearer ${this.config.authToken}`);
|
|
362
|
+
const e = new AbortController(), r = setTimeout(() => e.abort(), 5e3), i = A(this.config, this.adapter.id);
|
|
363
|
+
if (i === "none")
|
|
364
|
+
return clearTimeout(r), !0;
|
|
365
|
+
const o = i === "voices" ? k(this.config) : this.adapter.resolveSynthesisUrl(this.config), s = i === "voices" ? "GET" : "OPTIONS";
|
|
366
|
+
try {
|
|
367
|
+
const a = await fetch(o, {
|
|
368
|
+
method: s,
|
|
369
|
+
headers: t,
|
|
370
|
+
signal: e.signal
|
|
371
|
+
});
|
|
372
|
+
return clearTimeout(r), a.ok || a.status === 405;
|
|
373
|
+
} catch {
|
|
374
|
+
return clearTimeout(r), !1;
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
return !1;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
supportsFeature(t) {
|
|
381
|
+
switch (t) {
|
|
382
|
+
case "pause":
|
|
383
|
+
case "resume":
|
|
384
|
+
case "wordBoundary":
|
|
385
|
+
case "voiceSelection":
|
|
386
|
+
case "rateControl":
|
|
387
|
+
return !0;
|
|
388
|
+
case "pitchControl":
|
|
389
|
+
return !1;
|
|
390
|
+
default:
|
|
391
|
+
return !1;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
getCapabilities() {
|
|
395
|
+
return {
|
|
396
|
+
supportsPause: !0,
|
|
397
|
+
supportsResume: !0,
|
|
398
|
+
supportsWordBoundary: !0,
|
|
399
|
+
supportsVoiceSelection: !0,
|
|
400
|
+
supportsRateControl: !0,
|
|
401
|
+
supportsPitchControl: !1,
|
|
402
|
+
maxTextLength: S[this.config ? T(this.config) : "pie"]
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
destroy() {
|
|
406
|
+
this.config = null, this.adapter = null, this.telemetryReporter = void 0;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
export {
|
|
410
|
+
B as ServerTTSProvider
|
|
411
|
+
};
|