Reformat using black

This commit is contained in:
Deniz Şafak
2025-11-28 05:07:26 +03:00
parent bf43d1799d
commit 566158c132
5 changed files with 251 additions and 89 deletions
+6 -3
View File
@@ -48,13 +48,14 @@ logging.basicConfig(
_BRACKETED_NUMBERS_PATTERN = re.compile(r"\[\s*\d+\s*\]") _BRACKETED_NUMBERS_PATTERN = re.compile(r"\[\s*\d+\s*\]")
_STANDALONE_PAGE_NUMBERS_PATTERN = re.compile(r"^\s*\d+\s*$", re.MULTILINE) _STANDALONE_PAGE_NUMBERS_PATTERN = re.compile(r"^\s*\d+\s*$", re.MULTILINE)
_PAGE_NUMBERS_AT_END_PATTERN = re.compile(r"\s+\d+\s*$", re.MULTILINE) _PAGE_NUMBERS_AT_END_PATTERN = re.compile(r"\s+\d+\s*$", re.MULTILINE)
_PAGE_NUMBERS_WITH_DASH_PATTERN = re.compile(r"\s+[-–—]\s*\d+\s*[-–—]?\s*$", re.MULTILINE) _PAGE_NUMBERS_WITH_DASH_PATTERN = re.compile(
r"\s+[-–—]\s*\d+\s*[-–—]?\s*$", re.MULTILINE
)
_HTML_TAG_PATTERN = re.compile(r"<[^>]+>") _HTML_TAG_PATTERN = re.compile(r"<[^>]+>")
_LEADING_DASH_PATTERN = re.compile(r"^\s*[-–—]\s*") _LEADING_DASH_PATTERN = re.compile(r"^\s*[-–—]\s*")
_LEADING_SIMPLE_DASH_PATTERN = re.compile(r"^\s*-\s*") _LEADING_SIMPLE_DASH_PATTERN = re.compile(r"^\s*-\s*")
class HandlerDialog(QDialog): class HandlerDialog(QDialog):
# Class variables to remember checkbox states between dialog instances # Class variables to remember checkbox states between dialog instances
_save_chapters_separately = False _save_chapters_separately = False
@@ -2407,7 +2408,9 @@ class HandlerDialog(QDialog):
included_text_ids.add(child_id) included_text_ids.add(child_id)
if combined_text.strip(): if combined_text.strip():
# Use pre-compiled pattern for better performance # Use pre-compiled pattern for better performance
title = _LEADING_SIMPLE_DASH_PATTERN.sub("", parent_title).strip() title = _LEADING_SIMPLE_DASH_PATTERN.sub(
"", parent_title
).strip()
marker = f"<<CHAPTER_MARKER:{title}>>" marker = f"<<CHAPTER_MARKER:{title}>>"
section_titles.append((title, marker + "\n" + combined_text)) section_titles.append((title, marker + "\n" + combined_text))
included_text_ids.add(parent_id) included_text_ids.add(parent_id)
+107 -32
View File
@@ -29,7 +29,9 @@ import subprocess
import platform import platform
# Configuration constants # Configuration constants
_USER_RESPONSE_TIMEOUT = 0.1 # Timeout in seconds for checking user response/cancellation _USER_RESPONSE_TIMEOUT = (
0.1 # Timeout in seconds for checking user response/cancellation
)
# Pre-compile frequently used regex patterns for better performance # Pre-compile frequently used regex patterns for better performance
_METADATA_TAG_PATTERN = re.compile(r"<<METADATA_[^:]+:[^>]*>>") _METADATA_TAG_PATTERN = re.compile(r"<<METADATA_[^:]+:[^>]*>>")
@@ -48,7 +50,9 @@ _VTT_TIMESTAMP_PATTERN = re.compile(r"([\d:.]+)\s*-->\s*([\d:.]+)")
_TIMESTAMP_ONLY_PATTERN = re.compile(r"^(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3})?)$") _TIMESTAMP_ONLY_PATTERN = re.compile(r"^(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3})?)$")
_WINDOWS_ILLEGAL_CHARS_PATTERN = re.compile(r'[<>:"/\\|?*]') _WINDOWS_ILLEGAL_CHARS_PATTERN = re.compile(r'[<>:"/\\|?*]')
_CONTROL_CHARS_PATTERN = re.compile(r"[\x00-\x1f]") _CONTROL_CHARS_PATTERN = re.compile(r"[\x00-\x1f]")
_LINUX_CONTROL_CHARS_PATTERN = re.compile(r"[\x01-\x1f]") # Linux: exclude \x00 for separate handling _LINUX_CONTROL_CHARS_PATTERN = re.compile(
r"[\x01-\x1f]"
) # Linux: exclude \x00 for separate handling
_MACOS_ILLEGAL_CHARS_PATTERN = re.compile(r"[:]") _MACOS_ILLEGAL_CHARS_PATTERN = re.compile(r"[:]")
_LINUX_ILLEGAL_CHARS_PATTERN = re.compile(r"[/\x00]") _LINUX_ILLEGAL_CHARS_PATTERN = re.compile(r"[/\x00]")
@@ -221,7 +225,9 @@ def detect_timestamps_in_text(file_path):
# Count lines that are ONLY timestamps (no other text) # Count lines that are ONLY timestamps (no other text)
# Supports HH:MM:SS or HH:MM:SS,ms format # Supports HH:MM:SS or HH:MM:SS,ms format
# Use pre-compiled pattern for better performance # Use pre-compiled pattern for better performance
timestamp_lines = sum(1 for line in lines if _TIMESTAMP_ONLY_PATTERN.match(line)) timestamp_lines = sum(
1 for line in lines if _TIMESTAMP_ONLY_PATTERN.match(line)
)
# Must have at least 2 timestamp-only lines and they should be >5% of total lines # Must have at least 2 timestamp-only lines and they should be >5% of total lines
return timestamp_lines >= 2 and (timestamp_lines / max(len(lines), 1)) > 0.05 return timestamp_lines >= 2 and (timestamp_lines / max(len(lines), 1)) > 0.05
@@ -369,7 +375,9 @@ def parse_ass_file(file_path):
# Clean text of ASS styling tags using pre-compiled patterns # Clean text of ASS styling tags using pre-compiled patterns
text = _ASS_STYLING_PATTERN.sub("", text) # Remove {tags} text = _ASS_STYLING_PATTERN.sub("", text) # Remove {tags}
text = _ASS_NEWLINE_N_PATTERN.sub("\n", text) # Convert \N to newline text = _ASS_NEWLINE_N_PATTERN.sub("\n", text) # Convert \N to newline
text = _ASS_NEWLINE_LOWER_N_PATTERN.sub("\n", text) # Convert \n to newline text = _ASS_NEWLINE_LOWER_N_PATTERN.sub(
"\n", text
) # Convert \n to newline
# Remove chapter markers and metadata tags # Remove chapter markers and metadata tags
text = clean_subtitle_text(text) text = clean_subtitle_text(text)
@@ -643,7 +651,9 @@ class ConversionThread(QThread):
elif subtitle_mode == "Sentence": elif subtitle_mode == "Sentence":
return r"(?<=[{}]){}|\n+".format(self.PUNCTUATION_SENTENCE, spacing_pattern) return r"(?<=[{}]){}|\n+".format(self.PUNCTUATION_SENTENCE, spacing_pattern)
elif subtitle_mode == "Sentence + Comma": elif subtitle_mode == "Sentence + Comma":
return r"(?<=[{}]){}|\n+".format(self.PUNCTUATION_SENTENCE_COMMA, spacing_pattern) return r"(?<=[{}]){}|\n+".format(
self.PUNCTUATION_SENTENCE_COMMA, spacing_pattern
)
else: else:
return r"\n+" # Default to line breaks return r"\n+" # Default to line breaks
@@ -745,7 +755,9 @@ class ConversionThread(QThread):
# Clear segment bytes from memory # Clear segment bytes from memory
del segment_bytes del segment_bytes
except Exception as e: except Exception as e:
self.log_updated.emit((f"Error processing segment {i}: {str(e)}", "red")) self.log_updated.emit(
(f"Error processing segment {i}: {str(e)}", "red")
)
raise raise
return samples_processed return samples_processed
@@ -803,7 +815,9 @@ class ConversionThread(QThread):
self.log_updated.emit( self.log_updated.emit(
f"- Subtitle format: {next((label for value, label in SUBTITLE_FORMATS if value == getattr(self, 'subtitle_format', 'srt')), getattr(self, 'subtitle_format', 'srt'))}" f"- Subtitle format: {next((label for value, label in SUBTITLE_FORMATS if value == getattr(self, 'subtitle_format', 'srt')), getattr(self, 'subtitle_format', 'srt'))}"
) )
self.log_updated.emit(f"- Use spaCy for sentence segmentation: {'Yes' if getattr(self, 'use_spacy_segmentation', False) else 'No'}") self.log_updated.emit(
f"- Use spaCy for sentence segmentation: {'Yes' if getattr(self, 'use_spacy_segmentation', False) else 'No'}"
)
self.log_updated.emit(f"- Save option: {self.save_option}") self.log_updated.emit(f"- Save option: {self.save_option}")
if self.replace_single_newlines: if self.replace_single_newlines:
self.log_updated.emit(f"- Replace single newlines: Yes") self.log_updated.emit(f"- Replace single newlines: Yes")
@@ -884,11 +898,15 @@ class ConversionThread(QThread):
) )
elif file_ext == ".txt" and detect_timestamps_in_text(self.file_name): elif file_ext == ".txt" and detect_timestamps_in_text(self.file_name):
is_timestamp_text = True is_timestamp_text = True
self.log_updated.emit(("\nDetected timestamps in text file", "grey")) self.log_updated.emit(
("\nDetected timestamps in text file", "grey")
)
# Signal to ask user (-1 indicates timestamp detection) # Signal to ask user (-1 indicates timestamp detection)
self.chapters_detected.emit(-1) self.chapters_detected.emit(-1)
# Wait for user response using event with timeout for responsive cancellation # Wait for user response using event with timeout for responsive cancellation
while not self._timestamp_response_event.wait(timeout=_USER_RESPONSE_TIMEOUT): while not self._timestamp_response_event.wait(
timeout=_USER_RESPONSE_TIMEOUT
):
if self.cancel_requested: if self.cancel_requested:
self.conversion_finished.emit("Cancelled", None) self.conversion_finished.emit("Cancelled", None)
return return
@@ -1029,7 +1047,9 @@ class ConversionThread(QThread):
) )
# Only check for files with allowed extensions (extension without dot, case-insensitive) # Only check for files with allowed extensions (extension without dot, case-insensitive)
# Use generator expression to avoid processing all files upfront # Use generator expression to avoid processing all files upfront
file_parts = (os.path.splitext(fname) for fname in os.listdir(parent_dir)) file_parts = (
os.path.splitext(fname) for fname in os.listdir(parent_dir)
)
clash = any( clash = any(
name == f"{sanitized_base_name}{suffix}" name == f"{sanitized_base_name}{suffix}"
and ext[1:].lower() in allowed_exts and ext[1:].lower() in allowed_exts
@@ -1389,7 +1409,8 @@ class ConversionThread(QThread):
is_subtitle_input = ( is_subtitle_input = (
not self.is_direct_text not self.is_direct_text
and self.file_name and self.file_name
and os.path.splitext(self.file_name)[1].lower() in [".srt", ".ass", ".vtt"] and os.path.splitext(self.file_name)[1].lower()
in [".srt", ".ass", ".vtt"]
) )
use_spacy = ( use_spacy = (
getattr(self, "use_spacy_segmentation", False) getattr(self, "use_spacy_segmentation", False)
@@ -1401,30 +1422,57 @@ class ConversionThread(QThread):
spacing_pattern = r"\s*" if self.lang_code in ["z", "j"] else r"\s+" spacing_pattern = r"\s*" if self.lang_code in ["z", "j"] else r"\s+"
# Pre-load spaCy model for English if it will be needed for subtitle generation # Pre-load spaCy model for English if it will be needed for subtitle generation
if use_spacy and self.lang_code in ["a", "b"] and self.subtitle_mode in ["Sentence", "Sentence + Comma"]: if (
use_spacy
and self.lang_code in ["a", "b"]
and self.subtitle_mode in ["Sentence", "Sentence + Comma"]
):
from abogen.spacy_utils import get_spacy_model from abogen.spacy_utils import get_spacy_model
nlp = get_spacy_model(self.lang_code, log_callback=lambda msg: self.log_updated.emit(msg))
nlp = get_spacy_model(
self.lang_code,
log_callback=lambda msg: self.log_updated.emit(msg),
)
if nlp: if nlp:
self.log_updated.emit(("\nUsing spaCy for sentence segmentation (only for subtitles)...", "grey")) self.log_updated.emit(
(
"\nUsing spaCy for sentence segmentation (only for subtitles)...",
"grey",
)
)
if use_spacy and self.lang_code not in ["a", "b"]: if use_spacy and self.lang_code not in ["a", "b"]:
# Non-English: use spaCy for pre-TTS segmentation # Non-English: use spaCy for pre-TTS segmentation
self.log_updated.emit(("\nUsing spaCy for sentence segmentation (pre-TTS)...", "grey")) self.log_updated.emit(
("\nUsing spaCy for sentence segmentation (pre-TTS)...", "grey")
)
from abogen.spacy_utils import segment_sentences from abogen.spacy_utils import segment_sentences
spacy_sentences = segment_sentences( spacy_sentences = segment_sentences(
chapter_text, chapter_text,
self.lang_code, self.lang_code,
log_callback=lambda msg: self.log_updated.emit(msg) log_callback=lambda msg: self.log_updated.emit(msg),
) )
if spacy_sentences: if spacy_sentences:
self.log_updated.emit((f"\nspaCy: Text segmented into {len(spacy_sentences)} sentences...", "grey")) self.log_updated.emit(
(
f"\nspaCy: Text segmented into {len(spacy_sentences)} sentences...",
"grey",
)
)
# For Sentence + Comma mode, still split on commas within spaCy sentences # For Sentence + Comma mode, still split on commas within spaCy sentences
if self.subtitle_mode == "Sentence + Comma": if self.subtitle_mode == "Sentence + Comma":
active_split_pattern = r"(?<=[{}]){}|\n+".format(self.PUNCTUATION_COMMAS, spacing_pattern) active_split_pattern = r"(?<=[{}]){}|\n+".format(
self.PUNCTUATION_COMMAS, spacing_pattern
)
else: else:
active_split_pattern = "\n" # Use newline splitting for Sentence mode active_split_pattern = (
"\n" # Use newline splitting for Sentence mode
)
else: else:
self.log_updated.emit(("\nspaCy: Fallback to default segmentation...", "grey")) self.log_updated.emit(
("\nspaCy: Fallback to default segmentation...", "grey")
)
# Process text - either as spaCy sentences or as single text # Process text - either as spaCy sentences or as single text
text_segments = spacy_sentences if spacy_sentences else [chapter_text] text_segments = spacy_sentences if spacy_sentences else [chapter_text]
@@ -1498,7 +1546,9 @@ class ConversionThread(QThread):
self.end_ts = end self.end_ts = end
self.whitespace = "" self.whitespace = ""
tokens_list = [FakeToken(result.graphemes, 0, chunk_dur)] tokens_list = [
FakeToken(result.graphemes, 0, chunk_dur)
]
tokens_with_timestamps = [] tokens_with_timestamps = []
chapter_tokens_with_timestamps = [] chapter_tokens_with_timestamps = []
@@ -1518,7 +1568,8 @@ class ConversionThread(QThread):
{ {
"start": chapter_current_time "start": chapter_current_time
+ (tok.start_ts or 0), + (tok.start_ts or 0),
"end": chapter_current_time + (tok.end_ts or 0), "end": chapter_current_time
+ (tok.end_ts or 0),
"text": tok.text, "text": tok.text,
"whitespace": tok.whitespace, "whitespace": tok.whitespace,
} }
@@ -1602,7 +1653,10 @@ class ConversionThread(QThread):
chapter_current_time += chunk_dur chapter_current_time += chunk_dur
# Calculate percentage based on characters processed # Calculate percentage based on characters processed
percent = min( percent = min(
int(self.processed_char_count / self.total_char_count * 100), 99 int(
self.processed_char_count / self.total_char_count * 100
),
99,
) )
# Calculate ETR based on characters processed # Calculate ETR based on characters processed
@@ -1615,7 +1669,9 @@ class ConversionThread(QThread):
chars_done > 0 and elapsed > 0.5 chars_done > 0 and elapsed > 0.5
): # Check elapsed > 0.5 to avoid instability ): # Check elapsed > 0.5 to avoid instability
avg_time_per_char = elapsed / chars_done avg_time_per_char = elapsed / chars_done
remaining = self.total_char_count - self.processed_char_count remaining = (
self.total_char_count - self.processed_char_count
)
if remaining > 0: if remaining > 0:
secs = avg_time_per_char * remaining secs = avg_time_per_char * remaining
h = int(secs // 3600) h = int(secs // 3600)
@@ -1818,7 +1874,9 @@ class ConversionThread(QThread):
) )
return return
self.log_updated.emit((f"\nFound {len(subtitles)} subtitle entries", "grey")) self.log_updated.emit(
(f"\nFound {len(subtitles)} subtitle entries", "grey")
)
# Setup output paths # Setup output paths
base_name = os.path.splitext(os.path.basename(base_path))[0] base_name = os.path.splitext(os.path.basename(base_path))[0]
@@ -2000,7 +2058,11 @@ class ConversionThread(QThread):
int(start_time % 60), int(start_time % 60),
) )
ms1 = int((start_time - int(start_time)) * 1000) ms1 = int((start_time - int(start_time)) * 1000)
is_last = is_timestamp_text or (use_gaps and idx == len(subtitles)) or end_time is None is_last = (
is_timestamp_text
or (use_gaps and idx == len(subtitles))
or end_time is None
)
if is_last: if is_last:
time_str = ( time_str = (
f"{h1:02d}:{m1:02d}:{s1:02d}" f"{h1:02d}:{m1:02d}:{s1:02d}"
@@ -2477,7 +2539,10 @@ class ConversionThread(QThread):
if use_spacy_for_english and self.subtitle_mode != "Line": if use_spacy_for_english and self.subtitle_mode != "Line":
# Use spaCy for English sentence boundary detection (model already loaded) # Use spaCy for English sentence boundary detection (model already loaded)
from abogen.spacy_utils import get_spacy_model from abogen.spacy_utils import get_spacy_model
nlp = get_spacy_model(self.lang_code) # No log_callback since model is already loaded
nlp = get_spacy_model(
self.lang_code
) # No log_callback since model is already loaded
if nlp: if nlp:
# Build full text and track character positions to token indices # Build full text and track character positions to token indices
full_text = "" full_text = ""
@@ -2494,8 +2559,12 @@ class ConversionThread(QThread):
# For "Sentence + Comma" mode, also split on commas # For "Sentence + Comma" mode, also split on commas
if self.subtitle_mode == "Sentence + Comma": if self.subtitle_mode == "Sentence + Comma":
comma_positions = [i + 1 for i, c in enumerate(full_text) if c == ','] comma_positions = [
sentence_boundaries = sorted(set(sentence_boundaries + comma_positions)) i + 1 for i, c in enumerate(full_text) if c == ","
]
sentence_boundaries = sorted(
set(sentence_boundaries + comma_positions)
)
# Group tokens by sentence boundaries # Group tokens by sentence boundaries
current_sentence = [] current_sentence = []
@@ -2506,7 +2575,9 @@ class ConversionThread(QThread):
for idx, token in enumerate(processed_tokens): for idx, token in enumerate(processed_tokens):
current_sentence.append(token) current_sentence.append(token)
word_count += 1 word_count += 1
text_len = len(token["text"]) + len(token.get("whitespace", "") or "") text_len = len(token["text"]) + len(
token.get("whitespace", "") or ""
)
current_char_pos += text_len current_char_pos += text_len
# Check if we've hit a sentence boundary or max words # Check if we've hit a sentence boundary or max words
@@ -2522,7 +2593,9 @@ class ConversionThread(QThread):
t["text"] + (t.get("whitespace", "") or "") t["text"] + (t.get("whitespace", "") or "")
for t in current_sentence for t in current_sentence
) )
subtitle_entries.append((start_time, end_time, sentence_text.strip())) subtitle_entries.append(
(start_time, end_time, sentence_text.strip())
)
current_sentence = [] current_sentence = []
word_count = 0 word_count = 0
if at_boundary: if at_boundary:
@@ -2536,7 +2609,9 @@ class ConversionThread(QThread):
t["text"] + (t.get("whitespace", "") or "") t["text"] + (t.get("whitespace", "") or "")
for t in current_sentence for t in current_sentence
) )
subtitle_entries.append((start_time, end_time, sentence_text.strip())) subtitle_entries.append(
(start_time, end_time, sentence_text.strip())
)
# Fallback for last entry # Fallback for last entry
if subtitle_entries and fallback_end_time is not None: if subtitle_entries and fallback_end_time is not None:
+6 -2
View File
@@ -2851,7 +2851,7 @@ class abogen(QWidget):
# Cleanup pygame mixer if initialized # Cleanup pygame mixer if initialized
try: try:
pygame = sys.modules.get('pygame') pygame = sys.modules.get("pygame")
if pygame and pygame.mixer.get_init(): if pygame and pygame.mixer.get_init():
pygame.mixer.quit() pygame.mixer.quit()
except Exception: except Exception:
@@ -3222,7 +3222,9 @@ class abogen(QWidget):
menu.addSeparator() menu.addSeparator()
# Add "Pre-download models and voices for offline use" option # Add "Pre-download models and voices for offline use" option
predownload_action = QAction("Pre-download models and voices for offline use", self) predownload_action = QAction(
"Pre-download models and voices for offline use", self
)
predownload_action.triggered.connect(self.show_predownload_dialog) predownload_action.triggered.connect(self.show_predownload_dialog)
menu.addAction(predownload_action) menu.addAction(predownload_action)
@@ -3294,6 +3296,7 @@ class abogen(QWidget):
self.use_spacy_segmentation = enabled self.use_spacy_segmentation = enabled
self.config["use_spacy_segmentation"] = enabled self.config["use_spacy_segmentation"] = enabled
save_config(self.config) save_config(self.config)
def restart_app(self): def restart_app(self):
import sys import sys
@@ -3592,6 +3595,7 @@ Categories=AudioVideo;Audio;Utility;
def show_predownload_dialog(self): def show_predownload_dialog(self):
"""Show the pre-download models and voices dialog.""" """Show the pre-download models and voices dialog."""
from abogen.predownload_gui import PreDownloadDialog from abogen.predownload_gui import PreDownloadDialog
dialog = PreDownloadDialog(self) dialog = PreDownloadDialog(self)
dialog.exec() dialog.exec()
+89 -21
View File
@@ -10,7 +10,15 @@ from typing import List, Optional, Tuple
import importlib import importlib
import importlib.util import importlib.util
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSpacerItem, QSizePolicy from PyQt6.QtWidgets import (
QDialog,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
QSpacerItem,
QSizePolicy,
)
from PyQt6.QtCore import QThread, pyqtSignal from PyQt6.QtCore import QThread, pyqtSignal
from abogen.constants import COLORS, VOICES_INTERNAL from abogen.constants import COLORS, VOICES_INTERNAL
@@ -100,7 +108,9 @@ class PreDownloadWorker(QThread):
try: try:
from huggingface_hub import hf_hub_download, try_to_load_from_cache from huggingface_hub import hf_hub_download, try_to_load_from_cache
except Exception: except Exception:
self.progress.emit("voice", "warning", "huggingface_hub not installed, skipping voices...") self.progress.emit(
"voice", "warning", "huggingface_hub not installed, skipping voices..."
)
self._voices_success = False self._voices_success = False
return return
@@ -111,14 +121,22 @@ class PreDownloadWorker(QThread):
return return
filename = f"voices/{voice}.pt" filename = f"voices/{voice}.pt"
if try_to_load_from_cache(repo_id=self._repo_id, filename=filename): if try_to_load_from_cache(repo_id=self._repo_id, filename=filename):
self.progress.emit("voice", "installed", f"{idx}/{len(voice_list)}: {voice} already present") self.progress.emit(
"voice",
"installed",
f"{idx}/{len(voice_list)}: {voice} already present",
)
continue continue
self.progress.emit("voice", "downloading", f"{idx}/{len(voice_list)}: {voice}...") self.progress.emit(
"voice", "downloading", f"{idx}/{len(voice_list)}: {voice}..."
)
try: try:
hf_hub_download(repo_id=self._repo_id, filename=filename) hf_hub_download(repo_id=self._repo_id, filename=filename)
self.progress.emit("voice", "downloaded", f"{voice} downloaded") self.progress.emit("voice", "downloaded", f"{voice} downloaded")
except Exception as exc: except Exception as exc:
self.progress.emit("voice", "warning", f"could not download {voice}: {exc}") self.progress.emit(
"voice", "warning", f"could not download {voice}: {exc}"
)
self._voices_success = False self._voices_success = False
# Kokoro model # Kokoro model
@@ -127,7 +145,9 @@ class PreDownloadWorker(QThread):
try: try:
from huggingface_hub import hf_hub_download, try_to_load_from_cache from huggingface_hub import hf_hub_download, try_to_load_from_cache
except Exception: except Exception:
self.progress.emit("model", "warning", "huggingface_hub not installed, skipping model...") self.progress.emit(
"model", "warning", "huggingface_hub not installed, skipping model..."
)
self._model_success = False self._model_success = False
return return
for fname in self._model_files: for fname in self._model_files:
@@ -136,14 +156,18 @@ class PreDownloadWorker(QThread):
return return
category = "config" if fname == "config.json" else "model" category = "config" if fname == "config.json" else "model"
if try_to_load_from_cache(repo_id=self._repo_id, filename=fname): if try_to_load_from_cache(repo_id=self._repo_id, filename=fname):
self.progress.emit(category, "installed", f"file {fname} already present") self.progress.emit(
category, "installed", f"file {fname} already present"
)
continue continue
self.progress.emit(category, "downloading", f"file {fname}...") self.progress.emit(category, "downloading", f"file {fname}...")
try: try:
hf_hub_download(repo_id=self._repo_id, filename=fname) hf_hub_download(repo_id=self._repo_id, filename=fname)
self.progress.emit(category, "downloaded", f"file {fname} downloaded") self.progress.emit(category, "downloaded", f"file {fname} downloaded")
except Exception as exc: except Exception as exc:
self.progress.emit(category, "warning", f"could not download file {fname}: {exc}") self.progress.emit(
category, "warning", f"could not download file {fname}: {exc}"
)
self._model_success = False self._model_success = False
# spaCy models # spaCy models
@@ -158,7 +182,11 @@ class PreDownloadWorker(QThread):
parent = self.parent() parent = self.parent()
models_to_process: List[str] = _unique_sorted_models() models_to_process: List[str] = _unique_sorted_models()
try: try:
if parent is not None and hasattr(parent, "_spacy_models_missing") and parent._spacy_models_missing: if (
parent is not None
and hasattr(parent, "_spacy_models_missing")
and parent._spacy_models_missing
):
models_to_process = list(dict.fromkeys(parent._spacy_models_missing)) models_to_process = list(dict.fromkeys(parent._spacy_models_missing))
except Exception: except Exception:
pass pass
@@ -167,7 +195,9 @@ class PreDownloadWorker(QThread):
try: try:
import spacy.cli as _spacy_cli import spacy.cli as _spacy_cli
except Exception: except Exception:
self.progress.emit("spacy", "warning", "spaCy not available, skipping spaCy models...") self.progress.emit(
"spacy", "warning", "spaCy not available, skipping spaCy models..."
)
self._spacy_success = False self._spacy_success = False
return return
@@ -176,14 +206,24 @@ class PreDownloadWorker(QThread):
self._spacy_success = False self._spacy_success = False
return return
if _is_package_installed(model_name): if _is_package_installed(model_name):
self.progress.emit("spacy", "installed", f"{idx}/{len(models_to_process)}: {model_name} already installed") self.progress.emit(
"spacy",
"installed",
f"{idx}/{len(models_to_process)}: {model_name} already installed",
)
continue continue
self.progress.emit("spacy", "downloading", f"{idx}/{len(models_to_process)}: {model_name}...") self.progress.emit(
"spacy",
"downloading",
f"{idx}/{len(models_to_process)}: {model_name}...",
)
try: try:
_spacy_cli.download(model_name) _spacy_cli.download(model_name)
self.progress.emit("spacy", "downloaded", f"{model_name} downloaded") self.progress.emit("spacy", "downloaded", f"{model_name} downloaded")
except Exception as exc: except Exception as exc:
self.progress.emit("spacy", "warning", f"could not download {model_name}: {exc}") self.progress.emit(
"spacy", "warning", f"could not download {model_name}: {exc}"
)
self._spacy_success = False self._spacy_success = False
@@ -271,7 +311,9 @@ class PreDownloadDialog(QDialog):
layout.addLayout(status_layout) layout.addLayout(status_layout)
layout.addItem(QSpacerItem(0, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)) layout.addItem(
QSpacerItem(0, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
)
# Buttons # Buttons
button_row = QHBoxLayout() button_row = QHBoxLayout()
@@ -339,7 +381,12 @@ class PreDownloadDialog(QDialog):
# These are initialized in __init__ to keep consistent object state # These are initialized in __init__ to keep consistent object state
# Set checking visual state # Set checking visual state
for lbl in (self.voices_status, self.model_status, self.config_status, self.spacy_status): for lbl in (
self.voices_status,
self.model_status,
self.config_status,
self.spacy_status,
):
lbl.setStyleSheet(f"color: {COLORS['ORANGE']};") lbl.setStyleSheet(f"color: {COLORS['ORANGE']};")
self.spacy_status.setText(self.SPACY_PREFIX + "⏳ Checking...") self.spacy_status.setText(self.SPACY_PREFIX + "⏳ Checking...")
@@ -356,7 +403,9 @@ class PreDownloadDialog(QDialog):
checked = len(self._spacy_models_checked) checked = len(self._spacy_models_checked)
missing_count = len(self._spacy_models_missing) missing_count = len(self._spacy_models_missing)
if missing_count: if missing_count:
self.spacy_status.setText(f"{self.SPACY_PREFIX}{checked} checked, {missing_count} missing...") self.spacy_status.setText(
f"{self.SPACY_PREFIX}{checked} checked, {missing_count} missing..."
)
else: else:
self.spacy_status.setText(f"{self.SPACY_PREFIX}{checked} checked...") self.spacy_status.setText(f"{self.SPACY_PREFIX}{checked} checked...")
@@ -366,7 +415,9 @@ class PreDownloadDialog(QDialog):
else: else:
self.has_missing = True self.has_missing = True
if missing: if missing:
self._set_status("voice", f"✗ Missing {len(missing)} voices", COLORS["RED"]) self._set_status(
"voice", f"✗ Missing {len(missing)} voices", COLORS["RED"]
)
else: else:
self._set_status("voice", "✗ Not downloaded", COLORS["RED"]) self._set_status("voice", "✗ Not downloaded", COLORS["RED"])
@@ -390,7 +441,9 @@ class PreDownloadDialog(QDialog):
else: else:
self.has_missing = True self.has_missing = True
if missing: if missing:
self._set_status("spacy", f"✗ Missing {len(missing)} model(s)", COLORS["RED"]) self._set_status(
"spacy", f"✗ Missing {len(missing)} model(s)", COLORS["RED"]
)
else: else:
self._set_status("spacy", "✗ Not downloaded", COLORS["RED"]) self._set_status("spacy", "✗ Not downloaded", COLORS["RED"])
self.download_btn.setEnabled(self.has_missing) self.download_btn.setEnabled(self.has_missing)
@@ -408,8 +461,11 @@ class PreDownloadDialog(QDialog):
missing = [] missing = []
try: try:
from huggingface_hub import try_to_load_from_cache from huggingface_hub import try_to_load_from_cache
for voice in VOICES_INTERNAL: for voice in VOICES_INTERNAL:
if not try_to_load_from_cache(repo_id="hexgrad/Kokoro-82M", filename=f"voices/{voice}.pt"): if not try_to_load_from_cache(
repo_id="hexgrad/Kokoro-82M", filename=f"voices/{voice}.pt"
):
missing.append(voice) missing.append(voice)
except Exception: except Exception:
# If HF missing, report all as missing # If HF missing, report all as missing
@@ -419,14 +475,26 @@ class PreDownloadDialog(QDialog):
def _check_kokoro_model(self) -> bool: def _check_kokoro_model(self) -> bool:
try: try:
from huggingface_hub import try_to_load_from_cache from huggingface_hub import try_to_load_from_cache
return try_to_load_from_cache(repo_id="hexgrad/Kokoro-82M", filename="kokoro-v1_0.pth") is not None
return (
try_to_load_from_cache(
repo_id="hexgrad/Kokoro-82M", filename="kokoro-v1_0.pth"
)
is not None
)
except Exception: except Exception:
return False return False
def _check_kokoro_config(self) -> bool: def _check_kokoro_config(self) -> bool:
try: try:
from huggingface_hub import try_to_load_from_cache from huggingface_hub import try_to_load_from_cache
return try_to_load_from_cache(repo_id="hexgrad/Kokoro-82M", filename="config.json") is not None
return (
try_to_load_from_cache(
repo_id="hexgrad/Kokoro-82M", filename="config.json"
)
is not None
)
except Exception: except Exception:
return False return False
+15 -3
View File
@@ -26,6 +26,7 @@ def _load_spacy():
if _spacy is None: if _spacy is None:
try: try:
import spacy import spacy
_spacy = spacy _spacy = spacy
except ImportError: except ImportError:
return None return None
@@ -44,6 +45,7 @@ def get_spacy_model(lang_code, log_callback=None):
Returns: Returns:
Loaded spaCy model or None if unavailable Loaded spaCy model or None if unavailable
""" """
def log(msg, is_error=False): def log(msg, is_error=False):
# Prefer GUI log callback when provided to avoid spamming stdout. # Prefer GUI log callback when provided to avoid spamming stdout.
if log_callback: if log_callback:
@@ -75,7 +77,10 @@ def get_spacy_model(lang_code, log_callback=None):
# Try to load the model # Try to load the model
try: try:
log(f"\nLoading spaCy model '{model_name}'...") log(f"\nLoading spaCy model '{model_name}'...")
nlp = spacy.load(model_name, disable=["ner", "parser", "tagger", "lemmatizer", "attribute_ruler"]) nlp = spacy.load(
model_name,
disable=["ner", "parser", "tagger", "lemmatizer", "attribute_ruler"],
)
# Enable sentence segmentation only # Enable sentence segmentation only
if "sentencizer" not in nlp.pipe_names: if "sentencizer" not in nlp.pipe_names:
nlp.add_pipe("sentencizer") nlp.add_pipe("sentencizer")
@@ -86,16 +91,23 @@ def get_spacy_model(lang_code, log_callback=None):
log(f"\nspaCy: Downloading model '{model_name}'...") log(f"\nspaCy: Downloading model '{model_name}'...")
try: try:
from spacy.cli import download from spacy.cli import download
download(model_name) download(model_name)
# Retry loading # Retry loading
nlp = spacy.load(model_name, disable=["ner", "parser", "tagger", "lemmatizer", "attribute_ruler"]) nlp = spacy.load(
model_name,
disable=["ner", "parser", "tagger", "lemmatizer", "attribute_ruler"],
)
if "sentencizer" not in nlp.pipe_names: if "sentencizer" not in nlp.pipe_names:
nlp.add_pipe("sentencizer") nlp.add_pipe("sentencizer")
_nlp_cache[lang_code] = nlp _nlp_cache[lang_code] = nlp
log(f"spaCy model '{model_name}' downloaded and loaded") log(f"spaCy model '{model_name}' downloaded and loaded")
return nlp return nlp
except Exception as e: except Exception as e:
log(f"\nspaCy: Failed to download model '{model_name}': {e}...", is_error=True) log(
f"\nspaCy: Failed to download model '{model_name}': {e}...",
is_error=True,
)
return None return None
except Exception as e: except Exception as e:
log(f"\nspaCy: Error loading model '{model_name}': {e}...", is_error=True) log(f"\nspaCy: Error loading model '{model_name}': {e}...", is_error=True)