mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 19:50:59 +02:00
- Language enum: en-US, en-GB, es, fr, hi, it, ja, pt-BR, zh - Engine-specific mappings (kokoro → single-letter) live in pipeline_factory and synthesize - spacy_utils uses Language enum keys for model mapping - split_pattern uses Language enum properties (is_cjk) - Updated all tests to use ISO codes
180 lines
5.2 KiB
Python
180 lines
5.2 KiB
Python
"""Domain enums — typed constants for values tied to business logic.
|
|
|
|
Using Enum instead of bare strings ensures:
|
|
- Invalid values are caught at construction time
|
|
- IDE autocomplete and type checking work
|
|
- Adding new values is explicit (must update Enum)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
|
|
|
|
class SubtitleMode(str, Enum):
|
|
"""Subtitle generation mode."""
|
|
DISABLED = "Disabled"
|
|
LINE = "Line"
|
|
SENTENCE = "Sentence"
|
|
SENTENCE_COMMA = "Sentence + Comma"
|
|
SENTENCE_HIGHLIGHT = "Sentence + Highlighting"
|
|
|
|
@classmethod
|
|
def from_str(cls, value: str) -> SubtitleMode:
|
|
"""Parse from user input: case-insensitive, strips whitespace."""
|
|
normalized = value.strip()
|
|
for member in cls:
|
|
if member.value.lower() == normalized.lower():
|
|
return member
|
|
raise ValueError(f"Invalid SubtitleMode: {value!r}. Valid: {[m.value for m in cls]}")
|
|
|
|
|
|
class OutputFormat(str, Enum):
|
|
"""Audio output format."""
|
|
WAV = "wav"
|
|
MP3 = "mp3"
|
|
FLAC = "flac"
|
|
OPUS = "opus"
|
|
M4B = "m4b"
|
|
|
|
@property
|
|
def dot_ext(self) -> str:
|
|
"""File extension with dot: '.wav', '.mp3', etc."""
|
|
return f".{self.value}"
|
|
|
|
@property
|
|
def is_lossless(self) -> bool:
|
|
"""True for lossless formats."""
|
|
return self in (self.WAV, self.FLAC)
|
|
|
|
@classmethod
|
|
def from_str(cls, value: str) -> OutputFormat:
|
|
"""Parse from user input: strips dot prefix, case-insensitive."""
|
|
normalized = value.strip().lstrip(".").lower()
|
|
for member in cls:
|
|
if member.value == normalized:
|
|
return member
|
|
raise ValueError(f"Invalid OutputFormat: {value!r}. Valid: {[m.value for m in cls]}")
|
|
|
|
|
|
class SaveMode(str, Enum):
|
|
"""Where to save the output file."""
|
|
SAVE_NEXT_TO_INPUT = "save_next_to_input"
|
|
SAVE_TO_DESKTOP = "save_to_desktop"
|
|
CHOOSE_OUTPUT_FOLDER = "choose_output_folder"
|
|
DEFAULT_OUTPUT = "default_output"
|
|
CUSTOM_FOLDER = "custom_folder"
|
|
|
|
|
|
class SubtitleFormat(str, Enum):
|
|
"""Subtitle file format."""
|
|
SRT = "srt"
|
|
ASS = "ass"
|
|
VTT = "vtt"
|
|
|
|
@property
|
|
def dot_ext(self) -> str:
|
|
"""File extension with dot: '.srt', '.ass'."""
|
|
return f".{self.value}"
|
|
|
|
@classmethod
|
|
def from_str(cls, value: str) -> SubtitleFormat:
|
|
"""Parse from user input: strips dot prefix, case-insensitive."""
|
|
normalized = value.strip().lstrip(".").lower()
|
|
for member in cls:
|
|
if member.value == normalized:
|
|
return member
|
|
raise ValueError(f"Invalid SubtitleFormat: {value!r}. Valid: {[m.value for m in cls]}")
|
|
|
|
|
|
class InputFormat(str, Enum):
|
|
"""Input file format."""
|
|
EPUB = "epub"
|
|
PDF = "pdf"
|
|
TXT = "txt"
|
|
MD = "md"
|
|
SRT = "srt"
|
|
ASS = "ass"
|
|
VTT = "vtt"
|
|
|
|
@property
|
|
def is_book(self) -> bool:
|
|
"""True for book/document formats (epub, pdf, txt, md)."""
|
|
return self in (self.EPUB, self.PDF, self.TXT, self.MD)
|
|
|
|
@property
|
|
def is_subtitle(self) -> bool:
|
|
"""True for subtitle formats (srt, ass, vtt)."""
|
|
return self in (self.SRT, self.ASS, self.VTT)
|
|
|
|
@property
|
|
def dot_ext(self) -> str:
|
|
"""File extension with dot: '.epub', '.srt', etc."""
|
|
return f".{self.value}"
|
|
|
|
@classmethod
|
|
def from_path(cls, path: Path) -> InputFormat:
|
|
"""Detect format from file path extension."""
|
|
suffix = path.suffix.lower().lstrip(".")
|
|
if suffix == "markdown":
|
|
return cls.MD
|
|
try:
|
|
return cls(suffix)
|
|
except ValueError:
|
|
raise ValueError(f"Unsupported input format: {path.suffix!r}. Supported: {[m.value for m in cls]}")
|
|
|
|
|
|
class Language(str, Enum):
|
|
"""TTS language code (ISO 639-1 with region where needed).
|
|
|
|
Each engine (Kokoro, Supertonic) maps these to its own
|
|
internal language identifiers.
|
|
"""
|
|
EN_US = "en-US"
|
|
EN_GB = "en-GB"
|
|
ES = "es"
|
|
FR = "fr"
|
|
HI = "hi"
|
|
IT = "it"
|
|
JA = "ja"
|
|
PT_BR = "pt-BR"
|
|
ZH = "zh"
|
|
|
|
@property
|
|
def display_name(self) -> str:
|
|
"""Human-readable language name."""
|
|
_names = {
|
|
"en-US": "American English",
|
|
"en-GB": "British English",
|
|
"es": "Spanish",
|
|
"fr": "French",
|
|
"hi": "Hindi",
|
|
"it": "Italian",
|
|
"ja": "Japanese",
|
|
"pt-BR": "Brazilian Portuguese",
|
|
"zh": "Mandarin Chinese",
|
|
}
|
|
return _names[self.value]
|
|
|
|
@property
|
|
def is_cjk(self) -> bool:
|
|
"""True for CJK languages (Chinese, Japanese)."""
|
|
return self in (self.ZH, self.JA)
|
|
|
|
@property
|
|
def supports_subtitle_tokens(self) -> bool:
|
|
"""True if this language generates timestamped tokens for subtitles."""
|
|
return self in (self.EN_US, self.EN_GB)
|
|
|
|
@classmethod
|
|
def from_str(cls, value: str) -> Language:
|
|
"""Parse from user input: ISO code, case-insensitive."""
|
|
if isinstance(value, Language):
|
|
return value
|
|
normalized = value.strip()
|
|
for member in cls:
|
|
if member.value.lower() == normalized.lower():
|
|
return member
|
|
raise ValueError(f"Invalid Language: {value!r}. Valid: {[m.value for m in cls]}")
|