fix: 修复 Liblib provider 认证和多个图片生成关键缺陷
- 重写 LiblibImageProvider:Bearer token 改为 HMAC-SHA1 签名认证, 适配 Liblib 真实 API(Star-3 Alpha 文生图端点) - 修复 chapter.images JSON 列原地修改不持久化(深拷贝+整列重赋值) - 修复 generate_chapter_images 在事务提交前派发(改为 commit 后统一 delay) - 修复 initialize_chapter_images 覆盖已完成图片(新增 merge 去重逻辑) - 修复 Android failed 图片渲染为错误卡片(改为隐藏,保持正文连续) - 模型模板 UUID 改为环境变量配置(LIBLIB_TEMPLATE_UUID) - 更新 .env 凭证格式为 ACCESS_KEY/SECRET_KEY - 补充 test_memoir_image_bootstrap 缺失的 unittest.mock 导入 Made-with: Cursor
This commit is contained in:
@@ -78,6 +78,54 @@ def _update_task_status_sync(user_id: str, task_id: str, status: str, result: Di
|
||||
except Exception as e:
|
||||
logger.error(f"更新任务状态失败: {e}")
|
||||
|
||||
|
||||
def _merge_chapter_image_assets(
|
||||
existing_images: list[dict] | None,
|
||||
placeholders: list[dict],
|
||||
provider: str,
|
||||
style: str,
|
||||
size: str,
|
||||
now_iso: str,
|
||||
) -> list[dict]:
|
||||
existing_by_placeholder = {
|
||||
item.get("placeholder"): dict(item)
|
||||
for item in (existing_images or [])
|
||||
if item.get("placeholder")
|
||||
}
|
||||
merged_assets: list[dict] = []
|
||||
|
||||
for item in placeholders:
|
||||
existing = existing_by_placeholder.get(item["placeholder"])
|
||||
if existing:
|
||||
merged_item = dict(existing)
|
||||
merged_item["index"] = item["index"]
|
||||
merged_item["placeholder"] = item["placeholder"]
|
||||
merged_item["description"] = item["description"]
|
||||
merged_item["provider"] = merged_item.get("provider") or provider
|
||||
merged_item["style"] = merged_item.get("style") or style
|
||||
merged_item["size"] = merged_item.get("size") or size
|
||||
merged_item["created_at"] = merged_item.get("created_at") or now_iso
|
||||
merged_item["updated_at"] = merged_item.get("updated_at") or now_iso
|
||||
if merged_item.get("status") == "completed" and not merged_item.get("url"):
|
||||
merged_item["status"] = "failed"
|
||||
merged_item["error"] = merged_item.get("error") or "missing image url"
|
||||
else:
|
||||
merged_item = build_initial_image_assets(
|
||||
placeholders=[item],
|
||||
provider=provider,
|
||||
style=style,
|
||||
size=size,
|
||||
now_iso=now_iso,
|
||||
)[0]
|
||||
merged_assets.append(merged_item)
|
||||
|
||||
return merged_assets
|
||||
|
||||
|
||||
def chapter_has_images_to_generate(images: list[dict] | None) -> bool:
|
||||
return any(item.get("status") in {"pending", "failed"} for item in (images or []))
|
||||
|
||||
|
||||
def initialize_chapter_images(chapter) -> list[dict]:
|
||||
"""Parse IMAGE placeholders from chapter content and build pending image assets."""
|
||||
settings = MemoirImageSettings.from_env()
|
||||
@@ -88,15 +136,14 @@ def initialize_chapter_images(chapter) -> list[dict]:
|
||||
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(
|
||||
chapter.images = _merge_chapter_image_assets(
|
||||
existing_images=chapter.images,
|
||||
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
|
||||
|
||||
|
||||
@@ -234,6 +281,7 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
|
||||
try:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
chapters_to_enqueue: set[str] = set()
|
||||
# 获取段落
|
||||
stmt = select(Segment).where(Segment.id.in_(segment_ids))
|
||||
result = db.execute(stmt)
|
||||
@@ -401,6 +449,8 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
|
||||
db.flush()
|
||||
|
||||
initialize_chapter_images(chapter)
|
||||
if chapter_has_images_to_generate(chapter.images):
|
||||
chapters_to_enqueue.add(chapter.id)
|
||||
|
||||
# 更新 Book
|
||||
stmt_book = select(Book).where(Book.user_id == user_id).order_by(Book.updated_at.desc())
|
||||
@@ -426,6 +476,13 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
|
||||
seg.processed = True
|
||||
|
||||
db.commit()
|
||||
|
||||
for chapter_id in sorted(chapters_to_enqueue):
|
||||
try:
|
||||
generate_chapter_images.delay(chapter_id)
|
||||
except Exception as exc:
|
||||
logger.warning(f"补图任务派发失败: chapter={chapter_id}, error={exc}")
|
||||
|
||||
logger.info(f"回忆录处理完成: user_id={user_id}, task_id={task_id}")
|
||||
|
||||
# 更新任务状态为成功
|
||||
@@ -546,16 +603,21 @@ def generate_chapter_images(self, chapter_id: str):
|
||||
|
||||
settings = MemoirImageSettings.from_env()
|
||||
prompt_service = MemoirImagePromptService(llm_service.get_llm(), settings)
|
||||
provider = LiblibImageProvider()
|
||||
provider = LiblibImageProvider(template_uuid=settings.liblib_template_uuid)
|
||||
storage = TencentCosStorageService.from_env()
|
||||
images = [dict(item) for item in (chapter.images or [])]
|
||||
|
||||
for item in chapter.images:
|
||||
for index, item in enumerate(images):
|
||||
if item.get("status") == "completed" and item.get("url"):
|
||||
continue
|
||||
if item.get("status") not in {"pending", "failed"}:
|
||||
continue
|
||||
|
||||
item["status"] = "processing"
|
||||
current_item = dict(item)
|
||||
current_item["status"] = "processing"
|
||||
current_item["updated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
images[index] = current_item
|
||||
chapter.images = images
|
||||
db.commit()
|
||||
|
||||
try:
|
||||
@@ -580,21 +642,23 @@ def generate_chapter_images(self, chapter_id: str):
|
||||
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
|
||||
key = build_cos_key(chapter.user_id, chapter.id, current_item["index"], prompt_data["prompt"])
|
||||
current_item["url"] = storage.upload_bytes(image_bytes, key, "image/png")
|
||||
current_item["prompt"] = prompt_data["prompt"]
|
||||
current_item["style"] = prompt_data["style"]
|
||||
current_item["size"] = prompt_data["size"]
|
||||
current_item["status"] = "completed"
|
||||
current_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}")
|
||||
current_item["status"] = "failed"
|
||||
current_item["error"] = str(exc)
|
||||
logger.warning(f"图片生成失败: chapter={chapter_id}, index={current_item.get('index')}, error={exc}")
|
||||
|
||||
item["updated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
current_item["updated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
images[index] = current_item
|
||||
chapter.images = images
|
||||
db.commit()
|
||||
|
||||
db.commit()
|
||||
return {"status": "success"}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
Reference in New Issue
Block a user