Files
life-echo/api/app/features/memoir/helpers.py

143 lines
5.1 KiB
Python

"""
回忆录序列化与图片归一化辅助(供 MemoirService 使用)。
"""
from app.core.logging import get_logger
from app.core.config import settings
from app.features.memoir.models import Chapter, ChapterSection
from app.features.memoir.memoir_images.schema import (
IMAGE_STATUS_COMPLETED,
IMAGE_STATUS_FAILED,
completed_image_assets,
normalize_image_assets,
)
from app.features.memoir.memoir_images.serializers import memoir_image_to_dict
from app.features.memoir.memoir_images.settings import MemoirImageSettings
from app.features.memoir.memoir_images.storage import (
CosDownloadUrlError,
TencentCosStorageService,
mark_image_delivery_unavailable,
normalize_cos_url,
resolve_image_storage_key,
)
logger = get_logger(__name__)
def normalize_image_assets_for_api(images: list[dict] | None) -> list[dict]:
bucket = settings.tencent_cos_bucket or ""
region = settings.tencent_cos_region or ""
base_url = settings.tencent_cos_base_url or ""
storage = TencentCosStorageService.from_settings(settings)
img_settings = MemoirImageSettings.from_settings(settings)
source_assets = normalize_image_assets(images)
if not img_settings.enabled:
source_assets = completed_image_assets(source_assets)
normalized_assets: list[dict] = []
for item in source_assets:
asset = dict(item)
normalized_url = normalize_cos_url(
asset.get("url"), bucket=bucket, region=region, base_url=base_url
)
storage_key = resolve_image_storage_key(asset)
if asset.get("status") == IMAGE_STATUS_COMPLETED and storage_key:
try:
asset["url"] = storage.get_download_url(storage_key)
except CosDownloadUrlError as exc:
logger.warning(
"章节图片签名失败: key=%s, retryable=%s, error=%s",
storage_key, exc.retryable, exc,
)
asset = mark_image_delivery_unavailable(asset)
except Exception as exc:
logger.warning("章节图片签名失败: key=%s, error=%s", storage_key, exc)
asset = mark_image_delivery_unavailable(asset)
else:
asset["url"] = normalized_url
asset.pop("storage_key", None)
normalized_assets.append(asset)
return normalized_assets
def is_image_permanently_unavailable(rec) -> bool:
if not rec:
return False
status = getattr(rec, "status", None) or ""
retryable = getattr(rec, "retryable", None)
url = getattr(rec, "url", None)
storage_key = getattr(rec, "storage_key", None)
if status == IMAGE_STATUS_FAILED and retryable is False:
return True
if status == IMAGE_STATUS_COMPLETED and not url and not storage_key:
return True
return False
def section_image_to_dict(section) -> dict | None:
if getattr(section, "image_record", None):
return memoir_image_to_dict(section.image_record)
return None
def chapter_cover_to_dict(ch: Chapter) -> dict | None:
images = getattr(ch, "images", None) or []
for m in images:
if getattr(m, "section_id", None) is None:
return memoir_image_to_dict(m)
if getattr(ch, "cover_image", None) and isinstance(ch.cover_image, dict):
return ch.cover_image
return None
def sections_to_content_and_images(ch: Chapter) -> tuple[str, list[dict]]:
sections = getattr(ch, "sections", None) or []
ordered = sorted(sections, key=lambda s: getattr(s, "order_index", 0))
parts = []
images = []
for s in ordered:
text = (getattr(s, "content", None) or "").strip()
if text:
parts.append(text)
img = section_image_to_dict(s)
if img:
images.append(img)
placeholder = (img.get("placeholder") or "").strip()
if placeholder:
parts.append(placeholder)
content = "\n\n".join(parts) if parts else ""
return content, images
def chapter_to_dict(ch: Chapter) -> dict:
content, images_list = sections_to_content_and_images(ch)
normalized_images = normalize_image_assets_for_api(images_list)
cover = chapter_cover_to_dict(ch)
cover_normalized = (
normalize_image_assets_for_api([cover])[0] if cover else None
)
sections_data = []
if getattr(ch, "sections", None):
for s in sorted(ch.sections, key=lambda x: getattr(x, "order_index", 0)):
sec_img = section_image_to_dict(s)
sec_img = (
normalize_image_assets_for_api([sec_img])[0] if sec_img else None
)
sections_data.append({
"content": (getattr(s, "content", None) or "").strip(),
"image": sec_img,
})
return {
"id": ch.id,
"title": ch.title,
"content": content,
"order_index": ch.order_index,
"status": ch.status,
"category": ch.category,
"images": normalized_images,
"cover_image": cover_normalized,
"sections": sections_data,
"updated_at": ch.updated_at.isoformat() if ch.updated_at else None,
"is_new": ch.is_new,
"source_segments": ch.source_segments or [],
}