@pie-players/pie-section-player 0.3.7 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-section-player",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "Web component for rendering QTI 3.0 assessment sections with passages and items",
6
6
  "license": "MIT",
@@ -89,7 +89,7 @@
89
89
  "lint": "biome check ."
90
90
  },
91
91
  "peerDependencies": {
92
- "@pie-players/pie-tool-annotation-toolbar": "0.3.7"
92
+ "@pie-players/pie-tool-annotation-toolbar": "0.3.8"
93
93
  },
94
94
  "peerDependenciesMeta": {
95
95
  "@pie-players/pie-tool-annotation-toolbar": {
@@ -97,11 +97,11 @@
97
97
  }
98
98
  },
99
99
  "dependencies": {
100
- "@pie-players/pie-assessment-toolkit": "0.3.7",
101
- "@pie-players/pie-item-player": "0.3.7",
102
- "@pie-players/pie-context": "0.3.7",
103
- "@pie-players/pie-players-shared": "0.3.7",
104
- "@pie-players/pie-toolbars": "0.3.7"
100
+ "@pie-players/pie-assessment-toolkit": "0.3.8",
101
+ "@pie-players/pie-item-player": "0.3.8",
102
+ "@pie-players/pie-context": "0.3.8",
103
+ "@pie-players/pie-players-shared": "0.3.8",
104
+ "@pie-players/pie-toolbars": "0.3.8"
105
105
  },
106
106
  "devDependencies": {
107
107
  "@axe-core/playwright": "^4.11.1",
@@ -1,216 +0,0 @@
1
- var v = Object.defineProperty, y = (h, t, i) => t in h ? v(h, t, { enumerable: !0, configurable: !0, writable: !0, value: i }) : h[t] = i, o = (h, t, i) => y(h, typeof t != "symbol" ? t + "" : t, i);
2
- class T {
3
- constructor(t) {
4
- o(this, "config"), o(this, "currentAudio", null), o(this, "pausedState", !1), o(this, "wordTimings", []), o(this, "highlightInterval", null), o(this, "intentionallyStopped", !1), o(this, "onWordBoundary"), this.config = t;
5
- }
6
- async speak(t) {
7
- this.stop(), this.intentionallyStopped = !1;
8
- const { audioUrl: i, wordTimings: e } = await this.synthesizeSpeech(t), r = this.config.rate || 1;
9
- return this.wordTimings = e.map((n) => ({
10
- ...n,
11
- time: n.time / r
12
- })), new Promise((n, a) => {
13
- const s = new Audio(i);
14
- this.currentAudio = s, this.config.rate && (s.playbackRate = Math.max(0.25, Math.min(4, this.config.rate))), this.config.volume !== void 0 && (s.volume = Math.max(0, Math.min(1, this.config.volume))), s.onplay = () => {
15
- this.pausedState = !1, this.onWordBoundary && this.wordTimings.length > 0 && this.startWordHighlighting();
16
- }, s.onended = () => {
17
- this.stopWordHighlighting(), URL.revokeObjectURL(i), this.currentAudio = null, this.wordTimings = [], n();
18
- }, s.onerror = (d) => {
19
- this.stopWordHighlighting(), URL.revokeObjectURL(i), this.currentAudio = null, this.wordTimings = [], this.intentionallyStopped ? n() : a(new Error("Failed to play audio from server"));
20
- }, s.onpause = () => {
21
- this.stopWordHighlighting(), this.pausedState = !0;
22
- }, s.play().catch(a);
23
- });
24
- }
25
- /**
26
- * Call server API to synthesize speech
27
- */
28
- async synthesizeSpeech(t) {
29
- const i = {
30
- "Content-Type": "application/json",
31
- ...this.config.headers
32
- };
33
- this.config.authToken && (i.Authorization = `Bearer ${this.config.authToken}`);
34
- const e = this.config.providerOptions || {}, r = typeof this.config.engine == "string" ? this.config.engine : typeof e.engine == "string" ? e.engine : void 0, n = typeof e.sampleRate == "number" && Number.isFinite(e.sampleRate) ? e.sampleRate : void 0, a = e.format === "mp3" || e.format === "ogg" || e.format === "pcm" ? e.format : void 0, s = Array.isArray(e.speechMarkTypes) ? e.speechMarkTypes.filter((u) => u === "word" || u === "sentence" || u === "ssml") : void 0, d = {
35
- text: t,
36
- provider: this.config.provider || "polly",
37
- voice: this.config.voice,
38
- language: this.config.language,
39
- rate: this.config.rate,
40
- engine: r,
41
- sampleRate: n,
42
- format: a,
43
- speechMarkTypes: s,
44
- includeSpeechMarks: !0
45
- }, l = await fetch(`${this.config.apiEndpoint}/synthesize`, {
46
- method: "POST",
47
- headers: i,
48
- body: JSON.stringify(d)
49
- });
50
- if (!l.ok) {
51
- const u = await l.json().catch(() => ({})), m = u.message || u.error?.message || `Server returned ${l.status}`;
52
- throw new Error(m);
53
- }
54
- const c = await l.json(), g = this.base64ToBlob(c.audio, c.contentType), p = URL.createObjectURL(g), f = this.parseSpeechMarks(c.speechMarks);
55
- return { audioUrl: p, wordTimings: f };
56
- }
57
- /**
58
- * Convert base64 to Blob
59
- */
60
- base64ToBlob(t, i) {
61
- const e = atob(t), r = new Array(e.length);
62
- for (let a = 0; a < e.length; a++)
63
- r[a] = e.charCodeAt(a);
64
- const n = new Uint8Array(r);
65
- return new Blob([n], { type: i });
66
- }
67
- /**
68
- * Parse speech marks into word timings
69
- */
70
- parseSpeechMarks(t) {
71
- return t.filter((i) => i.type === "word").map((i, e) => ({
72
- time: i.time,
73
- wordIndex: e,
74
- charIndex: i.start,
75
- length: i.end - i.start
76
- }));
77
- }
78
- /**
79
- * Start word highlighting synchronized with audio playback
80
- */
81
- startWordHighlighting() {
82
- if (this.stopWordHighlighting(), !this.currentAudio || !this.onWordBoundary || this.wordTimings.length === 0) {
83
- console.log("[ServerTTSProvider] Cannot start highlighting:", {
84
- hasAudio: !!this.currentAudio,
85
- hasCallback: !!this.onWordBoundary,
86
- wordTimingsCount: this.wordTimings.length
87
- });
88
- return;
89
- }
90
- 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));
91
- let t = -1;
92
- this.highlightInterval = window.setInterval(() => {
93
- if (!this.currentAudio) {
94
- this.stopWordHighlighting();
95
- return;
96
- }
97
- const i = this.currentAudio.currentTime * 1e3;
98
- for (let e = 0; e < this.wordTimings.length; e++) {
99
- const r = this.wordTimings[e];
100
- if (i >= r.time && e > t) {
101
- this.onWordBoundary && (console.log("[ServerTTSProvider] Highlighting word at charIndex:", r.charIndex, "length:", r.length, "time:", r.time, "currentTime:", i), this.onWordBoundary("", r.charIndex, r.length)), t = e;
102
- break;
103
- }
104
- }
105
- }, 50);
106
- }
107
- /**
108
- * Stop word highlighting
109
- */
110
- stopWordHighlighting() {
111
- this.highlightInterval !== null && (clearInterval(this.highlightInterval), this.highlightInterval = null);
112
- }
113
- pause() {
114
- this.currentAudio && !this.pausedState && (this.currentAudio.pause(), this.stopWordHighlighting(), this.pausedState = !0);
115
- }
116
- resume() {
117
- this.currentAudio && this.pausedState && (this.currentAudio.play(), this.pausedState = !1, this.onWordBoundary && this.wordTimings.length > 0 && this.startWordHighlighting());
118
- }
119
- stop() {
120
- 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 = [];
121
- }
122
- isPlaying() {
123
- return this.currentAudio !== null && !this.pausedState;
124
- }
125
- isPaused() {
126
- return this.pausedState;
127
- }
128
- /**
129
- * Update settings dynamically (rate, pitch, voice)
130
- * Note: Voice changes require resynthesis, so voice updates are stored but
131
- * take effect on the next speak() call. Rate can be applied to current playback.
132
- */
133
- updateSettings(t) {
134
- 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);
135
- }
136
- }
137
- class w {
138
- constructor() {
139
- o(this, "providerId", "server-tts"), o(this, "providerName", "Server TTS"), o(this, "version", "1.0.0"), o(this, "config", null);
140
- }
141
- /**
142
- * Initialize the server TTS provider.
143
- *
144
- * This is designed to be fast by default (no API calls).
145
- * Set validateEndpoint: true in config to test API availability during initialization.
146
- *
147
- * @performance Default: <10ms, With validation: 100-500ms
148
- */
149
- async initialize(t) {
150
- const i = t;
151
- if (!i.apiEndpoint)
152
- throw new Error("apiEndpoint is required for ServerTTSProvider");
153
- if (this.config = i, i.validateEndpoint && !await this.testAPIAvailability())
154
- throw new Error(`Server TTS API not available at ${i.apiEndpoint}`);
155
- return new T(i);
156
- }
157
- /**
158
- * Test if API endpoint is available (with timeout).
159
- *
160
- * @performance 100-500ms depending on network
161
- */
162
- async testAPIAvailability() {
163
- if (!this.config)
164
- return !1;
165
- try {
166
- const t = { ...this.config.headers };
167
- this.config.authToken && (t.Authorization = `Bearer ${this.config.authToken}`);
168
- const i = new AbortController(), e = setTimeout(() => i.abort(), 5e3);
169
- try {
170
- const r = await fetch(`${this.config.apiEndpoint}/voices`, {
171
- headers: t,
172
- signal: i.signal
173
- });
174
- return clearTimeout(e), r.ok;
175
- } catch {
176
- return clearTimeout(e), !1;
177
- }
178
- } catch {
179
- return !1;
180
- }
181
- }
182
- supportsFeature(t) {
183
- switch (t) {
184
- case "pause":
185
- case "resume":
186
- case "wordBoundary":
187
- case "voiceSelection":
188
- case "rateControl":
189
- return !0;
190
- case "pitchControl":
191
- return !1;
192
- default:
193
- return !1;
194
- }
195
- }
196
- getCapabilities() {
197
- return {
198
- supportsPause: !0,
199
- supportsResume: !0,
200
- supportsWordBoundary: !0,
201
- // ✅ Via speech marks from server
202
- supportsVoiceSelection: !0,
203
- supportsRateControl: !0,
204
- supportsPitchControl: !1,
205
- // Depends on server provider
206
- maxTextLength: 3e3
207
- // Conservative estimate
208
- };
209
- }
210
- destroy() {
211
- this.config = null;
212
- }
213
- }
214
- export {
215
- w as ServerTTSProvider
216
- };
@@ -1,228 +0,0 @@
1
- var v = Object.defineProperty;
2
- var y = (h, t, e) => t in h ? v(h, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : h[t] = e;
3
- var o = (h, t, e) => y(h, typeof t != "symbol" ? t + "" : t, e);
4
- class T {
5
- constructor(t) {
6
- o(this, "config");
7
- o(this, "currentAudio", null);
8
- o(this, "pausedState", !1);
9
- o(this, "wordTimings", []);
10
- o(this, "highlightInterval", null);
11
- o(this, "intentionallyStopped", !1);
12
- o(this, "onWordBoundary");
13
- this.config = t;
14
- }
15
- async speak(t) {
16
- this.stop(), this.intentionallyStopped = !1;
17
- const { audioUrl: e, wordTimings: i } = await this.synthesizeSpeech(t), r = this.config.rate || 1;
18
- return this.wordTimings = i.map((s) => ({
19
- ...s,
20
- time: s.time / r
21
- })), new Promise((s, a) => {
22
- const n = new Audio(e);
23
- 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 = () => {
24
- this.pausedState = !1, this.onWordBoundary && this.wordTimings.length > 0 && this.startWordHighlighting();
25
- }, n.onended = () => {
26
- this.stopWordHighlighting(), URL.revokeObjectURL(e), this.currentAudio = null, this.wordTimings = [], s();
27
- }, n.onerror = (l) => {
28
- this.stopWordHighlighting(), URL.revokeObjectURL(e), this.currentAudio = null, this.wordTimings = [], this.intentionallyStopped ? s() : a(new Error("Failed to play audio from server"));
29
- }, n.onpause = () => {
30
- this.stopWordHighlighting(), this.pausedState = !0;
31
- }, n.play().catch(a);
32
- });
33
- }
34
- /**
35
- * Call server API to synthesize speech
36
- */
37
- async synthesizeSpeech(t) {
38
- const e = {
39
- "Content-Type": "application/json",
40
- ...this.config.headers
41
- };
42
- this.config.authToken && (e.Authorization = `Bearer ${this.config.authToken}`);
43
- const i = this.config.providerOptions || {}, r = typeof this.config.engine == "string" ? this.config.engine : typeof i.engine == "string" ? i.engine : void 0, s = typeof i.sampleRate == "number" && Number.isFinite(i.sampleRate) ? i.sampleRate : void 0, a = i.format === "mp3" || i.format === "ogg" || i.format === "pcm" ? i.format : void 0, n = Array.isArray(i.speechMarkTypes) ? i.speechMarkTypes.filter((c) => c === "word" || c === "sentence" || c === "ssml") : void 0, l = {
44
- text: t,
45
- provider: this.config.provider || "polly",
46
- voice: this.config.voice,
47
- language: this.config.language,
48
- rate: this.config.rate,
49
- engine: r,
50
- sampleRate: s,
51
- format: a,
52
- speechMarkTypes: n,
53
- includeSpeechMarks: !0
54
- }, u = await fetch(`${this.config.apiEndpoint}/synthesize`, {
55
- method: "POST",
56
- headers: e,
57
- body: JSON.stringify(l)
58
- });
59
- if (!u.ok) {
60
- const c = await u.json().catch(() => ({})), m = c.message || c.error?.message || `Server returned ${u.status}`;
61
- throw new Error(m);
62
- }
63
- const d = await u.json(), g = this.base64ToBlob(d.audio, d.contentType), p = URL.createObjectURL(g), f = this.parseSpeechMarks(d.speechMarks);
64
- return { audioUrl: p, wordTimings: f };
65
- }
66
- /**
67
- * Convert base64 to Blob
68
- */
69
- base64ToBlob(t, e) {
70
- const i = atob(t), r = new Array(i.length);
71
- for (let a = 0; a < i.length; a++)
72
- r[a] = i.charCodeAt(a);
73
- const s = new Uint8Array(r);
74
- return new Blob([s], { type: e });
75
- }
76
- /**
77
- * Parse speech marks into word timings
78
- */
79
- parseSpeechMarks(t) {
80
- return t.filter((e) => e.type === "word").map((e, i) => ({
81
- time: e.time,
82
- wordIndex: i,
83
- charIndex: e.start,
84
- length: e.end - e.start
85
- }));
86
- }
87
- /**
88
- * Start word highlighting synchronized with audio playback
89
- */
90
- startWordHighlighting() {
91
- if (this.stopWordHighlighting(), !this.currentAudio || !this.onWordBoundary || this.wordTimings.length === 0) {
92
- console.log("[ServerTTSProvider] Cannot start highlighting:", {
93
- hasAudio: !!this.currentAudio,
94
- hasCallback: !!this.onWordBoundary,
95
- wordTimingsCount: this.wordTimings.length
96
- });
97
- return;
98
- }
99
- 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));
100
- let t = -1;
101
- this.highlightInterval = window.setInterval(() => {
102
- if (!this.currentAudio) {
103
- this.stopWordHighlighting();
104
- return;
105
- }
106
- const e = this.currentAudio.currentTime * 1e3;
107
- for (let i = 0; i < this.wordTimings.length; i++) {
108
- const r = this.wordTimings[i];
109
- if (e >= r.time && i > t) {
110
- 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 = i;
111
- break;
112
- }
113
- }
114
- }, 50);
115
- }
116
- /**
117
- * Stop word highlighting
118
- */
119
- stopWordHighlighting() {
120
- this.highlightInterval !== null && (clearInterval(this.highlightInterval), this.highlightInterval = null);
121
- }
122
- pause() {
123
- this.currentAudio && !this.pausedState && (this.currentAudio.pause(), this.stopWordHighlighting(), this.pausedState = !0);
124
- }
125
- resume() {
126
- this.currentAudio && this.pausedState && (this.currentAudio.play(), this.pausedState = !1, this.onWordBoundary && this.wordTimings.length > 0 && this.startWordHighlighting());
127
- }
128
- stop() {
129
- 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 = [];
130
- }
131
- isPlaying() {
132
- return this.currentAudio !== null && !this.pausedState;
133
- }
134
- isPaused() {
135
- return this.pausedState;
136
- }
137
- /**
138
- * Update settings dynamically (rate, pitch, voice)
139
- * Note: Voice changes require resynthesis, so voice updates are stored but
140
- * take effect on the next speak() call. Rate can be applied to current playback.
141
- */
142
- updateSettings(t) {
143
- 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);
144
- }
145
- }
146
- class S {
147
- constructor() {
148
- o(this, "providerId", "server-tts");
149
- o(this, "providerName", "Server TTS");
150
- o(this, "version", "1.0.0");
151
- o(this, "config", null);
152
- }
153
- /**
154
- * Initialize the server TTS provider.
155
- *
156
- * This is designed to be fast by default (no API calls).
157
- * Set validateEndpoint: true in config to test API availability during initialization.
158
- *
159
- * @performance Default: <10ms, With validation: 100-500ms
160
- */
161
- async initialize(t) {
162
- const e = t;
163
- if (!e.apiEndpoint)
164
- throw new Error("apiEndpoint is required for ServerTTSProvider");
165
- if (this.config = e, e.validateEndpoint && !await this.testAPIAvailability())
166
- throw new Error(`Server TTS API not available at ${e.apiEndpoint}`);
167
- return new T(e);
168
- }
169
- /**
170
- * Test if API endpoint is available (with timeout).
171
- *
172
- * @performance 100-500ms depending on network
173
- */
174
- async testAPIAvailability() {
175
- if (!this.config)
176
- return !1;
177
- try {
178
- const t = { ...this.config.headers };
179
- this.config.authToken && (t.Authorization = `Bearer ${this.config.authToken}`);
180
- const e = new AbortController(), i = setTimeout(() => e.abort(), 5e3);
181
- try {
182
- const r = await fetch(`${this.config.apiEndpoint}/voices`, {
183
- headers: t,
184
- signal: e.signal
185
- });
186
- return clearTimeout(i), r.ok;
187
- } catch {
188
- return clearTimeout(i), !1;
189
- }
190
- } catch {
191
- return !1;
192
- }
193
- }
194
- supportsFeature(t) {
195
- switch (t) {
196
- case "pause":
197
- case "resume":
198
- case "wordBoundary":
199
- case "voiceSelection":
200
- case "rateControl":
201
- return !0;
202
- case "pitchControl":
203
- return !1;
204
- default:
205
- return !1;
206
- }
207
- }
208
- getCapabilities() {
209
- return {
210
- supportsPause: !0,
211
- supportsResume: !0,
212
- supportsWordBoundary: !0,
213
- // ✅ Via speech marks from server
214
- supportsVoiceSelection: !0,
215
- supportsRateControl: !0,
216
- supportsPitchControl: !1,
217
- // Depends on server provider
218
- maxTextLength: 3e3
219
- // Conservative estimate
220
- };
221
- }
222
- destroy() {
223
- this.config = null;
224
- }
225
- }
226
- export {
227
- S as ServerTTSProvider
228
- };