feat(api): add memoir image prompt settings and optimization service
Made-with: Cursor
This commit is contained in:
63
api/services/memoir_images/prompting.py
Normal file
63
api/services/memoir_images/prompting.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
|
||||
from .settings import MemoirImageSettings
|
||||
|
||||
|
||||
class MemoirImagePromptService:
|
||||
CATEGORY_STYLE_MAP = {
|
||||
"childhood": "watercolor",
|
||||
"family": "watercolor",
|
||||
"career_early": "realistic",
|
||||
"career_achievement": "realistic",
|
||||
"career_challenge": "realistic",
|
||||
"beliefs": "editorial illustration",
|
||||
"summary": "editorial illustration",
|
||||
}
|
||||
|
||||
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": parsed["prompt"],
|
||||
"style": parsed.get("style", style),
|
||||
"size": parsed.get("size", self.settings.default_size),
|
||||
"prompt_context": prompt_context,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"prompt": f"{description}\n\nScene context: {context_excerpt}",
|
||||
"style": style,
|
||||
"size": self.settings.default_size,
|
||||
"prompt_context": prompt_context,
|
||||
}
|
||||
25
api/services/memoir_images/settings.py
Normal file
25
api/services/memoir_images/settings.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MemoirImageSettings:
|
||||
enabled: bool
|
||||
max_per_chapter: int
|
||||
provider: str
|
||||
default_style: str
|
||||
default_size: str
|
||||
poll_interval_seconds: int
|
||||
max_attempts: int
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "MemoirImageSettings":
|
||||
return cls(
|
||||
enabled=os.getenv("MEMOIR_IMAGE_ENABLED", "").lower() in {"1", "true", "yes"},
|
||||
max_per_chapter=int(os.getenv("MEMOIR_IMAGE_MAX_PER_CHAPTER", "2")),
|
||||
provider=os.getenv("MEMOIR_IMAGE_PROVIDER", "liblib"),
|
||||
default_style=os.getenv("MEMOIR_IMAGE_STYLE_DEFAULT", "watercolor"),
|
||||
default_size=os.getenv("MEMOIR_IMAGE_SIZE_DEFAULT", "1024x1024"),
|
||||
poll_interval_seconds=int(os.getenv("MEMOIR_IMAGE_POLL_INTERVAL", "3")),
|
||||
max_attempts=int(os.getenv("MEMOIR_IMAGE_MAX_ATTEMPTS", "20")),
|
||||
)
|
||||
59
api/tests/test_memoir_image_prompting.py
Normal file
59
api/tests/test_memoir_image_prompting.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from api.services.memoir_images.prompting import MemoirImagePromptService
|
||||
from api.services.memoir_images.settings import MemoirImageSettings
|
||||
|
||||
|
||||
class MemoirImagePromptingTest(unittest.TestCase):
|
||||
def test_prompt_service_uses_category_style_and_plain_fallback_without_llm(self):
|
||||
settings = MemoirImageSettings(
|
||||
enabled=True,
|
||||
max_per_chapter=2,
|
||||
provider="liblib",
|
||||
default_style="watercolor",
|
||||
default_size="1024x1024",
|
||||
poll_interval_seconds=3,
|
||||
max_attempts=20,
|
||||
)
|
||||
service = MemoirImagePromptService(llm=None, settings=settings)
|
||||
|
||||
result = service.build_prompt(
|
||||
chapter_title="童年的夏天",
|
||||
chapter_category="childhood",
|
||||
description="奶奶坐在院子里的藤椅上",
|
||||
context_excerpt="梧桐树下很安静,夏天总有蝉鸣。",
|
||||
)
|
||||
|
||||
self.assertEqual(result["style"], "watercolor")
|
||||
self.assertEqual(result["size"], "1024x1024")
|
||||
self.assertIn("奶奶坐在院子里的藤椅上", result["prompt"])
|
||||
self.assertIn("childhood", result["prompt_context"])
|
||||
|
||||
def test_prompt_service_parses_structured_llm_response(self):
|
||||
settings = MemoirImageSettings(
|
||||
enabled=True,
|
||||
max_per_chapter=2,
|
||||
provider="liblib",
|
||||
default_style="watercolor",
|
||||
default_size="1024x1024",
|
||||
poll_interval_seconds=3,
|
||||
max_attempts=20,
|
||||
)
|
||||
llm = Mock()
|
||||
llm.invoke.return_value.content = (
|
||||
'{"prompt":"A grandmother in a quiet courtyard, summer cicadas, soft watercolor",'
|
||||
'"style":"watercolor","size":"1024x1024"}'
|
||||
)
|
||||
service = MemoirImagePromptService(llm=llm, settings=settings)
|
||||
|
||||
result = service.build_prompt(
|
||||
chapter_title="童年的夏天",
|
||||
chapter_category="childhood",
|
||||
description="奶奶坐在院子里的藤椅上",
|
||||
context_excerpt="梧桐树下很安静,夏天总有蝉鸣。",
|
||||
)
|
||||
|
||||
self.assertEqual(result["prompt"], "A grandmother in a quiet courtyard, summer cicadas, soft watercolor")
|
||||
self.assertEqual(result["style"], "watercolor")
|
||||
self.assertEqual(result["size"], "1024x1024")
|
||||
Reference in New Issue
Block a user