mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Implement normalization settings UI with dynamic groups and options
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
<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>
|
||||
</div>
|
||||
{% for toggle in normalization_toggles %}
|
||||
{% for group in options.normalization_groups %}
|
||||
{% for option in group.options %}
|
||||
<div class="field field--stack">
|
||||
<label class="toggle-pill">
|
||||
<input type="hidden" name="{{ toggle.name }}" value="false">
|
||||
<input type="checkbox" name="{{ toggle.name }}" value="true" {% if toggle.value %}checked{% endif %} {{ 'disabled' if readonly else '' }}>
|
||||
<span>{{ toggle.label }}</span>
|
||||
<input type="hidden" name="{{ option.key }}" value="false">
|
||||
<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>{{ option.label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -283,38 +283,10 @@
|
||||
</section>
|
||||
|
||||
<section class="settings-panel" data-section="normalization">
|
||||
{% for group in options.normalization_groups %}
|
||||
<fieldset class="settings__section">
|
||||
<legend>General Rules</legend>
|
||||
<div class="field field--choices">
|
||||
<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 → 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 & Contractions</legend>
|
||||
<legend>{{ group.label }}</legend>
|
||||
{% if group.label == "Apostrophes & Contractions" %}
|
||||
<div class="field">
|
||||
<span class="field__label">Strategy</span>
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
<div class="field field--choices">
|
||||
{% for option in group.options %}
|
||||
<label class="toggle-pill">
|
||||
<input type="checkbox" name="normalization_apostrophes_contractions" value="true" {% if settings.normalization_apostrophes_contractions %}checked{% endif %}>
|
||||
<span>Expand contractions ("it's" → "it is")</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' → 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 → 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 → 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 → 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>
|
||||
<input type="checkbox" name="{{ option.key }}" value="true" {% if settings[option.key] %}checked{% endif %}>
|
||||
<span>{{ option.label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if group.label == "Apostrophes & Contractions" %}
|
||||
<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>
|
||||
<p class="hint">Choose which contraction families are expanded.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
|
||||
<fieldset class="settings__section">
|
||||
<legend>Sample & Preview</legend>
|
||||
|
||||
Reference in New Issue
Block a user