query.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. """生成 query 的 demo 引擎(只到「生成 query」为止:不搜索、不解构)。
  2. ① 实质×创作阶段×需求点(LLM) ② 形式×载体位置(LLM) ③ 搜索词扩展(待接入真 sug)
  3. ④ 多轴正交组合(机械):实质×形式×阶段×动作×作用×知识类型
  4. 取自 scope_trees 节点 + 人工定义轴;设计见 开发文档/query构造.md。
  5. """
  6. from __future__ import annotations
  7. import json
  8. from pathlib import Path
  9. from typing import Optional
  10. from acquisition.suggest import suggest
  11. from core.config import Settings
  12. from core.llm import chat_json
  13. from core.prompts import load_prompt
  14. ROOT = Path(__file__).resolve().parent.parent
  15. TREES = ROOT / "scope_trees" / "trees_index.json"
  16. # ② 载体位置:载体 × 位置 的交叉(短视频/图片有封面,文章无封面);剧本/小说/长文属于实质,不在此
  17. CARRIERS = ["短视频", "图片", "文章"]
  18. POSITIONS = ["开头", "中间", "收尾", "封面"]
  19. CARRIER_POS = [f"{c}{p}" for c in CARRIERS for p in POSITIONS if not (c == "文章" and p == "封面")]
  20. # 分组形式(给前端「查看全部」展示 载体 × 位置 的交叉)
  21. CARRIER_POS_GROUPED = {c: [p for p in POSITIONS if not (c == "文章" and p == "封面")] for c in CARRIERS}
  22. # ④ 需求点(人工拟定):每个创作阶段下的细分需求,喂给 LLM 从中选 + 前端按钮展示
  23. DEMAND = {
  24. "灵感": ["找方向", "找素材", "拆案例"],
  25. "选题": ["选题", "爆款选题", "什么内容火"],
  26. "脚本": ["开头钩子", "结构", "标题", "文案"],
  27. }
  28. # 制作屏蔽词:④ 过滤 + 前端标记(创作 vs 制作边界)
  29. BLOCK = ["剪辑", "调色", "参数", "导出", "软件", "生成", "插件", "渲染", "压制"]
  30. # ④ 多轴正交「组合」query(机械拼接)的人工定义轴
  31. ACTIONS = ["构思", "策划", "组织", "撰写", "改编", "润色"] # 动作(待修改)
  32. STAGES = ["灵感", "选题", "脚本"] # 阶段
  33. KTYPE_SUFFIX = {"what": "有哪些", "why": "为什么", "how": "怎么做"} # 知识类型→句尾后缀
  34. MODALITIES = ["图文", "视频"] # 知识模态(搜索筛选维度,不进 query 串)
  35. def sample_nodes(source_type: str, depths=(3, 4), limit: int = 20,
  36. under: Optional[str] = None) -> list[str]:
  37. """读 trees_index.json,取某棵树的中层节点名(按出现序去重、封顶)。
  38. under 给定时只取 path 含该分支的节点(如实质取 实质树/理念、手法取 形式树/架构)。"""
  39. idx = json.loads(TREES.read_text("utf-8"))
  40. out: list[str] = []
  41. seen: set[str] = set()
  42. for n in idx:
  43. if n.get("source_type") != source_type:
  44. continue
  45. path = [x for x in (n.get("path") or "").split("/") if x]
  46. if under and under not in path:
  47. continue
  48. if len(path) in depths:
  49. name = n.get("name") or (path[-1] if path else "")
  50. if name and name not in seen:
  51. seen.add(name)
  52. out.append(name)
  53. if len(out) >= limit:
  54. break
  55. return out
  56. def tactic2_form_llm(form_nodes: list[str], settings: Settings) -> list[dict]:
  57. """② 形式树 × 载体位置 → LLM 正交生成自然 query(LLM 自行把书面形式标签理解成创作手法)。
  58. 返回 [{形式, 载体位置, query}],供前端表格从左到右展示正交。1 次批量调用。"""
  59. user = json.dumps({"形式树": form_nodes, "载体位置": CARRIER_POS, "屏蔽制作词": BLOCK}, ensure_ascii=False)
  60. try:
  61. res = chat_json(load_prompt("form_query_gen"), user, settings=settings, timeout=120)
  62. rows = res.get("rows") or []
  63. except Exception:
  64. rows = []
  65. return [{"形式": r.get("形式", ""), "载体位置": r.get("载体位置", ""), "query": r.get("query", "")}
  66. for r in rows if isinstance(r, dict) and r.get("query")]
  67. def tactic3_suggest(seeds: list[dict], settings: Settings) -> list[dict]:
  68. """③ 搜索词扩展(仅小红书):每个种子 → keyword_v2 → 从相关帖挖候选搜索词。
  69. seeds=[{query, 来源}];来源标明种子出处(实质+意图 / 形式+需求词)。"""
  70. from acquisition.crawler import RateLimiter
  71. rl = RateLimiter(min_interval_seconds=1.0)
  72. out = []
  73. for s in seeds:
  74. q, origin = s["query"], s.get("来源", "")
  75. try:
  76. cands = suggest(q, settings=settings, rate_limiter=rl, limit=12)
  77. except Exception as exc:
  78. cands = [f"(失败: {str(exc)[:40]})"]
  79. out.append({"seed": q, "来源": origin, "候选": cands})
  80. return out
  81. def tactic4_llm(topics: list[str], settings: Settings) -> list[dict]:
  82. """④ LLM 正交清洗:实质 × 创作阶段 × 需求点(人工拟定 DEMAND) → 自然 query(滤制作)。
  83. 返回逐条带正交三轴的行:[{实质, 阶段, 需求点, query}],供前端表格展示。1 次批量调用。"""
  84. user = json.dumps({"实质": topics, "需求点表": DEMAND, "屏蔽制作词": BLOCK}, ensure_ascii=False)
  85. try:
  86. res = chat_json(load_prompt("query_gen"), user, settings=settings, timeout=120)
  87. rows = res.get("rows") or []
  88. except Exception:
  89. rows = []
  90. return [{"实质": r.get("实质", ""), "阶段": r.get("阶段", ""),
  91. "需求点": r.get("需求点", ""), "query": r.get("query", "")}
  92. for r in rows if isinstance(r, dict) and r.get("query")]
  93. def _nonleaf_d4(source_type: str, limit: int, under: Optional[str] = None) -> list[str]:
  94. """取某棵树的【4级非叶子节点】名(实质 79 / 形式 47 / 作用 16…),按序采样封顶。
  95. under 给定时只取该分支(如形式限 架构,避开 呈现 里的剪辑/后期等制作节点)。"""
  96. idx = json.loads(TREES.read_text("utf-8"))
  97. paths = {(n.get("path") or "") for n in idx if n.get("source_type") == source_type
  98. and (not under or under in (n.get("path") or "").split("/"))}
  99. d4 = [p for p in paths if len([x for x in p.split("/") if x]) == 4]
  100. nonleaf = sorted(p for p in d4 if any(o != p and o.startswith(p + "/") for o in paths))
  101. return [p.split("/")[-1] for p in nonleaf][:limit]
  102. def tactic_multiaxis(n: int = 36) -> list[dict]:
  103. """④ 多轴正交组合(机械拼接):实质×形式×阶段×动作×作用×知识类型 → 拼成「组合 query」。
  104. 实质/形式/作用 取自分类树(4级非叶子),阶段/动作/知识类型 人工定义;模态不进 query。
  105. 组合空间 ~320 万,这里 round-robin 取不同轴做【采样】,避免爆炸。无 LLM、纯机械。"""
  106. sz = _nonleaf_d4("实质", 8)
  107. xs = _nonleaf_d4("形式", 6, under="架构") # 限创作手法(架构),避开呈现里的制作节点
  108. zy = _nonleaf_d4("作用", 6)
  109. ktypes = list(KTYPE_SUFFIX.items()) # [(what,有哪些),…]
  110. stage_act = [(s, a) for s in STAGES for a in ACTIONS] + [("", "")] # +「无动作」变体
  111. rows = []
  112. for i in range(n):
  113. s_ = sz[i % len(sz)]
  114. f_ = xs[i % len(xs)]
  115. st, ac = stage_act[i % len(stage_act)]
  116. zy_ = zy[i % len(zy)]
  117. kt, suf = ktypes[i % len(ktypes)]
  118. seg = (st + ac) if ac else "" # 脚本撰写 / 空
  119. parts = [s_, f_] + ([seg] if seg else []) + [zy_, suf]
  120. rows.append({"实质": s_, "形式": f_, "阶段": st or "/", "动作": ac or "/",
  121. "作用": zy_, "知识类型": kt, "query": " ".join(parts)})
  122. return rows