feat(api): generate memoir chapter images asynchronously via Celery

Made-with: Cursor
This commit is contained in:
Kevin
2026-03-10 16:05:13 +08:00
parent d5ed2940aa
commit 879466fde1
2 changed files with 173 additions and 2 deletions

View File

@@ -26,9 +26,13 @@ from agents.prompts.memory_prompts import (
CHAPTER_CATEGORIES,
)
from agents.prompts.profile_prompts import format_user_profile_context
import hashlib
from services.memoir_images.parser import build_initial_image_assets, parse_image_placeholders
from services.memoir_images.prompting import MemoirImagePromptService
from services.memoir_images.provider import LiblibImageProvider
from services.memoir_images.settings import MemoirImageSettings
from services.memoir_images.storage import TencentCosStorageService
logger = logging.getLogger(__name__)
@@ -526,8 +530,71 @@ def generate_chapter_content(self, user_id: str, stage: str, new_content: str):
raise self.retry(exc=e)
def build_cos_key(user_id: str, chapter_id: str, index: int, prompt: str) -> str:
short_hash = hashlib.sha1(prompt.encode("utf-8")).hexdigest()[:10]
return f"memoirs/{user_id}/{chapter_id}/{index}-{short_hash}.png"
@shared_task(bind=True, max_retries=3, default_retry_delay=30)
def generate_chapter_images(self, chapter_id: str):
"""Async task to generate images for a chapter's pending image assets."""
logger.info(f"图片生成任务(桩): chapter_id={chapter_id}")
return {"status": "stub", "chapter_id": chapter_id}
db = SessionLocal()
try:
chapter = db.get(Chapter, chapter_id)
if not chapter or not chapter.images:
return {"status": "no_images"}
settings = MemoirImageSettings.from_env()
prompt_service = MemoirImagePromptService(llm_service.get_llm(), settings)
provider = LiblibImageProvider()
storage = TencentCosStorageService.from_env()
for item in chapter.images:
if item.get("status") == "completed" and item.get("url"):
continue
if item.get("status") not in {"pending", "failed"}:
continue
item["status"] = "processing"
db.commit()
try:
context_lines = (chapter.content or "").split("\n")
context_excerpt = " ".join(context_lines[:5])[:200]
prompt_data = prompt_service.build_prompt(
chapter_title=chapter.title,
chapter_category=chapter.category or "",
description=item.get("description", ""),
context_excerpt=context_excerpt,
)
job = provider.submit_generation(
prompt=prompt_data["prompt"],
size=prompt_data["size"],
style=prompt_data["style"],
)
if job["status"] != "completed":
job = provider.poll_until_complete(
job,
poll_interval_seconds=settings.poll_interval_seconds,
max_attempts=settings.max_attempts,
)
image_bytes = provider.download_image(job)
key = build_cos_key(chapter.user_id, chapter.id, item["index"], prompt_data["prompt"])
item["url"] = storage.upload_bytes(image_bytes, key, "image/png")
item["prompt"] = prompt_data["prompt"]
item["style"] = prompt_data["style"]
item["size"] = prompt_data["size"]
item["status"] = "completed"
item["error"] = None
except Exception as exc:
item["status"] = "failed"
item["error"] = str(exc)
logger.warning(f"图片生成失败: chapter={chapter_id}, index={item.get('index')}, error={exc}")
item["updated_at"] = datetime.now(timezone.utc).isoformat()
db.commit()
return {"status": "success"}
finally:
db.close()