Add Supertonic TTS provider support and configuration options

This commit is contained in:
Juraj Borza
2026-05-23 21:12:05 +02:00
parent 9fa81fbe1e
commit 81937b7a40
4 changed files with 307 additions and 49 deletions
+52 -6
View File
@@ -20,6 +20,7 @@ from abogen.constants import (
SUPPORTED_SOUND_FORMATS, SUPPORTED_SOUND_FORMATS,
SUPPORTED_SUBTITLE_FORMATS, SUPPORTED_SUBTITLE_FORMATS,
) )
from abogen.tts_supertonic import SupertonicPipeline, SUPERTONIC_AVAILABLE_LANGS, DEFAULT_SUPERTONIC_VOICES
from abogen.voice_formulas import get_new_voice from abogen.voice_formulas import get_new_voice
import abogen.hf_tracker as hf_tracker import abogen.hf_tracker as hf_tracker
import static_ffmpeg import static_ffmpeg
@@ -266,7 +267,10 @@ class ConversionThread(QThread):
use_gpu=True, use_gpu=True,
from_queue=False, from_queue=False,
save_base_path=None, save_base_path=None,
): # Add use_gpu parameter tts_provider="kokoro",
supertonic_language="en",
supertonic_total_steps=5,
):
super().__init__() super().__init__()
self._chapter_options_event = threading.Event() self._chapter_options_event = threading.Event()
self._timestamp_response_event = threading.Event() self._timestamp_response_event = threading.Event()
@@ -276,6 +280,9 @@ class ConversionThread(QThread):
self.lang_code = lang_code self.lang_code = lang_code
self.speed = speed self.speed = speed
self.voice = voice 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.save_option = save_option
self.output_folder = output_folder self.output_folder = output_folder
self.subtitle_mode = subtitle_mode 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"- Voice: {self.voice}")
self.log_updated.emit(f"- Speed: {self.speed}") 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"- Subtitle mode: {self.subtitle_mode}")
self.log_updated.emit(f"- Output format: {self.output_format}") self.log_updated.emit(f"- Output format: {self.output_format}")
self.log_updated.emit( self.log_updated.emit(
@@ -499,6 +510,13 @@ class ConversionThread(QThread):
else: else:
device = "cpu" device = "cpu"
if self.tts_provider == "supertonic":
tts = SupertonicPipeline(
sample_rate=24000,
lang=self.supertonic_language,
total_steps=self.supertonic_total_steps,
)
else:
tts = self.KPipeline( tts = self.KPipeline(
lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device
) )
@@ -2447,6 +2465,9 @@ class VoicePreviewThread(QThread):
speed, speed,
use_gpu=False, use_gpu=False,
parent=None, parent=None,
tts_provider="kokoro",
supertonic_language="en",
supertonic_total_steps=5,
): ):
super().__init__(parent) super().__init__(parent)
self.np_module = np_module self.np_module = np_module
@@ -2455,6 +2476,9 @@ class VoicePreviewThread(QThread):
self.voice = voice self.voice = voice
self.speed = speed self.speed = speed
self.use_gpu = use_gpu 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 # Cache location for preview audio
self.cache_dir = get_user_cache_path("preview_cache") self.cache_dir = get_user_cache_path("preview_cache")
@@ -2464,6 +2488,11 @@ class VoicePreviewThread(QThread):
def _get_cache_path(self): def _get_cache_path(self):
"""Generate a unique filename for the voice with its parameters""" """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 # For a voice formula, use a hash of the formula
if "*" in self.voice: if "*" in self.voice:
voice_id = ( voice_id = (
@@ -2478,25 +2507,42 @@ class VoicePreviewThread(QThread):
def run(self): def run(self):
print( 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 # Generate the preview and save to cache
try: try:
if self.tts_provider == "supertonic":
from abogen.tts_supertonic import SupertonicPipeline
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 # Set device based on use_gpu setting and platform
if self.use_gpu: if self.use_gpu:
if platform.system() == "Darwin" and platform.processor() == "arm": if platform.system() == "Darwin" and platform.processor() == "arm":
device = "mps" # Use MPS for Apple Silicon device = "mps"
else: else:
device = "cuda" # Use CUDA for other platforms device = "cuda"
else: else:
device = "cpu" device = "cpu"
tts = self.kpipeline_class( tts = self.kpipeline_class(
lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device
) )
# Enable voice formula support for preview
if "*" in self.voice: if "*" in self.voice:
loaded_voice = get_new_voice(tts, self.voice, self.use_gpu) loaded_voice = get_new_voice(tts, self.voice, self.use_gpu)
else: else:
@@ -2507,9 +2553,9 @@ class VoicePreviewThread(QThread):
sample_text, voice=loaded_voice, speed=self.speed, split_pattern=None sample_text, voice=loaded_voice, speed=self.speed, split_pattern=None
): ):
audio_segments.append(result.audio) audio_segments.append(result.audio)
if audio_segments: if audio_segments:
audio = self.np_module.concatenate(audio_segments) audio = self.np_module.concatenate(audio_segments)
# Save directly to the cache path
sf.write(self.cache_path, audio, 24000) sf.write(self.cache_path, audio, 24000)
self.temp_wav = self.cache_path self.temp_wav = self.cache_path
self.finished.emit() self.finished.emit()
+202 -9
View File
@@ -9,6 +9,7 @@ from abogen.pyqt.queue_manager_gui import QueueManager
from abogen.pyqt.queued_item import QueuedItem from abogen.pyqt.queued_item import QueuedItem
import abogen.hf_tracker as hf_tracker import abogen.hf_tracker as hf_tracker
import hashlib # Added for cache path generation import hashlib # Added for cache path generation
from abogen.tts_supertonic import SUPERTONIC_AVAILABLE_LANGS, DEFAULT_SUPERTONIC_VOICES
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QApplication,
QWidget, QWidget,
@@ -970,6 +971,9 @@ class abogen(QWidget):
self.fix_nonstandard_punctuation = self.config.get( self.fix_nonstandard_punctuation = self.config.get(
"fix_nonstandard_punctuation", False "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._pending_close_event = None
self.gpu_ok = False # Initialize GPU availability status self.gpu_ok = False # Initialize GPU availability status
@@ -1018,6 +1022,14 @@ class abogen(QWidget):
else: else:
self.mixed_voice_state = entry self.mixed_voice_state = entry
self.selected_lang = entry[0][0] if entry and entry[0] else None 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: if self.save_option == "Choose output folder" and self.selected_output_folder:
self.save_path_label.setText(self.selected_output_folder) self.save_path_label.setText(self.selected_output_folder)
self.save_path_row_widget.show() self.save_path_row_widget.show()
@@ -1107,6 +1119,50 @@ class abogen(QWidget):
speed_layout.addWidget(self.speed_label) speed_layout.addWidget(self.speed_label)
controls_layout.addLayout(speed_layout) controls_layout.addLayout(speed_layout)
self.speed_slider.valueChanged.connect(self.update_speed_label) 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 selection
voice_layout = QHBoxLayout() voice_layout = QHBoxLayout()
voice_layout.setSpacing(7) voice_layout.setSpacing(7)
@@ -1764,6 +1820,12 @@ class abogen(QWidget):
Update the enabled state of subtitle options based on the selected language. 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. 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 # Check if current file is a subtitle file
is_subtitle_input = False is_subtitle_input = False
if self.selected_file and self.selected_file.lower().endswith( if self.selected_file and self.selected_file.lower().endswith(
@@ -1823,6 +1885,48 @@ class abogen(QWidget):
# Enable/disable subtitle options based on language # Enable/disable subtitle options based on language
self.update_subtitle_options_availability() 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): def on_voice_combo_changed(self, index):
data = self.voice_combo.itemData(index) data = self.voice_combo.itemData(index)
if isinstance(data, str) and data.startswith("profile:"): if isinstance(data, str) and data.startswith("profile:"):
@@ -1831,8 +1935,22 @@ class abogen(QWidget):
from abogen.voice_profiles import load_profiles from abogen.voice_profiles import load_profiles
entry = load_profiles().get(pname, {}) entry = load_profiles().get(pname, {})
# set mixed voices and language
if isinstance(entry, dict): if isinstance(entry, dict):
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.mixed_voice_state = entry.get("voices", [])
self.selected_lang = entry.get("language") self.selected_lang = entry.get("language")
else: else:
@@ -1847,7 +1965,12 @@ class abogen(QWidget):
else: else:
self.mixed_voice_state = None self.mixed_voice_state = None
self.selected_profile_name = 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 self.config["selected_voice"] = data
if "selected_profile_name" in self.config: if "selected_profile_name" in self.config:
del self.config["selected_profile_name"] del self.config["selected_profile_name"]
@@ -1866,13 +1989,26 @@ class abogen(QWidget):
def populate_profiles_in_voice_combo(self): def populate_profiles_in_voice_combo(self):
# preserve current voice or profile # preserve current voice or profile
current = self.voice_combo.currentData() current = self.voice_combo.currentData()
provider = self.provider_combo.currentData()
self.voice_combo.blockSignals(True) self.voice_combo.blockSignals(True)
self.voice_combo.clear() self.voice_combo.clear()
# re-add profiles # re-add profiles matching current provider
profile_icon = QIcon(get_resource_path("abogen.assets", "profile.png")) profile_icon = QIcon(get_resource_path("abogen.assets", "profile.png"))
for pname in load_profiles().keys(): 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}") self.voice_combo.addItem(profile_icon, pname, f"profile:{pname}")
# re-add voices # re-add voices
if provider == "supertonic":
for v in DEFAULT_SUPERTONIC_VOICES:
self.voice_combo.addItem(f"{v}", v)
else:
for v in VOICES_INTERNAL: for v in VOICES_INTERNAL:
icon = QIcon() icon = QIcon()
flag_path = get_resource_path("abogen.assets.flags", f"{v[0]}.png") flag_path = get_resource_path("abogen.assets.flags", f"{v[0]}.png")
@@ -2069,6 +2205,9 @@ class abogen(QWidget):
save_base_path=save_base_path, save_base_path=save_base_path,
save_chapters_separately=getattr(self, "save_chapters_separately", None), save_chapters_separately=getattr(self, "save_chapters_separately", None),
merge_chapters_at_end=getattr(self, "merge_chapters_at_end", 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 # Prevent adding duplicate items to the queue
@@ -2212,6 +2351,12 @@ class abogen(QWidget):
self.config["replace_numerals"] = self.replace_numerals self.config["replace_numerals"] = self.replace_numerals
self.config["fix_nonstandard_punctuation"] = self.fix_nonstandard_punctuation 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 # Sync Voice/Profile in config
self.config["selected_voice"] = self.selected_voice self.config["selected_voice"] = self.selected_voice
if "selected_profile_name" in self.config: if "selected_profile_name" in self.config:
@@ -2234,6 +2379,13 @@ class abogen(QWidget):
self.current_queue_index = 0 # Reset for next time self.current_queue_index = 0 # Reset for next time
def get_voice_formula(self) -> str: 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: if self.mixed_voice_state:
formula_components = [ formula_components = [
f"{name}*{weight}" for name, weight in self.mixed_voice_state f"{name}*{weight}" for name, weight in self.mixed_voice_state
@@ -2243,6 +2395,8 @@ class abogen(QWidget):
return self.selected_voice return self.selected_voice
def get_selected_lang(self, voice_formula) -> str: 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: if self.selected_profile_name:
from abogen.voice_profiles import load_profiles 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 # determine selected language: use profile setting if profile selected, else voice code
selected_lang = self.get_selected_lang(voice_formula) 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.conversion_thread = ConversionThread(
self.selected_file, self.selected_file,
selected_lang, selected_lang,
@@ -2347,8 +2505,11 @@ class abogen(QWidget):
total_char_count=self.char_count, total_char_count=self.char_count,
use_gpu=self.gpu_ok, use_gpu=self.gpu_ok,
from_queue=from_queue, from_queue=from_queue,
save_base_path=self.displayed_file_path, # Pass the save base path (original file for EPUB) save_base_path=self.displayed_file_path,
) # Use gpu_ok status 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 # Pass the displayed file path to the log_updated signal handler in ConversionThread
self.conversion_thread.display_path = display_path self.conversion_thread.display_path = display_path
# Pass the file size string # Pass the file size string
@@ -2425,6 +2586,11 @@ class abogen(QWidget):
# Store gpu_ok status to use when creating the conversion thread # Store gpu_ok status to use when creating the conversion thread
self.gpu_ok = gpu_ok self.gpu_ok = gpu_ok
self.update_log((gpu_msg, gpu_ok)) 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)
else:
self.update_log("Loading modules...") self.update_log("Loading modules...")
load_thread = LoadPipelineThread(pipeline_loaded_callback) load_thread = LoadPipelineThread(pipeline_loaded_callback)
load_thread.start() load_thread.start()
@@ -2743,6 +2909,16 @@ class abogen(QWidget):
def _get_preview_cache_path(self): def _get_preview_cache_path(self):
"""Generate the expected cache path for the current voice settings.""" """Generate the expected cache path for the current voice settings."""
speed = self.speed_slider.value() / 100.0 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 = "" voice_to_cache = ""
lang_to_cache = "" lang_to_cache = ""
@@ -2847,6 +3023,12 @@ class abogen(QWidget):
self.btn_voice_formula_mixer.setEnabled(False) # Disable mixer button self.btn_voice_formula_mixer.setEnabled(False) # Disable mixer button
self.btn_start.setEnabled(False) # Disable start button during preview 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 # Start loading animation - ensure signal connection is always active
if hasattr(self, "loading_movie"): if hasattr(self, "loading_movie"):
# Disconnect previous connections to avoid multiple connections # Disconnect previous connections to avoid multiple connections
@@ -2905,14 +3087,25 @@ class abogen(QWidget):
else None else None
) )
else: else:
lang = self.selected_voice[0] voice = self.selected_voice or ""
voice = self.selected_voice
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 # use same gpu/cpu logic as in conversion
gpu_msg, gpu_ok = get_gpu_acceleration(self.use_gpu) gpu_msg, gpu_ok = get_gpu_acceleration(self.use_gpu)
self.preview_thread = VoicePreviewThread( 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.finished.connect(self._play_preview_audio)
self.preview_thread.error.connect(self._preview_error) self.preview_thread.error.connect(self._preview_error)
+4
View File
@@ -26,3 +26,7 @@ class QueuedItem:
replace_all_caps: bool = False replace_all_caps: bool = False
replace_numerals: bool = False replace_numerals: bool = False
fix_nonstandard_punctuation: bool = False fix_nonstandard_punctuation: bool = False
# TTS Provider fields
tts_provider: str = "kokoro"
supertonic_language: str = "en"
supertonic_total_steps: int = 5
+16 -1
View File
@@ -4,6 +4,7 @@ import ast
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
import math import math
import os
import re import re
from typing import Any, Iterable, Iterator, Optional 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") 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 @dataclass
class SupertonicSegment: class SupertonicSegment:
@@ -166,10 +174,13 @@ class SupertonicPipeline:
auto_download: bool = True, auto_download: bool = True,
total_steps: int = 5, total_steps: int = 5,
max_chunk_length: int = 300, max_chunk_length: int = 300,
lang: str = "en",
intra_op_num_threads: Optional[int] = None,
) -> None: ) -> None:
self.sample_rate = int(sample_rate) self.sample_rate = int(sample_rate)
self.total_steps = int(total_steps) self.total_steps = int(total_steps)
self.max_chunk_length = int(max_chunk_length) self.max_chunk_length = int(max_chunk_length)
self.lang = str(lang or "en")
# Configure GPU providers before importing TTS # Configure GPU providers before importing TTS
_configure_supertonic_gpu() _configure_supertonic_gpu()
@@ -181,7 +192,8 @@ class SupertonicPipeline:
"Supertonic is not installed. Install it with `pip install supertonic`." "Supertonic is not installed. Install it with `pip install supertonic`."
) from exc ) 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__( def __call__(
self, self,
@@ -191,12 +203,14 @@ class SupertonicPipeline:
speed: float, speed: float,
split_pattern: Optional[str] = None, split_pattern: Optional[str] = None,
total_steps: Optional[int] = None, total_steps: Optional[int] = None,
lang: Optional[str] = None,
) -> Iterator[SupertonicSegment]: ) -> Iterator[SupertonicSegment]:
voice_name = (voice or "").strip() or "M1" voice_name = (voice or "").strip() or "M1"
steps = int(total_steps) if total_steps is not None else self.total_steps steps = int(total_steps) if total_steps is not None else self.total_steps
steps = max(2, min(15, steps)) steps = max(2, min(15, steps))
speed_value = float(speed) if speed is not None else 1.0 speed_value = float(speed) if speed is not None else 1.0
speed_value = max(0.7, min(2.0, speed_value)) 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) style = self._tts.get_voice_style(voice_name=voice_name)
chunks = _split_text( chunks = _split_text(
@@ -213,6 +227,7 @@ class SupertonicPipeline:
wav, duration = self._tts.synthesize( wav, duration = self._tts.synthesize(
text=chunk_to_speak, text=chunk_to_speak,
voice_style=style, voice_style=style,
lang=language,
total_steps=steps, total_steps=steps,
speed=speed_value, speed=speed_value,
max_chunk_length=self.max_chunk_length, max_chunk_length=self.max_chunk_length,