From 3a3f6e34e5fa5f0b3ba48e50b058811a2ee4ec63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Fri, 2 May 2025 17:33:15 +0300 Subject: [PATCH] Better EPUB handling, improvements in code and docs --- CHANGELOG.md | 1 + README.md | 5 +- abogen/VERSION | 2 +- abogen/book_handler.py | 1255 ++++++++++++++++++++++++++-------------- abogen/gui.py | 7 +- demo/abogen.png | Bin 22311 -> 23118 bytes pyproject.toml | 2 +- 7 files changed, 819 insertions(+), 453 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a987142..37a0963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Added profile system to voice mixer, allowing users to create and manage multiple voice profiles. - Improvements in the voice mixer, mostly for organizing controls and enhancing user experience. - Added icons for flags and genders in the GUI, making it easier to identify different options. +- Improved the content and chapter extraction process for EPUB files, ensuring better handling of various structures. - Switched to platformdirs for determining the correct desktop path, instead of using old methods. - Fixed preview voices was not using GPU acceleration, which was causing performance issues. - Improvements in code and documentation. \ No newline at end of file diff --git a/README.md b/README.md index 55abb0c..4ee7998 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Here’s Abogen in action: in this demo, it processes ∼3,000 characters of tex ## `Voice Mixer` -With voice mixer, you can create custom voices by mixing different voice models. You can adjust the weight of each voice and save your custom voice as a profile for future use. The voice mixer allows you to create unique and personalized voices. (Huge thanks to @jborza for making this possible through his contributions in #5) +With voice mixer, you can create custom voices by mixing different voice models. You can adjust the weight of each voice and save your custom voice as a profile for future use. The voice mixer allows you to create unique and personalized voices. (Huge thanks to [@jborza](https://github.com/jborza) for making this possible through his contributions in [#5](https://github.com/denizsafak/abogen/pull/5)) ## `Supported Languages` ``` @@ -168,8 +168,9 @@ Feel free to explore the code and make any changes you like. ## `Credits` - Abogen uses [Kokoro](https://github.com/hexgrad/kokoro) for its high-quality, natural-sounding text-to-speech synthesis. Huge thanks to the Kokoro team for making this possible. - Thanks to [@wojiushixiaobai](https://github.com/wojiushixiaobai) for [Embedded Python](https://github.com/wojiushixiaobai/Python-Embed-Win64) packages. These modified packages include pip pre-installed, enabling Abogen to function as a standalone application without requiring users to separately install Python in Windows. +- Thanks to creators of [EbookLib](https://github.com/aerkalov/ebooklib), a Python library for reading and writing ePub files, which is used for extracting text from ePub files. - Special thanks to the [PyQt](https://www.riverbankcomputing.com/software/pyqt/) team for providing the cross-platform GUI toolkit that powers Abogen's interface. -- Icons: [US](https://icons8.com/icon/aRiu1GGi6Aoe/usa), [Great Britain](https://icons8.com/icon/t3NE3BsOAQwq/great-britain), [Spain](https://icons8.com/icon/ly7tzANRt33n/spain), [France](https://icons8.com/icon/3muzEmi4dpD5/france), [India](https://icons8.com/icon/esGVrxg9VCJ1/india), [Italy](https://icons8.com/icon/PW8KZnP7qXzO/italy), [Japan](https://icons8.com/icon/McQbrq9qaQye/japan), [Brazil](https://icons8.com/icon/zHmH8HpOmM90/brazil), [China](https://icons8.com/icon/Ej50Oe3crXwF/china), [Female](https://icons8.com/icon/uI49hxbpxTkp/female), [Male](https://icons8.com/icon/12351/male) and [Voice Id](https://icons8.com/icon/GskSeVoroQ7u/voice-id), [Person](https://icons8.com/icon/34105/person) icons by [Icons8](https://icons8.com/). +- Icons: [US](https://icons8.com/icon/aRiu1GGi6Aoe/usa), [Great Britain](https://icons8.com/icon/t3NE3BsOAQwq/great-britain), [Spain](https://icons8.com/icon/ly7tzANRt33n/spain), [France](https://icons8.com/icon/3muzEmi4dpD5/france), [India](https://icons8.com/icon/esGVrxg9VCJ1/india), [Italy](https://icons8.com/icon/PW8KZnP7qXzO/italy), [Japan](https://icons8.com/icon/McQbrq9qaQye/japan), [Brazil](https://icons8.com/icon/zHmH8HpOmM90/brazil), [China](https://icons8.com/icon/Ej50Oe3crXwF/china), [Female](https://icons8.com/icon/uI49hxbpxTkp/female), [Male](https://icons8.com/icon/12351/male) and [Voice Id](https://icons8.com/icon/GskSeVoroQ7u/voice-id) icons by [Icons8](https://icons8.com/). ## `License` This project is available under the MIT License - see the [LICENSE](https://github.com/denizsafak/abogen/blob/main/LICENSE) file for details. diff --git a/abogen/VERSION b/abogen/VERSION index e6d5cb8..e4c0d46 100644 --- a/abogen/VERSION +++ b/abogen/VERSION @@ -1 +1 @@ -1.0.2 \ No newline at end of file +1.0.3 \ No newline at end of file diff --git a/abogen/book_handler.py b/abogen/book_handler.py index 3248f3d..fb79b9d 100644 --- a/abogen/book_handler.py +++ b/abogen/book_handler.py @@ -4,7 +4,7 @@ import ebooklib import base64 import fitz # PyMuPDF for PDF support from ebooklib import epub -from bs4 import BeautifulSoup +from bs4 import BeautifulSoup, NavigableString from PyQt5.QtWidgets import ( QDialog, QTreeWidget, @@ -24,6 +24,13 @@ from PyQt5.QtWidgets import ( from PyQt5.QtCore import Qt from utils import clean_text, calculate_text_length import os +import logging # Add logging +import urllib.parse + +# Setup logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) class HandlerDialog(QDialog): @@ -59,7 +66,37 @@ class HandlerDialog(QDialog): self.merge_chapters_at_end = HandlerDialog._merge_chapters_at_end # Load the book based on file type - self.book = epub.read_epub(book_path) if self.file_type == "epub" else None + try: + self.book = epub.read_epub(book_path) if self.file_type == "epub" else None + except KeyError as e: + logging.error( + f"EPUB file is missing a referenced file: {e}. Skipping missing file." + ) + # Try to patch ebooklib to skip missing files (monkey-patch read_file) + import types + + orig_read_file = None + try: + from ebooklib import epub as _epub_module + + reader_class = _epub_module.EpubReader + orig_read_file = reader_class.read_file + + def safe_read_file(self, name): + try: + return orig_read_file(self, name) + except KeyError: + logging.warning( + f"Missing file in EPUB: {name}. Returning empty bytes." + ) + return b"" + + reader_class.read_file = safe_read_file + self.book = epub.read_epub(book_path) + reader_class.read_file = orig_read_file # Restore + except Exception as patch_e: + logging.error(f"Failed to patch ebooklib for missing files: {patch_e}") + raise e self.pdf_doc = fitz.open(book_path) if self.file_type == "pdf" else None # Extract book metadata @@ -129,250 +166,18 @@ class HandlerDialog(QDialog): def _preprocess_content(self): """Pre-process content from the document""" if self.file_type == "epub": - # Always process EPUB content using the anchor-based approach - self._process_epub_content() + try: + self._process_epub_content_nav() # Use the new navigation-based method + except Exception as e: + logging.error( + f"Error processing EPUB with navigation: {e}. Falling back to TOC/spine.", + exc_info=True, + ) + # Fallback to a simpler spine-based processing if nav fails + self._process_epub_content_spine_fallback() else: self._preprocess_pdf_content() - def _process_epub_content(self, split_anchors=True): - """ - Process EPUB content by globally ordering TOC entries and slicing content between them. - Ensures all content between defined TOC start points is captured. - """ - # split_anchors parameter kept for compatibility but always treated as True - book = self.book - - # 1. Cache all document HTML and determine spine order - self.doc_content = {} - # Correctly get hrefs from spine items - spine_docs = [] - for spine_item_tuple in book.spine: - item_id = spine_item_tuple[0] - item = book.get_item_with_id(item_id) - if item: - spine_docs.append(item.get_name()) # Use get_name() for href - else: - print( - f"Warning: Spine item with id '{item_id}' not found in book items." - ) - - doc_order = {href: i for i, href in enumerate(spine_docs)} - - for item in book.get_items_of_type(ebooklib.ITEM_DOCUMENT): - href = item.get_name() - if href in doc_order: # Only process docs in spine - try: - html_content = item.get_content().decode("utf-8", errors="ignore") - self.doc_content[href] = html_content - except Exception: - self.doc_content[href] = "" # Handle decoding errors - - # 2. Get all TOC entries with hrefs and determine their positions - toc_entries_with_pos = [] - - def find_position(doc_href, fragment_id): - if doc_href not in self.doc_content: - return -1 - html_content = self.doc_content[doc_href] - if not fragment_id: # No fragment, position is 0 - return 0 - - # Find position of fragment identifier (id= or name=) - id_match_str = f'id="{fragment_id}"' - name_match_str = f'name="{fragment_id}"' - - id_pos = html_content.find(id_match_str) - name_pos = html_content.find(name_match_str) - - pos = -1 - if id_pos != -1 and name_pos != -1: - pos = min(id_pos, name_pos) - elif id_pos != -1: - pos = id_pos - elif name_pos != -1: - pos = name_pos - else: - return -1 # Anchor not found by simple string search - - # Backtrack to the start of the tag '<' - tag_start_pos = html_content.rfind("<", 0, pos) - return ( - tag_start_pos if tag_start_pos != -1 else 0 - ) # Default to 0 if '<' not found - - def collect_toc_entries(entries): - collected = [] - for entry in entries: - href, title = None, "Unknown" - children = [] - entry_obj = None # Store the original entry object - - if isinstance(entry, ebooklib.epub.Link): - href, title = entry.href, entry.title or entry.href - entry_obj = entry - elif isinstance(entry, tuple) and len(entry) >= 1: - section_or_link = entry[0] - entry_obj = section_or_link - if isinstance(section_or_link, ebooklib.epub.Section): - title = section_or_link.title - href = getattr(section_or_link, "href", None) - elif isinstance(section_or_link, ebooklib.epub.Link): - href, title = ( - section_or_link.href, - section_or_link.title or section_or_link.href, - ) - - if len(entry) > 1 and isinstance(entry[1], list): - children = entry[1] - - if href: - base_href, fragment = ( - href.split("#", 1) if "#" in href else (href, None) - ) - if ( - base_href in doc_order - ): # Only consider entries pointing to spine documents - position = find_position(base_href, fragment) - if position != -1: # Only add if position is valid - collected.append( - { - "href": href, # Use the original href from TOC as the key - "title": title, - "doc_href": base_href, - "position": position, - "doc_order": doc_order[base_href], - } - ) - - if children: - collected.extend(collect_toc_entries(children)) - return collected - - all_toc_entries = collect_toc_entries(self.book.toc) - - # Handle case where book has no TOC or empty TOC - if not all_toc_entries: - # Create a synthetic TOC entry for the first spine document - if spine_docs: - # Process all content as a single chapter - all_content_html = "" - for doc_href in spine_docs: - all_content_html += self.doc_content.get(doc_href, "") - - if all_content_html: - soup = BeautifulSoup(all_content_html, "html.parser") - text = clean_text(soup.get_text()).strip() - - # Use the first spine document as the identifier - first_doc = spine_docs[0] - self.content_texts[first_doc] = text - self.content_lengths[first_doc] = len(text) - - # Create a synthetic TOC entry for tree building - self.book.toc = [(epub.Link(first_doc, "Main Content", first_doc),)] - return - - # 3. Sort TOC entries globally - all_toc_entries.sort(key=lambda x: (x["doc_order"], x["position"])) - - # 4. Slice content between sorted entries - self.content_texts = {} - self.content_lengths = {} - num_entries = len(all_toc_entries) - - for i in range(num_entries): - current_entry = all_toc_entries[i] - current_href = current_entry["href"] - current_doc = current_entry["doc_href"] - current_pos = current_entry["position"] - current_doc_html = self.doc_content.get(current_doc, "") - - start_slice_pos = current_pos - slice_html = "" - - # Find the start of the next TOC entry - next_entry = all_toc_entries[i + 1] if (i + 1) < num_entries else None - - if next_entry: - next_doc = next_entry["doc_href"] - next_pos = next_entry["position"] - - if current_doc == next_doc: - # Next entry is in the same document - slice_html = current_doc_html[start_slice_pos:next_pos] - else: - # Next entry is in a different document - # Take content from current position to end of current document - slice_html = current_doc_html[start_slice_pos:] - # Include content from intermediate documents in the spine - current_doc_index = current_entry["doc_order"] - next_doc_index = next_entry["doc_order"] - for doc_idx in range(current_doc_index + 1, next_doc_index): - intermediate_doc_href = spine_docs[doc_idx] - slice_html += self.doc_content.get(intermediate_doc_href, "") - # Add content from the beginning of the next document up to the next entry's position - next_doc_html = self.doc_content.get(next_doc, "") - slice_html += next_doc_html[:next_pos] - else: - # This is the last TOC entry - # Take content from current position to end of current document - slice_html = current_doc_html[start_slice_pos:] - # Include content from all remaining documents in the spine - current_doc_index = current_entry["doc_order"] - for doc_idx in range(current_doc_index + 1, len(spine_docs)): - intermediate_doc_href = spine_docs[doc_idx] - slice_html += self.doc_content.get(intermediate_doc_href, "") - - # 5. Extract text and store - slice_soup = BeautifulSoup(slice_html, "html.parser") - - # Remove sup and sub tags from the HTML before extracting text - for tag in slice_soup.find_all(["sup", "sub"]): - tag.decompose() - - text = clean_text(slice_soup.get_text()).strip() - self.content_texts[current_href] = text # Store using the original TOC href - self.content_lengths[current_href] = len(text) - - # 6. Handle content BEFORE the first TOC entry - if all_toc_entries: - first_entry = all_toc_entries[0] - first_doc_href = first_entry["doc_href"] - first_pos = first_entry["position"] - first_doc_order = first_entry["doc_order"] - prefix_html = "" - # Include content from documents before the first entry's document - for doc_idx in range(first_doc_order): - intermediate_doc_href = spine_docs[doc_idx] - prefix_html += self.doc_content.get(intermediate_doc_href, "") - # Include content from the start of the first entry's document up to its position - first_doc_html = self.doc_content.get(first_doc_href, "") - prefix_html += first_doc_html[:first_pos] - - if prefix_html.strip(): - prefix_soup = BeautifulSoup(prefix_html, "html.parser") - # Remove sup and sub tags - for tag in prefix_soup.find_all(["sup", "sub"]): - tag.decompose() - prefix_text = clean_text(prefix_soup.get_text()).strip() - - if prefix_text: - # Create a new chapter for content before the first TOC entry - # Use a synthetic href to avoid collision with real TOC entries - prefix_chapter_href = "prefix_content_chapter" - self.content_texts[prefix_chapter_href] = prefix_text - self.content_lengths[prefix_chapter_href] = len(prefix_text) - - # Add a new entry to the TOC for the prefix content - prefix_link = epub.Link( - prefix_chapter_href, "Introduction", prefix_chapter_href - ) - # Insert at beginning of TOC - if isinstance(self.book.toc, list): - self.book.toc.insert(0, (prefix_link,)) - else: - self.book.toc = [(prefix_link,)] + (self.book.toc or []) - def _preprocess_pdf_content(self): """Pre-process all page contents from PDF document""" for page_num in range(len(self.pdf_doc)): @@ -395,16 +200,624 @@ class HandlerDialog(QDialog): self.content_texts[page_id] = text self.content_lengths[page_id] = calculate_text_length(text) - def _build_tree(self): - """Build tree based on file type""" - if self.file_type == "epub": - self._build_epub_tree() - else: - self._build_pdf_tree() + def _process_epub_content_spine_fallback(self): + """Fallback EPUB processing based purely on spine order.""" + logging.info("Using spine fallback for EPUB processing.") + self.doc_content = {} + spine_docs = [] + for spine_item_tuple in self.book.spine: + item_id = spine_item_tuple[0] + item = self.book.get_item_with_id(item_id) + if item: + spine_docs.append(item.get_name()) + else: + logging.warning(f"Spine item with id '{item_id}' not found.") - def _build_epub_tree(self): - """Build the tree for EPUB files from TOC""" + # Cache content + for item in self.book.get_items_of_type(ebooklib.ITEM_DOCUMENT): + href = item.get_name() + if href in spine_docs: + try: + html_content = item.get_content().decode("utf-8", errors="ignore") + self.doc_content[href] = html_content + except Exception as e: + logging.error(f"Error decoding content for {href}: {e}") + self.doc_content[href] = "" + + # Create a simple TOC based on spine order + synthetic_toc = [] + self.content_texts = {} + self.content_lengths = {} + for i, doc_href in enumerate(spine_docs): + html_content = self.doc_content.get(doc_href, "") + if html_content: + soup = BeautifulSoup(html_content, "html.parser") + # Remove sup and sub tags + for tag in soup.find_all(["sup", "sub"]): + tag.decompose() + text = clean_text(soup.get_text()).strip() + if text: + # Use doc_href as the identifier + self.content_texts[doc_href] = text + self.content_lengths[doc_href] = len(text) + # Create a synthetic TOC entry + title = f"Chapter {i+1}: {doc_href}" + # Try to get a better title from

or + h1 = soup.find("h1") + if h1 and h1.get_text(strip=True): + title = h1.get_text(strip=True) + else: + title_tag = soup.find("title") + if title_tag and title_tag.get_text(strip=True): + title = title_tag.get_text(strip=True) + + synthetic_toc.append( + (epub.Link(doc_href, title, doc_href), []) + ) # Wrap in tuple and empty list for compatibility + + # Replace book.toc with the synthetic one if it was empty or fallback was triggered + if not self.book.toc or not hasattr( + self, "processed_nav_structure" + ): # Check if nav processing failed + self.book.toc = synthetic_toc + logging.info(f"Generated synthetic TOC with {len(synthetic_toc)} entries.") + + def _process_epub_content_nav(self): + """ + Process EPUB content using ITEM_NAVIGATION (NAV HTML) or ITEM_NCX. + Globally orders navigation entries and slices content between them. + """ + logging.info( + "Attempting to process EPUB using navigation document (NAV/NCX)..." + ) + nav_item = None + nav_type = None + + # 1. Check ITEM_NAVIGATION for actual NAV HTML (.xhtml/.html) + nav_items = list(self.book.get_items_of_type(ebooklib.ITEM_NAVIGATION)) + if nav_items: + # Prefer files explicitly named 'nav.xhtml' or similar + preferred_nav = next( + ( + item + for item in nav_items + if "nav" in item.get_name().lower() + and item.get_name().lower().endswith((".xhtml", ".html")) + ), + None, + ) + if preferred_nav: + nav_item = preferred_nav + nav_type = "html" + logging.info(f"Found preferred NAV HTML item: {nav_item.get_name()}") + else: + # Check if any ITEM_NAVIGATION is actually HTML + html_nav = next( + ( + item + for item in nav_items + if item.get_name().lower().endswith((".xhtml", ".html")) + ), + None, + ) + if html_nav: + nav_item = html_nav + nav_type = "html" + logging.info( + f"Found NAV HTML item in ITEM_NAVIGATION: {html_nav.get_name()}" + ) + + # 2. If no NAV HTML found via ITEM_NAVIGATION, check if ITEM_NAVIGATION points to NCX + if not nav_item and nav_items: + ncx_in_nav = next( + ( + item + for item in nav_items + if item.get_name().lower().endswith(".ncx") + ), + None, + ) + if ncx_in_nav: + nav_item = ncx_in_nav + nav_type = "ncx" + logging.info( + f"Found NCX item via ITEM_NAVIGATION: {ncx_in_nav.get_name()}" + ) + + # 3. If still no nav_item, check for ITEM_NCX directly + if not nav_item: + ncx_items = list(self.book.get_items_of_type(ebooklib.ITEM_NCX)) + if ncx_items: + nav_item = ncx_items[0] # Take the first one + nav_type = "ncx" + logging.info(f"Found NCX item via ITEM_NCX: {ncx_items[0].get_name()}") + + # 4. If no navigation item found by any method, trigger fallback + if not nav_item or not nav_type: + logging.warning( + "No suitable EPUB navigation document (NAV HTML or NCX) found. Falling back." + ) + raise ValueError("No navigation document found") # Trigger fallback + + # Determine parser based on the confirmed nav_type + parser_type = "html.parser" if nav_type == "html" else "xml" + logging.info(f"Using parser: '{parser_type}' for {nav_item.get_name()}") + try: + nav_content = nav_item.get_content().decode("utf-8", errors="ignore") + nav_soup = BeautifulSoup(nav_content, parser_type) + except Exception as e: + logging.error( + f"Failed to parse navigation content ({nav_item.get_name()}) using {parser_type}: {e}", + exc_info=True, + ) + raise ValueError( + f"Failed to parse navigation content: {e}" + ) # Trigger fallback + + # --- Rest of the processing logic --- + # 1. Cache all document HTML and determine spine order (no changes needed here) + self.doc_content = {} + spine_docs = [] + for spine_item_tuple in self.book.spine: + item_id = spine_item_tuple[0] + item = self.book.get_item_with_id(item_id) + if item: + spine_docs.append(item.get_name()) + else: + logging.warning(f"Spine item with id '{item_id}' not found.") + doc_order = {href: i for i, href in enumerate(spine_docs)} + # Add a mapping for unquoted (decoded) hrefs as well + doc_order_decoded = { + urllib.parse.unquote(href): i for href, i in doc_order.items() + } + + # Clear previous content/lengths before processing + self.content_texts = {} + self.content_lengths = {} + + for item in self.book.get_items_of_type(ebooklib.ITEM_DOCUMENT): + href = item.get_name() + if href in doc_order or any( + href in nav_point.get("src", "") + for nav_point in nav_soup.find_all(["content", "a"]) + ): + try: + html_content = item.get_content().decode("utf-8", errors="ignore") + self.doc_content[href] = html_content + except Exception as e: + logging.error(f"Error decoding content for {href}: {e}") + self.doc_content[href] = "" + + # 2. Extract and order navigation entries globally + ordered_nav_entries = [] + + # Define find_position locally or ensure self._find_position_robust is used correctly + # Using self._find_position_robust is preferred as it's a method of the class + find_position_func = self._find_position_robust + + # Store the parsed structure for tree building later + self.processed_nav_structure = [] + + # Call the correct parsing function based on confirmed nav_type + parse_successful = False + if nav_type == "ncx": + nav_map = nav_soup.find("navMap") + if nav_map: + logging.info("Parsing NCX <navMap>...") + for nav_point in nav_map.find_all("navPoint", recursive=False): + self._parse_ncx_navpoint( + nav_point, + ordered_nav_entries, + doc_order, + doc_order_decoded, + self.processed_nav_structure, + find_position_func, + ) + parse_successful = bool( + ordered_nav_entries + ) # Success if entries were added + else: + logging.warning("Could not find <navMap> in NCX file.") + elif nav_type == "html": + logging.info("Parsing NAV HTML...") + toc_nav = nav_soup.find("nav", attrs={"epub:type": "toc"}) + if not toc_nav: + # Fallback: look for any <nav> element containing an <ol> + all_navs = nav_soup.find_all("nav") + for nav in all_navs: + if nav.find("ol"): + toc_nav = nav + logging.info("Found fallback TOC structure in <nav> with <ol>.") + break + if toc_nav: + top_ol = toc_nav.find("ol", recursive=False) + if top_ol: + for li in top_ol.find_all("li", recursive=False): + self._parse_html_nav_li( + li, + ordered_nav_entries, + doc_order, + doc_order_decoded, + self.processed_nav_structure, + find_position_func, + ) + parse_successful = bool( + ordered_nav_entries + ) # Success if entries were added + else: + logging.warning("Found <nav> for TOC but no top-level <ol> inside.") + else: + logging.warning( + "Could not find TOC structure (<nav epub:type='toc'> or <nav><ol>) in NAV HTML." + ) + + # Handle case where parsing ran but found no valid entries OR parsing failed + if not parse_successful: + logging.warning( + "Navigation parsing completed but found no valid entries, or parsing failed. Falling back." + ) + raise ValueError("No valid navigation entries found after parsing") + + # Sort entries globally by document order and position within the document + ordered_nav_entries.sort(key=lambda x: (x["doc_order"], x["position"])) + logging.info(f"Sorted {len(ordered_nav_entries)} navigation entries.") + + # 3. Slice content ONLY between sorted TOC entries + num_entries = len(ordered_nav_entries) + for i in range(num_entries): + current_entry = ordered_nav_entries[i] + current_src = current_entry["src"] + current_doc = current_entry["doc_href"] + current_pos = current_entry["position"] + current_doc_html = self.doc_content.get(current_doc, "") + + start_slice_pos = current_pos + slice_html = "" + + next_entry = ordered_nav_entries[i + 1] if (i + 1) < num_entries else None + + if next_entry: + next_doc = next_entry["doc_href"] + next_pos = next_entry["position"] + + # Always include all content from current position to next position, even if next_doc is before current_doc + if current_doc == next_doc: + slice_html = current_doc_html[start_slice_pos:next_pos] + else: + # Collect all content from current_doc (from start_slice_pos to end), + # then all intermediate docs (in spine order), + # then up to next_pos in next_doc (even if next_doc is before current_doc in spine) + slice_html = current_doc_html[start_slice_pos:] + docs_between = [] + try: + idx_current = spine_docs.index(current_doc) + idx_next = spine_docs.index(next_doc) + if idx_current < idx_next: + for doc_idx in range(idx_current + 1, idx_next): + docs_between.append(spine_docs[doc_idx]) + elif idx_current > idx_next: + for doc_idx in range(idx_current + 1, len(spine_docs)): + docs_between.append(spine_docs[doc_idx]) + for doc_idx in range(0, idx_next): + docs_between.append(spine_docs[doc_idx]) + except Exception: + pass + for doc_href in docs_between: + slice_html += self.doc_content.get(doc_href, "") + next_doc_html = self.doc_content.get(next_doc, "") + slice_html += next_doc_html[:next_pos] + else: + # Last TOC entry: include all content from current position to end of book + slice_html = current_doc_html[start_slice_pos:] + try: + idx_current = spine_docs.index(current_doc) + for doc_idx in range(idx_current + 1, len(spine_docs)): + intermediate_doc_href = spine_docs[doc_idx] + slice_html += self.doc_content.get(intermediate_doc_href, "") + except Exception: + pass + + if slice_html.strip(): + slice_soup = BeautifulSoup(slice_html, "html.parser") + # Add double newlines after <p> and <div> tags + for tag in slice_soup.find_all(["p", "div"]): + tag.append("\n\n") + for tag in slice_soup.find_all(["sup", "sub"]): + tag.decompose() + text = clean_text(slice_soup.get_text()).strip() + if text: + self.content_texts[current_src] = text + self.content_lengths[current_src] = len(text) + else: + self.content_texts[current_src] = "" + self.content_lengths[current_src] = 0 + else: + self.content_texts[current_src] = "" + self.content_lengths[current_src] = 0 + + # 4. Extract text and store using the original TOC entry src as the key + if ordered_nav_entries: + first_entry = ordered_nav_entries[0] + first_doc_href = first_entry["doc_href"] + first_pos = first_entry["position"] + first_doc_order = first_entry["doc_order"] + prefix_html = "" + + for doc_idx in range(first_doc_order): + if doc_idx < len(spine_docs): + intermediate_doc_href = spine_docs[doc_idx] + prefix_html += self.doc_content.get(intermediate_doc_href, "") + else: + logging.warning( + f"Document index {doc_idx} out of bounds for spine (length {len(spine_docs)})." + ) + + first_doc_html = self.doc_content.get(first_doc_href, "") + prefix_html += first_doc_html[:first_pos] + + if prefix_html.strip(): + prefix_soup = BeautifulSoup(prefix_html, "html.parser") + for tag in prefix_soup.find_all(["sup", "sub"]): + tag.decompose() + prefix_text = clean_text(prefix_soup.get_text()).strip() + + if prefix_text: + prefix_chapter_src = "internal:prefix_content" + self.content_texts[prefix_chapter_src] = prefix_text + self.content_lengths[prefix_chapter_src] = len(prefix_text) + self.processed_nav_structure.insert( + 0, + { + "src": prefix_chapter_src, + "title": "Introduction", + "children": [], + }, + ) + logging.info( + f"Added prefix content chapter '{prefix_chapter_src}'." + ) + + logging.info( + f"Finished processing EPUB navigation. Found {len(self.content_texts)} content sections linked to TOC." + ) + + def _parse_ncx_navpoint( + self, + nav_point, + ordered_entries, + doc_order, + doc_order_decoded, + tree_structure_list, + find_position_func, + ): + nav_label = nav_point.find("navLabel") + content = nav_point.find("content") + title = ( + nav_label.find("text").get_text(strip=True) + if nav_label and nav_label.find("text") + else "Untitled Section" + ) + src = content["src"] if content and "src" in content.attrs else None + + current_entry_node = {"title": title, "src": src, "children": []} + + if src: + base_href, fragment = src.split("#", 1) if "#" in src else (src, None) + # Try both original and decoded hrefs + doc_key = None + if base_href in doc_order: + doc_key = base_href + doc_idx = doc_order[base_href] + elif urllib.parse.unquote(base_href) in doc_order: + doc_key = urllib.parse.unquote(base_href) + doc_idx = doc_order[doc_key] + elif base_href in doc_order_decoded: + doc_key = base_href + doc_idx = doc_order_decoded[base_href] + elif urllib.parse.unquote(base_href) in doc_order_decoded: + doc_key = urllib.parse.unquote(base_href) + doc_idx = doc_order_decoded[doc_key] + else: + logging.warning( + f"Navigation entry '{title}' points to '{base_href}', which is not in the spine or document list." + ) + current_entry_node["has_content"] = False + doc_key = None + if doc_key is not None: + position = find_position_func(doc_key, fragment) + entry_data = { + "src": src, + "title": title, + "doc_href": doc_key, + "position": position, + "doc_order": doc_idx, + } + ordered_entries.append(entry_data) + current_entry_node["has_content"] = True + else: + logging.warning(f"Navigation entry '{title}' has no 'src' attribute.") + current_entry_node["has_content"] = False + + child_navpoints = nav_point.find_all("navPoint", recursive=False) + if child_navpoints: + for child_np in child_navpoints: + # Pass find_position_func down recursively + self._parse_ncx_navpoint( + child_np, + ordered_entries, + doc_order, + doc_order_decoded, + current_entry_node["children"], + find_position_func, + ) + + if title and ( + current_entry_node.get("has_content", False) + or current_entry_node["children"] + ): + tree_structure_list.append(current_entry_node) + + def _parse_html_nav_li( + self, + li_element, + ordered_entries, + doc_order, + doc_order_decoded, + tree_structure_list, + find_position_func, + ): + link = li_element.find("a", recursive=False) + span_text = li_element.find("span", recursive=False) + title = "Untitled Section" + src = None + current_entry_node = {"children": []} + + if link and "href" in link.attrs: + src = link["href"] + title = link.get_text(strip=True) or title + if not title.strip() and span_text: + title = span_text.get_text(strip=True) or title + if not title.strip(): + li_text = "".join( + t for t in li_element.contents if isinstance(t, NavigableString) + ).strip() + title = li_text or title + elif span_text: + title = span_text.get_text(strip=True) or title + if not title.strip(): + li_text = "".join( + t for t in li_element.contents if isinstance(t, NavigableString) + ).strip() + title = li_text or title + else: + li_text = "".join( + t for t in li_element.contents if isinstance(t, NavigableString) + ).strip() + title = li_text or title + + current_entry_node["title"] = title + current_entry_node["src"] = src + + if src: + base_href, fragment = src.split("#", 1) if "#" in src else (src, None) + # Try both original and decoded hrefs + doc_key = None + if base_href in doc_order: + doc_key = base_href + doc_idx = doc_order[base_href] + elif urllib.parse.unquote(base_href) in doc_order: + doc_key = urllib.parse.unquote(base_href) + doc_idx = doc_order[doc_key] + elif base_href in doc_order_decoded: + doc_key = base_href + doc_idx = doc_order_decoded[base_href] + elif urllib.parse.unquote(base_href) in doc_order_decoded: + doc_key = urllib.parse.unquote(base_href) + doc_idx = doc_order_decoded[doc_key] + else: + logging.warning( + f"Navigation entry '{title}' points to '{base_href}', which is not in the spine or document list." + ) + current_entry_node["has_content"] = False + doc_key = None + if doc_key is not None: + position = find_position_func(doc_key, fragment) + entry_data = { + "src": src, + "title": title, + "doc_href": doc_key, + "position": position, + "doc_order": doc_idx, + } + ordered_entries.append(entry_data) + current_entry_node["has_content"] = True + else: + current_entry_node["has_content"] = False + + child_ol = li_element.find("ol", recursive=False) + if child_ol: + for child_li in child_ol.find_all("li", recursive=False): + # Pass find_position_func down recursively + self._parse_html_nav_li( + child_li, + ordered_entries, + doc_order, + doc_order_decoded, + current_entry_node["children"], + find_position_func, + ) + + if title and ( + current_entry_node.get("has_content", False) + or current_entry_node["children"] + ): + tree_structure_list.append(current_entry_node) + + def _find_position_robust(self, doc_href, fragment_id): + if doc_href not in self.doc_content: + logging.warning(f"Document '{doc_href}' not found in cached content.") + return 0 + html_content = self.doc_content[doc_href] + if not fragment_id: + return 0 + + try: + temp_soup = BeautifulSoup(f"<div>{html_content}</div>", "html.parser") + target_element = temp_soup.find(id=fragment_id) + if target_element: + tag_str = str(target_element) + pos = html_content.find(tag_str[: min(len(tag_str), 200)]) + if pos != -1: + logging.debug( + f"Found position for id='{fragment_id}' in {doc_href} using BeautifulSoup: {pos}" + ) + return pos + except Exception as e: + logging.warning( + f"BeautifulSoup failed to find id='{fragment_id}' in {doc_href}: {e}" + ) + + safe_fragment_id = re.escape(fragment_id) + id_name_pattern = re.compile( + f"<[^>]+(?:id|name)\\s*=\\s*[\"']{safe_fragment_id}[\"']", re.IGNORECASE + ) + match = id_name_pattern.search(html_content) + if match: + pos = match.start() + logging.debug( + f"Found position for id/name='{fragment_id}' in {doc_href} using regex: {pos}" + ) + return pos + + id_match_str = f'id="{fragment_id}"' + name_match_str = f'name="{fragment_id}"' + id_pos = html_content.find(id_match_str) + name_pos = html_content.find(name_match_str) + + pos = -1 + if id_pos != -1 and name_pos != -1: + pos = min(id_pos, name_pos) + elif id_pos != -1: + pos = id_pos + elif name_pos != -1: + pos = name_pos + + if pos != -1: + tag_start_pos = html_content.rfind("<", 0, pos) + final_pos = tag_start_pos if tag_start_pos != -1 else 0 + logging.debug( + f"Found position for id/name='{fragment_id}' in {doc_href} using string search: {final_pos}" + ) + return final_pos + + logging.warning( + f"Anchor '{fragment_id}' not found in {doc_href}. Defaulting to position 0." + ) + return 0 + + def _build_tree(self): self.treeWidget.clear() + info_item = QTreeWidgetItem(self.treeWidget, ["Information"]) info_item.setData(0, Qt.UserRole, "info:bookinfo") info_item.setFlags(info_item.flags() & ~Qt.ItemIsUserCheckable) @@ -412,76 +825,129 @@ class HandlerDialog(QDialog): font.setBold(True) info_item.setFont(0, font) - # Regular tree building - def build_tree(toc_entries, parent_item): - for entry in toc_entries: - href, title, children = None, "Unknown", [] - if isinstance(entry, ebooklib.epub.Link): - href, title = entry.href, entry.title or entry.href - elif isinstance(entry, tuple) and len(entry) >= 1: - section_or_link = entry[0] - if isinstance(section_or_link, ebooklib.epub.Section): - title = section_or_link.title - href = getattr(section_or_link, "href", None) - elif isinstance(section_or_link, ebooklib.epub.Link): - href, title = ( - section_or_link.href, - section_or_link.title or section_or_link.href, - ) - if len(entry) > 1 and isinstance(entry[1], list): - children = entry[1] - else: - continue - - # Create tree item - item = QTreeWidgetItem(parent_item, [title]) - item.setData(0, Qt.UserRole, href) - - # Make item checkable if it has content - has_content = ( - href - and href in self.content_texts - and self.content_texts[href].strip() + if self.file_type == "epub": + if ( + hasattr(self, "processed_nav_structure") + and self.processed_nav_structure + ): + self._build_epub_tree_from_nav( + self.processed_nav_structure, self.treeWidget ) - if has_content or children: - item.setFlags(item.flags() | Qt.ItemIsUserCheckable) - is_checked = href and href in self.checked_chapters - item.setCheckState(0, Qt.Checked if is_checked else Qt.Unchecked) + else: + logging.warning("Building EPUB tree using fallback book.toc.") + self._build_epub_tree_fallback(self.book.toc, self.treeWidget) + else: + self._build_pdf_tree() + + has_parents = False + iterator = QTreeWidgetItemIterator( + self.treeWidget, QTreeWidgetItemIterator.HasChildren + ) + if iterator.value(): + has_parents = True + self.treeWidget.setRootIsDecorated(has_parents) + + def _build_epub_tree_from_nav( + self, nav_nodes, parent_item, seen_content_hashes=None + ): + if seen_content_hashes is None: + seen_content_hashes = set() + for node in nav_nodes: + title = node.get("title", "Unknown") + src = node.get("src") + children = node.get("children", []) + + item = QTreeWidgetItem(parent_item, [title]) + item.setData(0, Qt.UserRole, src) + + is_empty = ( + src + and (src in self.content_texts) + and (not self.content_texts[src].strip()) + ) + is_duplicate = False + if src and src in self.content_texts and self.content_texts[src].strip(): + content_hash = hash(self.content_texts[src]) + if content_hash in seen_content_hashes: + is_duplicate = True else: - item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) + seen_content_hashes.add(content_hash) - # Process children - if children: - build_tree(children, item) + if src and not is_empty and not is_duplicate: + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + is_checked = src in self.checked_chapters + item.setCheckState(0, Qt.Checked if is_checked else Qt.Unchecked) + elif is_duplicate: + # Mark as duplicate and remove checkbox + item.setText(0, f"{title} (Duplicate)") + item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) + elif children: + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setCheckState(0, Qt.Unchecked) + else: + item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) - build_tree(self.book.toc, self.treeWidget) + if children: + self._build_epub_tree_from_nav(children, item, seen_content_hashes) + + def _build_epub_tree_fallback(self, toc_entries, parent_item): + for entry in toc_entries: + href, title, children = None, "Unknown", [] + entry_obj = None + if isinstance(entry, ebooklib.epub.Link): + href, title = entry.href, entry.title or entry.href + entry_obj = entry + elif isinstance(entry, tuple) and len(entry) >= 1: + section_or_link = entry[0] + entry_obj = section_or_link + if isinstance(section_or_link, ebooklib.epub.Section): + title = section_or_link.title + href = getattr(section_or_link, "href", None) + elif isinstance(section_or_link, ebooklib.epub.Link): + href, title = ( + section_or_link.href, + section_or_link.title or section_or_link.href, + ) + + if len(entry) > 1 and isinstance(entry[1], list): + children = entry[1] + else: + continue + + item = QTreeWidgetItem(parent_item, [title]) + item.setData(0, Qt.UserRole, href) + + has_content = ( + href and href in self.content_texts and self.content_texts[href].strip() + ) + + if has_content or children: + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + is_checked = href and href in self.checked_chapters + item.setCheckState(0, Qt.Checked if is_checked else Qt.Unchecked) + else: + item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) + + if children: + self._build_epub_tree_fallback(children, item) def _build_pdf_tree(self): - """Build the tree for PDF files combining outline/bookmarks with pages""" - # Get outline and store if this PDF has bookmarks outline = self.pdf_doc.get_toc() self.has_pdf_bookmarks = bool(outline) if not outline: - # No bookmarks/outline available, create a simple page list self._build_pdf_pages_tree() return - # Process the outline to determine page ranges bookmark_pages = [] page_to_bookmark = {} next_page_boundaries = {} - # Track added pages to prevent duplicates added_pages = set() - # Extract page numbers from outline recursively def extract_page_numbers(entries): for entry in entries: - if ( - len(entry) >= 3 - ): # Valid outline entry has at least level, title, page + if len(entry) >= 3: _, title, page = entry[:3] - # Convert page reference to actual page number (0-based) page_num = ( page - 1 if isinstance(page, int) @@ -489,27 +955,21 @@ class HandlerDialog(QDialog): ) bookmark_pages.append((page_num, title)) - # Process children recursively if len(entry) > 3 and isinstance(entry[3], list): extract_page_numbers(entry[3]) extract_page_numbers(outline) bookmark_pages.sort() - # Determine page ranges for each bookmark for i, (page_num, title) in enumerate(bookmark_pages): if i < len(bookmark_pages) - 1: next_page_boundaries[page_num] = bookmark_pages[i + 1][0] page_to_bookmark[page_num] = title - # Helper function to build the tree structure recursively def build_outline_tree(entries, parent_item): for entry in entries: - if ( - len(entry) >= 3 - ): # Valid outline entry has at least level, title, page + if len(entry) >= 3: entry_level, title, page = entry[:3] - # Get actual page number (0-based) page_num = ( page - 1 if isinstance(page, int) @@ -517,7 +977,6 @@ class HandlerDialog(QDialog): ) page_id = f"page_{page_num+1}" - # Create bookmark item bookmark_item = QTreeWidgetItem(parent_item, [title]) bookmark_item.setData(0, Qt.UserRole, page_id) bookmark_item.setFlags( @@ -532,15 +991,10 @@ class HandlerDialog(QDialog): ), ) - # Mark this page as added added_pages.add(page_num) - # Add child pages that belong to this bookmark next_page = next_page_boundaries.get(page_num, len(self.pdf_doc)) - for sub_page_num in range( - page_num + 1, next_page - ): # Skip the bookmark page itself - # Skip if this page is a bookmark itself or already added as a child elsewhere + for sub_page_num in range(page_num + 1, next_page): if ( sub_page_num in page_to_bookmark or sub_page_num in added_pages @@ -550,7 +1004,6 @@ class HandlerDialog(QDialog): page_id = f"page_{sub_page_num+1}" page_title = f"Page {sub_page_num+1}" - # Try to get a better title from the first line of content page_text = self.content_texts.get(page_id, "").strip() if page_text: first_line = page_text.split("\n", 1)[0].strip() @@ -569,22 +1022,15 @@ class HandlerDialog(QDialog): ), ) - # Mark this page as added added_pages.add(sub_page_num) - # Process child bookmarks if any if len(entry) > 3 and isinstance(entry[3], list): build_outline_tree(entry[3], bookmark_item) - # Start building the tree from the outline build_outline_tree(outline, self.treeWidget) - # Add pages not covered by bookmarks - covered_pages = set( - added_pages - ) # Use our tracked pages to find uncategorized ones + covered_pages = set(added_pages) - # Add remaining pages as top-level items under "Other Pages" uncategorized_pages = [ i for i in range(len(self.pdf_doc)) if i not in covered_pages ] @@ -592,7 +1038,6 @@ class HandlerDialog(QDialog): self._add_other_pages(uncategorized_pages) def _build_pdf_pages_tree(self): - """Build a simple page list for PDFs without bookmarks""" pages_item = QTreeWidgetItem(self.treeWidget, ["Pages"]) pages_item.setFlags(pages_item.flags() & ~Qt.ItemIsUserCheckable) font = pages_item.font(0) @@ -603,7 +1048,6 @@ class HandlerDialog(QDialog): page_id = f"page_{page_num+1}" page_title = f"Page {page_num+1}" - # Try to get a better title from the first line of content page_text = self.content_texts.get(page_id, "").strip() if page_text: first_line = page_text.split("\n", 1)[0].strip() @@ -618,7 +1062,6 @@ class HandlerDialog(QDialog): ) def _add_other_pages(self, uncategorized_pages): - """Add uncategorized pages to the tree""" other_pages = QTreeWidgetItem(self.treeWidget, ["Other Pages"]) other_pages.setFlags(other_pages.flags() & ~Qt.ItemIsUserCheckable) font = other_pages.font(0) @@ -629,7 +1072,6 @@ class HandlerDialog(QDialog): page_id = f"page_{page_num+1}" page_title = f"Page {page_num+1}" - # Try to get better title from first line page_text = self.content_texts.get(page_id, "").strip() if page_text: first_line = page_text.split("\n", 1)[0].strip() @@ -644,11 +1086,9 @@ class HandlerDialog(QDialog): ) def _are_provided_checks_relevant(self): - """Check if provided checks are relevant to this book""" if not self.checked_chapters: return False - # Collect all identifiers present in tree all_identifiers = set() iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): @@ -659,18 +1099,14 @@ class HandlerDialog(QDialog): all_identifiers.add(identifier) iterator += 1 - # Check for any intersection with provided chapters return bool(self.checked_chapters.intersection(all_identifiers)) def _setup_ui(self): - """Set up the user interface""" - # Add preview panel self.previewEdit = QTextEdit(self) self.previewEdit.setReadOnly(True) self.previewEdit.setMinimumWidth(300) self.previewEdit.setStyleSheet("QTextEdit { border: none; }") - # Create informative text label below preview self.previewInfoLabel = QLabel( '*Note: You can modify the content later using the "Edit" button in the input box or by accessing the temporary files directory through settings.', self, @@ -680,7 +1116,6 @@ class HandlerDialog(QDialog): "QLabel { color: #666; font-style: italic; }" ) - # Right panel layout (preview and info label) previewLayout = QVBoxLayout() previewLayout.setContentsMargins(0, 0, 0, 0) previewLayout.addWidget(self.previewEdit, 1) @@ -689,30 +1124,24 @@ class HandlerDialog(QDialog): rightWidget = QWidget() rightWidget.setLayout(previewLayout) - # Dialog buttons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) - # Selection buttons item_type = "chapters" if self.file_type == "epub" else "pages" - # Auto-select button self.auto_select_btn = QPushButton(f"Auto-select {item_type}", self) self.auto_select_btn.clicked.connect(self.auto_select_chapters) self.auto_select_btn.setToolTip(f"Automatically select main {item_type}") - # Selection buttons layout buttons_layout = QVBoxLayout() buttons_layout.setContentsMargins(0, 0, 0, 0) buttons_layout.setSpacing(10) - # Row 1: Auto-select auto_select_layout = QHBoxLayout() auto_select_layout.addWidget(self.auto_select_btn) buttons_layout.addLayout(auto_select_layout) - # Row 2: Select/Deselect All select_layout = QHBoxLayout() self.select_all_btn = QPushButton("Select all", self) self.select_all_btn.clicked.connect(self.select_all_chapters) @@ -722,7 +1151,6 @@ class HandlerDialog(QDialog): select_layout.addWidget(self.deselect_all_btn) buttons_layout.addLayout(select_layout) - # Row 3: Parent selection parent_layout = QHBoxLayout() self.select_parents_btn = QPushButton("Select parents", self) self.select_parents_btn.clicked.connect(self.select_parent_chapters) @@ -732,7 +1160,6 @@ class HandlerDialog(QDialog): parent_layout.addWidget(self.deselect_parents_btn) buttons_layout.addLayout(parent_layout) - # Row 4: Expand/Collapse expand_layout = QHBoxLayout() self.expand_all_btn = QPushButton("Expand All", self) self.expand_all_btn.clicked.connect(self.treeWidget.expandAll) @@ -742,13 +1169,11 @@ class HandlerDialog(QDialog): expand_layout.addWidget(self.collapse_all_btn) buttons_layout.addLayout(expand_layout) - # Left panel layout leftLayout = QVBoxLayout() leftLayout.setContentsMargins(0, 0, 5, 0) leftLayout.addLayout(buttons_layout) leftLayout.addWidget(self.treeWidget) - # Save options checkboxes checkbox_text = ( "Save each chapter separately" if self.file_type == "epub" @@ -770,33 +1195,25 @@ class HandlerDialog(QDialog): leftLayout.addWidget(buttons) - # Create left panel widget leftWidget = QWidget() leftWidget.setLayout(leftLayout) - # Create splitter for left panel and preview self.splitter = QSplitter(Qt.Horizontal) self.splitter.addWidget(leftWidget) - self.splitter.addWidget( - rightWidget - ) # Now using rightWidget that includes preview and label + self.splitter.addWidget(rightWidget) self.splitter.setSizes([280, 420]) - # Set main layout mainLayout = QVBoxLayout(self) mainLayout.addWidget(self.splitter) self.setLayout(mainLayout) def _update_checkbox_states(self): - """Update checkboxes enabled states based on document type and selection""" - # Make sure checkboxes exist before trying to modify them if ( not hasattr(self, "save_chapters_checkbox") or not self.save_chapters_checkbox ): return - # For PDFs without bookmarks, always disable separate chapters option if ( self.file_type == "pdf" and hasattr(self, "has_pdf_bookmarks") @@ -806,11 +1223,9 @@ class HandlerDialog(QDialog): self.merge_chapters_checkbox.setEnabled(False) return - # Count checked items differently based on file type checked_count = 0 if self.file_type == "epub": - # For EPUB: Count all checked items iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() @@ -823,9 +1238,7 @@ class HandlerDialog(QDialog): break iterator += 1 - else: # PDF - # For PDF: Count distinct parent groups - # We need content from at least 2 different parents to enable "save separately" + else: parent_groups = set() iterator = QTreeWidgetItemIterator(self.treeWidget) @@ -835,30 +1248,24 @@ class HandlerDialog(QDialog): item.flags() & Qt.ItemIsUserCheckable and item.checkState(0) == Qt.Checked ): - # Get the parent (or the item itself if it's a top-level item) parent = item.parent() if parent and parent != self.treeWidget.invisibleRootItem(): - # Use memory address as a unique identifier since QTreeWidgetItem is not hashable parent_groups.add(id(parent)) else: - # Top-level items count as their own parent group parent_groups.add(id(item)) iterator += 1 checked_count = len(parent_groups) - # Enable save separately only if enough distinct groups are checked min_groups_required = 2 self.save_chapters_checkbox.setEnabled(checked_count >= min_groups_required) - # Enable merge only if save separately is enabled and checked self.merge_chapters_checkbox.setEnabled( self.save_chapters_checkbox.isEnabled() and self.save_chapters_checkbox.isChecked() ) def select_all_chapters(self): - """Select all chapters/pages""" self._block_signals = True iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): @@ -870,7 +1277,6 @@ class HandlerDialog(QDialog): self._update_checked_set_from_tree() def deselect_all_chapters(self): - """Deselect all chapters/pages""" self._block_signals = True iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): @@ -882,7 +1288,6 @@ class HandlerDialog(QDialog): self._update_checked_set_from_tree() def select_parent_chapters(self): - """Select only parent chapters/sections""" self._block_signals = True iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): @@ -894,7 +1299,6 @@ class HandlerDialog(QDialog): self._update_checked_set_from_tree() def deselect_parent_chapters(self): - """Deselect only parent chapters/sections""" self._block_signals = True iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): @@ -906,23 +1310,20 @@ class HandlerDialog(QDialog): self._update_checked_set_from_tree() def auto_select_chapters(self): - """Auto-select chapters/pages""" self._run_auto_check() def _run_auto_check(self): - """Run automatic content selection based on file type""" self._block_signals = True if self.file_type == "epub": self._run_epub_auto_check() - else: # PDF + else: self._run_pdf_auto_check() self._block_signals = False self._update_checked_set_from_tree() def _run_epub_auto_check(self): - """Auto-check logic for EPUB files""" iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() @@ -930,25 +1331,30 @@ class HandlerDialog(QDialog): iterator += 1 continue - href = item.data(0, Qt.UserRole) - lookup_href = href.split("#")[0] if href else None + src = item.data(0, Qt.UserRole) - # Check based on length (> 1000 chars) and parent status - if ( - lookup_href and self.content_lengths.get(lookup_href, 0) > 1000 - ) or item.childCount() > 0: + has_significant_content = src and self.content_lengths.get(src, 0) > 1000 + is_parent = item.childCount() > 0 + + if has_significant_content or is_parent: item.setCheckState(0, Qt.Checked) - # Check children of parents - if item.childCount() > 0: + if is_parent: for i in range(item.childCount()): child = item.child(i) if child.flags() & Qt.ItemIsUserCheckable: - child.setCheckState(0, Qt.Checked) + child_src = child.data(0, Qt.UserRole) + child_has_content = ( + child_src and self.content_lengths.get(child_src, 0) > 0 + ) + child_is_parent = child.childCount() > 0 + if child_has_content or child_is_parent: + child.setCheckState(0, Qt.Checked) + else: + item.setCheckState(0, Qt.Unchecked) + iterator += 1 def _run_pdf_auto_check(self): - """Auto-check logic for PDF files""" - # If there are no bookmarks, just check all pages if hasattr(self, "has_pdf_bookmarks") and not self.has_pdf_bookmarks: iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): @@ -958,7 +1364,6 @@ class HandlerDialog(QDialog): iterator += 1 return - # For PDFs with bookmarks, select all bookmark items and non-empty pages iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() @@ -968,7 +1373,6 @@ class HandlerDialog(QDialog): identifier = item.data(0, Qt.UserRole) - # Always select bookmark items or non-empty pages if not identifier: iterator += 1 continue @@ -982,7 +1386,6 @@ class HandlerDialog(QDialog): iterator += 1 def _update_checked_set_from_tree(self): - """Update the internal set of checked items""" self.checked_chapters.clear() iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): @@ -992,18 +1395,15 @@ class HandlerDialog(QDialog): if identifier: self.checked_chapters.add(identifier) iterator += 1 - # Only update checkbox states if they exist if hasattr(self, "save_chapters_checkbox") and self.save_chapters_checkbox: self._update_checkbox_states() def handle_item_check(self, item): - """Handle item check/uncheck by updating children""" if self._block_signals: return self._block_signals = True - # Update children recursively if item.flags() & Qt.ItemIsUserCheckable: for i in range(item.childCount()): child = item.child(i) @@ -1014,49 +1414,37 @@ class HandlerDialog(QDialog): self._update_checked_set_from_tree() def handle_item_double_click(self, item, column=0): - """Toggle check state when a non-parent item is double-clicked on the text, not the checkbox""" - # Only toggle items that are checkable and don't have children if item.flags() & Qt.ItemIsUserCheckable and item.childCount() == 0: - # Get the rectangle of the checkbox rect = self.treeWidget.visualItemRect(item) - checkbox_width = 20 # Approximate width of the checkbox + checkbox_width = 20 - # Get current mouse position mouse_pos = self.treeWidget.mapFromGlobal(self.treeWidget.cursor().pos()) - # Only toggle if click position is not on the checkbox if mouse_pos.x() > rect.x() + checkbox_width: - # Toggle the check state new_state = ( Qt.Unchecked if item.checkState(0) == Qt.Checked else Qt.Checked ) item.setCheckState(0, new_state) def update_preview(self, current): - """Update the preview panel with selected item content""" if not current: self.previewEdit.clear() return identifier = current.data(0, Qt.UserRole) - # Special case for the Information item if identifier == "info:bookinfo": self._display_book_info() return - # Get content based on file type text = None if self.file_type == "epub": - # For EPUB, always use the exact href from the TOC text = self.content_texts.get(identifier) - else: # PDF + else: text = self.content_texts.get(identifier) - # Display content or placeholder text - never remove titles if text is None: title = current.text(0) - # Add title to preview even if no content self.previewEdit.setPlainText( f"{title}\n\n(No content available for this item)" ) @@ -1067,18 +1455,15 @@ class HandlerDialog(QDialog): self.previewEdit.setPlainText(text) def _display_book_info(self): - """Display book metadata and cover image in the preview panel""" self.previewEdit.clear() html_content = "<html><body style='font-family: Arial, sans-serif;'>" - # Add cover image if available if self.book_metadata["cover_image"]: try: image_data = base64.b64encode(self.book_metadata["cover_image"]).decode( "utf-8" ) - # Determine image type image_type = "jpeg" if self.book_metadata["cover_image"].startswith(b"\x89PNG"): image_type = "png" @@ -1095,7 +1480,6 @@ class HandlerDialog(QDialog): except Exception as e: html_content += f"<p>Error displaying cover image: {str(e)}</p>" - # Add title, authors, publisher if self.book_metadata["title"]: html_content += ( f"<h2 style='text-align: center;'>{self.book_metadata['title']}</h2>" @@ -1110,12 +1494,10 @@ class HandlerDialog(QDialog): html_content += "<hr/>" - # Add description if self.book_metadata["description"]: desc = re.sub(r"<[^>]+>", "", self.book_metadata["description"]) html_content += f"<h3>Description:</h3><p>{desc}</p>" - # Add file type and page count for PDFs if self.file_type == "pdf": page_count = len(self.pdf_doc) if self.pdf_doc else 0 html_content += f"<p>File type: PDF<br>Page count: {page_count}</p>" @@ -1124,7 +1506,6 @@ class HandlerDialog(QDialog): self.previewEdit.setHtml(html_content) def _extract_book_metadata(self): - """Extract book metadata""" metadata = { "title": None, "authors": [], @@ -1134,7 +1515,6 @@ class HandlerDialog(QDialog): } if self.file_type == "epub": - # Extract EPUB metadata title_items = self.book.get_metadata("DC", "title") if title_items: metadata["title"] = title_items[0][0] @@ -1151,7 +1531,6 @@ class HandlerDialog(QDialog): if publisher_items: metadata["publisher"] = publisher_items[0][0] - # Try to find cover image for item in self.book.get_items_of_type(ebooklib.ITEM_COVER): metadata["cover_image"] = item.get_content() break @@ -1161,8 +1540,7 @@ class HandlerDialog(QDialog): if "cover" in item.get_name().lower(): metadata["cover_image"] = item.get_content() break - else: # PDF - # Extract PDF metadata + else: pdf_info = self.pdf_doc.metadata if pdf_info: metadata["title"] = pdf_info.get("title", None) @@ -1182,7 +1560,6 @@ class HandlerDialog(QDialog): metadata["publisher"] = pdf_info.get("creator", None) - # Try to get cover image from first page if len(self.pdf_doc) > 0: try: pix = self.pdf_doc[0].get_pixmap(matrix=fitz.Matrix(2, 2)) @@ -1193,54 +1570,52 @@ class HandlerDialog(QDialog): return metadata def get_selected_text(self): - """Get selected text and checked identifiers based on file type""" if self.file_type == "epub": return self._get_epub_selected_text() - else: # PDF + else: return self._get_pdf_selected_text() def _get_epub_selected_text(self): - """Get selected text from EPUB content""" - all_checked_hrefs = set() - chapter_titles = [] + all_checked_identifiers = set() + chapter_texts = [] - # Collect all checked hrefs in tree order to preserve chapter sequence + item_order_counter = 0 ordered_checked_items = [] + iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() + item_order_counter += 1 if item.checkState(0) == Qt.Checked: - href = item.data(0, Qt.UserRole) - if href and href != "info:bookinfo": - all_checked_hrefs.add(href) - ordered_checked_items.append((item, href)) + identifier = item.data(0, Qt.UserRole) + if identifier and identifier != "info:bookinfo": + all_checked_identifiers.add(identifier) + ordered_checked_items.append((item_order_counter, item, identifier)) iterator += 1 - # Process checked items in order - for item, href in ordered_checked_items: - # Always use the exact href (including fragment) from the TOC - text = self.content_texts.get(href) + ordered_checked_items.sort(key=lambda x: x[0]) + + for order, item, identifier in ordered_checked_items: + text = self.content_texts.get(identifier) if text and text.strip(): title = item.text(0) - title = re.sub(r"^\s*-\s*", "", title).strip() + title = re.sub(r"^\s*[-–—]\s*", "", title).strip() marker = f"<<CHAPTER_MARKER:{title}>>" - chapter_titles.append((title, marker + "\n" + text)) + chapter_texts.append(marker + "\n" + text) - return "\n\n".join([t[1] for t in chapter_titles]), all_checked_hrefs + full_text = "\n\n".join(chapter_texts) + return full_text, all_checked_identifiers def _get_pdf_selected_text(self): - """Get selected text from PDF content""" all_checked_identifiers = set() included_text_ids = set() section_titles = [] all_content = [] - # Check if PDF has no bookmarks pdf_has_no_bookmarks = ( hasattr(self, "has_pdf_bookmarks") and not self.has_pdf_bookmarks ) - # Collect all checked identifiers iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() @@ -1250,7 +1625,6 @@ class HandlerDialog(QDialog): all_checked_identifiers.add(identifier) iterator += 1 - # For PDFs without bookmarks, collect all content without chapter markers if pdf_has_no_bookmarks: sorted_page_ids = sorted( [id for id in all_checked_identifiers if id.startswith("page_")], @@ -1264,8 +1638,6 @@ class HandlerDialog(QDialog): included_text_ids.add(page_id) return "\n\n".join(all_content), all_checked_identifiers - # For PDFs with bookmarks, process content with parent-child relationships - # If only child pages are selected (not parent), use parent's name as chapter marker at first selected child iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() @@ -1273,7 +1645,6 @@ class HandlerDialog(QDialog): parent_checked = item.checkState(0) == Qt.Checked parent_id = item.data(0, Qt.UserRole) parent_title = item.text(0) - # Gather checked children checked_children = [] for i in range(item.childCount()): child = item.child(i) @@ -1284,7 +1655,6 @@ class HandlerDialog(QDialog): and child_id not in included_text_ids ): checked_children.append((child, child_id)) - # If parent is checked, use old logic (parent marker, all content) if parent_checked and parent_id and parent_id not in included_text_ids: combined_text = self.content_texts.get(parent_id, "") for child, child_id in checked_children: @@ -1297,7 +1667,6 @@ class HandlerDialog(QDialog): marker = f"<<CHAPTER_MARKER:{title}>>" section_titles.append((title, marker + "\n" + combined_text)) included_text_ids.add(parent_id) - # If only children are checked, use parent's name as marker at first child elif not parent_checked and checked_children: title = re.sub(r"^\s*-\s*", "", parent_title).strip() marker = f"<<CHAPTER_MARKER:{title}>>" @@ -1328,18 +1697,15 @@ class HandlerDialog(QDialog): return "\n\n".join([t[1] for t in section_titles]), all_checked_identifiers def on_save_chapters_changed(self, state): - """Update the save_chapters_separately flag""" self.save_chapters_separately = bool(state) self.merge_chapters_checkbox.setEnabled(self.save_chapters_separately) HandlerDialog._save_chapters_separately = self.save_chapters_separately def on_merge_chapters_changed(self, state): - """Update the merge_chapters_at_end flag""" self.merge_chapters_at_end = bool(state) HandlerDialog._merge_chapters_at_end = self.merge_chapters_at_end def get_save_chapters_separately(self): - """Return whether to save chapters separately""" return ( self.save_chapters_separately if self.save_chapters_checkbox.isEnabled() @@ -1347,11 +1713,9 @@ class HandlerDialog(QDialog): ) def get_merge_chapters_at_end(self): - """Return whether to merge chapters at the end""" return self.merge_chapters_at_end def on_tree_context_menu(self, pos): - """Handle context menu on tree items""" item = self.treeWidget.itemAt(pos) if ( not item @@ -1376,7 +1740,6 @@ class HandlerDialog(QDialog): menu.exec_(self.treeWidget.mapToGlobal(pos)) def closeEvent(self, event): - """Clean up resources when the dialog is closed""" if self.pdf_doc is not None: self.pdf_doc.close() event.accept() diff --git a/abogen/gui.py b/abogen/gui.py index a0c7ff6..3f7ceec 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -1445,10 +1445,10 @@ class abogen(QWidget): # stop loading animation and restore icon on error if error: self.loading_movie.stop() - self.btn_preview.setIcon(self.play_icon) self._show_error_message_box( "Loading Error", f"Error loading numpy or KPipeline: {error}" ) + self.btn_preview.setIcon(self.play_icon) self.btn_preview.setEnabled(True) self.btn_preview.setToolTip("Preview selected voice") self.voice_combo.setEnabled(True) @@ -1491,12 +1491,13 @@ class abogen(QWidget): temp_wav = self.preview_thread.temp_wav if not temp_wav: self.loading_movie.stop() - self.btn_preview.setIcon(self.play_icon) + self._show_error_message_box( "Preview Error", "Preview error: No audio generated." ) - self.btn_preview.setEnabled(True) + self.btn_preview.setIcon(self.play_icon) self.btn_preview.setToolTip("Preview selected voice") + self.btn_preview.setEnabled(True) self.voice_combo.setEnabled(True) self.btn_voice_formula_mixer.setEnabled(True) # Re-enable mixer button self.btn_start.setEnabled(True) diff --git a/demo/abogen.png b/demo/abogen.png index 16adbaf1aa46c0fd4dff6e22bf1c5c362178cfea..b11c5dd040928f1d9bf391c4282719eef6c9c202 100644 GIT binary patch literal 23118 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 literal 22311 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 YDDooPlOp;R%x#shJDlvew&d790WPtzE&u=k diff --git a/pyproject.toml b/pyproject.toml index 7f626a6..3f5ff16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ keywords = ["audiobook", "epub", "pdf", "text-to-speech", "subtitle", "tts", "ko dependencies = [ "PyQt5>=5.15.11", "kokoro>=0.9.4", - "ebooklib>=0.18", + "ebooklib>=0.19", "beautifulsoup4>=4.13.4", "PyMuPDF>=1.25.5", "platformdirs>=4.3.7",