', first_paragraph_start)
+ first_paragraph_end = chapter_doc.find("", first_paragraph_start)
first_paragraph = chapter_doc[first_paragraph_start:first_paragraph_end]
assert "First sentence." in first_paragraph
- assert "Second sentence in same paragraph." in first_paragraph
\ No newline at end of file
+ assert "Second sentence in same paragraph." in first_paragraph
diff --git a/tests/test_epub_heuristic_nav.py b/tests/test_epub_heuristic_nav.py
index a69a374..eb5a418 100644
--- a/tests/test_epub_heuristic_nav.py
+++ b/tests/test_epub_heuristic_nav.py
@@ -5,10 +5,11 @@ import sys
from ebooklib import epub
# Ensure import path
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from abogen.book_parser import get_book_parser
+
class TestEpubHeuristicNav(unittest.TestCase):
"""
Tests for the heuristic fallback in _identify_nav_item (Step 4),
@@ -31,12 +32,12 @@ class TestEpubHeuristicNav(unittest.TestCase):
book = epub.EpubBook()
book.set_identifier("heuristic123")
book.set_title("Heuristic Test Book")
-
+
# 1. Add Content
c1 = epub.EpubHtml(title="Chapter 1", file_name="chap1.xhtml", lang="en")
c1.content = "
Chapter 1
Text
"
book.add_item(c1)
-
+
# 2. Add a Nav file BUT as a regular EpubHtml (ITEM_DOCUMENT)
# We do NOT use EpubNav. We do NOT look like a standard nav file if possible,
# but content must contain the magical signature.
@@ -52,66 +53,71 @@ class TestEpubHeuristicNav(unittest.TestCase):
"""
- # Filename intentionally generic/obscure to avoid filename-based heuristics
+ # Filename intentionally generic/obscure to avoid filename-based heuristics
# (though current code checks content, not just filename)
nav_file = epub.EpubHtml(title="Hidden Nav", file_name="content_toc.xhtml")
nav_file.content = nav_content
book.add_item(nav_file)
-
+
# 3. Setup Spine
book.spine = [nav_file, c1]
-
+
# 4. Write EPUB
epub.write_epub(self.epub_path, book)
-
+
# 5. Patch OPF to ensure ebooklib didn't sneakily add ITEM_NAVIGATION or toc="ncx"
import zipfile
+
patched = False
- with zipfile.ZipFile(self.epub_path, 'r') as zin:
- opf_content = zin.read('EPUB/content.opf').decode('utf-8')
-
+ with zipfile.ZipFile(self.epub_path, "r") as zin:
+ opf_content = zin.read("EPUB/content.opf").decode("utf-8")
+
# Remove toc="ncx" attribute if present (causes crash if no NCX)
if 'toc="ncx"' in opf_content:
- opf_content = opf_content.replace('toc="ncx"', '')
+ opf_content = opf_content.replace('toc="ncx"', "")
patched = True
-
+
# Ideally we'd verify properties="nav" isn't there, but EpubHtml shouldn't add it.
# If ebooklib added it, we might need to strip it to force heuristic.
if 'properties="nav"' in opf_content:
- opf_content = opf_content.replace('properties="nav"', '')
+ opf_content = opf_content.replace('properties="nav"', "")
patched = True
if patched:
TEMP_EPUB = self.epub_path + ".temp"
- with zipfile.ZipFile(TEMP_EPUB, 'w') as zout:
+ with zipfile.ZipFile(TEMP_EPUB, "w") as zout:
for item in zin.infolist():
- if item.filename == 'EPUB/content.opf':
+ if item.filename == "EPUB/content.opf":
zout.writestr(item, opf_content)
else:
zout.writestr(item, zin.read(item.filename))
-
+
if patched:
shutil.move(TEMP_EPUB, self.epub_path)
# 6. Verify our setup: Ensure NO ITEM_NAVIGATION exists
# We can inspect using ebooklib again
import ebooklib
+
check_book = epub.read_epub(self.epub_path)
nav_items = list(check_book.get_items_of_type(ebooklib.ITEM_NAVIGATION))
- self.assertEqual(len(nav_items), 0, "Setup failed: explicit navigation item found!")
-
+ self.assertEqual(
+ len(nav_items), 0, "Setup failed: explicit navigation item found!"
+ )
+
# 7. Run Parser
parser = get_book_parser(self.epub_path)
parser.process_content()
chapters = parser.get_chapters()
-
+
# 8. Assertions
# Should have found the nav via content scanning
chapter_titles = [c[1] for c in chapters]
self.assertIn("Chapter 1", chapter_titles)
-
+
# Also verify we hit the "html" type in identification
# We can't easily check private variables, but success implies it worked.
-
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_epub_html_nav_parsing.py b/tests/test_epub_html_nav_parsing.py
index 7c255d8..5fea141 100644
--- a/tests/test_epub_html_nav_parsing.py
+++ b/tests/test_epub_html_nav_parsing.py
@@ -5,10 +5,11 @@ import sys
from ebooklib import epub
# Ensure import path
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from abogen.book_parser import get_book_parser
+
class TestEpubHtmlNavParsing(unittest.TestCase):
"""
Tests for EPUB 3 HTML5 Navigation Document parsing logic (_parse_html_nav_li).
@@ -32,12 +33,12 @@ class TestEpubHtmlNavParsing(unittest.TestCase):
book = epub.EpubBook()
book.set_identifier("navtest123")
book.set_title("Nav Test Book")
-
+
# Add some content files
c1 = epub.EpubHtml(title="Chapter 1", file_name="chap1.xhtml", lang="en")
c1.content = "
Chapter 1
Text 1
"
book.add_item(c1)
-
+
c2 = epub.EpubHtml(title="Chapter 2", file_name="chap2.xhtml", lang="en")
c2.content = "
Chapter 2
Text 2
"
book.add_item(c2)
@@ -47,29 +48,29 @@ class TestEpubHtmlNavParsing(unittest.TestCase):
nav = epub.EpubHtml(title="Nav", file_name="nav.xhtml")
nav.content = nav_html_content
book.add_item(nav)
-
+
# We must set spine manually
book.spine = [nav, c1, c2]
-
+
epub.write_epub(self.epub_path, book)
-
- # Patch the OPF to remove toc="ncx" default which causes crash
+
+ # Patch the OPF to remove toc="ncx" default which causes crash
# because we intentionally excluded the legacy NCX file.
import zipfile
-
- with zipfile.ZipFile(self.epub_path, 'r') as zin:
- opf_content = zin.read('EPUB/content.opf').decode('utf-8')
- opf_content = opf_content.replace('toc="ncx"', '')
-
+
+ with zipfile.ZipFile(self.epub_path, "r") as zin:
+ opf_content = zin.read("EPUB/content.opf").decode("utf-8")
+ opf_content = opf_content.replace('toc="ncx"', "")
+
# Repack
TEMP_EPUB = self.epub_path + ".temp"
- with zipfile.ZipFile(TEMP_EPUB, 'w') as zout:
+ with zipfile.ZipFile(TEMP_EPUB, "w") as zout:
for item in zin.infolist():
- if item.filename == 'EPUB/content.opf':
+ if item.filename == "EPUB/content.opf":
zout.writestr(item, opf_content)
else:
zout.writestr(item, zin.read(item.filename))
-
+
shutil.move(TEMP_EPUB, self.epub_path)
def test_basic_html_nav_parsing(self):
@@ -86,14 +87,14 @@ class TestEpubHtmlNavParsing(unittest.TestCase):
"""
self._create_epub_with_custom_nav(nav_html)
-
+
parser = get_book_parser(self.epub_path)
parser.process_content()
chapters = parser.get_chapters()
-
+
# Filter out "Nav" or "Introduction" prefix content found from the Nav file itself
chapters = [c for c in chapters if "Chapter" in c[1] or "Section" in c[1]]
-
+
self.assertEqual(len(chapters), 2)
self.assertEqual(chapters[0][1], "Chapter 1")
self.assertEqual(chapters[1][1], "Chapter 2")
@@ -116,11 +117,11 @@ class TestEpubHtmlNavParsing(unittest.TestCase):
"""
# Note: In this test setup, chap2 is serving as "Section 1.1" effectively
self._create_epub_with_custom_nav(nav_html)
-
+
parser = get_book_parser(self.epub_path)
parser.process_content()
chapters = parser.get_chapters()
-
+
ids = [c[1] for c in chapters]
self.assertIn("Chapter 1", ids)
self.assertIn("Section 1.1", ids)
@@ -143,25 +144,26 @@ class TestEpubHtmlNavParsing(unittest.TestCase):
"""
self._create_epub_with_custom_nav(nav_html)
-
+
parser = get_book_parser(self.epub_path)
parser.process_content()
-
+
chapters = parser.get_chapters()
chapter_titles = [c[1] for c in chapters]
-
+
self.assertIn("Chapter 1", chapter_titles)
- self.assertNotIn("Part I", chapter_titles)
-
+ self.assertNotIn("Part I", chapter_titles)
+
# Check internal structure
# Find the node named "Part I" in the processed structure
- root_node = next(node for node in parser.processed_nav_structure if node['title'] == "Part I")
-
- self.assertEqual(root_node['title'], "Part I")
- self.assertFalse(root_node['has_content'])
- self.assertEqual(len(root_node['children']), 1)
- self.assertEqual(root_node['children'][0]['title'], "Chapter 1")
+ root_node = next(
+ node for node in parser.processed_nav_structure if node["title"] == "Part I"
+ )
+ self.assertEqual(root_node["title"], "Part I")
+ self.assertFalse(root_node["has_content"])
+ self.assertEqual(len(root_node["children"]), 1)
+ self.assertEqual(root_node["children"][0]["title"], "Chapter 1")
def test_identify_nav_item(self):
"""Test the _identify_nav_item method specifically."""
@@ -175,10 +177,11 @@ class TestEpubHtmlNavParsing(unittest.TestCase):
# But here we can call load directly if needed, or rely on normal flow up until navigation
parser.load()
nav_item, nav_type = parser._identify_nav_item()
-
+
self.assertEqual(nav_type, "html")
self.assertIsNotNone(nav_item)
self.assertTrue("nav.xhtml" in nav_item.get_name())
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_epub_missing_file_error_handling.py b/tests/test_epub_missing_file_error_handling.py
index 8005092..18c69ca 100644
--- a/tests/test_epub_missing_file_error_handling.py
+++ b/tests/test_epub_missing_file_error_handling.py
@@ -7,10 +7,11 @@ import logging
from ebooklib import epub
# Ensure import path
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from abogen.book_parser import get_book_parser
+
class TestEpubMissingFileErrorHandling(unittest.TestCase):
"""
Tests for robust error handling and recovery in the book parser.
@@ -22,8 +23,8 @@ class TestEpubMissingFileErrorHandling(unittest.TestCase):
shutil.rmtree(self.test_dir)
os.makedirs(self.test_dir)
self.broken_epub_path = os.path.join(self.test_dir, "missing_file.epub")
-
- # Suppress logging during tests to keep output clean,
+
+ # Suppress logging during tests to keep output clean,
# or capture it if we want to assert on warnings.
# For now, we just let it be or set level to ERROR.
logging.getLogger().setLevel(logging.ERROR)
@@ -39,27 +40,27 @@ class TestEpubMissingFileErrorHandling(unittest.TestCase):
book = epub.EpubBook()
book.set_identifier("broken123")
book.set_title("Broken Book")
-
+
# 1. Add a valid chapter
c1 = epub.EpubHtml(title="Chapter 1", file_name="chap1.xhtml", lang="en")
c1.content = "
Chapter 1
Survivable content.
"
book.add_item(c1)
-
+
# 2. Add a 'ghost' chapter that we will delete later
c2 = epub.EpubHtml(title="Ghost Chapter", file_name="ghost.xhtml", lang="en")
c2.content = "
Ghost
I will disappear.
"
book.add_item(c2)
-
+
book.spine = ["nav", c1, c2]
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
-
+
temp_path = os.path.join(self.test_dir, "temp.epub")
epub.write_epub(temp_path, book)
-
+
# 3. Physically remove 'ghost.xhtml' from the ZIP
- with zipfile.ZipFile(temp_path, 'r') as zin:
- with zipfile.ZipFile(self.broken_epub_path, 'w') as zout:
+ with zipfile.ZipFile(temp_path, "r") as zin:
+ with zipfile.ZipFile(self.broken_epub_path, "w") as zout:
for item in zin.infolist():
# Copy everything EXCEPT the ghost file
# Note: ebooklib might put files in OEPS/ or EPUB/ folders depending on version,
@@ -73,14 +74,14 @@ class TestEpubMissingFileErrorHandling(unittest.TestCase):
Should log a warning instead of raising KeyError.
"""
self._create_broken_epub()
-
+
try:
parser = get_book_parser(self.broken_epub_path)
parser.process_content()
-
+
# 1. Ensure process didn't crash
self.assertTrue(True, "Parser should not crash on missing file")
-
+
# 2. Ensure valid content was extracted
# Identify the ID for chap1.xhtml (usually file path based)
# Since IDs can vary, we check if ANY content contains our known string
@@ -90,11 +91,12 @@ class TestEpubMissingFileErrorHandling(unittest.TestCase):
chap1_found = True
break
self.assertTrue(chap1_found, "The valid chapter should still be processed")
-
+
except KeyError:
self.fail("Parser raised KeyError instead of handling the missing file!")
except Exception as e:
self.fail(f"Parser raised unexpected exception: {e}")
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_epub_ncx_parsing.py b/tests/test_epub_ncx_parsing.py
index bf24710..313d785 100644
--- a/tests/test_epub_ncx_parsing.py
+++ b/tests/test_epub_ncx_parsing.py
@@ -5,13 +5,14 @@ import sys
from ebooklib import epub
# Ensure we can import the module
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from abogen.book_parser import get_book_parser, EpubParser
+
class TestEpubNcxParsing(unittest.TestCase):
"""
- Focused tests for NCX navigation scenarios, ensuring legacy/compatibility
+ Focused tests for NCX navigation scenarios, ensuring legacy/compatibility
modes work when HTML5 Navigation is missing.
"""
@@ -50,9 +51,9 @@ class TestEpubNcxParsing(unittest.TestCase):
# Add default NCX and generic spine
book.add_item(epub.EpubNcx())
# IMPORTANT: Do NOT add EpubNav() here, that's what we are testing!
-
+
book.spine = ["nav"] + epub_chapters
-
+
epub.write_epub(self.ncx_only_epub_path, book)
def test_ncx_only_parsing(self):
@@ -63,28 +64,28 @@ class TestEpubNcxParsing(unittest.TestCase):
# 1. Setup Data
chapters_data = [
("Chapter 1", "This is the first chapter."),
- ("Chapter 2", "This is the second chapter.")
+ ("Chapter 2", "This is the second chapter."),
]
self._create_ncx_only_epub(chapters_data)
# 2. Run Parser
parser = get_book_parser(self.ncx_only_epub_path)
parser.process_content()
-
+
# 3. Verify Breakdown
# We expect detailed breakdown based on NCX
chapters = parser.get_chapters()
-
+
# Should find exactly 2 chapters based on the Toc
self.assertEqual(len(chapters), 2, "Should have 2 chapters extracted from NCX")
-
+
# Check Titles and Sequence
self.assertEqual(chapters[0][1], "Chapter 1")
self.assertEqual(chapters[1][1], "Chapter 2")
-
+
# Verify content was extracted
# Note: 'src' in chapters usually points to file_name if no fragments
- id_1 = chapters[0][0]
+ id_1 = chapters[0][0]
self.assertIn("This is the first chapter", parser.content_texts[id_1])
def test_nested_ncx_parsing(self):
@@ -94,7 +95,7 @@ class TestEpubNcxParsing(unittest.TestCase):
book = epub.EpubBook()
book.set_identifier("nested_ncx")
book.set_title("Nested NCX")
-
+
# Create one big file with sections
c1 = epub.EpubHtml(title="Main Chapter", file_name="main.xhtml", lang="en")
c1.content = """
@@ -108,33 +109,32 @@ class TestEpubNcxParsing(unittest.TestCase):
# Manually construct nested TOC because ebooklib's default helpers are simple
# EbookLib automatically builds NCX from book.toc
# Nested tuple structure: (Section, (Subsection, Sub-subsection))
-
+
# We need to link to Fragments for this to really test nested NCX pointing to same file
# EbookLib Link object: epub.Link(href, title, uid)
-
+
link_root = epub.Link("main.xhtml#intro", "Introduction", "intro")
link_sect = epub.Link("main.xhtml#sect1", "Section 1", "sect1")
-
+
# Structure: Intro -> Section 1 (as child)
- book.toc = (
- (link_root, (link_sect, )),
- )
-
+ book.toc = ((link_root, (link_sect,)),)
+
book.add_item(epub.EpubNcx())
book.spine = ["nav", c1]
-
+
epub.write_epub(self.ncx_only_epub_path, book)
-
+
# Parse
parser = get_book_parser(self.ncx_only_epub_path)
parser.process_content()
-
+
chapters = parser.get_chapters()
-
+
# Depending on how the parser flattens, we should see both entries
titles = [node[1] for node in chapters]
self.assertIn("Introduction", titles)
self.assertIn("Section 1", titles)
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_epub_standard_nav.py b/tests/test_epub_standard_nav.py
index d916f1e..b4aaf39 100644
--- a/tests/test_epub_standard_nav.py
+++ b/tests/test_epub_standard_nav.py
@@ -7,10 +7,11 @@ import ebooklib
from unittest.mock import MagicMock
# Ensure import path
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from abogen.book_parser import get_book_parser
+
class TestEpubStandardNav(unittest.TestCase):
"""
Tests for the standard ITEM_NAVIGATION discovery in _identify_nav_item.
@@ -33,38 +34,39 @@ class TestEpubStandardNav(unittest.TestCase):
book = epub.EpubBook()
book.set_identifier("stdnav123")
book.set_title("Standard Nav Test")
-
+
c1 = epub.EpubHtml(title="Chapter 1", file_name="chap1.xhtml", lang="en")
c1.content = "
Chapter 1
Text 1
"
book.add_item(c1)
-
+
# Use Standard EpubNav
nav = epub.EpubNav()
book.add_item(nav)
book.spine = [nav, c1]
-
+
epub.write_epub(self.epub_path, book)
-
+
# "Zip Surgery" Patch:
# ebooklib unconditionally adds `toc="ncx"` to the spine, even for EPUB 3 files that purely use HTML Nav.
# This creates a dangling reference to a non-existent "ncx" item, causing ebooklib to crash on read.
# We manually remove this attribute to ensure the test EPUB is valid and readable.
# TODO - find real world examples of EPUB 3 files that use HTML Nav
import zipfile
+
patched = False
- with zipfile.ZipFile(self.epub_path, 'r') as zin:
- opf_content = zin.read('EPUB/content.opf').decode('utf-8')
+ with zipfile.ZipFile(self.epub_path, "r") as zin:
+ opf_content = zin.read("EPUB/content.opf").decode("utf-8")
if 'toc="ncx"' in opf_content:
- opf_content = opf_content.replace('toc="ncx"', '')
+ opf_content = opf_content.replace('toc="ncx"', "")
patched = True
TEMP_EPUB = self.epub_path + ".temp"
- with zipfile.ZipFile(TEMP_EPUB, 'w') as zout:
+ with zipfile.ZipFile(TEMP_EPUB, "w") as zout:
for item in zin.infolist():
- if item.filename == 'EPUB/content.opf':
+ if item.filename == "EPUB/content.opf":
zout.writestr(item, opf_content)
else:
zout.writestr(item, zin.read(item.filename))
-
+
if patched:
shutil.move(TEMP_EPUB, self.epub_path)
@@ -78,18 +80,18 @@ class TestEpubStandardNav(unittest.TestCase):
This exercises the first branch of _identify_nav_item.
"""
parser = self._create_and_load_epub()
-
+
# Inject an item that mocks the ITEM_NAVIGATION type behavior
# (This simulates a library/parser that correctly types the item as 4)
mock_nav = MagicMock()
mock_nav.get_name.return_value = "nav.xhtml"
mock_nav.get_type.return_value = ebooklib.ITEM_NAVIGATION
-
+
# We append this mock to the book items to ensure get_items_of_type(ITEM_NAVIGATION) finds it
parser.book.items.append(mock_nav)
-
+
nav_item, nav_type = parser._identify_nav_item()
-
+
self.assertEqual(nav_type, "html")
self.assertEqual(nav_item.get_name(), "nav.xhtml")
# Verify we are getting the object we expect (implied by success)
@@ -100,31 +102,32 @@ class TestEpubStandardNav(unittest.TestCase):
This is the standard EPUB 3 behavior and exercises the fallback branch.
"""
parser = self._create_and_load_epub()
-
+
# Locate the generic 'nav' item loaded by ebooklib
original_nav = parser.book.get_item_with_id("nav")
self.assertIsNotNone(original_nav)
-
+
# "Fix" the object to match what we expect from a correct EPUB 3 read:
# It should have properties=['nav'].
# We use a real EpubNav object to ensure structural correctness.
proper_nav = epub.EpubNav(uid=original_nav.id, file_name=original_nav.file_name)
proper_nav.content = original_nav.content
- proper_nav.properties = ['nav']
-
+ proper_nav.properties = ["nav"]
+
# Swap it into the book items list
try:
idx = parser.book.items.index(original_nav)
parser.book.items[idx] = proper_nav
except ValueError:
self.fail("Could not find original nav item to swap")
-
+
nav_item, nav_type = parser._identify_nav_item()
-
+
self.assertEqual(nav_type, "html")
self.assertEqual(nav_item.get_name(), "nav.xhtml")
# Check that we actually found the one with properties
- self.assertEqual(getattr(nav_item, 'properties', []), ['nav'])
+ self.assertEqual(getattr(nav_item, "properties", []), ["nav"])
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_output_paths.py b/tests/test_output_paths.py
index a1d14a8..77e8992 100644
--- a/tests/test_output_paths.py
+++ b/tests/test_output_paths.py
@@ -30,16 +30,22 @@ def _sample_job(tmp_path: Path) -> Job:
)
-def test_prepare_project_layout_uses_timestamped_folder(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+def test_prepare_project_layout_uses_timestamped_folder(
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
+) -> None:
job = _sample_job(tmp_path)
monkeypatch.setattr(
"abogen.webui.conversion_runner._output_timestamp_token",
lambda: "20250101-120000",
)
- project_root, audio_dir, subtitle_dir, metadata_dir = _prepare_project_layout(job, tmp_path)
+ project_root, audio_dir, subtitle_dir, metadata_dir = _prepare_project_layout(
+ job, tmp_path
+ )
- assert project_root.name.startswith("20250101-120000_Sample_Title"), project_root.name
+ assert project_root.name.startswith(
+ "20250101-120000_Sample_Title"
+ ), project_root.name
assert audio_dir == project_root
assert subtitle_dir == project_root
assert metadata_dir is None
@@ -48,7 +54,9 @@ def test_prepare_project_layout_uses_timestamped_folder(monkeypatch: pytest.Monk
assert output_path == project_root / "Sample_Title.mp3"
-def test_prepare_project_layout_creates_project_subdirs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+def test_prepare_project_layout_creates_project_subdirs(
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
+) -> None:
job = _sample_job(tmp_path)
job.save_as_project = True
monkeypatch.setattr(
@@ -56,7 +64,9 @@ def test_prepare_project_layout_creates_project_subdirs(monkeypatch: pytest.Monk
lambda: "20250101-120500",
)
- project_root, audio_dir, subtitle_dir, metadata_dir = _prepare_project_layout(job, tmp_path)
+ project_root, audio_dir, subtitle_dir, metadata_dir = _prepare_project_layout(
+ job, tmp_path
+ )
assert audio_dir == project_root / "audio"
assert subtitle_dir == project_root / "subtitles"
diff --git a/tests/test_prepare_form.py b/tests/test_prepare_form.py
index 4ea05eb..9456d0f 100644
--- a/tests/test_prepare_form.py
+++ b/tests/test_prepare_form.py
@@ -102,7 +102,9 @@ def test_resolve_voice_setting_handles_profile_reference():
}
}
- voice, profile_name, language = resolve_voice_setting("profile:Blend", profiles=profiles)
+ voice, profile_name, language = resolve_voice_setting(
+ "profile:Blend", profiles=profiles
+ )
assert voice == "af_nova*0.5+am_liam*0.5"
assert profile_name == "Blend"
@@ -112,9 +114,11 @@ def test_resolve_voice_setting_handles_profile_reference():
def test_apply_prepare_form_updates_closing_outro_flag():
pending = _make_pending_job()
pending.read_closing_outro = True
- form = MultiDict({
- "read_closing_outro": "false",
- })
+ form = MultiDict(
+ {
+ "read_closing_outro": "false",
+ }
+ )
apply_prepare_form(pending, form)
diff --git a/tests/test_preview_applies_manual_overrides.py b/tests/test_preview_applies_manual_overrides.py
index 76998c4..0f08512 100644
--- a/tests/test_preview_applies_manual_overrides.py
+++ b/tests/test_preview_applies_manual_overrides.py
@@ -13,7 +13,9 @@ def test_preview_applies_manual_override_before_normalization(monkeypatch):
def normalize_for_pipeline(text):
return text
- monkeypatch.setitem(__import__("sys").modules, "abogen.kokoro_text_normalization", _Norm)
+ monkeypatch.setitem(
+ __import__("sys").modules, "abogen.kokoro_text_normalization", _Norm
+ )
# And stub the kokoro pipeline path so generate_preview_audio won't proceed.
# We'll instead validate by calling the override logic through generate_preview_audio
@@ -28,7 +30,11 @@ def test_preview_applies_manual_override_before_normalization(monkeypatch):
captured["text"] = text
return iter(())
- monkeypatch.setitem(__import__("sys").modules, "abogen.tts_supertonic", type("M", (), {"SupertonicPipeline": DummyPipeline}))
+ monkeypatch.setitem(
+ __import__("sys").modules,
+ "abogen.tts_supertonic",
+ type("M", (), {"SupertonicPipeline": DummyPipeline}),
+ )
try:
preview.generate_preview_audio(
diff --git a/tests/test_regression_fixes.py b/tests/test_regression_fixes.py
index 7bf0396..babc2f8 100644
--- a/tests/test_regression_fixes.py
+++ b/tests/test_regression_fixes.py
@@ -1,16 +1,21 @@
import pytest
from unittest.mock import patch
-from abogen.kokoro_text_normalization import normalize_for_pipeline, DEFAULT_APOSTROPHE_CONFIG
+from abogen.kokoro_text_normalization import (
+ normalize_for_pipeline,
+ DEFAULT_APOSTROPHE_CONFIG,
+)
from abogen.normalization_settings import build_apostrophe_config, _SETTINGS_DEFAULTS
+
def normalize(text, overrides=None):
settings = dict(_SETTINGS_DEFAULTS)
if overrides:
settings.update(overrides)
-
+
config = build_apostrophe_config(settings=settings, base=DEFAULT_APOSTROPHE_CONFIG)
return normalize_for_pipeline(text, config=config, settings=settings)
+
def test_year_pronunciation():
# 1925 -> Nineteen Hundred Twenty Five
normalized = normalize("1925")
@@ -24,6 +29,7 @@ def test_year_pronunciation():
assert "twenty twenty" in normalized.lower()
assert "five" in normalized.lower()
+
def test_currency_pronunciation():
# $1.00 -> One dollar (no zero cents)
normalized = normalize("$1.00")
@@ -37,6 +43,7 @@ def test_currency_pronunciation():
assert "one dollar" in normalized.lower()
assert "five cents" in normalized.lower()
+
def test_url_pronunciation():
# https://www.amazon.com -> amazon dot com
normalized = normalize("https://www.amazon.com")
@@ -50,6 +57,7 @@ def test_url_pronunciation():
print(f"www.google.com -> {normalized}")
assert "google dot com" in normalized.lower()
+
def test_roman_numerals_world_war():
# World War I -> World War One
normalized = normalize("World War I")
@@ -61,6 +69,7 @@ def test_roman_numerals_world_war():
print(f"World War II -> {normalized}")
assert "world war two" in normalized.lower()
+
def test_footnote_removal():
# Bob is awesome1. -> Bob is awesome.
normalized = normalize("Bob is awesome1.")
@@ -74,8 +83,10 @@ def test_footnote_removal():
assert "citation needed." in normalized.lower()
assert "[1]" not in normalized
+
def test_manual_override_normalization():
from abogen.entity_analysis import normalize_manual_override_token
+
assert normalize_manual_override_token("The") == "the"
assert normalize_manual_override_token(" A ") == "a"
assert normalize_manual_override_token("word") == "word"
diff --git a/tests/test_service.py b/tests/test_service.py
index 07c61f1..afa56c5 100644
--- a/tests/test_service.py
+++ b/tests/test_service.py
@@ -2,7 +2,13 @@ from __future__ import annotations
import io
import time
-from abogen.webui.service import Job, JobStatus, build_service, _JOB_LOGGER, build_audiobookshelf_metadata
+from abogen.webui.service import (
+ Job,
+ JobStatus,
+ build_service,
+ _JOB_LOGGER,
+ build_audiobookshelf_metadata,
+)
def test_service_processes_job(tmp_path):
@@ -47,7 +53,11 @@ def test_service_processes_job(tmp_path):
)
deadline = time.time() + 5
- while time.time() < deadline and job.status not in {JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED}:
+ while time.time() < deadline and job.status not in {
+ JobStatus.COMPLETED,
+ JobStatus.FAILED,
+ JobStatus.CANCELLED,
+ }:
time.sleep(0.05)
service.shutdown()
@@ -287,4 +297,4 @@ def test_audiobookshelf_metadata_allows_decimal_sequence(tmp_path):
metadata = build_audiobookshelf_metadata(job)
- assert metadata["seriesSequence"] == "4.5"
\ No newline at end of file
+ assert metadata["seriesSequence"] == "4.5"
diff --git a/tests/test_speaker_analysis.py b/tests/test_speaker_analysis.py
index 88885a5..2b46085 100644
--- a/tests/test_speaker_analysis.py
+++ b/tests/test_speaker_analysis.py
@@ -24,9 +24,12 @@ def _chunk(text: str, idx: int) -> dict:
def test_analyze_speakers_infers_gender_from_pronouns():
chunks = [
- _chunk("\"Greetings,\" said John. He adjusted his hat as he smiled.", 0),
- _chunk("\"Hello,\" said Mary. She straightened her dress as she introduced herself.", 1),
- _chunk("\"Nice to meet you,\" said Alex.", 2),
+ _chunk('"Greetings," said John. He adjusted his hat as he smiled.', 0),
+ _chunk(
+ '"Hello," said Mary. She straightened her dress as she introduced herself.',
+ 1,
+ ),
+ _chunk('"Nice to meet you," said Alex.', 2),
]
analysis = analyze_speakers(_chapters(), chunks, threshold=1, max_speakers=0)
@@ -63,8 +66,8 @@ def test_analyze_speakers_ignores_leading_stopwords():
def test_analyze_speakers_applies_threshold_suppression():
chunks = [
- _chunk("\"Hello there,\" said Narrator.", 0),
- _chunk("\"It is lying,\" said Green.", 1),
+ _chunk('"Hello there," said Narrator.', 0),
+ _chunk('"It is lying," said Green.', 1),
]
analysis = analyze_speakers(_chapters(), chunks, threshold=3, max_speakers=0)
@@ -78,7 +81,7 @@ def test_analyze_speakers_applies_threshold_suppression():
def test_sample_excerpt_includes_context_paragraphs():
chunks = [
_chunk("The hallway was quiet as footsteps approached.", 0),
- _chunk('\"Open the door,\" said John as he reached for the handle.', 1),
+ _chunk('"Open the door," said John as he reached for the handle.', 1),
_chunk("Mary watched him closely, unsure of his intent.", 2),
]
@@ -89,5 +92,5 @@ def test_sample_excerpt_includes_context_paragraphs():
assert john.sample_quotes, "Expected John to have at least one sample quote"
excerpt = john.sample_quotes[0]["excerpt"]
assert "The hallway was quiet" in excerpt
- assert "\"Open the door,\" said John" in excerpt
+ assert '"Open the door," said John' in excerpt
assert "Mary watched him closely" in excerpt
diff --git a/tests/test_text_extractor.py b/tests/test_text_extractor.py
index f63b67f..cf7db58 100644
--- a/tests/test_text_extractor.py
+++ b/tests/test_text_extractor.py
@@ -6,7 +6,9 @@ from abogen.text_extractor import extract_from_path
from abogen.utils import calculate_text_length
-ASSET = Path("test_assets/alexandre-dumas_the-count-of-monte-cristo_chapman-and-hall.epub")
+ASSET = Path(
+ "test_assets/alexandre-dumas_the-count-of-monte-cristo_chapman-and-hall.epub"
+)
def test_epub_character_counts_align_with_calculated_total():
@@ -37,8 +39,12 @@ def test_epub_series_metadata_extracted_from_opf_meta(tmp_path):
book.add_author("Example Author")
# Calibre-style series metadata
- book.add_metadata("OPF", "meta", "", {"name": "calibre:series", "content": "Example Saga"})
- book.add_metadata("OPF", "meta", "", {"name": "calibre:series_index", "content": "2"})
+ book.add_metadata(
+ "OPF", "meta", "", {"name": "calibre:series", "content": "Example Saga"}
+ )
+ book.add_metadata(
+ "OPF", "meta", "", {"name": "calibre:series_index", "content": "2"}
+ )
chapter = epub.EpubHtml(title="Chapter 1", file_name="chap_01.xhtml", lang="en")
chapter.content = "
Chapter 1
Hello
"
diff --git a/tests/test_text_normalization.py b/tests/test_text_normalization.py
index 156fccf..4680017 100644
--- a/tests/test_text_normalization.py
+++ b/tests/test_text_normalization.py
@@ -16,14 +16,22 @@ from abogen.normalization_settings import (
from abogen.spacy_contraction_resolver import resolve_ambiguous_contractions
-SPACY_RESOLVER_AVAILABLE = bool(resolve_ambiguous_contractions("It's been a long time."))
+SPACY_RESOLVER_AVAILABLE = bool(
+ resolve_ambiguous_contractions("It's been a long time.")
+)
-def _normalize_text(text: str, *, normalization_overrides: dict[str, object] | None = None) -> str:
+def _normalize_text(
+ text: str, *, normalization_overrides: dict[str, object] | None = None
+) -> str:
runtime_settings = get_runtime_settings()
if normalization_overrides:
- runtime_settings = apply_normalization_overrides(runtime_settings, normalization_overrides)
- config = build_apostrophe_config(settings=runtime_settings, base=DEFAULT_APOSTROPHE_CONFIG)
+ runtime_settings = apply_normalization_overrides(
+ runtime_settings, normalization_overrides
+ )
+ config = build_apostrophe_config(
+ settings=runtime_settings, base=DEFAULT_APOSTROPHE_CONFIG
+ )
return normalize_for_pipeline(text, config=config, settings=runtime_settings)
@@ -202,7 +210,9 @@ def test_contractions_can_be_kept_when_override_disabled() -> None:
def test_sibilant_possessives_remain_when_marking_disabled() -> None:
normalized = _normalize_text(
"The boss's chair wobbled.",
- normalization_overrides={"normalization_apostrophes_sibilant_possessives": False},
+ normalization_overrides={
+ "normalization_apostrophes_sibilant_possessives": False
+ },
)
assert "boss's" in normalized
assert "boss iz" not in normalized.lower()
@@ -276,10 +286,13 @@ def test_spacy_disambiguates_she_would() -> None:
@pytest.mark.skipif(not SPACY_RESOLVER_AVAILABLE, reason="spaCy model unavailable")
def test_sample_sentence_handles_complex_contractions() -> None:
- sample = "I've heard the captain'll arrive by dusk, but they'd said the same yesterday."
+ sample = (
+ "I've heard the captain'll arrive by dusk, but they'd said the same yesterday."
+ )
normalized = _normalize_text(sample)
assert (
- "I have heard the captain will arrive by dusk, but they had said the same yesterday." == normalized
+ "I have heard the captain will arrive by dusk, but they had said the same yesterday."
+ == normalized
)
@@ -316,9 +329,12 @@ def mock_settings():
"normalization_footnotes": True,
"normalization_numbers_year_style": "american",
}
- with patch("tests.test_text_normalization.get_runtime_settings", return_value=defaults):
+ with patch(
+ "tests.test_text_normalization.get_runtime_settings", return_value=defaults
+ ):
yield
+
def test_currency_magnitude():
cases = [
("$2 million", "two million dollars"),
@@ -331,13 +347,15 @@ def test_currency_magnitude():
("$2.50", "two dollars, fifty cents"),
("$100", "one hundred dollars"),
]
-
+
settings = {
"normalization_numbers": True,
"normalization_currency": True,
- "normalization_apostrophe_mode": "spacy"
+ "normalization_apostrophe_mode": "spacy",
}
-
+
for input_text, expected in cases:
normalized = _normalize_text(input_text, normalization_overrides=settings)
- assert expected.lower() in normalized.lower(), f"Failed for {input_text}: got '{normalized}'"
+ assert (
+ expected.lower() in normalized.lower()
+ ), f"Failed for {input_text}: got '{normalized}'"
diff --git a/tests/test_voice_cache.py b/tests/test_voice_cache.py
index b0aa2ca..c428b3a 100644
--- a/tests/test_voice_cache.py
+++ b/tests/test_voice_cache.py
@@ -4,7 +4,11 @@ from typing import cast
import pytest
from abogen.constants import VOICES_INTERNAL
-from abogen.voice_cache import LocalEntryNotFoundError, _CACHED_VOICES, ensure_voice_assets
+from abogen.voice_cache import (
+ LocalEntryNotFoundError,
+ _CACHED_VOICES,
+ ensure_voice_assets,
+)
from abogen.webui.conversion_runner import _collect_required_voice_ids
from abogen.webui.service import Job