feat(api): initialize memoir chapter image assets on creation
Made-with: Cursor
This commit is contained in:
@@ -90,7 +90,7 @@ class Chapter(Base):
|
|||||||
content = Column(Text, nullable=False)
|
content = Column(Text, nullable=False)
|
||||||
order_index = Column(Integer, nullable=False)
|
order_index = Column(Integer, nullable=False)
|
||||||
status = Column(String, default="draft") # draft, completed
|
status = Column(String, default="draft") # draft, completed
|
||||||
images = Column(JSON, nullable=True) # 图片 URL 列表
|
images = Column(JSON, nullable=True) # 图片元数据对象列表
|
||||||
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
|
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
|
||||||
category = Column(String, nullable=True) # 章节分类
|
category = Column(String, nullable=True) # 章节分类
|
||||||
is_new = Column(Boolean, default=True) # 是否为新内容(未读)
|
is_new = Column(Boolean, default=True) # 是否为新内容(未读)
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
Celery 任务模块
|
Celery 任务模块
|
||||||
"""
|
"""
|
||||||
from .celery_app import celery_app
|
from .celery_app import celery_app
|
||||||
from .memoir_tasks import process_memoir_segments
|
from .memoir_tasks import process_memoir_segments, generate_chapter_images
|
||||||
|
|
||||||
__all__ = ["celery_app", "process_memoir_segments"]
|
__all__ = ["celery_app", "process_memoir_segments", "generate_chapter_images"]
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ from agents.prompts.memory_prompts import (
|
|||||||
CHAPTER_CATEGORIES,
|
CHAPTER_CATEGORIES,
|
||||||
)
|
)
|
||||||
from agents.prompts.profile_prompts import format_user_profile_context
|
from agents.prompts.profile_prompts import format_user_profile_context
|
||||||
|
from services.memoir_images.parser import build_initial_image_assets, parse_image_placeholders
|
||||||
|
from services.memoir_images.prompting import MemoirImagePromptService
|
||||||
|
from services.memoir_images.settings import MemoirImageSettings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -71,6 +74,28 @@ def _update_task_status_sync(user_id: str, task_id: str, status: str, result: Di
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"更新任务状态失败: {e}")
|
logger.error(f"更新任务状态失败: {e}")
|
||||||
|
|
||||||
|
def initialize_chapter_images(chapter) -> list[dict]:
|
||||||
|
"""Parse IMAGE placeholders from chapter content and build pending image assets."""
|
||||||
|
settings = MemoirImageSettings.from_env()
|
||||||
|
if not settings.enabled:
|
||||||
|
chapter.images = []
|
||||||
|
return chapter.images
|
||||||
|
|
||||||
|
prompt_service = MemoirImagePromptService(llm=None, settings=settings)
|
||||||
|
placeholders = parse_image_placeholders(chapter.content, settings.max_per_chapter)
|
||||||
|
style = prompt_service.CATEGORY_STYLE_MAP.get(chapter.category, settings.default_style)
|
||||||
|
chapter.images = build_initial_image_assets(
|
||||||
|
placeholders=placeholders,
|
||||||
|
provider=settings.provider,
|
||||||
|
style=style,
|
||||||
|
size=settings.default_size,
|
||||||
|
now_iso=datetime.now(timezone.utc).isoformat(),
|
||||||
|
)
|
||||||
|
if chapter.images:
|
||||||
|
generate_chapter_images.delay(chapter.id)
|
||||||
|
return chapter.images
|
||||||
|
|
||||||
|
|
||||||
STAGE_KEYWORDS = {
|
STAGE_KEYWORDS = {
|
||||||
"childhood": ["童年", "小时候", "出生", "家乡", "小镇"],
|
"childhood": ["童年", "小时候", "出生", "家乡", "小镇"],
|
||||||
"education": ["上学", "学校", "老师", "同学", "教育", "大学"],
|
"education": ["上学", "学校", "老师", "同学", "教育", "大学"],
|
||||||
@@ -371,6 +396,8 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
|
|||||||
|
|
||||||
db.flush()
|
db.flush()
|
||||||
|
|
||||||
|
initialize_chapter_images(chapter)
|
||||||
|
|
||||||
# 更新 Book
|
# 更新 Book
|
||||||
stmt_book = select(Book).where(Book.user_id == user_id).order_by(Book.updated_at.desc())
|
stmt_book = select(Book).where(Book.user_id == user_id).order_by(Book.updated_at.desc())
|
||||||
result_book = db.execute(stmt_book)
|
result_book = db.execute(stmt_book)
|
||||||
@@ -497,3 +524,10 @@ def generate_chapter_content(self, user_id: str, stage: str, new_content: str):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"章节生成失败: {e}")
|
logger.error(f"章节生成失败: {e}")
|
||||||
raise self.retry(exc=e)
|
raise self.retry(exc=e)
|
||||||
|
|
||||||
|
|
||||||
|
@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}
|
||||||
|
|||||||
26
api/tests/test_memoir_image_bootstrap.py
Normal file
26
api/tests/test_memoir_image_bootstrap.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from api.tasks.memoir_tasks import initialize_chapter_images
|
||||||
|
|
||||||
|
|
||||||
|
class MemoirImageBootstrapTest(unittest.TestCase):
|
||||||
|
@patch("api.tasks.memoir_tasks.generate_chapter_images.delay")
|
||||||
|
def test_initialize_chapter_images_sets_pending_assets_and_enqueues_task(self, delay_mock):
|
||||||
|
chapter = type(
|
||||||
|
"ChapterStub",
|
||||||
|
(),
|
||||||
|
{
|
||||||
|
"id": "chapter-1",
|
||||||
|
"title": "童年的夏天",
|
||||||
|
"category": "childhood",
|
||||||
|
"content": "那条路我一直记得。\n\n{{{{IMAGE:南方小镇的青石板路}}}}",
|
||||||
|
"images": [],
|
||||||
|
},
|
||||||
|
)()
|
||||||
|
|
||||||
|
assets = initialize_chapter_images(chapter)
|
||||||
|
|
||||||
|
self.assertEqual(len(assets), 1)
|
||||||
|
self.assertEqual(assets[0]["status"], "pending")
|
||||||
|
delay_mock.assert_called_once_with("chapter-1")
|
||||||
Reference in New Issue
Block a user