service.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. """Unified post reader for image-text and video decode inputs."""
  2. from __future__ import annotations
  3. from typing import Protocol
  4. from acquisition.domain import CandidateItem, MediaAsset
  5. from core.config import Settings
  6. from core.models import Card, ExtractedContent, Post
  7. from decode_content.models import ReadCard, ReadResult
  8. from decode_content.readers.imgtext import BailianExtractor, read_imgtext
  9. from decode_content.readers.video import read_video
  10. class ImageReader(Protocol):
  11. def __call__(self, post: Post) -> ExtractedContent:
  12. ...
  13. class VideoReader(Protocol):
  14. def __call__(self, post: Post) -> ExtractedContent:
  15. ...
  16. def _media_url(asset: MediaAsset) -> str | None:
  17. return asset.cdn_url or asset.oss_url or asset.source_url
  18. def post_from_candidate_item(item: CandidateItem, media_assets: list[MediaAsset]) -> Post:
  19. """Build the reader input from formal acquisition DB objects."""
  20. ordered = sorted(media_assets, key=lambda asset: asset.position)
  21. image_urls: list[str] = []
  22. video_urls: list[str] = []
  23. cards: list[Card] = []
  24. for asset in ordered:
  25. url = _media_url(asset)
  26. if not url:
  27. continue
  28. media_type = asset.media_type.lower()
  29. if media_type == "video":
  30. video_urls.append(url)
  31. continue
  32. if media_type in {"image", "cover", "frame"}:
  33. image_urls.append(url)
  34. cards.append(
  35. Card(
  36. index=len(cards) + 1,
  37. kind="frame" if media_type == "frame" else "image",
  38. url=url,
  39. timestamp=asset.metadata.get("timestamp"),
  40. )
  41. )
  42. source_payload = item.source_payload or {}
  43. content_id = item.platform_item_id or (str(item.id) if item.id else "")
  44. return Post(
  45. id=f"{item.platform}_{content_id}" if content_id and not content_id.startswith(f"{item.platform}_") else content_id,
  46. platform=item.platform,
  47. url=item.canonical_url or "",
  48. content_id=content_id,
  49. title=item.title or "",
  50. content_type=item.content_type or ("video" if video_urls else "图文"),
  51. body_text=(
  52. item.raw_summary
  53. or source_payload.get("body_text")
  54. or source_payload.get("desc")
  55. or source_payload.get("content")
  56. or ""
  57. ),
  58. topic_list=source_payload.get("topic_list") or source_payload.get("topics") or [],
  59. image_urls=image_urls,
  60. video_urls=video_urls,
  61. cards=[] if video_urls else cards,
  62. author_name=item.author_name,
  63. raw=source_payload,
  64. )
  65. def read_result_from_extracted(post: Post, extracted: ExtractedContent) -> ReadResult:
  66. cmap = {card.index: card.content for card in extracted.cards}
  67. cards: list[ReadCard] = []
  68. source_cards = post.cards
  69. if not source_cards and post.image_urls:
  70. source_cards = [
  71. {"index": idx, "kind": "image", "url": url}
  72. for idx, url in enumerate(post.image_urls, start=1)
  73. ]
  74. for raw in source_cards:
  75. if isinstance(raw, dict):
  76. index = int(raw.get("index") or 0)
  77. kind = str(raw.get("kind") or "image")
  78. url = raw.get("url")
  79. timestamp = raw.get("timestamp")
  80. start = raw.get("start")
  81. end = raw.get("end")
  82. else:
  83. index = raw.index
  84. kind = raw.kind
  85. url = raw.url
  86. timestamp = raw.timestamp
  87. start = raw.start
  88. end = raw.end
  89. cards.append(
  90. ReadCard(
  91. index=index,
  92. kind=kind,
  93. url=url,
  94. timestamp=timestamp,
  95. start=start,
  96. end=end,
  97. content=cmap.get(index, ""),
  98. )
  99. )
  100. parts = [extracted.text]
  101. if extracted.from_image:
  102. parts.append("【图片要点】\n" + extracted.from_image)
  103. if extracted.from_video:
  104. parts.append("【视频要点】\n" + extracted.from_video)
  105. parts.extend(f"【卡片{card.index}】{card.content}" for card in extracted.cards if card.content)
  106. text = "\n\n".join(p for p in parts if p)
  107. media = (
  108. {"type": "video", "video_url": post.video_urls[0] if post.video_urls else None, "images": []}
  109. if post.video_urls
  110. else {"type": "image", "video_url": None, "images": list(post.image_urls)}
  111. )
  112. return ReadResult(text=text, cards=cards, media=media, is_empty=bool(extracted.is_empty))
  113. def read_post(
  114. post: Post,
  115. *,
  116. settings: Settings | None = None,
  117. extractor: BailianExtractor | None = None,
  118. image_reader: ImageReader | None = None,
  119. video_reader: VideoReader | None = None,
  120. ) -> ReadResult:
  121. if post.video_urls:
  122. if video_reader is not None:
  123. extracted = video_reader(post)
  124. else:
  125. if settings is None:
  126. raise ValueError("settings is required when reading video posts")
  127. extracted = read_video(post, settings=settings)
  128. else:
  129. extracted = image_reader(post) if image_reader is not None else read_imgtext(post, extractor=extractor)
  130. return read_result_from_extracted(post, extracted)
  131. def read_item(
  132. item: CandidateItem,
  133. media_assets: list[MediaAsset],
  134. *,
  135. settings: Settings | None = None,
  136. extractor: BailianExtractor | None = None,
  137. image_reader: ImageReader | None = None,
  138. video_reader: VideoReader | None = None,
  139. ) -> ReadResult:
  140. return read_post(
  141. post_from_candidate_item(item, media_assets),
  142. settings=settings,
  143. extractor=extractor,
  144. image_reader=image_reader,
  145. video_reader=video_reader,
  146. )
  147. __all__ = [
  148. "ImageReader",
  149. "VideoReader",
  150. "post_from_candidate_item",
  151. "read_item",
  152. "read_post",
  153. "read_result_from_extracted",
  154. ]