@pie-players/pie-section-player 0.3.64 → 0.3.65

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.
@@ -1,492 +0,0 @@
1
- import { t as d } from "./defineProperty-f7fab5da.js";
2
- function T(t, e) {
3
- if (typeof t != "string" || t.length === 0) return null;
4
- try {
5
- const r = typeof e.apiEndpoint == "string" && /^https?:\/\//i.test(e.apiEndpoint) ? e.apiEndpoint : void 0, i = r ? new URL(t, r) : new URL(t);
6
- return i.protocol !== "http:" && i.protocol !== "https:" ? null : i;
7
- } catch {
8
- return null;
9
- }
10
- }
11
- function A(t, e) {
12
- const r = Array.isArray(e.assetOrigins) ? e.assetOrigins.filter((i) => typeof i == "string" && i.length > 0).map((i) => {
13
- try {
14
- return new URL(i).origin;
15
- } catch {
16
- return i;
17
- }
18
- }) : [];
19
- if (r.length === 0) {
20
- if (typeof e.apiEndpoint == "string" && /^https?:\/\//i.test(e.apiEndpoint)) try {
21
- return new URL(e.apiEndpoint).origin === t.origin;
22
- } catch {
23
- return !1;
24
- }
25
- return typeof window < "u" && typeof window.location?.origin == "string" && window.location.origin === t.origin;
26
- }
27
- return r.includes(t.origin);
28
- }
29
- function R(t) {
30
- const e = {};
31
- for (const [r, i] of Object.entries(t)) {
32
- const o = r.toLowerCase();
33
- o === "authorization" || o === "proxy-authorization" || o === "cookie" || o.startsWith("x-auth-") || (e[r] = i);
34
- }
35
- return e;
36
- }
37
- var I = (t) => {
38
- const e = (t.providerOptions && typeof t.providerOptions == "object" ? t.providerOptions : {}).__pieTelemetry;
39
- return typeof e == "function" ? e : void 0;
40
- }, M = {
41
- pie: 3e3,
42
- custom: 3e3
43
- }, P = /* @__PURE__ */ new Set(["polly", "google"]), O = (t) => {
44
- if (!t || b(t) !== "pie") return !1;
45
- const e = (t.provider || "polly").toLowerCase();
46
- return P.has(e);
47
- }, S = (t) => t.replace(/\/+$/, ""), U = (t) => {
48
- const e = S(t.apiEndpoint), r = (t.provider || "").toLowerCase();
49
- return r === "polly" || r === "google" ? `${e}/${r}/voices` : `${e}/voices`;
50
- }, b = (t) => t.transportMode === "custom" ? "custom" : t.transportMode === "pie" ? "pie" : t.provider === "custom" ? "custom" : "pie", k = (t, e) => t.endpointMode ? t.endpointMode : e === "custom" ? "rootPost" : "synthesizePath", L = (t, e) => t.endpointValidationMode ? t.endpointValidationMode : e === "custom" ? "none" : "voices", E = (t) => Number.isFinite(t) ? t < 1 ? "slow" : t > 1 ? "fast" : "medium" : "medium", B = (t) => {
51
- const e = t.providerOptions || {};
52
- return typeof e.speedRate == "string" ? e.speedRate : E(Number(t.rate ?? 1));
53
- }, C = (t) => {
54
- const e = [];
55
- let r = 0;
56
- const i = t.split(`
57
- `).map((o) => o.trim()).filter(Boolean);
58
- for (const o of i) try {
59
- const s = JSON.parse(o), a = typeof s.type == "string" ? s.type : "word", n = typeof s.time == "number" && Number.isFinite(s.time) ? s.time : 0, h = typeof s.value == "string" ? s.value : "", l = typeof s.start == "number" && Number.isFinite(s.start) ? s.start : null, u = typeof s.end == "number" && Number.isFinite(s.end) ? s.end : null, y = l ?? r, c = u ?? y + Math.max(1, h.length || String(s.value || "").length);
60
- r = Math.max(c + 1, r), e.push({
61
- time: n,
62
- type: a,
63
- start: y,
64
- end: c,
65
- value: h
66
- });
67
- } catch {
68
- }
69
- return e;
70
- }, z = (t) => {
71
- if (!Array.isArray(t)) return [];
72
- const e = [];
73
- let r = 0;
74
- for (const i of t) {
75
- if (!i || typeof i != "object") continue;
76
- 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 : "", n = typeof i.start == "number" && Number.isFinite(i.start) ? i.start : null, h = typeof i.end == "number" && Number.isFinite(i.end) ? i.end : null, l = n ?? r, u = h ?? l + Math.max(1, a.length || 1);
77
- r = Math.max(r, u + 1), e.push({
78
- time: s,
79
- type: o,
80
- start: l,
81
- end: u,
82
- value: a
83
- });
84
- }
85
- 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);
86
- }, x = {
87
- pie: {
88
- id: "pie",
89
- resolveSynthesisUrl: (t) => {
90
- const e = k(t, "pie"), r = S(t.apiEndpoint);
91
- return e === "rootPost" ? r : `${r}/synthesize`;
92
- },
93
- buildRequestBody: (t, e) => {
94
- 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((n) => n === "word" || n === "sentence" || n === "ssml") : void 0;
95
- return {
96
- text: t,
97
- provider: e.provider || "polly",
98
- voice: e.voice,
99
- language: e.language,
100
- rate: e.rate,
101
- engine: i,
102
- sampleRate: o,
103
- format: s,
104
- speechMarkTypes: a,
105
- includeSpeechMarks: !0
106
- };
107
- },
108
- parseResponse: async (t) => {
109
- const e = await t.json();
110
- return {
111
- audio: {
112
- kind: "base64",
113
- data: e.audio,
114
- contentType: e.contentType
115
- },
116
- speechMarks: Array.isArray(e.speechMarks) ? e.speechMarks : []
117
- };
118
- }
119
- },
120
- custom: {
121
- id: "custom",
122
- resolveSynthesisUrl: (t) => {
123
- const e = k(t, "custom"), r = S(t.apiEndpoint);
124
- return e === "synthesizePath" ? `${r}/synthesize` : r;
125
- },
126
- buildRequestBody: (t, e) => {
127
- 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;
128
- return {
129
- text: t,
130
- speedRate: B(e),
131
- lang_id: i,
132
- cache: o
133
- };
134
- },
135
- parseResponse: async (t, e, r, i) => {
136
- const o = await t.json(), s = {};
137
- if (e.includeAuthOnAssetFetch)
138
- for (const [l, u] of Object.entries(r)) l.toLowerCase() === "authorization" && (s[l] = u);
139
- let a = [];
140
- const n = z(o.speechMarks);
141
- if (n.length > 0) a = n;
142
- else if (typeof o.word == "string" && o.word.length > 0) {
143
- const l = T(o.word, e);
144
- if (l !== null) {
145
- const u = A(l, e) ? s : R(s), y = await fetch(l.toString(), {
146
- headers: u,
147
- signal: i,
148
- redirect: "error"
149
- });
150
- y.ok && (a = C(await y.text()));
151
- }
152
- }
153
- const h = T(o.audioContent, e);
154
- if (h === null) throw new Error("TTS server returned an invalid audio URL");
155
- return {
156
- audio: {
157
- kind: "url",
158
- url: h.toString()
159
- },
160
- speechMarks: a
161
- };
162
- }
163
- }
164
- }, N = class {
165
- constructor(t, e) {
166
- d(this, "config", void 0), d(this, "adapter", void 0), d(this, "currentAudio", null), d(this, "pausedState", !1), d(this, "wordTimings", []), d(this, "highlightInterval", null), d(this, "intentionallyStopped", !1), d(this, "activeSynthesisController", null), d(this, "synthesisRunId", 0), d(this, "telemetryReporter", void 0), d(this, "onWordBoundary", void 0), this.config = t, this.adapter = e, this.telemetryReporter = I(t);
167
- }
168
- async emitTelemetry(t, e) {
169
- try {
170
- await this.telemetryReporter?.(t, e);
171
- } catch (r) {
172
- console.warn("[ServerTTSProvider] telemetry callback failed:", r);
173
- }
174
- }
175
- async speak(t) {
176
- this.stop(), this.intentionallyStopped = !1;
177
- const e = ++this.synthesisRunId, r = new AbortController();
178
- this.activeSynthesisController = r;
179
- const { audioUrl: i, wordTimings: o } = await this.synthesizeSpeech(t, r.signal, e);
180
- if (e !== this.synthesisRunId) {
181
- URL.revokeObjectURL(i);
182
- return;
183
- }
184
- return this.wordTimings = o, new Promise((s, a) => {
185
- const n = new Audio(i);
186
- this.currentAudio = n, this.config.volume !== void 0 && (n.volume = Math.max(0, Math.min(1, this.config.volume))), n.onplay = () => {
187
- this.pausedState = !1, this.onWordBoundary && this.wordTimings.length > 0 && this.startWordHighlighting();
188
- }, n.onended = () => {
189
- this.stopWordHighlighting(), URL.revokeObjectURL(i), this.currentAudio = null, this.wordTimings = [], s();
190
- }, n.onerror = (h) => {
191
- this.stopWordHighlighting(), URL.revokeObjectURL(i), this.currentAudio = null, this.wordTimings = [], this.intentionallyStopped ? s() : a(/* @__PURE__ */ new Error("Failed to play audio from server"));
192
- }, n.onpause = () => {
193
- this.stopWordHighlighting(), this.pausedState = !0;
194
- }, n.play().catch(a);
195
- });
196
- }
197
- async synthesizeSpeech(t, e, r) {
198
- const i = Date.now();
199
- await this.emitTelemetry("pie-tool-backend-call-start", {
200
- toolId: "tts",
201
- backend: this.config.provider || "server",
202
- operation: "synthesize-speech"
203
- });
204
- const o = {
205
- "Content-Type": "application/json",
206
- ...this.config.headers
207
- };
208
- this.config.authToken && (o.Authorization = `Bearer ${this.config.authToken}`);
209
- const s = this.adapter.resolveSynthesisUrl(this.config), a = this.adapter.buildRequestBody(t, this.config), n = await (async () => {
210
- try {
211
- return await fetch(s, {
212
- method: "POST",
213
- headers: o,
214
- body: JSON.stringify(a),
215
- signal: e
216
- });
217
- } catch (c) {
218
- throw await this.emitTelemetry("pie-tool-backend-call-error", {
219
- toolId: "tts",
220
- backend: this.config.provider || "server",
221
- operation: "synthesize-speech",
222
- duration: Date.now() - i,
223
- errorType: "TTSBackendNetworkError",
224
- message: c instanceof Error ? c.message : String(c)
225
- }), c;
226
- }
227
- })();
228
- if (!n.ok) {
229
- const c = await n.text().catch(() => "");
230
- let p = {};
231
- if (c) try {
232
- p = JSON.parse(c);
233
- } catch (v) {
234
- console.warn("[ServerTTSProvider] non-OK response body was not JSON", {
235
- status: n.status,
236
- url: s,
237
- rawBodyPreview: c.slice(0, 500),
238
- parseError: v instanceof Error ? v.message : String(v)
239
- });
240
- }
241
- const g = typeof p.message == "string" && p.message || (typeof p.error?.message == "string" ? p.error.message : void 0) || `Server returned ${n.status}`;
242
- await this.emitTelemetry("pie-tool-backend-call-error", {
243
- toolId: "tts",
244
- backend: this.config.provider || "server",
245
- operation: "synthesize-speech",
246
- duration: Date.now() - i,
247
- statusCode: n.status,
248
- errorType: "TTSBackendRequestError",
249
- message: g
250
- });
251
- const m = t.replace(/\s+/g, " ").trim().slice(0, 120);
252
- throw console.error("[ServerTTSProvider] synthesize request failed", {
253
- status: n.status,
254
- url: s,
255
- backend: this.config.provider || "server",
256
- message: g,
257
- responseBody: p,
258
- textLength: t.length,
259
- textPreview: m.length < t.length ? `${m}…` : m
260
- }), new Error(g);
261
- }
262
- const h = await this.adapter.parseResponse(n, this.config, o, e);
263
- if (r !== this.synthesisRunId || e.aborted) throw new Error("Synthesis superseded by a newer request");
264
- let l;
265
- if (h.audio.kind === "base64") l = this.base64ToBlob(h.audio.data, h.audio.contentType);
266
- else {
267
- const c = h.audio.url, p = Date.now();
268
- await this.emitTelemetry("pie-tool-backend-call-start", {
269
- toolId: "tts",
270
- backend: this.config.provider || "server",
271
- operation: "fetch-synthesized-audio-asset"
272
- });
273
- const g = {};
274
- this.config.includeAuthOnAssetFetch && this.config.authToken && (g.Authorization = `Bearer ${this.config.authToken}`);
275
- const m = T(c, this.config);
276
- if (m === null)
277
- throw await this.emitTelemetry("pie-tool-backend-call-error", {
278
- toolId: "tts",
279
- backend: this.config.provider || "server",
280
- operation: "fetch-synthesized-audio-asset",
281
- duration: Date.now() - p,
282
- errorType: "TTSAssetInvalidUrl",
283
- message: "TTS asset URL rejected (non-http(s) or malformed)"
284
- }), new Error("TTS asset URL rejected (non-http(s) or malformed)");
285
- const v = A(m, this.config) ? g : R(g), f = await (async () => {
286
- try {
287
- return await fetch(m.toString(), {
288
- headers: v,
289
- signal: e,
290
- redirect: "error"
291
- });
292
- } catch (w) {
293
- throw await this.emitTelemetry("pie-tool-backend-call-error", {
294
- toolId: "tts",
295
- backend: this.config.provider || "server",
296
- operation: "fetch-synthesized-audio-asset",
297
- duration: Date.now() - p,
298
- errorType: "TTSAssetNetworkError",
299
- message: w instanceof Error ? w.message : String(w)
300
- }), w;
301
- }
302
- })();
303
- if (!f.ok)
304
- throw await this.emitTelemetry("pie-tool-backend-call-error", {
305
- toolId: "tts",
306
- backend: this.config.provider || "server",
307
- operation: "fetch-synthesized-audio-asset",
308
- duration: Date.now() - p,
309
- statusCode: f.status,
310
- errorType: "TTSAssetFetchError",
311
- message: `Failed to download synthesized audio (${f.status})`
312
- }), new Error(`Failed to download synthesized audio (${f.status})`);
313
- l = await f.blob(), await this.emitTelemetry("pie-tool-backend-call-success", {
314
- toolId: "tts",
315
- backend: this.config.provider || "server",
316
- operation: "fetch-synthesized-audio-asset",
317
- duration: Date.now() - p
318
- });
319
- }
320
- const u = URL.createObjectURL(l), y = this.parseSpeechMarks(h.speechMarks);
321
- return await this.emitTelemetry("pie-tool-backend-call-success", {
322
- toolId: "tts",
323
- backend: this.config.provider || "server",
324
- operation: "synthesize-speech",
325
- duration: Date.now() - i
326
- }), {
327
- audioUrl: u,
328
- wordTimings: y
329
- };
330
- }
331
- base64ToBlob(t, e) {
332
- const r = atob(t), i = new Array(r.length);
333
- for (let s = 0; s < r.length; s++) i[s] = r.charCodeAt(s);
334
- const o = new Uint8Array(i);
335
- return new Blob([o], { type: e });
336
- }
337
- parseSpeechMarks(t) {
338
- return t.filter((e) => e.type === "word").map((e, r) => ({
339
- time: e.time,
340
- wordIndex: r,
341
- charIndex: e.start,
342
- length: e.end - e.start,
343
- word: e.value
344
- }));
345
- }
346
- startWordHighlighting() {
347
- if (this.stopWordHighlighting(), !this.currentAudio || !this.onWordBoundary || this.wordTimings.length === 0) {
348
- console.log("[ServerTTSProvider] Cannot start highlighting:", {
349
- hasAudio: !!this.currentAudio,
350
- hasCallback: !!this.onWordBoundary,
351
- wordTimingsCount: this.wordTimings.length
352
- });
353
- return;
354
- }
355
- 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));
356
- let t = -1;
357
- this.highlightInterval = window.setInterval(() => {
358
- if (!this.currentAudio) {
359
- this.stopWordHighlighting();
360
- return;
361
- }
362
- const e = this.currentAudio.currentTime * 1e3;
363
- for (let r = 0; r < this.wordTimings.length; r++) {
364
- const i = this.wordTimings[r];
365
- if (e >= i.time && r > t) {
366
- this.onWordBoundary && (console.log("[ServerTTSProvider] Highlighting word at charIndex:", i.charIndex, "length:", i.length, "time:", i.time, "currentTime:", e), this.onWordBoundary(i.word, i.charIndex, i.length)), t = r;
367
- break;
368
- }
369
- }
370
- }, 50);
371
- }
372
- stopWordHighlighting() {
373
- this.highlightInterval !== null && (clearInterval(this.highlightInterval), this.highlightInterval = null);
374
- }
375
- pause() {
376
- this.currentAudio && !this.pausedState && (this.currentAudio.pause(), this.stopWordHighlighting(), this.pausedState = !0);
377
- }
378
- resume() {
379
- this.currentAudio && this.pausedState && (this.currentAudio.play(), this.pausedState = !1, this.onWordBoundary && this.wordTimings.length > 0 && this.startWordHighlighting());
380
- }
381
- stop() {
382
- 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 = [];
383
- }
384
- isPlaying() {
385
- return this.currentAudio !== null && !this.pausedState;
386
- }
387
- isPaused() {
388
- return this.pausedState;
389
- }
390
- updateSettings(t) {
391
- t.rate !== void 0 && (this.config.rate = t.rate, this.config.providerOptions = {
392
- ...this.config.providerOptions || {},
393
- speedRate: E(Number(t.rate))
394
- }), t.pitch !== void 0 && (this.config.pitch = t.pitch), t.voice !== void 0 && (this.config.voice = t.voice);
395
- }
396
- }, F = class {
397
- constructor() {
398
- d(this, "providerId", "server-tts"), d(this, "providerName", "Server TTS"), d(this, "version", "1.0.0"), d(this, "config", null), d(this, "adapter", null), d(this, "telemetryReporter", void 0);
399
- }
400
- async emitTelemetry(t, e) {
401
- try {
402
- await this.telemetryReporter?.(t, e);
403
- } catch (r) {
404
- console.warn("[ServerTTSProvider] telemetry callback failed:", r);
405
- }
406
- }
407
- async initialize(t) {
408
- const e = t;
409
- if (!e.apiEndpoint) throw new Error("apiEndpoint is required for ServerTTSProvider");
410
- this.config = e, this.telemetryReporter = I(e);
411
- const r = b(e);
412
- if (this.adapter = x[r], e.validateEndpoint) {
413
- const i = Date.now();
414
- if (await this.emitTelemetry("pie-tool-backend-call-start", {
415
- toolId: "tts",
416
- backend: e.provider || "server",
417
- operation: "validate-endpoint"
418
- }), !await this.testAPIAvailability())
419
- throw await this.emitTelemetry("pie-tool-backend-call-error", {
420
- toolId: "tts",
421
- backend: e.provider || "server",
422
- operation: "validate-endpoint",
423
- duration: Date.now() - i,
424
- errorType: "TTSEndpointValidationError",
425
- message: `Server TTS API not available at ${e.apiEndpoint}`
426
- }), new Error(`Server TTS API not available at ${e.apiEndpoint}`);
427
- await this.emitTelemetry("pie-tool-backend-call-success", {
428
- toolId: "tts",
429
- backend: e.provider || "server",
430
- operation: "validate-endpoint",
431
- duration: Date.now() - i
432
- });
433
- }
434
- return new N(e, this.adapter);
435
- }
436
- async testAPIAvailability() {
437
- if (!this.config || !this.adapter) return !1;
438
- try {
439
- const t = { ...this.config.headers };
440
- this.config.authToken && (t.Authorization = `Bearer ${this.config.authToken}`);
441
- const e = new AbortController(), r = setTimeout(() => e.abort(), 5e3), i = L(this.config, this.adapter.id);
442
- if (i === "none")
443
- return clearTimeout(r), !0;
444
- const o = i === "voices" ? U(this.config) : this.adapter.resolveSynthesisUrl(this.config), s = i === "voices" ? "GET" : "OPTIONS";
445
- try {
446
- const a = await fetch(o, {
447
- method: s,
448
- headers: t,
449
- signal: e.signal
450
- });
451
- return clearTimeout(r), a.ok || a.status === 405;
452
- } catch {
453
- return clearTimeout(r), !1;
454
- }
455
- } catch {
456
- return !1;
457
- }
458
- }
459
- supportsFeature(t) {
460
- switch (t) {
461
- case "pause":
462
- case "resume":
463
- case "wordBoundary":
464
- case "voiceSelection":
465
- case "rateControl":
466
- return !0;
467
- case "pitchControl":
468
- return !1;
469
- default:
470
- return !1;
471
- }
472
- }
473
- getCapabilities() {
474
- const t = this.config ? b(this.config) : "pie";
475
- return {
476
- supportsPause: !0,
477
- supportsResume: !0,
478
- supportsWordBoundary: !0,
479
- supportsVoiceSelection: !0,
480
- supportsRateControl: !0,
481
- supportsPitchControl: !1,
482
- supportsSSML: O(this.config),
483
- maxTextLength: M[t]
484
- };
485
- }
486
- destroy() {
487
- this.config = null, this.adapter = null, this.telemetryReporter = void 0;
488
- }
489
- };
490
- export {
491
- F as ServerTTSProvider
492
- };
@@ -1,88 +0,0 @@
1
- import { AccessibilityCatalogResolverApi, CatalogOwnerContext, SignLanguageMedia } from '@pie-players/pie-assessment-toolkit';
2
- import { ItemEntity } from '@pie-players/pie-players-shared/types';
3
- /**
4
- * QTI 3.0 / AfA support id gating signed alternates. Excluded from the computed
5
- * default PNP profile by `ACCOMMODATION_ONLY_SUPPORT_IDS`, so it is only ever
6
- * granted deliberately.
7
- */
8
- export declare const SIGN_LANGUAGE_FEATURE_ID = "signLanguage";
9
- /** A catalog on this item that carries at least one sign-language card. */
10
- export interface SignLanguageCatalogRef {
11
- catalogId: string;
12
- /** Set when the catalog is owned by one model rather than the item root. */
13
- modelId?: string;
14
- }
15
- export interface ResolvedSignLanguageAlternate extends SignLanguageMedia {
16
- catalogId: string;
17
- }
18
- /**
19
- * Collect the catalog identifiers on an item that carry signing cards.
20
- *
21
- * The region resolves through the catalog resolver, but the resolver is keyed by
22
- * identifier within an owner scope — something has to say *which* identifiers
23
- * this item put in play, and whether each belongs to the item or to one model.
24
- * That is the same walk the runtime does when it registers an item's catalogs,
25
- * so it is borrowed rather than repeated: a ref can only name a scope
26
- * registration actually files under. Only the "does this catalog carry signing"
27
- * filter is ours.
28
- *
29
- * The owner identity here is a placeholder: the assessment and section ids are
30
- * not known at extraction time and are supplied by the caller at lookup time
31
- * (see `resolveSignLanguageAlternate`). Nothing but `modelId` is read from the
32
- * walk's contexts.
33
- */
34
- export declare function collectSignLanguageCatalogRefs(item: ItemEntity | null | undefined): SignLanguageCatalogRef[];
35
- export interface SignLanguageLookupArgs {
36
- resolver: AccessibilityCatalogResolverApi;
37
- refs: SignLanguageCatalogRef[];
38
- /** Owner context for the item, without `modelId` — added per ref. */
39
- ownerContext: CatalogOwnerContext;
40
- /** Sign language the learner is entitled to, ISO 639-3. */
41
- requestedSignLang?: string;
42
- }
43
- /**
44
- * Resolve the signed alternate to show for an item, or `null`.
45
- *
46
- * Priority, language fallback and owner scoping are the resolver's job and are
47
- * not re-implemented here. What *is* enforced here is the one deliberate
48
- * departure from the resolver's default behaviour: **no cross-sign-language
49
- * substitution.** The resolver's last fallback rung matches any card of the
50
- * requested type regardless of language, which for spoken content is helpful
51
- * and for signing is not — handing an ASL learner a BSL recording is worse than
52
- * handing them nothing. So a card the resolver returns by that rung is accepted
53
- * only if its language actually matches, or if the card asserts no language at
54
- * all.
55
- *
56
- * MVP takes the first resolvable card: sample content is one recording per item.
57
- * The refs stay per-node capable for when choice-level docking lands.
58
- */
59
- export declare function resolveSignLanguageAlternate(args: SignLanguageLookupArgs): ResolvedSignLanguageAlternate | null;
60
- /**
61
- * Default and bounds for the media region's share of the card width.
62
- *
63
- * Signing needs height for hands and face, so the region is sized by an
64
- * aspect-ratio target with a minimum height rather than by width alone — a flat
65
- * viewport percentage either wastes space on a short clip or crushes it on a
66
- * narrow device. The percentage below only decides how the card's width is
67
- * split; legibility is defended by the region's CSS.
68
- */
69
- export declare const MEDIA_REGION_DEFAULT_PERCENT = 34;
70
- export declare const MEDIA_REGION_MIN_PERCENT = 20;
71
- export declare const MEDIA_REGION_MAX_PERCENT = 55;
72
- /** Below this card width the split is dropped and the region stacks. */
73
- export declare const MEDIA_REGION_STACK_BREAKPOINT_PX = 560;
74
- export declare function clampMediaRegionPercent(value: number, min?: number, max?: number): number;
75
- /**
76
- * Convert a pointer drag into a media-region width percentage.
77
- *
78
- * Container-relative, unlike the passage/items divider's fixed 0.1%-per-pixel
79
- * factor: the same drag has to mean the same thing in a wide card and a narrow
80
- * one. The region is on the right, so dragging left grows it.
81
- */
82
- export declare function mediaRegionPercentFromDrag(args: {
83
- startPercent: number;
84
- deltaX: number;
85
- containerWidthPx: number;
86
- min?: number;
87
- max?: number;
88
- }): number;