Custom Skills (ourdigital-custom-skills/): - 00-ourdigital-visual-storytelling: Blog featured image prompt generator - 01-ourdigital-research-publisher: Research-to-publication workflow - 02-notion-organizer: Notion workspace management - 03-research-to-presentation: Notion research to PPT/Figma - 04-seo-gateway-strategist: SEO gateway page strategy planning - 05-gateway-page-content-builder: Gateway page content generation - 20-jamie-brand-editor: Jamie Clinic branded content GENERATION - 21-jamie-brand-guardian: Jamie Clinic content REVIEW & evaluation Refinements applied: - All skills converted to SKILL.md format with YAML frontmatter - Added version fields to all skills - Flattened nested folder structures - Removed packaging artifacts (.zip, .skill files) - Reorganized file structures (scripts/, references/, etc.) - Differentiated Jamie skills with clear roles 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
474 lines
13 KiB
JavaScript
474 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Generate PowerPoint presentation from synthesis data
|
|
* Uses pptxgenjs library to create professional presentations
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Note: In production, install via npm install pptxgenjs
|
|
// For this example, we'll show the structure
|
|
let PptxGenJS;
|
|
try {
|
|
PptxGenJS = require('pptxgenjs');
|
|
} catch (e) {
|
|
console.log('📦 Installing pptxgenjs... Run: npm install pptxgenjs');
|
|
process.exit(1);
|
|
}
|
|
|
|
class PresentationGenerator {
|
|
constructor(synthesisData) {
|
|
this.synthesis = synthesisData;
|
|
this.pptx = new PptxGenJS();
|
|
this.setupPresentation();
|
|
this.defineLayouts();
|
|
}
|
|
|
|
setupPresentation() {
|
|
// Set presentation properties
|
|
this.pptx.layout = 'LAYOUT_16x9';
|
|
this.pptx.author = this.synthesis.metadata.author || 'Research Team';
|
|
this.pptx.company = 'Generated by Research-to-Presentation';
|
|
this.pptx.title = this.synthesis.metadata.title || 'Research Presentation';
|
|
|
|
// Define theme colors
|
|
this.colors = {
|
|
primary: '#1a73e8',
|
|
secondary: '#34a853',
|
|
accent: '#ea4335',
|
|
dark: '#202124',
|
|
light: '#f8f9fa',
|
|
text: '#3c4043',
|
|
subtext: '#5f6368'
|
|
};
|
|
|
|
// Define font styles
|
|
this.fonts = {
|
|
title: { face: 'Arial', size: 44, bold: true },
|
|
heading: { face: 'Arial', size: 32, bold: true },
|
|
subheading: { face: 'Arial', size: 24, bold: false },
|
|
body: { face: 'Arial', size: 18 },
|
|
small: { face: 'Arial', size: 14 }
|
|
};
|
|
}
|
|
|
|
defineLayouts() {
|
|
// Define master slides/layouts
|
|
this.pptx.defineSlideMaster({
|
|
title: 'CUSTOM_LAYOUT',
|
|
background: { color: this.colors.light },
|
|
objects: [
|
|
// Footer with slide number
|
|
{
|
|
text: {
|
|
text: '[[slideNumber]]',
|
|
options: {
|
|
x: 8.5,
|
|
y: 5,
|
|
w: 1,
|
|
h: 0.4,
|
|
fontSize: 12,
|
|
color: this.colors.subtext
|
|
}
|
|
}
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
generate() {
|
|
console.log('🎨 Generating slides...');
|
|
|
|
this.synthesis.slide_plan.forEach(slide => {
|
|
switch(slide.type) {
|
|
case 'title':
|
|
this.createTitleSlide(slide);
|
|
break;
|
|
case 'executive_summary':
|
|
this.createExecutiveSummarySlide(slide);
|
|
break;
|
|
case 'agenda':
|
|
this.createAgendaSlide(slide);
|
|
break;
|
|
case 'content':
|
|
this.createContentSlide(slide);
|
|
break;
|
|
case 'data_visualization':
|
|
this.createDataSlide(slide);
|
|
break;
|
|
case 'recommendations':
|
|
this.createRecommendationsSlide(slide);
|
|
break;
|
|
case 'closing':
|
|
this.createClosingSlide(slide);
|
|
break;
|
|
default:
|
|
this.createContentSlide(slide);
|
|
}
|
|
});
|
|
|
|
return this.pptx;
|
|
}
|
|
|
|
createTitleSlide(slideData) {
|
|
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
|
|
|
|
// Add background gradient
|
|
slide.background = {
|
|
fill: {
|
|
type: 'gradient',
|
|
colors: [
|
|
{ color: this.colors.primary, position: 0 },
|
|
{ color: this.colors.secondary, position: 100 }
|
|
]
|
|
}
|
|
};
|
|
|
|
// Title
|
|
slide.addText(slideData.title, {
|
|
x: 0.5,
|
|
y: 2,
|
|
w: 9,
|
|
h: 1.5,
|
|
fontSize: 44,
|
|
bold: true,
|
|
color: 'FFFFFF',
|
|
align: 'center'
|
|
});
|
|
|
|
// Subtitle
|
|
if (slideData.subtitle) {
|
|
slide.addText(slideData.subtitle, {
|
|
x: 0.5,
|
|
y: 3.5,
|
|
w: 9,
|
|
h: 0.75,
|
|
fontSize: 24,
|
|
color: 'FFFFFF',
|
|
align: 'center'
|
|
});
|
|
}
|
|
|
|
// Speaker notes
|
|
if (slideData.speaker_notes) {
|
|
slide.addNotes(slideData.speaker_notes);
|
|
}
|
|
}
|
|
|
|
createExecutiveSummarySlide(slideData) {
|
|
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
|
|
|
|
// Title
|
|
slide.addText(slideData.title, {
|
|
x: 0.5,
|
|
y: 0.5,
|
|
w: 9,
|
|
h: 0.75,
|
|
...this.fonts.heading,
|
|
color: this.colors.primary
|
|
});
|
|
|
|
// Summary content
|
|
slide.addText(slideData.content, {
|
|
x: 0.5,
|
|
y: 1.5,
|
|
w: 9,
|
|
h: 1.5,
|
|
...this.fonts.body,
|
|
color: this.colors.text
|
|
});
|
|
|
|
// Key points
|
|
if (slideData.key_points && slideData.key_points.length > 0) {
|
|
const bulletPoints = slideData.key_points.map(point => ({
|
|
text: point,
|
|
options: { bullet: true }
|
|
}));
|
|
|
|
slide.addText(bulletPoints, {
|
|
x: 0.5,
|
|
y: 3.25,
|
|
w: 9,
|
|
h: 2,
|
|
...this.fonts.body,
|
|
color: this.colors.text,
|
|
bullet: { type: 'circle' }
|
|
});
|
|
}
|
|
|
|
slide.addNotes(slideData.speaker_notes || '');
|
|
}
|
|
|
|
createAgendaSlide(slideData) {
|
|
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
|
|
|
|
// Title
|
|
slide.addText(slideData.title, {
|
|
x: 0.5,
|
|
y: 0.5,
|
|
w: 9,
|
|
h: 0.75,
|
|
...this.fonts.heading,
|
|
color: this.colors.primary
|
|
});
|
|
|
|
// Agenda items
|
|
const agendaItems = slideData.items.map((item, index) => ({
|
|
text: `${index + 1}. ${item}`,
|
|
options: {
|
|
fontSize: 20,
|
|
bullet: false,
|
|
indentLevel: 0
|
|
}
|
|
}));
|
|
|
|
slide.addText(agendaItems, {
|
|
x: 1,
|
|
y: 1.5,
|
|
w: 8,
|
|
h: 3.5,
|
|
color: this.colors.text,
|
|
lineSpacing: 32
|
|
});
|
|
|
|
// Duration footer
|
|
if (slideData.total_duration) {
|
|
slide.addText(`Total Duration: ${slideData.total_duration} minutes`, {
|
|
x: 0.5,
|
|
y: 5,
|
|
w: 4,
|
|
h: 0.4,
|
|
...this.fonts.small,
|
|
color: this.colors.subtext
|
|
});
|
|
}
|
|
|
|
slide.addNotes(slideData.speaker_notes || '');
|
|
}
|
|
|
|
createContentSlide(slideData) {
|
|
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
|
|
|
|
// Title
|
|
slide.addText(slideData.title, {
|
|
x: 0.5,
|
|
y: 0.5,
|
|
w: 9,
|
|
h: 0.75,
|
|
...this.fonts.heading,
|
|
color: this.colors.primary
|
|
});
|
|
|
|
// Determine layout based on content
|
|
const hasData = slideData.data && slideData.data.length > 0;
|
|
const contentWidth = hasData ? 5 : 9;
|
|
|
|
// Bullet points
|
|
if (slideData.bullets && slideData.bullets.length > 0) {
|
|
const bulletPoints = slideData.bullets.map(point => ({
|
|
text: point,
|
|
options: { bullet: true }
|
|
}));
|
|
|
|
slide.addText(bulletPoints, {
|
|
x: 0.5,
|
|
y: 1.5,
|
|
w: contentWidth,
|
|
h: 3.5,
|
|
...this.fonts.body,
|
|
color: this.colors.text,
|
|
bullet: { type: 'circle' },
|
|
lineSpacing: 24
|
|
});
|
|
}
|
|
|
|
// Data table if present
|
|
if (hasData) {
|
|
const tableData = [
|
|
['Metric', 'Value'],
|
|
...slideData.data.map(d => [d.metric, d.value])
|
|
];
|
|
|
|
slide.addTable(tableData, {
|
|
x: 6,
|
|
y: 1.5,
|
|
w: 3.5,
|
|
h: 2,
|
|
fontSize: 14,
|
|
border: { pt: 1, color: this.colors.secondary },
|
|
fill: { color: this.colors.light }
|
|
});
|
|
}
|
|
|
|
slide.addNotes(slideData.speaker_notes || '');
|
|
}
|
|
|
|
createDataSlide(slideData) {
|
|
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
|
|
|
|
// Title
|
|
slide.addText(slideData.title, {
|
|
x: 0.5,
|
|
y: 0.5,
|
|
w: 9,
|
|
h: 0.75,
|
|
...this.fonts.heading,
|
|
color: this.colors.primary
|
|
});
|
|
|
|
// Create data table
|
|
if (slideData.data_points && slideData.data_points.length > 0) {
|
|
const tableData = [
|
|
['Source', 'Metric', 'Value'],
|
|
...slideData.data_points.map(dp => [
|
|
dp.source || '',
|
|
dp.metric,
|
|
dp.value
|
|
])
|
|
];
|
|
|
|
slide.addTable(tableData, {
|
|
x: 0.5,
|
|
y: 1.5,
|
|
w: 9,
|
|
h: 3.5,
|
|
fontSize: 16,
|
|
border: { pt: 1, color: this.colors.secondary },
|
|
fill: { color: this.colors.light },
|
|
rowH: 0.5
|
|
});
|
|
}
|
|
|
|
slide.addNotes(slideData.speaker_notes || '');
|
|
}
|
|
|
|
createRecommendationsSlide(slideData) {
|
|
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
|
|
|
|
// Title
|
|
slide.addText(slideData.title, {
|
|
x: 0.5,
|
|
y: 0.5,
|
|
w: 9,
|
|
h: 0.75,
|
|
...this.fonts.heading,
|
|
color: this.colors.primary
|
|
});
|
|
|
|
// Recommendations as numbered list
|
|
const recommendations = slideData.items.map((item, index) => ({
|
|
text: `${index + 1}. ${item}`,
|
|
options: {
|
|
fontSize: 20,
|
|
bullet: false
|
|
}
|
|
}));
|
|
|
|
slide.addText(recommendations, {
|
|
x: 0.5,
|
|
y: 1.5,
|
|
w: 9,
|
|
h: 3.5,
|
|
color: this.colors.text,
|
|
lineSpacing: 28
|
|
});
|
|
|
|
slide.addNotes(slideData.speaker_notes || '');
|
|
}
|
|
|
|
createClosingSlide(slideData) {
|
|
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
|
|
|
|
// Gradient background
|
|
slide.background = {
|
|
fill: {
|
|
type: 'gradient',
|
|
colors: [
|
|
{ color: this.colors.secondary, position: 0 },
|
|
{ color: this.colors.primary, position: 100 }
|
|
]
|
|
}
|
|
};
|
|
|
|
// Thank you text
|
|
slide.addText(slideData.title, {
|
|
x: 0.5,
|
|
y: 2,
|
|
w: 9,
|
|
h: 1,
|
|
fontSize: 48,
|
|
bold: true,
|
|
color: 'FFFFFF',
|
|
align: 'center'
|
|
});
|
|
|
|
// Subtitle
|
|
if (slideData.subtitle) {
|
|
slide.addText(slideData.subtitle, {
|
|
x: 0.5,
|
|
y: 3.5,
|
|
w: 9,
|
|
h: 0.75,
|
|
fontSize: 28,
|
|
color: 'FFFFFF',
|
|
align: 'center'
|
|
});
|
|
}
|
|
|
|
slide.addNotes(slideData.speaker_notes || '');
|
|
}
|
|
|
|
async save(outputPath) {
|
|
try {
|
|
await this.pptx.writeFile({ fileName: outputPath });
|
|
console.log(`✅ Presentation saved: ${outputPath}`);
|
|
} catch (error) {
|
|
console.error(`❌ Error saving presentation: ${error}`);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Main execution
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.length < 2) {
|
|
console.log('Usage: node generate_pptx.js <synthesis.json> <output.pptx>');
|
|
process.exit(1);
|
|
}
|
|
|
|
const synthesisFile = args[0];
|
|
const outputFile = args[1];
|
|
|
|
console.log(`📊 Loading synthesis from: ${synthesisFile}`);
|
|
|
|
try {
|
|
// Load synthesis data
|
|
const synthesisData = JSON.parse(
|
|
fs.readFileSync(synthesisFile, 'utf8')
|
|
);
|
|
|
|
// Generate presentation
|
|
const generator = new PresentationGenerator(synthesisData);
|
|
const presentation = generator.generate();
|
|
|
|
// Save to file
|
|
await generator.save(outputFile);
|
|
|
|
console.log(`📈 Created ${synthesisData.slide_plan.length} slides`);
|
|
|
|
} catch (error) {
|
|
console.error(`❌ Error: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run if called directly
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = { PresentationGenerator };
|