Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def create_plan():
query = data.get('query', '').strip()
template_key = data.get('template', 'Basic')
search_mode = data.get('search_mode', 'normal')
report_mode = data.get('report_type', 'report')
num_sections = data.get('num_sections', None)

if not query:
Expand Down Expand Up @@ -165,7 +166,8 @@ def create_plan():
# Use enhanced orchestrator
orchestrator = PlanGeneratorOrchestrator(
api_key=api_key,
search_mode=search_mode
search_mode=search_mode,
report_mode=report_mode
)

# Generate plan with enforced diversity
Expand Down
55 changes: 51 additions & 4 deletions src/slidedeckai/agents/content_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ def generate_subtitle(self, slide_title: str, purpose: str,
return "Analysis"

def generate_bullets(self, slide_title: str, purpose: str,
search_facts: List[str], max_bullets: int = 5) -> List[str]:
search_facts: List[str], max_bullets: int = 5,
max_words_per_bullet: int = 15) -> List[str]:
"""
Generate bullet points from search facts
Generate bullet points from search facts with strict length control
"""

facts_text = "\n".join(search_facts) if search_facts else "No data available"
Expand All @@ -78,7 +79,7 @@ def generate_bullets(self, slide_title: str, purpose: str,

Requirements:
- Generate EXACTLY {max_bullets} bullet points
- Each bullet: 10-20 words
- Each bullet MUST be under {max_words_per_bullet} words to fit layout
- Include QUANTITATIVE data (numbers, percentages)
- Professional, executive-level tone
- NO preamble, ONLY bullet points
Expand Down Expand Up @@ -259,4 +260,50 @@ def generate_kpi(self, slide_title: str, fact: str) -> Dict:

except Exception as e:
logger.error(f"KPI generation failed: {e}")
return {"value": "N/A", "label": slide_title[:20]}
return {"value": "N/A", "label": slide_title[:20]}

def generate_speaker_notes(self, slide_title: str, bullets: List[str], key_facts: List[str]) -> str:
"""
Generate conversational speaker notes
"""

bullet_text = "\n- ".join(bullets) if bullets else "N/A"
fact_text = "\n- ".join(key_facts[:3]) if key_facts else "N/A"

prompt = f"""Generate speaker notes for this slide:

Title: {slide_title}

Visual Content:
- {bullet_text}

Supporting Data:
- {fact_text}

Requirements:
- Conversational tone ("Welcome to this slide...", "Here we see...")
- Explain the key points, don't just read them
- Add a transition sentence to the next topic if applicable
- Keep it under 150 words
- Professional and engaging

Return ONLY the speaker notes text."""

try:
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "Generate professional speaker notes."},
{"role": "user", "content": prompt}
],
temperature=0.7,
max_tokens=250
)

notes = response.choices[0].message.content.strip()
logger.info(f" ✓ Speaker notes generated")
return notes

except Exception as e:
logger.error(f"Speaker notes generation failed: {e}")
return f"Speaker notes for {slide_title}: Please cover the key points listed on the slide."
23 changes: 21 additions & 2 deletions src/slidedeckai/agents/core_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,23 @@ class ResearchPlan(BaseModel):
class PlanGeneratorOrchestrator:
"""FIX #1 & #6: Remove fallbacks, strengthen validation"""

def __init__(self, api_key: str, search_mode: str = 'normal'):
def __init__(self, api_key: str, search_mode: str = 'normal', report_mode: str = 'report'):
self.api_key = api_key
self.search_mode = search_mode
self.report_mode = report_mode
self.client = OpenAI(api_key=api_key)
self.model = "gpt-4o-mini"
self.used_topics: Set[str] = set()

# Mode-specific prompts
self.mode_prompts = {
'sales': "Focus on value proposition, customer benefits, market opportunity, and call to action. Tone: Persuasive and energetic.",
'executive': "Focus on high-level strategy, key metrics, financial impact, and critical decisions. Tone: Concise and authoritative.",
'professional': "Focus on industry standards, best practices, detailed analysis, and clear methodology. Tone: Formal and objective.",
'report': "Focus on comprehensive coverage, data accuracy, structured findings, and detailed conclusions. Tone: Informative and balanced."
}

self.mode_prompt_add = self.mode_prompts.get(report_mode, self.mode_prompts['report'])

def generate_plan(self, user_query: str, template_layouts: Dict,
num_sections: Optional[int] = None) -> ResearchPlan:
Expand Down Expand Up @@ -372,17 +383,25 @@ def _llm_generate_subtitle_guaranteed_unique(self, purpose: str, position: str,
# Keep all other existing methods unchanged
def _llm_deep_analysis(self, query: str) -> Dict:
"""Existing - unchanged"""

# Adjust for deep mode
aspect_count = "6-10" if self.search_mode == 'normal' else "10-15"
deep_instruction = "Perform a DEEP DRILL DOWN analysis." if self.search_mode == 'deep' else ""

prompt = f"""You are an expert business analyst. Analyze this presentation request:

"{query}"

Style/Mode: {self.mode_prompt_add}
{deep_instruction}

Your task:
1. Understand the MAIN SUBJECT (company, topic, product, etc.)
2. Understand the CONTEXT (financial report, market analysis, product launch, etc.)
3. Identify ALL DISTINCT ASPECTS that should be covered
- Think broadly: metrics, trends, comparisons, breakdowns, outlook, risks, etc.
- Be comprehensive but avoid overlap
- Aim for 6-10 distinct aspects
- Aim for {aspect_count} distinct aspects

Return ONLY valid JSON:
{{
Expand Down
62 changes: 59 additions & 3 deletions src/slidedeckai/agents/execution_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,18 @@ def _gen_for_ph(ph_id, ph_info):
)
return (ph_id, {'type': 'kpi', 'kpi_data': kpi})
else:
max_bullets = self._calculate_max_bullets(ph_info.get('area', 5))
max_words = self._calculate_word_limit(
ph_info.get('width', 5),
ph_info.get('height', 5),
max_bullets
)
bullets = self.content_generator.generate_bullets(
section.section_title,
section.section_purpose,
relevant_facts,
max_bullets=self._calculate_max_bullets(ph_info.get('area', 5))
max_bullets=max_bullets,
max_words_per_bullet=max_words
)
return (ph_id, {'type': 'bullets', 'bullets': bullets})
except Exception as e:
Expand Down Expand Up @@ -457,6 +464,34 @@ def _generate_slide_smart(self, section, search_results: Dict,
'error': str(e)
})

# 7. ADD SPEAKER NOTES (NEW)
try:
# Collect bullets and facts for context
bullets_context = []
facts_context = []

for ph_log in slide_log['placeholders']:
if 'bullets' in ph_log:
bullets_context.extend(ph_log['bullets'])

# Simple fact extraction from search results
for res_list in search_results.values():
facts_context.extend(res_list[:2])

speaker_notes = self.content_generator.generate_speaker_notes(
section.section_title,
bullets_context,
facts_context
)

if slide.has_notes_slide:
notes_slide = slide.notes_slide
notes_slide.notes_text_frame.text = speaker_notes
logger.info(" ✓ Speaker notes added")

except Exception as e:
logger.warning(f" ⚠️ Failed to add speaker notes: {e}")

logger.info(f" ✅ Complete")
return slide_log

Expand Down Expand Up @@ -1037,13 +1072,19 @@ def _fill_content(self, placeholder, ph_id: int, ph_info: Dict,
if query.query in search_results:
relevant_facts.extend(search_results[query.query])

max_bullets = self._calculate_max_bullets(ph_info['area'])
max_bullets = self._calculate_max_bullets(ph_info.get('area', 5))
max_words = self._calculate_word_limit(
ph_info.get('width', 0),
ph_info.get('height', 0),
max_bullets
)

bullets = self.content_generator.generate_bullets(
section.section_title,
section.section_purpose,
relevant_facts,
max_bullets=max_bullets
max_bullets=max_bullets,
max_words_per_bullet=max_words
)

text_frame = placeholder.text_frame
Expand Down Expand Up @@ -1089,6 +1130,21 @@ def _calculate_max_bullets(self, area: float) -> int:
else:
return 10

def _calculate_word_limit(self, width: float, height: float, max_bullets: int) -> int:
"""Calculate max words per bullet to fit in placeholder"""
if height <= 0 or width <= 0 or max_bullets <= 0:
return 15

# Estimate based on standard 18pt font (~0.3 inch line height)
lines_available = height / 0.3
lines_per_bullet = lines_available / max_bullets

# Estimate words per line (width * 8 chars/inch / 6 chars/word)
words_per_line = (width * 8) / 6

limit = int(lines_per_bullet * words_per_line)
return max(5, min(limit, 40)) # Clamp between 5 and 40

def _calculate_font_size_from_area(self, area: float, size_type: str) -> int:
"""FIX #4: Calculate from template base size"""
from pptx.util import Pt
Expand Down
57 changes: 57 additions & 0 deletions src/slidedeckai/layout_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class LayoutCapability:
semantic_story_type: str = "general_content"
executive_suitability: float = 0.0
content_density_recommendation: Dict = None
layout_category: str = "large_content" # NEW: blank, cover, section_divider, kpicards, small_content, large_content

def __post_init__(self):
if self.semantic_sections is None:
Expand Down Expand Up @@ -305,6 +306,13 @@ def _analyze_single_layout(self, idx: int, layout) -> LayoutCapability:
semantic_sections,
story_type
)

# ADDED: Determine specific layout category
layout_category = self._determine_layout_category(
has_title, has_subtitle, kpi_grid, content_placeholders,
text_placeholders, semantic_sections, layout.name
)

return LayoutCapability(
idx=idx,
name=layout.name,
Expand Down Expand Up @@ -333,6 +341,7 @@ def _analyze_single_layout(self, idx: int, layout) -> LayoutCapability:
semantic_story_type=story_type, # NEW
executive_suitability=executive_suitability, # NEW
content_density_recommendation=content_density_rec, # NEW
layout_category=layout_category # NEW
)

def _group_placeholders_semantically(self,
Expand Down Expand Up @@ -527,6 +536,54 @@ def _assess_fill_difficulty(self, semantic_sections: List[Dict],
return "medium", 8
return "hard", 9

def _determine_layout_category(self, has_title: bool, has_subtitle: bool,
kpi_grid: Optional[Dict],
content_placeholders: List[PlaceholderInfo],
text_placeholders: List[PlaceholderInfo],
semantic_sections: List[Dict],
layout_name: str) -> str:
"""NEW: Classify into 6 strict categories"""

# 1. Blank
if not content_placeholders and not has_title:
return "blank"

# 2. Cover / Title Only
if has_title and not content_placeholders:
# If it looks like a title slide
if "title" in layout_name.lower() and "only" not in layout_name.lower():
return "cover"
return "section_divider"

# 3. Section Divider
if has_title and not content_placeholders and "section" in layout_name.lower():
return "section_divider"

if len(content_placeholders) == 0 and len(text_placeholders) == 0:
if has_title:
return "section_divider"
return "blank"

# 4. KPI Cards
if kpi_grid:
return "kpicards"

# Check if it has many small boxes
small_boxes = sum(1 for ph in content_placeholders if ph.is_small_box)
if small_boxes >= 4:
return "kpicards"

# 5. Large Content (Charts, Tables, Big Text)
large_areas = sum(1 for ph in content_placeholders if ph.is_large_box)
if large_areas >= 1:
return "large_content"

if len(content_placeholders) == 1 and content_placeholders[0].area > 10:
return "large_content"

# 6. Small Content (Bullets, comparisons, multi-column)
return "small_content"

def _calculate_executive_suitability(self,
visual_balance: float,
complexity_score: float,
Expand Down
Loading