From 95d5921e679e08ccea0c2bd7c7a4dca0e8d9f8c3 Mon Sep 17 00:00:00 2001 From: JB Date: Sat, 20 Dec 2025 06:20:33 -0800 Subject: [PATCH] feat: Integrate Supertonic TTS provider with configuration options and UI updates; enhance voice handling and settings management --- abogen/tts_supertonic.py | 146 ++++++++++++++++++++++++++++ abogen/web/conversion_runner.py | 58 +++++++++-- abogen/web/routes/api.py | 5 + abogen/web/routes/settings.py | 10 ++ abogen/web/routes/utils/common.py | 9 +- abogen/web/routes/utils/form.py | 122 ++++++++++++++--------- abogen/web/routes/utils/preview.py | 62 ++++++++---- abogen/web/routes/utils/service.py | 2 + abogen/web/routes/utils/settings.py | 24 +++++ abogen/web/service.py | 14 +++ abogen/web/templates/base.html | 2 +- abogen/web/templates/settings.html | 40 +++++++- abogen/web/templates/voices.html | 6 +- pyproject.toml | 1 + tests/test_service.py | 34 +------ 15 files changed, 418 insertions(+), 117 deletions(-) create mode 100644 abogen/tts_supertonic.py diff --git a/abogen/tts_supertonic.py b/abogen/tts_supertonic.py new file mode 100644 index 0000000..709965b --- /dev/null +++ b/abogen/tts_supertonic.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +from dataclasses import dataclass +import math +import re +from typing import Any, Iterable, Iterator, Optional + +import numpy as np + + +DEFAULT_SUPERTONIC_VOICES = ("M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5") + + +@dataclass +class SupertonicSegment: + graphemes: str + audio: np.ndarray + + +def _ensure_float32_mono(wav: Any) -> np.ndarray: + arr = np.asarray(wav, dtype="float32") + if arr.ndim == 2: + # (n, 1) or (1, n) or (n, channels) + if arr.shape[0] == 1 and arr.shape[1] > 1: + arr = arr.reshape(-1) + else: + arr = arr[:, 0] + return arr.reshape(-1) + + +def _resample_linear(audio: np.ndarray, src_rate: int, dst_rate: int) -> np.ndarray: + if src_rate == dst_rate: + return audio + if audio.size == 0: + return audio + ratio = dst_rate / float(src_rate) + new_len = int(round(audio.size * ratio)) + if new_len <= 1: + return np.zeros(0, dtype="float32") + x_old = np.linspace(0.0, 1.0, num=audio.size, endpoint=False) + x_new = np.linspace(0.0, 1.0, num=new_len, endpoint=False) + return np.interp(x_new, x_old, audio).astype("float32", copy=False) + + +def _split_text(text: str, *, split_pattern: Optional[str], max_chunk_length: int) -> list[str]: + stripped = (text or "").strip() + if not stripped: + return [] + parts: list[str] + if split_pattern: + try: + parts = [p.strip() for p in re.split(split_pattern, stripped) if p.strip()] + except re.error: + parts = [stripped] + else: + parts = [stripped] + + # Enforce max length by hard-splitting long parts. + result: list[str] = [] + for part in parts: + if len(part) <= max_chunk_length: + result.append(part) + continue + start = 0 + while start < len(part): + end = min(len(part), start + max_chunk_length) + # Try to split at whitespace. + if end < len(part): + ws = part.rfind(" ", start, end) + if ws > start + 40: + end = ws + chunk = part[start:end].strip() + if chunk: + result.append(chunk) + start = end + return result + + +class SupertonicPipeline: + """Minimal adapter that mimics Kokoro's pipeline iteration interface.""" + + def __init__( + self, + *, + sample_rate: int, + auto_download: bool = True, + total_steps: int = 5, + max_chunk_length: int = 300, + ) -> None: + self.sample_rate = int(sample_rate) + self.total_steps = int(total_steps) + self.max_chunk_length = int(max_chunk_length) + + try: + from supertonic import TTS # type: ignore[import-not-found] + except Exception as exc: # pragma: no cover + raise RuntimeError( + "Supertonic is not installed. Install it with `pip install supertonic`." + ) from exc + + self._tts = TTS(auto_download=auto_download) + + def __call__( + self, + text: str, + *, + voice: str, + speed: float, + split_pattern: Optional[str] = None, + total_steps: Optional[int] = None, + ) -> Iterator[SupertonicSegment]: + voice_name = (voice or "").strip() or "M1" + steps = int(total_steps) if total_steps is not None else self.total_steps + steps = max(2, min(15, steps)) + speed_value = float(speed) if speed is not None else 1.0 + speed_value = max(0.7, min(2.0, speed_value)) + + style = self._tts.get_voice_style(voice_name=voice_name) + chunks = _split_text(text, split_pattern=split_pattern, max_chunk_length=self.max_chunk_length) + for chunk in chunks: + wav, duration = self._tts.synthesize( + text=chunk, + voice_style=style, + total_steps=steps, + speed=speed_value, + max_chunk_length=self.max_chunk_length, + silence_duration=0.0, + verbose=False, + ) + audio = _ensure_float32_mono(wav) + + # If duration is present, infer the source sample rate and resample if needed. + src_rate = self.sample_rate + try: + dur = float(duration) + if dur > 0 and audio.size > 0: + inferred = int(round(audio.size / dur)) + if 8000 <= inferred <= 96000: + src_rate = inferred + except Exception: + pass + + if src_rate != self.sample_rate: + audio = _resample_linear(audio, src_rate, self.sample_rate) + + yield SupertonicSegment(graphemes=chunk, audio=audio) diff --git a/abogen/web/conversion_runner.py b/abogen/web/conversion_runner.py index 939686e..339d02f 100644 --- a/abogen/web/conversion_runner.py +++ b/abogen/web/conversion_runner.py @@ -44,6 +44,7 @@ from abogen.voice_cache import ensure_voice_assets from abogen.voice_formulas import extract_voice_ids, get_new_voice from abogen.pronunciation_store import increment_usage from abogen.llm_client import LLMClientError +from abogen.tts_supertonic import DEFAULT_SUPERTONIC_VOICES, SupertonicPipeline from .service import Job, JobStatus @@ -52,6 +53,20 @@ SPLIT_PATTERN = r"\n+" SAMPLE_RATE = 24000 +def _supertonic_voice_from_spec(spec: Any, fallback: str) -> str: + raw = str(spec or "").strip() + if not raw: + raw = str(fallback or "").strip() + if not raw: + return "M1" + if "*" in raw or "+" in raw: + raw = str(fallback or "").strip() or "M1" + upper = raw.upper() + if upper in DEFAULT_SUPERTONIC_VOICES: + return upper + return str(fallback or "").strip() or "M1" + + class _JobCancelled(Exception): """Raised internally to abort a conversion when the client cancels.""" @@ -1371,7 +1386,8 @@ def run_conversion_job(job: Job) -> None: override_token_map: Dict[str, str] = {} try: pipeline = _load_pipeline(job) - _initialize_voice_cache(job) + if getattr(job, "tts_provider", "kokoro") == "kokoro": + _initialize_voice_cache(job) extraction = extract_from_path(job.stored_path) file_type = _infer_file_type(job.stored_path) pronunciation_rules = _compile_pronunciation_rules(job.pronunciation_overrides) @@ -1491,8 +1507,9 @@ def run_conversion_job(job: Job) -> None: base_voice_spec = _job_voice_fallback(job) voice_cache: Dict[str, Any] = {} - if base_voice_spec and "*" not in base_voice_spec: - voice_cache[base_voice_spec] = _resolve_voice(pipeline, base_voice_spec, job.use_gpu) + if getattr(job, "tts_provider", "kokoro") == "kokoro": + if base_voice_spec and "*" not in base_voice_spec: + voice_cache[base_voice_spec] = _resolve_voice(pipeline, base_voice_spec, job.use_gpu) processed_chars = 0 subtitle_index = 1 current_time = 0.0 @@ -1543,12 +1560,25 @@ def run_conversion_job(job: Job) -> None: raise local_segments = 0 - for segment in pipeline( - normalized, - voice=voice_choice, - speed=job.speed, - split_pattern=split_pattern, - ): + provider = getattr(job, "tts_provider", "kokoro") + if provider == "supertonic": + voice_name = _supertonic_voice_from_spec(voice_choice, getattr(job, "voice", "M1")) + segment_iter = pipeline( + normalized, + voice=voice_name, + speed=job.speed, + split_pattern=split_pattern, + total_steps=getattr(job, "supertonic_total_steps", 5), + ) + else: + segment_iter = pipeline( + normalized, + voice=voice_choice, + speed=job.speed, + split_pattern=split_pattern, + ) + + for segment in segment_iter: canceller() graphemes_raw = getattr(segment, "graphemes", "") or "" graphemes = graphemes_raw.strip() @@ -2066,7 +2096,7 @@ def run_conversion_job(job: Job) -> None: pipeline = None gc.collect() try: - import torch + import torch # type: ignore[import-not-found] if torch.cuda.is_available(): torch.cuda.empty_cache() except ImportError: @@ -2093,6 +2123,14 @@ def run_conversion_job(job: Job) -> None: def _load_pipeline(job: Job): cfg = load_config() disable_gpu = not job.use_gpu or not cfg.get("use_gpu", True) + provider = str(getattr(job, "tts_provider", "kokoro") or "kokoro").strip().lower() + if provider == "supertonic": + return SupertonicPipeline( + sample_rate=SAMPLE_RATE, + auto_download=True, + total_steps=int(getattr(job, "supertonic_total_steps", 5) or 5), + ) + device = "cpu" if not disable_gpu: device = _select_device() diff --git a/abogen/web/routes/api.py b/abogen/web/routes/api.py index 938e15d..bffc507 100644 --- a/abogen/web/routes/api.py +++ b/abogen/web/routes/api.py @@ -71,6 +71,8 @@ def api_speaker_preview() -> ResponseReturnValue: voice = payload.get("voice", "af_heart") language = payload.get("language", "a") speed = coerce_float(payload.get("speed"), 1.0) + tts_provider = str(payload.get("tts_provider") or "").strip().lower() + supertonic_total_steps = int(payload.get("supertonic_total_steps") or 5) settings = load_settings() use_gpu = settings.get("use_gpu", False) @@ -82,6 +84,9 @@ def api_speaker_preview() -> ResponseReturnValue: language=language, speed=speed, use_gpu=use_gpu + , + tts_provider=tts_provider or str(settings.get("tts_provider") or "kokoro"), + supertonic_total_steps=supertonic_total_steps or int(settings.get("supertonic_total_steps") or 5), ) except Exception as e: return jsonify({"error": str(e)}), 500 diff --git a/abogen/web/routes/settings.py b/abogen/web/routes/settings.py index c5e1623..b1e0b44 100644 --- a/abogen/web/routes/settings.py +++ b/abogen/web/routes/settings.py @@ -37,7 +37,17 @@ def update_settings() -> ResponseReturnValue: # General settings current["language"] = (form.get("language") or "en").strip() + current["tts_provider"] = (form.get("tts_provider") or current.get("tts_provider") or "kokoro").strip().lower() current["default_voice"] = (form.get("default_voice") or "").strip() + current["supertonic_default_voice"] = (form.get("supertonic_default_voice") or current.get("supertonic_default_voice") or "M1").strip() + try: + current["supertonic_total_steps"] = max(2, min(15, int(form.get("supertonic_total_steps", current.get("supertonic_total_steps", 5))))) + except (TypeError, ValueError): + pass + try: + current["supertonic_speed"] = max(0.7, min(2.0, float(form.get("supertonic_speed", current.get("supertonic_speed", 1.0))))) + except (TypeError, ValueError): + pass current["output_format"] = (form.get("output_format") or "mp3").strip() current["subtitle_mode"] = (form.get("subtitle_mode") or "Disabled").strip() current["subtitle_format"] = (form.get("subtitle_format") or "srt").strip() diff --git a/abogen/web/routes/utils/common.py b/abogen/web/routes/utils/common.py index 821b263..d280bdf 100644 --- a/abogen/web/routes/utils/common.py +++ b/abogen/web/routes/utils/common.py @@ -5,12 +5,19 @@ def split_profile_spec(value: Any) -> Tuple[str, Optional[str]]: text = str(value or "").strip() if not text: return "", None - if text.lower().startswith("profile:"): + lowered = text.lower() + if lowered.startswith("profile:") or lowered.startswith("speaker:"): _, _, remainder = text.partition(":") name = remainder.strip() return "", name or None return text, None + +def split_speaker_spec(value: Any) -> Tuple[str, Optional[str]]: + """Preferred alias for split_profile_spec (supports 'speaker:' and legacy 'profile:').""" + + return split_profile_spec(value) + def existing_paths(paths: Optional[Iterable[Path]]) -> List[Path]: if not paths: return [] diff --git a/abogen/web/routes/utils/form.py b/abogen/web/routes/utils/form.py index 63ca313..e9cb8ab 100644 --- a/abogen/web/routes/utils/form.py +++ b/abogen/web/routes/utils/form.py @@ -574,54 +574,86 @@ def apply_book_step_form( except ValueError: pass - profile_selection = (form.get("voice_profile") or pending.voice_profile or "__standard").strip() - custom_formula_raw = (form.get("voice_formula") or "").strip() - narrator_voice_raw = (form.get("voice") or pending.voice or settings.get("default_voice") or "").strip() + provider_value = (form.get("tts_provider") or getattr(pending, "tts_provider", None) or settings.get("tts_provider") or "kokoro").strip().lower() + if provider_value not in {"kokoro", "supertonic"}: + provider_value = "kokoro" + pending.tts_provider = provider_value + try: + pending.supertonic_total_steps = int( + form.get("supertonic_total_steps") + or getattr(pending, "supertonic_total_steps", None) + or settings.get("supertonic_total_steps") + or 5 + ) + except (TypeError, ValueError): + pending.supertonic_total_steps = int(settings.get("supertonic_total_steps") or 5) - profiles_map = dict(profiles) if isinstance(profiles, Mapping) else dict(profiles or {}) - resolved_default_voice, inferred_profile, _ = resolve_voice_setting( - narrator_voice_raw, - profiles=profiles_map, - ) - - if profile_selection in {"__standard", "", None} and inferred_profile: - profile_selection = inferred_profile - - if profile_selection == "__formula": - profile_name = "" - custom_formula = custom_formula_raw - elif profile_selection in {"__standard", "", None}: - profile_name = "" - custom_formula = "" - else: - profile_name = profile_selection - custom_formula = "" - - base_voice_spec = resolved_default_voice or narrator_voice_raw - if not base_voice_spec and VOICES_INTERNAL: - base_voice_spec = VOICES_INTERNAL[0] - - voice_choice, resolved_language, selected_profile = resolve_voice_choice( - pending.language, - base_voice_spec, - profile_name, - custom_formula, - profiles_map, - ) - - if resolved_language: - pending.language = resolved_language - - if profile_selection == "__formula" and custom_formula_raw: - pending.voice = custom_formula_raw + if provider_value == "supertonic": + narrator_voice_raw = ( + form.get("voice") + or pending.voice + or settings.get("supertonic_default_voice") + or "M1" + ).strip() + # Supertonic does not support Abogen voice mixing. pending.voice_profile = None - elif profile_selection not in {"__standard", "", None, "__formula"}: - pending.voice_profile = selected_profile or profile_selection - pending.voice = voice_choice + pending.voice = narrator_voice_raw + + # Provider-specific speed default. + if speed_value is None: + try: + pending.speed = float(settings.get("supertonic_speed") or 1.0) + except (TypeError, ValueError): + pending.speed = 1.0 else: - pending.voice_profile = None - fallback_voice = base_voice_spec or narrator_voice_raw - pending.voice = voice_choice or fallback_voice + profile_selection = (form.get("voice_profile") or pending.voice_profile or "__standard").strip() + custom_formula_raw = (form.get("voice_formula") or "").strip() + narrator_voice_raw = (form.get("voice") or pending.voice or settings.get("default_voice") or "").strip() + + profiles_map = dict(profiles) if isinstance(profiles, Mapping) else dict(profiles or {}) + resolved_default_voice, inferred_profile, _ = resolve_voice_setting( + narrator_voice_raw, + profiles=profiles_map, + ) + + if profile_selection in {"__standard", "", None} and inferred_profile: + profile_selection = inferred_profile + + if profile_selection == "__formula": + profile_name = "" + custom_formula = custom_formula_raw + elif profile_selection in {"__standard", "", None}: + profile_name = "" + custom_formula = "" + else: + profile_name = profile_selection + custom_formula = "" + + base_voice_spec = resolved_default_voice or narrator_voice_raw + if not base_voice_spec and VOICES_INTERNAL: + base_voice_spec = VOICES_INTERNAL[0] + + voice_choice, resolved_language, selected_profile = resolve_voice_choice( + pending.language, + base_voice_spec, + profile_name, + custom_formula, + profiles_map, + ) + + if resolved_language: + pending.language = resolved_language + + if profile_selection == "__formula" and custom_formula_raw: + pending.voice = custom_formula_raw + pending.voice_profile = None + elif profile_selection not in {"__standard", "", None, "__formula"}: + pending.voice_profile = selected_profile or profile_selection + pending.voice = voice_choice + else: + pending.voice_profile = None + fallback_voice = base_voice_spec or narrator_voice_raw + pending.voice = voice_choice or fallback_voice pending.applied_speaker_config = (form.get("speaker_config") or "").strip() or None diff --git a/abogen/web/routes/utils/preview.py b/abogen/web/routes/utils/preview.py index decaa60..2fe7f2d 100644 --- a/abogen/web/routes/utils/preview.py +++ b/abogen/web/routes/utils/preview.py @@ -31,26 +31,14 @@ def generate_preview_audio( language: str, speed: float, use_gpu: bool, + tts_provider: str = "kokoro", + supertonic_total_steps: int = 5, max_seconds: float = 8.0, ) -> bytes: if not text.strip(): raise ValueError("Preview text is required") - device = "cpu" - if use_gpu: - try: - device = _select_device() - except Exception: - device = "cpu" - use_gpu = False - - pipeline = get_preview_pipeline(language, device) - if pipeline is None: - raise RuntimeError("Preview pipeline is unavailable") - - voice_choice: Any = voice_spec - if voice_spec and "*" in voice_spec: - voice_choice = get_new_voice(pipeline, voice_spec, use_gpu) + provider = (tts_provider or "kokoro").strip().lower() try: normalized_text = normalize_for_pipeline(text) @@ -58,12 +46,40 @@ def generate_preview_audio( current_app.logger.exception("Preview normalization failed; using raw text") normalized_text = text - segments = pipeline( - normalized_text, - voice=voice_choice, - speed=speed, - split_pattern=SPLIT_PATTERN, - ) + if provider == "supertonic": + from abogen.tts_supertonic import SupertonicPipeline + + pipeline = SupertonicPipeline(sample_rate=SAMPLE_RATE, auto_download=True, total_steps=supertonic_total_steps) + segments = pipeline( + normalized_text, + voice=voice_spec, + speed=speed, + split_pattern=SPLIT_PATTERN, + total_steps=supertonic_total_steps, + ) + else: + device = "cpu" + if use_gpu: + try: + device = _select_device() + except Exception: + device = "cpu" + use_gpu = False + + pipeline = get_preview_pipeline(language, device) + if pipeline is None: + raise RuntimeError("Preview pipeline is unavailable") + + voice_choice: Any = voice_spec + if voice_spec and "*" in voice_spec: + voice_choice = get_new_voice(pipeline, voice_spec, use_gpu) + + segments = pipeline( + normalized_text, + voice=voice_choice, + speed=speed, + split_pattern=SPLIT_PATTERN, + ) audio_chunks: List[np.ndarray] = [] accumulated = 0 @@ -100,6 +116,8 @@ def synthesize_preview( language: str, speed: float, use_gpu: bool, + tts_provider: str = "kokoro", + supertonic_total_steps: int = 5, max_seconds: float = 8.0, ) -> ResponseReturnValue: try: @@ -109,6 +127,8 @@ def synthesize_preview( language=language, speed=speed, use_gpu=use_gpu, + tts_provider=tts_provider, + supertonic_total_steps=supertonic_total_steps, max_seconds=max_seconds, ) except Exception as e: diff --git a/abogen/web/routes/utils/service.py b/abogen/web/routes/utils/service.py index c138fcb..1cae32a 100644 --- a/abogen/web/routes/utils/service.py +++ b/abogen/web/routes/utils/service.py @@ -22,8 +22,10 @@ def submit_job(pending: PendingJob) -> str: original_filename=pending.original_filename, stored_path=pending.stored_path, language=pending.language, + tts_provider=getattr(pending, "tts_provider", "kokoro"), voice=pending.voice, speed=pending.speed, + supertonic_total_steps=getattr(pending, "supertonic_total_steps", 5), use_gpu=pending.use_gpu, subtitle_mode=pending.subtitle_mode, output_format=pending.output_format, diff --git a/abogen/web/routes/utils/settings.py b/abogen/web/routes/utils/settings.py index 6f1663a..f428191 100644 --- a/abogen/web/routes/utils/settings.py +++ b/abogen/web/routes/utils/settings.py @@ -170,10 +170,14 @@ def has_output_override() -> bool: def settings_defaults() -> Dict[str, Any]: llm_env_defaults = environment_llm_defaults() return { + "tts_provider": "kokoro", "output_format": "wav", "subtitle_format": "srt", "save_mode": "default_output" if has_output_override() else "save_next_to_input", "default_voice": VOICES_INTERNAL[0] if VOICES_INTERNAL else "", + "supertonic_default_voice": "M1", + "supertonic_total_steps": 5, + "supertonic_speed": 1.0, "replace_single_newlines": False, "use_gpu": True, "save_chapters_separately": False, @@ -340,6 +344,26 @@ def normalize_setting_value(key: str, value: Any, defaults: Dict[str, Any]) -> A parts = [item.strip().lower() for item in value.split(",") if item.strip()] return [code for code in parts if code in LANGUAGE_DESCRIPTIONS] return defaults.get(key, []) + if key == "tts_provider": + if isinstance(value, str): + candidate = value.strip().lower() + if candidate in {"kokoro", "supertonic"}: + return candidate + return defaults.get(key, "kokoro") + if key == "supertonic_default_voice": + return str(value or "").strip() or defaults.get(key, "M1") + if key == "supertonic_total_steps": + try: + steps = int(value) + except (TypeError, ValueError): + return defaults.get(key, 5) + return max(2, min(15, steps)) + if key == "supertonic_speed": + try: + speed = float(value) + except (TypeError, ValueError): + return defaults.get(key, 1.0) + return max(0.7, min(2.0, speed)) return value if value is not None else defaults.get(key) diff --git a/abogen/web/service.py b/abogen/web/service.py index 0c549b8..a04e9ce 100644 --- a/abogen/web/service.py +++ b/abogen/web/service.py @@ -110,6 +110,8 @@ class Job: replace_single_newlines: bool subtitle_format: str created_at: float + tts_provider: str = "kokoro" + supertonic_total_steps: int = 5 save_chapters_separately: bool = False merge_chapters_at_end: bool = True separate_chapters_format: str = "wav" @@ -201,6 +203,8 @@ class Job: }, "queue_position": self.queue_position, "options": { + "tts_provider": getattr(self, "tts_provider", "kokoro"), + "supertonic_total_steps": getattr(self, "supertonic_total_steps", 5), "save_chapters_separately": self.save_chapters_separately, "merge_chapters_at_end": self.merge_chapters_at_end, "separate_chapters_format": self.separate_chapters_format, @@ -547,6 +551,8 @@ class PendingJob: chapters: List[Dict[str, Any]] normalization_overrides: Dict[str, Any] created_at: float + tts_provider: str = "kokoro" + supertonic_total_steps: int = 5 cover_image_path: Optional[Path] = None cover_image_mime: Optional[str] = None chapter_intro_delay: float = 0.5 @@ -614,6 +620,8 @@ class ConversionService: language: str, voice: str, speed: float, + tts_provider: str = "kokoro", + supertonic_total_steps: int = 5, use_gpu: bool, subtitle_mode: str, output_format: str, @@ -665,6 +673,8 @@ class ConversionService: language=language, voice=voice, speed=speed, + tts_provider=tts_provider, + supertonic_total_steps=int(supertonic_total_steps or 5), use_gpu=use_gpu, subtitle_mode=subtitle_mode, output_format=output_format, @@ -1134,8 +1144,10 @@ class ConversionService: "original_filename": job.original_filename, "stored_path": str(job.stored_path), "language": job.language, + "tts_provider": getattr(job, "tts_provider", "kokoro"), "voice": job.voice, "speed": job.speed, + "supertonic_total_steps": getattr(job, "supertonic_total_steps", 5), "use_gpu": job.use_gpu, "subtitle_mode": job.subtitle_mode, "output_format": job.output_format, @@ -1252,6 +1264,7 @@ class ConversionService: original_filename=payload["original_filename"], stored_path=stored_path, language=payload.get("language", "a"), + tts_provider=str(payload.get("tts_provider") or "kokoro"), voice=payload.get("voice", ""), speed=float(payload.get("speed", 1.0)), use_gpu=bool(payload.get("use_gpu", True)), @@ -1262,6 +1275,7 @@ class ConversionService: replace_single_newlines=bool(payload.get("replace_single_newlines", False)), subtitle_format=payload.get("subtitle_format", "srt"), created_at=float(payload.get("created_at", time.time())), + supertonic_total_steps=int(payload.get("supertonic_total_steps", 5)), save_chapters_separately=bool(payload.get("save_chapters_separately", False)), merge_chapters_at_end=bool(payload.get("merge_chapters_at_end", True)), separate_chapters_format=payload.get("separate_chapters_format", "wav"), diff --git a/abogen/web/templates/base.html b/abogen/web/templates/base.html index 6235880..194f394 100644 --- a/abogen/web/templates/base.html +++ b/abogen/web/templates/base.html @@ -18,7 +18,7 @@