Commit Graph
11 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 12ed8a763c fix(i18n): translate through the gateway and let a backend declare what it serves
make i18n-translate addressed the engine directly, so a run could never reach more than the 47 languages Argos ships models for, while meta/languages.yml names 183. service_url now resolves web-svc-translate, which routes a pair the engine does not serve to a chat model, and the deploy brings both roles up.

The gateway advertised only what its backends declared, and the chat backend declared nothing, so /languages reported 51 targets of 183 and the readiness wait spun to its hour-long timeout without translating a line. The chat backend now declares every language the table names.

Routing offered libretranslate for every pair, because an undeclared catalogue read as any pair, so a language it holds no model for was answered with HTTP 400 instead of falling through to the next backend. serves() now reads the backend's own /languages, which catalogue() already exposed for the advertisement. Declaring the pairs in the role was the first attempt and carried a second copy that was already stale: the engine reports zh-Hans where the role's list says zh.

A small model copies the prompt markers back, echoes the source line and labels its answer with the target language. All three reached the catalog, and an entry carrying its own source scores below the oracle's middle level. clean() strips them, and only from an answer that carried scaffolding, because on its own a short word and a colon is a translation, not a label.

server() had lost its redeploy parameter while cli/build/i18n kept passing it, so every run raised TypeError before translating a line and the only symptom was an empty log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-18 09:47:53 +02:00
kevinveenbirkenbachandClaude Opus 5 c325d1c852 fix(i18n): bake one model set so a new batch stops rebuilding the image
load_only reaches the image as a build ARG, and the RUN that fetches the
argos models is keyed on it. Pinning it to the languages a run happens to
translate therefore changed the ARG on every batch, invalidated that layer,
and refetched every model it names. Ten batches meant ten full rebuilds,
each one downloading what the previous had already downloaded.

Accumulating the languages instead would have been worse, not better: the
layer is all or nothing, so a growing set refetches the whole set each
time. What works is the opposite of following the batch. The lane now pins
load_only to every translatable code of both domains plus the pivot, 48 in
total, and asks for from_source, which drops the reverse pairs a lane that
only ever translates out of English never requests. The ARG is constant, so
the layer is built once and every later batch reuses it.

codes no longer reaches the rendered vars, which left it unused in deploy()
and server(); both lose it, along with the two call sites. That undoes
44260b8786, whose subject was passing it to a parameter this removes.

Correction to 4f2c9140fa: that message said an empty load_only makes the
image skip prefetch_models.py. It does not. The script always runs, and an
empty value only drops its --load_only_lang_codes filter, which is what
made it reach for every package. The consequence it described was real, the
mechanism was not.

Measured: after the one rebuild the server offers all 48 languages, and the
three batches since ran with no ansible task at all in their logs, going
straight to translating 12,518 entries across nineteen catalogs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 23:01:35 +02:00
kevinveenbirkenbachandClaude Opus 5 4f2c9140fa fix(i18n): translate against the models the run actually asked for
A batch that named its languages still deployed a server that knew none of
them. The role renders load_only from the inventory, the tracked i18n.yml
leaves it empty, and an empty load_only makes the image skip
prefetch_models.py. Nothing was baked behind the package cache, so the
server reached for the whole argos set at runtime over a path no cache
serves. The readiness wait then sat for its full hour and gave up with
"does not offer", which reads like a server that refuses the language
rather than one still downloading it. The lane now renders its own host
vars with load_only pinned to the pivot plus the batch, beside the tracked
file so the container sees it, and deploy and server carry the codes that
decide it.

The span guard could not see a hyphen. Its dotted-name alternative matched
[A-Za-z]\w*(\.[A-Za-z]\w+)+, and \w excludes -, so a host like argos-net.com
was never a protected span: a translation was free to mangle it and the
comparison still balanced. Widening the head to [\w-]* brings those names
under the same guard the undotted ones already had.

A refusal named its criterion and nothing else. "protected-span" says an
entry was turned down without saying which span moved, which is the one
fact that separates a bad translation from a criterion that is too strict,
so the marker now carries lost= and gained= and the per-catalog summary
counts the criteria it saw instead of reporting every rejection as a
damaged span.

Verified in the container against babel 2.18.0: 171 unit tests pass, and
the catalog lint holds across all 218 catalogs under the widened regex,
which is the gate that would have caught a span the old pattern let through.
The rendered vars file pins de, el, en, et, hi and lv, and those catalogs
carry their translations.

make i18n-tune still calls server with one argument and is fixed separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 17:25:46 +02:00
kevinveenbirkenbachandClaude Opus 5 666312dfbd refactor(i18n): translate against the dedicated LibreTranslate alone
server() preferred the deployed service and fell back to a throwaway
container when it did not answer. That container ran the upstream image
directly, with LT_UPDATE_MODELS on and neither the package-cache CA nor
the argos-net.com hijack, so it refetched gigabytes from upstream on
every run while the deployed service already carries those models in its
image, installed through the cache at build time. The fallback is gone.

It was reached by accident rather than by failure. deploy() ended in
await_service(), which polled for 300 seconds; the first start of the
service installs its models and takes far longer, so the wait expired,
deploy() reported nothing running, and the caller spawned the throwaway
beside the service it had just deployed successfully. await_service and
SERVICE_TIMEOUT_SECONDS are removed, leaving the single readiness wait
the client already performs with the budget those models need.

Step 0 of the deploy also stopped forcing a recreate. It rebuilt a
healthy lane on every invocation and took whatever model install was in
flight with it; compose recreates on a changed configuration by itself.

The wait no longer looks like a hang: it reports how many of the wanted
languages the server offers, which ones are still missing, how long it
has waited against its budget, and what the lane has pulled. i18n-tune
waits the same way and gets the same report.

service_url() and lane_container() replace the two places that each
rebuilt one of those strings, and deploy() reads the environment once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 10:56:42 +02:00
kevinveenbirkenbachandClaude Opus 5 db0fa764aa fix(i18n): wait for the router to exec before reading its command line
The test started a router and asked `deploying()` about it in the next
statement. `/proc/<pid>/cmdline` is empty until the child reaches execve, so
the guard could not find `deploy/main.sh` and answered, correctly, that no
deploy is running. A probe repeating exactly this sequence read an empty
command line 376 times out of 400, which is why the case fails under CI load
and passes on an idle machine.

The router now announces its first line and the test reads it before it
looks at the pid. 30 consecutive runs of the case pass.

The guard itself is unchanged: in production the pid file is written by the
router, long after its own exec, so nothing there races.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-26 13:39:52 +02:00
kevinveenbirkenbachandClaude Opus 5 e363a5518a fix(i18n): stop treating a pid as an identity across namespaces
Every sandboxed command runs in its own pid namespace, where the low
numbers belong to that sandbox's own shell and children. Two places read a
pid as if it named the same thing everywhere, and both blocked translation.

The throwaway container was named after os.getpid(). A run whose cleanup
never ran left `infinito-i18n-libretranslate-8` behind, and the next run,
holding pid 8 in its own namespace, died on `docker run exited with 125:
Conflict. The container name is already in use`. A random token cannot
collide, so an orphan no longer blocks anything.

`deploying()` accepted any live pid as a running deploy. The router writes
build/deploy.pid and never removes it, so a file naming pid 8 refused every
translation run with "A deploy is running" until it was deleted by hand. The
recorded process now has to name the router in its command line; the marker
is verified against the invocation in Makefile:200. /proc is read uncached
because it is per-pid and vanishes with the process.

The test that guarded this asserted the bug: it wrote pytest's own pid and
expected a deploy. It now asserts the opposite for that input, and proves the
positive case with a real process whose argv carries the router path.

Neither fix addresses the root cause of the failures that exposed them: the
sandbox denies 127.0.0.6, so `deployed()` never reaches the running service
and every run falls down the throwaway path in the first place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-26 12:11:22 +02:00
kevinveenbirkenbachandClaude Opus 5 d1d4108e62 feat(i18n): let INFINITO_GPU_COUNT decide whether translation uses the GPU
accelerated() asked docker info directly, so the variable the project already
carries for this decision did nothing: setting it to 0 in .env left the CUDA
image and --gpus all in place, and test_catalogs_complete_on_gpu kept running.

The variable now wins where it is set, and the daemon is asked only where it
is absent, which keeps every host without the container toolkit behaving as
it did. utils/env/handlers/infinito/gpu_count.py derives it from the same
daemon query but through setdefault, so an operator value survives make
dotenv; that makes it the overridable view of the fact accelerated() used to
establish on its own.

Four call sites follow from it: the stale-container check, the stamp a
successful deploy leaves, the --gpus flag with the -cuda suffix, and the
skipUnless on the catalog completeness test. Turning it off therefore
translates on the CPU, which is what turning the GPU off means here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-26 00:39:30 +02:00
kevinveenbirkenbachandClaude Opus 5 25dff2a780 fix(i18n): stop the translator from losing entries the server only refused
Four defects in the client surfaced in one run. LibreTranslate serves Chinese as zh-Hans and zh-Hant, never as bare zh, so the readiness wait blocked on a target that was never coming and the run stood still for an hour without printing a line. A blanket except turned every transient refusal into a permanently discarded entry, and the fallback path answered a failed batch by firing twenty single-text requests at the same overloaded server; the same catalogs that lost 130 to 295 entries came back with 0 discarded once the batch is retried with backoff first.

The per-catalog count conflated a refusal with a damaged protected span, which is measurably wrong: of 228 entries left pending in core/sw only 36 carried a span at all. Outcome now carries values, refusals, damage and the last refusal message per call rather than on the client, where twenty lanes mixed their numbers into each other. That split is what surfaced the 502 the server had been answering with. The throwaway container also ran with check=True and capture_output=True, so docker error text died inside the swallowed exception and the log showed an argv list and rc 125.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-24 09:54:10 +02:00
kevinveenbirkenbachandClaude Opus 5 a0a24e2c0e feat(i18n): deploy libretranslate into the i18n runner before translating
server() now resolves a translation endpoint in three steps: use the running
service, rebuild when the host has a GPU but no accelerated deploy left its
stamp, otherwise deploy. A plain run therefore costs nothing, and the rebuild
fires once, on the move from a CPU-built container to an accelerated one.

deploy() brings the runner up, waits for State.Running and deploys the role
inside it. The exec steps address `docker compose exec i18n` directly:
cli.administration.deploy.development exec hardcodes the `infinito` service
(compose.py:261), so every deploy through it silently ran in the main
container, where mailu already holds port 25 and postfix cannot start.
check=True stops a failing step instead of carrying on against the wrong
target.

The deploy router writes its pid to build/deploy.pid, which survives the exec
into the subscripts, and deploying() refuses a translation while that process
lives. The running deploy is left untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-24 01:53:19 +02:00
kevinveenbirkenbachandClaude Opus 5 1832bbfe6f feat(i18n): translate on the GPU when the host can hand one to a container
The ephemeral LibreTranslate container starts against the -cuda image with
--gpus all wherever docker registers an nvidia runtime, and keeps the CPU
image everywhere else, so runners without the container toolkit behave as
before. Measured on an RTX 3080 Ti: 44% utilisation, 1744 MiB resident,
482 sentences per 30 seconds.

Only the tool derives the suffix. web-svc-libretranslate stays pinned to the
CPU image, so no deploy host is required to install the toolkit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 02:26:23 +02:00
kevinveenbirkenbachandClaude Opus 5 4c98624e07 feat(i18n): gettext catalogs with LibreTranslate machine translation
Core and the documentation get gettext catalogs for all 184 ISO 639-1
languages listed in meta/languages.yml, 47 of which LibreTranslate can fill.

`make i18n-extract` merges the current sources into locale/<code>/
LC_MESSAGES/{core,docs}.po: role descriptions, category and menu titles and
the logout strings for the core domain, the Sphinx gettext output for the
docs domain. A changed source keeps its previous translation as a fuzzy
entry, and catalogs are written deterministically so an unchanged merge
leaves the files untouched.

`make i18n-translate domain=<core|docs> [languages=<codes>]` fills only the
empty and fuzzy entries with a short-lived LibreTranslate container (the
image web-svc-libretranslate pins) and removes it afterwards. It starts the
container, and loads models, only for languages that still have pending
entries. Code spans, placeholders, URLs, tags and dotted names such as
Infinito.Nexus are masked for the request and a translation that damages
one is discarded. An unreachable server stops the run instead of
discarding every remaining entry, so a rerun continues where it stopped;
machine translations carry the translator comment `libretranslate`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 18:22:03 +02:00