extract_chapter_context.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. strand_str = ", ".join([f"Ch{h['chapter']}:{h['strand']}" for h in history])
  118. summary_parts.append(f"**近5章Strand**: {strand_str}")
  119. # 活跃伏笔(只显示紧急的)
  120. plot_threads = state.get("plot_threads", {}) if isinstance(state.get("plot_threads"), dict) else {}
  121. foreshadowing = plot_threads.get("foreshadowing", [])
  122. if isinstance(foreshadowing, list) and foreshadowing:
  123. active = [f for f in foreshadowing if f.get("status") in {"active", "未回收"}]
  124. urgent = [f for f in active if f.get("urgency", 0) > 50]
  125. if urgent:
  126. urgent_list = [f"{f.get('content', '?')[:30]}... (紧急度:{f.get('urgency')})" for f in urgent[:3]]
  127. summary_parts.append(f"**紧急伏笔**: {'; '.join(urgent_list)}")
  128. return "\n".join(summary_parts)
  129. def main():
  130. parser = argparse.ArgumentParser(description="提取章节创作所需的精简上下文")
  131. parser.add_argument("--chapter", type=int, required=True, help="目标章节号")
  132. parser.add_argument("--project-root", type=str, help="项目根目录")
  133. parser.add_argument("--format", choices=["text", "json"], default="text", help="输出格式")
  134. args = parser.parse_args()
  135. try:
  136. if args.project_root:
  137. project_root = Path(args.project_root)
  138. else:
  139. project_root = find_project_root()
  140. chapter_num = args.chapter
  141. # 提取各部分
  142. outline = extract_chapter_outline(project_root, chapter_num)
  143. # 提取前2章摘要
  144. prev_summaries = []
  145. for prev_ch in range(max(1, chapter_num - 2), chapter_num):
  146. summary = extract_chapter_summary(project_root, prev_ch)
  147. prev_summaries.append(f"### 第 {prev_ch} 章摘要\n{summary}")
  148. state_summary = extract_state_summary(project_root)
  149. if args.format == "json":
  150. result = {
  151. "chapter": chapter_num,
  152. "outline": outline,
  153. "previous_summaries": prev_summaries,
  154. "state_summary": state_summary,
  155. }
  156. print(json.dumps(result, ensure_ascii=False, indent=2))
  157. else:
  158. print(f"# 第 {chapter_num} 章创作上下文\n")
  159. print("## 本章大纲\n")
  160. print(outline)
  161. print("\n---\n")
  162. print("## 前文摘要\n")
  163. for s in prev_summaries:
  164. print(s)
  165. print()
  166. print("---\n")
  167. print("## 当前状态\n")
  168. print(state_summary)
  169. except Exception as e:
  170. print(f"❌ 错误: {e}", file=sys.stderr)
  171. sys.exit(1)
  172. if __name__ == "__main__":
  173. main()