Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

170 рядки
6.8KB

  1. """Export a book PDF to text and split it into per-chapter source-text.md files.
  2. Usage: python tools/split_book.py "<Book Title>"
  3. Reads library/<Book Title>/source-file.pdf and writes
  4. library/<Book Title>/<Chapter-XX-Title-Slug>/<Chapter-XX>-source-text.md for each section defined in SECTIONS
  5. below (add an entry for a new book). Sections are (folder, title, first_page, last_page,
  6. note) with 1-based PDF pages. Pages not covered by any section are reported.
  7. """
  8. import re
  9. import sys
  10. from pathlib import Path
  11. import fitz # PyMuPDF
  12. ROOT = Path(__file__).resolve().parent.parent / "library"
  13. YBYE_NOTE_AFTER = "Includes the Step action plan that closes this step."
  14. YBYE_NOTE_BEFORE = "Includes the Step introduction page that precedes the chapter."
  15. def ch(n):
  16. return f"Chapter-{n:02d}"
  17. def slug(title, limit=60):
  18. """Title -> folder-name slug (see AGENTS.md Naming Convention)."""
  19. t = re.sub(r"^\d+(\.\d+)?[a-z]?\s*-\s*", "", title.strip()) # drop the book's own numbering prefix
  20. t = re.sub(r"[:,.'\"‘’“”?/\()]", "", t)
  21. t = re.sub(r"[^\w\s-]", "", t)
  22. words, out = t.split(), ""
  23. for w in words:
  24. if len(out) + len(w) + (1 if out else 0) > limit:
  25. break
  26. out = f"{out}-{w}" if out else w
  27. return re.sub(r"-+", "-", out) or "Untitled"
  28. def folder_name(key, title):
  29. """Chapter-05 + title -> Chapter-05-Anatomy-of-a-Specification; Front/Back-Matter keep their name."""
  30. return key if key in ("Front-Matter", "Back-Matter") else f"{key}-{slug(title)}"
  31. def build_cetx():
  32. d = fitz.open(ROOT / "Context Engineering" / "source-file.pdf")
  33. top = [(t, p) for lvl, t, p in d.get_toc() if lvl == 1]
  34. secs = [("Front-Matter", "Front matter", 1, top[0][1] - 1, "")]
  35. for i, (t, p) in enumerate(top):
  36. end = (top[i + 1][1] - 1) if i + 1 < len(top) else len(d)
  37. secs.append((ch(i + 1), t, p, end, ""))
  38. return secs
  39. def build_12wy():
  40. d = fitz.open(ROOT / "The 12 Week Year" / "source-file.pdf")
  41. starts = []
  42. for lvl, t, p in d.get_toc():
  43. if t.startswith("Chapter ") and lvl in (1, 2):
  44. num = int(t.split(":")[0].split()[1])
  45. starts.append((num, t.split(":", 1)[1].strip(), p))
  46. secs = [("Front-Matter", "Front matter", 1, starts[0][2] - 1, "")]
  47. for i, (n, t, p) in enumerate(starts):
  48. end = starts[i + 1][2] - 1 if i + 1 < len(starts) else 168
  49. note = ""
  50. if n == 1:
  51. note = "Includes the Part I divider page that follows the chapter."
  52. secs.append((ch(n), t, p, end, note))
  53. secs.append(("Back-Matter", "References", 169, len(d), ""))
  54. return secs
  55. def build_ybye():
  56. chapters = [ # (n, title, first, last, note)
  57. (0, "Your Best Is Yet to Come (Opening)", 11, 21, ""),
  58. (1, "Your Beliefs Shape Your Reality", 22, 29, YBYE_NOTE_BEFORE),
  59. (2, "Some Beliefs Hold You Back", 30, 38, ""),
  60. (3, "You Can Upgrade Your Beliefs", 39, 53, YBYE_NOTE_AFTER),
  61. (4, "Thinking Backward Is a Must", 54, 64, YBYE_NOTE_BEFORE),
  62. (5, "Regret Reveals Opportunity", 65, 73, ""),
  63. (6, "Gratitude Makes the Difference", 74, 82, YBYE_NOTE_AFTER),
  64. (7, "Great Goals Check Seven Boxes", 83, 102, YBYE_NOTE_BEFORE),
  65. (8, "Achievements and Habits Work Together", 103, 109, ""),
  66. (9, "Seriously, Risk Is Your Friend", 110, 124, YBYE_NOTE_AFTER),
  67. (10, "Your What Needs a Why", 125, 139, YBYE_NOTE_BEFORE),
  68. (11, "You Can Master Your Own Motivation", 140, 148, ""),
  69. (12, "The Journey Is Better with Friends", 149, 160, YBYE_NOTE_AFTER),
  70. (13, "One Journey Is Many Steps", 161, 170, YBYE_NOTE_BEFORE),
  71. (14, "You Can Trigger Success", 171, 177, ""),
  72. (15, "Visibility Is Essential", 178, 190, YBYE_NOTE_AFTER),
  73. ]
  74. secs = [("Front-Matter", "Front matter (title, copyright, endorsements, contents)", 1, 10, "")]
  75. secs += [(ch(n), t, a, b, note) for n, t, a, b, note in chapters]
  76. secs.append(("Back-Matter", "LEAP Principle, Sample Goal Templates, Notes, Thanks, Index, About the Author", 191, 235, ""))
  77. return secs # pages 236-242 (ads, back cover) are intentionally excluded
  78. def parse_table(book, first_col_prefix, skip_rows=0):
  79. rows = []
  80. for line in (ROOT / book / "book-structure.md").read_text(encoding="utf-8").splitlines():
  81. if line.startswith("|") and not line.startswith("| ---") and not line.startswith("| Reading"):
  82. rows.append([c.strip() for c in line.strip("|").split("|")])
  83. return rows
  84. def build_sdd():
  85. d = fitz.open(ROOT / "Spec Driven Development" / "source-file.pdf")
  86. secs = [("Front-Matter", "Front matter (cover, title, contents)", 1, 10, "")]
  87. for r in parse_table("Spec Driven Development", ""):
  88. n = int(r[0].split("-")[1])
  89. a, b = r[1].replace("–", "-").split("-")
  90. secs.append((ch(n), r[2], int(a), int(b), ""))
  91. return secs
  92. def build_focus():
  93. d = fitz.open(ROOT / "FOCUS Architecture" / "source-file.pdf")
  94. rows = []
  95. for r in parse_table("FOCUS Architecture", ""):
  96. rows.append((r[0], r[1], int(r[2])))
  97. secs = [("Front-Matter", "Front matter", 1, rows[0][2] - 1, "")]
  98. for i, (kind, title, start) in enumerate(rows):
  99. end = rows[i + 1][2] - 1 if i + 1 < len(rows) else len(d)
  100. folder = "Interlude" if kind == "Interlude" else ch(int(kind.split()[1]))
  101. secs.append((folder, title, start, end, ""))
  102. return secs
  103. BUILDERS = {
  104. "Context Engineering": build_cetx,
  105. "The 12 Week Year": build_12wy,
  106. "Your Best Year Ever": build_ybye,
  107. "Spec Driven Development": build_sdd,
  108. "FOCUS Architecture": build_focus,
  109. }
  110. def main(book):
  111. pdf = ROOT / book / "source-file.pdf"
  112. doc = fitz.open(pdf)
  113. secs = BUILDERS[book]()
  114. covered = {}
  115. for folder, title, a, b, note in secs:
  116. pages, blank = [], []
  117. for p in range(a, b + 1):
  118. if p in covered:
  119. raise SystemExit(f"page {p} in both {covered[p]} and {folder}")
  120. covered[p] = folder
  121. text = doc[p - 1].get_text().strip()
  122. if len(text) < 30:
  123. blank.append(p)
  124. pages.append(f"<!-- PDF page {p} -->\n{text}\n")
  125. header = [
  126. f"# {book} — {folder}: {title}",
  127. f"- **Source**: /library/{book}/source-file.pdf",
  128. f"- **PDF pages**: {a}–{b}",
  129. f"- **Pages without text**: {', '.join(map(str, blank)) if blank else 'none'}",
  130. ]
  131. if note:
  132. header.append(f"- **Note**: {note}")
  133. out = ROOT / book / folder_name(folder, title)
  134. out.mkdir(exist_ok=True)
  135. (out / f"{folder}-source-text.md").write_text("\n".join(header) + "\n\n---\n\n" + "\n".join(pages), encoding="utf-8", newline="\n")
  136. print(f"{folder:14} pp {a}-{b} blank={blank} {title[:50]}")
  137. missing = [p for p in range(1, len(doc) + 1) if p not in covered]
  138. print("pages not covered:", missing)
  139. if __name__ == "__main__":
  140. main(sys.argv[1])

Powered by TurnKey Linux.