From 81937b7a40de815c6dce5c7e5ab9b2ab4ffb2ccb Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Sat, 23 May 2026 21:12:05 +0200 Subject: [PATCH] Add Supertonic TTS provider support and configuration options --- abogen/pyqt/conversion.py | 100 +++++++++++----- abogen/pyqt/gui.py | 235 +++++++++++++++++++++++++++++++++---- abogen/pyqt/queued_item.py | 4 + abogen/tts_supertonic.py | 17 ++- 4 files changed, 307 insertions(+), 49 deletions(-) diff --git a/abogen/pyqt/conversion.py b/abogen/pyqt/conversion.py index 8aa301e..c62f9e8 100644 --- a/abogen/pyqt/conversion.py +++ b/abogen/pyqt/conversion.py @@ -20,6 +20,7 @@ from abogen.constants import ( SUPPORTED_SOUND_FORMATS, SUPPORTED_SUBTITLE_FORMATS, ) +from abogen.tts_supertonic import SupertonicPipeline, SUPERTONIC_AVAILABLE_LANGS, DEFAULT_SUPERTONIC_VOICES from abogen.voice_formulas import get_new_voice import abogen.hf_tracker as hf_tracker import static_ffmpeg @@ -266,7 +267,10 @@ class ConversionThread(QThread): use_gpu=True, from_queue=False, save_base_path=None, - ): # Add use_gpu parameter + tts_provider="kokoro", + supertonic_language="en", + supertonic_total_steps=5, + ): super().__init__() self._chapter_options_event = threading.Event() self._timestamp_response_event = threading.Event() @@ -276,6 +280,9 @@ class ConversionThread(QThread): self.lang_code = lang_code self.speed = speed self.voice = voice + self.tts_provider = tts_provider + self.supertonic_language = supertonic_language + self.supertonic_total_steps = supertonic_total_steps self.save_option = save_option self.output_folder = output_folder self.subtitle_mode = subtitle_mode @@ -427,6 +434,10 @@ class ConversionThread(QThread): ) self.log_updated.emit(f"- Voice: {self.voice}") self.log_updated.emit(f"- Speed: {self.speed}") + tts_provider_label = self.tts_provider.capitalize() + if self.tts_provider == "supertonic": + tts_provider_label += f" (lang={self.supertonic_language}, steps={self.supertonic_total_steps})" + self.log_updated.emit(f"- TTS Engine: {tts_provider_label}") self.log_updated.emit(f"- Subtitle mode: {self.subtitle_mode}") self.log_updated.emit(f"- Output format: {self.output_format}") self.log_updated.emit( @@ -499,9 +510,16 @@ class ConversionThread(QThread): else: device = "cpu" - tts = self.KPipeline( - lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device - ) + if self.tts_provider == "supertonic": + tts = SupertonicPipeline( + sample_rate=24000, + lang=self.supertonic_language, + total_steps=self.supertonic_total_steps, + ) + else: + tts = self.KPipeline( + lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device + ) # Check if the input is a subtitle file or timestamp text file is_subtitle_file = False @@ -2447,6 +2465,9 @@ class VoicePreviewThread(QThread): speed, use_gpu=False, parent=None, + tts_provider="kokoro", + supertonic_language="en", + supertonic_total_steps=5, ): super().__init__(parent) self.np_module = np_module @@ -2455,6 +2476,9 @@ class VoicePreviewThread(QThread): self.voice = voice self.speed = speed self.use_gpu = use_gpu + self.tts_provider = tts_provider + self.supertonic_language = supertonic_language + self.supertonic_total_steps = supertonic_total_steps # Cache location for preview audio self.cache_dir = get_user_cache_path("preview_cache") @@ -2464,6 +2488,11 @@ class VoicePreviewThread(QThread): def _get_cache_path(self): """Generate a unique filename for the voice with its parameters""" + if self.tts_provider == "supertonic": + voice_id = self.voice or "M1" + filename = f"st_{voice_id}_{self.supertonic_language}_steps{self.supertonic_total_steps}_{self.speed:.2f}.wav" + return os.path.join(self.cache_dir, filename) + # For a voice formula, use a hash of the formula if "*" in self.voice: voice_id = ( @@ -2478,38 +2507,55 @@ class VoicePreviewThread(QThread): def run(self): print( - f"\nVoice: {self.voice}\nLanguage: {self.lang_code}\nSpeed: {self.speed}\nGPU: {self.use_gpu}\n" + f"\nVoice: {self.voice}\nLanguage: {self.lang_code}\nSpeed: {self.speed}\nGPU: {self.use_gpu}\nTTS Provider: {self.tts_provider}\n" ) # Generate the preview and save to cache try: + if self.tts_provider == "supertonic": + from abogen.tts_supertonic import SupertonicPipeline - # Set device based on use_gpu setting and platform - if self.use_gpu: - if platform.system() == "Darwin" and platform.processor() == "arm": - device = "mps" # Use MPS for Apple Silicon + tts = SupertonicPipeline( + sample_rate=24000, + lang=self.supertonic_language, + total_steps=self.supertonic_total_steps, + ) + loaded_voice = self.voice or "M1" + sample_text = "Hello, this is a sample of the selected voice." + audio_segments = [] + for result in tts( + sample_text, + voice=loaded_voice, + speed=self.speed, + split_pattern=None, + ): + audio_segments.append(result.audio) + else: + # Set device based on use_gpu setting and platform + if self.use_gpu: + if platform.system() == "Darwin" and platform.processor() == "arm": + device = "mps" + else: + device = "cuda" else: - device = "cuda" # Use CUDA for other platforms - else: - device = "cpu" + device = "cpu" + + tts = self.kpipeline_class( + lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device + ) + if "*" in self.voice: + loaded_voice = get_new_voice(tts, self.voice, self.use_gpu) + else: + loaded_voice = self.voice + sample_text = get_sample_voice_text(self.lang_code) + audio_segments = [] + for result in tts( + sample_text, voice=loaded_voice, speed=self.speed, split_pattern=None + ): + audio_segments.append(result.audio) - tts = self.kpipeline_class( - lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device - ) - # Enable voice formula support for preview - if "*" in self.voice: - loaded_voice = get_new_voice(tts, self.voice, self.use_gpu) - else: - loaded_voice = self.voice - sample_text = get_sample_voice_text(self.lang_code) - audio_segments = [] - for result in tts( - sample_text, voice=loaded_voice, speed=self.speed, split_pattern=None - ): - audio_segments.append(result.audio) if audio_segments: audio = self.np_module.concatenate(audio_segments) - # Save directly to the cache path sf.write(self.cache_path, audio, 24000) self.temp_wav = self.cache_path self.finished.emit() diff --git a/abogen/pyqt/gui.py b/abogen/pyqt/gui.py index 4ff103e..3ee53b0 100644 --- a/abogen/pyqt/gui.py +++ b/abogen/pyqt/gui.py @@ -9,6 +9,7 @@ from abogen.pyqt.queue_manager_gui import QueueManager from abogen.pyqt.queued_item import QueuedItem import abogen.hf_tracker as hf_tracker import hashlib # Added for cache path generation +from abogen.tts_supertonic import SUPERTONIC_AVAILABLE_LANGS, DEFAULT_SUPERTONIC_VOICES from PyQt6.QtWidgets import ( QApplication, QWidget, @@ -970,6 +971,9 @@ class abogen(QWidget): self.fix_nonstandard_punctuation = self.config.get( "fix_nonstandard_punctuation", False ) + self.tts_provider_config = self.config.get("tts_provider", "kokoro") + self.supertonic_language_config = self.config.get("supertonic_language", "en") + self.supertonic_total_steps_config = self.config.get("supertonic_total_steps", 5) self._pending_close_event = None self.gpu_ok = False # Initialize GPU availability status @@ -1018,6 +1022,14 @@ class abogen(QWidget): else: self.mixed_voice_state = entry self.selected_lang = entry[0][0] if entry and entry[0] else None + # Restore TTS provider and supertonic settings from config + provider_text = "SuperTonic" if self.tts_provider_config == "supertonic" else "Kokoro" + idx_st = self.provider_combo.findText(provider_text) + if idx_st >= 0: + self.provider_combo.setCurrentIndex(idx_st) + self.st_lang_combo.setCurrentText(self.supertonic_language_config) + self.st_steps_spin.setValue(self.supertonic_total_steps_config) + if self.save_option == "Choose output folder" and self.selected_output_folder: self.save_path_label.setText(self.selected_output_folder) self.save_path_row_widget.show() @@ -1107,6 +1119,50 @@ class abogen(QWidget): speed_layout.addWidget(self.speed_label) controls_layout.addLayout(speed_layout) self.speed_slider.valueChanged.connect(self.update_speed_label) + + # TTS Provider selection + provider_layout = QHBoxLayout() + provider_layout.setSpacing(7) + provider_label = QLabel("TTS Engine:", self) + provider_layout.addWidget(provider_label) + self.provider_combo = QComboBox(self) + self.provider_combo.addItem("Kokoro", "kokoro") + self.provider_combo.addItem("SuperTonic", "supertonic") + self.provider_combo.setStyleSheet("QComboBox { min-height: 20px; padding: 6px 12px; }") + self.provider_combo.currentIndexChanged.connect(self.on_provider_changed) + provider_layout.addWidget(self.provider_combo) + controls_layout.addLayout(provider_layout) + + # SuperTonic-specific controls (language + steps), hidden by default + self.supertonic_row = QWidget() + supertonic_row_layout = QHBoxLayout(self.supertonic_row) + supertonic_row_layout.setContentsMargins(0, 0, 0, 0) + supertonic_row_layout.setSpacing(7) + + st_lang_label = QLabel("Language:", self) + supertonic_row_layout.addWidget(st_lang_label) + self.st_lang_combo = QComboBox(self) + for code in SUPERTONIC_AVAILABLE_LANGS: + self.st_lang_combo.addItem(code, code) + self.st_lang_combo.setCurrentText("en") + self.st_lang_combo.setStyleSheet("QComboBox { min-height: 20px; padding: 6px 12px; }") + self.st_lang_combo.currentTextChanged.connect(self._on_st_lang_changed) + supertonic_row_layout.addWidget(self.st_lang_combo) + + st_steps_label = QLabel("Steps:", self) + supertonic_row_layout.addWidget(st_steps_label) + self.st_steps_spin = QSpinBox(self) + self.st_steps_spin.setMinimum(1) + self.st_steps_spin.setMaximum(10) + self.st_steps_spin.setValue(5) + self.st_steps_spin.setFixedWidth(60) + self.st_steps_spin.valueChanged.connect(self._on_st_steps_changed) + supertonic_row_layout.addWidget(self.st_steps_spin) + + supertonic_row_layout.addStretch() + self.supertonic_row.hide() + controls_layout.addWidget(self.supertonic_row) + # Voice selection voice_layout = QHBoxLayout() voice_layout.setSpacing(7) @@ -1764,6 +1820,12 @@ class abogen(QWidget): Update the enabled state of subtitle options based on the selected language. For non-English languages, only sentence-based and line-based modes are supported. """ + provider = self.provider_combo.currentData() + if provider == "supertonic": + self.subtitle_combo.setEnabled(False) + self.subtitle_format_combo.setEnabled(False) + return + # Check if current file is a subtitle file is_subtitle_input = False if self.selected_file and self.selected_file.lower().endswith( @@ -1823,6 +1885,48 @@ class abogen(QWidget): # Enable/disable subtitle options based on language self.update_subtitle_options_availability() + def on_provider_changed(self, index): + provider = self.provider_combo.itemData(index) + self.config["tts_provider"] = provider + save_config(self.config) + is_supertonic = provider == "supertonic" + + # Show/hide SuperTonic controls + self.supertonic_row.setVisible(is_supertonic) + + # Update subtitles availability + self.update_subtitle_options_availability() + + # Repopulate voice list + self.populate_profiles_in_voice_combo() + + # Clear/reset mixed voice state when switching provider + if is_supertonic: + self.mixed_voice_state = None + self.btn_voice_formula_mixer.setEnabled(False) + self.voice_combo.setToolTip( + "SuperTonic voices:\n" + "M1-M5 = Male voices\n" + "F1-F5 = Female voices" + ) + else: + self.btn_voice_formula_mixer.setEnabled(True) + self.voice_combo.setToolTip( + "The first character represents the language:\n" + '"a" => American English\n"b" => British English\n"e" => Spanish\n"f" => French\n"h" => Hindi\n"i" => Italian\n"j" => Japanese\n"p" => Brazilian Portuguese\n"z" => Mandarin Chinese\nThe second character represents the gender:\n"m" => Male\n"f" => Female' + ) + + def _on_st_lang_changed(self, lang): + self.config["supertonic_language"] = lang + save_config(self.config) + if self.provider_combo.currentData() == "supertonic": + self.selected_lang = lang + self.update_subtitle_options_availability() + + def _on_st_steps_changed(self, steps): + self.config["supertonic_total_steps"] = steps + save_config(self.config) + def on_voice_combo_changed(self, index): data = self.voice_combo.itemData(index) if isinstance(data, str) and data.startswith("profile:"): @@ -1831,10 +1935,24 @@ class abogen(QWidget): from abogen.voice_profiles import load_profiles entry = load_profiles().get(pname, {}) - # set mixed voices and language if isinstance(entry, dict): - self.mixed_voice_state = entry.get("voices", []) - self.selected_lang = entry.get("language") + entry_provider = str(entry.get("provider", "")).strip().lower() + if entry_provider == "supertonic": + # Switch provider to SuperTonic if not already + if self.provider_combo.currentData() != "supertonic": + self.provider_combo.setCurrentIndex(1) + self.mixed_voice_state = None + self.selected_lang = entry.get("language", self.st_lang_combo.currentText()) + # Sync supertonic controls from profile + profile_steps = entry.get("total_steps") + if profile_steps is not None: + self.st_steps_spin.setValue(int(profile_steps)) + profile_lang = entry.get("language") + if profile_lang and profile_lang in SUPERTONIC_AVAILABLE_LANGS: + self.st_lang_combo.setCurrentText(profile_lang) + else: + self.mixed_voice_state = entry.get("voices", []) + self.selected_lang = entry.get("language") else: self.mixed_voice_state = entry self.selected_lang = entry[0][0] if entry and entry[0] else None @@ -1847,7 +1965,12 @@ class abogen(QWidget): else: self.mixed_voice_state = None self.selected_profile_name = None - self.selected_voice, self.selected_lang = data, data[0] + self.selected_voice = data + provider = self.provider_combo.currentData() + if provider == "supertonic": + self.selected_lang = self.st_lang_combo.currentText() + else: + self.selected_lang = data[0] if data else "" self.config["selected_voice"] = data if "selected_profile_name" in self.config: del self.config["selected_profile_name"] @@ -1866,19 +1989,32 @@ class abogen(QWidget): def populate_profiles_in_voice_combo(self): # preserve current voice or profile current = self.voice_combo.currentData() + provider = self.provider_combo.currentData() self.voice_combo.blockSignals(True) self.voice_combo.clear() - # re-add profiles + # re-add profiles matching current provider profile_icon = QIcon(get_resource_path("abogen.assets", "profile.png")) - for pname in load_profiles().keys(): - self.voice_combo.addItem(profile_icon, pname, f"profile:{pname}") + for pname, entry in load_profiles().items(): + entry_provider = "" + if isinstance(entry, dict): + entry_provider = str(entry.get("provider", "")).strip().lower() + if provider == "supertonic": + if entry_provider == "supertonic": + self.voice_combo.addItem(profile_icon, pname, f"profile:{pname}") + else: + if entry_provider != "supertonic": + self.voice_combo.addItem(profile_icon, pname, f"profile:{pname}") # re-add voices - for v in VOICES_INTERNAL: - icon = QIcon() - flag_path = get_resource_path("abogen.assets.flags", f"{v[0]}.png") - if flag_path and os.path.exists(flag_path): - icon = QIcon(flag_path) - self.voice_combo.addItem(icon, f"{v}", v) + if provider == "supertonic": + for v in DEFAULT_SUPERTONIC_VOICES: + self.voice_combo.addItem(f"{v}", v) + else: + for v in VOICES_INTERNAL: + icon = QIcon() + flag_path = get_resource_path("abogen.assets.flags", f"{v[0]}.png") + if flag_path and os.path.exists(flag_path): + icon = QIcon(flag_path) + self.voice_combo.addItem(icon, f"{v}", v) # restore selection idx = -1 if self.selected_profile_name: @@ -2069,6 +2205,9 @@ class abogen(QWidget): save_base_path=save_base_path, save_chapters_separately=getattr(self, "save_chapters_separately", None), merge_chapters_at_end=getattr(self, "merge_chapters_at_end", None), + tts_provider=self.provider_combo.currentData(), + supertonic_language=self.st_lang_combo.currentText(), + supertonic_total_steps=self.st_steps_spin.value(), ) # Prevent adding duplicate items to the queue @@ -2212,6 +2351,12 @@ class abogen(QWidget): self.config["replace_numerals"] = self.replace_numerals self.config["fix_nonstandard_punctuation"] = self.fix_nonstandard_punctuation + # TTS provider settings + tts_provider = getattr(queued_item, "tts_provider", "kokoro") + self.provider_combo.setCurrentText("SuperTonic" if tts_provider == "supertonic" else "Kokoro") + self.st_lang_combo.setCurrentText(getattr(queued_item, "supertonic_language", "en")) + self.st_steps_spin.setValue(getattr(queued_item, "supertonic_total_steps", 5)) + # Sync Voice/Profile in config self.config["selected_voice"] = self.selected_voice if "selected_profile_name" in self.config: @@ -2234,6 +2379,13 @@ class abogen(QWidget): self.current_queue_index = 0 # Reset for next time def get_voice_formula(self) -> str: + if self.provider_combo.currentData() == "supertonic": + if self.selected_profile_name: + from abogen.voice_profiles import load_profiles + entry = load_profiles().get(self.selected_profile_name, {}) + if isinstance(entry, dict): + return str(entry.get("voice", self.selected_voice or "M1")) + return self.selected_voice or "M1" if self.mixed_voice_state: formula_components = [ f"{name}*{weight}" for name, weight in self.mixed_voice_state @@ -2243,6 +2395,8 @@ class abogen(QWidget): return self.selected_voice def get_selected_lang(self, voice_formula) -> str: + if self.provider_combo.currentData() == "supertonic": + return self.st_lang_combo.currentText() if self.selected_profile_name: from abogen.voice_profiles import load_profiles @@ -2332,6 +2486,10 @@ class abogen(QWidget): # determine selected language: use profile setting if profile selected, else voice code selected_lang = self.get_selected_lang(voice_formula) + tts_provider = self.provider_combo.currentData() + supertonic_language = self.st_lang_combo.currentText() + supertonic_total_steps = self.st_steps_spin.value() + self.conversion_thread = ConversionThread( self.selected_file, selected_lang, @@ -2347,8 +2505,11 @@ class abogen(QWidget): total_char_count=self.char_count, use_gpu=self.gpu_ok, from_queue=from_queue, - save_base_path=self.displayed_file_path, # Pass the save base path (original file for EPUB) - ) # Use gpu_ok status + save_base_path=self.displayed_file_path, + tts_provider=tts_provider, + supertonic_language=supertonic_language, + supertonic_total_steps=supertonic_total_steps, + ) # Pass the displayed file path to the log_updated signal handler in ConversionThread self.conversion_thread.display_path = display_path # Pass the file size string @@ -2425,9 +2586,14 @@ class abogen(QWidget): # Store gpu_ok status to use when creating the conversion thread self.gpu_ok = gpu_ok self.update_log((gpu_msg, gpu_ok)) - self.update_log("Loading modules...") - load_thread = LoadPipelineThread(pipeline_loaded_callback) - load_thread.start() + tts_provider = self.provider_combo.currentData() + if tts_provider == "supertonic": + # SuperTonic doesn't need KPipeline, call callback directly + pipeline_loaded_callback(None, None, None) + else: + self.update_log("Loading modules...") + load_thread = LoadPipelineThread(pipeline_loaded_callback) + load_thread.start() threading.Thread(target=gpu_and_load, daemon=True).start() @@ -2743,6 +2909,16 @@ class abogen(QWidget): def _get_preview_cache_path(self): """Generate the expected cache path for the current voice settings.""" speed = self.speed_slider.value() / 100.0 + provider = self.provider_combo.currentData() + + if provider == "supertonic": + voice_to_cache = self.selected_voice or "M1" + lang_to_cache = self.st_lang_combo.currentText() + steps = self.st_steps_spin.value() + cache_dir = get_user_cache_path("preview_cache") + filename = f"st_{voice_to_cache}_{lang_to_cache}_steps{steps}_{speed:.2f}.wav" + return os.path.join(cache_dir, filename) + voice_to_cache = "" lang_to_cache = "" @@ -2847,6 +3023,12 @@ class abogen(QWidget): self.btn_voice_formula_mixer.setEnabled(False) # Disable mixer button self.btn_start.setEnabled(False) # Disable start button during preview + # For SuperTonic, skip KPipeline loading and use SupertonicPipeline directly + if self.provider_combo.currentData() == "supertonic": + self.loading_movie.start() + self._on_pipeline_loaded_for_preview(None, None, None) + return + # Start loading animation - ensure signal connection is always active if hasattr(self, "loading_movie"): # Disconnect previous connections to avoid multiple connections @@ -2905,14 +3087,25 @@ class abogen(QWidget): else None ) else: - lang = self.selected_voice[0] - voice = self.selected_voice + voice = self.selected_voice or "" + + tts_provider = self.provider_combo.currentData() + supertonic_language = self.st_lang_combo.currentText() + supertonic_total_steps = self.st_steps_spin.value() + + if tts_provider == "supertonic": + lang = supertonic_language + else: + lang = self.selected_voice[0] if self.selected_voice else "" # use same gpu/cpu logic as in conversion gpu_msg, gpu_ok = get_gpu_acceleration(self.use_gpu) self.preview_thread = VoicePreviewThread( - np_module, kpipeline_class, lang, voice, speed, gpu_ok + np_module, kpipeline_class, lang, voice, speed, gpu_ok, + tts_provider=tts_provider, + supertonic_language=supertonic_language, + supertonic_total_steps=supertonic_total_steps, ) self.preview_thread.finished.connect(self._play_preview_audio) self.preview_thread.error.connect(self._preview_error) diff --git a/abogen/pyqt/queued_item.py b/abogen/pyqt/queued_item.py index 3c134d1..859d069 100644 --- a/abogen/pyqt/queued_item.py +++ b/abogen/pyqt/queued_item.py @@ -26,3 +26,7 @@ class QueuedItem: replace_all_caps: bool = False replace_numerals: bool = False fix_nonstandard_punctuation: bool = False + # TTS Provider fields + tts_provider: str = "kokoro" + supertonic_language: str = "en" + supertonic_total_steps: int = 5 diff --git a/abogen/tts_supertonic.py b/abogen/tts_supertonic.py index ed37c81..c83ab11 100644 --- a/abogen/tts_supertonic.py +++ b/abogen/tts_supertonic.py @@ -4,6 +4,7 @@ import ast from dataclasses import dataclass import logging import math +import os import re from typing import Any, Iterable, Iterator, Optional @@ -15,6 +16,13 @@ logger = logging.getLogger(__name__) DEFAULT_SUPERTONIC_VOICES = ("M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5") +SUPERTONIC_AVAILABLE_LANGS = [ + "en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", + "es", "et", "fi", "fr", "hi", "hr", "hu", "id", "it", + "lt", "lv", "nl", "pl", "pt", "ro", "ru", "sk", "sl", + "sv", "tr", "uk", "vi", "na", +] + @dataclass class SupertonicSegment: @@ -166,10 +174,13 @@ class SupertonicPipeline: auto_download: bool = True, total_steps: int = 5, max_chunk_length: int = 300, + lang: str = "en", + intra_op_num_threads: Optional[int] = None, ) -> None: self.sample_rate = int(sample_rate) self.total_steps = int(total_steps) self.max_chunk_length = int(max_chunk_length) + self.lang = str(lang or "en") # Configure GPU providers before importing TTS _configure_supertonic_gpu() @@ -181,7 +192,8 @@ class SupertonicPipeline: "Supertonic is not installed. Install it with `pip install supertonic`." ) from exc - self._tts = TTS(auto_download=auto_download) + threads = intra_op_num_threads if intra_op_num_threads is not None else os.cpu_count() + self._tts = TTS(auto_download=auto_download, intra_op_num_threads=threads) def __call__( self, @@ -191,12 +203,14 @@ class SupertonicPipeline: speed: float, split_pattern: Optional[str] = None, total_steps: Optional[int] = None, + lang: Optional[str] = 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)) + language = str(lang or self.lang or "en") style = self._tts.get_voice_style(voice_name=voice_name) chunks = _split_text( @@ -213,6 +227,7 @@ class SupertonicPipeline: wav, duration = self._tts.synthesize( text=chunk_to_speak, voice_style=style, + lang=language, total_steps=steps, speed=speed_value, max_chunk_length=self.max_chunk_length,