build_creation_demo.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. """创作知识 query 正交 demo:5 套家族机械正交 → LLM 只做排除(query_filter.txt) → 存 JSON。
  2. 不真实搜,只产 query 供前端看。轴严格取分类树的"创作支":
  3. 实质 = 实质树·理念支(排除表象) 形式 = 形式树·架构支(排除呈现)
  4. 目的池 = 作用树 + 感受树 + 意图树 全部合一(随机取)
  5. 创作阶段 = 灵感/选题/脚本(不补) 知识类型 = 怎么做/有哪些/为什么
  6. 5 家族:① 实质×形式 ② 实质×目的池 ③ 纯形式 ④ 形式×目的池 ⑤ 创作阶段×实质,各 20 条。
  7. 每条原串过 query_filter.txt(keep/排除),存 keep+reason 供前端展示。
  8. 用法:PYTHONPATH=. CK_ENV_FILE=.env python scripts/build_creation_demo.py
  9. """
  10. from __future__ import annotations
  11. import json
  12. import random
  13. from pathlib import Path
  14. import httpx
  15. from core.config import Settings
  16. ROOT = Path(__file__).resolve().parent.parent
  17. TREES = ROOT / "scope_trees" / "trees_index.json"
  18. FILTER_PROMPT = ROOT / "acquisition" / "query_filter.txt" # 筛选词在 acquisition/
  19. OUT = ROOT / "data" / "queries" / "creation_demo.json"
  20. PER = 20
  21. STAGES = ["灵感", "选题", "脚本"]
  22. KTYPE = ["怎么做", "有哪些", "为什么"]
  23. MODALITY = ["视频", "图文"] # 被创作内容的形态(与教学帖本身格式无关),正交进所有家族
  24. def _segs(p):
  25. return [x for x in (p or "").split("/") if x]
  26. def _leaves(idx, source_type, under=None):
  27. """某树某支下的叶子节点名(没有更深子节点的=元素层)。under 限定分支。"""
  28. paths = [(_segs(n["path"]), n.get("name")) for n in idx if n.get("source_type") == source_type]
  29. if under:
  30. paths = [(s, nm) for s, nm in paths if under in s]
  31. allp = {"/".join(s) for s, _ in paths}
  32. out, seen = [], set()
  33. for s, nm in paths:
  34. if len(s) < 2:
  35. continue
  36. full = "/".join(s)
  37. is_leaf = not any(o != full and o.startswith(full + "/") for o in allp)
  38. name = nm or s[-1]
  39. if is_leaf and name and name not in seen:
  40. seen.add(name)
  41. out.append(name)
  42. return out
  43. def _nonleaf(idx, source_type, depths=(3, 4), under=None):
  44. """某树某支下、指定层级的【非叶子"类目"节点】(底下还有元素,不取元素本身)。
  45. 对齐制作侧取法:实质/形式 取 depth 3-4 的类目层,而非最深的元素层。"""
  46. paths = [(_segs(n["path"]), n.get("name")) for n in idx if n.get("source_type") == source_type]
  47. if under:
  48. paths = [(s, nm) for s, nm in paths if under in s]
  49. allp = {"/".join(s) for s, _ in paths}
  50. out, seen = [], set()
  51. for s, nm in paths:
  52. if len(s) not in depths:
  53. continue
  54. full = "/".join(s)
  55. is_nonleaf = any(o != full and o.startswith(full + "/") for o in allp)
  56. name = nm or (s[-1] if s else "")
  57. if is_nonleaf and name and name not in seen:
  58. seen.add(name)
  59. out.append(name)
  60. return out
  61. def _filter(queries, settings):
  62. """把一批原串喂 query_filter.txt(LLM 只做 keep/排除)。返回 [{keep,reason}] 对齐顺序。"""
  63. user = json.dumps([{"idx": i, "query": q} for i, q in enumerate(queries)], ensure_ascii=False)
  64. api = settings.openrouter_base_url.rstrip("/") + "/chat/completions"
  65. headers = {"Authorization": f"Bearer {settings.openrouter_api_key}", "Content-Type": "application/json"}
  66. body = {"model": settings.llm_model, "messages": [
  67. {"role": "system", "content": FILTER_PROMPT.read_text("utf-8")},
  68. {"role": "user", "content": user}], "response_format": {"type": "json_object"}}
  69. try:
  70. resp = httpx.post(api, headers=headers, json=body, timeout=120)
  71. resp.raise_for_status()
  72. txt = resp.json()["choices"][0]["message"]["content"]
  73. # query_filter 要求输出数组;有的模型会包一层 {"result":[...]},都兜住
  74. data = json.loads(txt)
  75. arr = data if isinstance(data, list) else next((v for v in data.values() if isinstance(v, list)), [])
  76. by = {d.get("idx"): d for d in arr if isinstance(d, dict)}
  77. return [{"keep": bool(by.get(i, {}).get("keep", True)),
  78. "reason": str(by.get(i, {}).get("reason", ""))[:50]} for i in range(len(queries))]
  79. except Exception as exc:
  80. return [{"keep": True, "reason": f"筛选失败:{str(exc)[:30]}"} for _ in queries]
  81. def main():
  82. settings = Settings.from_env()
  83. rng = random.Random(7)
  84. idx = json.loads(TREES.read_text("utf-8"))
  85. SHI = _nonleaf(idx, "实质", depths=(3, 4), under="理念") # 类目层,非元素
  86. XING = _nonleaf(idx, "形式", depths=(3, 4), under="架构") # 类目层,非元素
  87. POOL = _leaves(idx, "作用") + _leaves(idx, "感受") + _leaves(idx, "意图")
  88. print(f"实质 {len(SHI)} / 形式 {len(XING)} / 目的池 {len(POOL)} / 阶段 {len(STAGES)}")
  89. def pick(seq):
  90. return rng.choice(seq)
  91. # 每家族:生成器 + 用到的轴(给前端标列)
  92. families = [
  93. {"key": "f1", "name": "实质 × 形式", "axes": ["实质", "模态", "形式", "知识类型"],
  94. "gen": lambda: {"parts": {"实质": pick(SHI), "模态": pick(MODALITY), "形式": pick(XING), "知识类型": pick(KTYPE)}}},
  95. {"key": "f2", "name": "实质 × (作用/感受/意图)", "axes": ["实质", "模态", "作用/感受/意图", "知识类型"],
  96. "gen": lambda: {"parts": {"实质": pick(SHI), "模态": pick(MODALITY), "目的": pick(POOL), "知识类型": pick(KTYPE)}}},
  97. {"key": "f3", "name": "纯形式", "axes": ["模态", "形式", "知识类型"],
  98. "gen": lambda: {"parts": {"模态": pick(MODALITY), "形式": pick(XING), "知识类型": pick(KTYPE)}}},
  99. {"key": "f4", "name": "形式 × (作用/感受/意图)", "axes": ["模态", "形式", "作用/感受/意图", "知识类型"],
  100. "gen": lambda: {"parts": {"模态": pick(MODALITY), "形式": pick(XING), "目的": pick(POOL), "知识类型": pick(KTYPE)}}},
  101. {"key": "f5", "name": "创作阶段 × 实质", "axes": ["实质", "模态", "创作阶段", "知识类型"],
  102. "gen": lambda: {"parts": {"实质": pick(SHI), "模态": pick(MODALITY), "创作阶段": pick(STAGES), "知识类型": pick(KTYPE)}}},
  103. ]
  104. # 各部件按固定顺序拼成原串(模态紧跟实质、在形式/目的之前)
  105. order = ["实质", "模态", "形式", "目的", "创作阶段", "知识类型"]
  106. out = {"axis_values": {"实质": SHI, "模态": MODALITY, "形式": XING, "目的池": POOL, "创作阶段": STAGES, "知识类型": KTYPE},
  107. "families": []}
  108. for fam in families:
  109. seen, items = set(), []
  110. while len(items) < PER and len(seen) < PER * 40:
  111. parts = fam["gen"]()["parts"]
  112. q = " ".join(parts[k] for k in order if k in parts)
  113. if q in seen:
  114. continue
  115. seen.add(q)
  116. items.append({"query": q, "parts": parts})
  117. verdicts = _filter([it["query"] for it in items], settings)
  118. for it, v in zip(items, verdicts):
  119. it.update(v)
  120. kept = sum(1 for it in items if it["keep"])
  121. print(f"[{fam['name']}] 生成 {len(items)} 条, 筛后保留 {kept}")
  122. out["families"].append({"key": fam["key"], "name": fam["name"], "axes": fam["axes"], "items": items})
  123. OUT.parent.mkdir(parents=True, exist_ok=True)
  124. OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
  125. print(f"→ {OUT}")
  126. if __name__ == "__main__":
  127. main()