pipeline.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. """流水线:把 6 步串起来,每步落库。INGEST_ENABLED=False 时只组装+存,不真实发送。
  2. 依赖都可注入(fetch/extract/chat/store),便于离线测试与替换平台。
  3. """
  4. from __future__ import annotations
  5. import logging
  6. from typing import Callable, Optional
  7. from creation_knowledge.config import Settings
  8. from creation_knowledge.integrations.crawler import CrawlerError, fetch_post_detail
  9. from creation_knowledge.integrations.db import CkStore
  10. from creation_knowledge.integrations.extractor import ExtractorError, GeminiExtractor
  11. from creation_knowledge.integrations.llm import ChatFn, default_chat
  12. from creation_knowledge.ingest import IngestError, ingest as real_ingest
  13. from creation_knowledge.models import ExtractedContent, Post
  14. from creation_knowledge.stages import (
  15. build_ingest_payload,
  16. deconstruct_item,
  17. screen_post,
  18. split_post,
  19. )
  20. logger = logging.getLogger(__name__)
  21. FetchFn = Callable[[str], Post]
  22. ExtractFn = Callable[[Post], ExtractedContent]
  23. def _process_one(
  24. url: str,
  25. *,
  26. settings: Settings,
  27. store: CkStore,
  28. fetch_fn: FetchFn,
  29. extract_fn: ExtractFn,
  30. chat: ChatFn,
  31. ingest_enabled: bool,
  32. ) -> dict:
  33. # 1) 拉取
  34. try:
  35. post = fetch_fn(url)
  36. except CrawlerError as exc:
  37. return {"url": url, "status": "fetch_failed", "error": str(exc)}
  38. # 1.5) 视频帖:抽帧补成卡片(best-effort,失败不阻塞整帖)
  39. if post.video_urls and not post.cards:
  40. try:
  41. from creation_knowledge.integrations.video_frames import extract_frames
  42. post.cards = extract_frames(
  43. post.video_urls[0],
  44. out_dir=f"{settings.frames_dir}/{post.id}",
  45. url_prefix=f"/frames/{post.id}",
  46. platform=post.platform,
  47. max_frames=settings.max_cards,
  48. )
  49. except Exception as exc: # 抽帧失败不影响图文/正文路径
  50. logger.warning("post %s 抽帧失败: %s", post.id, exc)
  51. store.upsert_post(post) # stage=fetched(含 cards)
  52. # 2) 多模态提取
  53. try:
  54. content = extract_fn(post)
  55. store.set_extracted(post.id, content.model_dump()) # stage=extracted
  56. except ExtractorError as exc:
  57. store.update_stage(post.id, "failed")
  58. return {"url": url, "post_id": post.id, "status": "extract_failed", "error": str(exc)}
  59. # 3) 筛选
  60. screening = screen_post(post, content, chat=chat)
  61. store.set_screening(post.id, screening.model_dump()) # stage=screened
  62. if not screening.passed:
  63. store.update_stage(post.id, "rejected")
  64. return {"url": url, "post_id": post.id, "status": "rejected",
  65. "score": screening.score, "reason": screening.reason}
  66. # 4) 拆分 -> 5) 解构 -> 6) 组装(+可选入库)
  67. items = split_post(post, content, chat=chat)
  68. item_ids = []
  69. for item in items:
  70. deco = deconstruct_item(item, chat=chat)
  71. payload = build_ingest_payload(post, item, deco)
  72. item_id = store.save_item(
  73. post.id, item.model_dump(), deco.model_dump(), payload.model_dump()
  74. )
  75. item_ids.append(item_id)
  76. if ingest_enabled:
  77. try:
  78. res = real_ingest(payload, settings=settings)
  79. store.update_item_ingest(item_id, "ingested", res.get("knowledge_id"))
  80. except IngestError:
  81. store.update_item_ingest(item_id, "failed", None)
  82. store.update_stage(post.id, "done")
  83. return {"url": url, "post_id": post.id, "status": "done", "items": len(item_ids)}
  84. def run_pipeline(
  85. urls: list[str],
  86. *,
  87. settings: Optional[Settings] = None,
  88. env_file: str = ".env",
  89. ingest_enabled: Optional[bool] = None,
  90. store: Optional[CkStore] = None,
  91. fetch_fn: Optional[FetchFn] = None,
  92. extract_fn: Optional[ExtractFn] = None,
  93. chat: Optional[ChatFn] = None,
  94. ) -> list[dict]:
  95. settings = settings or Settings.from_env(env_file)
  96. ingest_enabled = settings.ingest_enabled if ingest_enabled is None else ingest_enabled
  97. store = store or CkStore(settings.pg)
  98. fetch_fn = fetch_fn or (lambda url: fetch_post_detail(url, settings=settings))
  99. extract_fn = extract_fn or GeminiExtractor.from_env(env_file=env_file).extract
  100. chat = chat or default_chat(env_file)
  101. results = []
  102. for url in urls:
  103. results.append(_process_one(
  104. url, settings=settings, store=store, fetch_fn=fetch_fn,
  105. extract_fn=extract_fn, chat=chat, ingest_enabled=ingest_enabled,
  106. ))
  107. return results