extract_chapter_context.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  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. def find_project_root(start_path: Path = None) -> Path:
  18. """查找包含 .webnovel 目录的项目根目录"""
  19. if start_path is None:
  20. start_path = Path.cwd()
  21. search_paths = [
  22. start_path,
  23. start_path / "webnovel-project",
  24. start_path.parent,
  25. ]
  26. for path in search_paths:
  27. if (path / ".webnovel").exists():
  28. return path
  29. raise FileNotFoundError("未找到 .webnovel 目录,请确认项目路径")
  30. def extract_chapter_outline(project_root: Path, chapter_num: int) -> str:
  31. """从大纲文件中提取指定章节的大纲片段"""
  32. volume_num = (chapter_num - 1) // 50 + 1
  33. outline_file = project_root / "大纲" / f"第{volume_num}卷-详细大纲.md"
  34. if not outline_file.exists():
  35. return f"⚠️ 大纲文件不存在: {outline_file}"
  36. content = outline_file.read_text(encoding="utf-8")
  37. # 匹配章节大纲块
  38. # 格式:### 第 N 章:标题 或 ### 第 N 章: 标题
  39. pattern = rf"###\s*第\s*{chapter_num}\s*章[::]\s*(.+?)(?=###\s*第\s*\d+\s*章|##\s|$)"
  40. match = re.search(pattern, content, re.DOTALL)
  41. if match:
  42. outline = match.group(0).strip()
  43. # 限制长度
  44. if len(outline) > 1500:
  45. outline = outline[:1500] + "\n...(已截断)"
  46. return outline
  47. # 尝试另一种格式:### 第 1 章:标题(无空格)
  48. pattern2 = rf"###\s*第{chapter_num}章[::]\s*(.+?)(?=###\s*第\d+章|##\s|$)"
  49. match2 = re.search(pattern2, content, re.DOTALL)
  50. if match2:
  51. outline = match2.group(0).strip()
  52. if len(outline) > 1500:
  53. outline = outline[:1500] + "\n...(已截断)"
  54. return outline
  55. return f"⚠️ 未找到第 {chapter_num} 章的大纲"
  56. def _load_summary_file(project_root: Path, chapter_num: int) -> str:
  57. """从 .webnovel/summaries/chNNNN.md 提取剧情摘要"""
  58. summary_path = project_root / ".webnovel" / "summaries" / f"ch{chapter_num:04d}.md"
  59. if not summary_path.exists():
  60. return ""
  61. text = summary_path.read_text(encoding="utf-8")
  62. summary_match = re.search(r"##\s*剧情摘要\s*\r?\n(.+?)(?=\r?\n##|$)", text, re.DOTALL)
  63. if summary_match:
  64. return summary_match.group(1).strip()
  65. return ""
  66. def extract_chapter_summary(project_root: Path, chapter_num: int) -> str:
  67. """提取指定章节摘要(优先 summaries/,再降级正文)"""
  68. summary = _load_summary_file(project_root, chapter_num)
  69. if summary:
  70. return summary
  71. volume_num = (chapter_num - 1) // 50 + 1
  72. chapter_dir = project_root / "正文" / f"第{volume_num}卷"
  73. # 尝试匹配章节文件
  74. patterns = [
  75. f"第{chapter_num:03d}章*.md",
  76. f"第{chapter_num:04d}章*.md",
  77. ]
  78. chapter_file = None
  79. for pattern in patterns:
  80. matches = list(chapter_dir.glob(pattern))
  81. if matches:
  82. chapter_file = matches[0]
  83. break
  84. if not chapter_file or not chapter_file.exists():
  85. return f"⚠️ 第 {chapter_num} 章文件不存在"
  86. content = chapter_file.read_text(encoding="utf-8")
  87. # 兼容旧格式:尝试提取"本章摘要"部分
  88. summary_match = re.search(r"##\s*本章摘要\s*\r?\n(.+?)(?=\r?\n##|$)", content, re.DOTALL)
  89. if summary_match:
  90. return summary_match.group(1).strip()
  91. # 如果没有摘要,提取"本章统计"部分
  92. stats_match = re.search(r"##\s*本章统计\s*\r?\n(.+?)(?=\r?\n##|$)", content, re.DOTALL)
  93. if stats_match:
  94. return f"[无摘要,仅统计]\n{stats_match.group(1).strip()}"
  95. # 最后降级:提取前500字作为摘要
  96. lines = content.split("\n")
  97. text_lines = [l for l in lines if not l.startswith("#") and l.strip()]
  98. text = "\n".join(text_lines)[:500]
  99. return f"[自动截取前500字]\n{text}..."
  100. def extract_state_summary(project_root: Path) -> str:
  101. """提取 state.json 的关键字段"""
  102. state_file = project_root / ".webnovel" / "state.json"
  103. if not state_file.exists():
  104. return "⚠️ state.json 不存在"
  105. state = json.loads(state_file.read_text(encoding="utf-8"))
  106. # 提取关键字段
  107. summary_parts = []
  108. # 进度
  109. if "progress" in state:
  110. p = state["progress"]
  111. summary_parts.append(f"**进度**: 第 {p.get('current_chapter', '?')} 章 / {p.get('total_words', '?')} 字")
  112. # 主角状态
  113. if "protagonist_state" in state:
  114. ps = state["protagonist_state"]
  115. power = ps.get("power", {})
  116. summary_parts.append(f"**主角实力**: {power.get('realm', '?')} {power.get('layer', '?')}层")
  117. summary_parts.append(f"**当前位置**: {ps.get('location', '?')}")
  118. gf = ps.get("golden_finger", {})
  119. summary_parts.append(f"**金手指**: {gf.get('name', '?')} Lv.{gf.get('level', '?')}")
  120. # Strand 追踪
  121. if "strand_tracker" in state:
  122. st = state["strand_tracker"]
  123. history = st.get("history", [])[-5:] # 最近5章
  124. if history:
  125. strand_str = ", ".join([f"Ch{h['chapter']}:{h['strand']}" for h in history])
  126. summary_parts.append(f"**近5章Strand**: {strand_str}")
  127. # 活跃伏笔(只显示紧急的)
  128. if "foreshadowing" in state:
  129. fs = state["foreshadowing"]
  130. active = [f for f in fs if f.get("status") == "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()