refactor(skills): align MPI skills with Agent Skills best practices
The skill metadata had drifted: every SKILL.md name field lacked the mpi- prefix, contradicting the directory names and the Agent Skills specification. Descriptions were also missing negative triggers, making it easy for the agent to load the wrong skill. Rewrote the pdf-to-docx skill to follow progressive disclosure: the main SKILL.md dropped from 318 lines to 80, with detailed code examples moved to on-demand references. Added uv run instructions and /// script PEP 723 metadata so dependencies are declared inline and installed automatically. Fixed the pptx skill's script paths and added CLI usage messages to both pptx scripts and the normalize script. Removed the empty self-review directory that was superseded by the unified translation-review skill.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: chinese-text-normalize
|
||||
description: Normalize Chinese markdown files — remove extraneous mid-sentence line breaks from fixed-width exports while preserving TOC structures, section headers, and intentional paragraph breaks.
|
||||
name: mpi-chinese-text-normalize
|
||||
description: Normalize Chinese markdown files by removing extraneous mid-sentence line breaks from fixed-width exports while preserving TOC structures, section headers, and intentional paragraph breaks. Do not use for English prose, wiki-link index files, or mixed CJK/English documents without manual review.
|
||||
compatibility: Requires Python 3.9+ and uv. The script uses only the standard library.
|
||||
---
|
||||
|
||||
When Chinese text has hard line breaks at a fixed width (~20-25 chars) — common in PDF exports, OCR output, or poorly-converted documents — use this skill to join them into flowing paragraphs.
|
||||
@@ -13,7 +14,13 @@ When Chinese text has hard line breaks at a fixed width (~20-25 chars) — commo
|
||||
|
||||
## Approach
|
||||
|
||||
Run `scripts/normalize_breaks.py <directory>` — it processes all .md files in the directory.
|
||||
Run with `uv`:
|
||||
|
||||
```bash
|
||||
uv run skills/mpi-chinese-text-normalize/scripts/normalize_breaks.py <directory>
|
||||
```
|
||||
|
||||
It processes all `.md` files in the directory.
|
||||
|
||||
The script handles three file patterns:
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.9"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
"""
|
||||
Fix extraneous line breaks in Chinese markdown files.
|
||||
|
||||
@@ -6,8 +11,9 @@ Three file patterns:
|
||||
2. Mostly-paragraph with stray breaks + outline TOC -> join broken lines, preserve list items
|
||||
3. Already fine -> skip (idempotent)
|
||||
|
||||
Usage: python3 normalize_breaks.py <directory>
|
||||
Usage: uv run normalize_breaks.py <directory>
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -155,6 +161,9 @@ def process_file(filepath):
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: uv run normalize_breaks.py <directory>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
workdir = Path(sys.argv[1])
|
||||
files = sorted(workdir.glob('*.md'))
|
||||
|
||||
|
||||
@@ -1,253 +1,45 @@
|
||||
---
|
||||
name: pdf-to-docx-conversion
|
||||
description: "Convert flowing text PDFs (Chinese or multi-language) to DOCX with proper fonts, styles, native bullets, lists, and embedded images. Preserves visual hierarchy from PDF font/size/color data."
|
||||
version: 1.0.0
|
||||
author: Claude
|
||||
name: mpi-pdf-to-docx-conversion
|
||||
description: Convert flowing text PDFs (Chinese or multi-language) to DOCX with proper fonts, styles, native bullets, lists, and embedded images. Use for text-based PDFs. Do not use for scanned/image PDFs, form-heavy PDFs, or documents where exact page layout must be preserved.
|
||||
license: MIT
|
||||
platforms: [linux, macos, windows]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [PDF, DOCX, Documents, python-docx, pymupdf]
|
||||
compatibility: Requires Python 3.9+ and uv. Dependencies (pymupdf, python-docx) are declared in the script's /// script metadata.
|
||||
---
|
||||
|
||||
# PDF-to-DOCX Conversion
|
||||
|
||||
Convert PDF documents (especially flowing text documents in any language, including CJK) into well-structured DOCX files that preserve fonts, sizes, colors, and layout intent.
|
||||
Convert text-based PDF documents (including CJK) into structured DOCX files that preserve fonts, sizes, colors, and layout intent.
|
||||
|
||||
## Prerequisites
|
||||
## Quick start
|
||||
|
||||
For most PDFs, run the bundled converter with `uv`:
|
||||
|
||||
```bash
|
||||
pip install pymupdf python-docx
|
||||
uv run skills/mpi-pdf-to-docx-conversion/scripts/convert_pdf_to_docx.py input.pdf output.docx
|
||||
```
|
||||
|
||||
`uv` reads the `/// script` metadata block in the script and installs `pymupdf` and `python-docx` automatically.
|
||||
|
||||
For documents with unusual fonts or structure, inspect first and pass a config dict. See `references/config-patterns.md` for the config schema and common patterns.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Inspect the PDF.** Dump fonts, sizes, bullets, and header hierarchy. See `references/inspection-guide.md`.
|
||||
2. **Configure.** Build a config dict matching the PDF's patterns (fonts, bullet fonts, skip fonts, numbered patterns, header sizes). See `references/config-patterns.md`.
|
||||
3. **Convert.** Run `scripts/convert_pdf_to_docx.py` or import `PDFToDOCXConverter` in Python.
|
||||
4. **Verify.** Check paragraph count, styles, bullets, images, and page-number leakage. See the checklist below.
|
||||
|
||||
## Features
|
||||
|
||||
- **Style-aware**: reads actual font, size, bold, color from PDF spans
|
||||
- **Native bullets**: uses Word `List Bullet` style instead of Wingdings glyphs
|
||||
- **Native numbering**: uses numbered list style for sequential items
|
||||
- **Image extraction**: detects and embeds PDF images into the DOCX
|
||||
- **Verse/poetry handling**: splits merged verse lines at semantic boundaries
|
||||
- **Multi-language**: works with CJK, RTL, and mixed-script documents
|
||||
- **Flowing text**: text flows across pages; no forced page breaks
|
||||
- Style-aware extraction (font, size, bold, color)
|
||||
- Native Word bullets and numbering
|
||||
- Image extraction and embedding
|
||||
- Verse/poetry line splitting
|
||||
- Multi-language support (CJK, RTL, mixed scripts)
|
||||
- Flowing text across pages (no forced page breaks)
|
||||
|
||||
## Quick Start
|
||||
## Verification checklist
|
||||
|
||||
```python
|
||||
from pdf_to_docx import convert_pdf_to_docx
|
||||
|
||||
convert_pdf_to_docx("input.pdf", "output.docx")
|
||||
```
|
||||
|
||||
## Step-by-Step Workflow
|
||||
|
||||
### 1. Inspect the PDF
|
||||
|
||||
First, dump the PDF to understand its structure:
|
||||
|
||||
```bash
|
||||
python3 << 'PY'
|
||||
import pymupdf
|
||||
doc = pymupdf.open("input.pdf")
|
||||
for pi in range(len(doc)):
|
||||
page = doc[pi]
|
||||
blocks = page.get_text("dict")["blocks"]
|
||||
for block in blocks:
|
||||
if block["type"] != 0: continue
|
||||
for line in block["lines"]:
|
||||
for span in line["spans"]:
|
||||
bbox = span["bbox"]
|
||||
flags = span["flags"]
|
||||
attrs = []
|
||||
if flags & 2**1: attrs.append("I")
|
||||
if flags & 2**4: attrs.append("B")
|
||||
print(f" Y={bbox[1]:.0f} [{span['size']:.1f}pt {'+'.join(attrs) or '-'}] {span['font']} | {span['text']}")
|
||||
PY
|
||||
```
|
||||
|
||||
Key things to identify:
|
||||
- **Fonts used** (map to DOCX fonts)
|
||||
- **Bullet mechanism** (Wingdings? Unicode?)
|
||||
- **Header hierarchy** (what size = section header vs sub-header)
|
||||
- **Numbered lists** (what delimiter: `1)` `1.` `1)`)
|
||||
- **Images** (check `page.get_images()`)
|
||||
- **Special sections** (tables, verses, forms)
|
||||
|
||||
### 2. Configure the Converter
|
||||
|
||||
Create a config dict matching your PDF's patterns:
|
||||
|
||||
```python
|
||||
config = {
|
||||
"fonts": {
|
||||
"title": "STHeitiSC-Medium",
|
||||
"body": "HYShuSongErKW",
|
||||
"page_number": "HelveticaNeue",
|
||||
},
|
||||
"header_sizes": {"section": 18, "sub": 15},
|
||||
"body_size": 12,
|
||||
"bullet_fonts": ["Wingdings", "Wingdings 2", "Wingdings 3"],
|
||||
"page_number_font": "HelveticaNeue",
|
||||
"numbered_patterns": [r'^\d+\)', r'^\d+\.'], # detect numbered items
|
||||
"skip_fonts": ["HelveticaNeue"], # fonts to skip (page numbers)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Run the Conversion
|
||||
|
||||
```python
|
||||
from pdf_to_docx import PDFToDOCXConverter
|
||||
|
||||
converter = PDFToDOCXConverter(config)
|
||||
converter.convert("input.pdf", "output.docx")
|
||||
```
|
||||
|
||||
## Core Classes
|
||||
|
||||
### PDFToDOCXConverter
|
||||
|
||||
```python
|
||||
class PDFToDOCXConverter:
|
||||
def __init__(self, config=None):
|
||||
self.cfg = config or self._default_config()
|
||||
|
||||
def convert(self, pdf_path: str, docx_path: str):
|
||||
"""Main entry point."""
|
||||
# 1. Extract all spans
|
||||
# 2. Merge Wingdings bullets with body text
|
||||
# 3. Group lines into paragraphs by Y-gap and style changes
|
||||
# 4. Post-process: split compact lists, verses, merged steps
|
||||
# 5. Build DOCX with proper styles
|
||||
# 6. Embed images
|
||||
pass
|
||||
|
||||
def extract_spans(self, pdf_path: str) -> list[dict]:
|
||||
"""Extract all text spans with full style info."""
|
||||
doc = pymupdf.open(pdf_path)
|
||||
raw = []
|
||||
for pi in range(len(doc)):
|
||||
for block in doc[pi].get_text("dict")["blocks"]:
|
||||
if block["type"] != 0: continue
|
||||
for line in block["lines"]:
|
||||
for span in line["spans"]:
|
||||
raw.append({
|
||||
"text": span["text"], "font": span["font"],
|
||||
"size": span["size"], "flags": span["flags"],
|
||||
"color": span["color"], "bbox": span["bbox"],
|
||||
})
|
||||
return raw
|
||||
|
||||
def detect_bullets(self, line: list) -> bool:
|
||||
"""Check if first span in line is a Wingdings bullet."""
|
||||
return "Wingdings" in line[0]["font"]
|
||||
|
||||
def group_paragraphs(self, lines: list) -> list[dict]:
|
||||
"""Group raw lines into logical paragraphs."""
|
||||
# Group by Y-gap threshold (typically 20-30pt)
|
||||
# Break on style change (font change, size > threshold, bold toggle)
|
||||
# Break on attribution lines (——节选自...)
|
||||
# Break on special fonts (STHeitiSC-Light etc.)
|
||||
pass
|
||||
|
||||
def post_process(self, paras: list) -> list[dict]:
|
||||
"""Split merged compact lists and verses."""
|
||||
# See examples below for common patterns
|
||||
pass
|
||||
```
|
||||
|
||||
## Common Post-Processing Patterns
|
||||
|
||||
### Compact Numbered Lists
|
||||
|
||||
When the PDF flows list items together in one paragraph:
|
||||
|
||||
```python
|
||||
def split_compact_list(text: str) -> list[str]:
|
||||
"""Split '1) foo 2) bar 3) baz' into separate items."""
|
||||
parts = re.split(r'(?=\d+\))', text)
|
||||
return [p for p in parts if p.strip()]
|
||||
```
|
||||
|
||||
### Verse / Poetry Lines
|
||||
|
||||
When the PDF merges verse lines that should be on separate lines:
|
||||
|
||||
```python
|
||||
def split_verse(text: str, split_markers: list[str]) -> list[str]:
|
||||
"""Split verse at semantic phrase boundaries.
|
||||
Example markers: ['感恩', '愿', '更愿']"""
|
||||
pattern = '|'.join(f'(?={m})' for m in split_markers)
|
||||
return [p for p in re.split(pattern, text) if p.strip()]
|
||||
```
|
||||
|
||||
### 小组交流流程 / Process Steps
|
||||
|
||||
Split Chinese process steps numbered 一、二、三、etc.:
|
||||
|
||||
```python
|
||||
def split_chinese_steps(text: str) -> list[str]:
|
||||
"""Split '一、foo 二、bar' into separate items."""
|
||||
parts = re.split(r'(?=[一二三四五六七八九十]、)', text)
|
||||
return [p.strip() for p in parts if p.strip()]
|
||||
```
|
||||
|
||||
## DOCX Construction
|
||||
|
||||
### Font Setup (East Asian fonts)
|
||||
|
||||
```python
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
|
||||
def set_east_asian_font(run, name: str):
|
||||
"""Set CJK font properly in python-docx."""
|
||||
rPr = run._element.get_or_add_rPr()
|
||||
rFonts = rPr.find(qn('w:rFonts'))
|
||||
if rFonts is None:
|
||||
rFonts = OxmlElement('w:rFonts')
|
||||
rPr.insert(0, rFonts)
|
||||
rFonts.set(qn('w:eastAsia'), name)
|
||||
rFonts.set(qn('w:ascii'), name)
|
||||
rFonts.set(qn('w:hAnsi'), name)
|
||||
```
|
||||
|
||||
### Bullet Items
|
||||
|
||||
Use native Word bullets, NOT Wingdings characters:
|
||||
|
||||
```python
|
||||
p = doc.add_paragraph(style='List Bullet')
|
||||
p.clear()
|
||||
run = p.add_run("Your bullet text here")
|
||||
set_east_asian_font(run, font_name)
|
||||
```
|
||||
|
||||
### Image Embedding
|
||||
|
||||
```python
|
||||
from docx.shared import Inches
|
||||
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = p.add_run()
|
||||
run.add_picture(image_path, width=Inches(3.5))
|
||||
```
|
||||
|
||||
Extract images from PDF first:
|
||||
|
||||
```python
|
||||
doc = pymupdf.open("input.pdf")
|
||||
for pi in range(len(doc)):
|
||||
images = doc[pi].get_images()
|
||||
for idx, img in enumerate(images):
|
||||
xref = img[0]
|
||||
base = doc.extract_image(xref)
|
||||
with open(f"extracted_{pi}_{idx}.{base['ext']}", 'wb') as f:
|
||||
f.write(base['image'])
|
||||
```
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After conversion, verify the DOCX:
|
||||
After conversion, inspect the DOCX:
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
@@ -256,67 +48,33 @@ doc = Document('output.docx')
|
||||
print(f'Paragraphs: {len(doc.paragraphs)}')
|
||||
for i, p in enumerate(doc.paragraphs):
|
||||
style = p.style.name if p.style else '-'
|
||||
txt = p.text[:80]
|
||||
print(f'[{i:2d}] [{style:15s}] {txt}')
|
||||
print(f'[{i:2d}] [{style:15s}] {p.text[:80]}')
|
||||
"
|
||||
```
|
||||
|
||||
Check for:
|
||||
1. [ ] All sections present (count paragraphs)
|
||||
1. [ ] All sections present (paragraph count matches expectations)
|
||||
2. [ ] No merged verses or lists
|
||||
3. [ ] Headers are bold + larger size
|
||||
4. [ ] Bullets use `List Bullet` style
|
||||
3. [ ] Headers are bold and larger than body text
|
||||
4. [ ] Bullets use the `List Bullet` style
|
||||
5. [ ] Numbered items are separate paragraphs
|
||||
6. [ ] Images present in `word/media/`
|
||||
7. [ ] Attribution lines right-aligned/indented
|
||||
8. [ ] No page numbers leaked into body
|
||||
6. [ ] Images are embedded in `word/media/`
|
||||
7. [ ] Attribution lines are right-aligned or indented
|
||||
8. [ ] Page numbers are not leaked into body text
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Text over-merged | Y-gap threshold too high | Lower gap threshold (e.g. 20 → 15) |
|
||||
| Missing sections | Skipped by font filter | Add font to skip_fonts or remove filter |
|
||||
| Over-split lines | Y-gap threshold too low | Raise gap threshold (e.g. 20 → 30) |
|
||||
| Wingdings boxes | Unicode bullet inserted | Use `style='List Bullet'` instead |
|
||||
| CJK font wrong | East Asian font not set | Use `set_east_asian_font()` helper |
|
||||
| Image missing | Not extracted before DOCX build | Run `extract_images()` first |
|
||||
| Verse mangled | Regex too aggressive | Tune verse splitting pattern |
|
||||
If output is wrong, see `references/troubleshooting.md` for a full symptom/cause/fix table. Common first checks:
|
||||
|
||||
## Full Example Script
|
||||
- Over-merged text → lower the Y-gap threshold.
|
||||
- Over-split lines → raise the Y-gap threshold.
|
||||
- Wingdings boxes → use native `List Bullet` style instead.
|
||||
- Missing images → ensure images are extracted before DOCX construction.
|
||||
|
||||
See `scripts/convert_pdf_to_docx.py` for a production-ready converter with all patterns pre-configured.
|
||||
## References
|
||||
|
||||
```python
|
||||
# scripts/convert_pdf_to_docx.py
|
||||
import pymupdf, re
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Cm, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
|
||||
def convert_pdf_to_docx(pdf_path: str, docx_path: str, config: dict = None):
|
||||
"""Convert a flowing text PDF to DOCX."""
|
||||
cfg = config or {}
|
||||
|
||||
# Extract
|
||||
doc_pdf = pymupdf.open(pdf_path)
|
||||
raw = []
|
||||
for pi in range(len(doc_pdf)):
|
||||
for block in doc_pdf[pi].get_text("dict")["blocks"]:
|
||||
if block["type"] != 0: continue
|
||||
for line in block["lines"]:
|
||||
ss = line["spans"]
|
||||
is_b = any("Wingdings" in s["font"] for s in ss[:1])
|
||||
text = "".join(s["text"] for s in ss)
|
||||
dom = ss[1] if (is_b and len(ss) > 1) else ss[0]
|
||||
raw.append(dict(text=text, font=dom["font"], size=dom["size"],
|
||||
bold=bool(dom["flags"] & 2**4), color=dom["color"],
|
||||
x=dom["bbox"][0], y=dom["bbox"][1], is_bullet=is_b))
|
||||
|
||||
# ... (merge bullets, group paragraphs, post-process, build DOCX)
|
||||
|
||||
# Run:
|
||||
# python scripts/convert_pdf_to_docx.py input.pdf output.docx
|
||||
```
|
||||
- `references/inspection-guide.md` — dump PDF structure and interpret spans
|
||||
- `references/config-patterns.md` — config dict, compact lists, verses, numbered steps
|
||||
- `references/docx-construction.md` — East Asian fonts, bullets, image embedding
|
||||
- `references/troubleshooting.md` — symptom/cause/fix table
|
||||
- `scripts/convert_pdf_to_docx.py` — production-ready converter
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# PDF-to-DOCX Config Patterns
|
||||
|
||||
The converter accepts a config dict that maps PDF-specific patterns to DOCX behavior.
|
||||
|
||||
## Example config
|
||||
|
||||
```python
|
||||
config = {
|
||||
"fonts": {
|
||||
"title": "STHeitiSC-Medium",
|
||||
"body": "HYShuSongErKW",
|
||||
"page_number": "HelveticaNeue",
|
||||
},
|
||||
"header_sizes": {"section": 18, "sub": 15},
|
||||
"body_size": 12,
|
||||
"bullet_fonts": ["Wingdings", "Wingdings 2", "Wingdings 3"],
|
||||
"page_number_font": "HelveticaNeue",
|
||||
"numbered_patterns": [r'^\d+\)', r'^\d+\.'],
|
||||
"skip_fonts": ["HelveticaNeue"],
|
||||
}
|
||||
```
|
||||
|
||||
## Common post-processing patterns
|
||||
|
||||
### Compact numbered lists
|
||||
|
||||
Split items that flowed together in one PDF paragraph:
|
||||
|
||||
```python
|
||||
def split_compact_list(text: str) -> list[str]:
|
||||
parts = re.split(r'(?=\d+\))', text)
|
||||
return [p for p in parts if p.strip()]
|
||||
```
|
||||
|
||||
### Verse / poetry lines
|
||||
|
||||
Split merged verse at semantic phrase boundaries:
|
||||
|
||||
```python
|
||||
def split_verse(text: str, split_markers: list[str]) -> list[str]:
|
||||
pattern = '|'.join(f'(?={m})' for m in split_markers)
|
||||
return [p for p in re.split(pattern, text) if p.strip()]
|
||||
```
|
||||
|
||||
### Chinese process steps
|
||||
|
||||
Split `一、foo 二、bar` into separate items:
|
||||
|
||||
```python
|
||||
def split_chinese_steps(text: str) -> list[str]:
|
||||
parts = re.split(r'(?=[一二三四五六七八九十]、)', text)
|
||||
return [p.strip() for p in parts if p.strip()]
|
||||
```
|
||||
|
||||
## Tuning thresholds
|
||||
|
||||
- **Y-gap threshold** (typically 20–30 pt): controls how aggressively lines are grouped into paragraphs.
|
||||
- **Style-change threshold**: break paragraphs on font change, size jump, or bold toggle when the difference exceeds this value.
|
||||
|
||||
See `references/troubleshooting.md` for threshold adjustment guidance.
|
||||
@@ -0,0 +1,59 @@
|
||||
# DOCX Construction Notes
|
||||
|
||||
Low-level notes for building the DOCX output with python-docx.
|
||||
|
||||
## East Asian fonts
|
||||
|
||||
Set CJK fonts properly on a run:
|
||||
|
||||
```python
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
|
||||
def set_east_asian_font(run, name: str):
|
||||
rPr = run._element.get_or_add_rPr()
|
||||
rFonts = rPr.find(qn('w:rFonts'))
|
||||
if rFonts is None:
|
||||
rFonts = OxmlElement('w:rFonts')
|
||||
rPr.insert(0, rFonts)
|
||||
rFonts.set(qn('w:eastAsia'), name)
|
||||
rFonts.set(qn('w:ascii'), name)
|
||||
rFonts.set(qn('w:hAnsi'), name)
|
||||
```
|
||||
|
||||
## Native bullets
|
||||
|
||||
Use Word's `List Bullet` style, not Wingdings characters:
|
||||
|
||||
```python
|
||||
p = doc.add_paragraph(style='List Bullet')
|
||||
p.clear()
|
||||
run = p.add_run("Bullet text")
|
||||
set_east_asian_font(run, font_name)
|
||||
```
|
||||
|
||||
## Image embedding
|
||||
|
||||
Extract images from the PDF first, then embed them:
|
||||
|
||||
```python
|
||||
from docx.shared import Inches
|
||||
|
||||
# Extract
|
||||
doc = pymupdf.open("input.pdf")
|
||||
for pi in range(len(doc)):
|
||||
for idx, img in enumerate(doc[pi].get_images()):
|
||||
xref = img[0]
|
||||
base = doc.extract_image(xref)
|
||||
path = f"extracted_{pi}_{idx}.{base['ext']}"
|
||||
with open(path, 'wb') as f:
|
||||
f.write(base['image'])
|
||||
|
||||
# Embed
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = p.add_run()
|
||||
run.add_picture(image_path, width=Inches(3.5))
|
||||
```
|
||||
|
||||
See `scripts/convert_pdf_to_docx.py` for the production-ready implementation.
|
||||
@@ -0,0 +1,42 @@
|
||||
# PDF Inspection Guide
|
||||
|
||||
Before converting a PDF, dump its text spans to understand fonts, sizes, bullets, and structure.
|
||||
|
||||
## Dump spans
|
||||
|
||||
```bash
|
||||
python3 << 'PY'
|
||||
import pymupdf
|
||||
doc = pymupdf.open("input.pdf")
|
||||
for pi in range(len(doc)):
|
||||
page = doc[pi]
|
||||
blocks = page.get_text("dict")["blocks"]
|
||||
for block in blocks:
|
||||
if block["type"] != 0: continue
|
||||
for line in block["lines"]:
|
||||
for span in line["spans"]:
|
||||
bbox = span["bbox"]
|
||||
flags = span["flags"]
|
||||
attrs = []
|
||||
if flags & 2**1: attrs.append("I")
|
||||
if flags & 2**4: attrs.append("B")
|
||||
print(f" Y={bbox[1]:.0f} [{span['size']:.1f}pt {'+'.join(attrs) or '-'}] {span['font']} | {span['text']}")
|
||||
PY
|
||||
```
|
||||
|
||||
## What to identify
|
||||
|
||||
- **Fonts used** — map PDF font names to DOCX font names.
|
||||
- **Bullet mechanism** — Wingdings glyphs, Unicode bullets, or something else.
|
||||
- **Header hierarchy** — which font size marks section vs. sub-section headers.
|
||||
- **Numbered lists** — delimiter style: `1)`, `1.`, `1)`, `一、`.
|
||||
- **Images** — check `page.get_images()` on each page.
|
||||
- **Special sections** — tables, verses, attribution lines, page numbers, forms.
|
||||
|
||||
## Page numbers
|
||||
|
||||
Page-number fonts are usually small and repeated on every page. Add them to `skip_fonts` in the config so they do not leak into body text.
|
||||
|
||||
## Attribution lines
|
||||
|
||||
Lines like `——节选自...` often use a different font or indentation. The converter breaks paragraphs on these; verify the break point after conversion.
|
||||
@@ -0,0 +1,18 @@
|
||||
# PDF-to-DOCX Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Text over-merged | Y-gap threshold too high | Lower gap threshold (e.g. 20 → 15) |
|
||||
| Missing sections | Skipped by font filter | Add font to `skip_fonts` or remove filter |
|
||||
| Over-split lines | Y-gap threshold too low | Raise gap threshold (e.g. 20 → 30) |
|
||||
| Wingdings boxes | Unicode bullet inserted | Use `style='List Bullet'` instead |
|
||||
| CJK font wrong | East Asian font not set | Use `set_east_asian_font()` helper |
|
||||
| Image missing | Not extracted before DOCX build | Run image extraction first |
|
||||
| Verse mangled | Regex too aggressive | Tune verse splitting pattern |
|
||||
|
||||
## General debugging steps
|
||||
|
||||
1. Re-inspect the PDF with the span dump from `references/inspection-guide.md`.
|
||||
2. Compare the config against the actual fonts and sizes in the dump.
|
||||
3. Run the verification script in `SKILL.md` and check paragraph styles.
|
||||
4. Adjust one threshold at a time and re-convert.
|
||||
@@ -1,13 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.9"
|
||||
# dependencies = [
|
||||
# "pymupdf",
|
||||
# "python-docx",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
"""
|
||||
Production-ready PDF-to-DOCX converter.
|
||||
Handles multi-language text, mixed fonts, bullets, numbered lists, verses,
|
||||
attributions, images, and flowing text across pages.
|
||||
|
||||
Usage:
|
||||
python convert_pdf_to_docx.py input.pdf output.docx
|
||||
uv run convert_pdf_to_docx.py input.pdf output.docx
|
||||
|
||||
Requires: pip install pymupdf python-docx
|
||||
Requires: uv (dependencies are declared in the /// script block above)
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -544,6 +552,6 @@ def convert_pdf_to_docx(pdf_path: str, docx_path: str, config: dict = None):
|
||||
# CLI
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: python convert_pdf_to_docx.py <input.pdf> <output.docx>")
|
||||
print("Usage: uv run convert_pdf_to_docx.py <input.pdf> <output.docx>")
|
||||
sys.exit(1)
|
||||
convert_pdf_to_docx(sys.argv[1], sys.argv[2])
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
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.
|
||||
name: mpi-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. Use only for .pptx files. Do not use for .ppt, Google Slides exports, or PDFs.
|
||||
category: productivity
|
||||
compatibility: Requires Python 3.9+ and uv. Dependencies (python-pptx, pyyaml) are declared in the scripts' /// script metadata.
|
||||
---
|
||||
|
||||
# PPTX Translation
|
||||
@@ -12,7 +13,13 @@ Translate `.pptx` files between Chinese and English. Covers the full pipeline: e
|
||||
|
||||
### 1. Extract strings to YAML
|
||||
|
||||
Run `toolkit/scripts/extract.py original.pptx strings.yaml`. Produces YAML with entries:
|
||||
Run with `uv`:
|
||||
|
||||
```bash
|
||||
uv run skills/mpi-pptx-translate/scripts/extract.py original.pptx strings.yaml
|
||||
```
|
||||
|
||||
`uv` reads the `/// script` metadata block and installs `python-pptx` and `pyyaml` automatically. Produces YAML with entries:
|
||||
|
||||
```yaml
|
||||
- slide: 1
|
||||
@@ -57,7 +64,9 @@ Scan for:
|
||||
|
||||
### 4. Write back with layout fixes
|
||||
|
||||
Run `toolkit/scripts/build.py strings.yaml original.pptx translated.pptx`.
|
||||
```bash
|
||||
uv run skills/mpi-pptx-translate/scripts/build.py strings.yaml original.pptx translated.pptx
|
||||
```
|
||||
|
||||
The script:
|
||||
- Replaces text in matching paragraphs (clears all runs, sets first run)
|
||||
@@ -80,5 +89,5 @@ The absorbed `pptx-translation` skill had alternate script names: `extract_pptx.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `toolkit/scripts/extract.py` — extract strings from PPTX to YAML
|
||||
- `toolkit/scripts/build.py` — write translations back with font shrink + auto-fit
|
||||
- `uv run skills/mpi-pptx-translate/scripts/extract.py` — extract strings from PPTX to YAML
|
||||
- `uv run skills/mpi-pptx-translate/scripts/build.py` — write translations back with font shrink + auto-fit
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.9"
|
||||
# dependencies = [
|
||||
# "python-pptx",
|
||||
# "pyyaml",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import sys, yaml
|
||||
from pptx import Presentation
|
||||
from pptx.util import Pt
|
||||
@@ -5,15 +13,6 @@ 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:
|
||||
@@ -26,6 +25,14 @@ def shrink_font_tf(tf):
|
||||
pass
|
||||
|
||||
|
||||
def build(yaml_path, src_path, out_path):
|
||||
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"]
|
||||
|
||||
prs = Presentation(src_path)
|
||||
|
||||
for slide_num, slide in enumerate(prs.slides, 1):
|
||||
@@ -66,3 +73,10 @@ for slide_num, slide in enumerate(prs.slides, 1):
|
||||
|
||||
prs.save(out_path)
|
||||
print(f"Saved {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: uv run build.py <strings.yaml> <input.pptx> <output.pptx>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
build(sys.argv[1], sys.argv[2], sys.argv[3])
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.9"
|
||||
# dependencies = [
|
||||
# "python-pptx",
|
||||
# "pyyaml",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import sys, yaml
|
||||
from pptx import Presentation
|
||||
|
||||
@@ -62,6 +70,9 @@ def extract(pptx_path):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: uv run extract.py <input.pptx> <output.yaml>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
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)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
name: terms-search
|
||||
description: Full-text search across the MPI term database. Use when translating or looking up Chinese-English Buddhist/MPI terminology.
|
||||
name: mpi-terms-search
|
||||
description: Full-text search across the MPI Buddhist/Dharma term database. Use when translating or reviewing Chinese-English Buddhist terminology. Do not use for general Chinese-English dictionary lookup outside MPI conventions.
|
||||
category: research
|
||||
compatibility: Requires Python 3; SQLite database is bundled.
|
||||
---
|
||||
|
||||
# Terms Search
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
---
|
||||
name: translation-review
|
||||
name: mpi-translation-review
|
||||
description: |
|
||||
Unified review skill for Chinese-English Buddhist/Dharma translations.
|
||||
Supports two modes:
|
||||
- **self**: You translated the text. Edit target.dj directly.
|
||||
- **other**: Someone else translated. Write review-comments.dj, do not edit target.dj.
|
||||
What to review is identical in both modes — the same detection rules,
|
||||
editorial standards, and terminology checks. Only the action differs: self
|
||||
mode applies fixes directly; other mode records them for the translator.
|
||||
Unified review skill for Chinese-English Buddhist/Dharma translations in djot format.
|
||||
Use in self mode to edit your own target.dj, or in other mode to write review-comments.dj for a peer translator.
|
||||
Do not use for non-djot formats or for non-Buddhist texts.
|
||||
---
|
||||
|
||||
# Translation Review (unified)
|
||||
|
||||
@@ -17,9 +17,12 @@ correspondence, generate `bilingual.dj` directly from the DOCX:
|
||||
4. Write bilingual.dj
|
||||
The DOCX English is the authoritative target text. No PDF needed.
|
||||
|
||||
**Extraction approach**: start by adapting `toolkit/scripts/gen-bilingual-docx.py`.
|
||||
For articles where the body has strict CN→EN→CN→EN alternation, the simple
|
||||
extraction in that script (CN line, blank, EN line, blank) works directly.
|
||||
**Extraction approach**: write a custom extraction script. The standard
|
||||
`toolkit/scripts/gen-bilingual.py` expects separate `source.dj` and `target.dj` files;
|
||||
for DOCX→bilingual extraction, adapt its pattern-matching logic to read from the
|
||||
pandoc plain-text output instead. For articles where the body has strict
|
||||
CN→EN→CN→EN alternation, the simple extraction (CN line, blank, EN line, blank)
|
||||
works directly.
|
||||
|
||||
### A2. Bilingual from `.docx.md` (pandoc markdown output)
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
---
|
||||
name: translation
|
||||
description: Translate Chinese↔English Buddhist/Dharma content — register guidance from Mindfulness Bell corpus, tone, voice, cultural bridging technique.
|
||||
name: mpi-translation
|
||||
description: Translate Chinese↔English Buddhist/Dharma content. Use for oral talks, written articles, guided meditations, Q&A, and sutra commentary. Do not use for non-religious general Chinese-English translation, technical documentation, or marketing copy.
|
||||
inputs: source.dj (Chinese djot), or .docx via docx2dj.fish
|
||||
outputs: target.dj (English djot), bilingual.dj, edit-suggestions.dj
|
||||
compatibility: Requires pandoc for docx-to-djot conversion.
|
||||
---
|
||||
|
||||
# Translation
|
||||
@@ -12,7 +13,7 @@ terms DB query, workflows, output format) are in AGENTS.md.
|
||||
|
||||
## Source context
|
||||
|
||||
Before translating, understand the source's format and delivery context. Is it a transcript of an oral talk, a book excerpt, a guided meditation script, a Q&A, a written article, or another genre? The register shapes the translation. If the context is not clear from the file path or source content, ask the user before proceeding.
|
||||
Before translating, understand the source's format and delivery context. Is it a transcript of an oral talk, a book excerpt, a guided meditation script, a Q&A, a written article, or another genre? The register shapes the translation. If the context is not clear from the file path or source content, ask the user before proceeding. If you cannot identify the author, write the target text with the style of 济群法师.
|
||||
|
||||
## Mindfulness Bell Corpus
|
||||
|
||||
|
||||
@@ -14,6 +14,20 @@ skills:
|
||||
|
||||
{% Replace `/path/to/mpi` with the absolute path to this repository. Edit config.yaml directly — `hermes config set` stores list values as strings. %}
|
||||
|
||||
## Structure
|
||||
|
||||
Each skill follows the Agent Skills layout:
|
||||
|
||||
```
|
||||
skill-name/
|
||||
├── SKILL.md # frontmatter + core instructions
|
||||
├── scripts/ # tiny deterministic CLIs
|
||||
├── references/ # docs loaded on demand
|
||||
└── assets/ # templates and static files
|
||||
```
|
||||
|
||||
The `name` field in each `SKILL.md` frontmatter matches the directory name.
|
||||
|
||||
## Skills
|
||||
|
||||
| Name | What it does |
|
||||
|
||||
Reference in New Issue
Block a user