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