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.
- {content_core-2.1.0 → content_core-2.2.0}/.claude-plugin/plugin.json +1 -1
- {content_core-2.1.0 → content_core-2.2.0}/.codex-plugin/plugin.json +1 -1
- {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/claude-code-review.yml +5 -5
- {content_core-2.1.0 → content_core-2.2.0}/AGENTS.md +5 -4
- {content_core-2.1.0 → content_core-2.2.0}/CHANGELOG.md +23 -0
- {content_core-2.1.0 → content_core-2.2.0}/PKG-INFO +18 -9
- {content_core-2.1.0 → content_core-2.2.0}/README.md +8 -1
- {content_core-2.1.0 → content_core-2.2.0}/docs/cli.md +1 -0
- {content_core-2.1.0 → content_core-2.2.0}/docs/processors.md +9 -0
- {content_core-2.1.0 → content_core-2.2.0}/docs/usage.md +77 -2
- {content_core-2.1.0 → content_core-2.2.0}/pyproject.toml +12 -10
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/cli.py +1 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/exceptions.py +2 -9
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/retry.py +33 -1
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/config.py +3 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/identification/file_detector.py +17 -2
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/extraction.py +41 -19
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/__init__.py +24 -1
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/epub.py +2 -1
- content_core-2.2.0/src/content_core/processors/document/odf.py +218 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/pdf.py +3 -2
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/media/audio.py +75 -43
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/media/video.py +44 -59
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/text.py +3 -2
- content_core-2.2.0/src/content_core/processors/url/__init__.py +217 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/url/bs4.py +10 -12
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/url/crawl4ai.py +27 -8
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/url/firecrawl.py +3 -3
- content_core-2.2.0/src/content_core/processors/url/youtube.py +371 -0
- content_core-2.2.0/tests/input_content/file.odp +0 -0
- content_core-2.2.0/tests/input_content/file.ods +0 -0
- content_core-2.2.0/tests/input_content/file.odt +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/integration/test_extraction.py +54 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_config_v2.py +21 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_crawl4ai_docker.py +15 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_epub_extraction.py +8 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_exceptions.py +6 -13
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_file_detector.py +106 -1
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_media_pipeline.py +188 -1
- content_core-2.2.0/tests/unit/test_office_extraction.py +340 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_pdf_extraction.py +22 -1
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_retry.py +53 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_routing.py +116 -1
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_text_processing.py +13 -0
- content_core-2.2.0/tests/unit/test_url_engine_select.py +519 -0
- content_core-2.2.0/tests/unit/test_youtube_parsing.py +517 -0
- content_core-2.2.0/uv.lock +7720 -0
- content_core-2.1.0/src/content_core/processors/url/__init__.py +0 -175
- content_core-2.1.0/src/content_core/processors/url/youtube.py +0 -216
- content_core-2.1.0/tests/unit/test_office_extraction.py +0 -99
- content_core-2.1.0/tests/unit/test_url_engine_select.py +0 -206
- content_core-2.1.0/tests/unit/test_youtube_parsing.py +0 -163
- content_core-2.1.0/uv.lock +0 -6676
- {content_core-2.1.0 → content_core-2.2.0}/.agent-smith/index.json +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.agents/plugins/marketplace.json +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.claude-plugin/marketplace.json +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.gitattributes +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/claude.yml +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/create-tag.yml +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/package.yml +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/publish.yml +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.github/workflows/test.yml +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.gitignore +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.maintainer/PROFILE.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.maintainer/README.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.maintainer/gotchas.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.maintainer/profile.toml +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.maintainer/release/runbook.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.maintainer/release/test-matrix.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/.python-version +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/ARCHITECTURE.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/CLAUDE.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/CONTRIBUTING.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/LICENSE +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/Makefile +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/REFACTOR.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/TEST_PLAN.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/docs/mcp.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/examples/main.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/prompts/content/cleanup.jinja +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/prompts/content/summarize.jinja +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/skills/content-core/SKILL.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/__init__.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/__init__.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/messages.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/state.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/common/types.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/__init__.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/extraction/__init__.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/identification/__init__.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/summary/__init__.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/summary/core.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/logging.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/mcp/__init__.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/mcp/server.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/models.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/docling.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/docx.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/pptx.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/document/xlsx.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/media/__init__.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/url/jina.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/processors/url/reddit.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/py.typed +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/templated_message.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/tools/__init__.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/tools/extract.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/src/content_core/tools/summarize.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/conftest.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/__init__.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/test_docling.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/test_media.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/test_remote.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/test_url_engines.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/e2e/test_youtube.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.docx +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.epub +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.md +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.mp3 +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.mp4 +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.pdf +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.pptx +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.txt +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/file.xlsx +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/input_content/new_pdf.pdf +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/integration/conftest.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/integration/test_cli_v2.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_audio_concurrency.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_cli.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_config_file.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_docling_extraction.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_file_detector_critical.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_file_detector_performance.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_html_file_extraction.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_logging.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_mcp_server_version.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_mcp_v2.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_models_v2.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_pdf_helpers.py +0 -0
- {content_core-2.1.0 → content_core-2.2.0}/tests/unit/test_reddit_extraction.py +0 -0
|
@@ -12,11 +12,11 @@ on:
|
|
|
12
12
|
|
|
13
13
|
jobs:
|
|
14
14
|
claude-review:
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
|
|
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.
|
|
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.
|
|
9
|
+
Requires-Dist: aiohttp>=3.14.3
|
|
10
10
|
Requires-Dist: bs4>=0.0.2
|
|
11
|
-
Requires-Dist: click>=8.3.
|
|
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.
|
|
24
|
-
Requires-Dist: pydantic-settings>=2.
|
|
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.
|
|
37
|
+
Requires-Dist: crawl4ai>=0.9.0; extra == 'crawl4ai'
|
|
36
38
|
Provides-Extra: docling
|
|
37
|
-
Requires-Dist: docling>=2.
|
|
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>=
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
35
|
+
"pydantic-settings>=2.14.2",
|
|
36
36
|
"tomli>=2.0; python_version < '3.11'",
|
|
37
|
-
"click>=8.3.
|
|
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.
|
|
42
|
-
crawl4ai = ["crawl4ai>=0.
|
|
43
|
-
langchain = ["langchain-core>=
|
|
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.
|
|
64
|
-
"docling>=2.
|
|
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
|
|
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"
|
{content_core-2.1.0 → content_core-2.2.0}/src/content_core/content/identification/file_detector.py
RENAMED
|
@@ -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):
|