← 返回首页ai798LAB Public

vibe-coding-课程-干货集训

06 AI PPT

2026年1月14日Bash

Skills文件夹结构

.claude/skills/ppt/├── SKILL.md                      # Skill 主入口文件,定义交互流程├── references/│   └── prompts.md                # 提示词参考文档├── scripts/│   ├── create_pptx.py            # PPTX 打包脚本│   ├── create_viewer.py          # HTML 播放器生成脚本│   ├── export_ppt.py             # PPT 导出工具│   ├── fix_text.py               # 文字修复工具│   ├── generate_image.py         # 单张图片生成│   ├── generate_ppt.py           # 批量生成主脚本│   └── upscale.py                # 超分处理脚本├── style/│   ├── deep_tech.md              # 深空科技风格└── templates/    └── viewer.html               # HTML 播放器模板

Skills文件夹详细内容

文件代码块
SKILL.md```YAML
---name: pptdescription: 企业级 PPT 生成工具。选择预设风格、规划内容结构、生成统一风格的高质量 PPT。使用场景:当用户需要制作 PPT、演示文稿时使用。allowed-tools: Read, Write, Edit, Bash, Glob, Grep---# AtomSlides - 神级 PPT 生成工具****## 概述AtomSlides 是一个 AI PPT 生成工具,通过预设风格模板生成高质量演示文稿。核心流程:选择风格 → 规划内容 → 批量生成 → 超分处理 → 输出---## 交互流程当用户说"我要做一个 PPT"或类似请求时,按以下流程进行:### 第 1 轮:收集必要信息首先动态读取可用风格:执行 ls .claude/skills/ppt/style/*.md 获取当前可用的风格列表,读取每个风格文件的「风格名称」和「风格描述」部分。然后一次性询问所有必要信息:好的,我来帮你制作 PPT!请提供以下信息:**1. 内容来源**(必填) - 提供文件路径(如 @article.md) - 或直接粘贴文字内容**2. 风格选择**(从 .claude/skills/ppt/style/ 动态读取) [根据实际读取到的风格文件列出选项,格式如下] A. {风格名称} - {风格描述摘要} B. {风格名称} - {风格描述摘要} ...**3. 页数范围** - 5 页(精简版) - 5-10 页(标准版,推荐) - 10-15 页(详细版) - 20-25 页(完整版)请一次性告诉我这些信息,例如:"内容是 @article.md,风格选 A,10 页左右"### 第 2 轮:展示规划并确认收集完信息后,分析内容并展示页面规划:我已分析完内容,以下是 PPT 规划:**主题**:AI 产品设计指南**总页数**:8 页**风格**:渐变毛玻璃| 页码 | 类型 | 内容概要 ||------|------|----------|| 1 | 封面 | AI 产品设计指南 - 构建智能体验 || 2 | 内容 | 核心设计原则(4 个要点) || 3 | 内容 | 用户研究方法 || 4 | 数据 | 市场数据分析 || ... | ... | ... || 8 | 结束 | 感谢 & 联系方式 |确认无误后,我将开始生成。请回复"确认"开始,或告诉我需要调整的地方。### 确认后:开始生成用户确认后,开始执行生成流程(见下方"执行流程")。---## 输入支持| 格式 | 说明 ||------|------|| Markdown (.md) | 推荐,结构清晰 || PDF (.pdf) | 自动提取文本 || Word (.docx) | 自动提取文本 || 纯文本 | 直接粘贴到对话中 |---## 输出内容每次生成都会输出到 .atomslides/ 目录:.atomslides/├── slides_plan.json # 页面规划├── images/ # 原始图片├── images-4k/ # 超分后的 4K 图片├── output.pptx # 原始 PPTX├── output-4k.pptx # 4K 版 PPTX(推荐使用)└── index-4k.html # 4K HTML 播放器---## 用户选项****### 页数范围| 选项 | 页数 | 适合内容量 ||------|------|-----------|| 精简版 | 5 页 | 500 字以内 || 标准版 | 5-10 页 | 500-1500 字 || 详细版 | 10-15 页 | 1500-3000 字 || 完整版 | 20-25 页 | 3000 字以上 |### 可用风格动态读取:风格文件存放在 .claude/skills/ppt/style/ 目录下,每个 .md 文件即为一个风格。读取风格时,解析每个文件的:- ## 风格 ID:用于 slides_plan.json 中的 style 字段- ## 风格名称:展示给用户的名称- ## 风格描述:风格特点摘要添加新风格只需在 style/ 目录下新建 .md 文件,Agent 会自动识别。---## 执行流程****### 第 1 步:分析内容并生成规划读取用户提供的内容,分析结构,生成 slides_plan.json关键:加入 enhancement 字段进行智能增强根据每页内容的语义,添加定制化的视觉描述:json{ "title": "演示文稿标题", "style": "gradient-glass", "resolution": "4K", "slides": [ { "slide_number": 1, "page_type": "cover", "content": "标题:Claude Skills 指南\n副标题:从入门到精通", "enhancement": "中心放置一个代表「模块化扩展」的 3D 玻璃立方体组合,立方体之间有发光连接线" }, { "slide_number": 2, "page_type": "content", "content": "Skills vs MCP 核心区别\n- MCP:开放协议\n- Skill:完整方法", "enhancement": "使用左右分栏对比布局,左侧卡片配球体图标,右侧卡片配立方体图标" } ]}enhancement 字段使用指南:- 对比类内容:建议左右分栏布局- 列表类内容:建议 Bento 网格 + 图标- 数据类内容:建议配发光图表- 封面/结束页:建议描述独特的 3D 装饰物页面类型:- cover:封面页(第 1 页)- content:内容页(核心观点、要点列表)- data:数据页(统计、对比、图表)- end:结束页(最后一页)将 slides_plan.json 保存到 .atomslides/slides_plan.json,向用户展示规划,请求确认。### 第 2 步:批量生成图片用户确认后,调用生成脚本:bashpython3 .claude/skills/ppt/scripts/generate_ppt.py \ --plan .atomslides/slides_plan.json \ --style .claude/skills/ppt/style/{选择的风格文件}.md \ --resolution 4K \ --output .atomslides参数说明:- --plan:slides 规划 JSON 文件- --style:风格模板文件路径- --resolution:2K 或 4K- --output:输出目录- --start-from:从第 N 页开始(断点恢复)### 第 3 步:超分处理(必须)由于 API 返回的图片分辨率较低(约 1376×768),必须进行超分处理bash# 批量 4 倍超分python3 .claude/skills/ppt/scripts/upscale.py \ --images .atomslides/images/ \ --output .atomslides/images-4k/ \ --scale 4 \ --method lanczos# 重命名去掉后缀for f in .atomslides/images-4k/slide-*-4.0x.png; do newname=$(echo "$f" | sed 's/-4.0x//') mv "$f" "$newname"done### 第 4 步:打包输出bash# 生成 4K 版 HTML 播放器python3 .claude/skills/ppt/scripts/create_viewer.py \ --images .atomslides/images-4k/ \ --output .atomslides/index-4k.html# 生成 4K 版 PPTXpython3 .claude/skills/ppt/scripts/create_pptx.py \ --images .atomslides/images-4k/ \ --output .atomslides/output-4k.pptx### 第 5 步:返回结果告诉用户输出文件位置:- 4K PPTX.atomslides/output-4k.pptx(推荐使用)- 4K HTML.atomslides/index-4k.html(浏览器演示)- PNG 图片:.atomslides/images-4k/- 提示词记录:.atomslides/prompts.json---## 单页修改(重要)当需要修改某一页时,绝对不能影响其他页面:1. 创建只包含该页的临时计划文件2. 只生成该页3. 从备份恢复其他页面4. 重新打包核心原则:修改文字内容时,绝不能牺牲页面的精美度和视觉质量。---## API 配置生图 API 配置在 config.env(项目根目录)bash# Google Gemini API(必需)GEMINI_API_KEY=your_api_key_here获取 API Keyhttps://aistudio.google.com/apikey---**## 文件说明| 文件 | 说明 ||------|------|| SKILL.md | 本文件,Skill 主入口 || config.env(根目录) | API 配置 || style/*.md | 风格定义文件(动态读取) || scripts/generate_ppt.py | 批量生成脚本 || scripts/upscale.py | 超分处理脚本 || scripts/create_pptx.py | PPTX 打包脚本 || scripts/create_viewer.py | HTML 播放器生成脚本 || templates/viewer.html | HTML 播放器模板 |---## 常见问题****### Q: 生成的中文有错别字?在 enhancement 中添加英文注释:"enhancement": "... Chinese text: 团队效率 (Team Efficiency)"### Q: 图片模糊?确保执行了超分处理步骤,使用 .atomslides/output-4k.pptx### Q: 需要修改某一页?**参考"单页修改"章节,创建临时计划文件单独生成,不要使用 --start-from
| references/prompt | ```YAML
**# PPT 图像生成提示词模板****## 基础模板**```Professional presentation slide design.Title: {title}Content: {content}Style: {style_description}Requirements:- Aspect ratio: 16:9 (1920x1080)- Clear, readable text- Professional layout- High visual quality```**## 中文优化模板**针对中文内容,使用以下增强提示词:```Professional presentation slide design with Chinese text.Title (Chinese): {title}Content (Chinese): {content}Visual style: {style_description}Critical requirements for Chinese text rendering:- Use modern Chinese sans-serif font (similar to PingFang, Source Han Sans)- Text must be crisp, sharp, and highly readable- High contrast ratio between text and background (minimum 4.5:1)- Clear visual hierarchy: title > subtitle > body text- Proper line spacing for Chinese characters- No text distortion or artifactsLayout requirements:- Aspect ratio: 16:9 (1920x1080 pixels)- Title positioned at top, large and prominent- Content area with adequate margins- Clean, uncluttered design```**## 页面类型模板****### 封面页**```Professional presentation cover slide.Title: {main_title}Subtitle: {subtitle}Design style: {style}- Centered layout- Large, impactful title- Subtle background design- Professional and modern aesthetic```**### 内容页**```Professional presentation content slide.Title: {section_title}Key points:{bullet_points}Design requirements:- Title at top- Bullet points clearly formatted- Visual balance between text and whitespace- Icons or graphics to enhance readability if appropriate```**### 图表页**```Professional presentation slide with data visualization.Title: {chart_title}Data: {data_description}Chart type: {chart_type}Requirements:- Clear data labels- Readable legend- Professional color scheme- Data-to-ink ratio optimized```**### 总结页**```Professional presentation closing slide.Title: {summary_title}Key takeaways:{takeaways}Design:- Clean, memorable layout- Emphasis on key messages- Call to action if applicable```**## 风格关键词**根据不同场景选择合适的风格关键词:**### 商务风格**- Corporate, professional, clean, minimalist- Blue, navy, white color scheme- Sans-serif fonts**### 科技风格**- Modern, tech, innovative, digital- Dark background with accent colors- Geometric shapes, gradients**### 创意风格**- Creative, bold, vibrant, dynamic- Colorful palette- Unique layouts, artistic elements**### 学术风格**- Academic, scholarly, informative- Clean white background- Structured, organized layout
``` |
| scripts/create_pptx.py | ```Python
#!/usr/bin/env python3"""AtomSlides - PPTX Creator=========================将图片打包成 PPTX 文件使用方法:    python create_pptx.py --images ./images/ --output output.pptx"""import argparseimport sysfrom pathlib import Pathtry:    from pptx import Presentation    from pptx.util import Inchesexcept ImportError:    print("错误: 请安装 python-pptx 库")    print("运行: pip install python-pptx")    sys.exit(1)try:    from PIL import Imageexcept ImportError:    print("错误: 请安装 Pillow 库")    print("运行: pip install Pillow")    sys.exit(1)def create_pptx(images_dir, output_file):    """    将图片目录中的所有图片打包成 PPTX 文件    Args:        images_dir: 图片目录路径        output_file: 输出 PPTX 文件路径    Returns:        bool: 是否成功    """    images_path = Path(images_dir)    output_path = Path(output_file)    if not images_path.exists():        print(f"错误: 图片目录不存在: {images_dir}")        return False    # 收集所有图片文件    image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp'}    images = sorted([        f for f in images_path.iterdir()        if f.suffix.lower() in image_extensions    ])    if not images:        print(f"错误: 图片目录中没有找到图片: {images_dir}")        return False    print(f"找到 {len(images)} 张图片")    # 创建 PPTX    prs = Presentation()    # 设置幻灯片尺寸为 16:9    prs.slide_width = Inches(13.333)   # 16:9 宽度    prs.slide_height = Inches(7.5)     # 16:9 高度    slide_width = prs.slide_width.inches    slide_height = prs.slide_height.inches    # 获取空白布局    blank_layout = prs.slide_layouts[6]  # 空白布局    for i, image_path in enumerate(images, 1):        print(f"处理图片 {i}/{len(images)}: {image_path.name}")        # 添加空白幻灯片        slide = prs.slides.add_slide(blank_layout)        # 获取图片尺寸        with Image.open(image_path) as img:            img_width, img_height = img.size        # 计算缩放比例,保持宽高比        img_ratio = img_width / img_height        slide_ratio = slide_width / slide_height        if img_ratio > slide_ratio:            # 图片较宽,以宽度为准            width = Inches(slide_width)            height = Inches(slide_width / img_ratio)            left = Inches(0)            top = Inches((slide_height - slide_width / img_ratio) / 2)        else:            # 图片较高,以高度为准            height = Inches(slide_height)            width = Inches(slide_height * img_ratio)            left = Inches((slide_width - slide_height * img_ratio) / 2)            top = Inches(0)        # 添加图片到幻灯片        slide.shapes.add_picture(            str(image_path),            left, top,            width, height        )    # 确保输出目录存在    output_path.parent.mkdir(parents=True, exist_ok=True)    # 保存 PPTX    prs.save(str(output_path))    print(f"PPTX 文件已保存: {output_path}")    print(f"共 {len(images)} 页")    return Truedef main():    parser = argparse.ArgumentParser(        description="AtomSlides - 将图片打包成 PPTX 文件"    )    parser.add_argument(        "--images",        required=True,        help="图片目录路径"    )    parser.add_argument(        "--output",        required=True,        help="输出 PPTX 文件路径"    )    args = parser.parse_args()    success = create_pptx(args.images, args.output)    sys.exit(0 if success else 1)if __name__ == "__main__":    main()
``` |
| scripts/create_viewers.py | ```XML
#!/usr/bin/env python3"""AtomSlides - HTML Viewer Creator================================生成 HTML 播放器使用方法:    python create_viewer.py --images ./images/ --output index.html"""import argparseimport jsonimport sysfrom pathlib import Pathdef get_template():    """获取 HTML 模板"""    # 尝试从 templates 目录读取    template_paths = [        Path(__file__).parent.parent / "templates" / "viewer.html",        Path.home() / ".claude" / "skills" / "atomslides" / "templates" / "viewer.html",    ]    for path in template_paths:        if path.exists():            return path.read_text(encoding='utf-8')    # 如果找不到模板,使用内置简化版本    return get_builtin_template()def get_builtin_template():    """内置的简化 HTML 模板"""    return '''<!DOCTYPE html><html lang="zh-CN"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>AtomSlides Viewer</title>    <style>        * { margin: 0; padding: 0; box-sizing: border-box; }        body {            font-family: -apple-system, BlinkMacSystemFont, sans-serif;            background: #0a0a0a;            color: #fff;            min-height: 100vh;            display: flex;            flex-direction: column;            align-items: center;            justify-content: center;        }        .container {            width: 100%;            max-width: calc(100vh * 16 / 9);            aspect-ratio: 16 / 9;            background: #111;            position: relative;        }        .container img {            width: 100%;            height: 100%;            object-fit: contain;        }        .controls {            position: fixed;            bottom: 30px;            left: 50%;            transform: translateX(-50%);            display: flex;            gap: 20px;            background: rgba(30,30,30,0.9);            padding: 12px 24px;            border-radius: 50px;        }        button {            width: 44px;            height: 44px;            border-radius: 50%;            border: none;            background: rgba(255,255,255,0.1);            color: #fff;            cursor: pointer;            font-size: 18px;        }        button:hover { background: rgba(255,255,255,0.2); }        button:disabled { opacity: 0.3; }        .info { font-size: 14px; line-height: 44px; }    </style></head><body>    <div class="container">        <img id="slide" src="">    </div>    <div class="controls">        <button id="prev">&larr;</button>        <span class="info" id="info">1 / 1</span>        <button id="next">&rarr;</button>    </div>    <script>        const SLIDES = /*{{SLIDES_DATA}}*/[];        let current = 0;        const img = document.getElementById('slide');        const info = document.getElementById('info');        const prev = document.getElementById('prev');        const next = document.getElementById('next');        function show(i) {            if (i < 0 \|\| i >= SLIDES.length) return;            current = i;            img.src = SLIDES[current];            info.textContent = (current + 1) + ' / ' + SLIDES.length;            prev.disabled = current === 0;            next.disabled = current === SLIDES.length - 1;        }        prev.onclick = () => show(current - 1);        next.onclick = () => show(current + 1);        document.onkeydown = (e) => {            if (e.key === 'ArrowLeft') show(current - 1);            if (e.key === 'ArrowRight' \|\| e.key === ' ') show(current + 1);        };        show(0);    </script></body></html>'''def create_viewer(images_dir, output_file, relative_paths=True, base_path=None):    """    生成 HTML 播放器    Args:        images_dir: 图片目录路径        output_file: 输出 HTML 文件路径        relative_paths: 是否使用相对路径        base_path: 自定义图片基础路径(如 "../images")    Returns:        bool: 是否成功    """    images_path = Path(images_dir)    output_path = Path(output_file)    if not images_path.exists():        print(f"错误: 图片目录不存在: {images_dir}")        return False    # 收集所有图片文件    image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp'}    images = sorted([        f for f in images_path.iterdir()        if f.suffix.lower() in image_extensions    ])    if not images:        print(f"错误: 图片目录中没有找到图片: {images_dir}")        return False    print(f"找到 {len(images)} 张图片")    # 计算图片路径    if base_path:        # 使用指定的基础路径        slides_data = [f"{base_path}/{img.name}" for img in images]    elif relative_paths:        output_dir = output_path.parent.resolve()        slides_data = []        for img in images:            try:                rel_path = img.resolve().relative_to(output_dir)                slides_data.append(str(rel_path).replace('\\', '/'))            except ValueError:                # 如果无法计算相对路径,使用绝对路径                slides_data.append(str(img.resolve()).replace('\\', '/'))    else:        slides_data = [str(img.resolve()).replace('\\', '/') for img in images]    # 读取模板    template = get_template()    # 替换占位符    slides_json = json.dumps(slides_data, ensure_ascii=False, indent=2)    # 支持两种占位符格式:/*{{SLIDES_DATA}}*/[] 和 {{SLIDES_DATA}}    html_content = template.replace('/*{{SLIDES_DATA}}*/[]', slides_json)    html_content = html_content.replace('{{SLIDES_DATA}}', slides_json)    # 确保输出目录存在    output_path.parent.mkdir(parents=True, exist_ok=True)    # 写入 HTML 文件    with open(output_path, 'w', encoding='utf-8') as f:        f.write(html_content)    print(f"HTML 播放器已保存: {output_path}")    print(f"共 {len(images)} 页")    print(f"\n用浏览器打开即可演示:")    print(f"  open {output_path}")    return Truedef main():    parser = argparse.ArgumentParser(        description="AtomSlides - 生成 HTML 播放器"    )    parser.add_argument(        "--images",        required=True,        help="图片目录路径"    )    parser.add_argument(        "--output",        required=True,        help="输出 HTML 文件路径"    )    parser.add_argument(        "--absolute",        action="store_true",        help="使用绝对路径(默认使用相对路径)"    )    parser.add_argument(        "--base-path",        help="自定义图片基础路径(如 '../images')"    )    args = parser.parse_args()    success = create_viewer(        args.images,        args.output,        relative_paths=not args.absolute,        base_path=args.base_path    )    sys.exit(0 if success else 1)if __name__ == "__main__":    main()
``` |
| scripts/export_ppt.py | ```Python
#!/usr/bin/env python3"""AtomSlides - PPT Exporter=========================将 PPT/PPTX 文件导出为图片使用方法:    python export_ppt.py --input template.pptx --output ./images/"""import argparseimport osimport shutilimport subprocessimport sysfrom pathlib import Pathdef find_libreoffice():    """查找 LibreOffice 可执行文件"""    possible_paths = [        "soffice",  # Linux/macOS (in PATH)        "/Applications/LibreOffice.app/Contents/MacOS/soffice",  # macOS        "/usr/bin/soffice",  # Linux        "/usr/bin/libreoffice",  # Linux        "C:\\Program Files\\LibreOffice\\program\\soffice.exe",  # Windows        "C:\\Program Files (x86)\\LibreOffice\\program\\soffice.exe",  # Windows 32-bit    ]    for path in possible_paths:        if shutil.which(path) or os.path.exists(path):            return path    return Nonedef export_with_libreoffice(input_file, output_dir):    """使用 LibreOffice 导出 PPT 为图片"""    soffice = find_libreoffice()    if not soffice:        return False, "未找到 LibreOffice,请安装 LibreOffice"    input_path = Path(input_file).resolve()    output_path = Path(output_dir).resolve()    # 确保输出目录存在    output_path.mkdir(parents=True, exist_ok=True)    # LibreOffice 导出命令    cmd = [        soffice,        "--headless",        "--convert-to", "png",        "--outdir", str(output_path),        str(input_path)    ]    print(f"使用 LibreOffice 导出...")    print(f"输入: {input_path}")    print(f"输出: {output_path}")    try:        result = subprocess.run(            cmd,            capture_output=True,            text=True,            timeout=300        )        if result.returncode != 0:            return False, f"LibreOffice 导出失败: {result.stderr}"        # 检查是否有输出文件        png_files = list(output_path.glob("*.png"))        if not png_files:            return False, "未生成任何图片文件"        # 重命名文件为统一格式        png_files = sorted(png_files)        for i, f in enumerate(png_files, 1):            new_name = output_path / f"slide-{i:02d}.png"            if f != new_name:                f.rename(new_name)        return True, f"成功导出 {len(png_files)} 张图片"    except subprocess.TimeoutExpired:        return False, "LibreOffice 导出超时"    except Exception as e:        return False, f"导出错误: {e}"def export_with_pdf(input_file, output_dir):    """通过 PDF 中转导出(备用方案)"""    try:        from pdf2image import convert_from_path    except ImportError:        return False, "请安装 pdf2image: pip install pdf2image"    # 先转换为 PDF    soffice = find_libreoffice()    if not soffice:        return False, "未找到 LibreOffice"    input_path = Path(input_file).resolve()    output_path = Path(output_dir).resolve()    output_path.mkdir(parents=True, exist_ok=True)    # 临时 PDF 路径    pdf_path = output_path / "temp.pdf"    # 转换为 PDF    cmd = [        soffice,        "--headless",        "--convert-to", "pdf",        "--outdir", str(output_path),        str(input_path)    ]    try:        result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)        if result.returncode != 0:            return False, f"PDF 转换失败: {result.stderr}"        # 查找生成的 PDF        pdf_files = list(output_path.glob("*.pdf"))        if not pdf_files:            return False, "未生成 PDF 文件"        pdf_path = pdf_files[0]        # PDF 转图片        print("正在将 PDF 转换为图片...")        images = convert_from_path(str(pdf_path), dpi=150, fmt='png')        for i, image in enumerate(images, 1):            image_path = output_path / f"slide-{i:02d}.png"            image.save(str(image_path), 'PNG')            print(f"  保存: {image_path.name}")        # 删除临时 PDF        pdf_path.unlink()        return True, f"成功导出 {len(images)} 张图片"    except Exception as e:        return False, f"导出错误: {e}"def export_ppt(input_file, output_dir):    """    导出 PPT 为图片    Args:        input_file: 输入 PPT 文件路径        output_dir: 输出目录路径    Returns:        tuple: (成功与否, 消息)    """    input_path = Path(input_file)    if not input_path.exists():        return False, f"文件不存在: {input_file}"    if input_path.suffix.lower() not in {'.ppt', '.pptx', '.odp'}:        return False, f"不支持的文件格式: {input_path.suffix}"    # 尝试 LibreOffice 直接导出    success, message = export_with_libreoffice(input_file, output_dir)    if success:        return True, message    print(f"LibreOffice 直接导出失败: {message}")    print("尝试通过 PDF 中转...")    # 备用方案:通过 PDF 中转    return export_with_pdf(input_file, output_dir)def main():    parser = argparse.ArgumentParser(        description="AtomSlides - 将 PPT 导出为图片"    )    parser.add_argument(        "--input",        required=True,        help="输入 PPT 文件路径"    )    parser.add_argument(        "--output",        required=True,        help="输出图片目录路径"    )    args = parser.parse_args()    success, message = export_ppt(args.input, args.output)    print(message)    if not success:        print("\n如果自动导出失败,请手动导出:")        print("1. 在 PowerPoint 中打开模板")        print("2. 文件 → 导出 → 图片格式(PNG)")        print("3. 导出所有页面")        print(f"4. 将图片放入: {args.output}")    sys.exit(0 if success else 1)if __name__ == "__main__":    main()
``` |
| scripts/fix_text.py | ```Python
#!/usr/bin/env python3"""AtomSlides - Text Fixer=======================修正 AI 生成图片中的中文错别字使用方法:    python fix_text.py --image slide.png --fix "错字" "正确字" --output fixed.png注意:此脚本需要 OCR 和图像处理能力,目前提供基础实现。复杂的文字修正建议使用专业图像编辑软件。"""import argparseimport sysfrom pathlib import Pathtry:    from PIL import Image, ImageDraw, ImageFontexcept ImportError:    print("错误: 请安装 Pillow 库")    print("运行: pip install Pillow")    sys.exit(1)def find_system_font():    """查找系统中可用的中文字体"""    possible_fonts = [        # macOS        "/System/Library/Fonts/PingFang.ttc",        "/System/Library/Fonts/STHeiti Light.ttc",        "/Library/Fonts/Arial Unicode.ttf",        # Linux        "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",        "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",        # Windows        "C:\\Windows\\Fonts\\msyh.ttc",        "C:\\Windows\\Fonts\\simsun.ttc",    ]    for font_path in possible_fonts:        if Path(font_path).exists():            return font_path    return Nonedef fix_text_overlay(image_path, wrong_text, correct_text, output_path,                     position=None, font_size=48, text_color=None):    """    在图片上覆盖修正文字(简单覆盖方式)    注意:这是一个简化实现,只能在指定位置覆盖文字。    精确的文字修正需要 OCR 定位 + 颜色采样 + 背景重建。    Args:        image_path: 输入图片路径        wrong_text: 错误文字(用于日志记录)        correct_text: 正确文字        output_path: 输出图片路径        position: 文字位置 (x, y),如果为 None 则需要手动指定        font_size: 字体大小        text_color: 文字颜色 (R, G, B),如果为 None 则使用白色    Returns:        bool: 是否成功    """    try:        img = Image.open(image_path)        draw = ImageDraw.Draw(img)        # 查找字体        font_path = find_system_font()        if font_path:            font = ImageFont.truetype(font_path, font_size)        else:            print("警告: 未找到中文字体,使用默认字体")            font = ImageFont.load_default()        # 默认颜色        if text_color is None:            text_color = (255, 255, 255)  # 白色        # 如果没有指定位置,提示用户        if position is None:            print("错误: 请使用 --position 参数指定文字位置")            print("示例: --position 100 200")            return False        # 绘制文字        draw.text(position, correct_text, font=font, fill=text_color)        # 保存        output_path = Path(output_path)        output_path.parent.mkdir(parents=True, exist_ok=True)        img.save(str(output_path))        print(f"已修正: '{wrong_text}' → '{correct_text}'")        print(f"输出: {output_path}")        return True    except Exception as e:        print(f"错误: {e}")        return Falsedef suggest_manual_fix(image_path, wrong_text, correct_text):    """    当自动修正不可行时,提供手动修正建议    """    print("\n" + "="*60)    print("自动文字修正提示")    print("="*60)    print(f"\n图片: {image_path}")    print(f"需要修正: '{wrong_text}' → '{correct_text}'")    print("\n推荐方案:")    print("1. 使用 Photoshop / GIMP 手动修正")    print("2. 重新生成该页(调整提示词,添加英文对照)")    print("3. 使用在线 AI 图片编辑工具")    print("\n提示词优化建议:")    print(f'   Title: "{correct_text}" ({correct_text} in Chinese)')    print("="*60)def main():    parser = argparse.ArgumentParser(        description="AtomSlides - 修正 AI 生成图片中的文字"    )    parser.add_argument(        "--image",        required=True,        help="输入图片路径"    )    parser.add_argument(        "--fix",        nargs=2,        metavar=("WRONG", "CORRECT"),        required=True,        help="错误文字和正确文字"    )    parser.add_argument(        "--output",        required=True,        help="输出图片路径"    )    parser.add_argument(        "--position",        nargs=2,        type=int,        metavar=("X", "Y"),        help="文字位置坐标"    )    parser.add_argument(        "--font-size",        type=int,        default=48,        help="字体大小(默认: 48)"    )    parser.add_argument(        "--color",        nargs=3,        type=int,        metavar=("R", "G", "B"),        help="文字颜色 RGB 值"    )    parser.add_argument(        "--suggest",        action="store_true",        help="只显示修正建议,不执行修正"    )    args = parser.parse_args()    wrong_text, correct_text = args.fix    # 如果只是显示建议    if args.suggest:        suggest_manual_fix(args.image, wrong_text, correct_text)        sys.exit(0)    # 执行修正    position = tuple(args.position) if args.position else None    text_color = tuple(args.color) if args.color else None    success = fix_text_overlay(        args.image,        wrong_text,        correct_text,        args.output,        position=position,        font_size=args.font_size,        text_color=text_color    )    if not success:        print("\n自动修正失败,显示手动修正建议:")        suggest_manual_fix(args.image, wrong_text, correct_text)    sys.exit(0 if success else 1)if __name__ == "__main__":    main()
``` |
| scripts/generate_image.py | ```Python
#!/usr/bin/env python3"""AtomSlides - Image Generator============================使用 Google Gemini API 生成 PPT 图片使用方法:    python generate_image.py --prompt "提示词" --output output.png --resolution 2K"""import argparseimport base64import jsonimport osimport sysimport timefrom pathlib import Pathtry:    import requestsexcept ImportError:    print("错误: 请安装 requests 库")    print("运行: pip install requests")    sys.exit(1)def find_config_file():    """查找配置文件"""    # 可能的配置文件路径    possible_paths = [        Path(__file__).parent.parent / "config.env",  # skill 目录        Path.cwd() / ".atomslides" / "config.env",    # 工作目录        Path.cwd() / "config.env",                    # 当前目录        Path.home() / ".claude" / "skills" / "atomslides" / "config.env",  # 全局    ]    for path in possible_paths:        if path.exists():            return path    return Nonedef load_config():    """加载配置"""    config = {}    # 从配置文件加载    config_path = find_config_file()    if config_path:        with open(config_path, 'r', encoding='utf-8') as f:            for line in f:                line = line.strip()                if line and not line.startswith('#') and '=' in line:                    key, value = line.split('=', 1)                    config[key.strip()] = value.strip().strip('"\'')    # 环境变量覆盖    env_keys = [        'GEMINI_API_KEY',        'NANO_BANANA_MODEL',        'DEFAULT_RESOLUTION',        'OUTPUT_QUALITY',    ]    for key in env_keys:        if key in os.environ:            config[key] = os.environ[key]    return configdef get_aspect_ratio(width, height):    """计算宽高比字符串"""    ratio = width / height    if abs(ratio - 16/9) < 0.01:        return "16:9"    elif abs(ratio - 4/3) < 0.01:        return "4:3"    elif abs(ratio - 1) < 0.01:        return "1:1"    else:        return "16:9"  # 默认def generate_image(prompt, output_path, resolution="2K", config=None, seed=None):    """    使用 Gemini API 生成图片    Args:        prompt: 图片提示词        output_path: 输出文件路径        resolution: 分辨率 (2K 或 4K)        config: 配置字典        seed: 随机种子(用于控制随机性,便于重新生成相似结果)    Returns:        bool: 是否成功    """    if config is None:        config = load_config()    api_key = config.get('GEMINI_API_KEY')    if not api_key or api_key == 'your_api_key_here':        print("错误: 请在 config.env 中设置 GEMINI_API_KEY")        return False    model = config.get('NANO_BANANA_MODEL', 'gemini-2.0-flash-exp')    # API 端点    url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"    # 请求数据    data = {        "contents": [{            "parts": [{"text": prompt}]        }],        "generationConfig": {            "responseModalities": ["TEXT", "IMAGE"],            "imageConfig": {                "aspectRatio": "16:9"            }        }    }    # 添加 seed(如果指定)    if seed is not None:        data["generationConfig"]["seed"] = seed    headers = {        "Content-Type": "application/json"    }    # 配置代理    proxies = {}    http_proxy = config.get('HTTP_PROXY') or os.environ.get('HTTP_PROXY')    https_proxy = config.get('HTTPS_PROXY') or os.environ.get('HTTPS_PROXY')    if http_proxy:        proxies['http'] = http_proxy    if https_proxy:        proxies['https'] = https_proxy    print(f"正在生成图片...")    print(f"模型: {model}")    print(f"分辨率: {resolution}")    if seed is not None:        print(f"随机种子: {seed}")    if proxies:        print(f"使用代理: {https_proxy or http_proxy}")    try:        start_time = time.time()        response = requests.post(url, headers=headers, json=data, timeout=120, proxies=proxies if proxies else None)        if response.status_code != 200:            print(f"API 错误 ({response.status_code}): {response.text[:500]}")            return False        result = response.json()        # 提取图片数据        candidates = result.get("candidates", [])        if not candidates:            print(f"未生成图片,响应: {json.dumps(result, indent=2)[:500]}")            return False        parts = candidates[0].get("content", {}).get("parts", [])        for part in parts:            if "inlineData" in part:                image_data = part["inlineData"]["data"]                image_bytes = base64.b64decode(image_data)                # 确保输出目录存在                output_path = Path(output_path)                output_path.parent.mkdir(parents=True, exist_ok=True)                # 保存图片                with open(output_path, 'wb') as f:                    f.write(image_bytes)                elapsed = time.time() - start_time                print(f"图片已保存: {output_path}")                print(f"生成耗时: {elapsed:.1f} 秒")                return True        print("响应中未找到图片数据")        return False    except requests.exceptions.Timeout:        print("请求超时,请重试")        return False    except requests.exceptions.RequestException as e:        print(f"请求错误: {e}")        return False    except Exception as e:        print(f"发生错误: {e}")        return Falsedef main():    parser = argparse.ArgumentParser(        description="AtomSlides - 使用 Gemini API 生成 PPT 图片"    )    parser.add_argument(        "--prompt",        required=True,        help="图片生成提示词"    )    parser.add_argument(        "--output",        required=True,        help="输出图片路径"    )    parser.add_argument(        "--resolution",        choices=["2K", "4K"],        default="2K",        help="图片分辨率 (默认: 2K)"    )    parser.add_argument(        "--seed",        type=int,        help="随机种子(用于重新生成相似结果)"    )    args = parser.parse_args()    success = generate_image(        prompt=args.prompt,        output_path=args.output,        resolution=args.resolution,        seed=args.seed    )    sys.exit(0 if success else 1)if __name__ == "__main__":    main()
``` |
| scripts/generate_ppt.py | ```Python
#!/usr/bin/env python3"""AtomSlides - PPT 批量生成器===========================综合方案:自动组合提示词 + 支持智能增强使用方法:    python generate_ppt.py --plan slides_plan.json --style styles/gradient-glass.md --resolution 2K"""import argparseimport base64import jsonimport osimport subprocessimport sysimport timefrom datetime import datetimefrom pathlib import Pathtry:    import requestsexcept ImportError:    print("错误: 请安装 requests 库")    print("运行: pip install requests")    sys.exit(1)def find_config_file():    """查找配置文件"""    possible_paths = [        Path(__file__).parent.parent / "config.env",        Path.cwd() / ".atomslides" / "config.env",        Path.cwd() / "config.env",        Path.home() / ".claude" / "skills" / "atomslides" / "config.env",    ]    for path in possible_paths:        if path.exists():            return path    return Nonedef load_config():    """加载配置"""    config = {}    config_path = find_config_file()    if config_path:        with open(config_path, 'r', encoding='utf-8') as f:            for line in f:                line = line.strip()                if line and not line.startswith('#') and '=' in line:                    key, value = line.split('=', 1)                    config[key.strip()] = value.strip().strip('"\'')    # 环境变量覆盖    for key in ['GEMINI_API_KEY', 'NANO_BANANA_MODEL', 'HTTP_PROXY', 'HTTPS_PROXY']:        if key in os.environ:            config[key] = os.environ[key]    return configdef get_proxies(config):    """获取代理配置"""    proxies = {}    if config.get('HTTP_PROXY'):        proxies['http'] = config['HTTP_PROXY']    if config.get('HTTPS_PROXY'):        proxies['https'] = config['HTTPS_PROXY']    return proxies if proxies else Nonedef get_scripts_dir():    """获取脚本目录"""    return Path(__file__).parentdef create_html_viewer(images_dir, output_path, base_path=None):    """调用 create_viewer.py 生成 HTML 播放器"""    scripts_dir = get_scripts_dir()    viewer_script = scripts_dir / "create_viewer.py"    cmd = [sys.executable, str(viewer_script),           "--images", str(images_dir),           "--output", str(output_path)]    if base_path:        cmd.extend(["--base-path", base_path])    try:        result = subprocess.run(cmd, capture_output=True, text=True)        if result.returncode == 0:            print(f"  ✓ HTML 播放器: {output_path}")            return True        else:            print(f"  ✗ HTML 生成失败: {result.stderr}")            return False    except Exception as e:        print(f"  ✗ HTML 生成错误: {e}")        return Falsedef create_pptx(images_dir, output_path):    """调用 create_pptx.py 生成 PPTX 文件"""    scripts_dir = get_scripts_dir()    pptx_script = scripts_dir / "create_pptx.py"    try:        result = subprocess.run(            [sys.executable, str(pptx_script),             "--images", str(images_dir),             "--output", str(output_path)],            capture_output=True,            text=True        )        if result.returncode == 0:            print(f"  ✓ PPTX 文件: {output_path}")            return True        else:            print(f"  ✗ PPTX 生成失败: {result.stderr}")            return False    except Exception as e:        print(f"  ✗ PPTX 生成错误: {e}")        return Falsedef load_style_template(style_path):    """    加载风格模板    从风格文件中提取完整的基础提示词模板和页面类型模板    支持任意页面类型结构    """    import re    with open(style_path, 'r', encoding='utf-8') as f:        content = f.read()    # 提取基础提示词模板    base_start = content.find("## 基础提示词模板")    base_end = content.find("## 页面类型模板")    if base_start == -1 or base_end == -1:        print("警告: 无法解析风格模板中的基础提示词部分")        base_template = ""    else:        base_template = content[base_start + len("## 基础提示词模板"):base_end].strip()    # 提取页面类型模板 - 使用正则匹配任意页面类型    page_templates = {}    # 查找所有 ### 页面名 (type) 格式的章节    page_type_pattern = r'### [^(]+\((\w+)\)'    matches = list(re.finditer(page_type_pattern, content))    # 找到 ## 技术参数 的位置作为终止点    tech_params_pos = content.find("## 技术参数")    if tech_params_pos == -1:        tech_params_pos = len(content)    for i, match in enumerate(matches):        page_type = match.group(1)  # 提取括号内的类型名        start_pos = match.start()        # 确定结束位置:下一个 ### 章节或 ## 技术参数        if i + 1 < len(matches):            end_pos = matches[i + 1].start()        else:            end_pos = tech_params_pos        section = content[start_pos:end_pos]        # 提取代码块中的模板        prompt_start = section.find("```")        prompt_end = section.rfind("```")        if prompt_start != -1 and prompt_end != -1 and prompt_start < prompt_end:            template_content = section[prompt_start+3:prompt_end].strip()            page_templates[page_type] = template_content    return {        'base_template': base_template,        'page_templates': page_templates    }def add_text_clarity_constraints(prompt, content):    """    添加文字清晰度约束    针对 AI 生图中文字容易模糊、错字的问题,添加强化指令    """    # 强化的文字渲染指令    text_clarity_instructions = """---**TEXT RENDERING REQUIREMENTS (CRITICAL)**:1. **Text Clarity**: All text must be crystal clear, sharp, and perfectly legible. No blurry, smudged, or distorted characters.2. **Chinese Character Accuracy**:   - Each Chinese character must have correct stroke count and structure   - Pay special attention to similar characters (末/未, 己/已/巳, 土/士)   - Characters must not be merged, overlapping, or pixelated3. **Typography Rules**:   - Use clean, professional sans-serif fonts   - Maintain consistent font weight throughout   - Ensure adequate spacing between characters and lines   - Text must have sufficient contrast against background4. **Quality Check**:   - Before finalizing, verify all text is readable at normal viewing distance   - No gibberish, placeholder text, or random characters allowed   - Text content must match exactly what is specified below"""    return prompt + text_clarity_instructionsdef generate_prompt(style_data, slide_info, total_slides):    """    生成单页的完整提示词    组合逻辑:    1. 完整的基础模板(从风格文件提取,保证一致性)    2. 页面类型模板(从风格文件提取)    3. 具体内容    4. enhancement 智能补充(可选)    5. 文字清晰度约束(强化中文渲染)    """    base_template = style_data['base_template']    page_templates = style_data['page_templates']    slide_number = slide_info['slide_number']    page_type = slide_info.get('page_type', 'content')    content = slide_info['content']    enhancement = slide_info.get('enhancement', '')    # 确定页面类型    if slide_number == 1:        page_type = 'cover'    elif slide_number == total_slides:        page_type = 'end'    # 获取页面类型模板(优先使用风格文件中定义的模板)    page_template = page_templates.get(page_type, page_templates.get('content', ''))    # 组合完整提示词    prompt = f"{base_template}\n\n"    prompt += "---\n\n"    # 使用从风格文件加载的页面模板    if page_template:        # 替换模板中的占位符        page_prompt = page_template        # 处理常见占位符        page_prompt = page_prompt.replace("{TITLE}", "")        page_prompt = page_prompt.replace("{SUBTITLE}", "")        page_prompt = page_prompt.replace("{CONTENT}", "")        page_prompt = page_prompt.replace("{DATA}", "")        page_prompt = page_prompt.replace("{QUOTE}", "")        page_prompt = page_prompt.replace("{AUTHOR}", "")        page_prompt = page_prompt.replace("{ITEMS}", "")        prompt += f"{page_prompt}\n\n"    else:        # 如果没有找到模板,使用通用描述        prompt += f"请生成{page_type}页面。\n\n"    # 添加具体内容    prompt += f"{content}\n"    # 添加智能增强(如果有)    if enhancement:        prompt += f"\n**视觉增强**:\n{enhancement}\n"    # 添加文字清晰度约束    prompt = add_text_clarity_constraints(prompt, content)    return promptdef generate_image(prompt, output_path, resolution, config):    """调用 API 生成单张图片"""    api_key = config.get('GEMINI_API_KEY')    if not api_key or api_key == 'your_api_key_here':        print("错误: 请在 config.env 中设置 GEMINI_API_KEY")        return False    model = config.get('NANO_BANANA_MODEL', 'gemini-2.0-flash-exp')    url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"    data = {        "contents": [{"parts": [{"text": prompt}]}],        "generationConfig": {            "responseModalities": ["TEXT", "IMAGE"],            "imageConfig": {"aspectRatio": "16:9"}        }    }    # 获取代理配置    proxies = get_proxies(config)    try:        start_time = time.time()        response = requests.post(url, headers={"Content-Type": "application/json"}, json=data, timeout=180, proxies=proxies)        if response.status_code != 200:            print(f"  API 错误 ({response.status_code}): {response.text[:200]}")            return False        result = response.json()        candidates = result.get("candidates", [])        if not candidates:            print(f"  未生成图片")            return False        parts = candidates[0].get("content", {}).get("parts", [])        for part in parts:            if "inlineData" in part:                image_data = part["inlineData"]["data"]                image_bytes = base64.b64decode(image_data)                output_path = Path(output_path)                output_path.parent.mkdir(parents=True, exist_ok=True)                with open(output_path, 'wb') as f:                    f.write(image_bytes)                elapsed = time.time() - start_time                print(f"  ✓ 已保存 ({elapsed:.1f}s)")                return True        print("  响应中未找到图片数据")        return False    except Exception as e:        print(f"  错误: {e}")        return Falsedef check_image_quality(image_path, expected_content, config):    """    使用 AI 检查生成图片的文字质量    返回: (is_good, issues_description)    - is_good: True 表示质量合格,False 表示有问题    - issues_description: 问题描述(如果有)    """    api_key = config.get('GEMINI_API_KEY')    if not api_key:        return True, ""  # 没有 API key 时跳过检查    # 读取图片并编码为 base64    with open(image_path, 'rb') as f:        image_data = base64.b64encode(f.read()).decode('utf-8')    # 使用 Gemini 的视觉能力分析图片    model = "gemini-2.0-flash-exp"  # 使用支持视觉的模型    url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"    check_prompt = f"""请仔细检查这张 PPT 幻灯片图片中的中文文字质量。预期应该包含的内容关键词:{expected_content[:500]}请检查以下问题:1. 中文字符是否清晰可读?有没有模糊、笔画粘连、缺失笔画的情况?2. 有没有乱码或无法识别的字符?3. 有没有同一句话重复出现两次或多次的情况?4. 有没有明显的错别字?5. 文字是否有意义,还是随机的占位符文字?请用以下 JSON 格式回复:{{"quality": "good" 或 "bad", "issues": "问题描述,如果没问题则为空字符串"}}只返回 JSON,不要有其他内容。"""    data = {        "contents": [{            "parts": [                {"text": check_prompt},                {                    "inlineData": {                        "mimeType": "image/png",                        "data": image_data                    }                }            ]        }],        "generationConfig": {            "temperature": 0.1,  # 低温度确保稳定输出            "maxOutputTokens": 500        }    }    proxies = get_proxies(config)    try:        response = requests.post(url, headers={"Content-Type": "application/json"}, json=data, timeout=60, proxies=proxies)        if response.status_code != 200:            print(f"  质量检查 API 错误: {response.status_code}")            return True, ""  # API 错误时跳过检查        result = response.json()        candidates = result.get("candidates", [])        if not candidates:            return True, ""        text = candidates[0].get("content", {}).get("parts", [{}])[0].get("text", "")        # 解析 JSON 响应        import re        json_match = re.search(r'\{[^}]+\}', text)        if json_match:            try:                check_result = json.loads(json_match.group())                is_good = check_result.get("quality", "good").lower() == "good"                issues = check_result.get("issues", "")                return is_good, issues            except json.JSONDecodeError:                pass        # 如果无法解析,根据文本内容判断        if "bad" in text.lower() or "问题" in text or "模糊" in text or "重复" in text:            return False, text[:200]        return True, ""    except Exception as e:        print(f"  质量检查错误: {e}")        return True, ""  # 出错时跳过检查def generate_image_with_quality_check(prompt, output_path, resolution, config, expected_content, max_retries=3):    """    生成图片并进行质量检查,如有问题自动重试    返回: (success, retry_count, final_issues)    """    for attempt in range(max_retries):        # 生成图片        success = generate_image(prompt, output_path, resolution, config)        if not success:            return False, attempt + 1, "生成失败"        # 检查质量        print(f"  检查文字质量...")        is_good, issues = check_image_quality(output_path, expected_content, config)        if is_good:            if attempt > 0:                print(f"  ✓ 第 {attempt + 1} 次生成通过质量检查")            else:                print(f"  ✓ 质量检查通过")            return True, attempt + 1, ""        else:            print(f"  ✗ 质量问题: {issues[:100]}")            if attempt < max_retries - 1:                print(f"  重新生成 (第 {attempt + 2}/{max_retries} 次)...")                # 可以在这里修改 prompt 增加更强的文字约束            else:                print(f"  已达到最大重试次数,保留当前结果")                return True, attempt + 1, issues  # 即使有问题也保留,但记录问题    return False, max_retries, "达到最大重试次数"def main():    parser = argparse.ArgumentParser(        description='AtomSlides - PPT 批量生成器',        formatter_class=argparse.RawDescriptionHelpFormatter,        epilog="""示例用法:  python generate_ppt.py --plan slides_plan.json --style styles/gradient-glass.md --resolution 2Kslides_plan.json 格式:{  "title": "演示标题",  "slides": [    {      "slide_number": 1,      "page_type": "cover",      "content": "标题:xxx\\n副标题:xxx",      "enhancement": "中心放置代表模块化的 3D 立方体组合"  // 可选    }  ]}"""    )    parser.add_argument('--plan', required=True, help='slides 规划 JSON 文件路径')    parser.add_argument('--style', required=True, help='风格模板文件路径')    parser.add_argument('--resolution', choices=['2K', '4K'], default='2K', help='图片分辨率')    parser.add_argument('--output', default='.atomslides', help='输出目录路径')    parser.add_argument('--start-from', type=int, default=1, help='从第几页开始生成(用于断点恢复)')    parser.add_argument('--check-quality', action='store_true', default=True, help='启用 AI 文字质量检查(默认开启)')    parser.add_argument('--no-check-quality', action='store_false', dest='check_quality', help='禁用 AI 文字质量检查')    parser.add_argument('--max-retries', type=int, default=3, help='质量检查失败时的最大重试次数')    args = parser.parse_args()    # 加载配置    config = load_config()    # 读取 slides 规划    with open(args.plan, 'r', encoding='utf-8') as f:        slides_plan = json.load(f)    # 加载风格模板    style_data = load_style_template(args.style)    # 准备输出目录    output_dir = Path(args.output)    images_dir = output_dir / "images"    images_dir.mkdir(parents=True, exist_ok=True)    total_slides = len(slides_plan['slides'])    # 如果从第 1 页开始,清理旧图片(防止混入上次生成的残留)    if args.start_from == 1:        existing_images = list(images_dir.glob("slide-*.png"))        if existing_images:            print(f"清理 {len(existing_images)} 张旧图片...")            for img in existing_images:                img.unlink()            print("清理完成")    # 检查代理配置    proxies = get_proxies(config)    print("=" * 60)    print("AtomSlides - PPT 批量生成器")    print("=" * 60)    print(f"标题: {slides_plan.get('title', '未命名')}")    print(f"风格: {args.style}")    print(f"分辨率: {args.resolution}")    print(f"总页数: {total_slides}")    print(f"输出目录: {output_dir}")    if proxies:        print(f"代理: {proxies.get('https', proxies.get('http', 'N/A'))}")    if args.check_quality:        print(f"质量检查: 开启 (最多重试 {args.max_retries} 次)")    else:        print(f"质量检查: 关闭")    if args.start_from > 1:        print(f"从第 {args.start_from} 页开始(断点恢复)")    print("=" * 60)    print()    # 生成所有提示词并保存(用于审核)    prompts_data = {        "metadata": {            "title": slides_plan.get("title", "未命名"),            "total_slides": total_slides,            "resolution": args.resolution,            "style": args.style,            "generated_at": datetime.now().isoformat()        },        "slides": []    }    # 批量生成    success_count = 0    fail_count = 0    for slide_info in slides_plan['slides']:        slide_number = slide_info['slide_number']        # 生成完整提示词        prompt = generate_prompt(style_data, slide_info, total_slides)        # 记录提示词        prompt_record = {            "slide_number": slide_number,            "page_type": slide_info.get('page_type', 'content'),            "content": slide_info['content'],            "enhancement": slide_info.get('enhancement', ''),            "full_prompt": prompt,            "image_path": None        }        # 跳过已生成的页面(断点恢复)        if slide_number < args.start_from:            image_path = images_dir / f"slide-{slide_number:02d}.png"            prompt_record["image_path"] = str(image_path)            prompt_record["status"] = "skipped"            prompts_data["slides"].append(prompt_record)            continue        print(f"生成第 {slide_number}/{total_slides} 页...")        # 生成图片        image_path = images_dir / f"slide-{slide_number:02d}.png"        if args.check_quality:            # 使用带质量检查的生成            success, retry_count, issues = generate_image_with_quality_check(                prompt, image_path, args.resolution, config,                expected_content=slide_info['content'],                max_retries=args.max_retries            )            prompt_record["retry_count"] = retry_count            if issues:                prompt_record["quality_issues"] = issues        else:            # 不检查质量,直接生成            success = generate_image(prompt, image_path, args.resolution, config)        if success:            prompt_record["image_path"] = str(image_path)            prompt_record["status"] = "success"            success_count += 1        else:            prompt_record["status"] = "failed"            fail_count += 1        prompts_data["slides"].append(prompt_record)    # 保存提示词记录    prompts_path = output_dir / "prompts.json"    with open(prompts_path, 'w', encoding='utf-8') as f:        json.dump(prompts_data, f, ensure_ascii=False, indent=2)    # 自动生成 HTML 和 PPTX(如果全部成功)    if fail_count == 0 and success_count > 0:        print()        print("正在打包输出文件...")        # 生成文件名:主题 + 时间戳        title = slides_plan.get('title', '未命名').replace(' ', '_').replace('/', '_')        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")        archive_name = f"{title}_{timestamp}"        # 创建存档目录        archives_dir = output_dir / "archives"        archives_dir.mkdir(parents=True, exist_ok=True)        # 生成 HTML 播放器(存档版使用 ../images 路径)        html_path = archives_dir / f"{archive_name}.html"        create_html_viewer(images_dir, html_path, base_path="../images")        # 生成 PPTX 文件        pptx_path = archives_dir / f"{archive_name}.pptx"        create_pptx(images_dir, pptx_path)        # 同时在根目录保留最新版(使用默认相对路径)        create_html_viewer(images_dir, output_dir / "index.html")        create_pptx(images_dir, output_dir / "output.pptx")        print()        print("=" * 60)        print("生成完成!")        print("=" * 60)        print(f"成功: {success_count} 页")        print()        print(f"输出文件:")        print(f"  图片目录: {images_dir}/")        print(f"  提示词记录: {prompts_path}")        print(f"  HTML 播放器: {html_path}")        print(f"  PPTX 文件: {pptx_path}")        print()        print(f"打开播放器: open {html_path}")    else:        print()        print("=" * 60)        print("生成完成!")        print("=" * 60)        print(f"成功: {success_count} 页")        if fail_count > 0:            print(f"失败: {fail_count} 页")            print(f"提示: 使用 --start-from {args.start_from + success_count} 从断点继续")        print()        print(f"输出文件:")        print(f"  图片目录: {images_dir}/")        print(f"  提示词记录: {prompts_path}")        print()        print("下一步:")        print(f"  1. 修复失败的页面后重新生成")        print(f"  2. 或手动运行 create_viewer.py 和 create_pptx.py")        print()if __name__ == "__main__":    main()
``` |
| scripts/upscale.py | ```Python
#!/usr/bin/env python3"""AtomSlides - Image Upscaler===========================将图片放大到更高分辨率(2K → 4K)使用方法:    python upscale.py --image slide.png --scale 2 --output slide-4k.png支持多种放大方式:1. Pillow 内置(LANCZOS)- 基础质量,无需额外依赖2. Real-ESRGAN - 高质量 AI 放大(需要安装)"""import argparseimport sysfrom pathlib import Pathtry:    from PIL import Imageexcept ImportError:    print("错误: 请安装 Pillow 库")    print("运行: pip install Pillow")    sys.exit(1)def upscale_pillow(image_path, scale, output_path, method="lanczos"):    """    使用 Pillow 进行图片放大    Args:        image_path: 输入图片路径        scale: 放大倍数(2 = 2倍放大)        output_path: 输出图片路径        method: 放大算法 (lanczos, bicubic, bilinear)    Returns:        bool: 是否成功    """    resample_methods = {        "lanczos": Image.Resampling.LANCZOS,        "bicubic": Image.Resampling.BICUBIC,        "bilinear": Image.Resampling.BILINEAR,    }    resample = resample_methods.get(method.lower(), Image.Resampling.LANCZOS)    try:        img = Image.open(image_path)        original_size = img.size        new_size = (int(img.width * scale), int(img.height * scale))        print(f"放大中...")        print(f"原始尺寸: {original_size[0]}x{original_size[1]}")        print(f"目标尺寸: {new_size[0]}x{new_size[1]}")        print(f"放大算法: {method.upper()}")        # 放大图片        img_upscaled = img.resize(new_size, resample)        # 确保输出目录存在        output_path = Path(output_path)        output_path.parent.mkdir(parents=True, exist_ok=True)        # 保存        img_upscaled.save(str(output_path), quality=95)        print(f"已保存: {output_path}")        return True    except Exception as e:        print(f"错误: {e}")        return Falsedef try_realesrgan(image_path, scale, output_path):    """    尝试使用 Real-ESRGAN 进行 AI 放大    Returns:        bool: 是否成功(如果未安装则返回 False)    """    try:        from basicsr.archs.rrdbnet_arch import RRDBNet        from realesrgan import RealESRGANer        import numpy as np        import cv2        print("使用 Real-ESRGAN 进行 AI 放大...")        # 加载模型        model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64,                       num_block=23, num_grow_ch=32, scale=4)        upsampler = RealESRGANer(            scale=4,            model_path='weights/RealESRGAN_x4plus.pth',            model=model,            tile=0,            tile_pad=10,            pre_pad=0,            half=False        )        # 读取图片        img = cv2.imread(str(image_path), cv2.IMREAD_UNCHANGED)        # 放大        output, _ = upsampler.enhance(img, outscale=scale)        # 保存        cv2.imwrite(str(output_path), output)        print(f"AI 放大完成: {output_path}")        return True    except ImportError:        return False    except Exception as e:        print(f"Real-ESRGAN 错误: {e}")        return Falsedef upscale_image(image_path, scale, output_path, method="auto"):    """    放大图片    Args:        image_path: 输入图片路径        scale: 放大倍数        output_path: 输出图片路径        method: 放大方式 (auto, pillow, realesrgan)    Returns:        bool: 是否成功    """    input_path = Path(image_path)    if not input_path.exists():        print(f"错误: 文件不存在: {image_path}")        return False    if method == "auto":        # 首先尝试 Real-ESRGAN        if try_realesrgan(image_path, scale, output_path):            return True        # 回退到 Pillow        print("Real-ESRGAN 不可用,使用 Pillow LANCZOS 放大")        return upscale_pillow(image_path, scale, output_path, "lanczos")    elif method == "realesrgan":        if try_realesrgan(image_path, scale, output_path):            return True        print("错误: Real-ESRGAN 不可用")        print("安装方法: pip install realesrgan basicsr")        return False    else:        return upscale_pillow(image_path, scale, output_path, method)def batch_upscale(images_dir, output_dir, scale, method="auto"):    """    批量放大目录中的所有图片    Args:        images_dir: 输入图片目录        output_dir: 输出目录        scale: 放大倍数        method: 放大方式    Returns:        tuple: (成功数, 总数)    """    images_path = Path(images_dir)    output_path = Path(output_dir)    if not images_path.exists():        print(f"错误: 目录不存在: {images_dir}")        return 0, 0    # 收集所有图片    image_extensions = {'.png', '.jpg', '.jpeg', '.webp'}    images = sorted([        f for f in images_path.iterdir()        if f.suffix.lower() in image_extensions    ])    if not images:        print(f"错误: 目录中没有图片: {images_dir}")        return 0, 0    output_path.mkdir(parents=True, exist_ok=True)    success_count = 0    for i, img in enumerate(images, 1):        print(f"\n[{i}/{len(images)}] 处理: {img.name}")        output_file = output_path / f"{img.stem}-{scale}x{img.suffix}"        if upscale_image(str(img), scale, str(output_file), method):            success_count += 1    return success_count, len(images)def main():    parser = argparse.ArgumentParser(        description="AtomSlides - 图片放大工具"    )    parser.add_argument(        "--image",        help="输入图片路径(单张图片)"    )    parser.add_argument(        "--images",        help="输入图片目录(批量处理)"    )    parser.add_argument(        "--output",        required=True,        help="输出路径(文件或目录)"    )    parser.add_argument(        "--scale",        type=float,        default=2,        help="放大倍数(默认: 2)"    )    parser.add_argument(        "--method",        choices=["auto", "pillow", "lanczos", "bicubic", "realesrgan"],        default="auto",        help="放大方式(默认: auto)"    )    args = parser.parse_args()    # 验证输入    if not args.image and not args.images:        print("错误: 请指定 --image(单张)或 --images(目录)")        sys.exit(1)    if args.image and args.images:        print("错误: --image 和 --images 不能同时使用")        sys.exit(1)    # 处理方法映射    method = args.method    if method in ["lanczos", "bicubic"]:        method = "pillow"    # 执行放大    if args.image:        # 单张图片        success = upscale_image(args.image, args.scale, args.output, method)        sys.exit(0 if success else 1)    else:        # 批量处理        success, total = batch_upscale(args.images, args.output, args.scale, method)        print(f"\n完成: {success}/{total} 张图片放大成功")        sys.exit(0 if success == total else 1)if __name__ == "__main__":    main()
``` |
| styles/deep_tech.md | ```YAML
---id: deep-space-techname: 深空科技风格description: Apple 发布会极简主义 + 棱镜流光玻璃 + 深空神秘氛围tags: [科技, AI, 发布会, 高端, 硬科技]---**# 深空科技风格****## 适用场景**- 科技产品发布会- AI/硬科技公司路演- 技术架构展示- 高端品牌提案- 未来趋势报告---**## SYSTEM_PROMPT**你是一位专家级 UI/UX 演示设计师,请生成高保真的 16:9 演示文稿幻灯片。---**## DESIGN_SYSTEM****### STYLE_OVERVIEW**风格融合 Apple 发布会的极简克制、棱镜流光的优雅科技感和深空的神秘氛围。整体气质需要高端、优雅、深邃且有诗意的浪漫感。画面要极度克制,大量留白,让核心元素成为绝对焦点。3D 元素要有"呼吸感",像是有生命的能量体。**### COLOR**纯黑 #000000 或极深空灰 #0a0a0a 作为绝对基底,营造深空感。3D 元素使用棱镜色散产生的彩虹光谱:- 主色调:光谱蓝紫 → 青 → 粉的渐变流动- 棱镜折射产生的自然彩虹色(红橙黄绿蓝靛紫的光谱过渡)- 类似肥皂泡或油膜的 iridescent(彩虹光泽)效果文字使用纯白 #ffffff,与深色背景形成强对比。**### LIGHTING**采用柔和的体积光,创造梦幻的光影氛围。关键技法:- 棱镜色散效果(chromatic dispersion):光线穿过玻璃产生彩虹光谱分离- 柔和的边缘光晕(soft rim glow)而非锐利切割- 从 3D 物体内部向外散射的柔光- 轻微的光雾弥漫(light mist)增加氛围感光照要有诗意和呼吸感,像北极光或星云的柔和流动。**### LAYOUT**采用极简精准的网格系统,大量使用负空间(60%+ 留白)。内容区域使用精准对齐,无装饰性边框。如需容器,使用极细的发光描边(1px, 20% 透明度)或轻微的深色玻璃材质,绝不使用厚重的卡片或阴影。字体层级要有戏剧化对比:超大标题 + 极小正文。**### MATERIAL**渲染优雅的 3D 流光玻璃体作为视觉锚点。核心设计语言:- ****材质****:透明/半透明的棱镜玻璃,光线穿过时产生彩虹色散(chromatic dispersion / prismatic refraction)。表面有液态金属般的流动光纹,类似参考图中的玻璃蝴蝶或玻璃键帽质感。材质要有多层次的透明度和复杂的内部折射。- ****形状****:有机流动的曲面为主——像液态玻璃凝固的瞬间、飘动的丝带、展翅的蝴蝶、流体雕塑。边缘柔和有呼吸感,而非硬边切割。可以有几何感但要融入有机曲线。- ****色彩****:通过棱镜折射产生自然的彩虹光谱流动(红→橙→黄→绿→青→蓝→紫),类似肥皂泡的 iridescent 效果。颜色是光线穿过玻璃自然产生的,而非人工着色。- ****光效****:柔和的内部散射光,边缘有梦幻的光晕而非锐利轮廓。整体像在发光但又很优雅克制。- ****氛围****:物体周围可有轻微的光雾或星尘弥漫,增加诗意和呼吸感。**### BACKGROUND**深邃的黑色基底,可加入极其细微的星云纹理或轻微的径向渐变(从中心的深灰 #111 到边缘的纯黑 #000),营造深空感。可有若隐若现的星云光带增加诗意,但要非常克制,不能抢夺 3D 元素的焦点。**### TYPOGRAPHY**使用干净的无衬线字体(SF Pro、Inter、Helvetica Neue),建立极端的层级对比。标题可超大且加粗,正文保持克制。如有图表,使用发光描边的线性图表、带能量感的环形进度条或悬浮的大数字,图表风格要精密但不花哨。**### RENDER_QUALITY**超高清渲染,真实的玻璃折射和棱镜色散效果,柔和的光影过渡。整体质感要像高端艺术装置或概念设计——优雅、有生命力、科技与艺术的融合。参考:玻璃雕塑、液态金属、北极光的诗意美感。---**## RULES****### [MUST] 中文渲染**Chinese text rendering is CRITICAL. Follow these rules strictly:- All Chinese characters must be perfectly clear, sharp, and 100% legible- Use professional sans-serif Chinese fonts (like PingFang, Noto Sans SC, Source Han Sans)- Each character must have correct strokes - no merged, blurry, or malformed characters- Maintain proper character spacing (not too tight, not too loose)- Text must have strong contrast against background (white text on dark background)- VERIFY: Every Chinese character must be recognizable and correctly formed**### [MUST] 约束**Do NOT add any text, logos, or elements not specified in the content below. Keep the composition extremely minimal.---**## PAGE_TEMPLATES****### cover******构图****:标题为绝对主角,配合一个优雅的 3D 流光玻璃体作为视觉锚点。大量留白,深空背景。****PROMPT****:```请生成封面页。标题:{{TITLE}}副标题:{{SUBTITLE}}标题使用超大粗体白色字,位置居中或偏左下。画面中放置一个优雅的 3D 流光玻璃体作为视觉锚点——形态是有机流动的曲面,像液态玻璃凝固的瞬间、飘动的丝带或展翅的蝴蝶。材质为透明棱镜玻璃,光线穿过时产生彩虹色散效果(chromatic dispersion),表面有液态金属般的流动光纹和 iridescent 彩虹光泽。边缘有柔和的光晕而非锐利轮廓,整体像在呼吸的能量体。背景是纯黑的深空,可有轻微的光雾弥漫。整体画面极度克制,60% 以上为负空间,让标题和 3D 物体成为仅有的焦点。```**### content******构图****:极简网格布局,内容精准对齐,大量留白。可用极细的发光线条分隔区域。****PROMPT****:```请生成内容页。采用极简的网格布局,将以下内容精准排列。标题:{{TITLE}}要点:{{ITEMS}}背景为纯黑。标题使用中等大小的白色粗体字,位于页面上方。要点内容使用较小的白色或浅灰色字体,排列整齐。可使用极细的发光线条(1px, 科技蓝, 20% 透明度)作为视觉分隔。每个要点前可配一个极简的发光图标或光点作为标记。页面保持 50% 以上留白,内容区域精准对齐,体现精密感。不使用卡片、阴影或厚重的容器。```**### data******构图****:数据可视化为主角,使用有科技感的图表样式,配合发光效果。****PROMPT****:```请生成数据页。数据内容:{{CONTENT}}使用有科技感的数据可视化:发光描边的环形进度条、带能量脉冲效果的柱状图、或悬浮的超大关键数字。图表使用科技蓝作为主色,配合发光效果。关键数字可以超大显示,带有轻微的发光光晕。背景纯黑,图表元素像是悬浮在深空中。整体风格精密、克制,数据清晰易读,避免过多装饰。```**### end******构图****:极简收尾,中心放置感谢语或行动号召,配小型 3D 流光装饰物。****PROMPT****:```请生成结束页。画面极简有力。中心展示:{{CONTENT}}文字使用白色大字居中显示。可配一个精致的小型 3D 流光玻璃体——形态优雅流动,材质为透明棱镜玻璃,有彩虹色散和 iridescent 光泽。边缘有柔和光晕,像在轻轻呼吸。背景为纯黑深空,可有轻微的光雾点缀。画面主体是文字,3D 元素作为点睛,整体保持极度克制和诗意美感。```---**## TECH_SPECS**- 推荐模型:gemini-2.0-flash-exp 或 gemini-3-pro-image-preview- 比例:16:9- 分辨率:2K (2752×1536) 或 4K (5504×3072)- 推荐分辨率:2K(平衡质量和生成速度)- 生成时间:2K 约 30 秒/页,4K 约 60 秒/页
``` |
| templates/viewer.html | ```HTML
<!DOCTYPE html><html lang="zh-CN"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>AtomSlides - PPT Viewer</title>    <style>        * {            margin: 0;            padding: 0;            box-sizing: border-box;        }        body {            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;            background: #0a0a0a;            color: #fff;            min-height: 100vh;            display: flex;            flex-direction: column;            align-items: center;            justify-content: center;            overflow: hidden;        }        .viewer-container {            width: 100%;            height: 100vh;            display: flex;            flex-direction: column;            align-items: center;            justify-content: center;            position: relative;        }        .slide-container {            width: 100%;            max-width: calc(100vh * 16 / 9);            aspect-ratio: 16 / 9;            position: relative;            background: #111;            border-radius: 8px;            overflow: hidden;            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);        }        .slide-container img {            width: 100%;            height: 100%;            object-fit: contain;            display: block;        }        .controls {            position: fixed;            bottom: 30px;            left: 50%;            transform: translateX(-50%);            display: flex;            align-items: center;            gap: 20px;            background: rgba(30, 30, 30, 0.9);            backdrop-filter: blur(10px);            padding: 12px 24px;            border-radius: 50px;            border: 1px solid rgba(255, 255, 255, 0.1);            transition: opacity 0.3s;        }        .controls.hidden {            opacity: 0;            pointer-events: none;        }        .nav-btn {            width: 44px;            height: 44px;            border-radius: 50%;            border: none;            background: rgba(255, 255, 255, 0.1);            color: #fff;            cursor: pointer;            display: flex;            align-items: center;            justify-content: center;            transition: all 0.2s;            font-size: 18px;        }        .nav-btn:hover {            background: rgba(255, 255, 255, 0.2);            transform: scale(1.05);        }        .nav-btn:active {            transform: scale(0.95);        }        .nav-btn:disabled {            opacity: 0.3;            cursor: not-allowed;        }        .page-indicator {            font-size: 14px;            color: rgba(255, 255, 255, 0.8);            min-width: 80px;            text-align: center;        }        .progress-bar {            display: flex;            gap: 6px;            align-items: center;        }        .progress-dot {            width: 8px;            height: 8px;            border-radius: 50%;            background: rgba(255, 255, 255, 0.3);            cursor: pointer;            transition: all 0.2s;        }        .progress-dot:hover {            background: rgba(255, 255, 255, 0.5);            transform: scale(1.2);        }        .progress-dot.active {            background: #3B82F6;            transform: scale(1.3);        }        .fullscreen-btn {            background: transparent;            border: none;            color: rgba(255, 255, 255, 0.6);            cursor: pointer;            padding: 8px;            border-radius: 8px;            transition: all 0.2s;        }        .fullscreen-btn:hover {            color: #fff;            background: rgba(255, 255, 255, 0.1);        }        .shortcuts-hint {            position: fixed;            bottom: 100px;            left: 50%;            transform: translateX(-50%);            background: rgba(0, 0, 0, 0.8);            padding: 12px 20px;            border-radius: 8px;            font-size: 12px;            color: rgba(255, 255, 255, 0.6);            opacity: 0;            transition: opacity 0.3s;            pointer-events: none;        }        .shortcuts-hint.visible {            opacity: 1;        }        .shortcuts-hint kbd {            background: rgba(255, 255, 255, 0.1);            padding: 2px 6px;            border-radius: 4px;            margin: 0 4px;        }        /* Fullscreen styles */        .viewer-container:fullscreen {            background: #000;        }        .viewer-container:fullscreen .slide-container {            max-width: none;            width: 100%;            height: 100%;            border-radius: 0;        }        /* Loading state */        .slide-container.loading::after {            content: '';            position: absolute;            top: 50%;            left: 50%;            width: 40px;            height: 40px;            margin: -20px 0 0 -20px;            border: 3px solid rgba(255, 255, 255, 0.1);            border-top-color: #3B82F6;            border-radius: 50%;            animation: spin 0.8s linear infinite;        }        @keyframes spin {            to { transform: rotate(360deg); }        }        /* Slide transition */        .slide-container img {            transition: opacity 0.3s ease;        }        .slide-container img.fade-out {            opacity: 0;        }    </style></head><body>    <div class="viewer-container" id="viewer">        <div class="slide-container" id="slideContainer">            <img id="slideImage" src="" alt="Slide">        </div>        <div class="controls" id="controls">            <button class="nav-btn" id="prevBtn" title="Previous (Left Arrow)">                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">                    <polyline points="15 18 9 12 15 6"></polyline>                </svg>            </button>            <div class="progress-bar" id="progressBar"></div>            <button class="nav-btn" id="nextBtn" title="Next (Right Arrow)">                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">                    <polyline points="9 18 15 12 9 6"></polyline>                </svg>            </button>            <div class="page-indicator" id="pageIndicator">1 / 1</div>            <button class="fullscreen-btn" id="fullscreenBtn" title="Fullscreen (F)">                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">                    <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>                </svg>            </button>        </div>        <div class="shortcuts-hint" id="shortcutsHint">            <kbd>←</kbd><kbd>→</kbd> Navigate            <kbd>Space</kbd> Auto-play            <kbd>F</kbd> Fullscreen            <kbd>H</kbd> Hide controls        </div>    </div>    <script>        // Slide data will be injected here        const SLIDES = /*{{SLIDES_DATA}}*/[];        class SlideViewer {            constructor(slides) {                this.slides = slides;                this.currentIndex = 0;                this.autoPlay = false;                this.autoPlayInterval = null;                this.controlsHidden = false;                this.slideImage = document.getElementById('slideImage');                this.slideContainer = document.getElementById('slideContainer');                this.prevBtn = document.getElementById('prevBtn');                this.nextBtn = document.getElementById('nextBtn');                this.pageIndicator = document.getElementById('pageIndicator');                this.progressBar = document.getElementById('progressBar');                this.fullscreenBtn = document.getElementById('fullscreenBtn');                this.controls = document.getElementById('controls');                this.viewer = document.getElementById('viewer');                this.shortcutsHint = document.getElementById('shortcutsHint');                this.init();            }            init() {                this.createProgressDots();                this.bindEvents();                this.showSlide(0);                this.showShortcutsHint();            }            createProgressDots() {                this.progressBar.innerHTML = '';                if (this.slides.length <= 20) {                    this.slides.forEach((_, index) => {                        const dot = document.createElement('div');                        dot.className = 'progress-dot';                        dot.addEventListener('click', () => this.showSlide(index));                        this.progressBar.appendChild(dot);                    });                }            }            bindEvents() {                this.prevBtn.addEventListener('click', () => this.prev());                this.nextBtn.addEventListener('click', () => this.next());                this.fullscreenBtn.addEventListener('click', () => this.toggleFullscreen());                document.addEventListener('keydown', (e) => this.handleKeydown(e));                // Touch support                let touchStartX = 0;                this.slideContainer.addEventListener('touchstart', (e) => {                    touchStartX = e.touches[0].clientX;                });                this.slideContainer.addEventListener('touchend', (e) => {                    const touchEndX = e.changedTouches[0].clientX;                    const diff = touchStartX - touchEndX;                    if (Math.abs(diff) > 50) {                        if (diff > 0) this.next();                        else this.prev();                    }                });            }            handleKeydown(e) {                switch(e.key) {                    case 'ArrowLeft':                    case 'ArrowUp':                        e.preventDefault();                        this.prev();                        break;                    case 'ArrowRight':                    case 'ArrowDown':                    case ' ':                        e.preventDefault();                        this.next();                        break;                    case 'Home':                        e.preventDefault();                        this.showSlide(0);                        break;                    case 'End':                        e.preventDefault();                        this.showSlide(this.slides.length - 1);                        break;                    case 'f':                    case 'F':                        this.toggleFullscreen();                        break;                    case 'h':                    case 'H':                        this.toggleControls();                        break;                    case 'Escape':                        if (document.fullscreenElement) {                            document.exitFullscreen();                        }                        break;                }            }            showSlide(index) {                if (index < 0 \|\| index >= this.slides.length) return;                this.slideImage.classList.add('fade-out');                setTimeout(() => {                    this.currentIndex = index;                    this.slideImage.src = this.slides[index];                    this.slideImage.classList.remove('fade-out');                    this.updateUI();                }, 150);            }            prev() {                if (this.currentIndex > 0) {                    this.showSlide(this.currentIndex - 1);                }            }            next() {                if (this.currentIndex < this.slides.length - 1) {                    this.showSlide(this.currentIndex + 1);                }            }            updateUI() {                this.prevBtn.disabled = this.currentIndex === 0;                this.nextBtn.disabled = this.currentIndex === this.slides.length - 1;                this.pageIndicator.textContent = `${this.currentIndex + 1} / ${this.slides.length}`;                const dots = this.progressBar.querySelectorAll('.progress-dot');                dots.forEach((dot, index) => {                    dot.classList.toggle('active', index === this.currentIndex);                });            }            toggleFullscreen() {                if (!document.fullscreenElement) {                    this.viewer.requestFullscreen();                } else {                    document.exitFullscreen();                }            }            toggleControls() {                this.controlsHidden = !this.controlsHidden;                this.controls.classList.toggle('hidden', this.controlsHidden);            }            showShortcutsHint() {                this.shortcutsHint.classList.add('visible');                setTimeout(() => {                    this.shortcutsHint.classList.remove('visible');                }, 3000);            }        }        // Initialize viewer when DOM is ready        document.addEventListener('DOMContentLoaded', () => {            new SlideViewer(SLIDES);        });    </script></body></html>
``` |