Added subtitle generation support for non-English languages, improvements in code and documentation

This commit is contained in:
Deniz Şafak
2025-11-23 16:03:41 +03:00
parent 795bd8f1aa
commit f5d95547c5
5 changed files with 134 additions and 35 deletions
+1 -4
View File
@@ -61,10 +61,7 @@ SUPPORTED_INPUT_FORMATS = [
# Please refer to: https://github.com/hexgrad/kokoro/blob/6d87f4ae7abc2d14dbc4b3ef2e5f19852e861ac2/kokoro/pipeline.py
# 383 English processing (unchanged)
# 384 if self.lang_code in 'ab':
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION = [
"a",
"b",
]
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION = list(LANGUAGE_DESCRIPTIONS.keys())
# Voice and sample text constants
VOICES_INTERNAL = [
+35
View File
@@ -690,6 +690,27 @@ class ConversionThread(QThread):
None if lang_code in self.NO_SPLIT_LANGUAGES else self.DEFAULT_SPLIT_PATTERN
)
# Override split pattern for non-English languages when sentence-based subtitles are requested
# This ensures we get sentence-level audio chunks since we don't have word-level timestamps
if (
self.subtitle_mode in ["Sentence", "Sentence + Comma"]
and lang_code not in ["a", "b"]
):
if lang_code in self.NO_SPLIT_LANGUAGES:
# Split by CJK punctuation (keeping it)
if self.subtitle_mode == "Sentence + Comma":
# Use comma pattern if available
self.split_pattern = r"(?<=[。!?、,])"
else:
self.split_pattern = r"(?<=[。!?])"
else:
# Split by sentence endings (keeping punctuation) or newlines
if self.subtitle_mode == "Sentence + Comma":
# Include commas in split pattern
self.split_pattern = r"(?<=[.!?,])\s+|\n+"
else:
self.split_pattern = r"(?<=[.!?])\s+|\n+"
def _stream_audio_in_chunks(
self, segments, process_func, progress_prefix="Processing"
):
@@ -1419,6 +1440,20 @@ class ConversionThread(QThread):
# Subtitle logic
if self.subtitle_mode != "Disabled":
tokens_list = getattr(result, "tokens", [])
# Fallback for languages without token support (non-English)
# Create a single token representing the entire segment duration
if not tokens_list and result.graphemes:
class FakeToken:
def __init__(self, text, start, end):
self.text = text
self.start_ts = start
self.end_ts = end
self.whitespace = ""
tokens_list = [FakeToken(result.graphemes, 0, chunk_dur)]
tokens_with_timestamps = []
chapter_tokens_with_timestamps = []
+94 -29
View File
@@ -390,7 +390,7 @@ class InputBox(QLabel):
window = self.window()
if hasattr(window, "subtitle_combo"):
# Only enable if language supports it
current_lang = getattr(window, "lang_code", "a")
current_lang = getattr(window, "selected_lang", "a")
window.subtitle_combo.setEnabled(
current_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
)
@@ -824,7 +824,7 @@ class abogen(QWidget):
self.use_gpu = self.config.get(
"use_gpu", True # Load GPU setting with default True
)
self.replace_single_newlines = self.config.get("replace_single_newlines", False)
self.replace_single_newlines = self.config.get("replace_single_newlines", True)
self.use_silent_gaps = self.config.get("use_silent_gaps", True)
self.subtitle_speed_method = self.config.get("subtitle_speed_method", "tts")
self._pending_close_event = None
@@ -1062,9 +1062,6 @@ class abogen(QWidget):
)
self.subtitle_combo.setCurrentText(self.subtitle_mode)
self.subtitle_combo.currentTextChanged.connect(self.on_subtitle_mode_changed)
# Enable/disable subtitle options based on selected language (profile or voice)
enable = self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
self.subtitle_combo.setEnabled(enable)
subtitle_layout.addWidget(self.subtitle_combo)
controls_layout.addLayout(subtitle_layout)
@@ -1146,10 +1143,10 @@ class abogen(QWidget):
except Exception:
# Fail-safe: don't crash UI if model manipulation isn't supported on some platforms
pass
# Enable/disable subtitle format based on selected language
self.subtitle_format_combo.setEnabled(
self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
)
# Enable/disable subtitle options based on selected language (profile or voice)
self.update_subtitle_options_availability()
controls_layout.addLayout(subtitle_format_layout)
# Replace single newlines dropdown (acts like checkbox)
@@ -1590,19 +1587,69 @@ class abogen(QWidget):
self.config["speed"] = s
save_config(self.config)
def update_subtitle_options_availability(self):
"""
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.
"""
# Check if current file is a subtitle file
is_subtitle_input = False
if self.selected_file and self.selected_file.lower().endswith(
(".srt", ".ass", ".vtt")
):
is_subtitle_input = True
if self.selected_lang not in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION:
self.subtitle_combo.setEnabled(False)
self.subtitle_format_combo.setEnabled(False)
return
# Only enable subtitle_combo if it's NOT a subtitle input
self.subtitle_combo.setEnabled(not is_subtitle_input)
self.subtitle_format_combo.setEnabled(True)
is_english = self.selected_lang in ["a", "b"]
# Items to keep enabled for non-English
allowed_modes = ["Disabled", "Line", "Sentence", "Sentence + Comma"]
model = self.subtitle_combo.model()
for i in range(self.subtitle_combo.count()):
text = self.subtitle_combo.itemText(i)
item = model.item(i)
if not item:
continue
if is_english:
item.setEnabled(True)
else:
if text in allowed_modes:
item.setEnabled(True)
else:
item.setEnabled(False)
# If current selection is disabled, switch to a valid one
current_text = self.subtitle_combo.currentText()
current_idx = self.subtitle_combo.currentIndex()
current_item = model.item(current_idx)
if current_item and not current_item.isEnabled():
# Switch to "Sentence" if available, else "Disabled"
sentence_idx = self.subtitle_combo.findText("Sentence")
if sentence_idx >= 0:
self.subtitle_combo.setCurrentIndex(sentence_idx)
else:
self.subtitle_combo.setCurrentIndex(0) # Disabled
self.subtitle_mode = self.subtitle_combo.currentText()
def on_voice_changed(self, index):
voice = self.voice_combo.itemData(index)
self.selected_voice, self.selected_lang = voice, voice[0]
self.config["selected_voice"] = voice
save_config(self.config)
# Enable/disable subtitle options based on language
if self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION:
self.subtitle_combo.setEnabled(True)
self.subtitle_format_combo.setEnabled(True)
self.subtitle_mode = self.subtitle_combo.currentText()
else:
self.subtitle_combo.setEnabled(False)
self.subtitle_format_combo.setEnabled(False)
self.update_subtitle_options_availability()
def on_voice_combo_changed(self, index):
data = self.voice_combo.itemData(index)
@@ -1624,12 +1671,7 @@ class abogen(QWidget):
self.config.pop("selected_voice", None)
save_config(self.config)
# enable subtitles based on profile language
self.subtitle_combo.setEnabled(
self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
)
self.subtitle_format_combo.setEnabled(
self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
)
self.update_subtitle_options_availability()
else:
self.mixed_voice_state = None
self.selected_profile_name = None
@@ -1638,13 +1680,7 @@ class abogen(QWidget):
if "selected_profile_name" in self.config:
del self.config["selected_profile_name"]
save_config(self.config)
if self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION:
self.subtitle_combo.setEnabled(True)
self.subtitle_format_combo.setEnabled(True)
self.subtitle_mode = self.subtitle_combo.currentText()
else:
self.subtitle_combo.setEnabled(False)
self.subtitle_format_combo.setEnabled(False)
self.update_subtitle_options_availability()
def update_subtitle_combo_for_profile(self, profile_name):
from abogen.voice_profiles import load_profiles
@@ -2790,6 +2826,33 @@ class abogen(QWidget):
self.conversion_thread.cancel()
self.conversion_thread.wait()
def cleanup_preview_threads(self):
# Stop preview generation thread
if (
hasattr(self, "preview_thread")
and self.preview_thread is not None
and self.preview_thread.isRunning()
):
self.preview_thread.terminate()
self.preview_thread.wait()
# Stop audio playback thread
if (
hasattr(self, "play_audio_thread")
and self.play_audio_thread is not None
and self.play_audio_thread.isRunning()
):
self.play_audio_thread.stop()
self.play_audio_thread.wait()
# Cleanup pygame mixer if initialized
try:
import pygame
if pygame.mixer.get_init():
pygame.mixer.quit()
except ImportError:
pass
def closeEvent(self, event):
if self.is_converting:
box = QMessageBox(self)
@@ -2804,11 +2867,13 @@ class abogen(QWidget):
box.setDefaultButton(QMessageBox.StandardButton.No)
if box.exec() == QMessageBox.StandardButton.Yes:
self.cleanup_conversion_thread()
self.cleanup_preview_threads()
event.accept()
else:
event.ignore()
else:
self.cleanup_conversion_thread()
self.cleanup_preview_threads()
event.accept()
def show_chapter_options_dialog(self, chapter_count):