Source code
src/scripts/presentation/render-slides.mjs
This file is included in this documentation build. It is displayed as code and is not executed.
/**
* Regenerate editable decks and versioned video frames using an explicitly supplied artifact runtime.
*
* @author Laurent Declercq l.declercq@agon-innovation.ch
* @version 20260921
*/
import fs from 'node:fs/promises';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import { parseArgs } from 'node:util';
import { spawnSync } from 'node:child_process';
const { values: args } = parseArgs({
options: {
'locale': { type: 'string' },
'output': { type: 'string' },
'source': { type: 'string' },
'update-artifacts': { type: 'boolean' },
'help': { type: 'boolean' }
}
});
if (args.help) {
console.log('Usage: yarn presentation:slides [--locale en|de|fr] [--output NEW_DIRECTORY | --update-artifacts] [--source DIRECTORY]\nRequires ATRIUM_PRESENTATION_NODE_MODULES, ATRIUM_PRESENTATION_SKILL, and ATRIUM_PRESENTATION_PYTHON. Writes refreshed PNG frames and validated PPTX files. --update-artifacts replaces SOURCE/artifacts/decks after validation.');
process.exit(0);
}
if (args.output && args['update-artifacts']) {
throw new Error('Choose either --output for a draft or --update-artifacts for the versioned decks.');
}
const root = path.resolve(import.meta.dirname, '../../..');
const source = path.resolve(args.source || path.join(root, 'docs/presentation'));
const runtime = process.env.ATRIUM_PRESENTATION_NODE_MODULES;
const skill = process.env.ATRIUM_PRESENTATION_SKILL;
const python = process.env.ATRIUM_PRESENTATION_PYTHON;
if (!runtime || !skill || !python) {
throw new Error('Set ATRIUM_PRESENTATION_NODE_MODULES, ATRIUM_PRESENTATION_SKILL, and ATRIUM_PRESENTATION_PYTHON. See docs/presentation/README.md. Video-only builds do not require these.');
}
const requireRuntime = createRequire(path.join(path.resolve(runtime), '..', 'package.json'));
// The finalizer's child import check uses its own documented runtime variable.
process.env.RUNTIME_NODE_MODULES = path.resolve(runtime);
const sharp = requireRuntime('sharp');
const {
Presentation,
PresentationFile
} = await import(pathToFileURL(requireRuntime.resolve('@oai/artifact-tool')).href);
const {
finalizePresentation
} = await import(pathToFileURL(path.join(skill, 'container_tools/artifact_tool_utils.mjs')).href);
await fs.mkdir(path.join(root, '.build/presentation'), { recursive: true });
const tmp = await fs.mkdtemp(path.join(root, '.build/presentation/render-'));
await fs.mkdir(path.join(tmp, 'slides'));
const output = path.resolve(args.output || path.join(tmp, 'decks'));
if (output !== root && !output.startsWith(root + path.sep)) {
throw new Error('Keep the validated PPTX output inside the repository workspace.');
}
await fs.mkdir(output, { recursive: true });
const story = JSON.parse(await fs.readFile(path.join(source, 'storyboard.json'), 'utf8'));
if (args.locale && !Object.hasOwn(story.languages, args.locale)) {
throw new Error('Unknown presentation language.');
}
const manifestPath = path.join(source, 'source/slides/manifest.json');
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
const logo = await sharp(path.join(root, 'src/showcase/public/favicon.svg')).resize(240, 240).png().toBuffer();
const cover = await fs.readFile(path.join(source, 'source/cover.png'));
/**
* Hash stable slide inputs for stale-frame detection.
*
* @param {string | Buffer} value - Content to hash.
* @returns {string} SHA-256 digest.
*/
function sha(value) {
return createHash('sha256').update(value).digest('hex');
}
/**
* Load authoritative narration through the same parser used by video generation.
*
* @param {string} file - Language TXT file.
* @param {object[]} slides - Ordered storyboard slides.
* @returns {Promise<object>} Narration indexed by stable slide ID.
*/
async function narrationSections(file, slides) {
const ids = [];
for (const slide of slides) {
ids.push(slide.id);
}
const script = 'import sys,json; from pathlib import Path; sys.path.insert(0,sys.argv[1]); from pipeline import parse_narration; print(json.dumps(parse_narration(Path(sys.argv[2]).read_text(encoding=\'utf-8\'),json.loads(sys.argv[3]))))';
const result = spawnSync(python, [
'-c',
script,
import.meta.dirname,
file,
JSON.stringify(ids)
], { encoding: 'utf8' });
if (result.error || result.status !== 0) {
throw new Error(result.stderr || 'Cannot read narration.');
}
return JSON.parse(result.stdout);
}
const font = 'Arial';
/**
* Add an editable branded text box to a slide.
*
* @param {object} slide - Target slide.
* @param {string} copy - Visible copy.
* @param {number} x - Left coordinate.
* @param {number} y - Top coordinate.
* @param {number} w - Width.
* @param {number} h - Height.
* @param {number} size - Font size.
* @param {string} [color] - Text color.
* @param {boolean} [bold] - Whether text is bold.
* @returns {object} Editable text shape.
*/
function text(slide, copy, x, y, w, h, size, color = '#F5F7FA', bold = false) {
const s = slide.shapes.add({
geometry: 'textbox',
position: { left: x, top: y, width: w, height: h },
fill: 'none',
line: { fill: 'none', width: 0 }
});
s.text = copy;
s.text.style = {
typeface: font,
fontSize: size,
color,
bold,
autoFit: 'none',
verticalAlignment: 'middle',
wrap: true
};
return s;
}
/**
* Place an original screenshot without altering its aspect ratio.
*
* @param {object} slide - Target slide.
* @param {string} locale - Screenshot language.
* @param {string} name - Screenshot basename.
* @param {number} x - Left coordinate.
* @param {number} y - Top coordinate.
* @param {number} w - Width.
* @param {number} h - Height.
* @returns {Promise<void>} Resolves after the screenshot is added.
*/
async function picture(slide, locale, name, x, y, w, h) {
const bytes = await fs.readFile(path.join(source, `source/screenshots/${locale}-${name}.png`));
slide.images.add({
blob: bytes,
contentType: 'image/png',
alt: `Agon Atrium ${name} (${locale})`,
fit: 'contain',
position: { left: x, top: y, width: w, height: h }
});
}
for (const [ locale, slides ] of Object.entries(story.languages)) {
if (args.locale && args.locale !== locale) {
continue;
}
const narration = await narrationSections(path.join(source, `narration/${locale}.txt`), slides);
const records = {};
const p = Presentation.create({ slideSize: { width: 1920, height: 1080 } });
for (const [ i, data ] of slides.entries()) {
const s = p.slides.add();
s.background.fill = '#080D15';
const isCover = data.id === 'cover' || data.id === 'close';
if (isCover) {
s.images.add({
blob: cover,
contentType: 'image/png',
alt: 'Technology workspace overlooking an Alpine lake, conceptual brand artwork',
position: { left: 0, top: 0, width: 1920, height: 1080 },
fit: 'cover'
});
s.images.add({
blob: logo,
contentType: 'image/png',
alt: 'Agon Partners Innovation AG logo',
position: { left: 100, top: 95, width: 105, height: 105 },
fit: 'contain'
});
text(s, 'AGON PARTNERS\nINNOVATION AG', 235, 95, 600, 110, 38, '#FFFFFF', true);
text(s, data.title, 100, data.id === 'cover' ? 342 : 324, 765, 310, data.id === 'cover' ? 122 : 76, '#FFFFFF', true);
text(s, data.body, 105, 652, 700, 180, 40, '#E2EDF4');
if (data.id === 'cover') {
text(s, ({
en: 'ADMIN DASHBOARD TEMPLATE',
de: 'ADMIN-DASHBOARD-VORLAGE',
fr: 'MODÈLE DE TABLEAU DE BORD'
})[locale], 105, 902, 850, 45, 23, '#92DDEA');
}
} else {
s.images.add({
blob: logo,
contentType: 'image/png',
alt: 'Agon logo',
position: { left: 96, top: 60, width: 60, height: 60 },
fit: 'contain'
});
text(s, 'Agon Ātrium', 176, 64, 420, 58, 32, '#F5F7FA', true);
text(s, `${String(i + 1).padStart(2, '0')} / ${slides.length}`, 1715, 76, 112, 36, 22, '#7E91A7');
if ([ 'terminal', 'vnc' ].includes(data.id)) {
text(s, data.title.replace('\n', ' '), 96, 190, 1728, 105, 64, '#FFFFFF', true);
text(s, data.body, 100, 307, 1700, 90, 31, '#AEC0D3');
await picture(s, locale, data.screenshots[0], 220, 408, 1480, 590);
} else if (data.id === 'charts') {
text(s, data.title.replace('\n', ' '), 96, 185, 1728, 110, 70, '#FFFFFF', true);
text(s, data.body, 100, 298, 1700, 76, 32, '#AEC0D3');
await picture(s, locale, 'chart', 96, 410, 1138, 590);
await picture(s, locale, 'chart-donut', 1284, 474, 540, 518);
} else if (data.id === 'dialogs') {
text(s, data.title.replace('\n', ' '), 96, 185, 1728, 110, 70, '#FFFFFF', true);
text(s, data.body, 100, 298, 1700, 76, 32, '#AEC0D3');
await picture(s, locale, 'dialog', 96, 427, 840, 525);
await picture(s, locale, 'drawer', 984, 427, 840, 525);
} else if (data.id === 'library') {
text(s, data.title, 96, 250, 490, 210, 62, '#FFFFFF', true);
text(s, data.body, 100, 525, 452, 180, 33, '#AEC0D3');
await picture(s, locale, 'icons', 700, 260, 1124, 703);
await picture(s, locale, 'buttons', 96, 795, 550, 174);
} else if (data.id === 'responsive') {
text(s, data.title, 96, 210, 640, 280, 60, '#FFFFFF', true);
text(s, data.body, 100, 535, 540, 155, 33, '#AEC0D3');
await picture(s, locale, 'login', 674, 394, 756, 472);
await picture(s, locale, 'mobile', 1460, 254, 363, 726);
} else {
text(s, data.title, 96, 251, 470, 300, 58, '#FFFFFF', true);
text(s, data.body, 100, 602, 449, 245, 32, '#AEC0D3');
await picture(s, locale, data.screenshots[0], 612, 218, 1212, 758);
}
text(s, 'AGON PARTNERS INNOVATION AG', 96, 1024, 790, 34, 19, '#7891A6');
text(s, ({
en: 'SHOWCASE · SAMPLE DATA',
de: 'SHOWCASE · BEISPIELDATEN',
fr: 'SHOWCASE · DONNÉES DE DÉMONSTRATION'
})[locale], 1120, 1024, 705, 34, 18, '#7891A6');
}
s.speakerNotes.textFrame.setText(`${narration[data.id]}\n\nSource: actual Agon Atrium production showcase, repository snapshot 2026-09-19. Screenshots: ${data.screenshots.join(', ')}. UI and business data are illustrative showcase fixtures. Opening artwork: AI-generated concept inspired by the user-provided great.png reference. Original Agon logo preserved. Narration is synthetic.`);
const rendered = await p.export({ slide: s, format: 'png', scale: 1 });
await fs.writeFile(path.join(tmp, `slides/${locale}-${String(i + 1).padStart(2, '0')}.png`), new Uint8Array(await rendered.arrayBuffer()));
const key = `${locale}-${String(i + 1).padStart(2, '0')}`;
const visual = JSON.stringify([ data.id, data.title, data.body, data.screenshots ]);
const assetInputs = [
sha(await fs.readFile(path.join(root, 'src/showcase/public/favicon.svg'))),
isCover ? sha(cover) : null
];
for (const name of data.screenshots) {
assetInputs.push([
name,
sha(await fs.readFile(path.join(source, `source/screenshots/${locale}-${name}.png`)))
]);
}
records[key] = {
visualHash: sha(visual),
imageHash: sha(await fs.readFile(path.join(tmp, `slides/${key}.png`))),
assetHash: sha(JSON.stringify(assetInputs))
};
console.log('Rendered', locale, i + 1);
}
const candidatePath = path.join(tmp, `${locale}-candidate.pptx`);
await (await PresentationFile.exportPptx(p)).save(candidatePath);
const finalPath = path.join(output, `agon-atrium-${locale}.pptx`);
await finalizePresentation({
workspaceDir: root,
candidatePath,
finalPath,
pythonExecutable: python,
integrityValidatorPath: path.join(skill, 'container_tools/inspect_presentation_package_integrity.py'),
layoutValidatorPath: path.join(skill, 'container_tools/inspect_presentation_layout_geometry.py'),
layoutArgs: [ '--expected-slide-size-emu', '18288000,10287000', '--validate-heading-fit' ],
fontPolicy: { basis: 'design', families: [ font ] },
verifyArtifactToolImport: true,
receiptPath: path.join(tmp, `${locale}-validation.json`)
});
if (args['update-artifacts']) {
const decks = path.join(source, 'artifacts/decks');
await fs.mkdir(decks, { recursive: true });
await fs.copyFile(finalPath, path.join(decks, `agon-atrium-${locale}.pptx`));
console.log('Updated deck', path.join(decks, `agon-atrium-${locale}.pptx`));
}
for (const [ key, record ] of Object.entries(records)) {
await fs.copyFile(path.join(tmp, `slides/${key}.png`), path.join(source, `source/slides/${key}.png`));
manifest[key] = record;
}
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
console.log('Finalized', locale, finalPath);
}