feat: Supertonic language + total_steps propagation

Language enum expanded from 9 to 33 languages:
- Added 24 new ISO 639-1 languages: AR, BG, CS, DA, DE, EL, ET, FI,
  HR, HU, ID, KO, LT, LV, NL, PL, RO, RU, SK, SL, SV, TR, UK, VI
- Updated display_name, is_cjk (added KO)

Supertonic language mapping (32 languages, no ZH):
- engine.py: _SUPERTONIC_LANG_MAP, engine_language(), supported_languages()
- __init__.py: create_engine() passes config.language to pipeline
- pipeline.py: __init__() accepts language, resolves to ISO code;
  __call__() passes lang= to TTS.synthesize()

total_steps propagation:
- tts_segments(): +total_steps param, conditionally passed to backend
- synthesize_text(): +total_steps param
- run_tts_segment_loop(): +total_steps param
- executor: all 5 synthesize_text() calls pass total_steps

Integration:
- pipeline_factory: create_pipeline_for_job() passes language to supertonic
- preview path: create_pipeline('supertonic', language=language)

Tests updated to accept total_steps in FakeBackend.__call__
This commit is contained in:
Artem Akymenko
2026-07-28 14:09:42 +03:00
parent 953bef1e71
commit 2b70b9ca45
14 changed files with 146 additions and 15 deletions
@@ -277,6 +277,7 @@ def execute_conversion(
backend=intro_backend,
voice=intro_voice,
speed=intro_speed or request.speed,
total_steps=intro_steps,
chapter_sink=None,
preview_callback=lambda text: events.log(f" {text[:80]}"),
)
@@ -347,6 +348,7 @@ def execute_conversion(
backend=intro_backend,
voice=intro_voice,
speed=intro_speed or request.speed,
total_steps=intro_steps,
chapter_sink=chapter_sink,
preview_callback=lambda text: events.log(f" Intro: {text[:80]}"),
)
@@ -421,6 +423,7 @@ def execute_conversion(
seg_provider = chapter_provider
seg_voice = chapter_voice
seg_speed = chapter_speed
seg_steps = chapter_steps
seg_backend = chapter_backend
# Track voice for chapter marker
@@ -452,6 +455,7 @@ def execute_conversion(
backend=seg_backend,
voice=seg_voice,
speed=seg_speed or request.speed,
total_steps=seg_steps,
chapter_sink=chapter_sink,
preview_callback=lambda text: events.log(f" {text[:80]}"),
split_pattern_override=active_split,
@@ -539,6 +543,7 @@ def execute_conversion(
backend=outro_backend,
voice=outro_voice,
speed=outro_speed or request.speed,
total_steps=outro_steps,
chapter_sink=None,
preview_callback=lambda text: events.log(f" {text[:80]}"),
)
+5
View File
@@ -61,6 +61,7 @@ def run_tts_segment_loop(
voice: Any,
speed: float,
split_pattern: str,
total_steps: Optional[int] = None,
chapter_sink: Optional[AudioSink] = None,
preview_callback: Optional[Callable[[str], None]] = None,
on_segment: Optional[Callable[[SegmentInfo], None]] = None,
@@ -74,6 +75,7 @@ def run_tts_segment_loop(
voice: Voice name/id for the backend.
speed: Speech speed multiplier.
split_pattern: Regex pattern used by the TTS engine for sentence splitting.
total_steps: Inference quality steps (Supertonic only, ignored by Kokoro).
preview_callback: Called with a short preview string per segment.
on_segment: Called with a SegmentInfo for each segment *before*
audio is written. Useful for callers that need per-segment
@@ -95,6 +97,7 @@ def run_tts_segment_loop(
speed=speed,
split_pattern=split_pattern,
current_time=params.stats.current_time,
total_steps=total_steps,
):
if params.check_cancel():
break
@@ -212,6 +215,7 @@ def synthesize_text(
backend: Any,
voice: Any,
speed: float,
total_steps: Optional[int] = None,
chapter_sink: Optional[AudioSink] = None,
preview_callback: Optional[Callable[[str], None]] = None,
on_segment: Optional[Callable[[SegmentInfo], None]] = None,
@@ -229,6 +233,7 @@ def synthesize_text(
backend=backend,
voice=voice,
speed=speed,
total_steps=total_steps,
split_pattern=split_pattern_override or params.tts_context.split_pattern,
chapter_sink=chapter_sink,
preview_callback=preview_callback,
+9 -2
View File
@@ -146,6 +146,7 @@ def tts_segments(
speed: float,
split_pattern: str,
current_time: float = 0.0,
total_steps: Optional[int] = None,
) -> Iterator[SegmentResult]:
"""Invoke TTS backend on (already normalized) text and yield SegmentResults.
@@ -159,16 +160,20 @@ def tts_segments(
speed: TTS speed multiplier.
split_pattern: Regex pattern for sentence splitting.
current_time: Current position in the audio timeline (seconds).
total_steps: Inference quality steps (Supertonic only, ignored by Kokoro).
Yields:
SegmentResult for each non-empty TTS segment.
"""
segment_iter = backend(
text,
kwargs: dict[str, Any] = dict(
voice=voice,
speed=speed,
split_pattern=split_pattern,
)
if total_steps is not None:
kwargs["total_steps"] = total_steps
segment_iter = backend(text, **kwargs)
chunk_start = current_time
@@ -215,6 +220,7 @@ def emit_text_segments(
speed: float,
split_pattern: str,
current_time: float = 0.0,
total_steps: Optional[int] = None,
# normalization
heteronym_rules: Any = None,
pronunciation_rules: Any = None,
@@ -265,6 +271,7 @@ def emit_text_segments(
speed=speed,
split_pattern=split_pattern,
current_time=current_time,
total_steps=total_steps,
)
+52 -4
View File
@@ -128,8 +128,8 @@ class InputFormat(str, Enum):
class Language(str, Enum):
"""TTS language code (ISO 639-1 with region where needed).
Each engine (Kokoro, Supertonic) maps these to its own
internal language identifiers.
Each engine maps these to its own internal language identifiers.
Engines report which languages they support via ``supported_languages()``.
"""
EN_US = "en-US"
EN_GB = "en-GB"
@@ -140,6 +140,30 @@ class Language(str, Enum):
JA = "ja"
PT_BR = "pt-BR"
ZH = "zh"
AR = "ar"
BG = "bg"
CS = "cs"
DA = "da"
DE = "de"
EL = "el"
ET = "et"
FI = "fi"
HR = "hr"
HU = "hu"
ID = "id"
KO = "ko"
LT = "lt"
LV = "lv"
NL = "nl"
PL = "pl"
RO = "ro"
RU = "ru"
SK = "sk"
SL = "sl"
SV = "sv"
TR = "tr"
UK = "uk"
VI = "vi"
@property
def display_name(self) -> str:
@@ -154,13 +178,37 @@ class Language(str, Enum):
"ja": "Japanese",
"pt-BR": "Brazilian Portuguese",
"zh": "Mandarin Chinese",
"ar": "Arabic",
"bg": "Bulgarian",
"cs": "Czech",
"da": "Danish",
"de": "German",
"el": "Greek",
"et": "Estonian",
"fi": "Finnish",
"hr": "Croatian",
"hu": "Hungarian",
"id": "Indonesian",
"ko": "Korean",
"lt": "Lithuanian",
"lv": "Latvian",
"nl": "Dutch",
"pl": "Polish",
"ro": "Romanian",
"ru": "Russian",
"sk": "Slovak",
"sl": "Slovenian",
"sv": "Swedish",
"tr": "Turkish",
"uk": "Ukrainian",
"vi": "Vietnamese",
}
return _names[self.value]
@property
def is_cjk(self) -> bool:
"""True for CJK languages (Chinese, Japanese)."""
return self in (self.ZH, self.JA)
"""True for CJK languages (Chinese, Japanese, Korean)."""
return self in (self.ZH, self.JA, self.KO)
@property
def supports_subtitle_tokens(self) -> bool:
+1 -1
View File
@@ -45,7 +45,7 @@ def create_pipeline_for_job(
provider = "kokoro"
if provider == "supertonic":
return create_pipeline("supertonic")
return create_pipeline("supertonic", language=language)
device = resolve_device(use_gpu)
return create_pipeline("kokoro", language=language, device=device)
+1 -1
View File
@@ -116,7 +116,7 @@ def generate_preview_audio(
if provider == "supertonic":
from abogen.tts_plugin.utils import create_pipeline
pipeline = create_pipeline("supertonic")
pipeline = create_pipeline("supertonic", language=language)
segments = pipeline(
normalized_text,
voice=voice_spec,