Mercurial
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 |