Files
short-video-viral-replicator/agent-studio-mini/scripts/ppt-worker.mjs
T

84 lines
5.7 KiB
JavaScript

import fs from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { createHash } from "node:crypto";
import { checkContent, normalize } from "./ppt-acceptance.mjs";
const [action, dir, reference, runtime, skill] = process.argv.slice(2);
process.env.RUNTIME_NODE_MODULES = path.join(runtime, "node/node_modules");
process.env.RUNTIME_PYTHON = path.join(runtime, "python/bin/python3");
const { Presentation, PresentationFile, FileBlob } = await import(pathToFileURL(path.join(runtime, "node/node_modules/@oai/artifact-tool/dist/artifact_tool.mjs")));
const { finalizePresentation, resolvePresentationFont } = await import(pathToFileURL(path.join(skill, "container_tools/artifact_tool_utils.mjs")));
const input = JSON.parse(await fs.readFile(path.join(dir, "input.json"), "utf8"));
const task = await fs.readFile(path.join(dir, "task.json"), "utf8").then(JSON.parse).catch(e => { if (e.code === "ENOENT") return null; throw e; });
const purpose = input.purpose ?? "debug";
let originalTexts = [];
const templated = action === "inspect" || input.template === "swads";
const deck = templated ? await PresentationFile.importPptx(await FileBlob.load(reference)) : Presentation.create({ slideSize: { width: 1280, height: 720 } });
if (action === "inspect") {
const snapshot = await deck.inspect({ kind: "slide,textbox,table", maxChars: 120000 });
await fs.writeFile(path.join(dir, "result.json"), JSON.stringify({ template: "swads", snapshot: snapshot.ndjson }));
process.exit(0);
}
if (templated) {
if (!input.replacements?.length && !input.tableCells?.length) throw new Error("PPT_REPLACEMENTS_REQUIRED");
const snapshot = await deck.inspect({ kind: "textbox,table", maxChars: 1000000 });
const records = snapshot.ndjson.split("\n").filter(Boolean).map(line => JSON.parse(line));
originalTexts = records.filter(r => r.kind === "textbox").map(r => r.text ?? r.textPreview ?? "").filter(t => normalize(t).length > 12);
for (const change of input.replacements ?? []) {
const record = records.find(r => r.id === change.id);
if (!record || !(record.text ?? record.textPreview ?? "").includes(change.oldText)) throw new Error(`PPT_TEMPLATE_ANCHOR_MISMATCH: ${change.id}. Reinspect this object; do not generate subset test files as final.`);
deck.resolve(change.id).text.replace(change.oldText, change.newText);
}
for (const cell of input.tableCells ?? []) {
const record = records.find(r => r.id === cell.id && r.kind === "table");
if (!record) throw new Error("PPT_TABLE_ANCHOR_MISMATCH");
deck.resolve(cell.id).cells.set(cell.row, cell.column, cell.text);
}
} else {
if (!input.slides?.length) throw new Error("PPT_SLIDES_REQUIRED");
const font = resolvePresentationFont();
for (const data of input.slides) {
const slide = deck.slides.add();
slide.background.fill = "#FAFAF7";
for (const [text, top, height, size, bold] of [[data.title, 60, 100, 44, true], [data.body, 190, 440, 28, false]]) {
const shape = slide.shapes.add({ geometry: "textbox", position: { left: 80, top, width: 1120, height }, fill: "none", line: { fill: "none", width: 0 } });
shape.text = text;
shape.text.style = { typeface: font, fontSize: size, color: "#17372B", bold, autoFit: "none" };
}
slide.speakerNotes.textFrame.setText(data.sources ?? "来源由调用方提供;未提供外部来源。");
}
}
await fs.mkdir(path.join(dir, "output"));
const candidate = path.join(dir, "candidate.pptx");
await (await PresentationFile.exportPptx(deck)).save(candidate);
const finalPath = path.join(dir, "output/deck.pptx");
await finalizePresentation({ workspaceDir: dir, candidatePath: candidate, finalPath,
pythonExecutable: path.join(runtime, "python/bin/python3"),
integrityValidatorPath: path.join(skill, "container_tools/inspect_presentation_package_integrity.py"),
layoutValidatorPath: path.join(skill, "container_tools/inspect_presentation_layout_geometry.py"),
layoutArgs: ["--validate-heading-fit"], verifyArtifactToolImport: true,
receiptPath: path.join(dir, "validation.json"),
});
const finalDeck = await PresentationFile.importPptx(await FileBlob.load(finalPath));
let count = 0;
for (const slide of finalDeck.slides.items) {
const png = await finalDeck.export({ slide, format: "png", scale: 1 });
await fs.writeFile(path.join(dir, `slide-${++count}.png`), new Uint8Array(await png.arrayBuffer()));
}
if (!count) throw new Error("PPT_EMPTY");
const bytes = await fs.readFile(finalPath);
const finalSnapshot = await finalDeck.inspect({ kind: "textbox,table", maxChars: 1000000 });
await fs.writeFile(path.join(dir, "content.ndjson"), finalSnapshot.ndjson);
const finalText = normalize(finalSnapshot.ndjson.split("\n").filter(Boolean).map(line => {
const r = JSON.parse(line); return r.text ?? r.textPreview ?? JSON.stringify(r);
}).join("\n"));
const failures = checkContent({ purpose, task, template: input.template, originalTexts, finalText });
await fs.writeFile(path.join(dir, "acceptance.json"), JSON.stringify({ purpose, runId: task?.runId, taskRevision: task?.revision,
passed: purpose === "final" && failures.length === 0, failures,
limits: "Text coverage and explicit task terms only; embedded images and factual accuracy are not semantically verified." }, null, 2));
if (failures.length) throw new Error(`PPT_CONTENT_REJECTED: ${failures.slice(0,8).join("; ")}`);
await fs.writeFile(path.join(dir, "result.json"), JSON.stringify({ title: input.title, slideCount: count, validated: true,
purpose, deliveryAccepted: false, runId: task?.runId, taskRevision: task?.revision,
sha256: createHash("sha256").update(bytes).digest("hex"), template: input.template,
note: "Package, layout checks and rendering passed. Business facts and visual quality require review." }));