diff --git a/src/landppt/services/ppt_service.py b/src/landppt/services/ppt_service.py new file mode 100644 index 0000000..e22ae58 --- /dev/null +++ b/src/landppt/services/ppt_service.py @@ -0,0 +1,638 @@ +""" +PPT Service for generating presentations +""" + +import json +import re +from typing import Dict, Any, List, Optional +from ..api.models import PPTGenerationRequest, PPTOutline +import docx +import PyPDF2 +import io + +class PPTService: + """Service for PPT generation and processing""" + + def __init__(self): + self.scenario_configs = { + "general": { + "color_scheme": "#2E86AB", + "font_family": "Arial, sans-serif", + "style_class": "general-theme" + }, + "tourism": { + "color_scheme": "#27AE60", + "font_family": "Georgia, serif", + "style_class": "tourism-theme" + }, + "education": { + "color_scheme": "#E74C3C", + "font_family": "Comic Sans MS, cursive", + "style_class": "education-theme" + }, + "analysis": { + "color_scheme": "#34495E", + "font_family": "Helvetica, sans-serif", + "style_class": "analysis-theme" + }, + "history": { + "color_scheme": "#8B4513", + "font_family": "Times New Roman, serif", + "style_class": "history-theme" + }, + "technology": { + "color_scheme": "#9B59B6", + "font_family": "Roboto, sans-serif", + "style_class": "technology-theme" + }, + "business": { + "color_scheme": "#1F4E79", + "font_family": "Calibri, sans-serif", + "style_class": "business-theme" + } + } + + async def generate_ppt(self, task_id: str, request: PPTGenerationRequest) -> Dict[str, Any]: + """Generate complete PPT based on request""" + try: + # Step 1: Generate outline + outline = await self.generate_outline(request) + + # Step 2: Generate slides HTML + slides_html = await self.generate_slides_from_outline(outline, request.scenario) + + return { + "success": True, + "task_id": task_id, + "outline": outline, + "slides_html": slides_html + } + + except Exception as e: + return { + "success": False, + "task_id": task_id, + "error": str(e) + } + + async def generate_outline(self, request: PPTGenerationRequest) -> PPTOutline: + """Generate PPT outline based on request""" + topic = request.topic + scenario = request.scenario + language = request.language + ppt_style = request.ppt_style + custom_style_prompt = request.custom_style_prompt + description = request.description + # Generate slides based on scenario + slides = [] + + # Title slide + slides.append({ + "id": 1, + "type": "title", + "title": topic, + "subtitle": "专业演示" if language == "zh" else "Professional Presentation", + "content": "" + }) + + # Agenda slide + slides.append({ + "id": 2, + "type": "agenda", + "title": "目录" if language == "zh" else "Agenda", + "subtitle": "", + "content": self._generate_agenda_content(scenario, language,) + }) + + # Content slides based on scenario + content_slides = self._generate_content_slides(topic, scenario, language) + slides.extend(content_slides) + + # Thank you slide + slides.append({ + "id": len(slides) + 1, + "type": "thankyou", + "title": "谢谢" if language == "zh" else "Thank You", + "subtitle": "感谢聆听" if language == "zh" else "Thank you for your attention", + "content": "" + }) + + return PPTOutline( + title=topic, + slides=slides, + metadata={ + "scenario": scenario, + "language": language, + "total_slides": len(slides), + "generated_at": "2024-01-01T00:00:00Z" + } + ) + + async def generate_slides_from_outline(self, outline: PPTOutline, scenario: str) -> str: + """Generate HTML slides from outline""" + config = self.scenario_configs.get(scenario, self.scenario_configs["general"]) + + # Generate CSS styles + css_styles = self._generate_css_styles(config) + + # Generate HTML for each slide + slides_html = [] + for slide in outline.slides: + slide_html = self._generate_slide_html(slide, config) + slides_html.append(slide_html) + + # Combine into complete HTML document + complete_html = f""" + + + + + + {outline.title} + + + +
+
+ {''.join(slides_html)} +
+ +
+ + + + + """ + + return complete_html + + async def process_uploaded_file(self, filename: str, content: bytes, file_type: str) -> str: + """Process uploaded file and extract content""" + try: + if file_type == ".docx": + return self._process_docx(content) + elif file_type == ".pdf": + return self._process_pdf(content) + elif file_type in [".txt", ".md"]: + return content.decode('utf-8') + else: + raise ValueError(f"Unsupported file type: {file_type}") + + except Exception as e: + raise Exception(f"Error processing file: {str(e)}") + + def _process_docx(self, content: bytes) -> str: + """Process DOCX file and extract text""" + doc = docx.Document(io.BytesIO(content)) + text_content = [] + + for paragraph in doc.paragraphs: + if paragraph.text.strip(): + text_content.append(paragraph.text.strip()) + + return "\n".join(text_content) + + def _process_pdf(self, content: bytes) -> str: + """Process PDF file and extract text""" + pdf_reader = PyPDF2.PdfReader(io.BytesIO(content)) + text_content = [] + + for page in pdf_reader.pages: + text = page.extract_text() + if text.strip(): + text_content.append(text.strip()) + + return "\n".join(text_content) + + def _generate_agenda_content(self, scenario: str, language: str) -> str: + """Generate agenda content based on scenario""" + agenda_templates = { + "general": { + "zh": ["引言", "主要内容", "案例分析", "总结"], + "en": ["Introduction", "Main Content", "Case Study", "Conclusion"] + }, + "tourism": { + "zh": ["目的地概览", "主要景点", "行程安排", "实用信息"], + "en": ["Destination Overview", "Main Attractions", "Itinerary", "Practical Information"] + }, + "education": { + "zh": ["学习目标", "核心概念", "实例说明", "互动活动", "总结回顾"], + "en": ["Learning Objectives", "Key Concepts", "Examples", "Activities", "Summary"] + }, + "analysis": { + "zh": ["问题陈述", "研究方法", "研究发现", "深入分析", "建议方案"], + "en": ["Problem Statement", "Methodology", "Findings", "Analysis", "Recommendations"] + }, + "history": { + "zh": ["历史背景", "时间线", "关键事件", "重要意义", "历史影响"], + "en": ["Background", "Timeline", "Key Events", "Significance", "Legacy"] + }, + "technology": { + "zh": ["技术概览", "核心功能", "优势特点", "实施方案", "未来展望"], + "en": ["Overview", "Features", "Benefits", "Implementation", "Future"] + }, + "business": { + "zh": ["执行摘要", "问题分析", "解决方案", "市场分析", "财务预测"], + "en": ["Executive Summary", "Problem", "Solution", "Market Analysis", "Financial Projections"] + } + } + + template = agenda_templates.get(scenario, agenda_templates["general"]) + items = template.get(language, template["en"]) + + return "\n".join([f"• {item}" for item in items]) + + def _generate_content_slides(self, topic: str, scenario: str, language: str) -> List[Dict[str, Any]]: + """Generate content slides based on topic and scenario""" + slides = [] + + # Get agenda items to create content slides + agenda_templates = { + "general": { + "zh": ["引言", "主要内容", "案例分析", "总结"], + "en": ["Introduction", "Main Content", "Case Study", "Conclusion"] + }, + "tourism": { + "zh": ["目的地概览", "主要景点", "行程安排", "实用信息"], + "en": ["Destination Overview", "Main Attractions", "Itinerary", "Practical Information"] + }, + "education": { + "zh": ["学习目标", "核心概念", "实例说明", "互动活动", "总结回顾"], + "en": ["Learning Objectives", "Key Concepts", "Examples", "Activities", "Summary"] + }, + "analysis": { + "zh": ["问题陈述", "研究方法", "研究发现", "深入分析", "建议方案"], + "en": ["Problem Statement", "Methodology", "Findings", "Analysis", "Recommendations"] + }, + "history": { + "zh": ["历史背景", "时间线", "关键事件", "重要意义", "历史影响"], + "en": ["Background", "Timeline", "Key Events", "Significance", "Legacy"] + }, + "technology": { + "zh": ["技术概览", "核心功能", "优势特点", "实施方案", "未来展望"], + "en": ["Overview", "Features", "Benefits", "Implementation", "Future"] + }, + "business": { + "zh": ["执行摘要", "问题分析", "解决方案", "市场分析", "财务预测"], + "en": ["Executive Summary", "Problem", "Solution", "Market Analysis", "Financial Projections"] + } + } + + template = agenda_templates.get(scenario, agenda_templates["general"]) + items = template.get(language, template["en"]) + + for i, item in enumerate(items, 3): # Start from slide 3 (after title and agenda) + content = self._generate_slide_content(topic, item, scenario, language) + slides.append({ + "id": i, + "type": "content", + "title": item, + "subtitle": "", + "content": content + }) + + return slides + + def _generate_slide_content(self, topic: str, section: str, scenario: str, language: str) -> str: + """Generate content for a specific slide section""" + # This is a simplified content generation + + if language == "zh": + content_templates = { + "引言": f"• {topic}的重要性\n• 本次演示的目标\n• 主要讨论内容概览", + "主要内容": f"• {topic}的核心要点\n• 关键特征和优势\n• 实际应用场景", + "案例分析": f"• 成功案例展示\n• 实施过程分析\n• 经验教训总结", + "总结": f"• {topic}的主要收获\n• 关键要点回顾\n• 下一步行动计划" + } + return content_templates.get(section, f"• 关于{section}的要点\n• 详细说明和分析\n• 相关案例或数据") + else: + content_templates = { + "Introduction": f"• Importance of {topic}\n• Objectives of this presentation\n• Overview of main topics", + "Main Content": f"• Core aspects of {topic}\n• Key features and benefits\n• Practical applications", + "Case Study": f"• Success story showcase\n• Implementation process\n• Lessons learned", + "Conclusion": f"• Key takeaways from {topic}\n• Summary of main points\n• Next steps and action plan" + } + return content_templates.get(section, f"• Key points about {section}\n• Detailed explanation and analysis\n• Related examples or data") + + def _generate_css_styles(self, config: Dict[str, Any]) -> str: + """Generate CSS styles for the presentation""" + return f""" + * {{ + margin: 0; + padding: 0; + box-sizing: border-box; + }} + + body {{ + font-family: {config['font_family']}; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + }} + + .presentation-container {{ + width: 1280px; + height: 720px; + background: white; + border-radius: 15px; + box-shadow: 0 20px 40px rgba(0,0,0,0.1); + overflow: hidden; + margin: 0 auto; + }} + + .slides-wrapper {{ + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + }} + + .slide {{ + display: none; + padding: 80px; + width: 100%; + height: 100%; + background: white; + position: relative; + box-sizing: border-box; + }} + + .slide.active {{ + display: block; + }} + + .slide h1 {{ + color: {config['color_scheme']}; + font-size: 2.5em; + margin-bottom: 20px; + text-align: center; + font-weight: bold; + }} + + .slide h2 {{ + color: {config['color_scheme']}; + font-size: 2em; + margin-bottom: 30px; + border-bottom: 3px solid {config['color_scheme']}; + padding-bottom: 10px; + }} + + .slide h3 {{ + color: #555; + font-size: 1.2em; + margin-bottom: 20px; + text-align: center; + font-style: italic; + }} + + .slide .content {{ + font-size: 1.2em; + line-height: 1.8; + color: #333; + white-space: pre-line; + }} + + .slide.title-slide {{ + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: linear-gradient(135deg, {config['color_scheme']}22, {config['color_scheme']}11); + }} + + .slide.title-slide h1 {{ + font-size: 3.5em; + margin-bottom: 30px; + color: {config['color_scheme']}; + }} + + .slide.title-slide h3 {{ + font-size: 1.5em; + color: #666; + }} + + .slide.agenda-slide ul {{ + list-style: none; + padding: 0; + }} + + .slide.agenda-slide li {{ + padding: 15px 0; + font-size: 1.3em; + border-bottom: 1px solid #eee; + color: #555; + }} + + .slide.content-slide .content {{ + margin-top: 20px; + }} + + .slide.thankyou-slide {{ + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: linear-gradient(135deg, {config['color_scheme']}22, {config['color_scheme']}11); + }} + + .navigation {{ + background: {config['color_scheme']}; + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + color: white; + }} + + .navigation button {{ + background: rgba(255,255,255,0.2); + border: none; + color: white; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-size: 1em; + transition: background 0.3s; + }} + + .navigation button:hover {{ + background: rgba(255,255,255,0.3); + }} + + .navigation button:disabled {{ + opacity: 0.5; + cursor: not-allowed; + }} + + #slideCounter {{ + font-weight: bold; + font-size: 1.1em; + }} + + /* Responsive design for 1280x720 slides */ + @media (max-width: 1280px) {{ + .presentation-container {{ + width: 100vw; + height: 56.25vw; + max-height: 100vh; + transform: none; + }} + }} + + @media (max-width: 1024px) {{ + .presentation-container {{ + transform: scale(0.8); + transform-origin: center; + }} + }} + + @media (max-width: 768px) {{ + .presentation-container {{ + transform: scale(0.6); + transform-origin: center; + }} + + .slide {{ + padding: 40px 30px; + }} + + .slide h1 {{ + font-size: 2em; + }} + + .slide h2 {{ + font-size: 1.8em; + }} + + .slide .content {{ + font-size: 1.1em; + }} + + .navigation {{ + padding: 15px; + }} + + .navigation button {{ + padding: 10px 15px; + font-size: 0.9em; + }} + }} + + @media (max-width: 480px) {{ + .presentation-container {{ + transform: scale(0.4); + transform-origin: center; + }} + }} + """ + + def _generate_slide_html(self, slide: Dict[str, Any], config: Dict[str, Any]) -> str: + """Generate HTML for a single slide""" + slide_type = slide.get("type", "content") + slide_id = slide.get("id", 1) + title = slide.get("title", "") + subtitle = slide.get("subtitle", "") + content = slide.get("content", "") + + if slide_type == "title": + return f""" +
+

{title}

+ {f'

{subtitle}

' if subtitle else ''} +
+ """ + + elif slide_type == "agenda": + # Convert content to HTML list + agenda_items = [item.strip() for item in content.split('\n') if item.strip()] + agenda_html = '' + + return f""" +
+

{title}

+ {agenda_html} +
+ """ + + elif slide_type == "thankyou": + return f""" +
+

{title}

+ {f'

{subtitle}

' if subtitle else ''} +
+ """ + + else: # content slide + return f""" +
+

{title}

+ {f'

{subtitle}

' if subtitle else ''} +
{content}
+
+ """