mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
refactor: typed Enums for format/mode fields
- SubtitleMode, OutputFormat, SaveMode, SubtitleFormat, InputFormat - Properties: dot_ext, is_lossless, is_book, is_subtitle - from_str/from_path class methods with normalization - Updated domain and application layers to use Enums - 17 new tests for enum validation and properties
This commit is contained in:
@@ -33,6 +33,7 @@ from abogen.domain.conversion_engine import (
|
||||
process_and_write_subtitles,
|
||||
synthesize_text,
|
||||
)
|
||||
from abogen.domain.enums import OutputFormat, SubtitleMode
|
||||
from abogen.domain.normalization import TTSContext
|
||||
from abogen.domain.output_paths import sanitize_filename_for_chapter
|
||||
from abogen.infrastructure.subtitle_writer import make_subtitle_writer
|
||||
@@ -87,7 +88,7 @@ def execute_conversion(
|
||||
)
|
||||
|
||||
# Compute subtitle flag once (used in every synthesize_text call)
|
||||
use_spacy = request.subtitle_mode not in ("Disabled", "Line")
|
||||
use_spacy = request.subtitle_mode not in (SubtitleMode.DISABLED, SubtitleMode.LINE)
|
||||
|
||||
# Output paths
|
||||
output_layout = plan.output_layout
|
||||
@@ -96,7 +97,7 @@ def execute_conversion(
|
||||
|
||||
# Determine if merged output is needed
|
||||
merge_chapters = request.merge_chapters_at_end or not request.save_chapters_separately
|
||||
if request.output_format.lower() == "m4b":
|
||||
if request.output_format == OutputFormat.M4B:
|
||||
merge_chapters = True
|
||||
|
||||
# Resolve voices
|
||||
@@ -125,7 +126,7 @@ def execute_conversion(
|
||||
|
||||
# Open subtitle writer if needed
|
||||
subtitle_writer: Optional[SubtitleWriter] = None
|
||||
if request.subtitle_mode != "Disabled" and audio_sink:
|
||||
if request.subtitle_mode != SubtitleMode.DISABLED and audio_sink:
|
||||
subtitle_writer = make_subtitle_writer(
|
||||
audio_path,
|
||||
request.subtitle_format,
|
||||
@@ -137,7 +138,7 @@ def execute_conversion(
|
||||
stack.callback(subtitle_writer.close)
|
||||
result.subtitle_paths.append(subtitle_writer.path)
|
||||
|
||||
effective_subtitle_mode = request.subtitle_mode if subtitle_writer else "Disabled"
|
||||
effective_subtitle_mode = request.subtitle_mode if subtitle_writer else SubtitleMode.DISABLED
|
||||
|
||||
synth = SynthParams(
|
||||
tts_context=tts_context,
|
||||
|
||||
@@ -13,6 +13,8 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from abogen.domain.enums import OutputFormat, SaveMode, SubtitleFormat, SubtitleMode
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConversionRequest:
|
||||
@@ -37,17 +39,17 @@ class ConversionRequest:
|
||||
supertonic_total_steps: int = 5
|
||||
|
||||
# --- Output Format ---
|
||||
output_format: str = "wav"
|
||||
subtitle_mode: str = "Disabled"
|
||||
subtitle_format: str = "srt"
|
||||
output_format: OutputFormat = OutputFormat.WAV
|
||||
subtitle_mode: SubtitleMode = SubtitleMode.DISABLED
|
||||
subtitle_format: SubtitleFormat = SubtitleFormat.SRT
|
||||
max_subtitle_words: int = 50
|
||||
|
||||
# --- Save Options ---
|
||||
save_mode: str = "save_next_to_input"
|
||||
save_mode: SaveMode = SaveMode.SAVE_NEXT_TO_INPUT
|
||||
output_folder: Optional[Path] = None
|
||||
save_chapters_separately: bool = False
|
||||
merge_chapters_at_end: bool = True
|
||||
separate_chapters_format: str = "wav"
|
||||
separate_chapters_format: OutputFormat = OutputFormat.WAV
|
||||
save_as_project: bool = False
|
||||
|
||||
# --- Timing ---
|
||||
|
||||
@@ -27,6 +27,7 @@ from abogen.application.conversion_ports import (
|
||||
)
|
||||
from abogen.application.conversion_request import ConversionRequest
|
||||
from abogen.application.conversion_result import ConversionResult
|
||||
from abogen.domain.enums import SubtitleMode
|
||||
from abogen.domain.normalization import TTSContext
|
||||
from abogen.domain.split_pattern import get_split_pattern
|
||||
|
||||
@@ -135,7 +136,7 @@ def _prepare_tts_context(
|
||||
# Compute split pattern
|
||||
split_pattern = get_split_pattern(
|
||||
str(request.language or "a"),
|
||||
str(request.subtitle_mode or "Disabled"),
|
||||
request.subtitle_mode or SubtitleMode.DISABLED,
|
||||
)
|
||||
|
||||
# Merge pronunciation overrides (manual + pronunciation)
|
||||
|
||||
@@ -19,6 +19,7 @@ from typing import Optional
|
||||
|
||||
from abogen.application.conversion_models import OutputLayout
|
||||
from abogen.application.conversion_request import ConversionRequest
|
||||
from abogen.domain.enums import OutputFormat, SaveMode, SubtitleFormat
|
||||
from abogen.domain.output_paths import (
|
||||
resolve_project_layout,
|
||||
resolve_unique_path,
|
||||
@@ -39,7 +40,7 @@ def resolve_output_layout(request: ConversionRequest) -> OutputLayout:
|
||||
OutputLayout with resolved paths
|
||||
"""
|
||||
# Determine base output directory
|
||||
if request.save_mode == "custom_folder" and request.output_folder:
|
||||
if request.save_mode == SaveMode.CUSTOM_FOLDER and request.output_folder:
|
||||
parent_dir = Path(request.output_folder)
|
||||
elif request.source_path:
|
||||
parent_dir = request.source_path.parent
|
||||
@@ -55,7 +56,7 @@ def resolve_output_layout(request: ConversionRequest) -> OutputLayout:
|
||||
base_name = "output"
|
||||
|
||||
# Find unique output path
|
||||
allowed_exts = {request.output_format, "srt", "ass", "vtt", "mp4", "m4b"}
|
||||
allowed_exts = {request.output_format, SubtitleFormat.SRT, SubtitleFormat.ASS, "vtt", "mp4", OutputFormat.M4B}
|
||||
unique_base = resolve_unique_path(
|
||||
parent_dir, base_name, "", allowed_extensions=allowed_exts
|
||||
)
|
||||
@@ -142,7 +143,7 @@ def should_merge_output(request: ConversionRequest) -> bool:
|
||||
Returns:
|
||||
True if merged output should be created
|
||||
"""
|
||||
if request.output_format.lower() == "m4b":
|
||||
if request.output_format == OutputFormat.M4B:
|
||||
return True
|
||||
if not request.save_chapters_separately:
|
||||
return True
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import Any, Callable, List, Optional, Protocol
|
||||
|
||||
from abogen.domain.audio_sink import AudioSink
|
||||
from abogen.domain.conversion_pipeline import tts_segments
|
||||
from abogen.domain.enums import SubtitleMode
|
||||
from abogen.domain.normalization import TTSContext
|
||||
from abogen.domain.progress import calc_etr_str
|
||||
from abogen.domain.subtitle_generation import process_subtitle_tokens
|
||||
@@ -136,7 +137,7 @@ def run_tts_segment_loop(
|
||||
params.audio_sink.write(seg.audio)
|
||||
|
||||
# Accumulate subtitle tokens (default path; skipped if on_segment handles it)
|
||||
if not on_segment and params.subtitle_mode != "Disabled" and seg.tokens:
|
||||
if not on_segment and params.subtitle_mode != SubtitleMode.DISABLED and seg.tokens:
|
||||
accumulated_tokens.extend(seg.tokens)
|
||||
|
||||
# Update timing
|
||||
|
||||
@@ -8,6 +8,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from abogen.domain.enums import SubtitleMode
|
||||
from typing import Any, Callable, Dict, Iterator, List, Optional
|
||||
|
||||
import numpy as np
|
||||
@@ -221,7 +223,7 @@ def emit_text_to_sinks(
|
||||
|
||||
# Flush subtitle tokens
|
||||
if subtitle_writer and accumulated_tokens:
|
||||
_use_spacy = subtitle_mode not in ("Disabled", "Line")
|
||||
_use_spacy = subtitle_mode not in (SubtitleMode.DISABLED, SubtitleMode.LINE)
|
||||
new_entries: List[tuple] = []
|
||||
process_subtitle_tokens(
|
||||
accumulated_tokens,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"""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_HIGHLIGHTING = "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"
|
||||
|
||||
@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]}")
|
||||
@@ -99,6 +99,9 @@ def apply_newline_policy(chapters: List[ExtractedChapter], replace_single_newlin
|
||||
chapter.text = newline_regex.sub(" ", chapter.text)
|
||||
|
||||
|
||||
from abogen.domain.enums import SaveMode
|
||||
|
||||
|
||||
def resolve_output_directory(
|
||||
*,
|
||||
save_mode: str,
|
||||
@@ -108,13 +111,13 @@ def resolve_output_directory(
|
||||
user_output_path: Optional[Path],
|
||||
user_cache_outputs: Optional[Path],
|
||||
) -> Path:
|
||||
if save_mode == "Save to Desktop" and desktop_dir:
|
||||
if save_mode in (SaveMode.SAVE_TO_DESKTOP, "Save to Desktop") and desktop_dir:
|
||||
return desktop_dir
|
||||
if save_mode == "Save next to input file":
|
||||
if save_mode in (SaveMode.SAVE_NEXT_TO_INPUT, "Save next to input file"):
|
||||
return stored_path.parent
|
||||
if save_mode == "Choose output folder" and output_folder:
|
||||
if save_mode in (SaveMode.CHOOSE_OUTPUT_FOLDER, "Choose output folder") and output_folder:
|
||||
return Path(output_folder)
|
||||
if save_mode == "Use default save location" and user_output_path:
|
||||
if save_mode in (SaveMode.DEFAULT_OUTPUT, "Use default save location") and user_output_path:
|
||||
return user_output_path
|
||||
return user_cache_outputs or Path(".")
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
"""Unified split pattern logic extracted from 3 copies."""
|
||||
import re
|
||||
|
||||
from abogen.domain.enums import SubtitleMode
|
||||
|
||||
PUNCTUATION_SENTENCE = r".!?。!?"
|
||||
PUNCTUATION_SENTENCE_COMMA = r".!?,。!?、,"
|
||||
@@ -27,14 +28,14 @@ def get_split_pattern(language: str, subtitle_mode: str) -> str:
|
||||
|
||||
# For CJK languages, when subtitle mode is Disabled or Line, prefer
|
||||
# punctuation-based splitting instead of plain newline splitting.
|
||||
if subtitle_mode in ("Disabled", "Line") and language in ("z", "j"):
|
||||
if subtitle_mode in (SubtitleMode.DISABLED, SubtitleMode.LINE) and language in ("z", "j"):
|
||||
return rf"(?<=[{PUNCTUATION_SENTENCE}]){spacing}|\n+"
|
||||
|
||||
if subtitle_mode == "Line":
|
||||
if subtitle_mode == SubtitleMode.LINE:
|
||||
return "\n"
|
||||
elif subtitle_mode == "Sentence":
|
||||
elif subtitle_mode == SubtitleMode.SENTENCE:
|
||||
return rf"(?<=[{PUNCTUATION_SENTENCE}]){spacing}|\n+"
|
||||
elif subtitle_mode == "Sentence + Comma":
|
||||
elif subtitle_mode == SubtitleMode.SENTENCE_COMMA:
|
||||
return rf"(?<=[{PUNCTUATION_SENTENCE_COMMA}]){spacing}|\n+"
|
||||
else:
|
||||
return r"\n+"
|
||||
|
||||
@@ -10,6 +10,8 @@ from __future__ import annotations
|
||||
import re
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from abogen.domain.enums import SubtitleMode
|
||||
|
||||
|
||||
# Punctuation constants for sentence splitting
|
||||
PUNCTUATION_SENTENCE = ".!?\u061f\u3002\uff01\uff1f" # .!? .?. ??
|
||||
@@ -50,17 +52,17 @@ def process_subtitle_tokens(
|
||||
# spaCy is disabled when subtitle mode is "Disabled" or "Line"
|
||||
use_spacy_for_english = (
|
||||
use_spacy_segmentation
|
||||
and subtitle_mode not in ["Disabled", "Line"]
|
||||
and subtitle_mode not in [SubtitleMode.DISABLED, SubtitleMode.LINE]
|
||||
and lang_code in ["a", "b"]
|
||||
and subtitle_mode in ["Sentence", "Sentence + Comma"]
|
||||
and subtitle_mode in [SubtitleMode.SENTENCE, SubtitleMode.SENTENCE_COMMA]
|
||||
)
|
||||
|
||||
if subtitle_mode == "Sentence + Highlighting":
|
||||
if subtitle_mode == SubtitleMode.SENTENCE_HIGHLIGHTING:
|
||||
_process_karaoke_highlighting(
|
||||
processed_tokens, subtitle_entries, max_subtitle_words, fallback_end_time
|
||||
)
|
||||
elif subtitle_mode in ["Sentence", "Sentence + Comma", "Line"]:
|
||||
if use_spacy_for_english and subtitle_mode != "Line":
|
||||
elif subtitle_mode in [SubtitleMode.SENTENCE, SubtitleMode.SENTENCE_COMMA, SubtitleMode.LINE]:
|
||||
if use_spacy_for_english and subtitle_mode != SubtitleMode.LINE:
|
||||
_process_spacy_sentences(
|
||||
processed_tokens, subtitle_entries, max_subtitle_words,
|
||||
subtitle_mode, lang_code, fallback_end_time
|
||||
@@ -176,7 +178,7 @@ def _process_spacy_sentences(
|
||||
sentence_boundaries = [sent.end_char for sent in doc.sents]
|
||||
|
||||
# For "Sentence + Comma" mode, also split on commas
|
||||
if subtitle_mode == "Sentence + Comma":
|
||||
if subtitle_mode == SubtitleMode.SENTENCE_COMMA:
|
||||
comma_positions = [
|
||||
i + 1 for i, c in enumerate(full_text) if c == ","
|
||||
]
|
||||
@@ -242,9 +244,9 @@ def _process_regex_sentences(
|
||||
) -> None:
|
||||
"""Process tokens using regex for sentence boundary detection."""
|
||||
# Define separator pattern based on mode
|
||||
if subtitle_mode == "Line":
|
||||
if subtitle_mode == SubtitleMode.LINE:
|
||||
separator = r"\n"
|
||||
elif subtitle_mode == "Sentence":
|
||||
elif subtitle_mode == SubtitleMode.SENTENCE:
|
||||
# Use punctuation without comma
|
||||
separator = rf"[{re.escape(PUNCTUATION_SENTENCE)}]"
|
||||
else: # Sentence + Comma
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Tests for domain enums — validation, properties, from_str methods."""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from abogen.domain.enums import (
|
||||
InputFormat,
|
||||
OutputFormat,
|
||||
SaveMode,
|
||||
SubtitleFormat,
|
||||
SubtitleMode,
|
||||
)
|
||||
|
||||
|
||||
class TestSubtitleMode:
|
||||
def test_from_str_case_insensitive(self):
|
||||
assert SubtitleMode.from_str("disabled") == SubtitleMode.DISABLED
|
||||
assert SubtitleMode.from_str("SENTENCE") == SubtitleMode.SENTENCE
|
||||
assert SubtitleMode.from_str("line") == SubtitleMode.LINE
|
||||
|
||||
def test_from_str_strips_whitespace(self):
|
||||
assert SubtitleMode.from_str(" Disabled ") == SubtitleMode.DISABLED
|
||||
|
||||
def test_from_str_invalid(self):
|
||||
with pytest.raises(ValueError, match="Invalid SubtitleMode"):
|
||||
SubtitleMode.from_str("invalid")
|
||||
|
||||
def test_comparison_with_str(self):
|
||||
assert SubtitleMode.DISABLED == "Disabled"
|
||||
assert SubtitleMode.SENTENCE != "Disabled"
|
||||
|
||||
|
||||
class TestOutputFormat:
|
||||
def test_dot_ext(self):
|
||||
assert OutputFormat.WAV.dot_ext == ".wav"
|
||||
assert OutputFormat.M4B.dot_ext == ".m4b"
|
||||
|
||||
def test_is_lossless(self):
|
||||
assert OutputFormat.WAV.is_lossless is True
|
||||
assert OutputFormat.FLAC.is_lossless is True
|
||||
assert OutputFormat.MP3.is_lossless is False
|
||||
assert OutputFormat.M4B.is_lossless is False
|
||||
|
||||
def test_from_str_strips_dot(self):
|
||||
assert OutputFormat.from_str(".wav") == OutputFormat.WAV
|
||||
assert OutputFormat.from_str(".MP3") == OutputFormat.MP3
|
||||
|
||||
def test_from_str_case_insensitive(self):
|
||||
assert OutputFormat.from_str("WAV") == OutputFormat.WAV
|
||||
assert OutputFormat.from_str("opus") == OutputFormat.OPUS
|
||||
|
||||
def test_from_str_invalid(self):
|
||||
with pytest.raises(ValueError, match="Invalid OutputFormat"):
|
||||
OutputFormat.from_str("avi")
|
||||
|
||||
|
||||
class TestSaveMode:
|
||||
def test_values(self):
|
||||
assert SaveMode.SAVE_NEXT_TO_INPUT == "save_next_to_input"
|
||||
assert SaveMode.CUSTOM_FOLDER == "custom_folder"
|
||||
|
||||
|
||||
class TestSubtitleFormat:
|
||||
def test_dot_ext(self):
|
||||
assert SubtitleFormat.SRT.dot_ext == ".srt"
|
||||
assert SubtitleFormat.ASS.dot_ext == ".ass"
|
||||
|
||||
def test_from_str_strips_dot(self):
|
||||
assert SubtitleFormat.from_str(".srt") == SubtitleFormat.SRT
|
||||
assert SubtitleFormat.from_str(".ASS") == SubtitleFormat.ASS
|
||||
|
||||
|
||||
class TestInputFormat:
|
||||
def test_is_book(self):
|
||||
assert InputFormat.EPUB.is_book is True
|
||||
assert InputFormat.PDF.is_book is True
|
||||
assert InputFormat.TXT.is_book is True
|
||||
assert InputFormat.MD.is_book is True
|
||||
assert InputFormat.SRT.is_book is False
|
||||
|
||||
def test_is_subtitle(self):
|
||||
assert InputFormat.SRT.is_subtitle is True
|
||||
assert InputFormat.ASS.is_subtitle is True
|
||||
assert InputFormat.VTT.is_subtitle is True
|
||||
assert InputFormat.EPUB.is_subtitle is False
|
||||
|
||||
def test_from_path(self):
|
||||
assert InputFormat.from_path(Path("book.epub")) == InputFormat.EPUB
|
||||
assert InputFormat.from_path(Path("sub.srt")) == InputFormat.SRT
|
||||
assert InputFormat.from_path(Path("notes.MD")) == InputFormat.MD
|
||||
assert InputFormat.from_path(Path("doc.markdown")) == InputFormat.MD
|
||||
|
||||
def test_from_path_invalid(self):
|
||||
with pytest.raises(ValueError, match="Unsupported input format"):
|
||||
InputFormat.from_path(Path("video.mp4"))
|
||||
|
||||
def test_dot_ext(self):
|
||||
assert InputFormat.EPUB.dot_ext == ".epub"
|
||||
assert InputFormat.SRT.dot_ext == ".srt"
|
||||
Reference in New Issue
Block a user