From 52c0f83376e9ccdcd99e826a38be618773347dbb Mon Sep 17 00:00:00 2001 From: iacore Date: Fri, 10 Jul 2026 21:34:07 +0800 Subject: [PATCH] Refactor codebase --- .gitmodules | 3 + .omp/APPEND_SYSTEM.md | 7 - AGENTS.md | 195 +------ other/omp-translation-guide/manual.dj | 513 ++++++++++++++++ other/omp-translation-guide/presentation.typ | 249 ++++++++ readme.dj | 11 +- references/google-docs-comment-journey.dj | 133 +++++ references/guide/04 翻译常用工具/常见工具.doc | Bin 0 -> 15872 bytes references/thoughts-on-ai.dj | 7 + references/typst-template-design.md | 106 ++++ scripts/compile-typst.fish | 20 - scripts/dj2docx.fish | 14 - scripts/docx2dj.fish | 13 - .../gen-bilingual-buddhist-attitude-185a23.py | 130 ----- scripts/gen-bilingual-buddhist-attitude.py | 262 --------- .../gen-bilingual-dharma-management-e2bc35.py | 193 ------ scripts/gen-bilingual-life-design-de8422.py | 183 ------ scripts/gen-bilingual.py | 60 -- scripts/proofread-pdf.py | 169 ------ scripts/split-bilingual.fish | 22 - scripts/ten-elements-c7fcd9.py | 97 ---- skills/chinese-text-normalize/SKILL.md | 85 --- .../references/bold-fragments.md | 110 ---- .../scripts/normalize_breaks.py | 168 ------ skills/pdf-to-docx-conversion/SKILL.md | 322 ---------- .../scripts/convert_pdf_to_docx.py | 549 ------------------ .../templates/chinese-study-config.json | 31 - skills/pptx-translate/SKILL.md | 84 --- skills/pptx-translate/scripts/build.py | 68 --- skills/pptx-translate/scripts/extract.py | 68 --- skills/readme.dj | 26 - skills/terms-search/SKILL.md | 88 --- .../references/termbase-rebuild.md | 127 ---- .../references/translation-alignment.md | 58 -- .../references/buddhist-terminology.md | 132 ----- .../references/common-issues-taxonomy.md | 174 ------ .../references/deliberation-protocol.md | 90 --- .../references/docx-md-extraction.md | 171 ------ .../edit-suggestions-in-bilingual.md | 65 --- .../references/proofreading-patterns.md | 116 ---- .../references/terms-db-alignment.md | 64 -- .../references/translation-pitfalls.md | 441 -------------- skills/translation/SKILL.md | 187 ------ .../references/bilingual-format.md | 41 -- .../references/diacritics-convention.md | 13 - .../references/markdown-to-djot.md | 65 --- .../references/meditation-translation.md | 43 -- .../references/proofread-pdf-workflow.md | 50 -- terms-search/.codegraph/.gitignore | 16 - terms-search/.dockerignore | 4 - terms-search/.gitignore | 2 - terms-search/Dockerfile | 11 - terms-search/deploy | 2 - terms-search/fly.toml | 20 - terms-search/search.py | 124 ---- terms-search/server.py | 73 --- terms-search/templates/index.html | 30 - terms-search/templates/sources.html | 19 - todo | 19 + toolkit | 1 + .../用小读点亮心灯/alignment-report.dj | 2 +- 61 files changed, 1039 insertions(+), 5107 deletions(-) create mode 100644 .gitmodules delete mode 100644 .omp/APPEND_SYSTEM.md create mode 100644 other/omp-translation-guide/manual.dj create mode 100644 other/omp-translation-guide/presentation.typ create mode 100644 references/google-docs-comment-journey.dj create mode 100644 references/guide/04 翻译常用工具/常见工具.doc create mode 100644 references/thoughts-on-ai.dj create mode 100644 references/typst-template-design.md delete mode 100755 scripts/compile-typst.fish delete mode 100755 scripts/dj2docx.fish delete mode 100755 scripts/docx2dj.fish delete mode 100644 scripts/gen-bilingual-buddhist-attitude-185a23.py delete mode 100644 scripts/gen-bilingual-buddhist-attitude.py delete mode 100644 scripts/gen-bilingual-dharma-management-e2bc35.py delete mode 100644 scripts/gen-bilingual-life-design-de8422.py delete mode 100755 scripts/gen-bilingual.py delete mode 100644 scripts/proofread-pdf.py delete mode 100755 scripts/split-bilingual.fish delete mode 100644 scripts/ten-elements-c7fcd9.py delete mode 100644 skills/chinese-text-normalize/SKILL.md delete mode 100644 skills/chinese-text-normalize/references/bold-fragments.md delete mode 100644 skills/chinese-text-normalize/scripts/normalize_breaks.py delete mode 100644 skills/pdf-to-docx-conversion/SKILL.md delete mode 100644 skills/pdf-to-docx-conversion/scripts/convert_pdf_to_docx.py delete mode 100644 skills/pdf-to-docx-conversion/templates/chinese-study-config.json delete mode 100644 skills/pptx-translate/SKILL.md delete mode 100644 skills/pptx-translate/scripts/build.py delete mode 100644 skills/pptx-translate/scripts/extract.py delete mode 100644 skills/readme.dj delete mode 100644 skills/terms-search/SKILL.md delete mode 100644 skills/terms-search/references/termbase-rebuild.md delete mode 100644 skills/terms-search/references/translation-alignment.md delete mode 100644 skills/translation-review/references/buddhist-terminology.md delete mode 100644 skills/translation-review/references/common-issues-taxonomy.md delete mode 100644 skills/translation-review/references/deliberation-protocol.md delete mode 100644 skills/translation-review/references/docx-md-extraction.md delete mode 100644 skills/translation-review/references/edit-suggestions-in-bilingual.md delete mode 100644 skills/translation-review/references/proofreading-patterns.md delete mode 100644 skills/translation-review/references/terms-db-alignment.md delete mode 100644 skills/translation-review/references/translation-pitfalls.md delete mode 100644 skills/translation/SKILL.md delete mode 100644 skills/translation/references/bilingual-format.md delete mode 100644 skills/translation/references/diacritics-convention.md delete mode 100644 skills/translation/references/markdown-to-djot.md delete mode 100644 skills/translation/references/meditation-translation.md delete mode 100644 skills/translation/references/proofread-pdf-workflow.md delete mode 100644 terms-search/.codegraph/.gitignore delete mode 100644 terms-search/.dockerignore delete mode 100644 terms-search/.gitignore delete mode 100644 terms-search/Dockerfile delete mode 100755 terms-search/deploy delete mode 100644 terms-search/fly.toml delete mode 100755 terms-search/search.py delete mode 100644 terms-search/server.py delete mode 100644 terms-search/templates/index.html delete mode 100644 terms-search/templates/sources.html create mode 100644 todo create mode 160000 toolkit diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..913c95f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "toolkit"] + path = toolkit + url = git@git.sr.ht:~iacore/mpi-translation-toolkit diff --git a/.omp/APPEND_SYSTEM.md b/.omp/APPEND_SYSTEM.md deleted file mode 100644 index f606d92..0000000 --- a/.omp/APPEND_SYSTEM.md +++ /dev/null @@ -1,7 +0,0 @@ -# MPI Project Instructions - -You are working on the Mindful Peace International Chinese-English Buddhist/Dharma translation project. -- Before translating, load the `translation` and `terms-search` skills. -- Before reviewing, load `self-review` (and `other-review` for peer review). -- The agent IS the model: do not call external translation APIs. -- The workflow is not as rigid as the state machine in AGENTS.md. The user may ask for you to deviate from the default workflow (described by the state machine). Be flexible when asked. diff --git a/AGENTS.md b/AGENTS.md index ad3ecc3..4bd262a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,196 +1,3 @@ # MPI Project Conventions -## Skills - -Skills in `skills/`. - -Available: `translation`, `terms-search`, `translation-review`, `chinese-text-normalize`, `pptx-translate`, `pdf-to-docx-conversion`. - -## Terms Database - -See `terms-search` skill. Quick reference: -- CLI: `terms-search/search.py [limit]` -- Module: `from search import search; search("空性", limit=5, src="DoT定稿")` -- Priority: DoT定稿 > 内部特色词 > 佛教术语 > 经论名 - -## Directory Structure - -``` -translate-files//
/ - source.dj — Chinese original - target.dj — English translation (line count matches source) - bilingual.dj — interleaved (source line, blank, target line, blank) - Generated by `../../scripts/gen-bilingual.py source.dj target.dj`; - do not edit or commit. -``` - -Put `.docx` output in `/tmp/`. Don't commit binaries. -Generated files (`bilingual.dj`) are not committed either. - ---- - -## Translation State Machine - -All translation work follows this deterministic workflow. Non-deterministic LLM work (drafting, reviewing) happens at the edges; the states and transitions are fixed. - -```mermaid -stateDiagram-v2 - [*] --> idle - idle --> translating: SOURCE_LOADED - idle --> other_reviewing: BILINGUAL_LOADED - translating --> bilingual_ready: TRANSLATION_DRAFTED - bilingual_ready --> self_reviewing: BILINGUAL_GENERATED - self_reviewing --> translating: SELF_REJECTED - self_reviewing --> other_reviewing: SELF_APPROVED [peer review required] - self_reviewing --> approved: SELF_APPROVED [no peer review] - note right of self_reviewing - peer_review_required flag decides the branch - end note - other_reviewing --> translating: PEER_REJECTED - other_reviewing --> approved: PEER_APPROVED - approved --> typesetting: TYPESET_REQUESTED - approved --> done: COMPLETE - typesetting --> done: TYPESET_COMPLETE - done --> [*] -``` - -States: - -| State | Meaning | Output artifact | -|---|---|---| -| `idle` | Waiting for source or an existing bilingual file. | — | -| `translating` | Agent loads `translation` + `terms-search` skills and drafts `target.dj`. | `target.dj` | -| `bilingual_ready` | `bilingual.dj` generated from `source.dj` + `target.dj`. | `bilingual.dj` | -| `self_reviewing` | Self-review with unified ruleset (self mode). Edit target.dj. | `target.dj` (edited) | -| `other_reviewing` | Peer review with unified ruleset (other mode). Write review-comments.dj. | `review-comments.dj` | -| `approved` | Translation accepted. May typeset or finish. | — | -| `typesetting` | Producing PDF/DOCX from approved bilingual content. | `.pdf` / `.docx` | -| `done` | Complete. | — | - -## Workflow A: Translation(翻译) - -Translate Chinese source into English. The agent IS the model — no external APIs. This workflow covers the state machine path `idle` → `translating` → `bilingual_ready` → `self_reviewing`. - -### Source context - -Before translating, the agent must understand the source's format and delivery context. If the source is a transcript of an oral talk, a book excerpt, a guided meditation script, a Q&A, a written article, or any other genre, that register shapes the translation. If this context is not clear from the file path or source content, ask the user before proceeding. - -### Input - -Source text in `.dj` or `.docx` (Chinese only). - -### Deliverables - -- `source.dj` — extracted/cleaned Chinese -- `target.dj` — English translation, line count matches source -- `bilingual.dj` — interleaved (source line, target line adjacent, blank between pairs). - Generated by `../../scripts/gen-bilingual.py source.dj target.dj > bilingual.dj`. - Do not create or edit by hand; do not commit. -- `edit-suggestions.dj` — terminology/consistency issues flagged for review - -### Rules - -1. Load `translation` and `terms-search` skills before starting. -2. Search terms DB for key Buddhist terms. -3. TOC: plain bullet lists, no link targets, no page numbers. -4. Djot formatting: - - Emphasis: `*text*` (single asterisks). Never `**` (Markdown bold). - - Comments: `{% ... %}` -5. Preserve source formatting — don't add/remove emphasis. -6. Translate in-response — never call external translation APIs. - -### Review - -After translating, load `translation-review` (self mode) to check: -- Full detection rules (three passes + R1–R14 editorial polish) -- Terminology consistency against terms DB -- Grammar, fluency, calques -- Missing content (mid-paragraph truncation) -- Inconsistency (same term translated differently) - ---- - -## Workflow B: Proofread / Review(校对/审阅) - -Both self-review and other-review use the unified `translation-review` skill. -The **same detection rules** apply to both. The only difference: self mode edits -`target.dj` directly; other mode writes `review-comments.dj` with collaborative tone. - -A third mode — **Direct Edit Review** — applies when the user explicitly says to -edit the `.dj` files directly and skip any comments file. - -### B1: Self-Review(自审) - -You translated it. You own the English. Load `translation-review` skill (self mode). - -1. Read `source.dj` + `target.dj` fully. -2. Apply all detection rules (three passes + R1–R14 editorial polish). -3. Edit `target.dj` directly with `patch` (mode='replace'). -4. Record non-obvious choices in `translation-findings.dj` if needed. -5. Regenerate `bilingual.dj` with `../../scripts/gen-bilingual.py source.dj target.dj > bilingual.dj`. -6. Verify line counts: `source.dj` and `target.dj` must match. - -### B2: Other-Review(审他稿) - -Someone else translated it (volunteer, etc.). Load `translation-review` skill (other mode). - -1. Read `source.dj` + `target.dj` fully. -2. Apply all detection rules (three passes + R1–R14 editorial polish). -3. Do NOT edit `target.dj` — write `review-comments.dj` instead. -4. Follow deliberation protocol: 随喜 first, questions not commands. -5. Address translator by name. -6. Ask the user whether to apply the findings. If yes, switch to Direct Edit Mode. - -### B3: Direct Edit Review(直接修改稿) - -The user wants fixes applied directly, no separate comments file. This can follow -self-review or other-review, or be a standalone polish pass. - -1. Read full `target.dj` + `source.dj` in one pass. -2. Collect every issue using the full detection rules. -3. Batch all fixes into one set of exact-string replacements. Apply with `patch`. -4. Regenerate `bilingual.dj` with `../../scripts/gen-bilingual.py source.dj target.dj > bilingual.dj`. -5. Verify line counts match. - -Avoid iterative "find a few more, edit again" loops. If the user asks "anything -else?" after a direct-edit pass, do one more full systematic read and batch again. - ---- - -## Djot - -- Comments: `{% ... %}` -- Emphasis: `*text*` (single asterisks) -- Dashes in English: `---` em, `--` en. Pandoc converts in docx output. -- Preserve source formatting — don't add/remove emphasis - -When editing `.dj` files, use `patch` (mode='replace') — not regex-based -string replacement in `execute_code`. `patch` is safer, surfaces conflicts, -and produces a diff you can review. - -## Typst Bilingual Template - -For producing PDFs from bilingual Chinese-English articles: - -- Template: `translate-files/lib/mpi-bilingual-template.typ` -- Example host: `translate-files/从物品整理到心灵整理/mindful-organizing.typ` -- Design notes: `references/typst-template-design.md` -- Produce rendered PDF files in: /tmp/ - -## Scripts - -Utility scripts in `scripts/` (fish for CLI wrappers, Python for data processing). -Agents should write repetitive logic here and run via `terminal` rather than -regenerating the same Python in execute_code each turn. - -- `scripts/docx2dj.fish ` — pandoc .docx → .dj alongside the original -- `scripts/split-bilingual.fish ` — split into source.dj (CN) + target.dj (EN) -- `scripts/dj2docx.fish ` — pandoc .dj → .docx in `/tmp/` -- `scripts/proofread-pdf.py ` — word-level diff between manuscript and typeset PDF -- `scripts/gen-bilingual.py ` — produce `bilingual.dj` on stdout; run as `gen-bilingual.py source.dj target.dj > bilingual.dj` -- `scripts/gen-bilingual--.py` — article-specific extraction from DOCX or source/target pairing -- `scripts/compile-typst.fish ` — compile a Typst file to PDF - -Article-specific scripts (including Typst compile helpers) should be placed in -the article directory itself, named with a short hash: e.g. -`translate-files/
/compile-typst-.fish`. +See [toolkit/AGENTS.md](toolkit/AGENTS.md) for the full project conventions. diff --git a/other/omp-translation-guide/manual.dj b/other/omp-translation-guide/manual.dj new file mode 100644 index 0000000..2741adb --- /dev/null +++ b/other/omp-translation-guide/manual.dj @@ -0,0 +1,513 @@ +# Using omp to Translate Articles + +A beginner's guide for the MPI Translation Project. + +This guide assumes you have never used omp or an AI coding agent before. We will walk through every step: downloading omp, buying AI credits, connecting a provider, and running your first translation. + +--- + +## What this guide will teach you + +By the end of this guide you will be able to: + +- Install omp on your computer. +- Set up an AI model provider with credits. +- Connect that provider to omp. +- Open a translation project and start translating a Chinese article into English. +- Understand the basic workflow from source to approved translation. + +--- + +## What you need before you start + +- A computer running macOS, Linux, or Windows. +- A terminal (on macOS or Linux) or PowerShell / Windows Terminal (on Windows). +- An internet connection. +- A payment method for AI credits: credit card, debit card, or the payment options each provider accepts. +- A copy of the translation project files on your computer. + +No programming experience is required. You will mostly be typing plain commands and reading the output. + +--- + +## What is omp? + +omp ("Oh My Pi") is a terminal-first AI assistant --- a *coding agent* --- that runs on your computer. It reads files, runs commands, edits text, and follows detailed instructions you give it. + +For translation work, you use omp to: + +- Read the Chinese source file. +- Look up Buddhist terms in the project's term database. +- Draft an English translation. +- Review the draft for mistakes and awkward wording. +- Generate side-by-side bilingual files. + +You stay in charge. omp drafts and checks; you approve and correct. + +--- + +## What is an AI model provider? + +omp is the assistant. The *provider* is the AI service that actually thinks and writes. The provider charges you for the compute time used. + +For this project we recommend one of these three providers: + +| Provider | Best for | Pricing model | +|---|---|---| +| OpenCode Go | Low-cost, predictable monthly access to open coding models. | $5 first month, then $10/month. | +| OpenRouter | Pay-as-you-go access to many models, including Claude and GPT. | Buy credits; no minimum spend. | +| Kimi Code | Access to Kimi's coding models. | Recharge API credits; starts at $1. | + +You only need *one* of these to get started. Pick the one that is easiest to pay for in your region. + +--- + +## Step 1: Install omp + +### macOS and Linux + +Open your terminal and run one of these commands. + +**Recommended --- installer script:** + +```bash +curl -fsSL https://raw.githubusercontent.com/can1357/oh-my-pi/main/scripts/install.sh | sh +``` + +**If you use Homebrew:** + +```bash +brew install can1357/tap/omp +``` + +**If you already have Bun installed (version 1.3.14 or newer):** + +```bash +bun install -g @oh-my-pi/pi-coding-agent +``` + +### Windows + +Open PowerShell and run: + +```powershell +irm https://omp.sh/install.ps1 | iex +``` + +### Verify the installation + +Close and reopen your terminal, then run: + +```bash +omp --version +``` + +You should see a version number. If you see an error like "command not found," check that your terminal was restarted after installation. + +Also run: + +```bash +omp config path +``` + +This shows where omp keeps its settings, usually `~/.omp/agent/`. You will use this folder later. + +--- + +## Step 2: Get AI access + +Choose **one** provider below and follow the steps. You do not need all three. + +--- + +### Option A: OpenCode Go + +OpenCode Go is a subscription that gives you reliable access to several open coding models for a flat monthly price. + +1. Open a browser and go to `https://opencode.ai/auth`. +2. Create an account with email or a third-party login. +3. Subscribe to **OpenCode Go**. + - The first month costs $5. + - After the first month it costs $10/month. +4. Once you are subscribed, find the API key in your account page. +5. Copy the key and save it somewhere safe. Treat it like a password. + +Models included with OpenCode Go include GLM-5.2, Kimi K2.7 Code, Kimi K2.6, Qwen3.7 Max, DeepSeek V4 Pro, and others. The exact list can change; see `https://opencode.ai/docs/go` for the current models. + +--- + +### Option B: OpenRouter + +OpenRouter is a gateway that lets you use many different AI models through a single account. You pay only for what you use. + +1. Open a browser and go to `https://openrouter.ai/sign-up`. +2. Create an account with GitHub, Google, MetaMask, or email. +3. Sign in, then go to `https://openrouter.ai/settings/credits`. +4. Click **Buy Credits** and choose an amount. There is no minimum spend; $10 is enough to start. +5. Add a payment method and complete the purchase. +6. Go to `https://openrouter.ai/keys` and create an API key. +7. Copy the key and save it somewhere safe. Treat it like a password. + +OpenRouter charges a 5.5% platform fee on top of the model's price. You can explore models and prices at `https://openrouter.ai/models`. + +--- + +### Option C: Kimi Code + +Kimi Code is the coding model from Moonshot AI. You use the Kimi API platform to create an account and recharge credits. + +1. Open a browser and go to `https://platform.kimi.ai/`. +2. Create an account and sign in. +3. Go to the console or user center. +4. Recharge your account. + - You need at least $1 to start using the API. + - When your cumulative recharge reaches $5, you receive a $5 voucher. +5. Go to the API key section and create a key. +6. Copy the key and save it somewhere safe. Treat it like a password. + +Pricing for each model is listed at `https://platform.kimi.ai/docs/pricing/chat`. For coding, the relevant model is usually **Kimi K2.7 Code**. You are charged per token used. + +--- + +## Step 3: Connect your provider to omp + +omp reads your provider key from an environment variable or a `.env` file. The easiest way to start is with a `.env` file in your home directory. + +### Create the `.env` file + +Run this in your terminal to open or create the file: + +```bash +nano ~/.omp/.env +``` + +If you prefer a different editor, replace `nano` with `vim`, `code`, or another editor. + +Paste the block for the provider you chose: + +**OpenCode Go:** + +```bash +OPENCODE_API_KEY=opencode-your-key-here +``` + +**OpenRouter:** + +```bash +OPENROUTER_API_KEY=sk-or-v1-your-key-here +``` + +**Kimi Code:** + +```bash +KIMI_API_KEY=your-kimi-key-here +``` + +Replace the placeholder after the `=` with the real API key you copied. Save the file and close the editor. + +### Restart your terminal + +Close and reopen your terminal so the new `.env` file is read. Then verify that omp launches: + +```bash +omp +``` + +You should see the omp welcome screen. Press `Ctrl + C` to exit. + +### Test with a simple prompt + +Run: + +```bash +omp -p "hello" +``` + +If everything is connected, omp will reply with a short greeting. If you see an authentication error, check that your API key is pasted correctly and that the terminal was restarted. + +--- + +## Step 4: Open your translation project + +omp works inside a project folder. The project folder contains the Chinese source, the English translation, and the rules omp should follow. + +In your terminal, move to the project folder. For example: + +```bash +cd ~/documents/mpi/other/omp-translation-guide +``` + +Or, if you are working on a specific article: + +```bash +cd ~/documents/mpi/translate-files//
+``` + +Once you are inside the project folder, launch omp: + +```bash +omp +``` + +omp will detect the project files and rules. The current folder becomes the project root. + +--- + +## Step 5: Translate your first article + +Inside an active omp session, you can ask the agent to translate. A typical first prompt looks like this: + +```text +Please translate this article from Chinese to English following the project conventions. Read source.dj, look up key Buddhist terms in the terms database, and produce target.dj. +``` + +omp will: + +1. Read `source.dj` (the Chinese original). +2. Look up Buddhist terms in the MPI term database. +3. Draft `target.dj` (the English translation). +4. Generate `bilingual.dj` (a side-by-side file) if you ask for it. + +You can watch the process in the terminal. Each step is shown as a compact card. Press `Ctrl + O` to expand a card and see the full output. + +--- + +## The translation workflow + +All translation work in this project follows a fixed state machine. The AI handles the drafting and review stages; humans approve the result. + +```mermaid +stateDiagram-v2 + [*] --> idle + idle --> translating: SOURCE_LOADED + idle --> other_reviewing: BILINGUAL_LOADED + translating --> bilingual_ready: TRANSLATION_DRAFTED + bilingual_ready --> self_reviewing: BILINGUAL_GENERATED + self_reviewing --> translating: SELF_REJECTED + self_reviewing --> other_reviewing: SELF_APPROVED [peer review required] + self_reviewing --> approved: SELF_APPROVED [no peer review] + other_reviewing --> translating: PEER_REJECTED + other_reviewing --> approved: PEER_APPROVED + approved --> typesetting: TYPESET_REQUESTED + approved --> done: COMPLETE + typesetting --> done: TYPESET_COMPLETE + done --> [*] +``` + +States: + +| State | Meaning | Output artifact | +|---|---|---| +| `idle` | Waiting for source or an existing bilingual file. | --- | +| `translating` | AI drafts the English translation. | `target.dj` | +| `bilingual_ready` | `bilingual.dj` is generated from source and target. | `bilingual.dj` | +| `self_reviewing` | AI checks its own draft against the source. | edited `target.dj` | +| `other_reviewing` | A peer reviews the draft. | `review-comments.dj` | +| `approved` | Translation accepted. | --- | +| `typesetting` | Producing PDF or DOCX. | `.pdf` / `.docx` | +| `done` | Complete. | --- | + +For a total beginner, the most important path is: + +1. **Prepare the source** --- clean the Chinese and save it as `source.dj`. +2. **Draft** --- ask omp to create `target.dj`. +3. **Generate bilingual** --- create `bilingual.dj` to read both languages side by side. +4. **Self-review** --- ask omp to check its own draft for errors. +5. **Human review** --- you read the bilingual file and approve or flag issues. + +--- + +## Step 6: Prepare the source + +The source usually arrives as a Word document or a plain-text file. We clean it and convert it into a Djot file called `source.dj`. + +If you have a `.docx` file, run: + +```bash +toolkit/scripts/docx2dj.fish
.docx +``` + +If you do not have a `.docx` file, you can create `source.dj` by hand in a text editor. Keep the line structure intact: one physical line per logical line, and preserve paragraph breaks. + +Before translating, understand the source's genre. Is it a transcript of an oral talk, a book excerpt, a guided meditation script, a Q&A, or a written article? The genre shapes the translation register. If the context is unclear, ask the team before proceeding. + +--- + +## Step 7: Draft the translation + +Once `source.dj` exists, ask omp to draft the translation. A good prompt is: + +```text +Load the mpi-translation and mpi-terms-search skills. Translate source.dj into English, preserving the line structure, and write target.dj. Look up key Buddhist terms in the MPI terms database before translating. +``` + +omp will read the Chinese line by line, check the terms database, and write one English line for every Chinese line. + +The terms database is searched with: + +```bash +$MPI_PROJECT_ROOT/toolkit/terms-database/search.py [limit] +``` + +Source priority is: + +1. DoT定稿 +2. 内部特色词 +3. 佛教术语 +4. 经论名 + +omp also preserves emphasis markers (`*text*`) and soft-line markers (` ` at the end of a line). + +--- + +## Step 8: Generate the bilingual file + +Once `source.dj` and `target.dj` exist, generate the side-by-side file: + +```bash +../../toolkit/scripts/gen-bilingual.py source.dj target.dj > bilingual.dj +``` + +The bilingual file interleaves source and target lines with a blank line between each pair. Do not edit `bilingual.dj` by hand. Regenerate it whenever `source.dj` or `target.dj` changes. + +--- + +## Step 9: Self-review + +Ask omp to review its own draft: + +```text +Load the mpi-translation-review skill in self mode. Read source.dj and target.dj fully, check for missing content, terminology consistency, and awkward English, then edit target.dj directly. After editing, regenerate bilingual.dj and verify the line counts match. +``` + +The review runs three passes: + +1. **Accuracy and completeness** --- missing content, mistranslation, terminology inconsistency, unnecessary additions, number/time/person mismatches. +2. **Fluency and naturalness** --- calques, register drift, broken collocations, pronoun errors, sentence rhythm. +3. **Dharma and cultural fitness** --- Buddhist term register, cultural anachronism, tone of the teacher, implicit meaning, formatting fidelity. + +After the three passes, it applies an R1--R14 editorial checklist covering spelling, punctuation, capitalization, articles, agreement, tense, voice, prepositions, modifiers, parallelism, redundancy, word choice, sentence openings, flow, and a final read-aloud. + +In self mode, omp edits `target.dj` directly and then regenerates `bilingual.dj`. It verifies that `source.dj` and `target.dj` have exactly the same number of lines. + +--- + +## Step 10: Human review and peer review + +Read `bilingual.dj` carefully. You are checking that the English matches the Chinese, sounds natural, and keeps the right tone. + +If you want another person to review, they can ask omp to write a `review-comments.dj` file: + +```text +Load the mpi-translation-review skill in other mode. Read source.dj and target.dj and write review-comments.dj with must-fix issues and optional suggestions. Do not edit target.dj directly. +``` + +The peer review follows a deliberation protocol: begin with appreciation, phrase most issues as questions or options, and distinguish "must fix" from "consider." + +When the team asks omp to apply the findings, it switches to direct-edit mode, updates `target.dj`, and regenerates `bilingual.dj`. + +--- + +## Step 11: Typesetting and publishing + +Once the translation is approved, it can be exported. + +- **DOCX export** using `toolkit/scripts/dj2docx.fish ` produces a Word document in `/tmp/`. +- **Bilingual PDF** using the Typst template `translate-files/lib/mpi-bilingual-template.typ` produces a PDF through `toolkit/scripts/compile-typst.fish `. + +Rendered files are written to `/tmp/` and are not committed to the repository. + +--- + +## Djot conventions + +All translation files are written in Djot. Keep these rules in mind: + +- Emphasis: use a single asterisk on each side: `*text*`. Never use `**` for bold. +- Comments: wrap notes in `{% ... %}`. +- Dashes: use `---` for an em dash and `--` for an en dash. +- Preserve the source formatting. Do not add or remove emphasis. +- Keep the one-to-one line mapping: one physical target line per physical source line. +- Preserve soft-line markers (` ` at the end of a line) on both source and target. +- Table of contents: plain bullet lists only, no link targets and no page numbers. + +--- + +## Useful omp commands for translation + +| Command | What it does | +|---|---| +| `omp` | Start an interactive session in the current project. | +| `omp -p ""` | Run a single prompt and exit. | +| `Ctrl + O` | Expand the selected tool card to see full output. | +| `Ctrl + C` | Exit omp. | +| `/skill:` | Load a specific skill manually. | +| `/model` | Pick a different model from the providers you are signed into. | + +--- + +## Useful scripts + +| Script | Purpose | +|---|---| +| `toolkit/scripts/docx2dj.fish ` | Convert a Word document to Djot. | +| `toolkit/scripts/split-bilingual.fish ` | Split a combined bilingual file into `source.dj` and `target.dj`. | +| `toolkit/scripts/dj2docx.fish ` | Convert a Djot translation to DOCX in `/tmp/`. | +| `toolkit/scripts/gen-bilingual.py ` | Generate `bilingual.dj` on stdout. | +| `toolkit/scripts/compile-typst.fish ` | Compile a Typst file to PDF. | + +Article-specific scripts are placed in the article directory itself and named with a short hash. + +--- + +## Troubleshooting + +### "omp: command not found" + +- Restart your terminal after installation. +- Check that the install directory is on your `PATH`. Common locations are `~/.local/bin` and `/usr/local/bin`. + +### Authentication error + +- Double-check that your API key is pasted correctly in `~/.omp/.env`. +- Make sure you restarted your terminal after saving the `.env` file. +- Make sure the key has not expired. + +### No response or very slow response + +- Check your internet connection. +- Check the provider's status page (for example, `https://status.openrouter.ai/`). +- Switch to a different model with `/model` if the current model is overloaded. + +### Translation line counts do not match + +- Run `wc -l source.dj target.dj` to see which file is longer. +- Ask omp to regenerate `target.dj` or `bilingual.dj` from the current source. +- Check that no blank lines were accidentally added or removed. + +--- + +## Quick checklist + +Before declaring a translation ready for human review, confirm: + +- [ ] omp is installed and `omp --version` works. +- [ ] An AI provider is connected and `omp -p "hello"` replies. +- [ ] `source.dj` and `target.dj` have the same number of lines. +- [ ] `bilingual.dj` has been regenerated from the latest source and target. +- [ ] Key Buddhist terms have been checked in the MPI terms database. +- [ ] No missing content, truncation, or overtranslation. +- [ ] The English matches the source's genre and register. +- [ ] Emphasis, paragraph breaks, and soft-line markers are preserved. +- [ ] The final English has been read aloud. + +--- + +## Next steps + +- Read the `mpi-translation` skill for the full translation principles. +- Read the `mpi-terms-search` skill for how to query the MPI term database. +- Read the `mpi-translation-review` skill for the three-pass review and R1--R14 checklist. +- Read `AGENTS.md` for the project conventions and file layout. + +When you feel comfortable, try translating a short article from start to finish. The best way to learn is by doing. diff --git a/other/omp-translation-guide/presentation.typ b/other/omp-translation-guide/presentation.typ new file mode 100644 index 0000000..a705b26 --- /dev/null +++ b/other/omp-translation-guide/presentation.typ @@ -0,0 +1,249 @@ +#import "@preview/touying:0.7.4": * +#import themes.simple: * + +#let warm-accent = rgb("#A0522D") +#let dark-text = rgb("#333333") +#let soft-bg = rgb("#FDFBF7") + +#show: simple-theme.with( + aspect-ratio: "16-9", + config-colors( + primary: warm-accent, + secondary: dark-text, + tertiary: rgb("#8B7355"), + neutral: dark-text, + neutral-lightest: soft-bg, + ), +) + +#set text(font: "Liberation Sans", size: 22pt, fill: dark-text) +#set heading(numbering: none) + +// Custom title slide so title and subtitle appear together +#slide[ + #align(center + horizon)[ + #text(size: 2em, weight: "bold", fill: dark-text)[ + How we translate Buddhist articles + ] + #v(1em) + #text(size: 1.2em, fill: dark-text)[ + From Chinese source to English reader + ] + #v(0.5em) + #text(size: 1em, fill: warm-accent)[ + with a little help from AI + ] + #v(2em) + #text(size: 0.8em, fill: rgb("#666666"))[ + MPI Translation Project · omp + ] + ] +] + +== What is omp? + +#v(0.5em) + +"omp" stands for *Oh My Pi*. + +#pause + +Think of it as a shared workspace where AI assistants help us with careful, repetitive work. + +#pause + +- We give the AI our rules, our files, and our style guide. +- It follows the same process every time. +- We stay in charge: AI drafts, humans review. + +== What we have translated so far + +#v(0.5em) + +Our library holds *14 article projects* on Buddhist and Dharma topics. + +#pause + +So far, *2 articles* are fully translated: + +#pause + +- *What do we rely on to understand the world?* +- *12-Minute Mindful Ball Guidance* + +#pause + +More articles are in progress, covering mindfulness, life questions, gratitude, end-of-life care, and Dharma in management. + +== Our specialized helpers + +#v(0.5em) + +Inside omp, we keep special guidebooks called *skills*. They teach the AI how to do each task. + +#pause + +#grid( + columns: (1fr, 1fr), + gutter: 1.2em, + [ + *Translation skill* + + How to turn Chinese into English + ], + [ + *Terms-search skill* + + How to look up Buddhist terms in our database + ], + [ + *Review skill* + + How to check and improve drafts + ], + [ + *Other helpers* + + Clean up text, translate slides, convert documents + ], +) + +== Step 1: Receiving the article + +#v(0.5em) + +An article usually arrives as a Word document or a plain text file. + +#pause + +We clean it up and prepare a master Chinese file called `source.dj`. + +#pause + +This master copy becomes the original that every later step checks against. + +== Step 2: First draft + +#v(0.5em) + +The AI reads the Chinese line by line. + +#pause + +It looks up key Buddhist terms in our database so names and ideas stay consistent. + +#pause + +Then it writes a first English draft, called `target.dj`, with one English line for every Chinese line. + +== Step 3: Self-review + +#v(0.5em) + +A second AI checks the English draft against the original Chinese. + +#pause + +It looks for: + +#pause + +- missing sentences or paragraphs +- awkward English +- Buddhist terms used differently from before +- anything that changes the meaning + +#pause + +It edits the English directly, and we check the line counts match. + +== Step 4: Human review + +#v(0.5em) + +Now we read the Chinese and English side by side. + +#pause + +#grid( + columns: (1fr, 1fr), + gutter: 1em, + [ + #align(center, text(size: 1.2em)[中文原文]) + + #align(center, text(size: 0.8em)[Chinese original]) + ], + [ + #align(center, text(size: 1.2em)[English]) + + #align(center, text(size: 0.8em)[translation]) + ], +) + +#v(0.5em) + +#pause + +We approve the draft or flag problems for the AI to fix. This step may repeat several times until the text feels right. + +== Step 5: Review by others + +#v(0.5em) + +When the translation is clean, we send it to volunteers or editors. + +#pause + +They read it with fresh eyes. + +#pause + +Once they approve, the article is ready for the next step: typesetting or publishing. + +== The whole journey + +#v(1em) + +#text(size: 1.1em)[ + 1. Receive the article + + 2. Draft the translation + + 3. Self-review + + 4. Human review + + 5. Review by others + + 6. Publish +] + +#v(1em) + +#text(size: 0.85em, fill: rgb("#666666"))[ + AI helps at the first three stages; humans lead the last two. +] + +== Any questions? + +#v(2em) + +#align(center, text(size: 1.3em)[We would love to hear from you.]) + +#v(1em) + +#align(center, text(size: 0.9em, fill: rgb("#666666"))[ + Ask about the workflow, the AI, or how you can help review translations. +]) + +== Thank you + +#v(2.5em) + +#align(center, text(size: 1.5em)[Thank you for listening.]) + +#v(1em) + +#align(center, text(size: 0.9em, fill: rgb("#666666"))[ + MPI Translation Project · omp +]) diff --git a/readme.dj b/readme.dj index b5ff7bd..a02d5e0 100644 --- a/readme.dj +++ b/readme.dj @@ -4,12 +4,13 @@ | Directory | Contents | |---|---| -| `skills/` | Hermes Agent skills — loaded via `~/.hermes/config.yaml` `external_dirs`. [Install guide →](skills/readme.dj) | -| `terms-search/` | Term database (DuckDB) + search CLI (`search.py`). Priority: DoT定稿 > 内部特色词 > 佛教术语 | +| `toolkit/skills/` | Hermes Agent skills — loaded via `~/.hermes/config.yaml` `external_dirs`. [Install guide →](toolkit/skills/readme.dj) | +| `toolkit/terms-database/` | Term database (DuckDB) + search CLI (`search.py`). Priority: DoT定稿 > 内部特色词 > 佛教术语 | | `translate-files/` | Translation projects: `/
/` with `source.dj`, `target.dj`, `bilingual.dj` | -| `scripts/` | Utility scripts (fish shell): `dj2docx.fish` | -| `guide/` | Reference materials — 术语库 source data | -| `AGENTS.md` | Project conventions for AI agents — translation workflow, djot rules, skill usage | +| `toolkit/scripts/` | Utility scripts (fish shell): `dj2docx.fish` | +| `references/` | Reference materials — Typst design notes, Google Docs review journey, formatting guides | +| `toolkit/` | Git submodule containing skills, scripts, term database, and AGENTS.md | +| `AGENTS.md` | Stub pointing to `toolkit/AGENTS.md` | ## Translation Stages diff --git a/references/google-docs-comment-journey.dj b/references/google-docs-comment-journey.dj new file mode 100644 index 0000000..b194c74 --- /dev/null +++ b/references/google-docs-comment-journey.dj @@ -0,0 +1,133 @@ +# Google Docs Comment Automation — Journey Log + +2026-06-21 + +## Background + +Task: review Chinese→English Buddhist translation manuscripts on Google Docs +by adding editorial comments (the "Wade" style — anchored suggestions on +specific text). The reviewer is an AI agent running on Hermes with API access. + +## Phase 1: Google Workspace OAuth Setup + +Set up OAuth2 for Google Workspace access: +- Created Google Cloud project (932146571366) +- Enabled APIs: Gmail, Calendar, Drive, Sheets, Docs, People +- Created Desktop OAuth client, downloaded client_secret.json +- Completed OAuth PKCE flow → token at ~/.hermes/google_token.json + +Success: authenticated with scopes covering all needed services. + +## Phase 2: Reading Documents and Comments (Success) + +Used the Google Drive API v3 `comments.list` endpoint to fetch comments +from existing review documents: + +- Doc 1 (1DTzH7...): 61 人生佛教在当代的弘扬 — 121 comments +- Doc 2 (13S_0u...): 附录 我的判教观 — 151 comments +- Doc 3 (1LxCcX...): 16 觉醒的艺术 — 201 comments + +Total: 473 editor comments by Wade, spanning 2026-04-17 to 2026-06-20. +These comments were well-anchored (kix.XXXXXXXX format) and contained +actionable editorial suggestions in Chinese. + +## Phase 3: Pattern Analysis (Success) + +Analyzed all 473 comments to extract systematic review rules. The editor's +feedback follows clear patterns: + +R1 — Active voice + "we" subject +R2 — Noun → verb conversion +R3 — Simplify vocabulary +R4 — Break long sentences +R5 — Remove unnecessary words +R6 — Conversational / interview tone +R7 — No -ly adverbs +R8 — Concrete over abstract +R9 — Terminology alignment +R10 — Missing content detection +R11 — Source faithfulness +R12 — Sentence structure clarity +R13 — Specific word fixes +R14 — Positive feedback (随喜), naming the translator + +Created skill: toolkit/skills/mpi-translation-review-comment/SKILL.md + +## Phase 4: Attempting to Create Anchored Comments (Failure) + +Task: apply the review rules to a new document +(1NmJKNavB4IBg56ZRBdS5ux9ObRVlHPnSxVmjjXCyITs — 佛法与企业管理, translator: maple). + +### Attempt 1: comments.create without anchor + +Result: comments appear as document-level (unanchored). The translator has no +way to know which text each comment refers to. User rejected this approach: +"Without anchors, there is no way to know where the translator should edit." + +### Attempt 2: kix anchors from Docs API response + +Searched the document body via `documents.get` for kix segment IDs. +Result: the Docs API response does NOT expose kix anchors. Only footer IDs +(kix.hf0, kix.list) appear. Text segments have no kix identifiers. + +### Attempt 3: kix anchors from HTML export + +Exported the document as HTML via Drive API `files.export_media`. +Result: HTML export contains NO kix IDs. Since 2021, Google Docs renders +content on ``, so there are no DOM nodes with kix attributes in +the exported HTML. + +### Attempt 4: Browser-based kix extraction + +Tried navigating to the document via browser to extract kix IDs from the +live page DOM. Result: requires Google sign-in. Agent cannot authenticate +to Google in a browser session. + +### Attempt 5: Line-based anchor format (official docs) + +The Drive API docs describe an alternative anchor format using line numbers: +```json +{"region": {"kind": "drive#commentRegion", "line": 1, "rev": "head"}} +``` +Created a test comment with this format. Pending user verification on whether +it actually anchors in the Google Docs UI. + +### Official Position + +The Google Drive API documentation states (as of 2025): + +> "The anchor is saved and returned when retrieving the comment, however +> Google Workspace editor apps treat these comments as un-anchored comments." + +The kix.* anchor format is Google's internal, undocumented format that has +never been reverse-engineered. A 2016 thread on the Google Apps Script +community confirms this has been a known limitation for nearly a decade +with no resolution. + +## Phase 5: Workaround + +Created `translate-files/佛法与企业管理/review-comments.dj` — a structured +file listing each comment with: +- The specific text it should anchor to (quoted) +- The comment content + +The translator can manually add these comments in the Google Docs UI by +selecting the quoted text and inserting each comment. + +## Key Findings + +1. **Reading comments works perfectly** via Drive API `comments.list` +2. **Writing anchored comments does NOT work** — the anchor is silently + ignored by Google Docs editor apps +3. **The only reliable way** to add anchored comments to Google Docs is + through the browser UI (select text → Insert → Comment), which requires + human interaction or a headless browser with Google authentication +4. **Google Apps Script** might support anchored comments from within + the document environment (untested — requires different auth model) +5. The `translation-review-comment` skill remains useful for `.dj` file + review where `patch` can be used instead of comments + +## Files Created + +- toolkit/skills/mpi-translation-review-comment/SKILL.md — systematic review rule checklist +- translate-files/佛法与企业管理/review-comments.dj — manual comment reference diff --git a/references/guide/04 翻译常用工具/常见工具.doc b/references/guide/04 翻译常用工具/常见工具.doc new file mode 100644 index 0000000000000000000000000000000000000000..a9ab2561c01d95b67c21493857c98fe9e662a0c5 GIT binary patch literal 15872 zcmeI33vgA%8OQgWdz0KeNWv>(pmM<+N9omXxTWlk3r`1+Yf8RMd$sr_g6C?GJ zFZrK+eY^YZ?zi9W*|YcPk%7nWe?0#8suNwMJnFO=u0mb$8h(LAu}bN9Bz)R5O*>L4 zy#4e!>Vc1uRmNDx(2E-aE@}(=aKk|ahy+n!0Eh-L02NRJ!5}ag3;{zy9FX#c;a&{l z!Ei7Fd<95aKYccOV3FFuU%m3HxoSObt=hr}1y6BkjMEaa1)21wJut}5#3Q_s0LDlfL(yDttQmjUzS}UerRpDQ6vv&DRRJta5E47LoT{3kM z`O`K-7NRn#q0Ew@!s>yZ$&*I?PWywcKMZ+Zw!29cT5T;qONuT;?lLOAt$vxqJ*j`v zC3bPr4I+w+i8KEld(e~lFLPSp%zv>V-OYcgdL;0JQD8K<6eNJlz!)$VB!bJqI4~Ye z09Sw{FcBCa8Ki(oAQfB*CW9#;4NL{oz;rMJ%mi0~Ss)$E26I3L$OLnNDD6Cu1?Gb< zvb{RZjES%`fQGtA^y2uf-#ztVPG#)Fclgx!u}{1u{k@$8Spy_}BY>JV^AO!>0;1c^ zKy;@C$l9?V$l7rTNZvPrtRe3JSw~KSSnv-ZX5qg;){`bG5#4N|a0z=URP;u4bt7|D zNWxNfpxtIvqaWpj>aidl{6%|;_7JqjT&Z|I1g49s4Bh zlX|XebDtqX>Jr^4RJm9!zskW>cVnR?>e$ zZubC{NPZ#5^^#HQ5=sLi$I}u(4v|mF72T0?{tdh!3nmFmn&`#?MkI%Esb%ELvF2rr z{~X*ZRZVyaBUB5Qok#M|r$?9IieL67cK!nRYG_+Et5H3Edu@=s`NUjD%py?MX0yrG zrGAPIr&yyXqnV69iKnd78PYK{k#x{k&tC1;{9*}g=l~hX)WaN`c5soQH zlws+PXps2kphu;oi~W=Gia-Txcs4RF!-`d_b+mOYp(^IcR+Xn3;Ym>oDLp`G3*lXc zTSi?AiC+SL01aPE{6hL-A!SRv@Y$~T3z4;wawNSez!Xz-25FWi+WKfE zh+?*rXkL=VA-UV5@yI%Cnl|$o?;vcNro38=C4~g~)rtB3THaOlzjCv;)0(vUc@&|0V0WQ&l0ey$=0t;91Wssp+9a zBWS<0+aagibGc%#jB_#cFwLo3+#Y6CCm%YXzOg-==`ketIXknMbxS#az``1>7UVC; zf5FmKJGtZO^~EV?A1Sc`nG%p=%k5Cd({xR-?)(n-UbOuDcIv?Ez zpR6+Flr5H_8CTv+9XddG2-9>et-r>J?1h_)hPQP{P`Nj-Rg%>p46{&4#$znWva@{z zd1P1p|EA~Yw}<;sh@a5EclTj==PF9xIMTteb1_|@w58M|mjClYccPcaKQF?5iR9E( zl)c6H61BO%seRo819&4(D>?9u6W2E8i9eISIk_yF%ne$Hwt z*(cSrYnsLx%tYQM!~bxFpK=tAoYb=6JIs)(^M*73a*)+q8V*n)i2{-u+NnFfFa? z)YjG1RY&XUX5ytCx#4i$ml+}u5 zxdT_74E*P&*LOUh?`hHvU&-WJRXT0U(9*<)w{|52-fKQ`;~VR~wky6!H>4-R0>wFp z3-fM@2%Oy$z3GcHHCJ@If>VupdZA-%<&D4EdPfqCw$G%cmsT^<75rTR)>|4N+F#j8 z!#j`sr%|Kb-vd+}{rFzf zLA#~BHMFmTL)t2RP)>TSr6KaZe>FSlD(3zgP7`Xe33GYFpTt}JBJ9RIdQs9hk(Pmt zSWV6>w5x#Afn=U}oWaUl*G5hNI_fEfq1YPYz0q)XTd-J^43dFK6uBrb%hyn41W$9lauXHx!GQohg$+RB&wRMaBm%0 zpXK${gT+2gjWx0dsvvlSE$`9`TvjmkT}Yn6U9`WeI29%1xgVSae+8}J6p#K|WQ*Ds_F1XGualrh~4z6bV!=fR8MAQ-`$-z8uv zSO%5@YwGXXR<(Hcd8WRl8o|gA?40h`q2P5W*gjQ}GNvWqQP2W@1P+12;3)VNSjNfE za_}Pk1G#)Wm>7POZYEdM9q)4Hm^ugklTW8anRojX|eT^#ST);s$@XYVKb zKG_qv_X9fXfwM-)`XF}W-=mn@%H+A2iephB9o%=%o(u(!L>j{SFRzhn2^_Q0_Pr`%n4!X0?i9XQ|) z{Ma4%zB_Qn+5b*=irl$qr>b^eke~1zvV5AZ`Y8S! z68MtU$d<1r`CAUo9@XLtyR`YfkkPmaxR<==u*#ox6C}wMt8EbXB(QHm+r{ zM0e~$xjB@PJ(R;z#wlClV3plywysuY4^s=W^s`P^OS7!&@txE#LCsD-W!^9MO=LG{ zQK)+J-n~1XTsQNU!s7fKi3+Hy^uL==;C66%)%w-(&6V3{RLcGAX-gti>aRnUCU^QR zkM60qJzgiqM+{4uT?x-Ll3h+@ zs26b&xVv&@TT+U|E=j{fElHo#Rntc|97&5KO~{e7u}W0eH027en{+-im7y%oHE?&D z`2Dn0^KIvT&|dB3yh_^1k;juSj?LeB>?QGA5!PjJxd?8DyvpMYh_DbV@phV0o5Z+6 z9`!1pzDDswh6yOnG=7LMOI^!V?tSVh^^*RIKGYxQzu5m3{}}(|(VC$f9wWpEHzJJz zMvO7g7;0Q(Xr^w4nc-%X8EwX!!_ATAOf%hFZeDAan8$m3;m?+;_s>LC%$4@1)G+@D zzt4y;qKs%G))->M89wBjZWf!rH&1+GseEDgh5T6EqjcwYLcJF_$ejQExNX3?;?I@H z%UjJDO(o#pcMeu}cDcTHCTUHYdJh#52#L-(e#ezuGVf%1yWg@sz8(6g(Jc|%*WD0x zdD^k4t<%O1Xc+x?O!bmCVizU8a?!t1XI%Vr{Jj_7R`$m+kBrJlTsfj*d`b21CjNHZ zl9af0hf^M#5S@D8%0Ep>otiK;CG*zlKac;x^zqe=GaF)lJ@aAz@vCMxUNh^1hTZ9t zH;u}u-16UR^Y|Xm5VYvVBYTL`M|=bz1WB zcZcgGw``lYVoPM*ic?`b*X#&;tm>Hw&r}VcRb2DUg2(C_E*;%y42aqM+OU~h?kzZR z1I=e{eeG42fMpBQ#Aeq7E#TIF(|9A$|S6F9TcGHNkOZ{12{tn$exE}MZ~ zj!~XggD|(VTNABJ;jL%PYT|@+6=CI*kr6?Je;n?hws>omWivfVMYf9Zc}~;#y@Xr> z_o-PP8D_EMGe8Nr8MJ`c!5=_0d=o$=_#t=)h>;(>bhr7frn8@NyWGo~VSlHu!MLvH z8@l{vVWrwP;q=fMnBA^_17P3jH@J(R$v1Yni=P74f&xmA-w4PZ{mtC`*^0Z>a_6(> gC?dWHQ@+gNt+e>pSm8qAJMQwE=`^RGJ~ut^KR2u!$N&HU literal 0 HcmV?d00001 diff --git a/references/thoughts-on-ai.dj b/references/thoughts-on-ai.dj new file mode 100644 index 0000000..b756e44 --- /dev/null +++ b/references/thoughts-on-ai.dj @@ -0,0 +1,7 @@ +Deepseek V4 is fast and cheap +V4 can hardly find more issues if you ask it to reread. + +Minimax M3 holds the most effort, but is less capable than V4 +M3 generally finds more issues if you ask it to reread. + +GLM thinks too much before acting. diff --git a/references/typst-template-design.md b/references/typst-template-design.md new file mode 100644 index 0000000..604e481 --- /dev/null +++ b/references/typst-template-design.md @@ -0,0 +1,106 @@ +# MPI Bilingual Typst Template — Design Decisions + +## Location + +- Repo / project root: `~/documents/mpi/` +- Template directory (contains `lib/`): `~/documents/mpi/translate-files/` +- Template: `translate-files/lib/mpi-bilingual-template.typ` +- Example usage: `translate-files/从物品整理到心灵整理/mindful-organizing.typ` + +## Goal + +A single reusable Typst template for bilingual Chinese-English 静心学堂丛书 +publications. Host `.typ` files should contain almost no setup code — only an +`#import`, a `#show: mpi-bilingual.with(...)` call, and the article content. + +## Font choice + +- **Chinese:** Noto Serif CJK SC (Google Fonts) +- **English:** Noto Serif (Google Fonts), used as a freely available substitute + for Times New Roman. + +## Page and paragraph formatting + +| Setting | Value | Rationale | +|---|---|---| +| Paper | A4 | Standard publication size | +| Margins | 2.5 cm all around | Matches existing documents | +| Body size | 10.5 pt | 五号 per the formatting guide | +| Line spacing (`leading`) | 1.5 em | 1.5× line height per the guide | +| Paragraph spacing | 1.5 em | Equivalent to one blank line between paragraphs | +| Alignment | justified | Matches the existing `mindful-organizing.typ` look | +| First-line indent | 2 em | Keeps bilingual paragraphs visually distinct | +| Heading numbering | none | Headings are written verbatim in the host file | +| Heading spacing | `above: 2em`, `below: 1.2em` | Clear section breaks without page breaks | +| Footnotes | 8 pt, left aligned | Per the formatting guide | + +## Title page + +Titles and subtitles are passed as free Typst content (`title-cn`, `title-en`, +`subtitle-cn`, `subtitle-en`). + +The gap between title and subtitle uses `linebreak()` inside a single paragraph +so the spacing follows the `line-height: 1.5` rhythm instead of an arbitrary +`v()` length. A larger `v(1.5em)` separates the Chinese block from the English +block. + +## Table of contents + +The TOC shows only English headings with dotted leaders and page numbers. +Chinese headings are displayed as compact section headers above their English +counterparts. This mirrors the behavior of the old `helpers.typ` but lives in +the template. + +## Host file contract + +A host file should look like this and nothing else: + +```typst +#import "../lib/mpi-bilingual-template.typ": mpi-bilingual + +#show: mpi-bilingual.with( + title-cn: [中文标题], + title-en: [English Title], + subtitle-cn: [——副标题], + subtitle-en: [—Subtitle], +) + += 一、章节标题 += I. Chapter Title + +中文段落…… + +English paragraph … +``` + +Compile with the helper script: + +```bash +~/documents/mpi/toolkit/scripts/compile-typst.fish ./从物品整理到心灵整理/mindful-organizing.typ [output.pdf] +``` + +Or manually from the template directory (`~/documents/mpi/translate-files/`): + +```bash +cd ~/documents/mpi/translate-files +typst compile --root . ./从物品整理到心灵整理/mindful-organizing.typ +``` + +The `--root .` is required because the import `../lib/...` would otherwise +escape Typst's sandbox. + +## What is intentionally not in the template + +- No inline helpers like `#speaker` or `#quote-cn` in host files. The template + sets global formatting only. Dialogue speakers can be marked with `**Name:**` + when truly needed. +- No automatic blue styling for scripture quotes, because reliably detecting + quoted classics in plain text requires explicit markup. +- No per-chapter footnote numbering; Typst's default continuous numbering is + used. + +## References + +- `references/静心学堂丛书英文统一格式-20260211.pdf` — MPI formatting guide +- `translate-files/从物品整理到心灵整理/mindful-organizing.typ` — example host + file and current reference implementation diff --git a/scripts/compile-typst.fish b/scripts/compile-typst.fish deleted file mode 100755 index 46ff5dd..0000000 --- a/scripts/compile-typst.fish +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env fish -# Compile a Typst file to PDF. -# Usage: compile-typst [output.pdf] -# Output defaults to /tmp/.pdf -# The Typst project root is set to the parent of the file's directory -# (so imports like ../lib/... resolve correctly). - -set src (realpath $argv[1]) -set src_dir (dirname $src) -set root (dirname $src_dir) - -if set -q argv[2] - set out "$argv[2]" -else - set base (basename $src .typ) - set out "/tmp/$base.pdf" -end - -typst compile --root $root $src $out -echo $out diff --git a/scripts/dj2docx.fish b/scripts/dj2docx.fish deleted file mode 100755 index 710fb1c..0000000 --- a/scripts/dj2docx.fish +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env fish -# Convert target.dj to English docx -# Usage: dj2docx [output-filename] -# Output filename defaults to /tmp/-英文.docx - -set tgt (realpath $argv[1]) -if set -q argv[2] - set out "$argv[2]" -else - set parent (basename (dirname $tgt)) - set out "/tmp/$parent-英文.docx" -end -pandoc $tgt -f djot -t docx -o $out -echo $out diff --git a/scripts/docx2dj.fish b/scripts/docx2dj.fish deleted file mode 100755 index 92cb6d9..0000000 --- a/scripts/docx2dj.fish +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env fish -# Convert .docx to .dj (djot) via pandoc -# Usage: docx2dj.fish [output.dj] -# No output path → stdout - -set docx (realpath $argv[1]) - -if test (count $argv) -ge 2 - pandoc $docx -f docx -t djot --wrap=none -o $argv[2] - echo $argv[2] -else - pandoc $docx -f docx -t djot --wrap=none -end diff --git a/scripts/gen-bilingual-buddhist-attitude-185a23.py b/scripts/gen-bilingual-buddhist-attitude-185a23.py deleted file mode 100644 index 23c259b..0000000 --- a/scripts/gen-bilingual-buddhist-attitude-185a23.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Generate bilingual.dj from DOCX manuscript only (no PDF). -Source: Chinese from DOCX. Target: English from DOCX. -""" -import re, subprocess -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -DOCX = ROOT / "translate-files/佛教徒的人生态度/定稿 佛教徒的人生态度 善鑫慧炬照禅道靖妙一观轩慈德20260527.docx" -OUT_DIR = ROOT / "translate-files/佛教徒的人生态度" - -def has_cjk(s): - return any('\u4e00' <= c <= '\u9fff' for c in s) - -def pandoc(path): - r = subprocess.run(['pandoc', path, '-f', 'docx', '-t', 'plain', '--wrap=none'], - capture_output=True, text=True) - return r.stdout - -def extract_pairs(text): - """Return [(cn_para, en_para), ...] from body onwards.""" - lines = text.split('\n') - body_start = None - for i, l in enumerate(lines): - if '生活在这个世间' in l: - body_start = i - break - pairs = [] - i = body_start - while i < len(lines): - cn = lines[i].strip() - if not cn or not has_cjk(cn): - i += 1 - continue - en = '' - if i + 2 < len(lines) and lines[i+1].strip() == '': - ec = lines[i+2].strip() - if ec and not has_cjk(ec): - en = ec - i += 3 - else: - i += 1 - else: - i += 1 - continue - pairs.append((cn, en)) - return pairs - -SANSKRIT = [ - 'bodhisattva', 'bodhicitta', 'samsara', 'Dharma', 'karma', - 'nirvana', 'Sangha', 'sutra', 'Mahayana', 'Sravaka', - 'Vinaya', 'Lamrim', 'Ksitigarbha', 'Samantabhadra', - 'Chan', 'Arhatship', 'Theravada', -] - -def apply_fixes(en_text, italicized): - """Apply typesetting fixes to English text.""" - # Fix: "2.How" → "2. How" - en_text = re.sub(r'(\d)\.([A-Z][a-z])', r'\1. \2', en_text) - # Fix: "said,"When → "said, "When - en_text = re.sub(r'(said|says),"', r'\1, "', en_text) - # Italicize Sanskrit on first occurrence - for term in SANSKRIT: - if term not in italicized: - pattern = re.compile(r'\b' + re.escape(term) + r'\b') - m = pattern.search(en_text) - if m: - s, e = m.start(), m.end() - en_text = en_text[:s] + '*' + en_text[s:e] + '*' + en_text[e:] - italicized.add(term) - return en_text - -def generate(pairs, out_path): - italicized = set() - lines = [] - - # Title - lines.append('# 佛教徒的人生态度') - lines.append('# The Life Attitudes of Buddhists') - lines.append('') - lines.append('------2014年秋讲于第九届菩提静修营') - lines.append('---Lecture Given at the 9th Bodhi Meditation Retreat, 2014') - lines.append('') - lines.append(' 济群法师 ') - lines.append('Master Jiqun') - lines.append('') - - # TOC - lines.append('- 一、消极还是积极') - lines.append('- 二、悲观还是乐观') - lines.append('- 三、禁欲还是纵欲') - lines.append('- 四、重生还是重死') - lines.append('- 五、自利还是利他') - lines.append('- 六、出世还是入世') - lines.append('- 七、无情还是多情') - lines.append('- 八、随缘还是进取') - lines.append('- 九、结束语') - lines.append('') - lines.append('- I. Passive or Proactive') - lines.append('- II. Pessimism or Optimism') - lines.append('- III. Abstinence or Indulgence') - lines.append('- IV. Focus on Life or on Death') - lines.append('- V. Benefit Oneself or Benefit Others') - lines.append('- VI. Transcending the World or Engaging with the World') - lines.append('- VII. To Love or Not to Love') - lines.append('- VIII. Adapting to Conditions or Striving for Progress') - lines.append('- IX. Conclusion') - lines.append('') - - # Body - for cn, en in pairs: - en_fixed = apply_fixes(en, italicized) - lines.append(cn) - lines.append(en_fixed) - lines.append('') - - with open(out_path, 'w') as f: - f.write('\n'.join(lines)) - - print(f"Written: {out_path}") - print(f" Paragraphs: {len(pairs)}") - print(f" Sanskrit italicized: {sorted(italicized)}") - -if __name__ == '__main__': - print("Extracting DOCX...") - text = pandoc(DOCX) - pairs = extract_pairs(text) - print(f" Pairs: {len(pairs)}") - - out = OUT_DIR / "bilingual.dj" - generate(pairs, out) diff --git a/scripts/gen-bilingual-buddhist-attitude.py b/scripts/gen-bilingual-buddhist-attitude.py deleted file mode 100644 index aaec54a..0000000 --- a/scripts/gen-bilingual-buddhist-attitude.py +++ /dev/null @@ -1,262 +0,0 @@ -""" -Generate bilingual.dj for 佛教徒的人生态度. -Source: Chinese from DOCX manuscript. -Target: English from PDF typeset. - -Strategy: find each DOCX English paragraph in PDF body, extract -the PDF text region for that paragraph using position boundaries. -""" -import re, subprocess -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -DOCX = ROOT / "translate-files/佛教徒的人生态度/定稿 佛教徒的人生态度 善鑫慧炬照禅道靖妙一观轩慈德20260527.docx" -PDF = ROOT / "translate-files/佛教徒的人生态度/0607-二排-果澄-佛教徒的人生态度-一校-多人-0607.pdf" -OUT_DIR = ROOT / "translate-files/佛教徒的人生态度" - - -def has_cjk(s): - return any('\u4e00' <= c <= '\u9fff' for c in s) - - -def pandoc(path): - r = subprocess.run(['pandoc', path, '-f', 'docx', '-t', 'plain', '--wrap=none'], - capture_output=True, text=True) - return r.stdout - - -def extract_docx_pairs(text): - """Return [(cn_para, en_para), ...] from body onwards.""" - lines = text.split('\n') - body_start = None - for i, l in enumerate(lines): - if '生活在这个世间' in l: - body_start = i - break - pairs = [] - i = body_start - while i < len(lines): - cn = lines[i].strip() - if not cn or not has_cjk(cn): - i += 1 - continue - en = '' - if i + 2 < len(lines) and lines[i+1].strip() == '': - ec = lines[i+2].strip() - if ec and not has_cjk(ec): - en = ec - i += 3 - else: - i += 1 - else: - i += 1 - continue - pairs.append((cn, en)) - return pairs - - -def extract_pdf_body(text): - """Return cleaned PDF body string.""" - lines = text.split('\n') - body_start = None - for i, l in enumerate(lines): - if 'iving in this world' in l.strip(): - body_start = i - break - slug_re = re.compile(r'佛教徒的人生态度.*indd \d+') - hdr_re = re.compile(r'^(The Life Attitudes of Buddhists|The Mindful Peace Academy Collection)$') - pn_re = re.compile(r'^\d{1,3}$') - - tl = [] - for i in range(body_start, len(lines)): - s = lines[i].strip() - if not s or s == '\x0c': - continue - if slug_re.search(s) or hdr_re.match(s) or pn_re.match(s): - continue - tl.append(s) - - # Join hyphenation breaks — handle consecutive breaks - joined = [] - i = 0 - while i < len(tl): - line = tl[i].rstrip() - if line.endswith('-') and i + 1 < len(tl): - n = tl[i+1].lstrip() - if n and n[0].islower(): - merged = line[:-1] + n - # Check if MORE consecutive breaks follow - j = i + 2 - while j < len(tl) and merged.rstrip().endswith('-'): - nn = tl[j].lstrip() - if nn and nn[0].islower(): - merged = merged.rstrip()[:-1] + nn - j += 1 - else: - break - joined.append(merged) - i = j - continue - joined.append(line) - i += 1 - - body = ' '.join(joined) - body = re.sub(r'\s+', ' ', body).strip() - body = body.replace('L iving', 'Living') - return body - - -def norm(s): - s = re.sub(r'\s+', ' ', s).strip().lower() - s = s.replace('\u201c', '"').replace('\u201d', '"') - s = s.replace('\u2018', "'").replace('\u2019', "'") - return s - - -def find_positions(pairs, pdf_body): - """For each DOCX English para, find start position in PDF body. - Returns list of (start_pos or None, matched_text or None). - """ - positions = [] - last_pos = 0 - for cn, en in pairs: - needle = norm(en) - haystack = norm(pdf_body[last_pos:]) - - # Try full match - idx = haystack.find(needle) - if idx < 0: - # Try first 80 chars - key = needle[:80] - idx = haystack.find(key) - if idx < 0: - # Try first 40 chars - key = needle[:40] - idx = haystack.find(key) - if idx < 0: - # Try first 25 chars - key = needle[:25] - idx = haystack.find(key) - - if idx >= 0: - pos = last_pos + idx - positions.append(pos) - last_pos = pos + max(len(needle), 30) - else: - positions.append(None) - return positions - - -def extract_segments(pdf_body, positions): - """For each position, extract the PDF text region. - Region extends from positions[i] to positions[i+1] (or end), - trimmed to avoid bleeding into the next paragraph. - """ - segments = [] - for i, pos in enumerate(positions): - if pos is None: - segments.append(None) - continue - - start = pos - end = len(pdf_body) - for j in range(i + 1, len(positions)): - if positions[j] is not None: - end = positions[j] - break - - raw = pdf_body[start:end].strip() - - # Trim: if raw contains what looks like the NEXT paragraph's heading, - # cut at the last sentence boundary before it. - # Headings match patterns like: "I Passive", "1) Expressions", "1. The Definitions" - heading_pattern = re.compile( - r'\s+(?=[IVX]+\.?\s+[A-Z]' # Roman numeral chapter - r'|\d+\)\s+[A-Z]' # 1) Sub-heading - r'|\(\d+\)\s+[A-Z]' # (1) Sub-heading - r'|\d+\.\s+[A-Z][a-z]+.*?(?:Passive|Pessimism|Abstinence|Focus|Benefit|Transcending|Love|Adapting|Conclusion|Desire|Being|What|How|The|Buddhism|Set|Free|A Middle)' # Numbered heading - r')' - ) - m = heading_pattern.search(raw) - if m: - # Cut before this heading - raw = raw[:m.start()].strip() - - segments.append(raw) - return segments - - -def generate(pairs, segments, out_path): - lines = [] - - # Title - lines.append('# 佛教徒的人生态度') - lines.append('# The Life Attitudes of Buddhists') - lines.append('') - lines.append('------2014年秋讲于第九届菩提静修营') - lines.append('---Lecture Given at the 9th Bodhi Meditation Retreat, 2014') - lines.append('') - lines.append(' 济群法师 ') - lines.append('Master Jiqun') - lines.append('') - - # TOC from DOCX - lines.append('- 一、消极还是积极') - lines.append('- 二、悲观还是乐观') - lines.append('- 三、禁欲还是纵欲') - lines.append('- 四、重生还是重死') - lines.append('- 五、自利还是利他') - lines.append('- 六、出世还是入世') - lines.append('- 七、无情还是多情') - lines.append('- 八、随缘还是进取') - lines.append('- 九、结束语') - lines.append('') - lines.append('- I. Passive or Proactive') - lines.append('- II. Pessimism or Optimism') - lines.append('- III. Abstinence or Indulgence') - lines.append('- IV. Focus on Life or on Death') - lines.append('- V. Benefit Oneself or Benefit Others') - lines.append('- VI. Transcending the World or Engaging with the World') - lines.append('- VII. To Love or Not to Love') - lines.append('- VIII. Adapting to Conditions or Striving for Progress') - lines.append('- IX. Conclusion') - lines.append('') - - # Body - for (cn, en), seg in zip(pairs, segments): - target = seg if seg else en - lines.append(cn) - lines.append(target) - lines.append('') - - with open(out_path, 'w') as f: - f.write('\n'.join(lines)) - - matched = sum(1 for s in segments if s is not None) - print(f"Written: {out_path}") - print(f" Paragraphs: {len(pairs)}, matched from PDF: {matched}, fallback to DOCX: {len(pairs) - matched}") - - -if __name__ == '__main__': - print("Extracting DOCX...") - docx_text = pandoc(DOCX) - pairs = extract_docx_pairs(docx_text) - print(f" Pairs: {len(pairs)}") - - print("Extracting PDF...") - r = subprocess.run(['pdftotext', '-layout', PDF, '/tmp/_bilingual_pdf.txt'], check=True) - with open('/tmp/_bilingual_pdf.txt') as f: - pdf_raw = f.read() - pdf_body = extract_pdf_body(pdf_raw) - print(f" PDF body: {len(pdf_body)} chars") - - print("Finding positions...") - positions = find_positions(pairs, pdf_body) - found = sum(1 for p in positions if p is not None) - print(f" Found: {found}/{len(pairs)}") - - print("Extracting segments...") - segments = extract_segments(pdf_body, positions) - - out = OUT_DIR / "bilingual.dj" - generate(pairs, segments, out) diff --git a/scripts/gen-bilingual-dharma-management-e2bc35.py b/scripts/gen-bilingual-dharma-management-e2bc35.py deleted file mode 100644 index c826dea..0000000 --- a/scripts/gen-bilingual-dharma-management-e2bc35.py +++ /dev/null @@ -1,193 +0,0 @@ -"""Extract source.dj and bilingual.dj from .docx.md file. - -Handles: -- CN/EN paragraph pairs (CN line → EN line) -- Headings with merged CN+EN on same line (pandoc artifact: CN**EN) -- TOC with markdown links [CN text](#anchor) -- {#anchor} pandoc heading anchors -- Markdown heading cleanup -""" -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -SRC = ROOT / "translate-files/佛法与企业管理/副本59 佛法与企业管理-maple 初翻.docx.md" -OUT = ROOT / "translate-files/佛法与企业管理" - -def has_cjk(s): - return any('\u4e00' <= c <= '\u9fff' for c in s) - -def strip_anchors(s): - """Remove {#anchor} and markdown links [text](#anchor) — keep text.""" - s = re.sub(r'\{#[^}]*\}', '', s) # {#anchor} - s = re.sub(r'\[([^\]]*)\]\([^)]*\)', r'\1', s) # [text](#link) → text - return s - -def split_cnen(line): - """Split merged CN+EN line. First strips anchors, then finds CJK→EN boundary.""" - s = strip_anchors(line).strip() - if not has_cjk(s): - return ('', s) - # Find where CJK ends and ASCII English begins - # Pattern: CJK, optional ws, optional *, optional ws, then English letter - m = re.search(r'[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]\s*\**\s*([A-Za-z])', s) - if m: - split_at = m.start(1) - cn = s[:split_at].rstrip('* ').strip() - en = s[split_at:].strip() - if en and not has_cjk(en): - return (cn, en) - return (s, '') - -def clean_cn(s): - """Clean CN heading/paragraph.""" - s = re.sub(r'^\d+\.\s*', '', s) # leading number (e.g. "1. ") - s = re.sub(r'^#+\s*\**', '', s) # heading markers: # ** - s = re.sub(r'\**\s*$', '', s) # trailing ** - s = re.sub(r'\t\d+$', '', s) # trailing page number - s = s.strip() - return s - -def clean_en(s): - """Clean EN line.""" - s = re.sub(r'^\d+[、,.]\s*', '', s) # leading number/separator - s = re.sub(r'\*+$', '', s) # trailing orphaned italic * (from split) - s = s.strip() - return s - -def is_toc_line(line, cn_raw): - """True if this looks like a TOC entry (markdown link or tab+page-number).""" - if re.search(r'\[.*\]\(.*\)', line): - return True - if re.search(r'\t\d+', cn_raw): - return True - return False - -def parse_doc(text): - """Parse into list of (cn, en) pairs.""" - lines = text.split('\n') - pairs = [] - i = 0 - - while i < len(lines): - line = lines[i].strip() - if not line: - i += 1 - continue - - stripped = strip_anchors(line) - has_cn = has_cjk(stripped) - - if has_cn: - cn_raw, en_raw = split_cnen(line) - - if en_raw: - # Merged CN+EN on same line - pairs.append((clean_cn(cn_raw), clean_en(en_raw))) - i += 1 - continue - - # Look ahead for EN - nxt = lines[i+1].strip() if i+1 < len(lines) else '' - nnxt = lines[i+2].strip() if i+2 < len(lines) else '' - - if nxt and not has_cjk(nxt) and not is_toc_line(line, cn_raw): - # Standard: CN → EN - pairs.append((clean_cn(cn_raw), clean_en(nxt))) - i += 2 - elif not nxt and nnxt and not has_cjk(nnxt) and not is_toc_line(line, cn_raw): - # CN → blank → EN (heading pattern) - pairs.append((clean_cn(cn_raw), clean_en(nnxt))) - i += 3 - else: - # Solo CN (TOC entry, orphan, or heading) - pairs.append((clean_cn(cn_raw), '')) - i += 1 - else: - # Pure EN — TOC entry, pair with first unpaired CN TOC entry - en = clean_en(line) - for j in range(len(pairs)): - if not pairs[j][1] and has_cjk(pairs[j][0]): - pairs[j] = (pairs[j][0], en) - break - else: - pairs.append(('', en)) - i += 1 - - return pairs - -def classify(pairs): - """Classify each pair as title, subtitle, toc, heading, or para.""" - result = [] - # Pairs 0-1: title and subtitle - result.append(('title', 0)) - result.append(('subtitle', 1)) - # Pairs 2-9: TOC (一、 through 八、) - for i in range(2, min(10, len(pairs))): - result.append(('toc', i)) - # Remaining: heuristics - for i in range(10, len(pairs)): - cn = pairs[i][0] - if re.match(r'^[一二三四五六七八九十]、', cn): - result.append(('heading', i)) - elif re.match(r'^\d+\\?\.\s', cn): - result.append(('subheading', i)) - else: - result.append(('para', i)) - return result - -def write_bilingual(pairs, out_path): - types = classify(pairs) - lines = [] - # Title - lines.append('# ' + pairs[0][0]) - lines.append('# ' + pairs[0][1]) - lines.append('') - # Subtitle - lines.append('---' + pairs[1][0]) - lines.append('---' + pairs[1][1]) - lines.append('') - # Author - lines.append('济群法师') - lines.append('Master Jiqun') - lines.append('') - # TOC — CN block then EN block (not interleaved) - for typ, idx in types: - if typ == 'toc': - lines.append('- ' + pairs[idx][0]) - lines.append('') - for typ, idx in types: - if typ == 'toc': - lines.append('- ' + pairs[idx][1]) - lines.append('') - # Body - for typ, idx in types: - if typ in ('title', 'subtitle', 'toc'): - continue - cn, en = pairs[idx] - if cn: - lines.append(cn) - if en: - lines.append(en) - if cn or en: - lines.append('') - with open(out_path, 'w') as f: - f.write('\n'.join(lines)) - print(f"bilingual.dj: {len(pairs)} pairs -> {out_path}") - -def write_source(pairs, out_path): - lines = [cn for cn, en in pairs if cn] - with open(out_path, 'w') as f: - f.write('\n'.join(lines) + '\n') - print(f"source.dj: {len(lines)} CN lines -> {out_path}") - -if __name__ == '__main__': - text = SRC.read_text() - pairs = parse_doc(text) - print(f"Parsed {len(pairs)} pairs from .docx.md") - solo_cn = sum(1 for cn, en in pairs if cn and not en) - solo_en = sum(1 for cn, en in pairs if en and not cn) - both = sum(1 for cn, en in pairs if cn and en) - print(f" Both: {both}, CN-only: {solo_cn}, EN-only: {solo_en}") - write_source(pairs, OUT / "source.dj") - write_bilingual(pairs, OUT / "bilingual.dj") diff --git a/scripts/gen-bilingual-life-design-de8422.py b/scripts/gen-bilingual-life-design-de8422.py deleted file mode 100644 index 75bc370..0000000 --- a/scripts/gen-bilingual-life-design-de8422.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Generate bilingual.dj from DOCX for 「生命也可以被设计的」. -One-pass approach: walk interleaved paragraphs, handle multi-CN sequences. -""" -import re, subprocess -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -DOCX = ROOT / "translate-files/生命也可以被设计的/中英文定稿-260324-生命也是可以被设计的-妙一宽山静雅初翻 慈鎏妙一审议 宽山定稿.docx" -OUT_DIR = ROOT / "translate-files/生命也可以被设计的" - -def has_cjk(s): - return any('\u4e00' <= c <= '\u9fff' for c in s) - -def pandoc(path): - r = subprocess.run(['pandoc', path, '-f', 'docx', '-t', 'plain', '--wrap=none'], - capture_output=True, text=True) - return r.stdout - -def split_toc_line(line): - s = line.strip() - s = re.sub(r'\s+\d+\s*$', '', s) - m = re.match(r'^(.+[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef)\)])\s+([A-Z].+)$', s) - if m: - return m.group(1).strip(), m.group(2).strip() - return None, None - -def extract_toc_entries(text): - lines = text.split('\n') - toc_start = None - toc_end = None - for i, l in enumerate(lines): - s = l.strip() - if s.startswith('一、') and ('EDUCATION' in s or 'NURTURING' in s): - if toc_start is None: - toc_start = i - if toc_start is not None and s and has_cjk(s) and re.search(r'\d+$', s): - toc_end = i - elif toc_start is not None and toc_end is not None and s and not re.search(r'\d+$', s) and has_cjk(s): - break - - cn_entries = [] - en_entries = [] - for i in range(toc_start, toc_end + 1): - cn, en = split_toc_line(lines[i]) - if cn and en: - cn_entries.append(cn) - en_entries.append(en) - return cn_entries, en_entries - -def extract_body_pairs(text): - """One-pass: walk interleaved paras, joining consecutive same-language lines.""" - lines = text.split('\n') - - # Find body start - body_start = None - for i, l in enumerate(lines): - if '现在是一个浮躁的时代' in l: - body_start = i - break - - # Extract non-blank paragraphs with language tags - tagged = [] - for l in lines[body_start:]: - s = l.strip() - if s: - tagged.append(('cn' if has_cjk(s) else 'en', s)) - - # Accumulate consecutive same-language paragraphs (page-break splits only, not headings) - merged = [] - for lang, text in tagged: - # Heading-like patterns that should not be merged - prev_is_heading = (merged and merged[-1][0] == lang - and bool(re.match(r'^[\dIVX]+[\.\s]', merged[-1][1].strip()) - and len(merged[-1][1].strip()) < 60)) - if (merged and merged[-1][0] == lang - and not prev_is_heading - and len(merged[-1][1]) > 30 - and not re.search(r'[。!?:)\u201d\u2019\uff0c\uff0e\.!\?]$', merged[-1][1])): - # Long previous line, doesn't end naturally → page-break split, join - merged[-1] = (lang, merged[-1][1].rstrip() + text.lstrip()) - else: - merged.append((lang, text)) - - # Build pairs: group consecutive same-language items into blocks, then zip - blocks = [] - for lang, text in merged: - if blocks and blocks[-1][0] == lang: - blocks[-1][1].append(text) - else: - blocks.append((lang, [text])) - - pairs = [] - i = 0 - while i < len(blocks): - if blocks[i][0] == 'cn': - cn_block = blocks[i][1] - # Find next EN block - if i + 1 < len(blocks) and blocks[i+1][0] == 'en': - en_block = blocks[i+1][1] - n = min(len(cn_block), len(en_block)) - for j in range(n): - pairs.append((cn_block[j], en_block[j])) - if len(cn_block) != len(en_block): - print(f" WARNING: block mismatch CN={len(cn_block)} EN={len(en_block)} at CN[{j}]: {cn_block[j][:60]}...") - i += 2 - else: - print(f" WARNING: CN block without EN block: {cn_block[0][:60]}...") - i += 1 - else: - print(f" WARNING: orphan EN block: {blocks[i][1][0][:60]}...") - i += 1 - - return pairs - -SANSKRIT = [ - 'bodhisattva', 'bodhicitta', 'samsara', 'Dharma', 'karma', - 'nirvana', 'Sangha', 'sutra', 'Mahayana', 'Sravaka', - 'Vinaya', 'Lamrim', 'Ksitigarbha', 'Samantabhadra', - 'Chan', 'Arhatship', 'Theravada', 'buddha', 'Buddha', - 'buddhas', 'Buddhas', 'Bodhisattva', 'Bodhisattvas', -] - -def apply_fixes(en_text, italicized): - en_text = re.sub(r'(\d)\.([A-Z][a-z])', r'\1. \2', en_text) - en_text = re.sub(r'(said|says),\"', r'\1, "', en_text) - en_text = re.sub(r'\.([A-Z][a-z])', r'. \1', en_text) - for term in SANSKRIT: - if term not in italicized: - pattern = re.compile(r'\b' + re.escape(term) + r'\b') - m = pattern.search(en_text) - if m: - s, e = m.start(), m.end() - en_text = en_text[:s] + '*' + en_text[s:e] + '*' + en_text[e:] - italicized.add(term) - return en_text - -def generate(toc_cn, toc_en, pairs, out_path): - italicized = set() - lines = [] - - lines.append('# 生命也是可以被设计的') - lines.append('# Life Can Also Be Designed') - lines.append('') - lines.append('济群法师 2025年冬为母爱书院开示') - lines.append('A teaching given by the Master Jiqun in the winter of 2025 at Amrita Retreat Center for Motherly Love Academy') - lines.append('') - - for e in toc_cn: - lines.append(f'- {e}') - lines.append('') - for e in toc_en: - lines.append(f'- {e}') - lines.append('') - - for cn, en in pairs: - en_fixed = apply_fixes(en, italicized) - lines.append(cn) - lines.append(en_fixed) - lines.append('') - - with open(out_path, 'w') as f: - f.write('\n'.join(lines)) - - print(f"Written: {out_path}") - print(f" TOC entries: {len(toc_cn)}") - print(f" Body pairs: {len(pairs)}") - print(f" Sanskrit italicized: {sorted(italicized)}") - -if __name__ == '__main__': - print("Extracting DOCX...") - text = pandoc(DOCX) - - print("Extracting TOC...") - toc_cn, toc_en = extract_toc_entries(text) - for cn, en in zip(toc_cn, toc_en): - print(f" {cn} → {en}") - - print("Extracting body...") - pairs = extract_body_pairs(text) - print(f" Pairs: {len(pairs)}") - - out = OUT_DIR / "bilingual.dj" - generate(toc_cn, toc_en, pairs, out) diff --git a/scripts/gen-bilingual.py b/scripts/gen-bilingual.py deleted file mode 100755 index 6859cc0..0000000 --- a/scripts/gen-bilingual.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -"""Generate a bilingual .dj file from source (Chinese) and target (English) .dj files. - -Usage: - gen-bilingual.py source.dj target.dj > bilingual.dj - -Output format: source line, target line, blank line, repeated. Paragraph breaks -are preserved: blank lines in the input produce blank lines in the output. -""" - -import sys -from pathlib import Path - - -def main(): - if len(sys.argv) != 3: - print(__doc__, file=sys.stderr) - sys.exit(1) - - src_path = Path(sys.argv[1]) - tgt_path = Path(sys.argv[2]) - - if not src_path.exists(): - print(f"Source file not found: {src_path}", file=sys.stderr) - sys.exit(1) - if not tgt_path.exists(): - print(f"Target file not found: {tgt_path}", file=sys.stderr) - sys.exit(1) - - src_lines = src_path.read_text(encoding="utf-8").splitlines() - tgt_lines = tgt_path.read_text(encoding="utf-8").splitlines() - - if len(src_lines) != len(tgt_lines): - print( - f"Line count mismatch: source={len(src_lines)} target={len(tgt_lines)}", - file=sys.stderr, - ) - sys.exit(1) - - out = [] - for s, t in zip(src_lines, tgt_lines): - if s == "": - out.append("") - else: - out.append(s) - out.append(t) - out.append("") - - # Ensure the output always ends with a single trailing blank line to match - # the project convention: source, target, blank, source, target, blank... - if out and out[-1] != "": - out.append("") - - sys.stdout.write("\n".join(out)) - if out: - sys.stdout.write("\n") - - -if __name__ == "__main__": - main() diff --git a/scripts/proofread-pdf.py b/scripts/proofread-pdf.py deleted file mode 100644 index 03ba39d..0000000 --- a/scripts/proofread-pdf.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Compare manuscript (DOCX English body) against typeset (PDF English body). -Usage: python3 scripts/proofread-pdf.py -Output: sentences from DOCX not found in PDF, and word-level changes within matched sentences. -""" -import re, sys, subprocess - - -def extract_docx_en(path): - with open(path) as f: - lines = f.readlines() - - body_start = None - for i, line in enumerate(lines): - if '生活在这个世间' in line: - body_start = i - break - if body_start is None: - sys.exit("Could not find body start in DOCX") - - docx_en = [] - skip_next = 0 - for i in range(body_start, len(lines)): - if skip_next > 0: - skip_next -= 1 - continue - line = lines[i].strip() - if not line: - continue - has_cjk = any('\u4e00' <= c <= '\u9fff' for c in line) - if has_cjk: - if i + 1 < len(lines) and lines[i+1].strip() == '': - if i + 2 < len(lines): - en_line = lines[i+2].strip() - if en_line and not any('\u4e00' <= c <= '\u9fff' for c in en_line): - docx_en.append(en_line) - skip_next = 2 - else: - docx_en.append(line) - - # Split into sentences, filter out headings - text = ' '.join(docx_en) - sentences = re.split(r'(?<=[.!?"”])\s+', text) - return [(s.strip(), len(s.strip())) for s in sentences if len(s.strip()) >= 20] - - -def extract_pdf_en(path): - with open(path) as f: - lines = f.readlines() - - body_start = None - for i, line in enumerate(lines): - if 'iving in this world' in line: - body_start = i - break - if body_start is None: - sys.exit("Could not find body start in PDF") - - slug_re = re.compile(r'佛教徒的人生态度.*indd \d+') - header_re = re.compile(r'^(The Life Attitudes of Buddhists|The Mindful Peace Academy Collection)$') - page_num_re = re.compile(r'^\d{1,3}$') - - text_lines = [] - for i in range(body_start, len(lines)): - s = lines[i].strip() - if not s or s == '\x0c': - continue - if slug_re.search(s) or header_re.match(s) or page_num_re.match(s): - continue - text_lines.append(s) - - # Join hyphenated breaks - joined = [] - i = 0 - while i < len(text_lines): - line = text_lines[i] - if line.rstrip().endswith('-') and i + 1 < len(text_lines): - n = text_lines[i+1].lstrip() - if n and n[0].islower(): - joined.append(line.rstrip()[:-1] + n) - i += 2 - continue - joined.append(line) - i += 1 - - body = ' '.join(joined) - body = re.sub(r'\s+', ' ', body).strip() - body = body.replace('L iving', 'Living') - return body - - -def normalize_for_search(s): - """Normalize text for fuzzy matching.""" - s = re.sub(r'\s+', ' ', s).strip().lower() - # Normalize quotes - s = s.replace('\u201c', '"').replace('\u201d', '"') - s = s.replace('\u2018', "'").replace('\u2019', "'") - return s - - -def find_sentence_in_pdf(sentence, pdf_body): - """Try to locate sentence in PDF body. Returns (found, matched_text).""" - s_norm = normalize_for_search(sentence) - # Try full sentence - if s_norm in pdf_body.lower(): - return True, sentence - # Try first 60 chars - key = s_norm[:60] - if key in pdf_body.lower(): - return True, sentence - # Try first 30 chars - key = s_norm[:30] - if key in pdf_body.lower(): - return True, sentence - return False, None - - -def find_word_diff(docx_sentence, pdf_sentence): - """Find word-level differences between two matched sentences.""" - if not pdf_sentence: - return [] - dw = re.findall(r'\S+', docx_sentence) - pw = re.findall(r'\S+', pdf_sentence) - diffs = [] - for dwi, pwi in zip(dw, pw): - if dwi.lower() != pwi.lower(): - diffs.append((dwi, pwi)) - if len(dw) != len(pw): - diffs.append((f"[{len(dw)} words]", f"[{len(pw)} words]")) - return diffs - - -if __name__ == '__main__': - if len(sys.argv) != 3: - sys.exit("Usage: proofread-pdf.py ") - - docx_path, pdf_path = sys.argv[1], sys.argv[2] - docx_txt = '/tmp/proofread_docx.txt' - pdf_txt = '/tmp/proofread_pdf.txt' - - subprocess.run(['pandoc', docx_path, '-f', 'docx', '-t', 'plain', '--wrap=none', '-o', docx_txt], check=True) - subprocess.run(['pdftotext', '-layout', pdf_path, pdf_txt], check=True) - - docx_sentences = extract_docx_en(docx_txt) - pdf_body = extract_pdf_en(pdf_txt) - pdf_normalized = normalize_for_search(pdf_body) - - missing = [] - found_count = 0 - for sentence, length in docx_sentences: - s_norm = normalize_for_search(sentence) - if s_norm in pdf_normalized: - found_count += 1 - elif s_norm[:60] in pdf_normalized: - found_count += 1 - elif s_norm[:30] in pdf_normalized: - found_count += 1 - else: - missing.append(sentence) - - print(f"DOCX body sentences: {len(docx_sentences)}") - print(f"Matched in PDF: {found_count}") - print(f"Missing: {len(missing)}") - print() - - if missing: - print("=== Sentences from DOCX NOT found in PDF ===") - for i, s in enumerate(missing): - print(f"\n--- Missing #{i+1} ---") - print(s[:200]) diff --git a/scripts/split-bilingual.fish b/scripts/split-bilingual.fish deleted file mode 100755 index be09c2a..0000000 --- a/scripts/split-bilingual.fish +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env fish -# Split combined bilingual .dj into source.dj (CN) and target.dj (EN) -# Usage: split-bilingual.fish -# Output: source.dj and target.dj in same directory, paragraphs separated by blanks - -set dj (realpath $argv[1]) -set dir (dirname $dj) - -rm -f "$dir/source.dj" "$dir/target.dj" - -for line in (cat $dj) - if string match -qr '[\x{4e00}-\x{9fff}]' -- $line - echo $line >> "$dir/source.dj" - echo >> "$dir/source.dj" - else if test -n (string trim -- $line) - echo $line >> "$dir/target.dj" - echo >> "$dir/target.dj" - end -end - -echo "source.dj: $dir/source.dj" -echo "target.dj: $dir/target.dj" diff --git a/scripts/ten-elements-c7fcd9.py b/scripts/ten-elements-c7fcd9.py deleted file mode 100644 index ebd7370..0000000 --- a/scripts/ten-elements-c7fcd9.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Extract cleaned English body from DOCX manuscript and typeset PDF. -Usage: python3 ten-elements-c7fcd9.py -Output: two cleaned text files in /tmp/ for diffing. -""" -import re, sys, subprocess -from pathlib import Path - -DOCX_TXT = '/tmp/ten_elements_docx_body.txt' -PDF_TXT = '/tmp/ten_elements_pdf_body.txt' - - -def extract_docx_body(path): - with open(path) as f: - lines = f.readlines() - for i, line in enumerate(lines): - if 'The Dhyana Tea program team' in line: - body_start = i - break - else: - sys.exit("Could not find body start in DOCX") - body = [l.strip() for l in lines[body_start:] if l.strip()] - return '\n'.join(body) - - -def extract_pdf_body(path): - with open(path) as f: - lines = f.readlines() - - slug_re = re.compile(r'正念禅修十要素.*indd \d+') - header_re = re.compile( - r'^(The Mindful Peace Academy Collection|The Ten Key Elements of Mindfulness Meditation)$' - ) - page_re = re.compile(r'^\d{1,3}$') - skip_re = re.compile( - r'^(I|II|III|IV|Three Basic Elements|The Three Key Elements of Samatha|' - r'The Four Key Elements of Vipassana|Conclusion|Contents)$' - ) - - for i, line in enumerate(lines): - if 'Dhyana Tea program team' in line.strip(): - body_start = i - break - else: - sys.exit("Could not find body start in PDF") - - raw = [] - for line in lines[body_start:]: - s = line.strip() - if not s or s == '\x0c': - continue - if slug_re.search(s) or header_re.match(s) or page_re.match(s) or skip_re.match(s): - continue - raw.append(s) - - # Join hyphenated line breaks - joined = [] - i = 0 - while i < len(raw): - line = raw[i] - if line.rstrip().endswith('-') and i + 1 < len(raw): - nxt = raw[i + 1].lstrip() - if nxt and nxt[0].islower(): - joined.append(line.rstrip()[:-1] + nxt) - i += 2 - continue - joined.append(line) - i += 1 - - body = ' '.join(joined) - body = re.sub(r'\s+', ' ', body).strip() - body = body.replace('L iving', 'Living') - body = re.sub(r'T\s+he\b', 'The', body) - return body - - -if __name__ == '__main__': - if len(sys.argv) != 3: - sys.exit(f"Usage: {Path(__file__).name} ") - - docx_path, pdf_path = sys.argv[1], sys.argv[2] - - subprocess.run( - ['pandoc', docx_path, '-f', 'docx', '-t', 'plain', '--wrap=none', - '-o', '/tmp/_docx_raw.txt'], check=True - ) - subprocess.run( - ['pdftotext', '-layout', pdf_path, '/tmp/_pdf_raw.txt'], check=True - ) - - docx_body = extract_docx_body('/tmp/_docx_raw.txt') - pdf_body = extract_pdf_body('/tmp/_pdf_raw.txt') - - Path(DOCX_TXT).write_text(docx_body) - Path(PDF_TXT).write_text(pdf_body) - - print(f"DOCX body → {DOCX_TXT} ({len(docx_body)} chars)") - print(f"PDF body → {PDF_TXT} ({len(pdf_body)} chars)") diff --git a/skills/chinese-text-normalize/SKILL.md b/skills/chinese-text-normalize/SKILL.md deleted file mode 100644 index aa576f4..0000000 --- a/skills/chinese-text-normalize/SKILL.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -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. ---- - -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. - -## Triggers - -- User asks to "fix line breaks" or "remove extraneous breaks" in Chinese text -- Chinese markdown files with lines that break mid-sentence at a consistent short width -- Files with vertical TOC (single-char-per-line 【】 sections) that need preservation - -## Approach - -Run `scripts/normalize_breaks.py ` — it processes all .md files in the directory. - -The script handles three file patterns: - -1. **Vertical TOC + fixed-width body** — Preserves the decorative single-char TOC section, joins body paragraphs, strips inline page numbers (standalone digits like "3", "4") -2. **Outline TOC with stray breaks** — Preserves numbered outline items (一、...、1、...、...... separators), joins body paragraphs -3. **Already in paragraph format** — No change (safe to run idempotently) - -### What it preserves - -- Vertical TOC: single CJK/punctuation lines with 【】 brackets -- Section headers: 【...】、## ...、# ...、一、二、三、...、1、2、3、... -- Outline TOC entries: short numbered lines, lines with ...... separators -- Blank lines as paragraph separators - -### What it removes - -- Mid-sentence hard line breaks (joins consecutive CJK body lines) -- Inline page numbers (standalone 1-2 digit lines) -- Trailing blank lines - -## Beyond the script: bold fragments, conjoined paragraphs, encoding - -The script handles simple fixed-width body text. Some PDF→markdown conversions produce more complex artifacts that need manual multi-pass Python scripts via `execute_code`: - -### Bold marker fragmentation - -`**...text...**` blocks split across blank lines with stray `**` at fragment boundaries: - -``` -**第三条 特色——依据五大要素,构建次第修学。营造良好氛围,提供有效** - -引导。 -``` - -**Fix**: Join fragments, remove stray `**` from join point, add closing `**` to final result. See `references/bold-fragments.md` for full pattern catalog and multi-pass workflow. - -**Critical pitfall**: Do NOT join lines where BOTH the first and second line are complete bold blocks (start+end with `**`). These are separate entries, not fragments: -``` -**第一条 ...之道。** ← complete bold item - ← blank line -**第二条 ...合一。** ← complete bold item (DON'T JOIN) -``` - -### Conjoined paragraphs - -Separate paragraphs/sections merged into one line — opposite problem to the script. Common in song lyrics, dense instructional sections. Requires semantic splitting. See `references/bold-fragments.md`. - -### Encoding artifacts - -`川` (U+5DDD) replacing `"` (curly quote) — search-and-replace: `" 道理川` → `"道理"`, `" 自己的川` → `"自己的"`. - -### Multi-pass approach - -1. **Pass 1**: Join word fragments split by blank lines (conservative — only when current line doesn't end with `。!?` or is NOT a complete bold block) -2. **Pass 2**: Split obviously conjoined paragraphs (manual string replacements for known patterns) -3. **Pass 3**: Fix stray bold markers, encoding artifacts, stray page numbers -4. Verify after each pass; revert with `git checkout` if over-aggressive - -### Heuristic pitfalls - -- **Short-line join** (< 15 chars): Over-joins section headers with body, Q&A pairs (`正念是什么?\n\n就是...`). Only use for clear word-fragment continuations. -- **Bold-end join**: Lines ending with `**` are ambiguous — either broken bold fragment or complete bold item. Check if the content before `**` forms a complete sentence (ends with `。`). - -## Pitfalls - -- **TOC detection boundaries**: The vertical TOC end is detected by finding the first line with 3+ CJK characters. If a page number like "2" sits between TOC and body, it lands in the TOC section — harmless but visible. -- **Section headers without markers**: Plain-text section titles (e.g., "生命可以被设计的依据") without 【】 or number prefixes won't be detected as headers. They'll form standalone paragraphs separated by blank lines, which is fine as long as blank lines exist around them. -- **Wiki-link TOC files**: Files like a course index with [[wiki links]] are NOT prose and should be excluded. The script has no special handling — skip those files manually or restore from git. -- **Not for mixed CJK/English prose**: The script treats any line with CJK characters as body text. Mixed-language documents may need manual review. diff --git a/skills/chinese-text-normalize/references/bold-fragments.md b/skills/chinese-text-normalize/references/bold-fragments.md deleted file mode 100644 index e1fc972..0000000 --- a/skills/chinese-text-normalize/references/bold-fragments.md +++ /dev/null @@ -1,110 +0,0 @@ -# Bold fragments & conjoined paragraphs — fix patterns - -From session fixing `静心学堂学员手册.md` (1575→1478 lines, ~100 fixes). - -## Pattern A: Bold marker fragmentation - -**Problem**: `**...text...**` block split across blank line with stray `**` markers: - -``` -**第三条 特色——依据五大要素,构建次第修学。营造良好氛围,提供有效** - -引导。 -``` - -**Detection**: Line ends with `**`, next non-blank line continues the sentence (does NOT start with `**`). - -**Fix** (Python): -```python -# curr ends with **, nxt is continuation (no leading **) -curr_fixed = curr.rstrip()[:-2].rstrip() # strip trailing ** -nxt_fixed = nxt.lstrip() -if nxt_fixed.endswith('**'): - nxt_fixed = nxt_fixed[:-2].rstrip() - joined = curr_fixed + nxt_fixed + '**' -else: - joined = curr_fixed + nxt_fixed # lost closing ** — may need manual fix -``` - -### Anti-pattern: Complete bold items - -Do NOT join when BOTH lines are complete bold blocks (start+end with `**`): - -``` -**第一条 ...之道。** ← DON'T JOIN - ← blank line -**第二条 ...合一。** ← DON'T JOIN -``` - -**Detection**: Both `curr` and `nxt` start with `**` and end with `**`. - -## Pattern B: Conjoined paragraphs (Type 2) - -Separate sections merged into one line. Common cases: - -### Section headers merged with body -``` -导言:这本指引怎么用这本指引是什么这是一本修学地图... -``` -→ Split into: -``` -导言:这本指引怎么用 - -这本指引是什么 - -这是一本修学地图... -``` - -### Song titles merged mid-lyrics -``` -...生生世世不再久违《菩提花开》如果你渴求一滴水... -``` -→ Split into: -``` -...生生世世不再久违 - -### 《菩提花开》 - -如果你渴求一滴水... -``` - -### List items merged into one line -``` -不在班级群发布...不从事违法活动不在班级平台拉拢... -``` -→ Split into bullet list: -``` -- 不在班级群发布... -- 不从事违法活动 -- 不在班级平台拉拢... -``` - -**Approach**: Manual string replacements for known patterns. Regex is unreliable for semantic splits. - -## Pattern C: Stray page numbers - -Standalone digits at line ends, often from PDF page number artifacts: -- `42`, `43`, `46`, `47` at end of content lines - -**Fix**: Strip trailing digits that aren't part of dates, durations, or course numbers. - -## Pattern D: Encoding artifacts - -`川` (U+5DDD) replacing curly quotes `"` (U+201C/U+201D): -``` -把" 道理川变成" 自己的川 → 把"道理"变成"自己的" -``` - -**Fix**: Replace `" 道理川` → `"道理"`, `" 自己的川` → `"自己的"`. - -## Multi-pass workflow - -1. **Pass 1 — Join word fragments**: Scan for lines split by blank line where first line doesn't end with `。!?` and neither line is structural (header/list/table). Skip complete bold items. -2. **Pass 2 — Split conjoined**: Apply known string replacements for merged sections, song transitions, list items. -3. **Pass 3 — Clean artifacts**: Fix stray `**` markers, encoding issues, stray page numbers. -4. **Verify**: `git diff` after each pass; `git checkout` if over-aggressive. - -## Rejected heuristics - -- **Short-line join** (< 15 chars): Over-joins section headers (`中级和高级(以后的事)`) with body, and Q&A pairs (`正念是什么?\n\n就是...`). Only use for clear mid-word fragments. -- **Blind `**` stripping**: Removes valid bold formatting from complete bold items. diff --git a/skills/chinese-text-normalize/scripts/normalize_breaks.py b/skills/chinese-text-normalize/scripts/normalize_breaks.py deleted file mode 100644 index 8c5f3c4..0000000 --- a/skills/chinese-text-normalize/scripts/normalize_breaks.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -Fix extraneous line breaks in Chinese markdown files. - -Three file patterns: -1. Fixed-width body text (20-25 chars/line) + vertical TOC -> join lines, remove page nums -2. Mostly-paragraph with stray breaks + outline TOC -> join broken lines, preserve list items -3. Already fine -> skip (idempotent) - -Usage: python3 normalize_breaks.py -""" -import re -import sys -from pathlib import Path - -CJK = re.compile(r'[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]') -CN_PUNCT = ',。!?;:、""''()《》【】…—~·' -NUM_MARKER = re.compile(r'^[一二三四五六七八九十]+[、,,]') -DIGIT_MARKER = re.compile(r'^\d+[、.,]') -TOC_SEP = re.compile(r'\.{3,}') # "......" separators in outline TOCs - - -def has_cjk(s): - return bool(CJK.search(s)) - - -def is_page_num(line): - s = line.strip() - return s and s.isdigit() and len(s) <= 2 - - -def is_toc_line(line): - """Vertical TOC: single char, or 【, 】, ·, or solo digit""" - s = line.strip() - if not s: - return False - if len(s) == 1 and (has_cjk(s) or s in CN_PUNCT or s in '【】·' or s.isdigit()): - return True - return False - - -def is_section_header(line): - """Section headers: 【...】, ## ..., # ..., 一、..., 1、..., or standalone title lines""" - s = line.strip() - if not s: - return False - if s.startswith('【') and s.endswith('】'): - return True - if s.startswith('#'): - return True - if NUM_MARKER.match(s): - return True - if DIGIT_MARKER.match(s): - return True - return False - - -def is_outline_toc_line(line): - """Outline/list TOC: entries separated by ...... or short numbered items""" - s = line.strip() - if TOC_SEP.search(s): - return True - m = re.match(r'^(\d+[.、,]|[一二三四五六七八九十]+[、,])\s*\S', s) - if m and len(s) < 30: - return True - return False - - -def find_toc_end(lines): - """Find where the vertical TOC section ends and body text begins.""" - for i, line in enumerate(lines): - s = line.strip() - if has_cjk(s) and len([c for c in s if has_cjk(c)]) >= 3: - j = i - while j > 0 and not lines[j - 1].strip(): - j -= 1 - return j - return 0 - - -def process_body(lines): - """Join body text lines into paragraphs, preserving section headers and outline items.""" - result = [] - buf = [] - - def flush(): - nonlocal buf - if buf: - joined = ''.join(buf) - result.append(joined) - buf = [] - - for line in lines: - s = line.strip() - - if not s: - flush() - result.append('') - continue - - if is_section_header(s): - flush() - result.append(s) - continue - - if is_outline_toc_line(s): - flush() - result.append(s) - continue - - if is_page_num(s): - continue - - if has_cjk(s) or (buf and s): - buf.append(s) - else: - flush() - result.append(s) - - flush() - return result - - -def process_file(filepath): - content = filepath.read_text(encoding='utf-8') - lines = content.split('\n') - - toc_end = find_toc_end(lines) - - if toc_end > 10: - toc_part = lines[:toc_end] - body_part = lines[toc_end:] - body_processed = process_body(body_part) - new_lines = toc_part + body_processed - else: - new_lines = process_body(lines) - - cleaned = [] - prev_blank = False - for line in new_lines: - is_blank = line.strip() == '' - if is_blank and prev_blank: - continue - cleaned.append(line) - prev_blank = is_blank - - while cleaned and cleaned[-1] == '': - cleaned.pop() - - new_content = '\n'.join(cleaned) + '\n' - - if new_content != content: - filepath.write_text(new_content, encoding='utf-8') - return True - return False - - -def main(): - workdir = Path(sys.argv[1]) - files = sorted(workdir.glob('*.md')) - - for f in files: - changed = process_file(f) - status = 'FIXED' if changed else 'OK' - print(f'{status}: {f.name}') - - -if __name__ == '__main__': - main() diff --git a/skills/pdf-to-docx-conversion/SKILL.md b/skills/pdf-to-docx-conversion/SKILL.md deleted file mode 100644 index ba0a5f9..0000000 --- a/skills/pdf-to-docx-conversion/SKILL.md +++ /dev/null @@ -1,322 +0,0 @@ ---- -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 -license: MIT -platforms: [linux, macos, windows] -metadata: - hermes: - tags: [PDF, DOCX, Documents, python-docx, pymupdf] ---- - -# 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. - -## Prerequisites - -```bash -pip install pymupdf python-docx -``` - -## 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 - -## Quick Start - -```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: - -```bash -python3 -c " -from docx import Document -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}') -" -``` - -Check for: -1. [ ] All sections present (count paragraphs) -2. [ ] No merged verses or lists -3. [ ] Headers are bold + larger size -4. [ ] Bullets use `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 - -## 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 | - -## Full Example Script - -See `scripts/convert_pdf_to_docx.py` for a production-ready converter with all patterns pre-configured. - -```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 -``` diff --git a/skills/pdf-to-docx-conversion/scripts/convert_pdf_to_docx.py b/skills/pdf-to-docx-conversion/scripts/convert_pdf_to_docx.py deleted file mode 100644 index 6acd926..0000000 --- a/skills/pdf-to-docx-conversion/scripts/convert_pdf_to_docx.py +++ /dev/null @@ -1,549 +0,0 @@ -#!/usr/bin/env python3 -""" -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 - -Requires: pip install pymupdf python-docx -""" - -import sys -import re -import pymupdf -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 - - -# ══════════════════════════════════════════════════════════════════════════════════════ -# Config —— tweak these for your PDF's style conventions -# ═════════════════════════════════════════════════════════════════════════════════════ - -DEFAULT_CONFIG = { - # Font names used for different roles - "fonts": { - "title": "STHeitiSC-Medium", - "body": "HYShuSongErKW", - "page_num": "HelveticaNeue", - }, - # Size thresholds (pt) for paragraph classification - "thresholds": { - "section_header": 18, # —— bold, centered: 【法义】 【思考】 【练习】 - "sub_header": 14, # —— bold: 一、认识感恩, 【使用说明】 - "body": 12, - }, - # Fonts to treat as bullets (skipped as glyphs, trigger List Bullet style) - "bullet_fonts": ["Wingdings", "Wingdings 2", "Wingdings 3", "Symbol"], - # Fonts to skip entirely (page numbers, decorative markers) - "skip_fonts": ["HelveticaNeue"], - "skip_size_max": 9.5, - # Numbered-list delimiters in the PDF text - "numbered_patterns": [ - r'^\d+\)', # 1) 2) 3) - r'^\d+\.\s*', # 1. 2. 3. - r'^\d+)', # 1) 2) 3) (full-width parens) - r'^\d+、', # 1、 2、 3、 (ideographic comma) - ], - # Paragraph grouping: maximum Y-gap (pt) between lines to keep in same paragraph - "y_gap_threshold": 20, - # Indentation for body/numbered items (cm) - "indent_body": 0.8, - # Verse markers —— used to split merged poetic lines - "verse_markers": [ - r'感恩(?!恩)', # 感恩 (not followed by 恩) - r'愿我们', # 愿我们 - r'更愿', # 更愿 - r'愿人们', # 愿人们 - r'愿世界', # 愿世界 - ], -} - - -# ══════════════════════════════════════════════════════════════════════════════════════ -# DOCX helpers -# ═══════════════════════════════════════════════════════════════════════════════════════ - -def set_east_asian_font(run, name: str): - """Set CJK/RTL font properly in python-docx (East Asian + ascii + hAnsi).""" - run.font.name = name - 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) - for attr in ('eastAsia', 'ascii', 'hAnsi'): - rFonts.set(qn(f'w:{attr}'), name) - - -def _mk_color(val: int) -> RGBColor | None: - if val and val != 0: - return RGBColor((val >> 16) & 0xFF, (val >> 8) & 0xFF, val & 0xFF) - return None - - -def _is_numbered(text: str, cfg: dict) -> bool: - return any(re.match(pat, text) for pat in cfg["numbered_patterns"]) - - -def _is_bullet_font(font: str, cfg: dict) -> bool: - return any(b in font for b in cfg["bullet_fonts"]) - - -def _skip_span(span: dict, cfg: dict) -> bool: - """True if this span should be dropped entirely.""" - if span["font"] in cfg["skip_fonts"] and span["size"] <= cfg["skip_size_max"]: - return True - if _is_bullet_font(span["font"], cfg): - return False # bullets are handled upstream - return False - - -# ══════════════════════════════════════════════════════════════════════════════════════ -# Extraction -# ════════════════════════════════════════════════════════════════════════════════════════════ - -def extract_lines(pdf_path: str, cfg: dict) -> list[dict]: - """Return flattened list of text lines with style info.""" - doc_pdf = pymupdf.open(pdf_path) - lines = [] - for pi in range(len(doc_pdf)): - page = doc_pdf[pi] - for block in page.get_text("dict")["blocks"]: - if block["type"] != 0: - continue # skip images - for line in block["lines"]: - spans = line["spans"] - if not spans: - continue - - # Detect bullet: first span is Wingdings / Symbol - is_bullet = _is_bullet_font(spans[0]["font"], cfg) - - # Dominant span for style (skip Wingdings glyph) - dom = spans[1] if (is_bullet and len(spans) > 1) else spans[0] - - # Skip decorative spans entirely - if _skip_span(dom, cfg): - continue - - text = "".join(s["text"] for s in spans) - lines.append({ - "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_bullet, - }) - return lines - - -def extract_images(pdf_path: str, out_dir: str = "/tmp") -> list[str]: - """Extract all embedded images from PDF. Returns list of file paths.""" - doc = pymupdf.open(pdf_path) - paths = [] - for pi in range(len(doc)): - page = doc[pi] - for idx, img in enumerate(page.get_images()): - xref = img[0] - base = doc.extract_image(xref) - path = f"{out_dir}/pdf_img_p{pi}_{idx}.{base['ext']}" - with open(path, "wb") as f: - f.write(base["image"]) - paths.append((pi, path)) - return paths - - -# ═════════════════════════════════════════════════════════════════════════════════════════════ -# Grouping & Classification -# ══════════════════════════════════════════════════════════════════════════════════════════════ - -def group_paragraphs(lines: list[dict], cfg: dict) -> list[dict]: - """Group raw lines into logical paragraphs.""" - paras = [] - i = 0 - gap_thresh = cfg["y_gap_threshold"] - - while i < len(lines): - ln = lines[i] - - # ─── Headers ─── - if ln["bold"] and ln["size"] >= cfg["thresholds"]["section_header"]: - paras.append({ - "text": ln["text"], "font": ln["font"], "size": ln["size"], - "bold": True, "color": ln["color"], "x": ln["x"], - "kind": "header" - }) - i += 1 - continue - - if ln["bold"] and ln["size"] >= cfg["thresholds"]["sub_header"]: - paras.append({ - "text": ln["text"], "font": ln["font"], "size": ln["size"], - "bold": True, "color": ln["color"], "x": ln["x"], - "kind": "subheader" - }) - i += 1 - continue - - # ─── Attribution ─── - if ln["text"].startswith("——"): - paras.append({ - "text": ln["text"], "font": ln["font"], "size": ln["size"], - "bold": False, "color": ln["color"], "x": ln["x"], - "kind": "attribution" - }) - i += 1 - continue - - # ─── Bullet item ─── - if ln["is_bullet"]: - body = ln["text"].lstrip("\uf06c \uf0b7 \u2022 ").lstrip() # strip common bullet chars - buf = [body] - bf, bs = ln["font"], ln["size"] - i += 1 - while i < len(lines): - nxt = lines[i] - if nxt["bold"] and nxt["size"] >= cfg["thresholds"]["sub_header"]: - break - if nxt["is_bullet"]: - break - if nxt["text"].startswith("——"): - break - gap = nxt["y"] - (lines[i - 1]["y"] + lines[i - 1]["size"]) - if gap > gap_thresh: - break - if _is_numbered(nxt["text"], cfg) and nxt["x"] <= 115: - break # nested numbered item = new para - if nxt["font"] != bf: - break - buf.append(nxt["text"]) - i += 1 - paras.append({ - "text": "".join(buf), "font": bf, "size": bs, - "bold": False, "color": ln["color"], "x": ln["x"], - "kind": "bullet" - }) - continue - - # ─── Special fonts (one-liners like STHeitiSC-Light notes) ─── - if ln["font"] == "STHeitiSC-Light": - paras.append({ - "text": ln["text"], "font": cfg["fonts"]["body"], "size": ln["size"], - "bold": False, "color": ln["color"], "x": ln["x"], - "kind": "special" - }) - i += 1 - continue - - # ─── Body / numbered / exercise labels ─── - buf = [ln["text"]] - bf, bs, bc, bx = ln["font"], ln["size"], ln["color"], ln["x"] - i += 1 - while i < len(lines): - nxt = lines[i] - # Hard breaks - if nxt["bold"] and nxt["size"] >= cfg["thresholds"]["sub_header"]: - break - if nxt["is_bullet"]: - break - if nxt["text"].startswith("——"): - break - if nxt["font"] == "STHeitiSC-Light": - break - if _skip_span(nxt, cfg): - i += 1 - continue - - gap = nxt["y"] - (lines[i - 1]["y"] + lines[i - 1]["size"]) - style_changed = nxt["font"] != bf or abs(nxt["size"] - bs) > 1.5 - - # Break on new numbered item at left margin - is_new_numbered = _is_numbered(nxt["text"], cfg) and nxt["x"] <= 115 - # Break on exercise day headers - is_day = bool(re.match(r'^第\d+ 天', nxt["text"])) - is_ex_label = bool(re.match(r'^(今日感恩练习心得|感恩日记|我的练习)', nxt["text"])) - - if gap > gap_thresh or style_changed or is_new_numbered or is_day or is_ex_label: - break - buf.append(nxt["text"]) - i += 1 - - text = "".join(buf) - kind = "body" - if _is_numbered(text, cfg) and bx <= 115: - kind = "numbered" - elif bool(re.match(r'^第\d+ 天', text)): - kind = "day_header" - elif bool(re.match(r'^(今日感恩练习心得|感恩日记|我的练习)', text)): - kind = "exercise_label" - elif bx > 160: - kind = "centered_body" - - paras.append({ - "text": text, "font": bf, "size": bs, "bold": False, - "color": bc, "x": bx, "kind": kind - }) - - return paras - - -# ══════════════════════════════════════════════════════════════════════════════════════════════════════ -# Post-processing -# ════════════════════════════════════════════════════════════════════════════════════════════════════════ - -def split_compact_lists(paras: list[dict], cfg: dict) -> list[dict]: - """Split paragraphs that contain multiple numbered items.""" - out = [] - for p in paras: - text = p["text"] - numbers = re.findall(r'\d+\)', text) - # Only split if more than 2 numbered items in a body paragraph - if p["kind"] in ("numbered", "body") and len(numbers) > 2: - parts = re.split(r'(?=\d+\))', text) - for part in parts: - if part.strip(): - out.append({ - "text": part.strip(), "font": p["font"], "size": p["size"], - "bold": False, "color": p["color"], "x": p["x"], "kind": "numbered" - }) - else: - out.append(p) - return out - - -def split_verses(paras: list[dict], cfg: dict) -> list[dict]: - """Split merged poetic / verse lines.""" - out = [] - for p in paras: - text = p["text"] - markers = cfg.get("verse_markers", []) - if not markers: - out.append(p) - continue - - # Heuristic: paragraph contains repeated marker phrases - total_markers = sum(len(re.findall(m, text)) for m in markers) - if total_markers < 3: - out.append(p) - continue - - # Build a combined split regex from all markers - combined = '|'.join(f'(?={m})' for m in markers) - parts = re.split(combined, text) - # Also split Chinese process steps (一、二、三、) that may prefix the verse - prefix = "" - verse_start = 0 - for idx, part in enumerate(parts): - if re.match(r'[一二三四五六七八九十]、', part): - prefix += part - verse_start = idx + 1 - else: - break - - # Emit prefix steps - if prefix: - for step in re.split(r'(?=[一二三四五六七八九十]、)', prefix): - if step.strip(): - out.append({ - "text": step.strip(), "font": p["font"], "size": p["size"], - "bold": False, "color": p["color"], "x": p["x"], "kind": "numbered" - }) - - # Emit verse lines - for part in parts[verse_start:]: - part = part.strip() - if not part: - continue - # Check for trailing process step (五、回向 etc.) - tail_match = re.search(r'([一二三四五六七八九十]、.+)$', part) - if tail_match: - main_text = part[:tail_match.start()].strip() - tail = tail_match.group(1) - if main_text: - out.append({ - "text": main_text, "font": p["font"], "size": p["size"], - "bold": False, "color": p["color"], "x": p["x"] + 100, "kind": "verse_line" - }) - out.append({ - "text": tail, "font": p["font"], "size": p["size"], - "bold": False, "color": p["color"], "x": p["x"], "kind": "numbered" - }) - else: - out.append({ - "text": part, "font": p["font"], "size": p["size"], - "bold": False, "color": p["color"], "x": p["x"] + 100, "kind": "verse_line" - }) - return out - - -def split_chinese_steps(paras: list[dict]) -> list[dict]: - """Split merged Chinese process steps (一、二、etc.) in body paragraphs.""" - out = [] - for p in paras: - text = p["text"] - if p["kind"] == "body" and len(re.findall(r'[一二三四五六七八九十]、', text)) > 1: - parts = re.split(r'(?=[一二三四五六七八九十]、)', text) - for part in parts: - if part.strip(): - out.append({ - "text": part.strip(), "font": p["font"], "size": p["size"], - "bold": False, "color": p["color"], "x": p["x"], "kind": "numbered" - }) - else: - out.append(p) - return out - - -# ═════════════════════════════════════════════════════════════════════════════════════════════════════════════ -# DOCX building -# ═════════════════════════════════════════════════════════════════════════════════════════════════════════════ - -def build_docx(paras: list[dict], images: list[tuple[int, str]], cfg: dict) -> Document: - """Build a DOCX from classified paragraphs.""" - doc = Document() - section = doc.sections[0] - section.page_width = Cm(21.0) - section.page_height = Cm(29.7) - section.top_margin = Cm(2.54) - section.bottom_margin = Cm(2.54) - section.left_margin = Cm(3.18) - section.right_margin = Cm(3.18) - - # Track which images have been inserted (insert after first occurrence) - inserted_images = set() - - def _add_run(paragraph, text: str, font: str, size: float, bold: bool = False, - color=None, alignment=None): - if alignment is not None: - paragraph.alignment = alignment - run = paragraph.add_run(text) - run.font.size = Pt(size) - run.font.bold = bold - if color: - run.font.color.rgb = color - set_east_asian_font(run, font) - return run - - def _add_para(text: str, font: str, size: float, bold: bool = False, - alignment=None, sb: int = 0, sa: int = 0, - color=None, style=None, indent: float = None): - if style: - p = doc.add_paragraph(style=style) - p.clear() - else: - p = doc.add_paragraph() - p.paragraph_format.space_before = Pt(sb) - p.paragraph_format.space_after = Pt(sa) - p.paragraph_format.line_spacing = 1.15 - if indent: - p.paragraph_format.left_indent = Cm(indent) - _add_run(p, text, font, size, bold, color, alignment) - return p - - # Image helper - def _insert_image(image_path: str): - ip = doc.add_paragraph() - ip.alignment = WD_ALIGN_PARAGRAPH.CENTER - ip.paragraph_format.space_before = Pt(6) - ip.paragraph_format.space_after = Pt(6) - ir = ip.add_run() - ir.add_picture(image_path, width=Inches(3.3)) - - for p in paras: - text = p["text"] - font = p["font"] - size = p["size"] - bold = p["bold"] - color = _mk_color(p["color"]) - kind = p["kind"] - - if kind == "header": - _add_para(text, font, size, bold=True, - alignment=WD_ALIGN_PARAGRAPH.CENTER, sb=10, sa=8) - elif kind == "subheader": - _add_para(text, font, size, bold=True, sb=8, sa=4) - elif kind == "bullet": - _add_para(text, font, size, style='List Bullet', sb=0, sa=1, color=color) - elif kind == "attribution": - _add_para(text, font, size, - alignment=WD_ALIGN_PARAGRAPH.RIGHT, sb=2, sa=6, color=color) - elif kind == "numbered": - _add_para(text, font, size, indent=cfg["indent_body"], sb=1, sa=1, color=color) - elif kind == "day_header": - _add_para(text, font, size, bold=True, sb=6, sa=2, color=color) - elif kind == "exercise_label": - _add_para(text, font, size, sb=2, sa=1, color=color) - elif kind == "special": - _add_para(text, font, size, sb=6, sa=4) - elif kind == "verse_line": - _add_para(text, font, size, indent=2.0, sb=0, sa=0, color=color) - elif kind == "centered_body": - _add_para(text, font, size, - alignment=WD_ALIGN_PARAGRAPH.CENTER, sb=2, sa=4, color=color) - else: # body - indent = cfg["indent_body"] if p["x"] > 105 else None - _add_para(text, font, size, indent=indent, sb=1, sa=2, color=color) - - # Insert images after paragraphs containing "参考示例" or other markers - if "参考示例" in text or "示例" in text: - for pi, img_path in images: - if img_path not in inserted_images: - _insert_image(img_path) - inserted_images.add(img_path) - break - - return doc - - -# ════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -# Public API -# ════════════════════════════════════════════════════════════════════════════════════════════════════════════ - -def convert_pdf_to_docx(pdf_path: str, docx_path: str, config: dict = None): - """ - Convert a flowing text PDF to a well-structured DOCX. - - Args: - pdf_path: Path to input PDF - docx_path: Path to output DOCX - config: Optional override dict (merged with DEFAULT_CONFIG) - """ - cfg = DEFAULT_CONFIG.copy() - if config: - cfg.update(config) - - # 1. Extract images - images = extract_images(pdf_path) - - # 2. Extract text lines - lines = extract_lines(pdf_path, cfg) - - # 3. Group into paragraphs - paras = group_paragraphs(lines, cfg) - - # 4. Post-process - paras = split_compact_lists(paras, cfg) - paras = split_verses(paras, cfg) - paras = split_chinese_steps(paras) - - # 5. Build DOCX - doc = build_docx(paras, images, cfg) - doc.save(docx_path) - print(f"Saved: {docx_path} ({len(doc.paragraphs)} paragraphs)") - return docx_path - - -# CLI -if __name__ == "__main__": - if len(sys.argv) != 3: - print("Usage: python convert_pdf_to_docx.py ") - sys.exit(1) - convert_pdf_to_docx(sys.argv[1], sys.argv[2]) diff --git a/skills/pdf-to-docx-conversion/templates/chinese-study-config.json b/skills/pdf-to-docx-conversion/templates/chinese-study-config.json deleted file mode 100644 index 9d5e5c9..0000000 --- a/skills/pdf-to-docx-conversion/templates/chinese-study-config.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "_description": "Pre-configured for Chinese Sutra/study-material PDFs with STHeitiSC-HYShuSongErKW fonts", - "fonts": { - "title": "STHeitiSC-Medium", - "body": "HYShuSongErKW", - "page_num": "HelveticaNeue" - }, - "thresholds": { - "section_header": 18, - "sub_header": 14, - "body": 12 - }, - "bullet_fonts": ["Wingdings", "Wingdings 2", "Wingdings 3", "Symbol"], - "skip_fonts": ["HelveticaNeue", "Arial-BoldMT", "ArialMT"], - "skip_size_max": 9.9, - "numbered_patterns": [ - "^\\d+\\)", - "^\\d+\\.\\s*", - "^\\d+)", - "^\\d+、" - ], - "y_gap_threshold": 20, - "indent_body": 0.8, - "verse_markers": [ - "感恩(?!恩)", - "愿我们", - "更愿", - "愿人们", - "愿世界" - ] -} diff --git a/skills/pptx-translate/SKILL.md b/skills/pptx-translate/SKILL.md deleted file mode 100644 index 89abda4..0000000 --- a/skills/pptx-translate/SKILL.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -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 diff --git a/skills/pptx-translate/scripts/build.py b/skills/pptx-translate/scripts/build.py deleted file mode 100644 index f757cc8..0000000 --- a/skills/pptx-translate/scripts/build.py +++ /dev/null @@ -1,68 +0,0 @@ -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}") diff --git a/skills/pptx-translate/scripts/extract.py b/skills/pptx-translate/scripts/extract.py deleted file mode 100644 index 90d9063..0000000 --- a/skills/pptx-translate/scripts/extract.py +++ /dev/null @@ -1,68 +0,0 @@ -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]}") diff --git a/skills/readme.dj b/skills/readme.dj deleted file mode 100644 index 1b79e2d..0000000 --- a/skills/readme.dj +++ /dev/null @@ -1,26 +0,0 @@ -# MPI Skills - -These skills live in this directory and are loaded via `~/.hermes/config.yaml`. - -## Install for Hermes - -Add to `~/.hermes/config.yaml`: - -```yaml -skills: - external_dirs: - - $MPI_PROJECT_ROOT/skills -``` - -{% Edit config.yaml directly — `hermes config set` stores list values as strings. %} - -## Skills - -| Name | What it does | -|---|---| -| `translation` | Translate Chinese↔English Buddhist/Dharma content | -| `terms-search` | Full-text search across MPI term database | -| `translation-review` | Review translations for quality issues | -| `chinese-text-normalize` | Normalize Chinese markdown line breaks | -| `pptx-translate` | Translate PowerPoint files | -| `pdf-to-docx-conversion` | Convert PDFs to DOCX with layout preservation | diff --git a/skills/terms-search/SKILL.md b/skills/terms-search/SKILL.md deleted file mode 100644 index 3ac9570..0000000 --- a/skills/terms-search/SKILL.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -name: terms-search -description: Full-text search across the MPI term database. Use when translating or looking up Chinese-English Buddhist/MPI terminology. -category: research ---- - -# Terms Search - -Database: `$MPI_PROJECT_ROOT/terms-search/termlib.duckdb` -CLI: `$MPI_PROJECT_ROOT/terms-search/search.py` -Server: `$MPI_PROJECT_ROOT/terms-search/server.py` - -## CLI (preferred) - -``` -$MPI_PROJECT_ROOT/terms-search/search.py [limit] -``` - -Multi-word queries are ANDed. Searches both `zh` and `en` columns. - -## Python module - -```python -import sys -sys.path.insert(0, '$MPI_PROJECT_ROOT/terms-search') -from search import search -results = search("空性", limit=5, src="DoT定稿") -# → list of {zh, en, loc, source} dicts -``` - -Use this inside `execute_code` scripts for batch lookups — no subprocess needed. - -## HTTP API (use only when CLI is insufficient) - -Start: `python3 $MPI_PROJECT_ROOT/terms-search/server.py` (port 8910) - -- `GET /` — plain HTML UI (form + results table, no CSS) -- `GET /` — plain HTML UI (form + results table, no CSS) -- `GET /search?q=...&loc=...&src=...&limit=...` — JSON `{count, results: [{zh, en, loc, source}]}` -- `GET /sources` — JSON array of `{source, count}` for all source tables - -All params optional. Omit `limit` for all results. Query terms are ANDed across zh+en. -Errors return `{"error": "..."}` with HTTP 500 (API) or shown inline (UI). - -## Source tables - -| src | rows | description | -|---|---|---| -| BAICKZ | 7,679 | Main term bank with example sentences | -| 佛教术语 | 1,795 | Buddhist terminology from 定稿书目术语库 | -| DoT定稿 | 896 | DoT final translation decisions | -| DoT初步 | 412 | DoT preliminary queries | -| 偈颂经文名言 | 263 | Verses and sutra quotes | -| 成语俗语 | 184 | Idioms and common expressions | -| 经论名 | 89 | Sutra/shastra titles | -| 内部特色词 | 87 | MPI internal terminology | -| 海内外建筑名称 | 28+9 | MPI building/place names | -| MPI组织架构 | 4+28 | MPI org structure | -| 导师金句 | 24 | Teacher quotes | -| 静心学堂课程 | 17+20 | Course names | -| 禅意项目 | 14+11 | Zen program terms | -| 公案 | 8 | Chan koans | - -## Direct DuckDB - -``` -duckdb $MPI_PROJECT_ROOT/terms-search/termlib.duckdb -``` - -Key tables: `unified_terms_flat` (zh, en, loc, source), individual source tables, `unified_terms` view. - -## Rebuilding - -Terms data comes from `$MPI_PROJECT_ROOT/guide/03 术语库/`. To rebuild: -1. Convert source xlsx/ods → CSV+YAML in `_output/` -2. Rebuild DuckDB from CSVs -3. Materialize `unified_terms_flat` view → table for performance - -**Full rebuild pipeline:** See `references/termbase-rebuild.md` (absorbed from the `termbase-management` skill). - -## Translation Alignment - -When aligning translated djot files against the term database, load `references/translation-alignment.md` for the full workflow. Summary: -1. Extract Chinese terms from `{% "..." %}` glossary blocks in the translated file -2. Batch-search via CLI (`./search.py `). Query each term individually. -3. Prioritize DoT定稿 > 内部特色词 > 佛教术语 -4. Fix both glossary comments AND body-text occurrences -5. Verify with grep diff --git a/skills/terms-search/references/termbase-rebuild.md b/skills/terms-search/references/termbase-rebuild.md deleted file mode 100644 index 5fed33e..0000000 --- a/skills/terms-search/references/termbase-rebuild.md +++ /dev/null @@ -1,127 +0,0 @@ -# Termbase Rebuild (from absorbed termbase-management) - -How to rebuild the terms DuckDB from source spreadsheets. This is the full pipeline from the now-archived `termbase-management` skill. - -## Prerequisites - -```bash -pip install openpyxl odfpy pyyaml duckdb -``` - -## Step 1: Inspect spreadsheet structure - -```python -from openpyxl import load_workbook -wb = load_workbook(path, read_only=True, data_only=True) -for sn in wb.sheetnames: - ws = wb[sn] - rows = [list(r) for r in ws.iter_rows(min_row=1, max_row=6, values_only=True)] - print(f"[{sn}] {sum(1 for _ in ws.iter_rows())} rows, cols: {len(rows[0]) if rows else 0}") - for r in rows[:5]: print(f" {r}") -``` - -For ODS files, use odfpy: -```python -from odf.opendocument import load as odf_load -from odf.table import Table, TableRow, TableCell -from odf.text import P - -doc = odf_load(path) -for table in doc.getElementsByType(Table): - for row in table.getElementsByType(TableRow): - cells = row.getElementsByType(TableCell) - vals = [] - for cell in cells: - text = '' - for p in cell.getElementsByType(P): - for node in p.childNodes: - if node.nodeType == node.TEXT_NODE: - text += node.data - vals.append(text.strip() if text else None) -``` - -## Step 2: Convert to CSV + YAML - -### Cleaning -- Strip trailing None/empty values from each row: `while row and not row[-1]: row.pop()` -- Skip entirely empty rows -- Pad all rows to the max column count - -### Duplicate header handling -Some sheets have duplicate column names (e.g., two `英文` columns in paired layout). Deduplicate with suffixes: -```python -from collections import Counter -def dedup_headers(headers): - seen = Counter() - result = [] - for h in headers: - s = str(h) if h else '' - if s in seen: - seen[s] += 1 - result.append(f"{s}_{seen[s]}") - else: - seen[s] = 1 - result.append(s) - return result -``` - -### Output formats -- **CSV**: `csv.writer` — column-major, preserves all raw data -- **YAML**: `yaml.dump(data, allow_unicode=True, default_flow_style=False, sort_keys=False, width=200)` — list of dicts - -## Step 3: Load into DuckDB - -```python -import duckdb -con = duckdb.connect('termlib.duckdb') - -# Simple CSVs work with auto-detect: -con.execute(""" - CREATE TABLE table_name AS - SELECT * FROM read_csv_auto('file.csv', header=true, all_varchar=true) -""") -``` - -### Pitfall: Multiline CSV fields -CSV files with embedded newlines (common in glossary example-sentence columns) break DuckDB's auto-sniffer. Fall back to Python csv.reader: - -```python -import csv -with open(path, 'r', encoding='utf-8') as f: - rows = list(csv.reader(f)) - -headers = rows[0] -data = rows[1:] - -col_defs = ', '.join(f'"{h}" VARCHAR' for h in cleaned_headers) -con.execute(f'CREATE TABLE "{table}" ({col_defs})') - -batch_size = 500 -for i in range(0, len(data), batch_size): - batch = data[i:i+batch_size] - placeholders = ', '.join(['(' + ', '.join(['?' for _ in headers]) + ')' for _ in batch]) - flat = [v for row in batch for v in row] - con.execute(f'INSERT INTO "{table}" VALUES {placeholders}', flat) -``` - -### Pitfall: DuckDB CLI opens in-memory by default -Running plain `duckdb` gives an empty database. Always pass the file path: -``` -duckdb path/to/termlib.duckdb -``` - -## Step 4: Create unified views - -See `references/unified-view.sql` for the pattern. Key patterns: -- `UNION ALL` across all source tables -- Normalize column names to `zh`, `en`, `loc` (出处), `source` -- For paired-column sheets (e.g., `中文/英文` + `补充内容/英文_1`), emit two UNION branches -- Filter out rows where zh or en is NULL/empty - -## Pitfalls - -- **ODS reading**: Must traverse `odf.text.P` child elements, not direct text nodes -- **Duplicate headers**: JSON/YAML dict silently overwrites duplicate keys — always deduplicate -- **Multiline CSV + DuckDB**: `read_csv_auto` fails on CSVs with quoted newlines — use Python csv.reader -- **DuckDB path**: Always explicit file path; `duckdb` alone is in-memory -- **`execute_code` sandbox**: Does NOT share pip-installed packages — use `terminal` for Python scripts diff --git a/skills/terms-search/references/translation-alignment.md b/skills/terms-search/references/translation-alignment.md deleted file mode 100644 index 3be44a3..0000000 --- a/skills/terms-search/references/translation-alignment.md +++ /dev/null @@ -1,58 +0,0 @@ -# Translation Alignment Workflow - -How to align translated djot files against the MPI terms database. - -## When - -After producing a first-pass translation, or when the user asks to check terminology. Any time a `.dj` file contains glossary-style `{% "..." %}` blocks with Chinese→English term pairs. - -## Steps - -1. Read the full translated file. Extract all Chinese terms from `{% "TERM" (pinyin) = ENGLISH ... %}` blocks. - -2. Start the search server: `python3 $MPI_PROJECT_ROOT/terms-search/server.py &` (port 8910). It may already be running — check with `curl -s http://localhost:8910/`. - -3. Batch-search each term via the HTTP API: - ``` - curl -s "http://localhost:8910/search?q=TERM&limit=5" - ``` - Prefer `src=DoT定稿` filter for authoritative hits, but also check without filter to catch 内部特色词 and 佛教术语 entries. - -4. For each term, compare the database `en` against the file's translation. A mismatch exists when the core term translation differs (ignore explanatory commentary in glossary blocks). - -5. Priority order for which source to trust: - - DoT定稿 (highest authority — final translation decisions) - - 内部特色词 (MPI internal terminology) - - 佛教术语 (general Buddhist terms) - - 经论名 (sutra/shastra titles) - -6. Apply fixes with `patch` tool. Fix BOTH the glossary comment AND all body-text occurrences. Use `replace_all=true` for terms that appear identically in multiple places. - -7. After fixing, grep for remaining old forms to verify nothing was missed. - -## Pitfalls - -- Glossary blocks often include commentary after the term (pinyin, explanations). Compare only the core term translation, not the full comment. -- Some terms appear in body text without glossary blocks — scan the body for domain terms too. -- The search.py CLI does NOT support `src:` or `loc:` filter syntax directly; use the HTTP API or direct DuckDB queries instead. -- Replace-all can create double articles ("the The Eight Steps...") when body text already has the article before the term. Check each replacement site. -- Escaped quotes in patch old_string/new_string cause false "Escape-drift" errors. Use unescaped `"` characters from the actual file content. -- Terms may have different translations in different contexts (e.g., standalone 心灯 = "lamp of awakening" vs compound 点亮心灯 = "illuminate one's heart"). Use the standalone form for glossary entries. -- Sutra quote conventions (e.g., Diamond Sutra "lives" not "bodies" for 身体布施) are not always in the database. Apply standard English Buddhist idiom. - -## Example - -Searching 三无漏学: -``` -curl -s "http://localhost:8910/search?q=三无漏学&limit=5" -→ DoT定稿: "three uncontaminated forms of training" -→ Current file: "the three undefiled studies" -→ MISMATCH → fix -``` - -## Report - -After alignment, the user may ask for a report. Save it as `alignment-report.dj` in the project directory with: -- Summary line (source breakdown) -- Per-term before/after table with source annotation -- "Not Changed" section listing terms checked and found acceptable diff --git a/skills/translation-review/references/buddhist-terminology.md b/skills/translation-review/references/buddhist-terminology.md deleted file mode 100644 index 24053d5..0000000 --- a/skills/translation-review/references/buddhist-terminology.md +++ /dev/null @@ -1,132 +0,0 @@ -# Buddhist Text Translation — Terminology - -Terms encountered in Chinese-English translation of Dharma study materials. These may vary by translator/context; document actual usage per-project. - -## Section headers (common triad) - -| Chinese | English options seen | Notes | -|---------|---------------------|-------| -| 法义 | Understanding, Dharma Teachings | | -| 思考 | Contemplation, Reflection | Consistency within a document matters more than which word | -| 练习 | Practice, Application, Exercises | "Application" seen as section header; "Practice"/"Exercise" in running text | - -## Key Buddhist terms - -| Chinese | English | Pitfalls | -|---------|---------|----------| -| 慈经 | Metta Sutta (Karaniya Metta Sutta) | NOT "Mettavihari Sutta" | -| 回向 | Dedication (of merit) | | -| 因缘之网 | Web of Causes and Conditions | Also "Network of Causes and Conditions" | -| 感恩 | Gratitude | | -| 众生 | sentient beings | Consistent throughout | -| 使人内心调柔 | makes one's heart gentle | 使人 = makes ONE(self), never "makes others" | -| 利益思维 | benefit-oriented thinking | NOT "mindset of benefiting others" — it's about considering benefits TO oneself | -| 恩田 | field of gratitude / gratitude as a field of merit | | -| 观照 | attend to the mind / mindful observation | Contemplative practice, not intellectual study. NOT "observe" (passive) or "study" (analytical). | -| 闻思修 | hearing, contemplating, cultivating | 修 = broad cultivation/practice, not specifically 禅 (meditation). Distinct from 禅修 (meditative cultivation). | -| 八步三禅 | Eight Steps and Three Meditations | Community-specific structured contemplative method | -| 传帮带 | transmit, help, guide (three-part mentoring) | Core community methodology | -| 分灯 | lamp-dividing (decentralization) | Deliberate decentralization of authority across nodes | -| 自觉 | self-awareness, voluntary commitment | First of the "Three Spirits" | -| 法治 | rule-based governance | Second of the "Three Spirits" | -| 无我利他 | selfless service to others | Third of the "Three Spirits" | -| 凡夫心 | ordinary mind | Mind governed by afflictions, contrasted with awakened mind | -| 贪嗔痴 | greed, anger, ignorance (三毒) | The three root poisons: rāga, dveṣa, moha | -| 愿心 | mind of vows, bodhicitta aspiration | Plural "vows" in English | -| 重要感、优越感、主宰欲 | sense of importance, superiority, desire to control | Three ego-driven motivations | - -## Structural patterns - -- Section numbering: Chinese uses 一、二、三... English should pick one style (Part One/Two, First/Second, I/II) and stick with it. -- Poetry/prayer blocks: Chinese uses parallel structures (感恩... 感恩...; 愿... 愿...). English must match the parallelism. -- 愿 (yuàn) at sentence start = optative "May..." — not "we hope that", not "we should". - - -## Terms from 人生百问 review - -Terms encountered in the *人生百问* review that deserve explicit guidance. -These supplement the general table above. - -| Chinese | English | Pitfalls / notes | -|---|---|---| -| 皈依三宝 | take refuge in the Three Jewels | DoT定稿 preferred. Avoid "take refuge hastily" — use "take refuge lightly" | -| 三级修学 | Three-Stage Practice | DoT定稿. Not "Three-Level Study Program" | -| 同喜班 | Tongxi Class | MPI convention; verify if glossary requires translation | -| 人生佛教 | Buddhism for Human Life | Do not conflate with 人间佛教 / Humanistic Buddhism | -| 人间佛教 | Humanistic Buddhism | Distinct from 人生佛教 | -| 善知识 | wise teacher / great teacher / authentic teacher | Avoid redundant "great wise teachers" | -| 大善知识 | great teacher / wise teacher | Redundant to render as "great wise teacher" | -| 依止 | reliance / rely on | "reliance relationship" is awkward; prefer "teacher-student relationship" or "rely on" | -| 报身 | reward body / enjoyment body | Verify target convention; MPI often prefers "reward body" (Skt. *sambhogakāya*) | -| 化身 | transformation body / emanation body | Verify target convention | -| 等流果 | result of equal outflow / correlative effect | Avoid ad-hoc "continuative result" | -| 末法 | Dharma-ending age / Age of Dharma Decline | Check terms DB; avoid new coinages | -| 暇满 | precious human life / leisure and endowment | "Well-endowed Human Form" is formal | -| 暇满人身 | precious human body / well-endowed human form | Context-dependent | -| 定课 | daily practice / fixed chanting session / daily recitation | Context-dependent; check terms DB | -| 八步骤三种禅修 | Eight Steps and Three Types of Meditation | DoT定稿. Not "Three Kinds of Meditation" | -| 八步三禅 | Eight Steps and Three Meditations | Shorter form; verify convention | -| 功德 | merit / virtue | Context-dependent; do not default to "merit and virtue" | -| 福田 | field of merit / field of blessing | Standard | -| 止观 | śamatha-vipaśyanā / calm abiding and special insight | In lists: "śamatha-vipaśyanā" | -| 皈依 | refuge / take refuge | In catalogues: noun form "refuge" | -| 发心 | bodhicitta / aspiration / arousing the mind | In catalogues: noun form "aspiration" | -| 戒律 | precepts / moral discipline | In catalogues: noun form "precepts" | -| 正见 | right view | In catalogues: noun form "right view" | -| 五戒 | five precepts | Standard | -| 惑业 | delusion and karma | Not "confusion and karma" | -| 大自在 | great freedom and ease | "Great freedom" alone loses the "ease" nuance | -| 开启 | awaken / bring forth / unfold | Not "opened up" | -| 迷惑 | delusion | In Buddhist contexts, not "confusion" | -| 凡夫心 | mind of an ordinary being | Avoid Chan "ordinary mind" ambiguity | -| 加持 | blessing / empowerment | "empowers one another" loses nuance; "supports and blesses one another" | -| 四力 (忏悔) | four powers (of confession) | Standard: remorse, support, restraint, refuge/reliance | -| 无缘大慈 | unconditional great compassion | Standard | -| 同体大悲 | great sympathy of seeing others as oneself | Standard | -| 人成即佛成 | When a Human Is Perfected, Buddhahood Is Perfected | Standard rendering | -| 登地菩萨 | bodhisattva who has attained the grounds | Standard | -| 弟子相 | marks of a disciple | Standard | -| 自他相换 | exchanging self and others | Standard | -| 明心见性 | clear the mind and see the nature | "Clear mind and seeing the nature" is slightly off | -| 言语道断,心行处灭 | Where words fall silent and mental activity ceases | "Words are cut off" is too literal | -| 无住生心 | arouse the mind without attachment | Standard | -| 诸法如实相 | true suchness of all phenomena | Standard | -| 颠倒梦想 | inverted dreams | Standard | -| 颠倒妄想 | deluded inversion | Standard | -| 十二因缘 | Twelve Links of Dependent Origination | Standard | -| 无我 | no-self / anatman | Standard | -| 我法二执 | attachment to self and dharmas | Standard | -| 三性 (遍计所执性 / 依他起性 / 圆成实性) | three natures: falsely conceived, dependent, perfectly realized | Standard | -| 末那 | Manas | Standard (Consciousness-Only) | -| 阿赖耶 | Alaya | Standard (Consciousness-Only) | -| 唯识 | Consciousness-Only | Standard | -| 唯识三十论 | Thirty Verses on Consciousness-Only | Standard | -| 认识与存在 | Cognition and Existence | Verify if this is a published translation title | -| 三果 | third fruit / anāgāmi | Standard | -| 现报 / 生报 / 后报 | reward in this life / next life / future life | "present reward, next-life reward, later reward" is acceptable but less standard | -| 中观 | Madhyamaka | Standard | -| 天台 | Tiantai | Standard | -| 一念三千 | three thousand realms in a single thought | Standard (Tiantai) | -| 三传 | Theravada / Chinese Buddhism / Tibetan Buddhism | Standard | -| 南传 | Theravada | Standard | -| 汉传 | Chinese Buddhism | Standard | -| 藏传 | Tibetan Buddhism | Standard | -| 佛陀出世的本怀 | Buddha's original purpose in appearing in the world | Standard | -| 契理契机 | accordant with principle and adapted to the times | Standard | -| 正本清源 | return to the source and clarify what is truly central | Standard | -| 随缘 | responding to conditions / in harmony with conditions | Context-dependent | -| 随喜 | rejoice | Standard | -| 善知识 | wise teacher / good teacher / authentic teacher | Avoid "great wise teachers" | -| 定课 | daily practice / fixed chanting session | Context-dependent | - -{% The following terms already appear in the "Key Buddhist terms" table above; they are retained here only when the 人生百问 review adds distinct guidance. %} - -| 法义 | Dharma teachings / understanding | Consistency within a document matters | -| 闻思修 | hearing, contemplating, cultivating | 修 is broad cultivation, not just meditation | -| 传帮带 | transmit, help, guide | Three-part mentoring | -| 分灯 | lamp-dividing | Decentralization of authority | -| 自觉 | self-awareness / voluntary commitment | First of the "Three Spirits" | -| 法治 | rule-based governance | Second of the "Three Spirits" | -| 无我利他 | selfless service to others | Third of the "Three Spirits" | -| 重要感、优越感、主宰欲 | sense of importance, superiority, desire to control | Three ego-driven motivations | - diff --git a/skills/translation-review/references/common-issues-taxonomy.md b/skills/translation-review/references/common-issues-taxonomy.md deleted file mode 100644 index 340b446..0000000 --- a/skills/translation-review/references/common-issues-taxonomy.md +++ /dev/null @@ -1,174 +0,0 @@ -# Common Translation Issues Taxonomy - -From 译文常见问题与案例示范 (Common Issues in Translation, 2026-03-30). -Categorized by accuracy and readability. - -## Accuracy Issues - -Standard: convey meaning at the content level, not word-by-word correspondence. -"Translation is not simply word conversion." -"You must understand what thought it is actually trying to express." - -### 1. Omission (漏翻) - -Content present in source but absent from target. - -Example: -> 随着黑白牌的运用,象征这部分已能为大家提供系统的学习和训练资料。 -> -> With the help of the Black-and-White Cards, this approach now offers a structured -> system of study. -> -> *Missing:* 学习和训练 (learning and training) — "study" only covers half. -> Also: 象征这部分 (symbolizing this aspect) is dropped. - -### 2. Mistranslation (错翻) - -#### a. Over-free translation, weakening the original - -> "道德"一词,人们耳熟能详。即使在道德日渐边缘化的今天,人们依然会用"这人 -> 很有道德""这么做太缺德了"来评价周遭人事。 -> -> "Morality" is a familiar term, and even in today's world, where it has become -> increasingly marginalized, expressions like "immorality" are still commonly used -> to describe certain behaviors and individuals. -> -> *Issue:* The source gives two concrete colloquial expressions ("这人很有道德" / -> "这么做太缺德了") — the target collapses them into a single abstract "expressions -> like 'immorality'." The vividness and specificity are lost. - -#### b. Over-literal translation, harming comprehension - -> 作为素菜馆,应该尽量提供绿色食品。 -> -> A vegetarian restaurant should strive to offer green food. -> -> *Issue:* 绿色食品 = "safe and healthy food" (Chinese idiom), not -> "green food" (color of food in English). → `safe and healthy options` - -#### c. Misunderstanding the source - -> 政府现在倡导文化自信,习主席提出的"讲仁爱,重民本,守诚信,崇正义,尚和合,求大同"。 -> -> President Xi Jinping has called for a confident embrace of Chinese culture, -> highlighting virtues such as... -> -> *Issue:* Two different subjects: the government advocates cultural confidence; -> Xi proposed the six virtues. The target merges both into Xi alone. - -#### d. Wrong word choice causing misunderstanding - -> 提供一些通俗易懂的法宝 -> -> with easy Buddhist materials -> -> *Issue:* 通俗易懂 means "accessible to a general audience," not "easy/simple." -> The Dharma can be accessible but is never "simple." → `accessible` - -### 3. Overtranslation (多翻) - -Adding parentheticals or expansions not in the source. - -> 出家人不仅要对三宝、师长、道友建立没有染污的情感 -> -> Monastics should cultivate pure and untainted emotions not only toward the Three -> Jewels (the Buddha, the Dharma, the Sangha), their teachers (The Head Monk and -> Abbot), and fellow practitioners... -> -> *Issue:* Parenthetical expansions like "(the Buddha, the Dharma, the Sangha)" -> and "(The Head Monk and Abbot)" are not in the source and should be removed. - -### 4. Terminology Errors - -- 身心 → `body and heart mind` → should be `body and mind` -- 甘露别院 → `Ganlu Bieyuan The Amrita Retreat Center` → official: `Amrita Retreat Center` -- "六字" → `six characters` → should be `six words` - -### 5. Detail-Level Issues - -- Tone appropriateness -- Citation source authority -- CN→EN conversion rules: numerals, units, date formats -- Proper noun handling: transliteration vs. translation - -## Readability Issues - -Standard: fluent sentences, graceful expression, matching target reader habits. -"Conform to what foreign readers find readable." -"Concise, clear, accurate, not draggy, meaning clear." - -### 1. Redundancy & Wordiness - -#### a. Long / nested sentences - -Sentences with too many clauses that can't be read in one breath. Split them. - -#### b. Passive voice overuse - -> 如果对肉食消费得少,乃至完全不消费,那么从业者自然随之减少,很多动物就可以摆脱被屠宰、割截的厄运。 -> -> If meat consumption were reduced, or even eliminated entirely, then the number of -> businesses involved would naturally be lessened. This way, many animals could be -> spared from the adversity of being slaughtered and butchered. -> -> *Issue:* Three passives in two sentences. Active rewrite is more concise: -> *If people consume less meat — or none at all — fewer will work in the industry, -> and many animals will be spared the fate of slaughter and dismemberment.* - -#### c. Nominalization (动词名词化) - -> 我们前期倡导的正念禅修,比较重视培养觉知。 -> -> In the early stages of mindfulness practice, we placed greater emphasis on -> cultivating awareness. -> -> *Issue:* "placed greater emphasis on" is nominalized. → `we emphasized` - -### 2. Poor Structure (结构不当) - -Top-heavy sentences — the main clause arrives too late. - -> 没有如法的生活,不能严格要求自己,在今天这个红尘滚滚的时代,我们简直是没希望的。 -> -> Weathering today's deluge of worldly temptation, without a Dharma-aligned lifestyle -> and strict self-discipline, we have no hope. -> -> *Issue:* The condition clauses pile up before the main point. Restructure: -> *Without a Dharma-aligned lifestyle and strict self-discipline, we have no hope -> of weathering today's deluge of worldly temptations.* - -### 3. Wrong Word Choice (用词不当) - -> 当一个人开始关心生命的大问题时,对小问题自然云淡风轻。 -> -> when we begin to contemplate the profound questions of existence, the banal -> iterations of daily life lose their significance. -> -> *Issue:* "banal iterations" is obscure. Simplify. - -### 4. Weak Flow & Transitions (流畅性 & 连接词) - -> 修行的核心就是止恶行善。为什么某种行为能成为生命的主导?就是因为不断重复。 -> -> The essence of cultivation lies in ceasing unwholesome actions and promoting -> wholesome ones. But why do certain behaviors dominate our lives? Because of -> constant repetition. -> -> *Issue:* Adding transition words improves flow. The "But" here creates a false -> contrast — the second sentence is explanation, not counterpoint. - -## Detection Checklist - -When reviewing, run through: -- [ ] Any missing content? (compare paragraph-by-paragraph) -- [ ] Any added content? (parentheticals, expansions not in source) -- [ ] Any over-literal renderings that don't work in English? (idioms, set phrases) -- [ ] Any over-free renderings that lose specificity? (concrete examples → abstract) -- [ ] Subject confusion? (two actors merged into one) -- [ ] Buddhist terminology checked against terms DB? -- [ ] Sentences too long to read in one breath? (split at natural breaks) -- [ ] Passive voice clustering? (3+ in a paragraph) -- [ ] Nominalized verbs? ("placed emphasis on" → "emphasized") -- [ ] Top-heavy sentence structure? (main clause buried after long preamble) -- [ ] Obscure word choices? (would a general reader understand?) -- [ ] Missing or misleading transition words? diff --git a/skills/translation-review/references/deliberation-protocol.md b/skills/translation-review/references/deliberation-protocol.md deleted file mode 100644 index 7c9bb2d..0000000 --- a/skills/translation-review/references/deliberation-protocol.md +++ /dev/null @@ -1,90 +0,0 @@ -# Deliberation Protocol (审议规程) - -From 东方译场审议手册 (Oriental Translation Review Manual, 2026-03-25). -Guidance from Ven. Jiqun on translation review culture and process. - -## Core Standards - -1. **Accuracy (准确性)** — primary. Convey content-level meaning, not word-by-word - correspondence. Deepen accuracy iteratively; grasp the "inner spirit" rather than - fixating on literal vs free translation. - -2. **Readability (可读性)** — secondary. Fluent sentences, graceful expression. Do - not pursue endless revision; what is achievable now is the best for now. - -**Test**: invite 5–10 readers to read aloud and give feedback — is it clear or -obscure? Is it concise? Is the meaning clear? - -## Decision-Making - -When opinions differ: -- Compare which version is more accurate and more readable -- Do NOT cling to your own view (不执着于己见) -- If two people cannot decide, invite 5–10 to read and vote -- Remember: the translation is for the READERS, not for yourself - -## Core Principle: Rejoice First (随喜) - -Before flagging issues, affirm what is good. Professional translation companies -emphasize positive feedback to build translator confidence and good collaboration. -Criticism alone — however precise — breeds resistance and harms the final result. - -In the Oriental Translation Workshop, rejoicing in others' merits cultivates -sympathetic joy (随喜), compassion, selflessness, and boundless merit — while -encouraging others' wholesome efforts. - -**Practice**: develop eyes that SEE the merits in a translation. Rejoice with -genuine gladness. Examples: -- "So many technical terms and complex logic here — you handled it beautifully!" -- "This word choice is exactly right." -- "The prose is so clean and fluid!" -- "Excellent!" / "Good word choice!" - -## Three Principles for Giving Suggestions - -1. **Lower the self** — use compassionate, gentle, skillful language -2. **Be diligent and attentive** — find the actual problems -3. **Tier your suggestions** — make clear what MUST change, what COULD improve, - and what needs DISCUSSION - -## Three-Tier Issue System - -### Level 1: Spelling, Grammar, Format (拼写、语法、格式) - -**Action**: fix directly, keep tracked changes visible. - -May annotate for translator development: -- "MPI format uses American English — `toward` not `towards`." -- "Use `Chan` here, not `Zen`." -- "MPI format does not use diacritics — `Yogacara` not `Yogācāra`." - -### Level 2: Omission, Mistranslation, Wordiness, Tone, Word Choice -(漏翻、错翻、啰嗦、语气问题、用词问题) - -**Action**: preferably flag for translator to fix themselves; may also fix -directly with tracked changes. - -Suggested language: -- "Did we miss something here?" -- "This sentence is quite long — could we split it into 2–3?" -- "The translation here differs from the original. It reads as if the teacher - is saying X, but I think he means Y. What do you think?" -- "This word feels a bit strong here — your thoughts?" -- (After direct adjustment) "I tightened this sentence — does this work?" - -### Level 3: Citation Versions, Slightly Awkward Wording, Marginal Readability -(引用版本不当、用词稍微不当、阅读体验略差) - -**Action**: flag and discuss with translator. - -Suggested language: -- "I found another citation version that may be more concise — which do you prefer?" -- "Could we use this word instead? Might fit better." -- "Could you take another look at this passage? I feel readability could improve - but I'm not sure how — your thoughts?" - -## Tone Guide - -- Address the translator as 菩萨 (Bodhisattva) — respectful peer address -- Use questions, not commands: "Could we...?" "What do you think?" "Would this work?" -- Frame as collaborative inquiry, not correction diff --git a/skills/translation-review/references/docx-md-extraction.md b/skills/translation-review/references/docx-md-extraction.md deleted file mode 100644 index 82f5211..0000000 --- a/skills/translation-review/references/docx-md-extraction.md +++ /dev/null @@ -1,171 +0,0 @@ -# Extracting `.docx.md` to `source.dj` + `bilingual.dj` - -When the input is a `.docx.md` file (already pandoc markdown, not plain text), the -extraction differs from the standard DOCX→plain workflow. The markdown preserves -formatting artifacts that need specific handling. - -## CN/EN boundary detection in merged lines - -Pandoc markdown often merges CN and EN text on heading lines where the DOCX had -multiple runs in the same paragraph: - -``` -# **三、重视文化教育,重塑人生价值** Prioritize Cultural Education; Reshape Life Values {#三、...} -``` - -The boundary regex must account for whitespace on BOTH sides of `**` markers: - -```python -# WRONG: \** then \s* — fails when there's space BEFORE * (值 *Realizing) -r'[\u4e00-\u9fff](?:\*{0,2})\s*([A-Za-z])' - -# WRONG: \s* then \** — fails when there's space AFTER ** (值** Prioritize) -r'[\u4e00-\u9fff]\s*\**([A-Za-z])' - -# CORRECT: whitespace on both sides of optional * -r'[\u4e00-\u9fff]\s*\**\s*([A-Za-z])' -``` - -Include CJK punctuation ranges in the character class: -`[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]` - -## Anchor stripping - -Strip BOTH `{#anchor}` (pandoc heading anchors) AND `[text](#link)` (markdown TOC links) -before further processing: - -```python -def strip_anchors(s): - s = re.sub(r'\{#[^}]*\}', '', s) # {#anchor} - s = re.sub(r'\[([^\]]*)\]\([^)]*\)', r'\1', s) # [text](#link) → text - return s -``` - -## TOC handling — two critical guards - -### Guard 1: Don't pair TOC CN entries with following EN lines - -TOC entries often look like CN-heading-followed-by-EN (the standard pair pattern), -but the following EN is actually the next TOC entry or heading: - -``` -[一、营造禅意氛围,优化工作环境\t1](#anchor) -[二、重视慈善关爱...] -... -1、Create a Chan(Zen) Atmosphere; Optimize Your Environment -2、Focus on Compassion and Care; Build Good Relationships -``` - -If the last CN TOC entry is followed by a blank line then the first EN TOC entry, -the "CN → blank → EN" heading pattern will consume the first EN TOC entry. -Guard against this: - -```python -def is_toc_line(line, cn_raw): - if re.search(r'\[.*\]\(.*\)', line): # markdown link - return True - if re.search(r'\t\d+', cn_raw): # tab + page number - return True - return False -``` - -Skip the CN→EN and CN→blank→EN patterns when `is_toc_line()` returns True. -These CN TOC entries should stay unpaired (en='') and get their EN from the -separate EN TOC list. - -### Guard 2: Pair TOC EN entries forward, not backward - -EN TOC entries appear as standalone non-CJK lines. They must be paired with -the FIRST unpaired CN (not the last): - -```python -# CORRECT: forward iteration -for j in range(len(pairs)): - if not pairs[j][1] and has_cjk(pairs[j][0]): - pairs[j] = (pairs[j][0], en) - break - -# WRONG: reverse iteration (pairs last-first, shifting everything) -for j in range(len(pairs)-1, -1, -1): - ... -``` - -## Orphaned trailing `*` from italic splits - -When a line has italicized EN text (`*Realizing Ultimate Value*`) and the split -point is at the `R` (after the opening `*` is consumed by the CN cleanup), the -EN text retains a trailing `*`: `Realizing Ultimate Value*`. Strip it: - -```python -def clean_en(s): - s = re.sub(r'^\d+[、,.]\s*', '', s) - s = re.sub(r'\*+$', '', s) # orphaned italic close - return s.strip() -``` - -## `clean_cn` — operation order matters - -Strip leading numbers BEFORE heading markers. A line like `1. # **营造禅意...` -starts with a digit, so `^#+\s*\**` won't match until the number is gone: - -```python -def clean_cn(s): - s = re.sub(r'^\d+\.\s*', '', s) # leading number FIRST - s = re.sub(r'^#+\s*\**', '', s) # then heading markers - s = re.sub(r'\**\s*$', '', s) # trailing ** - s = re.sub(r'\t\d+$', '', s) # trailing page number - return s.strip() -``` - -## Full extraction recipe - -1. Read `.docx.md` text -2. For each line: strip anchors, detect CJK/EN -3. CJK lines: try `split_cnen()` first (merged CN+EN). If that gives EN, use it. - Otherwise look ahead for EN on next line or next+blank — but SKIP if TOC-like. -4. EN-only lines: pair forward with first unpaired CN. -5. Clean: strip heading markers, leading numbers, trailing page numbers. -6. Write `source.dj` (CN only) and `bilingual.dj` with proper markdown structure. - -## bilingual.dj output format - -Must follow the project convention (see reference bilingual.dj in any completed article): - -``` -# CN Title -# EN Title - ----CN Subtitle ----EN Subtitle - -CN Author -EN Author - -- CN TOC item 1 -- CN TOC item 2 -... - -- EN TOC item 1 -- EN TOC item 2 -... - -CN body paragraph -EN body paragraph - -CN section heading ← plain text, no # prefix -EN section heading - -... - -CN sub-heading ← plain text, e.g. "1. 创造精神财富" -EN sub-heading -``` - -- Title: `# ` prefix on both CN and EN -- Subtitle: `---` prefix (3 dashes, no space after) -- Author: plain text, one pair -- TOC: `- ` bullet, CN block then EN block (not interleaved), blank line between blocks -- Body headings: plain text, no `#` or `##` markers. Hierarchy conveyed by numbering: - `一、二、三、` for major sections, `1. 2. 3.` for sub-sections -- Body paragraphs: interleaved (CN line, EN line, blank) -- `source.dj` uses `# Title`, `---subtitle`, `## section headings` — different from bilingual.dj which uses plain body headings diff --git a/skills/translation-review/references/edit-suggestions-in-bilingual.md b/skills/translation-review/references/edit-suggestions-in-bilingual.md deleted file mode 100644 index 8cd1f51..0000000 --- a/skills/translation-review/references/edit-suggestions-in-bilingual.md +++ /dev/null @@ -1,65 +0,0 @@ -# Edit Suggestions in bilingual.dj - -When the user asks to add edit suggestions directly into bilingual.dj, use this -two-tier approach. - -## Two files - -- `bilingual.dj` — clean: inline corrections applied, NO `{% %}` comment lines -- `commented.dj` — same content + `{% ... %}` comment lines interleaved - -This lets the user diff them side by side. - -## Inline corrections - -Apply directly to the English text. These are fixes the reviewer is confident about: - -- Typos, mechanical issues (Chinese punctuation in EN, double periods, stray `*`, unbalanced quotes) -- Grammar fixes (subject-verb agreement, missing articles) -- Wording improvements (clunky literal translations → idiomatic English) - -Apply with `patch` (mode='replace'). Verify uniqueness before -replacing — many EN paragraphs are long single lines, so match a unique -substring. Never use regex-based string replacement in `execute_code`. - -## `{% %}` comment lines - -For translation decisions worth documenting but not "correct" per se: - -``` -CN paragraph -EN paragraph -{% reason for the choice, alternative renderings %} -(blank) -``` - -The comment goes AFTER the EN line (before the blank separator). One comment per -issue. Keep them terse. - -What to comment on: -- Literal translation of idioms (`单线程` → "single-threaded") -- Translation choices that differ from literal meaning (`精神利益` → "nourishing the spirit") -- Standard Buddhist terminology (`正命` → "Right Livelihood") -- Glosses added/dropped (`道场` — "(Dojo)" parenthetical removed) -- Idiom translations (`甘之若饴` → "as sweet as syrup") -- Paired terms where one rendering influences the other (`魔性` → "demon-nature" to parallel "Buddha-nature") -- Scripture citations with Sanskrit titles (`普贤行愿品` → "Gaṇḍavyūha Sūtra") - -What NOT to comment on: -- Obvious corrections (typos, grammar) -- Names, dates, simple connectives -- Standard renderings with no interesting choice - -## Pitfalls - -- **Never split a paragraph mid-sentence** with a `{% %}` comment. Comments go - AFTER the full EN paragraph, before the blank separator. If a string - replacement inserts `\n{% %}` in the middle of a paragraph, it breaks the - interleaved structure. - -- **Check for merged comments**: after inserting, scan for `{% ... %} ` followed - by EN text on the same line. These must be split so the EN text continues on - the next line. - -- **Collapse triple+ blank lines**: comment insertions can create extra blanks. - Run `re.sub(r'\n\n\n+', '\n\n', text)` after all insertions. diff --git a/skills/translation-review/references/proofreading-patterns.md b/skills/translation-review/references/proofreading-patterns.md deleted file mode 100644 index 0e68d18..0000000 --- a/skills/translation-review/references/proofreading-patterns.md +++ /dev/null @@ -1,116 +0,0 @@ -# Proofreading: Manuscript vs Typeset - -AGENTS.md defines two workflows: Translation (A) and Proofread (B). -The workflows below are Proofread mode — English comes from an existing -manuscript and is authoritative. Only flag mechanical/manuscript-level issues. - -## Two extraction workflows - -### A. Bilingual from DOCX (standard) - -When the DOCX manuscript has both Chinese and English in 1:1 paragraph -correspondence, generate `bilingual.dj` directly from the DOCX: - -1. `pandoc docx → plain text` -2. Extract Chinese-English pairs from body -3. Apply fixes: italicize Sanskrit on first occurrence, fix `N.Letter` → `N. Letter` spacing -4. Write bilingual.dj -The DOCX English is the authoritative target text. No PDF needed. - -**Extraction approach**: start by adapting `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. - -### A2. Bilingual from `.docx.md` (pandoc markdown output) - -When working with an already-converted `.docx.md` file (pandoc markdown, not plain -text), use the techniques in `references/docx-md-extraction.md`. Key differences -from plain-text extraction: merged CN+EN on heading lines, `{#anchor}` and -`[text](#link)` artifacts, TOC pairing guards. - -### B. Bilingual from PDF (when PDF is the typeset target) -sections that order CN content before EN content (CN heading → CN body → EN heading → -EN body), the simple alternation fails. Use block-based extraction instead: - -1. Tag each non-blank line as CN or EN (via `has_cjk()`) -2. Join page-break split paragraphs: merge consecutive same-language paragraphs - only when the first is long (>30 chars), doesn't end with CJK/ASCII terminal - punctuation (`[。!?:).?!]$`), and isn't heading-like (starts with - `^[\dIVX]+[\.\s]` and <60 chars) -3. Group consecutive same-language items into blocks -4. Walk blocks: for each CN block, pair with the next EN block via `zip()`. - `min(len(cn), len(en))` handles translator-introduced paragraph splits. - -**Page-break splits in pandoc plain-text output**: the DOCX→plain conversion -sometimes splits a Chinese paragraph mid-sentence (e.g. `白居` + `易、苏轼…`). -These appear as two consecutive CN lines separated by a blank. The join heuristic -above catches these reliably. For EN text, page-break splits are rare; the heading -detection (`^[\dIVX]+[\.\s]`, <60 chars) prevents false merges of EN headings -with following EN body paragraphs. -The DOCX English is the authoritative target text. No PDF needed. - -### B. Bilingual from PDF (when PDF is the typeset target) - -When the PDF English is the typeset "final" version and should be the target: - -1. Extract DOCX Chinese paragraphs (source) -2. Extract PDF body text via `pdftotext -layout` -3. Clean PDF: remove slug lines, headers, page numbers, join hyphenation breaks -4. Match DOCX English paragraphs against PDF body to find positions -5. Segment PDF body at matched positions -6. Write bilingual.dj with Chinese source + PDF English target - -**Pitfalls in PDF extraction:** -- Consecutive hyphenation breaks (e.g. `thou-` + `sand...al-` + `leviate`) — the join - loop must be recursive: after joining pair N, check if result still ends with `-` - and join with line N+2 -- Lines with leading whitespace: use `lstrip()` before checking `n[0].islower()` -- Drop-cap artifacts: `L iving` → `Living` -- Trailing section numbers: `...viewpoints. 1)` — the ` 1)` is a PDF section marker - bleeding into the previous paragraph - -### C. Edit suggestions (edit-suggestions.dj) - -After generating bilingual.dj, scan for issues and write `edit-suggestions.dj`: - -**Format**: follow the original document's section/chapter layout. Group suggestions -under the chapter headings where the issues occur. Use diff-style `-/+` notation. - -**What to flag:** -- Garbled Chinese text (merged duplicate edits in source DOCX) -- Repeated words (`the The`) -- Chapter numbering mismatches (e.g. `九` ↔ `VIII`) -- Translator notes in headings (`(某某翻,某某审)`) -- Missing quotes around dialogue/speech - -## Common source DOCX issues - -- Translator notes in Chinese headings: `(某某翻,某某审)` — delete for publication -- Merged duplicate edits: cut-paste errors where old+new text appear together -- `N.Letter` without space: `2.How` → `2. How` -- `the The` double article -- **Numbering mismatches**: CN and EN headings sometimes disagree (e.g. CN `3.` vs EN `2.`). - The TOC usually has the correct number — flag the body heading for correction. -- **Doubled names**: `岳麓书院岳麓书院` — cut-paste artifacts in Chinese body text. -- **EN paragraph splits without CN counterpart**: translator sometimes renders one CN - paragraph as two EN paragraphs. The block-based extractor drops the extra EN paragraph - (as `min(len_cn, len_en)`). Flag in edit-suggestions so it can be manually merged or - the CN paragraph can be split. - -## Sanskrit italicization - -On first occurrence in body text, wrap with `*term*`. Track seen terms across -the full body. Terms: bodhisattva, bodhicitta, samsara, Dharma, karma, nirvana, -Sangha, sutra, Mahayana, Sravaka, Vinaya, Lamrim, Ksitigarbha, Samantabhadra, -Chan, Arhatship, Theravada. - -## Proofread scope boundary - -When proofreading a DOCX manuscript: -- **DO flag**: typos, double words, double punctuation, numbering mismatches, - garbled text, translator notes, duplicate names, capitalization errors. -- **Do NOT flag**: em-dash formatting (`—` vs `---`), terminology choices, - translation style, calques, word order. The manuscript English is authoritative. -- **Do NOT apply fixes** — write `edit-suggestions.dj` only. -- If the user asks for translation review separately, write findings to - `translation-findings.dj`. diff --git a/skills/translation-review/references/terms-db-alignment.md b/skills/translation-review/references/terms-db-alignment.md deleted file mode 100644 index 6f48b4f..0000000 --- a/skills/translation-review/references/terms-db-alignment.md +++ /dev/null @@ -1,64 +0,0 @@ -# Terms Database Alignment - -Batch-align translation glossary entries and body text against the MPI terms database. - -## Module API (preferred) - -Import directly in `execute_code` scripts — no subprocess, no server, no text parsing: - -```python -import sys -sys.path.insert(0, '$MPI_PROJECT_ROOT/terms-search') -from search import search - -results = search("三级修学", limit=5) -results = search("空性", loc="心经", src="DoT定稿", limit=5) -# returns list of {zh, en, loc, source} dicts -``` - -## Batch lookup pattern - -```python -import sys -sys.path.insert(0, '$MPI_PROJECT_ROOT/terms-search') -from search import search - -terms = ["三无漏学", "八步三禅", "闻思修", ...] -author_sources = {"DoT定稿", "内部特色词", "佛教术语", "经论名"} - -for term in terms: - results = search(term, limit=10) - relevant = [r for r in results if r["source"] in author_sources] - for r in relevant: - print(f"{r['zh']} → {r['en']} [{r['source']}]") -``` - -Or filter to a single authoritative source directly: -```python -results = search("三级修学", src="DoT定稿", limit=5) -``` - -## Priority ranking - -When the same term has entries in multiple source tables, prefer: -1. DoT定稿 (highest authority — final translation decisions) -2. 内部特色词 (MPI internal terminology) -3. 佛教术语 (general Buddhist terminology) -4. 经论名 (sutra/shastra titles) - -## Alignment workflow - -1. Extract all Chinese glossary terms from `{% "TERM" ... %}` blocks in the .dj file -2. Extract body-text domain terms that may not have glossary entries -3. Batch-query each term against the HTTP API -4. Filter results to authoritative source tables -5. Compare DB canonical translation against current file translation -6. Flag mismatches where DB entry differs materially from current -7. Apply fixes with `patch` tool — fix both glossary comments AND body text occurrences -8. Verify with `grep` that no old terms remain - -## Pitfalls - -- `replace_all` can create doubled words when the surrounding context already contains the replacement string (e.g., "The Eight Steps" → "The The Eight Steps"). Prefer targeted single-replacement patches. -- Start patches from the bottom of the file upward to preserve line numbers. -- Some DB entries are contextual phrases (e.g., "珍惜法缘" → a full sentence), not standalone term translations. Use standalone term entries where available. diff --git a/skills/translation-review/references/translation-pitfalls.md b/skills/translation-review/references/translation-pitfalls.md deleted file mode 100644 index 8e6f61b..0000000 --- a/skills/translation-review/references/translation-pitfalls.md +++ /dev/null @@ -1,441 +0,0 @@ -# Translation Pitfalls — MPI Buddhist Texts - -Patterns found in CN→EN translation review. Add to this file as new patterns emerge. - -## Terminology conflation - -### 人生佛教/人间佛教 — distinct concepts - -人生佛教 (Taixu's "Buddhism for Human Life") and 人间佛教 (Yinshun's "Humanistic Buddhism") -are distinct doctrinal positions in modern Chinese Buddhism. Do not conflate both into -"Humanistic Buddhism." When the source uses 人生佛教, render as "Buddhism for Human Life" -or "Human Life Buddhism." User may propose a deliberately non-standard rendering -("Buddhism for daily lives") — that's their call. Don't unilaterally pick from -the standard set without checking. - -### 心性论 → buddha-nature (WRONG) - -心性 (mind-nature) is broader than 佛性 (buddha-nature / tathāgatagarbha). -When a text discusses 心性 in the context of Confucian self-cultivation or general -Buddhist psychology, use "mind-nature" or "nature of mind." Reserve "buddha-nature" -only when the text explicitly references tathāgatagarbha doctrine. - -### 恨 → resentment (WRONG) - -恨 means "hatred," not "resentment." In the triad 羡慕嫉妒恨 (envy, jealousy, hatred), -the force is strong. "Resentment" is too mild. - -### 感悟 → conversant / heartfelt (WRONG) - -感悟 means experiential insight or realization. It is not intellectual familiarity -("conversant") or emotional warmth ("heartfelt"). Render as "insight," "realization," -or "deep understanding." - -### 关爱/关怀 → compassion (WRONG) - -Chinese 关爱 and 关怀 mean "care" or "loving care." They are NOT 慈悲 (compassion / karuṇā). -Conflating them obscures two distinct Buddhist concepts. - -Check every occurrence of "compassion" in a translation against the source: -- If source is 关爱/关怀 → "care" -- If source is 慈悲 → "compassion" (correct) -- If source is 悬壶济世 → "compassionate mission" (correct — the healing spirit) - -### 生存层面 → making a living (WRONG) - -生存层面 = the existential/survival dimension. Not just earning wages. -→ "survival-level needs" or "the level of basic existence" - -### 修学 → "practice" (drops study dimension) - -修学 combines 修 (practice/cultivation) and 学 (study/learning). Rendering only -as "practice" loses the study dimension. Use "practice and study" or -"cultivation and learning" — especially in 静心学堂/Dharma study contexts where -the academic dimension is emphasized. - -### 信仰者 → "believers" (Christian connotation) - -信仰者 = "people of faith" but in Buddhist context, "believers" carries Christian -overtones. Use "practitioners" or "adherents" to avoid the implication. Reserve -"believers" for contexts where the source explicitly uses 信徒 or where the -Christian parallel is the point. - -## Loss of Dharma meaning - -### 生生增上 → continuously elevate our life (INCOMPLETE) - -生生 = life after life (multi-life Buddhist perspective). The single-life rendering -"continuously elevate our life" loses the Dharma meaning entirely. -→ "continuously elevate our life, life after life" - -## Degree / register shifts - -### "tantamount to" drops 几乎 - -几乎 = "almost." "tantamount to" = "is in fact." Dropping 几乎 strengthens the -claim. Source "几乎等同于一次全球化运动" = "almost equivalent to a globalization -movement," NOT "tantamount to a globalization movement." - -### "untenable" for 都是不行的 (overstates) - -都是不行的 = "is not acceptable / won't do." "Untenable" = "indefensible" or -"cannot be maintained." Too strong. Use "impermissible" or "is not acceptable." - -### "monumental event" for 大事 (slight stretch) - -大事 = "a major event" or "an event of importance." "Monumental" is typically -reserved for tasks, errors, or achievements (e.g. "monumental task," -"monumental mistake"). "Event of great importance" or "landmark event" is more -idiomatic. - -### "perennial" for 永恒 (wrong register) - -永恒 = eternal/ultimate. "Perennial" = recurring (per year, per season). They -are not synonyms. Use "eternal" or "ultimate." - -### 学部委员 → Member (UNDERSTATES) - -学部委员 is CASS's highest academic title, equivalent to "Academician." -"Member" understates the prestige significantly. → "Academician" or -"Member of the Academic Divisions." - -### 文明 → culture (WRONG) - -文明 is "civilization," not 文化 "culture." When a text discusses 文明传播 -(civilizational transmission), do not substitute "cultural transmission." - -### 教制建设 → reforming (ADDS CONNOTATION) - -教制建设 means "developing/building monastic institutions." Adding "reforming" -introduces a connotation of fixing something broken that is not in the source. -→ "developing monastic institutions" or "institutional development." - -### 一荣俱荣、一损俱损 → too loose - -This idiom has a conditional structure: "if one prospers, all prosper; if one -suffers, all suffer." Rendering as "thrived together and suffered together" -loses the mutual-dependence logic. → "shared prosperity and adversity alike" -or "rose and fell together." - -### 成圣成贤 collapsing - -圣 (sage) and 贤 (worthy) are distinct Confucian categories. Collapsing both -to "a sage" loses the distinction. → "sagehood and worthiness" or "a sage or worthy." - -### "must" overuse from 应当/倡导 - -Source patterns 应当 (should), 倡导 (advocate/champion), 今后要 (going forward, should) -often get rendered as "must" by reflex. "must" in English is a strong directive -appropriate only for 一定要, 必须, 务必. Default to "should" or "ought to" for -recommendations. A whole section may be 倡导 without 一定 anywhere — don't -manufacture urgency the source doesn't have. - -### 文明 → culture (WRONG) - -文明 is "civilization," not 文化 "culture." When a text discusses 文明传播 -(civilizational transmission), do not substitute "cultural transmission." - -#### 因病返贫 → "back into poverty" -"返贫" means becoming poor due to illness, not returning to previous poverty. Use "into poverty" or "driven into poverty." - -#### Diacritics: use DB form, not academic Sanskrit -| Wrong | Right | Source | -|---|---|---| -| `Mahāsthāmaprāpta` | `Mahasthamaprapta` | 佛教术语 | -| `Yogācārabhūmi Śāstra` | `Yogacarabhumi-Sastra` | 经论名 | -| `Avalokiteśvara` | `Guanyin` | 佛教术语 | -| `pravāraṇā` | `Pavarana` | BAICKZ | - -Exception: `Kṣitigarbha` — DoT定稿 uses diacritics, so keep them. -When in doubt, search the DB and follow the highest-priority source. See dharma-translation skill `references/diacritics-convention.md`. - -返贫 = become poor (from a non-poor state) due to medical costs. "Back" implies -the person was previously poor — not necessarily true. This is about medical bankruptcy. -→ "into poverty" or "fall into poverty" (no "back") - -## Subject-shift calques - -When the source uses an abstract/system noun as the grammatical subject (佛教, -文明, 宗教, 文化) and the target reflexively substitutes a more concrete -agent (Buddhist practitioners, civilization-builders, religious people, etc.), -the English is wrong: the source is *not* talking about people, it's talking -about the system. - -**Pattern**: 佛教都被社会大众赋予期望 → EN drifts to "Buddhist practitioners -are looked upon with hope." Wrong subject — source is 佛教, not 佛教徒. Right: -"Buddhism is regarded with hope by the broader society." - -**Detection**: For each translated sentence, find the *grammatical subject* -in the English and check it matches the *grammatical subject* in the Chinese. -If the English subject is a concrete agent and the Chinese is an abstract -system noun, it's a subject-shift calque. - -## Factual inconsistencies across paired descriptions - -When the same person, place, or thing is described in two different paragraphs -(intro + later reference), check that *every* descriptor matches: title, -affiliation, role, credentials. A translation can be factually inconsistent -even when each individual sentence is correct in isolation. - -**Pattern**: Prof. Wei's credentials in paragraph 1: "Academician of the -Chinese Academy of Social Sciences (CASS) and research fellow at the -Institute of World Religions." Paragraph 5 (same person, same source -reference): "a member of the CASS academic committee and Director of the CASS -Buddhist Research Center." Two different titles for the same CASS affiliation. -One is right, the other is wrong. - -**Detection**: For each named person, build a dict {name → {credential: -sentence_refs}} and check that all credentials in the dict match. Same -institution or title can have different renderings in different paragraphs. - -## Calque checklist (subtle English calques of Chinese verbs) - -These are common Chinese-verb → English-verb pairs where the English word -sounds natural in isolation but is a direct calque of the Chinese. They're -easy to miss in a first-pass review because each one parses correctly: - -| Chinese verb | Wrong (calque) | Right (idiomatic) | -|---|---|---| -| 赋予 (entrust with) | "look to with hope" | "regard with hope" | -| 得到 (obtain) | "draw forth" | "draw on" / "gain" | -| 发挥 (bring into play) | "bring into full play" | "make the most of" | -| 承担 (assume) | "shoulder" | "take on" | -| 重视 | "attach importance to" | "value" / "emphasize" | -| 体现 | "embody" / "reflect" | "show" / "demonstrate" (when abstract) | - -**Detection**: When the English uses an unusual verb that maps 1:1 to a -Chinese word, and the Chinese word is a high-frequency academic verb (发挥, -承担, 体现, 重视), check whether the English reads as a calque. A common -smell: the English verb is "correct" but more formal/dramatic than the -surrounding prose. - -## Tonal coherence inside a parallel list - -When a list of items in a section should have parallel structure (e.g. three -"champion X, oppose Y" items; six "developing X, strengthening Y" items), -check that the *verb choice* is consistent across the list. Inconsistency -within a parallel structure is a strong signal of drift. - -**Pattern**: Section V lists "First, Buddhism should serve as... Second, it -must serve as... Third, it must serve as..." Source has 应当/要做 for all -three. The English should match: all three "should serve as" or all three -"must serve as." Mixing is a tell. - -**Detection**: For each parallel-list section (First/Second/Third, etc.), -extract the verb (or other repeated slot) and verify it's identical. Drift -inside a parallel list is one of the easiest flow issues to catch -mechanically — just look for variance. - -## Multi-pass review structure - -Translation review benefits from three distinct passes, run separately, each -catching a different category of error: - -1. **Pass 1: terminology + consistency + line count** — fast, mechanical. - Catches: 人生佛教/人间佛教 conflation, 修行/修学, 恨→resentment, - paired inconsistencies (Buddhism vs Buddhist practitioners), missing - content, wrong numerals. - -2. **Pass 2: mechanical/formatting** — em-dash convention, double punctuation, - numbering mismatches, capitalisation typos, garbled text. Often skipped - if the file "looks clean." This pass is what makes the file safe to - publish; do it even when no content issues are obvious. - -3. **Pass 3: flow/tonal/calques** — *read the full English as a piece of - prose*. Catch: dramatic verbs that read as calques ("draw forth," - "into full play," "shoulder"), intensifier drift ("profoundly important" - × 3 in one section), consistency of "must"/"should" inside parallel - lists, subject misattribution in calque. Often the user will prompt - this pass with "are you sure it reads well?" or "do the words hang - together?" Treat that prompt as a signal to re-read the whole English - target, not just spot-check. - -Pass 3 in particular is the one that catches the *most embarrassing* errors -— the ones where the English is grammatical and faithful but reads as -"translationese." Don't skip it. - -## DoT定稿 term drift - -### 念死 → recollection of death (WRONG per DoT定稿) - -DoT定稿 has "Cultivating mindfulness of death" / 佛教术语 has "contemplating the -impermanence of death". The established term is "mindfulness of death", not -"recollection of death." → "mindfulness of death" / "death-mindfulness" - -### 三级修学 → Three-Level Study Program (WRONG per DoT定稿) - -DoT定稿 has "Three-Stage Practice." → "Three-Stage Practice" - -### 下士道/中士道/上士道 - -DoT定稿: "Path for Persons of Small/Medium/Great Capacity" — not "path of the -initial/middle/great scope." - -### 观音菩萨 → Avalokiteśvara (AVOID in MPI translations) - -佛教术语 has "Guanshiyin/Guanyin Bodhisattva." Use "Guanyin Bodhisattva." - -## Workflow pitfall - -### Translating before consulting terms DB - - -## Patterns from 人生百问 review (self-review findings) - -The review of *人生百问* surfaced a set of recurring calques, register -mismatches, and Buddhist-term choices that keep reappearing in MPI -CN→EN work. Treat these as a supplemental checklist after the main -translation-pitfalls list. - -### Stiff / formal calques to soften - -| Source pattern | Stiff / literal rendering | Smoother options | -|---|---|---| -| 热衷于 / 很热衷 | "keen on" | "eager to," "enthusiastically pursuing" | -| 善用理性 | "use reason skillfully" | "use reason well," "use reason wisely" | -| 随缘 | "responding to conditions" | "in harmony with conditions" (when it flows better) | -| 人云亦云 | "serve as reference" | "provide guidance," "serve as a reference" | -| 引起共鸣 | "arouse resonance" | "resonate," "strike a chord" | -| 强势而偏执 | "forceful and opinionated" | "forceful and dogmatic," "domineering and inflexible" | -| 执着外境 | "cling to all sorts of external pursuits" | "cling to all sorts of external conditions" | -| 一时兴起 | "spur-of-the-moment impulse" | "spur-of-the-moment idea" | -| 大龄青年 | "older unmarried young people" | "older singles," "unmarried adults" | -| 更受欢迎 | "more welcome" | "more well-liked," "more welcomed by others" | -| 想起来很急,做起来又不急 | "feel urgent but do not act urgently" | "urgent in thought but not in deed" | -| 随大流 / 大众化的 | "popular" | "mass-oriented," "public," "for the general public" | -| 养生之道 | "wisdom culture" | "wisdom tradition," "culture of wisdom" | -| 以此作为标准 | "take X as the only standard" | "make X the only standard" (when the sentence is top-heavy) | - -### 究竟 / 终极 / 更高层次 - -| Source | Awkward | Preferred | -|---|---|---| -| 更究竟 | "more ultimate" | "more complete," "more profound," "higher" | -| 更究竟的成功 | "a more ultimate success" | "a more complete success," "a higher success" | - -In conversational Q&A, 究竟 often reads better as "complete" or "profound" -than as the technical "ultimate." Reserve "ultimate" for explicit doctrinal -discussions of 究竟谛 / ultimate truth. - -### Buddhist term register - -| Source | Issue | Preferred guidance | -|---|---|---| -| 大自在 | "great freedom" loses the "ease" nuance | "great freedom and ease" | -| 开启智慧 | "opened up boundless wisdom" is odd | "awakened," "brought forth," "unfolded" | -| 迷惑 (as affliction) | "confusion" is too generic | "delusion" (in Buddhist contexts) | -| 惑业 | "confusion and karma" | "delusion and karma" (check terms DB) | -| 善知识 | "great wise teachers" is redundant | "great teachers," "wise teachers," "authentic teachers" | -| 皈依三宝 | "take refuge hastily" | "take refuge lightly," "take refuge without being ready" | -| 尽未来际 | "for all future time" is literal | "for all future lives," "throughout all future time" | -| 患得患失 | "worried about personal gains and losses" | "anxious about gain and loss" | -| religious community | vague | "Buddhist community," "Sangha" | -| 报身 | "enjoyment body" vs "reward body" | Verify target convention; MPI often prefers "reward body" | -| 化身 | "transformation body" is common | Verify against target convention | -| 等流果 | "continuative result" | Standard: "result of equal outflow," "correlative effect" | -| 四力 (忏悔) | "power of eradication / remedy" | Use standard four powers terminology: remorse, support, restraint, refuge/reliance | -| 末法 | "Dharma-ending age" / "Age of Dharma Decline" | Check terms DB; avoid inventing new renderings | -| 暇满 | "well-endowed human form" | Standard: "leisure and endowment," "precious human life" | -| 定课 | "fixed chanting session" | "daily practice," "daily recitation" (context-dependent) | -| 八步骤三种禅修 | "Eight Steps and Three Kinds of Meditation" | DoT定稿: "Eight Steps and Three Types of Meditation" | -| 功德 | "merit and virtue" (overused) | "merit" or "virtue" depending on context | -| 福田 | "field of blessing" | Standard: "field of merit/blessing" | -| 止观 | "calm abiding and special insight" | Or "śamatha-vipaśyanā"; avoid inventing new compounds | -| 皈依 / 发心 / 戒律 / 正见 | gerund lists | Noun forms in catalogues: "refuge, aspiration, precepts, right view" | -| 凡夫心 | "ordinary mind" | "mind of an ordinary being" (avoid Chan "ordinary mind" ambiguity) | -| 加持 | "empowers one another" | "supports and blesses one another" (retain religious nuance) | -| 登地菩萨 | "attained the grounds" | Standard rendering | -| 弟子相 | "marks of a disciple" | Standard rendering | -| 无缘大慈、同体大悲 | — | "unconditional great compassion and the great sympathy of seeing others as oneself" | -| 人成即佛成 | — | "When a Human Is Perfected, Buddhahood Is Perfected" | -| 人生佛教 | — | "Buddhism for Human Life" (do not conflate with 人间佛教 / Humanistic Buddhism) | -| 三级修学 | — | DoT定稿: "Three-Stage Practice" | -| 同喜班 | — | "Tongxi Class" (MPI convention; verify if glossary requires translation) | - -### Idioms and set phrases - -| Source | Literal / awkward | Preferred | -|---|---|---| -| 妄念纷飞 / 妄想纷飞 | "delusive thoughts still fly about" / "random thoughts fly about" | "delusions run wild," "wandering thoughts arise" | -| 望尘莫及 | "outdo animals by far" (wrong direction) | "animals cannot compare," "are left far behind" | -| 脚踏两条船 / 骑墙 | "straddling two boats" | "riding two boats," "trying to walk two paths" | -| 选择困难症 | "choice difficulty" | "decision paralysis," "choice paralysis" | -| 树欲静而风不止 | "the tree wishing stillness while the wind keeps blowing" | "the tree may crave stillness, but the wind will not cease" | -| 磨刀不误砍柴工 | — | "sharpen the ax before cutting wood" (standard) | -| 泥菩萨过江 | "clay bodhisattva" | Allude to the idiom; may need explanatory note | -| 鄙视链 | "contempt chains" | Loan-translation is acceptable; keep if context is clear | -| 十年浩劫 | "decade of catastrophe" | International readers may need a gloss | -| 红尘滚滚 | "rolling red dust" | Acceptable in Buddhist register; do not soften | -| 因上努力,果上随缘 | — | "work hard on the causes and let the results unfold as they may" | -| 朝不保夕 | "we may not last until evening" | "life is precarious" | -| 本末倒置 | — | "put the cart before the horse" | -| 润物细无声 | — | "quiet, pervasive influence, like rain soaking into the earth unnoticed" | -| 有口无心 | — | "with their mouths but not their hearts" | -| 人非圣贤,孰能无过 | "To err is human" | Acceptable, but note it loses the Buddhist register | -| Standard Buddhist idiom | — | Always prefer established English Buddhist idiom when one exists (e.g. Diamond Sutra "lives" not "bodies") | - -### Sentence structure / rhythm - -| Pattern | Problem | Fix | -|---|---|---| -| 3+ clause sentences | Can't be read in one breath | Split at natural breaks, especially after "world" or "Dharma" | -| Top-heavy sentences | Main clause arrives too late | Move the main clause forward | -| "is it not too late to X" | Inverted clause is awkward | "it is not too late to X" | -| "patterned, standardized" | "patterned" is odd in English | "structured, standardized" or "model-based, standardized" | -| "way of management" | awkward collocation | "approach to management," "style of governance" | -| "correction of X through correcting Y" | repetitive | "transform X by correcting Y" | -| "special circumstances are encountered" | passive/abstract | "special circumstances may create an exception" | -| "formed from the Buddha's wisdom and merit" | mechanical | "arises from," "embodied wisdom and merit" | -| "with every sound entering the ears" | flat idiom | "with every syllable clearly heard" | -| "influenced by the liberation of individuality" | wrong collocation | "emancipation of the individual," "individual liberation" | -| "the optimization of life" | technical-sounding | "flourishing of life," "growth in life" | -| "right-livelihood occupation" | redundant | "right livelihood" | -| "when time is used more valuably" | awkward adverb | "when time is used more wisely" | -| "doing great things" | OK | Keep plain; do not overtranslate 做大事 | -| "speak merely to show off your own cleverness" | misses idiom | 逞口舌之快 → "speak for the thrill of it," "indulge in sharp talk" | -| "the power of eradication... the power of remedy" | non-standard | Use standard four powers terminology | -| "pincer attack from within and without" | too militaristic in pastoral context | "pressure from within and without," "assault from both sides" | -| "stalemate" | OK but possibly cold | "stuck place," "impasse" in spiritual contexts | -| "persevere in one sutra" | loses metaphor | "go deeply into one sutra" | -| "muddle-headedly" | rare adverb | "in a muddled way," "without understanding" | -| "thoroughly applied" | slightly off | "when proficiency develops" | -| "Using relics to check seems a little too late" | OK but flat | "Using relics as a measure seems rather late in the day" | -| "unknowable mysticism" | 玄学 ≠ mysticism | "unknowable mystery" | -| "reliance relationship" | awkward | "teacher-student relationship" | -| "every place becomes a place of practice" | repetitive | "everywhere becomes a field of practice" | -| "keeping an ordinary mind when things go well and still keeping an ordinary mind when things go badly" | wordy | "keeping an ordinary mind in both good times and bad" | -| "ordinary, lax, and indulgent mind" | acceptable but loose | "ordinary mind of laxity and indulgence" | - -### Register: spoken Q&A vs. written Dharma - -*人生百问* is conversational Q&A. Keep the English warm and direct: - -| Stiff / written | Conversational | -|---|---| -| "I do not think so necessarily." | "Not necessarily." / "I don't think so." | -| "bustle about for liberation" | "rush about," "busy themselves" | -| "starting a family is for liberation from singlehood" | "liberation from being single" | -| "Seen only from the surface" | "On the surface," "From a surface view" | -| "If one is liberated, how can these dreams be realized?" | "...how can these dreams still be realized?" | -| "I deeply know that..." | "I know full well that..." | -| "For yourself, you must..." | "First, for yourself..." / "Personally, you must..." | -| "I believe that the external conditions we attract will also change." | "the conditions we draw to ourselves will also change" | - -### Parallelism and grammar fixes - -| Issue | Example | Fix | -|---|---|---| -| Gerund/infinitive mismatch | "drawing near to wise teachers and to rely on them" | "draw near to wise teachers and rely on them" | -| Missing "still" | "how can these dreams be realized?" | "how can these dreams still be realized?" | -| Mixed singular/plural | "the largest debt, not all of them" | "the largest debt, not every debt" | -| Redundant compound | "sharp and keen faculties" | "sharp faculties" or "keen faculties" | -| "disorder" as verb | "'nao' is to disorder" | "'nao' is to disturb" | - -### Meta-pattern: when the review produces `review-comments.dj` - -After a self-review or other-review pass, scan the resulting -`review-comments.dj` for **recurring patterns** and add them to this file -and to `buddhist-terminology.md`. The review of *人生百问* began as a -single article checklist and became a reusable pattern library; future -reviews should do the same. - diff --git a/skills/translation/SKILL.md b/skills/translation/SKILL.md deleted file mode 100644 index 9871837..0000000 --- a/skills/translation/SKILL.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -name: translation -description: Translate Chinese↔English Buddhist/Dharma content — register guidance from Mindfulness Bell corpus, tone, voice, cultural bridging technique. -inputs: source.dj (Chinese djot), or .docx via docx2dj.fish -outputs: target.dj (English djot), bilingual.dj, edit-suggestions.dj ---- - -# Translation - -Core translation technique for Buddhist/Dharma content. Conventions (djot format, -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. - -## Mindfulness Bell Corpus - -English Buddhist prose — register/style reference for translation. - -- **PDFs + index**: `~/meta/www.files/public/The Mindfulness Bell/` — 6 issues (MB92–MB97, 2023–2026), `index.yaml` lists all articles by author/title/page -- **Articles**: `~/documents/jingxin-lessons/Mindfulness Bell/MB{92..97}/*.md` — per-issue markdown files. Read with `read_file`. - -Four registers, useful as style targets: - -| Register | Example | Key features | -|----------|---------|-------------| -| Dharma talk | Thầy (MB94 "Roses and Garbage", MB97 "Go as a River") | Short sentences, concrete images, coined terms ("interbeing"), oral address, Sanskrit with narrative explanation | -| Teaching lineage | Sister Đoan Nghiêm (MB93 "Our Patriarch Liễu Quán") | "We" voice, terms explained, cultural bridging ("like Jesus"), dates in narrative | -| Personal narrative | Mick McEvoy (MB94 "Touching the True Nature") | First-person, confessional, borrowed Dharma vocabulary, vernacular | -| Editorial | Brother Pháp Lưu (MB94 welcome letter) | Polished but warm, conceptual framing, "we" address | - -Quick-find in index: Thầy talks → `"Thích Nhất Hạnh"` + page ≤ 10; teachings → `"Sister"` or `"Brother"`; narratives → first-page articles by non-monastics; lineage → `"Patriarch"` or `"ancestor"`. - -## Translation Principles - -1. **Terms**: Either keep Sanskrit w/ narrative explanation (bodhisattva, Māra) OR coin new English (interbeing, inter-are). Avoid clunky calques. -2. **Cultural bridging**: Add bridges for Western readers. A Chinese text mentioning 孔子 can stay; explain the function. Đoan Nghiêm's "like Jesus" is the pattern. -3. **Tone**: Chinese Dharma texts are typically more formal than English equivalents. Decide consciously: keep formality or warm up (Thầy style). -4. **Voice**: Direct address ("you"), concrete images, and oral rhythm make Dharma land in English. Abstract noun chains (common in Chinese→English translationese) kill it. -5. **Sutra quotes**: Use standard English Buddhist idiom. Check terse-idiom conventions (e.g., Diamond Sutra "lives" not "bodies"). - -## Pitfalls - -### Pre-flight accuracy check - -Before delivering a translation, run through the structured accuracy/readability -taxonomy in `../self-review/references/common-issues-taxonomy.md`. -Catching these before review saves iteration cycles: -- Omission, over-literal renderings, over-free renderings, subject confusion, - overtranslation, terminology errors -- Long/nested sentences, passive voice clustering, nominalization, top-heavy - structure, obscure word choices, weak transitions - -### 1:1 line mapping - -Each physical source line maps to exactly one physical target line. Never merge -continuation lines (lines ending with ` ` soft breaks) into a single -translation entry. The bilingual format preserves the original line structure — -breaking this destroys alignment. - -### Blank lines in bilingual - -When creating an initial bilingual template from source only: blank source -lines pass through as-is. Only non-blank lines get an empty target placeholder. -Treating blank lines as content lines (adding target+separator) creates -excessive blank clusters. See `references/bilingual-format.md`. - -### Soft break markers - -Trailing ` ` (two spaces) on source lines indicate soft line breaks (paragraph -continuations). Preserve these markers on both source and target lines. - -### Practice element lists: use noun forms - -When translating lists of Buddhist practice elements — especially the five -essentials (皈依、发心、戒律、正见、止观) or similar enumerated components — -render each as a noun phrase, not a gerund. "Refuge, aspiration, precepts, -right view, śamatha-vipaśyanā" — not "taking refuge, arousing aspiration." -These are named components of a system, not actions being described. The same -applies to any catalog-style listing (三学, 八正道 components, etc.). - -### Terminology decisions: user is the source of truth - -When the user proposes a non-standard rendering (e.g. "人间佛教 should be -'Buddhism for daily lives'") or asks for your input ("although I don't know -what X should be"), respond with a brief options table — strengths and -weaknesses — and let them choose. Do NOT unilaterally commit to a rendering -and start applying it across the file. The user often has a reason for their -proposal (e.g. deliberate departure from terms DB convention) or a strong -opinion they haven't voiced yet. Once they pick, then apply consistently and -annotate in the inline `{% %}` comment WHY this rendering was chosen so future -editors know it was deliberate, not a slip. - -## Polishing (润色) - -After translating, do a deliberate readability pass before submitting for review. -Read the English aloud — if a sentence can't be spoken in one breath, fix it. - -These are common patterns (not an exhaustive list). For the full taxonomy with -more categories and examples, read -`../self-review/references/common-issues-taxonomy.md`. - -Examples of systematic adjustments: - -1. **Passive → active**. Passive clusters (3+ per paragraph) are the top - readability killer in CN→EN translation. `If meat consumption were reduced` - → `If people consume less meat`. -2. **Nominalization → verb**. `placed emphasis on cultivating` → `emphasized - cultivating`; `the application of` → `applying`. -3. **Sentence splitting**. If a sentence has 3+ clauses and runs past one breath, - split it. The source's period is not a contract — English readers need - shorter breath units than Chinese readers. -4. **Academic → plain**. `constitute` → `make up`; `facilitate` → `help`; - `endeavor to` → `try to`; `in order to` → `to`. These texts are lectures - and conversations, not journal articles. -5. **Register match**. Dharma talks and dialogues should sound spoken — - contractions, direct address, concrete images. If the English reads like a - paper abstract, warm it up. Check against the MB corpus registers for the - target genre. -6. **Review-specific calques**. For *人生百问*-style Q&A, check the pattern - tables in `../self-review/references/translation-pitfalls.md` for recurring - stiff calques ("keen on," "more ultimate," "choice difficulty," etc.) and - the Buddhist-term register notes in `../self-review/references/buddhist-terminology.md`. - -Do NOT apply these mechanically — each is a judgment call. A passive may be -correct when the agent is unknown; a nominalization may be the right technical -term. The goal is natural English that matches the source's register, not a -formulaic rewrite. - -## Quality Gates (Before Declaring Done) - -Run this self-check before you hand off a first-pass translation. The goal is to -catch the most expensive errors while they are still cheap to fix. For the full -post-translation review workflows, load `self-review` and `other-review`. - -### Accuracy - -- [ ] **Line parity**: source and target line counts match exactly. -- [ ] **No missing content**: every Chinese paragraph, quote, poem, or rhetorical - climax has a corresponding English rendering. -- [ ] **No overtranslation**: no parenthetical expansions or explanations not in - the source. -- [ ] **Terminology**: key Buddhist terms checked against the MPI terms DB - (`terms-search` skill). Consistent within the file. -- [ ] **Source faithfulness**: no concepts added, no details dropped. - -### Readability - -- [ ] **Active voice**: abstract/subjectless passives converted to "we" or a - concrete agent where possible. -- [ ] **Noun → verb**: `the propagation of` → `spread`; `placed emphasis on` → - `emphasized`. -- [ ] **Sentence length**: no sentence that cannot be spoken in one breath; split - at natural breaks. -- [ ] **Plain vocabulary**: `constitute` → `is/make up`; `facilitate` → `help`; - `endeavor to` → `try to`. -- [ ] **Register**: match the genre — Dharma talks and dialogues should sound - spoken, not like paper abstracts. -- [ ] **Concrete over abstract**: `mode of existence` → `way of living`; - `ideological content` → `ideas`. - -### Final pass - -Read the entire English target aloud. If anything stalls, rephrase it. - -## Meditation / Mindfulness Content - -When translating guided meditation scripts, exercise guides, or posture instructions -(rather than Dharma talks), use a lighter workflow. See `references/meditation-translation.md`. - -## Other Pitfalls - -### article-specific scripts - -`scripts/proofread-pdf.py` is hardcoded for 佛教徒的人生态度 — body-start -markers, header patterns, slug regex. Do NOT reuse for other articles. -Create article-specific scripts per `references/proofread-pdf-workflow.md`. - -## References - -- `references/meditation-translation.md` — lighter workflow for meditation/mindfulness content -- `references/markdown-to-djot.md` — converting .docx.md to .dj for translation prep -- `references/bilingual-format.md` — bilingual.dj layout: source/target adjacent, blank separator between pairs -- `references/diacritics-convention.md` — diacritics rules -- `references/proofread-pdf-workflow.md` — pattern for creating article-specific PDF-vs-DOCX comparison scripts -- `../self-review/references/common-issues-taxonomy.md` (cross-skill) — structured accuracy/readability checklist for pre-flight review diff --git a/skills/translation/references/bilingual-format.md b/skills/translation/references/bilingual-format.md deleted file mode 100644 index 14cea0b..0000000 --- a/skills/translation/references/bilingual-format.md +++ /dev/null @@ -1,41 +0,0 @@ -# Bilingual DJ Format - -## Layout - -Each pair: source line immediately followed by target line. Blank line separates pairs. - -``` -source-line -target-line - -source-line -target-line -``` - -NOT: -``` -source-line - ← WRONG: extra blank between source and target -target-line -``` - -## Creating initial bilingual from source only - -Only non-blank source lines get an empty target placeholder. Blank lines in the -source pass through as-is and serve as natural pair separators. - -``` -source-A - -source-B -``` - -The blank line between source-A and source-B is an original blank from the -source — do NOT add an extra target+separator for it. - -Pitfall: treating blank source lines as content lines creates 3+ consecutive -blank lines (source-blank → target-blank → separator-blank). This is wrong. - -## Verification - -`non_blank_source_lines × 2 + total_source_lines = bilingual_line_count` diff --git a/skills/translation/references/diacritics-convention.md b/skills/translation/references/diacritics-convention.md deleted file mode 100644 index 901269a..0000000 --- a/skills/translation/references/diacritics-convention.md +++ /dev/null @@ -1,13 +0,0 @@ -# Diacritics Convention - -When in doubt, search the terms DB and use the highest-priority source's form. - -| Rule | Examples | -|---|---| -| No diacritics (default) | `Mahasthamaprapta` (佛教术语), `Yogacarabhumi-Sastra` (经论名), `Guanyin` (佛教术语), `Pabongkhapa` (nti) | -| With diacritics | `Kṣitigarbha` (DoT定稿 uses this form) | -| Sanskrit terms | Keep standard romanization: `bodhicitta`, `bardo`, `Amitabha`, `prajñā` | - -The DB uses simplified romanization. DoT定稿 is the authority — if it uses diacritics for a term, follow it. Otherwise strip them. - -Pitfall: academic/pedantic diacritics (`Mahāsthāmaprāpta`, `Yogācārabhūmi Śāstra`, `Avalokiteśvara`) are common in general knowledge but wrong per MPI conventions. diff --git a/skills/translation/references/markdown-to-djot.md b/skills/translation/references/markdown-to-djot.md deleted file mode 100644 index 79aa3cf..0000000 --- a/skills/translation/references/markdown-to-djot.md +++ /dev/null @@ -1,65 +0,0 @@ -# Markdown to Djot Conversion - -When source material arrives as `.docx.md` (pandoc-converted from docx), convert to `.dj` for translation workflows. - -## Splitting combined articles - -If a single markdown file contains multiple articles (common when docx has two talks in one file), split at the article boundary before converting. Use `sed` by line number: - -```bash -sed -n '1,218p' combined.md > a1.md -sed -n '220,282p' combined.md > a2.md -``` - -## TOC stripping - -Pandoc docx→md produces a markdown TOC with tab-separated page numbers: - -```markdown -[一、对佛教的感悟\t1](#一、对佛教的感悟) -[二、佛教与人类文明\t5](#二、佛教与人类文明) -``` - -Strip before conversion: - -```bash -sed -i '/^\[.*\t.*\](#.*)$/d' input.md -``` - -Or in Python: skip lines matching `line.startswith("[") and "\t" in line and "](#" in line`. - -## Heading anchor cleanup - -Pandoc's docx→md conversion adds `{#heading-id}` anchors to every heading: - -```markdown -## 1.安宁疗护 {#1.安宁疗护} -``` - -These must be stripped before markdown→djot conversion, otherwise pandoc's djot writer leaves stray `{#...}` lines in the output: - -```bash -sed 's/ {#[^}]*}//g' input.md > clean.md -``` - -## Conversion command - -```bash -pandoc clean.md -f markdown -t djot --wrap=none -o output.dj -``` - -`--wrap=none` prevents reflow of long paragraphs. - -## Post-conversion cleanup - -Pandoc may still leave stray `{#...}` lines in djot output. Remove them: - -```bash -sed -i '/^{#.*}$/d' output.dj -``` - -## Pandoc artifacts - -- Unicode `——` (U+2014 × 2) → `------` in djot (two em dashes, `---` each). This is correct djot syntax. -- Markdown hard line breaks (trailing ` `) → `\\\n` in djot. Preserves original paragraph structure. -- Pandoc normalizes heading IDs (strips `、` and other punctuation). Ignore; the stray-line cleanup handles it. diff --git a/skills/translation/references/meditation-translation.md b/skills/translation/references/meditation-translation.md deleted file mode 100644 index 5690f5d..0000000 --- a/skills/translation/references/meditation-translation.md +++ /dev/null @@ -1,43 +0,0 @@ -# Meditation / Mindfulness Content Translation - -When the source is a guided meditation script, exercise guide, posture instruction, -or breathing practice (rather than a Dharma talk, sutra commentary, or teaching text), -use a lighter workflow than the full dharma-translation pipeline. - -## Register - -Default to warm, direct instructional voice (Thầy-adjacent): -- Second-person address ("you") -- Concrete images, sensory details -- Oral rhythm, short sentences -- Present tense, imperative mood - -MB corpus consultation is NOT needed for register — this content type has its own -well-established English conventions (yoga/meditation instructional voice). - -## Terms - -Terms DB lookup for Buddhist-mindfulness vocabulary is useful but limited to key terms: -- 正念 → mindfulness -- 觉知 → awareness -- 无我 → depends on context: "non-self" for philosophical/Dharma content; "selflessly" for embodied/movement instruction where the sense is no separate controller imposing on the action -- 中道 → Middle Way -- 丹田 → dantian (keep as-is; well-known in meditation/qigong) - -Context-sensitive terms: -- 心 (xīn): in meditation/movement contexts it often means "mind/attention" not emotional "heart." 持心 means holding the mind with focused attention, not holding with emotion. -- 念 (niàn): mindfulness/attention/recollection — context between these. -- Buddhist philosophical terms (无我, 空, 缘起) in non-philosophical contexts (movement instruction, body scans) may need practical/concrete translations rather than doctrinal ones. - -Skip deep terms alignment unless dense Dharma vocabulary (emptiness, dependent origination, -Buddha-nature, etc.) appears in the text. - -## Comparison files - -Still create 对照.dj as usual. See comparison file format in this skill. - -## Pitfalls - -- **Don't add formatting the source doesn't have**: sub-section labels using `【】` in Chinese should become plain `[label]` in English, not `*[label]*` or `**[label]**`. Match the source's formatting level exactly. -- **`**text**` is Markdown, not Djot**: Djot emphasis uses single asterisks (`*text*`). Never use double asterisks in `.dj` files. -- **心 ≠ heart by default**: in meditation/movement contexts, 持心 = holding the mind with attention, not holding with emotion. Translate based on context, not dictionary defaults. diff --git a/skills/translation/references/proofread-pdf-workflow.md b/skills/translation/references/proofread-pdf-workflow.md deleted file mode 100644 index e2e6107..0000000 --- a/skills/translation/references/proofread-pdf-workflow.md +++ /dev/null @@ -1,50 +0,0 @@ -# PDF-vs-DOCX Proofread Workflow - -Compare typeset PDF against the authoritative DOCX manuscript. Catch -discrepancies introduced during typesetting: dropped words, terminology -drift, repositioned phrases, extra content. - -## Pattern - -The existing `scripts/proofread-pdf.py` is article-specific (hardcoded to -佛教徒的人生态度). For each new article, create a similarly-shaped script: - -``` -translate-files/
/-.py -``` - -Hash = `md5('translate-files/
')[:6]` - -## Script Structure - -1. Convert DOCX to plain text: `pandoc docx -f docx -t plain --wrap=none` -2. Convert PDF to plain text: `pdftotext -layout pdf` -3. Extract body from DOCX — find body start marker (first sentence of body) -4. Extract body from PDF — find same marker, filter out: - - Page slugs: `文章名.*indd \d+` - - Headers: `The Mindful Peace Academy Collection`, article title - - Page numbers: `^\d{1,3}$` - - Section numerals: `^(I|II|III|IV)$` - - Section name lines: `^(Three Basic Elements|...|Conclusion)$` -5. Join hyphenated line breaks (`word-` at end + lowercase continuation) -6. Fix PDF artifacts: `L iving` → `Living`, `T\s+he` → `The` -7. Normalize both (collapse whitespace, unify quotes/dashes) -8. Compare with difflib.SequenceMatcher or sentence-level substring search - -## Pitfalls - -- PDF hyphen joining drops the hyphen: `self-knowing` → `selfknowing`. - This causes cascading word-level diff failures. Use sentence-level or - chunk-based matching instead of word-by-word comparison. -- Section headers (I, II, III) may be present in DOCX body but filtered - from PDF — not real discrepancies. -- The existing `scripts/proofread-pdf.py` is hardcoded for 佛教徒的人生态度. - Do NOT reuse it for other articles without rewriting the body-start - markers and filter patterns. Create article-specific scripts instead. -- `git diff --word-diff` fails when one file is multi-line and the other - is single-line. Normalize both to single-line first. - -## Example - -`translate-files/正念禅修十要素/ten-elements-c7fcd9.py` — extracts cleaned -body from both sources, outputs to `/tmp/` for side-by-side diffing. diff --git a/terms-search/.codegraph/.gitignore b/terms-search/.codegraph/.gitignore deleted file mode 100644 index 9de0f16..0000000 --- a/terms-search/.codegraph/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -# CodeGraph data files -# These are local to each machine and should not be committed - -# Database -*.db -*.db-wal -*.db-shm - -# Cache -cache/ - -# Logs -*.log - -# Hook markers -.dirty diff --git a/terms-search/.dockerignore b/terms-search/.dockerignore deleted file mode 100644 index 44e4a9c..0000000 --- a/terms-search/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -.git -**/__pycache__ -**/__pypackages__ -fly.toml diff --git a/terms-search/.gitignore b/terms-search/.gitignore deleted file mode 100644 index cf57406..0000000 --- a/terms-search/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -__pycache__/ -__pypackages__/ diff --git a/terms-search/Dockerfile b/terms-search/Dockerfile deleted file mode 100644 index 0b2b938..0000000 --- a/terms-search/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM python:3.14-slim - -WORKDIR /app - -RUN pip install --no-cache-dir flask duckdb gunicorn - -COPY . . - -EXPOSE 8910 - -CMD ["gunicorn", "-b", "0.0.0.0:8910", "-w", "1", "--timeout", "30", "server:app"] diff --git a/terms-search/deploy b/terms-search/deploy deleted file mode 100755 index cdf866d..0000000 --- a/terms-search/deploy +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/fish -fly deploy -y --depot=false diff --git a/terms-search/fly.toml b/terms-search/fly.toml deleted file mode 100644 index c11f16e..0000000 --- a/terms-search/fly.toml +++ /dev/null @@ -1,20 +0,0 @@ -# fly.toml app configuration file generated for buddhist-terms-search on 2026-06-09T13:38:18+08:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - -app = 'buddhist-terms-search' -primary_region = 'sin' - -[http_service] - internal_port = 8910 - force_https = true - auto_stop_machines = 'stop' - auto_start_machines = true - min_machines_running = 0 - processes = ['app'] - -[[vm]] - memory = '256mb' - cpus = 1 - memory_mb = 256 diff --git a/terms-search/search.py b/terms-search/search.py deleted file mode 100755 index 12e4c0f..0000000 --- a/terms-search/search.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python3 -"""Full-text search over MPI term database. Queries unified_terms_flat via DuckDB LIKE. - -Module usage: - from search import search_terms - results = search_terms("空性") - results = search_terms("空性", limit=5, loc="心经", src="佛教术语") - # returns list of dicts: {zh, en, loc, source} - -CLI usage: - python search.py [limit] - python search.py 空性 loc:心经 src:公案 -""" - -import sys -import os -import duckdb - -DB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "termlib.duckdb") - - -def _connect(): - return duckdb.connect(DB, read_only=True) - - -def _search_rows(con, query, loc=None, src=None, limit=None): - terms = query.split() if query else [] - clauses = [] - params = [] - - for t in terms: - like = f"%{t}%" - clauses.append("(zh LIKE ? OR en LIKE ?)") - params.extend([like, like]) - - where = " AND ".join(clauses) if clauses else "1=1" - - if loc: - where += " AND loc LIKE ?" - params.append(f"%{loc}%") - if src: - where += " AND source = ?" - params.append(src) - - sql = f"SELECT zh, en, loc, source FROM unified_terms_flat WHERE {where}" - if limit is not None: - sql += " LIMIT ?" - params.append(limit) - - return con.execute(sql, params).fetchall() - - -def search(query, loc=None, src=None, limit=20): - """Search the terms database. Returns list of {zh, en, loc, source} dicts. - - query: str — space-separated search terms (AND logic) - loc: str — filter by loc column (LIKE match) - src: str — filter by source column (exact match) - limit: int — max results (default 20) - """ - con = _connect() - try: - rows = _search_rows(con, query, loc=loc, src=src, limit=limit) - return [ - {"zh": zh, "en": en, "loc": loc_val or "", "source": src_val} - for zh, en, loc_val, src_val in rows - ] - finally: - con.close() - - -def main(): - if len(sys.argv) < 2: - print("Usage: terms-search [limit]") - print(" queries: '空性', 'emptiness 中观'") - print() - print("Special prefixes (can be standalone or combined with query):") - print(" loc: — filter by loc (e.g. loc:心经)") - print(" src: — filter by source table (e.g. src:佛教术语)") - print() - print("Examples:") - print(" terms-search 空性") - print(" terms-search 'suffering 苦' loc:心经") - print(" terms-search src:公案") - sys.exit(1) - - raw = sys.argv[1] - limit = int(sys.argv[2]) if len(sys.argv) > 2 else 20 - - loc_filter = None - src_filter = None - query_parts = [] - - for token in raw.split(): - if token.startswith("loc:"): - loc_filter = token[4:] - elif token.startswith("src:"): - src_filter = token[4:] - else: - query_parts.append(token) - - query_str = " ".join(query_parts) - - if not query_str and not loc_filter and not src_filter: - print("No search terms or filters. Usage: terms-search [limit]") - return - - results = search(query_str, loc=loc_filter, src=src_filter, limit=limit) - - if not results: - print(f"No results for: {raw}") - return - - print(f"Results: {len(results)}") - print() - for r in results: - print(f"zh: {r['zh']}") - print(f"en: {r['en']}") - print(f"loc: {r['loc'] or '-'} | src: {r['source']}") - print() - - -if __name__ == "__main__": - main() diff --git a/terms-search/server.py b/terms-search/server.py deleted file mode 100644 index bf8b714..0000000 --- a/terms-search/server.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -import sys, os -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -import duckdb -from flask import Flask, request, jsonify, render_template -import search as s - -app = Flask(__name__) - -def do_search(q, loc, src, limit): - with duckdb.connect(s.DB, read_only=True) as con: - rows = s.search(con, q, loc, src, limit) - return [{'zh': r[0], 'en': r[1], 'loc': r[2] or None, 'source': r[3]} for r in rows] - -def do_sources(): - with duckdb.connect(s.DB, read_only=True) as con: - rows = con.execute( - 'SELECT source, COUNT(*) AS cnt FROM unified_terms_flat GROUP BY source ORDER BY cnt DESC' - ).fetchall() - return [{'source': r[0], 'count': r[1]} for r in rows] - -@app.route('/search') -def search(): - try: - q = request.args.get('q', '') - loc = request.args.get('loc') - src = request.args.get('src') - limit = request.args.get('limit', type=int) - results = do_search(q, loc, src, limit) - return jsonify({'count': len(results), 'results': results}) - except Exception as e: - return jsonify({'error': str(e)}), 500 - -@app.route('/sources') -def sources(): - try: - sources = do_sources() - return jsonify(sources) - except Exception as e: - return jsonify({'error': str(e)}), 500 - -@app.route('/sources.html') -def sources_html(): - try: - sources = do_sources() - total = sum(s['count'] for s in sources) - return render_template('sources.html', sources=sources, total=total) - except Exception as e: - return render_template('sources.html', sources=[], total=0, error=str(e)) - -@app.route('/') -def ui(): - q = request.args.get('q', '') - loc = request.args.get('loc', '') - src = request.args.get('src', '') - limit = request.args.get('limit', '') - searched = bool(request.args) - results = [] - count = 0 - error = None - if searched and (q or loc or src): - try: - lim = int(limit) if limit else None - results = do_search(q, loc or None, src or None, lim) - count = len(results) - except Exception as e: - error = str(e) - return render_template('index.html', q=q, loc=loc, src=src, limit=limit, - searched=searched, results=results, count=count, error=error) - -if __name__ == '__main__': - app.run(port=8910) diff --git a/terms-search/templates/index.html b/terms-search/templates/index.html deleted file mode 100644 index 4fa0237..0000000 --- a/terms-search/templates/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - -静心学堂中英文翻译对照表 -

静心学堂(非官方)中英文翻译对照表

-

search | sources

-
- - - - - - - -{% if searched %} -{% if error %}

Error: {{ error }}

-{% else %} -

{{ count }} results

-
- - {% for r in results %} - - - - - - - {% endfor %} -
ZHENLOCSRC
{{ r.zh }}{{ r.en }}{{ r.loc or '' }}{{ r.source }}
-{% endif %} -{% endif %} diff --git a/terms-search/templates/sources.html b/terms-search/templates/sources.html deleted file mode 100644 index 270d8e3..0000000 --- a/terms-search/templates/sources.html +++ /dev/null @@ -1,19 +0,0 @@ - - -Sources — 静心学堂翻译对照表 -

Sources

-

search | sources

- -{% if error %}

Error: {{ error }}

-{% else %} - - - {% for s in sources %} - - - - - {% endfor %} - -
SourceRows
{{ s.source }}{{ s.count }}
Total{{ total }}
-{% endif %} diff --git a/todo b/todo new file mode 100644 index 0000000..e54e78b --- /dev/null +++ b/todo @@ -0,0 +1,19 @@ +[x] Merge .omp/APPEND_SYSTEM.md into AGENTS.md +[x] Use git submodule for toolkit/ with URL git@git.sr.ht:~iacore/mpi-translation-toolkit + (push failed: remote may not exist or SSH key not configured; run `git push -u origin main` in toolkit/) +[x] Rename terms-search/ -> terms-database/ +[x] Rename skill directories with mpi- prefix +[x] Create toolkit/ and fold scripts/, references/, terms-database/, skills/ into it +[x] Update all in-repo references +[ ] Update external Hermes config: + ~/.hermes/config.yaml: skills.external_dirs -> $MPI_PROJECT_ROOT/toolkit/skills +[x] Test + - toolkit/terms-database/search.py runs + - toolkit/scripts/gen-bilingual.py runs + - no stale references to scripts/, references/, terms-search/, or skills/ +[x] Delete .omp/APPEND_SYSTEM.md + +Next: +- Replace the placeholder URL in .gitmodules with the real remote for toolkit/. +- Push the toolkit/ submodule to its remote, then run `git submodule update --init`. +- Update ~/.hermes/config.yaml as noted above. diff --git a/toolkit b/toolkit new file mode 160000 index 0000000..eaa5e0b --- /dev/null +++ b/toolkit @@ -0,0 +1 @@ +Subproject commit eaa5e0bf4b7360cf1c048ffeb191be2fa66d4d7e diff --git a/translate-files/用小读点亮心灯/alignment-report.dj b/translate-files/用小读点亮心灯/alignment-report.dj index d55c732..d6f66c4 100644 --- a/translate-files/用小读点亮心灯/alignment-report.dj +++ b/translate-files/用小读点亮心灯/alignment-report.dj @@ -1,7 +1,7 @@ # Term Alignment Report File: translated.dj -Database: $MPI_PROJECT_ROOT/terms-search/termlib.duckdb +Database: $MPI_PROJECT_ROOT/toolkit/terms-database/termlib.duckdb Date: 2026-06-09 ## Summary