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
|
||||
}
|
||||
|
||||
# Mapping from SuperTonic ISO 639-1 language codes to ISO 3166-1 alpha-2 country codes
|
||||
# Used for loading flag icons in the SuperTonic language picker
|
||||
# Mapping from Supertonic ISO 639-1 language codes to ISO 3166-1 alpha-2 country codes
|
||||
# Used for loading flag icons in the Supertonic language picker
|
||||
SUPERTONIC_LANG_TO_COUNTRY = {
|
||||
"en": "gb",
|
||||
"ko": "kr",
|
||||
|
||||
+15
-57
@@ -269,7 +269,7 @@ class ConversionThread(QThread):
|
||||
save_base_path=None,
|
||||
tts_provider="kokoro",
|
||||
supertonic_language="en",
|
||||
supertonic_total_steps=5,
|
||||
supertonic_total_steps=8,
|
||||
):
|
||||
super().__init__()
|
||||
self._chapter_options_event = threading.Event()
|
||||
@@ -283,6 +283,7 @@ class ConversionThread(QThread):
|
||||
self.tts_provider = tts_provider
|
||||
self.supertonic_language = supertonic_language
|
||||
self.supertonic_total_steps = supertonic_total_steps
|
||||
self.sample_rate = 44100 if tts_provider == "supertonic" else 24000
|
||||
self.save_option = save_option
|
||||
self.output_folder = output_folder
|
||||
self.subtitle_mode = subtitle_mode
|
||||
@@ -512,7 +513,7 @@ class ConversionThread(QThread):
|
||||
|
||||
if self.tts_provider == "supertonic":
|
||||
tts = SupertonicPipeline(
|
||||
sample_rate=24000,
|
||||
sample_rate=self.sample_rate,
|
||||
lang=self.supertonic_language,
|
||||
total_steps=self.supertonic_total_steps,
|
||||
)
|
||||
@@ -765,7 +766,7 @@ class ConversionThread(QThread):
|
||||
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
|
||||
subtitle_entries = []
|
||||
current_time = 0.0
|
||||
rate = 24000
|
||||
rate = self.sample_rate
|
||||
subtitle_mode = self.subtitle_mode
|
||||
self.etr_start_time = time.time()
|
||||
self.processed_char_count = 0
|
||||
@@ -781,7 +782,7 @@ class ConversionThread(QThread):
|
||||
merged_out_file = sf.SoundFile(
|
||||
merged_out_path,
|
||||
"w",
|
||||
samplerate=24000,
|
||||
samplerate=self.sample_rate,
|
||||
channels=1,
|
||||
format=self.output_format,
|
||||
)
|
||||
@@ -803,51 +804,7 @@ class ConversionThread(QThread):
|
||||
"-f",
|
||||
"f32le",
|
||||
"-ar",
|
||||
"24000",
|
||||
"-ac",
|
||||
"1",
|
||||
"-i",
|
||||
"pipe:0",
|
||||
]
|
||||
if cover_path and os.path.exists(cover_path):
|
||||
cmd.extend(
|
||||
[
|
||||
"-i",
|
||||
cover_path,
|
||||
"-map",
|
||||
"0:a",
|
||||
"-map",
|
||||
"1",
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-disposition:v",
|
||||
"attached_pic",
|
||||
]
|
||||
)
|
||||
cmd.extend(
|
||||
[
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-q:a",
|
||||
"2",
|
||||
"-movflags",
|
||||
"+faststart+use_metadata_tags",
|
||||
]
|
||||
)
|
||||
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",
|
||||
str(self.sample_rate),
|
||||
"-ac",
|
||||
"1",
|
||||
"-i",
|
||||
@@ -934,7 +891,7 @@ class ConversionThread(QThread):
|
||||
merged_out_path = None
|
||||
subtitle_entries = []
|
||||
current_time = 0.0
|
||||
rate = 24000
|
||||
rate = self.sample_rate
|
||||
subtitle_mode = self.subtitle_mode
|
||||
self.etr_start_time = time.time()
|
||||
self.processed_char_count = 0
|
||||
@@ -987,7 +944,7 @@ class ConversionThread(QThread):
|
||||
chapter_out_file = sf.SoundFile(
|
||||
chapter_out_path,
|
||||
"w",
|
||||
samplerate=24000,
|
||||
samplerate=self.sample_rate,
|
||||
channels=1,
|
||||
format=separate_chapters_format,
|
||||
)
|
||||
@@ -1002,7 +959,7 @@ class ConversionThread(QThread):
|
||||
"-f",
|
||||
"f32le",
|
||||
"-ar",
|
||||
"24000",
|
||||
str(self.sample_rate),
|
||||
"-ac",
|
||||
"1",
|
||||
"-i",
|
||||
@@ -1384,7 +1341,7 @@ class ConversionThread(QThread):
|
||||
# Add silence between chapters for merged output (except after the last chapter)
|
||||
if merge_chapters_at_end and chapter_idx < total_chapters:
|
||||
silence_samples = int(
|
||||
self.silence_duration * 24000
|
||||
self.silence_duration * self.sample_rate
|
||||
) # Silence duration at 24,000 Hz
|
||||
silence_audio = self.np.zeros(silence_samples, dtype="float32")
|
||||
silence_bytes = silence_audio.tobytes()
|
||||
@@ -1615,7 +1572,7 @@ class ConversionThread(QThread):
|
||||
parent_dir, f"{sanitized_base_name}{suffix}"
|
||||
)
|
||||
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
|
||||
rate = 24000
|
||||
rate = self.sample_rate
|
||||
|
||||
# Setup audio output
|
||||
merged_out_file, ffmpeg_proc = None, None
|
||||
@@ -2467,7 +2424,7 @@ class VoicePreviewThread(QThread):
|
||||
parent=None,
|
||||
tts_provider="kokoro",
|
||||
supertonic_language="en",
|
||||
supertonic_total_steps=5,
|
||||
supertonic_total_steps=8,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.np_module = np_module
|
||||
@@ -2479,6 +2436,7 @@ class VoicePreviewThread(QThread):
|
||||
self.tts_provider = tts_provider
|
||||
self.supertonic_language = supertonic_language
|
||||
self.supertonic_total_steps = supertonic_total_steps
|
||||
self.sample_rate = 44100 if tts_provider == "supertonic" else 24000
|
||||
|
||||
# Cache location for preview audio
|
||||
self.cache_dir = get_user_cache_path("preview_cache")
|
||||
@@ -2516,7 +2474,7 @@ class VoicePreviewThread(QThread):
|
||||
from abogen.tts_supertonic import SupertonicPipeline
|
||||
|
||||
tts = SupertonicPipeline(
|
||||
sample_rate=24000,
|
||||
sample_rate=self.sample_rate,
|
||||
lang=self.supertonic_language,
|
||||
total_steps=self.supertonic_total_steps,
|
||||
)
|
||||
@@ -2556,7 +2514,7 @@ class VoicePreviewThread(QThread):
|
||||
|
||||
if audio_segments:
|
||||
audio = self.np_module.concatenate(audio_segments)
|
||||
sf.write(self.cache_path, audio, 24000)
|
||||
sf.write(self.cache_path, audio, self.sample_rate)
|
||||
self.temp_wav = self.cache_path
|
||||
self.finished.emit()
|
||||
except Exception as e:
|
||||
|
||||
+61
-32
@@ -975,7 +975,7 @@ class abogen(QWidget):
|
||||
)
|
||||
self.tts_provider_config = self.config.get("tts_provider", "kokoro")
|
||||
self.supertonic_language_config = self.config.get("supertonic_language", "en")
|
||||
self.supertonic_total_steps_config = self.config.get("supertonic_total_steps", 5)
|
||||
self.supertonic_total_steps_config = self.config.get("supertonic_total_steps", 8)
|
||||
self._pending_close_event = None
|
||||
self.gpu_ok = False # Initialize GPU availability status
|
||||
|
||||
@@ -1025,12 +1025,14 @@ class abogen(QWidget):
|
||||
self.mixed_voice_state = entry
|
||||
self.selected_lang = entry[0][0] if entry and entry[0] else None
|
||||
# Restore TTS provider and supertonic settings from config
|
||||
provider_text = "SuperTonic" if self.tts_provider_config == "supertonic" else "Kokoro"
|
||||
provider_text = "Supertonic" if self.tts_provider_config == "supertonic" else "Kokoro"
|
||||
idx_st = self.provider_combo.findText(provider_text)
|
||||
if idx_st >= 0:
|
||||
self.provider_combo.setCurrentIndex(idx_st)
|
||||
self.st_lang_combo.setCurrentText(self.supertonic_language_config)
|
||||
self.st_steps_spin.setValue(self.supertonic_total_steps_config)
|
||||
idx_steps = self.st_steps_combo.findData(self.supertonic_total_steps_config)
|
||||
if idx_steps >= 0:
|
||||
self.st_steps_combo.setCurrentIndex(idx_steps)
|
||||
|
||||
if self.save_option == "Choose output folder" and self.selected_output_folder:
|
||||
self.save_path_label.setText(self.selected_output_folder)
|
||||
@@ -1129,13 +1131,13 @@ class abogen(QWidget):
|
||||
provider_layout.addWidget(provider_label)
|
||||
self.provider_combo = QComboBox(self)
|
||||
self.provider_combo.addItem("Kokoro", "kokoro")
|
||||
self.provider_combo.addItem("SuperTonic", "supertonic")
|
||||
self.provider_combo.addItem("Supertonic", "supertonic")
|
||||
self.provider_combo.setStyleSheet("QComboBox { min-height: 20px; padding: 6px 12px; }")
|
||||
self.provider_combo.currentIndexChanged.connect(self.on_provider_changed)
|
||||
provider_layout.addWidget(self.provider_combo)
|
||||
controls_layout.addLayout(provider_layout)
|
||||
|
||||
# SuperTonic-specific controls (language + steps), hidden by default
|
||||
# Supertonic-specific controls (language + steps), hidden by default
|
||||
self.supertonic_row = QWidget()
|
||||
supertonic_row_layout = QHBoxLayout(self.supertonic_row)
|
||||
supertonic_row_layout.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -1156,13 +1158,13 @@ class abogen(QWidget):
|
||||
|
||||
st_steps_label = QLabel("Steps:", self)
|
||||
supertonic_row_layout.addWidget(st_steps_label)
|
||||
self.st_steps_spin = QSpinBox(self)
|
||||
self.st_steps_spin.setMinimum(1)
|
||||
self.st_steps_spin.setMaximum(10)
|
||||
self.st_steps_spin.setValue(5)
|
||||
self.st_steps_spin.setFixedWidth(60)
|
||||
self.st_steps_spin.valueChanged.connect(self._on_st_steps_changed)
|
||||
supertonic_row_layout.addWidget(self.st_steps_spin)
|
||||
self.st_steps_combo = QComboBox(self)
|
||||
for val in range(2, 16):
|
||||
self.st_steps_combo.addItem(str(val), val)
|
||||
self.st_steps_combo.setCurrentIndex(self.st_steps_combo.findData(8))
|
||||
self.st_steps_combo.setStyleSheet("QComboBox { min-height: 20px; padding: 6px 12px; }")
|
||||
self.st_steps_combo.currentIndexChanged.connect(self._on_st_steps_changed)
|
||||
supertonic_row_layout.addWidget(self.st_steps_combo)
|
||||
|
||||
supertonic_row_layout.addStretch()
|
||||
self.supertonic_row.hide()
|
||||
@@ -1896,7 +1898,7 @@ class abogen(QWidget):
|
||||
save_config(self.config)
|
||||
is_supertonic = provider == "supertonic"
|
||||
|
||||
# Show/hide SuperTonic controls
|
||||
# Show/hide Supertonic controls
|
||||
self.supertonic_row.setVisible(is_supertonic)
|
||||
|
||||
# Update subtitles availability
|
||||
@@ -1910,7 +1912,7 @@ class abogen(QWidget):
|
||||
self.mixed_voice_state = None
|
||||
self.btn_voice_formula_mixer.setEnabled(False)
|
||||
self.voice_combo.setToolTip(
|
||||
"SuperTonic voices:\n"
|
||||
"Supertonic voices:\n"
|
||||
"M1-M5 = Male voices\n"
|
||||
"F1-F5 = Female voices"
|
||||
)
|
||||
@@ -1928,8 +1930,8 @@ class abogen(QWidget):
|
||||
self.selected_lang = lang
|
||||
self.update_subtitle_options_availability()
|
||||
|
||||
def _on_st_steps_changed(self, steps):
|
||||
self.config["supertonic_total_steps"] = steps
|
||||
def _on_st_steps_changed(self):
|
||||
self.config["supertonic_total_steps"] = self.st_steps_combo.currentData()
|
||||
save_config(self.config)
|
||||
|
||||
def on_voice_combo_changed(self, index):
|
||||
@@ -1943,7 +1945,7 @@ class abogen(QWidget):
|
||||
if isinstance(entry, dict):
|
||||
entry_provider = str(entry.get("provider", "")).strip().lower()
|
||||
if entry_provider == "supertonic":
|
||||
# Switch provider to SuperTonic if not already
|
||||
# Switch provider to Supertonic if not already
|
||||
if self.provider_combo.currentData() != "supertonic":
|
||||
self.provider_combo.setCurrentIndex(1)
|
||||
self.mixed_voice_state = None
|
||||
@@ -1951,7 +1953,9 @@ class abogen(QWidget):
|
||||
# Sync supertonic controls from profile
|
||||
profile_steps = entry.get("total_steps")
|
||||
if profile_steps is not None:
|
||||
self.st_steps_spin.setValue(int(profile_steps))
|
||||
idx_steps = self.st_steps_combo.findData(int(profile_steps))
|
||||
if idx_steps >= 0:
|
||||
self.st_steps_combo.setCurrentIndex(idx_steps)
|
||||
profile_lang = entry.get("language")
|
||||
if profile_lang and profile_lang in SUPERTONIC_AVAILABLE_LANGS:
|
||||
self.st_lang_combo.setCurrentText(profile_lang)
|
||||
@@ -2012,7 +2016,14 @@ class abogen(QWidget):
|
||||
# re-add voices
|
||||
if provider == "supertonic":
|
||||
for v in DEFAULT_SUPERTONIC_VOICES:
|
||||
self.voice_combo.addItem(f"{v}", v)
|
||||
icon = QIcon()
|
||||
if v.startswith("F"):
|
||||
icon_path = get_resource_path("abogen.assets", "female.png")
|
||||
else:
|
||||
icon_path = get_resource_path("abogen.assets", "male.png")
|
||||
if icon_path and os.path.exists(icon_path):
|
||||
icon = QIcon(icon_path)
|
||||
self.voice_combo.addItem(icon, f"{v}", v)
|
||||
else:
|
||||
for v in VOICES_INTERNAL:
|
||||
icon = QIcon()
|
||||
@@ -2213,7 +2224,7 @@ class abogen(QWidget):
|
||||
merge_chapters_at_end=getattr(self, "merge_chapters_at_end", None),
|
||||
tts_provider=self.provider_combo.currentData(),
|
||||
supertonic_language=self.st_lang_combo.currentText(),
|
||||
supertonic_total_steps=self.st_steps_spin.value(),
|
||||
supertonic_total_steps=self.st_steps_combo.currentData(),
|
||||
)
|
||||
|
||||
# Prevent adding duplicate items to the queue
|
||||
@@ -2359,9 +2370,12 @@ class abogen(QWidget):
|
||||
|
||||
# TTS provider settings
|
||||
tts_provider = getattr(queued_item, "tts_provider", "kokoro")
|
||||
self.provider_combo.setCurrentText("SuperTonic" if tts_provider == "supertonic" else "Kokoro")
|
||||
self.provider_combo.setCurrentText("Supertonic" if tts_provider == "supertonic" else "Kokoro")
|
||||
self.st_lang_combo.setCurrentText(getattr(queued_item, "supertonic_language", "en"))
|
||||
self.st_steps_spin.setValue(getattr(queued_item, "supertonic_total_steps", 5))
|
||||
steps_val = getattr(queued_item, "supertonic_total_steps", 8)
|
||||
idx_steps = self.st_steps_combo.findData(steps_val)
|
||||
if idx_steps >= 0:
|
||||
self.st_steps_combo.setCurrentIndex(idx_steps)
|
||||
|
||||
# Sync Voice/Profile in config
|
||||
self.config["selected_voice"] = self.selected_voice
|
||||
@@ -2390,8 +2404,11 @@ class abogen(QWidget):
|
||||
from abogen.voice_profiles import load_profiles
|
||||
entry = load_profiles().get(self.selected_profile_name, {})
|
||||
if isinstance(entry, dict):
|
||||
return str(entry.get("voice", self.selected_voice or "M1"))
|
||||
return self.selected_voice or "M1"
|
||||
return str(entry.get("voice", "M1"))
|
||||
current_data = self.voice_combo.currentData()
|
||||
if current_data and isinstance(current_data, str) and not current_data.startswith("profile:"):
|
||||
return current_data
|
||||
return "M1"
|
||||
if self.mixed_voice_state:
|
||||
formula_components = [
|
||||
f"{name}*{weight}" for name, weight in self.mixed_voice_state
|
||||
@@ -2494,7 +2511,7 @@ class abogen(QWidget):
|
||||
|
||||
tts_provider = self.provider_combo.currentData()
|
||||
supertonic_language = self.st_lang_combo.currentText()
|
||||
supertonic_total_steps = self.st_steps_spin.value()
|
||||
supertonic_total_steps = self.st_steps_combo.currentData()
|
||||
|
||||
self.conversion_thread = ConversionThread(
|
||||
self.selected_file,
|
||||
@@ -2594,8 +2611,9 @@ class abogen(QWidget):
|
||||
self.update_log((gpu_msg, gpu_ok))
|
||||
tts_provider = self.provider_combo.currentData()
|
||||
if tts_provider == "supertonic":
|
||||
# SuperTonic doesn't need KPipeline, call callback directly
|
||||
pipeline_loaded_callback(None, None, None)
|
||||
# Supertonic doesn't need KPipeline, call callback directly
|
||||
import numpy as np
|
||||
pipeline_loaded_callback(np, None, None)
|
||||
else:
|
||||
self.update_log("Loading modules...")
|
||||
load_thread = LoadPipelineThread(pipeline_loaded_callback)
|
||||
@@ -2918,9 +2936,13 @@ class abogen(QWidget):
|
||||
provider = self.provider_combo.currentData()
|
||||
|
||||
if provider == "supertonic":
|
||||
voice_to_cache = self.selected_voice or "M1"
|
||||
current_data = self.voice_combo.currentData()
|
||||
if current_data and isinstance(current_data, str) and not current_data.startswith("profile:"):
|
||||
voice_to_cache = current_data
|
||||
else:
|
||||
voice_to_cache = "M1"
|
||||
lang_to_cache = self.st_lang_combo.currentText()
|
||||
steps = self.st_steps_spin.value()
|
||||
steps = self.st_steps_combo.currentData()
|
||||
cache_dir = get_user_cache_path("preview_cache")
|
||||
filename = f"st_{voice_to_cache}_{lang_to_cache}_steps{steps}_{speed:.2f}.wav"
|
||||
return os.path.join(cache_dir, filename)
|
||||
@@ -3029,10 +3051,11 @@ class abogen(QWidget):
|
||||
self.btn_voice_formula_mixer.setEnabled(False) # Disable mixer button
|
||||
self.btn_start.setEnabled(False) # Disable start button during preview
|
||||
|
||||
# For SuperTonic, skip KPipeline loading and use SupertonicPipeline directly
|
||||
# For Supertonic, skip KPipeline loading and use SupertonicPipeline directly
|
||||
if self.provider_combo.currentData() == "supertonic":
|
||||
import numpy as np
|
||||
self.loading_movie.start()
|
||||
self._on_pipeline_loaded_for_preview(None, None, None)
|
||||
self._on_pipeline_loaded_for_preview(np, None, None)
|
||||
return
|
||||
|
||||
# Start loading animation - ensure signal connection is always active
|
||||
@@ -3092,12 +3115,18 @@ class abogen(QWidget):
|
||||
if self.mixed_voice_state and self.mixed_voice_state[0][0]
|
||||
else None
|
||||
)
|
||||
else:
|
||||
if self.provider_combo.currentData() == "supertonic":
|
||||
current_data = self.voice_combo.currentData()
|
||||
print(f"[DEBUG] voice_combo.currentData() = {current_data!r}")
|
||||
voice = current_data if (current_data and isinstance(current_data, str) and not current_data.startswith("profile:")) else "M1"
|
||||
print(f"[DEBUG] resolved voice = {voice!r}")
|
||||
else:
|
||||
voice = self.selected_voice or ""
|
||||
|
||||
tts_provider = self.provider_combo.currentData()
|
||||
supertonic_language = self.st_lang_combo.currentText()
|
||||
supertonic_total_steps = self.st_steps_spin.value()
|
||||
supertonic_total_steps = self.st_steps_combo.currentData()
|
||||
|
||||
if tts_provider == "supertonic":
|
||||
lang = supertonic_language
|
||||
|
||||
@@ -29,4 +29,4 @@ class QueuedItem:
|
||||
# TTS Provider fields
|
||||
tts_provider: str = "kokoro"
|
||||
supertonic_language: str = "en"
|
||||
supertonic_total_steps: int = 5
|
||||
supertonic_total_steps: int = 8
|
||||
|
||||
@@ -97,7 +97,7 @@ _UNSUPPORTED_CHARS_RE = re.compile(
|
||||
|
||||
|
||||
def _parse_unsupported_characters(error: BaseException) -> list[str]:
|
||||
"""Best-effort extraction of unsupported characters from SuperTonic errors."""
|
||||
"""Best-effort extraction of unsupported characters from Supertonic errors."""
|
||||
|
||||
message = " ".join(
|
||||
str(part) for part in getattr(error, "args", ()) if part is not None
|
||||
@@ -221,7 +221,7 @@ class SupertonicPipeline:
|
||||
removed: set[str] = set()
|
||||
last_exc: Exception | None = None
|
||||
|
||||
# SuperTonic can raise ValueError for unsupported characters; strip and retry.
|
||||
# Supertonic can raise ValueError for unsupported characters; strip and retry.
|
||||
for attempt in range(3):
|
||||
try:
|
||||
wav, duration = self._tts.synthesize(
|
||||
@@ -253,14 +253,14 @@ class SupertonicPipeline:
|
||||
chunk_to_speak = sanitized
|
||||
if not chunk_to_speak:
|
||||
logger.warning(
|
||||
"SuperTonic: dropped a chunk after removing unsupported characters: %s",
|
||||
"Supertonic: dropped a chunk after removing unsupported characters: %s",
|
||||
sorted(removed),
|
||||
)
|
||||
break
|
||||
|
||||
if attempt == 0:
|
||||
logger.warning(
|
||||
"SuperTonic: removed unsupported characters %s and retried.",
|
||||
"Supertonic: removed unsupported characters %s and retried.",
|
||||
sorted(removed),
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -59,7 +59,7 @@ def _supertonic_voice_from_spec(spec: Any, fallback: str) -> str:
|
||||
raw = str(spec or "").strip()
|
||||
fallback_raw = str(fallback or "").strip()
|
||||
|
||||
# SuperTonic voices are discrete IDs (M1/F3/...). If we see a Kokoro mix
|
||||
# Supertonic voices are discrete IDs (M1/F3/...). If we see a Kokoro mix
|
||||
# formula (contains '*' or '+'), ignore it and fall back to a safe voice.
|
||||
if not raw or "*" in raw or "+" in raw:
|
||||
raw = fallback_raw
|
||||
@@ -1584,7 +1584,7 @@ def run_conversion_job(job: Job) -> None:
|
||||
pipelines[provider_norm] = SupertonicPipeline(
|
||||
sample_rate=SAMPLE_RATE,
|
||||
auto_download=True,
|
||||
total_steps=int(getattr(job, "supertonic_total_steps", 5) or 5),
|
||||
total_steps=int(getattr(job, "supertonic_total_steps", 8) or 8),
|
||||
)
|
||||
return pipelines[provider_norm]
|
||||
|
||||
@@ -1618,7 +1618,7 @@ def run_conversion_job(job: Job) -> None:
|
||||
provider = str(entry.get("provider") or "kokoro").strip().lower() or "kokoro"
|
||||
if provider == "supertonic":
|
||||
voice = str(entry.get("voice") or getattr(job, "voice", "M1") or "M1").strip() or "M1"
|
||||
steps = int(entry.get("total_steps") or getattr(job, "supertonic_total_steps", 5) or 5)
|
||||
steps = int(entry.get("total_steps") or getattr(job, "supertonic_total_steps", 8) or 8)
|
||||
speed = float(entry.get("speed") or getattr(job, "speed", 1.0) or 1.0)
|
||||
return "supertonic", _supertonic_voice_from_spec(voice, getattr(job, "voice", "M1")), speed, steps
|
||||
formula = _formula_from_kokoro_entry(entry)
|
||||
@@ -1634,7 +1634,7 @@ def run_conversion_job(job: Job) -> None:
|
||||
"""Resolve a raw voice spec into (provider, resolved_spec, choice, speed, steps).
|
||||
|
||||
For Kokoro formulas, `choice` will be a resolved voice tensor (via `voice_formulas`).
|
||||
For SuperTonic, `choice` will be a valid SuperTonic voice id.
|
||||
For Supertonic, `choice` will be a valid Supertonic voice id.
|
||||
"""
|
||||
|
||||
provider, resolved, speed, steps = resolve_voice_target(raw_spec)
|
||||
@@ -1857,7 +1857,7 @@ def run_conversion_job(job: Job) -> None:
|
||||
voice=voice_name,
|
||||
speed=float(speed_override if speed_override is not None else job.speed),
|
||||
split_pattern=split_pattern,
|
||||
total_steps=int(supertonic_steps_override if supertonic_steps_override is not None else getattr(job, "supertonic_total_steps", 5)),
|
||||
total_steps=int(supertonic_steps_override if supertonic_steps_override is not None else getattr(job, "supertonic_total_steps", 8)),
|
||||
)
|
||||
else:
|
||||
kokoro_pipeline = get_pipeline("kokoro")
|
||||
@@ -2448,7 +2448,7 @@ def _load_pipeline(job: Job):
|
||||
return SupertonicPipeline(
|
||||
sample_rate=SAMPLE_RATE,
|
||||
auto_download=True,
|
||||
total_steps=int(getattr(job, "supertonic_total_steps", 5) or 5),
|
||||
total_steps=int(getattr(job, "supertonic_total_steps", 8) or 8),
|
||||
)
|
||||
|
||||
device = "cpu"
|
||||
@@ -2610,7 +2610,7 @@ def _build_ffmpeg_command(path: Path, fmt: str, metadata: Optional[Dict[str, str
|
||||
def _resolve_voice(pipeline, voice_spec: str, use_gpu: bool):
|
||||
if "*" in voice_spec:
|
||||
# Voice formulas are a Kokoro-only feature (they require a pipeline that can
|
||||
# load individual Kokoro voices). When running with SuperTonic (or when the
|
||||
# load individual Kokoro voices). When running with Supertonic (or when the
|
||||
# pipeline is otherwise unavailable), treat the spec as a plain string and
|
||||
# allow downstream provider-specific resolution to choose a safe fallback.
|
||||
if pipeline is None or not hasattr(pipeline, "load_single_voice"):
|
||||
|
||||
@@ -162,7 +162,7 @@ def api_voice_profiles_preview() -> ResponseReturnValue:
|
||||
formula = str(payload.get("formula") or "").strip()
|
||||
profile_name = str(payload.get("profile") or "").strip()
|
||||
provider = str(payload.get("tts_provider") or payload.get("provider") or "").strip().lower() or None
|
||||
supertonic_total_steps = int(payload.get("supertonic_total_steps") or payload.get("total_steps") or settings.get("supertonic_total_steps") or 5)
|
||||
supertonic_total_steps = int(payload.get("supertonic_total_steps") or payload.get("total_steps") or settings.get("supertonic_total_steps") or 8)
|
||||
|
||||
voice_spec = ""
|
||||
resolved_provider = provider or "kokoro"
|
||||
@@ -224,7 +224,7 @@ def api_speaker_preview() -> ResponseReturnValue:
|
||||
speed_value = payload.get("speed")
|
||||
speed = coerce_float(speed_value, 1.0)
|
||||
tts_provider = str(payload.get("tts_provider") or "").strip().lower()
|
||||
supertonic_total_steps = int(payload.get("supertonic_total_steps") or 5)
|
||||
supertonic_total_steps = int(payload.get("supertonic_total_steps") or 8)
|
||||
|
||||
settings = load_settings()
|
||||
use_gpu = settings.get("use_gpu", False)
|
||||
@@ -269,7 +269,7 @@ def api_speaker_preview() -> ResponseReturnValue:
|
||||
use_gpu=use_gpu
|
||||
,
|
||||
tts_provider=resolved_provider,
|
||||
supertonic_total_steps=supertonic_total_steps or int(settings.get("supertonic_total_steps") or 5),
|
||||
supertonic_total_steps=supertonic_total_steps or int(settings.get("supertonic_total_steps") or 8),
|
||||
pronunciation_overrides=pronunciation_overrides,
|
||||
manual_overrides=manual_overrides,
|
||||
speakers=speakers,
|
||||
|
||||
@@ -43,8 +43,12 @@ def update_settings() -> ResponseReturnValue:
|
||||
current["language"] = (form.get("language") or "en").strip()
|
||||
current["default_speaker"] = (form.get("default_speaker") or "").strip()
|
||||
current["default_voice"] = (form.get("default_voice") or "").strip()
|
||||
provider = str(form.get("tts_provider") or "kokoro").strip().lower()
|
||||
if provider in {"kokoro", "supertonic"}:
|
||||
current["tts_provider"] = provider
|
||||
try:
|
||||
current["supertonic_total_steps"] = max(2, min(15, int(form.get("supertonic_total_steps", current.get("supertonic_total_steps", 5)))))
|
||||
total_steps = int(form.get("supertonic_total_steps", current.get("supertonic_total_steps", 8)))
|
||||
current["supertonic_total_steps"] = max(2, min(15, total_steps))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
|
||||
@@ -577,7 +577,7 @@ def apply_book_step_form(
|
||||
# NOTE: Do not auto-set a global TTS provider at the book level based on the
|
||||
# narrator defaults. Provider is resolved per-speaker/per-chunk from the voice
|
||||
# spec (e.g. "speaker:Name" for saved speakers, or a Kokoro mix formula).
|
||||
# This enables mixed-provider conversions (e.g. narrator=SuperTonic, characters=Kokoro).
|
||||
# This enables mixed-provider conversions (e.g. narrator=Supertonic, characters=Kokoro).
|
||||
provider_value = str(form.get("tts_provider") or "").strip().lower()
|
||||
if provider_value in {"kokoro", "supertonic"}:
|
||||
pending.tts_provider = provider_value
|
||||
@@ -913,6 +913,15 @@ def build_pending_job_from_extraction(
|
||||
else:
|
||||
normalization_overrides[key] = default_val
|
||||
|
||||
provider_value = str(form.get("tts_provider") or "").strip().lower()
|
||||
if provider_value not in {"kokoro", "supertonic"}:
|
||||
provider_value = settings.get("tts_provider", "kokoro")
|
||||
try:
|
||||
total_steps = int(form.get("supertonic_total_steps", settings.get("supertonic_total_steps", 8)))
|
||||
supertonic_steps = max(2, min(15, total_steps))
|
||||
except (TypeError, ValueError):
|
||||
supertonic_steps = int(settings.get("supertonic_total_steps", 8))
|
||||
|
||||
pending = PendingJob(
|
||||
id=uuid.uuid4().hex,
|
||||
original_filename=original_name,
|
||||
@@ -928,6 +937,8 @@ def build_pending_job_from_extraction(
|
||||
replace_single_newlines=replace_single_newlines,
|
||||
subtitle_format=subtitle_format,
|
||||
total_characters=total_chars,
|
||||
tts_provider=provider_value,
|
||||
supertonic_total_steps=supertonic_steps,
|
||||
save_chapters_separately=save_chapters_separately,
|
||||
merge_chapters_at_end=merge_chapters_at_end,
|
||||
separate_chapters_format=separate_chapters_format,
|
||||
|
||||
@@ -92,7 +92,7 @@ def generate_preview_audio(
|
||||
speed: float,
|
||||
use_gpu: bool,
|
||||
tts_provider: str = "kokoro",
|
||||
supertonic_total_steps: int = 5,
|
||||
supertonic_total_steps: int = 8,
|
||||
max_seconds: float = 8.0,
|
||||
pronunciation_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||
manual_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||
@@ -201,7 +201,7 @@ def synthesize_preview(
|
||||
speed: float,
|
||||
use_gpu: bool,
|
||||
tts_provider: str = "kokoro",
|
||||
supertonic_total_steps: int = 5,
|
||||
supertonic_total_steps: int = 8,
|
||||
max_seconds: float = 8.0,
|
||||
pronunciation_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||
manual_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||
|
||||
@@ -25,7 +25,7 @@ def submit_job(pending: PendingJob) -> str:
|
||||
tts_provider=getattr(pending, "tts_provider", "kokoro"),
|
||||
voice=pending.voice,
|
||||
speed=pending.speed,
|
||||
supertonic_total_steps=getattr(pending, "supertonic_total_steps", 5),
|
||||
supertonic_total_steps=getattr(pending, "supertonic_total_steps", 8),
|
||||
use_gpu=pending.use_gpu,
|
||||
subtitle_mode=pending.subtitle_mode,
|
||||
output_format=pending.output_format,
|
||||
|
||||
@@ -175,7 +175,8 @@ def settings_defaults() -> Dict[str, Any]:
|
||||
"save_mode": "default_output" if has_output_override() else "save_next_to_input",
|
||||
"default_speaker": "",
|
||||
"default_voice": VOICES_INTERNAL[0] if VOICES_INTERNAL else "",
|
||||
"supertonic_total_steps": 5,
|
||||
"tts_provider": "kokoro",
|
||||
"supertonic_total_steps": 8,
|
||||
"supertonic_speed": 1.0,
|
||||
"replace_single_newlines": False,
|
||||
"use_gpu": True,
|
||||
|
||||
@@ -666,7 +666,7 @@ def resolve_voice_choice(
|
||||
|
||||
# Provider-aware behavior:
|
||||
# - Kokoro profiles typically represent mixes (formula strings).
|
||||
# - SuperTonic profiles represent a discrete voice id + settings.
|
||||
# - Supertonic profiles represent a discrete voice id + settings.
|
||||
# In that case, we return a speaker reference so downstream can
|
||||
# resolve provider per-speaker and allow mixed-provider casting.
|
||||
if provider == "supertonic":
|
||||
|
||||
@@ -111,7 +111,7 @@ class Job:
|
||||
subtitle_format: str
|
||||
created_at: float
|
||||
tts_provider: str = "kokoro"
|
||||
supertonic_total_steps: int = 5
|
||||
supertonic_total_steps: int = 8
|
||||
save_chapters_separately: bool = False
|
||||
merge_chapters_at_end: bool = True
|
||||
separate_chapters_format: str = "wav"
|
||||
@@ -204,7 +204,7 @@ class Job:
|
||||
"queue_position": self.queue_position,
|
||||
"options": {
|
||||
"tts_provider": getattr(self, "tts_provider", "kokoro"),
|
||||
"supertonic_total_steps": getattr(self, "supertonic_total_steps", 5),
|
||||
"supertonic_total_steps": getattr(self, "supertonic_total_steps", 8,
|
||||
"save_chapters_separately": self.save_chapters_separately,
|
||||
"merge_chapters_at_end": self.merge_chapters_at_end,
|
||||
"separate_chapters_format": self.separate_chapters_format,
|
||||
@@ -552,7 +552,7 @@ class PendingJob:
|
||||
normalization_overrides: Dict[str, Any]
|
||||
created_at: float
|
||||
tts_provider: str = "kokoro"
|
||||
supertonic_total_steps: int = 5
|
||||
supertonic_total_steps: int = 8
|
||||
cover_image_path: Optional[Path] = None
|
||||
cover_image_mime: Optional[str] = None
|
||||
chapter_intro_delay: float = 0.5
|
||||
@@ -621,7 +621,7 @@ class ConversionService:
|
||||
voice: str,
|
||||
speed: float,
|
||||
tts_provider: str = "kokoro",
|
||||
supertonic_total_steps: int = 5,
|
||||
supertonic_total_steps: int = 8,
|
||||
use_gpu: bool,
|
||||
subtitle_mode: str,
|
||||
output_format: str,
|
||||
@@ -674,7 +674,7 @@ class ConversionService:
|
||||
voice=voice,
|
||||
speed=speed,
|
||||
tts_provider=tts_provider,
|
||||
supertonic_total_steps=int(supertonic_total_steps or 5),
|
||||
supertonic_total_steps=int(supertonic_total_steps or 8),
|
||||
use_gpu=use_gpu,
|
||||
subtitle_mode=subtitle_mode,
|
||||
output_format=output_format,
|
||||
@@ -1147,7 +1147,7 @@ class ConversionService:
|
||||
"tts_provider": getattr(job, "tts_provider", "kokoro"),
|
||||
"voice": job.voice,
|
||||
"speed": job.speed,
|
||||
"supertonic_total_steps": getattr(job, "supertonic_total_steps", 5),
|
||||
"supertonic_total_steps": getattr(job, "supertonic_total_steps", 8,
|
||||
"use_gpu": job.use_gpu,
|
||||
"subtitle_mode": job.subtitle_mode,
|
||||
"output_format": job.output_format,
|
||||
@@ -1275,7 +1275,7 @@ class ConversionService:
|
||||
replace_single_newlines=bool(payload.get("replace_single_newlines", False)),
|
||||
subtitle_format=payload.get("subtitle_format", "srt"),
|
||||
created_at=float(payload.get("created_at", time.time())),
|
||||
supertonic_total_steps=int(payload.get("supertonic_total_steps", 5)),
|
||||
supertonic_total_steps=int(payload.get("supertonic_total_steps", 8),
|
||||
save_chapters_separately=bool(payload.get("save_chapters_separately", False)),
|
||||
merge_chapters_at_end=bool(payload.get("merge_chapters_at_end", True)),
|
||||
separate_chapters_format=payload.get("separate_chapters_format", "wav"),
|
||||
|
||||
@@ -6,13 +6,13 @@ from abogen.tts_supertonic import DEFAULT_SUPERTONIC_VOICES
|
||||
|
||||
def test_resolve_voice_formula_without_pipeline_does_not_crash() -> None:
|
||||
# This can happen when a previously-saved Kokoro mix formula is present
|
||||
# but the active provider is SuperTonic (no Kokoro pipeline object).
|
||||
# but the active provider is Supertonic (no Kokoro pipeline object).
|
||||
formula = "af_heart*0.5+af_sky*0.5"
|
||||
resolved = _resolve_voice(None, formula, use_gpu=False)
|
||||
assert resolved == formula
|
||||
|
||||
|
||||
def test_supertonic_voice_from_formula_falls_back_to_valid_voice() -> None:
|
||||
# When a stale Kokoro mix formula is present, SuperTonic should not receive it.
|
||||
# When a stale Kokoro mix formula is present, Supertonic should not receive it.
|
||||
chosen = _supertonic_voice_from_spec("af_heart*0.5+af_sky*0.5", "af_heart*1.0")
|
||||
assert chosen in DEFAULT_SUPERTONIC_VOICES
|
||||
|
||||
Reference in New Issue
Block a user