sergiopaniego HF Staff
Publish the deck source under source/ so the next deck can start from it
3bc016b verified Download source/scripts/export-deck.mjs from FineEnvs/multi-harness-rl-slides: direct link, hf CLI and curl.
- Browser
- Download file 5.55 kB
-
https://huggingface.co/spaces/FineEnvs/multi-harness-rl-slides/resolve/main/source/scripts/export-deck.mjs
- Command line
-
hf download hf://spaces/FineEnvs/multi-harness-rl-slides/source/scripts/export-deck.mjs
-
curl -L -o export-deck.mjs https://huggingface.co/spaces/FineEnvs/multi-harness-rl-slides/resolve/main/source/scripts/export-deck.mjs
5.55 kB
| /** | |
| * Export the deck to a PDF and a PPTX, one page per slide, 1280x720 landscape. | |
| * | |
| * Both come out of a single walk of the deck, because the expensive part is | |
| * waiting: the deck is a single-page app with entry animations, and the figures | |
| * are iframes that have to lay out before anything is captured. | |
| * | |
| * The PPTX is one full-bleed image per slide rather than editable shapes. That | |
| * is deliberate: the figures are sandboxed iframes, so there is nothing to | |
| * convert into native shapes, and an image per slide is what survives an import | |
| * into Google Slides, Keynote or PowerPoint intact. | |
| * | |
| * npm run build && npm run export | |
| * | |
| * Outputs land in public/ so they ship with the Space and the settings panel | |
| * can link straight at them. | |
| * | |
| * Env: PORT (default 7899), SCALE (default 2, the PPTX image density), SLIDES | |
| * (default: read from the built bundle). | |
| */ | |
| import { chromium } from "playwright"; | |
| import { createServer } from "node:http"; | |
| import { readFile, stat, mkdir, writeFile } from "node:fs/promises"; | |
| import { join, extname, dirname, resolve } from "node:path"; | |
| import { PDFDocument } from "pdf-lib"; | |
| import PptxGenJS from "pptxgenjs"; | |
| const ROOT = resolve(dirname(new URL(import.meta.url).pathname), ".."); | |
| const DIST = join(ROOT, "dist"); | |
| const OUTDIR = join(ROOT, "public"); | |
| const NAME = "multi-harness-rl-slides"; | |
| const PORT = Number(process.env.PORT || 7899); | |
| const SCALE = Number(process.env.SCALE || 2); | |
| const MIME = { | |
| ".html": "text/html", ".js": "text/javascript", ".css": "text/css", | |
| ".json": "application/json", ".png": "image/png", ".svg": "image/svg+xml", | |
| ".jpg": "image/jpeg", ".webp": "image/webp", ".pdf": "application/pdf", | |
| ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", | |
| }; | |
| // A static server for dist/, so the export runs against exactly what ships. | |
| function serve() { | |
| return new Promise((ok) => { | |
| const s = createServer(async (req, res) => { | |
| const path = decodeURIComponent((req.url || "/").split("?")[0]); | |
| let file = join(DIST, path === "/" ? "index.html" : path); | |
| try { | |
| if ((await stat(file)).isDirectory()) file = join(file, "index.html"); | |
| } catch { | |
| file = join(DIST, "index.html"); // SPA fallback | |
| } | |
| try { | |
| const body = await readFile(file); | |
| res.writeHead(200, { "Content-Type": MIME[extname(file)] || "application/octet-stream" }); | |
| res.end(body); | |
| } catch { | |
| res.writeHead(404).end("not found"); | |
| } | |
| }); | |
| s.listen(PORT, () => ok(s)); | |
| }); | |
| } | |
| const server = await serve(); | |
| const browser = await chromium.launch(); | |
| const page = await browser.newPage({ viewport: { width: 1280, height: 720 }, deviceScaleFactor: SCALE }); | |
| await page.goto(`http://localhost:${PORT}/`, { waitUntil: "networkidle" }); | |
| // start at the first slide regardless of what the last session left behind | |
| await page.evaluate(() => localStorage.setItem("rlenv-slides-index", "0")); | |
| await page.reload({ waitUntil: "networkidle" }); | |
| const total = await page.evaluate(() => { | |
| const m = document.body.innerText.match(/SLIDES\s*·\s*(\d+)/); | |
| return m ? Number(m[1]) : null; | |
| }); | |
| const count = Number(process.env.SLIDES || total || 35); | |
| console.log(`exporting ${count} slides`); | |
| const pdfPages = []; | |
| const shots = []; | |
| for (let i = 0; i < count; i++) { | |
| // let the entry animation finish and every figure iframe lay out | |
| await page.waitForTimeout(3200); | |
| await page.evaluate(async () => { | |
| await Promise.all([...document.querySelectorAll("iframe")].map((f) => | |
| f.contentDocument && f.contentDocument.readyState === "complete" | |
| ? Promise.resolve() | |
| : new Promise((r) => f.addEventListener("load", r, { once: true })))); | |
| }); | |
| pdfPages.push(await page.pdf({ | |
| width: "1280px", height: "720px", printBackground: true, | |
| pageRanges: "1", margin: { top: 0, right: 0, bottom: 0, left: 0 }, | |
| })); | |
| // JPEG rather than PNG: a 35-slide deck of 2560x1440 PNGs is an order of | |
| // magnitude larger than the whole Space, and these are photographs of a | |
| // rendered page, not line art that needs lossless. | |
| // no `scale` option, so the shot comes out at the device scale (2560x1440), | |
| // which is what keeps the type crisp on a projector. | |
| shots.push(await page.screenshot({ type: "jpeg", quality: 90, clip: { x: 0, y: 0, width: 1280, height: 720 } })); | |
| process.stdout.write(` ${i + 1}/${count}\r`); | |
| if (i < count - 1) await page.keyboard.press("ArrowRight"); | |
| } | |
| await browser.close(); | |
| server.close(); | |
| await mkdir(OUTDIR, { recursive: true }); | |
| // stitch the single-page PDFs into one deck | |
| const out = await PDFDocument.create(); | |
| for (const buf of pdfPages) { | |
| const src = await PDFDocument.load(buf); | |
| const [p] = await out.copyPages(src, [0]); | |
| out.addPage(p); | |
| } | |
| const pdfPath = join(OUTDIR, `${NAME}.pdf`); | |
| await writeFile(pdfPath, await out.save()); | |
| // one full-bleed image per PPTX slide, 16:9 at the standard 13.333 x 7.5 in | |
| const pptx = new PptxGenJS(); | |
| pptx.layout = "LAYOUT_16x9"; | |
| pptx.title = "Training a coding agent through a harness you did not write"; | |
| pptx.author = "Sergio Paniego Blanco"; | |
| for (const shot of shots) { | |
| const s = pptx.addSlide(); | |
| s.background = { color: "07090F" }; | |
| s.addImage({ data: `image/jpeg;base64,${shot.toString("base64")}`, x: 0, y: 0, w: 13.333, h: 7.5 }); | |
| } | |
| const pptxPath = join(OUTDIR, `${NAME}.pptx`); | |
| await pptx.writeFile({ fileName: pptxPath }); | |
| for (const p of [pdfPath, pptxPath]) { | |
| const { size } = await stat(p); | |
| console.log(`\nwrote ${p} (${(size / 1e6).toFixed(1)} MB)`); | |
| } | |