diff --git a/abogen/webui/routes/settings.py b/abogen/webui/routes/settings.py index d0bb991..4c1e72c 100644 --- a/abogen/webui/routes/settings.py +++ b/abogen/webui/routes/settings.py @@ -19,6 +19,7 @@ from abogen.webui.routes.utils.settings import ( _NORMALIZATION_STRING_KEYS, _DEFAULT_ANALYSIS_THRESHOLD, ) +from abogen.webui.routes.utils.common import extract_checkbox from abogen.webui.routes.utils.voice import template_options from abogen.webui.debug_tts_runner import run_debug_tts_wavs from abogen.debug_tts_samples import DEBUG_TTS_SAMPLES @@ -93,17 +94,9 @@ def update_settings() -> ResponseReturnValue: maximum=25, ) - def _extract_checkbox(name: str, default: bool) -> bool: - values = form.getlist(name) if hasattr(form, "getlist") else [] - if values: - return coerce_bool(values[-1], default) - if hasattr(form, "__contains__") and name in form: - return False - return default - # Normalization settings for key in _NORMALIZATION_BOOLEAN_KEYS: - current[key] = _extract_checkbox(key, bool(current.get(key, True))) + current[key] = extract_checkbox(form, key, bool(current.get(key, True))) for key in _NORMALIZATION_STRING_KEYS: if hasattr(form, "__contains__") and key in form: current[key] = (form.get(key) or "").strip() diff --git a/abogen/webui/routes/utils/common.py b/abogen/webui/routes/utils/common.py index d280bdf..5f3d93c 100644 --- a/abogen/webui/routes/utils/common.py +++ b/abogen/webui/routes/utils/common.py @@ -1,6 +1,17 @@ -from typing import Any, Optional, Tuple, Iterable, List +from typing import Any, Optional, Tuple, Iterable, List, Mapping from pathlib import Path + +def coerce_bool(value: Any, default: bool) -> bool: + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.lower() in {"true", "1", "yes", "on"} + if value is None: + return default + return bool(value) + + def split_profile_spec(value: Any) -> Tuple[str, Optional[str]]: text = str(value or "").strip() if not text: @@ -18,7 +29,31 @@ def split_speaker_spec(value: Any) -> Tuple[str, Optional[str]]: return split_profile_spec(value) + def existing_paths(paths: Optional[Iterable[Path]]) -> List[Path]: if not paths: return [] return [p for p in paths if p.exists()] + + +def extract_checkbox(form: Mapping[str, Any], name: str, default: bool) -> bool: + """Extract a boolean checkbox value from a form-like mapping. + + Handles both multi-value forms (Flask's `getlist`) and simple mappings. + If the checkbox name is present but has no value, it means unchecked (False). + """ + values: List[str] = [] + getter = getattr(form, "getlist", None) + if callable(getter): + raw_values = getter(name) + if raw_values: + values = list(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) + if name in form: + return False + return default diff --git a/abogen/webui/routes/utils/form.py b/abogen/webui/routes/utils/form.py index 39e033f..c1a81f7 100644 --- a/abogen/webui/routes/utils/form.py +++ b/abogen/webui/routes/utils/form.py @@ -29,7 +29,7 @@ from abogen.webui.routes.utils.voice import ( ) from abogen.webui.routes.utils.entity import sync_pronunciation_overrides from abogen.webui.routes.utils.epub import job_download_flags -from abogen.webui.routes.utils.common import split_profile_spec +from abogen.webui.routes.utils.common import split_profile_spec, extract_checkbox from abogen.utils import calculate_text_length from abogen.voice_profiles import serialize_profiles, normalize_profile_entry from abogen.chunking import ChunkLevel, build_chunks_for_chapters @@ -537,28 +537,11 @@ def apply_book_step_form( else: pending.normalize_chapter_opening_caps = caps_default - 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) - if hasattr(form, "__contains__") and name in form: - return False - return default - overrides_existing = getattr(pending, "normalization_overrides", None) overrides: Dict[str, Any] = dict(overrides_existing or {}) for key in _NORMALIZATION_BOOLEAN_KEYS: default_toggle = overrides.get(key, bool(settings.get(key, True))) - overrides[key] = _extract_checkbox(key, default_toggle) + overrides[key] = extract_checkbox(form, key, default_toggle) for key in _NORMALIZATION_STRING_KEYS: default_val = overrides.get(key, str(settings.get(key, ""))) val = form.get(key) @@ -886,25 +869,10 @@ 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) + normalization_overrides[key] = extract_checkbox(form, key, default_val) for key in _NORMALIZATION_STRING_KEYS: default_val = str(settings.get(key, "")) diff --git a/abogen/webui/routes/utils/settings.py b/abogen/webui/routes/utils/settings.py index 589f8a0..b8abef6 100644 --- a/abogen/webui/routes/utils/settings.py +++ b/abogen/webui/routes/utils/settings.py @@ -15,7 +15,7 @@ from abogen.normalization_settings import ( from abogen.utils import load_config, save_config from abogen.integrations.calibre_opds import CalibreOPDSClient from abogen.integrations.audiobookshelf import AudiobookshelfConfig -from abogen.webui.routes.utils.common import split_profile_spec +from abogen.webui.routes.utils.common import split_profile_spec, coerce_bool SAVE_MODE_LABELS = { "save_next_to_input": "Save next to input file", @@ -245,16 +245,6 @@ def render_prompt_template(template: str, context: Mapping[str, str]) -> str: return _PROMPT_TOKEN_RE.sub(_replace, template) -def coerce_bool(value: Any, default: bool) -> bool: - if isinstance(value, bool): - return value - if isinstance(value, str): - return value.lower() in {"true", "1", "yes", "on"} - if value is None: - return default - return bool(value) - - def coerce_float(value: Any, default: float) -> float: try: return max(0.0, float(value))