nav job mode: whole-menu titles as one nested list; Kimi Code API backend
- translate.py: new "nav" job mode (Hello.modes opt-in) — the whole navigation hierarchy crosses as one nested Markdown list of pending titles, decomposed back by align_nav: item count/depth must match or the job is rejected wholesale (titles fall back to scoped jobs); items failing title checks individually are skipped to scoped jobs. Dispatched ahead of per-title jobs; a lone pending title stays scoped. - article jobs carry the already-translated menu title and parent title as contexts, so the injected heading can match the menu while the model may adapt the in-article title to the content. - llm_translator.py: nav mode + nav_prompt; article prompt takes the title/location context; API keys from per-provider env vars only (KIMI/MOONSHOT/OPENAI_API_KEY, each sent only to its own host; LLM_API_KEY generic) — no CLI flag, no config file; Kimi Code /coding endpoint support (sampling fields dropped, reasoning_effort from config, field-proven with k3-256k at low effort); errors include the response body; verbose per-job logging with the raw response incl. thinking (stripped from results); Kimi models announce all languages.
This commit is contained in:
+35
-8
@@ -98,7 +98,7 @@ class Hello(msgspec.Struct, tag="hello"):
|
||||
modes: list[str] = ["segments"] # job granularities accepted
|
||||
```
|
||||
|
||||
Three job modes, in increasing granularity:
|
||||
Four job modes, in increasing granularity:
|
||||
|
||||
- **`segments`** — the current protocol, unchanged: `Job.texts` carries
|
||||
prose segments (markup never crosses the wire), `Result.texts` returns
|
||||
@@ -122,14 +122,29 @@ Three job modes, in increasing granularity:
|
||||
original Markdown (the chunk sequence is recoverable server-side via
|
||||
`node.chunks`); `Result.texts` carries one element, the full translated
|
||||
Markdown. The server decomposes (below) and stores per chunk.
|
||||
- **`nav`** — the whole navigation hierarchy. `Job.texts` carries one
|
||||
element, a nested Markdown list of every node title still pending for
|
||||
the language (`- Title`, indented by depth, in menu order);
|
||||
`Result.texts` carries one element, the translated list. The server
|
||||
decomposes by list structure (`align_nav`): item count and nesting
|
||||
depth must match the source item for item, then each item is stored as
|
||||
a per-title fragment under its title's chunk hash.
|
||||
|
||||
Titles are jobs like any other in all modes (`kind="title"` keeps its
|
||||
article-opening context rule; in `markdown` mode a title crosses as
|
||||
plain text, since it carries no markup by construction). Additionally, an
|
||||
plain text, since it carries no markup by construction) — but for
|
||||
nav-capable connections a single `nav` job names the entire menu first:
|
||||
one round trip instead of one per page, with siblings, parents and
|
||||
children translating in sight of each other. A structurally mangled list
|
||||
is rejected wholesale and the titles fall back to scoped title jobs.
|
||||
Additionally, an
|
||||
`article` job carries the page title injected as a `# {title}` line at
|
||||
the top when the render would inject it (the body has no h1 of its own):
|
||||
the title translates in document context and the opening paragraphs see
|
||||
the heading. The heading's pair in the decomposed result becomes the
|
||||
the heading. The menu title's and parent node's existing translations
|
||||
ride along as `Job.contexts` ("" where none), so the heading can match
|
||||
the menu while the model may still adapt the in-article title to the
|
||||
content. The heading's pair in the decomposed result becomes the
|
||||
title fragment (heading text only, never stored as a body chunk).
|
||||
|
||||
### Dispatch and validation
|
||||
@@ -175,20 +190,32 @@ A second client script next to `scripts/translator.py` speaking the
|
||||
`markdown` and `article` modes. Internally it targets the **OpenAI
|
||||
Chat Completions API shape** (`POST /v1/chat/completions`): ollama
|
||||
serves it at `:11434/v1`, llama.cpp's server likewise, and hosted APIs
|
||||
(OpenAI and compatible providers) natively — `base_url` + `model` +
|
||||
optional API key in the client's config selects local GPU, local CPU or
|
||||
a remote model, with backend quirks (ollama's `think: false`,
|
||||
`num_predict` cap, per-model sampling) in a per-model config section.
|
||||
(OpenAI and compatible providers) natively — `--base-url` + `--model`
|
||||
selects local GPU, local CPU or a remote model, the API key comes from
|
||||
the standard per-provider environment variable (`KIMI_API_KEY`,
|
||||
`MOONSHOT_API_KEY`, `OPENAI_API_KEY`, each sent only to its own
|
||||
provider's host; `LLM_API_KEY` for anything else) — deliberately never
|
||||
a CLI flag or a config file — and backend quirks (ollama's
|
||||
`think: false`, `num_predict` cap, per-model sampling) live in the
|
||||
script's `DEFAULT_CONFIG`.
|
||||
How the client drives its LLM is its internal matter; the wire protocol
|
||||
above is the contract.
|
||||
|
||||
Field-proven backends: the local qwen3.8:27b of the trials above, and
|
||||
the **Kimi Code API** (`--base-url https://api.kimi.com/coding` resp.
|
||||
`api.kimi.ai`, `--model k3-256k`): the `/coding` endpoint fixes sampling
|
||||
internally (the client drops `temperature`/`top_p` for it — they 400)
|
||||
and runs `reasoning_effort: low` from the config, which produces good
|
||||
translations at a fraction of the default (high) effort's latency and
|
||||
quota; thinking output is logged verbatim but stripped from the result.
|
||||
|
||||
The client announces in `Hello`:
|
||||
|
||||
- `model`: the model string it is actually serving (e.g. `qwen3.8:27b`)
|
||||
- `langs`: from its per-model language table — for the shipped qwen3.8
|
||||
configuration the site languages as configured server-side
|
||||
(de, es, fi, pt, zh; Finnish flagged as the weakest, patch-covered)
|
||||
- `modes`: `["markdown", "article"]` for a structure-proven model,
|
||||
- `modes`: `["markdown", "article", "nav"]` for a structure-proven model,
|
||||
`["markdown"]` for one that is only trusted in scoped mode
|
||||
|
||||
The Seed-X client is untouched and announces `["segments"]` (implicitly,
|
||||
|
||||
+23
-4
@@ -344,7 +344,9 @@ Frames are JSON-encoded tagged msgspec structs (`pagerite/translate.py`;
|
||||
client accepts (default `["segments"]`, see Job modes below).
|
||||
- `{"type": "job", "lang", "key", "texts", "path", "kind", "mode",
|
||||
"contexts"}` — server push: ONE fragment to translate (an article title
|
||||
or a chunk). In the default `segments` mode `texts` is a list of **prose
|
||||
or a chunk; the bulk `article`/`nav` modes carry a whole page resp. the
|
||||
whole navigation tree, see Job modes). In the default `segments` mode
|
||||
`texts` is a list of **prose
|
||||
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
|
||||
@@ -390,7 +392,7 @@ Results are stored into `trans` in one transaction and set
|
||||
pages gain a language from one fragment). Unknown keys are stored anyway
|
||||
and re-storing overwrites — results are idempotent.
|
||||
|
||||
#### Job modes: segments, markdown, article
|
||||
#### Job modes: segments, markdown, article, nav
|
||||
|
||||
Instruct LLMs understand Markdown natively, so for them the segmentation
|
||||
round trip below is unnecessary scaffolding (docs/llm-translation.md for
|
||||
@@ -421,7 +423,11 @@ offerable to clients of another approach.
|
||||
original Markdown — with the page title injected as a `# {title}` line
|
||||
at the top when the render would inject it (the body has no h1 of its
|
||||
own), so the title translates in document context and the opening
|
||||
paragraphs see the heading. The result is decomposed per chunk
|
||||
paragraphs see the heading. The menu title's and parent node's existing
|
||||
translations (from a nav job or earlier work) ride along as
|
||||
`Job.contexts`, so the heading can match the menu while the model may
|
||||
still adapt the in-article title to the content. 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
|
||||
@@ -431,8 +437,21 @@ offerable to clients of another approach.
|
||||
injected title heading's pair becomes the title fragment (heading text
|
||||
only — never a body chunk; a demoted or merged heading simply skips it
|
||||
and the title stays pending for a scoped title job).
|
||||
- **`nav`** — the whole navigation hierarchy at once, offered only to
|
||||
nav-capable connections and ahead of any per-title jobs: `Job.texts`
|
||||
carries one element, a nested Markdown list of every node title still
|
||||
pending for the language (`- Title`, indented by depth, in menu order —
|
||||
pages and category labels alike); the job's key is the hash of that
|
||||
list. One round trip names the entire menu, and sibling titles
|
||||
translate in sight of each other. The result is decomposed back into
|
||||
per-title fragments (`align_nav`): it must be the same list item for
|
||||
item — same count, same nesting depth at every position — or it is
|
||||
rejected wholesale and the titles fall back to scoped title jobs; an
|
||||
item that comes back empty, marked-up or with its destinations/
|
||||
placeholders lost is skipped individually and likewise stays pending
|
||||
for a scoped title job.
|
||||
|
||||
`scripts/llm_translator.py` is the reference markdown+article client
|
||||
`scripts/llm_translator.py` is the reference markdown+article+nav 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.
|
||||
|
||||
Reference in New Issue
Block a user