content-core 2.1.0__tar.gz → 2.2.0__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 (141) hide show
  1. {content_core-2.1.0 → content_core-2.2.0}/.claude-plugin/plugin.json +1 -1
  2. {content_core-2.1.0 → content_core-2.2.0}/.codex-plugin/plugin.json +1 -1
  3. {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/claude-code-review.yml +5 -5
  4. {content_core-2.1.0 → content_core-2.2.0}/AGENTS.md +5 -4
  5. {content_core-2.1.0 → content_core-2.2.0}/CHANGELOG.md +23 -0
  6. {content_core-2.1.0 → content_core-2.2.0}/PKG-INFO +18 -9
  7. {content_core-2.1.0 → content_core-2.2.0}/README.md +8 -1
  8. {content_core-2.1.0 → content_core-2.2.0}/docs/cli.md +1 -0
  9. {content_core-2.1.0 → content_core-2.2.0}/docs/processors.md +9 -0
  10. {content_core-2.1.0 → content_core-2.2.0}/docs/usage.md +77 -2
  11. {content_core-2.1.0 → content_core-2.2.0}/pyproject.toml +12 -10
  12. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/cli.py +1 -0
  13. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/exceptions.py +2 -9
  14. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/retry.py +33 -1
  15. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/config.py +3 -0
  16. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/identification/file_detector.py +17 -2
  17. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/extraction.py +41 -19
  18. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/__init__.py +24 -1
  19. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/epub.py +2 -1
  20. content_core-2.2.0/src/content_core/processors/document/odf.py +218 -0
  21. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/pdf.py +3 -2
  22. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/media/audio.py +75 -43
  23. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/media/video.py +44 -59
  24. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/text.py +3 -2
  25. content_core-2.2.0/src/content_core/processors/url/__init__.py +217 -0
  26. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/url/bs4.py +10 -12
  27. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/url/crawl4ai.py +27 -8
  28. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/url/firecrawl.py +3 -3
  29. content_core-2.2.0/src/content_core/processors/url/youtube.py +371 -0
  30. content_core-2.2.0/tests/input_content/file.odp +0 -0
  31. content_core-2.2.0/tests/input_content/file.ods +0 -0
  32. content_core-2.2.0/tests/input_content/file.odt +0 -0
  33. {content_core-2.1.0 → content_core-2.2.0}/tests/integration/test_extraction.py +54 -0
  34. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_config_v2.py +21 -0
  35. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_crawl4ai_docker.py +15 -0
  36. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_epub_extraction.py +8 -0
  37. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_exceptions.py +6 -13
  38. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_file_detector.py +106 -1
  39. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_media_pipeline.py +188 -1
  40. content_core-2.2.0/tests/unit/test_office_extraction.py +340 -0
  41. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_pdf_extraction.py +22 -1
  42. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_retry.py +53 -0
  43. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_routing.py +116 -1
  44. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_text_processing.py +13 -0
  45. content_core-2.2.0/tests/unit/test_url_engine_select.py +519 -0
  46. content_core-2.2.0/tests/unit/test_youtube_parsing.py +517 -0
  47. content_core-2.2.0/uv.lock +7720 -0
  48. content_core-2.1.0/src/content_core/processors/url/__init__.py +0 -175
  49. content_core-2.1.0/src/content_core/processors/url/youtube.py +0 -216
  50. content_core-2.1.0/tests/unit/test_office_extraction.py +0 -99
  51. content_core-2.1.0/tests/unit/test_url_engine_select.py +0 -206
  52. content_core-2.1.0/tests/unit/test_youtube_parsing.py +0 -163
  53. content_core-2.1.0/uv.lock +0 -6676
  54. {content_core-2.1.0 → content_core-2.2.0}/.agent-smith/index.json +0 -0
  55. {content_core-2.1.0 → content_core-2.2.0}/.agents/plugins/marketplace.json +0 -0
  56. {content_core-2.1.0 → content_core-2.2.0}/.claude-plugin/marketplace.json +0 -0
  57. {content_core-2.1.0 → content_core-2.2.0}/.gitattributes +0 -0
  58. {content_core-2.1.0 → content_core-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  59. {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/claude.yml +0 -0
  60. {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/create-tag.yml +0 -0
  61. {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/package.yml +0 -0
  62. {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/publish.yml +0 -0
  63. {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/test.yml +0 -0
  64. {content_core-2.1.0 → content_core-2.2.0}/.gitignore +0 -0
  65. {content_core-2.1.0 → content_core-2.2.0}/.maintainer/PROFILE.md +0 -0
  66. {content_core-2.1.0 → content_core-2.2.0}/.maintainer/README.md +0 -0
  67. {content_core-2.1.0 → content_core-2.2.0}/.maintainer/gotchas.md +0 -0
  68. {content_core-2.1.0 → content_core-2.2.0}/.maintainer/profile.toml +0 -0
  69. {content_core-2.1.0 → content_core-2.2.0}/.maintainer/release/runbook.md +0 -0
  70. {content_core-2.1.0 → content_core-2.2.0}/.maintainer/release/test-matrix.md +0 -0
  71. {content_core-2.1.0 → content_core-2.2.0}/.python-version +0 -0
  72. {content_core-2.1.0 → content_core-2.2.0}/ARCHITECTURE.md +0 -0
  73. {content_core-2.1.0 → content_core-2.2.0}/CLAUDE.md +0 -0
  74. {content_core-2.1.0 → content_core-2.2.0}/CONTRIBUTING.md +0 -0
  75. {content_core-2.1.0 → content_core-2.2.0}/LICENSE +0 -0
  76. {content_core-2.1.0 → content_core-2.2.0}/Makefile +0 -0
  77. {content_core-2.1.0 → content_core-2.2.0}/REFACTOR.md +0 -0
  78. {content_core-2.1.0 → content_core-2.2.0}/TEST_PLAN.md +0 -0
  79. {content_core-2.1.0 → content_core-2.2.0}/docs/mcp.md +0 -0
  80. {content_core-2.1.0 → content_core-2.2.0}/examples/main.py +0 -0
  81. {content_core-2.1.0 → content_core-2.2.0}/prompts/content/cleanup.jinja +0 -0
  82. {content_core-2.1.0 → content_core-2.2.0}/prompts/content/summarize.jinja +0 -0
  83. {content_core-2.1.0 → content_core-2.2.0}/skills/content-core/SKILL.md +0 -0
  84. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/__init__.py +0 -0
  85. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/__init__.py +0 -0
  86. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/messages.py +0 -0
  87. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/state.py +0 -0
  88. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/types.py +0 -0
  89. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/__init__.py +0 -0
  90. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/extraction/__init__.py +0 -0
  91. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/identification/__init__.py +0 -0
  92. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/summary/__init__.py +0 -0
  93. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/summary/core.py +0 -0
  94. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/logging.py +0 -0
  95. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/mcp/__init__.py +0 -0
  96. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/mcp/server.py +0 -0
  97. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/models.py +0 -0
  98. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/docling.py +0 -0
  99. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/docx.py +0 -0
  100. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/pptx.py +0 -0
  101. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/xlsx.py +0 -0
  102. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/media/__init__.py +0 -0
  103. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/url/jina.py +0 -0
  104. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/url/reddit.py +0 -0
  105. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/py.typed +0 -0
  106. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/templated_message.py +0 -0
  107. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/tools/__init__.py +0 -0
  108. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/tools/extract.py +0 -0
  109. {content_core-2.1.0 → content_core-2.2.0}/src/content_core/tools/summarize.py +0 -0
  110. {content_core-2.1.0 → content_core-2.2.0}/tests/conftest.py +0 -0
  111. {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/__init__.py +0 -0
  112. {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/test_docling.py +0 -0
  113. {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/test_media.py +0 -0
  114. {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/test_remote.py +0 -0
  115. {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/test_url_engines.py +0 -0
  116. {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/test_youtube.py +0 -0
  117. {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.docx +0 -0
  118. {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.epub +0 -0
  119. {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.md +0 -0
  120. {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.mp3 +0 -0
  121. {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.mp4 +0 -0
  122. {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.pdf +0 -0
  123. {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.pptx +0 -0
  124. {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.txt +0 -0
  125. {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.xlsx +0 -0
  126. {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/new_pdf.pdf +0 -0
  127. {content_core-2.1.0 → content_core-2.2.0}/tests/integration/conftest.py +0 -0
  128. {content_core-2.1.0 → content_core-2.2.0}/tests/integration/test_cli_v2.py +0 -0
  129. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_audio_concurrency.py +0 -0
  130. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_cli.py +0 -0
  131. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_config_file.py +0 -0
  132. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_docling_extraction.py +0 -0
  133. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_file_detector_critical.py +0 -0
  134. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_file_detector_performance.py +0 -0
  135. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_html_file_extraction.py +0 -0
  136. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_logging.py +0 -0
  137. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_mcp_server_version.py +0 -0
  138. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_mcp_v2.py +0 -0
  139. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_models_v2.py +0 -0
  140. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_pdf_helpers.py +0 -0
  141. {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_reddit_extraction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-core",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Extract text content from URLs, documents, YouTube videos, Reddit posts, and audio/video files — via CLI (uvx) or MCP.",
5
5
  "author": {
6
6
  "name": "Luis Novo",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-core",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Extract text content from URLs, documents, YouTube videos, Reddit posts, and audio/video files — via CLI (uvx) or MCP.",
5
5
  "skills": "./skills/"
6
6
  }
@@ -12,11 +12,11 @@ on:
12
12
 
13
13
  jobs:
14
14
  claude-review:
15
- # Optional: Filter by PR author
16
- # if: |
17
- # github.event.pull_request.user.login == 'external-contributor' ||
18
- # github.event.pull_request.user.login == 'new-developer' ||
19
- # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
15
+ # Fork PRs get no repository secrets, so the review could only fail there.
16
+ # (pull_request_target would expose the secrets to untrusted code.)
17
+ # To also filter by PR author, extend this expression with `&&` -- a job
18
+ # takes a single `if:`.
19
+ if: github.event.pull_request.head.repo.full_name == github.repository
20
20
 
21
21
  runs-on: ubuntu-latest
22
22
  permissions:
@@ -92,6 +92,7 @@ src/content_core/
92
92
  │ │ ├── docx.py # python-docx
93
93
  │ │ ├── pptx.py # python-pptx
94
94
  │ │ ├── xlsx.py # openpyxl
95
+ │ │ ├── odf.py # ODT/ODS/ODP via odfpy
95
96
  │ │ └── docling.py # Optional Docling integration
96
97
  │ └── media/ # Audio/video processing
97
98
  │ ├── __init__.py # Video→audio pipeline
@@ -129,7 +130,7 @@ from content_core import ContentCoreConfig
129
130
  config = ContentCoreConfig(url_engine="firecrawl", audio_concurrency=5)
130
131
  ```
131
132
 
132
- Key settings: `url_engine`, `document_engine`, `audio_provider`, `audio_model`, `firecrawl_api_url`, `youtube_languages`, `llm_provider`, `llm_model`, `docling_ocr`, `docling_formulas`, `docling_vision`
133
+ Key settings: `url_engine`, `document_engine`, `audio_provider`, `audio_model`, `firecrawl_api_url`, `youtube_languages`, `youtube_cookies_file`, `youtube_proxy`, `llm_provider`, `llm_model`, `docling_ocr`, `docling_formulas`, `docling_vision`
133
134
 
134
135
  Docling enrichment flags (`docling_ocr`, `docling_formulas`, `docling_vision`) control OCR, formula extraction, and image/chart processing when `document_engine="docling"`. These are also exposed as CLI flags (`--formulas`, `--pictures`, `--no-ocr`) and MCP parameters.
135
136
 
@@ -166,7 +167,7 @@ When you change a specific processor or module, run only the relevant tests for
166
167
  | `processors/url/youtube.py` | `uv run pytest -k "youtube"` |
167
168
  | `processors/url/reddit.py` | `uv run pytest -k "reddit"` |
168
169
  | `processors/document/pdf.py` | `uv run pytest -k "pdf"` |
169
- | `processors/document/docx.py` or `pptx.py` or `xlsx.py` | `uv run pytest -k "office"` |
170
+ | `processors/document/docx.py` or `pptx.py` or `xlsx.py` or `odf.py` | `uv run pytest -k "office"` |
170
171
  | `processors/document/docling.py` | `uv run pytest -k "docling"` |
171
172
  | `processors/text.py` | `uv run pytest -k "text_processing or html_file"` |
172
173
  | `processors/media/audio.py` | `uv run pytest -k "audio or media_pipeline"` |
@@ -191,7 +192,7 @@ tests/
191
192
  │ ├── test_pdf_extraction.py # PDF text cleaning, extraction with mocked pdfplumber
192
193
  │ ├── test_pdf_helpers.py # Formula detection, table conversion helpers
193
194
  │ ├── test_epub_extraction.py # EPUB extraction with mocked fast-ebook
194
- │ ├── test_office_extraction.py # DOCX/PPTX/XLSX routing and extraction
195
+ │ ├── test_office_extraction.py # DOCX/PPTX/XLSX/ODT/ODS/ODP routing and extraction
195
196
  │ ├── test_docling_extraction.py # Docling output formats with mocked converter
196
197
  │ ├── test_text_processing.py # HTML detection, markdown conversion, <title>/<head> handling
197
198
  │ ├── test_html_file_extraction.py # Local .html file → markdown via text processor
@@ -206,7 +207,7 @@ tests/
206
207
  │ └── test_file_detector*.py # MIME detection, performance, edge cases
207
208
  │
208
209
  ├── integration/ # Local files, no network (~22 tests)
209
- │ ├── test_extraction.py # Real file extraction (PDF, DOCX, PPTX, XLSX, EPUB, HTML, etc.)
210
+ │ ├── test_extraction.py # Real file extraction (PDF, DOCX, PPTX, XLSX, ODT/ODS/ODP, EPUB, HTML, etc.)
210
211
  │ └── test_cli_v2.py # CLI subcommands via CliRunner with real extraction
211
212
  │
212
213
  └── e2e/
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.0] - 2026-10-03
11
+
12
+ ### Added
13
+ - OpenDocument extraction for `.odt`, `.ods` and `.odp` through the office route, using `odfpy` (new core dependency). Text documents become markdown paragraphs, headings, lists and tables; spreadsheets become one markdown table per sheet; presentations become one block per slide. Detection reads the ZIP's `mimetype` member, so a wrong or missing extension still works (#43).
14
+ - YouTube cookie and proxy support for the transcript path (#46). `youtube_cookies_file` (`CCORE_YOUTUBE_COOKIES_FILE`) loads a Netscape `cookies.txt` into the `youtube-transcript-api` session, unblocking IP-flagged networks; a missing or unreadable file raises `ConfigurationError`. `youtube_proxy` (`CCORE_YOUTUBE_PROXY`) routes both `youtube-transcript-api` and the `pytubefix` fallback through a proxy URL — use a residential proxy, as datacenter proxies are blocked. With neither set, behavior is unchanged. Cookie values are never logged.
15
+ - `audio_segment_minutes` setting (`CCORE_AUDIO_SEGMENT_MINUTES`) controls the length of the segments long audio is split into before transcription. The default stays 10 minutes; `0` sends the file whole, for self-hosted STT endpoints without an upload size limit (#94).
16
+
17
+ ### Changed
18
+ - **URL extraction failures now raise instead of returning empty output** (#60). `content=""` now only means the source was genuinely empty. A named `url_engine` that fails raises `NetworkError` (connection/timeout/DNS), `NotFoundError` (HTTP 404/410 from the page, `simple` engine), `ExternalServiceError` (engine API or HTTP errors) or `ConfigurationError` (`crawl4ai` not installed). `auto` still falls through firecrawl → jina → crawl4ai → simple and raises the last error only when every engine failed. A malformed URL raises `InvalidInputError`. The `simple` engine no longer returns `"Failed to extract content: ..."` or `"No content found"` as page content. Remote-file downloads raise the same typed errors, and a download that cannot be saved locally raises `FileOperationError`.
19
+ - File extraction failures raise typed errors (#60): corrupted PDF, EPUB or unreadable text files raise `FileOperationError` (chained to the original cause) instead of bare `Exception`/`RuntimeError`; a missing file still raises `FileNotFoundError`. ffmpeg/ffprobe failures in audio and video processing raise `FileOperationError`: video extraction no longer returns empty content when ffprobe or ffmpeg fails. Speech-to-text provider failures (after retries) raise `ExternalServiceError`, and an STT model that cannot be created raises `ConfigurationError`.
20
+ - Retry classification now treats `ConfigurationError`, `UnsupportedTypeException`, `InvalidInputError` and `FileOperationError` as permanent and `NetworkError` as transient (#60).
21
+ - YouTube extraction raises instead of returning empty content when no transcript can be fetched (#60): `NoTranscriptFound` when neither youtube-transcript-api nor pytubefix finds a transcript, `ExternalServiceError` (chained to the provider error) when YouTube blocks or fails the request on both paths, e.g. `IpBlocked`, and `InvalidInputError` for a YouTube URL without a video ID. Provider errors from `get_best_transcript` and `extract_transcript_pytubefix` now propagate after retries instead of being swallowed (`extract_transcript_pytubefix` still returns `(None, None)` for a video without captions); a failure of the first path still falls back to the second.
22
+ - An explicit `audio_provider`/`audio_model` that cannot be created now raises `ConfigurationError` instead of silently transcribing with the default STT model, per "Explicit choice is a requirement" (#99).
23
+ - youtube-transcript-api failures are retried by type: blocked requests (`RequestBlocked`/`IpBlocked`) are retried, so a rotating proxy can get a new IP; other transcript errors that name a state of the video (unavailable, transcripts disabled, age-restricted...) are not (#99).
24
+
25
+ ### Fixed
26
+ - `youtube_proxy` now also applies to the YouTube video title lookup, which previously used only env-var proxies (#99).
27
+ - `requests`, imported directly for the YouTube cookie session, is declared as a dependency instead of arriving transitively (#99).
28
+ - A failed Crawl4AI crawl (navigation timeout, blocked page) now raises `ExternalServiceError` instead of failing with a pydantic `ValidationError` on `content=None`, and `auto` falls through to the next engine. Applies to both the local browser and the Docker API mode (#101).
29
+
30
+ ### Security
31
+ - Raised minimum versions to releases without known vulnerabilities: `aiohttp>=3.14.3`, `pillow>=12.3.0`, `click>=8.3.3`, `pydantic-settings>=2.14.2`, and in the extras `docling>=2.94.0`, `crawl4ai>=0.9.0` and `langchain-core>=1.3.3`. Upgrading content-core upgrades these packages in an existing environment. The development lock was refreshed; it now also exercises `fastmcp` 4 and `mcp` 2, which fresh installs already resolve to (#102).
32
+
10
33
  ## [2.1.0] - 2026-09-06
11
34
 
12
35
  ### Added
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: content-core
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Extract what matters from any media source. Available as Python Library, macOS Service, CLI and MCP Server
5
5
  Author-email: LUIS NOVO <lfnovo@gmail.com>
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.10
8
8
  Requires-Dist: ai-prompter>=0.4.0
9
- Requires-Dist: aiohttp>=3.13
9
+ Requires-Dist: aiohttp>=3.14.3
10
10
  Requires-Dist: bs4>=0.0.2
11
- Requires-Dist: click>=8.3.0
11
+ Requires-Dist: click>=8.3.3
12
12
  Requires-Dist: esperanto>=2.26.0
13
13
  Requires-Dist: fast-ebook>=0.2.0
14
14
  Requires-Dist: fastmcp>=3.0.0
@@ -17,28 +17,30 @@ Requires-Dist: jinja2>=3.1.6
17
17
  Requires-Dist: langdetect>=1.0.9
18
18
  Requires-Dist: loguru>=0.7.3
19
19
  Requires-Dist: markdownify>=0.14.0
20
+ Requires-Dist: odfpy>=1.4.1
20
21
  Requires-Dist: openpyxl>=3.1.5
21
22
  Requires-Dist: pandas>=2.2.3
22
23
  Requires-Dist: pdfplumber>=0.11.0
23
- Requires-Dist: pillow>=12.0.0
24
- Requires-Dist: pydantic-settings>=2.12.0
24
+ Requires-Dist: pillow>=12.3.0
25
+ Requires-Dist: pydantic-settings>=2.14.2
25
26
  Requires-Dist: python-docx>=1.1.2
26
27
  Requires-Dist: python-dotenv>=1.2.0
27
28
  Requires-Dist: python-pptx>=1.0.2
28
29
  Requires-Dist: pytubefix>=10.0.0
29
30
  Requires-Dist: readability-lxml>=0.8.4.1
31
+ Requires-Dist: requests>=2.31.0
30
32
  Requires-Dist: tenacity>=9.0.0
31
33
  Requires-Dist: tomli>=2.0; python_version < '3.11'
32
34
  Requires-Dist: validators>=0.34.0
33
35
  Requires-Dist: youtube-transcript-api>=1.2.0
34
36
  Provides-Extra: crawl4ai
35
- Requires-Dist: crawl4ai>=0.7.0; extra == 'crawl4ai'
37
+ Requires-Dist: crawl4ai>=0.9.0; extra == 'crawl4ai'
36
38
  Provides-Extra: docling
37
- Requires-Dist: docling>=2.86.0; extra == 'docling'
39
+ Requires-Dist: docling>=2.94.0; extra == 'docling'
38
40
  Requires-Dist: peft>=0.18.0; extra == 'docling'
39
41
  Requires-Dist: transformers<5,>=4.50; extra == 'docling'
40
42
  Provides-Extra: langchain
41
- Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
43
+ Requires-Dist: langchain-core>=1.3.3; extra == 'langchain'
42
44
  Description-Content-Type: text/markdown
43
45
 
44
46
  # Content Core
@@ -59,7 +61,7 @@ Extract, process, and summarize content from URLs, files, and text through a uni
59
61
  | Category | Formats |
60
62
  |----------|---------|
61
63
  | Web | URLs, HTML pages, YouTube videos, Reddit posts |
62
- | Documents | PDF, DOCX, PPTX, XLSX, EPUB, HTML, Markdown, plain text |
64
+ | Documents | PDF, DOCX, PPTX, XLSX, ODT, ODS, ODP, EPUB, HTML, Markdown, plain text |
63
65
  | Media | MP3, WAV, M4A, FLAC, OGG (audio); MP4, AVI, MOV, MKV (video) |
64
66
 
65
67
  ## Quick Start
@@ -269,6 +271,7 @@ Content Core uses `ContentCoreConfig` powered by pydantic-settings. Settings are
269
271
  | `CCORE_URL_ENGINE` | URL extraction engine (`auto`, `simple`, `firecrawl`, `jina`, `crawl4ai`) | `auto` |
270
272
  | `CCORE_DOCUMENT_ENGINE` | Document extraction engine (`auto`, `simple`, `docling`) — `docling` raises `ConfigurationError` if the extra is not installed; `auto` falls back silently | `auto` |
271
273
  | `CCORE_AUDIO_CONCURRENCY` | Concurrent audio transcriptions (1-10) | `3` |
274
+ | `CCORE_AUDIO_SEGMENT_MINUTES` | Length of the segments long audio is split into; `0` sends the file whole | `10` |
272
275
  | `CRAWL4AI_API_URL` | Crawl4AI Docker API URL (omit for local browser mode) | - |
273
276
  | `CRAWL4AI_API_TOKEN` | Bearer token for the Crawl4AI Docker API (required by Crawl4AI >= 0.9.0) | - |
274
277
  | `FIRECRAWL_API_URL` | Custom Firecrawl API URL for self-hosted instances or Firecrawl-compatible backends (e.g. fastCRW) | - |
@@ -280,6 +283,8 @@ Content Core uses `ContentCoreConfig` powered by pydantic-settings. Settings are
280
283
  | `CCORE_STT_MODEL` | Speech-to-text model | - |
281
284
  | `CCORE_STT_TIMEOUT` | Speech-to-text timeout in seconds | - |
282
285
  | `CCORE_YOUTUBE_LANGUAGES` | Preferred YouTube transcript languages | - |
286
+ | `CCORE_YOUTUBE_COOKIES_FILE` | Path to a Netscape `cookies.txt` for YouTube transcripts (missing file raises `ConfigurationError`) | - |
287
+ | `CCORE_YOUTUBE_PROXY` | Proxy URL for YouTube transcripts, e.g. `http://user:pass@host:port` | - |
283
288
 
284
289
  API keys for external services are set via their standard environment variables (e.g., `OPENAI_API_KEY`, `FIRECRAWL_API_KEY`, `JINA_API_KEY`).
285
290
 
@@ -287,6 +292,10 @@ API keys for external services are set via their standard environment variables
287
292
 
288
293
  Content Core reads standard `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` environment variables automatically. No additional configuration is needed.
289
294
 
295
+ ### YouTube on Blocked Networks
296
+
297
+ If YouTube extraction fails with `IpBlocked`/`RequestBlocked` on a flagged IP (cloud hosts, VPNs), either point `CCORE_YOUTUBE_COOKIES_FILE` at a `cookies.txt` exported from a logged-in browser (re-export it when the cookies expire), or set `CCORE_YOUTUBE_PROXY` to a **residential** proxy — datacenter proxies get blocked or CAPTCHA'd. See [docs/usage.md](docs/usage.md#youtube-on-blocked-networks).
298
+
290
299
  ## Optional Dependencies
291
300
 
292
301
  ```bash
@@ -16,7 +16,7 @@ Extract, process, and summarize content from URLs, files, and text through a uni
16
16
  | Category | Formats |
17
17
  |----------|---------|
18
18
  | Web | URLs, HTML pages, YouTube videos, Reddit posts |
19
- | Documents | PDF, DOCX, PPTX, XLSX, EPUB, HTML, Markdown, plain text |
19
+ | Documents | PDF, DOCX, PPTX, XLSX, ODT, ODS, ODP, EPUB, HTML, Markdown, plain text |
20
20
  | Media | MP3, WAV, M4A, FLAC, OGG (audio); MP4, AVI, MOV, MKV (video) |
21
21
 
22
22
  ## Quick Start
@@ -226,6 +226,7 @@ Content Core uses `ContentCoreConfig` powered by pydantic-settings. Settings are
226
226
  | `CCORE_URL_ENGINE` | URL extraction engine (`auto`, `simple`, `firecrawl`, `jina`, `crawl4ai`) | `auto` |
227
227
  | `CCORE_DOCUMENT_ENGINE` | Document extraction engine (`auto`, `simple`, `docling`) — `docling` raises `ConfigurationError` if the extra is not installed; `auto` falls back silently | `auto` |
228
228
  | `CCORE_AUDIO_CONCURRENCY` | Concurrent audio transcriptions (1-10) | `3` |
229
+ | `CCORE_AUDIO_SEGMENT_MINUTES` | Length of the segments long audio is split into; `0` sends the file whole | `10` |
229
230
  | `CRAWL4AI_API_URL` | Crawl4AI Docker API URL (omit for local browser mode) | - |
230
231
  | `CRAWL4AI_API_TOKEN` | Bearer token for the Crawl4AI Docker API (required by Crawl4AI >= 0.9.0) | - |
231
232
  | `FIRECRAWL_API_URL` | Custom Firecrawl API URL for self-hosted instances or Firecrawl-compatible backends (e.g. fastCRW) | - |
@@ -237,6 +238,8 @@ Content Core uses `ContentCoreConfig` powered by pydantic-settings. Settings are
237
238
  | `CCORE_STT_MODEL` | Speech-to-text model | - |
238
239
  | `CCORE_STT_TIMEOUT` | Speech-to-text timeout in seconds | - |
239
240
  | `CCORE_YOUTUBE_LANGUAGES` | Preferred YouTube transcript languages | - |
241
+ | `CCORE_YOUTUBE_COOKIES_FILE` | Path to a Netscape `cookies.txt` for YouTube transcripts (missing file raises `ConfigurationError`) | - |
242
+ | `CCORE_YOUTUBE_PROXY` | Proxy URL for YouTube transcripts, e.g. `http://user:pass@host:port` | - |
240
243
 
241
244
  API keys for external services are set via their standard environment variables (e.g., `OPENAI_API_KEY`, `FIRECRAWL_API_KEY`, `JINA_API_KEY`).
242
245
 
@@ -244,6 +247,10 @@ API keys for external services are set via their standard environment variables
244
247
 
245
248
  Content Core reads standard `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` environment variables automatically. No additional configuration is needed.
246
249
 
250
+ ### YouTube on Blocked Networks
251
+
252
+ If YouTube extraction fails with `IpBlocked`/`RequestBlocked` on a flagged IP (cloud hosts, VPNs), either point `CCORE_YOUTUBE_COOKIES_FILE` at a `cookies.txt` exported from a logged-in browser (re-export it when the cookies expire), or set `CCORE_YOUTUBE_PROXY` to a **residential** proxy — datacenter proxies get blocked or CAPTCHA'd. See [docs/usage.md](docs/usage.md#youtube-on-blocked-networks).
253
+
247
254
  ## Optional Dependencies
248
255
 
249
256
  ```bash
@@ -154,6 +154,7 @@ content-core config delete llm_provider
154
154
  |-----|-------------|---------|
155
155
  | `audio_concurrency` | Parallel transcription limit (1-10) | `3` |
156
156
  | `audio_model` | Override STT model | — |
157
+ | `audio_segment_minutes` | Audio segment length in minutes; `0` disables splitting | `10` |
157
158
  | `crawl4ai_api_url` | Crawl4AI Docker API URL (omit for local mode) | — |
158
159
  | `crawl4ai_api_token` | Bearer token for the Crawl4AI Docker API (required by Crawl4AI >= 0.9.0) | — |
159
160
  | `audio_provider` | Override STT provider | — |
@@ -106,6 +106,14 @@ The `document_engine` setting controls document processing. When set to `auto` (
106
106
  - File: `document/xlsx.py`
107
107
  - Extracts data from Excel spreadsheets
108
108
 
109
+ ### OpenDocument (ODT/ODS/ODP)
110
+
111
+ - File: `document/odf.py`
112
+ - Uses odfpy for LibreOffice/OpenOffice formats
113
+ - ODT: paragraphs, headings (by outline level), lists and tables as markdown
114
+ - ODS: one markdown table per sheet, preceded by `# Sheet: <sheet name>` (as for XLSX)
115
+ - ODP: one block per slide (`# Slide N` followed by its text frames, as for PPTX)
116
+
109
117
  ### Docling (Optional)
110
118
 
111
119
  - File: `document/docling.py`
@@ -160,6 +168,7 @@ Located in `src/content_core/processors/media/`.
160
168
  - Detection methods:
161
169
  - Binary signature matching for PDF, images, audio, video, and archives
162
170
  - ZIP structure inspection for Office formats (DOCX, XLSX, PPTX) and EPUB
171
+ - OpenDocument (ODT, ODS, ODP) identified by the ZIP's `mimetype` member
163
172
  - Content analysis for text-based formats (HTML, XML, JSON, YAML, CSV, Markdown)
164
173
  - Reads only the first 512 bytes for binary signatures and 1024 bytes for text content analysis
165
174
  - Works regardless of file extension
@@ -67,6 +67,9 @@ result = await content_core.extract_content(file_path="slides.pptx")
67
67
  # Excel
68
68
  result = await content_core.extract_content(file_path="data.xlsx")
69
69
 
70
+ # OpenDocument (LibreOffice): .odt, .ods, .odp
71
+ result = await content_core.extract_content(file_path="report.odt")
72
+
70
73
  # Use Docling engine for richer parsing
71
74
  from content_core import ContentCoreConfig
72
75
  config = ContentCoreConfig(document_engine="docling", docling_output_format="html")
@@ -95,6 +98,55 @@ result = await content_core.extract_content(url="https://www.youtube.com/watch?v
95
98
  print(result.content) # Video transcript
96
99
  ```
97
100
 
101
+ #### YouTube on blocked networks
102
+
103
+ On IP-flagged networks (cloud hosts, some ISPs, VPNs) YouTube refuses transcript
104
+ requests even for public videos with captions — the extraction fails with
105
+ `IpBlocked`/`RequestBlocked` from `youtube-transcript-api`. Two settings help;
106
+ both are off by default, and with neither set the behavior is unchanged.
107
+
108
+ **Browser cookies** (`youtube_cookies_file` / `CCORE_YOUTUBE_COOKIES_FILE`) —
109
+ path to a Netscape-format `cookies.txt` exported from a browser that is logged
110
+ in to YouTube (use a "Get cookies.txt"-style browser extension, or let yt-dlp
111
+ dump its cookie jar: `yt-dlp --cookies-from-browser chrome --cookies cookies.txt
112
+ --skip-download <any YouTube URL>`). This unblocks the transcript path on the
113
+ same IP.
114
+
115
+ - Cookies are credentials: keep the file private. Content Core logs only the
116
+ file path (at debug level), never cookie values.
117
+ - Session cookies expire. When extraction starts failing again, re-export the
118
+ file.
119
+ - A missing, unreadable or malformed file raises `ConfigurationError` — Content
120
+ Core does not silently fall back to anonymous requests.
121
+ - Cookies apply to the `youtube-transcript-api` path only; the `pytubefix`
122
+ fallback has no cookies-file support.
123
+
124
+ **Proxy** (`youtube_proxy` / `CCORE_YOUTUBE_PROXY`) — a proxy URL such as
125
+ `http://user:pass@host:port`, used by every YouTube request: the
126
+ `youtube-transcript-api` transcript, the `pytubefix` fallback and the video
127
+ title lookup. The standard `HTTP_PROXY`/`HTTPS_PROXY` variables keep working
128
+ too; this setting scopes the proxy to YouTube.
129
+
130
+ Use a **residential** proxy — preferably a rotating one (e.g. Webshare
131
+ "Residential"), so a blocked IP is swapped on the next retry. In testing
132
+ against a blocked network, **datacenter proxies failed** (`IpBlocked`,
133
+ `RequestBlocked` or a Google CAPTCHA), while rotating and sticky residential
134
+ proxies returned the full transcript. See the test matrix in
135
+ [issue #46](https://github.com/lfnovo/content-core/issues/46).
136
+
137
+ ```python
138
+ config = ContentCoreConfig(
139
+ youtube_cookies_file="~/secrets/youtube-cookies.txt",
140
+ youtube_proxy="http://user:pass@p.webshare.io:80",
141
+ )
142
+ result = await content_core.extract_content(url="https://youtu.be/dQw4w9WgXcQ", config=config)
143
+ ```
144
+
145
+ ```bash
146
+ CCORE_YOUTUBE_COOKIES_FILE=/path/to/cookies.txt
147
+ CCORE_YOUTUBE_PROXY=http://user:pass@host:port
148
+ ```
149
+
98
150
  ### Local HTML Files
99
151
 
100
152
  ```python
@@ -177,6 +229,8 @@ CCORE_STT_PROVIDER=openai
177
229
  CCORE_STT_MODEL=whisper-1
178
230
  CCORE_STT_TIMEOUT=3600
179
231
  CCORE_YOUTUBE_LANGUAGES=en,pt
232
+ CCORE_YOUTUBE_COOKIES_FILE=/path/to/cookies.txt
233
+ CCORE_YOUTUBE_PROXY=http://user:pass@host:port
180
234
  CCORE_FIRECRAWL_PROXY=auto
181
235
  CCORE_FIRECRAWL_WAIT_FOR=3000
182
236
  FIRECRAWL_API_URL=http://localhost:3002
@@ -217,12 +271,12 @@ FIRECRAWL_API_KEY=anykey
217
271
 
218
272
  ### Document Engines
219
273
 
220
- The `document_engine` setting controls how files (PDF, DOCX, PPTX, XLSX, HTML) are processed:
274
+ The `document_engine` setting controls how files (PDF, DOCX, PPTX, XLSX, ODT, ODS, ODP, HTML) are processed:
221
275
 
222
276
  | Engine | Description | Requirements |
223
277
  |--------|-------------|-------------|
224
278
  | `auto` (default) | Tries Docling first, falls back to simple | Depends on installed extras |
225
- | `simple` | pdfplumber for PDF, fast-ebook for EPUB, python-docx/openpyxl/python-pptx for Office, markdownify for HTML | None (included) |
279
+ | `simple` | pdfplumber for PDF, fast-ebook for EPUB, python-docx/openpyxl/python-pptx for Office, odfpy for OpenDocument, markdownify for HTML | None (included) |
226
280
  | `docling` | Docling library for rich document parsing | `pip install content-core[docling]` |
227
281
 
228
282
  `auto` is a preference: it uses Docling when installed and silently falls back to `simple` otherwise. `docling` is a requirement: if the extra is not installed, extraction raises `ConfigurationError` instead of silently producing simple-engine output. Install it with `pip install content-core[docling]`, or set `CCORE_DOCUMENT_ENGINE=simple` (or `auto`) to proceed without it.
@@ -305,6 +359,27 @@ config = ContentCoreConfig(audio_concurrency=5)
305
359
 
306
360
  Higher values speed up processing of long files but may hit API rate limits.
307
361
 
362
+ ### Segment Length
363
+
364
+ Long audio is split into 10-minute segments, a length that fits the upload
365
+ size limits of the cloud STT APIs (OpenAI rejects uploads over 25 MB). A
366
+ self-hosted endpoint has no such limit, and every cut costs it the context
367
+ that falls across it. Set a longer segment, or `0` to send the file whole:
368
+
369
+ ```bash
370
+ CCORE_AUDIO_SEGMENT_MINUTES=0
371
+ ```
372
+
373
+ Or in code:
374
+
375
+ ```python
376
+ config = ContentCoreConfig(audio_segment_minutes=0)
377
+ ```
378
+
379
+ Keep the default when using a cloud provider: with splitting off or a long
380
+ segment, a long file can exceed the provider's upload limit and the
381
+ transcription fails.
382
+
308
383
  ### Custom STT Models
309
384
 
310
385
  Override the speech-to-text provider and model per call:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "content-core"
3
- version = "2.1.0"
3
+ version = "2.2.0"
4
4
  description = "Extract what matters from any media source. Available as Python Library, macOS Service, CLI and MCP Server"
5
5
  readme = "README.md"
6
6
  homepage = "https://github.com/lfnovo/content-core"
@@ -9,7 +9,7 @@ authors = [
9
9
  ]
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
- "aiohttp>=3.13",
12
+ "aiohttp>=3.14.3",
13
13
  "bs4>=0.0.2",
14
14
  "esperanto>=2.26.0",
15
15
  "jinja2>=3.1.6",
@@ -27,20 +27,22 @@ dependencies = [
27
27
  "ai-prompter>=0.4.0",
28
28
  "readability-lxml>=0.8.4.1",
29
29
  "firecrawl-py>=4.13.0",
30
- "pillow>=12.0.0",
30
+ "pillow>=12.3.0",
31
31
  "pytubefix>=10.0.0",
32
32
  "fastmcp>=3.0.0",
33
33
  "tenacity>=9.0.0",
34
34
  "markdownify>=0.14.0",
35
- "pydantic-settings>=2.12.0",
35
+ "pydantic-settings>=2.14.2",
36
36
  "tomli>=2.0; python_version < '3.11'",
37
- "click>=8.3.0",
37
+ "click>=8.3.3",
38
+ "odfpy>=1.4.1",
39
+ "requests>=2.31.0",
38
40
  ]
39
41
 
40
42
  [project.optional-dependencies]
41
- docling = ["docling>=2.86.0", "peft>=0.18.0", "transformers>=4.50,<5"]
42
- crawl4ai = ["crawl4ai>=0.7.0"]
43
- langchain = ["langchain-core>=0.1.0"]
43
+ docling = ["docling>=2.94.0", "peft>=0.18.0", "transformers>=4.50,<5"]
44
+ crawl4ai = ["crawl4ai>=0.9.0"]
45
+ langchain = ["langchain-core>=1.3.3"]
44
46
 
45
47
  [project.scripts]
46
48
  content-core = "content_core.cli:cli"
@@ -60,8 +62,8 @@ package-dir = {"content_core" = "src/content_core"}
60
62
 
61
63
  [dependency-groups]
62
64
  dev = [
63
- "crawl4ai>=0.8.0",
64
- "docling>=2.86.0",
65
+ "crawl4ai>=0.9.0",
66
+ "docling>=2.94.0",
65
67
  "transformers>=4.50,<5",
66
68
  "ipykernel>=7.0.0",
67
69
  "ipywidgets>=8.0.0",
@@ -109,6 +109,7 @@ def config():
109
109
  \b
110
110
  Available keys:
111
111
  audio_concurrency Parallel transcription limit (1-10, default: 3)
112
+ audio_segment_minutes Audio segment length in minutes, 0 disables splitting (default: 10)
112
113
  crawl4ai_api_url Crawl4AI Docker API URL (default: none, uses local mode)
113
114
  crawl4ai_api_token Bearer token for the Crawl4AI Docker API (required by Crawl4AI >= 0.9.0)
114
115
  audio_model Override STT model
@@ -1,15 +1,8 @@
1
1
  """The public exception taxonomy for ``extract_content``.
2
2
 
3
3
  Per the raise/degrade boundary in ``ARCHITECTURE.md``, total failure raises
4
- one of these; degradation only exists within a single source.
5
-
6
- The taxonomy is complete, its raise sites are not yet: ``NotFoundError``,
7
- ``NetworkError``, ``ExternalServiceError`` and ``FileOperationError`` have
8
- no raise site in this release -- the failures they name still escape as
9
- untyped exceptions (an ``aiohttp.ClientError`` out of a download, say)
10
- until the migration in #60 lands. ``ContentCoreError`` catches every typed
11
- failure, so it is the right single handler -- but it does not yet cover the
12
- whole library.
4
+ one of these; degradation only exists within a single source. Every one
5
+ derives from ``ContentCoreError``, the single handler for typed failures.
13
6
  """
14
7
 
15
8
 
@@ -18,9 +18,11 @@ Usage:
18
18
  ...
19
19
  """
20
20
 
21
+ import re
21
22
  from typing import Callable, Optional
22
23
 
23
24
  import aiohttp
25
+ import youtube_transcript_api as yta # type: ignore
24
26
  from tenacity import (
25
27
  RetryError,
26
28
  retry,
@@ -29,7 +31,15 @@ from tenacity import (
29
31
  wait_random_exponential,
30
32
  )
31
33
 
32
- from content_core.common.exceptions import NoTranscriptFound, NotFoundError
34
+ from content_core.common.exceptions import (
35
+ ConfigurationError,
36
+ FileOperationError,
37
+ InvalidInputError,
38
+ NetworkError,
39
+ NoTranscriptFound,
40
+ NotFoundError,
41
+ UnsupportedTypeException,
42
+ )
33
43
  from content_core.logging import logger
34
44
 
35
45
  # Default retry configurations per operation type
@@ -61,6 +71,10 @@ def get_retry_config(operation_type: str) -> dict:
61
71
  NON_RETRYABLE_EXCEPTIONS = (
62
72
  NoTranscriptFound,
63
73
  NotFoundError,
74
+ ConfigurationError,
75
+ UnsupportedTypeException,
76
+ InvalidInputError,
77
+ FileOperationError,
64
78
  ValueError,
65
79
  TypeError,
66
80
  KeyError,
@@ -79,6 +93,24 @@ def is_retryable_exception(exception: BaseException) -> bool:
79
93
  return False
80
94
 
81
95
  # Always retry network-related errors
96
+ if isinstance(exception, NetworkError):
97
+ return True
98
+
99
+ # youtube-transcript-api: a blocked request (IpBlocked is a subclass) can
100
+ # succeed on the next attempt through a rotating proxy; every other
101
+ # CouldNotRetrieveTranscript names a state of the video (unavailable,
102
+ # transcripts disabled, age-restricted...) that a retry will not change.
103
+ # YouTubeRequestFailed wraps an HTTP error: judged by its status, which the
104
+ # library keeps only as the leading code of ``reason`` ("429 Client Error").
105
+ if isinstance(exception, yta.RequestBlocked):
106
+ return True
107
+ if isinstance(exception, yta.YouTubeRequestFailed):
108
+ match = re.match(r"\s*(\d{3})\b", getattr(exception, "reason", "") or "")
109
+ if match:
110
+ status = int(match.group(1))
111
+ return status >= 500 or status == 429
112
+ elif isinstance(exception, yta.CouldNotRetrieveTranscript):
113
+ return False
82
114
  if isinstance(exception, (aiohttp.ClientError, ConnectionError, TimeoutError, OSError)):
83
115
  # But not if it's a client error (4xx) - those are usually permanent
84
116
  if isinstance(exception, aiohttp.ClientResponseError):
@@ -62,6 +62,7 @@ class ContentCoreConfig(BaseSettings):
62
62
  audio_provider: str = "openai"
63
63
  audio_model: Optional[str] = None
64
64
  audio_concurrency: int = Field(default=3, ge=1, le=10)
65
+ audio_segment_minutes: int = Field(default=10, ge=0)
65
66
 
66
67
  # Crawl4AI
67
68
  crawl4ai_api_url: Optional[str] = None
@@ -84,6 +85,8 @@ class ContentCoreConfig(BaseSettings):
84
85
 
85
86
  # YouTube
86
87
  youtube_languages: list[str] = Field(default=["en", "es", "pt"])
88
+ youtube_cookies_file: Optional[str] = None # path to a Netscape cookies.txt
89
+ youtube_proxy: Optional[str] = None # proxy URL, e.g. http://user:pass@host:port
87
90
 
88
91
  # Docling
89
92
  docling_output_format: str = "markdown"
@@ -17,6 +17,7 @@ class FileDetector:
17
17
  # Configuration constants for binary/text detection
18
18
  SIGNATURE_READ_SIZE = 512 # Bytes to read for binary signature detection
19
19
  TEXT_READ_SIZE = 1024 # Bytes to read for text content analysis
20
+ ZIP_MIMETYPE_READ_SIZE = 256 # Bytes to read from a ZIP `mimetype` member (ODF/EPUB)
20
21
 
21
22
  # Configuration constants for CSV detection
22
23
  CSV_MAX_FIELD_LENGTH = 100 # Maximum average field length for CSV (longer suggests prose)
@@ -149,6 +150,11 @@ class FileDetector:
149
150
  '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
150
151
  '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
151
152
  '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
153
+
154
+ # OpenDocument formats
155
+ '.odt': 'application/vnd.oasis.opendocument.text',
156
+ '.ods': 'application/vnd.oasis.opendocument.spreadsheet',
157
+ '.odp': 'application/vnd.oasis.opendocument.presentation',
152
158
 
153
159
  # E-books
154
160
  '.epub': 'application/epub+zip',
@@ -283,11 +289,20 @@ class FileDetector:
283
289
  return None
284
290
 
285
291
  async def _detect_zip_format(self, file_path: Path) -> Optional[str]:
286
- """Detect specific ZIP-based format (DOCX, XLSX, PPTX, EPUB)."""
292
+ """Detect specific ZIP-based format (DOCX, XLSX, PPTX, EPUB, ODF)."""
287
293
  try:
288
294
  with zipfile.ZipFile(file_path, 'r') as zf:
289
295
  namelist = zf.namelist()
290
-
296
+
297
+ # OpenDocument stores its exact MIME type in a `mimetype` member
298
+ # (bounded read: the member is a short constant string)
299
+ if 'mimetype' in namelist:
300
+ with zf.open('mimetype') as member:
301
+ raw = member.read(self.ZIP_MIMETYPE_READ_SIZE)
302
+ odf_mime = raw.decode('ascii', errors='replace').strip()
303
+ if odf_mime.startswith('application/vnd.oasis.opendocument'):
304
+ return odf_mime
305
+
291
306
  # Check for specific content patterns
292
307
  for pattern, mime_type in self.zip_content_patterns.items():
293
308
  if any(name.startswith(pattern) for name in namelist):