build_creation_demo.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. """创作知识 query 正交 demo:5 套家族机械正交 → LLM 只做排除(query_filter.txt) → 存 JSON。
  2. 不真实搜,只产 query 供前端看。轴严格取分类树的"创作支":
  3. 实质 = 实质树·理念支(排除表象) 形式 = 形式树·架构支(排除呈现)
  4. 目的池 = 作用树 + 感受树 + 意图树 全部合一(随机取)
  5. 阶段意图 = 灵感/选题/脚本 展开成创作者真会搜的词(选题/开头/钩子/标题/封面/文案…)——脊柱,每族必带
  6. 模态 = 视频/图片 知识类型 = 怎么做/有哪些/为什么
  7. 脊柱(每条都带):… 阶段意图 + 模态 + 知识类型;前面配 实质/形式/目的 之一或组合。
  8. 5 家族:① 实质×阶段 ② 形式×阶段 ③ 实质×形式×阶段 ④ 目的池×阶段 ⑤ 纯阶段,各 20 条。
  9. 每条原串过 query_filter.txt(keep/排除),存 keep+reason 供前端展示。
  10. 用法:PYTHONPATH=. CK_ENV_FILE=.env python scripts/build_creation_demo.py
  11. """
  12. from __future__ import annotations
  13. import json
  14. import random
  15. from pathlib import Path
  16. from acquisition.query import ACTIONS, _nonleaf_d4 # 老正交动作轴 + 4级非叶子取作用节点
  17. from acquisition.query import STAGES as OLD_STAGES # 老的裸阶段 灵感/选题/脚本
  18. from acquisition.query_filter import filter_queries # 共享筛选器(query_filter.txt)
  19. from core.config import Settings
  20. ROOT = Path(__file__).resolve().parent.parent
  21. TREES = ROOT / "scope_trees" / "trees_index.json"
  22. OUT = ROOT / "data" / "queries" / "creation_demo.json"
  23. PER = 30
  24. BATCH_N = 30 # 全 demo 统一抽这么多个「实质 / 形式」,各族共用同一批;PER=BATCH_N 保证整批都用上、左右一一对应
  25. KTYPE = ["怎么做", "有哪些", "为什么"]
  26. MODALITY = ["视频", "图片"] # 被创作内容的形态(与教学帖本身格式无关),正交进所有家族
  27. # 创作阶段意图轴(脊柱):灵感/选题/脚本 展开成创作者真会搜的词(query构造.md)。真实数据里
  28. # "脚本"0次、但 开头/钩子/标题/封面/选题 各几十次——故用展开词,不用三个干阶段词。
  29. STAGE_INTENT = {
  30. "灵感": ["找素材", "内容方向", "案例拆解", "灵感", "拆解", "复盘"],
  31. "选题": ["选题", "爆款选题", "选题方向"],
  32. "脚本": ["脚本", "文案", "开头", "钩子", "结构", "标题", "封面", "结尾"],
  33. }
  34. INTENT = [w for ws in STAGE_INTENT.values() for w in ws] # 扁平成一个池,随机取
  35. def _segs(p):
  36. return [x for x in (p or "").split("/") if x]
  37. def _leaves(idx, source_type, under=None):
  38. """某树某支下的叶子节点名(没有更深子节点的=元素层)。under 限定分支。"""
  39. paths = [(_segs(n["path"]), n.get("name")) for n in idx if n.get("source_type") == source_type]
  40. if under:
  41. paths = [(s, nm) for s, nm in paths if under in s]
  42. allp = {"/".join(s) for s, _ in paths}
  43. out, seen = [], set()
  44. for s, nm in paths:
  45. if len(s) < 2:
  46. continue
  47. full = "/".join(s)
  48. is_leaf = not any(o != full and o.startswith(full + "/") for o in allp)
  49. name = nm or s[-1]
  50. if is_leaf and name and name not in seen:
  51. seen.add(name)
  52. out.append(name)
  53. return out
  54. def _nonleaf(idx, source_type, depths=(3, 4), under=None):
  55. """某树某支下、指定层级的【非叶子"类目"节点】(底下还有元素,不取元素本身)。
  56. 对齐制作侧取法:实质/形式 取 depth 3-4 的类目层,而非最深的元素层。"""
  57. paths = [(_segs(n["path"]), n.get("name")) for n in idx if n.get("source_type") == source_type]
  58. if under:
  59. paths = [(s, nm) for s, nm in paths if under in s]
  60. allp = {"/".join(s) for s, _ in paths}
  61. out, seen = [], set()
  62. for s, nm in paths:
  63. if len(s) not in depths:
  64. continue
  65. full = "/".join(s)
  66. is_nonleaf = any(o != full and o.startswith(full + "/") for o in allp)
  67. name = nm or (s[-1] if s else "")
  68. if is_nonleaf and name and name not in seen:
  69. seen.add(name)
  70. out.append(name)
  71. return out
  72. def main():
  73. settings = Settings.from_env()
  74. rng = random.Random(7)
  75. idx = json.loads(TREES.read_text("utf-8"))
  76. SHI = _nonleaf(idx, "实质", depths=(3, 4), under="理念") # 类目层,非元素
  77. XING = _nonleaf(idx, "形式", depths=(3, 4), under="架构") # 类目层,非元素
  78. POOL = _leaves(idx, "作用") + _leaves(idx, "感受") + _leaves(idx, "意图")
  79. print(f"实质 {len(SHI)} / 形式 {len(XING)} / 目的池 {len(POOL)} / 业务阶段 {len(INTENT)}")
  80. # 全 demo 统一「一批实质 / 一批形式」——各家族都取同一批、且顺序一致,方便切页签横向比较
  81. SHI_BATCH = rng.sample(SHI, min(BATCH_N, len(SHI)))
  82. XING_BATCH = rng.sample(XING, min(BATCH_N, len(XING)))
  83. print(f"统一批: 实质×{len(SHI_BATCH)} 形式×{len(XING_BATCH)}")
  84. def pick(seq):
  85. return rng.choice(seq)
  86. def shi(i): # 按 query 序号轮转,保证每族都覆盖整批、首次出现顺序一致
  87. return SHI_BATCH[i % len(SHI_BATCH)]
  88. def xing(i):
  89. return XING_BATCH[i % len(XING_BATCH)]
  90. # 第六、第七家族(老正交方案):实质 / 形式 一律沿用上面的统一批(shi/xing,与 f1/f2 完全一致、同序,便于横向对比);
  91. # 阶段=灵感/选题/脚本、动作=构思/策划/组织/撰写/改编/润色(+无动作变体)、作用取4级非叶子。query 形如「(实质) 叙事组织 脚本撰写 趣味互动 有哪些」
  92. F6_ZY = _nonleaf_d4("作用", 10)
  93. F6_STAGE_ACT = [(s, a) for s in OLD_STAGES for a in ACTIONS] + [("", "")] # +「无动作」=老方案的 /
  94. def _old_tail(i):
  95. st, ac = F6_STAGE_ACT[i % len(F6_STAGE_ACT)]
  96. zy_ = F6_ZY[i % len(F6_ZY)]
  97. suf = KTYPE[i % len(KTYPE)]
  98. seg = (st + ac) if ac else "" # 脚本撰写 / 空(动作=/ 时连阶段一起省)
  99. return st, ac, zy_, suf, seg
  100. def gen6(i): # 形式×阶段×动作×作用×知识类型,形式=共享批(同 f2)
  101. f_ = xing(i)
  102. st, ac, zy_, suf, seg = _old_tail(i)
  103. q = " ".join([f_] + ([seg] if seg else []) + [zy_, suf])
  104. return {"parts": {"形式": f_, "阶段": st or "/", "动作": ac or "/", "作用": zy_, "知识类型": suf}, "query": q}
  105. def gen7(i): # 实质×形式×阶段×动作×作用×知识类型,实质=共享批(同 f1)、形式=共享批(同 f2)
  106. s_, f_ = shi(i), xing(i)
  107. st, ac, zy_, suf, seg = _old_tail(i)
  108. q = " ".join([s_, f_] + ([seg] if seg else []) + [zy_, suf])
  109. return {"parts": {"实质": s_, "形式": f_, "阶段": st or "/", "动作": ac or "/", "作用": zy_, "知识类型": suf}, "query": q}
  110. # 每家族:生成器 + 用到的轴。家族名 = axes 用「×」连接,直接反映正交结构(见下方循环)。
  111. # axes 顺序即前端列顺序;业务阶段=脊柱每族必带,模态+知识类型收尾
  112. families = [
  113. {"key": "f1", "axes": ["实质", "模态", "业务阶段", "知识类型"],
  114. "gen": lambda i: {"parts": {"实质": shi(i), "业务阶段": pick(INTENT), "模态": pick(MODALITY), "知识类型": pick(KTYPE)}}},
  115. {"key": "f2", "axes": ["形式", "模态", "业务阶段", "知识类型"],
  116. "gen": lambda i: {"parts": {"形式": xing(i), "业务阶段": pick(INTENT), "模态": pick(MODALITY), "知识类型": pick(KTYPE)}}},
  117. {"key": "f4", "axes": ["作用/感受/意图", "模态", "业务阶段", "知识类型"],
  118. "gen": lambda i: {"parts": {"目的": pick(POOL), "业务阶段": pick(INTENT), "模态": pick(MODALITY), "知识类型": pick(KTYPE)}}},
  119. {"key": "f3", "axes": ["实质", "形式", "模态", "业务阶段", "知识类型"],
  120. "gen": lambda i: {"parts": {"实质": shi(i), "形式": xing(i), "业务阶段": pick(INTENT), "模态": pick(MODALITY), "知识类型": pick(KTYPE)}}},
  121. {"key": "f5", "axes": ["模态", "业务阶段", "知识类型"],
  122. "gen": lambda i: {"parts": {"业务阶段": pick(INTENT), "模态": pick(MODALITY), "知识类型": pick(KTYPE)}}},
  123. {"key": "f6", "axes": ["形式", "阶段", "动作", "作用", "知识类型"], "gen": gen6},
  124. {"key": "f7", "axes": ["实质", "形式", "阶段", "动作", "作用", "知识类型"], "gen": gen7},
  125. ]
  126. # 各部件按固定顺序拼成原串(内容维度在前,模态贴题材后,业务阶段+知识类型收尾)
  127. order = ["实质", "形式", "目的", "模态", "业务阶段", "知识类型"]
  128. # 业务阶段值存「分组结构」(STAGE_INTENT),前端按 灵感/选题/脚本 分组+缩进展示;其余轴是扁平数组
  129. out = {"axis_values": {"实质": SHI, "形式": XING, "目的池": POOL, "业务阶段": STAGE_INTENT,
  130. "模态": MODALITY, "知识类型": KTYPE,
  131. "阶段": OLD_STAGES, "动作": ACTIONS, "作用": F6_ZY}, # 第六家族的老轴
  132. "families": []}
  133. for fam in families:
  134. name = " × ".join(fam["axes"]) # 家族名直接反映正交结构
  135. seen, items = set(), []
  136. while len(items) < PER and len(seen) < PER * 40:
  137. g = fam["gen"](len(items)) # 序号轮转实质/形式批
  138. parts = g["parts"]
  139. q = g.get("query") or " ".join(parts[k] for k in order if k in parts) # f6 自带 query 串
  140. if q in seen:
  141. continue
  142. seen.add(q)
  143. items.append({"query": q, "parts": parts})
  144. verdicts = filter_queries([it["query"] for it in items], settings)
  145. for it, v in zip(items, verdicts):
  146. it.update(v)
  147. kept = sum(1 for it in items if it["keep"])
  148. print(f"[{name}] 生成 {len(items)} 条, 筛后保留 {kept}")
  149. out["families"].append({"key": fam["key"], "name": name, "axes": fam["axes"], "items": items})
  150. OUT.parent.mkdir(parents=True, exist_ok=True)
  151. OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
  152. print(f"→ {OUT}")
  153. if __name__ == "__main__":
  154. main()