Files
2026-07-13 13:08:41 +08:00

124 lines
3.9 KiB
Python

from __future__ import annotations
import asyncio
import json
import logging
import mimetypes
from pathlib import Path
from llmai.shared import ImageContentPart, Tool
from pydantic import PrivateAttr
from services.export_task_service import EXPORT_TASK_SERVICE
from templates.v2.models.layouts import SlideLayout
from utils.get_env import get_app_data_directory_env
SLIDE_PREVIEW_WIDTH = 1280
SLIDE_PREVIEW_HEIGHT = 720
PREVIEW_SLIDE_TOOL_NAME = "previewSlide"
LOGGER = logging.getLogger(__name__)
class PreviewSlideTool(Tool):
"""LLM tool definition and executor for rendering a candidate slide layout."""
_slide_index: int | None = PrivateAttr(default=None)
_preview_count: int = PrivateAttr(default=0)
_fonts: dict[str, str] = PrivateAttr(default_factory=dict)
def __init__(
self,
*,
slide_index: int | None = None,
fonts: dict[str, str] | None = None,
) -> None:
super().__init__(
name=PREVIEW_SLIDE_TOOL_NAME,
description=(
"Render a complete SlideLayout to a 1280x720 image for visual review. "
"Call this with a full candidate layout before returning the final layout. "
"At most two preview calls are allowed for each slide."
),
schema=SlideLayout,
strict=False,
)
self._slide_index = slide_index
self._preview_count = 0
self._fonts = dict(fonts or {})
def render(self, layout: SlideLayout) -> ImageContentPart:
self._preview_count += 1
preview_index = self._preview_count
layout_json = layout.model_dump(mode="json", exclude_none=True)
components = [
component.model_dump(mode="json", exclude_none=True)
for component in layout.components
]
result = asyncio.run(
EXPORT_TASK_SERVICE.render_json_to_image(
components,
SLIDE_PREVIEW_WIDTH,
SLIDE_PREVIEW_HEIGHT,
fonts=self._fonts,
)
)
with open(result.path, "rb") as image_file:
image_data = image_file.read()
self._save_preview_artifacts(
layout_json=layout_json,
image_data=image_data,
preview_index=preview_index,
)
mime_type = mimetypes.guess_type(result.path)[0] or "image/png"
return ImageContentPart(data=image_data, mime_type=mime_type)
def _save_preview_artifacts(
self,
*,
layout_json: dict,
image_data: bytes,
preview_index: int,
) -> None:
if self._slide_index is None:
return
app_data_dir = get_app_data_directory_env()
if not app_data_dir:
return
preview_dir = (
Path(app_data_dir) / "preview_slide" / str(self._slide_index)
)
try:
preview_dir.mkdir(parents=True, exist_ok=True)
json_path = preview_dir / f"{preview_index}.json"
png_path = preview_dir / f"{preview_index}.png"
json_path.write_text(
json.dumps(layout_json, indent=2) + "\n",
encoding="utf-8",
)
png_path.write_bytes(image_data)
LOGGER.info(
"[templates.v2.preview] saved preview artifacts slide=%s "
"preview=%d json=%s image=%s",
self._slide_index,
preview_index,
json_path,
png_path,
)
except OSError:
LOGGER.exception(
"[templates.v2.preview] failed to save preview artifacts "
"slide=%s preview=%d directory=%s",
self._slide_index,
preview_index,
preview_dir,
)
__all__ = [
"PREVIEW_SLIDE_TOOL_NAME",
"PreviewSlideTool",
"SLIDE_PREVIEW_HEIGHT",
"SLIDE_PREVIEW_WIDTH",
]