From 1e13c901fec607b6d6ce071d7b872ab49c7278eb Mon Sep 17 00:00:00 2001 From: JB Date: Tue, 2 Dec 2025 09:58:47 -0800 Subject: [PATCH] feat: Implement normalization settings UI with dynamic groups and options --- abogen/web/routes/utils/form.py | 33 ++++++- abogen/web/routes/utils/settings.py | 32 +++++++ abogen/web/routes/utils/voice.py | 3 +- abogen/web/static/dashboard.js | 17 ---- .../templates/partials/new_job_step_book.html | 92 ++----------------- abogen/web/templates/settings.html | 65 +++---------- 6 files changed, 80 insertions(+), 162 deletions(-) diff --git a/abogen/web/routes/utils/form.py b/abogen/web/routes/utils/form.py index 81d1793..16c3f8c 100644 --- a/abogen/web/routes/utils/form.py +++ b/abogen/web/routes/utils/form.py @@ -802,6 +802,34 @@ def build_pending_job_from_extraction( 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( id=uuid.uuid4().hex, original_filename=original_name, @@ -826,10 +854,7 @@ def build_pending_job_from_extraction( max_subtitle_words=max_subtitle_words, metadata_tags=metadata_tags, chapters=chapters_payload, - 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}, - }, + normalization_overrides=normalization_overrides, created_at=time.time(), cover_image_path=cover_path, cover_image_mime=cover_mime, diff --git a/abogen/web/routes/utils/settings.py b/abogen/web/routes/utils/settings.py index 2251f4d..7eef328 100644 --- a/abogen/web/routes/utils/settings.py +++ b/abogen/web/routes/utils/settings.py @@ -100,6 +100,38 @@ BOOLEAN_SETTINGS = { FLOAT_SETTINGS = {"silence_between_chapters", "chapter_intro_delay", "llm_timeout"} 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]]: return { "calibre_opds": { diff --git a/abogen/web/routes/utils/voice.py b/abogen/web/routes/utils/voice.py index 483e733..4407864 100644 --- a/abogen/web/routes/utils/voice.py +++ b/abogen/web/routes/utils/voice.py @@ -4,7 +4,7 @@ import numpy as np from abogen.speaker_configs import slugify_label 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.voice_profiles import ( load_profiles, @@ -605,6 +605,7 @@ def template_options() -> Dict[str, Any]: "speaker_pronunciation_sentence", settings_defaults()["speaker_pronunciation_sentence"] ), "apostrophe_modes": _APOSTROPHE_MODE_OPTIONS, + "normalization_groups": _NORMALIZATION_GROUPS, } diff --git a/abogen/web/static/dashboard.js b/abogen/web/static/dashboard.js index 2d13461..e2d0028 100644 --- a/abogen/web/static/dashboard.js +++ b/abogen/web/static/dashboard.js @@ -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) => { if (!option) return; const presetFormula = option.dataset.formula || ""; @@ -562,15 +549,11 @@ const initDashboard = () => { }; if (profileSelect) { - const hasSaved = selectFirstProfileIfAvailable(); if (profileSelect.dataset.dashboardBound !== "true") { profileSelect.dataset.dashboardBound = "true"; profileSelect.addEventListener("change", updateVoiceControls); } updateVoiceControls(); - if (!hasSaved) { - hydrateDefaultVoice(); - } } else { hydrateDefaultVoice(); } diff --git a/abogen/web/templates/partials/new_job_step_book.html b/abogen/web/templates/partials/new_job_step_book.html index ba7b1bb..c4b2879 100644 --- a/abogen/web/templates/partials/new_job_step_book.html +++ b/abogen/web/templates/partials/new_job_step_book.html @@ -127,88 +127,6 @@ {% set narrator_voice = '' %} {% endif %} {% 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 profile_value = narrator_profile if narrator_profile else '__standard' %} {% if profile_value == '__formula' %} @@ -371,15 +289,17 @@ - {% for toggle in normalization_toggles %} + {% for group in options.normalization_groups %} + {% for option in group.options %}
{% endfor %} + {% endfor %} diff --git a/abogen/web/templates/settings.html b/abogen/web/templates/settings.html index 9e5ec27..a5d1513 100644 --- a/abogen/web/templates/settings.html +++ b/abogen/web/templates/settings.html @@ -283,38 +283,10 @@
+ {% for group in options.normalization_groups %}
- General Rules -
- - - - - - -
-
- -
- Apostrophes & Contractions + {{ group.label }} + {% if group.label == "Apostrophes & Contractions" %}
Strategy
@@ -329,38 +301,23 @@

Configure the LLM connection before using it for audiobook runs.

{% endif %}
- + {% endif %}
+ {% for option in group.options %} - - - - - + {% endfor %}
+ {% if group.label == "Apostrophes & Contractions" %}

Choose which contraction families are expanded.

+ {% endif %}
+ {% endfor %}
Sample & Preview