├── .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 |
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 |
33 |
34 |
35 |
💡 建议解决方案
36 |
37 | - 检查网络连接是否正常
38 | - 刷新页面重试操作
39 | - 清除浏览器缓存
40 | - 尝试使用其他浏览器
41 | - 如果问题持续存在,请联系技术支持
42 |
43 |
44 |
45 |
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\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 \n
\n
\n {{ page_content }}\n
\n
\n \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 \n
\n
\n {{ page_content }}\n
\n
\n \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 |
50 |
51 |
52 |
53 |
🎯 操作选项
54 |
55 |
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 |
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 关键点三:产品满意度调查获得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
\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 |
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 |
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 |
127 | - 文件大小建议不超过 10MB
128 | - 确保文档内容清晰、结构完整
129 | - PDF 文件请确保文字可以正常选择和复制
130 | - Word 文档建议使用标准格式,避免复杂排版
131 |
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 \n
\n
\n {{ page_content }}\n
\n
\n\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
\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 \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 \n
\n
\n {{ page_content }}\n
\n
\n \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 \n
\n
\n {{ page_content }}\n
\n
\n \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 | }
--------------------------------------------------------------------------------