comparison love/pdf-mcp/pdf-mcp.py @ 38:cf9caa4abc3e

[Love] FE and BE. Can chat and render images. Also created MCP for powerpoint generations.
author MrJuneJune <me@mrjunejune.com>
date Mon, 01 Dec 2025 20:35:56 -0800
parents
children
comparison
equal deleted inserted replaced
37:fb9bcd3145cb 38:cf9caa4abc3e
1 import os
2 import uuid
3 import json
4 import re
5 from typing import List, Dict
6 import markdown2
7 import requests
8
9 from fastmcp import FastMCP
10
11 from xai_sdk import Client
12 from xai_sdk.chat import system, user
13 from xai_sdk.tools import web_search, code_execution
14
15 XAI_API_KEY = os.getenv("XAI_API_KEY", "no_apis")
16 OUTPUT_DIR = os.getenv("DECK_OUTPUT_DIR", "generated_decks")
17
18 xai_client = Client(api_key=XAI_API_KEY)
19
20 mcp = FastMCP("presentation-generator", port=7776, host="0.0.0.0")
21
22 def generate_summary_and_images(topic: str) -> Dict[str, any]:
23 """Use Grok API with live_search to summarize + extract image URLs."""
24 chat = xai_client.chat.create(
25 model="grok-4-fast",
26 tools=[
27 web_search(),
28 code_execution(),
29 ],
30 )
31
32 SYSTEM_PROMPT = """
33 You are an expert presentation creator. For the given topic, use live_search to fetch the latest, reliable web sources.
34 Summarize into concise, impactful bullet points perfect for slides (max 6-8 lines per slide, start with strong verbs/key facts).
35 Use markdown bullets. Separate logical sections with --- for slide breaks.
36 Also, search for 10-15 high-quality, royalty-free images (e.g., from Unsplash/Pexels via web search) suitable for slides—focus on visuals like diagrams/infographics.
37 Respond ONLY in valid JSON: {"summary": "markdown summary here", "images": ["url1", "url2", ...]}.
38 Ensure images are direct .jpg/.png links, diverse, and relevant.
39 """
40
41 chat.append(system(SYSTEM_PROMPT))
42 response = chat.append(user(f"Topic: {topic}"))
43
44 try:
45 content = response.sample().content
46 data = json.loads(content)
47 return data
48 except json.JSONDecodeError:
49 # Fallback: Parse markdown and extract URLs via regex
50 summary = content.split("images:")[0].strip() if "images:" in content else content
51 img_pattern = r'https?://[^\s<>"]+\.(?:jpg|jpeg|png|gif)(?:\?[^\s<>"]*)?'
52 images = re.findall(img_pattern, content)
53 return {"summary": summary, "images": images[:15]}
54
55 def download_image(url: str, path: str) -> bool:
56 """Download image to local path."""
57 try:
58 img_data = requests.get(url, timeout=10).content
59 os.makedirs(os.path.dirname(path), exist_ok=True)
60 with open(path, 'wb') as f:
61 f.write(img_data)
62 return True
63 except:
64 return False
65
66 def create_presentation_deck(topic: str) -> Dict[str, str | int]:
67 """Generate a complete presentation deck."""
68 # Create unique deck directory
69 deck_id = str(uuid.uuid4())[:8]
70 deck_dir = os.path.join(OUTPUT_DIR, deck_id)
71 os.makedirs(deck_dir, exist_ok=True)
72 os.makedirs(os.path.join(deck_dir, "img"), exist_ok=True)
73
74 # Generate summary and images using Grok
75 data = generate_summary_and_images(topic)
76 markdown_summary = data["summary"]
77 image_urls = data.get("images", [])
78
79 # Download images
80 local_images = []
81 for i, url in enumerate(image_urls[:15]):
82 ext = url.split(".")[-1].split("?")[0] or "jpg"
83 path = os.path.join(deck_dir, "img", f"{i}.{ext}")
84 if download_image(url, path):
85 local_images.append(f"img/{i}.{ext}")
86 else:
87 local_images.append(url)
88
89 # Split summary into slides
90 raw_slides = markdown_summary.split("---")
91 slides = []
92 for i, raw in enumerate(raw_slides):
93 html_content = markdown2.markdown(raw.strip())
94 img = local_images[i] if i < len(local_images) else (local_images[-1] if local_images else "")
95 slides.append({"content": html_content, "image": img})
96
97 # Generate Reveal.js HTML
98 html_content = generate_reveal_html(topic, slides)
99
100 deck_path = os.path.join(deck_dir, "index.html")
101 with open(deck_path, "w", encoding="utf-8") as f:
102 f.write(html_content)
103
104 # Get absolute path
105 abs_path = os.path.abspath(deck_path)
106
107 return {
108 "deck_id": deck_id,
109 "deck_path": abs_path,
110 "markdown_summary": markdown_summary,
111 "num_slides": len(slides),
112 "images_count": len(local_images)
113 }
114
115 def generate_reveal_html(topic: str, slides: List[Dict]) -> str:
116 """Generate Reveal.js HTML presentation."""
117 slides_html = ""
118 for slide in slides:
119 img_html = f'<img src="{slide["image"]}" style="max-width:50%;float:right;margin-left:20px;" />' if slide["image"] else ""
120 slides_html += f"""
121 <section>
122 {img_html}
123 <div style="text-align:left;">
124 {slide["content"]}
125 </div>
126 </section>
127 """
128
129 return f"""<!DOCTYPE html>
130 <html>
131 <head>
132 <meta charset="utf-8">
133 <title>{topic} - Presentation</title>
134 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.3.1/reset.min.css">
135 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.3.1/reveal.min.css">
136 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.3.1/theme/black.min.css">
137 </head>
138 <body>
139 <div class="reveal">
140 <div class="slides">
141 <section>
142 <h1>{topic}</h1>
143 <p>Generated with Grok AI</p>
144 </section>
145 {slides_html}
146 </div>
147 </div>
148 <script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.3.1/reveal.min.js"></script>
149 <script>
150 Reveal.initialize({{
151 hash: true,
152 transition: 'slide'
153 }});
154 </script>
155 </body>
156 </html>"""
157
158 # FastMCP Tool Definitions
159 @mcp.tool()
160 def generate_presentation(topic: str) -> str:
161 """Generate a complete PowerPoint-style presentation deck on any topic using Grok AI.
162
163 This tool researches the topic using live web search, creates concise slide content,
164 finds relevant images, and generates a Reveal.js HTML presentation that can be viewed in browser.
165
166 Args:
167 topic: The topic for the presentation (e.g., 'Dogs in Korea', 'Quantum Computing Basics', 'Climate Change
168
169 Returns:
170 A summary of the generated presentation with file location
171 """
172 try:
173 result = create_presentation_deck(topic)
174
175 # Construct the public URL
176 public_url = f"https://slides.babocoder.com/{result['deck_id']}/index.html"
177
178 # Return JSON response as Grok expects with instruction
179 json_response = json.dumps({
180 "file_path": public_url,
181 "type": "html",
182 "metadata": {
183 "topic": topic,
184 "deck_id": result['deck_id'],
185 "num_slides": result['num_slides'],
186 "images_count": result['images_count']
187 }
188 })
189
190 response = f"""Presentation generated successfully!
191
192 IMPORTANT: Return this exact JSON response to the client:
193
194 {json_response}
195
196 DO NOT modify or explain this JSON. Send it directly to the client as-is."""
197
198 return response
199 except Exception as e:
200 return f"❌ Error generating presentation: {str(e)}\n\nPlease try again or check the server logs."
201
202 @mcp.tool()
203 def get_presentation_preview(topic: str) -> str:
204 """Generate just the markdown summary for a presentation topic without creating the full deck.
205
206 Useful for previewing content or getting a quick outline before generating slides.
207
208 Args:
209 topic: The topic to preview
210
211 Returns:
212 A markdown preview of the presentation content
213 """
214 try:
215 data = generate_summary_and_images(topic)
216 markdown_summary = data["summary"]
217
218 response = f"""📝 Presentation Preview for: {topic}
219
220 {markdown_summary}
221
222 ---
223 **Images Found:** {len(data.get('images', []))}
224
225 To generate the full presentation deck with slides, use the 'generate_presentation' tool.
226 """
227 return response
228
229 except Exception as e:
230 return f"❌ Error generating preview: {str(e)}"
231
232 if __name__ == "__main__":
233 os.makedirs(OUTPUT_DIR, exist_ok=True)
234 mcp.run(transport="streamable-http")
235
236