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"
|
"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. 写回 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