Преглед изворни кода

feat(classify):视频分类改用严格创作知识判据,拦下「讲观点=创作」的拔高误判

原 extract_video.txt 是「解构提炼」提示词(默认有创作知识、抽到段就算),把"讲人生观/讲
道理"的视频拔高成创作知识。新增 prompts/classify_video.txt:与图文同款的严格两轴判据
(产出物是不是内容 + 是创作不是制作)+ 专门拦"一个人讲观点/感悟/道理=内容本身=非创作"、
明确"不要拔高"。classify_video 改为整段视频喂 Gemini + 此严格提示词判 is_empty。
不动 pipeline 的 extract_video.txt(解构仍用它)。实测 3 条「讲人生观」视频均改判非创作。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SamLee пре 2 недеља
родитељ
комит
4a7cf698e3
2 измењених фајлова са 55 додато и 7 уклоњено
  1. 27 7
      acquisition/classify.py
  2. 28 0
      prompts/classify_video.txt

+ 27 - 7
acquisition/classify.py

@@ -12,17 +12,20 @@ from __future__ import annotations
 
 import base64
 import concurrent.futures as cf
+import json
 import subprocess
 import sys
 import tempfile
 import time
 from pathlib import Path
 
+import httpx
+
 from acquisition import store
 from core.config import Settings
 from core.models import Card, Post
+from core.prompts import load_prompt
 from creation_knowledge.integrations.extractor import GeminiExtractor
-from creation_knowledge.integrations.video_extract import extract_video
 
 ROOT = Path(__file__).resolve().parent.parent
 PLATFORMS = ["xiaohongshu", "douyin"]    # 本轮重判平台(微信等正文下完再单独跑)
@@ -76,24 +79,41 @@ def classify_imgtext(p: dict, extractor: GeminiExtractor) -> tuple:
 
 
 def classify_video(p: dict, settings: Settings) -> tuple:
+    """看完整段视频,用与图文同款的【严格创作知识判据】(classify_video.txt:两轴+越界+拦观点拔高)判 is_empty。
+    不再用 extract_video 的松「提炼」闸(它会把讲观点的视频拔高成创作知识)。"""
     rel = p.get("video") or ""
     mp4 = ROOT / rel.lstrip("/")
     if not rel or not mp4.exists():
         return None, "无本地视频"
     use = _compress(mp4) if mp4.stat().st_size > COMPRESS_OVER_MB * 1048576 else mp4
-    post = Post(id="douyin_cls", platform="douyin", url=p["url"], content_id=p["url"],
-                title=p.get("title", ""), video_urls=["local"])
     try:
-        ec = extract_video(post, settings=settings, video_path=str(use), timeout=300)  # 完整 extract_video.txt
+        media = "data:video/mp4;base64," + base64.b64encode(use.read_bytes()).decode()
     finally:
         if use != mp4:
             try:
                 use.unlink()
             except Exception:
                 pass
-    if ec.is_empty:
-        return 0, "判为非创作:视频无创作知识段落"
-    return 1, (ec.text or "")[:60]
+    messages = [{"role": "system", "content": load_prompt("classify_video")},
+                {"role": "user", "content": [{"type": "text", "text": "判断这条视频是不是创作知识。"},
+                                             {"type": "video_url", "video_url": {"url": media}}]}]
+    api = settings.openrouter_base_url.rstrip("/") + "/chat/completions"
+    headers = {"Authorization": f"Bearer {settings.openrouter_api_key}", "Content-Type": "application/json"}
+    payload = {"model": settings.video_model, "messages": messages,
+               "response_format": {"type": "json_object"}}
+    last = ""
+    for attempt in range(3):
+        try:
+            resp = httpx.post(api, headers=headers, json=payload, timeout=300)
+            if resp.status_code == 200:
+                d = json.loads(resp.json()["choices"][0]["message"]["content"])
+                is_empty = bool(d.get("is_empty"))
+                return (0 if is_empty else 1), str(d.get("reason", ""))[:60]
+            last = f"http {resp.status_code}"
+        except Exception as exc:
+            last = str(exc)[:50]
+        time.sleep(2 * (attempt + 1))
+    return None, f"判定失败: {last}"
 
 
 def _safe(fn, *a) -> tuple:

+ 28 - 0
prompts/classify_video.txt

@@ -0,0 +1,28 @@
+你在判断一条短视频是不是「创作知识」——能教别人"如何创作内容"的方法、原理、认知或清单。请看完整段视频(听口播、看画面与字幕)再判断。只做判断,不提炼、不分段。
+
+<什么算创作知识>
+能迁移、能复用、能教别人"怎么创作图文/视频内容"的方法、原理、清单。**判断要同时过两条轴,缺一不算:**
+
+【轴A·产出物是不是「内容」】这条知识帮你创作出来的成品,是不是 图文/视频/脚本/剧本/小说 这类**内容**?题材不限(美食/游戏/健身/人生感悟…都行),卡的是产出物:
+- 算:产出"关于某题材的**内容**"——美食视频拍法、游戏解说脚本、健身图文写法。
+- 不算:产出"那个题材**本身**"——一道菜的做法、一个游戏的玩法、一个产品、一套运营/选品策略。
+
+【轴B·是「创作」不是「制作」】教的是"怎么构思/选题/写/结构/呈现"(创作),还是"用哪个软件/AI、点哪个按钮把成品产出来"(制作/工具操作)?
+- 算(创作):选题/构思方法、结构或排版公式、要素清单、为什么某种开头/呈现更抓人、镜头/构图/配色的设计判断。
+- 不算(制作):打开某 App/AI → 输入 → 点生成 → 导出 这类工具操作;纯器材/剪辑/调色/导出。
+- 特别强调:「用某 AI/App 帮你生成内容成品」的教程也**不算**。
+
+【两轴 AND】两条都过才算;任一不过 → 不算。
+</什么算创作知识>
+
+<最常见的误判·务必拦下>
+**一个人对着镜头讲他的人生观/价值观/人生感悟/某个道理/某个话题(如"人生的意义""如何看待 XX""我最有帮助的改变"),这是在表达「观点 / 内容本身」,不是在教别人"怎么创作内容"——判为不算(is_empty=true)。**
+- **不要拔高**:别把"一个人在表达观点/讲道理"说成"教你如何做观点输出/口播/治愈系内容"。除非视频里**明确在教可复用的创作方法**(选题怎么选、脚本怎么搭、开头怎么钩、结构怎么排、起号怎么做、文案怎么写),否则就是作品/观点本身。
+- 同理不算(作品/内容本身):一段具体文案或作品的展示、纯口播抒情、纯叙事讲故事、纯知识科普(讲某领域知识但不教"怎么做成内容")、励志鸡汤、个人经历/成长心得分享、新闻报道。
+
+<判定>
+- 视频里只要有哪怕一点"怎么**创作内容**"的可迁移方法(而非只是在讲观点/讲内容) → is_empty=false(算创作知识)。
+- 通篇只是讲观点 / 讲道理 / 讲某话题 / 展示作品 / 抒情励志 / 分享经历,没有教"怎么做内容"的方法 → is_empty=true(不算)。
+- "范围内但拿不准有没有方法" → is_empty=false;但"到底是讲观点还是教创作"要**果断**——明显是在讲观点 / 作品本身,就判 true,别用"拿不准"放进来。
+
+只输出一个 JSON 对象:{"is_empty": true/false, "reason": "一句话理由,点明是「讲观点/作品本身」还是「教了哪种可复用的创作方法」(≤30字)"}