test_decode_readers_service.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. from __future__ import annotations
  2. from uuid import uuid4
  3. from acquisition.domain import CandidateItem, MediaAsset
  4. from core.models import Card, CardExtract, ExtractedContent, Post
  5. from decode_content.readers.service import (
  6. UnsupportedPostModeError,
  7. post_from_candidate_item,
  8. read_item,
  9. read_post,
  10. )
  11. def test_post_from_candidate_item_prefers_cdn_then_oss_then_source():
  12. item_id = uuid4()
  13. item = CandidateItem(
  14. id=item_id,
  15. platform="xiaohongshu",
  16. platform_item_id="abc",
  17. unique_key="xhs:abc",
  18. canonical_url="https://xhs/item/abc",
  19. content_mode="image_post",
  20. title="脚本结构",
  21. author_name="sam",
  22. raw_summary="正文",
  23. source_payload={"topic_list": ["脚本"]},
  24. )
  25. media = [
  26. MediaAsset(item_id=item_id, media_type="image", source_url="source1", cdn_url="cdn1", position=2),
  27. MediaAsset(item_id=item_id, media_type="image", source_url="source0", oss_url="oss0", position=1),
  28. ]
  29. post = post_from_candidate_item(item, media)
  30. assert post.id == "xiaohongshu_abc"
  31. assert post.content_id == "abc"
  32. assert post.unique_key == "xhs:abc"
  33. assert post.content_mode == "image_post"
  34. assert post.image_urls == ["oss0", "cdn1"]
  35. assert post.media_count == 2
  36. assert post.cover_url == "oss0"
  37. assert [card.index for card in post.cards] == [1, 2]
  38. assert post.body_text == "正文"
  39. assert post.topic_list == ["脚本"]
  40. def test_post_from_candidate_item_prefers_full_body_text_over_summary():
  41. item_id = uuid4()
  42. item = CandidateItem(
  43. id=item_id,
  44. platform="weixin",
  45. platform_item_id="wx1",
  46. content_mode="article",
  47. title="长文",
  48. body_text="完整正文",
  49. raw_summary="摘要",
  50. )
  51. post = post_from_candidate_item(item, [])
  52. assert post.body_text == "完整正文"
  53. def test_post_from_candidate_item_reads_legacy_nested_body_before_summary():
  54. item_id = uuid4()
  55. item = CandidateItem(
  56. id=item_id,
  57. platform="xiaohongshu",
  58. platform_item_id="x1",
  59. content_mode="image_post",
  60. raw_summary="摘要",
  61. source_payload={"detail": {"data": {"data": {"body_text": "旧 raw 正文"}}}},
  62. )
  63. post = post_from_candidate_item(item, [])
  64. assert post.body_text == "旧 raw 正文"
  65. def test_read_post_imgtext_merges_text_cards_and_media():
  66. post = Post(
  67. id="xhs_1",
  68. platform="xiaohongshu",
  69. url="u",
  70. content_id="1",
  71. image_urls=["img1", "img2"],
  72. cards=[Card(index=1, kind="image", url="img1"), Card(index=2, kind="image", url="img2")],
  73. )
  74. def image_reader(p: Post) -> ExtractedContent:
  75. assert p.id == "xhs_1"
  76. return ExtractedContent(
  77. text="主体",
  78. from_image="图片知识",
  79. cards=[CardExtract(index=1, content="卡1"), CardExtract(index=2, content="卡2")],
  80. )
  81. result = read_post(post, image_reader=image_reader)
  82. assert "图片知识" in result.text
  83. assert result.cards[0].content == "卡1"
  84. assert result.media == {"type": "image", "video_url": None, "images": ["img1", "img2"]}
  85. def test_read_item_routes_video_to_video_reader():
  86. item_id = uuid4()
  87. item = CandidateItem(
  88. id=item_id,
  89. platform="douyin",
  90. platform_item_id="v1",
  91. content_mode="video_post",
  92. title="视频",
  93. )
  94. media = [MediaAsset(item_id=item_id, media_type="video", cdn_url="https://cdn/video.mp4", position=1)]
  95. def video_reader(post: Post) -> ExtractedContent:
  96. assert post.video_urls == ["https://cdn/video.mp4"]
  97. post.cards = [Card(index=1, kind="segment", url=post.video_urls[0], start=0, end=12)]
  98. return ExtractedContent(text="视频理解", cards=[CardExtract(index=1, content="片段知识")])
  99. result = read_item(item, media, video_reader=video_reader)
  100. assert result.media["type"] == "video"
  101. assert result.cards[0].kind == "segment"
  102. assert result.cards[0].content == "片段知识"
  103. def test_video_source_url_without_oss_or_cdn_is_not_decodable():
  104. import pytest
  105. item_id = uuid4()
  106. item = CandidateItem(
  107. id=item_id,
  108. platform="douyin",
  109. platform_item_id="v1",
  110. content_mode="video_post",
  111. title="视频",
  112. )
  113. media = [
  114. MediaAsset(
  115. item_id=item_id,
  116. media_type="video",
  117. source_url="https://video.test/temporary.mp4",
  118. position=1,
  119. )
  120. ]
  121. post = post_from_candidate_item(item, media)
  122. assert post.video_urls == []
  123. with pytest.raises(UnsupportedPostModeError, match="video_url_missing"):
  124. read_item(item, media)
  125. def test_read_post_rejects_unsupported_and_missing_video():
  126. unsupported = Post(
  127. id="dy_x",
  128. platform="douyin",
  129. url="u",
  130. content_id="x",
  131. content_mode="unsupported",
  132. )
  133. missing_video = Post(
  134. id="dy_v",
  135. platform="douyin",
  136. url="u",
  137. content_id="v",
  138. content_mode="video_post",
  139. )
  140. import pytest
  141. with pytest.raises(UnsupportedPostModeError, match="unsupported_content_mode"):
  142. read_post(unsupported)
  143. with pytest.raises(UnsupportedPostModeError, match="video_url_missing"):
  144. read_post(missing_video)