No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

233 líneas
10KB

  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. def build_inference():
  104. d = fitz.open(ROOT / "Inference Engineering" / "source-file.pdf")
  105. top = [(t, p) for lvl, t, p in d.get_toc() if lvl <= 2]
  106. # Skip the Table of Contents bookmark: it is front matter (pages 1-10 with the cover and TOC).
  107. top = [(t, p) for t, p in top if t != "Table of Contents"]
  108. last_body = next(p for t, p in top if t == "Acknowledgements")
  109. secs = [("Front-Matter", "Front matter (cover, title, contents)", 1, top[0][1] - 1, "")]
  110. body = [(t, p) for t, p in top if p < last_body]
  111. for i, (t, p) in enumerate(body):
  112. end = (body[i + 1][1] - 1) if i + 1 < len(body) else last_body - 1
  113. title = re.sub(r"^Chapter \d+:\s*", "", t)
  114. secs.append((ch(i + 1), title, p, end, ""))
  115. secs.append(("Back-Matter", "Acknowledgements", last_body, len(d), ""))
  116. return secs
  117. def build_os32():
  118. # Scanned book (no bookmarks): printed page N is PDF page N + 22. Chapters that open a Part
  119. # take in the Part divider plate and its blank verso, two pages before the chapter plate.
  120. chapters = [ # (n, title, printed start page, part that opens here)
  121. (1, "Overview", 3, "I"),
  122. (2, "General Discussion and Background", 13, ""),
  123. (3, "The Tasking Model", 25, "II"),
  124. (4, "Interprocess Communications", 41, ""),
  125. (5, "Memory Management", 55, ""),
  126. (6, "The Hardware Interface", 81, ""),
  127. (7, "OS Initialization", 99, ""),
  128. (8, "Programming Interfaces", 115, ""),
  129. (9, "Application Programming", 129, "III"),
  130. (10, "Systems Programming", 141, ""),
  131. (11, "The MMURTL Monitor Program", 159, ""),
  132. (12, "The Debugger", 181, ""),
  133. (13, "Keyboard Service", 189, ""),
  134. (14, "File System Service", 201, ""),
  135. (15, "API Specification", 221, ""),
  136. (16, "MMURTL Sample Software", 265, ""),
  137. (17, "Introduction to the Source Code", 327, "IV"),
  138. (18, "Kernel Code", 335, ""),
  139. (19, "Memory Management Code", 377, ""),
  140. (20, "Timer Management Code", 405, ""),
  141. (21, "Initialization Code", 419, ""),
  142. (22, "Job Management Code", 439, ""),
  143. (23, "Debugger Code", 471, ""),
  144. (24, "Selected Device Driver Code", 497, ""),
  145. (25, "Keyboard Code", 541, ""),
  146. (26, "Video Code", 571, ""),
  147. (27, "File System Code", 591, ""),
  148. (28, "DASM: A 32-Bit Intel-Based Assembler", 663, "V"),
  149. (29, "CM32: A 32-Bit C Compiler", 697, ""),
  150. ]
  151. index_start = 719 + 22
  152. starts = [(n, t, s + 22 - (2 if part else 0), part) for n, t, s, part in chapters]
  153. secs = [("Front-Matter", "Front matter (covers, title, copyright, contents, preface, acknowledgments, about the author)", 1, starts[0][2] - 1, "")]
  154. for i, (n, t, a, part) in enumerate(starts):
  155. end = starts[i + 1][2] - 1 if i + 1 < len(starts) else index_start - 1
  156. note = f"Includes the Part {part} divider plate and its blank verso before the chapter." if part else ""
  157. secs.append((ch(n), t, a, end, note))
  158. secs.append(("Back-Matter", "Index, publisher ads, CD-ROM installation and contents, back cover", index_start, 772, ""))
  159. return secs
  160. BUILDERS = {
  161. "Developing Your Own 32-Bit Operating System": build_os32,
  162. "Context Engineering": build_cetx,
  163. "The 12 Week Year": build_12wy,
  164. "Your Best Year Ever": build_ybye,
  165. "Spec Driven Development": build_sdd,
  166. "FOCUS Architecture": build_focus,
  167. "Inference Engineering": build_inference,
  168. }
  169. def main(book):
  170. pdf = ROOT / book / "source-file.pdf"
  171. doc = fitz.open(pdf)
  172. secs = BUILDERS[book]()
  173. covered = {}
  174. for folder, title, a, b, note in secs:
  175. pages, blank = [], []
  176. for p in range(a, b + 1):
  177. if p in covered:
  178. raise SystemExit(f"page {p} in both {covered[p]} and {folder}")
  179. covered[p] = folder
  180. text = doc[p - 1].get_text().strip()
  181. if len(text) < 30:
  182. blank.append(p)
  183. pages.append(f"<!-- PDF page {p} -->\n{text}\n")
  184. header = [
  185. f"# {book} — {folder}: {title}",
  186. f"- **Source**: /library/{book}/source-file.pdf",
  187. f"- **PDF pages**: {a}–{b}",
  188. f"- **Pages without text**: {', '.join(map(str, blank)) if blank else 'none'}",
  189. ]
  190. if note:
  191. header.append(f"- **Note**: {note}")
  192. out = ROOT / book / folder_name(folder, title)
  193. out.mkdir(exist_ok=True)
  194. (out / f"{folder}-source-text.md").write_text("\n".join(header) + "\n\n---\n\n" + "\n".join(pages), encoding="utf-8", newline="\n")
  195. print(f"{folder:14} pp {a}-{b} blank={blank} {title[:50]}")
  196. missing = [p for p in range(1, len(doc) + 1) if p not in covered]
  197. print("pages not covered:", missing)
  198. if __name__ == "__main__":
  199. main(sys.argv[1])

Powered by TurnKey Linux.