# Plan: from nodes to a finished translation of The Last Dwarf Written 2026-10-03. The goal is not a well-architected system; it is **a finished translation of The Last Dwarf**, produced by nodes, from scratch. This plan separates what is on the critical path from what can run beside it, because they do not block each other and pretending otherwise would serialise work that need not be serialised. --- ## What "finished" means here A person should be able to listen to a complete episode of The Last Dwarf in a language of their choosing, and it should have been produced by this system rather than assembled by hand. That is the test. Anything that does not move that number is not on this plan. The pipeline has three stages, and each is a separate kind of work: | stage | input | output | node kind | |---|---|---|---| | **translate** | source text | translated text | LLM | | **speak** | translated text | audio per line | TTS | | **assemble** | audio per line | one playable file | processing | Node 238 can do **speak** and **assemble** today: 12 cores, 47 GiB RAM, no GPU. CPU TTS is slower than a GPU but it is real, and it proves the path. **Translate** needs a model that does not exist on this hardware yet, and is the one stage with an open dependency. --- ## Track A — the critical path Nothing here can be backgrounded, because each step is the input to the next and each needs a decision from you. ### A1. Work functions — what a node can actually be asked to do The agent currently implements exactly one work function: `sleep`. Everything else is absent, so there is nothing to dispatch. Three work functions are needed, and the *protocol* matters more than the three implementations: - What a job declares it needs (a model, a device, a minimum size). - What it returns (an artifact reference, not bytes — bytes never transit the controller). - What it refuses, and how. A work function that cannot refuse is how a node ends up claiming work it will fail. **Done when:** a job of each of the three kinds can be claimed by a node, run, and reported, with refusal paths tested. ### A2. `speak` — TTS on node 238 The first real capability. Deliberately first because it needs no model download and no GPU, so it can be finished and heard. The audio node already holds the translated text and the voice decisions — 11 tracks, 461 speaker rows, `speaker_name` free text with three-way count disagreements. What it does not have is a way to say a line out loud. **Open decision for you:** which TTS engine. Piper is CPU-only and fast enough; its output quality is adequate for proof. XTTS and the neural options are better and need either a GPU or a slower CPU path. This is worth your ear before I build it, because "it works" and "it is listenable" are different bars for the thing you are trying to deliver. ### A3. `assemble` — the final product Per-line audio becomes one episode: normalise loudness, concatenate in cue order, write metadata. This is where "the system produced a translation" becomes demonstrable rather than asserted. **This is the stage you named, and it is the one that makes the other two worth having.** ### A4. Artifact lineage `artifacts` and `job_artifacts` exist (migration 0003 §5) but nothing writes to them. The chain — source text → translation → per-line audio → assembled episode — has to be recorded as it happens, or the pipeline produces files nobody can trace. ### A5. The last dwarf, end to end One episode, start to finish, no manual steps. The deliverable is the file. --- ## Track B — parallel, does not block Track A ### B1. Schema-per-tenant migration Fully specified in `Umbratools_2.md` §5.0. It touches 8 tables, 19 query sites, and the tenancy test suite — none of which the compute path reads. It can run to completion while Track A proceeds. **Constraint:** it must not touch `compute_nodes`, `jobs`, `node_credentials` or `node_heartbeats`. Those are **platform-level, not tenant-level** — a node is not owned by an organization, it *serves* organizations. Putting them in a tenant schema would mean a node in organization A cannot be seen by the controller. They stay in `public`, and that is a deliberate exception rather than an oversight. **Risk:** rewriting `test_rls_tenancy.py` while Track A is changing the compute surface means two large diffs in flight at once. This is the argument for sequencing them, and it is the one real argument against full parallelism. ### B2. Capability benchmarking The probe now measures instruction sets by executing them, on the principle that a declared capability is not a measured one. What it does not yet do is produce a `throughput_factor` from a **measurement** rather than a string match on a GPU model name. On 238 this is tractable: 12 cores, no GPU, and the numbers are honest. The string-matched factor stays on the GPU path until there is hardware to measure. ### B3. Ingest API and object storage How translated text and audio reach the system at all. Independent of who computes on it. Blocked on TLS, which is a prerequisite for any write path. --- ## The two dependencies that are not mine **TLS in front of the deployment.** Caddy is inactive and the Caddyfile is empty. Nothing should be written to this system over plaintext HTTP on a network others can reach. This blocks B3 and should not block A1–A3, all of which are compute-side. **The audio node's own work.** It has production translation work in flight. Track A needs nothing from it until A5, and A5 needs the translated text — so the audio node's schedule is a real constraint on the finish date, and I do not control it. --- ## What I would do first, and why **A1, the work-function protocol, before any TTS engine.** It is the smallest piece that makes the other three possible, and building it against `sleep` means the TTS work function is written once against a protocol that has already been tested rather than being the thing that discovers the protocol's gaps. The failure I would most want to avoid: a TTS function that works, and a protocol discovered afterwards to have no way to express "this node cannot do that" — which is exactly how a heterogeneous fleet ends up silently failing work it cannot perform. --- ## State at writing - Two nodes paired (`win-test-238`, `umbraaudio-m4`), one retired test node. - Bootstrap works with and without root; the root branch is **untested** — needs a sudo password on 238. - Capability measured by execution on both machines. On 238 the hypervisor presents a CPU that does not exist (`Ryzen 9 3900X`, faults on AVX2), which is now detected and reported rather than believed. - 525 tests pass. Two live defects: gate **G2** (the application connects as table owner) and `VIEWS` in `web/assets/admin.js` omitting `projects`. --- ## Decisions recorded 2026-10-03 ### TTS engine: Qwen3-TTS, as modified on the audio node Not a choice to be made here. The audio node already runs a **modified Qwen3-TTS**, and principle 1 of the specification — *quality is more important than time* — settles the question that was put to the owner about it. The engine in use stays. **What is not yet known:** what the modifications are. They live on the audio node, not in this repository, so a work function built here would be written blind. Before A2 is built, this needs to come back from the audio node: - what was changed and why, - whether it is a fork with its own history, or a patch that can be carried, - what the input contract is (text, voice, language, speed), - whether it loads a model, and how large. That last one decides whether node 238 can run it at all: 47 GiB of RAM, no GPU, and a hypervisor that faults on AVX2. ### Parallel TTS on node 238 Twelve cores and 47 GiB will run several synthesis processes at once, and this is a requirement rather than an optimisation. The mechanism already exists: `submit_children` and `POST /api/node/jobs/{id}/decompose` split one job into many, each claimed independently. So the work-function protocol (A1) has to be designed for decomposition from the start. A translation episode is thousands of lines; one job per line is thousands of jobs, and they claim concurrently. A `speak` function that cannot express "one job, many children" would force the parallelism into a hand-written loop on the node, which is exactly where it cannot be observed. The parent job must then wait for its children and refuse to complete early — otherwise the episode is assembled from whatever happened to finish, which is a race with a plausible-looking result.