@pie-players/pie-section-player-tools-tts-settings 0.3.20 → 0.3.22
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 +341 -87
- package/dist/section-player-tools-tts-settings.js +2137 -2025
- 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") {
|
|
@@ -1138,6 +1400,7 @@
|
|
|
1138
1400
|
}
|
|
1139
1401
|
} finally {
|
|
1140
1402
|
if (runId === previewRunId) {
|
|
1403
|
+
clearPreviewTracking();
|
|
1141
1404
|
isPreviewing = false;
|
|
1142
1405
|
previewBackend = null;
|
|
1143
1406
|
}
|
|
@@ -1258,6 +1521,22 @@
|
|
|
1258
1521
|
void checkActiveTabAvailability();
|
|
1259
1522
|
});
|
|
1260
1523
|
|
|
1524
|
+
$effect(() => {
|
|
1525
|
+
if (!dialogEl) return;
|
|
1526
|
+
cleanupFocusTrap?.();
|
|
1527
|
+
cleanupFocusTrap = createLocalFocusTrap(dialogEl, {
|
|
1528
|
+
initialFocus: closeButtonEl,
|
|
1529
|
+
onEscape: requestClose
|
|
1530
|
+
});
|
|
1531
|
+
queueMicrotask(() => {
|
|
1532
|
+
closeButtonEl?.focus?.();
|
|
1533
|
+
});
|
|
1534
|
+
return () => {
|
|
1535
|
+
cleanupFocusTrap?.();
|
|
1536
|
+
cleanupFocusTrap = null;
|
|
1537
|
+
};
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1261
1540
|
$effect(() => {
|
|
1262
1541
|
void normalizedCustomProviders;
|
|
1263
1542
|
untrack(() => {
|
|
@@ -1287,15 +1566,30 @@
|
|
|
1287
1566
|
});
|
|
1288
1567
|
|
|
1289
1568
|
onDestroy(() => {
|
|
1569
|
+
cleanupFocusTrap?.();
|
|
1570
|
+
cleanupFocusTrap = null;
|
|
1290
1571
|
stopPreview();
|
|
1291
1572
|
});
|
|
1292
1573
|
</script>
|
|
1293
1574
|
|
|
1294
1575
|
<div class="pie-tts-dialog-backdrop" style="z-index: {TTS_MODAL_Z_INDEX};">
|
|
1295
|
-
<div
|
|
1576
|
+
<div
|
|
1577
|
+
class="pie-tts-dialog"
|
|
1578
|
+
bind:this={dialogEl}
|
|
1579
|
+
role="dialog"
|
|
1580
|
+
aria-modal="true"
|
|
1581
|
+
aria-labelledby="pie-tts-dialog-title"
|
|
1582
|
+
tabindex="-1"
|
|
1583
|
+
>
|
|
1296
1584
|
<div class="pie-tts-dialog-header">
|
|
1297
|
-
<h3 class="pie-tts-dialog-title">TTS settings</h3>
|
|
1298
|
-
<button
|
|
1585
|
+
<h3 id="pie-tts-dialog-title" class="pie-tts-dialog-title">TTS settings</h3>
|
|
1586
|
+
<button
|
|
1587
|
+
class="btn btn-xs btn-ghost btn-circle"
|
|
1588
|
+
bind:this={closeButtonEl}
|
|
1589
|
+
type="button"
|
|
1590
|
+
onclick={requestClose}
|
|
1591
|
+
aria-label="Close TTS settings"
|
|
1592
|
+
>
|
|
1299
1593
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
1300
1594
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
1301
1595
|
</svg>
|
|
@@ -1345,18 +1639,6 @@
|
|
|
1345
1639
|
{/each}
|
|
1346
1640
|
</select>
|
|
1347
1641
|
</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
1642
|
</fieldset>
|
|
1361
1643
|
{:else if activeTab === "polly"}
|
|
1362
1644
|
<fieldset class="pie-tts-fieldset fieldset bg-base-200 border border-base-300 rounded-box" disabled={!pollyState.available}>
|
|
@@ -1404,21 +1686,14 @@
|
|
|
1404
1686
|
</div>
|
|
1405
1687
|
</div>
|
|
1406
1688
|
|
|
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>
|
|
1689
|
+
<div class="pie-tts-field">
|
|
1690
|
+
<label class="pie-tts-label" for="tts-polly-voice">Voice</label>
|
|
1691
|
+
<select id="tts-polly-voice" class="select select-sm select-bordered w-full" bind:value={pollyVoice}>
|
|
1692
|
+
<option value="">Provider default</option>
|
|
1693
|
+
{#each pollyState.voices as voice}
|
|
1694
|
+
<option value={voice.id || voice.name || ""}>{voice.name || voice.id} ({voice.languageCode || "n/a"})</option>
|
|
1695
|
+
{/each}
|
|
1696
|
+
</select>
|
|
1422
1697
|
</div>
|
|
1423
1698
|
|
|
1424
1699
|
<div class="pie-tts-grid-3">
|
|
@@ -1495,21 +1770,14 @@
|
|
|
1495
1770
|
</div>
|
|
1496
1771
|
</div>
|
|
1497
1772
|
|
|
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>
|
|
1773
|
+
<div class="pie-tts-field">
|
|
1774
|
+
<label class="pie-tts-label" for="tts-google-voice">Voice</label>
|
|
1775
|
+
<select id="tts-google-voice" class="select select-sm select-bordered w-full" bind:value={googleVoice}>
|
|
1776
|
+
<option value="">Provider default</option>
|
|
1777
|
+
{#each googleState.voices as voice}
|
|
1778
|
+
<option value={voice.id || voice.name || ""}>{voice.name || voice.id} ({voice.languageCode || "n/a"})</option>
|
|
1779
|
+
{/each}
|
|
1780
|
+
</select>
|
|
1513
1781
|
</div>
|
|
1514
1782
|
</fieldset>
|
|
1515
1783
|
{:else if activeCustomProvider}
|
|
@@ -1575,6 +1843,8 @@
|
|
|
1575
1843
|
SSML is unsupported in Browser preview.
|
|
1576
1844
|
{:else if activeTab === "google" && previewMode === "ssml"}
|
|
1577
1845
|
SSML preserved. Tracking disabled.
|
|
1846
|
+
{:else if !isBuiltInTab(activeTab)}
|
|
1847
|
+
Tracking enabled when provider returns speech marks.
|
|
1578
1848
|
{:else}
|
|
1579
1849
|
Tracking enabled while preview plays.
|
|
1580
1850
|
{/if}
|
|
@@ -1711,13 +1981,6 @@
|
|
|
1711
1981
|
padding: 0;
|
|
1712
1982
|
}
|
|
1713
1983
|
|
|
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
1984
|
.pie-tts-grid-3 {
|
|
1722
1985
|
display: grid;
|
|
1723
1986
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
@@ -1725,17 +1988,8 @@
|
|
|
1725
1988
|
align-items: end;
|
|
1726
1989
|
}
|
|
1727
1990
|
|
|
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
1991
|
@media (max-width: 32rem) {
|
|
1736
|
-
.pie-tts-grid-
|
|
1737
|
-
.pie-tts-grid-3,
|
|
1738
|
-
.pie-tts-grid-voice-rate {
|
|
1992
|
+
.pie-tts-grid-3 {
|
|
1739
1993
|
grid-template-columns: 1fr;
|
|
1740
1994
|
}
|
|
1741
1995
|
}
|