Download apps/api/src/services/renderers/pptx-renderer.ts from safetrack/edtech: direct link, hf CLI and curl.
- Browser
- Download file 6.93 kB
-
https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/api/src/services/renderers/pptx-renderer.ts
- Command line
-
hf download hf://spaces/safetrack/edtech/apps/api/src/services/renderers/pptx-renderer.ts
-
curl -L -o pptx-renderer.ts https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/api/src/services/renderers/pptx-renderer.ts
6.93 kB
| import { logger } from '../../logger'; | |
| import PptxGenJS from 'pptxgenjs'; | |
| import { PitchDeckData, SlideData } from '@repo/ai-sdk'; | |
| import { DocumentRenderer } from './types'; | |
| export class PptxDeckRenderer implements DocumentRenderer<PitchDeckData> { | |
| async render(data: PitchDeckData): Promise<Buffer> { | |
| const pres = new PptxGenJS(); | |
| // 🎨 Premium Design System | |
| // XAMLÉ Palette: Emerald (#1C7C54), Navy (#1B3A57), Saffron (#F4A261) | |
| pres.defineSlideMaster({ | |
| title: 'MASTER_SLIDE', | |
| bkgd: 'FFFFFF', | |
| objects: [ | |
| { rect: { x: 0, y: 0, w: '100%', h: 1.0, fill: { color: '1B3A57' } } }, // Header | |
| { rect: { x: 0, y: 1.0, w: '100%', h: 0.05, fill: { color: 'F4A261' } } }, // Accent line | |
| { text: { text: "XAMLÉ 🇸🇳 - Pitch Deck Stratégique", options: { x: 0.5, y: 5.15, fontSize: 10, color: '1B3A57' } } } | |
| ] | |
| }); | |
| // 1. Title Slide (Slide 1) | |
| const titleSlide = pres.addSlide(); | |
| titleSlide.bkgd = '1B3A57'; | |
| titleSlide.addText(data.title.toUpperCase(), { | |
| x: 0, y: 2, w: '100%', h: 1, | |
| fontSize: 48, color: 'FFFFFF', bold: true, fontFace: 'Montserrat', align: 'center' | |
| }); | |
| if (data.subtitle) { | |
| titleSlide.addText(data.subtitle, { | |
| x: 0, y: 3.2, w: '100%', h: 1, | |
| fontSize: 22, color: 'F4A261', fontFace: 'Inter', align: 'center', italic: true | |
| }); | |
| } | |
| titleSlide.addText("Propulsé par XAMLÉ AI", { | |
| x: 0, y: 4.8, w: '100%', h: 0.5, | |
| fontSize: 14, color: 'FFFFFF', fontFace: 'Inter', align: 'center' | |
| }); | |
| // 2. Content Slides | |
| data.slides.forEach((slideData: SlideData, index: number) => { | |
| const slide = pres.addSlide({ masterName: 'MASTER_SLIDE' }); | |
| // Slide Title (Montserrat Bold, White on Navy) | |
| slide.addText(slideData.title.toUpperCase(), { | |
| x: 0.5, y: 0.25, w: '90%', h: 0.5, | |
| fontSize: 28, color: 'FFFFFF', bold: true, fontFace: 'Montserrat' | |
| }); | |
| const hasVisual = slideData.visualType && slideData.visualType !== 'NONE' && slideData.visualData; | |
| const textWidth = hasVisual ? '50%' : '90%'; | |
| // 📝 Storytelling Text Blocks (Inter) | |
| const textBlocks = slideData.content.map((text: string) => ({ text: text + '\n', options: { breakLine: true } })); | |
| slide.addText(textBlocks, { | |
| x: 0.5, y: 1.3, w: textWidth, h: '75%', | |
| fontSize: 15, color: '2D3748', fontFace: 'Inter', | |
| valign: 'top', margin: 5, lineSpacing: 22 | |
| }); | |
| // 📊 Graphical Integration (WOW Effect - Flat Design) | |
| if (hasVisual) { | |
| try { | |
| const vizData = slideData.visualData; | |
| if (slideData.visualType === 'PIE_CHART' && vizData.labels && vizData.values) { | |
| const chartData = [{ name: 'Market Scale', labels: vizData.labels, values: vizData.values }]; | |
| slide.addChart(pres.ChartType.pie, chartData, { | |
| x: 5.2, y: 1.2, w: 4.5, h: 4.0, | |
| showLegend: true, legendPos: 'r', | |
| showValue: true, showPercent: true, | |
| chartColors: ['1C7C54', 'F4A261', '1B3A57'], | |
| dataLabelColor: 'FFFFFF' | |
| }); | |
| } | |
| else if (slideData.visualType === 'BAR_CHART' && vizData.labels && vizData.values) { | |
| const chartData = [{ name: 'Projections (FCFA)', labels: vizData.labels, values: vizData.values }]; | |
| slide.addChart(pres.ChartType.bar, chartData, { | |
| x: 5.2, y: 1.2, w: 4.5, h: 4.0, | |
| barDir: 'col', | |
| showValue: true, | |
| chartColors: ['1C7C54', 'F4A261', '1B3A57'], | |
| dataLabelColor: '2D3748', | |
| catAxisLabelColor: '2D3748', | |
| valAxisLabelColor: '2D3748' | |
| }); | |
| } | |
| else if (slideData.visualType === 'IMAGE' && typeof vizData === 'string' && vizData.startsWith('http')) { | |
| slide.addImage({ path: vizData, x: 5.2, y: 1.0, w: 4.5, h: 4.2 }); | |
| } | |
| else if (slideData.visualType === 'IMAGE') { | |
| slide.addText("📷 [Visual AI Placeholder]", { x: 6.5, y: 2.5, fontSize: 14, color: 'A0AEC0', fontFace: 'Inter' }); | |
| } | |
| else if (slideData.visualType === 'TEAM' && Array.isArray(vizData)) { | |
| const maxMembers = Math.min(vizData.length, 4); | |
| for (let i = 0; i < maxMembers; i++) { | |
| const member = vizData[i]; | |
| const row = Math.floor(i / 2); | |
| const col = i % 2; | |
| const xP = 5.2 + (col * 2.3); | |
| const yP = 1.0 + (row * 2.2); | |
| if (member.photoUrl && typeof member.photoUrl === 'string' && member.photoUrl.startsWith('http')) { | |
| slide.addImage({ path: member.photoUrl, x: xP, y: yP, w: 1.8, h: 1.8, sizing: { type: 'cover', w: 1.8, h: 1.8 } }); | |
| } else { | |
| // Default icon or placeholder shape | |
| slide.addShape(pres.ShapeType.ellipse, { x: xP + 0.15, y: yP + 0.15, w: 1.5, h: 1.5, fill: { color: 'E2E8F0' } }); | |
| } | |
| slide.addText(member.name?.toUpperCase() || "MEMBRE", { | |
| x: xP, y: yP + 1.85, w: 1.8, h: 0.3, | |
| fontSize: 12, color: '1B3A57', bold: true, align: 'center', fontFace: 'Montserrat' | |
| }); | |
| slide.addText(member.role || "Rôle", { | |
| x: xP, y: yP + 2.15, w: 1.8, h: 0.2, | |
| fontSize: 10, color: '1C7C54', align: 'center', fontFace: 'Inter' | |
| }); | |
| } | |
| } | |
| } catch (vErr) { | |
| logger.warn(`[RENDERER] Failed to add visual to slide ${index + 1}:`, vErr); | |
| } | |
| } | |
| // Speaker Notes | |
| if (slideData.notes) { | |
| slide.addNotes(slideData.notes); | |
| } | |
| }); | |
| const buffer = await pres.write({ outputType: 'nodebuffer' }) as Buffer; | |
| return buffer; | |
| } | |
| } | |