7 Commits

Author SHA1 Message Date
B.D. Murphy
bf83bb35cc Merge minieq/main: Apache-2.0 LICENSE, NOTICE and SPDX headers
The remote had one commit this branch did not (03f8a85), which adds the
Apache-2.0 LICENSE, the NOTICE and SPDX headers to every .py and .sh
file. The merge was clean - the headers sit at the top of each file and
my changes are further down.

The new files this branch added (languages.py, translations.py, the
cluster package, the new tests and scripts) were written after the
merge base, so the licence commit never saw them. They now carry the
same header, so the convention holds across the whole tree.

Nothing else changed. 883 tests passed / 8 failed - the same 8
pre-existing failures.
2026-11-06 17:12:24 -08:00
umbra audio
03f8a858dd Add Apache-2.0 LICENSE, NOTICE, and SPDX headers 2026-10-18 19:01:04 -07:00
B.D. Murphy
5832c70369 infra: add MCP shell server (restores command execution)
OpenCode 2.0.27 withholds the built-in Shell tool regardless of config,
client (desktop app vs TUI) or model -- all three were eliminated by test.
MCP is the one channel demonstrably under user control, so add a stdio
server that restores it.

Stdlib-only and deliberately unfiltered so every line is auditable: a
third-party package with unrestricted command execution is an opaque
trust decision.

Two choices taken from AGENTS.md:

- PATH correction. OpenCode spawns MCP servers that can inherit a bare
  PATH exactly as launchd does, which is the documented cause of
  ffmpeg failing for m4b/MP3 export. /opt/homebrew/bin is prepended to
  the child only.
- background=true detaches into its own session (start_new_session) so
  long-running processes -- TTS server, web UI -- survive both the MCP
  server exiting and an OpenCode restart, and log to a file rather than
  a pipe that would eventually block the writer.

Selftest drives a full handshake and exits non-zero on failure, so a
misbehaving server is caught before an OpenCode restart rather than
after. It found two real bugs during development: a double-encoding
blind spot in an exit-code assertion (tool output is JSON nested in
JSON-RPC, so its quotes are escaped), and an f-string escape that is a
syntax error on Python 3.11, the project's stated floor.

Verified: py_compile clean, ruff clean, selftest 8/8, background mode
detached and logged, exit codes and stdout/stderr separation correct.
Full suite unchanged at 8 failed (pre-existing) / 803 passed.
2026-10-17 06:30:58 -07:00
B.D. Murphy
3d69187449 Answer the controller's data request, and audit what we send
The UmbraTrans controller asked (commit 70c23fd) for machine-readable metadata
about the audio system, in a specified layout, with a validator that ships with
the request so the sender can check its own work. That request is answered here.

scripts/build_controller_bundle.py emits that bundle: one real book (Deal
Hunter), 8,884 records, no media, no credentials, no hostnames. It passes the
controller's own validator, exit 0.

Three things in this system will break a mapping that does not know them, and
the bundle's real purpose is to say so precisely rather than to look tidy:

* A chapter has THREE numbers. The database row id is what filenames use,
  chapter_index is reading order, and the distribution track number is a third
  thing again. Only the row id survives an edit. Front and back matter are
  numbered with roman numerals in the MP3 filenames.
* The narrator is written three ways, and the most common way is nothing at all.
  1,803 utterances have a NULL speaker_name, meaning narrator; 614 say
  "narrator" and 15 say "Narrator". A join that skips nulls loses most of the
  narration silently. Utterances now carry speaker_name_raw, the resolved name,
  and speaker_resolution, and the two spellings are not merged - that is a
  decision for whoever owns speaker identity, not something an export should do
  quietly.
* A .wav file is not always a WAV. 30 of the 91 voice reference files are MPEG.
  Every render reports the container measured from the bytes, with
  extension_matches_container, so a reader cannot be misled by the name.

A validator proves the bundle is internally consistent; it cannot prove the
bundle is true. scripts/audit_controller_bundle.py re-measures every hash, byte
size, duration and count against the project on disk - 66,011 claims, all
holding. It caught two real defects in the first draft of this bundle, including
one of my own: produced_at was stamped with the build time, which is a claim
about the audio that nothing supported.

The validator was also run against seven deliberately broken bundles to confirm
it can fail. A validator nobody has seen fail is not evidence of anything; those
cases are now tests.

scripts/audio_controller_connect.py runs the T01-T10 suite: 4 pass, 1 fails,
5 are blocked. T04 fails honestly - there is no ingest endpoint, and nothing was
pushed. T05-T08 are blocked because the controller's own plan sequences ingest
after the metadata arrives; a proposed contract is included rather than an
invented endpoint. T02 is blocked: no node key or service token is configured
here, and an operator has to supply one.

Two corrections to the node profile, which the controller reads: it claimed the
web UI has no authentication (a session gate was added 2026-10-09) and that the
controller host is offline (it is reachable and serving umbratrans-web 0.1.0).

Full suite 751 passed, 8 failed - the same pre-existing pronunciation and
accent failures, unchanged. ruff and mypy clean on everything added.
2026-10-14 10:56:32 -07:00
B.D. Murphy
0c9bea72e0 Add enterprise/infra tooling and repoint Qwen3-TTS submodule to internal fork
Two groups of changes, committed together as the follow-up to the initial push:

1. .gitmodules — submodule Qwen3-TTS now points at the internal Gitea fork
   (ssh://git@mini-eq:2222/umbragit/Qwen3-TTS.git) instead of upstream GitHub,
   so a clone actually gets the UmbraAudio branding/integration commit
   012e452 rather than vanilla upstream.

2. New infra/ tooling for the cross-machine work in spec 21: agent_bridge,
   remote_credential_broker (Keychain-backed, secrets read at runtime and
   generated via read_secret/random_secret — no hardcoded credentials), the
   enterprise bootstrap scripts, and the oldbox SSH/auth prompts. Plus
   backup/ restore scripts, two specs (18 UmbraTrans, 19 enterprise control
   plane), umbraaudio/voices/lmstudio_gate.py, and in-flight edits to the
   discovery/attribution/profiling modules, config, chapter_builder, emotion,
   pronunciation, qwen_tts and routes.

Also adds *.db to .gitignore: SQLite project databases are runtime state and no
.db file was tracked before.

Verified before staging: .env's only change is UMBRA_TTS__QWEN_TTS_HELPER_URL and
the committed copy has zero secret-looking values; the broker ships only
config.example.json.
2026-10-06 06:57:41 -07:00
B.D. Murphy
829a03b89f Action the controller alignment proposal: source_unit_key + phase reports
The controller (oldbox) sent a data-model alignment proposal and asked for six
additive changes on this node, the validation protocol, and an honest report.
Six of the seven changes are done; the seventh is declared missing rather than faked.

Stage 0 - reachability verified exactly as documented: health 200, heartbeat 401
(the correct answer - auth is refusing), missing Host 400. Nothing else was sent.

source_unit_key (controller 5.1, the blocking item)
  umbraaudio/source_units.py. sha256("uk1" | chapter_source_text_sha256 |
  occurrence | text_sha256). Nothing positional in the key: "occurrence"
  disambiguates repeated text in document order.
  - 18,181/18,181 units keyed across all four projects, 0 collisions
  - chapter 1 renumbered +100 -> 0 keys changed (the load-bearing proof)
  - backfill 10,015 rows in 0.11s, second run stamps 0 (idempotent)
  - alignment phase 3: 200/200 probe units resolved with used_index: false
  - tolerates the older projects that have no chapters.chapter_index
  Recorded at generation time in chapter_builder, BEFORE the rows are read and
  BEFORE the marker is written, so every path that renders audio also produces
  identity and the clip can name its source unit.

5.7 recorded: utterance_index is KEPT as a display ordinal and made
  non-load-bearing. spec 21 §10.

Blob hashes and effect chain (5.2)
  Markers now carry the full sha256 of the delivered bytes and the effects that
  produced them. Verified the existing order is already correct - write_marker
  runs AFTER apply_effects - and closed the remaining hole: a changed trait list
  changed the hash with no recorded cause. sha256 verification is opt-in, since
  hashing every file on every validation pass would re-read gigabytes.

Vocabularies published (5.4) - umbraaudio/definitions.py, 19 sets, 126 values,
  each value carrying whether it is 'declared' (spec/controller) or 'extracted'
  (read out of code), so a diff shows a difference of meaning, not of wording.

Alignment report tool (6.6) - scripts/alignment_report.py --phase all. Emits the
  controller's envelope with the per-phase evidence shapes, and obeys the two
  rules they set: no evidence means blocked, never pass; a field we cannot produce
  is null PLUS a finding, never omitted. Phase 2 is deliberately 'fail' - the
  three-way store disagreement is real, and a pass there would be a check that
  passed by doing nothing.

Fingerprint discipline (5.5) - the drift detector reported its OWN fingerprint
  change when source_unit_key was added rather than hiding it. Node profile
  updated with the new fingerprints and an honest note that they moved.

Node report extended - per-project source-unit-key coverage, per-voice-asset
  sha256 + object_key, definition_set_version, controller stage reached.

Six gaps found in the proposal (spec 21 §11), including the one they invited plus
five more. The sharpest: "hash at generation" ignores that audio is
post-processed afterwards, and our key is anchored to chapters.source_text - a
second copy of the manuscript - so any change to chapter storage silently
invalidates every key in that chapter.

5.6 deliberately NOT faked: personality has no versioned artifact here, only
  unversioned LLM prose in speakers.json. Reported as phase 6b finding B-03
  instead, because wrapping unversioned inputs in a version field would produce
  artifacts claiming a lineage they do not have.

Answered the controller's two open questions (infra/controller-handoff-2026-10-03.md):
forward-only link for a revealed recurring character, and yes - a new edition may
contradict a frozen state, since this node's correction path IS a regeneration.

Full suite 484 passed / 8 failed - the same 8 pre-existing and unrelated failures.
ruff clean on every file touched. The parallel session's chapter_builder hunk
(helper_url) was filtered out of the commit and left intact in the working tree.
2026-10-02 20:16:06 -07:00
B.D. Murphy
507ed95424 Declare this machine an Umbra node + cross-machine system contract (spec 21)
A parallel effort is moving UmbraTrans into the main Umbra system on oldbox
(192.168.123.14). This commit makes this machine describable to that system and
records what the system still needs to decide. Everything is a proposal for the
controller to ratify; no architecture is decided here.

specs/21-umbra-system-nodes-tasks-and-changes.md
  Four workstreams, each with what exists, what is missing, and a proposal:
  - WS1 data model alignment / copy / verify / transition. Design is far along
    already (manifest.md + spec 20); the missing piece is ratification.
  - WS2 every machine is a node that gets assigned any task. Job types, leases
    and pools exist (spec 19 §12); the node half does not. Proposes node
    identity, capability advertisement, task/result envelopes, and refusal as a
    valid audited outcome.
  - WS3 cross-product change management. Nothing exists today. Proposes a
    requirement catalog owned by the controller, four change classes, and a
    distribute-then-confirm-with-evidence loop. Includes the worked example of a
    controller-owned entity (designation, pronunciation, voice attribute) that
    must apply across every product.
  - WS4 UI topology. Recommends hybrid: controller gets the system portal,
    the audio review UI stays on the worker because it is artifact-native and
    has no authentication at all. Adds a git-tag deploy + verify + confirm flow.

  Also: 25 questions grouped by who can answer, each naming what it blocks, and
  a task list for this machine (NOW / NEXT / LATER) where every task has a
  machine-checkable acceptance criterion.

infra/umbra-node.json
  This machine's declared profile: node_id umbraaudio-m4, role worker, services,
  capabilities, resource pools, constraints, project list with lifecycle, schema
  fingerprints, and an honest facts_for_the_controller block (empty tables, no
  stable utterance id, no auth, no shared-postgres client). No secrets - ids,
  names, labels and counts only.

scripts/umbra_node_report.py
  Read-only node report. Git state, listening ports, .env KEY NAMES only, per
  project row counts, and a schema fingerprint hashed over sorted
  table.column:type so ALTER TABLE reordering does not read as drift. --json for
  machine consumption; --check compares declared profile to measured reality and
  exits 2 on drift. Drift is deliberately limited to stable facts (contract
  version, project set, schema fingerprints) - git HEAD and live ports are
  reported but never drift.

manifest.md
  Now explicitly a dated measurement with an update log, because other machines
  read it as current. 2026-10-01 refresh: Last Dwarf speakers.json 70 -> 93
  after the designation merge, DealHunter ref WAVs 94 -> 97. Every other §1.1
  count re-verified unchanged.

Measured this machine: 4 projects, 2 distinct schema fingerprints (real
per-project drift), only 4 of 11 tables populated, 461 tests passing / 8 known
pre-existing failures. No UmbraAudio behaviour changed.
2026-10-01 07:14:54 -07:00