Initial commit: Claude Skills Factory with 8 refined custom skills
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>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Apply brand styling to PowerPoint presentations
|
||||
Customizes colors, fonts, and visual elements based on brand guidelines
|
||||
"""
|
||||
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# Note: In production, install python-pptx via pip
|
||||
try:
|
||||
from pptx import Presentation
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.util import Pt
|
||||
except ImportError:
|
||||
print("📦 Please install python-pptx: pip install python-pptx")
|
||||
exit(1)
|
||||
|
||||
class BrandStyler:
|
||||
"""Apply brand guidelines to PowerPoint presentations"""
|
||||
|
||||
def __init__(self, brand_config_path: str):
|
||||
"""Load brand configuration"""
|
||||
with open(brand_config_path, 'r') as f:
|
||||
self.brand_config = json.load(f)
|
||||
|
||||
self.colors = self.brand_config.get('colors', {})
|
||||
self.fonts = self.brand_config.get('fonts', {})
|
||||
self.logos = self.brand_config.get('logos', {})
|
||||
|
||||
def apply_to_presentation(self, pptx_path: str, output_path: str = None):
|
||||
"""Apply brand styling to PowerPoint file"""
|
||||
|
||||
if not output_path:
|
||||
output_path = pptx_path.replace('.pptx', '_branded.pptx')
|
||||
|
||||
print(f"🎨 Applying brand styles to: {pptx_path}")
|
||||
|
||||
# Load presentation
|
||||
prs = Presentation(pptx_path)
|
||||
|
||||
# Apply styles to each slide
|
||||
for slide_num, slide in enumerate(prs.slides, 1):
|
||||
self.style_slide(slide, slide_num)
|
||||
|
||||
# Save branded version
|
||||
prs.save(output_path)
|
||||
print(f"✅ Branded presentation saved: {output_path}")
|
||||
|
||||
return output_path
|
||||
|
||||
def style_slide(self, slide, slide_num: int):
|
||||
"""Apply brand styling to individual slide"""
|
||||
|
||||
# Style text elements
|
||||
for shape in slide.shapes:
|
||||
if shape.has_text_frame:
|
||||
self.style_text_frame(shape.text_frame, slide_num)
|
||||
|
||||
# Style tables
|
||||
if shape.has_table:
|
||||
self.style_table(shape.table)
|
||||
|
||||
def style_text_frame(self, text_frame, slide_num: int):
|
||||
"""Apply brand fonts and colors to text"""
|
||||
|
||||
for paragraph in text_frame.paragraphs:
|
||||
for run in paragraph.runs:
|
||||
# Apply font
|
||||
if slide_num == 1: # Title slide
|
||||
run.font.name = self.fonts.get('heading', 'Arial')
|
||||
if paragraph.level == 0:
|
||||
run.font.size = Pt(44)
|
||||
else:
|
||||
run.font.name = self.fonts.get('body', 'Arial')
|
||||
|
||||
# Apply colors based on context
|
||||
if slide_num == 1: # Title slide - white text
|
||||
run.font.color.rgb = RGBColor(255, 255, 255)
|
||||
elif paragraph.level == 0: # Headings
|
||||
color_hex = self.colors.get('primary', '#1a73e8').lstrip('#')
|
||||
run.font.color.rgb = RGBColor(
|
||||
int(color_hex[0:2], 16),
|
||||
int(color_hex[2:4], 16),
|
||||
int(color_hex[4:6], 16)
|
||||
)
|
||||
else: # Body text
|
||||
color_hex = self.colors.get('text', '#3c4043').lstrip('#')
|
||||
run.font.color.rgb = RGBColor(
|
||||
int(color_hex[0:2], 16),
|
||||
int(color_hex[2:4], 16),
|
||||
int(color_hex[4:6], 16)
|
||||
)
|
||||
|
||||
def style_table(self, table):
|
||||
"""Apply brand styling to tables"""
|
||||
|
||||
# Style header row
|
||||
if len(table.rows) > 0:
|
||||
for cell in table.rows[0].cells:
|
||||
# Apply header background color
|
||||
if hasattr(cell, 'fill'):
|
||||
color_hex = self.colors.get('secondary', '#34a853').lstrip('#')
|
||||
# Note: python-pptx table cell fill is complex
|
||||
# This is simplified for example
|
||||
pass
|
||||
|
||||
# Style table text
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
if cell.text_frame:
|
||||
for paragraph in cell.text_frame.paragraphs:
|
||||
for run in paragraph.runs:
|
||||
run.font.name = self.fonts.get('body', 'Arial')
|
||||
run.font.size = Pt(14)
|
||||
|
||||
def add_logo(self, slide, position='top-right'):
|
||||
"""Add company logo to slide"""
|
||||
|
||||
logo_path = self.logos.get('primary')
|
||||
if not logo_path or not Path(logo_path).exists():
|
||||
return
|
||||
|
||||
# Position mappings
|
||||
positions = {
|
||||
'top-right': {'left': 8.5, 'top': 0.25, 'width': 1.5},
|
||||
'bottom-right': {'left': 8.5, 'top': 4.75, 'width': 1.5},
|
||||
'top-left': {'left': 0.25, 'top': 0.25, 'width': 1.5}
|
||||
}
|
||||
|
||||
pos = positions.get(position, positions['top-right'])
|
||||
|
||||
# Add logo image
|
||||
# Note: Requires python-pptx inches conversion
|
||||
# slide.shapes.add_picture(logo_path, left, top, width)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Apply brand styling to PowerPoint presentations"
|
||||
)
|
||||
parser.add_argument(
|
||||
"presentation",
|
||||
help="Path to PowerPoint presentation"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default="assets/brand_config.json",
|
||||
help="Path to brand configuration JSON"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
help="Output path for branded presentation"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Apply branding
|
||||
styler = BrandStyler(args.config)
|
||||
output_path = styler.apply_to_presentation(
|
||||
args.presentation,
|
||||
args.output
|
||||
)
|
||||
|
||||
print(f"✨ Brand styling complete: {output_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Extract research content from Notion pages and databases
|
||||
Outputs structured JSON for downstream processing
|
||||
"""
|
||||
|
||||
import json
|
||||
import argparse
|
||||
from typing import Dict, List, Any
|
||||
from datetime import datetime
|
||||
|
||||
def extract_notion_content(notion_url: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract and structure content from Notion
|
||||
|
||||
This function would integrate with Notion MCP tools:
|
||||
- notion-search for finding related pages
|
||||
- notion-fetch for getting full content
|
||||
|
||||
Args:
|
||||
notion_url: URL of Notion page or database
|
||||
|
||||
Returns:
|
||||
Structured research data
|
||||
"""
|
||||
|
||||
# Parse Notion URL to get page/database ID
|
||||
page_id = parse_notion_url(notion_url)
|
||||
|
||||
# This would use actual Notion MCP tools in production
|
||||
# Simulating the structure for now
|
||||
extracted_data = {
|
||||
"source": {
|
||||
"url": notion_url,
|
||||
"id": page_id,
|
||||
"type": "page", # or "database"
|
||||
"extracted_at": datetime.now().isoformat()
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Q4 Research Summary",
|
||||
"last_edited": "2024-12-15T10:30:00Z",
|
||||
"created_by": "user@company.com",
|
||||
"tags": ["research", "Q4", "strategy"]
|
||||
},
|
||||
"content": {
|
||||
"sections": [
|
||||
{
|
||||
"title": "Executive Summary",
|
||||
"content": "Key findings from Q4 research indicate...",
|
||||
"level": 1,
|
||||
"data_points": [
|
||||
{"metric": "Growth Rate", "value": "25%"},
|
||||
{"metric": "User Satisfaction", "value": "4.5/5"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Market Analysis",
|
||||
"content": "The market landscape shows...",
|
||||
"level": 1,
|
||||
"subsections": [
|
||||
{
|
||||
"title": "Competitive Landscape",
|
||||
"content": "Our position relative to competitors...",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"title": "Growth Opportunities",
|
||||
"content": "Identified opportunities include...",
|
||||
"level": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Customer Insights",
|
||||
"content": "Customer feedback reveals...",
|
||||
"level": 1,
|
||||
"data_points": [
|
||||
{"metric": "NPS Score", "value": "72"},
|
||||
{"metric": "Retention Rate", "value": "89%"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Recommendations",
|
||||
"content": "Based on the research, we recommend...",
|
||||
"level": 1,
|
||||
"action_items": [
|
||||
"Expand into new market segments",
|
||||
"Enhance product features based on feedback",
|
||||
"Increase investment in customer success"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"linked_pages": [
|
||||
{
|
||||
"title": "Detailed Customer Survey Results",
|
||||
"url": "notion://page/survey-results-id",
|
||||
"relevance": "high"
|
||||
},
|
||||
{
|
||||
"title": "Competitor Analysis Deep Dive",
|
||||
"url": "notion://page/competitor-analysis-id",
|
||||
"relevance": "medium"
|
||||
}
|
||||
],
|
||||
"attachments": [
|
||||
{
|
||||
"type": "spreadsheet",
|
||||
"title": "Q4 Metrics Dashboard",
|
||||
"url": "notion://attachment/metrics-id"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return extracted_data
|
||||
|
||||
def parse_notion_url(url: str) -> str:
|
||||
"""Extract page/database ID from Notion URL"""
|
||||
# Simplified URL parsing
|
||||
if "notion.so/" in url or "notion://" in url:
|
||||
parts = url.split("/")
|
||||
return parts[-1].split("?")[0]
|
||||
return url
|
||||
|
||||
def fetch_linked_content(linked_pages: List[Dict], depth: int = 1) -> List[Dict]:
|
||||
"""
|
||||
Recursively fetch linked page content
|
||||
|
||||
Args:
|
||||
linked_pages: List of linked page references
|
||||
depth: How deep to follow links
|
||||
|
||||
Returns:
|
||||
Expanded content from linked pages
|
||||
"""
|
||||
if depth <= 0:
|
||||
return []
|
||||
|
||||
expanded_content = []
|
||||
for page in linked_pages:
|
||||
if page.get("relevance") in ["high", "medium"]:
|
||||
# Would fetch actual content here
|
||||
expanded_content.append({
|
||||
"source": page["url"],
|
||||
"title": page["title"],
|
||||
"content": f"Content from {page['title']}..."
|
||||
})
|
||||
|
||||
return expanded_content
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract research content from Notion"
|
||||
)
|
||||
parser.add_argument(
|
||||
"notion_url",
|
||||
help="URL of Notion page or database"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
default="research.json",
|
||||
help="Output JSON file (default: research.json)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-linked",
|
||||
action="store_true",
|
||||
help="Include content from linked pages"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--depth",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Link following depth (default: 1)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"📚 Extracting content from: {args.notion_url}")
|
||||
|
||||
# Extract main content
|
||||
research_data = extract_notion_content(args.notion_url)
|
||||
|
||||
# Optionally fetch linked content
|
||||
if args.include_linked and research_data.get("linked_pages"):
|
||||
print("📎 Fetching linked pages...")
|
||||
linked_content = fetch_linked_content(
|
||||
research_data["linked_pages"],
|
||||
args.depth
|
||||
)
|
||||
research_data["linked_content"] = linked_content
|
||||
|
||||
# Save to JSON
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
json.dump(research_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✅ Research data saved to: {args.output}")
|
||||
print(f"📊 Extracted {len(research_data['content']['sections'])} sections")
|
||||
|
||||
if research_data.get("linked_pages"):
|
||||
print(f"🔗 Found {len(research_data['linked_pages'])} linked pages")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,473 @@
|
||||
#!/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 };
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Main workflow orchestrator for Research to Presentation
|
||||
Coordinates the complete pipeline from Notion to final presentation
|
||||
"""
|
||||
|
||||
import json
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
def run_workflow(notion_url, output_format='pptx', brand_config=None):
|
||||
"""
|
||||
Execute the complete research to presentation workflow
|
||||
|
||||
Args:
|
||||
notion_url: URL of Notion page or database
|
||||
output_format: 'pptx' or 'figma'
|
||||
brand_config: Path to brand configuration JSON
|
||||
"""
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
work_dir = Path(f"/tmp/r2p_{timestamp}")
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print("🚀 Starting Research to Presentation Workflow")
|
||||
print(f"📍 Source: {notion_url}")
|
||||
print(f"📄 Output Format: {output_format}")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
# Step 1: Extract Notion content
|
||||
print("\n📚 Step 1: Extracting Notion research...")
|
||||
research_file = work_dir / "research.json"
|
||||
subprocess.run([
|
||||
sys.executable, "scripts/extract_notion.py",
|
||||
notion_url,
|
||||
"--output", str(research_file)
|
||||
], check=True)
|
||||
print(f"✅ Research extracted to {research_file}")
|
||||
|
||||
# Step 2: Synthesize content
|
||||
print("\n🔍 Step 2: Synthesizing content and extracting topics...")
|
||||
synthesis_file = work_dir / "synthesis.json"
|
||||
subprocess.run([
|
||||
sys.executable, "scripts/synthesize_content.py",
|
||||
str(research_file),
|
||||
"--output", str(synthesis_file)
|
||||
], check=True)
|
||||
print(f"✅ Synthesis completed: {synthesis_file}")
|
||||
|
||||
# Step 3: Generate presentation
|
||||
print(f"\n🎨 Step 3: Generating {output_format.upper()} presentation...")
|
||||
|
||||
if output_format == 'pptx':
|
||||
output_file = f"presentation_{timestamp}.pptx"
|
||||
subprocess.run([
|
||||
"node", "scripts/generate_pptx.js",
|
||||
str(synthesis_file),
|
||||
output_file
|
||||
], check=True)
|
||||
print(f"✅ PowerPoint created: {output_file}")
|
||||
|
||||
elif output_format == 'figma':
|
||||
output_file = f"figma_slides_{timestamp}.json"
|
||||
subprocess.run([
|
||||
"node", "scripts/export_to_figma.js",
|
||||
str(synthesis_file),
|
||||
"--output", output_file
|
||||
], check=True)
|
||||
print(f"✅ Figma slides exported: {output_file}")
|
||||
|
||||
# Step 4: Apply branding (if config provided)
|
||||
if brand_config and output_format == 'pptx':
|
||||
print("\n🎨 Step 4: Applying brand styles...")
|
||||
subprocess.run([
|
||||
sys.executable, "scripts/apply_brand.py",
|
||||
output_file,
|
||||
"--config", brand_config
|
||||
], check=True)
|
||||
print("✅ Brand styling applied")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(f"🎉 Workflow completed successfully!")
|
||||
print(f"📁 Output: {output_file}")
|
||||
print(f"🗂️ Work files: {work_dir}")
|
||||
|
||||
return output_file
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"\n❌ Error in workflow: {e}")
|
||||
print(f"💡 Check work directory for debugging: {work_dir}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"\n❌ Unexpected error: {e}")
|
||||
raise
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Transform Notion research into presentations"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--notion-url",
|
||||
required=True,
|
||||
help="URL of Notion page or database"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-format",
|
||||
choices=['pptx', 'figma'],
|
||||
default='pptx',
|
||||
help="Output format (default: pptx)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--brand-config",
|
||||
help="Path to brand configuration JSON"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--preview",
|
||||
action='store_true',
|
||||
help="Generate HTML preview only"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.preview:
|
||||
print("🔍 Preview mode - generating HTML only")
|
||||
# Preview implementation here
|
||||
else:
|
||||
run_workflow(
|
||||
args.notion_url,
|
||||
args.output_format,
|
||||
args.brand_config
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,375 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Synthesize research content and extract presentation topics
|
||||
Analyzes research data to identify key themes, agenda items, and slide structure
|
||||
"""
|
||||
|
||||
import json
|
||||
import argparse
|
||||
from typing import Dict, List, Any
|
||||
from collections import Counter
|
||||
import re
|
||||
|
||||
class ContentSynthesizer:
|
||||
"""Analyzes research and generates presentation structure"""
|
||||
|
||||
def __init__(self, research_data: Dict):
|
||||
self.research_data = research_data
|
||||
self.synthesis = {
|
||||
"metadata": {},
|
||||
"executive_summary": "",
|
||||
"key_topics": [],
|
||||
"agenda_items": [],
|
||||
"supporting_data": [],
|
||||
"recommendations": [],
|
||||
"slide_plan": []
|
||||
}
|
||||
|
||||
def synthesize(self) -> Dict[str, Any]:
|
||||
"""Execute complete synthesis pipeline"""
|
||||
self.extract_metadata()
|
||||
self.generate_executive_summary()
|
||||
self.extract_key_topics()
|
||||
self.derive_agenda_items()
|
||||
self.collect_supporting_data()
|
||||
self.extract_recommendations()
|
||||
self.create_slide_plan()
|
||||
|
||||
return self.synthesis
|
||||
|
||||
def extract_metadata(self):
|
||||
"""Extract presentation metadata from research"""
|
||||
source = self.research_data.get("metadata", {})
|
||||
self.synthesis["metadata"] = {
|
||||
"title": source.get("title", "Research Presentation"),
|
||||
"date": source.get("last_edited", ""),
|
||||
"author": source.get("created_by", ""),
|
||||
"tags": source.get("tags", [])
|
||||
}
|
||||
|
||||
def generate_executive_summary(self):
|
||||
"""Create concise executive summary"""
|
||||
sections = self.research_data.get("content", {}).get("sections", [])
|
||||
|
||||
# Find executive summary section or generate from content
|
||||
for section in sections:
|
||||
if "executive" in section.get("title", "").lower():
|
||||
self.synthesis["executive_summary"] = section.get("content", "")
|
||||
return
|
||||
|
||||
# Generate summary from first paragraph of each section
|
||||
summary_parts = []
|
||||
for section in sections[:3]: # First 3 sections
|
||||
content = section.get("content", "")
|
||||
first_sentence = content.split(".")[0] + "."
|
||||
summary_parts.append(first_sentence)
|
||||
|
||||
self.synthesis["executive_summary"] = " ".join(summary_parts)
|
||||
|
||||
def extract_key_topics(self):
|
||||
"""Identify main topics from research"""
|
||||
sections = self.research_data.get("content", {}).get("sections", [])
|
||||
|
||||
for section in sections:
|
||||
topic = {
|
||||
"title": section.get("title", ""),
|
||||
"importance": self.calculate_importance(section),
|
||||
"key_points": self.extract_key_points(section),
|
||||
"data_points": section.get("data_points", []),
|
||||
"speaker_notes": self.generate_speaker_notes(section)
|
||||
}
|
||||
|
||||
# Include subsections as subtopics
|
||||
if section.get("subsections"):
|
||||
topic["subtopics"] = [
|
||||
{
|
||||
"title": sub.get("title", ""),
|
||||
"key_points": self.extract_key_points(sub)
|
||||
}
|
||||
for sub in section["subsections"]
|
||||
]
|
||||
|
||||
self.synthesis["key_topics"].append(topic)
|
||||
|
||||
# Sort by importance
|
||||
self.synthesis["key_topics"].sort(
|
||||
key=lambda x: x["importance"],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
def calculate_importance(self, section: Dict) -> float:
|
||||
"""Calculate topic importance score"""
|
||||
score = 1.0
|
||||
|
||||
# Higher level sections are more important
|
||||
if section.get("level") == 1:
|
||||
score += 0.5
|
||||
|
||||
# Sections with data are more important
|
||||
if section.get("data_points"):
|
||||
score += 0.3 * len(section["data_points"])
|
||||
|
||||
# Sections with action items are important
|
||||
if section.get("action_items"):
|
||||
score += 0.4
|
||||
|
||||
# Length indicates detail
|
||||
content_length = len(section.get("content", ""))
|
||||
if content_length > 500:
|
||||
score += 0.2
|
||||
|
||||
return score
|
||||
|
||||
def extract_key_points(self, section: Dict) -> List[str]:
|
||||
"""Extract bullet points from section content"""
|
||||
content = section.get("content", "")
|
||||
|
||||
# Extract sentences that look like key points
|
||||
key_points = []
|
||||
sentences = content.split(".")
|
||||
|
||||
for sentence in sentences:
|
||||
sentence = sentence.strip()
|
||||
# Look for important indicators
|
||||
if any(indicator in sentence.lower() for indicator in
|
||||
["key", "important", "significant", "critical", "major"]):
|
||||
key_points.append(sentence + ".")
|
||||
# Or if it's short and punchy
|
||||
elif 10 < len(sentence) < 100:
|
||||
key_points.append(sentence + ".")
|
||||
|
||||
# Add action items if present
|
||||
if section.get("action_items"):
|
||||
key_points.extend(section["action_items"])
|
||||
|
||||
return key_points[:5] # Limit to 5 points per slide
|
||||
|
||||
def generate_speaker_notes(self, section: Dict) -> str:
|
||||
"""Generate speaker notes for section"""
|
||||
content = section.get("content", "")
|
||||
|
||||
# Take first 2-3 sentences as speaker notes
|
||||
sentences = content.split(".")[:3]
|
||||
notes = ". ".join(sentences).strip()
|
||||
|
||||
# Add context about data if present
|
||||
if section.get("data_points"):
|
||||
notes += " Key metrics to highlight: "
|
||||
metrics = [f"{dp['metric']}: {dp['value']}"
|
||||
for dp in section["data_points"]]
|
||||
notes += ", ".join(metrics)
|
||||
|
||||
return notes
|
||||
|
||||
def derive_agenda_items(self):
|
||||
"""Generate meeting agenda from topics"""
|
||||
# Create agenda from top topics
|
||||
for i, topic in enumerate(self.synthesis["key_topics"][:5]):
|
||||
agenda_item = {
|
||||
"order": i + 1,
|
||||
"title": topic["title"],
|
||||
"duration": self.estimate_duration(topic),
|
||||
"discussion_points": topic["key_points"][:3],
|
||||
"decision_required": self.needs_decision(topic)
|
||||
}
|
||||
self.synthesis["agenda_items"].append(agenda_item)
|
||||
|
||||
def estimate_duration(self, topic: Dict) -> int:
|
||||
"""Estimate discussion time in minutes"""
|
||||
base_time = 5
|
||||
|
||||
# Add time for subtopics
|
||||
if topic.get("subtopics"):
|
||||
base_time += 2 * len(topic["subtopics"])
|
||||
|
||||
# Add time for data discussion
|
||||
if topic.get("data_points"):
|
||||
base_time += 3
|
||||
|
||||
# Cap at 15 minutes per topic
|
||||
return min(base_time, 15)
|
||||
|
||||
def needs_decision(self, topic: Dict) -> bool:
|
||||
"""Check if topic requires a decision"""
|
||||
indicators = ["recommend", "decide", "choose", "select", "approve"]
|
||||
content = " ".join(topic.get("key_points", []))
|
||||
|
||||
return any(ind in content.lower() for ind in indicators)
|
||||
|
||||
def collect_supporting_data(self):
|
||||
"""Aggregate all data points from research"""
|
||||
sections = self.research_data.get("content", {}).get("sections", [])
|
||||
|
||||
for section in sections:
|
||||
if section.get("data_points"):
|
||||
for data_point in section["data_points"]:
|
||||
self.synthesis["supporting_data"].append({
|
||||
"source": section["title"],
|
||||
"metric": data_point["metric"],
|
||||
"value": data_point["value"],
|
||||
"context": section.get("title", "")
|
||||
})
|
||||
|
||||
def extract_recommendations(self):
|
||||
"""Extract actionable recommendations"""
|
||||
sections = self.research_data.get("content", {}).get("sections", [])
|
||||
|
||||
for section in sections:
|
||||
# Look for recommendation sections
|
||||
if "recommend" in section.get("title", "").lower():
|
||||
if section.get("action_items"):
|
||||
self.synthesis["recommendations"].extend(
|
||||
section["action_items"]
|
||||
)
|
||||
else:
|
||||
# Extract from content
|
||||
content = section.get("content", "")
|
||||
if "recommend" in content.lower():
|
||||
# Simple extraction of recommendation sentences
|
||||
sentences = content.split(".")
|
||||
for sentence in sentences:
|
||||
if "recommend" in sentence.lower():
|
||||
self.synthesis["recommendations"].append(
|
||||
sentence.strip() + "."
|
||||
)
|
||||
|
||||
def create_slide_plan(self):
|
||||
"""Generate detailed slide-by-slide plan"""
|
||||
slides = []
|
||||
|
||||
# Title slide
|
||||
slides.append({
|
||||
"number": 1,
|
||||
"type": "title",
|
||||
"title": self.synthesis["metadata"]["title"],
|
||||
"subtitle": f"Research Synthesis - {self.synthesis['metadata']['date']}",
|
||||
"speaker_notes": self.synthesis["executive_summary"]
|
||||
})
|
||||
|
||||
# Executive Summary slide
|
||||
slides.append({
|
||||
"number": 2,
|
||||
"type": "executive_summary",
|
||||
"title": "Executive Summary",
|
||||
"content": self.synthesis["executive_summary"],
|
||||
"key_points": self.synthesis["key_topics"][0]["key_points"][:3],
|
||||
"speaker_notes": "Overview of key findings and recommendations"
|
||||
})
|
||||
|
||||
# Agenda slide
|
||||
if self.synthesis["agenda_items"]:
|
||||
slides.append({
|
||||
"number": 3,
|
||||
"type": "agenda",
|
||||
"title": "Agenda",
|
||||
"items": [item["title"] for item in self.synthesis["agenda_items"]],
|
||||
"total_duration": sum(item["duration"] for item in self.synthesis["agenda_items"]),
|
||||
"speaker_notes": "Today's discussion topics and time allocation"
|
||||
})
|
||||
|
||||
# Content slides for each major topic
|
||||
slide_num = 4
|
||||
for topic in self.synthesis["key_topics"][:6]: # Limit to 6 main topics
|
||||
slides.append({
|
||||
"number": slide_num,
|
||||
"type": "content",
|
||||
"title": topic["title"],
|
||||
"bullets": topic["key_points"],
|
||||
"data": topic.get("data_points", []),
|
||||
"speaker_notes": topic["speaker_notes"]
|
||||
})
|
||||
slide_num += 1
|
||||
|
||||
# Add subtopic slides if important
|
||||
if topic.get("subtopics") and topic["importance"] > 1.5:
|
||||
for subtopic in topic["subtopics"][:2]: # Max 2 subtopic slides
|
||||
slides.append({
|
||||
"number": slide_num,
|
||||
"type": "content",
|
||||
"title": subtopic["title"],
|
||||
"bullets": subtopic["key_points"],
|
||||
"speaker_notes": f"Deep dive into {subtopic['title']}"
|
||||
})
|
||||
slide_num += 1
|
||||
|
||||
# Data summary slide if we have metrics
|
||||
if self.synthesis["supporting_data"]:
|
||||
slides.append({
|
||||
"number": slide_num,
|
||||
"type": "data_visualization",
|
||||
"title": "Key Metrics",
|
||||
"data_points": self.synthesis["supporting_data"][:8],
|
||||
"chart_type": "dashboard",
|
||||
"speaker_notes": "Summary of key performance indicators"
|
||||
})
|
||||
slide_num += 1
|
||||
|
||||
# Recommendations slide
|
||||
if self.synthesis["recommendations"]:
|
||||
slides.append({
|
||||
"number": slide_num,
|
||||
"type": "recommendations",
|
||||
"title": "Recommendations",
|
||||
"items": self.synthesis["recommendations"][:5],
|
||||
"speaker_notes": "Proposed next steps based on research findings"
|
||||
})
|
||||
slide_num += 1
|
||||
|
||||
# Thank you / Questions slide
|
||||
slides.append({
|
||||
"number": slide_num,
|
||||
"type": "closing",
|
||||
"title": "Thank You",
|
||||
"subtitle": "Questions & Discussion",
|
||||
"speaker_notes": "Open floor for questions and discussion"
|
||||
})
|
||||
|
||||
self.synthesis["slide_plan"] = slides
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Synthesize research content into presentation structure"
|
||||
)
|
||||
parser.add_argument(
|
||||
"research_file",
|
||||
help="Input research JSON file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
default="synthesis.json",
|
||||
help="Output synthesis JSON file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-slides",
|
||||
type=int,
|
||||
default=15,
|
||||
help="Maximum number of slides (default: 15)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"🔍 Synthesizing content from: {args.research_file}")
|
||||
|
||||
# Load research data
|
||||
with open(args.research_file, 'r', encoding='utf-8') as f:
|
||||
research_data = json.load(f)
|
||||
|
||||
# Synthesize content
|
||||
synthesizer = ContentSynthesizer(research_data)
|
||||
synthesis = synthesizer.synthesize()
|
||||
|
||||
# Limit slides if specified
|
||||
if args.max_slides and len(synthesis["slide_plan"]) > args.max_slides:
|
||||
synthesis["slide_plan"] = synthesis["slide_plan"][:args.max_slides]
|
||||
|
||||
# Save synthesis
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
json.dump(synthesis, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✅ Synthesis saved to: {args.output}")
|
||||
print(f"📊 Generated plan for {len(synthesis['slide_plan'])} slides")
|
||||
print(f"🎯 Identified {len(synthesis['key_topics'])} key topics")
|
||||
print(f"📝 Created {len(synthesis['agenda_items'])} agenda items")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user