classify_creation_items.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. #!/usr/bin/env python3
  2. """补跑 430 query 采集结果的创作知识判断。
  3. 示例:
  4. CK_ENV_FILE=.env PYTHONPATH=. .venv/bin/python scripts/classify_creation_items.py \
  5. --run-id creation-search-full-20260629 --platform xiaohongshu,weixin --workers 4
  6. CK_ENV_FILE=.env PYTHONPATH=. .venv/bin/python scripts/classify_creation_items.py \
  7. --run-id creation-search-full-20260629 --platform xiaohongshu --provider qwen --model qwen3.7-plus --workers 4
  8. CK_ENV_FILE=.env PYTHONPATH=. .venv/bin/python scripts/classify_creation_items.py \
  9. --run-id creation-search-full-20260629 --platform weixin --provider ark --model ep-20260506151915-jqvw7 --workers 4
  10. """
  11. from __future__ import annotations
  12. import argparse
  13. import concurrent.futures as cf
  14. import os
  15. import sys
  16. import time
  17. from pathlib import Path
  18. ROOT = Path(__file__).resolve().parent.parent
  19. if str(ROOT) not in sys.path:
  20. sys.path.insert(0, str(ROOT))
  21. from acquisition import store
  22. from acquisition.classify import classify_imgtext, classify_video
  23. from acquisition.creation_search import PLATFORMS, prompt_version
  24. from core.config import Settings
  25. def _parse_platforms(value: str | None) -> list[str] | None:
  26. if not value:
  27. return None
  28. out = [p.strip() for p in value.split(",") if p.strip()]
  29. bad = [p for p in out if p not in PLATFORMS]
  30. if bad:
  31. raise SystemExit(f"unsupported platform(s): {', '.join(bad)}")
  32. return out
  33. def _classify_one(item: dict, settings: Settings) -> tuple[int, int | None, str, str, str, str]:
  34. platform = item["platform"]
  35. if platform == "douyin":
  36. version = prompt_version("classify_video")
  37. is_creation, reason, knowledge, _points = classify_video(
  38. {
  39. "platform": platform,
  40. "title": item.get("title") or "",
  41. "body_text": item.get("body_text") or "",
  42. "video": item.get("video_url") or "",
  43. },
  44. settings,
  45. )
  46. else:
  47. version = prompt_version("classify_imgtext")
  48. is_creation, reason, knowledge, _points = classify_imgtext(
  49. {
  50. "platform": platform,
  51. "title": item.get("title") or "",
  52. "body_text": item.get("body_text") or "",
  53. "images": item.get("image_urls") or [],
  54. },
  55. settings,
  56. )
  57. return item["id"], is_creation, reason, knowledge, version, reason if is_creation is None else ""
  58. def main() -> None:
  59. ap = argparse.ArgumentParser()
  60. ap.add_argument("--run-id", help="默认使用最新 creation_search_run")
  61. ap.add_argument("--platform", help="平台,多个用逗号分隔;默认全部")
  62. ap.add_argument("--limit", type=int, default=0, help="最多补判多少条;0=不限制")
  63. ap.add_argument("--workers", type=int, default=3)
  64. ap.add_argument("--only-missing", action="store_true", help="只补完全未分类,不重试失败分类")
  65. ap.add_argument("--provider", choices=["auto", "openrouter", "ark", "qwen", "bailian"], help="本次补判使用的模型通道")
  66. ap.add_argument("--model", help="本次补判使用的模型名/方舟接入点,如 qwen3.7-plus 或 ep-...")
  67. args = ap.parse_args()
  68. if args.provider:
  69. os.environ["CLASSIFY_PROVIDER"] = args.provider
  70. if args.model:
  71. os.environ["CLASSIFY_MODEL"] = args.model
  72. settings = Settings.from_env(os.getenv("CK_ENV_FILE", ".env"))
  73. platforms = _parse_platforms(args.platform)
  74. conn = store.connect()
  75. rows = store.creation_items_to_classify(
  76. conn,
  77. run_id=args.run_id,
  78. platforms=platforms,
  79. retry_failed=not args.only_missing,
  80. limit=args.limit if args.limit > 0 else None,
  81. )
  82. print(f"待补判 {len(rows)} 条 platforms={','.join(platforms or PLATFORMS)} workers={args.workers}")
  83. if not rows:
  84. conn.close()
  85. return
  86. done = fail = 0
  87. with cf.ThreadPoolExecutor(max_workers=max(1, args.workers)) as ex:
  88. futs = {ex.submit(_classify_one, item, settings): item for item in rows}
  89. for fut in cf.as_completed(futs):
  90. item = futs[fut]
  91. try:
  92. item_id, is_creation, reason, knowledge, version, error = fut.result()
  93. except Exception as exc:
  94. item_id = item["id"]
  95. is_creation = None
  96. reason = f"判定失败: {str(exc)[:80]}"
  97. knowledge = ""
  98. version = ""
  99. error = reason
  100. store.upsert_creation_classification(
  101. conn,
  102. item_id,
  103. is_creation,
  104. reason=reason,
  105. knowledge=knowledge,
  106. prompt_version=version,
  107. error=error,
  108. ts=int(time.time()),
  109. )
  110. done += 1
  111. if is_creation is None:
  112. fail += 1
  113. if done % 20 == 0 or done == len(rows):
  114. print(f" {done}/{len(rows)} done failed={fail}", flush=True)
  115. conn.close()
  116. print(f"完成:{done} 条,失败 {fail} 条")
  117. if __name__ == "__main__":
  118. main()