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:
@@ -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": "文档处理"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": "文档处理"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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. 写回 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
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user