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)
|
_apply_none_defaults(self)
|
||||||
if not self.tts_provider:
|
if not self.tts_provider:
|
||||||
self.tts_provider = "kokoro"
|
self.tts_provider = "kokoro"
|
||||||
|
_coerce_enums(self)
|
||||||
_clamp_numerics(self)
|
_clamp_numerics(self)
|
||||||
|
|
||||||
|
|
||||||
@@ -119,6 +120,25 @@ def _apply_none_defaults(obj: ConversionRequest) -> None:
|
|||||||
setattr(obj, f.name, f.default_factory())
|
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:
|
def _clamp_numerics(obj: ConversionRequest) -> None:
|
||||||
"""Clamp numeric fields to valid ranges."""
|
"""Clamp numeric fields to valid ranges."""
|
||||||
for attr, (min_v, max_v) in _NUMERIC_CONSTRAINTS.items():
|
for attr, (min_v, max_v) in _NUMERIC_CONSTRAINTS.items():
|
||||||
|
|||||||
@@ -184,6 +184,11 @@ def api_voice_profiles_preview() -> ResponseReturnValue:
|
|||||||
voice_spec = ""
|
voice_spec = ""
|
||||||
resolved_provider = provider or "kokoro"
|
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()
|
profiles = load_profiles()
|
||||||
if resolved_provider == "supertonic" and not profile_name:
|
if resolved_provider == "supertonic" and not profile_name:
|
||||||
voice_spec = str(payload.get("voice") or payload.get("supertonic_voice") or "M1").strip() or "M1"
|
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 ""
|
voice_spec = formula_from_profile(normalized_entry) or ""
|
||||||
resolved_provider = "kokoro"
|
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:
|
if not voice_spec:
|
||||||
|
current_app.logger.warning("[preview] empty voice_spec, returning 400")
|
||||||
return jsonify({"error": "Unable to resolve preview voice"}), 400
|
return jsonify({"error": "Unable to resolve preview voice"}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -229,6 +240,7 @@ def api_voice_profiles_preview() -> ResponseReturnValue:
|
|||||||
max_seconds=max_seconds,
|
max_seconds=max_seconds,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
current_app.logger.exception("[preview] synthesis failed: %s", exc)
|
||||||
return jsonify({"error": str(exc)}), 500
|
return jsonify({"error": str(exc)}), 500
|
||||||
|
|
||||||
@api_bp.post("/speaker-preview")
|
@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 import request, render_template, jsonify
|
||||||
from flask.typing import ResponseReturnValue
|
from flask.typing import ResponseReturnValue
|
||||||
|
|
||||||
|
from abogen.domain.enums import Language
|
||||||
from abogen.domain.chapter_classification import (
|
from abogen.domain.chapter_classification import (
|
||||||
supplement_score,
|
supplement_score,
|
||||||
should_preselect_chapter,
|
should_preselect_chapter,
|
||||||
@@ -346,7 +347,10 @@ def apply_book_step_form(
|
|||||||
language_fallback = pending.language or settings.get("language", "en")
|
language_fallback = pending.language or settings.get("language", "en")
|
||||||
raw_language = (form.get("language") or language_fallback or "en").strip()
|
raw_language = (form.get("language") or language_fallback or "en").strip()
|
||||||
if raw_language:
|
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()
|
subtitle_mode = (form.get("subtitle_mode") or pending.subtitle_mode or "Disabled").strip()
|
||||||
if subtitle_mode:
|
if subtitle_mode:
|
||||||
@@ -513,7 +517,10 @@ def apply_book_step_form(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if resolved_language:
|
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:
|
if profile_selection == "__formula" and custom_formula_raw:
|
||||||
pending.voice = 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)
|
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 {})
|
profiles_map = dict(profiles) if isinstance(profiles, Mapping) else dict(profiles or {})
|
||||||
default_voice_setting = settings.get("default_voice") or ""
|
default_voice_setting = settings.get("default_voice") or ""
|
||||||
resolved_default_voice, inferred_profile, inferred_language = resolve_voice_setting(
|
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
|
return pipeline
|
||||||
from abogen.tts_plugin.utils import create_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
|
_preview_pipelines[key] = pipeline
|
||||||
return pipeline
|
return pipeline
|
||||||
|
|
||||||
|
|||||||
@@ -578,7 +578,7 @@ def template_options() -> Dict[str, Any]:
|
|||||||
)
|
)
|
||||||
voice_catalog = build_voice_catalog()
|
voice_catalog = build_voice_catalog()
|
||||||
return {
|
return {
|
||||||
"languages": LANGUAGE_DESCRIPTIONS,
|
"languages": {lang.value: label for lang, label in LANGUAGE_DESCRIPTIONS.items()},
|
||||||
"voices": get_voices("kokoro"),
|
"voices": get_voices("kokoro"),
|
||||||
"subtitle_formats": SUBTITLE_FORMATS,
|
"subtitle_formats": SUBTITLE_FORMATS,
|
||||||
"supported_langs_for_subs": SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
"supported_langs_for_subs": SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
||||||
|
|||||||
@@ -452,6 +452,9 @@ const initDashboard = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openUploadModal(dropzone);
|
openUploadModal(dropzone);
|
||||||
|
if (sourceFileInput) {
|
||||||
|
sourceFileInput.click();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dropzone.addEventListener("keydown", (event) => {
|
dropzone.addEventListener("keydown", (event) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user