c56bef871b
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled
151 lines
5.8 KiB
Python
151 lines
5.8 KiB
Python
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
import mimetypes
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
from typing import Any, Literal
|
|
|
|
from haystack import component, logging
|
|
from haystack.components.converters.image.image_utils import _encode_image_to_base64
|
|
from haystack.components.converters.utils import get_bytestream_from_source, normalize_metadata
|
|
from haystack.dataclasses import ByteStream
|
|
from haystack.dataclasses.image_content import ImageContent
|
|
from haystack.lazy_imports import LazyImport
|
|
|
|
with LazyImport(
|
|
"The 'size' parameter is set. "
|
|
"Image resizing will be applied, which requires the Pillow library. "
|
|
"Run 'pip install pillow'"
|
|
) as pillow_import:
|
|
import PIL # noqa: F401
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
_EMPTY_BYTE_STRING = b""
|
|
|
|
|
|
@component
|
|
class ImageFileToImageContent:
|
|
"""
|
|
Converts image files to ImageContent objects.
|
|
|
|
### Usage example
|
|
```python
|
|
from haystack.components.converters.image import ImageFileToImageContent
|
|
|
|
converter = ImageFileToImageContent()
|
|
|
|
sources = ["image.jpg", "another_image.png"]
|
|
|
|
image_contents = converter.run(sources=sources)["image_contents"]
|
|
print(image_contents)
|
|
|
|
# [ImageContent(base64_image='...',
|
|
# mime_type='image/jpeg',
|
|
# detail=None,
|
|
# meta={'file_path': 'image.jpg'}),
|
|
# ...]
|
|
```
|
|
"""
|
|
|
|
def __init__(
|
|
self, *, detail: Literal["auto", "high", "low"] | None = None, size: tuple[int, int] | None = None
|
|
) -> None:
|
|
"""
|
|
Create the ImageFileToImageContent component.
|
|
|
|
:param detail: Optional detail level of the image (only supported by OpenAI). One of "auto", "high", or "low".
|
|
This will be passed to the created ImageContent objects.
|
|
:param size: If provided, resizes the image to fit within the specified dimensions (width, height) while
|
|
maintaining aspect ratio. This reduces file size, memory usage, and processing time, which is beneficial
|
|
when working with models that have resolution constraints or when transmitting images to remote services.
|
|
"""
|
|
self.detail = detail
|
|
self.size = size
|
|
|
|
if self.size is not None:
|
|
pillow_import.check()
|
|
|
|
@component.output_types(image_contents=list[ImageContent])
|
|
def run(
|
|
self,
|
|
sources: list[str | Path | ByteStream],
|
|
meta: dict[str, Any] | list[dict[str, Any]] | None = None,
|
|
*,
|
|
detail: Literal["auto", "high", "low"] | None = None,
|
|
size: tuple[int, int] | None = None,
|
|
) -> dict[str, list[ImageContent]]:
|
|
"""
|
|
Converts files to ImageContent objects.
|
|
|
|
:param sources:
|
|
List of file paths or ByteStream objects to convert.
|
|
:param meta:
|
|
Optional metadata to attach to the ImageContent objects.
|
|
This value can be a list of dictionaries or a single dictionary.
|
|
If it's a single dictionary, its content is added to the metadata of all produced ImageContent objects.
|
|
If it's a list, its length must match the number of sources as they're zipped together.
|
|
For ByteStream objects, their `meta` is added to the output ImageContent objects.
|
|
:param detail:
|
|
Optional detail level of the image (only supported by OpenAI). One of "auto", "high", or "low".
|
|
This will be passed to the created ImageContent objects.
|
|
If not provided, the detail level will be the one set in the constructor.
|
|
:param size: If provided, resizes the image to fit within the specified dimensions (width, height) while
|
|
maintaining aspect ratio. This reduces file size, memory usage, and processing time, which is beneficial
|
|
when working with models that have resolution constraints or when transmitting images to remote services.
|
|
If not provided, the size value will be the one set in the constructor.
|
|
|
|
:returns:
|
|
A dictionary with the following keys:
|
|
- `image_contents`: A list of ImageContent objects.
|
|
"""
|
|
if not sources:
|
|
return {"image_contents": []}
|
|
|
|
resolved_detail = detail or self.detail
|
|
resolved_size = size or self.size
|
|
|
|
# Check import
|
|
if resolved_size:
|
|
pillow_import.check()
|
|
|
|
image_contents = []
|
|
|
|
meta_list = normalize_metadata(meta, sources_count=len(sources))
|
|
|
|
for source, metadata in zip(sources, meta_list, strict=True):
|
|
if isinstance(source, str):
|
|
source = Path(source)
|
|
|
|
try:
|
|
bytestream = get_bytestream_from_source(source)
|
|
except Exception as e:
|
|
logger.warning("Could not read {source}. Skipping it. Error: {error}", source=source, error=e)
|
|
continue
|
|
|
|
if bytestream.mime_type is None and isinstance(source, Path):
|
|
bytestream = replace(bytestream, mime_type=mimetypes.guess_type(source.as_posix())[0])
|
|
|
|
if bytestream.data == _EMPTY_BYTE_STRING:
|
|
logger.warning("File {source} is empty. Skipping it.", source=source)
|
|
continue
|
|
|
|
try:
|
|
inferred_mime_type, base64_image = _encode_image_to_base64(bytestream=bytestream, size=resolved_size)
|
|
except Exception as e:
|
|
logger.warning(
|
|
"Could not convert file {source}. Skipping it. Error message: {error}", source=source, error=e
|
|
)
|
|
continue
|
|
|
|
merged_metadata = {**bytestream.meta, **metadata}
|
|
image_content = ImageContent(
|
|
base64_image=base64_image, mime_type=inferred_mime_type, meta=merged_metadata, detail=resolved_detail
|
|
)
|
|
image_contents.append(image_content)
|
|
|
|
return {"image_contents": image_contents}
|