diff --git a/abogen/application/conversion_request.py b/abogen/application/conversion_request.py index 2b83411..82b2934 100644 --- a/abogen/application/conversion_request.py +++ b/abogen/application/conversion_request.py @@ -105,6 +105,7 @@ class ConversionRequest: _apply_none_defaults(self) if not self.tts_provider: self.tts_provider = "kokoro" + _coerce_enums(self) _clamp_numerics(self) @@ -119,6 +120,25 @@ def _apply_none_defaults(obj: ConversionRequest) -> None: setattr(obj, f.name, f.default_factory()) +# Enum fields that accept string coercion: attr -> (enum_class, fallback) +_ENUM_COERCIONS: dict[str, tuple[type, Any]] = { + "language": (Language, Language.EN_US), + "output_format": (OutputFormat, OutputFormat.WAV), +} + + +def _coerce_enums(obj: ConversionRequest) -> None: + """Coerce string values to their expected enum types.""" + for attr, (enum_cls, fallback) in _ENUM_COERCIONS.items(): + val = getattr(obj, attr) + if isinstance(val, enum_cls): + continue + try: + setattr(obj, attr, enum_cls.from_str(str(val))) + except (ValueError, AttributeError): + setattr(obj, attr, fallback) + + def _clamp_numerics(obj: ConversionRequest) -> None: """Clamp numeric fields to valid ranges.""" for attr, (min_v, max_v) in _NUMERIC_CONSTRAINTS.items(): diff --git a/abogen/webui/routes/api.py b/abogen/webui/routes/api.py index 30a54ed..fa4aa59 100644 --- a/abogen/webui/routes/api.py +++ b/abogen/webui/routes/api.py @@ -184,6 +184,11 @@ def api_voice_profiles_preview() -> ResponseReturnValue: voice_spec = "" resolved_provider = provider or "kokoro" + current_app.logger.info( + "[preview] provider=%s language=%s speed=%.2f profile=%s formula=%s", + resolved_provider, language, speed, profile_name or "-", formula or "-", + ) + profiles = load_profiles() if resolved_provider == "supertonic" and not profile_name: voice_spec = str(payload.get("voice") or payload.get("supertonic_voice") or "M1").strip() or "M1" @@ -214,7 +219,13 @@ def api_voice_profiles_preview() -> ResponseReturnValue: voice_spec = formula_from_profile(normalized_entry) or "" resolved_provider = "kokoro" + current_app.logger.info( + "[preview] resolved: provider=%s voice_spec=%s", + resolved_provider, voice_spec[:80] if voice_spec else "-", + ) + if not voice_spec: + current_app.logger.warning("[preview] empty voice_spec, returning 400") return jsonify({"error": "Unable to resolve preview voice"}), 400 try: @@ -229,6 +240,7 @@ def api_voice_profiles_preview() -> ResponseReturnValue: max_seconds=max_seconds, ) except Exception as exc: + current_app.logger.exception("[preview] synthesis failed: %s", exc) return jsonify({"error": str(exc)}), 500 @api_bp.post("/speaker-preview") diff --git a/abogen/webui/routes/utils/form.py b/abogen/webui/routes/utils/form.py index 9301c55..fbc163d 100644 --- a/abogen/webui/routes/utils/form.py +++ b/abogen/webui/routes/utils/form.py @@ -4,6 +4,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, cast from flask import request, render_template, jsonify from flask.typing import ResponseReturnValue +from abogen.domain.enums import Language from abogen.domain.chapter_classification import ( supplement_score, should_preselect_chapter, @@ -346,7 +347,10 @@ def apply_book_step_form( language_fallback = pending.language or settings.get("language", "en") raw_language = (form.get("language") or language_fallback or "en").strip() if raw_language: - pending.language = raw_language + try: + pending.language = Language.from_str(raw_language) + except (ValueError, AttributeError): + pending.language = Language.EN_US subtitle_mode = (form.get("subtitle_mode") or pending.subtitle_mode or "Disabled").strip() if subtitle_mode: @@ -513,7 +517,10 @@ def apply_book_step_form( ) if resolved_language: - pending.language = resolved_language + try: + pending.language = Language.from_str(str(resolved_language)) + except (ValueError, AttributeError): + pass # keep existing language if profile_selection == "__formula" and custom_formula_raw: pending.voice = custom_formula_raw @@ -666,7 +673,11 @@ def build_pending_job_from_extraction( ensure_at_least_one_chapter_enabled(chapters_payload) - language = str(form.get("language") or "a").strip() or "a" + raw_language = str(form.get("language") or "a").strip() or "a" + try: + language = Language.from_str(raw_language) + except (ValueError, AttributeError): + language = Language.EN_US profiles_map = dict(profiles) if isinstance(profiles, Mapping) else dict(profiles or {}) default_voice_setting = settings.get("default_voice") or "" resolved_default_voice, inferred_profile, inferred_language = resolve_voice_setting( diff --git a/abogen/webui/routes/utils/synthesize.py b/abogen/webui/routes/utils/synthesize.py index 8702157..ecb0716 100644 --- a/abogen/webui/routes/utils/synthesize.py +++ b/abogen/webui/routes/utils/synthesize.py @@ -58,7 +58,7 @@ def get_preview_pipeline(language: Language, device: str) -> Any: return pipeline from abogen.tts_plugin.utils import create_pipeline - pipeline = create_pipeline("kokoro", language=lang, device=device) + pipeline = create_pipeline("kokoro", language=language, device=device) _preview_pipelines[key] = pipeline return pipeline diff --git a/abogen/webui/routes/utils/voice.py b/abogen/webui/routes/utils/voice.py index 5b8ab74..921f918 100644 --- a/abogen/webui/routes/utils/voice.py +++ b/abogen/webui/routes/utils/voice.py @@ -578,7 +578,7 @@ def template_options() -> Dict[str, Any]: ) voice_catalog = build_voice_catalog() return { - "languages": LANGUAGE_DESCRIPTIONS, + "languages": {lang.value: label for lang, label in LANGUAGE_DESCRIPTIONS.items()}, "voices": get_voices("kokoro"), "subtitle_formats": SUBTITLE_FORMATS, "supported_langs_for_subs": SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION, diff --git a/abogen/webui/static/dashboard.js b/abogen/webui/static/dashboard.js index e2d0028..709da33 100644 --- a/abogen/webui/static/dashboard.js +++ b/abogen/webui/static/dashboard.js @@ -452,6 +452,9 @@ const initDashboard = () => { return; } openUploadModal(dropzone); + if (sourceFileInput) { + sourceFileInput.click(); + } }); dropzone.addEventListener("keydown", (event) => {