mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Implement heteronym handling with extraction, UI integration, and processing logic
This commit is contained in:
@@ -906,6 +906,75 @@ def _compile_pronunciation_rules(
|
||||
return compiled
|
||||
|
||||
|
||||
def _compile_heteronym_sentence_rules(
|
||||
overrides: Optional[Iterable[Mapping[str, Any]]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Compile sentence-level replacements for heteronym disambiguation.
|
||||
|
||||
These are intentionally scoped to a specific sentence string rather than a token,
|
||||
so we can apply different pronunciations for the same word in different contexts.
|
||||
|
||||
Expected override entry shape (from pending/job):
|
||||
- sentence: original sentence text
|
||||
- choice: selected option key
|
||||
- options: [{key, replacement_sentence, ...}]
|
||||
"""
|
||||
if not overrides:
|
||||
return []
|
||||
|
||||
compiled: List[Dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
for entry in overrides:
|
||||
if not isinstance(entry, Mapping):
|
||||
continue
|
||||
sentence = str(entry.get("sentence") or "").strip()
|
||||
if not sentence:
|
||||
continue
|
||||
choice = str(entry.get("choice") or "").strip()
|
||||
if not choice:
|
||||
continue
|
||||
|
||||
replacement_sentence = ""
|
||||
options = entry.get("options")
|
||||
if isinstance(options, list):
|
||||
for opt in options:
|
||||
if not isinstance(opt, Mapping):
|
||||
continue
|
||||
if str(opt.get("key") or "").strip() == choice:
|
||||
replacement_sentence = str(opt.get("replacement_sentence") or "").strip()
|
||||
break
|
||||
if not replacement_sentence:
|
||||
continue
|
||||
|
||||
rule_key = f"{sentence}\n{choice}".casefold()
|
||||
if rule_key in seen:
|
||||
continue
|
||||
seen.add(rule_key)
|
||||
|
||||
parts = [p for p in re.split(r"\s+", sentence) if p]
|
||||
if not parts:
|
||||
continue
|
||||
pattern_text = r"\s+".join(re.escape(p) for p in parts)
|
||||
pattern = re.compile(pattern_text)
|
||||
compiled.append({"pattern": pattern, "replacement": replacement_sentence})
|
||||
|
||||
# Replace longer sentences first to avoid partial matches.
|
||||
compiled.sort(key=lambda item: len(item["pattern"].pattern), reverse=True)
|
||||
return compiled
|
||||
|
||||
|
||||
def _apply_heteronym_sentence_rules(text: str, rules: List[Dict[str, Any]]) -> str:
|
||||
if not text or not rules:
|
||||
return text
|
||||
result = text
|
||||
for rule in rules:
|
||||
pattern = rule["pattern"]
|
||||
replacement = rule["replacement"]
|
||||
result = pattern.sub(replacement, result)
|
||||
return result
|
||||
|
||||
|
||||
def _apply_pronunciation_rules(
|
||||
text: str,
|
||||
rules: List[Dict[str, Any]],
|
||||
@@ -1306,6 +1375,14 @@ def run_conversion_job(job: Job) -> None:
|
||||
extraction = extract_from_path(job.stored_path)
|
||||
file_type = _infer_file_type(job.stored_path)
|
||||
pronunciation_rules = _compile_pronunciation_rules(job.pronunciation_overrides)
|
||||
heteronym_sentence_rules = _compile_heteronym_sentence_rules(
|
||||
getattr(job, "heteronym_overrides", None)
|
||||
)
|
||||
if heteronym_sentence_rules:
|
||||
job.add_log(
|
||||
f"Applying {len(heteronym_sentence_rules)} heteronym override{'s' if len(heteronym_sentence_rules) != 1 else ''} during conversion.",
|
||||
level="debug",
|
||||
)
|
||||
if pronunciation_rules:
|
||||
count = len(pronunciation_rules)
|
||||
job.add_log(
|
||||
@@ -1447,6 +1524,8 @@ def run_conversion_job(job: Job) -> None:
|
||||
) -> int:
|
||||
nonlocal processed_chars, subtitle_index, current_time
|
||||
source_text = str(text or "")
|
||||
if heteronym_sentence_rules:
|
||||
source_text = _apply_heteronym_sentence_rules(source_text, heteronym_sentence_rules)
|
||||
if pronunciation_rules:
|
||||
source_text = _apply_pronunciation_rules(
|
||||
source_text,
|
||||
|
||||
@@ -64,6 +64,7 @@ def list_manual_overrides(pending_id: str) -> ResponseReturnValue:
|
||||
return jsonify({
|
||||
"overrides": pending.manual_overrides or [],
|
||||
"pronunciation_overrides": pending.pronunciation_overrides or [],
|
||||
"heteronym_overrides": getattr(pending, "heteronym_overrides", None) or [],
|
||||
"language": pending.language or "en",
|
||||
})
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from abogen.pronunciation_store import (
|
||||
search_overrides as search_pronunciation_overrides,
|
||||
)
|
||||
from abogen.web.routes.utils.settings import load_settings
|
||||
from abogen.heteronym_overrides import extract_heteronym_overrides
|
||||
|
||||
def collect_pronunciation_overrides(pending: PendingJob) -> List[Dict[str, Any]]:
|
||||
language = pending.language or "en"
|
||||
@@ -140,22 +141,34 @@ def sync_pronunciation_overrides(pending: PendingJob) -> None:
|
||||
|
||||
def refresh_entity_summary(pending: PendingJob, chapters: Iterable[Mapping[str, Any]]) -> None:
|
||||
settings = load_settings()
|
||||
if not bool(settings.get("enable_entity_recognition", True)):
|
||||
pending.entity_summary = {}
|
||||
pending.entity_cache_key = ""
|
||||
pending.pronunciation_overrides = pending.pronunciation_overrides or []
|
||||
return
|
||||
|
||||
language = pending.language or "en"
|
||||
chapter_list: List[Mapping[str, Any]] = [chapter for chapter in chapters if isinstance(chapter, Mapping)]
|
||||
if not chapter_list:
|
||||
pending.entity_summary = {}
|
||||
pending.entity_cache_key = ""
|
||||
pending.pronunciation_overrides = pending.pronunciation_overrides or []
|
||||
pending.heteronym_overrides = pending.heteronym_overrides or []
|
||||
return
|
||||
|
||||
enabled_only = [chapter for chapter in chapter_list if chapter.get("enabled")]
|
||||
target_chapters = enabled_only or chapter_list
|
||||
|
||||
# Always compute heteronym overrides (English only). Preserve any prior selections.
|
||||
try:
|
||||
pending.heteronym_overrides = extract_heteronym_overrides(
|
||||
target_chapters,
|
||||
language=language,
|
||||
existing=getattr(pending, "heteronym_overrides", None),
|
||||
)
|
||||
except Exception:
|
||||
pending.heteronym_overrides = getattr(pending, "heteronym_overrides", []) or []
|
||||
|
||||
if not bool(settings.get("enable_entity_recognition", True)):
|
||||
pending.entity_summary = {}
|
||||
pending.entity_cache_key = ""
|
||||
pending.pronunciation_overrides = pending.pronunciation_overrides or []
|
||||
return
|
||||
|
||||
result = extract_entities(target_chapters, language=language)
|
||||
summary = dict(result.summary)
|
||||
tokens: List[str] = []
|
||||
@@ -343,6 +356,7 @@ def pending_entities_payload(pending: PendingJob) -> Dict[str, Any]:
|
||||
"summary": pending.entity_summary or {},
|
||||
"manual_overrides": pending.manual_overrides or [],
|
||||
"pronunciation_overrides": pending.pronunciation_overrides or [],
|
||||
"heteronym_overrides": getattr(pending, "heteronym_overrides", None) or [],
|
||||
"cache_key": pending.entity_cache_key,
|
||||
"language": pending.language or "en",
|
||||
"recognition_enabled": recognition_enabled,
|
||||
|
||||
@@ -396,6 +396,31 @@ def apply_prepare_form(
|
||||
|
||||
enabled_overrides = [entry for entry in overrides if entry.get("enabled")]
|
||||
|
||||
heteronym_entries = getattr(pending, "heteronym_overrides", None)
|
||||
if isinstance(heteronym_entries, list) and heteronym_entries:
|
||||
for entry in heteronym_entries:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
entry_id = str(entry.get("entry_id") or entry.get("id") or "").strip()
|
||||
if not entry_id:
|
||||
continue
|
||||
raw_choice = form.get(f"heteronym-{entry_id}-choice")
|
||||
if raw_choice is None:
|
||||
continue
|
||||
choice = str(raw_choice).strip()
|
||||
if not choice:
|
||||
continue
|
||||
options = entry.get("options")
|
||||
if isinstance(options, list) and options:
|
||||
allowed = {
|
||||
str(opt.get("key")).strip()
|
||||
for opt in options
|
||||
if isinstance(opt, dict) and str(opt.get("key") or "").strip()
|
||||
}
|
||||
if allowed and choice not in allowed:
|
||||
continue
|
||||
entry["choice"] = choice
|
||||
|
||||
sync_pronunciation_overrides(pending)
|
||||
|
||||
return (
|
||||
|
||||
@@ -59,6 +59,7 @@ def submit_job(pending: PendingJob) -> str:
|
||||
entity_summary=getattr(pending, "entity_summary", None),
|
||||
manual_overrides=getattr(pending, "manual_overrides", None),
|
||||
pronunciation_overrides=getattr(pending, "pronunciation_overrides", None),
|
||||
heteronym_overrides=getattr(pending, "heteronym_overrides", None),
|
||||
normalization_overrides=pending.normalization_overrides,
|
||||
)
|
||||
return job.id
|
||||
|
||||
@@ -152,6 +152,7 @@ class Job:
|
||||
entity_summary: Dict[str, Any] = field(default_factory=dict)
|
||||
manual_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
pronunciation_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
heteronym_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
normalization_overrides: Dict[str, Any] = field(default_factory=dict)
|
||||
speaker_voice_languages: List[str] = field(default_factory=list)
|
||||
applied_speaker_config: Optional[str] = None
|
||||
@@ -242,6 +243,7 @@ class Job:
|
||||
"entity_summary": dict(self.entity_summary),
|
||||
"manual_overrides": [dict(entry) for entry in self.manual_overrides],
|
||||
"pronunciation_overrides": [dict(entry) for entry in self.pronunciation_overrides],
|
||||
"heteronym_overrides": [dict(entry) for entry in self.heteronym_overrides],
|
||||
"normalization_overrides": dict(self.normalization_overrides),
|
||||
}
|
||||
|
||||
@@ -565,6 +567,7 @@ class PendingJob:
|
||||
entity_summary: Dict[str, Any] = field(default_factory=dict)
|
||||
manual_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
pronunciation_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
heteronym_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
entity_cache_key: Optional[str] = None
|
||||
wizard_max_step_index: int = 0
|
||||
|
||||
@@ -646,6 +649,7 @@ class ConversionService:
|
||||
entity_summary: Optional[Mapping[str, Any]] = None,
|
||||
manual_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||
pronunciation_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||
heteronym_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||
normalization_overrides: Optional[Mapping[str, Any]] = None,
|
||||
) -> Job:
|
||||
job_id = uuid.uuid4().hex
|
||||
@@ -697,6 +701,7 @@ class ConversionService:
|
||||
entity_summary=dict(entity_summary or {}),
|
||||
manual_overrides=[dict(entry) for entry in manual_overrides] if manual_overrides else [],
|
||||
pronunciation_overrides=[dict(entry) for entry in pronunciation_overrides] if pronunciation_overrides else [],
|
||||
heteronym_overrides=[dict(entry) for entry in heteronym_overrides] if heteronym_overrides else [],
|
||||
normalization_overrides=dict(normalization_overrides or {}),
|
||||
)
|
||||
with self._lock:
|
||||
@@ -1184,6 +1189,7 @@ class ConversionService:
|
||||
"entity_summary": dict(job.entity_summary),
|
||||
"manual_overrides": [dict(entry) for entry in job.manual_overrides],
|
||||
"pronunciation_overrides": [dict(entry) for entry in job.pronunciation_overrides],
|
||||
"heteronym_overrides": [dict(entry) for entry in job.heteronym_overrides],
|
||||
"normalization_overrides": dict(job.normalization_overrides),
|
||||
}
|
||||
|
||||
@@ -1310,6 +1316,9 @@ class ConversionService:
|
||||
job.pronunciation_overrides = [
|
||||
dict(entry) for entry in payload.get("pronunciation_overrides", []) if isinstance(entry, Mapping)
|
||||
]
|
||||
job.heteronym_overrides = [
|
||||
dict(entry) for entry in payload.get("heteronym_overrides", []) if isinstance(entry, Mapping)
|
||||
]
|
||||
job.normalization_overrides = dict(payload.get("normalization_overrides", {}) or {})
|
||||
job.pause_event.set()
|
||||
return job
|
||||
|
||||
@@ -1107,6 +1107,7 @@ const initPrepare = (root = document) => {
|
||||
const entityCacheKeyData = parseJSONScript("entity-cache-key");
|
||||
const manualOverridesSeed = parseJSONScript("manual-overrides-data") || [];
|
||||
const pronunciationOverridesSeed = parseJSONScript("pronunciation-overrides-data") || [];
|
||||
const heteronymOverridesSeed = parseJSONScript("heteronym-overrides-data") || [];
|
||||
|
||||
const entityTabs = form.querySelector('[data-role="entity-tabs"]');
|
||||
const entitiesUrl = form.dataset.entitiesUrl || "";
|
||||
@@ -1124,6 +1125,7 @@ const initPrepare = (root = document) => {
|
||||
cacheKey: typeof entityCacheKeyData === "string" ? entityCacheKeyData : "",
|
||||
manualOverrides: Array.isArray(manualOverridesSeed) ? [...manualOverridesSeed] : [],
|
||||
pronunciationOverrides: Array.isArray(pronunciationOverridesSeed) ? [...pronunciationOverridesSeed] : [],
|
||||
heteronymOverrides: Array.isArray(heteronymOverridesSeed) ? [...heteronymOverridesSeed] : [],
|
||||
filters: {
|
||||
people: 0,
|
||||
entities: 0,
|
||||
@@ -1171,6 +1173,10 @@ const initPrepare = (root = document) => {
|
||||
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"]');
|
||||
const heteronymOverridesRoot = manualOverridesRoot?.querySelector('[data-role="heteronym-overrides"]');
|
||||
const heteronymOverrideList = heteronymOverridesRoot?.querySelector('[data-role="heteronym-override-list"]');
|
||||
const heteronymOverrideTemplate = heteronymOverridesRoot?.querySelector('template[data-role="heteronym-override-template"]');
|
||||
const heteronymOverridesEmpty = heteronymOverridesRoot?.querySelector('[data-role="heteronym-overrides-empty"]');
|
||||
let manualOverrideStatusTimer = null;
|
||||
let manualOverrideStatusNonce = 0;
|
||||
|
||||
@@ -1879,6 +1885,165 @@ const initPrepare = (root = document) => {
|
||||
});
|
||||
}
|
||||
|
||||
const escapeRegExp = (value) => String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
const resolveHeteronymTooltip = (entry) => {
|
||||
if (!entry || typeof entry !== "object") return "";
|
||||
const options = Array.isArray(entry.options) ? entry.options : [];
|
||||
if (options.length < 2) return "";
|
||||
const parts = [];
|
||||
options.slice(0, 2).forEach((opt) => {
|
||||
const label = String(opt?.label || "").trim();
|
||||
const example = String(opt?.example_sentence || "").trim();
|
||||
if (label && example) {
|
||||
parts.push(`${label}: ${example}`);
|
||||
} else if (example) {
|
||||
parts.push(example);
|
||||
}
|
||||
});
|
||||
return parts.join("\n");
|
||||
};
|
||||
|
||||
const fillHighlightedSentence = (container, sentence, token, tooltip) => {
|
||||
if (!container) return;
|
||||
container.textContent = "";
|
||||
const rawSentence = String(sentence || "");
|
||||
const rawToken = String(token || "");
|
||||
if (!rawSentence) {
|
||||
return;
|
||||
}
|
||||
if (!rawToken) {
|
||||
container.textContent = rawSentence;
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = new RegExp(`\\b${escapeRegExp(rawToken)}\\b`, "i");
|
||||
const match = pattern.exec(rawSentence);
|
||||
if (!match) {
|
||||
container.textContent = rawSentence;
|
||||
return;
|
||||
}
|
||||
|
||||
const before = rawSentence.slice(0, match.index);
|
||||
const hit = rawSentence.slice(match.index, match.index + match[0].length);
|
||||
const after = rawSentence.slice(match.index + match[0].length);
|
||||
|
||||
if (before) {
|
||||
container.appendChild(document.createTextNode(before));
|
||||
}
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "chip";
|
||||
chip.textContent = hit;
|
||||
if (tooltip) {
|
||||
chip.title = tooltip;
|
||||
}
|
||||
container.appendChild(chip);
|
||||
if (after) {
|
||||
container.appendChild(document.createTextNode(after));
|
||||
}
|
||||
};
|
||||
|
||||
function renderHeteronymOverrides() {
|
||||
if (!heteronymOverrideList) return;
|
||||
heteronymOverrideList.innerHTML = "";
|
||||
const entries = Array.isArray(entityState.heteronymOverrides) ? entityState.heteronymOverrides : [];
|
||||
if (!entries.length) {
|
||||
if (heteronymOverridesEmpty) {
|
||||
heteronymOverridesEmpty.hidden = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (heteronymOverridesEmpty) {
|
||||
heteronymOverridesEmpty.hidden = true;
|
||||
}
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (!entry || typeof entry !== "object") return;
|
||||
const entryId = String(entry.entry_id || entry.id || "").trim();
|
||||
if (!entryId) return;
|
||||
const sentence = String(entry.sentence || "").trim();
|
||||
if (!sentence) return;
|
||||
const token = String(entry.token || "").trim();
|
||||
|
||||
const node = cloneTemplate(heteronymOverrideTemplate);
|
||||
if (!node) return;
|
||||
node.dataset.entryId = entryId;
|
||||
|
||||
const sentenceEl = node.querySelector('[data-role="heteronym-sentence"]');
|
||||
if (sentenceEl) {
|
||||
const tooltip = resolveHeteronymTooltip(entry);
|
||||
fillHighlightedSentence(sentenceEl, sentence, token, tooltip);
|
||||
}
|
||||
|
||||
const notesEl = node.querySelector('[data-role="heteronym-notes"]');
|
||||
if (notesEl) {
|
||||
const suggested = String(entry.default_choice || "").trim();
|
||||
if (suggested) {
|
||||
const options = Array.isArray(entry.options) ? entry.options : [];
|
||||
const match = options.find((opt) => String(opt?.key || "").trim() === suggested);
|
||||
const label = String(match?.label || "").trim();
|
||||
notesEl.textContent = label ? `Suggested: ${label}` : "";
|
||||
notesEl.hidden = !notesEl.textContent;
|
||||
} else {
|
||||
notesEl.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
const optionsContainer = node.querySelector('[data-role="heteronym-options"]');
|
||||
if (optionsContainer) {
|
||||
optionsContainer.innerHTML = "";
|
||||
const options = Array.isArray(entry.options) ? entry.options : [];
|
||||
const selectedKey = String(entry.choice || entry.default_choice || "").trim();
|
||||
options.slice(0, 2).forEach((opt, index) => {
|
||||
if (!opt || typeof opt !== "object") return;
|
||||
const key = String(opt.key || "").trim();
|
||||
const label = String(opt.label || "Option").trim();
|
||||
const previewSentence = String(opt.replacement_sentence || sentence).trim();
|
||||
if (!key) return;
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "entity-inline-override__actions";
|
||||
|
||||
const choiceLabel = document.createElement("label");
|
||||
choiceLabel.className = "toggle-pill";
|
||||
const input = document.createElement("input");
|
||||
input.type = "radio";
|
||||
input.name = `heteronym-${entryId}-choice`;
|
||||
input.value = key;
|
||||
if (selectedKey) {
|
||||
input.checked = selectedKey === key;
|
||||
} else {
|
||||
input.checked = index === 0;
|
||||
}
|
||||
const span = document.createElement("span");
|
||||
span.textContent = label;
|
||||
choiceLabel.appendChild(input);
|
||||
choiceLabel.appendChild(span);
|
||||
|
||||
const previewButton = document.createElement("button");
|
||||
previewButton.type = "button";
|
||||
previewButton.className = "button button--ghost button--small";
|
||||
previewButton.dataset.role = "speaker-preview";
|
||||
previewButton.dataset.previewText = previewSentence;
|
||||
previewButton.dataset.voice = baseVoice || "";
|
||||
previewButton.dataset.language = languageCode;
|
||||
previewButton.dataset.speed = defaultSpeed;
|
||||
previewButton.dataset.useGpu = useGpuDefault;
|
||||
previewButton.textContent = "Preview";
|
||||
previewButton.disabled = !previewButton.dataset.voice;
|
||||
previewButton.setAttribute("aria-disabled", previewButton.disabled ? "true" : "false");
|
||||
|
||||
wrapper.appendChild(choiceLabel);
|
||||
wrapper.appendChild(previewButton);
|
||||
optionsContainer.appendChild(wrapper);
|
||||
});
|
||||
}
|
||||
|
||||
heteronymOverrideList.appendChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
function applyEntityPayload(payload, options = {}) {
|
||||
if (payload && typeof payload === "object") {
|
||||
if (payload.summary) {
|
||||
@@ -1890,6 +2055,9 @@ const initPrepare = (root = document) => {
|
||||
if (Array.isArray(payload.pronunciation_overrides)) {
|
||||
entityState.pronunciationOverrides = payload.pronunciation_overrides;
|
||||
}
|
||||
if (Array.isArray(payload.heteronym_overrides)) {
|
||||
entityState.heteronymOverrides = payload.heteronym_overrides;
|
||||
}
|
||||
if (typeof payload.cache_key === "string") {
|
||||
entityState.cacheKey = payload.cache_key;
|
||||
}
|
||||
@@ -1907,6 +2075,7 @@ const initPrepare = (root = document) => {
|
||||
}
|
||||
renderEntitySummary();
|
||||
renderManualOverrides();
|
||||
renderHeteronymOverrides();
|
||||
}
|
||||
|
||||
const filterVoicesByGender = (voices, genderHint) => {
|
||||
@@ -2517,6 +2686,7 @@ const initPrepare = (root = document) => {
|
||||
activateEntityTab(initialTab?.dataset.panel || "people");
|
||||
renderEntitySummary();
|
||||
renderManualOverrides();
|
||||
renderHeteronymOverrides();
|
||||
triggerEntitiesRefresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -526,6 +526,28 @@
|
||||
</article>
|
||||
</template>
|
||||
<p class="manual-overrides__empty" data-role="manual-overrides-empty" hidden>No overrides yet. Use search or add a custom entry to begin.</p>
|
||||
|
||||
<section class="manual-overrides__section" data-role="heteronym-overrides">
|
||||
<header class="manual-overrides__header">
|
||||
<div class="manual-overrides__copy">
|
||||
<h2>Heteronyms</h2>
|
||||
<p class="hint">Review sentences that contain a word with multiple pronunciations. The suggested option is selected by default. Hover the highlighted word for examples and use Preview to audition each pronunciation.</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="manual-overrides__list" data-role="heteronym-override-list"></div>
|
||||
<template data-role="heteronym-override-template">
|
||||
<article class="manual-override" data-entry-id="">
|
||||
<header class="manual-override__header">
|
||||
<div>
|
||||
<h3 class="manual-override__label" data-role="heteronym-sentence"></h3>
|
||||
<p class="manual-override__notes" data-role="heteronym-notes"></p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="manual-override__body" data-role="heteronym-options"></div>
|
||||
</article>
|
||||
</template>
|
||||
<p class="manual-overrides__empty" data-role="heteronym-overrides-empty" hidden>No heteronyms found in the current selection.</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -567,6 +589,7 @@
|
||||
<script id="entity-cache-key" type="application/json">{{ pending.entity_cache_key or '' | tojson }}</script>
|
||||
<script id="manual-overrides-data" type="application/json">{{ pending.manual_overrides or [] | tojson }}</script>
|
||||
<script id="pronunciation-overrides-data" type="application/json">{{ pending.pronunciation_overrides or [] | tojson }}</script>
|
||||
<script id="heteronym-overrides-data" type="application/json">{{ pending.heteronym_overrides or [] | tojson }}</script>
|
||||
<script id="voice-catalog-data" type="application/json">{{ options.voice_catalog | tojson }}</script>
|
||||
<script id="voice-language-map" type="application/json">{{ options.languages | tojson }}</script>
|
||||
{% if embed_scripts %}
|
||||
|
||||
Reference in New Issue
Block a user