crisp-docx

June 30, 2026 · View on GitHub

Cross-platform OOXML (.docx) surgery + a complete LLM/NMT document translation pipeline — all Rust, all offline-capable.

┌─ Word document ──────────────────────────────────────────────────┐
│                                                                  │
│  paragraphs (text + run-level rPr + footnote refs + bookmarks)   │
│                                                                  │
│      ┌──────────────────┐    ┌──────────────────┐                │
│      │ crisp-docx-core  │ ─→ │ crisp-docx-llm   │ ─→ translated  │
│      │  • clean         │    │  • OpenAI / etc. │   paragraphs   │
│      │  • notes-kind    │    │  • Anthropic     │                │
│      │  • transplant    │    │  • Ollama        │                │
│      │  • extract runs  │    │  • CrispASR NMT  │                │
│      │  • replace runs  │    └──────────────────┘                │
│      └──────────────────┘                                        │
│                                ┌──────────────────┐              │
│                                │ crisp-docx-align │ → re-map     │
│                                │  • SimAlign via  │   bold/italic│
│                                │    CrispEmbed    │   spans      │
│                                └──────────────────┘              │
│                                                                  │
└─ Translated Word document ───────────────────────────────────────┘

Sister project to:

  • CrispStrobe/CrispTranslator — Python ancestor; this Rust port has feature parity with most of its OOXML primitives. See PARITY.md.
  • CrispStrobe/CrispSorter — Tauri 2 desktop app; consumes the workspace crates directly and exposes a "Translate" UI tab.
  • CrispStrobe/CrispEmbed — ggml-based multilingual encoder; provides token embeddings for crisp-docx-align.
  • CrispStrobe/CrispASR — ggml-based ASR + NMT engine; provides the offline m2m100 / wmt21 / madlad / gemma4-e2b backends for crisp-docx-llm.

Workspace layout

CratePurposeDefault features compile time
crates/crisp-docx-corePure-Rust OOXML primitives. Zero deps beyond zip + quick-xml.seconds
crates/crisp-docx-clicrisp-docx binary — clap-driven CLI over core.seconds
crates/crisp-docx-pyPyO3 bindings → pip install-able wheel.seconds
crates/crisp-docx-llmAsync LLM HTTP clients (12 providers) + optional offline NMT (CrispASR).seconds (default) / minutes (with nmt)
crates/crisp-docx-alignSimAlign transformer-grade word aligner (argmax / intersection / itermax).seconds (default) / minutes (with crispembed)
crates/crisp-translate-clicrisp-translate binary — full docx-to-docx translation pipeline.seconds (default) / minutes (with align)

OOXML operations covered

OperationModuleUse
Strip rsid / paraId tracking attrsrsid_stripCures Word's "found unreadable content" dialog after transplants.
Normalize Apple textutil quirksnormalize_tagsw:sz-csw:szCs, etc.
Notes-kind conversionnotes_kindSwitch footnotes ↔ endnotes (part, rels, content-types, body refs).
Quotation-mark unifierquote_styleNormalize all quotes to one national style — German „…“, English “…”, French « … », Swiss «…», inward German guillemets »…« — across body + notes, preserving apostrophes.
Footnote-reference injectionnote_injectionGiven inline [N] markers, split runs and append <w:footnote> entries. Note text is parsed as light inline markdown — *italic*, **bold**, [label](url) and bare URLs become real runs / hyperlinks.
Body transplanttransplantClone a blueprint package, swap in source paragraphs, preserve sectPr / formatting / footnotes.
Run-level paragraph IOparagraph_runsExtract each <w:r> with verbatim <w:rPr> bytes for downstream reformatting.
Text-only paragraph IOparagraph_textLight flavour for plain-text round-trips (collapses runs into one).
Style mappingstyle_mapperMultilingual heading classifier + blueprint-driven pStyle remapping.
Heading inferenceheading_inferenceDetect H1/H2/H3 from font-size clusters when explicit styles aren't set.
Strip cosmetic paragraph boldstrip_paragraph_boldRemove whole-paragraph bold inherited from RTF→md conversion.
Validity checkcheck7-axis cargo-clean diagnostic (XML parse, rsid, paraId, rels, body shape, bookmarks, rIds).

LLM provider matrix (crisp-docx-llm)

Every provider implements the same Provider::translate(text, src_lang, tgt_lang, opts) trait. The LlmTranslator orchestrator falls back through the chain on failure, so you can mix offline NMT, local Ollama, and cloud APIs.

ProviderWire formatDefault base URLEnv key
OpenAIChat Completionshttps://api.openai.com/v1OPENAI_API_KEY
AnthropicMessageshttps://api.anthropic.com/v1ANTHROPIC_API_KEY
Ollama (local)/api/generatehttp://localhost:11434/api
GroqOpenAI-compathttps://api.groq.com/openai/v1GROQ_API_KEY
OpenRouterOpenAI-compathttps://openrouter.ai/api/v1OPENROUTER_API_KEY
TogetherOpenAI-compathttps://api.together.xyz/v1TOGETHER_API_KEY
CerebrasOpenAI-compathttps://api.cerebras.ai/v1CEREBRAS_API_KEY
MistralOpenAI-compathttps://api.mistral.ai/v1MISTRAL_API_KEY
NebiusOpenAI-compathttps://api.studio.nebius.ai/v1NEBIUS_API_KEY
ScalewayOpenAI-compathttps://api.scaleway.ai/v1SCALEWAY_API_KEY
PoeOpenAI-compathttps://api.poe.com/v1POE_API_KEY
Google (Gemini)OpenAI-compathttps://generativelanguage.googleapis.com/v1beta/openaiGOOGLEAI_API_KEY
CrispASR NMToffline (GGUF)n/a — runs in-process

Live-verified end-to-end translation pairs (May 2026):

  • ✅ Groq, OpenRouter, Together, Nebius, Scaleway: "Der Hund schläft." from "The dog is sleeping."
  • ✅ CrispASR m2m100-418m-q8_0: "Der Hund schläft.", round-trip EN↔DE via offline GGML

Quickstart

Pure CLI (no LLM keys needed, OOXML surgery only)

cargo install --git https://github.com/CrispStrobe/crisp-docx crisp-docx-cli
crisp-docx clean broken.docx
crisp-docx notes-kind paper.docx --to endnotes
crisp-docx normalize-quotes paper.docx --style german   # english/french/swiss/german-guillemets
crisp-docx check paper.docx        # 7-axis validity report
crisp-docx analyze paper.docx      # blueprint metadata
crisp-docx transplant blueprint.docx source.docx -o out.docx

# Turn inline [N] markers into real footnotes from a {"N": "text"} map.
# Note text may use light markdown (*italic*, **bold**, [label](url)).
crisp-docx inject-footnotes paper.docx --notes notes.json -o out.docx

Markdown → docx with real footnotes

Source markdown that cites footnotes as bare [N] markers plus a trailing [N] text block (instead of pandoc's [^N] / [^N]: syntax) won't footnote correctly through pandoc alone. scripts/md-footnotes-to-docx.py rewrites that convention into pandoc footnote syntax and runs pandoc, producing page-bottom Word footnotes (not endnotes) with note formatting preserved:

scripts/md-footnotes-to-docx.py paper.md -o paper.docx
scripts/md-footnotes-to-docx.py paper.md --reference-doc house-style.docx

Requires pandoc on PATH. Only purely-numeric [N] markers with a matching definition are converted; markers like [S2] or [Author] are left alone.

Document translation (cloud LLM)

cargo install --git https://github.com/CrispStrobe/crisp-docx crisp-translate-cli
export GROQ_API_KEY=
crisp-translate input.docx -o out.docx \
    --source-lang English --target-lang German \
    --provider groq

Provider auto-pick scans OPENAI_API_KEY, ANTHROPIC_API_KEY, GROQ_API_KEY, etc. in cost / latency order if --provider is omitted. --concurrency 4 (default) translates four paragraphs in parallel.

Document translation (offline NMT, no network)

# Sibling repos required for the heavy features:
#   ../CrispEmbed/   when building --features align (or full)
#   ../CrispASR/     when building --features nmt   (or full)
git clone https://github.com/CrispStrobe/crisp-docx
cd crisp-docx
# `--features nmt` enables `--provider nmt` (offline NMT via CrispASR).
# Doesn't pull in CrispEmbed, so the build is faster than `--features align`.
cargo build --release -p crisp-translate-cli --features nmt
./target/release/crisp-translate input.docx -o out.docx \
    --target-lang German \
    --provider nmt \
    --model /path/to/m2m100-418m-q8_0.gguf

Format-preserving translation (v0.2)

# Cloud LLM (Groq) + format-preservation alignment encoder
cargo build --release -p crisp-translate-cli --features align
./target/release/crisp-translate input.docx -o out.docx \
    --target-lang German \
    --provider groq \
    --preserve-formatting \
    --align-model /path/to/paraphrase-multilingual-MiniLM-L12-v2.gguf

# Or, fully offline: NMT + alignment together
cargo build --release -p crisp-translate-cli --features full
./target/release/crisp-translate input.docx -o out.docx \
    --target-lang German \
    --provider nmt \
    --model /path/to/m2m100-418m-q8_0.gguf \
    --preserve-formatting \
    --align-model /path/to/paraphrase-multilingual-MiniLM-L12-v2.gguf

--preserve-formatting keeps intra-paragraph bold / italic spans across the translation by aligning source and target words via a multilingual encoder, then redistributing the source runs' <w:rPr> onto the matching target text.

Python bindings

pip install crisp-docx          # once published
# — or from source:
pip install maturin
maturin develop --release --manifest-path crates/crisp-docx-py/Cargo.toml
from crisp_docx import (
    strip_rsids, convert_notes_kind, NotesKind,
    extract_paragraph_runs, replace_paragraph_runs,
    transplant_body, check_package, analyze_blueprint,
)

strip_rsids("paper.docx")
convert_notes_kind("paper.docx", NotesKind.Endnotes)
runs = extract_paragraph_runs("paper.docx")
print(f"{len(runs)} paragraphs, first runs: {[r.text for r in runs[0].runs[:3]]}")

Build matrix

# Default — fast, no C++ deps, OOXML-only
cargo build --workspace

# `crisp-translate-cli` only; features are independent:
cargo build -p crisp-translate-cli --features nmt    # CrispASR (offline NMT)
cargo build -p crisp-translate-cli --features align  # CrispEmbed (word alignment for format-preservation)
cargo build -p crisp-translate-cli --features full   # both

CrispEmbed and CrispASR are sibling-repo path deps; clone them next to this repo if you build with their respective features. See .github/workflows/ci.yml for the CI checkout pattern.

Testing

cargo test --workspace --exclude crisp-docx-py
# 145 tests passing (March 2026 baseline)

Live-integration tests for the LLM providers are env-gated. Set CRISP_DOCX_LLM_LIVE_<PROVIDER>=1 + the corresponding API key to exercise the real wire:

CRISP_DOCX_LLM_LIVE_GROQ=1 GROQ_API_KEY= \
    cargo test -p crisp-docx-llm --test live live_groq -- --nocapture

The parity harness (crates/crisp-docx-core/tests/parity.rs) runs each ported primitive side-by-side against the original Python implementation when both CrispTranslator/ and a Python interpreter are available; CI auto-skips when they aren't.

Status

PhaseGoalState
APure-Rust OOXML primitives
BPython wheel (PyO3 / maturin)
CCross-platform CLI
DWord-level transformer alignment
ELLM HTTP clients✅ (12 providers)
FOffline NMT (CrispASR)
GEnd-to-end translate-cli
HCrispSorter Tauri integration

See PARITY.md for the Python ↔ Rust per-primitive status ledger, PLAN.md for the phased execution roadmap, and PUBLISH.md for the crates.io / PyPI release checklist.

License

GNU Affero General Public License v3.0 or later. See LICENSE.