Metadata-Version: 2.5
Name: rindo-convert
Version: 0.1.0
Summary: The Rindo converter agent: vision-grounded conversion of PDF and PPTX files into reviewable Rindo documents, run through rindo-runner
Project-URL: Homepage, https://rindo.io
Project-URL: Repository, https://github.com/rindohq/rindo
Author: Rindo
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agent,converter,pdf,pptx,rindo
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development
Requires-Python: >=3.11
Requires-Dist: anthropic>=0.116
Requires-Dist: httpx>=0.28
Requires-Dist: pillow>=10
Requires-Dist: pypdfium2>=5.10
Requires-Dist: python-pptx<2,>=1.0.2
Provides-Extra: docling
Requires-Dist: docling-core==2.89.0; extra == 'docling'
Requires-Dist: docling-ibm-models==3.13.3; extra == 'docling'
Requires-Dist: docling-parse==7.8.1; extra == 'docling'
Requires-Dist: docling==2.117.0; extra == 'docling'
Description-Content-Type: text/markdown

# rindo-convert — the Rindo converter agent (KB2)

Vision-grounded conversion of hard formats (text-layer PDF, scanned PDF, PPTX)
into reviewable Rindo documents. Invoked by [`rindo-runner`](../../runner/)
through the command-template contract; the result is always a **REV-8 pending
draft** a human approves — the agent can never publish.

The rendered page is the only ground truth. Machine extraction (Docling for
PDFs, python-pptx for decks) is a *text donor* the model may copy characters
from — never a structure authority. A deterministic repair pass (ported from
the Rindo worker's extraction pipeline, with drift tests) runs on every page
regardless of model quality. DOCX / XLSX / CSV / HTML / MD / TXT are declined
politely: their machine representation loses ~nothing a render carries.

Operator documentation lives in the Rindo ops runbook §4.7 (register the
agent, mint the runner token, designate `converter_agent_id`, duration + cost
guidance). This README is the package-local quickstart.

## Install (on the runner host)

`rindo-convert` is Apache-2.0 (`LICENSE` beside this file) — like
`rindo-runner`, not part of the proprietary Software. It is published on PyPI:

```sh
uv tool install rindo-convert                # base install — scanned-PDF + PPTX arms work
uv tool install 'rindo-convert[docling]'     # + the Docling scaffold for text-layer PDFs (~5 GB)
# from a source checkout instead: cd agents/converter && uv sync [--extra docling]
```

Host prerequisites:

- **PPTX arm:** LibreOffice (`soffice`) and CJK fonts covering the deck's
  script (`fonts-noto-cjk`; verify `fc-match :lang=ja` resolves to a JP face —
  the render is the ground truth, so a substituted face degrades silently).
  The tool refuses to run the PPTX arm without a JA-capable font.
- **PDF arms:** nothing — pypdfium2 ships wheels; poppler is not needed.
- An Anthropic API key (`ANTHROPIC_API_KEY`, or an `ant auth login` profile).

## Wire it to the runner

```toml
# ~/.config/rindo-runner/config.toml
server_url = "https://rindo.example.com"
token      = "rndr_..."
command    = "/opt/rindo-convert/run.sh {payload_file}"
poll_timeout = 60
```

```sh
#!/usr/bin/env bash
# /opt/rindo-convert/run.sh   (chmod 700 — it holds the API key)
set -euo pipefail
export ANTHROPIC_API_KEY="sk-ant-..."
export RINDO_CONVERT_MODEL="claude-opus-5"
exec /opt/rindo-convert/.venv/bin/rindo-convert "$1"
```

The runner injects `RINDO_SERVER_URL` / `RINDO_MCP_URL` /
`RINDO_RUNNER_TOKEN` / `RINDO_JOB_ID`; model and engine knobs ride the
ambient environment (the wrapper). One job at a time per runner token.

## Knobs (`RINDO_CONVERT_*`)

| Var | Default | Meaning |
|---|---|---|
| `MODEL` | `claude-opus-5` | Vision model. The request shape is capability-gated per model — see `config.py`'s `MODEL_CAPS`. |
| `ESCALATE_MODEL` | *(unset)* | Documented re-request rung for decks whose review effort is too high. |
| `EFFORT` | `high` | `output_config.effort` on models that take it. |
| `MAX_PAGES` | `60` | Above it the job declines with a split-the-file message. |
| `MAX_TOKENS` | `3000000` | Cumulative spend ceiling for the whole job (checked between pages against real `usage`). |
| `PAGE_MAX_TOKENS` | `64000` | Per-call `max_tokens` (thinking shares this cap). |
| `PAGE_TIMEOUT_SECONDS` | `240` | Per-page wall clock. |
| `DEADLINE_MARGIN_SECONDS` | `90` | Reserved before the job's `deadline_at` — stop early and salvage rather than be killed. |
| `MAX_FAILED_PAGE_RATIO` | `0.25` | Above it, abort instead of submitting a placeholder-riddled draft. |
| `RENDER_DPI` | `150` | Raster DPI; the long edge is clamped to the model's vision ceiling regardless. A cost knob. |
| `ENGINE` | `docling` | `docling` \| `none` (pure vision). Docling failure degrades to `none` with a provenance note. |
| `DONOR_ESCALATION` | `0` | §5.4 conditional second donor. OFF unless measurement M2 ruled it in. |
| `SOFFICE` | `soffice` | LibreOffice binary path. |
| `WORKDIR` | *(mkdtemp)* | Scratch — defaults **outside** any repo; kept on failure (it holds `final.md`, the manual-resubmit remedy). |
| `KEEP_WORKDIR` | `0` | Keep it on success too (debugging, bench). |
| `DRY_RUN` | `0` | Convert but skip all MCP traffic. The bench's measurement lever. |
| `FALLBACKS` | `default` | Server-side refusal fallback on models that support it (`off` to disable). |
| `USAGE_LOG` | `~/.cache/rindo-convert/usage.jsonl` | One JSONL line per page (real `usage`, never estimates). |
| `LOG_LEVEL` / `LOG_JSON` | `info` / `0` | stderr logging (stdout is the job-summary channel). |

## Exit codes (the runner maps non-zero → job failed)

| Code | Meaning |
|---|---|
| 0 | Draft submitted (or content hash-equal to head — a recorded no-op) |
| 1 | Decline: out-of-scope format, page cap, or a pending draft already on the target |
| 2 | Config/environment error (API key, soffice, CJK font, bad env value) |
| 3 | Conversion failure (download, render, model, submit, budget) |

The classified reason is always the final `RESULT:` line of the output tail
(`agent_jobs.result.summary_md` on the server), after the usage/cost summary.

## Development

```sh
uv run pytest -q                      # offline: no network, no key, no docling, no soffice
uv run ruff check . && uv run ruff format --check .
```

Ported backend symbols (`postpass.py`, `ooxmlguard.py`, the router constants)
are byte-copies with drift tests: `tests/test_ported_drift.py` reads the
backend sources by relative path and AST-compares. When it fails, re-sync the
copy, re-record the sha, and re-run the postpass goldens. The bench harness
(measurements M1/M2, the judging protocol) lives in [`bench/`](bench/) and
writes only outside the repo.
