epubchapterize 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,4 @@
1
+ # __init__.py
2
+ from .chapterize import chapterize
3
+
4
+ __all__ = ["chapterize"]
@@ -0,0 +1,320 @@
1
+ from glob import glob
2
+ import os
3
+ import re
4
+ import ebooklib
5
+ from ebooklib import epub
6
+ from bs4 import BeautifulSoup
7
+ import nltk
8
+ nltk.download('punkt_tab')
9
+ from lxml import etree
10
+ from dataclasses import dataclass
11
+ import syntok.segmenter as segmenter
12
+ import spacy
13
+ import sys
14
+ from spacy.util import is_package
15
+
16
+
17
+ def load_model(name="en_core_web_sm"):
18
+ if not is_package(name):
19
+ raise RuntimeError(
20
+ f"SpaCy model '{name}' not installed. Run: python -m spacy download {name}"
21
+ )
22
+ return spacy.load(name)
23
+
24
+ def get_nlp_model(language_code):
25
+ if language_code == 'en':
26
+ return load_model('en_core_web_trf')
27
+ elif language_code == 'de':
28
+ return load_model('de_dep_news_trf')
29
+ elif language_code == 'it':
30
+ return load_model('it_core_news_trf')
31
+ elif language_code == 'es':
32
+ return load_model('es_dep_news_trf')
33
+ elif language_code == 'fr':
34
+ return load_model('fr_dep_news_trf')
35
+ else:
36
+ return None
37
+
38
+ nlp_models = {}
39
+
40
+ @dataclass
41
+ class NavItem:
42
+ nav_label: str
43
+ doc_href: str
44
+ element_id: str
45
+
46
+ @dataclass
47
+ class HeaderMatch:
48
+ header: object
49
+ header_text: str
50
+ header_xpath: str
51
+ nav_item: NavItem
52
+ sent_method = "nltk" # Options: "nltk", "spacy", "syntok"
53
+
54
+ def syntok_segmenter(text):
55
+ sentences = []
56
+ for paragraph in segmenter.process(text):
57
+ for sentence in paragraph:
58
+ sentence = ''.join(token.spacing + token.value for token in sentence)
59
+ sentences.append(sentence)
60
+ return sentences
61
+
62
+ def get_punkt_tokenizer(langage_code):
63
+ language_tokenizer_map = {
64
+ 'en': 'tokenizers/punkt/english.pickle',
65
+ 'fr': 'tokenizers/punkt/french.pickle',
66
+ 'de': 'tokenizers/punkt/german.pickle',
67
+ 'es': 'tokenizers/punkt/spanish.pickle',
68
+ 'it': 'tokenizers/punkt/italian.pickle',
69
+ 'nl': 'tokenizers/punkt/dutch.pickle',
70
+ 'pt': 'tokenizers/punkt/portuguese.pickle',
71
+ }
72
+ return language_tokenizer_map.get(langage_code, 'tokenizers/punkt/english.pickle')
73
+
74
+ def get_sent_method(language_code):
75
+ if sent_method == "nltk":
76
+ tokenizer = nltk.data.load(get_punkt_tokenizer(language_code))
77
+ def custom_sent_tokenize(text):
78
+ return tokenizer.tokenize(text)
79
+ return custom_sent_tokenize
80
+ elif sent_method == "spacy":
81
+ nlp = get_nlp_model(language_code)
82
+ if nlp:
83
+ return lambda text: [sent.text for sent in nlp(text).sents]
84
+ else:
85
+ raise ValueError(f"Unsupported language code for spaCy: {language_code}")
86
+ elif sent_method == "syntok":
87
+ return lambda text: [sent for sent in syntok_segmenter(text)]
88
+ else:
89
+ raise ValueError(f"Unknown sentence segmentation method: {sent_method}")
90
+
91
+ def get_matched_header_for_nav_item(nav_item: NavItem, book) -> HeaderMatch:
92
+ nav_label = nav_item.nav_label
93
+ doc_href = nav_item.doc_href
94
+ element_id = nav_item.element_id
95
+ linked_item = book.get_item_with_href(doc_href)
96
+ if linked_item:
97
+ linked_content = linked_item.get_content().decode()
98
+ linked_soup = BeautifulSoup(linked_content, 'html.parser') # Parse as HTML
99
+ extracted_text = None
100
+ if element_id:
101
+ linked_element = linked_soup.find(id=element_id)
102
+ if linked_element:
103
+ # Extract headers (h1, h2, h3) from the linked content
104
+ h1s = linked_soup.find_all('h1')
105
+ h2s = linked_soup.find_all('h2')
106
+ h3s = linked_soup.find_all('h3')
107
+
108
+ # Combine headers into a single list with their tag type
109
+ headers = [(header, 'h1') for header in h1s] + \
110
+ [(header, 'h2') for header in h2s] + \
111
+ [(header, 'h3') for header in h3s]
112
+
113
+ print(f"Found {len(headers)} headers in {doc_href} with element ID: {element_id}")
114
+
115
+ best_match = None
116
+ pattern = generate_header_pattern(nav_label)
117
+ print(f"Searching for header matching pattern: {pattern.pattern} in {doc_href} with element ID: {element_id}")
118
+ for header, _ in headers:
119
+ header_text = header.get_text(' ', strip=True).replace('\n', ' ')
120
+ print(f"Checking header: {header_text}")
121
+ if pattern.search(header_text):
122
+ print(f"Pattern matched in header: {header_text}")
123
+ if not best_match or len(header_text) < len(best_match.header_text):
124
+ def get_xpath(element):
125
+ tree = etree.HTML(str(element))
126
+ return tree.getroottree().getpath(tree)
127
+ header_xpath = get_xpath(header)
128
+ best_match = HeaderMatch(header, header_text, header_xpath, nav_item)
129
+ extracted_text = best_match if best_match else None
130
+ return extracted_text
131
+
132
+ def generate_header_pattern(target_text):
133
+ words = re.findall(r'\w+', target_text.lower())
134
+ pattern = r'.*'.join(re.escape(word) for word in words)
135
+ return re.compile(pattern, re.IGNORECASE)
136
+
137
+ def get_nav_items_standard_gutenberg_epub3(file_path) -> list[NavItem]:
138
+ book = epub.read_epub(file_path)
139
+ nav_items = []
140
+
141
+ for item in book.get_items():
142
+ if item.get_type() == ebooklib.ITEM_NAVIGATION:
143
+ html_content = item.get_content().decode()
144
+ soup = BeautifulSoup(html_content, 'xml')
145
+ navpoints = soup.find_all('navPoint')
146
+ for navpoint in navpoints:
147
+ nav_label = navpoint.find('navLabel').find("text").get_text(strip=True)
148
+ content = navpoint.find('content')
149
+ doc_href = None
150
+ element_id = None
151
+ if content and content.has_attr('src'):
152
+ src = content['src']
153
+ src_parts = src.split('#')
154
+ doc_href = src_parts[0]
155
+ element_id = src_parts[1] if len(src_parts) > 1 else None # ID within the document
156
+ nav_items.append(NavItem(nav_label, doc_href, element_id))
157
+ return nav_items
158
+
159
+ def filter_by_chapter_class(combined_header_info: list[tuple[any, NavItem]], book) -> list[tuple[any, NavItem]]:
160
+ found_chapter_divs = False
161
+ filtered_header_info = []
162
+ for header, nav_item in combined_header_info:
163
+ for item in book.get_items():
164
+ if item.get_type() == ebooklib.ITEM_DOCUMENT:
165
+ soup = BeautifulSoup(item.get_body_content(), 'html.parser')
166
+ chapter_divs = soup.find_all('div', class_='chapter')
167
+ if chapter_divs:
168
+ found_chapter_divs = True
169
+ for chapter_div in chapter_divs:
170
+ if header in chapter_div.descendants:
171
+ filtered_header_info.append((header, nav_item))
172
+ break
173
+
174
+ if not found_chapter_divs:
175
+ return combined_header_info
176
+
177
+ return filtered_header_info
178
+
179
+ def chapterize(file_path):
180
+ book = epub.read_epub(file_path)
181
+ nav_item_infos = get_nav_items_standard_gutenberg_epub3(file_path)
182
+ language = book.get_metadata('DC', 'language')
183
+ language = language[0][0] if language else 'en'
184
+ if language in nlp_models:
185
+ get_sentences = nlp_models[language]
186
+ else:
187
+ get_sentences = get_sent_method(language)
188
+ nlp_models[language] = get_sentences
189
+ chapters = []
190
+ matched_candidate_headers: list[HeaderMatch] = []
191
+ for nav_item_info in nav_item_infos:
192
+ matched_candidate_headers.append(get_matched_header_for_nav_item(nav_item_info, book))
193
+ print(f"Matched header for nav item '{nav_item_info.nav_label}': {matched_candidate_headers[-1]}")
194
+
195
+ matched_candidate_headers = [candidate_header for candidate_header in matched_candidate_headers if candidate_header is not None]
196
+
197
+ for matched_header in matched_candidate_headers:
198
+ print(f"Matched Header: {matched_header.header_text}, XPath: {matched_header.header_xpath}, Nav Label: {matched_header.nav_item.nav_label}")
199
+
200
+ for item in book.get_items():
201
+ if item.get_type() == ebooklib.ITEM_DOCUMENT:
202
+ soup = BeautifulSoup(item.get_body_content(), 'html.parser')
203
+
204
+ current_document_all_headers = []
205
+ for header_tag in ['h1', 'h2', 'h3']:
206
+ current_document_all_headers.extend(soup.find_all(header_tag))
207
+
208
+ headers_with_nav_items = []
209
+ for header in current_document_all_headers:
210
+ for header_match in matched_candidate_headers:
211
+ if str(header_match.header) == str(header):
212
+ headers_with_nav_items.append((header, header_match.nav_item))
213
+
214
+ headers_with_nav_items = filter_by_chapter_class(headers_with_nav_items, book)
215
+ print(f"Number of headers with nav items: {len(headers_with_nav_items)}")
216
+
217
+ sections = []
218
+ for i, combined_header in enumerate(headers_with_nav_items):
219
+ header_match = combined_header[0]
220
+ nav_item_info = combined_header[1]
221
+ heading_text = nav_item_info.nav_label
222
+ if "THE FULL PROJECT GUTENBERG LICENSE" in heading_text:
223
+ continue
224
+ section = {'title': heading_text, 'paragraphs': []}
225
+ next_heading = headers_with_nav_items[i + 1] if i + 1 < len(headers_with_nav_items) else None
226
+ current_element = header_match
227
+ while current_element and current_element != next_heading:
228
+ if current_element.name == 'p':
229
+ section['paragraphs'].append(current_element.get_text(separator=" ", strip=True))
230
+ current_element = current_element.find_next()
231
+ sections.append(section)
232
+
233
+ no_sentences_heading = ''
234
+ for section in sections:
235
+ chapter_title = no_sentences_heading + section['title']
236
+ chapter_title = chapter_title.replace('\n', ' ')
237
+ paragraphs = section['paragraphs']
238
+ sentences = []
239
+ for paragraph in paragraphs:
240
+ # Replace all occurrences of chapter_title and newlines in the paragraph
241
+ transformed_sentences = get_sentences(paragraph.replace(chapter_title, '').replace('\n', ' '))
242
+ for sentence in transformed_sentences:
243
+ stripped_sentence = sentence.strip()
244
+ if stripped_sentence and not all(char in '.,!?;:"\'-()[]{}' for char in stripped_sentence): # Check if the sentence is not empty, whitespace, or only punctuation
245
+ sentences.append(sentence)
246
+ if sentences:
247
+ chapters.append({
248
+ 'title': chapter_title,
249
+ 'sentences': [s.strip() for s in sentences]
250
+ })
251
+ no_sentences_heading = ''
252
+ else:
253
+ no_sentences_heading += chapter_title + ' '
254
+
255
+ return chapters, language
256
+
257
+
258
+
259
+ if __name__ == "__main__":
260
+
261
+ books_to_add = []
262
+ books_directory = "books"
263
+
264
+ #["/Users/matthewgrant/Source/EpubChapterize/epub_chapterize/books/to_import/Röschen-Jaköble-und-andere-kleine-Leute-De.epub"]:
265
+ for file_path in glob(os.path.join(books_directory, "**", "*.epub"), recursive=True):
266
+ if "archive" in file_path: # Include only files in the archive folder
267
+ continue
268
+ book = epub.read_epub(file_path)
269
+ language = book.get_metadata('DC', 'language')
270
+ language = language[0][0] if language else 'en'
271
+
272
+ title = book.get_metadata('DC', 'title')
273
+ author = book.get_metadata('DC', 'creator')
274
+ title = title[0][0] if title else "Unknown Title"
275
+ author = author[0][0] if author else "Unknown Author"
276
+
277
+ books_to_add.append({
278
+ 'file_path': os.path.relpath(file_path, books_directory),
279
+ 'title': title,
280
+ 'author': author
281
+ })
282
+
283
+ output_test_files = True
284
+ unable_to_parse_file = os.path.join(books_directory, "unable_to_parse.txt")
285
+ if os.path.exists(unable_to_parse_file):
286
+ os.remove(unable_to_parse_file)
287
+ for book_to_add in books_to_add:
288
+ if len(sys.argv) > 1:
289
+ input_file_path = sys.argv[1]
290
+ if os.path.exists(input_file_path):
291
+ chapters, language = chapterize(input_file_path)
292
+ else:
293
+ print(f"File {input_file_path} does not exist. Falling back to default behavior.")
294
+ chapters, language = chapterize(os.path.join(books_directory, book_to_add["file_path"]))
295
+ else:
296
+ chapters, language = chapterize(os.path.join(books_directory, book_to_add["file_path"]))
297
+
298
+ print("Chapters found:", len(chapters))
299
+ if not chapters:
300
+ unable_to_parse_file = os.path.join(books_directory, "unable_to_parse.txt")
301
+ os.makedirs(os.path.dirname(unable_to_parse_file), exist_ok=True)
302
+ with open(unable_to_parse_file, "a", encoding="utf-8") as f:
303
+ f.write(os.path.join(books_directory, book_to_add["file_path"]) + "\n")
304
+ for chapter in chapters:
305
+ print(chapter["title"])
306
+ print(chapter["sentences"][:1])
307
+ if output_test_files:
308
+ book_folder = os.path.join("output", book_to_add["title"][:100])
309
+ os.makedirs(book_folder, exist_ok=True)
310
+ print("Book folder created:", book_folder)
311
+ chapter_number = chapters.index(chapter) + 1
312
+ chapter_file_path = os.path.join(book_folder, f"{chapter_number} - {chapter['title'][:100]}.txt")
313
+
314
+ with open(chapter_file_path, "w", encoding="utf-8") as chapter_file:
315
+ chapter_file.write(chapter["title"] + "\n\n")
316
+ chapter_file.write("\n".join(f"<start> {sentence} <end>" for sentence in chapter["sentences"]))
317
+
318
+
319
+
320
+
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: epubchapterize
3
+ Version: 0.1.0
4
+ Summary: A Python package for parsing chapters from EPUBs.
5
+ Author-email: Matthew Grant <nzmattgrant@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/nzmattgrant/epubchapterize
8
+ Project-URL: Documentation, https://github.com/nzmattgrant/epubchapterize/wiki
9
+ Project-URL: Source, https://github.com/nzmattgrant/epubchapterize
10
+ Project-URL: Tracker, https://github.com/nzmattgrant/epubchapterize/issues
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.13
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: annotated-types==0.7.0
18
+ Requires-Dist: beautifulsoup4==4.13.3
19
+ Requires-Dist: blis==1.3.0
20
+ Requires-Dist: bs4==0.0.2
21
+ Requires-Dist: build==1.3.0
22
+ Requires-Dist: catalogue==2.0.10
23
+ Requires-Dist: certifi==2025.1.31
24
+ Requires-Dist: charset-normalizer==3.4.1
25
+ Requires-Dist: click==8.1.8
26
+ Requires-Dist: cloudpathlib==0.21.0
27
+ Requires-Dist: confection==0.1.5
28
+ Requires-Dist: cymem==2.0.11
29
+ Requires-Dist: EbookLib==0.18
30
+ Requires-Dist: idna==3.10
31
+ Requires-Dist: importlib_metadata==8.7.0
32
+ Requires-Dist: Jinja2==3.1.6
33
+ Requires-Dist: joblib==1.4.2
34
+ Requires-Dist: langcodes==3.5.0
35
+ Requires-Dist: language_data==1.3.0
36
+ Requires-Dist: lxml==5.3.2
37
+ Requires-Dist: marisa-trie==1.2.1
38
+ Requires-Dist: markdown-it-py==3.0.0
39
+ Requires-Dist: MarkupSafe==3.0.2
40
+ Requires-Dist: mdurl==0.1.2
41
+ Requires-Dist: murmurhash==1.0.12
42
+ Requires-Dist: nltk==3.9.1
43
+ Requires-Dist: numpy==2.0.2
44
+ Requires-Dist: packaging==24.2
45
+ Requires-Dist: preshed==3.0.9
46
+ Requires-Dist: pydantic==2.11.3
47
+ Requires-Dist: pydantic_core==2.33.1
48
+ Requires-Dist: Pygments==2.19.1
49
+ Requires-Dist: pyproject_hooks==1.2.0
50
+ Requires-Dist: regex==2024.11.6
51
+ Requires-Dist: requests==2.32.3
52
+ Requires-Dist: rich==14.0.0
53
+ Requires-Dist: shellingham==1.5.4
54
+ Requires-Dist: six==1.17.0
55
+ Requires-Dist: smart-open==7.1.0
56
+ Requires-Dist: soupsieve==2.6
57
+ Requires-Dist: spacy==3.8.7
58
+ Requires-Dist: spacy-legacy==3.0.12
59
+ Requires-Dist: spacy-loggers==1.0.5
60
+ Requires-Dist: srsly==2.5.1
61
+ Requires-Dist: syntok==1.4.4
62
+ Requires-Dist: thinc==8.3.6
63
+ Requires-Dist: tomli==2.2.1
64
+ Requires-Dist: tqdm==4.67.1
65
+ Requires-Dist: typer==0.15.2
66
+ Requires-Dist: typing-inspection==0.4.0
67
+ Requires-Dist: typing_extensions==4.13.1
68
+ Requires-Dist: urllib3==2.4.0
69
+ Requires-Dist: wasabi==1.1.3
70
+ Requires-Dist: weasel==0.4.1
71
+ Requires-Dist: wrapt==1.17.2
72
+ Requires-Dist: zipp==3.23.0
73
+ Dynamic: license-file
74
+
75
+ # EpubChapterize
76
+ ### A tool to split out chapters from ePub documents. Initially just for Project Gutenberg ePub3s.
77
+
78
+ ## Setup
79
+
80
+ To set up the project, follow these steps:
81
+
82
+ 1. Clone the repository:
83
+ ```bash
84
+ git clone https://github.com/yourusername/EpubChapterize.git
85
+ cd EpubChapterize
86
+ ```
87
+
88
+ 2. Create a virtual environment:
89
+ ```bash
90
+ python -m venv venv
91
+ ```
92
+
93
+ 3. Activate the virtual environment:
94
+ - On macOS/Linux:
95
+ ```bash
96
+ source venv/bin/activate
97
+ ```
98
+ - On Windows:
99
+ ```bash
100
+ venv\Scripts\activate
101
+ ```
102
+
103
+ 4. Install the required dependencies:
104
+ ```bash
105
+ pip install -r requirements.txt
106
+ ```
107
+ 5. Install additional language models for spaCy (if needed):
108
+
109
+ Depending on the languages you plan to process, you may need to install specific spaCy language models. Use the following commands to install them:
110
+
111
+ - For English:
112
+ ```bash
113
+ python -m spacy download en_core_web_trf
114
+ ```
115
+ - For German:
116
+ ```bash
117
+ python -m spacy download de_dep_news_trf
118
+ ```
119
+ - For Italian:
120
+ ```bash
121
+ python -m spacy download it_core_news_trf
122
+ ```
123
+ - For Spanish:
124
+ ```bash
125
+ python -m spacy download es_dep_news_trf
126
+ ```
127
+ - For French:
128
+ ```bash
129
+ python -m spacy download fr_dep_news_trf
130
+ ```
131
+
132
+ If you are not using spacy then skip this step
133
+
134
+ ## Usage
135
+
136
+ This tool is primarily designed to extract chapters from Project Gutenberg ePub3 files. It works by analyzing the navigation structure, matching headers, and attempting to identify chapter divisions. Note that it may also include some preamble content, and its accuracy is not guaranteed.
137
+
138
+ To use the tool, run:
139
+ ```bash
140
+ python chapterize.py /path/to/your/epub/files/
141
+ ```
142
+ or
143
+ ```bash
144
+ python chapterize.py
145
+ ```
146
+ which will use the books directory by default
147
+
148
+ ## Notes
149
+
150
+ - The tool is not perfect and may require manual adjustments to the output.
151
+ - It is currently a standalone script but may be packaged in the future.
152
+ - Feel free to fork the repository and modify it as needed.
153
+
154
+ ## Contributing
155
+
156
+ If you encounter any issues, please raise a ticket in the repository. Contributions are welcome!
@@ -0,0 +1,8 @@
1
+ epub_chapterize/__init__.py,sha256=2m7lAQJFn2QxO4taQdBM8hqD7ifpRv9J3MkV4k1AdWI,74
2
+ epub_chapterize/chapterize.py,sha256=LOcdSVzVMhEVB3sFg6zxF978aRYGt1KvXL5GP-yjvlA,14102
3
+ epubchapterize-0.1.0.dist-info/licenses/LICENSE,sha256=LMLma5_BtgDYi_jPdgFCnyyyrDWR9d85t2Wlt7cFGmo,1068
4
+ epubchapterize-0.1.0.dist-info/METADATA,sha256=LW70yZl4LWEzq2685WegVXmWLEkvYrJ93OW_u8roRqk,4710
5
+ epubchapterize-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ epubchapterize-0.1.0.dist-info/entry_points.txt,sha256=QNNVQU0TvFN3Xel-m3CkxDbhPntdTuwbhJNOKtRKNAs,74
7
+ epubchapterize-0.1.0.dist-info/top_level.txt,sha256=rhi6aeSlCEp9HyPXXCwp5DrO9QOQ0YSMSqJeSuhXH5g,16
8
+ epubchapterize-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ epub-chapterize = epub_chapterize.chapterize:parse_epub
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 nzmattgrant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ epub_chapterize