content-core 2.2.1__tar.gz → 2.3.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.2.1 → content_core-2.3.0}/.claude-plugin/plugin.json +1 -1
- {content_core-2.2.1 → content_core-2.3.0}/.codex-plugin/plugin.json +1 -1
- {content_core-2.2.1 → content_core-2.3.0}/AGENTS.md +2 -0
- {content_core-2.2.1 → content_core-2.3.0}/CHANGELOG.md +5 -0
- {content_core-2.2.1 → content_core-2.3.0}/PKG-INFO +2 -1
- {content_core-2.2.1 → content_core-2.3.0}/README.md +1 -0
- {content_core-2.2.1 → content_core-2.3.0}/docs/cli.md +7 -0
- {content_core-2.2.1 → content_core-2.3.0}/docs/usage.md +31 -0
- {content_core-2.2.1 → content_core-2.3.0}/pyproject.toml +1 -1
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/cli.py +7 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/config.py +16 -1
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/document/docling.py +62 -1
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_config_file.py +18 -0
- content_core-2.3.0/tests/unit/test_docling_vision_api.py +138 -0
- {content_core-2.2.1 → content_core-2.3.0}/uv.lock +1 -1
- {content_core-2.2.1 → content_core-2.3.0}/.agent-smith/index.json +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.agents/plugins/marketplace.json +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.claude-plugin/marketplace.json +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.gitattributes +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.github/workflows/claude-code-review.yml +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.github/workflows/claude.yml +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.github/workflows/create-tag.yml +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.github/workflows/package.yml +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.github/workflows/publish.yml +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.github/workflows/test.yml +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.gitignore +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.maintainer/PROFILE.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.maintainer/README.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.maintainer/gotchas.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.maintainer/profile.toml +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.maintainer/release/runbook.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.maintainer/release/test-matrix.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/.python-version +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/ARCHITECTURE.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/CLAUDE.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/CONTRIBUTING.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/LICENSE +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/Makefile +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/REFACTOR.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/TEST_PLAN.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/docs/mcp.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/docs/processors.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/examples/main.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/prompts/content/cleanup.jinja +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/prompts/content/summarize.jinja +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/skills/content-core/SKILL.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/common/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/common/exceptions.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/common/http.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/common/messages.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/common/retry.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/common/state.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/common/types.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/content/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/content/extraction/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/content/identification/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/content/identification/file_detector.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/content/summary/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/content/summary/core.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/extraction.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/logging.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/mcp/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/mcp/server.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/models.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/document/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/document/docx.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/document/epub.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/document/odf.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/document/pdf.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/document/pptx.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/document/xlsx.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/media/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/media/audio.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/media/video.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/text.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/url/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/url/bs4.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/url/crawl4ai.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/url/firecrawl.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/url/jina.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/url/reddit.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/processors/url/youtube.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/py.typed +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/templated_message.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/tools/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/tools/extract.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/src/content_core/tools/summarize.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/conftest.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/e2e/__init__.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/e2e/test_docling.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/e2e/test_media.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/e2e/test_remote.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/e2e/test_url_engines.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/e2e/test_youtube.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.docx +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.epub +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.md +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.mp3 +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.mp4 +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.odp +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.ods +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.odt +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.pdf +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.pptx +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.txt +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/file.xlsx +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/input_content/new_pdf.pdf +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/integration/conftest.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/integration/test_cli_v2.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/integration/test_extraction.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_audio_concurrency.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_cli.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_config_v2.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_crawl4ai_docker.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_docling_extraction.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_epub_extraction.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_exceptions.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_file_detector.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_file_detector_critical.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_file_detector_performance.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_guarded_http.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_html_file_extraction.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_logging.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_mcp_server_version.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_mcp_v2.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_media_pipeline.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_models_v2.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_office_extraction.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_pdf_extraction.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_pdf_helpers.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_reddit_extraction.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_retry.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_routing.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_text_processing.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_url_engine_select.py +0 -0
- {content_core-2.2.1 → content_core-2.3.0}/tests/unit/test_youtube_parsing.py +0 -0
|
@@ -134,6 +134,8 @@ Key settings: `url_engine`, `document_engine`, `audio_provider`, `audio_model`,
|
|
|
134
134
|
|
|
135
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.
|
|
136
136
|
|
|
137
|
+
`docling_vision_url` (plus `docling_vision_model`, `_api_key`, `_prompt`, `_max_tokens`, `_timeout`, `_token_param`) sends Docling's image descriptions to an OpenAI-compatible endpoint instead of the inline model. It only applies when `docling_vision` is on; without the URL, nothing is sent over the network.
|
|
138
|
+
|
|
137
139
|
Priority: constructor args > env vars (`CCORE_*`) > config file (`~/.content-core/config.toml`) > defaults
|
|
138
140
|
|
|
139
141
|
## Code Style
|
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.3.0] - 2026-11-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Docling image descriptions can use any OpenAI-compatible chat completions endpoint (vLLM, Ollama, LM Studio, MLX servers...) instead of Docling's small inline model (#111, building on #104 by PhantombrainM). Set `docling_vision_url` (`CCORE_DOCLING_VISION_URL`), with optional `docling_vision_model`, `docling_vision_api_key`, `docling_vision_prompt`, `docling_vision_max_tokens` (default 400), `docling_vision_timeout` (default 20s) and `docling_vision_token_param` (`max_completion_tokens` or `max_tokens`). Applies only when `docling_vision` is on; without the URL, behavior is unchanged and nothing leaves the machine. Chart data extraction still uses Docling's own model. All settings work from the constructor, env vars and `content-core config set`.
|
|
14
|
+
|
|
10
15
|
## [2.2.1] - 2026-11-03
|
|
11
16
|
|
|
12
17
|
### Security
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: content-core
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -285,6 +285,7 @@ Content Core uses `ContentCoreConfig` powered by pydantic-settings. Settings are
|
|
|
285
285
|
| `CCORE_YOUTUBE_LANGUAGES` | Preferred YouTube transcript languages | - |
|
|
286
286
|
| `CCORE_YOUTUBE_COOKIES_FILE` | Path to a Netscape `cookies.txt` for YouTube transcripts (missing file raises `ConfigurationError`) | - |
|
|
287
287
|
| `CCORE_YOUTUBE_PROXY` | Proxy URL for YouTube transcripts, e.g. `http://user:pass@host:port` | - |
|
|
288
|
+
| `CCORE_DOCLING_VISION_URL` | OpenAI-compatible endpoint (vLLM, Ollama, LM Studio...) for Docling image descriptions; unset keeps Docling's inline model. Related: `CCORE_DOCLING_VISION_MODEL`, `_API_KEY`, `_PROMPT`, `_MAX_TOKENS`, `_TIMEOUT`, `_TOKEN_PARAM` ([details](docs/usage.md#image-descriptions-via-an-openai-compatible-endpoint)) | - |
|
|
288
289
|
|
|
289
290
|
API keys for external services are set via their standard environment variables (e.g., `OPENAI_API_KEY`, `FIRECRAWL_API_KEY`, `JINA_API_KEY`).
|
|
290
291
|
|
|
@@ -240,6 +240,7 @@ Content Core uses `ContentCoreConfig` powered by pydantic-settings. Settings are
|
|
|
240
240
|
| `CCORE_YOUTUBE_LANGUAGES` | Preferred YouTube transcript languages | - |
|
|
241
241
|
| `CCORE_YOUTUBE_COOKIES_FILE` | Path to a Netscape `cookies.txt` for YouTube transcripts (missing file raises `ConfigurationError`) | - |
|
|
242
242
|
| `CCORE_YOUTUBE_PROXY` | Proxy URL for YouTube transcripts, e.g. `http://user:pass@host:port` | - |
|
|
243
|
+
| `CCORE_DOCLING_VISION_URL` | OpenAI-compatible endpoint (vLLM, Ollama, LM Studio...) for Docling image descriptions; unset keeps Docling's inline model. Related: `CCORE_DOCLING_VISION_MODEL`, `_API_KEY`, `_PROMPT`, `_MAX_TOKENS`, `_TIMEOUT`, `_TOKEN_PARAM` ([details](docs/usage.md#image-descriptions-via-an-openai-compatible-endpoint)) | - |
|
|
243
244
|
|
|
244
245
|
API keys for external services are set via their standard environment variables (e.g., `OPENAI_API_KEY`, `FIRECRAWL_API_KEY`, `JINA_API_KEY`).
|
|
245
246
|
|
|
@@ -162,6 +162,13 @@ content-core config delete llm_provider
|
|
|
162
162
|
| `docling_ocr` | Enable OCR for scanned PDFs | `true` |
|
|
163
163
|
| `docling_output_format` | Docling output format | `markdown` |
|
|
164
164
|
| `docling_vision` | Enable image description + chart data extraction | `false` |
|
|
165
|
+
| `docling_vision_url` | OpenAI-compatible chat completions URL for image descriptions (see [usage](usage.md#image-descriptions-via-an-openai-compatible-endpoint)) | — |
|
|
166
|
+
| `docling_vision_model` | Model name sent to `docling_vision_url` | — |
|
|
167
|
+
| `docling_vision_api_key` | Bearer token for `docling_vision_url` | — |
|
|
168
|
+
| `docling_vision_prompt` | Prompt for image descriptions | Docling's prompt |
|
|
169
|
+
| `docling_vision_max_tokens` | Token limit per image description | `400` |
|
|
170
|
+
| `docling_vision_timeout` | Request timeout in seconds | `20` |
|
|
171
|
+
| `docling_vision_token_param` | Token limit field name (`max_completion_tokens`, `max_tokens`) | `max_completion_tokens` |
|
|
165
172
|
| `document_engine` | Document extraction engine (`auto`, `simple`, `docling`) | `auto` |
|
|
166
173
|
| `firecrawl_api_url` | Firecrawl API URL | `https://api.firecrawl.dev` |
|
|
167
174
|
| `firecrawl_proxy` | Firecrawl proxy mode (`auto`, `basic`, `stealth`) | `auto` |
|
|
@@ -476,6 +476,37 @@ extract_content(file_path="paper.pdf", engine="docling", formulas=true, pictures
|
|
|
476
476
|
|
|
477
477
|
> **Note:** Enrichment flags are only applied when `document_engine="docling"`. A warning is logged if flags are set without the Docling engine.
|
|
478
478
|
|
|
479
|
+
### Image descriptions via an OpenAI-compatible endpoint
|
|
480
|
+
|
|
481
|
+
By default, `docling_vision` describes images with Docling's small inline model, which often produces vague descriptions. You can send image descriptions to any OpenAI-compatible `/v1/chat/completions` endpoint instead (vLLM, Ollama, LM Studio, MLX servers, a hosted API...). Setting `docling_vision_url` is the switch: without it, nothing leaves the machine and the other settings below are ignored. `docling_vision` must still be on.
|
|
482
|
+
|
|
483
|
+
| Setting | Env var | What it does | Default |
|
|
484
|
+
|---------|---------|-------------|---------|
|
|
485
|
+
| `docling_vision_url` | `CCORE_DOCLING_VISION_URL` | Chat completions endpoint | — (inline model) |
|
|
486
|
+
| `docling_vision_model` | `CCORE_DOCLING_VISION_MODEL` | Model name sent in the request | — (server default) |
|
|
487
|
+
| `docling_vision_api_key` | `CCORE_DOCLING_VISION_API_KEY` | Bearer token; no `Authorization` header when unset | — |
|
|
488
|
+
| `docling_vision_prompt` | `CCORE_DOCLING_VISION_PROMPT` | Prompt sent with each image, e.g. to describe in another language | Docling's prompt |
|
|
489
|
+
| `docling_vision_max_tokens` | `CCORE_DOCLING_VISION_MAX_TOKENS` | Token limit per image | `400` |
|
|
490
|
+
| `docling_vision_timeout` | `CCORE_DOCLING_VISION_TIMEOUT` | Request timeout in seconds | `20` |
|
|
491
|
+
| `docling_vision_token_param` | `CCORE_DOCLING_VISION_TOKEN_PARAM` | Name of the token limit field: `max_completion_tokens`, or `max_tokens` for servers that don't accept the newer field | `max_completion_tokens` |
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
export CCORE_DOCLING_VISION_URL=http://localhost:11434/v1/chat/completions
|
|
495
|
+
export CCORE_DOCLING_VISION_MODEL=qwen2.5vl:7b
|
|
496
|
+
content-core extract --engine docling --pictures report.pdf
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
```python
|
|
500
|
+
config = ContentCoreConfig(
|
|
501
|
+
document_engine="docling",
|
|
502
|
+
docling_vision=True,
|
|
503
|
+
docling_vision_url="http://localhost:11434/v1/chat/completions",
|
|
504
|
+
docling_vision_model="qwen2.5vl:7b",
|
|
505
|
+
)
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Only image descriptions go to the endpoint. Chart data extraction keeps using Docling's own model, because it depends on special output tokens that a general-purpose vision model does not produce.
|
|
509
|
+
|
|
479
510
|
## Proxy Configuration
|
|
480
511
|
|
|
481
512
|
Content Core uses standard HTTP proxy environment variables:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "content-core"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.3.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"
|
|
@@ -130,6 +130,13 @@ def config():
|
|
|
130
130
|
docling_formulas Enable formula extraction (default: false)
|
|
131
131
|
docling_ocr Enable OCR for scanned PDFs (default: true)
|
|
132
132
|
docling_vision Enable image description + chart extraction (default: false)
|
|
133
|
+
docling_vision_url OpenAI-compatible chat completions URL for image descriptions (default: none, inline model)
|
|
134
|
+
docling_vision_model Model name sent to docling_vision_url
|
|
135
|
+
docling_vision_api_key Bearer token for docling_vision_url
|
|
136
|
+
docling_vision_prompt Prompt for image descriptions (default: docling's)
|
|
137
|
+
docling_vision_max_tokens Token limit per image description (default: 400)
|
|
138
|
+
docling_vision_timeout Request timeout in seconds (default: 20)
|
|
139
|
+
docling_vision_token_param Token limit field: max_completion_tokens or max_tokens (default: max_completion_tokens)
|
|
133
140
|
"""
|
|
134
141
|
pass
|
|
135
142
|
|
|
@@ -94,6 +94,19 @@ class ContentCoreConfig(BaseSettings):
|
|
|
94
94
|
docling_formulas: bool = False
|
|
95
95
|
docling_vision: bool = False
|
|
96
96
|
|
|
97
|
+
# Docling picture description via an OpenAI-compatible endpoint (vLLM,
|
|
98
|
+
# LM Studio, Ollama, ...). Only used when docling_vision is on; with no
|
|
99
|
+
# URL, docling's inline default model describes the pictures.
|
|
100
|
+
docling_vision_url: Optional[str] = None
|
|
101
|
+
docling_vision_model: Optional[str] = None
|
|
102
|
+
docling_vision_api_key: Optional[str] = None
|
|
103
|
+
docling_vision_prompt: Optional[str] = None # None keeps docling's prompt
|
|
104
|
+
docling_vision_max_tokens: int = Field(default=400, ge=1)
|
|
105
|
+
docling_vision_timeout: float = Field(default=20.0, gt=0)
|
|
106
|
+
docling_vision_token_param: Literal["max_completion_tokens", "max_tokens"] = (
|
|
107
|
+
"max_completion_tokens"
|
|
108
|
+
)
|
|
109
|
+
|
|
97
110
|
@classmethod
|
|
98
111
|
def settings_customise_sources(
|
|
99
112
|
cls,
|
|
@@ -155,7 +168,7 @@ def _write_config_file(data: dict) -> None:
|
|
|
155
168
|
for key, value in sorted(data.items()):
|
|
156
169
|
if isinstance(value, bool):
|
|
157
170
|
lines.append(f"{key} = {str(value).lower()}")
|
|
158
|
-
elif isinstance(value, int):
|
|
171
|
+
elif isinstance(value, (int, float)):
|
|
159
172
|
lines.append(f"{key} = {value}")
|
|
160
173
|
elif isinstance(value, list):
|
|
161
174
|
items = ", ".join(f'"{_escape_toml_string(str(v))}"' for v in value)
|
|
@@ -206,6 +219,8 @@ def config_set(key: str, value: str) -> None:
|
|
|
206
219
|
annotation = field.annotation
|
|
207
220
|
if annotation is int or (hasattr(annotation, "__origin__") and annotation is int):
|
|
208
221
|
data[key] = int(value)
|
|
222
|
+
elif annotation is float:
|
|
223
|
+
data[key] = float(value)
|
|
209
224
|
elif annotation is bool:
|
|
210
225
|
token = value.lower()
|
|
211
226
|
if token not in _TRUE_TOKENS | _FALSE_TOKENS:
|
|
@@ -2,20 +2,28 @@
|
|
|
2
2
|
Docling-based document extraction processor.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from urllib.parse import urlsplit
|
|
7
|
+
|
|
5
8
|
from content_core.config import ContentCoreConfig
|
|
6
9
|
from content_core.common.messages import DOCLING_MISSING_MESSAGE
|
|
7
10
|
from content_core.common.state import ExtractionOutput
|
|
11
|
+
from content_core.logging import logger
|
|
8
12
|
|
|
9
13
|
DOCLING_AVAILABLE = False
|
|
10
14
|
try:
|
|
11
15
|
from docling.datamodel.base_models import InputFormat
|
|
12
|
-
from docling.datamodel.pipeline_options import
|
|
16
|
+
from docling.datamodel.pipeline_options import (
|
|
17
|
+
PdfPipelineOptions,
|
|
18
|
+
PictureDescriptionApiOptions,
|
|
19
|
+
)
|
|
13
20
|
from docling.document_converter import DocumentConverter, PdfFormatOption
|
|
14
21
|
|
|
15
22
|
DOCLING_AVAILABLE = True
|
|
16
23
|
except ImportError:
|
|
17
24
|
InputFormat = None # type: ignore
|
|
18
25
|
PdfPipelineOptions = None # type: ignore
|
|
26
|
+
PictureDescriptionApiOptions = None # type: ignore
|
|
19
27
|
PdfFormatOption = None # type: ignore
|
|
20
28
|
|
|
21
29
|
class DocumentConverter: # type: ignore[no-redef]
|
|
@@ -45,14 +53,67 @@ DOCLING_SUPPORTED = {
|
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
|
|
56
|
+
def _picture_description_api_options(
|
|
57
|
+
config: ContentCoreConfig,
|
|
58
|
+
) -> Optional["PictureDescriptionApiOptions"]:
|
|
59
|
+
"""Build the options that send picture description to an OpenAI-compatible
|
|
60
|
+
endpoint, or return None to keep docling's inline default model.
|
|
61
|
+
|
|
62
|
+
``docling_vision_url`` is the switch: without it, nothing leaves the
|
|
63
|
+
machine and the other ``docling_vision_*`` settings are ignored.
|
|
64
|
+
"""
|
|
65
|
+
if not config.docling_vision_url or PictureDescriptionApiOptions is None:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# The token limit field name differs across OpenAI-compatible servers:
|
|
69
|
+
# newer ones expect max_completion_tokens, older ones only accept
|
|
70
|
+
# max_tokens. docling_vision_token_param selects the name.
|
|
71
|
+
params: dict = {config.docling_vision_token_param: config.docling_vision_max_tokens}
|
|
72
|
+
if config.docling_vision_model:
|
|
73
|
+
params["model"] = config.docling_vision_model
|
|
74
|
+
|
|
75
|
+
# Only send an Authorization header when a key is configured; keyless
|
|
76
|
+
# local servers may reject an empty bearer token.
|
|
77
|
+
headers = {}
|
|
78
|
+
if config.docling_vision_api_key:
|
|
79
|
+
headers["Authorization"] = f"Bearer {config.docling_vision_api_key}"
|
|
80
|
+
|
|
81
|
+
kwargs = {}
|
|
82
|
+
if config.docling_vision_prompt:
|
|
83
|
+
kwargs["prompt"] = config.docling_vision_prompt
|
|
84
|
+
|
|
85
|
+
# Log only scheme and host: the URL may carry credentials in its
|
|
86
|
+
# userinfo or query string.
|
|
87
|
+
endpoint = urlsplit(config.docling_vision_url)
|
|
88
|
+
logger.debug(
|
|
89
|
+
f"Docling picture description via {endpoint.scheme}://{endpoint.hostname} "
|
|
90
|
+
f"(model={config.docling_vision_model or 'server default'})"
|
|
91
|
+
)
|
|
92
|
+
return PictureDescriptionApiOptions(
|
|
93
|
+
url=config.docling_vision_url,
|
|
94
|
+
headers=headers,
|
|
95
|
+
params=params,
|
|
96
|
+
timeout=config.docling_vision_timeout,
|
|
97
|
+
**kwargs,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
48
101
|
async def extract_docling(source: str, config: ContentCoreConfig) -> ExtractionOutput:
|
|
49
102
|
"""Extract content using Docling."""
|
|
50
103
|
if DOCLING_AVAILABLE and PdfPipelineOptions is not None:
|
|
104
|
+
pipeline_kwargs = {}
|
|
105
|
+
api_vision_options = None
|
|
106
|
+
if config.docling_vision:
|
|
107
|
+
api_vision_options = _picture_description_api_options(config)
|
|
108
|
+
if api_vision_options is not None:
|
|
109
|
+
pipeline_kwargs["picture_description_options"] = api_vision_options
|
|
51
110
|
pipeline_options = PdfPipelineOptions(
|
|
52
111
|
do_ocr=config.docling_ocr,
|
|
53
112
|
do_formula_enrichment=config.docling_formulas,
|
|
54
113
|
do_picture_description=config.docling_vision,
|
|
55
114
|
do_chart_extraction=config.docling_vision,
|
|
115
|
+
enable_remote_services=api_vision_options is not None,
|
|
116
|
+
**pipeline_kwargs,
|
|
56
117
|
)
|
|
57
118
|
converter = DocumentConverter(
|
|
58
119
|
format_options={
|
|
@@ -54,6 +54,24 @@ class TestConfigSet:
|
|
|
54
54
|
config_set("stt_timeout", "1800")
|
|
55
55
|
assert config_list()["stt_timeout"] == 1800
|
|
56
56
|
|
|
57
|
+
def test_set_float_value(self, config_dir):
|
|
58
|
+
config_set("docling_vision_timeout", "45.5")
|
|
59
|
+
assert config_list()["docling_vision_timeout"] == 45.5
|
|
60
|
+
assert ContentCoreConfig().docling_vision_timeout == 45.5
|
|
61
|
+
|
|
62
|
+
def test_set_docling_vision_endpoint(self, config_dir):
|
|
63
|
+
config_set("docling_vision_url", "http://localhost:1234/v1/chat/completions")
|
|
64
|
+
config_set("docling_vision_max_tokens", "800")
|
|
65
|
+
config_set("docling_vision_token_param", "max_tokens")
|
|
66
|
+
config = ContentCoreConfig()
|
|
67
|
+
assert config.docling_vision_url == "http://localhost:1234/v1/chat/completions"
|
|
68
|
+
assert config.docling_vision_max_tokens == 800
|
|
69
|
+
assert config.docling_vision_token_param == "max_tokens"
|
|
70
|
+
|
|
71
|
+
def test_set_invalid_token_param_raises(self, config_dir):
|
|
72
|
+
with pytest.raises(ValueError, match="max_completion_tokens"):
|
|
73
|
+
config_set("docling_vision_token_param", "max_length")
|
|
74
|
+
|
|
57
75
|
def test_set_list_value(self, config_dir):
|
|
58
76
|
config_set("youtube_languages", "en,pt,fr")
|
|
59
77
|
assert config_list()["youtube_languages"] == ["en", "pt", "fr"]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Unit tests for Docling picture description via an OpenAI-compatible endpoint
|
|
2
|
+
(docling_vision_* settings / CCORE_DOCLING_VISION_* env vars)."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from content_core.config import ContentCoreConfig
|
|
11
|
+
from content_core.processors.document import docling as docling_module
|
|
12
|
+
|
|
13
|
+
pytest.importorskip("docling")
|
|
14
|
+
|
|
15
|
+
URL = "http://localhost:1234/v1/chat/completions"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(autouse=True)
|
|
19
|
+
def isolated_settings(monkeypatch, tmp_path):
|
|
20
|
+
"""Keep the caller's CCORE_DOCLING_VISION_* env vars and config file out."""
|
|
21
|
+
for var in list(os.environ):
|
|
22
|
+
if var.startswith("CCORE_DOCLING_VISION_"):
|
|
23
|
+
monkeypatch.delenv(var)
|
|
24
|
+
monkeypatch.setattr("content_core.config.CONFIG_FILE", tmp_path / "config.toml")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_url_with_credentials_is_not_logged():
|
|
28
|
+
config = ContentCoreConfig(docling_vision_url="https://user:pw@vlm.example.com/v1/chat/completions?key=abc")
|
|
29
|
+
with patch.object(docling_module, "logger") as logger:
|
|
30
|
+
docling_module._picture_description_api_options(config)
|
|
31
|
+
logged = " ".join(str(c) for c in logger.debug.call_args_list)
|
|
32
|
+
assert "vlm.example.com" in logged
|
|
33
|
+
assert "pw" not in logged and "abc" not in logged
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_no_url_keeps_inline_default():
|
|
37
|
+
# URL is the switch: a model name alone must not enable the API path
|
|
38
|
+
config = ContentCoreConfig(docling_vision_url=None, docling_vision_model="some-model")
|
|
39
|
+
assert docling_module._picture_description_api_options(config) is None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_url_builds_api_options():
|
|
43
|
+
config = ContentCoreConfig(
|
|
44
|
+
docling_vision_url=URL,
|
|
45
|
+
docling_vision_model="my-local-vlm",
|
|
46
|
+
docling_vision_api_key="secret",
|
|
47
|
+
docling_vision_max_tokens=800,
|
|
48
|
+
docling_vision_timeout=120,
|
|
49
|
+
docling_vision_prompt="Describe it.",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
options = docling_module._picture_description_api_options(config)
|
|
53
|
+
|
|
54
|
+
assert options is not None
|
|
55
|
+
assert "localhost:1234" in str(options.url)
|
|
56
|
+
assert options.params["model"] == "my-local-vlm"
|
|
57
|
+
assert options.params["max_completion_tokens"] == 800
|
|
58
|
+
assert options.headers["Authorization"] == "Bearer secret"
|
|
59
|
+
assert options.timeout == 120
|
|
60
|
+
assert options.prompt == "Describe it."
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_defaults_keep_docling_prompt_and_timeout():
|
|
64
|
+
config = ContentCoreConfig(docling_vision_url=URL, docling_vision_api_key=None)
|
|
65
|
+
options = docling_module._picture_description_api_options(config)
|
|
66
|
+
default = docling_module.PictureDescriptionApiOptions()
|
|
67
|
+
|
|
68
|
+
assert options is not None
|
|
69
|
+
assert "Authorization" not in options.headers
|
|
70
|
+
assert "model" not in options.params
|
|
71
|
+
assert options.params["max_completion_tokens"] == 400
|
|
72
|
+
assert options.prompt == default.prompt
|
|
73
|
+
assert options.timeout == 20
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_token_param_name_is_selectable():
|
|
77
|
+
config = ContentCoreConfig(docling_vision_url=URL, docling_vision_token_param="max_tokens")
|
|
78
|
+
options = docling_module._picture_description_api_options(config)
|
|
79
|
+
assert options is not None
|
|
80
|
+
assert "max_tokens" in options.params
|
|
81
|
+
assert "max_completion_tokens" not in options.params
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_settings_read_from_env(monkeypatch):
|
|
85
|
+
monkeypatch.setenv("CCORE_DOCLING_VISION_URL", URL)
|
|
86
|
+
monkeypatch.setenv("CCORE_DOCLING_VISION_MODEL", "env-vlm")
|
|
87
|
+
monkeypatch.setenv("CCORE_DOCLING_VISION_MAX_TOKENS", "256")
|
|
88
|
+
monkeypatch.setenv("CCORE_DOCLING_VISION_TIMEOUT", "45.5")
|
|
89
|
+
monkeypatch.setenv("CCORE_DOCLING_VISION_TOKEN_PARAM", "max_tokens")
|
|
90
|
+
|
|
91
|
+
options = docling_module._picture_description_api_options(ContentCoreConfig())
|
|
92
|
+
|
|
93
|
+
assert options is not None
|
|
94
|
+
assert options.params == {"max_tokens": 256, "model": "env-vlm"}
|
|
95
|
+
assert options.timeout == 45.5
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize(
|
|
99
|
+
"field,value",
|
|
100
|
+
[
|
|
101
|
+
("docling_vision_max_tokens", "banana"),
|
|
102
|
+
("docling_vision_max_tokens", 0),
|
|
103
|
+
("docling_vision_timeout", "not-a-number"),
|
|
104
|
+
("docling_vision_token_param", "max_length"),
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
def test_invalid_settings_are_rejected(field, value):
|
|
108
|
+
with pytest.raises(ValidationError):
|
|
109
|
+
ContentCoreConfig(**{field: value})
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def _run_extract(config):
|
|
113
|
+
mock_result = MagicMock()
|
|
114
|
+
mock_result.document.export_to_markdown.return_value = "content"
|
|
115
|
+
pipeline_cls = MagicMock()
|
|
116
|
+
with patch.object(docling_module, "DocumentConverter") as converter_cls, patch.object(
|
|
117
|
+
docling_module, "DOCLING_AVAILABLE", True
|
|
118
|
+
), patch.object(docling_module, "PdfPipelineOptions", pipeline_cls), patch.object(
|
|
119
|
+
docling_module, "PdfFormatOption", MagicMock()
|
|
120
|
+
), patch.object(docling_module, "InputFormat", MagicMock()):
|
|
121
|
+
converter_cls.return_value.convert.return_value = mock_result
|
|
122
|
+
await docling_module.extract_docling("/fake/doc.pdf", config)
|
|
123
|
+
return pipeline_cls.call_args[1]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def test_extract_wires_api_options_when_url_set():
|
|
127
|
+
config = ContentCoreConfig(docling_vision=True, docling_vision_url=URL)
|
|
128
|
+
kwargs = await _run_extract(config)
|
|
129
|
+
assert kwargs["enable_remote_services"] is True
|
|
130
|
+
assert "localhost:1234" in str(kwargs["picture_description_options"].url)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def test_extract_stays_local_without_vision():
|
|
134
|
+
# The URL alone does not turn picture description on
|
|
135
|
+
config = ContentCoreConfig(docling_vision=False, docling_vision_url=URL)
|
|
136
|
+
kwargs = await _run_extract(config)
|
|
137
|
+
assert kwargs["enable_remote_services"] is False
|
|
138
|
+
assert "picture_description_options" not in kwargs
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{content_core-2.2.1 → content_core-2.3.0}/src/content_core/content/identification/__init__.py
RENAMED
|
File without changes
|
{content_core-2.2.1 → content_core-2.3.0}/src/content_core/content/identification/file_detector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|