feat: Add contraction handling options and UI for normalization settings

This commit is contained in:
JB
2025-11-02 05:47:20 -08:00
parent d238524cbb
commit af90da0b99
7 changed files with 473 additions and 137 deletions
+18
View File
@@ -2100,6 +2100,12 @@ BOOLEAN_SETTINGS = {
"normalization_apostrophes_sibilant_possessives",
"normalization_apostrophes_decades",
"normalization_apostrophes_leading_elisions",
"normalization_contraction_aux_be",
"normalization_contraction_aux_have",
"normalization_contraction_modal_will",
"normalization_contraction_modal_would",
"normalization_contraction_negation_not",
"normalization_contraction_let_us",
}
FLOAT_SETTINGS = {"silence_between_chapters", "chapter_intro_delay", "llm_timeout"}
@@ -2111,6 +2117,12 @@ _APOSTROPHE_OVERRIDE_KEYS = (
"normalization_apostrophes_sibilant_possessives",
"normalization_apostrophes_decades",
"normalization_apostrophes_leading_elisions",
"normalization_contraction_aux_be",
"normalization_contraction_aux_have",
"normalization_contraction_modal_will",
"normalization_contraction_modal_would",
"normalization_contraction_negation_not",
"normalization_contraction_let_us",
)
@@ -3376,6 +3388,12 @@ def api_normalization_preview() -> ResponseReturnValue:
"normalization_apostrophes_sibilant_possessives",
"normalization_apostrophes_decades",
"normalization_apostrophes_leading_elisions",
"normalization_contraction_aux_be",
"normalization_contraction_aux_have",
"normalization_contraction_modal_will",
"normalization_contraction_modal_would",
"normalization_contraction_negation_not",
"normalization_contraction_let_us",
)
for key in boolean_keys:
if key in normalization_payload:
+81 -3
View File
@@ -28,6 +28,11 @@ let folderModalOpener = null;
let folderModalPreviousFocus = null;
let audiobookshelfFolderSource = [];
const contractionModal = document.querySelector('[data-role="contraction-modal"]');
const contractionModalOverlay = contractionModal ? contractionModal.querySelector('[data-role="contraction-modal-overlay"]') : null;
let contractionModalOpener = null;
let contractionModalPreviousFocus = null;
function setStatus(target, message, state) {
if (!target) {
return;
@@ -177,6 +182,66 @@ function closeFolderModal(event) {
folderModalOpener = null;
}
function openContractionModal(opener) {
if (!contractionModal) {
return;
}
contractionModalOpener = opener || null;
contractionModalPreviousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
contractionModal.hidden = false;
contractionModal.dataset.open = 'true';
document.body.classList.add('modal-open');
const focusTarget = contractionModal.querySelector('input, button, select, textarea');
if (focusTarget instanceof HTMLElement) {
focusTarget.focus({ preventScroll: true });
}
}
function closeContractionModal(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (!contractionModal || contractionModal.hidden) {
return;
}
contractionModal.dataset.open = 'false';
contractionModal.hidden = true;
document.body.classList.remove('modal-open');
const focusTarget =
(contractionModalPreviousFocus && typeof contractionModalPreviousFocus.focus === 'function'
? contractionModalPreviousFocus
: contractionModalOpener) || null;
if (focusTarget && typeof focusTarget.focus === 'function') {
focusTarget.focus({ preventScroll: true });
}
contractionModalPreviousFocus = null;
contractionModalOpener = null;
}
function initContractionModal() {
if (!contractionModal) {
return;
}
const openButton = document.querySelector('[data-action="contraction-modal-open"]');
if (openButton) {
openButton.addEventListener('click', () => openContractionModal(openButton));
}
const closeButtons = contractionModal.querySelectorAll('[data-action="contraction-modal-close"]');
closeButtons.forEach((button) => {
button.addEventListener('click', closeContractionModal);
});
if (contractionModalOverlay) {
contractionModalOverlay.addEventListener('click', closeContractionModal);
}
contractionModal.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
event.preventDefault();
closeContractionModal(event);
}
});
}
function renderFolderList(query) {
if (!folderList) {
return;
@@ -461,6 +526,9 @@ async function previewLLM(button) {
}
function collectNormalizationSettings() {
if (!form) {
return null;
}
const normalization = {
normalization_numbers: Boolean(form.querySelector('input[name="normalization_numbers"]')?.checked),
normalization_titles: Boolean(form.querySelector('input[name="normalization_titles"]')?.checked),
@@ -471,6 +539,12 @@ function collectNormalizationSettings() {
normalization_apostrophes_sibilant_possessives: Boolean(form.querySelector('input[name="normalization_apostrophes_sibilant_possessives"]')?.checked),
normalization_apostrophes_decades: Boolean(form.querySelector('input[name="normalization_apostrophes_decades"]')?.checked),
normalization_apostrophes_leading_elisions: Boolean(form.querySelector('input[name="normalization_apostrophes_leading_elisions"]')?.checked),
normalization_contraction_aux_be: Boolean(form.querySelector('input[name="normalization_contraction_aux_be"]')?.checked),
normalization_contraction_aux_have: Boolean(form.querySelector('input[name="normalization_contraction_aux_have"]')?.checked),
normalization_contraction_modal_will: Boolean(form.querySelector('input[name="normalization_contraction_modal_will"]')?.checked),
normalization_contraction_modal_would: Boolean(form.querySelector('input[name="normalization_contraction_modal_would"]')?.checked),
normalization_contraction_negation_not: Boolean(form.querySelector('input[name="normalization_contraction_negation_not"]')?.checked),
normalization_contraction_let_us: Boolean(form.querySelector('input[name="normalization_contraction_let_us"]')?.checked),
normalization_apostrophe_mode: form.querySelector('input[name="normalization_apostrophe_mode"]:checked')?.value || 'spacy',
};
return normalization;
@@ -687,10 +761,13 @@ async function previewNormalization(button) {
normalizationAudio.removeAttribute('src');
}
setStatus(status, 'Building preview…');
button.disabled = true;
const normalization = collectNormalizationSettings();
if (!normalization) {
setStatus(status, 'Unable to gather normalization settings.', 'error');
return;
}
const llmFields = collectLLMFields();
try {
const normalization = collectNormalizationSettings();
const llmFields = collectLLMFields();
const response = await fetch('/api/normalization/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -797,5 +874,6 @@ if (form) {
initSampleSelector();
initActions();
initFolderPicker();
initContractionModal();
initLLMStateWatchers();
}
+47
View File
@@ -324,6 +324,10 @@
<span>Expand leading elisions ('tis -> it is)</span>
</label>
</div>
<div class="field field--inline field--actions">
<button type="button" class="button button--ghost button--small" data-action="contraction-modal-open">Advanced…</button>
<p class="hint">Choose which contraction families are expanded.</p>
</div>
<div class="field">
<span class="field__label">Apostrophe strategy</span>
<div class="choices choices--inline">
@@ -494,6 +498,49 @@
</section>
</div>
<div class="modal" data-role="contraction-modal" hidden>
<div class="modal__overlay" data-role="contraction-modal-overlay" tabindex="-1"></div>
<div class="modal__content card card--modal" role="dialog" aria-modal="true" aria-labelledby="contraction-modal-title">
<header class="modal__header">
<p class="modal__eyebrow">Normalization</p>
<h2 class="modal__title" id="contraction-modal-title">Contraction Options</h2>
<button type="button" class="button button--ghost button--small" data-action="contraction-modal-close" aria-label="Close contraction options">Close</button>
</header>
<div class="modal__body">
<p class="hint">Enable or disable specific contraction families. These controls apply when contraction expansion is enabled.</p>
<div class="field field--choices">
<label class="toggle-pill">
<input type="checkbox" name="normalization_contraction_aux_be" value="true" {% if settings.normalization_contraction_aux_be %}checked{% endif %}>
<span>Be verbs ('m, 're, 's -> am/is/are)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_contraction_aux_have" value="true" {% if settings.normalization_contraction_aux_have %}checked{% endif %}>
<span>Have auxiliaries ('ve, 'd -> have/had)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_contraction_modal_will" value="true" {% if settings.normalization_contraction_modal_will %}checked{% endif %}>
<span>Will modals ('ll -> will)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_contraction_modal_would" value="true" {% if settings.normalization_contraction_modal_would %}checked{% endif %}>
<span>Would conditionals ('d -> would)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_contraction_negation_not" value="true" {% if settings.normalization_contraction_negation_not %}checked{% endif %}>
<span>Negative forms (can't -> can not)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_contraction_let_us" value="true" {% if settings.normalization_contraction_let_us %}checked{% endif %}>
<span>Let us (let's -> let us)</span>
</label>
</div>
</div>
<footer class="modal__footer">
<button type="button" class="button button--ghost" data-action="contraction-modal-close">Done</button>
</footer>
</div>
</div>
<div class="modal" data-role="audiobookshelf-folder-modal" hidden>
<div class="modal__overlay" data-role="audiobookshelf-folder-overlay" tabindex="-1"></div>
<div class="modal__content card card--modal folder-picker-modal" role="dialog" aria-modal="true" aria-labelledby="audiobookshelf-folder-picker-title">