refactor(tooling): T-1290 — the assets domain, where OFF is the normal case

tooling/db/ (a misnamed directory: connectors, not database work),
trellis-batch.sh and synth_ui_sounds.py become `reach assets`:
audio {health,generate,batch,post {convert,normalize,trim,pipeline}},
image {health,generate}, trellis {health,generate,batch}, and synth-ui.
The four audio bash wrappers are retired, and tooling/db/ is gone.

Parity, from baselines taken before anything moved:

- the four UI-sound WAVs and the harmonic-synth WAVs (exponential and linear
  decay) are byte-identical
- the ffmpeg pipeline's decoded PCM is identical. Its .ogg bytes are not,
  even between two runs of the OLD code: Ogg picks a random stream serial,
  so the encoded file was never the right thing to compare
- the network success paths can't be run in a gate (Stable Audio and Trellis
  are kept off, Gemini costs money), so tooling/test_assets.py stands up a
  fake Gradio and pins every payload: the audio submit, Trellis's six-call
  session sequence with its 9-input image_to_3d, and the Gemini body. It
  failed when one Trellis value was mutated (7.5 → 7.0)

Failure classification, in endpoints.py, is the point of the port. The
services are OFF by design (VRAM on tower-of-joy, D-17), and the topology doc
warns against "fixing" one by restarting it. So a refused connection says OFF
and asks for the service to be turned on rather than restarted; a 4xx/5xx says
the request was rejected; 401/403 says credentials; 429 says quota; and an
unreachable Gemini blames the network, not VRAM.

Behaviour changes, each a failure that used to read as success or crash:

- audio batch and trellis batch exited 0 with failures in their summaries;
  they now print the summary and exit 1
- trellis generate on a missing image crashed with a TypeError
  (print(..., indent=2)); it now names the file, and checks it before the
  service so a typo is not reported as an outage
- the ffmpeg pipeline left its intermediates behind when a step failed

Structure: the connectors called each other as subprocesses (batch spawned
the connector, which spawned audio_post) and parsed each other's stdout. They
are now function calls, and ffmpeg is the only exec, through core/process.
ensure_venv() is removed: it os.execv'd into .venv, which D-263's exec rule
forbids, and reach declares the dependencies itself. config.json moved into
the domain deliberately, and the local-services rule follows it.

Output contract: results are still JSON on stdout with the same keys, so skill
readers keep working. Failures are an exit status with a Fix line, never
{"ok": false}. The audio-gen, glb-gen and image-gen skills, Araminta's agent
file and the allow-list are updated to match. glb-gen's "trellis-batch.sh is
hardcoded to one category" caveat is gone: batch takes --input-dir or --names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 19:49:20 +02:00
co-authored by Claude Opus 5.5
parent 4537b71b92
commit ddce4441a9
36 changed files with 1929 additions and 1754 deletions
+1 -1
View File
@@ -113,7 +113,7 @@ Synthesize findings.
### Araminta (Visual Designer)
- Joins discussions only when visual consistency decisions are needed
- Drives image-gen/sprite-gen/glb-gen/audio-gen via Bash — no Skill/MCP tool grant, so she invokes the connector scripts directly, e.g. `python3 tooling/db/image_connector.py generate ...`
- Drives image-gen/sprite-gen/glb-gen/audio-gen via Bash — no Skill/MCP tool grant, so she invokes the connectors directly, e.g. `reach assets image generate ...`
- **image-gen calls the paid Gemini API (`GEMINI_API_KEY`) — always ask Team Leader for permission before generating. glb-gen/audio-gen run against self-hosted tower-of-joy infrastructure and sprite-gen renders locally, so they don't carry the same per-call cost, but confirm intent before large batch jobs.**
### SI (Refinement Manager)
+1 -1
View File
@@ -48,7 +48,7 @@ PBR assets from these sources go through our `toon_masked` shader and come out m
You have `Bash` but no `Skill`/MCP tool grant, so asset generation runs through the project's connector scripts directly, not a slash-skill invocation:
- **image-gen** (`.claude/skills/image-gen/`) — concept art, icons, UI mockups, reference images via the Gemini API: `python3 tooling/db/image_connector.py generate "prompt" --output .tmp/image-gen/[category]/[name].png --aspect 1:1`. Requires `GEMINI_API_KEY` (set in `.claude/settings.local.json`) — this is the one that costs real money per call.
- **image-gen** (`.claude/skills/image-gen/`) — concept art, icons, UI mockups, reference images via the Gemini API: `reach assets image generate "prompt" --output .tmp/image-gen/[category]/[name].png --aspect 1:1`. Requires `GEMINI_API_KEY` (set in `.claude/settings.local.json`) — this is the one that costs real money per call.
- **sprite-gen** (`.claude/skills/sprite-gen/`) — flat 2D artwork (paintings, flags, billboards, signage) rendered as PNG textures/decals, via `scripts/render.sh`.
- **glb-gen** (`.claude/skills/glb-gen/`) — converts a concept PNG to a game-ready `.glb` via Trellis (self-hosted on tower-of-joy) plus Blender post-processing.
- **audio-gen** (`.claude/skills/audio-gen/`) — ambient loops, SFX, and UI sounds via the self-hosted Stable Audio Open Gradio app (tower-of-joy).
+1 -1
View File
@@ -1,6 +1,6 @@
# Local Services
Endpoints are also preconfigured in `tooling/db/config.json`.
Endpoints are also preconfigured in `tooling/domains/assets/config.json` (`reach assets …`).
- **Gitea:** `https://git.schweitz.net` (login: `schweitz`; LAN-hairpinned via
AdGuard — resolves to tower-of-joy directly on the home network). The old
+13 -5
View File
@@ -59,12 +59,20 @@ docs/
workshops/ # Workshop briefs and outputs
db/
schema.sql # Database schema
tooling/
db/ # Asset/connector scripts (audio, image, trellis, wiki)
config.json # Endpoint configuration
common.py # Shared venv/config helpers
audio_connector.py # Stable Audio Open connector
tooling/ # ONE package behind the `reach` CLI (D-263) — `reach --help`
main.py # routing only; the domain registry
core/ # config, console (the single output path), errors,
# process (the single guarded exec), jobs, command
domains/<name>/ # router.py (transport) + service/helper modules (logic)
atlas/ planet/ # the spatial ladder; `atlas planet` is its rung 3
ledger/ # economics import — the sole systems.db generator
wiki/ # wiki fill rates + GTTR hook
assets/ # Stable Audio / Gemini / Trellis connectors
config.json # endpoint URLs (never keys — tracked file)
… # check, validate, godot, visual, generate, blender, pr, jobs, dev
scripts/blender/ # Blender payloads — run by Blender's Python, never imported
archive/ # Provenance only, never run: pql-migrate/, wiki-bootstrap/
test_*.py # gate tests, run by `make test-tooling`
.claude/
agents/ # Agent personality files
skills/ # Skill definitions
-4
View File
@@ -24,10 +24,6 @@
"Bash(git rm *)",
"Bash(git ls-tree *)",
"Bash(git rev-parse --show-toplevel)",
"Bash(tooling/db/audio-generate *)",
"Bash(tooling/db/audio-health)",
"Bash(tooling/db/audio-post *)",
"Bash(tooling/db/audio-batch *)",
"Bash(make *)",
"Bash(make)",
"Bash(pql)",
+34 -29
View File
@@ -13,39 +13,44 @@ description: >
# Audio Generation — The Settled Reach
Generate sonically consistent audio assets using the Stable Audio Open API via
wrapper scripts at `tooling/db/audio-*`.
`reach assets audio …` (`tooling/domains/assets/`, T-1290).
Asset descriptions, filenames, bus routing, and design intent are documented in
`docs/assets/audio/`. This skill provides the prompt system, generation
workflow, and quality validation.
`audio-health`, `audio-generate`, and `audio-batch` re-exec into the project
`.venv` on startup (`audio-post` doesn't need to — it only shells out to
ffmpeg). On a fresh clone with no `.venv` yet, they fail fast with `error:
.venv not found — run make setup-venv first.` — run that once before using
this skill.
**Stable Audio is kept switched OFF** on tower-of-joy — VRAM is scarce, so
the service is turned on only when needed, and restarting it blindly takes VRAM
from whatever else is running. `reach assets audio health` tells you which:
OFF exits 1 with a remedy that says so. Get it turned on before a session.
**Read the exit status, not an `ok` field.** Each verb prints its result as
JSON on stdout (`file`, `size_bytes`, `ogg_file`, … — the same keys as before)
and fails with a non-zero exit and a `Fix:` line. There is no `{"ok": false}`
on stdout any more, and a batch with any failed asset now exits 1.
## API Access
**Never call the API directly.** Use the wrapper scripts:
**Never call the API directly.** Use the reach verbs:
```bash
# Check API health
tooling/db/audio-health
# Check API health (OFF is the normal resting state)
reach assets audio health
# Generate a single asset (WAV only)
tooling/db/audio-generate "prompt text" \
reach assets audio generate "prompt text" \
--duration 10 --steps 100 --cfg 7 \
--output path/to/output.wav
# Generate + post-process in one command (WAV → trim → normalize → OGG)
tooling/db/audio-generate "prompt text" \
reach assets audio generate "prompt text" \
--duration 10 --steps 100 --cfg 7 \
--output path/to/gen/intermediate.wav \
--output-ogg client/assets/audio/final.ogg
# Batch-generate from a manifest (preferred for multiple assets)
tooling/db/audio-batch docs/assets/audio/<manifest>.json
# Batch-generate from a manifest (preferred for multiple assets) — long:
# add --detach and follow it with `reach jobs log <id>`
reach assets audio batch docs/assets/audio/<manifest>.json
```
### Parameters
@@ -142,9 +147,9 @@ Asset `id` values must match IDs in `docs/assets/audio/{category}.md` (e.g.,
AMB-001, SFX-002, UI-005). This couples the manifest to the asset inventory.
**`lufs`/`quality` defaults apply to `synth` assets only.** `audio_batch.py`'s
SAO path (`run_sao_generate`) only forwards `steps`/`cfg`/`timeout` to the
connector — `--lufs`/`--quality` aren't even exposed as CLI flags on
`audio_connector.py generate`, so a manifest's `defaults.lufs`/`defaults.quality`
SAO path (`run_sao_generate`) only forwards `steps`/`cfg`/`timeout` to
`audio.generate` — `--lufs`/`--quality` aren't exposed on
`reach assets audio generate` either, so a manifest's `defaults.lufs`/`defaults.quality`
are silently ignored for `method: "sao"` assets. SAO post-processing is fixed
at -16 LUFS / quality 6 regardless of what the manifest says.
@@ -152,16 +157,16 @@ at -16 LUFS / quality 6 regardless of what the manifest says.
```bash
# Full run
tooling/db/audio-batch docs/assets/audio/<manifest>.json
reach assets audio batch docs/assets/audio/<manifest>.json
# Dry run — preview what would be generated
tooling/db/audio-batch docs/assets/audio/<manifest>.json --dry-run
reach assets audio batch docs/assets/audio/<manifest>.json --dry-run
# Generate only specific assets
tooling/db/audio-batch docs/assets/audio/<manifest>.json --only AMB-001,AMB-002
reach assets audio batch docs/assets/audio/<manifest>.json --only AMB-001,AMB-002
# Skip assets that already have OGG files
tooling/db/audio-batch docs/assets/audio/<manifest>.json --skip-existing
reach assets audio batch docs/assets/audio/<manifest>.json --skip-existing
```
### 3. Update asset docs with prompts
@@ -204,8 +209,8 @@ For one-off generation or iteration on a specific asset:
2. Read `references/sonic-palette.md` for the sonic family prefix.
3. Read `references/category-templates.md` for the matching template.
4. Assemble the full prompt.
5. Run `tooling/db/audio-health` to verify the API is up.
6. Run `tooling/db/audio-generate` with `--post` or `--output-ogg` to
5. Run `reach assets audio health` to verify the API is up.
6. Run `reach assets audio generate` with `--post` or `--output-ogg` to
generate and post-process in one step.
7. Verify the output (file size, duration).
8. Update the asset status and prompt in `docs/assets/audio/{category}.md`.
@@ -232,23 +237,23 @@ If you need to post-process separately (e.g., re-normalizing an existing file):
```bash
# Full pipeline: trim → normalize → convert
tooling/db/audio-post pipeline input.wav --output output.ogg
reach assets audio post pipeline input.wav --output output.ogg
# Individual steps
tooling/db/audio-post trim input.wav
tooling/db/audio-post normalize input.wav --lufs -16
tooling/db/audio-post convert input.wav --output output.ogg
reach assets audio post trim input.wav
reach assets audio post normalize input.wav --lufs -16
reach assets audio post convert input.wav --output output.ogg
```
## Manual Synthesis (Insert-Tech Sounds)
For sounds under 200ms (cursor hover, weapon aim), Stable Audio Open cannot
produce meaningful output. Use manual synthesis via `tooling/synth_ui_sounds.py`
produce meaningful output. Use manual synthesis via `reach assets synth-ui`
or the batch manifest's `method: "synth"` with harmonic parameters.
For complex synthesis beyond the `harmonic` type (FM, filtered noise, bandpass
impulse), write a custom script in `tooling/` following the pattern in
`tooling/synth_ui_sounds.py`.
impulse), add a function beside the four in
`tooling/domains/assets/synth_ui.py` and call it from its `run()`.
## Quality Checklist
+25 -20
View File
@@ -17,15 +17,19 @@ Convert approved concept images to game-ready .glb models.
| Service | Check |
|---------|-------|
| Trellis | `tooling/db/trellis_connector.py health` |
| Trellis | `reach assets trellis health` |
| Blender | `reach blender which` |
Trellis runs on tower-of-joy and may be switched off. Check before batching.
Trellis runs on tower-of-joy and is kept **switched off** to save VRAM — that is
its normal state, and `health` exits 1 saying so. Get it turned on before a
session; do not restart it blindly (that takes VRAM from whatever is running).
Each verb prints its result JSON on stdout and fails with a non-zero exit —
check the exit status, not an `ok` field.
## Usage
```bash
python3 tooling/db/trellis_connector.py generate input.png \
reach assets trellis generate input.png \
--output .tmp/glb-gen/[name].glb \
--simplify 0.95 \
--texture-size 1024
@@ -41,26 +45,27 @@ python3 tooling/db/trellis_connector.py generate input.png \
## Batch Usage
`tooling/trellis-batch.sh` exists but is **hardcoded to one category**: it
takes no arguments and always processes the 16 character body types from
`.tmp/image-gen/characters/bodies` into `.tmp/glb-gen/characters/bodies`.
Running it for any other asset category (furniture, props, etc.) does
nothing useful — it will just re-run (or skip, if outputs already exist) the
same 16 character bodies regardless of what you intended.
For any other category, loop `trellis_connector.py` calls yourself with a
cooldown between jobs:
`reach assets trellis batch` runs a whole directory, one job at a time, with a
cooldown between jobs and retries on failure. (It replaced `trellis-batch.sh`,
which was hardcoded to the 16 character bodies — T-1290.) It skips outputs that
already exist and exits 1 if any item failed. It is long; add `--detach`.
```bash
for f in .tmp/image-gen/furniture/tables/*.png; do
name=$(basename "$f" .png)
python3 tooling/db/trellis_connector.py generate "$f" \
--output ".tmp/glb-gen/furniture/tables/${name}.glb" \
--simplify 0.95 --texture-size 1024
sleep 15
done
# every .png in a directory
reach --detach assets trellis batch \
--input-dir .tmp/image-gen/furniture/tables \
--output-dir .tmp/glb-gen/furniture/tables
# only some of them
reach assets trellis batch --input-dir <dir> --output-dir <dir> --names table_a,table_b
# no --input-dir: the character bodies, as the bash script did
reach assets trellis batch
```
`--cooldown` (15 s), `--retries` (3) and `--retry-delay` (60 s) keep the
original script's pacing.
**Never run Trellis jobs in parallel** — it uses the full GPU and concurrent
jobs will OOM and corrupt the CUDA state.
@@ -143,7 +148,7 @@ For best Trellis results:
- **Do NOT force isometric angle** — Trellis reconstructs full 3D, the game camera handles the view
- To maintain consistency across a batch, generate the concept images with
`/image-gen` using its `--input` style anchor flag — `--input` is an
/image-gen flag, not a Trellis one; `trellis_connector.py` has no `--input`
/image-gen flag, not a Trellis one; `reach assets trellis generate` has no `--input`
argument and exits on unrecognized flags.
These match `/image-gen` output with the Settled Reach style guide.
@@ -1,6 +1,6 @@
# Trellis API Reference
The connector (`tooling/db/trellis_connector.py`) talks to a Gradio API. The
The connector (`tooling/domains/assets/trellis.py`, `reach assets trellis`) talks to a Gradio API. The
parameter layout is fragile — document changes here when the container is
updated. This is the canonical copy; the connector's module docstring carries
a duplicate for at-a-glance reference when reading the script directly — keep
+8 -5
View File
@@ -19,18 +19,21 @@ not something this skill triggers itself. Output is a PNG file.
Requires `GEMINI_API_KEY` in environment (set in `.claude/settings.local.json`).
Uses the canonical connector at `tooling/db/image_connector.py` (see
`.claude/rules/project-structure.md` — `tooling/db/` is the documented home
for asset/connector scripts; this skill has no local fork of it).
Uses the canonical connector behind `reach assets image`
(`tooling/domains/assets/image.py`, T-1290; this skill has no local fork of it).
`health` only lists models and is free; **every `generate` costs money**.
A verb prints its result JSON on stdout and fails with a non-zero exit — check
the exit status, not an `ok` field. An invalid `--aspect` is rejected up front
with the accepted list.
```bash
python3 tooling/db/image_connector.py health
reach assets image health
```
## Usage
```bash
python3 tooling/db/image_connector.py generate \
reach assets image generate \
"prompt text" \
--output path/to/output.png \
--aspect 1:1