@pie-players/pie-section-player 0.3.5 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -148,6 +148,62 @@ function canAdvance() {
148
148
  }
149
149
  ```
150
150
 
151
+ If you already have a `ToolkitCoordinator` from `toolkit-ready`, prefer helper subscriptions for host logic:
152
+
153
+ ```ts
154
+ const unsubscribeItem = coordinator.subscribeItemEvents({
155
+ sectionId,
156
+ attemptId,
157
+ listener: (event: any) => {
158
+ // item-scoped stream
159
+ },
160
+ });
161
+
162
+ const unsubscribeSection = coordinator.subscribeSectionLifecycleEvents({
163
+ sectionId,
164
+ attemptId,
165
+ listener: (event: any) => {
166
+ // section-loading-complete / section-items-complete-changed / section-error / section-navigation-change
167
+ },
168
+ });
169
+ ```
170
+
171
+ Use `subscribeSectionEvents(...)` only for advanced mixed filtering requirements.
172
+
173
+ ### Item session management
174
+
175
+ Section session data can be managed either through persistence hooks or directly through the controller API.
176
+
177
+ ```ts
178
+ const host = document.querySelector("pie-section-player-splitpane") as any;
179
+ const controller = await host.waitForSectionController?.(5000);
180
+
181
+ // Read current section session snapshot.
182
+ const currentSession = controller?.getSession?.();
183
+
184
+ // Replace section session state (resume from backend snapshot).
185
+ await controller?.applySession?.({
186
+ currentItemIndex: 0,
187
+ visitedItemIdentifiers: ["q1"],
188
+ itemSessions: {
189
+ q1: {
190
+ itemIdentifier: "q1",
191
+ pieSessionId: "q1-session",
192
+ session: { id: "q1-session", data: [{ id: "choice", value: "a" }] }
193
+ }
194
+ }
195
+ }, { mode: "replace" });
196
+
197
+ // Update a single item session directly.
198
+ await controller?.updateItemSession?.("q1", {
199
+ session: { id: "q1-session", data: [{ id: "choice", value: "b" }] },
200
+ complete: true,
201
+ });
202
+ ```
203
+
204
+ The same controller snapshot is what the persistence strategy saves/loads.
205
+ When a controller is reused for the same `sectionId`/`attemptId`, `updateInput()` refreshes composition input while preserving in-memory section session data.
206
+
151
207
  ## Exports
152
208
 
153
209
  Published exports are intentionally minimal:
@@ -1,13 +1,12 @@
1
1
  import { TestAttemptSession } from '@pie-players/pie-assessment-toolkit';
2
- import { SectionControllerContext, SectionControllerHandle, SectionControllerPersistenceStrategy, SectionControllerRuntimeState, SectionControllerSessionState } from './toolkit-section-contracts.js';
2
+ import { SectionControllerHandle, SectionSessionPersistenceConfig, SectionControllerRuntimeState, SectionControllerSessionState } from './toolkit-section-contracts.js';
3
3
  import { ItemEntity } from '@pie-players/pie-players-shared';
4
4
  import { NavigationResult, SectionControllerChangeListener, SectionCompositionModel, SectionNavigationState, SectionViewModel, SessionChangedResult } from './types.js';
5
5
  export declare class SectionController implements SectionControllerHandle {
6
6
  private readonly contentService;
7
7
  private readonly sessionService;
8
8
  private readonly itemNavigationService;
9
- private persistenceStrategy;
10
- private persistenceContext;
9
+ private sessionPersistence;
11
10
  private state;
12
11
  private readonly listeners;
13
12
  private readonly trackedRenderables;
@@ -23,8 +22,7 @@ export declare class SectionController implements SectionControllerHandle {
23
22
  subscribe(listener: SectionControllerChangeListener): () => void;
24
23
  initialize(input?: unknown): Promise<void>;
25
24
  updateInput(input?: unknown): Promise<void>;
26
- setPersistenceStrategy(strategy: SectionControllerPersistenceStrategy): Promise<void>;
27
- setPersistenceContext(context: SectionControllerContext): void;
25
+ configureSessionPersistence(config: SectionSessionPersistenceConfig): void;
28
26
  hydrate(): Promise<void>;
29
27
  persist(): Promise<void>;
30
28
  dispose(): void;
@@ -47,18 +45,21 @@ export declare class SectionController implements SectionControllerHandle {
47
45
  * Use this for serializing/restoring section session state across reloads.
48
46
  * This is intentionally compact and not a full runtime diagnostics view.
49
47
  */
50
- getSessionState(): SectionControllerSessionState | null;
48
+ getSession(): SectionControllerSessionState | null;
51
49
  private getSectionItemIdentifiers;
52
50
  /**
53
51
  * Runtime/debugger shape scoped to the current section.
54
52
  * Use this for widgets that need a section-scoped live snapshot (debug panels, diagnostics).
55
- * Unlike getSessionState(), this is optimized for runtime introspection, not host persistence.
53
+ * Unlike getSession(), this is optimized for runtime introspection, not host persistence.
56
54
  */
57
55
  getRuntimeState(): SectionControllerRuntimeState | null;
58
56
  getCurrentItem(): ItemEntity | null;
59
57
  getCurrentItemSession(): unknown;
60
58
  getNavigationState(isLoading?: boolean): SectionNavigationState;
61
- handleItemSessionChanged(itemId: string, sessionDetail: any): SessionChangedResult | null;
59
+ updateItemSession(itemId: string, sessionDetail: any): SessionChangedResult | null;
60
+ applySession(session: SectionControllerSessionState | null, options?: {
61
+ mode?: "replace" | "merge";
62
+ }): Promise<void>;
62
63
  /**
63
64
  * Move between items inside the current section only.
64
65
  * Cross-section navigation belongs to the higher-level assessment player.
@@ -1 +1 @@
1
- {"version":3,"file":"SectionController.d.ts","sourceRoot":"","sources":["../../src/controllers/SectionController.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SectionController.d.ts","sourceRoot":"","sources":["../../src/controllers/SectionController.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { ToolkitCoordinator, SectionControllerHandle } from '@pie-players/pie-assessment-toolkit';
2
- export type { SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionControllerHandle, SectionControllerKey, SectionControllerPersistenceStrategy, SectionControllerRuntimeState, SectionControllerSessionState, } from '@pie-players/pie-assessment-toolkit';
2
+ export type { SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionControllerHandle, SectionControllerKey, SectionSessionPersistenceConfig, SectionSessionPersistenceStrategy, SectionControllerRuntimeState, SectionControllerSessionState, } from '@pie-players/pie-assessment-toolkit';
3
3
  export type CoordinatorWithSectionControllers = ToolkitCoordinator & {
4
4
  getOrCreateSectionController(args: {
5
5
  sectionId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"toolkit-section-contracts.d.ts","sourceRoot":"","sources":["../../src/controllers/toolkit-section-contracts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,YAAY,EACX,wBAAwB,EACxB,sBAAsB,EACtB,0BAA0B,EAC1B,uBAAuB,EACvB,oBAAoB,EACpB,oCAAoC,EACpC,6BAA6B,EAC7B,6BAA6B,GAC7B,MAAM,qCAAqC,CAAC;AAE7C,MAAM,MAAM,iCAAiC,GAAG,kBAAkB,GAAG;IACpE,4BAA4B,CAAC,IAAI,EAAE;QAClC,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,uBAAuB,EAAE,MACtB,uBAAuB,GACvB,OAAO,CAAC,uBAAuB,CAAC,CAAC;KACpC,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACrC,wBAAwB,CAAC,IAAI,EAAE;QAC9B,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,oBAAoB,CAAC,EAAE,OAAO,CAAC;QAC/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;KAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB,CAAC"}
1
+ {"version":3,"file":"toolkit-section-contracts.d.ts","sourceRoot":"","sources":["../../src/controllers/toolkit-section-contracts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,YAAY,EACX,wBAAwB,EACxB,sBAAsB,EACtB,0BAA0B,EAC1B,uBAAuB,EACvB,oBAAoB,EACpB,+BAA+B,EAC/B,iCAAiC,EACjC,6BAA6B,EAC7B,6BAA6B,GAC7B,MAAM,qCAAqC,CAAC;AAE7C,MAAM,MAAM,iCAAiC,GAAG,kBAAkB,GAAG;IACpE,4BAA4B,CAAC,IAAI,EAAE;QAClC,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,uBAAuB,EAAE,MACtB,uBAAuB,GACvB,OAAO,CAAC,uBAAuB,CAAC,CAAC;KACpC,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACrC,wBAAwB,CAAC,IAAI,EAAE;QAC9B,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,oBAAoB,CAAC,EAAE,OAAO,CAAC;QAC/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;KAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB,CAAC"}
@@ -0,0 +1,338 @@
1
+ var v = Object.defineProperty, w = (i, t, e) => t in i ? v(i, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : i[t] = e, u = (i, t, e) => w(i, typeof t != "symbol" ? t + "" : t, e);
2
+ const T = {
3
+ pie: 3e3,
4
+ custom: 3e3
5
+ }, m = (i) => i.replace(/\/+$/, ""), f = (i) => i.transportMode === "custom" ? "custom" : i.transportMode === "pie" ? "pie" : i.provider === "custom" ? "custom" : "pie", y = (i, t) => i.endpointMode ? i.endpointMode : t === "custom" ? "rootPost" : "synthesizePath", S = (i, t) => i.endpointValidationMode ? i.endpointValidationMode : t === "custom" ? "none" : "voices", b = (i) => {
6
+ const t = i.providerOptions || {};
7
+ if (typeof t.speedRate == "string")
8
+ return t.speedRate;
9
+ const e = Number(i.rate ?? 1);
10
+ return !Number.isFinite(e) || e <= 0.95 ? "slow" : e >= 1.5 ? "fast" : "medium";
11
+ }, A = (i) => {
12
+ const t = [];
13
+ let e = 0;
14
+ const s = i.split(`
15
+ `).map((r) => r.trim()).filter(Boolean);
16
+ for (const r of s)
17
+ try {
18
+ const n = JSON.parse(r), h = typeof n.type == "string" ? n.type : "word", o = typeof n.time == "number" && Number.isFinite(n.time) ? n.time : 0, a = typeof n.value == "string" ? n.value : "", d = typeof n.start == "number" && Number.isFinite(n.start) ? n.start : null, p = typeof n.end == "number" && Number.isFinite(n.end) ? n.end : null, g = d ?? e, l = p ?? g + Math.max(1, a.length || String(n.value || "").length);
19
+ e = Math.max(l + 1, e), t.push({
20
+ time: o,
21
+ type: h,
22
+ start: g,
23
+ end: l,
24
+ value: a
25
+ });
26
+ } catch {
27
+ }
28
+ return t;
29
+ }, k = {
30
+ id: "pie",
31
+ resolveSynthesisUrl: (i) => {
32
+ const t = y(i, "pie"), e = m(i.apiEndpoint);
33
+ return t === "rootPost" ? e : `${e}/synthesize`;
34
+ },
35
+ buildRequestBody: (i, t) => {
36
+ const e = t.providerOptions || {}, s = typeof t.engine == "string" ? t.engine : typeof e.engine == "string" ? e.engine : void 0, r = typeof e.sampleRate == "number" && Number.isFinite(e.sampleRate) ? e.sampleRate : void 0, n = e.format === "mp3" || e.format === "ogg" || e.format === "pcm" ? e.format : void 0, h = Array.isArray(e.speechMarkTypes) ? e.speechMarkTypes.filter((o) => o === "word" || o === "sentence" || o === "ssml") : void 0;
37
+ return {
38
+ text: i,
39
+ provider: t.provider || "polly",
40
+ voice: t.voice,
41
+ language: t.language,
42
+ rate: t.rate,
43
+ engine: s,
44
+ sampleRate: r,
45
+ format: n,
46
+ speechMarkTypes: h,
47
+ includeSpeechMarks: !0
48
+ };
49
+ },
50
+ parseResponse: async (i) => {
51
+ const t = await i.json();
52
+ return {
53
+ audio: {
54
+ kind: "base64",
55
+ data: t.audio,
56
+ contentType: t.contentType
57
+ },
58
+ speechMarks: Array.isArray(t.speechMarks) ? t.speechMarks : []
59
+ };
60
+ }
61
+ }, R = {
62
+ id: "custom",
63
+ resolveSynthesisUrl: (i) => {
64
+ const t = y(i, "custom"), e = m(i.apiEndpoint);
65
+ return t === "synthesizePath" ? `${e}/synthesize` : e;
66
+ },
67
+ buildRequestBody: (i, t) => {
68
+ const e = t.providerOptions || {}, s = typeof e.lang_id == "string" ? e.lang_id : t.language || "en-US", r = typeof e.cache == "boolean" ? e.cache : !0;
69
+ return {
70
+ text: i,
71
+ speedRate: b(t),
72
+ lang_id: s,
73
+ cache: r
74
+ };
75
+ },
76
+ parseResponse: async (i, t, e, s) => {
77
+ const r = await i.json(), n = {};
78
+ if (t.includeAuthOnAssetFetch)
79
+ for (const [o, a] of Object.entries(e))
80
+ o.toLowerCase() === "authorization" && (n[o] = a);
81
+ let h = [];
82
+ if (typeof r.word == "string" && r.word.length > 0) {
83
+ const o = await fetch(r.word, {
84
+ headers: n,
85
+ signal: s
86
+ });
87
+ if (o.ok) {
88
+ const a = await o.text();
89
+ h = A(a);
90
+ }
91
+ }
92
+ return {
93
+ audio: {
94
+ kind: "url",
95
+ url: r.audioContent
96
+ },
97
+ speechMarks: h
98
+ };
99
+ }
100
+ }, M = {
101
+ pie: k,
102
+ custom: R
103
+ };
104
+ class I {
105
+ constructor(t, e) {
106
+ u(this, "config"), u(this, "adapter"), u(this, "currentAudio", null), u(this, "pausedState", !1), u(this, "wordTimings", []), u(this, "highlightInterval", null), u(this, "intentionallyStopped", !1), u(this, "activeSynthesisController", null), u(this, "synthesisRunId", 0), u(this, "onWordBoundary"), this.config = t, this.adapter = e;
107
+ }
108
+ async speak(t) {
109
+ this.stop(), this.intentionallyStopped = !1;
110
+ const e = ++this.synthesisRunId, s = new AbortController();
111
+ this.activeSynthesisController = s;
112
+ const { audioUrl: r, wordTimings: n } = await this.synthesizeSpeech(t, s.signal, e);
113
+ if (e !== this.synthesisRunId) {
114
+ URL.revokeObjectURL(r);
115
+ return;
116
+ }
117
+ const h = this.config.rate || 1;
118
+ return this.wordTimings = n.map((o) => ({
119
+ ...o,
120
+ time: o.time / h
121
+ })), new Promise((o, a) => {
122
+ const d = new Audio(r);
123
+ this.currentAudio = d, this.config.rate && (d.playbackRate = Math.max(0.25, Math.min(4, this.config.rate))), this.config.volume !== void 0 && (d.volume = Math.max(0, Math.min(1, this.config.volume))), d.onplay = () => {
124
+ this.pausedState = !1, this.onWordBoundary && this.wordTimings.length > 0 && this.startWordHighlighting();
125
+ }, d.onended = () => {
126
+ this.stopWordHighlighting(), URL.revokeObjectURL(r), this.currentAudio = null, this.wordTimings = [], o();
127
+ }, d.onerror = (p) => {
128
+ this.stopWordHighlighting(), URL.revokeObjectURL(r), this.currentAudio = null, this.wordTimings = [], this.intentionallyStopped ? o() : a(new Error("Failed to play audio from server"));
129
+ }, d.onpause = () => {
130
+ this.stopWordHighlighting(), this.pausedState = !0;
131
+ }, d.play().catch(a);
132
+ });
133
+ }
134
+ /**
135
+ * Call server API to synthesize speech
136
+ */
137
+ async synthesizeSpeech(t, e, s) {
138
+ const r = {
139
+ "Content-Type": "application/json",
140
+ ...this.config.headers
141
+ };
142
+ this.config.authToken && (r.Authorization = `Bearer ${this.config.authToken}`);
143
+ const n = this.adapter.resolveSynthesisUrl(this.config), h = this.adapter.buildRequestBody(t, this.config), o = await fetch(n, {
144
+ method: "POST",
145
+ headers: r,
146
+ body: JSON.stringify(h),
147
+ signal: e
148
+ });
149
+ if (!o.ok) {
150
+ const l = await o.json().catch(() => ({})), c = l.message || l.error?.message || `Server returned ${o.status}`;
151
+ throw new Error(c);
152
+ }
153
+ const a = await this.adapter.parseResponse(o, this.config, r, e);
154
+ if (s !== this.synthesisRunId || e.aborted)
155
+ throw new Error("Synthesis superseded by a newer request");
156
+ let d;
157
+ if (a.audio.kind === "base64")
158
+ d = this.base64ToBlob(a.audio.data, a.audio.contentType);
159
+ else {
160
+ const l = {};
161
+ this.config.includeAuthOnAssetFetch && this.config.authToken && (l.Authorization = `Bearer ${this.config.authToken}`);
162
+ const c = await fetch(a.audio.url, {
163
+ headers: l,
164
+ signal: e
165
+ });
166
+ if (!c.ok)
167
+ throw new Error(`Failed to download synthesized audio (${c.status})`);
168
+ d = await c.blob();
169
+ }
170
+ const p = URL.createObjectURL(d), g = this.parseSpeechMarks(a.speechMarks);
171
+ return { audioUrl: p, wordTimings: g };
172
+ }
173
+ /**
174
+ * Convert base64 to Blob
175
+ */
176
+ base64ToBlob(t, e) {
177
+ const s = atob(t), r = new Array(s.length);
178
+ for (let h = 0; h < s.length; h++)
179
+ r[h] = s.charCodeAt(h);
180
+ const n = new Uint8Array(r);
181
+ return new Blob([n], { type: e });
182
+ }
183
+ /**
184
+ * Parse speech marks into word timings
185
+ */
186
+ parseSpeechMarks(t) {
187
+ return t.filter((e) => e.type === "word").map((e, s) => ({
188
+ time: e.time,
189
+ wordIndex: s,
190
+ charIndex: e.start,
191
+ length: e.end - e.start
192
+ }));
193
+ }
194
+ /**
195
+ * Start word highlighting synchronized with audio playback
196
+ */
197
+ startWordHighlighting() {
198
+ if (this.stopWordHighlighting(), !this.currentAudio || !this.onWordBoundary || this.wordTimings.length === 0) {
199
+ console.log("[ServerTTSProvider] Cannot start highlighting:", {
200
+ hasAudio: !!this.currentAudio,
201
+ hasCallback: !!this.onWordBoundary,
202
+ wordTimingsCount: this.wordTimings.length
203
+ });
204
+ return;
205
+ }
206
+ 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));
207
+ let t = -1;
208
+ this.highlightInterval = window.setInterval(() => {
209
+ if (!this.currentAudio) {
210
+ this.stopWordHighlighting();
211
+ return;
212
+ }
213
+ const e = this.currentAudio.currentTime * 1e3;
214
+ for (let s = 0; s < this.wordTimings.length; s++) {
215
+ const r = this.wordTimings[s];
216
+ if (e >= r.time && s > t) {
217
+ this.onWordBoundary && (console.log("[ServerTTSProvider] Highlighting word at charIndex:", r.charIndex, "length:", r.length, "time:", r.time, "currentTime:", e), this.onWordBoundary("", r.charIndex, r.length)), t = s;
218
+ break;
219
+ }
220
+ }
221
+ }, 50);
222
+ }
223
+ /**
224
+ * Stop word highlighting
225
+ */
226
+ stopWordHighlighting() {
227
+ this.highlightInterval !== null && (clearInterval(this.highlightInterval), this.highlightInterval = null);
228
+ }
229
+ pause() {
230
+ this.currentAudio && !this.pausedState && (this.currentAudio.pause(), this.stopWordHighlighting(), this.pausedState = !0);
231
+ }
232
+ resume() {
233
+ this.currentAudio && this.pausedState && (this.currentAudio.play(), this.pausedState = !1, this.onWordBoundary && this.wordTimings.length > 0 && this.startWordHighlighting());
234
+ }
235
+ stop() {
236
+ 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 = [];
237
+ }
238
+ isPlaying() {
239
+ return this.currentAudio !== null && !this.pausedState;
240
+ }
241
+ isPaused() {
242
+ return this.pausedState;
243
+ }
244
+ /**
245
+ * Update settings dynamically (rate, pitch, voice)
246
+ * Note: Voice changes require resynthesis, so voice updates are stored but
247
+ * take effect on the next speak() call. Rate can be applied to current playback.
248
+ */
249
+ updateSettings(t) {
250
+ 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);
251
+ }
252
+ }
253
+ class P {
254
+ constructor() {
255
+ u(this, "providerId", "server-tts"), u(this, "providerName", "Server TTS"), u(this, "version", "1.0.0"), u(this, "config", null), u(this, "adapter", null);
256
+ }
257
+ /**
258
+ * Initialize the server TTS provider.
259
+ *
260
+ * This is designed to be fast by default (no API calls).
261
+ * Set validateEndpoint: true in config to test API availability during initialization.
262
+ *
263
+ * @performance Default: <10ms, With validation: 100-500ms
264
+ */
265
+ async initialize(t) {
266
+ const e = t;
267
+ if (!e.apiEndpoint)
268
+ throw new Error("apiEndpoint is required for ServerTTSProvider");
269
+ this.config = e;
270
+ const s = f(e);
271
+ if (this.adapter = M[s], e.validateEndpoint && !await this.testAPIAvailability())
272
+ throw new Error(`Server TTS API not available at ${e.apiEndpoint}`);
273
+ return new I(e, this.adapter);
274
+ }
275
+ /**
276
+ * Test if API endpoint is available (with timeout).
277
+ *
278
+ * @performance 100-500ms depending on network
279
+ */
280
+ async testAPIAvailability() {
281
+ if (!this.config || !this.adapter)
282
+ return !1;
283
+ try {
284
+ const t = { ...this.config.headers };
285
+ this.config.authToken && (t.Authorization = `Bearer ${this.config.authToken}`);
286
+ const e = new AbortController(), s = setTimeout(() => e.abort(), 5e3), r = S(this.config, this.adapter.id);
287
+ if (r === "none")
288
+ return clearTimeout(s), !0;
289
+ const n = m(this.config.apiEndpoint), h = r === "voices" ? `${n}/voices` : this.adapter.resolveSynthesisUrl(this.config), o = r === "voices" ? "GET" : "OPTIONS";
290
+ try {
291
+ const a = await fetch(h, {
292
+ method: o,
293
+ headers: t,
294
+ signal: e.signal
295
+ });
296
+ return clearTimeout(s), a.ok || a.status === 405;
297
+ } catch {
298
+ return clearTimeout(s), !1;
299
+ }
300
+ } catch {
301
+ return !1;
302
+ }
303
+ }
304
+ supportsFeature(t) {
305
+ switch (t) {
306
+ case "pause":
307
+ case "resume":
308
+ case "wordBoundary":
309
+ case "voiceSelection":
310
+ case "rateControl":
311
+ return !0;
312
+ case "pitchControl":
313
+ return !1;
314
+ default:
315
+ return !1;
316
+ }
317
+ }
318
+ getCapabilities() {
319
+ const t = this.config ? f(this.config) : "pie";
320
+ return {
321
+ supportsPause: !0,
322
+ supportsResume: !0,
323
+ supportsWordBoundary: !0,
324
+ // ✅ Via speech marks from server
325
+ supportsVoiceSelection: !0,
326
+ supportsRateControl: !0,
327
+ supportsPitchControl: !1,
328
+ // Depends on server provider
329
+ maxTextLength: T[t]
330
+ };
331
+ }
332
+ destroy() {
333
+ this.config = null, this.adapter = null;
334
+ }
335
+ }
336
+ export {
337
+ P as ServerTTSProvider
338
+ };