kenkui 2.3.2__tar.gz → 2.3.3__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.
Files changed (187) hide show
  1. {kenkui-2.3.2/src/kenkui.egg-info → kenkui-2.3.3}/PKG-INFO +1 -1
  2. {kenkui-2.3.2 → kenkui-2.3.3}/pyproject.toml +1 -1
  3. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/epub.py +55 -1
  4. {kenkui-2.3.2 → kenkui-2.3.3/src/kenkui.egg-info}/PKG-INFO +1 -1
  5. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_parsing.py +48 -0
  6. {kenkui-2.3.2 → kenkui-2.3.3}/LICENSE +0 -0
  7. {kenkui-2.3.2 → kenkui-2.3.3}/MANIFEST.in +0 -0
  8. {kenkui-2.3.2 → kenkui-2.3.3}/README.md +0 -0
  9. {kenkui-2.3.2 → kenkui-2.3.3}/setup.cfg +0 -0
  10. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/__init__.py +0 -0
  11. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/analytics.py +0 -0
  12. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/api.py +0 -0
  13. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/audio_build.py +0 -0
  14. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/chapter_classifier.py +0 -0
  15. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/chapter_filter.py +0 -0
  16. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/cli.py +0 -0
  17. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/config.py +0 -0
  18. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/errors.py +0 -0
  19. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/file_finder.py +0 -0
  20. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/huggingface_auth.py +0 -0
  21. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/log.py +0 -0
  22. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/model_names.py +0 -0
  23. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/__init__.py +0 -0
  24. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/api.py +0 -0
  25. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/audio.py +0 -0
  26. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/book.py +0 -0
  27. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/common.py +0 -0
  28. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/config.py +0 -0
  29. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/job.py +0 -0
  30. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/nlp.py +0 -0
  31. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/queue.py +0 -0
  32. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/__init__.py +0 -0
  33. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/_async.py +0 -0
  34. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/_cache.py +0 -0
  35. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/_filters.py +0 -0
  36. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/_retry.py +0 -0
  37. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/annotator.py +0 -0
  38. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/attribution.py +0 -0
  39. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/booknlp_roster.py +0 -0
  40. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/chunker.py +0 -0
  41. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/entities.py +0 -0
  42. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/llm.py +0 -0
  43. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/models.py +0 -0
  44. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/pipeline.py +0 -0
  45. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/__init__.py +0 -0
  46. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/_base.py +0 -0
  47. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/_factory.py +0 -0
  48. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/base.py +0 -0
  49. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/booknlp.py +0 -0
  50. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/litellm.py +0 -0
  51. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/local.py +0 -0
  52. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/ollama.py +0 -0
  53. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/quotes.py +0 -0
  54. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/setup.py +0 -0
  55. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp_config.py +0 -0
  56. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/parsing.py +0 -0
  57. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/post_processing.py +0 -0
  58. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/progress.py +0 -0
  59. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/progress_tracking.py +0 -0
  60. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/__init__.py +0 -0
  61. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/_pdf_extract.py +0 -0
  62. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/_pdf_to_epub.py +0 -0
  63. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/fb2.py +0 -0
  64. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/mobi.py +0 -0
  65. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/pdf.py +0 -0
  66. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/samples/Les Miserables - Victor Hugo.epub +0 -0
  67. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/samples/Pride and Predjudice.epub +0 -0
  68. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/samples/__init__.py +0 -0
  69. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/series.py +0 -0
  70. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/server/__init__.py +0 -0
  71. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/server/api.py +0 -0
  72. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/server/openapi.py +0 -0
  73. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/server/server.py +0 -0
  74. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/__init__.py +0 -0
  75. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/application_service.py +0 -0
  76. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/auth_service.py +0 -0
  77. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/book_cache.py +0 -0
  78. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/book_service.py +0 -0
  79. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/confirmation_service.py +0 -0
  80. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/download_service.py +0 -0
  81. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/execution_service.py +0 -0
  82. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/job_executor.py +0 -0
  83. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/job_service.py +0 -0
  84. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/nlp_service.py +0 -0
  85. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/openrouter_service.py +0 -0
  86. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/provider_service.py +0 -0
  87. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/queue_manager.py +0 -0
  88. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/runtime_service.py +0 -0
  89. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/series_service.py +0 -0
  90. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/setup_service.py +0 -0
  91. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/task_coordinator.py +0 -0
  92. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/task_service.py +0 -0
  93. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/voice_service.py +0 -0
  94. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/workflow_service.py +0 -0
  95. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/system_check.py +0 -0
  96. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/text_rules.py +0 -0
  97. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/utils.py +0 -0
  98. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_compiler.py +0 -0
  99. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_download.py +0 -0
  100. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_loader.py +0 -0
  101. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_metadata.py +0 -0
  102. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_pool.py +0 -0
  103. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_registry.py +0 -0
  104. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voices/manifest.json +0 -0
  105. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voices/preview_phrases.json +0 -0
  106. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/workers.py +0 -0
  107. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui.egg-info/SOURCES.txt +0 -0
  108. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui.egg-info/dependency_links.txt +0 -0
  109. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui.egg-info/entry_points.txt +0 -0
  110. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui.egg-info/requires.txt +0 -0
  111. {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui.egg-info/top_level.txt +0 -0
  112. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_analytics.py +0 -0
  113. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_annotator.py +0 -0
  114. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_api_attribution_options.py +0 -0
  115. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_application_service.py +0 -0
  116. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_audio_build.py +0 -0
  117. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_auth_service.py +0 -0
  118. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_book_cache.py +0 -0
  119. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_book_service.py +0 -0
  120. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_chapter_classifier.py +0 -0
  121. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_chapter_filter.py +0 -0
  122. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_character_record_dataclass.py +0 -0
  123. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_credential_management.py +0 -0
  124. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_download_service.py +0 -0
  125. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_generation_progress.py +0 -0
  126. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_huggingface_auth.py +0 -0
  127. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_job_executor.py +0 -0
  128. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_library_contracts.py +0 -0
  129. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_modal_runtime_config.py +0 -0
  130. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_modal_runtime_registration.py +0 -0
  131. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_modal_status.py +0 -0
  132. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_model_names.py +0 -0
  133. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_models.py +0 -0
  134. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_multi_voice_models.py +0 -0
  135. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_name_filters.py +0 -0
  136. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_attribution.py +0 -0
  137. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_booknlp_adapters.py +0 -0
  138. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_booknlp_roster.py +0 -0
  139. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_cache.py +0 -0
  140. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_chunker.py +0 -0
  141. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_config.py +0 -0
  142. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_coreference_pierce_brown.py +0 -0
  143. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_entities.py +0 -0
  144. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_factory.py +0 -0
  145. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_italic_dialogue.py +0 -0
  146. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_litellm_adapters.py +0 -0
  147. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_llm_limits.py +0 -0
  148. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_models_enriched.py +0 -0
  149. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_ollama_adapters.py +0 -0
  150. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_pipeline.py +0 -0
  151. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_pipeline_split.py +0 -0
  152. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_providers.py +0 -0
  153. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_providers_base.py +0 -0
  154. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_quotes.py +0 -0
  155. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_quotes_non_dialogue.py +0 -0
  156. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_retry.py +0 -0
  157. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_segments.py +0 -0
  158. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_service.py +0 -0
  159. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_openrouter_service.py +0 -0
  160. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_optional_dependencies.py +0 -0
  161. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_pdf_extractor.py +0 -0
  162. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_pdf_reader.py +0 -0
  163. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_pdf_to_epub.py +0 -0
  164. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_post_processing.py +0 -0
  165. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_progress_tracking.py +0 -0
  166. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_provider_service.py +0 -0
  167. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_qp14_efficiency.py +0 -0
  168. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_queue_manager.py +0 -0
  169. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_scare_quote_stripping.py +0 -0
  170. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_series.py +0 -0
  171. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_series_roster.py +0 -0
  172. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_series_service.py +0 -0
  173. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_setup_service.py +0 -0
  174. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_speaker_voices_migration.py +0 -0
  175. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_suggest_cast.py +0 -0
  176. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_task_coordinator.py +0 -0
  177. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_utils.py +0 -0
  178. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_download.py +0 -0
  179. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_loader.py +0 -0
  180. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_metadata.py +0 -0
  181. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_pack_build.py +0 -0
  182. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_pools.py +0 -0
  183. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_registry.py +0 -0
  184. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_review_ux.py +0 -0
  185. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_service.py +0 -0
  186. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_workers.py +0 -0
  187. {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_workers_scene_breaks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kenkui
3
- Version: 2.3.2
3
+ Version: 2.3.3
4
4
  Summary: Convert Ebooks to Audiobooks with [custom] voice samples
5
5
  Author-email: Sumner MacArthur <spn1kolat3sla@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kenkui"
7
- version = "2.3.2"
7
+ version = "2.3.3"
8
8
  description = "Convert Ebooks to Audiobooks with [custom] voice samples"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Sumner MacArthur", email = "spn1kolat3sla@gmail.com" }]
@@ -7,6 +7,7 @@ Provides EbookReader interface for EPUB files using ebooklib.
7
7
  from __future__ import annotations
8
8
 
9
9
  import re
10
+ import tempfile
10
11
  import warnings
11
12
  import zipfile
12
13
  from pathlib import Path
@@ -30,7 +31,60 @@ class EpubReader(EbookReader):
30
31
  super().__init__(filepath, verbose)
31
32
  with warnings.catch_warnings(record=True):
32
33
  warnings.simplefilter("always")
33
- self.book = epub.read_epub(str(filepath))
34
+ try:
35
+ self.book = epub.read_epub(str(filepath))
36
+ except AttributeError as exc:
37
+ if "'NoneType' object has no attribute" not in str(exc):
38
+ raise
39
+ try:
40
+ self.book = self._read_epub_without_nav_parsing()
41
+ except Exception:
42
+ raise exc
43
+
44
+ def _read_epub_without_nav_parsing(self):
45
+ """Load an EPUB while bypassing EbookLib's brittle NAV parser.
46
+
47
+ EbookLib parses any manifest item marked ``properties=\"nav\"`` during
48
+ ``read_epub``. Some real EPUBs contain malformed NAV documents (for
49
+ example a TOC ``<nav>`` without an ``<ol>``), which makes EbookLib raise
50
+ an opaque ``'NoneType' object has no attribute ...`` before kenkui can
51
+ fall back to its own TOC/chapter extraction. Strip only the ``nav``
52
+ property from a temporary copy so EbookLib still loads manifest/spine
53
+ items while kenkui parses the original file itself.
54
+ """
55
+ from xml.etree import ElementTree as ET
56
+
57
+ with tempfile.NamedTemporaryFile(suffix=".epub") as temp_epub:
58
+ with zipfile.ZipFile(str(self.filepath), "r") as source_zip:
59
+ names = source_zip.namelist()
60
+ container_path = "META-INF/container.xml"
61
+ if container_path not in names:
62
+ raise ValueError("EPUB is missing META-INF/container.xml")
63
+ container_tree = ET.fromstring(source_zip.read(container_path))
64
+ container_ns = {"container": "urn:oasis:names:tc:opendocument:xmlns:container"}
65
+ rootfile = container_tree.find(".//container:rootfile", container_ns)
66
+ if rootfile is None or rootfile.get("full-path") is None:
67
+ raise ValueError("EPUB container is missing rootfile full-path")
68
+ opf_path = rootfile.get("full-path") or ""
69
+ opf_tree = ET.fromstring(source_zip.read(opf_path))
70
+ opf_ns = {"opf": "http://www.idpf.org/2007/opf"}
71
+ for item in opf_tree.findall(".//opf:item", opf_ns):
72
+ properties = item.get("properties")
73
+ if not properties:
74
+ continue
75
+ remaining = [prop for prop in properties.split() if prop != "nav"]
76
+ if remaining:
77
+ item.set("properties", " ".join(remaining))
78
+ else:
79
+ item.attrib.pop("properties", None)
80
+ sanitized_opf = ET.tostring(opf_tree, encoding="utf-8", xml_declaration=True)
81
+
82
+ with zipfile.ZipFile(temp_epub.name, "w") as sanitized_zip:
83
+ for info in source_zip.infolist():
84
+ content = sanitized_opf if info.filename == opf_path else source_zip.read(info.filename)
85
+ sanitized_zip.writestr(info, content)
86
+
87
+ return epub.read_epub(temp_epub.name)
34
88
 
35
89
  def get_metadata(self) -> EbookMetadata:
36
90
  """Extract metadata from EPUB."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kenkui
3
- Version: 2.3.2
3
+ Version: 2.3.3
4
4
  Summary: Convert Ebooks to Audiobooks with [custom] voice samples
5
5
  Author-email: Sumner MacArthur <spn1kolat3sla@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,7 @@
1
1
  """Tests for kenkui parsing functionality."""
2
2
 
3
3
  import json
4
+ import zipfile
4
5
  from pathlib import Path
5
6
  from types import SimpleNamespace
6
7
 
@@ -13,6 +14,38 @@ from kenkui.readers.epub import EpubReader
13
14
  TEST_EPUB = Path("src/kenkui/samples/Les Miserables - Victor Hugo.epub")
14
15
 
15
16
 
17
+ def _write_epub_with_nav(epub_path: Path, *, nav_body: str) -> None:
18
+ files = {
19
+ "META-INF/container.xml": """<?xml version="1.0"?>
20
+ <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
21
+ <rootfiles>
22
+ <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
23
+ </rootfiles>
24
+ </container>""",
25
+ "OEBPS/content.opf": """<?xml version="1.0"?>
26
+ <package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="id">
27
+ <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
28
+ <dc:identifier id="id">bad-nav</dc:identifier>
29
+ <dc:title>Bad Nav</dc:title>
30
+ <dc:language>en</dc:language>
31
+ </metadata>
32
+ <manifest>
33
+ <item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
34
+ <item id="c1" href="chap.xhtml" media-type="application/xhtml+xml"/>
35
+ </manifest>
36
+ <spine>
37
+ <itemref idref="c1"/>
38
+ </spine>
39
+ </package>""",
40
+ "OEBPS/nav.xhtml": f"""<html xmlns="http://www.w3.org/1999/xhtml"><body>{nav_body}</body></html>""",
41
+ "OEBPS/chap.xhtml": """<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Chapter One</h1><p>Hello world.</p></body></html>""",
42
+ }
43
+ with zipfile.ZipFile(epub_path, "w") as epub_zip:
44
+ epub_zip.writestr("mimetype", "application/epub+zip", compress_type=zipfile.ZIP_STORED)
45
+ for name, content in files.items():
46
+ epub_zip.writestr(name, content)
47
+
48
+
16
49
  class TestEpubReader:
17
50
  """Tests for the EpubReader class."""
18
51
 
@@ -53,6 +86,21 @@ class TestEpubReader:
53
86
  assert isinstance(chapter.title, str)
54
87
  assert isinstance(chapter.paragraphs, list)
55
88
 
89
+ def test_reader_handles_nav_document_without_ordered_list(self, tmp_path):
90
+ """EPUBs with a malformed NAV document still load chapters."""
91
+ epub_path = tmp_path / "bad-nav.epub"
92
+ _write_epub_with_nav(
93
+ epub_path,
94
+ nav_body='<nav epub:type="toc" xmlns:epub="http://www.idpf.org/2007/ops"><h1>Contents</h1></nav>',
95
+ )
96
+
97
+ reader = EpubReader(epub_path)
98
+
99
+ assert reader.get_metadata().title == "Bad Nav"
100
+ chapters = reader.get_chapters(min_text_len=1)
101
+ assert len(chapters) == 1
102
+ assert chapters[0].paragraphs == ["Chapter One", "Hello world."]
103
+
56
104
 
57
105
  class TestChapterDataclass:
58
106
  """Tests for the Chapter dataclass."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes