feat: Implement normalization settings UI with dynamic groups and options

This commit is contained in:
JB
2025-12-02 09:58:47 -08:00
parent 609af66748
commit 1e13c901fe
6 changed files with 80 additions and 162 deletions
+29 -4
View File
@@ -802,6 +802,34 @@ def build_pending_job_from_extraction(
apply_config=bool(speaker_config_payload), apply_config=bool(speaker_config_payload),
) )
def _extract_checkbox(name: str, default: bool) -> bool:
values: List[str] = []
getter = getattr(form, "getlist", None)
if callable(getter):
raw_values = getter(name)
if raw_values:
values = list(cast(Iterable[str], raw_values))
else:
raw_flag = form.get(name)
if raw_flag is not None:
values = [raw_flag]
if values:
return coerce_bool(values[-1], default)
return default
normalization_overrides = {}
for key in _NORMALIZATION_BOOLEAN_KEYS:
default_val = bool(settings.get(key, True))
normalization_overrides[key] = _extract_checkbox(key, default_val)
for key in _NORMALIZATION_STRING_KEYS:
default_val = str(settings.get(key, ""))
val = form.get(key)
if val is not None:
normalization_overrides[key] = str(val)
else:
normalization_overrides[key] = default_val
pending = PendingJob( pending = PendingJob(
id=uuid.uuid4().hex, id=uuid.uuid4().hex,
original_filename=original_name, original_filename=original_name,
@@ -826,10 +854,7 @@ def build_pending_job_from_extraction(
max_subtitle_words=max_subtitle_words, max_subtitle_words=max_subtitle_words,
metadata_tags=metadata_tags, metadata_tags=metadata_tags,
chapters=chapters_payload, chapters=chapters_payload,
normalization_overrides={ normalization_overrides=normalization_overrides,
**{key: bool(settings.get(key, True)) for key in _NORMALIZATION_BOOLEAN_KEYS},
**{key: str(settings.get(key, "")) for key in _NORMALIZATION_STRING_KEYS},
},
created_at=time.time(), created_at=time.time(),
cover_image_path=cover_path, cover_image_path=cover_path,
cover_image_mime=cover_mime, cover_image_mime=cover_mime,
+32
View File
@@ -100,6 +100,38 @@ BOOLEAN_SETTINGS = {
FLOAT_SETTINGS = {"silence_between_chapters", "chapter_intro_delay", "llm_timeout"} FLOAT_SETTINGS = {"silence_between_chapters", "chapter_intro_delay", "llm_timeout"}
INT_SETTINGS = {"max_subtitle_words", "speaker_analysis_threshold"} INT_SETTINGS = {"max_subtitle_words", "speaker_analysis_threshold"}
_NORMALIZATION_GROUPS = [
{
"label": "General Rules",
"options": [
{"key": "normalization_numbers", "label": "Convert grouped numbers to words"},
{"key": "normalization_currency", "label": "Convert currency symbols ($10 → ten dollars)"},
{"key": "normalization_titles", "label": "Expand titles and suffixes (Dr., St., Jr., …)"},
{"key": "normalization_footnotes", "label": "Remove footnote indicators ([1], [2])"},
{"key": "normalization_terminal", "label": "Ensure sentences end with terminal punctuation"},
{"key": "normalization_caps_quotes", "label": "Convert ALL CAPS dialogue inside quotes"},
]
},
{
"label": "Apostrophes & Contractions",
"options": [
{"key": "normalization_apostrophes_contractions", "label": "Expand contractions (it's → it is)"},
{"key": "normalization_apostrophes_plural_possessives", "label": "Collapse plural possessives (dogs' → dogs)"},
{"key": "normalization_apostrophes_sibilant_possessives", "label": "Mark sibilant possessives (boss's → boss + IZ marker)"},
{"key": "normalization_apostrophes_decades", "label": "Expand decades ('90s → 1990s)"},
{"key": "normalization_apostrophes_leading_elisions", "label": "Expand leading elisions ('tis → it is)"},
{"key": "normalization_phoneme_hints", "label": "Add phoneme hints for possessives"},
{"key": "normalization_contraction_aux_be", "label": "Expand auxiliary 'be' (I'm → I am)"},
{"key": "normalization_contraction_aux_have", "label": "Expand auxiliary 'have' (I've → I have)"},
{"key": "normalization_contraction_modal_will", "label": "Expand modal 'will' (I'll → I will)"},
{"key": "normalization_contraction_modal_would", "label": "Expand modal 'would' (I'd → I would)"},
{"key": "normalization_contraction_negation_not", "label": "Expand negation 'not' (don't → do not)"},
{"key": "normalization_contraction_let_us", "label": "Expand 'let's' → let us"},
]
}
]
def integration_defaults() -> Dict[str, Dict[str, Any]]: def integration_defaults() -> Dict[str, Dict[str, Any]]:
return { return {
"calibre_opds": { "calibre_opds": {
+2 -1
View File
@@ -4,7 +4,7 @@ import numpy as np
from abogen.speaker_configs import slugify_label from abogen.speaker_configs import slugify_label
from abogen.speaker_analysis import analyze_speakers from abogen.speaker_analysis import analyze_speakers
from abogen.web.routes.utils.settings import load_settings, settings_defaults, _DEFAULT_ANALYSIS_THRESHOLD, _CHUNK_LEVEL_OPTIONS, _APOSTROPHE_MODE_OPTIONS from abogen.web.routes.utils.settings import load_settings, settings_defaults, _DEFAULT_ANALYSIS_THRESHOLD, _CHUNK_LEVEL_OPTIONS, _APOSTROPHE_MODE_OPTIONS, _NORMALIZATION_GROUPS
from abogen.web.routes.utils.common import split_profile_spec from abogen.web.routes.utils.common import split_profile_spec
from abogen.voice_profiles import ( from abogen.voice_profiles import (
load_profiles, load_profiles,
@@ -605,6 +605,7 @@ def template_options() -> Dict[str, Any]:
"speaker_pronunciation_sentence", settings_defaults()["speaker_pronunciation_sentence"] "speaker_pronunciation_sentence", settings_defaults()["speaker_pronunciation_sentence"]
), ),
"apostrophe_modes": _APOSTROPHE_MODE_OPTIONS, "apostrophe_modes": _APOSTROPHE_MODE_OPTIONS,
"normalization_groups": _NORMALIZATION_GROUPS,
} }
-17
View File
@@ -480,19 +480,6 @@ const initDashboard = () => {
} }
}; };
const selectFirstProfileIfAvailable = () => {
if (!profileSelect) return false;
const saved = Array.from(profileSelect.options).filter(
(option) => option.value && option.value !== "__standard" && option.value !== "__formula",
);
if (!saved.length) {
profileSelect.value = "__standard";
return false;
}
saved[0].selected = true;
return true;
};
const applySavedProfile = (option) => { const applySavedProfile = (option) => {
if (!option) return; if (!option) return;
const presetFormula = option.dataset.formula || ""; const presetFormula = option.dataset.formula || "";
@@ -562,15 +549,11 @@ const initDashboard = () => {
}; };
if (profileSelect) { if (profileSelect) {
const hasSaved = selectFirstProfileIfAvailable();
if (profileSelect.dataset.dashboardBound !== "true") { if (profileSelect.dataset.dashboardBound !== "true") {
profileSelect.dataset.dashboardBound = "true"; profileSelect.dataset.dashboardBound = "true";
profileSelect.addEventListener("change", updateVoiceControls); profileSelect.addEventListener("change", updateVoiceControls);
} }
updateVoiceControls(); updateVoiceControls();
if (!hasSaved) {
hydrateDefaultVoice();
}
} else { } else {
hydrateDefaultVoice(); hydrateDefaultVoice();
} }
@@ -127,88 +127,6 @@
{% set narrator_voice = '' %} {% set narrator_voice = '' %}
{% endif %} {% endif %}
{% set normalization_overrides = pending.normalization_overrides if pending and pending.normalization_overrides else {} %} {% set normalization_overrides = pending.normalization_overrides if pending and pending.normalization_overrides else {} %}
{% set normalization_toggles = [
{
'name': 'normalization_numbers',
'label': "Convert grouped numbers to words",
'value': normalization_overrides.get('normalization_numbers', settings_dict.get('normalization_numbers', True)),
},
{
'name': 'normalization_titles',
'label': "Expand titles and suffixes (Dr., St., Jr., …)",
'value': normalization_overrides.get('normalization_titles', settings_dict.get('normalization_titles', True)),
},
{
'name': 'normalization_terminal',
'label': "Ensure sentences end with terminal punctuation",
'value': normalization_overrides.get('normalization_terminal', settings_dict.get('normalization_terminal', True)),
},
{
'name': 'normalization_phoneme_hints',
'label': "Apply phoneme hints for common abbreviations",
'value': normalization_overrides.get('normalization_phoneme_hints', settings_dict.get('normalization_phoneme_hints', True)),
},
{
'name': 'normalization_caps_quotes',
'label': "Convert ALL CAPS dialogue inside quotes",
'value': normalization_overrides.get('normalization_caps_quotes', settings_dict.get('normalization_caps_quotes', True)),
},
{
'name': 'normalization_apostrophes_contractions',
'label': "Expand contractions (it's -> it is)",
'value': normalization_overrides.get('normalization_apostrophes_contractions', settings_dict.get('normalization_apostrophes_contractions', True)),
},
{
'name': 'normalization_apostrophes_plural_possessives',
'label': "Collapse plural possessives (dogs' -> dogs)",
'value': normalization_overrides.get('normalization_apostrophes_plural_possessives', settings_dict.get('normalization_apostrophes_plural_possessives', True)),
},
{
'name': 'normalization_apostrophes_sibilant_possessives',
'label': "Mark sibilant possessives (boss's -> boss + IZ marker)",
'value': normalization_overrides.get('normalization_apostrophes_sibilant_possessives', settings_dict.get('normalization_apostrophes_sibilant_possessives', True)),
},
{
'name': 'normalization_apostrophes_decades',
'label': "Expand decades ('90s -> 1990s)",
'value': normalization_overrides.get('normalization_apostrophes_decades', settings_dict.get('normalization_apostrophes_decades', True)),
},
{
'name': 'normalization_apostrophes_leading_elisions',
'label': "Expand leading elisions ('tis -> it is)",
'value': normalization_overrides.get('normalization_apostrophes_leading_elisions', settings_dict.get('normalization_apostrophes_leading_elisions', True)),
},
{
'name': 'normalization_contraction_aux_be',
'label': "Expand auxiliary 'be' (I'm -> I am)",
'value': normalization_overrides.get('normalization_contraction_aux_be', settings_dict.get('normalization_contraction_aux_be', True)),
},
{
'name': 'normalization_contraction_aux_have',
'label': "Expand auxiliary 'have' (I've -> I have)",
'value': normalization_overrides.get('normalization_contraction_aux_have', settings_dict.get('normalization_contraction_aux_have', True)),
},
{
'name': 'normalization_contraction_modal_will',
'label': "Expand modal 'will' (I'll -> I will)",
'value': normalization_overrides.get('normalization_contraction_modal_will', settings_dict.get('normalization_contraction_modal_will', True)),
},
{
'name': 'normalization_contraction_modal_would',
'label': "Expand modal 'would' (I'd -> I would)",
'value': normalization_overrides.get('normalization_contraction_modal_would', settings_dict.get('normalization_contraction_modal_would', True)),
},
{
'name': 'normalization_contraction_negation_not',
'label': "Expand negation 'not' (don't -> do not)",
'value': normalization_overrides.get('normalization_contraction_negation_not', settings_dict.get('normalization_contraction_negation_not', True)),
},
{
'name': 'normalization_contraction_let_us',
'label': "Expand 'let's' -> let us",
'value': normalization_overrides.get('normalization_contraction_let_us', settings_dict.get('normalization_contraction_let_us', True)),
},
] %}
{% set voice_formula_value = '' %} {% set voice_formula_value = '' %}
{% set profile_value = narrator_profile if narrator_profile else '__standard' %} {% set profile_value = narrator_profile if narrator_profile else '__standard' %}
{% if profile_value == '__formula' %} {% if profile_value == '__formula' %}
@@ -371,15 +289,17 @@
<option value="off" {% if normalization_overrides.get('normalization_numbers_year_style', settings_dict.get('normalization_numbers_year_style', 'american')) == 'off' %}selected{% endif %}>Off (read as number)</option> <option value="off" {% if normalization_overrides.get('normalization_numbers_year_style', settings_dict.get('normalization_numbers_year_style', 'american')) == 'off' %}selected{% endif %}>Off (read as number)</option>
</select> </select>
</div> </div>
{% for toggle in normalization_toggles %} {% for group in options.normalization_groups %}
{% for option in group.options %}
<div class="field field--stack"> <div class="field field--stack">
<label class="toggle-pill"> <label class="toggle-pill">
<input type="hidden" name="{{ toggle.name }}" value="false"> <input type="hidden" name="{{ option.key }}" value="false">
<input type="checkbox" name="{{ toggle.name }}" value="true" {% if toggle.value %}checked{% endif %} {{ 'disabled' if readonly else '' }}> <input type="checkbox" name="{{ option.key }}" value="true" {% if normalization_overrides.get(option.key, settings_dict.get(option.key, True)) %}checked{% endif %} {{ 'disabled' if readonly else '' }}>
<span>{{ toggle.label }}</span> <span>{{ option.label }}</span>
</label> </label>
</div> </div>
{% endfor %} {% endfor %}
{% endfor %}
</div> </div>
</section> </section>
+11 -54
View File
@@ -283,38 +283,10 @@
</section> </section>
<section class="settings-panel" data-section="normalization"> <section class="settings-panel" data-section="normalization">
{% for group in options.normalization_groups %}
<fieldset class="settings__section"> <fieldset class="settings__section">
<legend>General Rules</legend> <legend>{{ group.label }}</legend>
<div class="field field--choices"> {% if group.label == "Apostrophes & Contractions" %}
<label class="toggle-pill">
<input type="checkbox" name="normalization_numbers" value="true" {% if settings.normalization_numbers %}checked{% endif %}>
<span>Convert grouped numbers to words</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_currency" value="true" {% if settings.normalization_currency %}checked{% endif %}>
<span>Convert currency symbols ($10 &rarr; ten dollars)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_titles" value="true" {% if settings.normalization_titles %}checked{% endif %}>
<span>Expand titles and suffixes (Dr., St., Jr., …)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_footnotes" value="true" {% if settings.normalization_footnotes %}checked{% endif %}>
<span>Remove footnote indicators ([1], [2])</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_terminal" value="true" {% if settings.normalization_terminal %}checked{% endif %}>
<span>Ensure sentences end with terminal punctuation</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_caps_quotes" value="true" {% if settings.normalization_caps_quotes %}checked{% endif %}>
<span>Convert ALL CAPS dialogue inside quotes</span>
</label>
</div>
</fieldset>
<fieldset class="settings__section">
<legend>Apostrophes &amp; Contractions</legend>
<div class="field"> <div class="field">
<span class="field__label">Strategy</span> <span class="field__label">Strategy</span>
<div class="choices choices--inline"> <div class="choices choices--inline">
@@ -329,38 +301,23 @@
<p class="hint hint--warning">Configure the LLM connection before using it for audiobook runs.</p> <p class="hint hint--warning">Configure the LLM connection before using it for audiobook runs.</p>
{% endif %} {% endif %}
</div> </div>
{% endif %}
<div class="field field--choices"> <div class="field field--choices">
{% for option in group.options %}
<label class="toggle-pill"> <label class="toggle-pill">
<input type="checkbox" name="normalization_apostrophes_contractions" value="true" {% if settings.normalization_apostrophes_contractions %}checked{% endif %}> <input type="checkbox" name="{{ option.key }}" value="true" {% if settings[option.key] %}checked{% endif %}>
<span>Expand contractions ("it's" &rarr; "it is")</span> <span>{{ option.label }}</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_apostrophes_plural_possessives" value="true" {% if settings.normalization_apostrophes_plural_possessives %}checked{% endif %}>
<span>Collapse plural possessives (dogs' &rarr; dogs)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_apostrophes_sibilant_possessives" value="true" {% if settings.normalization_apostrophes_sibilant_possessives %}checked{% endif %}>
<span>Add guidance for sibilant possessives (boss's &rarr; boss + IZ marker)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_apostrophes_decades" value="true" {% if settings.normalization_apostrophes_decades %}checked{% endif %}>
<span>Expand decades ('90s &rarr; 1990s)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_apostrophes_leading_elisions" value="true" {% if settings.normalization_apostrophes_leading_elisions %}checked{% endif %}>
<span>Expand leading elisions ('tis &rarr; it is)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_phoneme_hints" value="true" {% if settings.normalization_phoneme_hints %}checked{% endif %}>
<span>Add phoneme hints for possessives</span>
</label> </label>
{% endfor %}
</div> </div>
{% if group.label == "Apostrophes & Contractions" %}
<div class="field field--inline field--actions"> <div class="field field--inline field--actions">
<button type="button" class="button button--ghost button--small" data-action="contraction-modal-open">Advanced Contraction Settings…</button> <button type="button" class="button button--ghost button--small" data-action="contraction-modal-open">Advanced Contraction Settings…</button>
<p class="hint">Choose which contraction families are expanded.</p> <p class="hint">Choose which contraction families are expanded.</p>
</div> </div>
{% endif %}
</fieldset> </fieldset>
{% endfor %}
<fieldset class="settings__section"> <fieldset class="settings__section">
<legend>Sample &amp; Preview</legend> <legend>Sample &amp; Preview</legend>