Skip to content

Commit e5608fd

Browse files
committed
Release abstractcore 2.13.33
1 parent 3457dcb commit e5608fd

37 files changed

Lines changed: 2110 additions & 387 deletions

ACKNOWLEDGEMENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Features:
2525
- **Tools / web**: **requests**, **beautifulsoup4**, **lxml**, **ddgs** / **duckduckgo-search**, **psutil**
2626
- **Embeddings**: **sentence-transformers**, **numpy**
2727
- **Tokens**: **tiktoken**
28-
- **Media / documents**: **Pillow**, **pymupdf4llm**, **pymupdf-layout**, **unstructured**, **pandas**
28+
- **Media / documents**: **Pillow**, **pypdf**, **unstructured**, **pandas**. PyMuPDF-family PDF tooling is available only through the explicit commercial-license opt-in extra.
2929
- **Compression**: **Pillow** (glyph rendering)
3030
- **Server**: **fastapi**, **uvicorn**, **python-multipart**, **sse-starlette**
3131
- **Vision plugin integration**: **abstractvision**

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.13.33] - 2026-06-07
11+
1012
### Added
13+
- **Image upscaling routes**: added `/v1/images/upscale`, `/{provider}/v1/images/upscale`, and async `/v1/vision/jobs/images/upscale` with polling/progress support.
14+
- **Generated image upscaling**: `generate(..., output={"task": "image_upscale"})` routes source images through the AbstractVision upscaler capability.
1115
- **HTTP server CLI**: added `abstractcore serve` as the first-class command for starting the OpenAI-compatible AbstractCore server. Existing module and uvicorn entrypoints remain available for compatibility.
16+
- **MLX-Gen reference-image edits**: `/v1/images/edits` and async `/v1/vision/jobs/images/edits` now accept repeated multipart `reference_images` files and forward them to AbstractVision backends for composition/style-reference image edits.
17+
- **Image progress events**: async image generation/edit jobs now capture AbstractVision `on_progress(event)` payloads in `progress.last_event`, matching the existing video job surface.
18+
- **Wan A14B second guidance**: video generation routes, async video jobs, and generated-video output specs now accept typed `guidance_2` for dual-transformer video models.
1219

1320
### Removed
1421
- **Capability default CLI compatibility flags**: removed the top-level `abstractcore --set-capability-default` / `--clear-capability-default` form. Use `abstractcore config set-default`, `abstractcore config defaults`, and `abstractcore config clear-default` instead.
1522

23+
### Changed
24+
- **Permissive PDF media path**: moved the default `PDFProcessor` and `media`/aggregate install profiles from PyMuPDF-family packages to the BSD-licensed `pypdf` baseline. PyMuPDF4LLM and `pymupdf-layout` remain available only through the explicit `pdf-pymupdf-commercial` opt-in extra.
25+
- **Vision plugin floor**: raised AbstractVision integration requirements to `abstractvision>=0.3.22` so Core installs pick up MLX-Gen `0.18.13`, SeedVR2 image upscaling, canonical q8/q4 upscaler packages, and the current upscaler progress event surface.
26+
- **Vision job progress semantics**: normalized server job payloads now preserve AbstractVision `step_progress` and `frame_progress`; `progress` follows the backend event's canonical progress value, which is denoise-step progress for MLX-Gen.
27+
- **Generated media examples**: updated Core docs and OpenAPI examples to use task-specific MLX-Gen A14B text-to-video and image-to-video model ids.
28+
29+
### Fixed
30+
- **PDF capability truth**: the default `pypdf` processor no longer advertises image extraction support, and page-level text extraction errors are reported as warnings instead of aborting the whole document.
31+
- **Vision upscaler discovery**: local vision catalogs now surface MLX-Gen models that only support `image_upscale`, including canonical `AbstractFramework/seedvr2-{3b,7b}-{8bit,4bit}` packages.
32+
- **Generated image callback forwarding**: server-local generated image/edit dispatch now forwards top-level progress callbacks and backend-specific parameters through the same AbstractVision `extra` path used for video generation.
33+
- **Reference media routing**: unified Python image-edit generation forwards `media` items with `reference`, `style`, or `context` roles as AbstractVision `reference_images`.
34+
1635
## [2.13.32] - 2026-06-03
1736

1837
### Added

README.md

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Generative vision uses `abstractvision` when installed. In server mode, omit
5252
`model` only when the server has a configured default, or use explicit
5353
provider/model ids such as `diffusers/default`, `diffusers/<huggingface-repo>`,
5454
`mlx-gen/AbstractFramework/qwen-image-2512-4bit`,
55-
`mlx-gen/Wan-AI/Wan2.2-TI2V-5B-Diffusers`, `sdcpp/default`, or
55+
`mlx-gen/AbstractFramework/wan2.2-t2v-a14b-diffusers-8bit`, `sdcpp/default`, or
5656
`openai-compatible/<model>`. Quantized MLX-Gen models are selected by their
5757
published repo id; Core does not add a separate quant override.
5858

@@ -339,11 +339,40 @@ Optional capability plugins can also generate media through the normal
339339
image = llm.generate("A red ceramic mug on a white table.", output="image")
340340
png_bytes = image.outputs["image"][0].data
341341

342-
# Image edit: image media + image output infers image-to-image.
343-
edited = llm.generate("Make the mug blue.", media="mug.png", output="image")
342+
# Image edit: source plus optional reference/style media infers image-to-image.
343+
edited = llm.generate(
344+
"Make the mug blue using the second image as a style reference.",
345+
media=[
346+
{"type": "image", "path": "mug.png", "role": "source"},
347+
{"type": "image", "path": "style.png", "role": "style"},
348+
],
349+
output="image",
350+
)
344351

345352
def progress(event):
346-
print("video progress", event)
353+
print("media progress", event)
354+
355+
# Image upscale via the AbstractVision plugin. Canonical q8 packages do not
356+
# need runtime quantize; pass a local prepared folder as model when needed.
357+
upscaled_direct = llm.vision.upscale_image(
358+
"mug.png",
359+
provider="mlx-gen",
360+
model="AbstractFramework/seedvr2-3b-8bit",
361+
scale="2x",
362+
on_progress=progress,
363+
)
364+
365+
upscaled = llm.generate(
366+
media={"type": "image", "path": "mug.png", "role": "source"},
367+
on_progress=progress,
368+
output={
369+
"task": "image_upscale",
370+
"provider": "mlx-gen",
371+
"model": "AbstractFramework/seedvr2-3b-8bit",
372+
"scale": "2x",
373+
},
374+
)
375+
upscaled_png = upscaled.outputs["image"][0].data
347376

348377
# Text-to-video via abstractvision. The callback is forwarded to the plugin.
349378
video = llm.generate(
@@ -353,9 +382,15 @@ video = llm.generate(
353382
"modality": "video",
354383
"task": "text_to_video",
355384
"provider": "mlx-gen",
356-
"model": "Wan-AI/Wan2.2-TI2V-5B-Diffusers",
357-
"num_frames": 121,
385+
"model": "AbstractFramework/wan2.2-t2v-a14b-diffusers-8bit",
386+
"width": 432,
387+
"height": 240,
388+
"num_frames": 41,
358389
"fps": 24,
390+
"steps": 20,
391+
"guidance_scale": 4.0,
392+
"guidance_2": 3.0,
393+
"extra": {"max_sequence_length": 256},
359394
},
360395
)
361396
mp4_bytes = video.outputs["video"][0].data
@@ -367,9 +402,15 @@ i2v = llm.generate(
367402
output={
368403
"task": "image_to_video",
369404
"provider": "mlx-gen",
370-
"model": "Wan-AI/Wan2.2-TI2V-5B-Diffusers",
371-
"num_frames": 121,
405+
"model": "AbstractFramework/wan2.2-i2v-a14b-diffusers-8bit",
406+
"width": 432,
407+
"height": 240,
408+
"num_frames": 41,
372409
"fps": 24,
410+
"steps": 20,
411+
"guidance_scale": 3.5,
412+
"guidance_2": 3.5,
413+
"extra": {"max_sequence_length": 256},
373414
},
374415
)
375416

@@ -394,7 +435,7 @@ Text-only `generate(...)` is unchanged. For advanced/provider-specific work,
394435
the direct `llm.vision.*`, `llm.voice.*`, `llm.audio.*`, and `llm.music.*` facades remain
395436
available. Configure `abstractvision` and `abstractvoice` backends first for
396437
real generation; configure `abstractmusic` for music generation. With
397-
`abstractmusic>=0.1.12`, the default music backend is the lightweight remote
438+
`abstractmusic>=0.1.13`, the default music backend is the lightweight remote
398439
ACE Music path; set `ACEMUSIC_API_KEY` before use. Local music engines remain
399440
optional plugin extras.
400441

@@ -416,8 +457,8 @@ The HTTP server exposes equivalent discovery at
416457
Generated media HTTP routes include `/v1/images/generations`,
417458
`/v1/images/edits`, `/v1/videos/generations`, `/v1/videos/edits`, and
418459
async polling routes under `/v1/vision/jobs/images/*` and
419-
`/v1/vision/jobs/videos/*`; video jobs include the latest backend progress event
420-
when the selected backend reports it.
460+
`/v1/vision/jobs/videos/*`; image and video jobs include the latest backend
461+
progress event when the selected backend reports it.
421462
`/v1/models` remains focused on LLM and embedding provider models.
422463
Use `capability_route` to filter those models by precise route-keyed support:
423464
`/v1/models?capability_route=input.image,output.text`,

abstractcore/capabilities/registry.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,20 @@ def backend_id(self) -> Optional[str]:
10981098
def t2i(self, prompt: str, **kwargs: Any) -> Any:
10991099
return self._registry.get_vision().t2i(prompt, **kwargs)
11001100

1101+
def upscale_image(self, image: Any, **kwargs: Any) -> Any:
1102+
backend = self._registry.get_vision()
1103+
method = getattr(backend, "upscale_image", None)
1104+
if not callable(method):
1105+
method = getattr(backend, "image_upscale", None)
1106+
if not callable(method):
1107+
raise CapabilityUnavailableError(
1108+
capability="vision",
1109+
reason="The selected vision capability backend does not expose image_upscale.",
1110+
install_hint=self._registry._default_install_hint("vision"),
1111+
details={"backend_id": getattr(backend, "backend_id", None)},
1112+
)
1113+
return method(image, **kwargs)
1114+
11011115
def load_resident_model(self, request: Mapping[str, Any]) -> Dict[str, Any]:
11021116
return _call_residency_mapping_method(
11031117
self._registry.get_vision(),

abstractcore/capabilities/types.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,17 @@ def i2i(
366366
**kwargs: Any,
367367
) -> BytesOrArtifactRef: ...
368368

369+
def upscale_image(
370+
self,
371+
image: Union[bytes, ArtifactRef, str],
372+
*,
373+
artifact_store: Optional[ArtifactStoreLike] = None,
374+
run_id: Optional[str] = None,
375+
tags: Optional[Dict[str, str]] = None,
376+
metadata: Optional[Dict[str, Any]] = None,
377+
**kwargs: Any,
378+
) -> BytesOrArtifactRef: ...
379+
369380
def t2v(
370381
self,
371382
prompt: str,

abstractcore/capabilities/vision_catalog.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,13 @@ def get_local_vision_cache_catalog() -> Dict[str, Any]:
434434
for model_id in model_ids:
435435
spec = registry.get(model_id)
436436
supported_tasks = sorted(spec.tasks.keys())
437-
if not {"text_to_image", "image_to_image", "text_to_video", "image_to_video"}.intersection(spec.tasks):
437+
if not {
438+
"text_to_image",
439+
"image_to_image",
440+
"image_upscale",
441+
"text_to_video",
442+
"image_to_video",
443+
}.intersection(spec.tasks):
438444
continue
439445

440446
for download in list(getattr(spec, "downloads", []) or []):

abstractcore/core/output_specs.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
"transcript",
1212
"transcription",
1313
"image",
14+
"upscale",
15+
"image_upscale",
16+
"upscale_image",
1417
"video",
1518
"t2v",
1619
"i2v",
@@ -40,6 +43,9 @@
4043
"image",
4144
"image_generation",
4245
"image_edit",
46+
"upscale",
47+
"image_upscale",
48+
"upscale_image",
4349
"t2i",
4450
"i2i",
4551
"image_to_image",
@@ -78,6 +84,9 @@
7884
"i2i": ("image", "image_edit"),
7985
"image_to_image": ("image", "image_edit"),
8086
"image_edit": ("image", "image_edit"),
87+
"upscale": ("image", "image_upscale"),
88+
"image_upscale": ("image", "image_upscale"),
89+
"upscale_image": ("image", "image_upscale"),
8190
"video": ("video", "video_generation"),
8291
"video_generation": ("video", "video_generation"),
8392
"t2v": ("video", "text_to_video"),
@@ -107,6 +116,8 @@
107116
"t2i": "image_generation",
108117
"i2i": "image_edit",
109118
"image_to_image": "image_edit",
119+
"upscale": "image_upscale",
120+
"upscale_image": "image_upscale",
110121
"t2v": "text_to_video",
111122
"i2v": "image_to_video",
112123
"song": "music_generation",
@@ -120,6 +131,7 @@
120131
"transcription": "text",
121132
"image_generation": "image",
122133
"image_edit": "image",
134+
"image_upscale": "image",
123135
"video_generation": "video",
124136
"text_to_video": "video",
125137
"image_to_video": "video",

abstractcore/media/README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The media module provides unified, provider-agnostic multimodal processing capab
1313
| File Type | Processor | Best For | Requirements |
1414
|-----------|-----------|----------|--------------|
1515
| Images (jpg, png) | ImageProcessor | Vision models, analysis | PIL/Pillow |
16-
| PDF (text-heavy) | PDFProcessor | Text extraction | PyMuPDF4LLM |
16+
| PDF (text-heavy) | PDFProcessor | Text/metadata extraction | pypdf |
1717
| PDF (math/tables) | DirectPDFProcessor | Visual fidelity | pdf2image |
1818
| Office docs | OfficeProcessor | DOCX, XLSX, PPTX | unstructured |
1919
| Text/CSV/JSON | TextProcessor | Data files | Built-in |
@@ -122,7 +122,7 @@ media/
122122
123123
├── processors/ # Media type processors
124124
│ ├── image_processor.py # Image processing with PIL (resize, optimize)
125-
│ ├── pdf_processor.py # PDF extraction with PyMuPDF4LLM (markdown)
125+
│ ├── pdf_processor.py # PDF extraction with pypdf by default
126126
│ ├── direct_pdf_processor.py # Direct PDF→image conversion (Glyph)
127127
│ ├── glyph_pdf_processor.py # Glyph-optimized PDF extraction (math/tables)
128128
│ ├── text_processor.py # Text/CSV/JSON/Markdown processing
@@ -226,7 +226,7 @@ caps = get_media_capabilities("gpt-4o", provider="openai")
226226

227227
**Features**:
228228
- Lazy processor initialization
229-
- Dependency checking (PIL, PyMuPDF4LLM, unstructured)
229+
- Dependency checking (PIL, pypdf, unstructured)
230230
- Glyph compression integration
231231
- Fallback processing
232232

@@ -247,7 +247,7 @@ if result.success:
247247
**Processor Selection Logic**:
248248
```python
249249
.jpg/.png → ImageProcessor (if PIL available)
250-
.pdf → PDFProcessor (if PyMuPDF4LLM) else TextProcessor
250+
.pdf → PDFProcessor (pypdf by default; PyMuPDF4LLM requires explicit opt-in)
251251
.docx/.xlsx/.pptx → OfficeProcessor (if unstructured) else TextProcessor
252252
.txt/.md/.csv → TextProcessor (always available)
253253
```
@@ -325,7 +325,7 @@ result = processor.process_file("photo.jpg", model_name="gpt-4o")
325325

326326
#### 7. `pdf_processor.py` - PDF Extraction
327327

328-
**Dependencies**: PyMuPDF4LLM, PyMuPDF
328+
**Dependencies**: pypdf by default. Optional PyMuPDF4LLM / PyMuPDF support requires `abstractcore[pdf-pymupdf-commercial]` after license review.
329329

330330
**Features**:
331331
- LLM-optimized markdown output
@@ -392,7 +392,7 @@ result = processor.process_file("research.pdf")
392392

393393
#### 9. `glyph_pdf_processor.py` - Glyph-Optimized PDF Extraction
394394

395-
**Dependencies**: PyMuPDF
395+
**Dependencies**: PyMuPDF from the explicit `abstractcore[pdf-pymupdf-commercial]` opt-in extra.
396396

397397
**Purpose**: Extracts PDF content while preserving compact mathematical notation and table layouts for optimal Glyph visual compression.
398398

@@ -1182,7 +1182,7 @@ result = handler.process_file("report.docx")
11821182

11831183
# ✓ Good: Install media extras
11841184
# $ pip install "abstractcore[media]"
1185-
# Includes PIL, PyMuPDF4LLM, unstructured
1185+
# Includes PIL, pypdf, unstructured
11861186
```
11871187

11881188
### 6. PDF Processing Choice
@@ -1397,7 +1397,7 @@ print(result.media_content.content)
13971397
**Module Statistics**:
13981398
- **Total Files**: 15 (5 root + 6 processors + 3 handlers + 1 util)
13991399
- **Total Lines**: ~7,500
1400-
- **Dependencies**: PIL, PyMuPDF4LLM, PyMuPDF, pdf2image, unstructured, pandas (optional)
1400+
- **Dependencies**: PIL, pypdf, pdf2image, unstructured, pandas (optional). PyMuPDF-family tooling is an explicit commercial-license opt-in.
14011401
- **Supported Formats**: Images (8), Documents (3), Text (8), Office (3)
14021402
- **Providers**: OpenAI, Anthropic, Ollama, MLX, LMStudio, HuggingFace
14031403

abstractcore/media/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from __future__ import annotations
1414

1515
# NOTE: Keep this package import-safe for minimal installs.
16-
# Many submodules have optional dependencies (Pillow, PyMuPDF4LLM, unstructured, ...).
16+
# Many submodules have optional dependencies (Pillow, pypdf, unstructured, ...).
1717
# Import them lazily so `from abstractcore.media.capabilities import ...` works without extras.
1818

1919
from importlib import import_module

abstractcore/media/auto_handler.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ def _check_processor_availability(self) -> Dict[str, bool]:
8686
# TextProcessor (always available - uses built-in libraries)
8787
availability['text'] = True
8888

89-
# PDFProcessor (requires PyMuPDF4LLM)
89+
# PDFProcessor (requires pypdf by default; PyMuPDF is explicit opt-in)
9090
try:
91-
import pymupdf4llm
91+
import pypdf
9292
availability['pdf'] = True
9393
except ImportError:
9494
availability['pdf'] = False
@@ -182,9 +182,10 @@ def _select_processor(self, file_path: Path, media_type: MediaType) -> Optional[
182182
if self._available_processors.get('pdf', False):
183183
return self._get_pdf_processor()
184184
else:
185-
self.logger.warning("PDF processing requested but PyMuPDF4LLM not available")
186-
# Fall back to text processor for basic extraction
187-
return self._get_text_processor()
185+
self.logger.warning("PDF processing requested but pypdf not available")
186+
# Let PDFProcessor raise the actionable dependency error instead of
187+
# pretending a binary PDF can be handled as plain text.
188+
return self._get_pdf_processor()
188189

189190
# Office documents
190191
elif file_extension in {'.docx', '.xlsx', '.pptx'}:

0 commit comments

Comments
 (0)