aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/test_audio.py57
-rw-r--r--tests/test_chunking.py39
-rw-r--r--tests/test_cleaning.py54
-rw-r--r--tests/test_extractors.py94
5 files changed, 244 insertions, 0 deletions
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/test_audio.py b/tests/test_audio.py
new file mode 100644
index 0000000..a629224
--- /dev/null
+++ b/tests/test_audio.py
@@ -0,0 +1,57 @@
+"""Tests for audio helpers: speed parameters and chunk cleanup."""
+
+import tempfile
+import unittest
+from pathlib import Path
+
+from converter import config
+from converter.audio import cleanup_chunks, speed_export_params
+
+
+class SpeedExportParamsTests(unittest.TestCase):
+ def test_normal_speed_no_filter(self):
+ self.assertEqual(speed_export_params(1.0), [])
+
+ def test_simple_speedup(self):
+ self.assertEqual(speed_export_params(1.5), ["-filter:a", "atempo=1.5"])
+
+ def test_simple_slowdown(self):
+ self.assertEqual(speed_export_params(0.75), ["-filter:a", "atempo=0.75"])
+
+ def test_chained_speedup_beyond_2x(self):
+ self.assertEqual(speed_export_params(3.0), ["-filter:a", "atempo=2.0,atempo=1.5"])
+
+ def test_chained_slowdown_below_half(self):
+ self.assertEqual(speed_export_params(0.25), ["-filter:a", "atempo=0.5,atempo=0.5"])
+
+ def test_zero_speed_rejected(self):
+ with self.assertRaises(ValueError):
+ speed_export_params(0)
+
+ def test_negative_speed_rejected(self):
+ with self.assertRaises(ValueError):
+ speed_export_params(-1.5)
+
+
+class CleanupChunksTests(unittest.TestCase):
+ def test_removes_only_chunk_files(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ chunks_dir = Path(tmp)
+ (chunks_dir / "chunk_0001.wav").write_bytes(b"stale")
+ (chunks_dir / "chunk_0002.wav").write_bytes(b"stale")
+ (chunks_dir / "keep.txt").write_bytes(b"keep")
+
+ original = config.CHUNKS_FOLDER
+ config.CHUNKS_FOLDER = chunks_dir
+ try:
+ cleanup_chunks()
+ finally:
+ config.CHUNKS_FOLDER = original
+
+ self.assertFalse((chunks_dir / "chunk_0001.wav").exists())
+ self.assertFalse((chunks_dir / "chunk_0002.wav").exists())
+ self.assertTrue((chunks_dir / "keep.txt").exists())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_chunking.py b/tests/test_chunking.py
new file mode 100644
index 0000000..da45f65
--- /dev/null
+++ b/tests/test_chunking.py
@@ -0,0 +1,39 @@
+"""Tests for text chunking."""
+
+import unittest
+
+from converter.chunking import split_into_chunks
+
+
+class SplitIntoChunksTests(unittest.TestCase):
+ def test_empty_input(self):
+ self.assertEqual(split_into_chunks(""), [])
+ self.assertEqual(split_into_chunks(" \n "), [])
+
+ def test_short_text_single_chunk(self):
+ self.assertEqual(split_into_chunks("One short sentence."), ["One short sentence."])
+
+ def test_respects_word_limit_across_sentences(self):
+ # 10 sentences of 9 words each = 90 words total
+ sentences = [f"S{i} " + " ".join(["word"] * 8) + "." for i in range(10)]
+ chunks = split_into_chunks(" ".join(sentences), max_words=25)
+ self.assertGreater(len(chunks), 1)
+ self.assertTrue(all(len(c.split()) <= 25 for c in chunks))
+ self.assertEqual(sum(len(c.split()) for c in chunks), 90)
+
+ def test_long_sentence_split_keeps_punctuation(self):
+ # 10 clauses of 5 words each, joined by commas
+ sentence = ",".join([" ".join(["w"] * 5) for _ in range(10)]) + "."
+ chunks = split_into_chunks(sentence, max_words=12)
+ self.assertGreater(len(chunks), 1)
+ self.assertTrue(all(len(c.split()) <= 12 for c in chunks))
+ self.assertIn(",", chunks[0]) # commas retained for TTS prosody
+
+ def test_single_oversized_sentence_stays_intact(self):
+ sentence = " ".join(["word"] * 30) + "."
+ chunks = split_into_chunks(sentence, max_words=10)
+ self.assertEqual(chunks, [sentence])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_cleaning.py b/tests/test_cleaning.py
new file mode 100644
index 0000000..41f4ed7
--- /dev/null
+++ b/tests/test_cleaning.py
@@ -0,0 +1,54 @@
+"""Tests for text and HTML cleaning."""
+
+import unittest
+
+from converter.extractors import clean_html, clean_text
+
+
+class CleanTextTests(unittest.TestCase):
+ def test_empty_input(self):
+ self.assertEqual(clean_text(""), "")
+ self.assertEqual(clean_text(None), "")
+
+ def test_collapses_whitespace(self):
+ self.assertEqual(clean_text("a\n\n b \t c"), "a b c")
+
+ def test_preserves_inline_numbers(self):
+ self.assertEqual(clean_text("He was 42 years old."), "He was 42 years old.")
+
+ def test_preserves_grouped_and_decimal_numbers(self):
+ self.assertEqual(
+ clean_text("Over 1,000 pages and 3.5 stars."),
+ "Over 1,000 pages and 3.5 stars.",
+ )
+
+ def test_removes_standalone_page_numbers(self):
+ self.assertEqual(
+ clean_text("End of page.\n7\nNext page text."),
+ "End of page. Next page text.",
+ )
+
+ def test_page_number_removal_leaves_single_spacing(self):
+ result = clean_text("Chapter one\n\n12\n\nChapter two")
+ self.assertEqual(result, "Chapter one Chapter two")
+ self.assertNotIn(" ", result)
+
+
+class CleanHtmlTests(unittest.TestCase):
+ def test_strips_tags(self):
+ self.assertEqual(clean_html("<p>Hello <b>world</b></p>"), "Hello world")
+
+ def test_removes_script_and_style(self):
+ html = "<style>.x{color:red}</style><p>Text</p><script>var a=1;</script>"
+ self.assertEqual(clean_html(html), "Text")
+
+ def test_unescapes_entities(self):
+ self.assertEqual(clean_html("Tom &amp; Jerry"), "Tom & Jerry")
+
+ def test_empty(self):
+ self.assertEqual(clean_html(""), "")
+ self.assertEqual(clean_html(None), "")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_extractors.py b/tests/test_extractors.py
new file mode 100644
index 0000000..c497267
--- /dev/null
+++ b/tests/test_extractors.py
@@ -0,0 +1,94 @@
+"""Tests for file text extraction."""
+
+import tempfile
+import unittest
+from pathlib import Path
+
+from converter.extractors import extract_text
+
+
+class TxtExtractionTests(unittest.TestCase):
+ def _extract(self, data: bytes) -> str:
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / "book.txt"
+ path.write_bytes(data)
+ return extract_text(path)
+
+ def test_utf8(self):
+ self.assertEqual(self._extract("héllo wörld".encode("utf-8")), "héllo wörld")
+
+ def test_utf16_with_bom(self):
+ self.assertEqual(self._extract("héllo".encode("utf-16")), "héllo")
+
+ def test_cp1252(self):
+ self.assertEqual(self._extract("“quotes”".encode("cp1252")), "“quotes”")
+
+ def test_latin1_fallback(self):
+ # 0x81 is undefined in cp1252, forcing the latin-1 catch-all
+ self.assertEqual(self._extract(b"caf\x81"), "caf\x81")
+
+ def test_unsupported_format(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / "book.xyz"
+ path.write_bytes(b"data")
+ with self.assertRaises(ValueError):
+ extract_text(path)
+
+
+def _build_test_epub(path: Path) -> None:
+ from ebooklib import epub
+
+ book = epub.EpubBook()
+ book.set_identifier("test-id")
+ book.set_title("Test Book")
+ book.set_language("en")
+
+ chapter1 = epub.EpubHtml(title="One", file_name="chap1.xhtml", lang="en")
+ chapter1.content = "<html><body><p>First chapter text.</p></body></html>"
+ chapter2 = epub.EpubHtml(title="Two", file_name="chap2.xhtml", lang="en")
+ chapter2.content = "<html><body><p>Second chapter text.</p></body></html>"
+
+ book.add_item(chapter1)
+ book.add_item(chapter2)
+ book.toc = (chapter1, chapter2)
+ book.spine = ["nav", chapter1, chapter2]
+ book.add_item(epub.EpubNcx())
+ book.add_item(epub.EpubNav())
+
+ epub.write_epub(str(path), book)
+
+
+class EpubExtractionTests(unittest.TestCase):
+ def setUp(self):
+ try:
+ import ebooklib # noqa: F401
+ except ImportError:
+ self.skipTest("ebooklib not installed")
+
+ def test_ebooklib_extraction(self):
+ # Regression test: the ebooklib path used to silently return "" due to
+ # isinstance(item, ebooklib.ITEM_DOCUMENT) (an int, not a class).
+ from converter.extractors import _extract_epub_ebooklib
+
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / "book.epub"
+ _build_test_epub(path)
+ text = _extract_epub_ebooklib(path)
+
+ self.assertIn("First chapter text.", text)
+ self.assertIn("Second chapter text.", text)
+
+ def test_epub_extraction_follows_spine_order(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / "book.epub"
+ _build_test_epub(path)
+ text = extract_text(path)
+
+ self.assertIn("First chapter text.", text)
+ self.assertIn("Second chapter text.", text)
+ self.assertLess(text.index("First chapter text."),
+ text.index("Second chapter text."))
+
+
+if __name__ == "__main__":
+ unittest.main()