diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 4c2c820b7..5c6c660ab 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -231,6 +231,18 @@ "authentication": "ON_INSTALL" }, "category": "开发工具" + }, + { + "name": "ppt-translator", + "source": { + "source": "local", + "path": "./plugins/codex/plugins/ppt-translator" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "文档处理" } ] } diff --git a/plugins/codex/.agents/plugins/marketplace.json b/plugins/codex/.agents/plugins/marketplace.json index bdc468f01..c0ea69917 100644 --- a/plugins/codex/.agents/plugins/marketplace.json +++ b/plugins/codex/.agents/plugins/marketplace.json @@ -231,6 +231,18 @@ "authentication": "ON_INSTALL" }, "category": "开发工具" + }, + { + "name": "ppt-translator", + "source": { + "source": "local", + "path": "./plugins/ppt-translator" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "文档处理" } ] } diff --git a/plugins/codex/plugins/ppt-translator/.codex-plugin/plugin.json b/plugins/codex/plugins/ppt-translator/.codex-plugin/plugin.json new file mode 100644 index 000000000..f319b6759 --- /dev/null +++ b/plugins/codex/plugins/ppt-translator/.codex-plugin/plugin.json @@ -0,0 +1,37 @@ +{ + "name": "ppt-translator", + "version": "1.0.0", + "description": "易瞳-牟新麒 提供,可以将 PowerPoint 文件翻译成任何语言,同时保持原有的布局格式。该系统采用“渲染-验证”循环机制(LibreOffice + Vision),从而确保文本不会溢出显示。用户只需在需要翻译 PPT/PPTX 文件时激活该功能即可。", + "author": { + "name": "EAPIL", + "url": "https://git.playones.com/arechen/EapilSkillMarket" + }, + "homepage": "https://git.playones.com/arechen/EapilSkillMarket", + "repository": "https://git.playones.com/arechen/EapilSkillMarket", + "license": "Proprietary", + "keywords": [ + "eapil", + "codex-skill", + "ppt-translator" + ], + "skills": "./skills/", + "interface": { + "displayName": "ppt-translator", + "shortDescription": "易瞳-牟新麒 提供,可以将 PowerPoint 文件翻译成任何语言,同时保持原有的布局格式。该系统采用“渲染-验证”循环机制(LibreOffice + Vision),从而确保文本不会溢出显示。用户只需在需要翻译 PPT/PPTX 文件时激活该功能即可。", + "longDescription": "易瞳-牟新麒 提供,可以将 PowerPoint 文件翻译成任何语言,同时保持原有的布局格式。该系统采用“渲染-验证”循环机制(LibreOffice + Vision),从而确保文本不会溢出显示。用户只需在需要翻译 PPT/PPTX 文件时激活该功能即可。", + "developerName": "EAPIL", + "category": "文档处理", + "capabilities": [ + "Read", + "Write" + ], + "defaultPrompt": [ + "使用 ppt-translator 帮我完成这个任务。" + ], + "websiteURL": "https://git.playones.com/arechen/EapilSkillMarket", + "privacyPolicyURL": "https://git.playones.com/arechen/EapilSkillMarket", + "termsOfServiceURL": "https://git.playones.com/arechen/EapilSkillMarket", + "brandColor": "#2563EB", + "screenshots": [] + } +} diff --git a/plugins/codex/plugins/ppt-translator/skills/ppt-translator/SKILL.md b/plugins/codex/plugins/ppt-translator/skills/ppt-translator/SKILL.md new file mode 100644 index 000000000..493936f39 --- /dev/null +++ b/plugins/codex/plugins/ppt-translator/skills/ppt-translator/SKILL.md @@ -0,0 +1,53 @@ +--- +name: ppt-translator +description: Translate PowerPoint files to any language while preserving layout. Uses a render-and-verify agent loop (LibreOffice + Vision) to guarantee no text overflow. Activate when user wants to translate a PPT/PPTX file. +--- + +# 🎯 PPT Translator + +保持排版翻译 PPT,支持任意语言对。 + +## 核心原理 + +**Agent Loop(渲染验证):** +1. 提取 PPT 文字 + 样式 +2. LLM 翻译(保证准确) +3. 写回 PPT(python-pptx) +4. LibreOffice 渲染成真实 PNG +5. Vision 模型检测文字溢出 +6. 有溢出 → 缩字号 15% → 回步骤 3 +7. 通过 → 输出最终 PPTX + +## 依赖 + +- `python-pptx`(pip) +- `libreoffice`(yum/apt) + +## 使用方法 + +```bash +python3 scripts/translate.py \ + --input /path/to/file.pptx \ + --output /path/to/output.pptx \ + --translations '{"原文": "translation", ...}' \ + --max-iter 5 +``` + +## Agent 调用流程 + +1. 用户发送 PPTX 文件 +2. Agent 提取所有文字 → 翻译(自己翻译或调用 LLM) +3. 调用 `scripts/translate.py` 执行渲染验证循环 +4. 循环完成后将输出 PPTX 发回用户 + +## 输出 + +- 最终 PPTX 文件(可编辑) +- 渲染验证 PNG(可选,用于确认) +- 迭代日志(几轮收敛、最终字号缩放比) + +## 注意事项 + +- LibreOffice 首次启动较慢(~5s),之后正常 +- 字号缩放是全局的(所有 shape 等比缩),未来可优化为 per-shape +- 不支持 .ppt(老格式),仅支持 .pptx diff --git a/plugins/codex/plugins/ppt-translator/skills/ppt-translator/_meta.json b/plugins/codex/plugins/ppt-translator/skills/ppt-translator/_meta.json new file mode 100644 index 000000000..981d7675a --- /dev/null +++ b/plugins/codex/plugins/ppt-translator/skills/ppt-translator/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn76b3dg4zkgwkrqk358gn9bph81s6aw", + "slug": "ppt-translator", + "version": "1.0.0", + "publishedAt": 1771959266988 +} \ No newline at end of file diff --git a/plugins/codex/plugins/ppt-translator/skills/ppt-translator/scripts/translate.py b/plugins/codex/plugins/ppt-translator/skills/ppt-translator/scripts/translate.py new file mode 100644 index 000000000..426c9d089 --- /dev/null +++ b/plugins/codex/plugins/ppt-translator/skills/ppt-translator/scripts/translate.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +""" +PPT Translator - Agent Loop Core +用法: + python3 translate.py --input file.pptx --output out.pptx --translations '{"中文": "English"}' [--max-iter 5] + +检测溢出:本脚本只做渲染+写回,溢出检测由外部调用(Agent Vision)完成。 +完整 loop 由 Agent 驱动:写PPT → 渲染 → Vision检测 → 缩字号 → 重复。 +""" + +import argparse +import json +import subprocess +import sys +import os +from pathlib import Path +from pptx import Presentation +from pptx.util import Pt + + +def get_para_text(para): + return "".join(run.text for run in para.runs).strip() + + +def write_translated_pptx(input_path: str, output_path: str, + translations: dict, scale: float = 1.0): + """ + Write translated PPTX with optional font scale. + translations: {original_text: translated_text} + scale: font size multiplier (1.0 = no change, 0.85 = shrink 15%) + """ + prs = Presentation(input_path) + + for slide in prs.slides: + for shape in slide.shapes: + if not shape.has_text_frame: + continue + tf = shape.text_frame + tf.word_wrap = True + + for para in tf.paragraphs: + orig = get_para_text(para) + if not orig or orig not in translations: + continue + translated = translations[orig] + if not para.runs: + continue + + # Get original font size + orig_pt = next((r.font.size.pt for r in para.runs if r.font.size), 12.0) + new_pt = max(6.0, round(orig_pt * scale, 1)) + + # Apply: first run gets full text, others cleared + para.runs[0].text = translated + para.runs[0].font.size = Pt(new_pt) + for run in para.runs[1:]: + run.text = "" + + prs.save(output_path) + return output_path + + +def render_to_png(pptx_path: str, output_dir: str) -> str: + """Render PPTX to PNG using LibreOffice. Returns PNG path.""" + result = subprocess.run( + ['libreoffice', '--headless', '--convert-to', 'png', + '--outdir', output_dir, pptx_path], + capture_output=True, text=True, timeout=60 + ) + if result.returncode != 0: + raise RuntimeError(f"LibreOffice failed: {result.stderr}") + + stem = Path(pptx_path).stem + png_path = Path(output_dir) / f"{stem}.png" + if not png_path.exists(): + pngs = sorted(Path(output_dir).glob(f"{stem}*.png")) + if not pngs: + raise RuntimeError(f"No PNG generated in {output_dir}") + png_path = pngs[0] + return str(png_path) + + +def extract_texts(input_path: str) -> list: + """Extract all non-empty paragraph texts from PPTX.""" + prs = Presentation(input_path) + texts = [] + for slide in prs.slides: + for shape in slide.shapes: + if shape.has_text_frame: + for para in shape.text_frame.paragraphs: + t = get_para_text(para) + if t: + texts.append(t) + return list(dict.fromkeys(texts)) # deduplicated + + +def main(): + parser = argparse.ArgumentParser(description="PPT Translator Core") + parser.add_argument("--input", required=True, help="Input PPTX path") + parser.add_argument("--output", required=False, help="Output PPTX path") + parser.add_argument("--translations", required=False, + help="JSON string: {original: translated}") + parser.add_argument("--scale", type=float, default=1.0, + help="Font scale factor (default: 1.0)") + parser.add_argument("--render", action="store_true", + help="Also render to PNG after writing") + parser.add_argument("--extract", action="store_true", + help="Just extract texts and print as JSON, then exit") + args = parser.parse_args() + + if args.extract: + texts = extract_texts(args.input) + print(json.dumps(texts, ensure_ascii=False, indent=2)) + return + + if not args.translations: + print("Error: --translations required unless --extract", file=sys.stderr) + sys.exit(1) + + translations = json.loads(args.translations) + + output_path = write_translated_pptx( + args.input, args.output, translations, args.scale + ) + print(json.dumps({"status": "ok", "output": output_path, "scale": args.scale})) + + if args.render: + output_dir = str(Path(args.output).parent) + png_path = render_to_png(output_path, output_dir) + print(json.dumps({"status": "rendered", "png": png_path})) + + +if __name__ == "__main__": + main()