Qwen3-TTS as modified on the audio node, kept. Quality over time is the principle that settles it, and the engine already in use is the one worth keeping. The modifications are on the audio node and unknown here, so the speak work function cannot be written blind. Listed what is needed: what changed and why, whether it is a fork or a carryable patch, the input contract, and whether it loads a model and how large - which decides whether node 238 can run it at all. Parallel synthesis is a requirement, not an optimisation. The mechanism exists already (submit_children and the decompose route), which means the work-function protocol has to be designed for decomposition rather than have parallelism retrofitted into it.
195 lines
8.4 KiB
Markdown
195 lines
8.4 KiB
Markdown
# 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.
|