Initial commit
This commit is contained in:
370
skills/presentation-master/scripts/analyze-context.js
Normal file
370
skills/presentation-master/scripts/analyze-context.js
Normal file
@@ -0,0 +1,370 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Presentation Context Analyzer
|
||||
*
|
||||
* Analyzes topic, audience, duration, purpose to detect presentation type
|
||||
* and recommend appropriate story framework.
|
||||
*
|
||||
* Usage:
|
||||
* bun analyze-context.js --topic "Cybersecurity Trends" --audience "School Board" --duration "15min" --purpose "inform"
|
||||
*
|
||||
* Output: JSON with presentation type, recommended framework, pattern distribution
|
||||
*/
|
||||
|
||||
const PRESENTATION_TYPES = {
|
||||
BOARD_UPDATE: {
|
||||
name: 'board-update',
|
||||
indicators: ['board', 'executive', 'update', 'quarterly', 'status', 'report'],
|
||||
characteristics: {
|
||||
audience_technical: 'low',
|
||||
focus: 'data-driven',
|
||||
primary_goal: 'inform + recommend',
|
||||
typical_duration: '15-20min'
|
||||
},
|
||||
pattern_distribution: {
|
||||
'data-viz': 0.40,
|
||||
'visual-caption': 0.30,
|
||||
'transition': 0.20,
|
||||
'title': 0.10
|
||||
},
|
||||
recommended_frameworks: ['classic-three-act', 'rule-of-three']
|
||||
},
|
||||
|
||||
KEYNOTE: {
|
||||
name: 'keynote',
|
||||
indicators: ['keynote', 'conference', 'inspire', 'vision', 'future', 'transform'],
|
||||
characteristics: {
|
||||
audience_technical: 'mixed',
|
||||
focus: 'inspirational',
|
||||
primary_goal: 'shift perspective',
|
||||
typical_duration: '20-30min'
|
||||
},
|
||||
pattern_distribution: {
|
||||
'big-idea': 0.40,
|
||||
'visual-caption': 0.30,
|
||||
'title': 0.20,
|
||||
'transition': 0.10
|
||||
},
|
||||
recommended_frameworks: ['sparkline', 'rule-of-three', 'ted']
|
||||
},
|
||||
|
||||
TRAINING: {
|
||||
name: 'training',
|
||||
indicators: ['training', 'workshop', 'tutorial', 'learn', 'how-to', 'guide'],
|
||||
characteristics: {
|
||||
audience_technical: 'variable',
|
||||
focus: 'educational',
|
||||
primary_goal: 'teach + practice',
|
||||
typical_duration: '30-60min'
|
||||
},
|
||||
pattern_distribution: {
|
||||
'process': 0.40,
|
||||
'data-viz': 0.30,
|
||||
'visual-caption': 0.20,
|
||||
'title': 0.10
|
||||
},
|
||||
recommended_frameworks: ['classic-three-act']
|
||||
},
|
||||
|
||||
PITCH: {
|
||||
name: 'pitch',
|
||||
indicators: ['pitch', 'proposal', 'investment', 'funding', 'sell', 'convince'],
|
||||
characteristics: {
|
||||
audience_technical: 'mixed',
|
||||
focus: 'persuasive',
|
||||
primary_goal: 'convince + action',
|
||||
typical_duration: '10-20min'
|
||||
},
|
||||
pattern_distribution: {
|
||||
'data-viz': 0.30,
|
||||
'big-idea': 0.30,
|
||||
'visual-caption': 0.25,
|
||||
'title-transition': 0.15
|
||||
},
|
||||
recommended_frameworks: ['rule-of-three', 'classic-three-act']
|
||||
},
|
||||
|
||||
TED_STYLE: {
|
||||
name: 'ted-style',
|
||||
indicators: ['ted', 'idea', 'story', 'personal', 'change the world'],
|
||||
characteristics: {
|
||||
audience_technical: 'general',
|
||||
focus: 'idea-centric',
|
||||
primary_goal: 'inspire + spread idea',
|
||||
typical_duration: '15-20min'
|
||||
},
|
||||
pattern_distribution: {
|
||||
'big-idea': 0.35,
|
||||
'visual-caption': 0.35,
|
||||
'transition': 0.20,
|
||||
'title': 0.10
|
||||
},
|
||||
recommended_frameworks: ['ted']
|
||||
}
|
||||
};
|
||||
|
||||
const STORY_FRAMEWORKS = {
|
||||
'sparkline': {
|
||||
name: "Nancy Duarte's Sparkline",
|
||||
duration: '20-30min',
|
||||
slides: '18-25',
|
||||
best_for: ['keynote', 'ted-style'],
|
||||
structure: 'Alternate "what is" (reality) with "what could be" (aspiration)',
|
||||
complexity: 'high'
|
||||
},
|
||||
|
||||
'rule-of-three': {
|
||||
name: "Steve Jobs' Rule of Three",
|
||||
duration: '15-30min',
|
||||
slides: '12-18',
|
||||
best_for: ['keynote', 'pitch', 'board-update'],
|
||||
structure: 'Three main sections, three points per section',
|
||||
complexity: 'medium'
|
||||
},
|
||||
|
||||
'ted': {
|
||||
name: 'TED Talk Structure',
|
||||
duration: '15-20min',
|
||||
slides: '12-18',
|
||||
best_for: ['ted-style', 'keynote'],
|
||||
structure: 'Hook → Personal → Core Idea → Call to Action → Close',
|
||||
complexity: 'medium'
|
||||
},
|
||||
|
||||
'classic-three-act': {
|
||||
name: 'Classic Three-Act Structure',
|
||||
duration: '20-45min',
|
||||
slides: '15-35',
|
||||
best_for: ['training', 'board-update', 'pitch'],
|
||||
structure: 'Setup (25%) → Confrontation (50%) → Resolution (25%)',
|
||||
complexity: 'low'
|
||||
}
|
||||
};
|
||||
|
||||
function parseDuration(durationStr) {
|
||||
// Parse strings like "15min", "20 minutes", "1 hour", "45m"
|
||||
const matches = durationStr.toLowerCase().match(/(\d+)\s*(min|minute|minutes|m|hour|hours|h)/);
|
||||
if (!matches) return null;
|
||||
|
||||
const value = parseInt(matches[1]);
|
||||
const unit = matches[2];
|
||||
|
||||
if (unit.startsWith('h')) {
|
||||
return value * 60; // convert to minutes
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function detectPresentationType(topic, audience, purpose) {
|
||||
const combined = `${topic} ${audience} ${purpose}`.toLowerCase();
|
||||
const scores = {};
|
||||
|
||||
for (const [key, type] of Object.entries(PRESENTATION_TYPES)) {
|
||||
let score = 0;
|
||||
|
||||
// Check for indicator keywords
|
||||
for (const indicator of type.indicators) {
|
||||
if (combined.includes(indicator)) {
|
||||
score += 10;
|
||||
}
|
||||
}
|
||||
|
||||
scores[type.name] = score;
|
||||
}
|
||||
|
||||
// Find highest scoring type
|
||||
const sortedTypes = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
||||
|
||||
if (sortedTypes[0][1] === 0) {
|
||||
// No clear match, default based on purpose
|
||||
if (purpose.includes('teach') || purpose.includes('train')) return 'training';
|
||||
if (purpose.includes('inspire') || purpose.includes('motivate')) return 'keynote';
|
||||
if (purpose.includes('convince') || purpose.includes('sell')) return 'pitch';
|
||||
return 'board-update'; // conservative default
|
||||
}
|
||||
|
||||
return sortedTypes[0][0];
|
||||
}
|
||||
|
||||
function recommendFrameworks(presentationType, durationMinutes) {
|
||||
const type = Object.values(PRESENTATION_TYPES).find(t => t.name === presentationType);
|
||||
if (!type) return [];
|
||||
|
||||
// Get recommended frameworks for this type
|
||||
const recommendations = type.recommended_frameworks.map(fw => {
|
||||
const framework = STORY_FRAMEWORKS[fw];
|
||||
|
||||
// Parse framework duration range
|
||||
const durationMatch = framework.duration.match(/(\d+)-(\d+)min/);
|
||||
if (!durationMatch) return { ...framework, key: fw, fit_score: 50 };
|
||||
|
||||
const minDuration = parseInt(durationMatch[1]);
|
||||
const maxDuration = parseInt(durationMatch[2]);
|
||||
|
||||
// Score based on duration fit
|
||||
let fit_score = 50;
|
||||
if (durationMinutes >= minDuration && durationMinutes <= maxDuration) {
|
||||
fit_score = 100;
|
||||
} else if (durationMinutes < minDuration) {
|
||||
fit_score = Math.max(0, 100 - ((minDuration - durationMinutes) * 5));
|
||||
} else {
|
||||
fit_score = Math.max(0, 100 - ((durationMinutes - maxDuration) * 5));
|
||||
}
|
||||
|
||||
return {
|
||||
key: fw,
|
||||
...framework,
|
||||
fit_score
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by fit score
|
||||
return recommendations.sort((a, b) => b.fit_score - a.fit_score);
|
||||
}
|
||||
|
||||
function estimateSlideCount(durationMinutes, presentationType) {
|
||||
// Guy Kawasaki: 10 slides for 20 minutes = 0.5 slides/min
|
||||
// Adjust based on presentation type
|
||||
|
||||
const rates = {
|
||||
'board-update': 0.5, // More data-heavy, slower pace
|
||||
'keynote': 0.7, // More visuals, faster pace
|
||||
'training': 0.4, // More explanation needed
|
||||
'pitch': 0.6, // Balanced
|
||||
'ted-style': 0.8 // Fast-paced, visual
|
||||
};
|
||||
|
||||
const rate = rates[presentationType] || 0.5;
|
||||
const baseCount = Math.round(durationMinutes * rate);
|
||||
|
||||
// Kawasaki's hard limit: max 10 core concepts
|
||||
// But we can have transition/title slides
|
||||
const maxCoreSlides = 10;
|
||||
const coreSlides = Math.min(baseCount, maxCoreSlides);
|
||||
const supportSlides = Math.ceil(coreSlides * 0.3); // 30% support slides
|
||||
|
||||
return {
|
||||
total: coreSlides + supportSlides,
|
||||
core: coreSlides,
|
||||
support: supportSlides,
|
||||
recommendation: coreSlides + supportSlides <= 15 ? 'optimal' : 'consider-simplifying'
|
||||
};
|
||||
}
|
||||
|
||||
function analyzeAudience(audienceStr) {
|
||||
const lower = audienceStr.toLowerCase();
|
||||
|
||||
// Technical level
|
||||
let technical_level = 'medium';
|
||||
if (lower.match(/board|executive|c-suite|non-technical/)) {
|
||||
technical_level = 'low';
|
||||
} else if (lower.match(/engineer|developer|technical|expert|specialist/)) {
|
||||
technical_level = 'high';
|
||||
}
|
||||
|
||||
// Decision-making power
|
||||
let decision_power = 'medium';
|
||||
if (lower.match(/board|executive|ceo|cio|cto|director|vp/)) {
|
||||
decision_power = 'high';
|
||||
} else if (lower.match(/staff|team|individual contributor|ic/)) {
|
||||
decision_power = 'low';
|
||||
}
|
||||
|
||||
// Size
|
||||
let size = 'medium';
|
||||
if (lower.match(/small|intimate|1:1|one-on-one/)) {
|
||||
size = 'small';
|
||||
} else if (lower.match(/large|conference|hundreds|auditorium/)) {
|
||||
size = 'large';
|
||||
}
|
||||
|
||||
return {
|
||||
technical_level,
|
||||
decision_power,
|
||||
size,
|
||||
recommendations: {
|
||||
font_size: size === 'large' ? '42pt+' : '36pt+',
|
||||
detail_level: technical_level === 'high' ? 'can-include-technical-details' : 'avoid-jargon',
|
||||
call_to_action: decision_power === 'high' ? 'specific-next-steps' : 'awareness-building'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function generateAnalysis(args) {
|
||||
const topic = args.topic || '';
|
||||
const audience = args.audience || '';
|
||||
const duration = args.duration || '20min';
|
||||
const purpose = args.purpose || '';
|
||||
|
||||
const durationMinutes = parseDuration(duration);
|
||||
const presentationType = detectPresentationType(topic, audience, purpose);
|
||||
const typeDetails = Object.values(PRESENTATION_TYPES).find(t => t.name === presentationType);
|
||||
const frameworks = recommendFrameworks(presentationType, durationMinutes);
|
||||
const slideEstimate = estimateSlideCount(durationMinutes, presentationType);
|
||||
const audienceAnalysis = analyzeAudience(audience);
|
||||
|
||||
return {
|
||||
input: {
|
||||
topic,
|
||||
audience,
|
||||
duration,
|
||||
purpose
|
||||
},
|
||||
analysis: {
|
||||
presentation_type: presentationType,
|
||||
type_characteristics: typeDetails.characteristics,
|
||||
audience_analysis: audienceAnalysis,
|
||||
duration_minutes: durationMinutes,
|
||||
slide_estimate: slideEstimate,
|
||||
pattern_distribution: typeDetails.pattern_distribution
|
||||
},
|
||||
recommendations: {
|
||||
primary_framework: frameworks[0],
|
||||
alternative_frameworks: frameworks.slice(1),
|
||||
key_principles: [
|
||||
'Maximum 6 words per slide (Seth Godin)',
|
||||
`Font size minimum: ${audienceAnalysis.recommendations.font_size}`,
|
||||
`${slideEstimate.core} core concepts maximum (Guy Kawasaki)`,
|
||||
'One idea per slide',
|
||||
'Visual dominance over text'
|
||||
]
|
||||
},
|
||||
warnings: [
|
||||
slideEstimate.recommendation === 'consider-simplifying'
|
||||
? `⚠️ ${slideEstimate.total} slides may be too many - consider simplifying to ${Math.min(slideEstimate.core, 10)} core concepts`
|
||||
: null,
|
||||
durationMinutes > 30
|
||||
? '⚠️ Presentations >30min risk losing audience attention - consider breaking into sections'
|
||||
: null,
|
||||
audienceAnalysis.technical_level === 'low' && topic.toLowerCase().match(/technical|technology|ai|software/)
|
||||
? '⚠️ Non-technical audience + technical topic = extra emphasis on visual metaphors'
|
||||
: null
|
||||
].filter(w => w !== null)
|
||||
};
|
||||
}
|
||||
|
||||
// CLI Interface
|
||||
function main() {
|
||||
const args = {};
|
||||
|
||||
for (let i = 2; i < process.argv.length; i += 2) {
|
||||
const key = process.argv[i].replace(/^--/, '');
|
||||
const value = process.argv[i + 1];
|
||||
args[key] = value;
|
||||
}
|
||||
|
||||
if (!args.topic && !args.audience && !args.duration) {
|
||||
console.error('Usage: bun analyze-context.js --topic "Topic" --audience "Audience" --duration "15min" [--purpose "Purpose"]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const analysis = generateAnalysis(args);
|
||||
console.log(JSON.stringify(analysis, null, 2));
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { generateAnalysis, detectPresentationType, recommendFrameworks, estimateSlideCount, analyzeAudience };
|
||||
342
skills/presentation-master/scripts/suggest-images.js
Normal file
342
skills/presentation-master/scripts/suggest-images.js
Normal file
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Visual Recommendation Engine
|
||||
*
|
||||
* Analyzes slide content and recommends visual types and generation prompts
|
||||
* for the image-gen skill.
|
||||
*
|
||||
* Usage:
|
||||
* bun suggest-images.js --slides slides.json --presentation-id "cyber-2025"
|
||||
*
|
||||
* Output: JSON with image recommendations, prompts, cost estimates
|
||||
*/
|
||||
|
||||
const IMAGE_COSTS = {
|
||||
'1K': 0.065, // 1024x1024
|
||||
'2K': 0.13, // 2048x2048
|
||||
'4K': 0.24 // 4096x4096
|
||||
};
|
||||
|
||||
const VISUAL_TYPES = {
|
||||
INFOGRAPHIC: {
|
||||
name: 'infographic',
|
||||
best_for: ['statistics', 'comparisons', 'processes', 'timelines'],
|
||||
style_keywords: ['clean', 'modern', 'professional', 'data-driven', 'minimalist'],
|
||||
default_resolution: '2K',
|
||||
prompt_template: 'Clean, modern infographic showing {concept}. Minimalist design, professional color palette, clear visual hierarchy. {specifics}. High contrast, data-focused, no text.'
|
||||
},
|
||||
|
||||
CONCEPTUAL: {
|
||||
name: 'conceptual',
|
||||
best_for: ['abstract ideas', 'metaphors', 'themes', 'emotions'],
|
||||
style_keywords: ['metaphorical', 'symbolic', 'artistic', 'thought-provoking'],
|
||||
default_resolution: '2K',
|
||||
prompt_template: 'Conceptual image representing {concept}. Symbolic, thought-provoking visual metaphor. {specifics}. Artistic but professional, high quality, suitable for business presentation.'
|
||||
},
|
||||
|
||||
REALISTIC: {
|
||||
name: 'realistic',
|
||||
best_for: ['examples', 'scenarios', 'people', 'places', 'objects'],
|
||||
style_keywords: ['photographic', 'realistic', 'professional', 'high-quality'],
|
||||
default_resolution: '2K',
|
||||
prompt_template: 'Professional photograph of {concept}. Realistic, high-quality, well-lit. {specifics}. Commercial photography style, suitable for corporate presentation.'
|
||||
},
|
||||
|
||||
DIAGRAM: {
|
||||
name: 'diagram',
|
||||
best_for: ['technical concepts', 'architectures', 'flows', 'relationships'],
|
||||
style_keywords: ['technical', 'schematic', 'architectural', 'structured'],
|
||||
default_resolution: '2K',
|
||||
prompt_template: 'Clean technical diagram illustrating {concept}. {specifics}. Professional, minimal color, clear connections, no labels or text.'
|
||||
},
|
||||
|
||||
DATA_VIZ: {
|
||||
name: 'data-visualization',
|
||||
best_for: ['trends', 'growth', 'decline', 'distributions', 'correlations'],
|
||||
style_keywords: ['chart', 'graph', 'visualization', 'quantitative'],
|
||||
default_resolution: '2K',
|
||||
prompt_template: 'Modern data visualization showing {concept}. {specifics}. Clean design, professional color scheme, Tufte-style minimalism, no grid lines or labels.'
|
||||
},
|
||||
|
||||
ABSTRACT: {
|
||||
name: 'abstract',
|
||||
best_for: ['innovation', 'transformation', 'future', 'technology'],
|
||||
style_keywords: ['abstract', 'modern', 'technological', 'futuristic'],
|
||||
default_resolution: '2K',
|
||||
prompt_template: 'Abstract visual representing {concept}. Modern, technological feel. {specifics}. Clean, professional, suitable for corporate presentation.'
|
||||
}
|
||||
};
|
||||
|
||||
const CONTENT_PATTERNS = {
|
||||
// Numeric indicators
|
||||
numeric: {
|
||||
patterns: [/\d+%/, /\$\d+/, /\d+[KMB]/, /\d+ (percent|million|billion|thousand)/i],
|
||||
recommended_types: ['infographic', 'data-visualization']
|
||||
},
|
||||
|
||||
// Comparison indicators
|
||||
comparison: {
|
||||
patterns: [/vs\.?/i, /versus/i, /compared to/i, /before.*after/i, /old.*new/i],
|
||||
recommended_types: ['infographic', 'data-visualization']
|
||||
},
|
||||
|
||||
// Process indicators
|
||||
process: {
|
||||
patterns: [/step/i, /phase/i, /stage/i, /process/i, /workflow/i, /pipeline/i],
|
||||
recommended_types: ['diagram', 'infographic']
|
||||
},
|
||||
|
||||
// Time indicators
|
||||
timeline: {
|
||||
patterns: [/timeline/i, /history/i, /evolution/i, /\d{4}/, /past.*future/i],
|
||||
recommended_types: ['infographic', 'diagram']
|
||||
},
|
||||
|
||||
// Abstract concepts
|
||||
abstract: {
|
||||
patterns: [/future/i, /innovation/i, /transform/i, /vision/i, /imagine/i, /potential/i],
|
||||
recommended_types: ['conceptual', 'abstract']
|
||||
},
|
||||
|
||||
// People/scenarios
|
||||
human: {
|
||||
patterns: [/people/i, /user/i, /student/i, /teacher/i, /team/i, /customer/i],
|
||||
recommended_types: ['realistic', 'conceptual']
|
||||
},
|
||||
|
||||
// Technical
|
||||
technical: {
|
||||
patterns: [/architecture/i, /system/i, /infrastructure/i, /network/i, /api/i],
|
||||
recommended_types: ['diagram', 'abstract']
|
||||
}
|
||||
};
|
||||
|
||||
function analyzeSlideContent(slide) {
|
||||
const content = `${slide.title || ''} ${slide.text || ''} ${slide.notes || ''}`.toLowerCase();
|
||||
|
||||
// Detect patterns
|
||||
const detectedPatterns = [];
|
||||
for (const [patternName, pattern] of Object.entries(CONTENT_PATTERNS)) {
|
||||
for (const regex of pattern.patterns) {
|
||||
if (regex.test(content)) {
|
||||
detectedPatterns.push({
|
||||
pattern: patternName,
|
||||
recommended_types: pattern.recommended_types
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return detectedPatterns;
|
||||
}
|
||||
|
||||
function selectVisualType(patterns, slidePattern) {
|
||||
// If no patterns detected, use slide pattern to guide
|
||||
if (patterns.length === 0) {
|
||||
switch (slidePattern) {
|
||||
case 'data-viz':
|
||||
return VISUAL_TYPES.DATA_VIZ;
|
||||
case 'process':
|
||||
case 'timeline':
|
||||
return VISUAL_TYPES.DIAGRAM;
|
||||
case 'big-idea':
|
||||
return VISUAL_TYPES.CONCEPTUAL;
|
||||
default:
|
||||
return VISUAL_TYPES.REALISTIC;
|
||||
}
|
||||
}
|
||||
|
||||
// Count type recommendations
|
||||
const typeCounts = {};
|
||||
for (const p of patterns) {
|
||||
for (const type of p.recommended_types) {
|
||||
typeCounts[type] = (typeCounts[type] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Get highest scoring type
|
||||
const sorted = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]);
|
||||
const typeName = sorted[0][0];
|
||||
|
||||
// Find matching visual type
|
||||
for (const visualType of Object.values(VISUAL_TYPES)) {
|
||||
if (visualType.name === typeName) {
|
||||
return visualType;
|
||||
}
|
||||
}
|
||||
|
||||
return VISUAL_TYPES.CONCEPTUAL; // fallback
|
||||
}
|
||||
|
||||
function extractConcepts(slide) {
|
||||
// Extract key concepts from slide content
|
||||
const title = slide.title || '';
|
||||
const text = slide.text || '';
|
||||
|
||||
// Primary concept is the title (simplified)
|
||||
const concept = title.replace(/[?!.]/g, '').trim();
|
||||
|
||||
// Secondary details from text
|
||||
const specifics = text
|
||||
? text.split(/[.!?]/).slice(0, 2).join('. ').trim()
|
||||
: '';
|
||||
|
||||
return { concept, specifics };
|
||||
}
|
||||
|
||||
function generatePrompt(visualType, slide) {
|
||||
const { concept, specifics } = extractConcepts(slide);
|
||||
|
||||
// Use template
|
||||
let prompt = visualType.prompt_template
|
||||
.replace('{concept}', concept)
|
||||
.replace('{specifics}', specifics);
|
||||
|
||||
// Add brand context if available
|
||||
if (slide.brand) {
|
||||
if (slide.brand === 'psd') {
|
||||
prompt += ' Peninsula School District brand: teal/green color palette (#6CA18A), professional education context.';
|
||||
} else if (slide.brand === 'personal') {
|
||||
prompt += ' Modern, clean aesthetic.';
|
||||
}
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
function estimateCost(imageCount, resolution = '2K') {
|
||||
const perImage = IMAGE_COSTS[resolution];
|
||||
return {
|
||||
per_image: perImage,
|
||||
total: perImage * imageCount,
|
||||
resolution,
|
||||
currency: 'USD'
|
||||
};
|
||||
}
|
||||
|
||||
function shouldGenerateImage(slide) {
|
||||
// Skip image generation for certain patterns
|
||||
if (slide.pattern === 'title' && slide.index === 0) {
|
||||
return false; // Title slide often doesn't need generated image
|
||||
}
|
||||
|
||||
if (slide.pattern === 'transition') {
|
||||
return false; // Transitions are usually text-only
|
||||
}
|
||||
|
||||
if (slide.pattern === 'big-idea' && !slide.needs_visual) {
|
||||
return false; // Some big ideas are typography-only
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function generateRecommendations(slides, presentationId, options = {}) {
|
||||
const brand = options.brand || null;
|
||||
const resolution = options.resolution || '2K';
|
||||
const autoInclude = options.auto_include !== false;
|
||||
|
||||
const recommendations = [];
|
||||
|
||||
for (let i = 0; i < slides.length; i++) {
|
||||
const slide = { ...slides[i], index: i, brand };
|
||||
|
||||
if (!shouldGenerateImage(slide)) {
|
||||
recommendations.push({
|
||||
slide_index: i,
|
||||
slide_title: slide.title,
|
||||
recommendation: 'no-image-needed',
|
||||
reason: `Pattern '${slide.pattern}' typically doesn't require generated image`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Analyze content
|
||||
const patterns = analyzeSlideContent(slide);
|
||||
const visualType = selectVisualType(patterns, slide.pattern);
|
||||
|
||||
// Generate prompt
|
||||
const prompt = generatePrompt(visualType, slide);
|
||||
|
||||
// Generate filename
|
||||
const slideNum = String(i + 1).padStart(2, '0');
|
||||
const slugTitle = slide.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.substring(0, 40);
|
||||
const filename = `${presentationId}-slide-${slideNum}-${slugTitle}.png`;
|
||||
|
||||
recommendations.push({
|
||||
slide_index: i,
|
||||
slide_title: slide.title,
|
||||
slide_pattern: slide.pattern,
|
||||
visual_type: visualType.name,
|
||||
prompt,
|
||||
filename,
|
||||
resolution,
|
||||
auto_include: autoInclude,
|
||||
confidence: patterns.length > 0 ? 'high' : 'medium'
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate costs
|
||||
const imagesToGenerate = recommendations.filter(r => r.recommendation !== 'no-image-needed');
|
||||
const costEstimate = estimateCost(imagesToGenerate.length, resolution);
|
||||
|
||||
return {
|
||||
presentation_id: presentationId,
|
||||
total_slides: slides.length,
|
||||
images_recommended: imagesToGenerate.length,
|
||||
cost_estimate: costEstimate,
|
||||
recommendations,
|
||||
summary: {
|
||||
by_type: imagesToGenerate.reduce((acc, r) => {
|
||||
acc[r.visual_type] = (acc[r.visual_type] || 0) + 1;
|
||||
return acc;
|
||||
}, {}),
|
||||
high_confidence: imagesToGenerate.filter(r => r.confidence === 'high').length,
|
||||
medium_confidence: imagesToGenerate.filter(r => r.confidence === 'medium').length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// CLI Interface
|
||||
function main() {
|
||||
const args = {};
|
||||
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
if (process.argv[i].startsWith('--')) {
|
||||
const key = process.argv[i].replace(/^--/, '');
|
||||
const value = process.argv[i + 1];
|
||||
args[key] = value;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.slides || !args['presentation-id']) {
|
||||
console.error('Usage: bun suggest-images.js --slides slides.json --presentation-id "cyber-2025" [--brand psd] [--resolution 2K]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read slides from file
|
||||
const fs = require('fs');
|
||||
const slidesData = JSON.parse(fs.readFileSync(args.slides, 'utf-8'));
|
||||
const slides = Array.isArray(slidesData) ? slidesData : slidesData.slides;
|
||||
|
||||
const options = {
|
||||
brand: args.brand || null,
|
||||
resolution: args.resolution || '2K'
|
||||
};
|
||||
|
||||
const recommendations = generateRecommendations(slides, args['presentation-id'], options);
|
||||
console.log(JSON.stringify(recommendations, null, 2));
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { generateRecommendations, analyzeSlideContent, selectVisualType, generatePrompt, estimateCost };
|
||||
549
skills/presentation-master/scripts/validate-presentation.js
Normal file
549
skills/presentation-master/scripts/validate-presentation.js
Normal file
@@ -0,0 +1,549 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Presentation Quality Validator
|
||||
*
|
||||
* Scores presentations 0-100 against best practices from presentation masters.
|
||||
* Implements validation rules from principles/validation-rules.md
|
||||
*
|
||||
* Usage:
|
||||
* bun validate-presentation.js --presentation presentation.json
|
||||
*
|
||||
* Output: JSON with score, breakdown, critical issues, warnings, recommendations
|
||||
*/
|
||||
|
||||
const SCORING_WEIGHTS = {
|
||||
simplicity: 10,
|
||||
visual_dominance: 10,
|
||||
story_structure: 10,
|
||||
one_idea_per_slide: 10,
|
||||
typography: 8,
|
||||
layout: 7,
|
||||
color_contrast: 7,
|
||||
media_quality: 8,
|
||||
cognitive_load: 20,
|
||||
data_integrity: 10
|
||||
};
|
||||
|
||||
const CRITICAL_VIOLATIONS = {
|
||||
font_too_small: { penalty: -20, threshold: 30 },
|
||||
too_many_concepts: { penalty: -15, threshold: 10 },
|
||||
bullet_points: { penalty: -10 },
|
||||
paragraphs: { penalty: -10 },
|
||||
poor_contrast: { penalty: -15, threshold: 4.5 },
|
||||
default_template: { penalty: -20 }
|
||||
};
|
||||
|
||||
const WARNING_FLAGS = {
|
||||
too_many_words: { penalty: -2, threshold: 6 },
|
||||
too_many_slides: { penalty: -5, ratio: 0.75 },
|
||||
no_images: { penalty: -10 },
|
||||
text_heavy: { penalty: -3, threshold: 3 },
|
||||
inconsistent_fonts: { penalty: -5, threshold: 2 },
|
||||
low_res_images: { penalty: -3 }
|
||||
};
|
||||
|
||||
function countWords(text) {
|
||||
if (!text) return 0;
|
||||
return text.trim().split(/\s+/).filter(w => w.length > 0).length;
|
||||
}
|
||||
|
||||
function detectBulletPoints(text) {
|
||||
if (!text) return false;
|
||||
return /[•\-*]\s/.test(text) || /^\s*[\d]+\.\s/m.test(text);
|
||||
}
|
||||
|
||||
function detectParagraphs(text) {
|
||||
if (!text) return false;
|
||||
const sentences = text.split(/[.!?]/).filter(s => s.trim().length > 0);
|
||||
return sentences.length > 2;
|
||||
}
|
||||
|
||||
function estimateContrastRatio(slide) {
|
||||
// Simplified contrast estimation
|
||||
// In real implementation, would analyze actual colors
|
||||
if (slide.contrast_ratio) return slide.contrast_ratio;
|
||||
|
||||
// Conservative default
|
||||
return 7.0;
|
||||
}
|
||||
|
||||
function countElements(slide) {
|
||||
let elements = 0;
|
||||
if (slide.title) elements++;
|
||||
if (slide.text) elements++;
|
||||
if (slide.image) elements++;
|
||||
if (slide.chart) elements++;
|
||||
return elements;
|
||||
}
|
||||
|
||||
function scoreSimplicity(slides) {
|
||||
let score = 0;
|
||||
const maxScore = SCORING_WEIGHTS.simplicity;
|
||||
|
||||
for (const slide of slides) {
|
||||
const wordCount = countWords(slide.text);
|
||||
const elements = countElements(slide);
|
||||
|
||||
// Word count scoring
|
||||
let wordScore = 0;
|
||||
if (wordCount <= 3) wordScore = 10;
|
||||
else if (wordCount <= 6) wordScore = 8;
|
||||
else if (wordCount <= 10) wordScore = 5;
|
||||
else wordScore = 0;
|
||||
|
||||
// Visual clutter scoring
|
||||
let elementScore = 0;
|
||||
if (elements === 1) elementScore = 10;
|
||||
else if (elements <= 3) elementScore = 8;
|
||||
else if (elements <= 5) elementScore = 5;
|
||||
else elementScore = 0;
|
||||
|
||||
score += (wordScore + elementScore) / 2;
|
||||
}
|
||||
|
||||
return Math.min(maxScore, score / slides.length);
|
||||
}
|
||||
|
||||
function scoreVisualDominance(slides) {
|
||||
let score = 0;
|
||||
const maxScore = SCORING_WEIGHTS.visual_dominance;
|
||||
|
||||
const slidesWithImages = slides.filter(s => s.image || s.chart).length;
|
||||
const imageRatio = slidesWithImages / slides.length;
|
||||
|
||||
// Image quality check
|
||||
const highResImages = slides.filter(s => {
|
||||
if (!s.image) return false;
|
||||
return s.image.resolution >= 2000;
|
||||
}).length;
|
||||
|
||||
const imageQualityScore = slidesWithImages > 0
|
||||
? (highResImages / slidesWithImages) * 10
|
||||
: 0;
|
||||
|
||||
// Text-to-visual ratio
|
||||
const textVsVisualScore = imageRatio >= 0.8 ? 10 :
|
||||
imageRatio >= 0.5 ? 5 : 0;
|
||||
|
||||
score = (imageQualityScore + textVsVisualScore) / 2;
|
||||
|
||||
return Math.min(maxScore, score);
|
||||
}
|
||||
|
||||
function scoreStoryStructure(presentation) {
|
||||
const maxScore = SCORING_WEIGHTS.story_structure;
|
||||
|
||||
// Check if presentation has defined structure
|
||||
if (!presentation.framework) return maxScore * 0.3; // partial credit
|
||||
|
||||
const framework = presentation.framework.toLowerCase();
|
||||
|
||||
// Narrative arc check
|
||||
let narrativeScore = 0;
|
||||
if (presentation.slides.length >= 3) {
|
||||
const hasOpening = presentation.slides[0].pattern === 'title';
|
||||
const hasClosing = presentation.slides[presentation.slides.length - 1].pattern === 'transition' ||
|
||||
presentation.slides[presentation.slides.length - 1].pattern === 'big-idea';
|
||||
|
||||
if (hasOpening && hasClosing) narrativeScore = 10;
|
||||
else if (hasOpening || hasClosing) narrativeScore = 5;
|
||||
}
|
||||
|
||||
// Emotional beats check
|
||||
const bigIdeaSlides = presentation.slides.filter(s => s.pattern === 'big-idea').length;
|
||||
const emotionalScore = bigIdeaSlides >= 3 ? 10 :
|
||||
bigIdeaSlides >= 1 ? 5 : 0;
|
||||
|
||||
return Math.min(maxScore, (narrativeScore + emotionalScore) / 2);
|
||||
}
|
||||
|
||||
function scoreOneIdeaPerSlide(slides) {
|
||||
const maxScore = SCORING_WEIGHTS.one_idea_per_slide;
|
||||
|
||||
let clearSlides = 0;
|
||||
for (const slide of slides) {
|
||||
const wordCount = countWords(slide.text);
|
||||
const hasBullets = detectBulletPoints(slide.text);
|
||||
const elements = countElements(slide);
|
||||
|
||||
// Single concept if: few words, no bullets, few elements
|
||||
if (wordCount <= 10 && !hasBullets && elements <= 3) {
|
||||
clearSlides++;
|
||||
}
|
||||
}
|
||||
|
||||
const ratio = clearSlides / slides.length;
|
||||
|
||||
if (ratio === 1.0) return maxScore;
|
||||
if (ratio >= 0.7) return maxScore * 0.7;
|
||||
if (ratio >= 0.5) return maxScore * 0.3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function scoreTypography(slides) {
|
||||
const maxScore = SCORING_WEIGHTS.typography;
|
||||
|
||||
// Font size check
|
||||
const minFontSize = Math.min(...slides.map(s => s.min_font_size || 36));
|
||||
|
||||
let fontSizeScore = 0;
|
||||
if (minFontSize >= 36) fontSizeScore = 8;
|
||||
else if (minFontSize >= 30) fontSizeScore = 6;
|
||||
else if (minFontSize >= 24) fontSizeScore = 2;
|
||||
else fontSizeScore = 0;
|
||||
|
||||
// Font consistency
|
||||
const fontFamilies = new Set(slides.map(s => s.font_family || 'default'));
|
||||
let consistencyScore = 0;
|
||||
if (fontFamilies.size === 1) consistencyScore = 8;
|
||||
else if (fontFamilies.size === 2) consistencyScore = 6;
|
||||
else consistencyScore = 0;
|
||||
|
||||
return Math.min(maxScore, (fontSizeScore + consistencyScore) / 2);
|
||||
}
|
||||
|
||||
function scoreLayout(slides) {
|
||||
const maxScore = SCORING_WEIGHTS.layout;
|
||||
|
||||
// Simplified layout scoring
|
||||
// In real implementation, would analyze actual layouts
|
||||
let hierarchyScore = 7; // assume good by default
|
||||
let whitespaceScore = 7; // assume good by default
|
||||
let alignmentScore = 7; // assume good by default
|
||||
|
||||
return Math.min(maxScore, (hierarchyScore + whitespaceScore + alignmentScore) / 3);
|
||||
}
|
||||
|
||||
function scoreColorContrast(slides) {
|
||||
const maxScore = SCORING_WEIGHTS.color_contrast;
|
||||
|
||||
const avgContrast = slides.reduce((sum, s) => sum + estimateContrastRatio(s), 0) / slides.length;
|
||||
|
||||
let contrastScore = 0;
|
||||
if (avgContrast >= 7.0) contrastScore = 7;
|
||||
else if (avgContrast >= 4.5) contrastScore = 5;
|
||||
else contrastScore = 0;
|
||||
|
||||
return Math.min(maxScore, contrastScore);
|
||||
}
|
||||
|
||||
function scoreMediaQuality(slides) {
|
||||
const maxScore = SCORING_WEIGHTS.media_quality;
|
||||
|
||||
const slidesWithMedia = slides.filter(s => s.image || s.chart);
|
||||
if (slidesWithMedia.length === 0) return 0;
|
||||
|
||||
// Resolution check
|
||||
const highRes = slidesWithMedia.filter(s => {
|
||||
if (s.image) return s.image.resolution >= 2000;
|
||||
return true; // charts assumed high quality
|
||||
}).length;
|
||||
|
||||
const resolutionScore = (highRes / slidesWithMedia.length) * 8;
|
||||
|
||||
// Relevance is assumed (would need manual review)
|
||||
const relevanceScore = 8;
|
||||
|
||||
return Math.min(maxScore, (resolutionScore + relevanceScore) / 2);
|
||||
}
|
||||
|
||||
function scoreCognitiveLoad(slides, presentation) {
|
||||
const maxScore = SCORING_WEIGHTS.cognitive_load;
|
||||
|
||||
// Mayer's 12 principles adherence (simplified)
|
||||
let principlesFollowed = 0;
|
||||
|
||||
// 1. Coherence (extraneous material excluded)
|
||||
const avgWordCount = slides.reduce((sum, s) => sum + countWords(s.text), 0) / slides.length;
|
||||
if (avgWordCount <= 10) principlesFollowed++;
|
||||
|
||||
// 2. Signaling (essential material highlighted)
|
||||
const hasTransitions = slides.some(s => s.pattern === 'transition');
|
||||
if (hasTransitions) principlesFollowed++;
|
||||
|
||||
// 3. Redundancy (graphics + narration, not graphics + text + narration)
|
||||
const textHeavySlides = slides.filter(s => countWords(s.text) > 15).length;
|
||||
if (textHeavySlides / slides.length < 0.2) principlesFollowed++;
|
||||
|
||||
// 4. Spatial contiguity (related words/pictures near each other)
|
||||
principlesFollowed++; // assume good
|
||||
|
||||
// 5. Temporal contiguity (corresponding narration/animation together)
|
||||
principlesFollowed++; // assume good
|
||||
|
||||
// 6. Segmenting (user-paced)
|
||||
principlesFollowed++; // presentations are inherently segmented
|
||||
|
||||
// 7. Pre-training (key concepts introduced early)
|
||||
const hasOpening = slides[0]?.pattern === 'title';
|
||||
if (hasOpening) principlesFollowed++;
|
||||
|
||||
// 8. Modality (graphics + narration better than graphics + text)
|
||||
const avgTextPerSlide = slides.reduce((sum, s) => sum + (s.text || '').length, 0) / slides.length;
|
||||
if (avgTextPerSlide < 100) principlesFollowed++;
|
||||
|
||||
// 9-12: Multimedia, personalization, voice, image principles
|
||||
principlesFollowed += 4; // assume followed
|
||||
|
||||
// Score based on principles followed
|
||||
if (principlesFollowed >= 10) return maxScore;
|
||||
if (principlesFollowed >= 7) return maxScore * 0.75;
|
||||
if (principlesFollowed >= 4) return maxScore * 0.5;
|
||||
return maxScore * 0.25;
|
||||
}
|
||||
|
||||
function scoreDataIntegrity(slides) {
|
||||
const maxScore = SCORING_WEIGHTS.data_integrity;
|
||||
|
||||
const dataSlides = slides.filter(s => s.chart || s.pattern === 'data-viz');
|
||||
if (dataSlides.length === 0) return maxScore; // not applicable, full credit
|
||||
|
||||
// Lie factor check (if provided)
|
||||
let lieFactor = 1.0; // assume honest by default
|
||||
if (dataSlides[0].lie_factor) {
|
||||
lieFactor = dataSlides[0].lie_factor;
|
||||
}
|
||||
|
||||
let lieFactorScore = 0;
|
||||
if (lieFactor >= 0.95 && lieFactor <= 1.05) lieFactorScore = 10;
|
||||
else if (lieFactor >= 0.90 && lieFactor <= 1.10) lieFactorScore = 7;
|
||||
else if (lieFactor >= 0.80 && lieFactor <= 1.20) lieFactorScore = 3;
|
||||
else lieFactorScore = 0;
|
||||
|
||||
// Data-ink ratio (assume maximized by default)
|
||||
const dataInkScore = 10;
|
||||
|
||||
return Math.min(maxScore, (lieFactorScore + dataInkScore) / 2);
|
||||
}
|
||||
|
||||
function checkCriticalViolations(slides, presentation) {
|
||||
const violations = [];
|
||||
|
||||
// Font size check
|
||||
const minFont = Math.min(...slides.map(s => s.min_font_size || 36));
|
||||
if (minFont < CRITICAL_VIOLATIONS.font_too_small.threshold) {
|
||||
violations.push({
|
||||
type: 'font_too_small',
|
||||
penalty: CRITICAL_VIOLATIONS.font_too_small.penalty,
|
||||
message: `CRITICAL: Minimum font size is ${minFont}pt (required: 30pt+)`,
|
||||
slides_affected: slides.filter(s => (s.min_font_size || 36) < 30).map(s => s.index)
|
||||
});
|
||||
}
|
||||
|
||||
// Concept count check
|
||||
if (slides.length > CRITICAL_VIOLATIONS.too_many_concepts.threshold) {
|
||||
violations.push({
|
||||
type: 'too_many_concepts',
|
||||
penalty: CRITICAL_VIOLATIONS.too_many_concepts.penalty,
|
||||
message: `CRITICAL: ${slides.length} slides exceeds Kawasaki's 10 core concepts limit`
|
||||
});
|
||||
}
|
||||
|
||||
// Bullet points check
|
||||
const bulletSlides = slides.filter(s => detectBulletPoints(s.text));
|
||||
if (bulletSlides.length > 0) {
|
||||
violations.push({
|
||||
type: 'bullet_points',
|
||||
penalty: CRITICAL_VIOLATIONS.bullet_points.penalty * bulletSlides.length,
|
||||
message: `CRITICAL: ${bulletSlides.length} slides have bullet points`,
|
||||
slides_affected: bulletSlides.map(s => s.index)
|
||||
});
|
||||
}
|
||||
|
||||
// Paragraphs check
|
||||
const paragraphSlides = slides.filter(s => detectParagraphs(s.text));
|
||||
if (paragraphSlides.length > 0) {
|
||||
violations.push({
|
||||
type: 'paragraphs',
|
||||
penalty: CRITICAL_VIOLATIONS.paragraphs.penalty * paragraphSlides.length,
|
||||
message: `CRITICAL: ${paragraphSlides.length} slides have paragraphs (>2 sentences)`,
|
||||
slides_affected: paragraphSlides.map(s => s.index)
|
||||
});
|
||||
}
|
||||
|
||||
// Contrast check
|
||||
const avgContrast = slides.reduce((sum, s) => sum + estimateContrastRatio(s), 0) / slides.length;
|
||||
if (avgContrast < CRITICAL_VIOLATIONS.poor_contrast.threshold) {
|
||||
violations.push({
|
||||
type: 'poor_contrast',
|
||||
penalty: CRITICAL_VIOLATIONS.poor_contrast.penalty,
|
||||
message: `CRITICAL: Average contrast ratio ${avgContrast.toFixed(1)}:1 (required: 4.5:1+)`
|
||||
});
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
function checkWarnings(slides, presentation) {
|
||||
const warnings = [];
|
||||
|
||||
// Word count warnings
|
||||
const wordySlides = slides.filter(s => countWords(s.text) > WARNING_FLAGS.too_many_words.threshold);
|
||||
if (wordySlides.length > 0) {
|
||||
warnings.push({
|
||||
type: 'too_many_words',
|
||||
penalty: WARNING_FLAGS.too_many_words.penalty * wordySlides.length,
|
||||
message: `${wordySlides.length} slides exceed 6-word limit (Seth Godin standard)`,
|
||||
slides_affected: wordySlides.map(s => ({
|
||||
index: s.index,
|
||||
words: countWords(s.text)
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// Slide count for duration
|
||||
if (presentation.duration_minutes) {
|
||||
const recommendedSlides = presentation.duration_minutes * WARNING_FLAGS.too_many_slides.ratio;
|
||||
if (slides.length > recommendedSlides) {
|
||||
warnings.push({
|
||||
type: 'too_many_slides',
|
||||
penalty: WARNING_FLAGS.too_many_slides.penalty,
|
||||
message: `${slides.length} slides for ${presentation.duration_minutes}min (recommended: ≤${Math.round(recommendedSlides)})`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Images check
|
||||
const slidesWithImages = slides.filter(s => s.image || s.chart).length;
|
||||
if (slidesWithImages === 0) {
|
||||
warnings.push({
|
||||
type: 'no_images',
|
||||
penalty: WARNING_FLAGS.no_images.penalty,
|
||||
message: 'No images detected - presentations should be visual-dominant'
|
||||
});
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
function generateRecommendations(violations, warnings, scores) {
|
||||
const recommendations = [];
|
||||
|
||||
// Address critical violations first
|
||||
if (violations.length > 0) {
|
||||
recommendations.push({
|
||||
priority: 'CRITICAL',
|
||||
action: 'Fix all critical violations before presenting',
|
||||
details: violations.map(v => v.message)
|
||||
});
|
||||
}
|
||||
|
||||
// Typography recommendations
|
||||
if (scores.typography < 6) {
|
||||
recommendations.push({
|
||||
priority: 'high',
|
||||
action: 'Increase font sizes to 36pt minimum',
|
||||
details: ['Guy Kawasaki: 30pt absolute minimum', 'TED: 42pt recommended']
|
||||
});
|
||||
}
|
||||
|
||||
// Simplicity recommendations
|
||||
if (scores.simplicity < 7) {
|
||||
recommendations.push({
|
||||
priority: 'high',
|
||||
action: 'Simplify slides - reduce word count and elements',
|
||||
details: ['Seth Godin: 6 words maximum per slide', 'One idea per slide']
|
||||
});
|
||||
}
|
||||
|
||||
// Visual recommendations
|
||||
if (scores.visual_dominance < 7) {
|
||||
recommendations.push({
|
||||
priority: 'medium',
|
||||
action: 'Add more high-quality images',
|
||||
details: ['Garr Reynolds: Pictures > text', '80% of slides should have visuals']
|
||||
});
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
function validatePresentation(presentation) {
|
||||
const slides = presentation.slides || [];
|
||||
|
||||
// Calculate scores
|
||||
const scores = {
|
||||
simplicity: scoreSimplicity(slides),
|
||||
visual_dominance: scoreVisualDominance(slides),
|
||||
story_structure: scoreStoryStructure(presentation),
|
||||
one_idea_per_slide: scoreOneIdeaPerSlide(slides),
|
||||
typography: scoreTypography(slides),
|
||||
layout: scoreLayout(slides),
|
||||
color_contrast: scoreColorContrast(slides),
|
||||
media_quality: scoreMediaQuality(slides),
|
||||
cognitive_load: scoreCognitiveLoad(slides, presentation),
|
||||
data_integrity: scoreDataIntegrity(slides)
|
||||
};
|
||||
|
||||
// Calculate total
|
||||
const baseScore = Object.values(scores).reduce((sum, score) => sum + score, 0);
|
||||
|
||||
// Check violations
|
||||
const violations = checkCriticalViolations(slides, presentation);
|
||||
const warnings = checkWarnings(slides, presentation);
|
||||
|
||||
// Apply penalties
|
||||
const violationPenalty = violations.reduce((sum, v) => sum + v.penalty, 0);
|
||||
const warningPenalty = warnings.reduce((sum, w) => sum + w.penalty, 0);
|
||||
|
||||
const finalScore = Math.max(0, Math.min(100, baseScore + violationPenalty + warningPenalty));
|
||||
|
||||
// Generate report
|
||||
const status = finalScore >= 90 ? 'Exceptional' :
|
||||
finalScore >= 80 ? 'Excellent' :
|
||||
finalScore >= 70 ? 'Good' :
|
||||
finalScore >= 60 ? 'Acceptable' : 'Poor';
|
||||
|
||||
const recommendations = generateRecommendations(violations, warnings, scores);
|
||||
|
||||
return {
|
||||
overall_score: Math.round(finalScore),
|
||||
status,
|
||||
score_breakdown: Object.entries(scores).map(([category, score]) => ({
|
||||
category,
|
||||
score: Math.round(score * 10) / 10,
|
||||
max: SCORING_WEIGHTS[category]
|
||||
})),
|
||||
critical_violations: violations,
|
||||
warnings,
|
||||
recommendations,
|
||||
summary: {
|
||||
total_slides: slides.length,
|
||||
avg_words_per_slide: Math.round(slides.reduce((sum, s) => sum + countWords(s.text), 0) / slides.length),
|
||||
slides_with_images: slides.filter(s => s.image || s.chart).length,
|
||||
min_font_size: Math.min(...slides.map(s => s.min_font_size || 36))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// CLI Interface
|
||||
function main() {
|
||||
const args = {};
|
||||
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
if (process.argv[i].startsWith('--')) {
|
||||
const key = process.argv[i].replace(/^--/, '');
|
||||
const value = process.argv[i + 1];
|
||||
args[key] = value;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.presentation) {
|
||||
console.error('Usage: bun validate-presentation.js --presentation presentation.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const presentation = JSON.parse(fs.readFileSync(args.presentation, 'utf-8'));
|
||||
|
||||
const report = validatePresentation(presentation);
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { validatePresentation };
|
||||
Reference in New Issue
Block a user