epub2text 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epub2text-0.2.4/.codecrate.toml +10 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/.gitignore +1 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/.ruff.toml +1 -1
- {epub2text-0.2.2 → epub2text-0.2.4}/PKG-INFO +3 -4
- {epub2text-0.2.2 → epub2text-0.2.4}/docs/installation.rst +0 -4
- {epub2text-0.2.2 → epub2text-0.2.4}/docs/make.py +1 -1
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text/__init__.py +2 -2
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text/_version.py +3 -3
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text/bookmarks.py +9 -5
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text/cleaner.py +2 -4
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text/cli.py +19 -20
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text/formatters.py +17 -101
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text/models.py +11 -12
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text/parser.py +81 -143
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text/reader.py +6 -7
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text.egg-info/PKG-INFO +3 -4
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text.egg-info/SOURCES.txt +1 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text.egg-info/requires.txt +1 -3
- {epub2text-0.2.2 → epub2text-0.2.4}/examples/demo.py +2 -19
- {epub2text-0.2.2 → epub2text-0.2.4}/pyproject.toml +16 -5
- {epub2text-0.2.2 → epub2text-0.2.4}/tests/test_epub_download.py +11 -4
- {epub2text-0.2.2 → epub2text-0.2.4}/tests/test_epub_generation.py +6 -13
- {epub2text-0.2.2 → epub2text-0.2.4}/tests/test_page_extraction_improvements.py +1 -1
- {epub2text-0.2.2 → epub2text-0.2.4}/tests/test_title_deduplication.py +4 -2
- {epub2text-0.2.2 → epub2text-0.2.4}/.coveragerc +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/.github/pytest.ini +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/.github/workflows/codecov.yml +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/.github/workflows/pre-commit.yml +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/.github/workflows/python-publish.yml +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/.github/workflows/tests.yml +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/.pre-commit-config.yaml +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/.prettierrc.yml +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/.readthedocs.yaml +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/LICENSE +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/README.md +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/docs/api.rst +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/docs/changelog.rst +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/docs/conf.py +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/docs/index.rst +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/docs/make.bat +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/docs/requirements.txt +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/docs/usage.rst +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text/__main__.py +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text/py.typed +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text.egg-info/dependency_links.txt +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text.egg-info/entry_points.txt +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/epub2text.egg-info/top_level.txt +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/examples/__init__.py +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/examples/__main__.py +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/requirements-test.txt +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/setup.cfg +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/setup.py +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/tests/__init__.py +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/tests/test_cleaner.py +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/tests/test_cli_ranges.py +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/tests/test_parser_page_list.py +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/tests/test_parser_spine_slice.py +0 -0
- {epub2text-0.2.2 → epub2text-0.2.4}/tests/test_reader_bookmarks.py +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
[codecrate]
|
|
2
|
+
output = "context_epub2text.md"
|
|
3
|
+
keep_docstrings = true
|
|
4
|
+
dedupe = true
|
|
5
|
+
metadata = false
|
|
6
|
+
respect_gitignore = true
|
|
7
|
+
exclude = ["examples", "*/.venv/*"]
|
|
8
|
+
include = ["**/*.py", "**/*.toml", "**/*.rst", "**/*.md"]
|
|
9
|
+
split_max_chars = 0
|
|
10
|
+
layout = "full"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: epub2text
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: A simple tool to extract text from EPUB files.
|
|
5
5
|
Author-email: Holger Nahrstaedt <nahrstaedt@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -37,7 +37,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
37
37
|
Classifier: Programming Language :: Python :: 3.13
|
|
38
38
|
Classifier: Programming Language :: Python :: 3.14
|
|
39
39
|
Classifier: Topic :: Software Development :: Libraries
|
|
40
|
-
Requires-Python: >=3.
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
42
42
|
License-File: LICENSE
|
|
43
43
|
Requires-Dist: click>=8.0.0
|
|
@@ -45,10 +45,9 @@ Requires-Dist: rich>=13.0.0
|
|
|
45
45
|
Requires-Dist: ebooklib>=0.18
|
|
46
46
|
Requires-Dist: beautifulsoup4>=4.12.0
|
|
47
47
|
Requires-Dist: defusedxml>=0.7.1
|
|
48
|
+
Requires-Dist: phrasplit>=0.3.2
|
|
48
49
|
Provides-Extra: lxml
|
|
49
50
|
Requires-Dist: lxml>=4.9.0; extra == "lxml"
|
|
50
|
-
Provides-Extra: sentences
|
|
51
|
-
Requires-Dist: phrasplit>=0.1.0; extra == "sentences"
|
|
52
51
|
Provides-Extra: dev
|
|
53
52
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
54
53
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
@@ -31,10 +31,6 @@ For better HTML parsing performance::
|
|
|
31
31
|
|
|
32
32
|
pip install epub2text[lxml]
|
|
33
33
|
|
|
34
|
-
For sentence-level formatting (requires phrasplit/spaCy)::
|
|
35
|
-
|
|
36
|
-
pip install epub2text[sentences]
|
|
37
|
-
|
|
38
34
|
After installing, download a spaCy language model::
|
|
39
35
|
|
|
40
36
|
python -m spacy download en_core_web_sm
|
|
@@ -136,7 +136,7 @@ def _download_epub(
|
|
|
136
136
|
except Exception as exc:
|
|
137
137
|
if tmp_path:
|
|
138
138
|
Path(tmp_path).unlink(missing_ok=True)
|
|
139
|
-
if isinstance(exc,
|
|
139
|
+
if isinstance(exc, urllib.error.URLError | socket.timeout):
|
|
140
140
|
if isinstance(exc, socket.timeout) or isinstance(
|
|
141
141
|
getattr(exc, "reason", None), socket.timeout
|
|
142
142
|
):
|
|
@@ -152,7 +152,7 @@ def epub2txt(
|
|
|
152
152
|
max_bytes: int = DEFAULT_MAX_DOWNLOAD_BYTES,
|
|
153
153
|
user_agent: str | None = None,
|
|
154
154
|
allowed_schemes: tuple[str, ...] = ("https", "http"),
|
|
155
|
-
) ->
|
|
155
|
+
) -> str | list[str]:
|
|
156
156
|
"""
|
|
157
157
|
Extract text from EPUB file (compatibility function for old epub2txt API).
|
|
158
158
|
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.2.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
31
|
+
__version__ = version = '0.2.4'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 4)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'ge900e4fef'
|
|
@@ -7,7 +7,7 @@ import tempfile
|
|
|
7
7
|
from dataclasses import asdict, dataclass
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, cast
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
@@ -54,7 +54,7 @@ class Bookmark:
|
|
|
54
54
|
class BookmarkManager:
|
|
55
55
|
"""Manages bookmarks for EPUB files."""
|
|
56
56
|
|
|
57
|
-
def __init__(self, bookmark_file:
|
|
57
|
+
def __init__(self, bookmark_file: Path | None = None) -> None:
|
|
58
58
|
"""
|
|
59
59
|
Initialize bookmark manager.
|
|
60
60
|
|
|
@@ -119,11 +119,15 @@ class BookmarkManager:
|
|
|
119
119
|
"""Return the path for the bookmark backup file."""
|
|
120
120
|
return self.bookmark_file.with_suffix(self.bookmark_file.suffix + ".bak")
|
|
121
121
|
|
|
122
|
-
def _read_bookmarks_file(self, path: Path) ->
|
|
122
|
+
def _read_bookmarks_file(self, path: Path) -> dict[str, Any] | None:
|
|
123
123
|
"""Read bookmarks JSON from disk."""
|
|
124
124
|
try:
|
|
125
125
|
with open(path, encoding="utf-8") as f:
|
|
126
|
-
|
|
126
|
+
data = json.load(f)
|
|
127
|
+
if isinstance(data, dict):
|
|
128
|
+
return cast(dict[str, Any], data)
|
|
129
|
+
logger.warning("Unexpected bookmark data structure in %s", path)
|
|
130
|
+
return None
|
|
127
131
|
except (json.JSONDecodeError, OSError) as exc:
|
|
128
132
|
logger.warning("Failed to load bookmarks from %s: %s", path, exc)
|
|
129
133
|
return None
|
|
@@ -144,7 +148,7 @@ class BookmarkManager:
|
|
|
144
148
|
self._bookmarks[key] = asdict(bookmark)
|
|
145
149
|
self._save()
|
|
146
150
|
|
|
147
|
-
def load(self, epub_path: str) ->
|
|
151
|
+
def load(self, epub_path: str) -> Bookmark | None:
|
|
148
152
|
"""
|
|
149
153
|
Load bookmark for a specific EPUB file.
|
|
150
154
|
|
|
@@ -96,7 +96,7 @@ class TextCleaner:
|
|
|
96
96
|
markers = {}
|
|
97
97
|
marker_count = [0]
|
|
98
98
|
|
|
99
|
-
def save_marker(match):
|
|
99
|
+
def save_marker(match: re.Match[str]) -> str:
|
|
100
100
|
placeholder = f"___MARKER_{marker_count[0]}___"
|
|
101
101
|
markers[placeholder] = match.group(0)
|
|
102
102
|
marker_count[0] += 1
|
|
@@ -147,8 +147,6 @@ class TextCleaner:
|
|
|
147
147
|
def apply_gutenberg_spacing(self, text: str) -> str:
|
|
148
148
|
"""Apply Gutenberg Project formatting: two spaces after sentences and colons.
|
|
149
149
|
|
|
150
|
-
Uses phrasplit for accurate sentence boundary detection.
|
|
151
|
-
|
|
152
150
|
Args:
|
|
153
151
|
text: Text to format
|
|
154
152
|
|
|
@@ -198,7 +196,7 @@ class TextCleaner:
|
|
|
198
196
|
abbrev_map = {}
|
|
199
197
|
abbrev_counter = [0]
|
|
200
198
|
|
|
201
|
-
def protect_abbrev(match):
|
|
199
|
+
def protect_abbrev(match: re.Match[str]) -> str:
|
|
202
200
|
placeholder = f"__ABBREV_{abbrev_counter[0]}__"
|
|
203
201
|
abbrev_map[placeholder] = match.group(0)
|
|
204
202
|
abbrev_counter[0] += 1
|
|
@@ -7,7 +7,6 @@ import sys
|
|
|
7
7
|
from collections.abc import Iterator
|
|
8
8
|
from contextlib import contextmanager
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Optional
|
|
11
10
|
|
|
12
11
|
import click
|
|
13
12
|
from rich.console import Console
|
|
@@ -46,7 +45,7 @@ def spinner(task_description: str) -> Iterator[None]:
|
|
|
46
45
|
yield
|
|
47
46
|
|
|
48
47
|
|
|
49
|
-
def parse_range_tokens(range_str: str) -> list[tuple[str,
|
|
48
|
+
def parse_range_tokens(range_str: str) -> list[tuple[str, str | None]]:
|
|
50
49
|
"""
|
|
51
50
|
Parse a range string into ordered tokens.
|
|
52
51
|
|
|
@@ -59,7 +58,7 @@ def parse_range_tokens(range_str: str) -> list[tuple[str, Optional[str]]]:
|
|
|
59
58
|
if not range_str:
|
|
60
59
|
raise ValueError("Range string cannot be empty")
|
|
61
60
|
|
|
62
|
-
tokens: list[tuple[str,
|
|
61
|
+
tokens: list[tuple[str, str | None]] = []
|
|
63
62
|
parts = range_str.split(",")
|
|
64
63
|
|
|
65
64
|
for part in parts:
|
|
@@ -205,7 +204,7 @@ def parse_page_range(range_str: str, pages: list[Page]) -> list[str]:
|
|
|
205
204
|
result: list[str] = []
|
|
206
205
|
seen: set[str] = set()
|
|
207
206
|
|
|
208
|
-
def parse_int(value: str) ->
|
|
207
|
+
def parse_int(value: str) -> int | None:
|
|
209
208
|
try:
|
|
210
209
|
return int(value)
|
|
211
210
|
except ValueError:
|
|
@@ -590,8 +589,8 @@ def list_pages(filepath: Path, page_size: int, use_words: bool) -> None:
|
|
|
590
589
|
)
|
|
591
590
|
def extract_pages_cmd(
|
|
592
591
|
filepath: Path,
|
|
593
|
-
output:
|
|
594
|
-
pages:
|
|
592
|
+
output: Path | None,
|
|
593
|
+
pages: str | None,
|
|
595
594
|
page_size: int,
|
|
596
595
|
use_words: bool,
|
|
597
596
|
raw: bool,
|
|
@@ -786,13 +785,13 @@ def extract_pages_cmd(
|
|
|
786
785
|
)
|
|
787
786
|
def extract(
|
|
788
787
|
filepath: Path,
|
|
789
|
-
output:
|
|
790
|
-
chapters:
|
|
788
|
+
output: Path | None,
|
|
789
|
+
chapters: str | None,
|
|
791
790
|
interactive: bool,
|
|
792
791
|
paragraphs: bool,
|
|
793
792
|
sentences: bool,
|
|
794
793
|
comma: bool,
|
|
795
|
-
max_length:
|
|
794
|
+
max_length: int | None,
|
|
796
795
|
empty_lines: bool,
|
|
797
796
|
separator: str,
|
|
798
797
|
raw: bool,
|
|
@@ -801,7 +800,7 @@ def extract(
|
|
|
801
800
|
no_markers: bool,
|
|
802
801
|
language_model: str,
|
|
803
802
|
offset: int,
|
|
804
|
-
limit:
|
|
803
|
+
limit: int | None,
|
|
805
804
|
line_numbers: bool,
|
|
806
805
|
) -> None:
|
|
807
806
|
"""
|
|
@@ -1012,8 +1011,8 @@ def extract(
|
|
|
1012
1011
|
)
|
|
1013
1012
|
def extract_gutenberg(
|
|
1014
1013
|
filepath: Path,
|
|
1015
|
-
output:
|
|
1016
|
-
chapters:
|
|
1014
|
+
output: Path | None,
|
|
1015
|
+
chapters: str | None,
|
|
1017
1016
|
interactive: bool,
|
|
1018
1017
|
) -> None:
|
|
1019
1018
|
"""
|
|
@@ -1124,9 +1123,9 @@ def extract_gutenberg(
|
|
|
1124
1123
|
# First, normalize the chapter text to help with splitting
|
|
1125
1124
|
chapter_lines = chapter_text.split("\n")
|
|
1126
1125
|
|
|
1127
|
-
current_title = None
|
|
1128
|
-
current_content = []
|
|
1129
|
-
chapters_list = []
|
|
1126
|
+
current_title: str | None = None
|
|
1127
|
+
current_content: list[str] = []
|
|
1128
|
+
chapters_list: list[tuple[str, str]] = []
|
|
1130
1129
|
|
|
1131
1130
|
i = 0
|
|
1132
1131
|
while i < len(chapter_lines):
|
|
@@ -1418,11 +1417,11 @@ def info(filepath: Path, format: str) -> None:
|
|
|
1418
1417
|
)
|
|
1419
1418
|
def read(
|
|
1420
1419
|
filepath: Path,
|
|
1421
|
-
chapter:
|
|
1422
|
-
line:
|
|
1420
|
+
chapter: int | None,
|
|
1421
|
+
line: int | None,
|
|
1423
1422
|
resume: bool,
|
|
1424
|
-
bookmark_file:
|
|
1425
|
-
page_size:
|
|
1423
|
+
bookmark_file: Path | None,
|
|
1424
|
+
page_size: int | None,
|
|
1426
1425
|
no_header: bool,
|
|
1427
1426
|
no_footer: bool,
|
|
1428
1427
|
sentences: bool,
|
|
@@ -1432,7 +1431,7 @@ def read(
|
|
|
1432
1431
|
raw: bool,
|
|
1433
1432
|
keep_footnotes: bool,
|
|
1434
1433
|
keep_page_numbers: bool,
|
|
1435
|
-
width:
|
|
1434
|
+
width: int | None,
|
|
1436
1435
|
) -> None:
|
|
1437
1436
|
"""
|
|
1438
1437
|
Interactively read an EPUB file in the terminal.
|
|
@@ -1,70 +1,15 @@
|
|
|
1
1
|
"""Text formatting utilities for different output styles."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
from typing import Callable, Optional
|
|
5
|
-
|
|
6
|
-
# Type aliases for phrasplit functions
|
|
7
|
-
_SplitFunc = Callable[[str, str], list[str]]
|
|
8
|
-
_SplitLongLinesFunc = Callable[[str, int, str], list[str]]
|
|
9
|
-
|
|
10
|
-
# Try to import phrasplit, fall back to None when not available
|
|
11
|
-
PHRASPLIT_AVAILABLE: bool
|
|
12
|
-
phrasplit_clauses: Optional[_SplitFunc]
|
|
13
|
-
phrasplit_long_lines: Optional[_SplitLongLinesFunc]
|
|
14
|
-
phrasplit_paragraphs: Optional[Callable[[str], list[str]]]
|
|
15
|
-
phrasplit_sentences: Optional[_SplitFunc]
|
|
16
|
-
|
|
17
|
-
try:
|
|
18
|
-
from phrasplit import ( # type: ignore[import-not-found]
|
|
19
|
-
split_clauses as _phrasplit_clauses,
|
|
20
|
-
)
|
|
21
|
-
from phrasplit import (
|
|
22
|
-
split_long_lines as _phrasplit_long_lines,
|
|
23
|
-
)
|
|
24
|
-
from phrasplit import (
|
|
25
|
-
split_paragraphs as _phrasplit_paragraphs,
|
|
26
|
-
)
|
|
27
|
-
from phrasplit import (
|
|
28
|
-
split_sentences as _phrasplit_sentences,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
PHRASPLIT_AVAILABLE = True
|
|
32
|
-
phrasplit_clauses = _phrasplit_clauses
|
|
33
|
-
phrasplit_long_lines = _phrasplit_long_lines
|
|
34
|
-
phrasplit_paragraphs = _phrasplit_paragraphs
|
|
35
|
-
phrasplit_sentences = _phrasplit_sentences
|
|
36
|
-
except ImportError:
|
|
37
|
-
PHRASPLIT_AVAILABLE = False
|
|
38
|
-
phrasplit_clauses = None
|
|
39
|
-
phrasplit_long_lines = None
|
|
40
|
-
phrasplit_paragraphs = None
|
|
41
|
-
phrasplit_sentences = None
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _check_phrasplit() -> None:
|
|
45
|
-
"""Check if phrasplit is available, raise ImportError if not."""
|
|
46
|
-
if not PHRASPLIT_AVAILABLE:
|
|
47
|
-
raise ImportError(
|
|
48
|
-
"phrasplit is required for this feature. "
|
|
49
|
-
"Install with: pip install epub2text[sentences]"
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def split_paragraphs(text: str) -> list[str]:
|
|
54
|
-
"""
|
|
55
|
-
Split text into paragraphs (separated by double newlines).
|
|
56
4
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# Simple implementation that doesn't require phrasplit
|
|
66
|
-
paragraphs = re.split(r"\n\s*\n", text)
|
|
67
|
-
return [p.strip() for p in paragraphs if p.strip()]
|
|
5
|
+
from phrasplit import (
|
|
6
|
+
split_clauses,
|
|
7
|
+
split_paragraphs,
|
|
8
|
+
split_sentences,
|
|
9
|
+
)
|
|
10
|
+
from phrasplit import (
|
|
11
|
+
split_long_lines as _phrasplit_split_long_lines,
|
|
12
|
+
)
|
|
68
13
|
|
|
69
14
|
|
|
70
15
|
def collapse_paragraph(paragraph: str) -> str:
|
|
@@ -146,8 +91,6 @@ def format_sentences(
|
|
|
146
91
|
"""
|
|
147
92
|
Format text with one sentence per line.
|
|
148
93
|
|
|
149
|
-
Requires phrasplit to be installed.
|
|
150
|
-
|
|
151
94
|
Args:
|
|
152
95
|
text: Input text with paragraph breaks
|
|
153
96
|
separator: String to prepend at paragraph boundaries (default: " ")
|
|
@@ -157,9 +100,6 @@ def format_sentences(
|
|
|
157
100
|
Text with one sentence per line, separator at paragraph boundaries.
|
|
158
101
|
Chapter titles are preserved.
|
|
159
102
|
"""
|
|
160
|
-
_check_phrasplit()
|
|
161
|
-
assert phrasplit_sentences is not None
|
|
162
|
-
|
|
163
103
|
paragraphs = split_paragraphs(text)
|
|
164
104
|
|
|
165
105
|
if not paragraphs:
|
|
@@ -180,7 +120,7 @@ def format_sentences(
|
|
|
180
120
|
continue
|
|
181
121
|
|
|
182
122
|
# Process paragraph into sentences using phrasplit
|
|
183
|
-
sentences =
|
|
123
|
+
sentences = split_sentences(para, language_model)
|
|
184
124
|
|
|
185
125
|
if not sentences:
|
|
186
126
|
continue
|
|
@@ -224,8 +164,6 @@ def format_clauses(
|
|
|
224
164
|
"I do like coffee,
|
|
225
165
|
and I like wine."
|
|
226
166
|
"""
|
|
227
|
-
_check_phrasplit()
|
|
228
|
-
assert phrasplit_clauses is not None
|
|
229
167
|
|
|
230
168
|
paragraphs = split_paragraphs(text)
|
|
231
169
|
|
|
@@ -246,7 +184,7 @@ def format_clauses(
|
|
|
246
184
|
continue
|
|
247
185
|
|
|
248
186
|
# Process paragraph into clauses using phrasplit
|
|
249
|
-
clauses =
|
|
187
|
+
clauses = split_clauses(para, language_model)
|
|
250
188
|
|
|
251
189
|
if not clauses:
|
|
252
190
|
continue
|
|
@@ -277,8 +215,6 @@ def split_long_lines(
|
|
|
277
215
|
2. If still too long, split at clause boundaries (commas, semicolons, etc.)
|
|
278
216
|
3. If still too long, split at word boundaries
|
|
279
217
|
|
|
280
|
-
Requires phrasplit to be installed.
|
|
281
|
-
|
|
282
218
|
Args:
|
|
283
219
|
text: Input text (may already be formatted)
|
|
284
220
|
max_length: Maximum line length in characters
|
|
@@ -288,9 +224,6 @@ def split_long_lines(
|
|
|
288
224
|
Returns:
|
|
289
225
|
Text with long lines split. Chapter titles are preserved.
|
|
290
226
|
"""
|
|
291
|
-
_check_phrasplit()
|
|
292
|
-
assert phrasplit_long_lines is not None
|
|
293
|
-
|
|
294
227
|
lines = text.split("\n")
|
|
295
228
|
result_lines: list[str] = []
|
|
296
229
|
|
|
@@ -315,9 +248,13 @@ def split_long_lines(
|
|
|
315
248
|
has_separator = line.startswith(separator) if separator else False
|
|
316
249
|
content = line[len(separator) :] if has_separator else line
|
|
317
250
|
|
|
318
|
-
# Split the long line using phrasplit
|
|
319
|
-
split_lines_list =
|
|
320
|
-
|
|
251
|
+
# Split the long line using phrasplit (avoid calling ourselves)
|
|
252
|
+
split_lines_list = _phrasplit_split_long_lines(
|
|
253
|
+
content, max_length, language_model
|
|
254
|
+
)
|
|
255
|
+
# Defensive: if upstream ever returns a string, normalize to list
|
|
256
|
+
if isinstance(split_lines_list, str):
|
|
257
|
+
split_lines_list = [split_lines_list]
|
|
321
258
|
# Add separator to first line if original had it
|
|
322
259
|
for k, split_line in enumerate(split_lines_list):
|
|
323
260
|
if k == 0 and has_separator:
|
|
@@ -328,32 +265,11 @@ def split_long_lines(
|
|
|
328
265
|
return "\n".join(result_lines)
|
|
329
266
|
|
|
330
267
|
|
|
331
|
-
# Keep backward compatibility alias
|
|
332
|
-
def format_as_sentences(text: str, language_model: str = "en_core_web_sm") -> str:
|
|
333
|
-
"""
|
|
334
|
-
Format text with one sentence per line using spaCy.
|
|
335
|
-
|
|
336
|
-
Deprecated: Use format_sentences() instead.
|
|
337
|
-
|
|
338
|
-
Requires phrasplit to be installed.
|
|
339
|
-
|
|
340
|
-
Args:
|
|
341
|
-
text: Input text with paragraph breaks
|
|
342
|
-
language_model: spaCy language model to use (default: "en_core_web_sm")
|
|
343
|
-
|
|
344
|
-
Returns:
|
|
345
|
-
Text with sentences separated by newlines
|
|
346
|
-
"""
|
|
347
|
-
return format_sentences(text, separator="", language_model=language_model)
|
|
348
|
-
|
|
349
|
-
|
|
350
268
|
__all__ = [
|
|
351
|
-
"PHRASPLIT_AVAILABLE",
|
|
352
269
|
"collapse_paragraph",
|
|
353
270
|
"format_paragraphs",
|
|
354
271
|
"format_sentences",
|
|
355
272
|
"format_clauses",
|
|
356
273
|
"split_long_lines",
|
|
357
|
-
"format_as_sentences",
|
|
358
274
|
"split_paragraphs",
|
|
359
275
|
]
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
class PageSource(Enum):
|
|
@@ -25,8 +24,8 @@ class Page:
|
|
|
25
24
|
text: str
|
|
26
25
|
char_count: int
|
|
27
26
|
source: PageSource
|
|
28
|
-
chapter_id:
|
|
29
|
-
chapter_title:
|
|
27
|
+
chapter_id: str | None = None # Which chapter this page belongs to
|
|
28
|
+
chapter_title: str | None = None # Title of the chapter
|
|
30
29
|
|
|
31
30
|
def __str__(self) -> str:
|
|
32
31
|
source_str = "print" if self.source == PageSource.EPUB_PAGE_LIST else "syn"
|
|
@@ -41,7 +40,7 @@ class Chapter:
|
|
|
41
40
|
title: str
|
|
42
41
|
text: str
|
|
43
42
|
char_count: int
|
|
44
|
-
parent_id:
|
|
43
|
+
parent_id: str | None = None
|
|
45
44
|
level: int = 0
|
|
46
45
|
|
|
47
46
|
def __str__(self) -> str:
|
|
@@ -52,18 +51,18 @@ class Chapter:
|
|
|
52
51
|
class Metadata:
|
|
53
52
|
"""EPUB metadata."""
|
|
54
53
|
|
|
55
|
-
title:
|
|
54
|
+
title: str | None = None
|
|
56
55
|
authors: list[str] = field(default_factory=list)
|
|
57
|
-
publisher:
|
|
58
|
-
publication_year:
|
|
59
|
-
description:
|
|
56
|
+
publisher: str | None = None
|
|
57
|
+
publication_year: str | None = None
|
|
58
|
+
description: str | None = None
|
|
60
59
|
# Required EPUB3 metadata
|
|
61
|
-
identifier:
|
|
62
|
-
language:
|
|
60
|
+
identifier: str | None = None
|
|
61
|
+
language: str | None = None
|
|
63
62
|
# Optional Dublin Core metadata
|
|
64
63
|
contributors: list[str] = field(default_factory=list)
|
|
65
|
-
rights:
|
|
66
|
-
coverage:
|
|
64
|
+
rights: str | None = None
|
|
65
|
+
coverage: str | None = None
|
|
67
66
|
|
|
68
67
|
def __str__(self) -> str:
|
|
69
68
|
lines = []
|