extract_chapter_context.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. #!/usr/bin/env python3
  2. """
  3. extract_chapter_context.py - 提取章节创作所需的精简上下文
  4. 功能:
  5. - 提取当前章节的大纲片段(~500字)
  6. - 提取前2章的摘要(优先 .webnovel/summaries)
  7. - 提取 state.json 关键字段(~300字)
  8. 用法:
  9. python extract_chapter_context.py --chapter 7
  10. python extract_chapter_context.py --chapter 7 --project-root ./webnovel-project
  11. """
  12. import argparse
  13. import json
  14. import re
  15. import sys
  16. from pathlib import Path
  17. try:
  18. from chapter_paths import find_chapter_file
  19. except ImportError: # pragma: no cover
  20. from scripts.chapter_paths import find_chapter_file
  21. def find_project_root(start_path: Path = None) -> Path:
  22. """查找包含 .webnovel 目录的项目根目录"""
  23. if start_path is None:
  24. start_path = Path.cwd()
  25. search_paths = [
  26. start_path,
  27. start_path / "webnovel-project",
  28. start_path.parent,
  29. ]
  30. for path in search_paths:
  31. if (path / ".webnovel").exists():
  32. return path
  33. raise FileNotFoundError("未找到 .webnovel 目录,请确认项目路径")
  34. def extract_chapter_outline(project_root: Path, chapter_num: int) -> str:
  35. """从大纲文件中提取指定章节的大纲片段"""
  36. volume_num = (chapter_num - 1) // 50 + 1
  37. outline_file = project_root / "大纲" / f"第{volume_num}卷-详细大纲.md"
  38. if not outline_file.exists():
  39. return f"⚠️ 大纲文件不存在: {outline_file}"
  40. content = outline_file.read_text(encoding="utf-8")
  41. # 匹配章节大纲块
  42. # 格式:### 第 N 章:标题 或 ### 第 N 章: 标题
  43. pattern = rf"###\s*第\s*{chapter_num}\s*章[::]\s*(.+?)(?=###\s*第\s*\d+\s*章|##\s|$)"
  44. match = re.search(pattern, content, re.DOTALL)
  45. if match:
  46. outline = match.group(0).strip()
  47. # 限制长度
  48. if len(outline) > 1500:
  49. outline = outline[:1500] + "\n...(已截断)"
  50. return outline
  51. # 尝试另一种格式:### 第 1 章:标题(无空格)
  52. pattern2 = rf"###\s*第{chapter_num}章[::]\s*(.+?)(?=###\s*第\d+章|##\s|$)"
  53. match2 = re.search(pattern2, content, re.DOTALL)
  54. if match2:
  55. outline = match2.group(0).strip()
  56. if len(outline) > 1500:
  57. outline = outline[:1500] + "\n...(已截断)"
  58. return outline
  59. return f"⚠️ 未找到第 {chapter_num} 章的大纲"
  60. def _load_summary_file(project_root: Path, chapter_num: int) -> str:
  61. """从 .webnovel/summaries/chNNNN.md 提取剧情摘要"""
  62. summary_path = project_root / ".webnovel" / "summaries" / f"ch{chapter_num:04d}.md"
  63. if not summary_path.exists():
  64. return ""
  65. text = summary_path.read_text(encoding="utf-8")
  66. summary_match = re.search(r"##\s*剧情摘要\s*\r?\n(.+?)(?=\r?\n##|$)", text, re.DOTALL)
  67. if summary_match:
  68. return summary_match.group(1).strip()
  69. return ""
  70. def extract_chapter_summary(project_root: Path, chapter_num: int) -> str:
  71. """提取指定章节摘要(优先 summaries/,再降级正文)"""
  72. summary = _load_summary_file(project_root, chapter_num)
  73. if summary:
  74. return summary
  75. chapter_file = find_chapter_file(project_root, chapter_num)
  76. if not chapter_file or not chapter_file.exists():
  77. return f"⚠️ 第 {chapter_num} 章文件不存在"
  78. content = chapter_file.read_text(encoding="utf-8")
  79. # 兼容旧格式:尝试提取"本章摘要"部分
  80. summary_match = re.search(r"##\s*本章摘要\s*\r?\n(.+?)(?=\r?\n##|$)", content, re.DOTALL)
  81. if summary_match:
  82. return summary_match.group(1).strip()
  83. # 如果没有摘要,提取"本章统计"部分
  84. stats_match = re.search(r"##\s*本章统计\s*\r?\n(.+?)(?=\r?\n##|$)", content, re.DOTALL)
  85. if stats_match:
  86. return f"[无摘要,仅统计]\n{stats_match.group(1).strip()}"
  87. # 最后降级:提取前500字作为摘要
  88. lines = content.split("\n")
  89. text_lines = [l for l in lines if not l.startswith("#") and l.strip()]
  90. text = "\n".join(text_lines)[:500]
  91. return f"[自动截取前500字]\n{text}..."
  92. def extract_state_summary(project_root: Path) -> str:
  93. """提取 state.json 的关键字段"""
  94. state_file = project_root / ".webnovel" / "state.json"
  95. if not state_file.exists():
  96. return "⚠️ state.json 不存在"
  97. state = json.loads(state_file.read_text(encoding="utf-8"))
  98. # 提取关键字段
  99. summary_parts = []
  100. # 进度
  101. if "progress" in state:
  102. p = state["progress"]
  103. summary_parts.append(f"**进度**: 第 {p.get('current_chapter', '?')} 章 / {p.get('total_words', '?')} 字")
  104. # 主角状态
  105. if "protagonist_state" in state:
  106. ps = state["protagonist_state"]
  107. power = ps.get("power", {})
  108. summary_parts.append(f"**主角实力**: {power.get('realm', '?')} {power.get('layer', '?')}层")
  109. summary_parts.append(f"**当前位置**: {ps.get('location', '?')}")
  110. gf = ps.get("golden_finger", {})
  111. summary_parts.append(f"**金手指**: {gf.get('name', '?')} Lv.{gf.get('level', '?')}")
  112. # Strand 追踪
  113. if "strand_tracker" in state:
  114. st = state["strand_tracker"]
  115. history = st.get("history", [])[-5:] # 最近5章
  116. if history:
  117. items = []
  118. for h in history:
  119. if not isinstance(h, dict):
  120. continue
  121. ch = h.get("chapter", "?")
  122. strand = h.get("strand") or h.get("dominant") or "unknown"
  123. items.append(f"Ch{ch}:{strand}")
  124. strand_str = ", ".join(items)
  125. summary_parts.append(f"**近5章Strand**: {strand_str}")
  126. # 活跃伏笔(只显示紧急的)
  127. plot_threads = state.get("plot_threads", {}) if isinstance(state.get("plot_threads"), dict) else {}
  128. foreshadowing = plot_threads.get("foreshadowing", [])
  129. if isinstance(foreshadowing, list) and foreshadowing:
  130. active = [f for f in foreshadowing if f.get("status") in {"active", "未回收"}]
  131. urgent = [f for f in active if f.get("urgency", 0) > 50]
  132. if urgent:
  133. urgent_list = [f"{f.get('content', '?')[:30]}... (紧急度:{f.get('urgency')})" for f in urgent[:3]]
  134. summary_parts.append(f"**紧急伏笔**: {'; '.join(urgent_list)}")
  135. return "\n".join(summary_parts)
  136. def main():
  137. parser = argparse.ArgumentParser(description="提取章节创作所需的精简上下文")
  138. parser.add_argument("--chapter", type=int, required=True, help="目标章节号")
  139. parser.add_argument("--project-root", type=str, help="项目根目录")
  140. parser.add_argument("--format", choices=["text", "json"], default="text", help="输出格式")
  141. args = parser.parse_args()
  142. try:
  143. if args.project_root:
  144. project_root = Path(args.project_root)
  145. else:
  146. project_root = find_project_root()
  147. chapter_num = args.chapter
  148. # 提取各部分
  149. outline = extract_chapter_outline(project_root, chapter_num)
  150. # 提取前2章摘要
  151. prev_summaries = []
  152. for prev_ch in range(max(1, chapter_num - 2), chapter_num):
  153. summary = extract_chapter_summary(project_root, prev_ch)
  154. prev_summaries.append(f"### 第 {prev_ch} 章摘要\n{summary}")
  155. state_summary = extract_state_summary(project_root)
  156. if args.format == "json":
  157. result = {
  158. "chapter": chapter_num,
  159. "outline": outline,
  160. "previous_summaries": prev_summaries,
  161. "state_summary": state_summary,
  162. }
  163. print(json.dumps(result, ensure_ascii=False, indent=2))
  164. else:
  165. print(f"# 第 {chapter_num} 章创作上下文\n")
  166. print("## 本章大纲\n")
  167. print(outline)
  168. print("\n---\n")
  169. print("## 前文摘要\n")
  170. for s in prev_summaries:
  171. print(s)
  172. print()
  173. print("---\n")
  174. print("## 当前状态\n")
  175. print(state_summary)
  176. except Exception as e:
  177. print(f"❌ 错误: {e}", file=sys.stderr)
  178. sys.exit(1)
  179. if __name__ == "__main__":
  180. main()