├── .python-version ├── .vscode └── settings.json ├── src ├── landppt │ ├── api │ │ ├── __init__.py │ │ └── openai_compat.py │ ├── core │ │ └── __init__.py │ ├── services │ │ ├── __init__.py │ │ ├── image │ │ │ └── matching │ │ │ │ └── __init__.py │ │ ├── research │ │ │ └── __init__.py │ │ ├── pdf_to_pptx_worker.py │ │ ├── service_instances.py │ │ ├── prompts │ │ │ ├── system_prompts.py │ │ │ ├── content_prompts.py │ │ │ ├── repair_prompts.py │ │ │ └── __init__.py │ │ ├── url_service.py │ │ └── share_service.py │ ├── web │ │ ├── __init__.py │ │ ├── static │ │ │ ├── images │ │ │ │ ├── favicon.ico │ │ │ │ ├── landppt-logo.png │ │ │ │ └── placeholder.svg │ │ │ └── js │ │ │ │ └── modules │ │ │ │ ├── eventBus.js │ │ │ │ ├── domUtils.js │ │ │ │ └── apiClient.js │ │ └── templates │ │ │ ├── error.html │ │ │ ├── index.html │ │ │ ├── result.html │ │ │ └── upload_result.html │ ├── __init__.py │ ├── ai │ │ ├── __init__.py │ │ └── base.py │ ├── auth │ │ └── __init__.py │ ├── database │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── add_speech_scripts_table.py │ │ │ └── add_project_share_fields.py │ │ └── database.py │ ├── utils │ │ └── thread_pool.py │ └── main.py └── summeryanyfile │ ├── graph │ └── __init__.py │ ├── config │ └── __init__.py │ ├── generators │ └── __init__.py │ ├── utils │ ├── __init__.py │ └── logger.py │ ├── core │ ├── __init__.py │ ├── chunkers │ │ ├── __init__.py │ │ ├── base_chunker.py │ │ └── recursive_chunker.py │ └── json_parser.py │ └── __init__.py ├── .claude └── settings.local.json ├── .gitignore ├── uv.toml ├── docker-compose.yml ├── .dockerignore ├── run.py ├── docs └── base_url_configuration.md ├── pyproject.toml ├── docker-healthcheck.sh ├── template_examples ├── 日落大道.json ├── 五彩斑斓的黑.json ├── 科技风.json ├── 商务.json ├── 清新笔记.json ├── 素白风.json ├── 星月蓝.json ├── 中式书卷风.json ├── 森林绿.json ├── 竹简风.json ├── 终端风.json ├── 宣纸风.json └── 简约答辩风.json ├── .github └── workflows │ ├── docker-release.yml │ └── docker-build.yml └── Dockerfile /.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /src/landppt/api/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | API modules for LandPPT 3 | """ 4 | -------------------------------------------------------------------------------- /src/landppt/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core modules for LandPPT 3 | """ 4 | -------------------------------------------------------------------------------- /src/landppt/services/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Service modules for LandPPT 3 | """ 4 | -------------------------------------------------------------------------------- /src/landppt/web/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Web interface modules for LandPPT 3 | """ 4 | 5 | from .routes import router 6 | -------------------------------------------------------------------------------- /src/landppt/web/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sligter/LandPPT/HEAD/src/landppt/web/static/images/favicon.ico -------------------------------------------------------------------------------- /src/landppt/web/static/images/landppt-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sligter/LandPPT/HEAD/src/landppt/web/static/images/landppt-logo.png -------------------------------------------------------------------------------- /src/landppt/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | LandPPT - AI-powered PPT generation platform with OpenAI-compatible API 3 | """ 4 | 5 | __version__ = "0.1.0" 6 | -------------------------------------------------------------------------------- /src/landppt/services/image/matching/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 图片智能匹配模块 3 | """ 4 | 5 | from .image_matcher import ImageMatcher 6 | 7 | __all__ = ['ImageMatcher'] 8 | -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(find . -name \"*.md\" -type f -exec grep -l \"^!image$\" {} ;)" 5 | ], 6 | "deny": [], 7 | "ask": [] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/summeryanyfile/graph/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 图模块 - 包含LangGraph工作流节点和定义 3 | """ 4 | 5 | from .nodes import GraphNodes 6 | from .workflow import WorkflowManager 7 | 8 | __all__ = [ 9 | "GraphNodes", 10 | "WorkflowManager", 11 | ] 12 | -------------------------------------------------------------------------------- /src/summeryanyfile/config/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 配置模块 - 包含设置管理和提示模板 3 | """ 4 | 5 | from .settings import Settings, load_settings 6 | from .prompts import PromptTemplates 7 | 8 | __all__ = [ 9 | "Settings", 10 | "load_settings", 11 | "PromptTemplates", 12 | ] 13 | -------------------------------------------------------------------------------- /src/summeryanyfile/generators/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 生成器模块 - 包含PPT生成器和处理链 3 | """ 4 | 5 | from .chains import ChainManager 6 | 7 | # Note: PPTOutlineGenerator is not imported here to avoid langgraph dependency issues 8 | # Import it directly when needed: from summeryanyfile.generators.ppt_generator import PPTOutlineGenerator 9 | 10 | __all__ = [ 11 | "ChainManager", 12 | ] 13 | -------------------------------------------------------------------------------- /src/landppt/ai/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | AI modules for LandPPT 3 | """ 4 | 5 | from .providers import AIProviderFactory, get_ai_provider, get_role_provider 6 | from .base import AIProvider, AIMessage, AIResponse, MessageRole 7 | 8 | __all__ = [ 9 | "AIProviderFactory", 10 | "get_ai_provider", 11 | "get_role_provider", 12 | "AIProvider", 13 | "AIMessage", 14 | "AIResponse", 15 | "MessageRole" 16 | ] 17 | -------------------------------------------------------------------------------- /src/summeryanyfile/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 工具模块 - 包含文件处理、日志、验证等工具 3 | """ 4 | 5 | from .file_handler import FileHandler 6 | from .logger import setup_logging, get_logger 7 | from .validators import validate_file_path, validate_url, validate_config 8 | 9 | __all__ = [ 10 | "FileHandler", 11 | "setup_logging", 12 | "get_logger", 13 | "validate_file_path", 14 | "validate_url", 15 | "validate_config", 16 | ] 17 | -------------------------------------------------------------------------------- /src/landppt/web/static/js/modules/eventBus.js: -------------------------------------------------------------------------------- 1 | const bus = new EventTarget(); 2 | 3 | export function emit(eventName, detail) { 4 | bus.dispatchEvent(new CustomEvent(eventName, { detail })); 5 | } 6 | 7 | export function on(eventName, handler) { 8 | const wrapped = (event) => handler(event.detail); 9 | bus.addEventListener(eventName, wrapped); 10 | return () => bus.removeEventListener(eventName, wrapped); 11 | } 12 | 13 | export default bus; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | lib/ 7 | research_reports/ 8 | wheels/ 9 | .claude/ 10 | docs/ 11 | *.egg-info 12 | requires.md 13 | prompts.md 14 | WARP.md 15 | CLAUDE.md 16 | .env 17 | # Virtual environments 18 | .venv 19 | 20 | # Temporary files and cache 21 | temp/ 22 | *.tmp 23 | *.cache 24 | *.db 25 | 26 | 27 | # uv cache and lock files 28 | .uv-cache/ 29 | # Keep uv.lock for reproducible builds 30 | -------------------------------------------------------------------------------- /src/summeryanyfile/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 核心模块 - 包含数据模型、文档处理、LLM管理等核心功能 3 | """ 4 | 5 | from .models import SlideInfo, PPTState, ChunkStrategy 6 | from .document_processor import DocumentProcessor 7 | from .llm_manager import LLMManager 8 | from .json_parser import JSONParser 9 | from .file_cache_manager import FileCacheManager 10 | 11 | __all__ = [ 12 | "SlideInfo", 13 | "PPTState", 14 | "ChunkStrategy", 15 | "DocumentProcessor", 16 | "LLMManager", 17 | "JSONParser", 18 | "FileCacheManager", 19 | ] 20 | -------------------------------------------------------------------------------- /uv.toml: -------------------------------------------------------------------------------- 1 | # uv configuration file for LandPPT 2 | 3 | # Extra index URLs for additional package sources 4 | extra-index-url = ["https://pypi.apryse.com"] 5 | 6 | # Use the fastest available index 7 | index-strategy = "first-index" 8 | 9 | # Cache configuration - use project temp directory 10 | cache-dir = "temp/.uv-cache" 11 | 12 | # Python version preference 13 | python-preference = "managed" 14 | 15 | # Development dependencies (defined in pyproject.toml instead) 16 | # This section is moved to pyproject.toml [project.optional-dependencies] 17 | -------------------------------------------------------------------------------- /src/summeryanyfile/core/chunkers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 分块模块 - 提供各种文档分块策略 3 | """ 4 | 5 | from .base_chunker import BaseChunker, DocumentChunk 6 | from .semantic_chunker import SemanticChunker 7 | from .recursive_chunker import RecursiveChunker 8 | from .paragraph_chunker import ParagraphChunker 9 | from .hybrid_chunker import HybridChunker 10 | from .fast_chunker import FastChunker 11 | 12 | __all__ = [ 13 | "BaseChunker", 14 | "DocumentChunk", 15 | "SemanticChunker", 16 | "RecursiveChunker", 17 | "ParagraphChunker", 18 | "HybridChunker", 19 | "FastChunker" 20 | ] 21 | -------------------------------------------------------------------------------- /src/landppt/web/static/images/placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 图片加载失败 5 | 6 | 7 | Image not found 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/summeryanyfile/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 通用文本转PPT大纲生成器 3 | 4 | 基于LLM的智能文档分析和演示大纲生成工具 5 | """ 6 | 7 | __version__ = "0.1.0" 8 | __author__ = "SummeryAnyFile Team" 9 | __description__ = "通用文本转PPT大纲生成器 - 基于LLM的智能文档分析和演示大纲生成工具" 10 | 11 | from .core.models import SlideInfo, PPTState 12 | from .core.markitdown_converter import MarkItDownConverter 13 | from .core.document_processor import DocumentProcessor 14 | 15 | # Note: PPTOutlineGenerator is not imported here to avoid langgraph dependency issues 16 | # Import it directly when needed: from summeryanyfile.generators.ppt_generator import PPTOutlineGenerator 17 | 18 | __all__ = [ 19 | "SlideInfo", 20 | "PPTState", 21 | "MarkItDownConverter", 22 | "DocumentProcessor", 23 | "__version__", 24 | "__author__", 25 | "__description__", 26 | ] 27 | -------------------------------------------------------------------------------- /src/landppt/services/research/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Research module for LandPPT 3 | 4 | This module provides comprehensive research functionality including: 5 | - SearXNG content search provider 6 | - Web content extraction pipeline 7 | - Enhanced research service with multiple providers 8 | """ 9 | 10 | from .searxng_provider import SearXNGContentProvider, SearXNGSearchResult, SearXNGSearchResponse 11 | from .content_extractor import WebContentExtractor, ExtractedContent 12 | from .enhanced_research_service import ( 13 | EnhancedResearchService, 14 | EnhancedResearchStep, 15 | EnhancedResearchReport 16 | ) 17 | 18 | __all__ = [ 19 | 'SearXNGContentProvider', 20 | 'SearXNGSearchResult', 21 | 'SearXNGSearchResponse', 22 | 'WebContentExtractor', 23 | 'ExtractedContent', 24 | 'EnhancedResearchService', 25 | 'EnhancedResearchStep', 26 | 'EnhancedResearchReport' 27 | ] 28 | -------------------------------------------------------------------------------- /src/landppt/auth/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Authentication module for LandPPT 3 | """ 4 | 5 | from .auth_service import AuthService, get_auth_service, init_default_admin 6 | from .middleware import ( 7 | AuthMiddleware, 8 | create_auth_middleware, 9 | get_current_user, 10 | require_auth, 11 | require_admin, 12 | get_current_user_optional, 13 | get_current_user_required, 14 | get_current_admin_user, 15 | is_authenticated, 16 | is_admin, 17 | get_user_info 18 | ) 19 | from .routes import router as auth_router 20 | 21 | __all__ = [ 22 | "AuthService", 23 | "get_auth_service", 24 | "init_default_admin", 25 | "AuthMiddleware", 26 | "create_auth_middleware", 27 | "get_current_user", 28 | "require_auth", 29 | "require_admin", 30 | "get_current_user_optional", 31 | "get_current_user_required", 32 | "get_current_admin_user", 33 | "is_authenticated", 34 | "is_admin", 35 | "get_user_info", 36 | "auth_router" 37 | ] 38 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | landppt: 5 | image: bradleylzh/landppt:latest 6 | container_name: landppt 7 | ports: 8 | - "8000:8000" 9 | volumes: 10 | # Configuration 11 | - ./.env:/app/.env 12 | # Data persistence 13 | - landppt_data:/app/data 14 | - landppt_uploads:/app/uploads 15 | - landppt_reports:/app/research_reports 16 | - landppt_cache:/app/temp 17 | environment: 18 | - PYTHONPATH=/app/src 19 | - PYTHONUNBUFFERED=1 20 | restart: unless-stopped 21 | healthcheck: 22 | test: ["CMD", "./docker-healthcheck.sh"] 23 | interval: 30s 24 | timeout: 30s 25 | retries: 3 26 | start_period: 40s 27 | networks: 28 | - landppt_network 29 | 30 | volumes: 31 | landppt_data: 32 | driver: local 33 | landppt_uploads: 34 | driver: local 35 | landppt_reports: 36 | driver: local 37 | landppt_cache: 38 | driver: local 39 | 40 | networks: 41 | landppt_network: 42 | driver: bridge 43 | -------------------------------------------------------------------------------- /src/landppt/web/static/js/modules/domUtils.js: -------------------------------------------------------------------------------- 1 | export const qs = (selector, scope = document) => scope.querySelector(selector); 2 | export const qsa = (selector, scope = document) => Array.from(scope.querySelectorAll(selector)); 3 | 4 | export function createFragment(nodes = []) { 5 | const fragment = document.createDocumentFragment(); 6 | nodes.forEach(node => fragment.appendChild(node)); 7 | return fragment; 8 | } 9 | 10 | export function debounce(fn, delay = 200) { 11 | let timer; 12 | return (...args) => { 13 | clearTimeout(timer); 14 | timer = setTimeout(() => fn(...args), delay); 15 | }; 16 | } 17 | 18 | export function formatBytes(bytes) { 19 | if (!bytes && bytes !== 0) return '-'; 20 | const units = ['B', 'KB', 'MB', 'GB']; 21 | const base = Math.floor(Math.log(bytes) / Math.log(1024)); 22 | const value = bytes / Math.pow(1024, base); 23 | return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[base]}`; 24 | } 25 | 26 | export function toggleElement(el, show) { 27 | if (!el) return; 28 | el.style.display = show ? '' : 'none'; 29 | } 30 | -------------------------------------------------------------------------------- /src/landppt/database/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Database package for LandPPT 3 | """ 4 | 5 | from .database import engine, SessionLocal, get_db, init_db, get_async_db 6 | from .models import Project, TodoBoard, TodoStage, ProjectVersion, SlideData, PPTTemplate 7 | from .migrations import migration_manager 8 | from .health_check import health_checker 9 | from .service import DatabaseService 10 | from .repositories import ( 11 | ProjectRepository, TodoBoardRepository, TodoStageRepository, 12 | ProjectVersionRepository, SlideDataRepository, PPTTemplateRepository 13 | ) 14 | 15 | __all__ = [ 16 | 'engine', 17 | 'SessionLocal', 18 | 'get_db', 19 | 'get_async_db', 20 | 'init_db', 21 | 'Project', 22 | 'TodoBoard', 23 | 'TodoStage', 24 | 'ProjectVersion', 25 | 'SlideData', 26 | 'PPTTemplate', 27 | 'migration_manager', 28 | 'health_checker', 29 | 'DatabaseService', 30 | 'ProjectRepository', 31 | 'TodoBoardRepository', 32 | 'TodoStageRepository', 33 | 'ProjectVersionRepository', 34 | 'SlideDataRepository', 35 | 'PPTTemplateRepository' 36 | ] 37 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | .gitattributes 5 | 6 | # Docker 7 | Dockerfile* 8 | docker-compose* 9 | .dockerignore 10 | 11 | # Documentation 12 | docs/ 13 | 14 | # IDE and Editor files 15 | .vscode/ 16 | .idea/ 17 | *.swp 18 | *.swo 19 | *~ 20 | 21 | # OS generated files 22 | .DS_Store 23 | .DS_Store? 24 | ._* 25 | .Spotlight-V100 26 | .Trashes 27 | ehthumbs.db 28 | Thumbs.db 29 | 30 | # Python 31 | __pycache__/ 32 | *.py[cod] 33 | *$py.class 34 | *.so 35 | .Python 36 | build/ 37 | develop-eggs/ 38 | dist/ 39 | downloads/ 40 | eggs/ 41 | .eggs/ 42 | lib/ 43 | lib64/ 44 | parts/ 45 | sdist/ 46 | var/ 47 | wheels/ 48 | *.egg-info/ 49 | .installed.cfg 50 | *.egg 51 | MANIFEST 52 | 53 | # Virtual environments 54 | .env 55 | .venv 56 | env/ 57 | venv/ 58 | ENV/ 59 | env.bak/ 60 | venv.bak/ 61 | 62 | # Testing 63 | .tox/ 64 | .coverage 65 | .coverage.* 66 | .cache 67 | .pytest_cache/ 68 | nosetests.xml 69 | coverage.xml 70 | *.cover 71 | .hypothesis/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # Environments 80 | .env.local 81 | .env.development 82 | .env.test 83 | .env.production 84 | 85 | # Database files (will be created in container) 86 | *.db 87 | *.sqlite 88 | *.sqlite3 89 | 90 | # Logs 91 | *.log 92 | logs/ 93 | 94 | # Temporary files 95 | temp/ 96 | tmp/ 97 | *.tmp 98 | 99 | # Cache directories 100 | .cache/ 101 | *_cache/ 102 | 103 | # Research reports (will be generated in container) 104 | research_reports/ 105 | 106 | # Uploads (will be handled by volumes) 107 | uploads/ 108 | 109 | # Node modules (if any) 110 | node_modules/ 111 | npm-debug.log* 112 | yarn-debug.log* 113 | yarn-error.log* 114 | 115 | # Lock files (keep uv.lock for dependency resolution) 116 | # uv.lock is kept for proper dependency installation 117 | 118 | # Keep Docker-related scripts 119 | !docker-healthcheck.sh 120 | !docker-entrypoint.sh 121 | -------------------------------------------------------------------------------- /src/landppt/services/pdf_to_pptx_worker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Worker entrypoint to run PDF->PPTX conversion in a separate process. 3 | """ 4 | 5 | import argparse 6 | import logging 7 | import sys 8 | from pathlib import Path 9 | 10 | from .pdf_to_pptx_converter import PDFToPPTXConverter 11 | 12 | 13 | def configure_logging(level: int) -> None: 14 | """Initialise basic logging for the worker.""" 15 | logging.basicConfig( 16 | level=level, 17 | format="%(levelname)s:%(name)s:%(message)s" 18 | ) 19 | 20 | 21 | def main() -> int: 22 | """CLI entry point.""" 23 | parser = argparse.ArgumentParser( 24 | description="Convert PDF to PPTX using Apryse SDK." 25 | ) 26 | parser.add_argument("--input", required=True, help="Path to the source PDF file.") 27 | parser.add_argument("--output", help="Path for the output PPTX file.") 28 | parser.add_argument( 29 | "--log-level", 30 | default="INFO", 31 | help="Logging level (default: INFO)." 32 | ) 33 | args = parser.parse_args() 34 | 35 | log_level = getattr(logging, args.log_level.upper(), logging.INFO) 36 | configure_logging(log_level) 37 | 38 | pdf_path = Path(args.input).expanduser().resolve() 39 | if not pdf_path.exists(): 40 | print(f"Input PDF not found: {pdf_path}", file=sys.stderr) 41 | return 1 42 | 43 | if args.output: 44 | output_path = Path(args.output).expanduser() 45 | if not output_path.is_absolute(): 46 | output_path = output_path.resolve() 47 | else: 48 | output_path = pdf_path.with_suffix(".pptx") 49 | 50 | output_path.parent.mkdir(parents=True, exist_ok=True) 51 | 52 | converter = PDFToPPTXConverter() 53 | success, result = converter.convert_pdf_to_pptx(str(pdf_path), str(output_path)) 54 | if success: 55 | print(result) 56 | return 0 57 | 58 | print(result, file=sys.stderr) 59 | return 1 60 | 61 | 62 | if __name__ == "__main__": 63 | sys.exit(main()) 64 | -------------------------------------------------------------------------------- /src/landppt/web/static/js/modules/apiClient.js: -------------------------------------------------------------------------------- 1 | const defaultHeaders = { 2 | 'Content-Type': 'application/json' 3 | }; 4 | 5 | async function request(url, options = {}) { 6 | const { method = 'GET', body, headers = {}, signal, responseType, returnResponse = false } = options; 7 | const init = { 8 | method, 9 | headers: body instanceof FormData ? headers : { ...defaultHeaders, ...headers }, 10 | body: body instanceof FormData ? body : body ? JSON.stringify(body) : undefined, 11 | signal, 12 | credentials: 'same-origin' 13 | }; 14 | 15 | const response = await fetch(url, init); 16 | const contentType = response.headers.get('content-type') || ''; 17 | const expectedType = responseType || (contentType.includes('application/json') ? 'json' : 'text'); 18 | let payload; 19 | 20 | if (expectedType === 'blob') { 21 | payload = await response.blob(); 22 | } else if (expectedType === 'json') { 23 | payload = await response.json(); 24 | } else { 25 | payload = await response.text(); 26 | } 27 | 28 | if (!response.ok) { 29 | const message = payload?.message || response.statusText; 30 | throw new Error(message); 31 | } 32 | 33 | if (returnResponse) { 34 | return { data: payload, headers: response.headers, status: response.status }; 35 | } 36 | 37 | return payload; 38 | } 39 | 40 | const withQuery = (url, params) => { 41 | if (!params) return url; 42 | const search = new URLSearchParams(params); 43 | return `${url}?${search.toString()}`; 44 | }; 45 | 46 | export const apiClient = { 47 | request, 48 | get: (url, params, options = {}) => request(withQuery(url, params), { ...options, method: 'GET' }), 49 | post: (url, body, options = {}) => request(url, { ...options, method: 'POST', body }), 50 | put: (url, body, options = {}) => request(url, { ...options, method: 'PUT', body }), 51 | del: (url, options = {}) => request(url, { ...options, method: 'DELETE' }) 52 | }; 53 | 54 | export default apiClient; 55 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | LandPPT Application Runner 4 | 5 | This script starts the LandPPT FastAPI application with proper configuration. 6 | """ 7 | 8 | import uvicorn 9 | import sys 10 | import os 11 | import asyncio 12 | from dotenv import load_dotenv 13 | 14 | # Add src to Python path 15 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) 16 | 17 | # Load environment variables with error handling 18 | try: 19 | load_dotenv() 20 | except PermissionError as e: 21 | print(f"Warning: Could not load .env file due to permission error: {e}") 22 | print("Continuing with system environment variables...") 23 | except Exception as e: 24 | print(f"Warning: Could not load .env file: {e}") 25 | print("Continuing with system environment variables...") 26 | 27 | def main(): 28 | """Main entry point for running the application""" 29 | 30 | # Get configuration from environment variables with defaults 31 | host = os.getenv("HOST", "0.0.0.0") 32 | port = int(os.getenv("PORT", "8000")) 33 | reload = os.getenv("RELOAD", "true").lower() in ("true", "1", "yes", "on") 34 | log_level = os.getenv("LOG_LEVEL", "info").lower() 35 | 36 | # Configuration 37 | config = { 38 | "app": "landppt.main:app", 39 | "host": host, 40 | "port": port, 41 | "reload": reload, 42 | "log_level": log_level, 43 | "access_log": True, 44 | } 45 | 46 | print("🚀 Starting LandPPT Server...") 47 | print(f"📍 Host: {config['host']}") 48 | print(f"🔌 Port: {config['port']}") 49 | print(f"🔄 Reload: {config['reload']}") 50 | print(f"📊 Log Level: {config['log_level']}") 51 | print(f"📍 Server will be available at: http://localhost:{config['port']}") 52 | print(f"📚 API Documentation: http://localhost:{config['port']}/docs") 53 | print(f"🌐 Web Interface: http://localhost:{config['port']}/web") 54 | print("=" * 60) 55 | 56 | try: 57 | uvicorn.run(**config) 58 | except KeyboardInterrupt: 59 | print("\n👋 Server stopped by user") 60 | except Exception as e: 61 | print(f"❌ Error starting server: {e}") 62 | sys.exit(1) 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /src/landppt/database/migrations/add_speech_scripts_table.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add speech_scripts table migration 3 | """ 4 | 5 | import sys 6 | import os 7 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../..')) 8 | 9 | from sqlalchemy import text 10 | from landppt.database.database import engine 11 | 12 | 13 | def upgrade(): 14 | """Create speech_scripts table""" 15 | with engine.connect() as conn: 16 | # Create speech_scripts table 17 | conn.execute(text(""" 18 | CREATE TABLE IF NOT EXISTS speech_scripts ( 19 | id INTEGER PRIMARY KEY AUTOINCREMENT, 20 | project_id VARCHAR(36) NOT NULL, 21 | slide_index INTEGER NOT NULL, 22 | slide_title VARCHAR(255) NOT NULL, 23 | script_content TEXT NOT NULL, 24 | estimated_duration VARCHAR(50), 25 | speaker_notes TEXT, 26 | generation_type VARCHAR(20) NOT NULL, 27 | tone VARCHAR(50) NOT NULL, 28 | target_audience VARCHAR(100) NOT NULL, 29 | custom_audience TEXT, 30 | language_complexity VARCHAR(20) NOT NULL, 31 | speaking_pace VARCHAR(20) NOT NULL, 32 | custom_style_prompt TEXT, 33 | include_transitions BOOLEAN NOT NULL DEFAULT 1, 34 | include_timing_notes BOOLEAN NOT NULL DEFAULT 0, 35 | created_at REAL NOT NULL, 36 | updated_at REAL NOT NULL, 37 | FOREIGN KEY (project_id) REFERENCES projects (project_id) 38 | ) 39 | """)) 40 | 41 | # Create indexes 42 | conn.execute(text(""" 43 | CREATE INDEX IF NOT EXISTS idx_speech_scripts_project_id 44 | ON speech_scripts (project_id) 45 | """)) 46 | 47 | conn.execute(text(""" 48 | CREATE INDEX IF NOT EXISTS idx_speech_scripts_project_slide 49 | ON speech_scripts (project_id, slide_index) 50 | """)) 51 | 52 | conn.commit() 53 | 54 | 55 | def downgrade(): 56 | """Drop speech_scripts table""" 57 | with engine.connect() as conn: 58 | conn.execute(text("DROP TABLE IF EXISTS speech_scripts")) 59 | conn.commit() 60 | 61 | 62 | if __name__ == "__main__": 63 | upgrade() 64 | print("Speech scripts table created successfully!") 65 | -------------------------------------------------------------------------------- /src/landppt/web/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}错误 - LandPPT{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
😵
8 | 9 |

出现了一些问题

10 | 11 |
12 | {% if error %} 13 |
14 |

错误详情:

15 |

16 | {{ error }} 17 |

18 |
19 | {% else %} 20 |

21 | 很抱歉,系统遇到了未知错误。请稍后重试或联系技术支持。 22 |

23 | {% endif %} 24 | 25 |
26 | 27 | ← 返回上一页 28 | 29 | 30 | 🏠 返回首页 31 | 32 |
33 | 34 |
35 |

💡 建议解决方案

36 |
    37 |
  • 检查网络连接是否正常
  • 38 |
  • 刷新页面重试操作
  • 39 |
  • 清除浏览器缓存
  • 40 |
  • 尝试使用其他浏览器
  • 41 |
  • 如果问题持续存在,请联系技术支持
  • 42 |
43 |
44 | 45 |
46 |

🔗 快速链接

47 |
48 | 🎯 创建 PPT 49 | 📊 项目列表 50 | 📈 项目仪表板 51 | 🎮 查看演示 52 | 📚 API 文档 53 |
54 |
55 |
56 |
57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /src/landppt/database/migrations/add_project_share_fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add share_token and share_enabled fields to projects table 3 | """ 4 | 5 | import sys 6 | import os 7 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../..')) 8 | 9 | from sqlalchemy import text 10 | from landppt.database.database import engine 11 | 12 | 13 | def upgrade(): 14 | """Add share_token and share_enabled columns to projects table""" 15 | with engine.connect() as conn: 16 | # Check if columns exist before adding them 17 | try: 18 | # Add share_token column (without UNIQUE constraint in ALTER TABLE for SQLite) 19 | conn.execute(text(""" 20 | ALTER TABLE projects 21 | ADD COLUMN share_token VARCHAR(64) 22 | """)) 23 | print("Added share_token column") 24 | except Exception as e: 25 | print(f"share_token column may already exist: {e}") 26 | 27 | try: 28 | # Add share_enabled column 29 | conn.execute(text(""" 30 | ALTER TABLE projects 31 | ADD COLUMN share_enabled BOOLEAN NOT NULL DEFAULT 0 32 | """)) 33 | print("Added share_enabled column") 34 | except Exception as e: 35 | print(f"share_enabled column may already exist: {e}") 36 | 37 | try: 38 | # Create unique index on share_token for faster lookups and uniqueness 39 | conn.execute(text(""" 40 | CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_share_token 41 | ON projects (share_token) 42 | WHERE share_token IS NOT NULL 43 | """)) 44 | print("Created unique index on share_token") 45 | except Exception as e: 46 | print(f"Index creation error: {e}") 47 | 48 | conn.commit() 49 | print("Migration completed successfully!") 50 | 51 | 52 | def downgrade(): 53 | """Remove share_token and share_enabled columns from projects table""" 54 | with engine.connect() as conn: 55 | try: 56 | # Drop index 57 | conn.execute(text("DROP INDEX IF EXISTS idx_projects_share_token")) 58 | 59 | # SQLite doesn't support DROP COLUMN in older versions 60 | # We'll need to recreate the table without these columns 61 | # For now, just set them to NULL 62 | print("Note: SQLite doesn't support DROP COLUMN. Columns will remain but can be ignored.") 63 | except Exception as e: 64 | print(f"Error during downgrade: {e}") 65 | 66 | conn.commit() 67 | 68 | 69 | if __name__ == "__main__": 70 | upgrade() 71 | print("Project share fields migration completed!") 72 | -------------------------------------------------------------------------------- /src/landppt/database/database.py: -------------------------------------------------------------------------------- 1 | """ 2 | Database configuration and session management 3 | """ 4 | 5 | import os 6 | from sqlalchemy import create_engine 7 | from sqlalchemy.ext.declarative import declarative_base 8 | from sqlalchemy.orm import sessionmaker 9 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker 10 | 11 | from ..core.config import app_config 12 | 13 | # Create database URL 14 | DATABASE_URL = app_config.database_url 15 | 16 | # For async SQLite, we need to use aiosqlite 17 | if DATABASE_URL.startswith("sqlite:///"): 18 | ASYNC_DATABASE_URL = DATABASE_URL.replace("sqlite:///", "sqlite+aiosqlite:///") 19 | else: 20 | ASYNC_DATABASE_URL = DATABASE_URL 21 | 22 | # Create engines 23 | # SQLite-specific configuration for better concurrency 24 | sqlite_connect_args = { 25 | "check_same_thread": False, 26 | "timeout": 30, # Wait up to 30 seconds for lock 27 | } if "sqlite" in DATABASE_URL else {} 28 | 29 | engine = create_engine( 30 | DATABASE_URL, 31 | connect_args=sqlite_connect_args, 32 | echo=False, # Disable SQL logging to reduce noise 33 | pool_pre_ping=True, # Verify connections before using 34 | pool_size=100, # Larger pool for better concurrency 35 | max_overflow=200 # Allow overflow connections 36 | ) 37 | 38 | async_engine = create_async_engine( 39 | ASYNC_DATABASE_URL, 40 | echo=False, # Disable SQL logging to reduce noise 41 | pool_pre_ping=True, 42 | connect_args={"timeout": 30} if "sqlite" in ASYNC_DATABASE_URL else {} 43 | ) 44 | 45 | # Create session makers 46 | SessionLocal = sessionmaker( 47 | autocommit=False, 48 | autoflush=False, 49 | bind=engine, 50 | expire_on_commit=False # Prevent errors after commit 51 | ) 52 | AsyncSessionLocal = async_sessionmaker( 53 | async_engine, 54 | class_=AsyncSession, 55 | expire_on_commit=False 56 | ) 57 | 58 | def get_db(): 59 | """Dependency to get database session""" 60 | db = SessionLocal() 61 | try: 62 | yield db 63 | finally: 64 | db.close() 65 | 66 | 67 | async def get_async_db(): 68 | """Dependency to get async database session""" 69 | async with AsyncSessionLocal() as session: 70 | yield session 71 | 72 | 73 | async def init_db(): 74 | """Initialize database tables""" 75 | # Import here to avoid circular imports 76 | from .models import Base 77 | 78 | async with async_engine.begin() as conn: 79 | # Create all tables 80 | await conn.run_sync(Base.metadata.create_all) 81 | 82 | # Initialize default admin user 83 | from ..auth.auth_service import init_default_admin 84 | db = SessionLocal() 85 | try: 86 | init_default_admin(db) 87 | finally: 88 | db.close() 89 | 90 | 91 | async def close_db(): 92 | """Close database connections""" 93 | await async_engine.dispose() 94 | 95 | -------------------------------------------------------------------------------- /docs/base_url_configuration.md: -------------------------------------------------------------------------------- 1 | # 反向代理域名配置指南 2 | 3 | 当您为LandPPT主服务设置了反向代理域名时,需要配置`base_url`参数以确保图床服务的链接能够正确显示。 4 | 5 | ## 问题描述 6 | 7 | 在使用反向代理(如Nginx、Apache等)时,如果没有正确配置`base_url`,会出现以下问题: 8 | - 图片链接仍然显示为`localhost:8000` 9 | - 前端无法正确加载图片 10 | - 图片预览、下载等功能异常 11 | 12 | ## 解决方案 13 | 14 | ### 1. 通过Web界面配置 15 | 16 | 1. 访问系统配置页面:`https://your-domain.com/ai-config` 17 | 2. 切换到"应用配置"标签页 18 | 3. 在"基础URL (BASE_URL)"字段中输入您的代理域名 19 | 4. 例如:`https://your-domain.com` 或 `http://your-domain.com:8080` 20 | 5. 点击"保存应用配置" 21 | 22 | ### 2. 通过环境变量配置 23 | 24 | 在`.env`文件中添加或修改: 25 | 26 | ```bash 27 | # 基础URL配置 - 用于生成图片等资源的绝对URL 28 | BASE_URL=https://your-domain.com 29 | ``` 30 | 31 | ### 3. 通过命令行参数配置 32 | 33 | 启动服务时指定: 34 | 35 | ```bash 36 | BASE_URL=https://your-domain.com python -m uvicorn src.landppt.main:app --host 0.0.0.0 --port 8000 37 | ``` 38 | 39 | ## 配置示例 40 | 41 | ### Nginx反向代理配置示例 42 | 43 | ```nginx 44 | server { 45 | listen 80; 46 | server_name your-domain.com; 47 | 48 | location / { 49 | proxy_pass http://localhost:8000; 50 | proxy_set_header Host $host; 51 | proxy_set_header X-Real-IP $remote_addr; 52 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 53 | proxy_set_header X-Forwarded-Proto $scheme; 54 | } 55 | } 56 | ``` 57 | 58 | 对应的LandPPT配置: 59 | ```bash 60 | BASE_URL=http://your-domain.com 61 | ``` 62 | 63 | ### HTTPS反向代理配置示例 64 | 65 | ```nginx 66 | server { 67 | listen 443 ssl; 68 | server_name your-domain.com; 69 | 70 | ssl_certificate /path/to/your/cert.pem; 71 | ssl_certificate_key /path/to/your/key.pem; 72 | 73 | location / { 74 | proxy_pass http://localhost:8000; 75 | proxy_set_header Host $host; 76 | proxy_set_header X-Real-IP $remote_addr; 77 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 78 | proxy_set_header X-Forwarded-Proto https; 79 | } 80 | } 81 | ``` 82 | 83 | 对应的LandPPT配置: 84 | ```bash 85 | BASE_URL=https://your-domain.com 86 | ``` 87 | 88 | ## 验证配置 89 | 90 | 配置完成后,您可以通过以下方式验证: 91 | 92 | 1. **检查图片URL**:在PPT编辑器中生成或上传图片,查看图片URL是否使用了正确的域名 93 | 2. **测试图片访问**:直接访问图片URL,确认能够正常显示 94 | 3. **检查API响应**:调用图片相关API,查看返回的URL是否正确 95 | 96 | ## 技术实现 97 | 98 | 系统通过以下方式实现统一的URL管理: 99 | 100 | 1. **URL服务**:`src/landppt/services/url_service.py` - 统一管理所有URL生成逻辑 101 | 2. **配置集成**:自动从配置服务获取`base_url`设置 102 | 3. **实时更新**:配置更改后立即生效,无需重启服务 103 | 4. **向后兼容**:如果未配置`base_url`,自动使用`http://localhost:8000`作为默认值 104 | 105 | ## 注意事项 106 | 107 | 1. **URL格式**:`base_url`不能以斜杠(`/`)结尾 108 | 2. **协议匹配**:确保`base_url`的协议(http/https)与实际访问协议一致 109 | 3. **端口配置**:如果使用非标准端口,需要在`base_url`中包含端口号 110 | 4. **配置优先级**:环境变量 > Web界面配置 > 默认值 111 | 112 | ## 故障排除 113 | 114 | ### 图片仍然显示localhost 115 | 116 | 1. 检查`base_url`配置是否正确 117 | 2. 确认配置已保存并生效 118 | 3. 清除浏览器缓存 119 | 4. 检查反向代理配置 120 | 121 | ### 图片无法加载 122 | 123 | 1. 确认反向代理正确转发了`/api/image/`路径 124 | 2. 检查防火墙和安全组设置 125 | 3. 验证SSL证书配置(如果使用HTTPS) 126 | 127 | ### 配置不生效 128 | 129 | 1. 重启LandPPT服务 130 | 2. 检查环境变量是否正确设置 131 | 3. 查看服务日志中的配置加载信息 132 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "landppt" 3 | version = "0.1.6" 4 | description = "AI-powered PPT generation platform with OpenAI-compatible API" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | license = {text = "Apache-2.0"} 8 | authors = [ 9 | {name = "LandPPT Team", email = "contact@landppt.com"}, 10 | ] 11 | keywords = ["ai", "ppt", "presentation", "fastapi", "openai"] 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: Apache Software License", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.11", 18 | "Programming Language :: Python :: 3.12", 19 | "Topic :: Software Development :: Libraries :: Python Modules", 20 | "Topic :: Office/Business :: Office Suites", 21 | ] 22 | dependencies = [ 23 | "fastapi>=0.104.0", 24 | "uvicorn[standard]>=0.24.0", 25 | "pydantic>=2.5.0", 26 | "python-multipart>=0.0.6", 27 | "jinja2>=3.1.2", 28 | "aiofiles>=23.2.1", 29 | "python-jose[cryptography]>=3.3.0", 30 | "passlib[bcrypt]>=1.7.4", 31 | "httpx>=0.25.0", 32 | "markdown>=3.5.1", 33 | "python-docx>=1.1.0", 34 | "pypdf2>=3.0.1", 35 | "requests>=2.31.0", 36 | # Database libraries 37 | "sqlalchemy>=2.0.0", 38 | "alembic>=1.13.0", 39 | "aiosqlite>=0.19.0", 40 | # AI and LLM libraries 41 | "openai>=1.0.0", 42 | "anthropic>=0.7.0", 43 | "google-generativeai>=0.3.0", 44 | "ollama>=0.1.0", 45 | "transformers>=4.35.0", 46 | "torch>=2.0.0", 47 | "tiktoken>=0.5.0", 48 | "aiohttp>=3.12.4", 49 | "pdfkit>=1.0.0", 50 | "langchain>=0.1.0", 51 | "langchain-core>=0.1.0", 52 | "langchain-openai>=0.1.0", 53 | "langchain-anthropic>=0.1.0", 54 | "langchain-ollama>=0.1.0", 55 | "langchain-google-genai>=1.0.0", 56 | "langgraph>=0.2.0", 57 | "click>=8.0.0", 58 | "python-dotenv>=1.0.0", 59 | "chardet>=5.0.0", 60 | "pandas>=2.0.0", 61 | "beautifulsoup4>=4.12.0", 62 | "rich>=13.0.0", 63 | "pydantic-settings>=2.0.0", 64 | "tavily-python>=0.7.8", 65 | "mineru[core]>=2.0.6", 66 | "markitdown[all]>=0.1.2", 67 | "onnxruntime==1.19.2", 68 | "playwright>=1.40.0", 69 | "apryse-sdk>=11.6.0", 70 | "langchain-community>=0.3.27", 71 | ] 72 | 73 | [project.optional-dependencies] 74 | dev = [ 75 | "pytest>=7.0.0", 76 | "pytest-asyncio>=0.21.0", 77 | "pytest-cov>=4.0.0", 78 | "black>=23.0.0", 79 | "isort>=5.12.0", 80 | "flake8>=6.0.0", 81 | "mypy>=1.0.0", 82 | "pre-commit>=3.0.0", 83 | ] 84 | test = [ 85 | "pytest>=7.0.0", 86 | "pytest-asyncio>=0.21.0", 87 | "pytest-cov>=4.0.0", 88 | "httpx>=0.25.0", 89 | ] 90 | 91 | [project.scripts] 92 | landppt = "landppt.main:main" 93 | 94 | [project.urls] 95 | Homepage = "https://github.com/sligter/LandPPT" 96 | Documentation = "https://github.com/sligter/LandPPT#readme" 97 | Repository = "https://github.com/sligter/LandPPT.git" 98 | Issues = "https://github.com/sligter/LandPPT/issues" 99 | 100 | [build-system] 101 | requires = ["hatchling"] 102 | build-backend = "hatchling.build" 103 | 104 | [tool.hatch.build.targets.wheel] 105 | packages = ["src/landppt"] 106 | sources = ["src"] 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /docker-healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # LandPPT Docker Health Check Script 4 | # This script performs comprehensive health checks for the LandPPT application 5 | 6 | set -e 7 | 8 | # Configuration 9 | HOST=${HOST:-localhost} 10 | PORT=${PORT:-8000} 11 | TIMEOUT=${HEALTH_CHECK_TIMEOUT:-30} 12 | 13 | # Colors for output 14 | RED='\033[0;31m' 15 | GREEN='\033[0;32m' 16 | YELLOW='\033[1;33m' 17 | NC='\033[0m' # No Color 18 | 19 | # Logging function 20 | log() { 21 | echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" 22 | } 23 | 24 | warn() { 25 | echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}" 26 | } 27 | 28 | error() { 29 | echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" 30 | } 31 | 32 | # Check if service is responding 33 | check_http_health() { 34 | log "Checking HTTP health endpoint..." 35 | 36 | if curl -f -s --max-time $TIMEOUT "http://${HOST}:${PORT}/health" > /dev/null; then 37 | log "✅ HTTP health check passed" 38 | return 0 39 | else 40 | error "❌ HTTP health check failed" 41 | return 1 42 | fi 43 | } 44 | 45 | # Check if API documentation is accessible 46 | check_api_docs() { 47 | log "Checking API documentation..." 48 | 49 | if curl -f -s --max-time $TIMEOUT "http://${HOST}:${PORT}/docs" > /dev/null; then 50 | log "✅ API documentation accessible" 51 | return 0 52 | else 53 | warn "⚠️ API documentation not accessible" 54 | return 1 55 | fi 56 | } 57 | 58 | # Check database connectivity 59 | check_database() { 60 | log "Checking database connectivity..." 61 | 62 | if [ -f "/app/data/landppt.db" ]; then 63 | log "✅ Database file exists" 64 | return 0 65 | else 66 | warn "⚠️ Database file not found" 67 | return 1 68 | fi 69 | } 70 | 71 | # Check required directories 72 | check_directories() { 73 | log "Checking required directories..." 74 | 75 | local dirs=("/app/uploads" "/app/temp" "/app/data") 76 | local all_good=true 77 | 78 | for dir in "${dirs[@]}"; do 79 | if [ -d "$dir" ]; then 80 | log "✅ Directory $dir exists" 81 | else 82 | warn "⚠️ Directory $dir missing" 83 | all_good=false 84 | fi 85 | done 86 | 87 | if $all_good; then 88 | return 0 89 | else 90 | return 1 91 | fi 92 | } 93 | 94 | # Check Python process 95 | check_python_process() { 96 | log "Checking Python process..." 97 | 98 | if pgrep -f "python.*run.py" > /dev/null; then 99 | log "✅ Python process running" 100 | return 0 101 | else 102 | error "❌ Python process not found" 103 | return 1 104 | fi 105 | } 106 | 107 | # Main health check function 108 | main() { 109 | log "Starting LandPPT health check..." 110 | 111 | local exit_code=0 112 | 113 | # Critical checks (must pass) 114 | if ! check_http_health; then 115 | exit_code=1 116 | fi 117 | 118 | if ! check_python_process; then 119 | exit_code=1 120 | fi 121 | 122 | # Non-critical checks (warnings only) 123 | check_api_docs || true 124 | check_database || true 125 | check_directories || true 126 | 127 | if [ $exit_code -eq 0 ]; then 128 | log "🎉 All critical health checks passed!" 129 | else 130 | error "💥 Some critical health checks failed!" 131 | fi 132 | 133 | exit $exit_code 134 | } 135 | 136 | # Run health check 137 | main "$@" 138 | -------------------------------------------------------------------------------- /src/summeryanyfile/core/chunkers/base_chunker.py: -------------------------------------------------------------------------------- 1 | """ 2 | 基础分块器抽象类 3 | """ 4 | 5 | import uuid 6 | import logging 7 | from abc import ABC, abstractmethod 8 | from dataclasses import dataclass, field 9 | from typing import List, Dict, Any, Optional 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | @dataclass 15 | class DocumentChunk: 16 | """文档块数据类""" 17 | content: str 18 | metadata: Dict[str, Any] = field(default_factory=dict) 19 | chunk_id: str = field(default_factory=lambda: str(uuid.uuid4())) 20 | 21 | def __post_init__(self): 22 | """后处理初始化""" 23 | if not self.chunk_id: 24 | self.chunk_id = str(uuid.uuid4()) 25 | 26 | @property 27 | def size(self) -> int: 28 | """获取块大小(字符数)""" 29 | return len(self.content) 30 | 31 | def to_dict(self) -> Dict[str, Any]: 32 | """转换为字典格式""" 33 | return { 34 | "content": self.content, 35 | "metadata": self.metadata, 36 | "chunk_id": self.chunk_id, 37 | "size": self.size 38 | } 39 | 40 | 41 | class BaseChunker(ABC): 42 | """ 43 | 基础分块器抽象类 44 | 45 | 所有分块器都应该继承这个类并实现chunk_text方法 46 | """ 47 | 48 | def __init__(self, chunk_size: int = 1000, chunk_overlap: int = 200) -> None: 49 | """ 50 | 初始化分块器 51 | 52 | Args: 53 | chunk_size: 块大小限制 54 | chunk_overlap: 块重叠大小 55 | """ 56 | self.chunk_size = chunk_size 57 | self.chunk_overlap = chunk_overlap 58 | self.logger = logging.getLogger(self.__class__.__name__) 59 | 60 | @abstractmethod 61 | def chunk_text(self, text: str, metadata: Optional[Dict[str, Any]] = None) -> List[DocumentChunk]: 62 | """ 63 | 分块文本的抽象方法 64 | 65 | Args: 66 | text: 要分块的文本 67 | metadata: 可选的元数据 68 | 69 | Returns: 70 | DocumentChunk对象列表 71 | """ 72 | pass 73 | 74 | def validate_chunk_size(self, chunk: DocumentChunk) -> bool: 75 | """ 76 | 验证块大小是否符合要求 77 | 78 | Args: 79 | chunk: 要验证的块 80 | 81 | Returns: 82 | 是否符合大小要求 83 | """ 84 | return chunk.size <= self.chunk_size 85 | 86 | def _create_chunk(self, content: str, metadata: Optional[Dict[str, Any]] = None) -> DocumentChunk: 87 | """ 88 | 创建文档块 89 | 90 | Args: 91 | content: 块内容 92 | metadata: 块元数据 93 | 94 | Returns: 95 | DocumentChunk对象 96 | """ 97 | if metadata is None: 98 | metadata = {} 99 | 100 | return DocumentChunk( 101 | content=content.strip(), 102 | metadata=metadata 103 | ) 104 | 105 | def get_chunk_statistics(self, chunks: List[DocumentChunk]) -> Dict[str, Any]: 106 | """ 107 | 获取分块统计信息 108 | 109 | Args: 110 | chunks: 块列表 111 | 112 | Returns: 113 | 统计信息字典 114 | """ 115 | if not chunks: 116 | return { 117 | "total_chunks": 0, 118 | "total_size": 0, 119 | "avg_size": 0, 120 | "min_size": 0, 121 | "max_size": 0 122 | } 123 | 124 | sizes = [chunk.size for chunk in chunks] 125 | 126 | return { 127 | "total_chunks": len(chunks), 128 | "total_size": sum(sizes), 129 | "avg_size": sum(sizes) / len(sizes), 130 | "min_size": min(sizes), 131 | "max_size": max(sizes) 132 | } 133 | -------------------------------------------------------------------------------- /src/landppt/services/service_instances.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shared service instances to ensure data consistency across modules 3 | """ 4 | 5 | from .enhanced_ppt_service import EnhancedPPTService 6 | from .db_project_manager import DatabaseProjectManager 7 | 8 | # Global service instances (lazy initialization) 9 | _ppt_service = None 10 | _project_manager = None 11 | 12 | def get_ppt_service() -> EnhancedPPTService: 13 | """Get PPT service instance (lazy initialization)""" 14 | global _ppt_service 15 | if _ppt_service is None: 16 | _ppt_service = EnhancedPPTService() 17 | return _ppt_service 18 | 19 | def get_project_manager() -> DatabaseProjectManager: 20 | """Get project manager instance (lazy initialization)""" 21 | global _project_manager 22 | if _project_manager is None: 23 | _project_manager = DatabaseProjectManager() 24 | return _project_manager 25 | 26 | def reload_services(): 27 | """Reload all service instances to pick up new configuration""" 28 | global _ppt_service, _project_manager 29 | 30 | # First, reload research configuration in existing PPT service instances before clearing them 31 | try: 32 | if _ppt_service is not None: 33 | _ppt_service.reload_research_config() 34 | except Exception as e: 35 | import logging 36 | logger = logging.getLogger(__name__) 37 | logger.warning(f"Failed to reload research config in PPT service: {e}") 38 | 39 | # Also reload research service if it exists 40 | try: 41 | from ..api.landppt_api import reload_research_service 42 | reload_research_service() 43 | except ImportError: 44 | pass # Research service may not be available 45 | 46 | # Clear service instances to force recreation with new config 47 | _ppt_service = None 48 | _project_manager = None 49 | 50 | # Also reload PDF to PPTX converter configuration 51 | try: 52 | from .pdf_to_pptx_converter import reload_pdf_to_pptx_converter 53 | reload_pdf_to_pptx_converter() 54 | except ImportError: 55 | pass # PDF converter may not be available 56 | 57 | # Backward compatibility - create module-level variables that get updated 58 | def _update_module_vars(): 59 | """Update module-level variables for backward compatibility""" 60 | import sys 61 | current_module = sys.modules[__name__] 62 | current_module.ppt_service = get_ppt_service() 63 | current_module.project_manager = get_project_manager() 64 | 65 | # Initialize module variables 66 | _update_module_vars() 67 | 68 | # Override reload_services to also update module variables 69 | _original_reload_services = reload_services 70 | 71 | def reload_services(): 72 | """Reload all service instances and update module variables""" 73 | import logging 74 | logger = logging.getLogger(__name__) 75 | logger.info("Starting service reload process...") 76 | 77 | _original_reload_services() 78 | logger.info("Original service reload completed") 79 | 80 | # Refresh existing PPT service instance so existing imports pick up new config 81 | global _ppt_service 82 | if _ppt_service is not None: 83 | try: 84 | _ppt_service.update_ai_config() 85 | logger.info("Existing PPT service configuration refreshed") 86 | except Exception as refresh_error: # noqa: BLE001 87 | logger.warning("Failed to refresh PPT service configuration: %s", refresh_error, exc_info=True) 88 | else: 89 | logger.info("No existing PPT service instance, will initialize on next access") 90 | 91 | # Update module variables after services are reloaded 92 | _update_module_vars() 93 | logger.info("Module variables updated with new service instances") 94 | 95 | # Export for easy import 96 | __all__ = ['get_ppt_service', 'get_project_manager', 'reload_services', 'ppt_service', 'project_manager'] 97 | -------------------------------------------------------------------------------- /template_examples/日落大道.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "日落大道", 3 | "description": "日落大道", 4 | "html_template": "\n\n\n \n \n 日落大道 - HTML 母版模板\n \n \n \n\n\n\n\n \n
\n\n \n
\n\n \n
\n\n \n
'); \n background-repeat: no-repeat; background-size: cover;\">\n
\n\n \n
\n

\n {{ page_title }}\n

\n
\n\n \n
\n
\n {{ page_content }}\n
\n
\n\n \n
\n {{ current_page_number }} / {{ total_page_count }}\n
\n\n
\n
\n\n \n \n\n\n\n", 5 | "tags": [ 6 | "日落", 7 | "橙黄", 8 | "渐变" 9 | ], 10 | "is_default": false, 11 | "export_info": { 12 | "exported_at": "2025-10-15T13:34:04.008Z", 13 | "original_id": 11, 14 | "original_created_at": 1760185063.7941372 15 | } 16 | } -------------------------------------------------------------------------------- /src/landppt/services/prompts/system_prompts.py: -------------------------------------------------------------------------------- 1 | """ 2 | PPT系统提示词和默认配置 3 | 包含所有系统级别的提示词和默认配置 4 | """ 5 | 6 | import os 7 | from pathlib import Path 8 | 9 | 10 | class SystemPrompts: 11 | """PPT系统提示词和默认配置集合""" 12 | 13 | @staticmethod 14 | def get_default_ppt_system_prompt() -> str: 15 | """获取默认PPT生成系统提示词""" 16 | return """你是一个专业的PPT设计师和HTML开发专家。 17 | 18 | 核心职责: 19 | - 根据幻灯片内容生成高质量的HTML页面 20 | - 确保设计风格的一致性和专业性 21 | - 优化视觉表现和用户体验 22 | 23 | 设计原则: 24 | - 内容驱动设计:让设计服务于内容表达 25 | - 视觉层级清晰:突出重点信息,引导视觉流向 26 | - 风格统一协调:保持整体PPT的视觉一致性 27 | - 创意与一致性平衡:在保持风格一致性的前提下展现创意""" 28 | 29 | @staticmethod 30 | def get_keynote_style_prompt() -> str: 31 | """获取Keynote风格提示词""" 32 | return """请生成Apple风格的发布会PPT页面,具有以下特点: 33 | 1. 黑色背景,简洁现代的设计 34 | 2. 卡片式布局,突出重点信息 35 | 3. 使用科技蓝或品牌色作为高亮色 36 | 4. 大字号标题,清晰的视觉层级 37 | 5. 响应式设计,支持多设备显示 38 | 6. 使用Font Awesome图标和Chart.js图表 39 | 7. 平滑的动画效果 40 | 41 | 特别注意: 42 | - **结尾页(thankyou/conclusion类型)**:必须设计得令人印象深刻!使用Apple风格的特殊背景效果、发光文字、动态装饰、庆祝元素等,留下深刻的最后印象""" 43 | 44 | @staticmethod 45 | def load_prompts_md_system_prompt() -> str: 46 | """加载prompts.md系统提示词""" 47 | try: 48 | # 获取当前文件的目录 49 | current_dir = Path(__file__).parent 50 | # 构建prompts.md的路径 51 | prompts_file = current_dir / "prompts.md" 52 | 53 | if prompts_file.exists(): 54 | with open(prompts_file, 'r', encoding='utf-8') as f: 55 | content = f.read() 56 | return content 57 | else: 58 | # 如果文件不存在,返回默认提示词 59 | return SystemPrompts.get_default_ppt_system_prompt() 60 | except Exception as e: 61 | # 如果读取失败,返回默认提示词 62 | return SystemPrompts.get_default_ppt_system_prompt() 63 | 64 | @staticmethod 65 | def get_ai_assistant_system_prompt() -> str: 66 | """获取AI助手系统提示词""" 67 | return """你是一个专业的PPT制作助手,具备以下能力: 68 | 69 | 1. **内容理解与分析**: 70 | - 深入理解用户需求和项目背景 71 | - 分析目标受众和应用场景 72 | - 提取关键信息和重点内容 73 | 74 | 2. **结构化思维**: 75 | - 设计清晰的信息架构 76 | - 组织逻辑性强的内容流程 77 | - 确保信息传达的有效性 78 | 79 | 3. **设计美学**: 80 | - 运用专业的设计原则 81 | - 保持视觉风格的一致性 82 | - 平衡美观性与实用性 83 | 84 | 4. **技术实现**: 85 | - 生成高质量的HTML/CSS代码 86 | - 确保跨平台兼容性 87 | - 优化用户体验 88 | 89 | 请始终以专业、准确、高效的方式完成任务。""" 90 | 91 | @staticmethod 92 | def get_html_generation_system_prompt() -> str: 93 | """获取HTML生成系统提示词""" 94 | return """你是一个专业的前端开发专家,专门负责生成PPT页面的HTML代码。 95 | 96 | 技术要求: 97 | 1. **代码质量**: 98 | - 编写语义化的HTML结构 99 | - 使用现代CSS技术(Flexbox、Grid等) 100 | - 确保代码的可维护性和可扩展性 101 | 102 | 2. **响应式设计**: 103 | - 适配不同屏幕尺寸 104 | - 优化移动端体验 105 | - 确保内容的可访问性 106 | 107 | 3. **性能优化**: 108 | - 优化加载速度 109 | - 减少不必要的资源请求 110 | - 使用高效的CSS选择器 111 | 112 | 4. **兼容性**: 113 | - 支持主流浏览器 114 | - 处理兼容性问题 115 | - 提供降级方案 116 | 117 | 5. **交互效果**: 118 | - 实现平滑的动画效果 119 | - 添加适当的交互反馈 120 | - 增强用户体验 121 | 122 | 请确保生成的HTML代码符合现代Web标准。""" 123 | 124 | @staticmethod 125 | def get_content_analysis_system_prompt() -> str: 126 | """获取内容分析系统提示词""" 127 | return """你是一个专业的内容分析专家,负责分析和优化PPT内容。 128 | 129 | 分析维度: 130 | 1. **内容结构**: 131 | - 评估信息的逻辑性和完整性 132 | - 检查内容的层次结构 133 | - 确保信息流的连贯性 134 | 135 | 2. **语言表达**: 136 | - 优化文字表达的准确性 137 | - 提升语言的专业性和吸引力 138 | - 确保语言风格的一致性 139 | 140 | 3. **信息密度**: 141 | - 控制每页的信息量 142 | - 平衡详细程度和简洁性 143 | - 优化信息的可读性 144 | 145 | 4. **目标适配**: 146 | - 确保内容符合目标受众需求 147 | - 调整语言风格和专业程度 148 | - 优化信息传达效果 149 | 150 | 5. **视觉化建议**: 151 | - 识别适合图表化的数据 152 | - 提供可视化方案建议 153 | - 增强信息的表达力 154 | 155 | 请提供专业、准确的内容分析和优化建议。""" 156 | 157 | @staticmethod 158 | def get_custom_style_prompt(custom_prompt: str) -> str: 159 | """获取自定义风格提示词""" 160 | return f""" 161 | 请根据以下自定义风格要求生成PPT页面: 162 | 163 | {custom_prompt} 164 | 165 | 请确保生成的HTML页面符合上述风格要求,同时保持良好的可读性和用户体验。 166 | """ 167 | -------------------------------------------------------------------------------- /src/landppt/web/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}LandPPT - AI PPT Generator{% endblock %} 4 | 5 | {% block extra_css %} 6 | 105 | {% endblock %} 106 | 107 | {% block content %} 108 |
109 |

欢迎使用 LandPPT

110 |

只需输入主题和需求,AI 即可自动生成专业的 PPT 演示文稿

111 | {% if ai_provider %} 112 |
113 | 当前 AI 提供者: {{ ai_provider }} 114 | 系统配置 115 |
116 | {% endif %} 117 |
118 | 119 |
120 |
121 |

快速生成

122 |

选择场景模板,输入主题,一键生成专业 PPT。

123 | 开始创建 PPT 124 |
125 | 126 |
127 |

项目仪表板

128 |

管理项目、查看 TODO 看板与进度跟踪,掌握项目全局。

129 | 打开仪表板 130 |
131 |
132 | 133 |
134 |

API 接口支持

135 |

LandPPT 提供完整的 OpenAI 兼容 API,支持与现有工具和客户端无缝集成。

136 | 查看 API 文档 137 |
138 | {% endblock %} 139 | -------------------------------------------------------------------------------- /src/landppt/ai/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base classes for AI providers 3 | """ 4 | 5 | from abc import ABC, abstractmethod 6 | from typing import List, Dict, Any, Optional, AsyncGenerator, Union 7 | from pydantic import BaseModel 8 | from enum import Enum 9 | 10 | class MessageRole(str, Enum): 11 | """Message roles for AI conversations""" 12 | SYSTEM = "system" 13 | USER = "user" 14 | ASSISTANT = "assistant" 15 | 16 | class MessageContentType(str, Enum): 17 | """Message content types for multimodal support""" 18 | TEXT = "text" 19 | IMAGE_URL = "image_url" 20 | 21 | class ImageContent(BaseModel): 22 | """Image content for multimodal messages""" 23 | type: MessageContentType = MessageContentType.IMAGE_URL 24 | image_url: Dict[str, str] # {"url": "data:image/jpeg;base64,..." or "http://..."} 25 | 26 | class TextContent(BaseModel): 27 | """Text content for multimodal messages""" 28 | type: MessageContentType = MessageContentType.TEXT 29 | text: str 30 | 31 | class AIMessage(BaseModel): 32 | """AI message model with multimodal support""" 33 | role: MessageRole 34 | content: Union[str, List[Union[TextContent, ImageContent]]] # Support both simple string and multimodal content 35 | name: Optional[str] = None 36 | 37 | class AIResponse(BaseModel): 38 | """AI response model""" 39 | content: str 40 | model: str 41 | usage: Dict[str, int] 42 | finish_reason: Optional[str] = None 43 | metadata: Dict[str, Any] = {} 44 | 45 | class AIProvider(ABC): 46 | """Abstract base class for AI providers""" 47 | 48 | def __init__(self, config: Dict[str, Any]): 49 | self.config = config 50 | self.model = config.get("model", "unknown") 51 | 52 | @abstractmethod 53 | async def chat_completion( 54 | self, 55 | messages: List[AIMessage], 56 | **kwargs 57 | ) -> AIResponse: 58 | """Generate chat completion""" 59 | pass 60 | 61 | @abstractmethod 62 | async def text_completion( 63 | self, 64 | prompt: str, 65 | **kwargs 66 | ) -> AIResponse: 67 | """Generate text completion""" 68 | pass 69 | 70 | async def stream_chat_completion( 71 | self, 72 | messages: List[AIMessage], 73 | **kwargs 74 | ) -> AsyncGenerator[str, None]: 75 | """Stream chat completion (optional)""" 76 | # Default implementation: return full response at once 77 | response = await self.chat_completion(messages, **kwargs) 78 | yield response.content 79 | 80 | async def stream_text_completion( 81 | self, 82 | prompt: str, 83 | **kwargs 84 | ) -> AsyncGenerator[str, None]: 85 | """Stream text completion (optional)""" 86 | # Default implementation: return full response at once 87 | response = await self.text_completion(prompt, **kwargs) 88 | yield response.content 89 | 90 | def get_model_info(self) -> Dict[str, Any]: 91 | """Get model information""" 92 | return { 93 | "model": self.model, 94 | "provider": self.__class__.__name__, 95 | "config": {k: v for k, v in self.config.items() if "key" not in k.lower()} 96 | } 97 | 98 | def _calculate_usage(self, prompt: str, response: str) -> Dict[str, int]: 99 | """Calculate token usage (simplified)""" 100 | # Simplified calculation 101 | prompt_tokens = len(prompt.split()) 102 | completion_tokens = len(response.split()) 103 | 104 | return { 105 | "prompt_tokens": prompt_tokens, 106 | "completion_tokens": completion_tokens, 107 | "total_tokens": prompt_tokens + completion_tokens 108 | } 109 | 110 | def _merge_config(self, **kwargs) -> Dict[str, Any]: 111 | """Merge provider config with request parameters""" 112 | merged = self.config.copy() 113 | merged.update(kwargs) 114 | return merged 115 | -------------------------------------------------------------------------------- /.github/workflows/docker-release.yml: -------------------------------------------------------------------------------- 1 | # LandPPT Release Workflow 2 | # 当创建 Release 或 Tag 时自动构建并推送带版本号的 Docker 镜像 3 | 4 | name: Release Docker Image 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | push: 11 | tags: 12 | - 'v*.*.*' # 匹配 v1.0.0, v2.1.3 等版本标签 13 | 14 | env: 15 | IMAGE_NAME: landppt 16 | PLATFORMS: linux/amd64,linux/arm64 17 | 18 | jobs: 19 | release: 20 | name: Build and Release 21 | runs-on: ubuntu-latest 22 | 23 | permissions: 24 | contents: read 25 | packages: write 26 | 27 | steps: 28 | - name: Checkout Repository 29 | uses: actions/checkout@v4 30 | 31 | # 清理磁盘空间(防止构建时空间不足) 32 | - name: Free Disk Space 33 | run: | 34 | sudo rm -rf /usr/share/dotnet 35 | sudo rm -rf /usr/local/lib/android 36 | sudo rm -rf /opt/ghc 37 | sudo rm -rf /opt/hostedtoolcache/CodeQL 38 | sudo docker system prune -af 39 | df -h 40 | 41 | # 设置 QEMU(用于多平台构建) 42 | - name: Set up QEMU 43 | uses: docker/setup-qemu-action@v3 44 | 45 | - name: Set up Docker Buildx 46 | uses: docker/setup-buildx-action@v3 47 | 48 | # 登录到 GitHub Container Registry 49 | - name: Login to GitHub Container Registry 50 | uses: docker/login-action@v3 51 | with: 52 | registry: ghcr.io 53 | username: ${{ github.actor }} 54 | password: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | # 登录到 Docker Hub(如果配置了) 57 | - name: Login to Docker Hub 58 | uses: docker/login-action@v3 59 | with: 60 | username: ${{ secrets.DOCKERHUB_USERNAME }} 61 | password: ${{ secrets.DOCKERHUB_TOKEN }} 62 | continue-on-error: true 63 | 64 | # 提取版本信息 65 | - name: Extract Version 66 | id: version 67 | run: | 68 | if [[ "${GITHUB_REF}" == refs/tags/* ]]; then 69 | VERSION=${GITHUB_REF#refs/tags/} 70 | VERSION=${VERSION#v} # 移除 v 前缀 71 | else 72 | VERSION="latest" 73 | fi 74 | echo "version=$VERSION" >> $GITHUB_OUTPUT 75 | echo "Version: $VERSION" 76 | 77 | # 提取 Docker 元数据 78 | - name: Extract Docker Metadata 79 | id: meta 80 | uses: docker/metadata-action@v5 81 | with: 82 | images: | 83 | ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} 84 | ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} 85 | tags: | 86 | type=semver,pattern={{version}} 87 | type=semver,pattern={{major}}.{{minor}} 88 | type=semver,pattern={{major}} 89 | type=raw,value=latest 90 | 91 | # 构建并推送多平台镜像 92 | - name: Build and Push Multi-Platform Image 93 | uses: docker/build-push-action@v5 94 | with: 95 | context: . 96 | file: ./Dockerfile 97 | platforms: ${{ env.PLATFORMS }} 98 | push: true 99 | tags: ${{ steps.meta.outputs.tags }} 100 | labels: ${{ steps.meta.outputs.labels }} 101 | cache-from: type=gha 102 | cache-to: type=gha,mode=max 103 | build-args: | 104 | VERSION=${{ steps.version.outputs.version }} 105 | 106 | # 生成发布说明 107 | - name: Create Release Summary 108 | run: | 109 | echo "### 🎉 Release ${{ steps.version.outputs.version }} Published!" >> $GITHUB_STEP_SUMMARY 110 | echo "" >> $GITHUB_STEP_SUMMARY 111 | echo "**Docker Images:**" >> $GITHUB_STEP_SUMMARY 112 | echo "" >> $GITHUB_STEP_SUMMARY 113 | echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY 114 | echo "# 拉取最新版本" >> $GITHUB_STEP_SUMMARY 115 | echo "docker pull ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY 116 | echo "" >> $GITHUB_STEP_SUMMARY 117 | echo "# 拉取指定版本" >> $GITHUB_STEP_SUMMARY 118 | echo "docker pull ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY 119 | echo "\`\`\`" >> $GITHUB_STEP_SUMMARY 120 | echo "" >> $GITHUB_STEP_SUMMARY 121 | echo "**Supported Platforms:** \`${{ env.PLATFORMS }}\`" >> $GITHUB_STEP_SUMMARY 122 | -------------------------------------------------------------------------------- /src/landppt/utils/thread_pool.py: -------------------------------------------------------------------------------- 1 | """ 2 | 线程池工具类,用于将阻塞操作放入线程池中执行 3 | """ 4 | 5 | import asyncio 6 | import concurrent.futures 7 | import functools 8 | import logging 9 | import os 10 | from typing import Any, Callable, TypeVar, Coroutine, Optional, Dict 11 | 12 | # 类型变量 13 | T = TypeVar('T') 14 | R = TypeVar('R') 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | class ThreadPoolManager: 19 | """线程池管理器,提供全局线程池和辅助方法""" 20 | 21 | _instance = None 22 | _initialized = False 23 | 24 | def __new__(cls, *args, **kwargs): 25 | if cls._instance is None: 26 | cls._instance = super(ThreadPoolManager, cls).__new__(cls) 27 | return cls._instance 28 | 29 | def __init__(self, max_workers: Optional[int] = None): 30 | """初始化线程池管理器 31 | 32 | Args: 33 | max_workers: 最大工作线程数,默认为 None(使用 CPU 核心数 * 5) 34 | """ 35 | if not self._initialized: 36 | # 如果未指定最大工作线程数,则使用 CPU 核心数 * 5 37 | if max_workers is None: 38 | max_workers = os.cpu_count() * 5 if os.cpu_count() else 20 39 | 40 | self.executor = concurrent.futures.ThreadPoolExecutor( 41 | max_workers=max_workers, 42 | thread_name_prefix="landppt_worker" 43 | ) 44 | 45 | # 统计信息 46 | self.stats = { 47 | "total_tasks": 0, 48 | "completed_tasks": 0, 49 | "failed_tasks": 0, 50 | "active_tasks": 0 51 | } 52 | 53 | self._initialized = True 54 | logger.info(f"线程池初始化完成,最大工作线程数: {max_workers}") 55 | 56 | async def run_in_thread(self, func: Callable[..., T], *args, **kwargs) -> T: 57 | """在线程池中运行同步函数 58 | 59 | Args: 60 | func: 要运行的同步函数 61 | *args: 传递给函数的位置参数 62 | **kwargs: 传递给函数的关键字参数 63 | 64 | Returns: 65 | 函数的返回值 66 | 67 | Raises: 68 | Exception: 如果函数执行失败,则抛出异常 69 | """ 70 | self.stats["total_tasks"] += 1 71 | self.stats["active_tasks"] += 1 72 | 73 | try: 74 | loop = asyncio.get_running_loop() 75 | result = await loop.run_in_executor( 76 | self.executor, 77 | functools.partial(func, *args, **kwargs) 78 | ) 79 | 80 | self.stats["completed_tasks"] += 1 81 | return result 82 | except Exception as e: 83 | self.stats["failed_tasks"] += 1 84 | logger.error(f"线程池任务执行失败: {e}") 85 | raise 86 | finally: 87 | self.stats["active_tasks"] -= 1 88 | 89 | def get_stats(self) -> Dict[str, int]: 90 | """获取线程池统计信息""" 91 | return self.stats.copy() 92 | 93 | def shutdown(self, wait: bool = True): 94 | """关闭线程池 95 | 96 | Args: 97 | wait: 是否等待所有线程完成 98 | """ 99 | if self._initialized: 100 | self.executor.shutdown(wait=wait) 101 | self._initialized = False 102 | logger.info("线程池已关闭") 103 | 104 | 105 | # 全局线程池实例 106 | thread_pool = ThreadPoolManager() 107 | 108 | 109 | async def run_blocking_io(func: Callable[..., T], *args, **kwargs) -> T: 110 | """运行阻塞的 I/O 操作 111 | 112 | 这是一个便捷函数,用于将阻塞的 I/O 操作放入线程池中执行 113 | 114 | Args: 115 | func: 要运行的阻塞函数 116 | *args: 传递给函数的位置参数 117 | **kwargs: 传递给函数的关键字参数 118 | 119 | Returns: 120 | 函数的返回值 121 | """ 122 | return await thread_pool.run_in_thread(func, *args, **kwargs) 123 | 124 | 125 | def to_thread(func: Callable[..., T]) -> Callable[..., Coroutine[Any, Any, T]]: 126 | """装饰器:将同步函数转换为在线程池中运行的异步函数 127 | 128 | 用法: 129 | @to_thread 130 | def blocking_function(arg1, arg2): 131 | # 执行阻塞操作 132 | return result 133 | 134 | # 调用 135 | result = await blocking_function(arg1, arg2) 136 | 137 | Args: 138 | func: 要装饰的同步函数 139 | 140 | Returns: 141 | 异步包装函数 142 | """ 143 | @functools.wraps(func) 144 | async def wrapper(*args, **kwargs): 145 | return await thread_pool.run_in_thread(func, *args, **kwargs) 146 | return wrapper 147 | -------------------------------------------------------------------------------- /template_examples/五彩斑斓的黑.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "五彩斑斓的黑", 3 | "description": "以深邃的黑色或暗色调为基底,巧妙地融入了霓虹、渐变、镭射等五彩斑斓的光影元素。它旨在创造一种既神秘、高级,又不失活力与未来感的视觉冲击力。", 4 | "html_template": "\n\n\n \n \n 五彩斑斓的黑 - PPT模板\n \n \n \n \n \n \n \n \n \n \n\n\n\n \n
\n \n \n
\n \n \n
\n\n \n
\n

{{ page_title }}

\n
\n\n \n
\n \n {{ page_content }}\n
\n\n \n \n\n
\n
\n
\n\n", 5 | "tags": [ 6 | "科技感", 7 | "未来主义", 8 | "赛博朋克", 9 | "黑色", 10 | "暗黑", 11 | "霓虹", 12 | "渐变", 13 | "视觉冲击", 14 | "酷炫", 15 | "简约", 16 | "高级" 17 | ], 18 | "is_default": false, 19 | "export_info": { 20 | "exported_at": "2025-10-04T06:51:32.173Z", 21 | "original_id": 11, 22 | "original_created_at": 1755685358.599486 23 | } 24 | } -------------------------------------------------------------------------------- /src/summeryanyfile/utils/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | 日志工具 - 配置和管理应用日志 3 | """ 4 | 5 | import logging 6 | import sys 7 | from typing import Optional 8 | from pathlib import Path 9 | from rich.logging import RichHandler 10 | from rich.console import Console 11 | 12 | 13 | def setup_logging( 14 | level: str = "INFO", 15 | log_file: Optional[str] = None, 16 | rich_logging: bool = True, 17 | format_string: Optional[str] = None 18 | ) -> logging.Logger: 19 | """ 20 | 设置日志配置 21 | 22 | Args: 23 | level: 日志级别 24 | log_file: 日志文件路径 25 | rich_logging: 是否使用Rich格式化 26 | format_string: 自定义格式字符串 27 | 28 | Returns: 29 | 配置好的logger 30 | """ 31 | # 清除现有的handlers 32 | root_logger = logging.getLogger() 33 | for handler in root_logger.handlers[:]: 34 | root_logger.removeHandler(handler) 35 | 36 | # 设置日志级别 37 | numeric_level = getattr(logging, level.upper(), logging.INFO) 38 | root_logger.setLevel(numeric_level) 39 | 40 | # 默认格式 41 | if format_string is None: 42 | format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 43 | 44 | handlers = [] 45 | 46 | # 控制台处理器 47 | if rich_logging: 48 | try: 49 | console = Console(stderr=True) 50 | console_handler = RichHandler( 51 | console=console, 52 | show_time=True, 53 | show_path=True, 54 | markup=True, 55 | rich_tracebacks=True 56 | ) 57 | console_handler.setLevel(numeric_level) 58 | except ImportError: 59 | # 如果Rich不可用,使用标准处理器 60 | console_handler = logging.StreamHandler(sys.stderr) 61 | console_handler.setLevel(numeric_level) 62 | formatter = logging.Formatter(format_string) 63 | console_handler.setFormatter(formatter) 64 | else: 65 | console_handler = logging.StreamHandler(sys.stderr) 66 | console_handler.setLevel(numeric_level) 67 | formatter = logging.Formatter(format_string) 68 | console_handler.setFormatter(formatter) 69 | 70 | handlers.append(console_handler) 71 | 72 | # 文件处理器 73 | if log_file: 74 | log_path = Path(log_file) 75 | log_path.parent.mkdir(parents=True, exist_ok=True) 76 | 77 | file_handler = logging.FileHandler(log_file, encoding='utf-8') 78 | file_handler.setLevel(numeric_level) 79 | file_formatter = logging.Formatter(format_string) 80 | file_handler.setFormatter(file_formatter) 81 | handlers.append(file_handler) 82 | 83 | # 添加处理器 84 | for handler in handlers: 85 | root_logger.addHandler(handler) 86 | 87 | # 设置第三方库的日志级别 88 | logging.getLogger("httpx").setLevel(logging.WARNING) 89 | logging.getLogger("httpcore").setLevel(logging.WARNING) 90 | logging.getLogger("urllib3").setLevel(logging.WARNING) 91 | logging.getLogger("requests").setLevel(logging.WARNING) 92 | 93 | return root_logger 94 | 95 | 96 | def get_logger(name: str) -> logging.Logger: 97 | """ 98 | 获取指定名称的logger 99 | 100 | Args: 101 | name: logger名称 102 | 103 | Returns: 104 | logger实例 105 | """ 106 | return logging.getLogger(name) 107 | 108 | 109 | class ProgressLogger: 110 | """进度日志记录器""" 111 | 112 | def __init__(self, logger: logging.Logger, total_steps: int): 113 | self.logger = logger 114 | self.total_steps = total_steps 115 | self.current_step = 0 116 | 117 | def update(self, step_name: str, increment: int = 1): 118 | """更新进度""" 119 | self.current_step += increment 120 | progress = (self.current_step / self.total_steps) * 100 121 | self.logger.info(f"[{progress:.1f}%] {step_name}") 122 | 123 | def set_step(self, step: int, step_name: str): 124 | """设置当前步骤""" 125 | self.current_step = step 126 | progress = (self.current_step / self.total_steps) * 100 127 | self.logger.info(f"[{progress:.1f}%] {step_name}") 128 | 129 | def complete(self, message: str = "处理完成"): 130 | """标记完成""" 131 | self.current_step = self.total_steps 132 | self.logger.info(f"[100.0%] {message}") 133 | 134 | 135 | class LoggerMixin: 136 | """日志混入类""" 137 | 138 | @property 139 | def logger(self) -> logging.Logger: 140 | """获取logger""" 141 | if not hasattr(self, '_logger'): 142 | self._logger = get_logger(self.__class__.__name__) 143 | return self._logger 144 | -------------------------------------------------------------------------------- /template_examples/科技风.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "科技风", 3 | "description": "科技风", 4 | "html_template": "\n\n\n \n \n 科技风PPT母版模板\n\n \n \n \n \n\n \n \n\n \n \n \n \n \n \n \n\n\n\n \n
\n\n \n \n
\n\n \n
\n
\n
\n
\n\n \n
\n

\n \n {{ page_title }}\n

\n
\n\n \n
\n \n \n \n
\n {{ page_content }}\n
\n
\n\n \n \n\n
\n
\n\n\n", 5 | "tags": [ 6 | "科技风" 7 | ], 8 | "is_default": false, 9 | "export_info": { 10 | "exported_at": "2025-10-04T06:54:24.814Z", 11 | "original_id": 14, 12 | "original_created_at": 1755855138.9754753 13 | } 14 | } -------------------------------------------------------------------------------- /template_examples/商务.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "默认商务模板", 3 | "description": "现代简约的商务PPT模板,适用于各种商务演示场景。采用深色背景和蓝色主色调,支持多种内容类型展示。", 4 | "html_template": "\n\n\n \n \n {{ page_title }}\n \n \n \n \n\n\n
\n
\n

{{ main_heading }}

\n
\n \n
\n
\n {{ page_content }}\n
\n
\n \n
\n {{ current_page_number }} / {{ total_page_count }}\n
\n
\n\n", 5 | "tags": [ 6 | "默认", 7 | "商务", 8 | "现代", 9 | "简约", 10 | "深色" 11 | ], 12 | "is_default": false, 13 | "export_info": { 14 | "exported_at": "2025-06-28T10:11:20.488Z", 15 | "original_id": 1, 16 | "original_created_at": 1749553414.0671556 17 | } 18 | } -------------------------------------------------------------------------------- /src/landppt/services/prompts/content_prompts.py: -------------------------------------------------------------------------------- 1 | """ 2 | PPT内容生成和增强相关提示词 3 | 包含所有用于生成和优化PPT内容的提示词模板 4 | """ 5 | 6 | from typing import Dict, Any, List 7 | 8 | 9 | class ContentPrompts: 10 | """PPT内容生成和增强相关的提示词集合""" 11 | 12 | @staticmethod 13 | def get_slide_content_prompt_zh(slide_title: str, scenario: str, topic: str) -> str: 14 | """获取中文幻灯片内容生成提示词""" 15 | return f"""为PPT幻灯片生成内容: 16 | 17 | PPT主题:{topic} 18 | 幻灯片标题:{slide_title} 19 | 场景类型:{scenario} 20 | 21 | 请生成这张幻灯片的具体内容,包括: 22 | - 3-5个要点 23 | - 每个要点的简短说明 24 | - 适合{scenario}场景的语言风格 25 | 26 | 内容要求: 27 | - 简洁明了,适合幻灯片展示 28 | - 逻辑清晰,层次分明 29 | - 语言专业但易懂 30 | - 符合中文表达习惯 31 | 32 | 请直接输出内容,不需要额外说明。""" 33 | 34 | @staticmethod 35 | def get_slide_content_prompt_en(slide_title: str, scenario: str, topic: str) -> str: 36 | """获取英文幻灯片内容生成提示词""" 37 | return f"""Generate content for a PPT slide: 38 | 39 | PPT Topic: {topic} 40 | Slide Title: {slide_title} 41 | Scenario: {scenario} 42 | 43 | Please generate specific content for this slide, including: 44 | - 3-5 key points 45 | - Brief explanation for each point 46 | - Language style appropriate for {scenario} scenario 47 | 48 | Content requirements: 49 | - Concise and suitable for slide presentation 50 | - Clear logic and structure 51 | - Professional but understandable language 52 | - Appropriate for the target audience 53 | 54 | Please output the content directly without additional explanations.""" 55 | 56 | @staticmethod 57 | def get_enhancement_prompt_zh(content: str, scenario: str) -> str: 58 | """获取中文内容增强提示词""" 59 | return f"""请优化以下PPT内容,使其更适合{scenario}场景: 60 | 61 | 原始内容: 62 | {content} 63 | 64 | 优化要求: 65 | - 保持原有信息的完整性 66 | - 改善语言表达和逻辑结构 67 | - 增加适合{scenario}场景的专业术语 68 | - 使内容更具吸引力和说服力 69 | - 保持简洁明了的风格 70 | 71 | 请输出优化后的内容:""" 72 | 73 | @staticmethod 74 | def get_enhancement_prompt_en(content: str, scenario: str) -> str: 75 | """获取英文内容增强提示词""" 76 | return f"""Please enhance the following PPT content to make it more suitable for {scenario} scenario: 77 | 78 | Original content: 79 | {content} 80 | 81 | Enhancement requirements: 82 | - Maintain the completeness of original information 83 | - Improve language expression and logical structure 84 | - Add professional terminology suitable for {scenario} scenario 85 | - Make content more attractive and persuasive 86 | - Keep concise and clear style 87 | 88 | Please output the enhanced content:""" 89 | 90 | @staticmethod 91 | def get_ppt_creation_context(topic: str, stage_type: str, focus_content: List[str], 92 | tech_highlights: List[str], target_audience: str, description: str) -> str: 93 | """获取PPT创建上下文提示词""" 94 | focus_content_str = ', '.join(focus_content) if focus_content else '无' 95 | tech_highlights_str = ', '.join(tech_highlights) if tech_highlights else '无' 96 | 97 | return f"""请为以下项目生成PPT页面: 98 | 99 | 项目信息: 100 | - 主题:{topic} 101 | - 类型:{stage_type} 102 | - 重点展示内容:{focus_content_str} 103 | - 技术亮点:{tech_highlights_str} 104 | - 目标受众:{target_audience} 105 | - 其他说明:{description or '无'} 106 | 107 | 请根据大纲内容生成专业的HTML PPT页面,确保设计风格统一,内容表达清晰。""" 108 | 109 | @staticmethod 110 | def get_general_stage_prompt(topic: str, stage_type: str, description: str) -> str: 111 | """获取通用阶段任务提示词""" 112 | return f"""项目信息: 113 | - 主题:{topic} 114 | - 类型:{stage_type} 115 | - 其他说明:{description or '无'} 116 | 117 | 当前阶段:{stage_type} 118 | 119 | 请根据以上信息完成当前阶段的任务。""" 120 | 121 | @staticmethod 122 | def get_general_subtask_context(topic: str, stage_type: str, focus_content: List[str], 123 | tech_highlights: List[str], target_audience: str, 124 | description: str, subtask: str) -> str: 125 | """获取通用子任务上下文提示词""" 126 | focus_content_str = ', '.join(focus_content) if focus_content else '无' 127 | tech_highlights_str = ', '.join(tech_highlights) if tech_highlights else '无' 128 | 129 | return f"""项目信息: 130 | - 主题:{topic} 131 | - 类型:{stage_type} 132 | - 重点展示内容:{focus_content_str} 133 | - 技术亮点:{tech_highlights_str} 134 | - 目标受众:{target_audience} 135 | - 其他说明:{description or '无'} 136 | 137 | 当前子任务:{subtask} 138 | 139 | 请根据以上信息完成当前子任务。""" 140 | 141 | @staticmethod 142 | def get_general_subtask_prompt(confirmed_requirements: Dict[str, Any], stage_name: str, subtask: str) -> str: 143 | """获取通用子任务提示词""" 144 | return f"""项目信息: 145 | - 主题:{confirmed_requirements['topic']} 146 | - 类型:{confirmed_requirements['type']} 147 | - 重点展示内容:{confirmed_requirements['focus_content']} 148 | - 技术亮点:{confirmed_requirements['tech_highlights']} 149 | - 目标受众:{confirmed_requirements['target_audience']} 150 | - 其他说明:{confirmed_requirements.get('description', '无')} 151 | 152 | 当前阶段:{stage_name} 153 | 当前子任务:{subtask} 154 | 155 | 请根据以上信息执行当前子任务。 156 | """ 157 | -------------------------------------------------------------------------------- /template_examples/清新笔记.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "清新笔记", 3 | "description": "一款简约清新的笔记风格模板,适合个人分享、生活记录、作品展示等场景。采用柔和的暖色调和优雅字体,营造温馨舒适的氛围。", 4 | "html_template": "\n\n\n \n \n {{ page_title }}\n \n \n \n \n \n \n \n\n\n
\n
\n

{{ main_heading }}

\n
\n \n
\n
\n {{ page_content }}\n
\n
\n \n
\n {{ current_page_number }} / {{ total_page_count }}\n
\n
\n\n", 5 | "tags": [ 6 | "生活", 7 | "文艺", 8 | "笔记", 9 | "简约", 10 | "浅色", 11 | "暖色调" 12 | ], 13 | "is_default": false, 14 | "export_info": { 15 | "exported_at": "2025-10-04T06:52:49.624Z", 16 | "original_id": 9, 17 | "original_created_at": 1752919914.5996413 18 | } 19 | } -------------------------------------------------------------------------------- /src/landppt/services/url_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL生成服务 3 | 统一管理所有URL生成逻辑,支持反向代理域名配置 4 | """ 5 | 6 | import logging 7 | from typing import Optional 8 | from urllib.parse import urljoin 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class URLService: 14 | """URL生成服务""" 15 | 16 | def __init__(self): 17 | self._base_url = None 18 | self._config_service = None 19 | 20 | def _get_config_service(self): 21 | """延迟加载配置服务,避免循环导入""" 22 | if self._config_service is None: 23 | from .config_service import config_service 24 | self._config_service = config_service 25 | return self._config_service 26 | 27 | def _get_base_url(self) -> str: 28 | """获取基础URL配置""" 29 | # 每次都重新获取配置,确保使用最新的配置 30 | try: 31 | config_service = self._get_config_service() 32 | app_config = config_service.get_config_by_category('app_config') 33 | base_url = app_config.get('base_url', 'http://localhost:8000') 34 | 35 | # 确保URL不以斜杠结尾 36 | if base_url.endswith('/'): 37 | base_url = base_url[:-1] 38 | 39 | logger.debug(f"Using base URL: {base_url}") 40 | return base_url 41 | 42 | except Exception as e: 43 | logger.warning(f"无法获取基础URL配置,使用默认值: {e}") 44 | return 'http://localhost:8000' 45 | 46 | def build_absolute_url(self, relative_path: str) -> str: 47 | """构建绝对URL""" 48 | base_url = self._get_base_url() 49 | 50 | # 确保相对路径以斜杠开头 51 | if not relative_path.startswith('/'): 52 | relative_path = '/' + relative_path 53 | 54 | absolute_url = f"{base_url}{relative_path}" 55 | logger.debug(f"Built absolute URL: {relative_path} -> {absolute_url}") 56 | return absolute_url 57 | 58 | def build_image_url(self, image_id: str) -> str: 59 | """构建图片访问URL""" 60 | return self.build_absolute_url(f"/api/image/view/{image_id}") 61 | 62 | def build_image_thumbnail_url(self, image_id: str) -> str: 63 | """构建图片缩略图URL""" 64 | return self.build_absolute_url(f"/api/image/thumbnail/{image_id}") 65 | 66 | def build_image_download_url(self, image_id: str) -> str: 67 | """构建图片下载URL""" 68 | return self.build_absolute_url(f"/api/image/download/{image_id}") 69 | 70 | def build_static_url(self, static_path: str) -> str: 71 | """构建静态资源URL""" 72 | # 确保静态路径不以斜杠开头(因为/static已经包含了) 73 | if static_path.startswith('/'): 74 | static_path = static_path[1:] 75 | return self.build_absolute_url(f"/static/{static_path}") 76 | 77 | def build_temp_url(self, temp_path: str) -> str: 78 | """构建临时文件URL""" 79 | # 确保临时路径不以斜杠开头(因为/temp已经包含了) 80 | if temp_path.startswith('/'): 81 | temp_path = temp_path[1:] 82 | return self.build_absolute_url(f"/temp/{temp_path}") 83 | 84 | def get_current_base_url(self) -> str: 85 | """获取当前配置的基础URL""" 86 | return self._get_base_url() 87 | 88 | def is_localhost_url(self, url: str) -> bool: 89 | """检查URL是否为localhost""" 90 | return 'localhost' in url or '127.0.0.1' in url 91 | 92 | def validate_base_url(self, base_url: str) -> bool: 93 | """验证基础URL格式""" 94 | try: 95 | if not base_url: 96 | return False 97 | 98 | # 基本格式检查 99 | if not (base_url.startswith('http://') or base_url.startswith('https://')): 100 | return False 101 | 102 | # 不能以斜杠结尾 103 | if base_url.endswith('/'): 104 | return False 105 | 106 | return True 107 | 108 | except Exception as e: 109 | logger.error(f"URL validation error: {e}") 110 | return False 111 | 112 | 113 | # 全局URL服务实例 114 | _url_service = None 115 | 116 | 117 | def get_url_service() -> URLService: 118 | """获取全局URL服务实例""" 119 | global _url_service 120 | if _url_service is None: 121 | _url_service = URLService() 122 | return _url_service 123 | 124 | 125 | # 便捷函数 126 | def build_absolute_url(relative_path: str) -> str: 127 | """构建绝对URL的便捷函数""" 128 | return get_url_service().build_absolute_url(relative_path) 129 | 130 | 131 | def build_image_url(image_id: str) -> str: 132 | """构建图片URL的便捷函数""" 133 | return get_url_service().build_image_url(image_id) 134 | 135 | 136 | def build_image_thumbnail_url(image_id: str) -> str: 137 | """构建图片缩略图URL的便捷函数""" 138 | return get_url_service().build_image_thumbnail_url(image_id) 139 | 140 | 141 | def build_image_download_url(image_id: str) -> str: 142 | """构建图片下载URL的便捷函数""" 143 | return get_url_service().build_image_download_url(image_id) 144 | 145 | 146 | def get_current_base_url() -> str: 147 | """获取当前基础URL的便捷函数""" 148 | return get_url_service().get_current_base_url() 149 | -------------------------------------------------------------------------------- /src/landppt/services/prompts/repair_prompts.py: -------------------------------------------------------------------------------- 1 | """ 2 | PPT修复和验证相关提示词 3 | 包含所有用于修复和验证PPT数据的提示词模板 4 | """ 5 | 6 | from typing import Dict, Any, List 7 | 8 | 9 | class RepairPrompts: 10 | """PPT修复和验证相关的提示词集合""" 11 | 12 | @staticmethod 13 | def get_repair_prompt(outline_data: Dict[str, Any], validation_errors: List[str], 14 | confirmed_requirements: Dict[str, Any]) -> str: 15 | """获取大纲修复提示词""" 16 | # 获取页数要求 17 | page_count_settings = confirmed_requirements.get('page_count_settings', {}) 18 | page_count_mode = page_count_settings.get('mode', 'ai_decide') 19 | 20 | page_count_instruction = "" 21 | if page_count_mode == 'custom_range': 22 | min_pages = page_count_settings.get('min_pages', 8) 23 | max_pages = page_count_settings.get('max_pages', 15) 24 | page_count_instruction = f"- 页数要求:必须严格生成{min_pages}-{max_pages}页的PPT" 25 | elif page_count_mode == 'fixed': 26 | fixed_pages = page_count_settings.get('fixed_pages', 10) 27 | page_count_instruction = f"- 页数要求:必须生成恰好{fixed_pages}页的PPT" 28 | else: 29 | page_count_instruction = "- 页数要求:保持现有页数和内容,仅修复错误" 30 | 31 | errors_text = '\n'.join(["- " + str(error) for error in validation_errors]) 32 | 33 | return f"""作为专业的PPT大纲修复助手,请修复以下PPT大纲JSON数据中的错误。 34 | 35 | 项目信息: 36 | - 主题:{confirmed_requirements.get('topic', '未知')} 37 | - 类型:{confirmed_requirements.get('type', '未知')} 38 | - 重点内容:{', '.join(confirmed_requirements.get('focus_content', []))} 39 | - 技术亮点:{', '.join(confirmed_requirements.get('tech_highlights', []))} 40 | - 目标受众:{confirmed_requirements.get('target_audience', '通用受众')} 41 | {page_count_instruction} 42 | 43 | 发现的错误: 44 | {errors_text} 45 | 46 | 原始JSON数据: 47 | ```json 48 | {outline_data} 49 | ``` 50 | 51 | 修复要求: 52 | 1. 修复所有发现的错误 53 | 2. 确保JSON格式正确且完整 54 | 3. 保持原有内容 55 | 4. 严格遵守页数要求 56 | 5. 确保所有必需字段都存在且格式正确 57 | 58 | 请输出修复后的完整JSON数据,使用```json```代码块包裹:""" 59 | 60 | @staticmethod 61 | def get_json_validation_prompt(json_data: str, expected_structure: Dict[str, Any]) -> str: 62 | """获取JSON验证提示词""" 63 | return f"""作为数据验证专家,请验证以下JSON数据是否符合预期结构: 64 | 65 | **待验证的JSON数据:** 66 | ```json 67 | {json_data} 68 | ``` 69 | 70 | **预期结构:** 71 | ```json 72 | {expected_structure} 73 | ``` 74 | 75 | 请检查以下方面: 76 | 1. **JSON格式正确性**:语法是否正确,是否可以正常解析 77 | 2. **必需字段完整性**:所有必需字段是否存在 78 | 3. **数据类型匹配**:字段值类型是否符合预期 79 | 4. **数据有效性**:字段值是否在有效范围内 80 | 5. **结构一致性**:嵌套结构是否符合预期 81 | 82 | 如果发现问题,请提供: 83 | - 具体的错误描述 84 | - 错误位置定位 85 | - 修复建议 86 | 87 | 如果数据正确,请确认验证通过。""" 88 | 89 | @staticmethod 90 | def get_content_validation_prompt(content: str, requirements: Dict[str, Any]) -> str: 91 | """获取内容验证提示词""" 92 | return f"""作为内容质量专家,请验证以下内容是否符合要求: 93 | 94 | **待验证内容:** 95 | {content} 96 | 97 | **质量要求:** 98 | {requirements} 99 | 100 | 请从以下维度进行验证: 101 | 102 | 1. **内容完整性**: 103 | - 是否包含所有必需的信息点 104 | - 内容是否完整表达了主题 105 | - 是否遗漏重要信息 106 | 107 | 2. **逻辑一致性**: 108 | - 内容逻辑是否清晰 109 | - 信息流是否连贯 110 | - 是否存在矛盾或冲突 111 | 112 | 3. **语言质量**: 113 | - 语言表达是否准确 114 | - 是否符合目标受众水平 115 | - 语言风格是否一致 116 | 117 | 4. **格式规范**: 118 | - 格式是否符合要求 119 | - 结构是否清晰 120 | - 标记是否正确 121 | 122 | 请提供详细的验证结果和改进建议。""" 123 | 124 | @staticmethod 125 | def get_structure_repair_prompt(data: Dict[str, Any], target_structure: Dict[str, Any]) -> str: 126 | """获取结构修复提示词""" 127 | return f"""作为数据结构专家,请将以下数据修复为目标结构: 128 | 129 | **原始数据:** 130 | ```json 131 | {data} 132 | ``` 133 | 134 | **目标结构:** 135 | ```json 136 | {target_structure} 137 | ``` 138 | 139 | 修复要求: 140 | 1. **保持数据完整性**:不丢失原有的有效信息 141 | 2. **结构标准化**:严格按照目标结构组织数据 142 | 3. **类型转换**:确保数据类型符合要求 143 | 4. **字段映射**:正确映射相似字段 144 | 5. **默认值填充**:为缺失的必需字段提供合理默认值 145 | 146 | 请输出修复后的完整数据结构。""" 147 | 148 | @staticmethod 149 | def get_quality_check_prompt(ppt_data: Dict[str, Any], quality_standards: Dict[str, Any]) -> str: 150 | """获取质量检查提示词""" 151 | return f"""作为PPT质量检查专家,请对以下PPT数据进行全面质量检查: 152 | 153 | **PPT数据:** 154 | ```json 155 | {ppt_data} 156 | ``` 157 | 158 | **质量标准:** 159 | ```json 160 | {quality_standards} 161 | ``` 162 | 163 | 请从以下维度进行检查: 164 | 165 | 1. **内容质量**: 166 | - 信息准确性和完整性 167 | - 逻辑结构和连贯性 168 | - 语言表达和专业性 169 | 170 | 2. **结构规范**: 171 | - 页面结构的合理性 172 | - 信息层级的清晰性 173 | - 导航逻辑的顺畅性 174 | 175 | 3. **设计一致性**: 176 | - 视觉风格的统一性 177 | - 色彩搭配的协调性 178 | - 字体使用的规范性 179 | 180 | 4. **用户体验**: 181 | - 信息传达的有效性 182 | - 交互体验的流畅性 183 | - 可访问性的考虑 184 | 185 | 5. **技术实现**: 186 | - 代码质量和规范性 187 | - 性能优化和兼容性 188 | - 错误处理和容错性 189 | 190 | 请提供详细的质量评估报告和改进建议。""" 191 | 192 | @staticmethod 193 | def get_error_recovery_prompt(error_info: str, context: Dict[str, Any]) -> str: 194 | """获取错误恢复提示词""" 195 | return f"""作为错误处理专家,请分析以下错误并提供恢复方案: 196 | 197 | **错误信息:** 198 | {error_info} 199 | 200 | **上下文信息:** 201 | ```json 202 | {context} 203 | ``` 204 | 205 | 请提供: 206 | 207 | 1. **错误分析**: 208 | - 错误的根本原因 209 | - 错误的影响范围 210 | - 错误的严重程度 211 | 212 | 2. **恢复策略**: 213 | - 立即修复方案 214 | - 预防措施建议 215 | - 备用方案选择 216 | 217 | 3. **实施步骤**: 218 | - 具体的修复步骤 219 | - 验证方法 220 | - 回滚计划 221 | 222 | 请确保提供的方案安全可靠,不会造成数据丢失或系统不稳定。""" 223 | -------------------------------------------------------------------------------- /src/landppt/web/templates/result.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}生成结果 - LandPPT{% endblock %} 4 | 5 | {% block content %} 6 | {% if success %} 7 |
8 |

🎉 PPT 生成成功!

9 |

您的 PPT 已经成功生成,可以预览和下载。

10 |
11 | 12 |
13 |

📊 生成结果

14 |

任务ID: {{ task_id }}

15 |
16 | 17 |
18 |
19 |

📋 PPT 大纲

20 | {% if outline %} 21 |
22 |

{{ outline.title }}

23 |

24 | 总共 {{ outline.metadata.total_slides }} 页幻灯片 | 25 | 场景: {{ outline.metadata.scenario }} | 26 | 语言: {{ outline.metadata.language }} 27 |

28 | 29 |
30 | {% for slide in outline.slides %} 31 |
32 | 第{{ slide.id }}页: {{ slide.title }} 33 | {% if slide.subtitle %} 34 |
{{ slide.subtitle }} 35 | {% endif %} 36 | {% if slide.content %} 37 |
38 | {{ slide.content[:100] }}{% if slide.content|length > 100 %}...{% endif %} 39 |
40 | {% endif %} 41 |
42 | {% endfor %} 43 |
44 |
45 | {% endif %} 46 | 47 |
48 | 查看项目列表 49 |
50 |
51 | 52 |
53 |

🎯 操作选项

54 | 55 |
56 | 57 | 🔍 预览 PPT 58 | 59 | 60 | 63 | 64 | 65 | 🔄 创建新的 PPT 66 | 67 | 68 | 69 | 📊 查看所有项目 70 | 71 |
72 | 73 |
74 |

💡 提示

75 |
    76 |
  • 点击"预览 PPT"可以在新窗口中查看完整的演示文稿
  • 77 |
  • PPT 支持键盘导航(左右箭头键)
  • 78 |
  • 如需修改,可以重新生成或联系技术支持
  • 79 |
80 |
81 |
82 |
83 | 84 | {% if slides_html %} 85 |
86 |

🎬 PPT 预览

87 |
88 | 93 |
94 |
95 | {% endif %} 96 | 97 | {% else %} 98 |
99 |

❌ 生成失败

100 |

很抱歉,PPT 生成过程中出现了错误。

101 | {% if error %} 102 |
103 | 错误详情: {{ error }} 104 |
105 | {% endif %} 106 |
107 | 108 |
109 | 🔄 重新尝试 110 | 🏠 返回首页 111 |
112 | {% endif %} 113 | {% endblock %} 114 | 115 | {% block extra_js %} 116 | 128 | {% endblock %} 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # LandPPT Docker Image 2 | # Multi-stage build for minimal image size 3 | 4 | # Build stage 5 | FROM python:3.11-slim AS builder 6 | 7 | # Set environment variables for build 8 | ENV PYTHONUNBUFFERED=1 \ 9 | PYTHONDONTWRITEBYTECODE=1 \ 10 | PIP_NO_CACHE_DIR=1 \ 11 | PIP_DISABLE_PIP_VERSION_CHECK=1 \ 12 | UV_PROJECT_ENVIRONMENT=/opt/venv \ 13 | PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers 14 | 15 | # Install build dependencies 16 | RUN apt-get update && \ 17 | apt-get install -y --no-install-recommends \ 18 | build-essential \ 19 | curl \ 20 | git \ 21 | && rm -rf /var/lib/apt/lists/* 22 | 23 | # Install uv for faster dependency management 24 | RUN pip install --no-cache-dir uv 25 | 26 | # Set work directory and copy dependency files 27 | WORKDIR /app 28 | COPY pyproject.toml uv.lock* README.md ./ 29 | COPY src/ ./src/ 30 | 31 | # Install Python dependencies using uv 32 | # uv sync will create venv at UV_PROJECT_ENVIRONMENT and install all dependencies 33 | RUN uv sync && \ 34 | uv pip install apryse-sdk>=11.5.0 --extra-index-url=https://pypi.apryse.com && \ 35 | # Verify key packages are installed 36 | /opt/venv/bin/python -c "import uvicorn; import playwright; import fastapi" && \ 37 | # Clean up build artifacts 38 | find /opt/venv -name "*.pyc" -delete && \ 39 | find /opt/venv -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true 40 | 41 | # Install Playwright browsers in builder stage 42 | # This downloads chromium to /opt/playwright-browsers 43 | RUN apt-get update && \ 44 | apt-get install -y --no-install-recommends \ 45 | libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \ 46 | libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \ 47 | libgbm1 libasound2 libpango-1.0-0 libcairo2 \ 48 | && /opt/venv/bin/python -m playwright install chromium \ 49 | && rm -rf /var/lib/apt/lists/* 50 | 51 | # Production stage 52 | FROM python:3.11-slim AS production 53 | 54 | # Set environment variables 55 | ENV PYTHONUNBUFFERED=1 \ 56 | PYTHONDONTWRITEBYTECODE=1 \ 57 | PYTHONPATH=/app/src:/opt/venv/lib/python3.11/site-packages \ 58 | PATH=/opt/venv/bin:$PATH \ 59 | PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers \ 60 | HOME=/root \ 61 | VIRTUAL_ENV=/opt/venv 62 | 63 | # Install essential runtime dependencies and wkhtmltopdf 64 | RUN apt-get update && \ 65 | apt-get install -y --no-install-recommends \ 66 | poppler-utils \ 67 | libmagic1 \ 68 | ca-certificates \ 69 | curl \ 70 | wget \ 71 | libgomp1 \ 72 | fonts-liberation \ 73 | fonts-noto-cjk \ 74 | fontconfig \ 75 | netcat-openbsd \ 76 | xfonts-75dpi \ 77 | xfonts-base \ 78 | libjpeg62-turbo \ 79 | libxrender1 \ 80 | libfontconfig1 \ 81 | libx11-6 \ 82 | libxext6 \ 83 | # Chromium/Playwright runtime dependencies 84 | libnss3 \ 85 | libatk1.0-0 \ 86 | libatk-bridge2.0-0 \ 87 | libcups2 \ 88 | libdrm2 \ 89 | libxkbcommon0 \ 90 | libxcomposite1 \ 91 | libxdamage1 \ 92 | libxfixes3 \ 93 | libxrandr2 \ 94 | libgbm1 \ 95 | libasound2 \ 96 | libpango-1.0-0 \ 97 | libcairo2 \ 98 | && \ 99 | # Download and install wkhtmltopdf from official releases 100 | WKHTMLTOPDF_VERSION="0.12.6.1-3" && \ 101 | wget -q "https://github.com/wkhtmltopdf/packaging/releases/download/${WKHTMLTOPDF_VERSION}/wkhtmltox_${WKHTMLTOPDF_VERSION}.bookworm_amd64.deb" -O /tmp/wkhtmltox.deb && \ 102 | dpkg -i /tmp/wkhtmltox.deb || apt-get install -f -y && \ 103 | rm /tmp/wkhtmltox.deb && \ 104 | fc-cache -fv && \ 105 | apt-get clean && \ 106 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /root/.cache 107 | 108 | # Create non-root user (for compatibility, but run as root) 109 | RUN groupadd -r landppt && \ 110 | useradd -r -g landppt -m -d /home/landppt landppt 111 | 112 | # Copy Python packages from builder 113 | COPY --from=builder /opt/venv /opt/venv 114 | 115 | # Copy Playwright browsers from builder 116 | COPY --from=builder /opt/playwright-browsers /opt/playwright-browsers 117 | 118 | # Set permissions for landppt user and playwright browsers 119 | RUN chown -R landppt:landppt /home/landppt && \ 120 | chmod -R 755 /opt/playwright-browsers 121 | 122 | # Set work directory 123 | WORKDIR /app 124 | 125 | # Copy application code (minimize layers) 126 | COPY run.py ./ 127 | COPY src/ ./src/ 128 | COPY template_examples/ ./template_examples/ 129 | COPY docker-healthcheck.sh docker-entrypoint.sh ./ 130 | COPY .env.example ./.env 131 | 132 | # Create directories and set permissions in one layer 133 | RUN chmod +x docker-healthcheck.sh docker-entrypoint.sh && \ 134 | mkdir -p temp/ai_responses_cache temp/style_genes_cache temp/summeryanyfile_cache temp/templates_cache \ 135 | research_reports lib/Linux lib/MacOS lib/Windows uploads data && \ 136 | chown -R landppt:landppt /app /home/landppt && \ 137 | chmod -R 755 /app /home/landppt && \ 138 | chmod 666 /app/.env 139 | 140 | # Keep landppt user but run as root to handle file permissions 141 | # USER landppt 142 | 143 | # Expose port 144 | EXPOSE 8000 145 | 146 | # Minimal health check 147 | HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=2 \ 148 | CMD ./docker-healthcheck.sh 149 | 150 | # Set entrypoint and command 151 | ENTRYPOINT ["./docker-entrypoint.sh"] 152 | CMD ["python", "run.py"] -------------------------------------------------------------------------------- /src/landppt/services/share_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Project Share Service 3 | Handles generation and validation of public share links for presentations 4 | """ 5 | 6 | import secrets 7 | import logging 8 | from typing import Optional 9 | from sqlalchemy.orm import Session 10 | from ..database.models import Project 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class ShareService: 16 | """Service for managing project sharing functionality""" 17 | 18 | def __init__(self, db: Session): 19 | self.db = db 20 | 21 | def generate_share_token(self, project_id: str) -> Optional[str]: 22 | """ 23 | Generate a unique share token for a project, or return existing one 24 | 25 | Args: 26 | project_id: The project ID to generate a share link for 27 | 28 | Returns: 29 | The generated share token, or None if project not found 30 | """ 31 | try: 32 | # Get the project 33 | project = self.db.query(Project).filter( 34 | Project.project_id == project_id 35 | ).first() 36 | 37 | if not project: 38 | logger.error(f"Project {project_id} not found") 39 | return None 40 | 41 | # If project already has a valid share token, return it 42 | if project.share_token: 43 | # Enable sharing if it was disabled 44 | if not project.share_enabled: 45 | project.share_enabled = True 46 | self.db.commit() 47 | logger.info(f"Re-enabled sharing for project {project_id}") 48 | else: 49 | logger.info(f"Returning existing share token for project {project_id}") 50 | return project.share_token 51 | 52 | # Generate a new secure random token 53 | share_token = secrets.token_urlsafe(32) 54 | 55 | # Update project with share token and enable sharing 56 | project.share_token = share_token 57 | project.share_enabled = True 58 | self.db.commit() 59 | 60 | logger.info(f"Generated new share token for project {project_id}") 61 | return share_token 62 | 63 | except Exception as e: 64 | logger.error(f"Error generating share token: {e}") 65 | self.db.rollback() 66 | return None 67 | 68 | def disable_sharing(self, project_id: str) -> bool: 69 | """ 70 | Disable sharing for a project 71 | 72 | Args: 73 | project_id: The project ID to disable sharing for 74 | 75 | Returns: 76 | True if successful, False otherwise 77 | """ 78 | try: 79 | project = self.db.query(Project).filter( 80 | Project.project_id == project_id 81 | ).first() 82 | 83 | if not project: 84 | logger.error(f"Project {project_id} not found") 85 | return False 86 | 87 | project.share_enabled = False 88 | self.db.commit() 89 | 90 | logger.info(f"Disabled sharing for project {project_id}") 91 | return True 92 | 93 | except Exception as e: 94 | logger.error(f"Error disabling sharing: {e}") 95 | self.db.rollback() 96 | return False 97 | 98 | def validate_share_token(self, share_token: str) -> Optional[Project]: 99 | """ 100 | Validate a share token and return the associated project 101 | 102 | Args: 103 | share_token: The share token to validate 104 | 105 | Returns: 106 | The Project object if valid, None otherwise 107 | """ 108 | try: 109 | project = self.db.query(Project).filter( 110 | Project.share_token == share_token, 111 | Project.share_enabled == True 112 | ).first() 113 | 114 | if not project: 115 | logger.warning(f"Invalid or disabled share token") 116 | return None 117 | 118 | return project 119 | 120 | except Exception as e: 121 | logger.error(f"Error validating share token: {e}") 122 | return None 123 | 124 | def get_share_info(self, project_id: str) -> dict: 125 | """ 126 | Get sharing information for a project 127 | 128 | Args: 129 | project_id: The project ID 130 | 131 | Returns: 132 | Dictionary with share information 133 | """ 134 | try: 135 | project = self.db.query(Project).filter( 136 | Project.project_id == project_id 137 | ).first() 138 | 139 | if not project: 140 | return { 141 | "enabled": False, 142 | "share_token": None, 143 | "share_url": None 144 | } 145 | 146 | return { 147 | "enabled": project.share_enabled, 148 | "share_token": project.share_token, 149 | "share_url": f"/share/{project.share_token}" if project.share_token else None 150 | } 151 | 152 | except Exception as e: 153 | logger.error(f"Error getting share info: {e}") 154 | return { 155 | "enabled": False, 156 | "share_token": None, 157 | "share_url": None 158 | } 159 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | # LandPPT Docker Build & Push Workflow 2 | # 自动构建并推送 Docker 镜像到容器注册表 3 | 4 | name: Docker Build and Push 5 | 6 | on: 7 | # 在 main 分支推送时触发 8 | push: 9 | branches: 10 | - main 11 | - master 12 | # 仅当以下文件变更时触发 13 | paths: 14 | - 'Dockerfile' 15 | - 'docker-compose.yml' 16 | - 'docker-entrypoint.sh' 17 | - 'docker-healthcheck.sh' 18 | - 'src/**' 19 | - 'pyproject.toml' 20 | - 'uv.lock' 21 | - '.github/workflows/docker-build.yml' 22 | 23 | # 在 PR 时进行构建测试(不推送) 24 | pull_request: 25 | branches: 26 | - main 27 | - master 28 | 29 | # 手动触发 30 | workflow_dispatch: 31 | inputs: 32 | push_image: 33 | description: '是否推送镜像到注册表' 34 | required: false 35 | default: 'true' 36 | type: boolean 37 | 38 | env: 39 | # Docker 镜像名称 - 请根据实际情况修改 40 | IMAGE_NAME: landppt 41 | # 构建平台 42 | PLATFORMS: linux/amd64 43 | 44 | jobs: 45 | build: 46 | name: Build Docker Image 47 | runs-on: ubuntu-latest 48 | 49 | permissions: 50 | contents: read 51 | packages: write 52 | 53 | steps: 54 | # 检出代码 55 | - name: Checkout Repository 56 | uses: actions/checkout@v4 57 | 58 | # 清理磁盘空间(防止构建时空间不足) 59 | - name: Free Disk Space 60 | run: | 61 | sudo rm -rf /usr/share/dotnet 62 | sudo rm -rf /usr/local/lib/android 63 | sudo rm -rf /opt/ghc 64 | sudo rm -rf /opt/hostedtoolcache/CodeQL 65 | sudo docker system prune -af 66 | df -h 67 | 68 | # 设置 Docker Buildx(用于高级构建功能) 69 | - name: Set up Docker Buildx 70 | uses: docker/setup-buildx-action@v3 71 | 72 | # 登录到 GitHub Container Registry (ghcr.io) 73 | - name: Login to GitHub Container Registry 74 | if: github.event_name != 'pull_request' 75 | uses: docker/login-action@v3 76 | with: 77 | registry: ghcr.io 78 | username: ${{ github.actor }} 79 | password: ${{ secrets.GITHUB_TOKEN }} 80 | 81 | # 登录到 Docker Hub(可选 - 如果需要推送到 Docker Hub) 82 | # 需要在 Repository Settings > Secrets 中设置 DOCKERHUB_USERNAME 和 DOCKERHUB_TOKEN 83 | - name: Login to Docker Hub 84 | if: github.event_name != 'pull_request' 85 | uses: docker/login-action@v3 86 | with: 87 | username: ${{ secrets.DOCKERHUB_USERNAME }} 88 | password: ${{ secrets.DOCKERHUB_TOKEN }} 89 | continue-on-error: true 90 | 91 | # 提取 Docker 元数据(标签、标签等) 92 | - name: Extract Docker Metadata 93 | id: meta 94 | uses: docker/metadata-action@v5 95 | with: 96 | images: | 97 | ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} 98 | ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} 99 | tags: | 100 | # 最新标签(仅在 main/master 分支) 101 | type=raw,value=latest,enable={{is_default_branch}} 102 | # Git 短 SHA 103 | type=sha,prefix= 104 | # 分支名称 105 | type=ref,event=branch 106 | # PR 编号 107 | type=ref,event=pr 108 | # 语义化版本(如果有标签) 109 | type=semver,pattern={{version}} 110 | type=semver,pattern={{major}}.{{minor}} 111 | # 日期标签 112 | type=raw,value={{date 'YYYYMMDD-HHmmss'}} 113 | 114 | # 构建并推送 Docker 镜像 115 | - name: Build and Push Docker Image 116 | uses: docker/build-push-action@v5 117 | with: 118 | context: . 119 | file: ./Dockerfile 120 | platforms: ${{ env.PLATFORMS }} 121 | # PR 时不推送,仅构建验证 122 | push: ${{ github.event_name != 'pull_request' && (github.event.inputs.push_image != 'false') }} 123 | tags: ${{ steps.meta.outputs.tags }} 124 | labels: ${{ steps.meta.outputs.labels }} 125 | # 利用 GitHub Actions 缓存加速构建 126 | cache-from: type=gha 127 | cache-to: type=gha,mode=max 128 | # 构建参数(如需要可添加) 129 | build-args: | 130 | BUILDTIME=${{ github.event.repository.updated_at }} 131 | VERSION=${{ github.sha }} 132 | 133 | # 输出镜像摘要 134 | - name: Image Digest 135 | run: | 136 | echo "### Docker Image Built Successfully! :rocket:" >> $GITHUB_STEP_SUMMARY 137 | echo "" >> $GITHUB_STEP_SUMMARY 138 | echo "**Image Tags:**" >> $GITHUB_STEP_SUMMARY 139 | echo "\`\`\`" >> $GITHUB_STEP_SUMMARY 140 | echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY 141 | echo "\`\`\`" >> $GITHUB_STEP_SUMMARY 142 | 143 | # 安全扫描(可选但推荐) 144 | security-scan: 145 | name: Security Scan 146 | runs-on: ubuntu-latest 147 | needs: build 148 | if: github.event_name != 'pull_request' 149 | 150 | steps: 151 | - name: Checkout Repository 152 | uses: actions/checkout@v4 153 | 154 | # 使用 Trivy 扫描 Dockerfile 漏洞 155 | - name: Run Trivy vulnerability scanner 156 | uses: aquasecurity/trivy-action@master 157 | with: 158 | scan-type: 'config' 159 | scan-ref: '.' 160 | format: 'sarif' 161 | output: 'trivy-results.sarif' 162 | continue-on-error: true 163 | 164 | # 上传扫描结果到 GitHub Security 165 | - name: Upload Trivy scan results 166 | uses: github/codeql-action/upload-sarif@v3 167 | with: 168 | sarif_file: 'trivy-results.sarif' 169 | continue-on-error: true 170 | -------------------------------------------------------------------------------- /template_examples/素白风.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "素白风", 3 | "description": "\"少即是多”,通过大量的留白、清晰的字体和极简的色彩来突出内容本身,营造一种宁静、专业且高级的视觉感受。", 4 | "html_template": "\n\n\n \n \n 素白风 - 简约至上 PPT模板\n\n \n \n \n \n \n \n \n\n\n\n\n \n
\n\n \n
\n\n \n
\n

\n {{ page_title }}\n

\n
\n\n \n
\n
\n \n
\n

1. 核心数据洞察

\n

留白,是设计的呼吸。在此处阐述核心观点,简洁的文字配合清晰的布局,能更有效地传达信息。

\n \n
\n \n
\n \n

图表分析区域

\n
\n \n
    \n
  • \n \n 关键点一:用户增长率达到新的高峰。\n
  • \n
  • \n \n 关键点二:市场份额显著提升,超越主要竞争对手。\n
  • \n
  • \n \n 关键点三:产品满意度调查获得95%的正面评价。\n
  • \n
\n
\n
\n
\n
\n\n \n \n\n
\n\n
\n\n\n", 5 | "tags": [ 6 | "素白风" 7 | ], 8 | "is_default": false, 9 | "export_info": { 10 | "exported_at": "2025-11-01T08:09:35.713Z", 11 | "original_id": 13, 12 | "original_created_at": "2025-10-29 20:55:14" 13 | } 14 | } -------------------------------------------------------------------------------- /template_examples/星月蓝.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "星月蓝", 3 | "description": "星月蓝", 4 | "html_template": "\n\n\n\n\n\n星月夜 - 静谧之旅 PPT模板\n\n\n\n\n\n\n\n\n\n\n
\n\n \n
\n\n \n
\n

\n {{ page_title }}\n

\n
\n\n \n
\n
\n \n
\n

1. 季度星象观察

\n

在此处输入详细的文本内容,用于阐述图表数据或核心观点。段落文本使用较为柔和的颜色以提升阅读体验。

\n \n
\n \n
\n \n

图表区域
(支持 Chart.js, ECharts, D3.js)

\n
\n \n
    \n
  • \n \n 关键发现一:新星体发现数量同比增长30%。\n
  • \n
  • \n \n 关键发现二:用户参与度创下历史新高,互动频率提升。\n
  • \n
  • \n \n 关键发现三:项目满意度调研中,98%的用户给予积极反馈。\n
  • \n
\n
\n
\n
\n
\n\n \n
\n \n {{ current_page_number }} / {{ total_page_count }}\n \n
\n\n
\n\n \n \n
\n \n
\n\n
\n\n", 5 | "tags": [ 6 | "星月蓝" 7 | ], 8 | "is_default": false, 9 | "export_info": { 10 | "exported_at": "2025-11-01T08:09:07.346Z", 11 | "original_id": 15, 12 | "original_created_at": "2025-10-29 20:55:30" 13 | } 14 | } -------------------------------------------------------------------------------- /src/landppt/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main FastAPI application entry point 3 | """ 4 | 5 | from fastapi import FastAPI, HTTPException 6 | from fastapi.middleware.cors import CORSMiddleware 7 | from fastapi.staticfiles import StaticFiles 8 | from fastapi.responses import HTMLResponse, FileResponse 9 | import uvicorn 10 | import asyncio 11 | import logging 12 | import os 13 | import sys 14 | from .api.openai_compat import router as openai_router 15 | from .api.landppt_api import router as landppt_router 16 | from .api.database_api import router as database_router 17 | from .api.global_master_template_api import router as template_api_router 18 | from .api.config_api import router as config_router 19 | from .api.image_api import router as image_router 20 | 21 | from .web import router as web_router 22 | from .auth import auth_router, create_auth_middleware 23 | from .database.database import init_db 24 | from .database.create_default_template import ensure_default_templates_exist_first_time 25 | 26 | # Configure logging 27 | logging.basicConfig(level=logging.INFO) 28 | logger = logging.getLogger(__name__) 29 | 30 | # Disable SQLAlchemy verbose logging completely 31 | logging.getLogger('sqlalchemy').setLevel(logging.WARNING) 32 | logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING) 33 | logging.getLogger('sqlalchemy.engine.Engine').setLevel(logging.WARNING) 34 | logging.getLogger('sqlalchemy.pool').setLevel(logging.WARNING) 35 | logging.getLogger('sqlalchemy.dialects').setLevel(logging.WARNING) 36 | 37 | # Create FastAPI app 38 | app = FastAPI( 39 | title="LandPPT API", 40 | description="AI-powered PPT generation platform with OpenAI-compatible API", 41 | version="0.1.0", 42 | docs_url="/docs", 43 | redoc_url="/redoc" 44 | ) 45 | 46 | 47 | @app.on_event("startup") 48 | async def startup_event(): 49 | """Initialize database on startup""" 50 | try: 51 | # Check if database file exists before initialization 52 | import os 53 | db_file_path = "landppt.db" # 默认数据库文件路径 54 | db_exists = os.path.exists(db_file_path) 55 | 56 | logger.info("Initializing database...") 57 | await init_db() 58 | logger.info("Database initialized successfully") 59 | 60 | # Only import templates if database file didn't exist before (first time setup) 61 | if not db_exists: 62 | logger.info("First time setup detected - importing templates from examples...") 63 | template_ids = await ensure_default_templates_exist_first_time() 64 | logger.info(f"Template initialization completed. {len(template_ids)} templates available.") 65 | else: 66 | logger.info("Database already exists - skipping template import") 67 | 68 | except Exception as e: 69 | logger.error(f"Failed to initialize application: {e}") 70 | raise 71 | 72 | 73 | @app.on_event("shutdown") 74 | async def shutdown_event(): 75 | """Clean up database connections on shutdown""" 76 | try: 77 | logger.info("Shutting down application...") 78 | logger.info("Application shutdown complete") 79 | except Exception as e: 80 | logger.error(f"Error during shutdown: {e}") 81 | 82 | # Add CORS middleware 83 | app.add_middleware( 84 | CORSMiddleware, 85 | allow_origins=["*"], # In production, specify actual origins 86 | allow_credentials=True, 87 | allow_methods=["*"], 88 | allow_headers=["*"], 89 | ) 90 | 91 | # Add authentication middleware 92 | auth_middleware = create_auth_middleware() 93 | app.middleware("http")(auth_middleware) 94 | 95 | # Include routers 96 | app.include_router(auth_router, prefix="", tags=["Authentication"]) 97 | app.include_router(config_router, prefix="", tags=["Configuration Management"]) 98 | app.include_router(image_router, prefix="", tags=["Image Service"]) 99 | 100 | app.include_router(openai_router, prefix="/v1", tags=["OpenAI Compatible"]) 101 | app.include_router(landppt_router, prefix="/api", tags=["LandPPT API"]) 102 | app.include_router(template_api_router, tags=["Global Master Templates"]) 103 | app.include_router(database_router, tags=["Database Management"]) 104 | app.include_router(web_router, prefix="", tags=["Web Interface"]) 105 | 106 | # Mount static files 107 | static_dir = os.path.join(os.path.dirname(__file__), "web", "static") 108 | app.mount("/static", StaticFiles(directory=static_dir), name="static") 109 | 110 | # Mount temp directory for image cache 111 | temp_dir = os.path.join(os.getcwd(), "temp") 112 | if os.path.exists(temp_dir): 113 | app.mount("/temp", StaticFiles(directory=temp_dir), name="temp") 114 | logger.info(f"Mounted temp directory: {temp_dir}") 115 | else: 116 | logger.warning(f"Temp directory not found: {temp_dir}") 117 | 118 | @app.get("/", response_class=HTMLResponse) 119 | async def root(): 120 | """Root endpoint - redirect to dashboard""" 121 | from fastapi.responses import RedirectResponse 122 | return RedirectResponse(url="/dashboard", status_code=302) 123 | 124 | @app.get("/favicon.ico") 125 | async def favicon(): 126 | """Serve favicon""" 127 | favicon_path = os.path.join(os.path.dirname(__file__), "web", "static", "images", "favicon.svg") 128 | if os.path.exists(favicon_path): 129 | return FileResponse(favicon_path, media_type="image/svg+xml") 130 | else: 131 | raise HTTPException(status_code=404, detail="Favicon not found") 132 | 133 | @app.get("/health") 134 | async def health_check(): 135 | """Health check endpoint""" 136 | return {"status": "healthy", "service": "LandPPT API"} 137 | 138 | if __name__ == "__main__": 139 | uvicorn.run( 140 | "src.landppt.main:app", 141 | host="0.0.0.0", 142 | port=8000, 143 | reload=True, 144 | log_level="info" 145 | ) 146 | -------------------------------------------------------------------------------- /src/landppt/web/templates/upload_result.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}上传结果 - LandPPT{% endblock %} 4 | 5 | {% block content %} 6 | {% if success %} 7 |
8 |

🎉 文件上传成功!

9 |

您的文件已成功上传并处理,可以基于此内容生成 PPT。

10 |
11 | 12 |
13 |

📁 文件处理结果

14 |
15 | 16 |
17 |
18 |

📋 文件信息

19 |
20 |

文件名: {{ filename }}

21 |

文件类型: {{ type }}

22 |

文件大小: {{ (size / 1024) | round(2) }} KB

23 |

处理状态: ✅ 成功

24 |
25 |
26 | 27 |
28 |

🎯 下一步操作

29 |

30 | 文件内容已提取完成,您可以基于此内容创建 PPT 31 |

32 |
33 | 34 | 🚀 基于此内容创建 PPT 35 | 36 | 37 | 🏠 返回首页 38 | 39 |
40 |
41 |
42 | 43 | {% if processed_content %} 44 |
45 |

📄 提取的内容预览

46 |
47 |
48 | {{ processed_content | replace('\n', '
') | safe }} 49 |
50 | {% if processed_content | length > 500 %} 51 |

52 | * 显示前 500 个字符,完整内容已保存用于 PPT 生成 53 |

54 | {% endif %} 55 |
56 |
57 | {% endif %} 58 | 59 |
60 |

💡 使用建议

61 | 62 |
63 |
64 |
🎯
65 |

选择合适场景

66 |

根据文档内容选择最匹配的 PPT 场景模板

67 |
68 | 69 |
70 |
✏️
71 |

补充要求

72 |

在生成时添加具体要求,如目标受众、重点内容等

73 |
74 | 75 |
76 |
🔄
77 |

迭代优化

78 |

生成后可以重新调整和优化 PPT 内容

79 |
80 |
81 |
82 | 83 | {% else %} 84 |
85 |

❌ 文件上传失败

86 |

很抱歉,文件处理过程中出现了错误。

87 | {% if error %} 88 |
89 | 错误详情: {{ error }} 90 |
91 | {% endif %} 92 |
93 | 94 |
95 | 🔄 重新尝试 96 | 🏠 返回首页 97 |
98 | 99 |
100 |

📋 支持的文件格式

101 |
102 |
103 |
📄
104 | .docx 105 |

Word 文档

106 |
107 |
108 |
📕
109 | .pdf 110 |

PDF 文档

111 |
112 |
113 |
📝
114 | .txt 115 |

纯文本文件

116 |
117 |
118 |
📋
119 | .md 120 |

Markdown 文档

121 |
122 |
123 | 124 |
125 |
💡 上传提示
126 | 132 |
133 |
134 | {% endif %} 135 | {% endblock %} 136 | -------------------------------------------------------------------------------- /template_examples/中式书卷风.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "中式书卷风", 3 | "description": "融合了古典书卷的雅致与现代排版的清晰。模拟卷轴的边框和水墨质感,营造出沉静、典雅的阅读体验。", 4 | "html_template": "\n\n\n \n \n {{ page_title }}\n \n \n \n \n\n\n
\n
\n

{{ main_heading }}

\n
\n \n
\n
\n {{ page_content }}\n
\n
\n\n
\n {{ current_page_number }} / {{ total_page_count }}\n
\n
\n\n", 5 | "tags": [ 6 | "书卷", 7 | "中式", 8 | "复古", 9 | "古典", 10 | "宋体", 11 | "典雅" 12 | ], 13 | "is_default": false, 14 | "export_info": { 15 | "exported_at": "2025-10-04T06:52:01.801Z", 16 | "original_id": 10, 17 | "original_created_at": 1753277238.7648685 18 | } 19 | } -------------------------------------------------------------------------------- /template_examples/森林绿.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "森林绿", 3 | "description": "森林绿", 4 | "html_template": "\n\n\n \n \n 森林绿 - 探索之旅 PPT模板\n\n \n \n \n \n \n \n \n\n \n \n\n\n\n\n \n
\n\n \n
\n\n \n
\n

\n {{ page_title }}\n

\n
\n\n \n
\n
\n \n \n
\n

1. 核心数据分析

\n

这里是详细的文本内容区域,用于解释和说明图表或要点。段落文本使用辅助色以保证阅读舒适性。

\n \n
\n \n
\n \n

图表区域
(支持 Chart.js, ECharts, D3.js)

\n \n
\n \n
    \n
  • \n \n 关键点一:用户增长率达到新的高峰。\n
  • \n
  • \n \n 关键点二:市场份额显著提升,超越主要竞争对手。\n
  • \n
  • \n \n 关键点三:产品满意度调查获得95%的正面评价。\n
  • \n
\n
\n
\n
\n
\n\n \n
\n \n {{ current_page_number }} / {{ total_page_count }}\n \n
\n\n
\n\n \n \n
\n \n
\n\n
\n\n\n", 5 | "tags": [ 6 | "森林绿", 7 | "清新" 8 | ], 9 | "is_default": false, 10 | "export_info": { 11 | "exported_at": "2025-11-01T08:09:42.581Z", 12 | "original_id": 12, 13 | "original_created_at": "2025-10-26 17:25:34" 14 | } 15 | } -------------------------------------------------------------------------------- /src/landppt/services/prompts/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | PPT提示词模块统一入口 3 | 提供所有提示词类的便捷导入 4 | """ 5 | 6 | from typing import Dict, Any, List 7 | from .outline_prompts import OutlinePrompts 8 | from .content_prompts import ContentPrompts 9 | from .design_prompts import DesignPrompts 10 | from .system_prompts import SystemPrompts 11 | from .repair_prompts import RepairPrompts 12 | 13 | __all__ = [ 14 | 'OutlinePrompts', 15 | 'ContentPrompts', 16 | 'DesignPrompts', 17 | 'SystemPrompts', 18 | 'RepairPrompts' 19 | ] 20 | 21 | # 为了向后兼容,提供一个统一的提示词管理器 22 | class PPTPromptsManager: 23 | """PPT提示词统一管理器""" 24 | 25 | def __init__(self): 26 | self.outline = OutlinePrompts() 27 | self.content = ContentPrompts() 28 | self.design = DesignPrompts() 29 | self.system = SystemPrompts() 30 | self.repair = RepairPrompts() 31 | 32 | # 大纲相关提示词 33 | def get_outline_prompt_zh(self, *args, **kwargs): 34 | return self.outline.get_outline_prompt_zh(*args, **kwargs) 35 | 36 | def get_outline_prompt_en(self, *args, **kwargs): 37 | return self.outline.get_outline_prompt_en(*args, **kwargs) 38 | 39 | def get_streaming_outline_prompt(self, *args, **kwargs): 40 | return self.outline.get_streaming_outline_prompt(*args, **kwargs) 41 | 42 | def get_outline_generation_context(self, *args, **kwargs): 43 | return self.outline.get_outline_generation_context(*args, **kwargs) 44 | 45 | # 内容相关提示词 46 | def get_slide_content_prompt_zh(self, *args, **kwargs): 47 | return self.content.get_slide_content_prompt_zh(*args, **kwargs) 48 | 49 | def get_slide_content_prompt_en(self, *args, **kwargs): 50 | return self.content.get_slide_content_prompt_en(*args, **kwargs) 51 | 52 | def get_enhancement_prompt_zh(self, *args, **kwargs): 53 | return self.content.get_enhancement_prompt_zh(*args, **kwargs) 54 | 55 | def get_enhancement_prompt_en(self, *args, **kwargs): 56 | return self.content.get_enhancement_prompt_en(*args, **kwargs) 57 | 58 | def get_ppt_creation_context(self, *args, **kwargs): 59 | return self.content.get_ppt_creation_context(*args, **kwargs) 60 | 61 | def get_general_stage_prompt(self, *args, **kwargs): 62 | return self.content.get_general_stage_prompt(*args, **kwargs) 63 | 64 | def get_general_subtask_context(self, *args, **kwargs): 65 | return self.content.get_general_subtask_context(*args, **kwargs) 66 | 67 | def get_general_subtask_prompt(self, *args, **kwargs): 68 | return self.content.get_general_subtask_prompt(*args, **kwargs) 69 | 70 | # 设计相关提示词 71 | def get_style_gene_extraction_prompt(self, *args, **kwargs): 72 | return self.design.get_style_gene_extraction_prompt(*args, **kwargs) 73 | 74 | def get_unified_design_guide_prompt(self, *args, **kwargs): 75 | return self.design.get_unified_design_guide_prompt(*args, **kwargs) 76 | 77 | def get_creative_variation_prompt(self, *args, **kwargs): 78 | return self.design.get_creative_variation_prompt(*args, **kwargs) 79 | 80 | def get_content_driven_design_prompt(self, *args, **kwargs): 81 | return self.design.get_content_driven_design_prompt(*args, **kwargs) 82 | 83 | def get_style_genes_extraction_prompt(self, *args, **kwargs): 84 | return self.design.get_style_genes_extraction_prompt(*args, **kwargs) 85 | 86 | def get_creative_template_context_prompt(self, *args, **kwargs): 87 | return self.design.get_creative_template_context_prompt(*args, **kwargs) 88 | 89 | # 系统相关提示词 90 | def get_default_ppt_system_prompt(self, *args, **kwargs): 91 | return self.system.get_default_ppt_system_prompt(*args, **kwargs) 92 | 93 | def get_keynote_style_prompt(self, *args, **kwargs): 94 | return self.system.get_keynote_style_prompt(*args, **kwargs) 95 | 96 | def load_prompts_md_system_prompt(self, *args, **kwargs): 97 | return self.system.load_prompts_md_system_prompt(*args, **kwargs) 98 | 99 | def get_ai_assistant_system_prompt(self, *args, **kwargs): 100 | return self.system.get_ai_assistant_system_prompt(*args, **kwargs) 101 | 102 | def get_html_generation_system_prompt(self, *args, **kwargs): 103 | return self.system.get_html_generation_system_prompt(*args, **kwargs) 104 | 105 | def get_content_analysis_system_prompt(self, *args, **kwargs): 106 | return self.system.get_content_analysis_system_prompt(*args, **kwargs) 107 | 108 | def get_custom_style_prompt(self, *args, **kwargs): 109 | return self.system.get_custom_style_prompt(*args, **kwargs) 110 | 111 | # 修复相关提示词 112 | def get_repair_prompt(self, *args, **kwargs): 113 | return self.repair.get_repair_prompt(*args, **kwargs) 114 | 115 | def get_json_validation_prompt(self, *args, **kwargs): 116 | return self.repair.get_json_validation_prompt(*args, **kwargs) 117 | 118 | def get_content_validation_prompt(self, *args, **kwargs): 119 | return self.repair.get_content_validation_prompt(*args, **kwargs) 120 | 121 | def get_structure_repair_prompt(self, *args, **kwargs): 122 | return self.repair.get_structure_repair_prompt(*args, **kwargs) 123 | 124 | def get_quality_check_prompt(self, *args, **kwargs): 125 | return self.repair.get_quality_check_prompt(*args, **kwargs) 126 | 127 | def get_error_recovery_prompt(self, *args, **kwargs): 128 | return self.repair.get_error_recovery_prompt(*args, **kwargs) 129 | 130 | def get_single_slide_html_prompt(self, slide_data: Dict[str, Any], confirmed_requirements: Dict[str, Any], 131 | page_number: int, total_pages: int, context_info: str, 132 | style_genes: str, unified_design_guide: str, template_html: str) -> str: 133 | """获取单页HTML生成提示词""" 134 | return self.design.get_single_slide_html_prompt( 135 | slide_data, confirmed_requirements, page_number, total_pages, 136 | context_info, style_genes, unified_design_guide, template_html 137 | ) 138 | 139 | def get_slide_context_prompt(self, page_number: int, total_pages: int) -> str: 140 | """获取幻灯片上下文提示词(特殊页面设计要求)""" 141 | return self.design.get_slide_context_prompt(page_number, total_pages) 142 | 143 | 144 | # 创建默认实例 145 | prompts_manager = PPTPromptsManager() 146 | -------------------------------------------------------------------------------- /template_examples/竹简风.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "竹简风", 3 | "description": "竹简的颜色和纹理,使用浅米色与淡褐色的纵向渐变", 4 | "html_template": "\n\n\n \n \n 竹简风 - 策略与洞见 PPT模板\n\n \n \n \n \n \n \n \n\n\n\n\n \n
\n\n \n
\n \n \n
\n \n \n
\n\n \n
\n

\n {{ page_title }}\n

\n
\n\n \n
\n
\n \n
\n

一、核心要点剖析

\n

此区域为正文叙述部分。文字以深棕色呈现,模拟墨书质感,确保在米色背景上的可读性与古朴美感。

\n \n
\n \n
\n \n

图表区域
(支持 Chart.js, ECharts, D3.js)

\n
\n \n
    \n
  • \n \n 其一:用户增长之道,如春笋破土,势不可挡。\n
  • \n
  • \n \n 其二:市场格局之变,我方占据要津,竞者退避。\n
  • \n
  • \n \n 其三:产品口碑之誉,获九成五用户首肯,声名远播。\n
  • \n
\n
\n
\n
\n
\n\n \n \n\n
\n\n
\n\n\n", 5 | "tags": [ 6 | "竹简风" 7 | ], 8 | "is_default": false, 9 | "export_info": { 10 | "exported_at": "2025-11-01T08:09:30.258Z", 11 | "original_id": 14, 12 | "original_created_at": "2025-10-29 20:55:23" 13 | } 14 | } -------------------------------------------------------------------------------- /src/summeryanyfile/core/json_parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | JSON解析工具 - 处理LLM返回的JSON响应 3 | """ 4 | 5 | import json 6 | import re 7 | from typing import Dict, Any, Optional 8 | import logging 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class JSONParser: 14 | """JSON解析器,用于处理LLM返回的各种格式的JSON响应""" 15 | 16 | @staticmethod 17 | def extract_json_from_response(response: str) -> Dict[str, Any]: 18 | """ 19 | 从LLM响应中提取JSON 20 | 21 | Args: 22 | response: LLM的原始响应文本 23 | 24 | Returns: 25 | 解析后的JSON字典,如果解析失败则返回默认结构 26 | """ 27 | if not response or not response.strip(): 28 | logger.warning("收到空响应,返回默认JSON结构") 29 | return JSONParser._get_default_structure() 30 | 31 | # 尝试方法1:直接解析 32 | try: 33 | return json.loads(response.strip()) 34 | except json.JSONDecodeError: 35 | logger.debug("直接JSON解析失败,尝试其他方法") 36 | 37 | # 尝试方法2:提取JSON代码块 38 | json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL | re.IGNORECASE) 39 | if json_match: 40 | try: 41 | json_content = json_match.group(1).strip() 42 | return json.loads(json_content) 43 | except json.JSONDecodeError: 44 | logger.debug("JSON代码块解析失败") 45 | 46 | # 尝试方法3:提取普通代码块 47 | code_match = re.search(r'```\s*(.*?)\s*```', response, re.DOTALL) 48 | if code_match: 49 | try: 50 | code_content = code_match.group(1).strip() 51 | return json.loads(code_content) 52 | except json.JSONDecodeError: 53 | logger.debug("代码块解析失败") 54 | 55 | # 尝试方法4:寻找JSON结构 56 | json_patterns = [ 57 | r'\{.*\}', # 匹配大括号包围的内容 58 | r'\[.*\]', # 匹配方括号包围的内容 59 | ] 60 | 61 | for pattern in json_patterns: 62 | json_match = re.search(pattern, response, re.DOTALL) 63 | if json_match: 64 | try: 65 | json_content = json_match.group(0) 66 | return json.loads(json_content) 67 | except json.JSONDecodeError: 68 | continue 69 | 70 | # 尝试方法5:清理并重试 71 | cleaned_response = JSONParser._clean_response(response) 72 | if cleaned_response: 73 | try: 74 | return json.loads(cleaned_response) 75 | except json.JSONDecodeError: 76 | logger.debug("清理后的响应解析失败") 77 | 78 | logger.warning(f"所有JSON解析方法都失败,响应内容: {response}...") 79 | return JSONParser._get_default_structure() 80 | 81 | @staticmethod 82 | def _clean_response(response: str) -> Optional[str]: 83 | """ 84 | 清理响应文本,尝试提取可能的JSON内容 85 | 86 | Args: 87 | response: 原始响应文本 88 | 89 | Returns: 90 | 清理后的文本,如果无法清理则返回None 91 | """ 92 | # 移除常见的非JSON前缀和后缀 93 | prefixes_to_remove = [ 94 | "Here's the JSON:", 95 | "Here is the JSON:", 96 | "JSON:", 97 | "Result:", 98 | "Output:", 99 | "Response:", 100 | ] 101 | 102 | cleaned = response.strip() 103 | 104 | for prefix in prefixes_to_remove: 105 | if cleaned.lower().startswith(prefix.lower()): 106 | cleaned = cleaned[len(prefix):].strip() 107 | 108 | # 移除可能的Markdown格式 109 | cleaned = re.sub(r'^```.*?\n', '', cleaned, flags=re.MULTILINE) 110 | cleaned = re.sub(r'\n```$', '', cleaned, flags=re.MULTILINE) 111 | 112 | # 查找第一个 { 和最后一个 } 113 | first_brace = cleaned.find('{') 114 | last_brace = cleaned.rfind('}') 115 | 116 | if first_brace != -1 and last_brace != -1 and first_brace < last_brace: 117 | return cleaned[first_brace:last_brace + 1] 118 | 119 | return None 120 | 121 | @staticmethod 122 | def _get_default_structure() -> Dict[str, Any]: 123 | """ 124 | 返回默认的JSON结构 125 | 126 | Returns: 127 | 默认的PPT大纲结构 128 | """ 129 | return { 130 | "title": "PPT大纲", 131 | "total_pages": 10, 132 | "page_count_mode": "estimated", 133 | "slides": [ 134 | { 135 | "page_number": 1, 136 | "title": "标题页", 137 | "content_points": ["演示标题", "演示者信息", "日期"], 138 | "slide_type": "title", 139 | "description": "PPT的开场标题页" 140 | } 141 | ] 142 | } 143 | 144 | @staticmethod 145 | def validate_ppt_structure(data: Dict[str, Any]) -> Dict[str, Any]: 146 | """ 147 | 验证并修复PPT结构 148 | 149 | Args: 150 | data: 待验证的PPT数据 151 | 152 | Returns: 153 | 验证并修复后的PPT数据 154 | """ 155 | # 确保必需字段存在 156 | if "title" not in data: 157 | data["title"] = "PPT大纲" 158 | 159 | if "slides" not in data or not isinstance(data["slides"], list): 160 | data["slides"] = [] 161 | 162 | if "total_pages" not in data: 163 | data["total_pages"] = len(data["slides"]) 164 | 165 | if "page_count_mode" not in data: 166 | data["page_count_mode"] = "final" 167 | 168 | # 验证和修复每个幻灯片 169 | valid_slides = [] 170 | for i, slide in enumerate(data["slides"]): 171 | if not isinstance(slide, dict): 172 | continue 173 | 174 | # 确保幻灯片必需字段 175 | slide.setdefault("page_number", i + 1) 176 | slide.setdefault("title", f"幻灯片 {i + 1}") 177 | slide.setdefault("content_points", []) 178 | slide.setdefault("slide_type", "content") 179 | slide.setdefault("description", "") 180 | 181 | # 验证slide_type 182 | if slide["slide_type"] not in ["title", "content", "conclusion"]: 183 | slide["slide_type"] = "content" 184 | 185 | # 确保content_points是列表 186 | if not isinstance(slide["content_points"], list): 187 | slide["content_points"] = [] 188 | 189 | valid_slides.append(slide) 190 | 191 | data["slides"] = valid_slides 192 | data["total_pages"] = len(valid_slides) 193 | 194 | return data 195 | -------------------------------------------------------------------------------- /src/summeryanyfile/core/chunkers/recursive_chunker.py: -------------------------------------------------------------------------------- 1 | """ 2 | 递归分块器 - 使用递归字符分割策略 3 | """ 4 | 5 | import logging 6 | from typing import List, Dict, Any, Optional 7 | 8 | from .base_chunker import BaseChunker, DocumentChunk 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class RecursiveChunker(BaseChunker): 14 | """ 15 | 递归分块器,使用分层分隔符递归分割文本 16 | 17 | 这个分块器尝试在自然断点处分割文本,如段落、句子等 18 | """ 19 | 20 | def __init__( 21 | self, 22 | chunk_size: int = 1000, 23 | chunk_overlap: int = 200, 24 | separators: Optional[List[str]] = None 25 | ) -> None: 26 | """ 27 | 初始化递归分块器 28 | 29 | Args: 30 | chunk_size: 每个块的最大大小 31 | chunk_overlap: 块之间的重叠 32 | separators: 分隔符列表,按优先级排序 33 | """ 34 | super().__init__(chunk_size, chunk_overlap) 35 | 36 | if separators is None: 37 | self.separators = [ 38 | "\n\n", # 段落分隔符 39 | "\n", # 行分隔符 40 | ". ", # 英文句子分隔符 41 | "。", # 中文句子分隔符 42 | "! ", # 英文感叹句 43 | "!", # 中文感叹句 44 | "? ", # 英文疑问句 45 | "?", # 中文疑问句 46 | "; ", # 分号 47 | ";", # 中文分号 48 | ", ", # 逗号 49 | ",", # 中文逗号 50 | " ", # 空格 51 | "" # 字符级分割(最后手段) 52 | ] 53 | else: 54 | self.separators = separators 55 | 56 | def chunk_text(self, text: str, metadata: Optional[Dict[str, Any]] = None) -> List[DocumentChunk]: 57 | """ 58 | 使用递归策略分块文本 59 | 60 | Args: 61 | text: 输入文本 62 | metadata: 可选的元数据 63 | 64 | Returns: 65 | DocumentChunk对象列表 66 | """ 67 | if metadata is None: 68 | metadata = {} 69 | 70 | # 递归分割文本 71 | text_chunks = self._split_text_recursive(text) 72 | 73 | # 转换为DocumentChunk对象 74 | chunks = [] 75 | for i, chunk_text in enumerate(text_chunks): 76 | chunk_metadata = metadata.copy() 77 | chunk_metadata.update({ 78 | "chunk_index": i, 79 | "chunking_strategy": "recursive" 80 | }) 81 | chunks.append(self._create_chunk(chunk_text, chunk_metadata)) 82 | 83 | # 添加重叠 84 | if self.chunk_overlap > 0: 85 | chunks = self._add_overlap_to_chunks(chunks) 86 | 87 | logger.info(f"创建了 {len(chunks)} 个递归块") 88 | return chunks 89 | 90 | def _split_text_recursive(self, text: str) -> List[str]: 91 | """ 92 | 递归分割文本 93 | 94 | Args: 95 | text: 要分割的文本 96 | 97 | Returns: 98 | 文本块列表 99 | """ 100 | if len(text) <= self.chunk_size: 101 | return [text] if text.strip() else [] 102 | 103 | # 尝试每个分隔符 104 | for separator in self.separators: 105 | if separator in text: 106 | return self._split_by_separator(text, separator) 107 | 108 | # 如果没有找到分隔符,强制分割 109 | logger.warning("未找到合适的分隔符,强制分割文本") 110 | return [text[:self.chunk_size], text[self.chunk_size:]] 111 | 112 | def _split_by_separator(self, text: str, separator: str) -> List[str]: 113 | """ 114 | 使用指定分隔符分割文本 115 | 116 | Args: 117 | text: 要分割的文本 118 | separator: 分隔符 119 | 120 | Returns: 121 | 文本块列表 122 | """ 123 | if separator == "": 124 | # 字符级分割 125 | mid_point = self.chunk_size 126 | left_part = text[:mid_point] 127 | right_part = text[mid_point:] 128 | 129 | left_chunks = self._split_text_recursive(left_part) 130 | right_chunks = self._split_text_recursive(right_part) 131 | 132 | return left_chunks + right_chunks 133 | 134 | # 分割文本 135 | parts = text.split(separator) 136 | 137 | # 重新组合块 138 | chunks = [] 139 | current_chunk = "" 140 | 141 | for part in parts: 142 | # 检查添加这部分是否会超过限制 143 | potential_chunk = current_chunk + separator + part if current_chunk else part 144 | 145 | if len(potential_chunk) <= self.chunk_size: 146 | current_chunk = potential_chunk 147 | else: 148 | # 保存当前块 149 | if current_chunk: 150 | chunks.append(current_chunk) 151 | 152 | # 如果单个部分太大,递归分割 153 | if len(part) > self.chunk_size: 154 | sub_chunks = self._split_text_recursive(part) 155 | chunks.extend(sub_chunks) 156 | current_chunk = "" 157 | else: 158 | current_chunk = part 159 | 160 | # 添加最后一个块 161 | if current_chunk: 162 | chunks.append(current_chunk) 163 | 164 | return chunks 165 | 166 | def _add_overlap_to_chunks(self, chunks: List[DocumentChunk]) -> List[DocumentChunk]: 167 | """ 168 | 为块添加重叠 169 | 170 | Args: 171 | chunks: 原始块列表 172 | 173 | Returns: 174 | 带重叠的块列表 175 | """ 176 | if len(chunks) <= 1: 177 | return chunks 178 | 179 | overlapped_chunks = [chunks[0]] 180 | 181 | for i in range(1, len(chunks)): 182 | prev_chunk = chunks[i - 1] 183 | current_chunk = chunks[i] 184 | 185 | # 从前一个块的末尾提取重叠内容 186 | prev_content = prev_chunk.content 187 | overlap_text = prev_content[-self.chunk_overlap:] if len(prev_content) > self.chunk_overlap else prev_content 188 | 189 | # 创建新的块内容 190 | new_content = overlap_text + "\n\n" + current_chunk.content 191 | 192 | # 更新块内容 193 | new_metadata = current_chunk.metadata.copy() 194 | new_metadata["has_overlap"] = True 195 | new_metadata["overlap_size"] = len(overlap_text) 196 | 197 | overlapped_chunk = self._create_chunk(new_content, new_metadata) 198 | overlapped_chunk.chunk_id = current_chunk.chunk_id # 保持原始ID 199 | 200 | overlapped_chunks.append(overlapped_chunk) 201 | 202 | return overlapped_chunks 203 | -------------------------------------------------------------------------------- /template_examples/终端风.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "终端风", 3 | "description": "终端风", 4 | "html_template": "\n\n\n \n \n {{ page_title }}\n \n\n\n
\n
\n

{{ main_heading }}

\n
\n \n
\n

{{ page_content }}

\n
\n \n \n
\n\n", 5 | "tags": [ 6 | "终端", 7 | "编程", 8 | "游戏" 9 | ], 10 | "is_default": false, 11 | "export_info": { 12 | "exported_at": "2025-06-28T10:12:44.559Z", 13 | "original_id": 2, 14 | "original_created_at": 1749556645.1032526 15 | } 16 | } -------------------------------------------------------------------------------- /template_examples/宣纸风.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "宣纸风", 3 | "description": "传统宣纸的水墨质感,营造出一种典雅而又富有变化的视觉风格。", 4 | "html_template": "\n\n\n \n \n {{ page_title }}\n \n \n \n \n\n\n
\n
\n

{{ main_heading }}

\n
\n \n
\n
\n {{ page_content }}\n
\n
\n \n
\n {{ current_page_number }} / {{ total_page_count }}\n
\n
\n\n", 5 | "tags": [ 6 | "宣纸", 7 | "国风", 8 | "水墨", 9 | "典雅" 10 | ], 11 | "is_default": false, 12 | "export_info": { 13 | "exported_at": "2025-07-23T08:06:24.463Z", 14 | "original_id": 11, 15 | "original_created_at": 1753257751.6069405 16 | } 17 | } -------------------------------------------------------------------------------- /src/landppt/api/openai_compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenAI-compatible API endpoints 3 | """ 4 | 5 | from fastapi import APIRouter, HTTPException, Request 6 | from fastapi.responses import StreamingResponse 7 | import json 8 | import asyncio 9 | from typing import AsyncGenerator 10 | 11 | from .models import ( 12 | ChatCompletionRequest, ChatCompletionResponse, ChatCompletionChoice, 13 | CompletionRequest, CompletionResponse, CompletionChoice, 14 | ChatMessage, Usage 15 | ) 16 | from ..services.ai_service import AIService 17 | from ..core.config import ai_config 18 | 19 | router = APIRouter() 20 | ai_service = AIService() 21 | 22 | @router.post("/chat/completions", response_model=ChatCompletionResponse) 23 | async def create_chat_completion(request: ChatCompletionRequest): 24 | """ 25 | Create a chat completion (OpenAI compatible) 26 | """ 27 | try: 28 | # Check if this is a PPT-related request 29 | last_message = request.messages[-1].content if request.messages else "" 30 | 31 | if ai_service.is_ppt_request(last_message): 32 | # Handle PPT generation request 33 | response_content = await ai_service.handle_ppt_chat_request(request) 34 | else: 35 | # Handle general chat request 36 | response_content = await ai_service.handle_general_chat_request(request) 37 | 38 | # Calculate token usage (simplified) 39 | prompt_tokens = sum(len(msg.content.split()) for msg in request.messages) 40 | completion_tokens = len(response_content.split()) 41 | 42 | choice = ChatCompletionChoice( 43 | index=0, 44 | message=ChatMessage(role="assistant", content=response_content), 45 | finish_reason="stop" 46 | ) 47 | 48 | return ChatCompletionResponse( 49 | model=request.model, 50 | choices=[choice], 51 | usage=Usage( 52 | prompt_tokens=prompt_tokens, 53 | completion_tokens=completion_tokens, 54 | total_tokens=prompt_tokens + completion_tokens 55 | ) 56 | ) 57 | 58 | except Exception as e: 59 | raise HTTPException(status_code=500, detail=f"Error generating completion: {str(e)}") 60 | 61 | @router.post("/completions", response_model=CompletionResponse) 62 | async def create_completion(request: CompletionRequest): 63 | """ 64 | Create a text completion (OpenAI compatible) 65 | """ 66 | try: 67 | prompt = request.prompt if isinstance(request.prompt, str) else request.prompt[0] 68 | 69 | if ai_service.is_ppt_request(prompt): 70 | # Handle PPT generation request 71 | response_text = await ai_service.handle_ppt_completion_request(request) 72 | else: 73 | # Handle general completion request 74 | response_text = await ai_service.handle_general_completion_request(request) 75 | 76 | # Calculate token usage (simplified) 77 | prompt_tokens = len(prompt.split()) 78 | completion_tokens = len(response_text.split()) 79 | 80 | choice = CompletionChoice( 81 | text=response_text, 82 | index=0, 83 | finish_reason="stop" 84 | ) 85 | 86 | return CompletionResponse( 87 | model=request.model, 88 | choices=[choice], 89 | usage=Usage( 90 | prompt_tokens=prompt_tokens, 91 | completion_tokens=completion_tokens, 92 | total_tokens=prompt_tokens + completion_tokens 93 | ) 94 | ) 95 | 96 | except Exception as e: 97 | raise HTTPException(status_code=500, detail=f"Error generating completion: {str(e)}") 98 | 99 | @router.get("/models") 100 | async def list_models(): 101 | """ 102 | List available models (OpenAI compatible) 103 | """ 104 | return { 105 | "object": "list", 106 | "data": [ 107 | { 108 | "id": "landppt-v1", 109 | "object": "model", 110 | "created": 1677610602, 111 | "owned_by": "landppt", 112 | "permission": [], 113 | "root": "landppt-v1", 114 | "parent": None 115 | }, 116 | { 117 | "id": "landppt-ppt-generator", 118 | "object": "model", 119 | "created": 1677610602, 120 | "owned_by": "landppt", 121 | "permission": [], 122 | "root": "landppt-ppt-generator", 123 | "parent": None 124 | } 125 | ] 126 | } 127 | 128 | async def stream_chat_completion(request: ChatCompletionRequest) -> AsyncGenerator[str, None]: 129 | """ 130 | Stream chat completion responses 131 | """ 132 | try: 133 | # Simulate streaming response 134 | last_message = request.messages[-1].content if request.messages else "" 135 | 136 | if ai_service.is_ppt_request(last_message): 137 | response_content = await ai_service.handle_ppt_chat_request(request) 138 | else: 139 | response_content = await ai_service.handle_general_chat_request(request) 140 | 141 | # Split response into chunks for streaming 142 | words = response_content.split() 143 | for i, word in enumerate(words): 144 | chunk_data = { 145 | "id": f"chatcmpl-{i}", 146 | "object": "chat.completion.chunk", 147 | "created": 1677610602, 148 | "model": request.model, 149 | "choices": [{ 150 | "index": 0, 151 | "delta": {"content": word + " "}, 152 | "finish_reason": None 153 | }] 154 | } 155 | yield f"data: {json.dumps(chunk_data)}\n\n" 156 | await asyncio.sleep(0.05) # Simulate processing delay 157 | 158 | # Send final chunk 159 | final_chunk = { 160 | "id": f"chatcmpl-final", 161 | "object": "chat.completion.chunk", 162 | "created": 1677610602, 163 | "model": request.model, 164 | "choices": [{ 165 | "index": 0, 166 | "delta": {}, 167 | "finish_reason": "stop" 168 | }] 169 | } 170 | yield f"data: {json.dumps(final_chunk)}\n\n" 171 | yield "data: [DONE]\n\n" 172 | 173 | except Exception as e: 174 | error_chunk = { 175 | "error": { 176 | "message": str(e), 177 | "type": "server_error" 178 | } 179 | } 180 | yield f"data: {json.dumps(error_chunk)}\n\n" 181 | -------------------------------------------------------------------------------- /template_examples/简约答辩风.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "简约答辩风", 3 | "description": "以蓝白为主色调的大学答辩风格", 4 | "html_template": "\n\n\n \n \n {{ page_title }}\n \n \n \n \n\n\n
\n
\n

{{ main_heading }}

\n
\n \n
\n
\n {{ page_content }}\n
\n
\n \n
\n {{ current_page_number }} / {{ total_page_count }}\n
\n
\n\n\n", 5 | "tags": [ 6 | "蓝白", 7 | "大学答辩" 8 | ], 9 | "is_default": false, 10 | "export_info": { 11 | "exported_at": "2025-07-23T07:58:37.924Z", 12 | "original_id": 10, 13 | "original_created_at": 1753257350.5712268 14 | } 15 | } --------------------------------------------------------------------------------