Files

13 KiB

const ordinals = [
  "first",
  "second",
  "third",
  "fourth",
  "fifth",
  "sixth",
  "seventh",
  "eighth",
  "ninth",
  "tenth",
  "eleventh",
  "twelfth",
  "thirteenth",
  "fourteenth",
  "fifteenth",
  "sixteenth",
  "seventeenth",
  "eighteenth",
  "nineteenth",
  "twentieth",
];
import { load } from "jsr:@std/dotenv";
import OpenAI from "jsr:@openai/openai";

// flush all previous env vars
/* for (const key of Object.keys(Deno.env.toObject())) {
  Deno.env.delete(key);
} */
const _ = await load({ export: true });
const openai = new OpenAI();
const safeName = (s: string) => s.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_").trim();
const bookName = Deno.env.get("BOOK_NAME");

if (!bookName) {
  throw new Error("BOOK_NAME environment variable is not set");
}

console.log(bookName);
progit
// BOOKMARK_LEVEL=1
// BOOK_FROM=10
// BOOK_TO=-1

Deno.env.set("BOOKMARK_LEVEL", "1");
Deno.env.set("BOOK_FROM", "10");
Deno.env.set("BOOK_TO", "-1");
import { PDFDocument } from "https://cdn.skypack.dev/pdf-lib@^1.11.1?dts";

const INPUT_PDF_PATH = `./books/${bookName}.pdf`;

type Bookmark = { title: string; page: number; level: number; endpage: number };
const bookmarks = await new Deno.Command("pdftk", {
  args: [
    INPUT_PDF_PATH,
    "dump_data",
    "output",
    "-",
  ],
}).output().then(
  (res) => new TextDecoder().decode(res.stdout),
).then((data) => {
  const lines = data.split("\n");
  let bookmarks: Bookmark[] = [];
  let currentBookmark: Partial<Bookmark> | null = null;

  for (const line of lines) {
    if (line.startsWith("BookmarkBegin")) {
      if (currentBookmark) {
        bookmarks.push(currentBookmark as Bookmark);
      }
      currentBookmark = {};
    } else if (line.startsWith("BookmarkTitle:")) {
      if (currentBookmark) {
        currentBookmark.title = line.replace("BookmarkTitle: ", "").trim();
      }
    } else if (line.startsWith("BookmarkLevel:")) {
      if (currentBookmark) {
        currentBookmark.level = parseInt(
          line.replace("BookmarkLevel: ", "").trim(),
        );
      }
    } else if (line.startsWith("BookmarkPageNumber:")) {
      if (currentBookmark) {
        currentBookmark.page = parseInt(
          line.replace("BookmarkPageNumber: ", "").trim(),
        );
      }
    }
  }
  if (currentBookmark) {
    bookmarks.push(currentBookmark as Bookmark);
  }

  bookmarks = bookmarks.filter((b) =>
    b.level === parseInt(Deno.env.get("BOOKMARK_LEVEL") ?? "1")
  ).slice(
    parseInt(Deno.env.get("BOOK_FROM") ?? "0"),
    parseInt(Deno.env.get("BOOK_TO") ?? String(bookmarks.length)),
  );

  for (let i = 0; i < bookmarks.length; i++) {
    const current = bookmarks[i] as Bookmark;
    const next = bookmarks[i + 1] as Bookmark | undefined;
    const currentPage = current.page ?? 0;
    const nextPage = next?.page ?? 0;
    current.endpage = nextPage
      ? Math.max(currentPage, nextPage - 1)
      : currentPage;
  }

  return bookmarks;
});

if (!bookmarks || bookmarks.length === 0) {
  throw new Error("No bookmarks found in the PDF.");
} /* else if (bookmarks.length > ordinals.length) {
  throw new Error(
    `Not enough ordinals for the number of chapters: ${bookmarks.length} chapters but only ${ordinals.length} ordinals.`,
  );
} */

console.log(bookmarks);
[
  { title: "Git Branching", level: 1, page: 69, endpage: 110 },
  { title: "Git on the Server", level: 1, page: 111, endpage: 131 },
  { title: "Distributed Git", level: 1, page: 132, endpage: 172 },
  { title: "GitHub", level: 1, page: 173, endpage: 225 },
  { title: "Git Tools", level: 1, page: 226, endpage: 348 },
  { title: "Customizing Git", level: 1, page: 349, endpage: 380 },
  { title: "Git and Other Systems", level: 1, page: 381, endpage: 436 },
  { title: "Git Internals", level: 1, page: 437, endpage: 476 },
  {
    title: "Appendix A: Git in Other Environments",
    level: 1,
    page: 477,
    endpage: 489
  },
  {
    title: "Appendix B: Embedding Git in your Applications",
    level: 1,
    page: 490,
    endpage: 501
  },
  {
    title: "Appendix C: Git Commands",
    level: 1,
    page: 502,
    endpage: 502
  }
]
const promises = [];
for (const [idx, ch] of bookmarks.entries()) {
  async function processChapter(idx: number, title: string, file: File) {
    const upload = await openai.files.create({
      file,
      purpose: "user_data",
    });

    const response = await openai.responses.create({
      model: "gpt-5.2",
      reasoning: { effort: "xhigh" },
      input: [{
        role: "user",
        content: [
          {
            type: "input_text",
            text:
              `A complete, exhaustive and very detailed mind map in markmap syntax of only and only this ${
                ordinals[idx]
              } chapter`,
          },
          { type: "input_file", file_id: upload.id },
        ],
      }],
      metadata: {
        chapter: title,
        ordinal: ordinals[idx],
      },
    });
    console.log(`Processing chapter: ${title} at ${response.id}`);

    await openai.files.delete(upload.id);

    const mindMapContent = response.output_text;
    await Deno.writeTextFile(
      `./mindmap/${safeName(title)}.md`,
      mindMapContent,
    );

    console.log(`Finished processing chapter: ${title}`);
  }
  {
    // Split the chapter into a separate PDF file
    const pdfBytes = await Deno.readFile(INPUT_PDF_PATH);
    const pdfDoc = await PDFDocument.load(pdfBytes);
    const chapterPdf = await PDFDocument.create();

    const startPage = ch.page - 1; // zero-based index
    const endPage = ch.endpage - 1; // zero-based index

    const pagesToCopy = await chapterPdf.copyPages(
      pdfDoc,
      Array.from(
        { length: endPage - startPage + 1 },
        (_, i) => i + startPage,
      ),
    );

    type PdfPage =
      import("https://cdn.skypack.dev/pdf-lib@^1.11.1?dts").PDFPage;

    pagesToCopy.forEach((page: PdfPage): void => {
      chapterPdf.addPage(page);
    });

    const chapterPdfBytes = await chapterPdf.save();
    const chapterFile = new File(
      [chapterPdfBytes],
      `${safeName(ch.title)}.pdf`,
      { type: "application/pdf" },
    );
    promises.push(processChapter(idx, ch.title, chapterFile));
  }
}
await Promise.all(promises);
Processing chapter: Appendix C: Git Commands at resp_04d1e841c5c4dfc2006984fd06079c81949afb817229705523
Finished processing chapter: Appendix C: Git Commands
Processing chapter: Customizing Git at resp_0ee8a5d1ba6d78a6006984fd06313c819087c4177391a50101
Finished processing chapter: Customizing Git
Processing chapter: Appendix A: Git in Other Environments at resp_00ee07fd3ffdee22006984fd069c7c8193bf192277fb80f4bc
Finished processing chapter: Appendix A: Git in Other Environments
Processing chapter: Git Internals at resp_0d312b5627d2306a006984fd0660c081939ebc561caf071506
Finished processing chapter: Git Internals
Processing chapter: Git on the Server at resp_07581a2e7f9be083006984fd05d32c8190b87cf1e8d7d68a5c
Finished processing chapter: Git on the Server
Processing chapter: GitHub at resp_0e4a9edfb24ed6a1006984fd06722081909497d0c7e35bfb1d
Finished processing chapter: GitHub
Processing chapter: Appendix B: Embedding Git in your Applications at resp_079044470442fcc0006984fd05fc1881978cacbd612d6e2f30
Finished processing chapter: Appendix B: Embedding Git in your Applications
Processing chapter: Distributed Git at resp_0947b861b710eb8a006984fd06918881968e0e1f70b418cc88
Finished processing chapter: Distributed Git
Processing chapter: Git Tools at resp_01ec7c1b665f5b12006984fd06b9048196a56f1e376843b55a
Finished processing chapter: Git Tools
Processing chapter: Git and Other Systems at resp_0e3a4b42775ae33f006984fd061cf8819389817080ff57b78f
Finished processing chapter: Git and Other Systems
Processing chapter: Git Branching at resp_0bb2c9b2b1435efc006984fd0641d08194b3fd55b5e3b3b432
Finished processing chapter: Git Branching
[
  undefined, undefined,
  undefined, undefined,
  undefined, undefined,
  undefined, undefined,
  undefined, undefined,
  undefined
]
// delete all files from openai file storage
const userFiles = await openai.files.list({ purpose: "user_data" });
for (const file of userFiles.data) {
  await openai.files.delete(file.id);
}
{ object: "file", deleted: true, id: "file-CxDoeyfgNLkuizajP6EHas" }