media.py 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. """媒体本地落盘:把图文帖的图片、视频帖的整段 mp4 存到 data/<run_id>/<platform>/<post_id>/。
  2. 解耦设计:仅当 settings.data_dir 非空时调用;视频落盘在 extract_video 内(复用其内存
  3. 字节),这里只管图片下载 + 卡片 url 改写为本地 /data 路径。
  4. """
  5. from __future__ import annotations
  6. import datetime
  7. import logging
  8. from dataclasses import dataclass
  9. from pathlib import Path
  10. from typing import Callable
  11. from creation_knowledge.integrations.video_extract import _default_download
  12. from core.models import Post
  13. logger = logging.getLogger(__name__)
  14. Downloader = Callable[[str, str], bytes]
  15. def run_id() -> str:
  16. """运行批次 id,默认时间戳到秒。"""
  17. return datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
  18. @dataclass
  19. class MediaTarget:
  20. dir: Path # 本地目录 data/<run_id>/<platform>/<post_id>/
  21. public_base: str # 浏览器前缀 /data/<run_id>/<platform>/<post_id>
  22. def media_target(data_dir: str, batch: str, platform: str, post_id: str) -> MediaTarget:
  23. rel = f"{batch}/{platform}/{post_id}"
  24. return MediaTarget(dir=Path(data_dir) / rel, public_base=f"/data/{rel}")
  25. def _ext_from_bytes(b: bytes) -> str:
  26. """按文件头嗅探扩展名(不依赖 Content-Type,便于注入式测试)。"""
  27. if b[:4] == b"\x89PNG":
  28. return ".png"
  29. if b[:3] == b"GIF":
  30. return ".gif"
  31. if b[:4] == b"RIFF" and b[8:12] == b"WEBP":
  32. return ".webp"
  33. return ".jpg"
  34. def save_images(
  35. post: Post,
  36. media_dir: Path,
  37. public_base: str,
  38. *,
  39. downloader: Downloader = _default_download,
  40. ) -> None:
  41. """下载图文帖每张远程图片 → media_dir/image_<index>.<ext>,并把 card.url 改写为本地路径。
  42. 单张失败只告警跳过(不影响其余卡片与整条流水线)。
  43. """
  44. media_dir = Path(media_dir)
  45. for card in post.cards:
  46. if card.kind != "image" or not card.url or not card.url.startswith("http"):
  47. continue
  48. try:
  49. data = downloader(card.url, post.platform)
  50. except Exception as exc: # 单张图失败不致命
  51. logger.warning("图片下载失败 idx=%s %s: %s", card.index, card.url, exc)
  52. continue
  53. if not data:
  54. continue
  55. media_dir.mkdir(parents=True, exist_ok=True)
  56. fname = f"image_{card.index}{_ext_from_bytes(data)}"
  57. (media_dir / fname).write_bytes(data)
  58. card.url = f"{public_base}/{fname}"