vibe-coding-课程-干货集训
06 AI PPT
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 Key:https://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">←</button> <span class="info" id="info">1 / 1</span> <button id="next">→</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>
``` |