mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Enhance manual override functionality with status updates and improved UI elements
This commit is contained in:
+181
-26
@@ -1168,6 +1168,36 @@ const initPrepare = (root = document) => {
|
|||||||
const manualOverrideSearchButton = manualOverridesRoot?.querySelector('[data-role="manual-override-search"]');
|
const manualOverrideSearchButton = manualOverridesRoot?.querySelector('[data-role="manual-override-search"]');
|
||||||
const manualOverrideAddCustomButton = manualOverridesRoot?.querySelector('[data-role="manual-override-add-custom"]');
|
const manualOverrideAddCustomButton = manualOverridesRoot?.querySelector('[data-role="manual-override-add-custom"]');
|
||||||
const manualOverridesEmpty = manualOverridesRoot?.querySelector('[data-role="manual-overrides-empty"]');
|
const manualOverridesEmpty = manualOverridesRoot?.querySelector('[data-role="manual-overrides-empty"]');
|
||||||
|
const manualOverrideSaveButton = manualOverridesRoot?.querySelector('[data-role="manual-override-save-all"]');
|
||||||
|
const manualOverrideStatusNode = manualOverridesRoot?.querySelector('[data-role="manual-override-status"]');
|
||||||
|
let manualOverrideStatusTimer = null;
|
||||||
|
let manualOverrideStatusNonce = 0;
|
||||||
|
|
||||||
|
const setManualOverrideStatus = (message, state = "") => {
|
||||||
|
if (!manualOverrideStatusNode) return;
|
||||||
|
manualOverrideStatusNonce += 1;
|
||||||
|
const nonce = manualOverrideStatusNonce;
|
||||||
|
if (manualOverrideStatusTimer) {
|
||||||
|
window.clearTimeout(manualOverrideStatusTimer);
|
||||||
|
manualOverrideStatusTimer = null;
|
||||||
|
}
|
||||||
|
manualOverrideStatusNode.textContent = message || "";
|
||||||
|
if (state) {
|
||||||
|
manualOverrideStatusNode.dataset.state = state;
|
||||||
|
} else {
|
||||||
|
manualOverrideStatusNode.removeAttribute("data-state");
|
||||||
|
}
|
||||||
|
if (state === "success" && message) {
|
||||||
|
manualOverrideStatusTimer = window.setTimeout(() => {
|
||||||
|
if (manualOverrideStatusNonce !== nonce) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
manualOverrideStatusNode.textContent = "";
|
||||||
|
manualOverrideStatusNode.removeAttribute("data-state");
|
||||||
|
manualOverrideStatusTimer = null;
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (entitiesRefreshButton) {
|
if (entitiesRefreshButton) {
|
||||||
entitiesRefreshButton.disabled = !entitiesEnabled;
|
entitiesRefreshButton.disabled = !entitiesEnabled;
|
||||||
@@ -1932,6 +1962,7 @@ const initPrepare = (root = document) => {
|
|||||||
markOverrideDirty = (overrideId) => {
|
markOverrideDirty = (overrideId) => {
|
||||||
if (!overrideId) return;
|
if (!overrideId) return;
|
||||||
dirtyOverrideIds.add(overrideId);
|
dirtyOverrideIds.add(overrideId);
|
||||||
|
setManualOverrideStatus("Unsaved changes", "pending");
|
||||||
};
|
};
|
||||||
|
|
||||||
flushManualOverrides = (options = {}) => {
|
flushManualOverrides = (options = {}) => {
|
||||||
@@ -1939,6 +1970,7 @@ const initPrepare = (root = document) => {
|
|||||||
const pendingIds = Array.isArray(overrideId) ? overrideId : overrideId ? [overrideId] : Array.from(dirtyOverrideIds);
|
const pendingIds = Array.isArray(overrideId) ? overrideId : overrideId ? [overrideId] : Array.from(dirtyOverrideIds);
|
||||||
const targetIds = pendingIds.filter((id) => dirtyOverrideIds.has(id));
|
const targetIds = pendingIds.filter((id) => dirtyOverrideIds.has(id));
|
||||||
if (!targetIds.length) return null;
|
if (!targetIds.length) return null;
|
||||||
|
setManualOverrideStatus("Saving overrides…", "pending");
|
||||||
targetIds.forEach((id) => dirtyOverrideIds.delete(id));
|
targetIds.forEach((id) => dirtyOverrideIds.delete(id));
|
||||||
const runFlush = async () => {
|
const runFlush = async () => {
|
||||||
if (overrideFlushPromise) {
|
if (overrideFlushPromise) {
|
||||||
@@ -1949,15 +1981,23 @@ const initPrepare = (root = document) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const results = await Promise.all(targetIds.map((id) => saveManualOverride(id)));
|
const results = await Promise.all(targetIds.map((id) => saveManualOverride(id)));
|
||||||
|
let hasFailure = false;
|
||||||
results.forEach((ok, index) => {
|
results.forEach((ok, index) => {
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
hasFailure = true;
|
||||||
dirtyOverrideIds.add(targetIds[index]);
|
dirtyOverrideIds.add(targetIds[index]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (hasFailure) {
|
||||||
|
setManualOverrideStatus("Some overrides failed to save.", "error");
|
||||||
|
} else {
|
||||||
|
setManualOverrideStatus("Overrides saved.", "success");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
overrideFlushPromise = runFlush()
|
overrideFlushPromise = runFlush()
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Failed flushing manual overrides", error);
|
console.error("Failed flushing manual overrides", error);
|
||||||
|
setManualOverrideStatus("Failed to save overrides.", "error");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
overrideFlushPromise = null;
|
overrideFlushPromise = null;
|
||||||
@@ -2060,9 +2100,9 @@ const initPrepare = (root = document) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createOverrideFromData(data) {
|
async function createOverrideFromData(data) {
|
||||||
if (!data || !manualUpsertUrl) return;
|
if (!data || !manualUpsertUrl) return false;
|
||||||
const token = (data.token || "").trim();
|
const token = (data.token || "").trim();
|
||||||
if (!token) return;
|
if (!token) return false;
|
||||||
const normalized = (data.normalized || "").trim();
|
const normalized = (data.normalized || "").trim();
|
||||||
const voiceChoice = resolveOverrideVoice({ ...data, token, normalized });
|
const voiceChoice = resolveOverrideVoice({ ...data, token, normalized });
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -2095,8 +2135,10 @@ const initPrepare = (root = document) => {
|
|||||||
manualOverrideResultsList.innerHTML = "";
|
manualOverrideResultsList.innerHTML = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create manual override", error);
|
console.error("Failed to create manual override", error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2114,7 +2156,103 @@ const initPrepare = (root = document) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEntityListClick = (event) => {
|
const extractInlineOverrideState = (inlineOverride, row) => {
|
||||||
|
if (!inlineOverride || inlineOverride.hidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const container = row || inlineOverride.closest('[data-role="entity-row"]');
|
||||||
|
if (!container) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const tokenLabel = inlineOverride.dataset.pendingToken || container.querySelector('[data-role="entity-label"]')?.textContent || "";
|
||||||
|
if (!tokenLabel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalized = (inlineOverride.dataset.pendingNormalized || container.dataset.normalized || tokenLabel || "").trim();
|
||||||
|
const context = (inlineOverride.dataset.pendingContext || "").trim();
|
||||||
|
const pronInput = inlineOverride.querySelector('[data-role="manual-override-pronunciation"]');
|
||||||
|
const voiceSelect = inlineOverride.querySelector('[data-role="manual-override-voice"]');
|
||||||
|
const pronunciationRaw = pronInput?.value ?? "";
|
||||||
|
const pronunciation = pronunciationRaw.trim();
|
||||||
|
const voiceRaw = voiceSelect?.value ?? "";
|
||||||
|
const voice = voiceRaw;
|
||||||
|
const previewVoice = voice || baseVoice || "";
|
||||||
|
const previewText = pronunciation || tokenLabel;
|
||||||
|
return {
|
||||||
|
token: tokenLabel,
|
||||||
|
normalized,
|
||||||
|
context,
|
||||||
|
pronunciation,
|
||||||
|
voice,
|
||||||
|
previewVoice,
|
||||||
|
previewText,
|
||||||
|
category: inlineOverride.dataset.pendingCategory || container.dataset.entityCategory || "",
|
||||||
|
kind: inlineOverride.dataset.pendingKind || container.dataset.entityKind || "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncInlinePreviewUI = (row, inlineOverride, state, targetButton) => {
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previewText = state.previewText || state.token;
|
||||||
|
const previewVoice = state.previewVoice || baseVoice || "";
|
||||||
|
const applyDatasets = (button) => {
|
||||||
|
if (!button) return;
|
||||||
|
button.dataset.previewText = previewText;
|
||||||
|
button.dataset.voice = previewVoice;
|
||||||
|
button.dataset.language = languageCode;
|
||||||
|
button.dataset.speed = defaultSpeed;
|
||||||
|
button.dataset.useGpu = useGpuDefault;
|
||||||
|
if (!button.dataset.previewSource || button.dataset.previewSource === "entity") {
|
||||||
|
button.dataset.previewSource = "manual";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (targetButton) {
|
||||||
|
applyDatasets(targetButton);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inlineOverride) {
|
||||||
|
applyDatasets(inlineOverride.querySelector('[data-role="speaker-preview"]'));
|
||||||
|
}
|
||||||
|
if (row) {
|
||||||
|
applyDatasets(row.querySelector('[data-entity-preview="true"]'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistInlineOverrideState = (state) => {
|
||||||
|
if (!state) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
highlightMode = "inline";
|
||||||
|
setManualOverrideStatus("Saving overrides…", "pending");
|
||||||
|
return createOverrideFromData({
|
||||||
|
token: state.token,
|
||||||
|
normalized: state.normalized,
|
||||||
|
context: state.context,
|
||||||
|
pronunciation: state.pronunciation,
|
||||||
|
voice: state.voice,
|
||||||
|
category: state.category,
|
||||||
|
kind: state.kind,
|
||||||
|
source: "entity-inline",
|
||||||
|
})
|
||||||
|
.then((ok) => {
|
||||||
|
if (ok) {
|
||||||
|
setManualOverrideStatus("Overrides saved.", "success");
|
||||||
|
} else {
|
||||||
|
setManualOverrideStatus("Failed to save overrides.", "error");
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setManualOverrideStatus("Failed to save overrides.", "error");
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEntityListClick = (event) => {
|
||||||
const addTrigger = event.target.closest('[data-role="entity-add-override"]');
|
const addTrigger = event.target.closest('[data-role="entity-add-override"]');
|
||||||
if (addTrigger) {
|
if (addTrigger) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -2160,13 +2298,8 @@ const initPrepare = (root = document) => {
|
|||||||
inlineOverride.dataset.pendingCategory = addTrigger.dataset.entityCategory || row.dataset.entityCategory || "";
|
inlineOverride.dataset.pendingCategory = addTrigger.dataset.entityCategory || row.dataset.entityCategory || "";
|
||||||
inlineOverride.dataset.pendingKind = addTrigger.dataset.entityKind || row.dataset.entityKind || "";
|
inlineOverride.dataset.pendingKind = addTrigger.dataset.entityKind || row.dataset.entityKind || "";
|
||||||
|
|
||||||
if (previewButton) {
|
const inlineState = extractInlineOverrideState(inlineOverride, row);
|
||||||
previewButton.dataset.previewText = pronInput?.value || token;
|
syncInlinePreviewUI(row, inlineOverride, inlineState, previewButton);
|
||||||
previewButton.dataset.voice = voiceSelect?.value || baseVoice || "";
|
|
||||||
previewButton.dataset.language = languageCode;
|
|
||||||
previewButton.dataset.speed = defaultSpeed;
|
|
||||||
previewButton.dataset.useGpu = useGpuDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
highlightMode = "inline";
|
highlightMode = "inline";
|
||||||
highlightedOverrideId = row.dataset.overrideId || "";
|
highlightedOverrideId = row.dataset.overrideId || "";
|
||||||
@@ -2182,22 +2315,9 @@ const initPrepare = (root = document) => {
|
|||||||
if (!inlineOverride || !row) {
|
if (!inlineOverride || !row) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pronInput = inlineOverride.querySelector('[data-role="manual-override-pronunciation"]');
|
const inlineState = extractInlineOverrideState(inlineOverride, row);
|
||||||
const voiceSelect = inlineOverride.querySelector('[data-role="manual-override-voice"]');
|
syncInlinePreviewUI(row, inlineOverride, inlineState);
|
||||||
const token = inlineOverride.dataset.pendingToken || row.querySelector('[data-role="entity-label"]')?.textContent || "";
|
void persistInlineOverrideState(inlineState);
|
||||||
const normalized = inlineOverride.dataset.pendingNormalized || row.dataset.normalized || token;
|
|
||||||
const context = inlineOverride.dataset.pendingContext || "";
|
|
||||||
highlightMode = "inline";
|
|
||||||
createOverrideFromData({
|
|
||||||
token,
|
|
||||||
normalized,
|
|
||||||
context,
|
|
||||||
pronunciation: pronInput?.value || "",
|
|
||||||
voice: voiceSelect?.value || "",
|
|
||||||
category: inlineOverride.dataset.pendingCategory || row.dataset.entityCategory || "",
|
|
||||||
kind: inlineOverride.dataset.pendingKind || row.dataset.entityKind || "",
|
|
||||||
source: "entity-inline",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2351,6 +2471,15 @@ const initPrepare = (root = document) => {
|
|||||||
|
|
||||||
form.addEventListener("click", (event) => {
|
form.addEventListener("click", (event) => {
|
||||||
const previewFromOverride = event.target.closest('[data-role="speaker-preview"]');
|
const previewFromOverride = event.target.closest('[data-role="speaker-preview"]');
|
||||||
|
if (previewFromOverride) {
|
||||||
|
const entityRow = previewFromOverride.closest('[data-role="entity-row"]');
|
||||||
|
const inlineOverride = entityRow?.querySelector('[data-role="inline-override"]');
|
||||||
|
if (inlineOverride && !inlineOverride.hidden) {
|
||||||
|
const inlineState = extractInlineOverrideState(inlineOverride, entityRow);
|
||||||
|
syncInlinePreviewUI(entityRow, inlineOverride, inlineState, previewFromOverride);
|
||||||
|
void persistInlineOverrideState(inlineState);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (previewFromOverride?.dataset.overrideId) {
|
if (previewFromOverride?.dataset.overrideId) {
|
||||||
void flushManualOverrides({ overrideId: previewFromOverride.dataset.overrideId });
|
void flushManualOverrides({ overrideId: previewFromOverride.dataset.overrideId });
|
||||||
}
|
}
|
||||||
@@ -2439,6 +2568,32 @@ const initPrepare = (root = document) => {
|
|||||||
select.dataset.prevManual = select.value || "";
|
select.dataset.prevManual = select.value || "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
form.addEventListener("input", (event) => {
|
||||||
|
const inlineOverride = event.target.closest('[data-role="inline-override"]');
|
||||||
|
if (!inlineOverride || inlineOverride.hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entityRow = inlineOverride.closest('[data-role="entity-row"]');
|
||||||
|
const inlineState = extractInlineOverrideState(inlineOverride, entityRow);
|
||||||
|
syncInlinePreviewUI(entityRow, inlineOverride, inlineState);
|
||||||
|
if (inlineState) {
|
||||||
|
setManualOverrideStatus("Unsaved changes", "pending");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener("change", (event) => {
|
||||||
|
const inlineOverride = event.target.closest('[data-role="inline-override"]');
|
||||||
|
if (!inlineOverride || inlineOverride.hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entityRow = inlineOverride.closest('[data-role="entity-row"]');
|
||||||
|
const inlineState = extractInlineOverrideState(inlineOverride, entityRow);
|
||||||
|
syncInlinePreviewUI(entityRow, inlineOverride, inlineState);
|
||||||
|
if (inlineState) {
|
||||||
|
setManualOverrideStatus("Unsaved changes", "pending");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
if (!form.contains(event.target)) {
|
if (!form.contains(event.target)) {
|
||||||
hideGenderMenus();
|
hideGenderMenus();
|
||||||
|
|||||||
@@ -1816,10 +1816,47 @@ button.step-indicator__item:focus-visible {
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.manual-overrides__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-overrides__copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
.manual-overrides__header h2 {
|
.manual-overrides__header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.manual-overrides__header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-overrides__status {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--muted);
|
||||||
|
min-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-overrides__status[data-state="success"] {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-overrides__status[data-state="error"] {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-overrides__status[data-state="pending"] {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.manual-overrides__search {
|
.manual-overrides__search {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
@@ -1849,6 +1886,16 @@ button.step-indicator__item:focus-visible {
|
|||||||
grid-column: span 1;
|
grid-column: span 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.manual-overrides__header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-overrides__header-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.entity-tabs__nav {
|
.entity-tabs__nav {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
|
|||||||
@@ -21,6 +21,33 @@
|
|||||||
data-entities-enabled="{{ 'true' if recognition_enabled else 'false' }}">
|
data-entities-enabled="{{ 'true' if recognition_enabled else 'false' }}">
|
||||||
<input type="hidden" name="active_step" value="entities" data-role="active-step-input">
|
<input type="hidden" name="active_step" value="entities" data-role="active-step-input">
|
||||||
|
|
||||||
|
<div class="wizard-hidden-inputs" aria-hidden="true">
|
||||||
|
<input type="hidden" name="chunk_level" value="{{ pending.chunk_level }}">
|
||||||
|
<input type="hidden" name="speaker_analysis_threshold" value="{{ pending.speaker_analysis_threshold }}">
|
||||||
|
<input type="hidden" name="chapter_intro_delay" value="{{ '%.2f'|format(pending.chapter_intro_delay) }}">
|
||||||
|
{% if pending.generate_epub3 %}
|
||||||
|
<input type="hidden" name="generate_epub3" value="true">
|
||||||
|
{% endif %}
|
||||||
|
{% for chapter in pending.chapters %}
|
||||||
|
{% set idx = loop.index0 %}
|
||||||
|
{% set is_enabled = chapter.enabled is not defined or chapter.enabled %}
|
||||||
|
{% set selected_option = '__default' %}
|
||||||
|
{% if chapter.voice_profile %}
|
||||||
|
{% set selected_option = 'profile:' ~ chapter.voice_profile %}
|
||||||
|
{% elif chapter.voice %}
|
||||||
|
{% set selected_option = 'voice:' ~ chapter.voice %}
|
||||||
|
{% elif chapter.voice_formula %}
|
||||||
|
{% set selected_option = 'formula' %}
|
||||||
|
{% endif %}
|
||||||
|
{% if is_enabled %}
|
||||||
|
<input type="hidden" name="chapter-{{ idx }}-enabled" value="on">
|
||||||
|
{% endif %}
|
||||||
|
<input type="hidden" name="chapter-{{ idx }}-title" value="{{ chapter.title }}">
|
||||||
|
<input type="hidden" name="chapter-{{ idx }}-voice" value="{{ selected_option }}">
|
||||||
|
<input type="hidden" name="chapter-{{ idx }}-formula" value="{{ chapter.voice_formula or '' }}">
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal__body wizard-card__body">
|
<div class="modal__body wizard-card__body">
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert--error">{{ error }}</div>
|
<div class="alert alert--error">{{ error }}</div>
|
||||||
@@ -444,8 +471,14 @@
|
|||||||
hidden>
|
hidden>
|
||||||
<div class="manual-overrides" data-role="manual-overrides">
|
<div class="manual-overrides" data-role="manual-overrides">
|
||||||
<header class="manual-overrides__header">
|
<header class="manual-overrides__header">
|
||||||
<h2>Manual overrides</h2>
|
<div class="manual-overrides__copy">
|
||||||
<p class="hint">Search tokens from the book or add custom entries. Set pronunciations and assign voices to ensure previews and conversions use your preferred delivery.</p>
|
<h2>Manual overrides</h2>
|
||||||
|
<p class="hint">Search tokens from the book or add custom entries. Set pronunciations and assign voices to ensure previews and conversions use your preferred delivery.</p>
|
||||||
|
</div>
|
||||||
|
<div class="manual-overrides__header-actions">
|
||||||
|
<span class="manual-overrides__status" data-role="manual-override-status" aria-live="polite"></span>
|
||||||
|
<button type="button" class="button button--ghost button--small" data-role="manual-override-save-all">Save overrides</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="manual-overrides__search">
|
<div class="manual-overrides__search">
|
||||||
<label class="field" for="manual-override-query">
|
<label class="field" for="manual-override-query">
|
||||||
|
|||||||
Reference in New Issue
Block a user