mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 05:40:26 +02:00
Add Supertonic TTS provider support and configuration options
This commit is contained in:
+73
-27
@@ -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()
|
||||
|
||||
+214
-21
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user