mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
fix: language enum coercion, preview logging, file picker
- form.py: convert language string to Language enum at all entry points - conversion_request.py: add _coerce_enums() for defense-in-depth - synthesize.py: fix NameError (lang -> language) in preview pipeline - voice.py: render LANGUAGE_DESCRIPTIONS keys as .value strings for Jinja - dashboard.js: open file picker on dropzone click - api.py: add logging to preview endpoint for debugging
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -452,6 +452,9 @@ const initDashboard = () => {
|
||||
return;
|
||||
}
|
||||
openUploadModal(dropzone);
|
||||
if (sourceFileInput) {
|
||||
sourceFileInput.click();
|
||||
}
|
||||
});
|
||||
|
||||
dropzone.addEventListener("keydown", (event) => {
|
||||
|
||||
Reference in New Issue
Block a user