skills: move translation skills to ./skills/, symlink from ~/.hermes/skills/
pptx-translate, chinese-text-normalize, dharma-translation: canonical location now ./skills/ with symlinks in ~/.hermes/skills/. translation-review: merged CSV/XLSX review + .dj comparison workflows into single SKILL.md. Added buddhist-terminology.md and terms-db-alignment.md references from Hermes version.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: pptx-translate
|
||||
description: Translate PowerPoint files between Chinese and English — extract strings to YAML, translate, quality review, and write back with font-shrink + auto-fit for layout.
|
||||
category: productivity
|
||||
---
|
||||
|
||||
# PPTX Translation
|
||||
|
||||
Translate `.pptx` files between Chinese and English. Covers the full pipeline: extraction → translation → review → write-back.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Extract strings to YAML
|
||||
|
||||
Run `scripts/extract.py original.pptx strings.yaml`. Produces YAML with entries:
|
||||
|
||||
```yaml
|
||||
- slide: 1
|
||||
shape: 0
|
||||
run: 0
|
||||
kind: title
|
||||
zh: 开启生命的富足
|
||||
en: ""
|
||||
```
|
||||
|
||||
- `slide` — 1-based slide number
|
||||
- `shape` — 0-based shape index within slide
|
||||
- `run` — 0-based paragraph index within text frame (or computed index for tables)
|
||||
- `kind` — `title` | `subtitle` | `center_title` | `body` | `table` | `notes`
|
||||
- `zh` — source text
|
||||
- `en` — translation target (initially empty)
|
||||
|
||||
Table `run` index formula: `num_rows * col + row`. Reverse with `row = run % num_rows`, `col = run // num_rows`.
|
||||
|
||||
Speaker notes use `shape: -1`.
|
||||
|
||||
### 2. Translate
|
||||
|
||||
**Do NOT call external translation APIs.** Translate directly — the agent IS the model. The user corrects this: "Why do you call external models to do it? You can do it yourself!"
|
||||
|
||||
Fill in the `en` field for every entry. Batch if needed, but translate in your response, not via API calls.
|
||||
|
||||
Terminology guidance for Buddhist/gratitude content:
|
||||
- 感恩=gratitude, 缘起=dependent origination, 众生=sentient beings
|
||||
- 因缘=causes and conditions, 三宝=Three Jewels, 福报=merit/blessings
|
||||
- 座上=formal practice, 座下=daily life practice, 共修=group practice
|
||||
- 上报四重恩=repaying the four great kindnesses
|
||||
|
||||
### 3. Quality review
|
||||
|
||||
Scan for:
|
||||
- Terminology consistency (same zh term → same en term throughout)
|
||||
- Ellipsis convention — English uses 3 dots `...`, zh may use 6
|
||||
- Buddhist term accuracy
|
||||
- Missing translations
|
||||
- Overly literal renderings
|
||||
|
||||
### 4. Write back with layout fixes
|
||||
|
||||
Run `scripts/build.py strings.yaml original.pptx translated.pptx`.
|
||||
|
||||
The script:
|
||||
- Replaces text in matching paragraphs (clears all runs, sets first run)
|
||||
- Replaces table cell text (using row/col from computed index)
|
||||
- Reduces font size by 18% (`FONT_SCALE = 0.82`) on all translated shapes and tables
|
||||
- Sets `auto_size = TEXT_TO_FIT_SHAPE` on text frames to handle overflow
|
||||
- English text is ~1.3–1.5× longer than Chinese — font shrink + auto-fit handles most cases
|
||||
|
||||
## Alternate scripts
|
||||
|
||||
The absorbed `pptx-translation` skill had alternate script names: `extract_pptx.py` and `build_pptx.py`. These are functionally equivalent to `extract.py` and `build.py` with minor formatting differences (docstrings, variable naming). If the primary scripts fail, the alternates are available in the archive at `~/.hermes/skills/.archive/pptx-translation/scripts/`.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- "run" in the YAML is actually the **paragraph index** within a text frame, not the OOXML text-run index. python-pptx iterates paragraphs, not runs.
|
||||
- Font shrink only applies to runs that have an explicit `font.size` — inherited sizes from paragraph/layout defaults are skipped.
|
||||
- After write-back, verify with `python -m markitdown translated.pptx` to check text landed correctly.
|
||||
- Tables: font shrink is applied per-cell text frame. Each cell is its own text frame.
|
||||
- markitdown may fail with `ModuleNotFoundError: dotenv` — run `pip install python-dotenv` first.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/extract.py` — extract strings from PPTX to YAML
|
||||
- `scripts/build.py` — write translations back with font shrink + auto-fit
|
||||
@@ -0,0 +1,68 @@
|
||||
import sys, yaml
|
||||
from pptx import Presentation
|
||||
from pptx.util import Pt
|
||||
from pptx.enum.text import MSO_AUTO_SIZE
|
||||
|
||||
FONT_SCALE = 0.82 # shrink ~18%
|
||||
|
||||
yaml_path, src_path, out_path = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
|
||||
with open(yaml_path) as f:
|
||||
entries = yaml.safe_load(f)
|
||||
|
||||
index = {}
|
||||
for e in entries:
|
||||
index[(e["slide"], e["shape"], e["run"])] = e["en"]
|
||||
|
||||
|
||||
def shrink_font_tf(tf):
|
||||
for para in tf.paragraphs:
|
||||
for run in para.runs:
|
||||
if run.font.size:
|
||||
run.font.size = Pt(int(run.font.size.pt * FONT_SCALE))
|
||||
try:
|
||||
tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
prs = Presentation(src_path)
|
||||
|
||||
for slide_num, slide in enumerate(prs.slides, 1):
|
||||
for shape_idx, shape in enumerate(slide.shapes):
|
||||
if shape.has_text_frame:
|
||||
has_translation = False
|
||||
for para_idx, para in enumerate(shape.text_frame.paragraphs):
|
||||
key = (slide_num, shape_idx, para_idx)
|
||||
if key in index:
|
||||
has_translation = True
|
||||
en = index[key]
|
||||
for r in para.runs:
|
||||
r.text = ""
|
||||
if para.runs:
|
||||
para.runs[0].text = en
|
||||
else:
|
||||
para.add_run().text = en
|
||||
if has_translation:
|
||||
shrink_font_tf(shape.text_frame)
|
||||
|
||||
elif shape.has_table:
|
||||
num_rows = len(shape.table.rows)
|
||||
for r in range(num_rows * len(shape.table.columns)):
|
||||
key = (slide_num, shape_idx, r)
|
||||
if key in index:
|
||||
row = r % num_rows
|
||||
col = r // num_rows
|
||||
shape.table.cell(row, col).text = index[key]
|
||||
for row in shape.table.rows:
|
||||
for cell in row.cells:
|
||||
shrink_font_tf(cell.text_frame)
|
||||
|
||||
key = (slide_num, -1, 0)
|
||||
if key in index and slide.has_notes_slide:
|
||||
ns = slide.notes_slide
|
||||
ns.notes_text_frame.clear()
|
||||
ns.notes_text_frame.paragraphs[0].add_run().text = index[key]
|
||||
|
||||
prs.save(out_path)
|
||||
print(f"Saved {out_path}")
|
||||
@@ -0,0 +1,68 @@
|
||||
import sys, yaml
|
||||
from pptx import Presentation
|
||||
|
||||
PLACEHOLDER_KINDS = {
|
||||
1: "title", 2: "body", 3: "center_title",
|
||||
4: "subtitle", 5: "body", 6: "body", 7: "body",
|
||||
}
|
||||
|
||||
|
||||
def shape_kind(shape):
|
||||
if shape.has_table:
|
||||
return "table"
|
||||
try:
|
||||
ph = shape.placeholder_format
|
||||
if ph is not None and ph.type is not None:
|
||||
return PLACEHOLDER_KINDS.get(ph.type, "body")
|
||||
except ValueError:
|
||||
pass
|
||||
return "body"
|
||||
|
||||
|
||||
def extract(pptx_path):
|
||||
prs = Presentation(pptx_path)
|
||||
entries = []
|
||||
|
||||
for slide_num, slide in enumerate(prs.slides, 1):
|
||||
for shape_idx, shape in enumerate(slide.shapes):
|
||||
kind = shape_kind(shape)
|
||||
|
||||
if shape.has_text_frame:
|
||||
for para_idx, para in enumerate(shape.text_frame.paragraphs):
|
||||
full = para.text.strip()
|
||||
if not full:
|
||||
continue
|
||||
entries.append({
|
||||
"slide": slide_num, "shape": shape_idx,
|
||||
"run": para_idx, "kind": kind,
|
||||
"zh": full, "en": "",
|
||||
})
|
||||
|
||||
elif shape.has_table:
|
||||
for row_idx, row in enumerate(shape.table.rows):
|
||||
for col_idx, cell in enumerate(row.cells):
|
||||
text = cell.text.strip()
|
||||
if not text:
|
||||
continue
|
||||
entries.append({
|
||||
"slide": slide_num, "shape": shape_idx,
|
||||
"run": len(shape.table.rows) * col_idx + row_idx,
|
||||
"kind": "table", "zh": text, "en": "",
|
||||
})
|
||||
|
||||
if slide.has_notes_slide:
|
||||
notes = slide.notes_slide.notes_text_frame.text.strip()
|
||||
if notes:
|
||||
entries.append({
|
||||
"slide": slide_num, "shape": -1, "run": 0,
|
||||
"kind": "notes", "zh": notes, "en": "",
|
||||
})
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
entries = extract(sys.argv[1])
|
||||
with open(sys.argv[2], "w") as f:
|
||||
yaml.dump(entries, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
||||
print(f"Extracted {len(entries)} entries to {sys.argv[2]}")
|
||||
Reference in New Issue
Block a user