From e0026d01724e603376729353450e5b120a64b968 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:00:45 +0000 Subject: [PATCH] Implement features: speaker notes, report types, layout categories, and content sizing - Added `generate_speaker_notes` to `ContentGenerator` and integrated it into `ExecutionOrchestrator` to add conversational notes to slides. - Implemented `report_mode` support in `PlanGeneratorOrchestrator` and `flask_app.py`, allowing users to select report types (Sales, Executive, etc.) with specific prompts. - Added UI tabs in `html_ui.py` for selecting report types. - Enhanced `layout_analyzer.py` to classify layouts into 6 categories (blank, cover, section_divider, kpicards, small_content, large_content). - Implemented content sizing logic (`_calculate_word_limit`) in `ExecutionOrchestrator` to prevent text overflow by dynamically setting word limits based on placeholder dimensions. - Updated `ContentGenerator` to respect `max_words_per_bullet`. --- flask_app.py | 4 +- src/slidedeckai/agents/content_generator.py | 55 ++++++++++++++-- src/slidedeckai/agents/core_agents.py | 23 ++++++- .../agents/execution_orchestrator.py | 62 ++++++++++++++++++- src/slidedeckai/layout_analyzer.py | 57 +++++++++++++++++ src/slidedeckai/ui/html_ui.py | 57 ++++++++++++++++- 6 files changed, 247 insertions(+), 11 deletions(-) diff --git a/flask_app.py b/flask_app.py index e668fbe..b95fc28 100644 --- a/flask_app.py +++ b/flask_app.py @@ -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: @@ -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 diff --git a/src/slidedeckai/agents/content_generator.py b/src/slidedeckai/agents/content_generator.py index 3394bb4..3609595 100644 --- a/src/slidedeckai/agents/content_generator.py +++ b/src/slidedeckai/agents/content_generator.py @@ -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" @@ -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 @@ -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]} \ No newline at end of file + 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." \ No newline at end of file diff --git a/src/slidedeckai/agents/core_agents.py b/src/slidedeckai/agents/core_agents.py index 55e1b50..25dad29 100644 --- a/src/slidedeckai/agents/core_agents.py +++ b/src/slidedeckai/agents/core_agents.py @@ -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: @@ -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: {{ diff --git a/src/slidedeckai/agents/execution_orchestrator.py b/src/slidedeckai/agents/execution_orchestrator.py index b7da58d..2a80fa6 100644 --- a/src/slidedeckai/agents/execution_orchestrator.py +++ b/src/slidedeckai/agents/execution_orchestrator.py @@ -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: @@ -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 @@ -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 @@ -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 diff --git a/src/slidedeckai/layout_analyzer.py b/src/slidedeckai/layout_analyzer.py index 30cf4d2..9f71342 100644 --- a/src/slidedeckai/layout_analyzer.py +++ b/src/slidedeckai/layout_analyzer.py @@ -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: @@ -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, @@ -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, @@ -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, diff --git a/src/slidedeckai/ui/html_ui.py b/src/slidedeckai/ui/html_ui.py index bdf0756..d247c22 100644 --- a/src/slidedeckai/ui/html_ui.py +++ b/src/slidedeckai/ui/html_ui.py @@ -64,6 +64,32 @@ border-color: #2563eb; background: #eff6ff; } + .report-type-section { + margin: 25px 0; + } + .tabs { + display: flex; + gap: 10px; + margin-bottom: 15px; + border-bottom: 2px solid #e5e7eb; + padding-bottom: 2px; + } + .tab { + padding: 10px 20px; + cursor: pointer; + font-weight: 600; + color: #6b7280; + border-bottom: 2px solid transparent; + margin-bottom: -4px; + transition: all 0.2s; + } + .tab:hover { + color: #2563eb; + } + .tab.active { + color: #2563eb; + border-bottom: 2px solid #2563eb; + } .input-group { margin: 20px 0; } @@ -229,6 +255,19 @@ + +
+
Report Type
+
+
🚀 Sales Pitch
+
👔 Executive
+
💼 Professional
+
📊 Report
+
+
+ Focus on value proposition, customer benefits, and call to action. +
+
@@ -280,10 +319,25 @@