refactor: Reorganize skill numbering and update documentation

Skill Numbering Changes:
- 01-03: OurDigital core (was 30-32)
- 31-32: Notion tools (was 01-02)
- 99_archive: Renamed from _archive for sorting

New Files:
- AGENTS.md: Claude Code agent routing guide
- requirements.txt for 00-claude-code-setting, 32-notion-writer, 43-jamie-youtube-manager

Documentation Updates:
- CLAUDE.md: Updated skill inventory (23 skills)
- AUDIT_REPORT.md: Current completion status (91%)
- Archived REFACTORING_PLAN.md (most tasks complete)

Removed:
- ga-agent-skills/ (moved to separate repo ~/Project/dintel-ga4-agent)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 18:42:39 +07:00
parent ae193d5e08
commit b69e4b6f3a
100 changed files with 655 additions and 1812 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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 };

View File

@@ -0,0 +1,6 @@
# 32-ourdigital-presentation dependencies
notion-client>=2.0.0
python-pptx>=0.6.21
jinja2>=3.1.0
pyyaml>=6.0.0
python-dotenv>=1.0.0

View File

@@ -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()

View File

@@ -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()