Publish ppt-translator skill to marketplace

Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources.
Confidence: high
Scope-risk: narrow
Directive: Keep private/internal skills out of the public marketplace.
Tested: Marketplace validation passed.
This commit is contained in:
KeyInfo Bot
2026-06-11 18:34:26 +08:00
parent 6355d59800
commit 834c9ae766
6 changed files with 254 additions and 0 deletions
+12
View File
@@ -231,6 +231,18 @@
"authentication": "ON_INSTALL" "authentication": "ON_INSTALL"
}, },
"category": "开发工具" "category": "开发工具"
},
{
"name": "ppt-translator",
"source": {
"source": "local",
"path": "./plugins/codex/plugins/ppt-translator"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "文档处理"
} }
] ]
} }
@@ -231,6 +231,18 @@
"authentication": "ON_INSTALL" "authentication": "ON_INSTALL"
}, },
"category": "开发工具" "category": "开发工具"
},
{
"name": "ppt-translator",
"source": {
"source": "local",
"path": "./plugins/ppt-translator"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "文档处理"
} }
] ]
} }
@@ -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": []
}
}
@@ -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. 写回 PPTpython-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
@@ -0,0 +1,6 @@
{
"ownerId": "kn76b3dg4zkgwkrqk358gn9bph81s6aw",
"slug": "ppt-translator",
"version": "1.0.0",
"publishedAt": 1771959266988
}
@@ -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()