From 81937b7a40de815c6dce5c7e5ab9b2ab4ffb2ccb Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Sat, 23 May 2026 21:12:05 +0200 Subject: [PATCH 1/7] 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, From 08bcb3f4924334d4c66fe6ab92f3916661e1deb3 Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Sun, 24 May 2026 08:07:05 +0200 Subject: [PATCH 2/7] Refactor flag icon handling and update language code mappings for SuperTonic and Kokoro --- abogen/assets/flags/a.png | Bin 571 -> 0 bytes abogen/assets/flags/ae.png | Bin 0 -> 992 bytes abogen/assets/flags/b.png | Bin 786 -> 0 bytes abogen/assets/flags/bg.png | Bin 0 -> 877 bytes abogen/assets/flags/br.png | Bin 0 -> 1570 bytes abogen/assets/flags/cn.png | Bin 0 -> 1227 bytes abogen/assets/flags/cz.png | Bin 0 -> 1051 bytes abogen/assets/flags/de.png | Bin 0 -> 832 bytes abogen/assets/flags/dk.png | Bin 0 -> 1046 bytes abogen/assets/flags/e.png | Bin 521 -> 0 bytes abogen/assets/flags/ee.png | Bin 0 -> 868 bytes abogen/assets/flags/es.png | Bin 0 -> 808 bytes abogen/assets/flags/f.png | Bin 372 -> 0 bytes abogen/assets/flags/fi.png | Bin 0 -> 1102 bytes abogen/assets/flags/fr.png | Bin 0 -> 883 bytes abogen/assets/flags/gb.png | Bin 0 -> 1655 bytes abogen/assets/flags/gr.png | Bin 0 -> 1235 bytes abogen/assets/flags/h.png | Bin 465 -> 0 bytes abogen/assets/flags/hr.png | Bin 0 -> 1513 bytes abogen/assets/flags/hu.png | Bin 0 -> 885 bytes abogen/assets/flags/i.png | Bin 381 -> 0 bytes abogen/assets/flags/id.png | Bin 0 -> 855 bytes abogen/assets/flags/in.png | Bin 0 -> 1278 bytes abogen/assets/flags/it.png | Bin 0 -> 917 bytes abogen/assets/flags/j.png | Bin 441 -> 0 bytes abogen/assets/flags/jp.png | Bin 0 -> 1233 bytes abogen/assets/flags/kr.png | Bin 0 -> 1574 bytes abogen/assets/flags/lt.png | Bin 0 -> 856 bytes abogen/assets/flags/lv.png | Bin 0 -> 837 bytes abogen/assets/flags/na.png | Bin 0 -> 1196 bytes abogen/assets/flags/nl.png | Bin 0 -> 875 bytes abogen/assets/flags/p.png | Bin 617 -> 0 bytes abogen/assets/flags/pl.png | Bin 0 -> 843 bytes abogen/assets/flags/pt.png | Bin 0 -> 1210 bytes abogen/assets/flags/ro.png | Bin 0 -> 875 bytes abogen/assets/flags/ru.png | Bin 0 -> 891 bytes abogen/assets/flags/se.png | Bin 0 -> 1060 bytes abogen/assets/flags/si.png | Bin 0 -> 1166 bytes abogen/assets/flags/sk.png | Bin 0 -> 1299 bytes abogen/assets/flags/tr.png | Bin 0 -> 1125 bytes abogen/assets/flags/ua.png | Bin 0 -> 851 bytes abogen/assets/flags/us.png | Bin 0 -> 1331 bytes abogen/assets/flags/vn.png | Bin 0 -> 1076 bytes abogen/assets/flags/z.png | Bin 431 -> 0 bytes abogen/constants.py | 51 +++++++++++++++++++++++++++++++ abogen/pyqt/gui.py | 10 ++++-- abogen/pyqt/voice_formula_gui.py | 7 +++-- 47 files changed, 64 insertions(+), 4 deletions(-) delete mode 100644 abogen/assets/flags/a.png create mode 100644 abogen/assets/flags/ae.png delete mode 100644 abogen/assets/flags/b.png create mode 100644 abogen/assets/flags/bg.png create mode 100644 abogen/assets/flags/br.png create mode 100644 abogen/assets/flags/cn.png create mode 100644 abogen/assets/flags/cz.png create mode 100644 abogen/assets/flags/de.png create mode 100644 abogen/assets/flags/dk.png delete mode 100644 abogen/assets/flags/e.png create mode 100644 abogen/assets/flags/ee.png create mode 100644 abogen/assets/flags/es.png delete mode 100644 abogen/assets/flags/f.png create mode 100644 abogen/assets/flags/fi.png create mode 100644 abogen/assets/flags/fr.png create mode 100644 abogen/assets/flags/gb.png create mode 100644 abogen/assets/flags/gr.png delete mode 100644 abogen/assets/flags/h.png create mode 100644 abogen/assets/flags/hr.png create mode 100644 abogen/assets/flags/hu.png delete mode 100644 abogen/assets/flags/i.png create mode 100644 abogen/assets/flags/id.png create mode 100644 abogen/assets/flags/in.png create mode 100644 abogen/assets/flags/it.png delete mode 100644 abogen/assets/flags/j.png create mode 100644 abogen/assets/flags/jp.png create mode 100644 abogen/assets/flags/kr.png create mode 100644 abogen/assets/flags/lt.png create mode 100644 abogen/assets/flags/lv.png create mode 100644 abogen/assets/flags/na.png create mode 100644 abogen/assets/flags/nl.png delete mode 100644 abogen/assets/flags/p.png create mode 100644 abogen/assets/flags/pl.png create mode 100644 abogen/assets/flags/pt.png create mode 100644 abogen/assets/flags/ro.png create mode 100644 abogen/assets/flags/ru.png create mode 100644 abogen/assets/flags/se.png create mode 100644 abogen/assets/flags/si.png create mode 100644 abogen/assets/flags/sk.png create mode 100644 abogen/assets/flags/tr.png create mode 100644 abogen/assets/flags/ua.png create mode 100644 abogen/assets/flags/us.png create mode 100644 abogen/assets/flags/vn.png delete mode 100644 abogen/assets/flags/z.png diff --git a/abogen/assets/flags/a.png b/abogen/assets/flags/a.png deleted file mode 100644 index f770fcfff221beaa6793b6b32caadebf7a96ceb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 571 zcmV-B0>u4^P)MdwD|8i`OT_lb*zAMr9fG&s2ua!d)hlY}ckh^; z5A6cBF8}0Y7V_WNn3XKUYdg!f67X#!fUvL|JGo~;BEhdllT6|MSDf>1Y=kbp<|rE6<@!f zW73?fuh7H17Ec_Y1Q8U!e)sX-|NsA4Ff6bQ+8RzM%)`)V^dN!LG-byW;ccf z_5oXef;5qWzu*X>&rW7PF)RRi3#XUglZfAaxP@WChnZ7*aC!Lysd(np9t;aW1sCpA zVMY=LnT27&|NsBl-n{#GhtlAA{qEzvTmS#FGhhR+PaX56GV1V!^9due{&SnzE5#u*{yi0i>2_ zgHe1Fs>Sx%#WW05wtRLnz4zJK^cxsEncjnlnT3MXkY)Ig0RSvjwUx;I93}t&002ov JPDHLkV1m307Zm^i diff --git a/abogen/assets/flags/ae.png b/abogen/assets/flags/ae.png new file mode 100644 index 0000000000000000000000000000000000000000..64f87c67e0f96fdc6ae0d91fb3bef2ef2f442982 GIT binary patch literal 992 zcmV<610Vc}P)3U=3x&}t@`y!Y-E1u zZUPWc0oYWCPeha?TWe_wvfq@=$(KHU^i6Nv)0G8#!}zR@ z9E|AZ64E&V45`hX5q_E;0Yk~WxZNP=RD?kk7~8UkXP%!wH90wHvart-M(;+Feg|^E z`m+L65+2~^w|D0P@G+5erfqU9(&FCtrq2HmcyMdeBW21RtDsfDba{o?Zh(>pAj8lk zrHv|K-?g>o8w96hE*#y1B>7f>N=JI}X(F`PsBXj6+>$GQouI`1+Xo*G zB(+b6kiHmnH$)c^q)aeaxeHJ3JqlASfY!D1GYW*P71+1Rvw31o$mMU_z({ zAoBxcQP41yXu@l7U+S~*76g#KzLsj5hl^B0G*i~qT7S{**aN+TX;EZlT-gXeGnhXyQ@5}DZ$*wbH%5$r5*;C;_Uy;O7_M*$RT&-WUHi)#K2hxX&F)98viAM-}C(#KHjrQPh`fu7asX~QmD!vQU#x|GW(qXh{zlr+!}@!f6Rzj!lKuQO8V z-4bXK4K-Ih*1~TGCDM4A4BSrW5hf@K5A#|~}Qq{S)q@;MM+C+;y z&qGGU&l6#HMRjky5P_x5`(-_u`mx;gz{8aiG4AM*2>>Y0A&5WkL0l;zuo_||+TO~5 zD2kXxlH>r(C#bCi<37PT=U*9G|K^+GkLi5)JZRL9iNWA60647{o^~l5o)sGIxx?l< zX64n(p^ZPmB8P98-)9N14FFT(!h&D8ZC3C}m(#lm|0stbVr{lXnj2k_mZtuwwT0QN zkw%LnScQ7C1lY#7NK>Oz4U2u{5a0&NI#5$qLojM~9&F-&6RhSC2+r+B(GCI~;bWYFw=ao$q~I zQ)$vbckF0fXN5#|q%4zd2uMWDkjP#*EcpblL%bIAtxWugl3K5NLidgQ0eh-}ik4$O QZU6uP07*qoM6N<$f*(Y2q5uE@ diff --git a/abogen/assets/flags/bg.png b/abogen/assets/flags/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..6c79a4244723d813bd34ca8090496d322602a3c5 GIT binary patch literal 877 zcmV-z1CsoSP)XrNC)_(_Knp$I6_f~YJc zOcAOyAq_fDJ=&{o0B2*a>y^E^)h{E+AQXRTIiWmx2iz+hl&YwMjTioT5F z_*9yvu+|QYNzu0zZM1KU;Ur0rW!XPjmVGcYGxPO9k->!O#pdSbl_W{Nu-2-_R_6~$ zk3wI}g<%-fYBi*3dTn}o`g)&JXh6QRvvav#uP>El>78@xEKHxTS++3u^Y)=MkZ@;m$aH;uUd z?%r3QA$Pj=Hi?z19*V%bJ z_j^Z*67y$41$M_3`-A9HW}4qMGOPuE=oCB-L?1(1-G6du z=`Qy-&^Q)C`r{#n2!X>r`nKJkKe@;EL!sF9(sOJb28n|dkDK&HMfMlkOEJvl$MBAT zV;vO&xF*`Ht)yQV)U>FstmoQY9*__u0_m24f9+hk{($x9m@n2I zLF7tZh>So#UaM}zuQvi83vns9s^rUhpJ>dhngIL?PG4(+KkY(b;VnRFK>uUYI0gp| z5cc41oWbQ>mcVz;sB7b1^{Rrk}13a-02Uo4Ua^Ud&-78qpV>#anz zzlzyc1ay%>=RME{5ciXrGh|&r_W*o9ht>PTa|iert`mxoz`#b>00000NkvXXu0mjf DHKC^y literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/br.png b/abogen/assets/flags/br.png new file mode 100644 index 0000000000000000000000000000000000000000..9cb11fb5cbb28dc3bd3b16e0cb72d5b6c11a0c31 GIT binary patch literal 1570 zcmV+-2Hp9IP)%-k_t+qn49rQ zVq)wgRY5)o(;7NaLPCPc%%o}>gnppZzR(HGlt)_%!_3f`xp&TEFSF0RGmIK{a_&9Z zd+q<;>%Z3euOs}|=sDbD`#1Mz{(X1f`cI|8R}tNS$|_VBsUW~4@J|sLFi!roXWPK5 z-oC75(<{AhA)Ys$ALVk{^!E0;dw+N9^ljOz(iqxwP}(ZCeYW?MqYVJQaTPoOk|&nH1U!wf9R^C{_kd zzEpA#5?8aWU&y$`X;ie$R_zb-xt)hLuCbc^U5qrjOl$nsDd{M}re(rsm&U|XMb2Eh zB9HE>jt^*PsdDl{NdG8YtU$@uJn`#d@`Rf;m@fo@%Qo49ZBF9UStG{Z%2V>0HrTve z=x9ypKXwB@A6Unkv9|o0tJ)@cxceox-E@kjrI5p;$}3|~uwV^jPBkn9#Vk(m4jj6{ zsH%5!HUlg0m22S3oq{dI{Ql_meDmo$z*B6%@lNBbO5XdS;S8pLmZ8 zZ#h;ho8slyz(w&=^gupDdN|a45lX%WOkFW6SuLwJ zUkhulj=4P1$jOWCURhXk)%z@Jn_~Fgh(jar&I~lzdAaaP=#@5>%-oWL-QTyquhUO2D0RCv$lg-(E_+ zpR*Z1jjQP$HZ;vy<{2liXrEPn-}7%P=ca_amq&RaJ~Ilhe4zQQqXppVOD8ZASF?&$ z^P@bHUbpvrWALoY+;?nXd>p-c(DXmY!cUL;4|abJq(?;7<5-DMsuNyC7*^FGajt*gjw3Jrzt5I`1JFp} URX1$V?EnA(07*qoM6N<$f=I3v%>V!Z literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/cn.png b/abogen/assets/flags/cn.png new file mode 100644 index 0000000000000000000000000000000000000000..18ee3094cdc55937461c3860ea06db84706579d9 GIT binary patch literal 1227 zcmV;+1T_1JP)a(ERb4$KooECND&5tc2}KC%#)TR%ilT`Vq8q`u$tD4V(SYE96HeeN z8A9-jy+`hSRozu>1P`xx@76u@`Oi7R3i=qL zhrs&pDwWNG-ml0uK}SIb6a@4V$drMvA^Nn!&&NE!f)Q|?`lR#FX84IpB|-NovLiwI z6lkJLk_L#8@UEk?pr0>bUcG_I6Dy4@8E9WjF!Fc-^HvcvG>0Qa1G8kjZGP1t0uh)J z5%btu9rw&8P95wt5(L_RoM3owiRewApC&E^iCT)gY01peV`EI5N0`MO*vQmNEuAWD z4tvnYb)@*H>Cqa1m&NiDuEKHZXb6*V7x22{v4x!yb*0fhMX!x!0g8 zwD^mHAW0TideI`{{aiMZ3vZbNxn-E@Lt0GGG6iLQ%;p|eIr7UX*8fPqS-ch(F=IZga4DIg2 zIoFvg4{ufEPTwz;hFU4!Sdj8vMQqKIY$_m$Vbzm;X>!<^; zU_egBVpN?v6AOKJ7pc8{iPI1K$=SWH0`(# zy%h@eYwXZN25$BVjd~-_y*tg^_ltq?{P;8rKh`lt@3QaOFhMXin;GTgap1F3=AScRc(l6Hf%tP<%DFo^r@x;FB??j;xez(?8Ce?e+~{9peB1002ovPDHLkV1k)XPdfkr literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/cz.png b/abogen/assets/flags/cz.png new file mode 100644 index 0000000000000000000000000000000000000000..310cc93b6c312563df70054fd1f805c067f34dad GIT binary patch literal 1051 zcmV+$1mydPP)r$^(J>JjAfg^sB~`65^Ma}tnfYpOZ|^O$S5+n=*~~{<*vFNT zCs|xvJe14jMv3TnCX+cJB8cM{_B1dvf*?qrwOZ{V5nZd->l6L`{WIGIS?3;C_5my{ zEuE@Vt4m3eXr)@!QmK?Qmz2w8Z7ylOuT(1PoYR$+mHVYq>Ft)17O%fq+Pk0Md_R{9 zFGCZQ?^hfXJR;gK-mKhU8#_p~T1|Z4?+L^3v*qRG$B5{BlT+E!V&uWe>!&{bZu#=r zFYk(nL_8*6u}-oZ(nv(Ub53ftTGHL!{gEj#&O)KUMnD~Je*aWV$uA1@V6CC2PdvdB z!w0e2NFWNNR{MaeirtN>t~%#l>g(&9HTfEIG^T#G7vh9-0Z&ft?_~V!ue)2ynvD~X_gPC7um+%Sr zon4T`&Ph-}px~^J$?1EKq(nqCm&;{DWY~x5F=(5s1)+a~7*ZwmBsJah>J@NttSWNU z%JdM#k?Gnshys`1I*3(a+qQM+RuI6B0P5^`+5+^X-aDK`Na1BYOkTZ*hyz@Fr3-Q7 zEYZ6LX-3MWjkcFu*iTo8^2cAGH2MP^1y+;T4}5^C0D5;);i)1CvKYBj#JRa?)a??j z)b>SBbq4e<*f)0Bmb@d2k@+I>zfYk~0fah1reK$)6le!^0PO$CAS!~G+9hw0&lQos z-6U56S+AJw?n})diW}^+eubp52KG#~?nJIezEd%@fh{+XNId@v$5XG}BIq^vS+WlK zzvK!{LNE#Ow}Wox=X4YYo_C&H?E!fKB|%mmogC_es76{-ALj^IAJ@^;t8+e*_t?+y z$b3=rw+qr}Lx|O`m9Nc_ErHS+NmBq0 z!Kjn2InNs#NM=7#O=n~JecM0%h2ZcJI2}_YuVf(JsKBqE}3g#8PH zZsynKR8PrUw(S>{PG;LLWCvf=2q#JOr~uE9YGyqG)d~8CGTlJnO%2p%e{A<{;UC61 VKME^hvf=;$002ovPDHLkV1mm!1ZV&N literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/de.png b/abogen/assets/flags/de.png new file mode 100644 index 0000000000000000000000000000000000000000..f7521cdef9f09febcb8c47b794a947b1468d1b48 GIT binary patch literal 832 zcmV-G1Hb%pQOy2iG!50PRtk`jWc>MJw5I6FIg zb8V2OjMhPtBp1B*Qvfz+S(aH-vb5G(F)Yr5P0MyzL?)9YxfZx8l}hv}40UX5?6h<4 zmIm3YK|4Y4GZArlo}ZnYo0|+Zp#l87T;8@M@{r`Psu>AsCZGc_)Cn3T1B<;`cHmyE zHt)mvt{%Bka(*OD)0|F1CwwTx%yRGD;A@Ak3^>K}Tlekxt7H$%$;MWfw0Bm=AYm}P z*_7s=`ExIpzTBX0a2cu$3eXi;KcrM=e#(2r(Qr`6Ubp-fLW6tbVGm$u3XR3?7C8V( zn_aS901k)#>LPhWE7b#?_Ik$mgM~DQ&~-eA z3Iu6tIfi;1meV`3rFz^$LHvmc9ZZjja1fa)$Peo-GldgkIbA}XUt4BjI913`etjaQ z7f=$y(?HXeS3v&v(&)Rvh215Vr+>GfPr8_Ivx<<{)#k^qVaR`jw+dvI#W$A!(ZgY^ z5h1l60}gnVe#<@8tAP`^zA}PBZG?o{BGKP81JBOP)~1TIXl=+ zUYNGkshO_0e=@!wjwb;ehT0C011AnjIZG^yrYEAe=7nD*^3XPGaZMREUUm^-HOD31AP%T{sPI$B^Ls0yp2^V1&3wzR07mlmma< zl@L-$Aixq4VE_o460%Ms67Oq)rdn%}%mA!bWWMmkUL$hOK}55wo(&@Ihu#SQbr6CL z{*zYJm^co2^khrvCOs>BLo7mDhe@~viG1$8k4UrAY@UPVq*AZ>pHiNI2e(8RZb8jkZzjA z3;`r=L;M}U>w({b0Z?6B?QZX;U z!N}q-8?%T0N@7RMrvI8G9l+(@Qn3I;Q6A2HfU=k2ze-*D9`27ngN;A?`YwAJ39@!u zbL>5e&m4l;^Kf?5nY{vceKC(@YZB$@GnCln)02M`VfJ^BC4qZM9(m|<*zrdJ5hT;! zBcA>egyAhESz|3^i4Hd{7a#b^r2YU=$g`Z4X#|Sa8!eSfAX5i%9SDOBsEtU7LPWDn zfiEQDV&jG2gTM!5t0$R4WzjS3sebU%s!YUpou_ZOkzTtF(05gkCVkVe*bW5Zt6=h= zA4lEXm8>;TWIl*VR-!yN+cqMASP-=l+L6VSik-FVMt8oI5}zu-Xj4#ewqqn{i!Ot? z%Y0^Ux_EU4FjlQ(Ga)Q|vB1q;HQ}iwt0XuPG1~73Ij#ekUndGDK&^os^a6&c@ zdJ||YES~+_@EZ(S87h{ov6e(6h-}RGx*STZ4%EShiQ-jDZ;hjMW^ITmuXgAhxsgd8 z^>gP}&^@$TDb9pAX;(`XF4}0*`>y)}h*XY1lCF?kN{xN8>E=Hb$;o?tVAEHH4t{Bn@T3R~LuGf#H?e^gS(_oQO?mZ=rn+a literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/e.png b/abogen/assets/flags/e.png deleted file mode 100644 index 693cf2a8f10cdbd63aac2bc64862a532c99e8c19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 521 zcmV+k0`~ohP)FQ4~I@H$#f?vo%E)n)_0stYo2Bk$)o=HcB>@ zR(7)UTOx_%Co_nlY=jv<3&P3<_YR{JQC5rveY$uX9y9YEic{a_op-?&f&RH6-G8 zF&2-j5sgXcb?_jQ*Gmc5>`%i~SsYed@~}11iJgf|TzYJgbP&`l5R}QYVLLWjbJ1O& zg7)G9Y>&-jyQ2_d$R0rl*1JzfbTMwn?mz)trEzF4%Exk*4F`k8I3Kf{9?{E^k3FgI zPGn=DF$1nPCsvxW;G4|BktfyUk~b9Rw_Q`OQvLfs+K)?x7E%>Wh+QXzDc8M5loa8} zwHDGuq>F_les#JsG7{1bIU}b~X__jA{&9=AVgYNKqvnGrZvk00000 LNkvXXu0mjf^{(8E diff --git a/abogen/assets/flags/ee.png b/abogen/assets/flags/ee.png new file mode 100644 index 0000000000000000000000000000000000000000..c52311d97d74407ce0cda28083db67d29f7661e6 GIT binary patch literal 868 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 JPDHLkV1oZ8rqTca literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/gb.png b/abogen/assets/flags/gb.png new file mode 100644 index 0000000000000000000000000000000000000000..19fec2575770d8ae35249b5220446bae6a5a38ab GIT binary patch literal 1655 zcmV--28j8IP)dR4*6+Zf@>xylqs;uH~n^p!sCUvW`O-rp@}_+_JN$gP`TdzuR4fpKrax zylc)7QFJWTWa>>Nz!@u0n7nPIl`hKtZuLu#+nJ7(@NLVuh~5)I4vI-R*32wLCmGWNJ})7f2bR!(+k5=sj6|&2*YUYb@J!{q+I3y6wC?i`KocR_ z3tSDs(|zB1D1Ob*=9gY;EsmU5R$lZQT^Lj_rjN`jTRb`MnwQrXju>JG7eWdZ3BfpH zjI+KhTr{&MIOp(uk}-reKXI}quC0CkXwmPNqI!j{LM!p=xd=*Fs2wda>*)%dAN?a_Z+;qE-aWw|S$BX=_gWc&t(Tdk za=Lfxt$vT12JMmC-pPxbJZ$+xHRSf)I5qD{WYdHtjl&S!+K+x2pxh9`d{WDRj@)JUJ>m@hgwn8HN!;B)E11Y&aIjjcaRhdv!Iw z+^`v0i;Dn#k$EsBS6y9D(}+5rKm?j3>ku~@?wM(A}7be+iUKE6buyQlHfQo z@`7MIGYS=!9IP}&%rMC|zb5aB89)vQ4u8)F@b~{a@^9Ue3+KK&K_)29Aq4AJa(@o{ zLWK^)UiKdhQ-p6z{BmY87$uu!ECj*-#`9DXTsp(RO~CjLM)?0sX3l-ElS?^BX&oU( z$o-HeZKk*^=D^?kA%cU4qkZe9Y}j|_0ilX0%ZZ}G_vODodd5IHbfsOl@<^I(k9IxR zd<-|v4Pip1k>PMaTING>ZqX_P2M_cCjLSpT^Hor1&O!{%M33g+o(Exx2!u$J<2cOq zgOB9{<<0MSC;?O-o8;NX%c zP_~alpX~y7ENDd}>=<+4?UozJAs(Zpn80`;TF!`ox0ynmyL*}F)5hd_~=M1 zo^~_ZOP_*h*nz`KpTKz%5Tv6*n`N44x2Ms1 zF-B%M%(*}3g*z8jUzOR^TD2oti{G@c&^z5JoZ$(Un*ISZd$wfI0R8&e!%8zQi(XiX zV?`A;56>C*%)k&cGntY|5-NxfPqLNUHZ`3oZMAAIbh-(a=9tE%iD;maUJ{-OSrakI zxi8XW>84{n35%8#Y+kcI8o!KD5~(dB$+SV8GH=3jUmg8yoNb2<6lzykac($mgp(>F zvnQ+=C#3_6yNwqv^xR#LpS>P#U?Lyj!#Dp`n9*rQC{ND#R|phf;v1ta@}~PboIpW1 z0zf>9UWcw1hHWP9VJeERYzlep z0EA)S*iRDUmkGWLXahJBczn>H*8?;!5HP3NIF5rj<;JF&fT=p+xz7}?)d6=Az}_o7I^=>8WDi2VvMW)^~RX zK(~{xoUG;=ST!xsY(%&#U<4jzW84%VW#tw;X_`-~-CwD#P=ZaWV3;uagvN%I410>B z;{qqo3j|%c$bknO-k!p4!j3x>{uhMjwm6FP!>Xk*2u=uXYOE#g@vVrqwKUMB^Mt7e zdOV=FE3@e>03e8Bc-_2Okw70=GTW19BV;04utZaCO-oT$B66V>+5Z;SfyF^YKCM4g zHS_mkX|PFcSpIFI{6giktVOLRH=%z~D1c@`q6p5*MGlUQfaNOmYErMv-xmt`bT)&4>!zMMrZ8q)s0aYV> zLvH`L6&c@NaNhlSB6>15eh-_(5QJE|Pi_QT?xi9}#o=H-?FX`QZN&2r&~GdERd<() z{t+Fsp{ycN+pJ3qwq=ebE6)3`C+WD_#S zbxxWy9M`9_mNaIV;~KLLxm7E-I4qxFo?36=ZF0*>C+FhR+X?Ll`_f<6H@Z^nR8l;` xY}Nqq+pI)H;(}rGeb~bBdsg+IUbwas{{T?~e?_ZIk=_6R002ovPDHLkV1nhpMdbhh literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/h.png b/abogen/assets/flags/h.png deleted file mode 100644 index 015ec29ca282a4038f85e95d4b171fc7158bc488..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 465 zcmV;?0WSWDP)L^VB>jhkHrW^6Iu%+`^xsT7n)_@X+&pST z;4R6!Ubl;Qw9uU3jzaANp~D;BP>?pawRx!r9vJW5@A+--%LnkUg&oLX18z`1^n!T7_JpA83SP>T_c8%S`t4rd9% z(X|HW{^3nXqXx6Y03D_%!=VSTGAo6z=ac?EyfP~V4}dZDz!<|_UM!Z_ZVz4C_Ht+6 z+HDWOah%tn@Os@q)3neqEL2q^5W=(k0M4!ht=13?V}_;D0L$fp3*xl^fa3@F3<8*@ zwFQ_30vKZ-et>b%h*Z^Z|H3rqQARZW^^$FS>3((WC3i=?qWp5(--o-SUeN={HnVAg zl6R33OlE14m-F6>#+9p*)1-hSdhESq+Y+gy#0Dtj4nieEN>C&k15gEogK)rRTs2aR zv=c>|@?MA|zIc&J*8(Mv0wr&vLKruH=Czzcu6R$Z@IRw(`roYpl|@}v00000NkvXX Hu0mjfQnk%r diff --git a/abogen/assets/flags/hr.png b/abogen/assets/flags/hr.png new file mode 100644 index 0000000000000000000000000000000000000000..0712ad968b5a6c3aea7d833cad5940a47bdd1025 GIT binary patch literal 1513 zcmVWge(v4Kf!NSCd0vAm2w-@1j#@*oE`Zs+X4O5W~W z#`izHol4(QUi(BfJ|Hk*TaxmfDQ|~$_Svt6azSaf8w<`2~uoqLfg^Xkob$r8%Kz$=$KEHI+ ztxSwv8V;ssbaK3sOjRZ{-IvF;TB~X0#aDHdl3v`gU$3g_*=2X=nSoEH=dnGH=_m!Q zjQl~X^}0@78t>>m^Hc>Vp% z?D`?o2X}MBi8nCac`8HeC_VUH%0JypQmv*AE!0U7M6jxX^SIYq+p#{k)gt*E$+-(m z-1J!pS`40egZj4*GJXFpZur|VOsT-c&_?2aoJ9(GScH+96v;alEgna6E_9R{&CYEx z4`BqY2q_j)09y}IXXaD$9?L~AZHz%xQ>ApfE@-y!KD>e@7mq_eN9Cq-z;*pohH5+sv z+C$^X-%51J$$omqXC4Dmc#QSyr1ec;$F7pu|hR;7^U)4AyC zWR=A8=(cTkjs1z+Mqeef^TCw9(IbcW`_S#|U%rWHlf_G7)J?QG5{bCm=mVw99sV=B zH@wN_k)srP?jx)x81Xt3aPC;h<~S9t#S?Ln1Mr~rPwTjz`%b1PvS~bKM#5Z`Ydiu=0 zo0GGNTC2udlQlMub~b;r`J`^U$8(7ghE>=nmaiOr7_a*gtQ%KJ4B{>RtiH!mCyaPL zUKXOp4>)?~Zidyc&wa3*fCG0Ap1o^7adSJ!3|-xR+V841ZIbI?9amh-i{KZ1kS6V5 zM+ba31y5UQULiFyY#6!o=S0y)qQ=u$4e)aYFXMQ%u+2%Ixp(eE7B@#oTAyL$OF!v= z@0;(p>%;Ga*7;X(*K@aFxChK8(A7X;wwNu%oC97#+P~$Qt*>9FJG=e|N?Q6h8v@f* P00000NkvXXu0mjf(tYuv literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/hu.png b/abogen/assets/flags/hu.png new file mode 100644 index 0000000000000000000000000000000000000000..beeb302c5bba2634fe77c8219e97be3c7a5203b0 GIT binary patch literal 885 zcmV-*1B(2KP)<6r0FJk+{0zOa39szd{vZW)%6@gq8U_OJ+O=9t<{B#ovy-xbU z09uAY1NQF}d}9LFK3c0p)>}S)xClMzeN5ppKunkTj$r4p0W^ppsKL!l;_9(3C#6Te zi0uapkP9j3yaMS&;;uo!8Gr?-se!LdVClTiiI9}vMjw69)si0#WGwYs?~!~BT+o^8 zEJiyYYW#9J5D{L%18px?DH&B31rG@*qJ{}Bx^4?UrTpDg9i4&2HhGrcc$#SVwsMqTU0xibRFb*j`wFBGU zffBHxp%miXvb?!c`fADA?68{LJ33lNI^4_a&-g1Sh(xp0 z9yzi9o1drlO&P69l@(U>%=C93HyhbZ5E)}seYa<~qxwIF1DUF7R0vGFmcD!T(86iY zIXgS+R?}G_v?_3~eYX46Fy!$d4o2FE_0~D~BJN8)3HsU-@x%sM%QmcaZ=IQ5_@Gj$ zXti3+%W5^&p4@MoJ2d}cyXl^2H?l84CSe?C5cV~^0RErfBVDVVq;u_7_GCYKS9CqR zYgMV{P51iePd^68J`vdkhj9o>U9l#BUkNUVHM6H*`1Vr&*&hA@uYJ)W!dNh+W3x|Nk?*ceJY$cjH%;4DqU=I!fcSS(Vjvbb$Z*4R)+-d_jW-BM`sbE2EKnH=< zJolQ;YXF8Q4MHOik<|giO5^%FlWXt5bayF$V@f{uQRB-Dq|&!@l|D+P-;A<#dQ$b&$A}u0VETy=}A_+FhOp>|p-cQb(88b`^vCVw&{_Z{J`_4J{zH%bqxt zdcG6EV;QAM03~Py|2ZX)>gk0YLcS}LKe#dzn!LO(#rSJ{E5;LD$F5ko(shb12( zI*1W;ktWZ*>~G&RA_iZ+pHpUhKZ-{sH)sj625GP?PuhT7ObujuuYAc4SzfA}N>2IaYr0j0~S*zEd z^;_GfX@B*ez^e3-d0vz4?XABw0$0?zO0uHg<0aLlOtTc~+!a|}U7ZJTRaJ{vb)gCT zXd)!pbI!e>tyb#>$!{BthQxuwUJzpp&1Tbr{9$Tp>blxWlRVSubk?#gQ<9G?LXv5k zN~hD=6p_=GiN+YU-ERNky?-mub5S*ohZi9kMI;tQq2BuwGcz;4k#v@U5t_)S>+9<$ zi=uEzl9))%>_qC9<@%G@s}Bdn8}1*X#8V0=)M&U)h;f znx=5h8F5cUz7HXsnVXyYWe~YFy2|*zEjbji_vPi~!>YOf;0S<&0E&Ulma5*0G5!SL h>)F}atK+^y{0mI>hlxb3Z`1$)002ovPDHLkV1mZApnU)U literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/in.png b/abogen/assets/flags/in.png new file mode 100644 index 0000000000000000000000000000000000000000..a21747ba05d31af14baf28ca313212de20ac29dd GIT binary patch literal 1278 zcmVnqQo~5NH8Ki7{o{f5r0Tbu%bbt#sC^zmtZg?1R|oT zJ{b*SA8c%bKG5*Sk4764L=+(y8U+e1g|_>1@5~%Cv)j_$669o)y?5@J?|kQ*bIuSd zj5{D{C5(Umym=06yc||60dXt94c>_`2#8Z4bc5+zYP;Oy{TYG?c03-Hfu|uT1Iz7` zS^;bV+vTXDT3Ch;$>Vk@_fJ5?BOvMm)OKp?J3fjrA;4&KZlAmc!t6u>laOb?jyY2W zq2fA0AS58hRT$n#ZCBdI2ca_NpESOWkaoa$#u|XE%<=~2@-h%m2ms_MP3s1>I>I0@ z6~TqopEkcJL;VF}wk0XV{{UB-W6Vg9M66z+wSDc}VI?txumdxZO8f*t^)Md(2H@;u zEUMyjx?y70=L}?VH9wCw;kY6KN1aRG4HCJAc-R<0kX*UTGL$5UpcvQ23~elgC=Wt_ zU}mFgR4R_Dw*UuLQh$H{Yyoprs4%5<45A{xG2RT+h7wnE8h>5L`S+PqLp0U|7|NMa zHf4!I2+7t)1ra6}C}w^-tf{NbX0w(^lu6^jk_mzRKVC-W>$5m>so)W2G$pX*nLF^@ z{AT0|wmb*{+GpX$SNg-SYoX!8z;yQUFH;eEzhg-Dbz?{>i&thJT(f*oHy1h(Z|DF^#Yb3YFIaaAsa!x`nKKeCg?{hO# zL^LRbXd0gdfl!Do?wS%}W=j$uclF}+Wz&(WmVSNF1C7|#)r*!X)tGf#5{1}+DofVY zKXcAya4HA_cOcgXlp8IE(Ug+d_~djTw)pMb2qvWhG}i|>d2R&AX>43J9aHNSB3(Rz zvCOUIAaHX7XOuCfTL>|?tc2VU5`^KL#p-#@zM|gWe-`HlBCpTnx&R-ozZWk(G7ZCd z<9*BNgOySgMNu~m3=GTx_(-4_FFQ49qw7tu|oRROLLksl(Fl- zeDu8_8Dw;1oh+Je@xpP0gdiD&3Q^X4xaawUo73!wXV|f12cAx+#hw=qZPhufBf?N3 zsg%>oF=N--6Hxunx>HUh14)9iaW;O_gQtt&&aI4{|B%Vp4PPyO5CZR5@DnPKp&J9n z)#|uQUrGW=0bMXLfxb7y+xD*R{<(xPHaRDxUrNn4-G+USKg2d#NkorX;2wa(VgSku o&}qv!B&F=wz3PXn+a07*qoM6N<$f*sXQH2?qr literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/it.png b/abogen/assets/flags/it.png new file mode 100644 index 0000000000000000000000000000000000000000..93d02795154d24c13e6c00c6aedbbfefd401cb75 GIT binary patch literal 917 zcmV;G18V$=t2~wc7oKOolVSpGp0%2%e;5bk^3gorfDT@Gav8G+;^G#efNInymJQF zgwfei(;I(se&W%E#jBI8R_og^3>Tv)%EB;oi;I33eHYHpPyIPI{#ft#3h-t=yYZo3 zuXE!-!;Q~8H@M>3Z?)V%Bb9P>haro#P681H5y7w-D3pqOxK!HH{^PIlIXv)%4BY=N zwSKtdMF@~v1nLTIP(xmu+4r*A^utoAxCB-maBJPqm2tz@>iYcTe7fUzr1RA1;MnG(}IllMR%vkdQqK0jxN9%oX#XalZ^^X6>r`JF%jt$+e@ zGRC6V=jHlCeI4YpOQCyxf+dE&?mfk% zv06+Kpn17|f8>0V933?UlSf1Ko00m;j?Z#cdme{|^UDHrQ2%(N6#3f9*TRTWX+ zpf!@|84mX$3>~BfAEn$00000NkvXXu0mjf%&x}* literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/j.png b/abogen/assets/flags/j.png deleted file mode 100644 index ad5569e2dda699487c537899dcd2e8a795635767..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 441 zcmV;q0Y?6bP)OCRxrW6=9iLggRwepu&0f!Yyw_KL0;IOC}%#TdcG!}aD z7pM0=MUI=5-Wv&|dSwTnyXD)3_qUkB{U!}9>q`Yqn`Y63B}ov^7oh@HxI@aol=6R<8U3MyCZO1#z{(X5Y!AXf^Ox?mo;~26E5fL26N!QiY)lez*aTG;w%+AjKFezjtV0h7LwVo@L zN^jR{wPqNGNRnj8tc2FsnPp>H7S(DstgNi`;y8XW2!fMEA!-8rg=)3>R+1#5aU$;v zptq!4khX2xc3l@?7#?Xho5ym@)SoGMx7*$0y6&ksj*Zq@)TIfe?8|J)zHvc@h}iQy zD-6TEVu{341gOQu#d`pJLqzpBj(H56bKp`5s^nxxaSZeaHwh9U5*ub-PLkx|AP9a; z&CpsO_IoOZ zolfTgrPNoJW!1#@BcB_Fa!cT)5X;9;B07B*`dSBIz`Csg_o;2D9@+~E>fopk(3&Np zG&A>!=rLmEos~+(Pm)AjlW{RaIX28vh~Dl42;X@R$)DF#aG_+Oh4B3k(A)VuO#AQ5 zb;J0GZpKI~%Ep-8L_|_VYqvw~Jr~PwowBHAHh~K~w{@UIUOt_BsKPkN$_}kX*hBl-;5b$}u+z zXnk0rBkfBLlHcZGu6F=q13Xs!YYAX%m|hS1^3}{u1xMs{=!t+^7gE9G{~{g+=N2Ss z+LNAAI7#!|-f#z~ISo((Ow?RTk`JUoQ&X_-z7sr%Co@TwReAY~<2VwK%nx$WAHdph zJIc>&g)yNtobk$)y=}voCAiz4fW2`&AgMY^LS`vA09QzBeV(WwItP&4t{&b8cgv&T zxCL-ByaZUo#4Wg+x1jp!{;aacTL;^=E$eyJ?RGZdJqIJ$()hv zn?`1*)7j@Z&T-H4*2&1!T8lNsaXA8(MVguK`!X_Dk|a4aJw5&IaGH$?C<<9vSlH~k z?qO!$Ua3?nGT~(C=jERkk>t5$SpqyzN`0cWer;xE=DU&RnSh(yL~&rZ+wF&l=ve?; v0o*ISCFhBV=vQORX8=wIL2z;M?U4QfrI(l=0(`{Y00000NkvXXu0mjfwU$~` literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/kr.png b/abogen/assets/flags/kr.png new file mode 100644 index 0000000000000000000000000000000000000000..302889266c509b0d0bc483eb134a7a13cd5ecaef GIT binary patch literal 1574 zcmV+>2HE+EP)H zOcdg=!jHVc<;$1nnx^@NuIo>EJRVC3fowJldeh*X!!Qi>PN&o3LWu8DsZ?V{MMdA# zAU6W!7p`8t`kcq(*=<=?*|lre2v{(t%ovTE=4wFKbzWRtjPddD(R4cf`plU#KP?Qx z3&F$T@N1T3H5-Oe*3r@7WV2am7zWwUdf{nl&Iz2NkgaXo0n4)XMt0UVEgy)*IQd#g=w0uWm#L} z@%T%G+0xR&>B3kf61m4Pj9*OC4D8vnN1i=bg2&Yb+!lq4|P+3{2_(tvyz&L~y z7#bSFwr$%`U0sc(OPAupg$tslrUs^IPE1TpJThz6tbW?PWQ@ICQc_aV-Q6wDoja$% zJ3Bj-V^dQT`g+g9JIx2_H6gtoc&2&r*Pnl4$M)^0t*uqICr_Tlkt0WVPfw5Z`~7}h z*WXcliO1vf7-K!0^XZP`fN0sZYnK8)cI+6cmn^~hhF4)n;t)fZh#zS8R)Ccj<9O?j zxNG)Z=T&+`8C1@%g2&@g zzI4GwMMccE?ej!YOw+W|yoe}6C?kpU%t#W+#m~YC4S;FWAzdYKFs?z0321lDMxgsk zcxIL%Gd=;4^UaBduu?jJKps55K-rRZvdH{Bj4Q*}AkA`G0Q7vSGJs77uBKpwEQ(<${JFGK7QBv{KYpH^6nobxFz_LRN0{`L(|s4+{q=u>S)A&>3T+ zobwsCP{bK@-9*K#Y4F`yj*FKj0IvqPxf5s%$hZjJ=EdwgENFTWIJY6Cm`YKa_Vt%x z801OvzOGs!@aylr2+r_fOKlKv9UwIcCLHz99FtkR`D8h64;10o-+JLVHx*CWLd6ik z-`sWGpE&1?PC)V}EesA0V#l_v_;}xEShs8rKG|?T<^+9UOqC_veVZ2_uD=hj)K#MW zco(+5{~ki2kn$xA3P8aC{6uN`Ab>NRb00;|3vK1fl{k9z8?0Ed0vldlhkv8v2wh35 z9nKB<5G*m#e*84b%gb@`;y-9^Zbm~xgA!V<+D>Ykwpj5Mi9|l9f7;vIoz<&X%Yz3G z%D%on+1%VLYnCmOCr)js)kE|Ae^er9=LZfPpsJ#JNGuk6CC8JdGAbV=91d@xAY)@=bkuA-9v8XVas#|D z2-U8Ukr6?*;c!@zZ!{X+o^N7`qNy!28jY?uO>?K;@3)f4BxMG*%))kgOV89ajRyh& zYMCk9w%;l%EBh#iMQq-X<}E?O;qZgHuD{JWU*+|By{_x3B9`xJ6pQklQZkiFO$s4? z$Y!(KgTdgr92<1I%J_fkE^OGbSnMIr`O^TN0B|4GmYhw7b3Q19I0oQwX=&-1slNH~ YAAx6nP)B$FRP{{WFBOqwZ z+%D7oqjM+0Ud7}*a15BnU4Si+4UoH#-;wzr9wfeBRgr79S5Nkd_Xz&m(i|1J1iVf) zOyUk52lFmb1u_8zo>~*}Z^-RaIdN}o(8#Uih|M|5k(Sj?`fksP46P-pP~R} z7!No6_p?`$mS0r|k=?LtE?uA+Zjzg&P{hr{z}*Pq)Iwmpd|T#MZlruhZyv9ney&li z{6vsxa&yrk#d~naQf{2>qFuaLJ#lX>3i(t$yR1{$j232^b>$DTG@x3VYa=v{)WQeJ z`_*609G_DCQG!gw(4=%80v<1t0@iMp`cn4Qx-N%mXmr|z2@(@?C`byGd!?4uigxj4 z);2$%mF`8%E2DwORYXlWm#N4+g~eJNl|C9HQCh6++00?4|ACGQzI$QpK;<|LRNNn@ z;Eebb(v@tfE1O!;*dzCK!Nb5Emx{)A^@oPs?`kan*fCT3N3d`YKf=_K|ivo|LQ7q+lcAw<%!R=l^zpqxa*HpQK1dGuNqpEk$k zPraJ*DBt0O$2yndX`E8dMl8sL3aoV2#e z>^n8WXLWRPo02kxH94?bAZ ztE=b^;e@y;CNChgu(sgeIr_;0>p$-m?{f#wn~cQ!4Q5|(WLc5326M5u#ODT26@Tc+ i_rUE1HtrAZ?c-m@nTscL0Wrird}tB8i8C8eMU{(y?0rP@Uo zf-YQHcR>+xq2i*@P^P#Mv6Y(1o4N1aQ|{!=I3%JrnFBL#=5g;i-*>)uzwZ&-a0zhF z0Wtr+*t+-OSl7n@JSFfjq}kyj@+N^R0=}ZhXN#fxL%uD*2&73qPX0L!*eZ}dnwdN7 zg5CslY(~6G0BR@`j5m33cn#DBz|ZCVJ14{X&m)c<0weVcHos&_?-%013MsEl6Kf{} zN?FhVD`6Ng{Tb-qSWef@O^R@m{Jb4{y%64uQue48;J7Wze?ZoW2KW|;p5gS1se7j3 z6i4JA+x*E=Iv(0<)^4{St@@RwD3FSxV881B$Ukn} z_}zJbFvhqyZ?+-VL@D*WQksn+AU`~#lR6D3#@tf&WlN?R_zrlQ-E_{~JynZ1{@V1h z8eAzl?MLbmq-hmdNx8^X)xYY$efp)#z8@BkJfIFZ2anR!Otxy>NtTXAw3|VY&KOOuL8VGr%NYdcdbcj$7{=NFdAXkbY*b# P00000NkvXXu0mjfLMD{~ literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/na.png b/abogen/assets/flags/na.png new file mode 100644 index 0000000000000000000000000000000000000000..83b8744bc285a03baa68a778f5971ee75f4273e5 GIT binary patch literal 1196 zcmV;d1XKHoP)7`BC=ZVt_&oI)};%O;W*3K5i)&S=lG=f3<-{O;qlX@%%fd?cvc8017tn~#R7~6Kt#9>!38M#%p%?&v_0Qv&bI-o3W#%ugGaz! zmkm5oeXv+nbncFzPYBztko}Yp7v4BeB9ww<&k>!d3;gK#t|J%sipUHD7sL3+083E9N<}Edo`K|II>1}8Y`!_VE zQj~gc4^Le0sd{~55~7>%vaTgg5+9AHj9&J1A7(xB8~-unp#E4X28J9Aq#clw9NgE~ zQZotbDyE~Seml;&8^Ppgh7yOMuIVc)mLs#H5>TcqN>m@65(MEX@$FRWqApqb%y*pc z_y_}qQzOfN4xxP+0q=J=!r~+d41w07Gnlq!WxBN4D13?m;}2iObJKnTM2swGyenJ~>Or2odZm;$dt<-qIpP{gExYQ_ zj3`7oFetwc6!6t_60k#XHUU-|BnlU=q<|noQ|)BDS$;S8pD0?h3Uix1)p@*KcI@A@ zaKUR&&gL-U3T-)KMIyQ8C#dWEB%HlG0k)IHF9QYmxVIRWuO`t@b5C#_>HZYk+cro@ zful%QTR9V%6(lfgy!+*BT)I|(v5t>CE5z=TC3t7)M64|@LQg~PgZ>+Q+b6C;kz!k- zAeu*1*73W59suO#j*C%0Ynfd{3@>{EJq_E?zy2QJOt9F37Hx1tEI?`)9v3j_A|M_@ zQ>}rQj23Pp$$-QL8a5Gb?D^&;M)Q(bmQliUPA1UxxbI(J`wMW zKk%-u@AuX&n6~(IeM{8uZc^$#-~x&4gr!1XA2{54c2QhFM5gz-TfaV6-!x9h7AD1` zfkTY;W)CULWIx-{(%WanDMaUYB5!Sq9@?1&w20QB4Dx@3YatQKRx0%A?T*&ozRF4{ z_RIjT%gZ?Penzny-};i_qSdRyxy_#316)KT0uvi)A|IQ(wtds;y5RP9=$j@p2v3pz zAR4z~=Id`X&e`e~95(QMt~N7T%Ayf)G0OrW0$T}%DAZ{Zs_Sh1w#C3bFTNgbxj4>o zT_(@dA~LJ{^wGMez3Uc+2$B}th{6#+3t(l;N#=dP;7-MA0tB`|_(C{y7wCTr&KNd>>0000< KMNUMnLSTYyNkR_* literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/nl.png b/abogen/assets/flags/nl.png new file mode 100644 index 0000000000000000000000000000000000000000..684116e738f3e482e64b1bedc84cf997fac8df65 GIT binary patch literal 875 zcmV-x1C;!UP)f=Z+YI}|ZRce)e>Erm=cC>C1XcTvyVz)AJ@4RYDr}~3PKlvy6(Az0-!jqv!IRPgFD*p$CKn0>$6is36HT!M8 z$4OZu|5SI)CbTmdX(6-}$!!V2jVaWvs=PjHOQ#No1WC%dd!Kw1H&=cTay$=Fw@zMw zfVpBOO?OZF)S;E1uS6ok)0jLuhG^mrEJ*FzY!8|Pz5*R>-n=shuqv0PUU@wK^ZIw{ ztEH5wLaL!G7?{{9CK8R{i(u@F%n;B$S>G!}O0~iT4A1?C%R76&^ zE2n75VGVTiZUqHblTwKyVqr0i0g_1)hY;7awVEv&5IVZ(^)nYJxUda_T2@1b>PKt7GdIn z@_V;#XVZ+53M>zMc6%L7BP15lyfslt=9dLj&YAvwdjC%}d&?wNqtZOhjM&mBeALV( zp*lxyoPG8(Upg4>69{S))Wvk>^EpN%b@~oKp92z3*3B;*(xA@4r4ZjZ&I{8=P@|$Q z45eToN3EuI@r8Nut8a+tZA7C{(cZziI9)atyZcQ9am9&fGqU!ZD9CFCc`*8KcEyLf z#nnrDa?_(QPSYi6vAZl^Z=vpe=L&)1+nK2QQrUSwzi{Bk9>;pWZIs`YR_xdEp)dA! zJgP={hGvfelJ>0HBJzuZU&l^9YoCAqn?c_-`~#E7$3vPfHckKl002ovPDHLkV1jYK Bok0Kq literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/p.png b/abogen/assets/flags/p.png deleted file mode 100644 index 8abf47f0730402c3148037d9384b9c1bd8d423db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 617 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 literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/ro.png b/abogen/assets/flags/ro.png new file mode 100644 index 0000000000000000000000000000000000000000..62aea3fc99953fd2855281996af02d464541ee99 GIT binary patch literal 875 zcmV-x1C;!UP)9QMD|gZV`?00{bpxwGC1qi^!+2Nw{AG7 z{1drez?BIxM!q_|>d6O``}Z3u?PkZysbPpHk!J|G({c|QH3n)Vr`t_C`4fX@g@upT zi)c4{Y`tm6&QFG?a#1NipU9^Am23zkQEy$kEe(Z;h7+tpO|ex zGdzR_Q?R}GZgO|gYf1I)<@~>(xG!ztK5ZL4#%5G7kEC(I?i$p8g@dPsg#W6DT|>fZ zb`>512Hz->enHk+MUz5uH`o=ViM11w&y=aBegtMnE3zAHa>(}-=Ie7q-1@GtRHSr~ zPGbp8mBQtBOztTX**67$r#ocEU6_92XT;8+B|+48C}LsJ6-3)`Zit2Jbz&CAEEugP zOg;Sxoz?}Cz64bT&$hBBk#)o>qI=L}iTu5XnV*b-GYZyq&nshjF?eQAULuVP}tqLRjh#K}5!;Oyl`8;yLl+Gmg9 zaxlE5+ihC3ZILy1BCpEP)9T?g z(S;F&F2qIYQl(;pi^dSDpd~IsU^L0=%$vM>?~(gvrX9x^n^pl&$-|E?suPH z1PcobW`2I&g?X#hdI-P?5jpO?pAnHJNg=tRsw*Tfs_Iu06B9p$c#P@@$eM;KTa*`YV-6 z<)buBr?M=AwRR9J0k^`*Ffqnxnx<&C+kX{B@#f^@NUWmF~9*25{6!+-*E}+mkcCC9l65t4E zT zsQ6W@bQdLTJxKI(*zW+rQ0K(DOLFe;%SZ3oT>CC2YyE3;-#$rb1(9McX+Bw=%|CG8 z=LzB$H{qQCW`GxspCIo8H*EEMTKRY1JK_E&_}op$y%vTK(~3 z?Zw4VWPLchyzB%g6GEfjKhZ0RJq~Y3-yqZ)oI2LNvbX^DuIq4j3gCa%nRimkyIm3v z-Vzc5{;wvXO}zn39R?f1Gjm)%#hwei^~(e_h%D>u>lNaIgfgB58T1-tRH%P-Q2}j0 zkqg__nA01;?S6#5?m!y+s3+)-F}n&2}DHE|$pC|^G=EQ%m75Ol;BD)P10=>1uH z^~XNPpe6pD{y-$mS8B75VjJ`De999K3YhdIauNOll}n7xmoxd&x1(zZ_#2xE#<^Yu R&zk@M002ovPDHLkV1gI?s6_w( literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/se.png b/abogen/assets/flags/se.png new file mode 100644 index 0000000000000000000000000000000000000000..447e74b783983b42ffafe9ec42e1405e7b6145cc GIT binary patch literal 1060 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 literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/ua.png b/abogen/assets/flags/ua.png new file mode 100644 index 0000000000000000000000000000000000000000..77b4ac88a2f717fa16a408b4a1fb32c374a2a3c2 GIT binary patch literal 851 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 United States + "b": "gb", # British English -> United Kingdom + "e": "es", # Spanish -> Spain + "f": "fr", # French -> France + "h": "in", # Hindi -> India + "i": "it", # Italian -> Italy + "j": "jp", # Japanese -> Japan + "p": "br", # Brazilian Portuguese -> Brazil + "z": "cn", # Mandarin Chinese -> China +} + +# Mapping from SuperTonic ISO 639-1 language codes to ISO 3166-1 alpha-2 country codes +# Used for loading flag icons in the SuperTonic language picker +SUPERTONIC_LANG_TO_COUNTRY = { + "en": "gb", + "ko": "kr", + "ja": "jp", + "ar": "ae", + "bg": "bg", + "cs": "cz", + "da": "dk", + "de": "de", + "el": "gr", + "es": "es", + "et": "ee", + "fi": "fi", + "fr": "fr", + "hi": "in", + "hr": "hr", + "hu": "hu", + "id": "id", + "it": "it", + "lt": "lt", + "lv": "lv", + "nl": "nl", + "pl": "pl", + "pt": "pt", + "ro": "ro", + "ru": "ru", + "sk": "sk", + "sl": "si", + "sv": "se", + "tr": "tr", + "uk": "ua", + "vi": "vn", + "na": "na", +} + # Supported sound formats SUPPORTED_SOUND_FORMATS = [ "wav", diff --git a/abogen/pyqt/gui.py b/abogen/pyqt/gui.py index 3ee53b0..578e8b0 100644 --- a/abogen/pyqt/gui.py +++ b/abogen/pyqt/gui.py @@ -84,6 +84,8 @@ from abogen.constants import ( PROGRAM_DESCRIPTION, LANGUAGE_DESCRIPTIONS, VOICES_INTERNAL, + KOKORO_LANG_TO_COUNTRY, + SUPERTONIC_LANG_TO_COUNTRY, SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION, COLORS, SUBTITLE_FORMATS, @@ -1143,7 +1145,10 @@ class abogen(QWidget): 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) + country_code = SUPERTONIC_LANG_TO_COUNTRY.get(code, code) + flag = get_resource_path("abogen.assets.flags", f"{country_code}.png") + icon_st = QIcon(flag) if flag and os.path.exists(flag) else QIcon() + self.st_lang_combo.addItem(icon_st, 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) @@ -2011,7 +2016,8 @@ class abogen(QWidget): else: for v in VOICES_INTERNAL: icon = QIcon() - flag_path = get_resource_path("abogen.assets.flags", f"{v[0]}.png") + country_code = KOKORO_LANG_TO_COUNTRY.get(v[0], v[0]) + flag_path = get_resource_path("abogen.assets.flags", f"{country_code}.png") if flag_path and os.path.exists(flag_path): icon = QIcon(flag_path) self.voice_combo.addItem(icon, f"{v}", v) diff --git a/abogen/pyqt/voice_formula_gui.py b/abogen/pyqt/voice_formula_gui.py index feefb3e..284683a 100644 --- a/abogen/pyqt/voice_formula_gui.py +++ b/abogen/pyqt/voice_formula_gui.py @@ -31,6 +31,7 @@ from abogen.constants import ( VOICES_INTERNAL, SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION, LANGUAGE_DESCRIPTIONS, + KOKORO_LANG_TO_COUNTRY, COLORS, ) import re @@ -189,8 +190,9 @@ class VoiceMixer(QWidget): ) # Center the icons horizontally # Flag icon + country_code = KOKORO_LANG_TO_COUNTRY.get(language_code, language_code) flag_icon_path = get_resource_path( - "abogen.assets.flags", f"{language_code}.png" + "abogen.assets.flags", f"{country_code}.png" ) gender_icon_path = get_resource_path( "abogen.assets", "female.png" if is_female else "male.png" @@ -512,7 +514,8 @@ class VoiceFormulaDialog(QDialog): header_row.addWidget(QLabel("Language:")) self.language_combo = QComboBox() for code, desc in LANGUAGE_OPTIONS: - flag = get_resource_path("abogen.assets.flags", f"{code}.png") + country_code = KOKORO_LANG_TO_COUNTRY.get(code, code) + flag = get_resource_path("abogen.assets.flags", f"{country_code}.png") if flag and os.path.exists(flag): self.language_combo.addItem(QIcon(flag), desc, code) else: From 502a2d5a2ea85c5d61da27f91cc865a82a802789 Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Sun, 24 May 2026 11:11:05 +0200 Subject: [PATCH 3/7] set default steps to 8, updated supertonic rate from 24000 to 44100 - #163 --- abogen/constants.py | 4 +- abogen/pyqt/conversion.py | 92 +++++++------------------ abogen/pyqt/gui.py | 95 +++++++++++++++++--------- abogen/pyqt/queued_item.py | 2 +- abogen/tts_supertonic.py | 8 +-- abogen/webui/conversion_runner.py | 14 ++-- abogen/webui/routes/api.py | 6 +- abogen/webui/routes/settings.py | 6 +- abogen/webui/routes/utils/form.py | 13 +++- abogen/webui/routes/utils/preview.py | 4 +- abogen/webui/routes/utils/service.py | 2 +- abogen/webui/routes/utils/settings.py | 3 +- abogen/webui/routes/utils/voice.py | 2 +- abogen/webui/service.py | 14 ++-- tests/test_voice_formula_resolution.py | 4 +- 15 files changed, 136 insertions(+), 133 deletions(-) diff --git a/abogen/constants.py b/abogen/constants.py index 28b7b2a..4d73942 100644 --- a/abogen/constants.py +++ b/abogen/constants.py @@ -43,8 +43,8 @@ KOKORO_LANG_TO_COUNTRY = { "z": "cn", # Mandarin Chinese -> China } -# Mapping from SuperTonic ISO 639-1 language codes to ISO 3166-1 alpha-2 country codes -# Used for loading flag icons in the SuperTonic language picker +# Mapping from Supertonic ISO 639-1 language codes to ISO 3166-1 alpha-2 country codes +# Used for loading flag icons in the Supertonic language picker SUPERTONIC_LANG_TO_COUNTRY = { "en": "gb", "ko": "kr", diff --git a/abogen/pyqt/conversion.py b/abogen/pyqt/conversion.py index c62f9e8..1a1a221 100644 --- a/abogen/pyqt/conversion.py +++ b/abogen/pyqt/conversion.py @@ -269,7 +269,7 @@ class ConversionThread(QThread): save_base_path=None, tts_provider="kokoro", supertonic_language="en", - supertonic_total_steps=5, + supertonic_total_steps=8, ): super().__init__() self._chapter_options_event = threading.Event() @@ -283,6 +283,7 @@ class ConversionThread(QThread): self.tts_provider = tts_provider self.supertonic_language = supertonic_language self.supertonic_total_steps = supertonic_total_steps + self.sample_rate = 44100 if tts_provider == "supertonic" else 24000 self.save_option = save_option self.output_folder = output_folder self.subtitle_mode = subtitle_mode @@ -512,7 +513,7 @@ class ConversionThread(QThread): if self.tts_provider == "supertonic": tts = SupertonicPipeline( - sample_rate=24000, + sample_rate=self.sample_rate, lang=self.supertonic_language, total_steps=self.supertonic_total_steps, ) @@ -765,7 +766,7 @@ class ConversionThread(QThread): merged_out_path = f"{base_filepath_no_ext}.{self.output_format}" subtitle_entries = [] current_time = 0.0 - rate = 24000 + rate = self.sample_rate subtitle_mode = self.subtitle_mode self.etr_start_time = time.time() self.processed_char_count = 0 @@ -781,7 +782,7 @@ class ConversionThread(QThread): merged_out_file = sf.SoundFile( merged_out_path, "w", - samplerate=24000, + samplerate=self.sample_rate, channels=1, format=self.output_format, ) @@ -797,62 +798,18 @@ class ConversionThread(QThread): # Prepare ffmpeg command for m4b output cmd = [ "ffmpeg", - "-y", - "-thread_queue_size", - "32768", - "-f", - "f32le", - "-ar", - "24000", - "-ac", - "1", - "-i", - "pipe:0", - ] - if cover_path and os.path.exists(cover_path): - cmd.extend( - [ - "-i", - cover_path, - "-map", - "0:a", - "-map", - "1", - "-c:v", - "copy", - "-disposition:v", - "attached_pic", - ] - ) - cmd.extend( - [ - "-c:a", - "aac", - "-q:a", - "2", - "-movflags", - "+faststart+use_metadata_tags", + "-y", + "-thread_queue_size", + "32768", + "-f", + "f32le", + "-ar", + str(self.sample_rate), + "-ac", + "1", + "-i", + "pipe:0", ] - ) - cmd += metadata_options - cmd.append(merged_out_path) - ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False) - elif self.output_format == "opus": - static_ffmpeg.add_paths() - cmd = [ - "ffmpeg", - "-y", - "-thread_queue_size", - "32768", - "-f", - "f32le", - "-ar", - "24000", - "-ac", - "1", - "-i", - "pipe:0", - ] cmd.extend(["-c:a", "libopus", "-b:a", "24000"]) cmd.append(merged_out_path) ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False) @@ -934,7 +891,7 @@ class ConversionThread(QThread): merged_out_path = None subtitle_entries = [] current_time = 0.0 - rate = 24000 + rate = self.sample_rate subtitle_mode = self.subtitle_mode self.etr_start_time = time.time() self.processed_char_count = 0 @@ -987,7 +944,7 @@ class ConversionThread(QThread): chapter_out_file = sf.SoundFile( chapter_out_path, "w", - samplerate=24000, + samplerate=self.sample_rate, channels=1, format=separate_chapters_format, ) @@ -1002,7 +959,7 @@ class ConversionThread(QThread): "-f", "f32le", "-ar", - "24000", + str(self.sample_rate), "-ac", "1", "-i", @@ -1384,7 +1341,7 @@ class ConversionThread(QThread): # Add silence between chapters for merged output (except after the last chapter) if merge_chapters_at_end and chapter_idx < total_chapters: silence_samples = int( - self.silence_duration * 24000 + self.silence_duration * self.sample_rate ) # Silence duration at 24,000 Hz silence_audio = self.np.zeros(silence_samples, dtype="float32") silence_bytes = silence_audio.tobytes() @@ -1615,7 +1572,7 @@ class ConversionThread(QThread): parent_dir, f"{sanitized_base_name}{suffix}" ) merged_out_path = f"{base_filepath_no_ext}.{self.output_format}" - rate = 24000 + rate = self.sample_rate # Setup audio output merged_out_file, ffmpeg_proc = None, None @@ -2467,7 +2424,7 @@ class VoicePreviewThread(QThread): parent=None, tts_provider="kokoro", supertonic_language="en", - supertonic_total_steps=5, + supertonic_total_steps=8, ): super().__init__(parent) self.np_module = np_module @@ -2479,6 +2436,7 @@ class VoicePreviewThread(QThread): self.tts_provider = tts_provider self.supertonic_language = supertonic_language self.supertonic_total_steps = supertonic_total_steps + self.sample_rate = 44100 if tts_provider == "supertonic" else 24000 # Cache location for preview audio self.cache_dir = get_user_cache_path("preview_cache") @@ -2516,7 +2474,7 @@ class VoicePreviewThread(QThread): from abogen.tts_supertonic import SupertonicPipeline tts = SupertonicPipeline( - sample_rate=24000, + sample_rate=self.sample_rate, lang=self.supertonic_language, total_steps=self.supertonic_total_steps, ) @@ -2556,7 +2514,7 @@ class VoicePreviewThread(QThread): if audio_segments: audio = self.np_module.concatenate(audio_segments) - sf.write(self.cache_path, audio, 24000) + sf.write(self.cache_path, audio, self.sample_rate) self.temp_wav = self.cache_path self.finished.emit() except Exception as e: diff --git a/abogen/pyqt/gui.py b/abogen/pyqt/gui.py index 578e8b0..4a725f3 100644 --- a/abogen/pyqt/gui.py +++ b/abogen/pyqt/gui.py @@ -975,7 +975,7 @@ class abogen(QWidget): ) 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.supertonic_total_steps_config = self.config.get("supertonic_total_steps", 8) self._pending_close_event = None self.gpu_ok = False # Initialize GPU availability status @@ -1025,12 +1025,14 @@ class abogen(QWidget): 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" + 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) + idx_steps = self.st_steps_combo.findData(self.supertonic_total_steps_config) + if idx_steps >= 0: + self.st_steps_combo.setCurrentIndex(idx_steps) if self.save_option == "Choose output folder" and self.selected_output_folder: self.save_path_label.setText(self.selected_output_folder) @@ -1129,13 +1131,13 @@ class abogen(QWidget): 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.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 + # 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) @@ -1156,13 +1158,13 @@ class abogen(QWidget): 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) + self.st_steps_combo = QComboBox(self) + for val in range(2, 16): + self.st_steps_combo.addItem(str(val), val) + self.st_steps_combo.setCurrentIndex(self.st_steps_combo.findData(8)) + self.st_steps_combo.setStyleSheet("QComboBox { min-height: 20px; padding: 6px 12px; }") + self.st_steps_combo.currentIndexChanged.connect(self._on_st_steps_changed) + supertonic_row_layout.addWidget(self.st_steps_combo) supertonic_row_layout.addStretch() self.supertonic_row.hide() @@ -1896,7 +1898,7 @@ class abogen(QWidget): save_config(self.config) is_supertonic = provider == "supertonic" - # Show/hide SuperTonic controls + # Show/hide Supertonic controls self.supertonic_row.setVisible(is_supertonic) # Update subtitles availability @@ -1910,7 +1912,7 @@ class abogen(QWidget): self.mixed_voice_state = None self.btn_voice_formula_mixer.setEnabled(False) self.voice_combo.setToolTip( - "SuperTonic voices:\n" + "Supertonic voices:\n" "M1-M5 = Male voices\n" "F1-F5 = Female voices" ) @@ -1928,8 +1930,8 @@ class abogen(QWidget): self.selected_lang = lang self.update_subtitle_options_availability() - def _on_st_steps_changed(self, steps): - self.config["supertonic_total_steps"] = steps + def _on_st_steps_changed(self): + self.config["supertonic_total_steps"] = self.st_steps_combo.currentData() save_config(self.config) def on_voice_combo_changed(self, index): @@ -1943,7 +1945,7 @@ class abogen(QWidget): if isinstance(entry, dict): entry_provider = str(entry.get("provider", "")).strip().lower() if entry_provider == "supertonic": - # Switch provider to SuperTonic if not already + # Switch provider to Supertonic if not already if self.provider_combo.currentData() != "supertonic": self.provider_combo.setCurrentIndex(1) self.mixed_voice_state = None @@ -1951,7 +1953,9 @@ class abogen(QWidget): # 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)) + idx_steps = self.st_steps_combo.findData(int(profile_steps)) + if idx_steps >= 0: + self.st_steps_combo.setCurrentIndex(idx_steps) profile_lang = entry.get("language") if profile_lang and profile_lang in SUPERTONIC_AVAILABLE_LANGS: self.st_lang_combo.setCurrentText(profile_lang) @@ -2012,7 +2016,14 @@ class abogen(QWidget): # re-add voices if provider == "supertonic": for v in DEFAULT_SUPERTONIC_VOICES: - self.voice_combo.addItem(f"{v}", v) + icon = QIcon() + if v.startswith("F"): + icon_path = get_resource_path("abogen.assets", "female.png") + else: + icon_path = get_resource_path("abogen.assets", "male.png") + if icon_path and os.path.exists(icon_path): + icon = QIcon(icon_path) + self.voice_combo.addItem(icon, f"{v}", v) else: for v in VOICES_INTERNAL: icon = QIcon() @@ -2213,7 +2224,7 @@ class abogen(QWidget): 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(), + supertonic_total_steps=self.st_steps_combo.currentData(), ) # Prevent adding duplicate items to the queue @@ -2359,9 +2370,12 @@ class abogen(QWidget): # TTS provider settings tts_provider = getattr(queued_item, "tts_provider", "kokoro") - self.provider_combo.setCurrentText("SuperTonic" if tts_provider == "supertonic" else "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)) + steps_val = getattr(queued_item, "supertonic_total_steps", 8) + idx_steps = self.st_steps_combo.findData(steps_val) + if idx_steps >= 0: + self.st_steps_combo.setCurrentIndex(idx_steps) # Sync Voice/Profile in config self.config["selected_voice"] = self.selected_voice @@ -2390,8 +2404,11 @@ class abogen(QWidget): 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" + return str(entry.get("voice", "M1")) + current_data = self.voice_combo.currentData() + if current_data and isinstance(current_data, str) and not current_data.startswith("profile:"): + return current_data + return "M1" if self.mixed_voice_state: formula_components = [ f"{name}*{weight}" for name, weight in self.mixed_voice_state @@ -2494,7 +2511,7 @@ class abogen(QWidget): tts_provider = self.provider_combo.currentData() supertonic_language = self.st_lang_combo.currentText() - supertonic_total_steps = self.st_steps_spin.value() + supertonic_total_steps = self.st_steps_combo.currentData() self.conversion_thread = ConversionThread( self.selected_file, @@ -2594,8 +2611,9 @@ class abogen(QWidget): self.update_log((gpu_msg, gpu_ok)) tts_provider = self.provider_combo.currentData() if tts_provider == "supertonic": - # SuperTonic doesn't need KPipeline, call callback directly - pipeline_loaded_callback(None, None, None) + # Supertonic doesn't need KPipeline, call callback directly + import numpy as np + pipeline_loaded_callback(np, None, None) else: self.update_log("Loading modules...") load_thread = LoadPipelineThread(pipeline_loaded_callback) @@ -2918,9 +2936,13 @@ class abogen(QWidget): provider = self.provider_combo.currentData() if provider == "supertonic": - voice_to_cache = self.selected_voice or "M1" + current_data = self.voice_combo.currentData() + if current_data and isinstance(current_data, str) and not current_data.startswith("profile:"): + voice_to_cache = current_data + else: + voice_to_cache = "M1" lang_to_cache = self.st_lang_combo.currentText() - steps = self.st_steps_spin.value() + steps = self.st_steps_combo.currentData() 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) @@ -3029,10 +3051,11 @@ 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 + # For Supertonic, skip KPipeline loading and use SupertonicPipeline directly if self.provider_combo.currentData() == "supertonic": + import numpy as np self.loading_movie.start() - self._on_pipeline_loaded_for_preview(None, None, None) + self._on_pipeline_loaded_for_preview(np, None, None) return # Start loading animation - ensure signal connection is always active @@ -3093,11 +3116,17 @@ class abogen(QWidget): else None ) else: - voice = self.selected_voice or "" + if self.provider_combo.currentData() == "supertonic": + current_data = self.voice_combo.currentData() + print(f"[DEBUG] voice_combo.currentData() = {current_data!r}") + voice = current_data if (current_data and isinstance(current_data, str) and not current_data.startswith("profile:")) else "M1" + print(f"[DEBUG] resolved voice = {voice!r}") + else: + 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() + supertonic_total_steps = self.st_steps_combo.currentData() if tts_provider == "supertonic": lang = supertonic_language diff --git a/abogen/pyqt/queued_item.py b/abogen/pyqt/queued_item.py index 859d069..455a0ac 100644 --- a/abogen/pyqt/queued_item.py +++ b/abogen/pyqt/queued_item.py @@ -29,4 +29,4 @@ class QueuedItem: # TTS Provider fields tts_provider: str = "kokoro" supertonic_language: str = "en" - supertonic_total_steps: int = 5 + supertonic_total_steps: int = 8 diff --git a/abogen/tts_supertonic.py b/abogen/tts_supertonic.py index c83ab11..ec94047 100644 --- a/abogen/tts_supertonic.py +++ b/abogen/tts_supertonic.py @@ -97,7 +97,7 @@ _UNSUPPORTED_CHARS_RE = re.compile( def _parse_unsupported_characters(error: BaseException) -> list[str]: - """Best-effort extraction of unsupported characters from SuperTonic errors.""" + """Best-effort extraction of unsupported characters from Supertonic errors.""" message = " ".join( str(part) for part in getattr(error, "args", ()) if part is not None @@ -221,7 +221,7 @@ class SupertonicPipeline: removed: set[str] = set() last_exc: Exception | None = None - # SuperTonic can raise ValueError for unsupported characters; strip and retry. + # Supertonic can raise ValueError for unsupported characters; strip and retry. for attempt in range(3): try: wav, duration = self._tts.synthesize( @@ -253,14 +253,14 @@ class SupertonicPipeline: chunk_to_speak = sanitized if not chunk_to_speak: logger.warning( - "SuperTonic: dropped a chunk after removing unsupported characters: %s", + "Supertonic: dropped a chunk after removing unsupported characters: %s", sorted(removed), ) break if attempt == 0: logger.warning( - "SuperTonic: removed unsupported characters %s and retried.", + "Supertonic: removed unsupported characters %s and retried.", sorted(removed), ) else: diff --git a/abogen/webui/conversion_runner.py b/abogen/webui/conversion_runner.py index f41e624..f2644bd 100644 --- a/abogen/webui/conversion_runner.py +++ b/abogen/webui/conversion_runner.py @@ -59,7 +59,7 @@ def _supertonic_voice_from_spec(spec: Any, fallback: str) -> str: raw = str(spec or "").strip() fallback_raw = str(fallback or "").strip() - # SuperTonic voices are discrete IDs (M1/F3/...). If we see a Kokoro mix + # Supertonic voices are discrete IDs (M1/F3/...). If we see a Kokoro mix # formula (contains '*' or '+'), ignore it and fall back to a safe voice. if not raw or "*" in raw or "+" in raw: raw = fallback_raw @@ -1584,7 +1584,7 @@ def run_conversion_job(job: Job) -> None: pipelines[provider_norm] = SupertonicPipeline( sample_rate=SAMPLE_RATE, auto_download=True, - total_steps=int(getattr(job, "supertonic_total_steps", 5) or 5), + total_steps=int(getattr(job, "supertonic_total_steps", 8) or 8), ) return pipelines[provider_norm] @@ -1618,7 +1618,7 @@ def run_conversion_job(job: Job) -> None: provider = str(entry.get("provider") or "kokoro").strip().lower() or "kokoro" if provider == "supertonic": voice = str(entry.get("voice") or getattr(job, "voice", "M1") or "M1").strip() or "M1" - steps = int(entry.get("total_steps") or getattr(job, "supertonic_total_steps", 5) or 5) + steps = int(entry.get("total_steps") or getattr(job, "supertonic_total_steps", 8) or 8) speed = float(entry.get("speed") or getattr(job, "speed", 1.0) or 1.0) return "supertonic", _supertonic_voice_from_spec(voice, getattr(job, "voice", "M1")), speed, steps formula = _formula_from_kokoro_entry(entry) @@ -1634,7 +1634,7 @@ def run_conversion_job(job: Job) -> None: """Resolve a raw voice spec into (provider, resolved_spec, choice, speed, steps). For Kokoro formulas, `choice` will be a resolved voice tensor (via `voice_formulas`). - For SuperTonic, `choice` will be a valid SuperTonic voice id. + For Supertonic, `choice` will be a valid Supertonic voice id. """ provider, resolved, speed, steps = resolve_voice_target(raw_spec) @@ -1857,7 +1857,7 @@ def run_conversion_job(job: Job) -> None: voice=voice_name, speed=float(speed_override if speed_override is not None else job.speed), split_pattern=split_pattern, - total_steps=int(supertonic_steps_override if supertonic_steps_override is not None else getattr(job, "supertonic_total_steps", 5)), + total_steps=int(supertonic_steps_override if supertonic_steps_override is not None else getattr(job, "supertonic_total_steps", 8)), ) else: kokoro_pipeline = get_pipeline("kokoro") @@ -2448,7 +2448,7 @@ def _load_pipeline(job: Job): return SupertonicPipeline( sample_rate=SAMPLE_RATE, auto_download=True, - total_steps=int(getattr(job, "supertonic_total_steps", 5) or 5), + total_steps=int(getattr(job, "supertonic_total_steps", 8) or 8), ) device = "cpu" @@ -2610,7 +2610,7 @@ def _build_ffmpeg_command(path: Path, fmt: str, metadata: Optional[Dict[str, str def _resolve_voice(pipeline, voice_spec: str, use_gpu: bool): if "*" in voice_spec: # Voice formulas are a Kokoro-only feature (they require a pipeline that can - # load individual Kokoro voices). When running with SuperTonic (or when the + # load individual Kokoro voices). When running with Supertonic (or when the # pipeline is otherwise unavailable), treat the spec as a plain string and # allow downstream provider-specific resolution to choose a safe fallback. if pipeline is None or not hasattr(pipeline, "load_single_voice"): diff --git a/abogen/webui/routes/api.py b/abogen/webui/routes/api.py index f02ab9d..7dc749e 100644 --- a/abogen/webui/routes/api.py +++ b/abogen/webui/routes/api.py @@ -162,7 +162,7 @@ def api_voice_profiles_preview() -> ResponseReturnValue: formula = str(payload.get("formula") or "").strip() profile_name = str(payload.get("profile") or "").strip() provider = str(payload.get("tts_provider") or payload.get("provider") or "").strip().lower() or None - supertonic_total_steps = int(payload.get("supertonic_total_steps") or payload.get("total_steps") or settings.get("supertonic_total_steps") or 5) + supertonic_total_steps = int(payload.get("supertonic_total_steps") or payload.get("total_steps") or settings.get("supertonic_total_steps") or 8) voice_spec = "" resolved_provider = provider or "kokoro" @@ -224,7 +224,7 @@ def api_speaker_preview() -> ResponseReturnValue: speed_value = payload.get("speed") speed = coerce_float(speed_value, 1.0) tts_provider = str(payload.get("tts_provider") or "").strip().lower() - supertonic_total_steps = int(payload.get("supertonic_total_steps") or 5) + supertonic_total_steps = int(payload.get("supertonic_total_steps") or 8) settings = load_settings() use_gpu = settings.get("use_gpu", False) @@ -269,7 +269,7 @@ def api_speaker_preview() -> ResponseReturnValue: use_gpu=use_gpu , tts_provider=resolved_provider, - supertonic_total_steps=supertonic_total_steps or int(settings.get("supertonic_total_steps") or 5), + supertonic_total_steps=supertonic_total_steps or int(settings.get("supertonic_total_steps") or 8), pronunciation_overrides=pronunciation_overrides, manual_overrides=manual_overrides, speakers=speakers, diff --git a/abogen/webui/routes/settings.py b/abogen/webui/routes/settings.py index d0bb991..9e5a525 100644 --- a/abogen/webui/routes/settings.py +++ b/abogen/webui/routes/settings.py @@ -43,8 +43,12 @@ def update_settings() -> ResponseReturnValue: current["language"] = (form.get("language") or "en").strip() current["default_speaker"] = (form.get("default_speaker") or "").strip() current["default_voice"] = (form.get("default_voice") or "").strip() + provider = str(form.get("tts_provider") or "kokoro").strip().lower() + if provider in {"kokoro", "supertonic"}: + current["tts_provider"] = provider try: - current["supertonic_total_steps"] = max(2, min(15, int(form.get("supertonic_total_steps", current.get("supertonic_total_steps", 5))))) + total_steps = int(form.get("supertonic_total_steps", current.get("supertonic_total_steps", 8))) + current["supertonic_total_steps"] = max(2, min(15, total_steps)) except (TypeError, ValueError): pass try: diff --git a/abogen/webui/routes/utils/form.py b/abogen/webui/routes/utils/form.py index 41d605e..47b8889 100644 --- a/abogen/webui/routes/utils/form.py +++ b/abogen/webui/routes/utils/form.py @@ -577,7 +577,7 @@ def apply_book_step_form( # NOTE: Do not auto-set a global TTS provider at the book level based on the # narrator defaults. Provider is resolved per-speaker/per-chunk from the voice # spec (e.g. "speaker:Name" for saved speakers, or a Kokoro mix formula). - # This enables mixed-provider conversions (e.g. narrator=SuperTonic, characters=Kokoro). + # This enables mixed-provider conversions (e.g. narrator=Supertonic, characters=Kokoro). provider_value = str(form.get("tts_provider") or "").strip().lower() if provider_value in {"kokoro", "supertonic"}: pending.tts_provider = provider_value @@ -913,6 +913,15 @@ def build_pending_job_from_extraction( else: normalization_overrides[key] = default_val + provider_value = str(form.get("tts_provider") or "").strip().lower() + if provider_value not in {"kokoro", "supertonic"}: + provider_value = settings.get("tts_provider", "kokoro") + try: + total_steps = int(form.get("supertonic_total_steps", settings.get("supertonic_total_steps", 8))) + supertonic_steps = max(2, min(15, total_steps)) + except (TypeError, ValueError): + supertonic_steps = int(settings.get("supertonic_total_steps", 8)) + pending = PendingJob( id=uuid.uuid4().hex, original_filename=original_name, @@ -928,6 +937,8 @@ def build_pending_job_from_extraction( replace_single_newlines=replace_single_newlines, subtitle_format=subtitle_format, total_characters=total_chars, + tts_provider=provider_value, + supertonic_total_steps=supertonic_steps, save_chapters_separately=save_chapters_separately, merge_chapters_at_end=merge_chapters_at_end, separate_chapters_format=separate_chapters_format, diff --git a/abogen/webui/routes/utils/preview.py b/abogen/webui/routes/utils/preview.py index 58b2391..89c754d 100644 --- a/abogen/webui/routes/utils/preview.py +++ b/abogen/webui/routes/utils/preview.py @@ -92,7 +92,7 @@ def generate_preview_audio( speed: float, use_gpu: bool, tts_provider: str = "kokoro", - supertonic_total_steps: int = 5, + supertonic_total_steps: int = 8, max_seconds: float = 8.0, pronunciation_overrides: Optional[Iterable[Mapping[str, Any]]] = None, manual_overrides: Optional[Iterable[Mapping[str, Any]]] = None, @@ -201,7 +201,7 @@ def synthesize_preview( speed: float, use_gpu: bool, tts_provider: str = "kokoro", - supertonic_total_steps: int = 5, + supertonic_total_steps: int = 8, max_seconds: float = 8.0, pronunciation_overrides: Optional[Iterable[Mapping[str, Any]]] = None, manual_overrides: Optional[Iterable[Mapping[str, Any]]] = None, diff --git a/abogen/webui/routes/utils/service.py b/abogen/webui/routes/utils/service.py index b48f94a..beb9ee0 100644 --- a/abogen/webui/routes/utils/service.py +++ b/abogen/webui/routes/utils/service.py @@ -25,7 +25,7 @@ def submit_job(pending: PendingJob) -> str: tts_provider=getattr(pending, "tts_provider", "kokoro"), voice=pending.voice, speed=pending.speed, - supertonic_total_steps=getattr(pending, "supertonic_total_steps", 5), + supertonic_total_steps=getattr(pending, "supertonic_total_steps", 8), use_gpu=pending.use_gpu, subtitle_mode=pending.subtitle_mode, output_format=pending.output_format, diff --git a/abogen/webui/routes/utils/settings.py b/abogen/webui/routes/utils/settings.py index c96a66c..0786be3 100644 --- a/abogen/webui/routes/utils/settings.py +++ b/abogen/webui/routes/utils/settings.py @@ -175,7 +175,8 @@ def settings_defaults() -> Dict[str, Any]: "save_mode": "default_output" if has_output_override() else "save_next_to_input", "default_speaker": "", "default_voice": VOICES_INTERNAL[0] if VOICES_INTERNAL else "", - "supertonic_total_steps": 5, + "tts_provider": "kokoro", + "supertonic_total_steps": 8, "supertonic_speed": 1.0, "replace_single_newlines": False, "use_gpu": True, diff --git a/abogen/webui/routes/utils/voice.py b/abogen/webui/routes/utils/voice.py index 3d9081a..c15172e 100644 --- a/abogen/webui/routes/utils/voice.py +++ b/abogen/webui/routes/utils/voice.py @@ -666,7 +666,7 @@ def resolve_voice_choice( # Provider-aware behavior: # - Kokoro profiles typically represent mixes (formula strings). - # - SuperTonic profiles represent a discrete voice id + settings. + # - Supertonic profiles represent a discrete voice id + settings. # In that case, we return a speaker reference so downstream can # resolve provider per-speaker and allow mixed-provider casting. if provider == "supertonic": diff --git a/abogen/webui/service.py b/abogen/webui/service.py index a04e9ce..05afab6 100644 --- a/abogen/webui/service.py +++ b/abogen/webui/service.py @@ -111,7 +111,7 @@ class Job: subtitle_format: str created_at: float tts_provider: str = "kokoro" - supertonic_total_steps: int = 5 + supertonic_total_steps: int = 8 save_chapters_separately: bool = False merge_chapters_at_end: bool = True separate_chapters_format: str = "wav" @@ -204,7 +204,7 @@ class Job: "queue_position": self.queue_position, "options": { "tts_provider": getattr(self, "tts_provider", "kokoro"), - "supertonic_total_steps": getattr(self, "supertonic_total_steps", 5), + "supertonic_total_steps": getattr(self, "supertonic_total_steps", 8, "save_chapters_separately": self.save_chapters_separately, "merge_chapters_at_end": self.merge_chapters_at_end, "separate_chapters_format": self.separate_chapters_format, @@ -552,7 +552,7 @@ class PendingJob: normalization_overrides: Dict[str, Any] created_at: float tts_provider: str = "kokoro" - supertonic_total_steps: int = 5 + supertonic_total_steps: int = 8 cover_image_path: Optional[Path] = None cover_image_mime: Optional[str] = None chapter_intro_delay: float = 0.5 @@ -621,7 +621,7 @@ class ConversionService: voice: str, speed: float, tts_provider: str = "kokoro", - supertonic_total_steps: int = 5, + supertonic_total_steps: int = 8, use_gpu: bool, subtitle_mode: str, output_format: str, @@ -674,7 +674,7 @@ class ConversionService: voice=voice, speed=speed, tts_provider=tts_provider, - supertonic_total_steps=int(supertonic_total_steps or 5), + supertonic_total_steps=int(supertonic_total_steps or 8), use_gpu=use_gpu, subtitle_mode=subtitle_mode, output_format=output_format, @@ -1147,7 +1147,7 @@ class ConversionService: "tts_provider": getattr(job, "tts_provider", "kokoro"), "voice": job.voice, "speed": job.speed, - "supertonic_total_steps": getattr(job, "supertonic_total_steps", 5), + "supertonic_total_steps": getattr(job, "supertonic_total_steps", 8, "use_gpu": job.use_gpu, "subtitle_mode": job.subtitle_mode, "output_format": job.output_format, @@ -1275,7 +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)), + supertonic_total_steps=int(payload.get("supertonic_total_steps", 8), 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/tests/test_voice_formula_resolution.py b/tests/test_voice_formula_resolution.py index 2bc3430..99b161f 100644 --- a/tests/test_voice_formula_resolution.py +++ b/tests/test_voice_formula_resolution.py @@ -6,13 +6,13 @@ from abogen.tts_supertonic import DEFAULT_SUPERTONIC_VOICES def test_resolve_voice_formula_without_pipeline_does_not_crash() -> None: # This can happen when a previously-saved Kokoro mix formula is present - # but the active provider is SuperTonic (no Kokoro pipeline object). + # but the active provider is Supertonic (no Kokoro pipeline object). formula = "af_heart*0.5+af_sky*0.5" resolved = _resolve_voice(None, formula, use_gpu=False) assert resolved == formula def test_supertonic_voice_from_formula_falls_back_to_valid_voice() -> None: - # When a stale Kokoro mix formula is present, SuperTonic should not receive it. + # When a stale Kokoro mix formula is present, Supertonic should not receive it. chosen = _supertonic_voice_from_spec("af_heart*0.5+af_sky*0.5", "af_heart*1.0") assert chosen in DEFAULT_SUPERTONIC_VOICES From 9cfe799a8ec70ccae1d535012eecb041cb8c7064 Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Sun, 24 May 2026 11:11:23 +0200 Subject: [PATCH 4/7] supertonic picker for web version - #163 --- .../templates/partials/new_job_step_book.html | 97 ++++++++++++++++++- abogen/webui/templates/settings.html | 9 ++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/abogen/webui/templates/partials/new_job_step_book.html b/abogen/webui/templates/partials/new_job_step_book.html index 5bf14b4..d4f2d10 100644 --- a/abogen/webui/templates/partials/new_job_step_book.html +++ b/abogen/webui/templates/partials/new_job_step_book.html @@ -26,6 +26,26 @@ {% set subtitle_value = settings_dict.get('subtitle_mode', 'Disabled') %} {% endif %} {% endif %} +{% set tts_provider_value = form_values.get('tts_provider') if form_values else None %} +{% if not tts_provider_value %} + {% if pending and pending.tts_provider %} + {% set tts_provider_value = pending.tts_provider %} + {% else %} + {% set tts_provider_value = settings_dict.get('tts_provider', 'kokoro') %} + {% endif %} +{% endif %} +{% set supertonic_steps_value = form_values.get('supertonic_total_steps') if form_values else None %} +{% if supertonic_steps_value is none %} + {% if pending and pending.supertonic_total_steps is not none %} + {% set supertonic_steps_value = pending.supertonic_total_steps %} + {% else %} + {% set supertonic_steps_value = settings_dict.get('supertonic_total_steps', 8 %} + {% endif %} +{% endif %} +{% if supertonic_steps_value is not none and supertonic_steps_value is string %} + {% set supertonic_steps_value = supertonic_steps_value|int %} +{% endif %} +{% set is_supertonic = tts_provider_value == 'supertonic' %} {% set generate_flag = form_values.get('generate_epub3') if form_values else None %} {% if generate_flag is not none %} {% set generate_epub3 = True %} @@ -273,6 +293,13 @@
+ + +
+
-