Files
2025-11-30 08:35:59 +08:00

371 lines
11 KiB
JavaScript

#!/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 };