@pie-players/pie-section-player-tools-tts-settings 0.3.20 → 0.3.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/TtsSettingsPanel.svelte +340 -87
- package/dist/section-player-tools-tts-settings.js +2136 -2024
- package/package.json +2 -2
package/TtsSettingsPanel.svelte
CHANGED
|
@@ -44,6 +44,13 @@
|
|
|
44
44
|
contentType?: string;
|
|
45
45
|
speechMarks?: Array<{ time: number; start: number; end: number }>;
|
|
46
46
|
};
|
|
47
|
+
type PreviewSpeechMark = { time: number; start: number; end: number; value?: string };
|
|
48
|
+
type CustomProviderPreviewResult = {
|
|
49
|
+
note?: string;
|
|
50
|
+
audioUrl?: string;
|
|
51
|
+
speechMarks?: PreviewSpeechMark[];
|
|
52
|
+
trackingText?: string;
|
|
53
|
+
};
|
|
47
54
|
|
|
48
55
|
type TtsSettingsAdapters = {
|
|
49
56
|
fetchPollyVoices?: (args: {
|
|
@@ -83,6 +90,8 @@
|
|
|
83
90
|
toolkitCoordinator: any;
|
|
84
91
|
apiEndpoint: string;
|
|
85
92
|
state: Record<string, unknown>;
|
|
93
|
+
previewText?: string;
|
|
94
|
+
previewMode?: PreviewMode;
|
|
86
95
|
};
|
|
87
96
|
|
|
88
97
|
type CustomProviderAdapter = {
|
|
@@ -96,7 +105,7 @@
|
|
|
96
105
|
buildApplyConfig: (
|
|
97
106
|
context: CustomProviderContext
|
|
98
107
|
) => ProviderApplyResult | Promise<ProviderApplyResult>;
|
|
99
|
-
preview?: (context: CustomProviderContext) => Promise<void |
|
|
108
|
+
preview?: (context: CustomProviderContext) => Promise<void | CustomProviderPreviewResult>;
|
|
100
109
|
initialState?: Record<string, unknown>;
|
|
101
110
|
};
|
|
102
111
|
|
|
@@ -113,7 +122,7 @@
|
|
|
113
122
|
buildApplyConfig?: (
|
|
114
123
|
context: CustomProviderContext
|
|
115
124
|
) => ProviderApplyResult | Promise<ProviderApplyResult>;
|
|
116
|
-
preview?: (context: CustomProviderContext) => Promise<void |
|
|
125
|
+
preview?: (context: CustomProviderContext) => Promise<void | CustomProviderPreviewResult>;
|
|
117
126
|
initialState?: Record<string, unknown>;
|
|
118
127
|
};
|
|
119
128
|
|
|
@@ -223,6 +232,48 @@
|
|
|
223
232
|
let previewPollingTimer: number | null = null;
|
|
224
233
|
let previewRunId = 0;
|
|
225
234
|
let activeCustomProviderElement = $state<Element | null>(null);
|
|
235
|
+
let dialogEl = $state<HTMLElement | null>(null);
|
|
236
|
+
let closeButtonEl = $state<HTMLButtonElement | null>(null);
|
|
237
|
+
let cleanupFocusTrap: (() => void) | null = null;
|
|
238
|
+
|
|
239
|
+
function createLocalFocusTrap(
|
|
240
|
+
container: HTMLElement,
|
|
241
|
+
options: { initialFocus?: HTMLElement | null; onEscape?: () => void } = {}
|
|
242
|
+
): () => void {
|
|
243
|
+
const focusable = () =>
|
|
244
|
+
Array.from(
|
|
245
|
+
container.querySelectorAll<HTMLElement>(
|
|
246
|
+
"button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])",
|
|
247
|
+
),
|
|
248
|
+
).filter((el) => el.offsetParent !== null || el.getClientRects().length > 0);
|
|
249
|
+
const previous = document.activeElement as HTMLElement | null;
|
|
250
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
251
|
+
if (event.key === "Escape") {
|
|
252
|
+
options.onEscape?.();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (event.key !== "Tab") return;
|
|
256
|
+
const nodes = focusable();
|
|
257
|
+
if (!nodes.length) return;
|
|
258
|
+
const current = document.activeElement as HTMLElement | null;
|
|
259
|
+
const currentIndex = nodes.indexOf(current || nodes[0]);
|
|
260
|
+
if (event.shiftKey && currentIndex <= 0) {
|
|
261
|
+
event.preventDefault();
|
|
262
|
+
nodes[nodes.length - 1].focus();
|
|
263
|
+
} else if (!event.shiftKey && currentIndex === nodes.length - 1) {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
nodes[0].focus();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
container.addEventListener("keydown", onKeyDown);
|
|
269
|
+
queueMicrotask(() => {
|
|
270
|
+
(options.initialFocus || focusable()[0] || container)?.focus?.();
|
|
271
|
+
});
|
|
272
|
+
return () => {
|
|
273
|
+
container.removeEventListener("keydown", onKeyDown);
|
|
274
|
+
previous?.focus?.();
|
|
275
|
+
};
|
|
276
|
+
}
|
|
226
277
|
|
|
227
278
|
const DEFAULT_PREVIEW_TEXT: Record<BackendTab, string> = {
|
|
228
279
|
browser:
|
|
@@ -239,6 +290,16 @@
|
|
|
239
290
|
'<speak>This is a <prosody rate="95%">Google Cloud SSML sample</prosody>. <break time="250ms"/> The preview preserves authored SSML.</speak>'
|
|
240
291
|
};
|
|
241
292
|
const BUILT_IN_TABS: BuiltInBackendTab[] = ["browser", "polly", "google"];
|
|
293
|
+
const PREVIEW_DEBUG_PREFIX = "[pie-tts-preview]";
|
|
294
|
+
|
|
295
|
+
function debugPreview(event: string, payload?: Record<string, unknown>): void {
|
|
296
|
+
if (typeof console === "undefined") return;
|
|
297
|
+
if (payload) {
|
|
298
|
+
console.debug(`${PREVIEW_DEBUG_PREFIX} ${event}`, payload);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
console.debug(`${PREVIEW_DEBUG_PREFIX} ${event}`);
|
|
302
|
+
}
|
|
242
303
|
|
|
243
304
|
const normalizedCustomProviders = $derived.by(() => {
|
|
244
305
|
const reserved = new Set<string>(BUILT_IN_TABS);
|
|
@@ -272,12 +333,16 @@
|
|
|
272
333
|
dispatch("close");
|
|
273
334
|
}
|
|
274
335
|
|
|
275
|
-
function createProviderContext(
|
|
336
|
+
function createProviderContext(
|
|
337
|
+
providerId: string,
|
|
338
|
+
overrides: Partial<CustomProviderContext> = {}
|
|
339
|
+
): CustomProviderContext {
|
|
276
340
|
return {
|
|
277
341
|
id: providerId,
|
|
278
342
|
toolkitCoordinator,
|
|
279
343
|
apiEndpoint: getDefaultApiEndpoint(),
|
|
280
|
-
state: customProviderStateById[providerId] || {}
|
|
344
|
+
state: customProviderStateById[providerId] || {},
|
|
345
|
+
...overrides
|
|
281
346
|
};
|
|
282
347
|
}
|
|
283
348
|
|
|
@@ -430,6 +495,10 @@
|
|
|
430
495
|
typeof source?.apiEndpoint === "string" && source.apiEndpoint.trim().length > 0
|
|
431
496
|
? source.apiEndpoint
|
|
432
497
|
: resolvedDefaultApiEndpoint;
|
|
498
|
+
const defaultPollyEndpoint =
|
|
499
|
+
backend === "polly" ? defaultEndpoint : resolvedDefaultApiEndpoint;
|
|
500
|
+
const defaultGoogleEndpoint =
|
|
501
|
+
backend === "google" ? defaultEndpoint : resolvedDefaultApiEndpoint;
|
|
433
502
|
const defaultLanguage =
|
|
434
503
|
typeof source?.language === "string" && source.language.trim().length > 0
|
|
435
504
|
? source.language
|
|
@@ -469,7 +538,7 @@
|
|
|
469
538
|
browserRate = defaultRate;
|
|
470
539
|
browserPitch = defaultPitch;
|
|
471
540
|
|
|
472
|
-
pollyApiEndpoint =
|
|
541
|
+
pollyApiEndpoint = defaultPollyEndpoint;
|
|
473
542
|
pollyLanguage = defaultLanguage;
|
|
474
543
|
pollyEngine = defaultEngine;
|
|
475
544
|
pollySampleRate = defaultSampleRate;
|
|
@@ -478,7 +547,7 @@
|
|
|
478
547
|
pollyVoice = backend === "polly" ? defaultVoice : "";
|
|
479
548
|
pollyRate = defaultRate;
|
|
480
549
|
|
|
481
|
-
googleApiEndpoint =
|
|
550
|
+
googleApiEndpoint = defaultGoogleEndpoint;
|
|
482
551
|
googleLanguage = defaultLanguage;
|
|
483
552
|
googleGender = typeof source?.googleGender === "string" ? source.googleGender : "";
|
|
484
553
|
googleVoiceType =
|
|
@@ -798,9 +867,7 @@
|
|
|
798
867
|
if (activeTab === nextTab) return;
|
|
799
868
|
stopPreview();
|
|
800
869
|
activeTab = nextTab;
|
|
801
|
-
|
|
802
|
-
setPreviewTextForCurrentTab();
|
|
803
|
-
}
|
|
870
|
+
setPreviewTextForCurrentTab();
|
|
804
871
|
void checkActiveTabAvailability();
|
|
805
872
|
}
|
|
806
873
|
|
|
@@ -902,10 +969,12 @@
|
|
|
902
969
|
|
|
903
970
|
function setPreviewTextForCurrentTab() {
|
|
904
971
|
if (!isBuiltInTab(activeTab)) {
|
|
972
|
+
const customLabel = activeCustomProvider?.label || "custom";
|
|
973
|
+
const customSample = `This is a ${customLabel} TTS provider sample.`;
|
|
905
974
|
if (previewMode === "ssml") {
|
|
906
|
-
previewText =
|
|
907
|
-
} else
|
|
908
|
-
previewText =
|
|
975
|
+
previewText = `<speak>${customSample}</speak>`;
|
|
976
|
+
} else {
|
|
977
|
+
previewText = customSample;
|
|
909
978
|
}
|
|
910
979
|
return;
|
|
911
980
|
}
|
|
@@ -936,19 +1005,50 @@
|
|
|
936
1005
|
if (previewPollingTimer !== null) {
|
|
937
1006
|
window.clearInterval(previewPollingTimer);
|
|
938
1007
|
}
|
|
939
|
-
|
|
1008
|
+
const orderedMarks = [...speechMarks].sort((left, right) => {
|
|
1009
|
+
if (left.time !== right.time) return left.time - right.time;
|
|
1010
|
+
if (left.start !== right.start) return left.start - right.start;
|
|
1011
|
+
return left.end - right.end;
|
|
1012
|
+
});
|
|
1013
|
+
if (orderedMarks.length === 0) return;
|
|
1014
|
+
const firstMark = orderedMarks[0];
|
|
1015
|
+
debugPreview("tracking:init", {
|
|
1016
|
+
audioCurrentMs: Math.round(audio.currentTime * 1000),
|
|
1017
|
+
marks: orderedMarks.length,
|
|
1018
|
+
firstMark,
|
|
1019
|
+
firstSlice: previewText.slice(
|
|
1020
|
+
Math.max(0, firstMark.start),
|
|
1021
|
+
Math.max(firstMark.start, firstMark.end)
|
|
1022
|
+
)
|
|
1023
|
+
});
|
|
1024
|
+
// Ensure preview starts on the first spoken word instead of waiting for the first poll tick.
|
|
1025
|
+
previewTrackIndex = firstMark.start;
|
|
1026
|
+
previewTrackLength = Math.max(1, firstMark.end - firstMark.start);
|
|
1027
|
+
let lastIndex = 0;
|
|
1028
|
+
let emittedTransitions = 0;
|
|
940
1029
|
previewPollingTimer = window.setInterval(() => {
|
|
941
1030
|
const currentMs = audio.currentTime * 1000;
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1031
|
+
let nextIndex = lastIndex;
|
|
1032
|
+
while (
|
|
1033
|
+
nextIndex + 1 < orderedMarks.length &&
|
|
1034
|
+
currentMs >= orderedMarks[nextIndex + 1].time
|
|
1035
|
+
) {
|
|
1036
|
+
nextIndex += 1;
|
|
1037
|
+
}
|
|
1038
|
+
if (nextIndex !== lastIndex) {
|
|
1039
|
+
lastIndex = nextIndex;
|
|
1040
|
+
const mark = orderedMarks[lastIndex];
|
|
1041
|
+
previewTrackIndex = mark.start;
|
|
1042
|
+
previewTrackLength = Math.max(1, mark.end - mark.start);
|
|
1043
|
+
if (emittedTransitions < 12) {
|
|
1044
|
+
emittedTransitions += 1;
|
|
1045
|
+
debugPreview("tracking:step", {
|
|
1046
|
+
audioCurrentMs: Math.round(currentMs),
|
|
1047
|
+
index: lastIndex,
|
|
1048
|
+
mark,
|
|
1049
|
+
slice: previewText.slice(Math.max(0, mark.start), Math.max(mark.start, mark.end))
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
952
1052
|
}
|
|
953
1053
|
}, 40);
|
|
954
1054
|
}
|
|
@@ -969,6 +1069,149 @@
|
|
|
969
1069
|
previewBackend = null;
|
|
970
1070
|
}
|
|
971
1071
|
|
|
1072
|
+
function normalizePreviewSpeechMarks(
|
|
1073
|
+
speechMarks: PreviewSpeechMark[],
|
|
1074
|
+
audioDurationSeconds: number
|
|
1075
|
+
): PreviewSpeechMark[] {
|
|
1076
|
+
if (speechMarks.length === 0) return speechMarks;
|
|
1077
|
+
const ordered = [...speechMarks].sort((left, right) => {
|
|
1078
|
+
if (left.time !== right.time) return left.time - right.time;
|
|
1079
|
+
if (left.start !== right.start) return left.start - right.start;
|
|
1080
|
+
return left.end - right.end;
|
|
1081
|
+
});
|
|
1082
|
+
const times = ordered.map((mark) => Number(mark.time) || 0).filter((value) => value >= 0);
|
|
1083
|
+
const maxTime = times.length ? Math.max(...times) : 0;
|
|
1084
|
+
const deltas: number[] = [];
|
|
1085
|
+
for (let index = 1; index < times.length; index += 1) {
|
|
1086
|
+
const delta = times[index] - times[index - 1];
|
|
1087
|
+
if (Number.isFinite(delta) && delta > 0) deltas.push(delta);
|
|
1088
|
+
}
|
|
1089
|
+
const medianDelta =
|
|
1090
|
+
deltas.length > 0
|
|
1091
|
+
? [...deltas].sort((a, b) => a - b)[Math.floor(deltas.length / 2)]
|
|
1092
|
+
: 0;
|
|
1093
|
+
const durationSuggestsSeconds =
|
|
1094
|
+
Number.isFinite(audioDurationSeconds) &&
|
|
1095
|
+
audioDurationSeconds > 0 &&
|
|
1096
|
+
maxTime > 0 &&
|
|
1097
|
+
maxTime <= audioDurationSeconds * 1.5;
|
|
1098
|
+
// SC marks are commonly seconds; when metadata is unavailable, use time-shape heuristics.
|
|
1099
|
+
const shapeSuggestsSeconds =
|
|
1100
|
+
(maxTime > 0 && maxTime < 100 && ordered.length > 3) || (medianDelta > 0 && medianDelta < 10);
|
|
1101
|
+
const shouldConvertSecondsToMs = durationSuggestsSeconds || shapeSuggestsSeconds;
|
|
1102
|
+
if (!shouldConvertSecondsToMs) return ordered;
|
|
1103
|
+
return ordered.map((mark) => ({ ...mark, time: Number(mark.time) * 1000 }));
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function normalizePreviewSpeechMarkOffsets(
|
|
1107
|
+
speechMarks: PreviewSpeechMark[],
|
|
1108
|
+
trackingText: string
|
|
1109
|
+
): PreviewSpeechMark[] {
|
|
1110
|
+
if (speechMarks.length === 0) return speechMarks;
|
|
1111
|
+
const ordered = [...speechMarks].sort((left, right) => {
|
|
1112
|
+
if (left.time !== right.time) return left.time - right.time;
|
|
1113
|
+
if (left.start !== right.start) return left.start - right.start;
|
|
1114
|
+
return left.end - right.end;
|
|
1115
|
+
});
|
|
1116
|
+
const safeText = typeof trackingText === "string" ? trackingText : "";
|
|
1117
|
+
const textLength = safeText.length;
|
|
1118
|
+
if (!textLength) return ordered;
|
|
1119
|
+
const maxEnd = Math.max(...ordered.map((mark) => Number(mark.end) || 0));
|
|
1120
|
+
const firstMark = ordered[0];
|
|
1121
|
+
const firstWord =
|
|
1122
|
+
typeof firstMark.value === "string" ? firstMark.value.trim() : "";
|
|
1123
|
+
const firstWordIndex = firstWord
|
|
1124
|
+
? safeText.toLowerCase().indexOf(firstWord.toLowerCase())
|
|
1125
|
+
: -1;
|
|
1126
|
+
const anchoredShift =
|
|
1127
|
+
firstWordIndex >= 0 ? Number(firstMark.start || 0) - firstWordIndex : 0;
|
|
1128
|
+
const fallbackShift = Number(firstMark.start || 0);
|
|
1129
|
+
const shouldRebase = maxEnd > textLength + 2 && (anchoredShift > 0 || fallbackShift > 0);
|
|
1130
|
+
if (!shouldRebase) return ordered;
|
|
1131
|
+
const shift = anchoredShift > 0 ? anchoredShift : fallbackShift;
|
|
1132
|
+
const rebased = ordered.map((mark) => {
|
|
1133
|
+
const start = Math.max(0, Number(mark.start || 0) - shift);
|
|
1134
|
+
const end = Math.max(start + 1, Number(mark.end || 0) - shift);
|
|
1135
|
+
return { ...mark, start, end };
|
|
1136
|
+
});
|
|
1137
|
+
debugPreview("marks:offset-rebase", {
|
|
1138
|
+
textLength,
|
|
1139
|
+
firstWord,
|
|
1140
|
+
firstWordIndex,
|
|
1141
|
+
shift,
|
|
1142
|
+
firstBefore: firstMark,
|
|
1143
|
+
firstAfter: rebased[0],
|
|
1144
|
+
lastAfter: rebased[rebased.length - 1]
|
|
1145
|
+
});
|
|
1146
|
+
return rebased;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
async function playPreviewAudioFromUrl(
|
|
1150
|
+
audioUrl: string,
|
|
1151
|
+
speechMarks: PreviewSpeechMark[] = []
|
|
1152
|
+
): Promise<void> {
|
|
1153
|
+
const audio = new Audio(audioUrl);
|
|
1154
|
+
currentPreviewAudio = audio;
|
|
1155
|
+
if (speechMarks.length > 0) {
|
|
1156
|
+
await new Promise<void>((resolve) => {
|
|
1157
|
+
if (Number.isFinite(audio.duration) && audio.duration > 0) {
|
|
1158
|
+
resolve();
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
let settled = false;
|
|
1162
|
+
const finish = () => {
|
|
1163
|
+
if (settled) return;
|
|
1164
|
+
settled = true;
|
|
1165
|
+
audio.removeEventListener("loadedmetadata", finish);
|
|
1166
|
+
audio.removeEventListener("error", finish);
|
|
1167
|
+
resolve();
|
|
1168
|
+
};
|
|
1169
|
+
audio.addEventListener("loadedmetadata", finish, { once: true });
|
|
1170
|
+
audio.addEventListener("error", finish, { once: true });
|
|
1171
|
+
if (typeof window !== "undefined") {
|
|
1172
|
+
window.setTimeout(finish, 1200);
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
const audioDurationSeconds = Number(audio.duration);
|
|
1176
|
+
const normalizedMarks = normalizePreviewSpeechMarks(speechMarks, audioDurationSeconds);
|
|
1177
|
+
const offsetNormalizedMarks = normalizePreviewSpeechMarkOffsets(normalizedMarks, previewText);
|
|
1178
|
+
const rawMaxTime = Math.max(...speechMarks.map((mark) => Number(mark.time) || 0));
|
|
1179
|
+
const normalizedMaxTime = Math.max(...offsetNormalizedMarks.map((mark) => Number(mark.time) || 0));
|
|
1180
|
+
debugPreview("marks:normalize", {
|
|
1181
|
+
audioDurationSeconds,
|
|
1182
|
+
rawCount: speechMarks.length,
|
|
1183
|
+
normalizedCount: offsetNormalizedMarks.length,
|
|
1184
|
+
rawMaxTime,
|
|
1185
|
+
normalizedMaxTime,
|
|
1186
|
+
rawFirst: speechMarks[0],
|
|
1187
|
+
normalizedFirst: offsetNormalizedMarks[0],
|
|
1188
|
+
rawLast: speechMarks[speechMarks.length - 1],
|
|
1189
|
+
normalizedLast: offsetNormalizedMarks[offsetNormalizedMarks.length - 1]
|
|
1190
|
+
});
|
|
1191
|
+
updateTrackingFromSpeechMarks(audio, offsetNormalizedMarks);
|
|
1192
|
+
} else {
|
|
1193
|
+
debugPreview("marks:missing", { audioUrl });
|
|
1194
|
+
}
|
|
1195
|
+
await new Promise<void>((resolve, reject) => {
|
|
1196
|
+
audio.onended = () => {
|
|
1197
|
+
if (previewPollingTimer !== null) {
|
|
1198
|
+
window.clearInterval(previewPollingTimer);
|
|
1199
|
+
previewPollingTimer = null;
|
|
1200
|
+
}
|
|
1201
|
+
resolve();
|
|
1202
|
+
};
|
|
1203
|
+
audio.onerror = () => {
|
|
1204
|
+
if (previewPollingTimer !== null) {
|
|
1205
|
+
window.clearInterval(previewPollingTimer);
|
|
1206
|
+
previewPollingTimer = null;
|
|
1207
|
+
}
|
|
1208
|
+
reject(new Error("Failed to play preview audio."));
|
|
1209
|
+
};
|
|
1210
|
+
void audio.play().catch(reject);
|
|
1211
|
+
});
|
|
1212
|
+
currentPreviewAudio = null;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
972
1215
|
async function previewServerVoice(provider: "polly" | "google") {
|
|
973
1216
|
const endpoint = normalizeApiEndpoint(
|
|
974
1217
|
provider === "polly" ? pollyApiEndpoint : googleApiEndpoint,
|
|
@@ -1115,8 +1358,27 @@
|
|
|
1115
1358
|
try {
|
|
1116
1359
|
if (!isBuiltInTab(activeTab)) {
|
|
1117
1360
|
const provider = getCustomProviderOrThrow(activeTab);
|
|
1118
|
-
const result = await provider.preview?.(
|
|
1119
|
-
|
|
1361
|
+
const result = await provider.preview?.(
|
|
1362
|
+
createProviderContext(provider.id, {
|
|
1363
|
+
previewText,
|
|
1364
|
+
previewMode
|
|
1365
|
+
})
|
|
1366
|
+
);
|
|
1367
|
+
const customResult = (result || null) as CustomProviderPreviewResult | null;
|
|
1368
|
+
if (customResult?.audioUrl && typeof customResult.audioUrl === "string") {
|
|
1369
|
+
const speechMarks = Array.isArray(customResult.speechMarks)
|
|
1370
|
+
? customResult.speechMarks
|
|
1371
|
+
: [];
|
|
1372
|
+
const trackingText =
|
|
1373
|
+
typeof customResult.trackingText === "string"
|
|
1374
|
+
? customResult.trackingText
|
|
1375
|
+
: null;
|
|
1376
|
+
if (trackingText) {
|
|
1377
|
+
previewText = trackingText;
|
|
1378
|
+
}
|
|
1379
|
+
await playPreviewAudioFromUrl(customResult.audioUrl, speechMarks);
|
|
1380
|
+
}
|
|
1381
|
+
previewNote = result?.note || null;
|
|
1120
1382
|
} else if (activeTab === "browser") {
|
|
1121
1383
|
await previewBrowserVoice();
|
|
1122
1384
|
} else if (activeTab === "polly") {
|
|
@@ -1258,6 +1520,22 @@
|
|
|
1258
1520
|
void checkActiveTabAvailability();
|
|
1259
1521
|
});
|
|
1260
1522
|
|
|
1523
|
+
$effect(() => {
|
|
1524
|
+
if (!dialogEl) return;
|
|
1525
|
+
cleanupFocusTrap?.();
|
|
1526
|
+
cleanupFocusTrap = createLocalFocusTrap(dialogEl, {
|
|
1527
|
+
initialFocus: closeButtonEl,
|
|
1528
|
+
onEscape: requestClose
|
|
1529
|
+
});
|
|
1530
|
+
queueMicrotask(() => {
|
|
1531
|
+
closeButtonEl?.focus?.();
|
|
1532
|
+
});
|
|
1533
|
+
return () => {
|
|
1534
|
+
cleanupFocusTrap?.();
|
|
1535
|
+
cleanupFocusTrap = null;
|
|
1536
|
+
};
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1261
1539
|
$effect(() => {
|
|
1262
1540
|
void normalizedCustomProviders;
|
|
1263
1541
|
untrack(() => {
|
|
@@ -1287,15 +1565,30 @@
|
|
|
1287
1565
|
});
|
|
1288
1566
|
|
|
1289
1567
|
onDestroy(() => {
|
|
1568
|
+
cleanupFocusTrap?.();
|
|
1569
|
+
cleanupFocusTrap = null;
|
|
1290
1570
|
stopPreview();
|
|
1291
1571
|
});
|
|
1292
1572
|
</script>
|
|
1293
1573
|
|
|
1294
1574
|
<div class="pie-tts-dialog-backdrop" style="z-index: {TTS_MODAL_Z_INDEX};">
|
|
1295
|
-
<div
|
|
1575
|
+
<div
|
|
1576
|
+
class="pie-tts-dialog"
|
|
1577
|
+
bind:this={dialogEl}
|
|
1578
|
+
role="dialog"
|
|
1579
|
+
aria-modal="true"
|
|
1580
|
+
aria-labelledby="pie-tts-dialog-title"
|
|
1581
|
+
tabindex="-1"
|
|
1582
|
+
>
|
|
1296
1583
|
<div class="pie-tts-dialog-header">
|
|
1297
|
-
<h3 class="pie-tts-dialog-title">TTS settings</h3>
|
|
1298
|
-
<button
|
|
1584
|
+
<h3 id="pie-tts-dialog-title" class="pie-tts-dialog-title">TTS settings</h3>
|
|
1585
|
+
<button
|
|
1586
|
+
class="btn btn-xs btn-ghost btn-circle"
|
|
1587
|
+
bind:this={closeButtonEl}
|
|
1588
|
+
type="button"
|
|
1589
|
+
onclick={requestClose}
|
|
1590
|
+
aria-label="Close TTS settings"
|
|
1591
|
+
>
|
|
1299
1592
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
1300
1593
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
1301
1594
|
</svg>
|
|
@@ -1345,18 +1638,6 @@
|
|
|
1345
1638
|
{/each}
|
|
1346
1639
|
</select>
|
|
1347
1640
|
</div>
|
|
1348
|
-
<div class="pie-tts-grid-2">
|
|
1349
|
-
<div class="pie-tts-field">
|
|
1350
|
-
<label class="pie-tts-label" for="tts-browser-rate">Rate</label>
|
|
1351
|
-
<input id="tts-browser-rate" class="range range-primary pie-tts-range" type="range" min="0.25" max="4" step="0.05" bind:value={browserRate} />
|
|
1352
|
-
<div class="pie-tts-range-value">{Number(browserRate).toFixed(2)}x</div>
|
|
1353
|
-
</div>
|
|
1354
|
-
<div class="pie-tts-field">
|
|
1355
|
-
<label class="pie-tts-label" for="tts-browser-pitch">Pitch</label>
|
|
1356
|
-
<input id="tts-browser-pitch" class="range range-secondary pie-tts-range" type="range" min="0" max="2" step="0.05" bind:value={browserPitch} />
|
|
1357
|
-
<div class="pie-tts-range-value">{Number(browserPitch).toFixed(2)}</div>
|
|
1358
|
-
</div>
|
|
1359
|
-
</div>
|
|
1360
1641
|
</fieldset>
|
|
1361
1642
|
{:else if activeTab === "polly"}
|
|
1362
1643
|
<fieldset class="pie-tts-fieldset fieldset bg-base-200 border border-base-300 rounded-box" disabled={!pollyState.available}>
|
|
@@ -1404,21 +1685,14 @@
|
|
|
1404
1685
|
</div>
|
|
1405
1686
|
</div>
|
|
1406
1687
|
|
|
1407
|
-
<div class="pie-tts-
|
|
1408
|
-
<
|
|
1409
|
-
|
|
1410
|
-
<
|
|
1411
|
-
|
|
1412
|
-
{
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
</select>
|
|
1416
|
-
</div>
|
|
1417
|
-
<div class="pie-tts-field">
|
|
1418
|
-
<label class="pie-tts-label" for="tts-polly-rate">Rate</label>
|
|
1419
|
-
<input id="tts-polly-rate" class="range range-primary pie-tts-range" type="range" min="0.25" max="4" step="0.05" bind:value={pollyRate} />
|
|
1420
|
-
<div class="pie-tts-range-value">{Number(pollyRate).toFixed(2)}x</div>
|
|
1421
|
-
</div>
|
|
1688
|
+
<div class="pie-tts-field">
|
|
1689
|
+
<label class="pie-tts-label" for="tts-polly-voice">Voice</label>
|
|
1690
|
+
<select id="tts-polly-voice" class="select select-sm select-bordered w-full" bind:value={pollyVoice}>
|
|
1691
|
+
<option value="">Provider default</option>
|
|
1692
|
+
{#each pollyState.voices as voice}
|
|
1693
|
+
<option value={voice.id || voice.name || ""}>{voice.name || voice.id} ({voice.languageCode || "n/a"})</option>
|
|
1694
|
+
{/each}
|
|
1695
|
+
</select>
|
|
1422
1696
|
</div>
|
|
1423
1697
|
|
|
1424
1698
|
<div class="pie-tts-grid-3">
|
|
@@ -1495,21 +1769,14 @@
|
|
|
1495
1769
|
</div>
|
|
1496
1770
|
</div>
|
|
1497
1771
|
|
|
1498
|
-
<div class="pie-tts-
|
|
1499
|
-
<
|
|
1500
|
-
|
|
1501
|
-
<
|
|
1502
|
-
|
|
1503
|
-
{
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
</select>
|
|
1507
|
-
</div>
|
|
1508
|
-
<div class="pie-tts-field">
|
|
1509
|
-
<label class="pie-tts-label" for="tts-google-rate">Rate</label>
|
|
1510
|
-
<input id="tts-google-rate" class="range range-primary pie-tts-range" type="range" min="0.25" max="4" step="0.05" bind:value={googleRate} />
|
|
1511
|
-
<div class="pie-tts-range-value">{Number(googleRate).toFixed(2)}x</div>
|
|
1512
|
-
</div>
|
|
1772
|
+
<div class="pie-tts-field">
|
|
1773
|
+
<label class="pie-tts-label" for="tts-google-voice">Voice</label>
|
|
1774
|
+
<select id="tts-google-voice" class="select select-sm select-bordered w-full" bind:value={googleVoice}>
|
|
1775
|
+
<option value="">Provider default</option>
|
|
1776
|
+
{#each googleState.voices as voice}
|
|
1777
|
+
<option value={voice.id || voice.name || ""}>{voice.name || voice.id} ({voice.languageCode || "n/a"})</option>
|
|
1778
|
+
{/each}
|
|
1779
|
+
</select>
|
|
1513
1780
|
</div>
|
|
1514
1781
|
</fieldset>
|
|
1515
1782
|
{:else if activeCustomProvider}
|
|
@@ -1575,6 +1842,8 @@
|
|
|
1575
1842
|
SSML is unsupported in Browser preview.
|
|
1576
1843
|
{:else if activeTab === "google" && previewMode === "ssml"}
|
|
1577
1844
|
SSML preserved. Tracking disabled.
|
|
1845
|
+
{:else if !isBuiltInTab(activeTab)}
|
|
1846
|
+
Tracking enabled when provider returns speech marks.
|
|
1578
1847
|
{:else}
|
|
1579
1848
|
Tracking enabled while preview plays.
|
|
1580
1849
|
{/if}
|
|
@@ -1711,13 +1980,6 @@
|
|
|
1711
1980
|
padding: 0;
|
|
1712
1981
|
}
|
|
1713
1982
|
|
|
1714
|
-
.pie-tts-grid-2 {
|
|
1715
|
-
display: grid;
|
|
1716
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1717
|
-
gap: 0.45rem 0.65rem;
|
|
1718
|
-
align-items: end;
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
1983
|
.pie-tts-grid-3 {
|
|
1722
1984
|
display: grid;
|
|
1723
1985
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
@@ -1725,17 +1987,8 @@
|
|
|
1725
1987
|
align-items: end;
|
|
1726
1988
|
}
|
|
1727
1989
|
|
|
1728
|
-
.pie-tts-grid-voice-rate {
|
|
1729
|
-
display: grid;
|
|
1730
|
-
grid-template-columns: minmax(0, 1fr) minmax(9.5rem, 11rem);
|
|
1731
|
-
gap: 0.45rem 0.65rem;
|
|
1732
|
-
align-items: end;
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
1990
|
@media (max-width: 32rem) {
|
|
1736
|
-
.pie-tts-grid-
|
|
1737
|
-
.pie-tts-grid-3,
|
|
1738
|
-
.pie-tts-grid-voice-rate {
|
|
1991
|
+
.pie-tts-grid-3 {
|
|
1739
1992
|
grid-template-columns: 1fr;
|
|
1740
1993
|
}
|
|
1741
1994
|
}
|