|
|
@@ -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:
|