docs: job modes in localization.md, implementation status in llm-translation.md, new scripts in AGENTS.md

This commit is contained in:
2026-09-20 23:25:29 +00:00
parent 277a75ae23
commit bc1e8a8357
3 changed files with 78 additions and 13 deletions
+2
View File
@@ -37,6 +37,8 @@ Pagerite is a CMS. See `docs` for the full design and implementation details. Ke
- `assets/` — base CSS, Pygments styles, fonts. - `assets/` — base CSS, Pygments styles, fonts.
- `scripts/devserver.py` — dev server with auto reload (the user mostly uses this; avoid running the server yourself, ask the user to test). - `scripts/devserver.py` — dev server with auto reload (the user mostly uses this; avoid running the server yourself, ask the user to test).
- `scripts/translator.py` — Seed-X translator service client for the `/_translate/{key}` socket (reference client, runs in its own uv env via PEP 723); stays connected full time, unloads the model after 60 s idle and reloads on the next job. - `scripts/translator.py` — Seed-X translator service client for the `/_translate/{key}` socket (reference client, runs in its own uv env via PEP 723); stays connected full time, unloads the model after 60 s idle and reloads on the next job.
- `scripts/llm_translator.py` — instruct-LLM translator service client (docs/llm-translation.md): speaks the `markdown`/`article` job modes against an OpenAI Chat Completions endpoint or ollama's native `/api/chat` (its `/v1` ignores `think: false`); all LLM specifics (prompts, sampling, generation caps) live here, not in pagerite.
- `scripts/import_translation.py` — import a human-made whole-article translation file into the fragment store (same `align_article` validation as article-mode results; run with the server stopped).
Server run by CLI entry point `uv run pagerite` (no auto reloads, build needed). Dev mode is `scripts/devserver.py` (auto reloads, no build needed). Server run by CLI entry point `uv run pagerite` (no auto reloads, build needed). Dev mode is `scripts/devserver.py` (auto reloads, no build needed).
+9
View File
@@ -1,5 +1,14 @@
# Whole-article and scoped LLM translation # Whole-article and scoped LLM translation
**Status: implemented** (protocol modes and validation in
`pagerite/translate.py`, reference client `scripts/llm_translator.py`,
human-translation import `scripts/import_translation.py`; wire-level docs
in docs/localization.md). One deviation from the text below: ollama's
OpenAI-compatible `/v1/chat/completions` silently ignores `think: false`
(verified on 0.34.2), so the client's `api` config selects ollama's native
`/api/chat` for ollama backends; the OpenAI shape serves llama.cpp and
hosted APIs.
Design for augmenting the fragment-based machine translation Design for augmenting the fragment-based machine translation
(docs/localization.md) with general-purpose instruct LLMs that understand (docs/localization.md) with general-purpose instruct LLMs that understand
Markdown natively — as opposed to pure text-to-text models like Seed-X. Markdown natively — as opposed to pure text-to-text models like Seed-X.
+67 -13
View File
@@ -337,15 +337,18 @@ transaction `user`.
Frames are JSON-encoded tagged msgspec structs (`pagerite/translate.py`; Frames are JSON-encoded tagged msgspec structs (`pagerite/translate.py`;
`bytes` fields ride as base64): `bytes` fields ride as base64):
- `{"type": "hello", "langs": [...]}` — client greeting announcing its - `{"type": "hello", "langs": [...], "model", "modes"}` — client greeting
**capabilities**: the language codes its model can produce (normalized announcing its **capabilities**: the language codes its model can produce
to base subtags; `en`/empty dropped). (normalized to base subtags; `en`/empty dropped). `model` is a free-form
- `{"type": "job", "lang", "key", "texts", "path", "kind", "contexts"}` model string (logging only); `modes` lists the job granularities the
server push: ONE fragment to translate (an article title or a chunk), as client accepts (default `["segments"]`, see Job modes below).
a list of **prose segments** (see Segmentation below). `contexts` is - `{"type": "job", "lang", "key", "texts", "path", "kind", "mode",
parallel to `texts` ("" = none): the surround to translate the segment "contexts"}` — server push: ONE fragment to translate (an article title
in — for clients that translate better with context (see below). or a chunk). In the default `segments` mode `texts` is a list of **prose
Contexts are not part of the result. segments** (see Segmentation below) and `contexts` is parallel to `texts`
("" = none): the surround to translate the segment in — for clients that
translate better with context (see below). Contexts are not part of the
result. See Job modes for the other modes.
- `{"type": "result", "lang", "key", "texts"}` — client reply: the - `{"type": "result", "lang", "key", "texts"}` — client reply: the
segments translated, same order and count, matching its job by (lang, key). segments translated, same order and count, matching its job by (lang, key).
@@ -387,6 +390,55 @@ Results are stored into `trans` in one transaction and set
pages gain a language from one fragment). Unknown keys are stored anyway pages gain a language from one fragment). Unknown keys are stored anyway
and re-storing overwrites — results are idempotent. and re-storing overwrites — results are idempotent.
#### Job modes: segments, markdown, article
Instruct LLMs understand Markdown natively, so for them the segmentation
round trip below is unnecessary scaffolding (docs/llm-translation.md for
the design and the model trial evidence). `Hello.modes` announces which
job granularities a connection accepts; routing is per connection and per
mode, so a mixed fleet (a Seed-X instance, a local qwen, an API-backed
client) shares the work by capability. The validation skip-list is
mode-scoped — `(lang, key, mode)` — so a fragment one model rejects stays
offerable to clients of another approach.
- **`segments`** (default when a client omits `modes`) — the protocol as
described so far: `Job.texts` carries prose segments, `Result.texts`
returns them, the server splices by offset.
- **`markdown`** — one fragment as full Markdown: `Job.texts` carries a
single element, the chunk (a title crosses as plain text, with the
article's opening as its context as today). `Job.contexts` carries the
previous and next block of the **served hybrid** in the target language
(machine translation with user patches applied, "" where none), so human
corrections propagate into fresh translations as terminology/tone
reference; contexts are never part of the result. The result must
re-chunk to exactly one block with the source's anchor constructs (link
and image destinations, `{...}` placeholders) intact (`clean_block`),
then stores to `Data.trans` as usual.
- **`article`** — a whole page at once, offered only to article-capable
connections and only while a page is *mostly* pending (a new article or
a full refresh; steady-state edit follow-up stays scoped jobs). The
job's key is the page's first chunk; `Job.texts` carries the full
original Markdown. The result is decomposed per chunk
(`align_article`): non-translatable blocks (code fences, container
fences, raw HTML — everything `needs_translation` rejects) must appear
verbatim and in order and anchor the alignment; regions between anchors
pair positionally, a region whose block count changed stores nothing
(its chunks stay pending and fall back to scoped jobs), and a paired
block whose destinations/placeholders did not survive likewise.
`scripts/llm_translator.py` is the reference markdown+article client
(instruct LLMs via an OpenAI Chat Completions endpoint or ollama's native
API); `scripts/translator.py` (Seed-X) is untouched and announces
`["segments"]` implicitly.
**Importing human-made full translations:** `align_article` doubles as an
import path — `scripts/import_translation.py PATH LANG FILE.md` (run with
the server stopped) decomposes a pasted whole-article translation (e.g.
from ChatGPT) into proper `Data.trans` fragments with the same validation,
so later source edits invalidate and re-translate per chunk rather than
letting the translation editor's one monolithic patch go stale hunk by
hunk.
#### Segmentation #### Segmentation
Fragments cross the wire as **prose segments** (`pagerite/segments.py`): the Fragments cross the wire as **prose segments** (`pagerite/segments.py`): the
@@ -423,10 +475,12 @@ of the block it splices into, closing fence included — segments are
inline prose, so `pure_prose` alone cannot see this) — each returned inline prose, so `pure_prose` alone cannot see this) — each returned
segment must parse as segment must parse as
pure prose with no block-starting line or blank line, or the whole result pure prose with no block-starting line or blank line, or the whole result
is dropped and logged, and the (lang, key) is dropped and logged, and the (lang, key, mode)
pair is skipped for the rest of the server run (generation is combination is skipped for the rest of the server run (generation is
near-deterministic, so an immediate retry would re-fail; the fragment stays near-deterministic per model, so an immediate retry in the same mode would
pending and gets another chance on restart or `DELETE /_api/translations`). re-fail; the fragment stays
pending and gets another chance on restart, in another mode, or on
`DELETE /_api/translations`).
`Data.trans` therefore only ever holds clean translated Markdown. `Data.trans` therefore only ever holds clean translated Markdown.
Link- and formatting-carrying blocks are the one place a segment is not Link- and formatting-carrying blocks are the one place a segment is not