CODE · 8.5 KB
src/generate-sales-deck.mjs
Workspace snapshot · 09/04 13:45
import fs from 'node:fs';
import path from 'node:path';
import PptxGenJS from 'pptxgenjs';
import sharp from 'sharp';
import { PDFDocument } from 'pdf-lib';
const root = process.cwd();
const input = JSON.parse(fs.readFileSync(path.join(root, 'samples/sales-brief.json'), 'utf8'));
const out = path.join(root, 'artifacts', 'sales-pilot');
const previewDir = path.join(out, 'preview');
fs.mkdirSync(previewDir, { recursive: true });
const pptx = new PptxGenJS();
pptx.layout = 'LAYOUT_WIDE';
pptx.author = 'AI Slide Lab';
pptx.subject = input.purpose;
pptx.title = input.deckTitle;
pptx.company = 'AI Slide Lab';
pptx.lang = 'ja-JP';
pptx.theme = {
headFontFace: 'Yu Gothic', bodyFontFace: 'Yu Gothic', lang: 'ja-JP'
};
pptx.defineSlideMaster({
title: 'LAB', background: { color: 'F5F2EA' },
objects: [
{ line: { x: 0.55, y: 7.08, w: 12.23, h: 0, line: { color: 'CBC6B8', width: 0.7 } } },
{ text: { text: 'AI SLIDE LAB', options: { x: 0.58, y: 7.12, w: 2, h: 0.18, fontFace: 'Arial', fontSize: 5.8, color: '6D695F', charSpacing: 1.8, margin: 0 } } }
], slideNumber: { x: 12.35, y: 7.1, color: '6D695F', fontFace: 'Arial', fontSize: 6 }
});
const C = { bg:'#F5F2EA', ink:'#17211B', muted:'#646A64', green:'#0B6B4F', lime:'#CBEA9B', white:'#FFFFFF', line:'#CBC6B8', orange:'#F36F45' };
const esc = s => String(s).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>');
const tspan = (text, x, y, size, weight=400, fill=C.ink, anchor='start') => `<text x="${x}" y="${y}" font-family="Yu Gothic,Meiryo,sans-serif" font-size="${size}" font-weight="${weight}" fill="${fill}" text-anchor="${anchor}">${esc(text)}</text>`;
const wrap = (text, max) => { const a=[]; let s=''; for (const ch of text) { if ((s.length + 1) > max) { a.push(s); s=''; } s+=ch; } if(s)a.push(s); return a; };
const multiline = (text,x,y,size,max,leading,weight=400,fill=C.ink) => wrap(text,max).map((l,i)=>tspan(l,x,y+i*leading,size,weight,fill)).join('');
const baseSvg = (slide, idx, inner) => `<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900"><rect width="1600" height="900" fill="${C.bg}"/>${tspan(slide.eyebrow,72,72,15,700,C.green)}${inner}<line x1="70" y1="850" x2="1530" y2="850" stroke="${C.line}"/>${tspan('AI SLIDE LAB',72,875,10,700,C.muted)}${tspan(String(idx+1).padStart(2,'0'),1528,875,10,700,C.muted,'end')}</svg>`;
const previewBody = s => {
if (s.type === 'flow') return s.steps.map((it,i)=>{
const x=135+i*290;
return `<circle cx="${x}" cy="470" r="48" fill="${i===4?C.orange:C.green}"/>${tspan(String(i+1),x,482,24,700,C.white,'middle')}${multiline(it,x-65,560,18,8,25,700)}${i<4?`<path d="M ${x+72} 470 L ${x+174} 470" stroke="${C.line}" stroke-width="12"/><path d="M ${x+174} 470 l -22 -16 v 32 z" fill="${C.line}"/>`:''}`;
}).join('');
if (s.type === 'compare') return [[s.left,75,C.white],[s.right,815,C.lime]].map(([box,x,fill])=>`<rect x="${x}" y="370" width="650" height="250" rx="18" fill="${fill}" stroke="${C.line}"/>${tspan(box.label,x+35,425,24,700)}${box.items.map((it,i)=>tspan(`• ${it}`,x+42,485+i*43,18,600)).join('')}`).join('');
if (s.type === 'cta') return `<rect x="75" y="350" width="850" height="280" rx="20" fill="${C.ink}"/>${tspan(s.claim,115,415,25,700,C.white)}${s.items.map((it,i)=>tspan(`✓ ${it}`,120,485+i*45,18,600,C.lime)).join('')}<circle cx="1250" cy="490" r="135" fill="${C.lime}" stroke="${C.green}" stroke-width="3"/>${tspan('1 BRIEF',1250,455,25,700,C.ink,'middle')}${tspan('↓',1250,500,27,700,C.green,'middle')}${tspan('1 EDITABLE DECK',1250,550,18,700,C.ink,'middle')}`;
if (s.items) return s.items.map((it,i)=>`<rect x="${80+(i%2)*720}" y="${390+Math.floor(i/2)*100}" width="650" height="70" rx="12" fill="${i===1?C.lime:C.white}" stroke="${C.line}"/>${tspan('• '+it,105+(i%2)*720,435+Math.floor(i/2)*100,20,600)}`).join('');
return '';
};
const addText = (sl,text,x,y,w,h,size,color=C.ink,bold=false,align='left') => sl.addText(text,{x,y,w,h,fontFace:'Yu Gothic',fontSize:size,color:color.slice(1),bold,margin:0,breakLine:false,fit:'shrink',valign:'mid',align});
const header = (sl,s) => { addText(sl,s.eyebrow,.6,.38,4,.25,7,C.green,true); addText(sl,s.title,.6,.82,12.05,1.0,25,C.ink,true); addText(sl,s.body,.62,1.82,11.7,.55,11,C.muted); };
const addClaim = (sl,s) => { sl.addShape(pptx.ShapeType.roundRect,{x:.62,y:6.18,w:12.05,h:.6,rectRadius:.06,fill:{color:'17211B'},line:{color:'17211B'}}); addText(sl,s.claim,.9,6.31,11.5,.3,10,C.white,true); };
const renderers = {
cover(sl,s){ addText(sl,s.eyebrow,.65,.55,4,.3,8,C.green,true); addText(sl,s.title,.65,1.25,11.8,1.3,34,C.ink,true); addText(sl,s.body,.7,2.75,10.6,.6,15,C.muted); sl.addShape(pptx.ShapeType.arc,{x:8.65,y:3.25,w:3.5,h:3.5,adjustPoint:.4,rotate:18,fill:{color:'CBEA9B',transparency:10},line:{color:'0B6B4F',width:2}}); addText(sl,'NOTE → STORY → DECK',8.4,4.62,4,.4,11,C.ink,true,'center'); addClaim(sl,s); },
problem(sl,s){ header(sl,s); s.items.forEach((it,i)=>{ const y=2.65+i*.92; sl.addShape(pptx.ShapeType.roundRect,{x:.65,y,w:7.75,h:.65,fill:{color:i===1?'CBEA9B':'FFFFFF'},line:{color:'CBC6B8'}}); addText(sl,`0${i+1}`,.9,y+.14,.5,.25,8,C.green,true); addText(sl,it,1.55,y+.08,6.5,.42,14,C.ink,true); }); sl.addShape(pptx.ShapeType.rect,{x:9.15,y:2.55,w:3.15,h:2.85,fill:{color:'17211B'},line:{color:'17211B'}}); addText(sl,'BEFORE\nMEETING',9.55,3.04,2.4,.7,20,C.white,true,'center'); addText(sl,'情報を集める\n↓\n順序を決める\n↓\n見た目を整える',9.55,3.9,2.35,1.2,10,C.lime,false,'center'); addClaim(sl,s); },
flow(sl,s){ header(sl,s); s.steps.forEach((it,i)=>{ const x=.65+i*2.48; sl.addShape(pptx.ShapeType.ellipse,{x,y:3.0,w:1.0,h:1.0,fill:{color:i===4?'F36F45':'0B6B4F'},line:{color:i===4?'F36F45':'0B6B4F'}}); addText(sl,String(i+1),x,3.22,1,.35,17,C.white,true,'center'); addText(sl,it,x-.42,4.2,1.85,.7,10,C.ink,true,'center'); if(i<4) sl.addShape(pptx.ShapeType.chevron,{x:x+1.28,y:3.32,w:.55,h:.35,fill:{color:'CBC6B8'},line:{color:'CBC6B8'}}); }); addClaim(sl,s); },
compare(sl,s){ header(sl,s); [[s.left,.65,'FFFFFF'],[s.right,6.82,'CBEA9B']].forEach(([box,x,fill])=>{ sl.addShape(pptx.ShapeType.roundRect,{x,y:2.6,w:5.85,h:2.95,fill:{color:fill},line:{color:'CBC6B8'}}); addText(sl,box.label,x+.35,2.9,5.1,.4,15,C.ink,true); box.items.forEach((it,i)=>addText(sl,`• ${it}`,x+.4,3.6+i*.53,4.95,.35,11,C.ink)); }); addClaim(sl,s); },
pilot(sl,s){ header(sl,s); s.items.forEach((it,i)=>{ const x=.65+(i%2)*6.15, y=2.75+Math.floor(i/2)*1.25; sl.addShape(pptx.ShapeType.roundRect,{x,y,w:5.65,h:.88,fill:{color:'FFFFFF'},line:{color:'CBC6B8'}}); addText(sl,`0${i+1}`,x+.25,y+.24,.55,.3,9,C.orange,true); addText(sl,it,x+.95,y+.16,4.35,.48,12,C.ink,true); }); addClaim(sl,s); },
cta(sl,s){ header(sl,s); sl.addShape(pptx.ShapeType.roundRect,{x:.65,y:2.7,w:8.0,h:2.7,fill:{color:'17211B'},line:{color:'17211B'}}); addText(sl,s.claim,1.05,3.2,7.2,.65,20,C.white,true); s.items.forEach((it,i)=>addText(sl,`✓ ${it}`,1.08,4.1+i*.38,6.8,.28,9,C.lime)); sl.addShape(pptx.ShapeType.ellipse,{x:9.55,y:2.75,w:2.65,h:2.65,fill:{color:'CBEA9B'},line:{color:'0B6B4F',width:2}}); addText(sl,'1 brief\n→\n1 editable\ndeck',9.85,3.25,2.05,1.6,16,C.ink,true,'center'); addClaim(sl,s); }
};
const svgSlides=[];
for (const [idx,s] of input.slides.entries()) {
const sl=pptx.addSlide('LAB'); renderers[s.type](sl,s); sl.addNotes(`Source: ${input.sourceNote}\nClaim: ${s.claim}`);
let inner = multiline(s.title,72,150,42,26,56,700)+multiline(s.body,76,255,21,48,32,400,C.muted);
inner += previewBody(s);
inner += `<rect x="75" y="730" width="1450" height="72" rx="12" fill="${C.ink}"/>${tspan(s.claim,110,775,20,700,C.white)}`;
svgSlides.push(baseSvg(s,idx,inner));
}
await pptx.writeFile({ fileName:path.join(out,'ai-slide-lab-sales-pilot.pptx') });
const pdf=await PDFDocument.create();
for (let i=0;i<svgSlides.length;i++) {
const png=await sharp(Buffer.from(svgSlides[i])).png().toBuffer();
const pngPath=path.join(previewDir,`slide-${String(i+1).padStart(2,'0')}.png`); fs.writeFileSync(pngPath,png);
const img=await pdf.embedPng(png); const page=pdf.addPage([1600,900]); page.drawImage(img,{x:0,y:0,width:1600,height:900});
}
fs.writeFileSync(path.join(out,'ai-slide-lab-sales-pilot.pdf'),await pdf.save());
fs.writeFileSync(path.join(out,'manifest.json'),JSON.stringify({title:input.deckTitle,purpose:input.purpose,audience:input.audience,slides:input.slides.map((s,i)=>({page:i+1,type:s.type,claim:s.claim,source:input.sourceNote})),outputs:['ai-slide-lab-sales-pilot.pptx','ai-slide-lab-sales-pilot.pdf','preview/*.png']},null,2));
console.log(`Generated ${input.slides.length} editable slides in ${out}`);