mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
set default steps to 8, updated supertonic rate from 24000 to 44100 - #163
This commit is contained in:
+2
-2
@@ -43,8 +43,8 @@ KOKORO_LANG_TO_COUNTRY = {
|
|||||||
"z": "cn", # Mandarin Chinese -> China
|
"z": "cn", # Mandarin Chinese -> China
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mapping from SuperTonic ISO 639-1 language codes to ISO 3166-1 alpha-2 country codes
|
# 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
|
# Used for loading flag icons in the Supertonic language picker
|
||||||
SUPERTONIC_LANG_TO_COUNTRY = {
|
SUPERTONIC_LANG_TO_COUNTRY = {
|
||||||
"en": "gb",
|
"en": "gb",
|
||||||
"ko": "kr",
|
"ko": "kr",
|
||||||
|
|||||||
+25
-67
@@ -269,7 +269,7 @@ class ConversionThread(QThread):
|
|||||||
save_base_path=None,
|
save_base_path=None,
|
||||||
tts_provider="kokoro",
|
tts_provider="kokoro",
|
||||||
supertonic_language="en",
|
supertonic_language="en",
|
||||||
supertonic_total_steps=5,
|
supertonic_total_steps=8,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._chapter_options_event = threading.Event()
|
self._chapter_options_event = threading.Event()
|
||||||
@@ -283,6 +283,7 @@ class ConversionThread(QThread):
|
|||||||
self.tts_provider = tts_provider
|
self.tts_provider = tts_provider
|
||||||
self.supertonic_language = supertonic_language
|
self.supertonic_language = supertonic_language
|
||||||
self.supertonic_total_steps = supertonic_total_steps
|
self.supertonic_total_steps = supertonic_total_steps
|
||||||
|
self.sample_rate = 44100 if tts_provider == "supertonic" else 24000
|
||||||
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
|
||||||
@@ -512,7 +513,7 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
if self.tts_provider == "supertonic":
|
if self.tts_provider == "supertonic":
|
||||||
tts = SupertonicPipeline(
|
tts = SupertonicPipeline(
|
||||||
sample_rate=24000,
|
sample_rate=self.sample_rate,
|
||||||
lang=self.supertonic_language,
|
lang=self.supertonic_language,
|
||||||
total_steps=self.supertonic_total_steps,
|
total_steps=self.supertonic_total_steps,
|
||||||
)
|
)
|
||||||
@@ -765,7 +766,7 @@ class ConversionThread(QThread):
|
|||||||
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
|
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
|
||||||
subtitle_entries = []
|
subtitle_entries = []
|
||||||
current_time = 0.0
|
current_time = 0.0
|
||||||
rate = 24000
|
rate = self.sample_rate
|
||||||
subtitle_mode = self.subtitle_mode
|
subtitle_mode = self.subtitle_mode
|
||||||
self.etr_start_time = time.time()
|
self.etr_start_time = time.time()
|
||||||
self.processed_char_count = 0
|
self.processed_char_count = 0
|
||||||
@@ -781,7 +782,7 @@ class ConversionThread(QThread):
|
|||||||
merged_out_file = sf.SoundFile(
|
merged_out_file = sf.SoundFile(
|
||||||
merged_out_path,
|
merged_out_path,
|
||||||
"w",
|
"w",
|
||||||
samplerate=24000,
|
samplerate=self.sample_rate,
|
||||||
channels=1,
|
channels=1,
|
||||||
format=self.output_format,
|
format=self.output_format,
|
||||||
)
|
)
|
||||||
@@ -797,62 +798,18 @@ class ConversionThread(QThread):
|
|||||||
# Prepare ffmpeg command for m4b output
|
# Prepare ffmpeg command for m4b output
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-y",
|
"-y",
|
||||||
"-thread_queue_size",
|
"-thread_queue_size",
|
||||||
"32768",
|
"32768",
|
||||||
"-f",
|
"-f",
|
||||||
"f32le",
|
"f32le",
|
||||||
"-ar",
|
"-ar",
|
||||||
"24000",
|
str(self.sample_rate),
|
||||||
"-ac",
|
"-ac",
|
||||||
"1",
|
"1",
|
||||||
"-i",
|
"-i",
|
||||||
"pipe:0",
|
"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",
|
|
||||||
]
|
]
|
||||||
)
|
|
||||||
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.extend(["-c:a", "libopus", "-b:a", "24000"])
|
||||||
cmd.append(merged_out_path)
|
cmd.append(merged_out_path)
|
||||||
ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False)
|
ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False)
|
||||||
@@ -934,7 +891,7 @@ class ConversionThread(QThread):
|
|||||||
merged_out_path = None
|
merged_out_path = None
|
||||||
subtitle_entries = []
|
subtitle_entries = []
|
||||||
current_time = 0.0
|
current_time = 0.0
|
||||||
rate = 24000
|
rate = self.sample_rate
|
||||||
subtitle_mode = self.subtitle_mode
|
subtitle_mode = self.subtitle_mode
|
||||||
self.etr_start_time = time.time()
|
self.etr_start_time = time.time()
|
||||||
self.processed_char_count = 0
|
self.processed_char_count = 0
|
||||||
@@ -987,7 +944,7 @@ class ConversionThread(QThread):
|
|||||||
chapter_out_file = sf.SoundFile(
|
chapter_out_file = sf.SoundFile(
|
||||||
chapter_out_path,
|
chapter_out_path,
|
||||||
"w",
|
"w",
|
||||||
samplerate=24000,
|
samplerate=self.sample_rate,
|
||||||
channels=1,
|
channels=1,
|
||||||
format=separate_chapters_format,
|
format=separate_chapters_format,
|
||||||
)
|
)
|
||||||
@@ -1002,7 +959,7 @@ class ConversionThread(QThread):
|
|||||||
"-f",
|
"-f",
|
||||||
"f32le",
|
"f32le",
|
||||||
"-ar",
|
"-ar",
|
||||||
"24000",
|
str(self.sample_rate),
|
||||||
"-ac",
|
"-ac",
|
||||||
"1",
|
"1",
|
||||||
"-i",
|
"-i",
|
||||||
@@ -1384,7 +1341,7 @@ class ConversionThread(QThread):
|
|||||||
# Add silence between chapters for merged output (except after the last chapter)
|
# Add silence between chapters for merged output (except after the last chapter)
|
||||||
if merge_chapters_at_end and chapter_idx < total_chapters:
|
if merge_chapters_at_end and chapter_idx < total_chapters:
|
||||||
silence_samples = int(
|
silence_samples = int(
|
||||||
self.silence_duration * 24000
|
self.silence_duration * self.sample_rate
|
||||||
) # Silence duration at 24,000 Hz
|
) # Silence duration at 24,000 Hz
|
||||||
silence_audio = self.np.zeros(silence_samples, dtype="float32")
|
silence_audio = self.np.zeros(silence_samples, dtype="float32")
|
||||||
silence_bytes = silence_audio.tobytes()
|
silence_bytes = silence_audio.tobytes()
|
||||||
@@ -1615,7 +1572,7 @@ class ConversionThread(QThread):
|
|||||||
parent_dir, f"{sanitized_base_name}{suffix}"
|
parent_dir, f"{sanitized_base_name}{suffix}"
|
||||||
)
|
)
|
||||||
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
|
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
|
||||||
rate = 24000
|
rate = self.sample_rate
|
||||||
|
|
||||||
# Setup audio output
|
# Setup audio output
|
||||||
merged_out_file, ffmpeg_proc = None, None
|
merged_out_file, ffmpeg_proc = None, None
|
||||||
@@ -2467,7 +2424,7 @@ class VoicePreviewThread(QThread):
|
|||||||
parent=None,
|
parent=None,
|
||||||
tts_provider="kokoro",
|
tts_provider="kokoro",
|
||||||
supertonic_language="en",
|
supertonic_language="en",
|
||||||
supertonic_total_steps=5,
|
supertonic_total_steps=8,
|
||||||
):
|
):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.np_module = np_module
|
self.np_module = np_module
|
||||||
@@ -2479,6 +2436,7 @@ class VoicePreviewThread(QThread):
|
|||||||
self.tts_provider = tts_provider
|
self.tts_provider = tts_provider
|
||||||
self.supertonic_language = supertonic_language
|
self.supertonic_language = supertonic_language
|
||||||
self.supertonic_total_steps = supertonic_total_steps
|
self.supertonic_total_steps = supertonic_total_steps
|
||||||
|
self.sample_rate = 44100 if tts_provider == "supertonic" else 24000
|
||||||
|
|
||||||
# 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")
|
||||||
@@ -2516,7 +2474,7 @@ class VoicePreviewThread(QThread):
|
|||||||
from abogen.tts_supertonic import SupertonicPipeline
|
from abogen.tts_supertonic import SupertonicPipeline
|
||||||
|
|
||||||
tts = SupertonicPipeline(
|
tts = SupertonicPipeline(
|
||||||
sample_rate=24000,
|
sample_rate=self.sample_rate,
|
||||||
lang=self.supertonic_language,
|
lang=self.supertonic_language,
|
||||||
total_steps=self.supertonic_total_steps,
|
total_steps=self.supertonic_total_steps,
|
||||||
)
|
)
|
||||||
@@ -2556,7 +2514,7 @@ class VoicePreviewThread(QThread):
|
|||||||
|
|
||||||
if audio_segments:
|
if audio_segments:
|
||||||
audio = self.np_module.concatenate(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.temp_wav = self.cache_path
|
||||||
self.finished.emit()
|
self.finished.emit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+62
-33
@@ -975,7 +975,7 @@ class abogen(QWidget):
|
|||||||
)
|
)
|
||||||
self.tts_provider_config = self.config.get("tts_provider", "kokoro")
|
self.tts_provider_config = self.config.get("tts_provider", "kokoro")
|
||||||
self.supertonic_language_config = self.config.get("supertonic_language", "en")
|
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._pending_close_event = None
|
||||||
self.gpu_ok = False # Initialize GPU availability status
|
self.gpu_ok = False # Initialize GPU availability status
|
||||||
|
|
||||||
@@ -1025,12 +1025,14 @@ class abogen(QWidget):
|
|||||||
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
|
# 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)
|
idx_st = self.provider_combo.findText(provider_text)
|
||||||
if idx_st >= 0:
|
if idx_st >= 0:
|
||||||
self.provider_combo.setCurrentIndex(idx_st)
|
self.provider_combo.setCurrentIndex(idx_st)
|
||||||
self.st_lang_combo.setCurrentText(self.supertonic_language_config)
|
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:
|
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)
|
||||||
@@ -1129,13 +1131,13 @@ class abogen(QWidget):
|
|||||||
provider_layout.addWidget(provider_label)
|
provider_layout.addWidget(provider_label)
|
||||||
self.provider_combo = QComboBox(self)
|
self.provider_combo = QComboBox(self)
|
||||||
self.provider_combo.addItem("Kokoro", "kokoro")
|
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.setStyleSheet("QComboBox { min-height: 20px; padding: 6px 12px; }")
|
||||||
self.provider_combo.currentIndexChanged.connect(self.on_provider_changed)
|
self.provider_combo.currentIndexChanged.connect(self.on_provider_changed)
|
||||||
provider_layout.addWidget(self.provider_combo)
|
provider_layout.addWidget(self.provider_combo)
|
||||||
controls_layout.addLayout(provider_layout)
|
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()
|
self.supertonic_row = QWidget()
|
||||||
supertonic_row_layout = QHBoxLayout(self.supertonic_row)
|
supertonic_row_layout = QHBoxLayout(self.supertonic_row)
|
||||||
supertonic_row_layout.setContentsMargins(0, 0, 0, 0)
|
supertonic_row_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
@@ -1156,13 +1158,13 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
st_steps_label = QLabel("Steps:", self)
|
st_steps_label = QLabel("Steps:", self)
|
||||||
supertonic_row_layout.addWidget(st_steps_label)
|
supertonic_row_layout.addWidget(st_steps_label)
|
||||||
self.st_steps_spin = QSpinBox(self)
|
self.st_steps_combo = QComboBox(self)
|
||||||
self.st_steps_spin.setMinimum(1)
|
for val in range(2, 16):
|
||||||
self.st_steps_spin.setMaximum(10)
|
self.st_steps_combo.addItem(str(val), val)
|
||||||
self.st_steps_spin.setValue(5)
|
self.st_steps_combo.setCurrentIndex(self.st_steps_combo.findData(8))
|
||||||
self.st_steps_spin.setFixedWidth(60)
|
self.st_steps_combo.setStyleSheet("QComboBox { min-height: 20px; padding: 6px 12px; }")
|
||||||
self.st_steps_spin.valueChanged.connect(self._on_st_steps_changed)
|
self.st_steps_combo.currentIndexChanged.connect(self._on_st_steps_changed)
|
||||||
supertonic_row_layout.addWidget(self.st_steps_spin)
|
supertonic_row_layout.addWidget(self.st_steps_combo)
|
||||||
|
|
||||||
supertonic_row_layout.addStretch()
|
supertonic_row_layout.addStretch()
|
||||||
self.supertonic_row.hide()
|
self.supertonic_row.hide()
|
||||||
@@ -1896,7 +1898,7 @@ class abogen(QWidget):
|
|||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
is_supertonic = provider == "supertonic"
|
is_supertonic = provider == "supertonic"
|
||||||
|
|
||||||
# Show/hide SuperTonic controls
|
# Show/hide Supertonic controls
|
||||||
self.supertonic_row.setVisible(is_supertonic)
|
self.supertonic_row.setVisible(is_supertonic)
|
||||||
|
|
||||||
# Update subtitles availability
|
# Update subtitles availability
|
||||||
@@ -1910,7 +1912,7 @@ class abogen(QWidget):
|
|||||||
self.mixed_voice_state = None
|
self.mixed_voice_state = None
|
||||||
self.btn_voice_formula_mixer.setEnabled(False)
|
self.btn_voice_formula_mixer.setEnabled(False)
|
||||||
self.voice_combo.setToolTip(
|
self.voice_combo.setToolTip(
|
||||||
"SuperTonic voices:\n"
|
"Supertonic voices:\n"
|
||||||
"M1-M5 = Male voices\n"
|
"M1-M5 = Male voices\n"
|
||||||
"F1-F5 = Female voices"
|
"F1-F5 = Female voices"
|
||||||
)
|
)
|
||||||
@@ -1928,8 +1930,8 @@ class abogen(QWidget):
|
|||||||
self.selected_lang = lang
|
self.selected_lang = lang
|
||||||
self.update_subtitle_options_availability()
|
self.update_subtitle_options_availability()
|
||||||
|
|
||||||
def _on_st_steps_changed(self, steps):
|
def _on_st_steps_changed(self):
|
||||||
self.config["supertonic_total_steps"] = steps
|
self.config["supertonic_total_steps"] = self.st_steps_combo.currentData()
|
||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
|
|
||||||
def on_voice_combo_changed(self, index):
|
def on_voice_combo_changed(self, index):
|
||||||
@@ -1943,7 +1945,7 @@ class abogen(QWidget):
|
|||||||
if isinstance(entry, dict):
|
if isinstance(entry, dict):
|
||||||
entry_provider = str(entry.get("provider", "")).strip().lower()
|
entry_provider = str(entry.get("provider", "")).strip().lower()
|
||||||
if entry_provider == "supertonic":
|
if entry_provider == "supertonic":
|
||||||
# Switch provider to SuperTonic if not already
|
# Switch provider to Supertonic if not already
|
||||||
if self.provider_combo.currentData() != "supertonic":
|
if self.provider_combo.currentData() != "supertonic":
|
||||||
self.provider_combo.setCurrentIndex(1)
|
self.provider_combo.setCurrentIndex(1)
|
||||||
self.mixed_voice_state = None
|
self.mixed_voice_state = None
|
||||||
@@ -1951,7 +1953,9 @@ class abogen(QWidget):
|
|||||||
# Sync supertonic controls from profile
|
# Sync supertonic controls from profile
|
||||||
profile_steps = entry.get("total_steps")
|
profile_steps = entry.get("total_steps")
|
||||||
if profile_steps is not None:
|
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")
|
profile_lang = entry.get("language")
|
||||||
if profile_lang and profile_lang in SUPERTONIC_AVAILABLE_LANGS:
|
if profile_lang and profile_lang in SUPERTONIC_AVAILABLE_LANGS:
|
||||||
self.st_lang_combo.setCurrentText(profile_lang)
|
self.st_lang_combo.setCurrentText(profile_lang)
|
||||||
@@ -2012,7 +2016,14 @@ class abogen(QWidget):
|
|||||||
# re-add voices
|
# re-add voices
|
||||||
if provider == "supertonic":
|
if provider == "supertonic":
|
||||||
for v in DEFAULT_SUPERTONIC_VOICES:
|
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:
|
else:
|
||||||
for v in VOICES_INTERNAL:
|
for v in VOICES_INTERNAL:
|
||||||
icon = QIcon()
|
icon = QIcon()
|
||||||
@@ -2213,7 +2224,7 @@ class abogen(QWidget):
|
|||||||
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(),
|
tts_provider=self.provider_combo.currentData(),
|
||||||
supertonic_language=self.st_lang_combo.currentText(),
|
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
|
# Prevent adding duplicate items to the queue
|
||||||
@@ -2359,9 +2370,12 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
# TTS provider settings
|
# TTS provider settings
|
||||||
tts_provider = getattr(queued_item, "tts_provider", "kokoro")
|
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_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
|
# Sync Voice/Profile in config
|
||||||
self.config["selected_voice"] = self.selected_voice
|
self.config["selected_voice"] = self.selected_voice
|
||||||
@@ -2390,8 +2404,11 @@ class abogen(QWidget):
|
|||||||
from abogen.voice_profiles import load_profiles
|
from abogen.voice_profiles import load_profiles
|
||||||
entry = load_profiles().get(self.selected_profile_name, {})
|
entry = load_profiles().get(self.selected_profile_name, {})
|
||||||
if isinstance(entry, dict):
|
if isinstance(entry, dict):
|
||||||
return str(entry.get("voice", self.selected_voice or "M1"))
|
return str(entry.get("voice", "M1"))
|
||||||
return 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:"):
|
||||||
|
return current_data
|
||||||
|
return "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
|
||||||
@@ -2494,7 +2511,7 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
tts_provider = self.provider_combo.currentData()
|
tts_provider = self.provider_combo.currentData()
|
||||||
supertonic_language = self.st_lang_combo.currentText()
|
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.conversion_thread = ConversionThread(
|
||||||
self.selected_file,
|
self.selected_file,
|
||||||
@@ -2594,8 +2611,9 @@ class abogen(QWidget):
|
|||||||
self.update_log((gpu_msg, gpu_ok))
|
self.update_log((gpu_msg, gpu_ok))
|
||||||
tts_provider = self.provider_combo.currentData()
|
tts_provider = self.provider_combo.currentData()
|
||||||
if tts_provider == "supertonic":
|
if tts_provider == "supertonic":
|
||||||
# SuperTonic doesn't need KPipeline, call callback directly
|
# Supertonic doesn't need KPipeline, call callback directly
|
||||||
pipeline_loaded_callback(None, None, None)
|
import numpy as np
|
||||||
|
pipeline_loaded_callback(np, None, None)
|
||||||
else:
|
else:
|
||||||
self.update_log("Loading modules...")
|
self.update_log("Loading modules...")
|
||||||
load_thread = LoadPipelineThread(pipeline_loaded_callback)
|
load_thread = LoadPipelineThread(pipeline_loaded_callback)
|
||||||
@@ -2918,9 +2936,13 @@ class abogen(QWidget):
|
|||||||
provider = self.provider_combo.currentData()
|
provider = self.provider_combo.currentData()
|
||||||
|
|
||||||
if provider == "supertonic":
|
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()
|
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")
|
cache_dir = get_user_cache_path("preview_cache")
|
||||||
filename = f"st_{voice_to_cache}_{lang_to_cache}_steps{steps}_{speed:.2f}.wav"
|
filename = f"st_{voice_to_cache}_{lang_to_cache}_steps{steps}_{speed:.2f}.wav"
|
||||||
return os.path.join(cache_dir, filename)
|
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_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
|
# For Supertonic, skip KPipeline loading and use SupertonicPipeline directly
|
||||||
if self.provider_combo.currentData() == "supertonic":
|
if self.provider_combo.currentData() == "supertonic":
|
||||||
|
import numpy as np
|
||||||
self.loading_movie.start()
|
self.loading_movie.start()
|
||||||
self._on_pipeline_loaded_for_preview(None, None, None)
|
self._on_pipeline_loaded_for_preview(np, None, None)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Start loading animation - ensure signal connection is always active
|
# Start loading animation - ensure signal connection is always active
|
||||||
@@ -3093,11 +3116,17 @@ class abogen(QWidget):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
else:
|
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()
|
tts_provider = self.provider_combo.currentData()
|
||||||
supertonic_language = self.st_lang_combo.currentText()
|
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":
|
if tts_provider == "supertonic":
|
||||||
lang = supertonic_language
|
lang = supertonic_language
|
||||||
|
|||||||
@@ -29,4 +29,4 @@ class QueuedItem:
|
|||||||
# TTS Provider fields
|
# TTS Provider fields
|
||||||
tts_provider: str = "kokoro"
|
tts_provider: str = "kokoro"
|
||||||
supertonic_language: str = "en"
|
supertonic_language: str = "en"
|
||||||
supertonic_total_steps: int = 5
|
supertonic_total_steps: int = 8
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ _UNSUPPORTED_CHARS_RE = re.compile(
|
|||||||
|
|
||||||
|
|
||||||
def _parse_unsupported_characters(error: BaseException) -> list[str]:
|
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(
|
message = " ".join(
|
||||||
str(part) for part in getattr(error, "args", ()) if part is not None
|
str(part) for part in getattr(error, "args", ()) if part is not None
|
||||||
@@ -221,7 +221,7 @@ class SupertonicPipeline:
|
|||||||
removed: set[str] = set()
|
removed: set[str] = set()
|
||||||
last_exc: Exception | None = None
|
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):
|
for attempt in range(3):
|
||||||
try:
|
try:
|
||||||
wav, duration = self._tts.synthesize(
|
wav, duration = self._tts.synthesize(
|
||||||
@@ -253,14 +253,14 @@ class SupertonicPipeline:
|
|||||||
chunk_to_speak = sanitized
|
chunk_to_speak = sanitized
|
||||||
if not chunk_to_speak:
|
if not chunk_to_speak:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"SuperTonic: dropped a chunk after removing unsupported characters: %s",
|
"Supertonic: dropped a chunk after removing unsupported characters: %s",
|
||||||
sorted(removed),
|
sorted(removed),
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
if attempt == 0:
|
if attempt == 0:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"SuperTonic: removed unsupported characters %s and retried.",
|
"Supertonic: removed unsupported characters %s and retried.",
|
||||||
sorted(removed),
|
sorted(removed),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ def _supertonic_voice_from_spec(spec: Any, fallback: str) -> str:
|
|||||||
raw = str(spec or "").strip()
|
raw = str(spec or "").strip()
|
||||||
fallback_raw = str(fallback 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.
|
# formula (contains '*' or '+'), ignore it and fall back to a safe voice.
|
||||||
if not raw or "*" in raw or "+" in raw:
|
if not raw or "*" in raw or "+" in raw:
|
||||||
raw = fallback_raw
|
raw = fallback_raw
|
||||||
@@ -1584,7 +1584,7 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
pipelines[provider_norm] = SupertonicPipeline(
|
pipelines[provider_norm] = SupertonicPipeline(
|
||||||
sample_rate=SAMPLE_RATE,
|
sample_rate=SAMPLE_RATE,
|
||||||
auto_download=True,
|
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]
|
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"
|
provider = str(entry.get("provider") or "kokoro").strip().lower() or "kokoro"
|
||||||
if provider == "supertonic":
|
if provider == "supertonic":
|
||||||
voice = str(entry.get("voice") or getattr(job, "voice", "M1") or "M1").strip() or "M1"
|
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)
|
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
|
return "supertonic", _supertonic_voice_from_spec(voice, getattr(job, "voice", "M1")), speed, steps
|
||||||
formula = _formula_from_kokoro_entry(entry)
|
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).
|
"""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 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)
|
provider, resolved, speed, steps = resolve_voice_target(raw_spec)
|
||||||
@@ -1857,7 +1857,7 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
voice=voice_name,
|
voice=voice_name,
|
||||||
speed=float(speed_override if speed_override is not None else job.speed),
|
speed=float(speed_override if speed_override is not None else job.speed),
|
||||||
split_pattern=split_pattern,
|
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:
|
else:
|
||||||
kokoro_pipeline = get_pipeline("kokoro")
|
kokoro_pipeline = get_pipeline("kokoro")
|
||||||
@@ -2448,7 +2448,7 @@ def _load_pipeline(job: Job):
|
|||||||
return SupertonicPipeline(
|
return SupertonicPipeline(
|
||||||
sample_rate=SAMPLE_RATE,
|
sample_rate=SAMPLE_RATE,
|
||||||
auto_download=True,
|
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"
|
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):
|
def _resolve_voice(pipeline, voice_spec: str, use_gpu: bool):
|
||||||
if "*" in voice_spec:
|
if "*" in voice_spec:
|
||||||
# Voice formulas are a Kokoro-only feature (they require a pipeline that can
|
# 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
|
# pipeline is otherwise unavailable), treat the spec as a plain string and
|
||||||
# allow downstream provider-specific resolution to choose a safe fallback.
|
# allow downstream provider-specific resolution to choose a safe fallback.
|
||||||
if pipeline is None or not hasattr(pipeline, "load_single_voice"):
|
if pipeline is None or not hasattr(pipeline, "load_single_voice"):
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ def api_voice_profiles_preview() -> ResponseReturnValue:
|
|||||||
formula = str(payload.get("formula") or "").strip()
|
formula = str(payload.get("formula") or "").strip()
|
||||||
profile_name = str(payload.get("profile") 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
|
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 = ""
|
voice_spec = ""
|
||||||
resolved_provider = provider or "kokoro"
|
resolved_provider = provider or "kokoro"
|
||||||
@@ -224,7 +224,7 @@ def api_speaker_preview() -> ResponseReturnValue:
|
|||||||
speed_value = payload.get("speed")
|
speed_value = payload.get("speed")
|
||||||
speed = coerce_float(speed_value, 1.0)
|
speed = coerce_float(speed_value, 1.0)
|
||||||
tts_provider = str(payload.get("tts_provider") or "").strip().lower()
|
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()
|
settings = load_settings()
|
||||||
use_gpu = settings.get("use_gpu", False)
|
use_gpu = settings.get("use_gpu", False)
|
||||||
@@ -269,7 +269,7 @@ def api_speaker_preview() -> ResponseReturnValue:
|
|||||||
use_gpu=use_gpu
|
use_gpu=use_gpu
|
||||||
,
|
,
|
||||||
tts_provider=resolved_provider,
|
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,
|
pronunciation_overrides=pronunciation_overrides,
|
||||||
manual_overrides=manual_overrides,
|
manual_overrides=manual_overrides,
|
||||||
speakers=speakers,
|
speakers=speakers,
|
||||||
|
|||||||
@@ -43,8 +43,12 @@ def update_settings() -> ResponseReturnValue:
|
|||||||
current["language"] = (form.get("language") or "en").strip()
|
current["language"] = (form.get("language") or "en").strip()
|
||||||
current["default_speaker"] = (form.get("default_speaker") or "").strip()
|
current["default_speaker"] = (form.get("default_speaker") or "").strip()
|
||||||
current["default_voice"] = (form.get("default_voice") 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:
|
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):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -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
|
# 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
|
# 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).
|
# 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()
|
provider_value = str(form.get("tts_provider") or "").strip().lower()
|
||||||
if provider_value in {"kokoro", "supertonic"}:
|
if provider_value in {"kokoro", "supertonic"}:
|
||||||
pending.tts_provider = provider_value
|
pending.tts_provider = provider_value
|
||||||
@@ -913,6 +913,15 @@ def build_pending_job_from_extraction(
|
|||||||
else:
|
else:
|
||||||
normalization_overrides[key] = default_val
|
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(
|
pending = PendingJob(
|
||||||
id=uuid.uuid4().hex,
|
id=uuid.uuid4().hex,
|
||||||
original_filename=original_name,
|
original_filename=original_name,
|
||||||
@@ -928,6 +937,8 @@ def build_pending_job_from_extraction(
|
|||||||
replace_single_newlines=replace_single_newlines,
|
replace_single_newlines=replace_single_newlines,
|
||||||
subtitle_format=subtitle_format,
|
subtitle_format=subtitle_format,
|
||||||
total_characters=total_chars,
|
total_characters=total_chars,
|
||||||
|
tts_provider=provider_value,
|
||||||
|
supertonic_total_steps=supertonic_steps,
|
||||||
save_chapters_separately=save_chapters_separately,
|
save_chapters_separately=save_chapters_separately,
|
||||||
merge_chapters_at_end=merge_chapters_at_end,
|
merge_chapters_at_end=merge_chapters_at_end,
|
||||||
separate_chapters_format=separate_chapters_format,
|
separate_chapters_format=separate_chapters_format,
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ def generate_preview_audio(
|
|||||||
speed: float,
|
speed: float,
|
||||||
use_gpu: bool,
|
use_gpu: bool,
|
||||||
tts_provider: str = "kokoro",
|
tts_provider: str = "kokoro",
|
||||||
supertonic_total_steps: int = 5,
|
supertonic_total_steps: int = 8,
|
||||||
max_seconds: float = 8.0,
|
max_seconds: float = 8.0,
|
||||||
pronunciation_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
pronunciation_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||||
manual_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
manual_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||||
@@ -201,7 +201,7 @@ def synthesize_preview(
|
|||||||
speed: float,
|
speed: float,
|
||||||
use_gpu: bool,
|
use_gpu: bool,
|
||||||
tts_provider: str = "kokoro",
|
tts_provider: str = "kokoro",
|
||||||
supertonic_total_steps: int = 5,
|
supertonic_total_steps: int = 8,
|
||||||
max_seconds: float = 8.0,
|
max_seconds: float = 8.0,
|
||||||
pronunciation_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
pronunciation_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||||
manual_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
manual_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def submit_job(pending: PendingJob) -> str:
|
|||||||
tts_provider=getattr(pending, "tts_provider", "kokoro"),
|
tts_provider=getattr(pending, "tts_provider", "kokoro"),
|
||||||
voice=pending.voice,
|
voice=pending.voice,
|
||||||
speed=pending.speed,
|
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,
|
use_gpu=pending.use_gpu,
|
||||||
subtitle_mode=pending.subtitle_mode,
|
subtitle_mode=pending.subtitle_mode,
|
||||||
output_format=pending.output_format,
|
output_format=pending.output_format,
|
||||||
|
|||||||
@@ -175,7 +175,8 @@ def settings_defaults() -> Dict[str, Any]:
|
|||||||
"save_mode": "default_output" if has_output_override() else "save_next_to_input",
|
"save_mode": "default_output" if has_output_override() else "save_next_to_input",
|
||||||
"default_speaker": "",
|
"default_speaker": "",
|
||||||
"default_voice": VOICES_INTERNAL[0] if VOICES_INTERNAL else "",
|
"default_voice": VOICES_INTERNAL[0] if VOICES_INTERNAL else "",
|
||||||
"supertonic_total_steps": 5,
|
"tts_provider": "kokoro",
|
||||||
|
"supertonic_total_steps": 8,
|
||||||
"supertonic_speed": 1.0,
|
"supertonic_speed": 1.0,
|
||||||
"replace_single_newlines": False,
|
"replace_single_newlines": False,
|
||||||
"use_gpu": True,
|
"use_gpu": True,
|
||||||
|
|||||||
@@ -666,7 +666,7 @@ def resolve_voice_choice(
|
|||||||
|
|
||||||
# Provider-aware behavior:
|
# Provider-aware behavior:
|
||||||
# - Kokoro profiles typically represent mixes (formula strings).
|
# - 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
|
# In that case, we return a speaker reference so downstream can
|
||||||
# resolve provider per-speaker and allow mixed-provider casting.
|
# resolve provider per-speaker and allow mixed-provider casting.
|
||||||
if provider == "supertonic":
|
if provider == "supertonic":
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class Job:
|
|||||||
subtitle_format: str
|
subtitle_format: str
|
||||||
created_at: float
|
created_at: float
|
||||||
tts_provider: str = "kokoro"
|
tts_provider: str = "kokoro"
|
||||||
supertonic_total_steps: int = 5
|
supertonic_total_steps: int = 8
|
||||||
save_chapters_separately: bool = False
|
save_chapters_separately: bool = False
|
||||||
merge_chapters_at_end: bool = True
|
merge_chapters_at_end: bool = True
|
||||||
separate_chapters_format: str = "wav"
|
separate_chapters_format: str = "wav"
|
||||||
@@ -204,7 +204,7 @@ class Job:
|
|||||||
"queue_position": self.queue_position,
|
"queue_position": self.queue_position,
|
||||||
"options": {
|
"options": {
|
||||||
"tts_provider": getattr(self, "tts_provider", "kokoro"),
|
"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,
|
"save_chapters_separately": self.save_chapters_separately,
|
||||||
"merge_chapters_at_end": self.merge_chapters_at_end,
|
"merge_chapters_at_end": self.merge_chapters_at_end,
|
||||||
"separate_chapters_format": self.separate_chapters_format,
|
"separate_chapters_format": self.separate_chapters_format,
|
||||||
@@ -552,7 +552,7 @@ class PendingJob:
|
|||||||
normalization_overrides: Dict[str, Any]
|
normalization_overrides: Dict[str, Any]
|
||||||
created_at: float
|
created_at: float
|
||||||
tts_provider: str = "kokoro"
|
tts_provider: str = "kokoro"
|
||||||
supertonic_total_steps: int = 5
|
supertonic_total_steps: int = 8
|
||||||
cover_image_path: Optional[Path] = None
|
cover_image_path: Optional[Path] = None
|
||||||
cover_image_mime: Optional[str] = None
|
cover_image_mime: Optional[str] = None
|
||||||
chapter_intro_delay: float = 0.5
|
chapter_intro_delay: float = 0.5
|
||||||
@@ -621,7 +621,7 @@ class ConversionService:
|
|||||||
voice: str,
|
voice: str,
|
||||||
speed: float,
|
speed: float,
|
||||||
tts_provider: str = "kokoro",
|
tts_provider: str = "kokoro",
|
||||||
supertonic_total_steps: int = 5,
|
supertonic_total_steps: int = 8,
|
||||||
use_gpu: bool,
|
use_gpu: bool,
|
||||||
subtitle_mode: str,
|
subtitle_mode: str,
|
||||||
output_format: str,
|
output_format: str,
|
||||||
@@ -674,7 +674,7 @@ class ConversionService:
|
|||||||
voice=voice,
|
voice=voice,
|
||||||
speed=speed,
|
speed=speed,
|
||||||
tts_provider=tts_provider,
|
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,
|
use_gpu=use_gpu,
|
||||||
subtitle_mode=subtitle_mode,
|
subtitle_mode=subtitle_mode,
|
||||||
output_format=output_format,
|
output_format=output_format,
|
||||||
@@ -1147,7 +1147,7 @@ class ConversionService:
|
|||||||
"tts_provider": getattr(job, "tts_provider", "kokoro"),
|
"tts_provider": getattr(job, "tts_provider", "kokoro"),
|
||||||
"voice": job.voice,
|
"voice": job.voice,
|
||||||
"speed": job.speed,
|
"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,
|
"use_gpu": job.use_gpu,
|
||||||
"subtitle_mode": job.subtitle_mode,
|
"subtitle_mode": job.subtitle_mode,
|
||||||
"output_format": job.output_format,
|
"output_format": job.output_format,
|
||||||
@@ -1275,7 +1275,7 @@ class ConversionService:
|
|||||||
replace_single_newlines=bool(payload.get("replace_single_newlines", False)),
|
replace_single_newlines=bool(payload.get("replace_single_newlines", False)),
|
||||||
subtitle_format=payload.get("subtitle_format", "srt"),
|
subtitle_format=payload.get("subtitle_format", "srt"),
|
||||||
created_at=float(payload.get("created_at", time.time())),
|
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)),
|
save_chapters_separately=bool(payload.get("save_chapters_separately", False)),
|
||||||
merge_chapters_at_end=bool(payload.get("merge_chapters_at_end", True)),
|
merge_chapters_at_end=bool(payload.get("merge_chapters_at_end", True)),
|
||||||
separate_chapters_format=payload.get("separate_chapters_format", "wav"),
|
separate_chapters_format=payload.get("separate_chapters_format", "wav"),
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from abogen.tts_supertonic import DEFAULT_SUPERTONIC_VOICES
|
|||||||
|
|
||||||
def test_resolve_voice_formula_without_pipeline_does_not_crash() -> None:
|
def test_resolve_voice_formula_without_pipeline_does_not_crash() -> None:
|
||||||
# This can happen when a previously-saved Kokoro mix formula is present
|
# 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"
|
formula = "af_heart*0.5+af_sky*0.5"
|
||||||
resolved = _resolve_voice(None, formula, use_gpu=False)
|
resolved = _resolve_voice(None, formula, use_gpu=False)
|
||||||
assert resolved == formula
|
assert resolved == formula
|
||||||
|
|
||||||
|
|
||||||
def test_supertonic_voice_from_formula_falls_back_to_valid_voice() -> None:
|
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")
|
chosen = _supertonic_voice_from_spec("af_heart*0.5+af_sky*0.5", "af_heart*1.0")
|
||||||
assert chosen in DEFAULT_SUPERTONIC_VOICES
|
assert chosen in DEFAULT_SUPERTONIC_VOICES
|
||||||
|
|||||||
Reference in New Issue
Block a user