@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.
@@ -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
+ };