import fs from "node:fs"; import crypto from "node:crypto"; import os from "node:os"; import path from "node:path"; import axios from "axios"; import { connect } from "nats"; import { Client as Minio } from "minio"; import PptxGenJS from "pptxgenjs"; const NATS_URL = process.env.NATS_URL || "nats://nats:4222"; const REGISTRY_URL = (process.env.ARTIFACT_REGISTRY_URL || "http://artifact-registry:9220").replace(/\/$/, ""); const MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || "minio:9000"; const MINIO_ACCESS_KEY = process.env.MINIO_ACCESS_KEY || "minioadmin"; const MINIO_SECRET_KEY = process.env.MINIO_SECRET_KEY || "minioadmin"; const MINIO_BUCKET = process.env.MINIO_BUCKET || "artifacts"; const MINIO_SECURE = (process.env.MINIO_SECURE || "false").toLowerCase() === "true"; const minioClient = new Minio({ endPoint: MINIO_ENDPOINT.split(":")[0], port: Number(MINIO_ENDPOINT.split(":")[1] || 9000), useSSL: MINIO_SECURE, accessKey: MINIO_ACCESS_KEY, secretKey: MINIO_SECRET_KEY, }); const toBuffer = async (stream) => { const chunks = []; for await (const chunk of stream) { chunks.push(chunk); } return Buffer.concat(chunks); }; const sha256 = (buf) => crypto.createHash("sha256").update(buf).digest("hex"); const renderPptx = async (slidespec, outPath) => { const pptx = new PptxGenJS(); pptx.layout = "LAYOUT_WIDE"; pptx.author = "DAARION Artifact Worker"; const slides = slidespec.slides || []; if (!slides.length) { throw new Error("No slides in slidespec"); } slides.forEach((slide, idx) => { const s = pptx.addSlide(); const title = slide.title || slidespec.title || `Slide ${idx + 1}`; s.addText(title, { x: 0.6, y: 0.6, w: 12, h: 0.8, fontSize: idx === 0 ? 36 : 28, bold: true }); if (slide.subtitle) { s.addText(slide.subtitle, { x: 0.6, y: 1.6, w: 12, h: 0.5, fontSize: 16, color: "666666" }); } if (slide.bullets && Array.isArray(slide.bullets) && slide.bullets.length) { s.addText(slide.bullets.map((b) => ({ text: b, options: { bullet: { indent: 18 } } })), { x: 0.8, y: 2.0, w: 11.5, h: 4.5, fontSize: 18, color: "333333", }); } }); await pptx.writeFile({ fileName: outPath }); }; const handleJob = async (msg) => { const data = JSON.parse(msg.data.toString()); const { job_id, storage_key, theme_id, artifact_id, input_version_id } = data; const slidespecKey = storage_key || `artifacts/${artifact_id}/versions/${input_version_id}/slidespec.json`; try { const objectStream = await minioClient.getObject(MINIO_BUCKET, slidespecKey); const buf = await toBuffer(objectStream); const slidespec = JSON.parse(buf.toString("utf-8")); const tmpFile = path.join(os.tmpdir(), `${job_id}.pptx`); await renderPptx(slidespec, tmpFile, theme_id); const pptxBuf = fs.readFileSync(tmpFile); const pptxSha = sha256(pptxBuf); const pptxKey = `artifacts/${artifact_id}/versions/${input_version_id}/presentation.pptx`; await minioClient.putObject(MINIO_BUCKET, pptxKey, pptxBuf, pptxBuf.length, { "Content-Type": "application/vnd.openxmlformats-officedocument.presentationml.presentation", }); await axios.post(`${REGISTRY_URL}/jobs/${job_id}/complete`, { output_storage_key: pptxKey, mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation", size_bytes: pptxBuf.length, sha256: pptxSha, label: "pptx", }); fs.unlinkSync(tmpFile); } catch (err) { await axios.post(`${REGISTRY_URL}/jobs/${job_id}/fail`, { error_text: String(err?.message || err), }); } }; const main = async () => { const nc = await connect({ servers: [NATS_URL] }); const sub = nc.subscribe("artifact.job.render_pptx.requested"); for await (const msg of sub) { await handleJob(msg); } }; main().catch((err) => { console.error("worker failed", err); process.exit(1); });