import json import logging import re from typing import Any, Optional from .settings import MemoirImageSettings logger = logging.getLogger(__name__) _CJK_RE = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]") class MemoirImagePromptService: CATEGORY_STYLE_MAP = { "childhood": "watercolor", "family": "watercolor", "career_early": "realistic", "career_achievement": "realistic", "career_challenge": "realistic", "beliefs": "editorial illustration", "summary": "editorial illustration", } CATEGORY_FALLBACK_SUBJECT_MAP = { "childhood": "childhood memory", "education": "school memory", "career_early": "early career memory", "career_achievement": "career achievement memory", "career_challenge": "career challenge memory", "family": "family memory", "beliefs": "reflective life memory", "summary": "memoir summary scene", } def __init__(self, llm: Optional[Any], settings: MemoirImageSettings): self.llm = llm self.settings = settings def build_prompt( self, chapter_title: str, chapter_category: str, description: str, context_excerpt: str, ) -> dict[str, str]: style = self.CATEGORY_STYLE_MAP.get(chapter_category, self.settings.default_style) prompt_context = f"{chapter_category}: {chapter_title}" llm_input = { "chapter_title": chapter_title, "chapter_category": chapter_category, "description": description, "context_excerpt": context_excerpt, "default_style": style, "default_size": self.settings.default_size, } if self.llm: try: response = self.llm.invoke( "Return JSON only with keys prompt, style, size. " "Convert the memoir scene into an image-generation prompt.\n" + json.dumps(llm_input, ensure_ascii=False) ) parsed = json.loads(response.content) return { "prompt": _ensure_style_in_prompt(parsed["prompt"], parsed.get("style", style)), "style": parsed.get("style", style), "size": parsed.get("size", self.settings.default_size), "prompt_context": prompt_context, } except Exception as exc: logger.warning( "图片 prompt 生成回退到默认模板: chapter_category=%s, title=%s, error=%s", chapter_category, chapter_title, exc, ) return { "prompt": _ensure_style_in_prompt( self._build_fallback_prompt( chapter_category=chapter_category, description=description, context_excerpt=context_excerpt, style=style, ), style, ), "style": style, "size": self.settings.default_size, "prompt_context": prompt_context, } def _build_fallback_prompt( self, chapter_category: str, description: str, context_excerpt: str, style: str, ) -> str: subject = self.CATEGORY_FALLBACK_SUBJECT_MAP.get(chapter_category, "memoir scene") if _contains_cjk(description) or _contains_cjk(context_excerpt): return ( f"A {style} illustration of a {subject}, emotionally resonant, cinematic composition, " "authentic everyday details, natural lighting, expressive environment, no text overlay." ) details = ". ".join(part.strip() for part in (description, context_excerpt) if part.strip()) if not details: details = "A personal life story scene with authentic emotional detail" return ( f"A {style} illustration of a {subject}. " f"Scene details: {details}. " "Cinematic composition, authentic emotions, natural lighting, no text overlay." ) def _contains_cjk(value: str) -> bool: return bool(_CJK_RE.search(value or "")) def _ensure_style_in_prompt(prompt: str, style: str) -> str: cleaned_prompt = (prompt or "").strip() cleaned_style = (style or "").strip() if not cleaned_style: return cleaned_prompt if cleaned_style.lower() in cleaned_prompt.lower(): return cleaned_prompt if not cleaned_prompt: return cleaned_style return f"{cleaned_style}, {cleaned_prompt}"