mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Reformat using black
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
+16
-4
@@ -16,7 +16,7 @@ SPACY_MODELS = {
|
|||||||
"p": "pt_core_news_sm", # Brazilian Portuguese
|
"p": "pt_core_news_sm", # Brazilian Portuguese
|
||||||
"z": "zh_core_web_sm", # Mandarin Chinese
|
"z": "zh_core_web_sm", # Mandarin Chinese
|
||||||
"j": "ja_core_news_sm", # Japanese
|
"j": "ja_core_news_sm", # Japanese
|
||||||
"h": "xx_sent_ud_sm", # Hindi (multi-language model)
|
"h": "xx_sent_ud_sm", # Hindi (multi-language model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user