- Database deployment script (scripts/deploy-database.sh) - Kubernetes manifests for CNPG PostgreSQL cluster (k8s/base/) - Documentation (docs/DEPLOYMENT_GUIDE.md, DATASTORE_SPEC.md, INTEGRATION_SPEC.md) - README with architecture overview and operations guide
23 KiB
23 KiB
UmbraTrans Integration Specification for External Services
Version: 1.0
Audience: UmbraAudio, UmbraFormat teams
Status: Active
Last Updated: 2026-10-31
This document defines the contract for external services (UmbraAudio, UmbraFormat) to integrate with the UmbraTrans datastore. It covers PostgreSQL access, NFS shared storage, and API boundaries.
1. Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ UMBRA ECOSYSTEM │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ UmbraTrans │ │ UmbraAudio │ │ UmbraFormat │ │
│ │ (this cluster) │◄───►│ (remote node) │◄───►│ (remote node) │ │
│ │ │ │ │ │ │ │
│ │ • Web API/UI │ │ • TTS/ASR │ │ • EPUB/PDF │ │
│ │ • Job Queue │ │ • Audio Master │ │ • Cover Gen │ │
│ │ • PostgreSQL │ │ • Speaker DB │ │ • Print Layout │ │
│ │ • NFS Server │ │ │ │ │ │
│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └────────────────────────┼────────────────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ SHARED STORE │ │
│ │ │ │
│ │ PostgreSQL │ │
│ │ (192.168.123.14)│ │
│ │ │ │
│ │ NFS │ │
│ │ (192.168.123.14)│ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ WordPress │ (External - API only) │
│ │ (sales site) │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Git Server │ (192.168.123.18 - Mini-Eq) │
│ │ (source code) │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
2. Network Connectivity
2.1 Required Access from Remote Nodes
| Service | Host | Port | Protocol | Purpose |
|---|---|---|---|---|
| PostgreSQL (PgBouncer) | 192.168.123.14 |
6432 |
TCP | Pooled DB connections |
| PostgreSQL (Direct) | 192.168.123.14 |
5432 |
TCP | Admin/backup (optional) |
| NFS | 192.168.123.14 |
2049 |
TCP/UDP | Shared storage mount |
| Web API | umbra.bdmurphy.me |
443 |
HTTPS | REST API, webhooks |
2.2 Firewall Rules (on 192.168.123.14)
# PostgreSQL (PgBouncer) - allow from cluster nodes
iptables -A INPUT -s 192.168.123.0/24 -p tcp --dport 6432 -j ACCEPT
# PostgreSQL (Direct) - allow from cluster nodes
iptables -A INPUT -s 192.168.123.0/24 -p tcp --dport 5432 -j ACCEPT
# NFS - allow from cluster nodes
iptables -A INPUT -s 192.168.123.0/24 -p tcp --dport 2049 -j ACCEPT
iptables -A INPUT -s 192.168.123.0/24 -p udp --dport 2049 -j ACCEPT
# Also need: 111 (rpcbind), 20048 (mountd) - check rpcinfo -p
2.3 DNS / Hosts
Add to /etc/hosts on remote machines (or configure DNS):
192.168.123.14 umbra-db umbra-nfs umbra-api
3. PostgreSQL Access
3.1 Connection Parameters
# PgBouncer (RECOMMENDED - pooled, 6432)
host=192.168.123.14
port=6432
dbname=umbratrans
user=umbraudio # or umbraformat
password=<from secret>
sslmode=require
connect_timeout=10
application_name=umbraudio-worker # or umbraformat-worker
# Direct (for admin/backup only - 5432)
host=192.168.123.14
port=5432
dbname=umbratrans
user=umbraudio
password=<from secret>
sslmode=require
3.2 Database Roles
| Role | Permissions | Used By |
|---|---|---|
umbraudio |
SELECT, INSERT, UPDATE on jobs, artifacts, compute_nodes, node_heartbeats, model_registry, node_models; SELECT on projects, organizations |
UmbraAudio workers |
umbraformat |
SELECT, INSERT, UPDATE on jobs, artifacts, format_metadata; SELECT on projects, organizations |
UmbraFormat workers |
umbra_web |
Full access to all tables (web API) | UmbraTrans web |
3.3 Required Extensions
-- Already installed by migrations
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "citext";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
4. NFS Shared Storage
4.1 Mount Points (on Remote Nodes)
# /etc/fstab
192.168.123.14:/mnt/umbra-data/productions /mnt/umbra/productions nfs4 rw,hard,intr,rsize=1048576,wsize=1048576,timeo=600,retrans=2 0 0
192.168.123.14:/mnt/umbra-data/artifacts /mnt/umbra/artifacts nfs4 rw,hard,intr,rsize=1048576,wsize=1048576,timeo=600,retrans=2 0 0
192.168.123.14:/mnt/umbra-data/covers /mnt/umbra/covers nfs4 rw,hard,intr,rsize=1048576,wsize=1048576,timeo=600,retrans=2 0 0
192.168.123.14:/mnt/umbra-data/audio /mnt/umbra/audio nfs4 rw,hard,intr,rsize=1048576,wsize=1048576,timeo=600,retrans=2 0 0
4.2 Directory Permissions
# On NFS server (192.168.123.14)
mkdir -p /mnt/umbra-data/{productions,artifacts,covers,audio,backups}
chown -R 1000:1000 /mnt/umbra-data
chmod -R 700 /mnt/umbra-data
# On remote nodes - same UID/GID
groupadd -g 1000 umbra
useradd -u 1000 -g umbra -s /bin/bash umbra
4.3 Access Patterns by Service
| Path | UmbraAudio | UmbraFormat | UmbraTrans Web |
|---|---|---|---|
/productions/<project>-<lang>/source/ |
Read (source text) | Read (metadata) | Read/Write |
/productions/<project>-<lang>/runs/ |
Read/Write (translation artifacts) | Read | Read |
/artifacts/<org>/<project>/<hash>/ |
Read/Write (audio renders) | Read/Write (EPUB/PDF) | Read |
/covers/<asset_id>/ |
Read | Read/Write (cover gen) | Read/Write |
/audio/<project>/<lang>/ |
Read/Write (master audio) | Read | Read |
5. Job Queue Integration
5.1 How Workers Claim Jobs
-- Atomic job claim (run in transaction)
BEGIN;
SET LOCAL statement_timeout = 5000;
SET LOCAL app.user_id = '<node-user-id>';
SET LOCAL search_path = 'public';
UPDATE public.jobs
SET status = 'running',
lease_owner = '<compute_node_id>',
lease_expires_at = now() + interval '30 minutes',
started_at = now(),
attempt = attempt + 1
WHERE id = (
SELECT id FROM public.jobs
WHERE status = 'queued'
AND not_before <= now()
AND (lease_expires_at IS NULL OR lease_expires_at <= now())
AND kind IN ('<allowed-kinds>')
AND organization_id = '<org-id>'
ORDER BY priority, not_before, created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *;
COMMIT;
5.2 Job Kinds by Service
| Service | Job Kinds (claims) | Job Kinds (enqueues) |
|---|---|---|
| UmbraAudio | tts, asr, assemble, encode, master, speaker_profile |
tts (after translation), assemble (after tts) |
| UmbraFormat | build_epub, build_pdf, cover_generate, metadata_validate |
build_epub (after translation+audio), cover_generate |
5.3 Job Payload Structure
// TTS Job
{
"utterance_ids": ["utt-001", "utt-002"],
"speaker_id": "spk-narrator",
"voice_config": {
"model": "xtts-v2",
"language": "es",
"speed": 1.0
},
"output_format": "wav"
}
// EPUB Build Job
{
"project_id": "uuid",
"language": "es",
"include_audio": true,
"cover_asset_id": "asset-uuid",
"format_options": {
"epub_version": "3.2",
"drm": false,
"embedded_fonts": ["Source Serif Pro"]
}
}
5.4 Job Result Structure
// TTS Result
{
"artifacts": [
{
"artifact_id": "uuid",
"kind": "audio",
"storage_uri": "file://192.168.123.14/mnt/umbra-data/audio/proj123/es/chapters/ch-001/track-001.wav",
"content_hash": "sha256...",
"byte_size": 1048576,
"media_type": "audio/wav",
"duration_ms": 5432
}
],
"metrics": {
"total_duration_ms": 5432,
"rtf": 0.15
}
}
// EPUB Build Result
{
"artifacts": [
{
"artifact_id": "uuid",
"kind": "epub",
"storage_uri": "file://192.168.123.14/mnt/umbra-data/artifacts/org1/proj123/hash123.epub",
"content_hash": "sha256...",
"byte_size": 5242880,
"media_type": "application/epub+zip"
}
],
"validation": {
"epubcheck": "passed",
"page_count": 312
}
}
6. Artifact Protocol (Content-Addressed Storage)
6.1 Writing Artifacts
# 1. Compute content hash
import hashlib
content_hash = hashlib.sha256(audio_bytes).hexdigest()
# 2. Write to NFS (atomic)
artifact_path = f"/mnt/umbra/artifacts/{org_id}/{project_id}/{content_hash[:2]}/{content_hash}"
os.makedirs(os.path.dirname(artifact_path), exist_ok=True)
tmp_path = artifact_path + ".tmp"
with open(tmp_path, "wb") as f:
f.write(audio_bytes)
os.rename(tmp_path, artifact_path)
# 3. Insert artifact row (PostgreSQL)
INSERT INTO public.artifacts (
id, project_id, organization_id, lang, kind, status,
content_hash, byte_size, media_type, storage_key,
storage_uri, integrity, derivation, created_at
) VALUES (
gen_random_uuid(), $project_id, $org_id, $lang, 'audio', 'final',
$content_hash, $byte_size, 'audio/wav', $content_hash,
$storage_uri, 'unverified', $derivation_json, now()
)
ON CONFLICT (project_id, content_hash, kind, lang) DO NOTHING;
# 4. Link to job
INSERT INTO public.job_artifacts (job_id, artifact_id, role, created_at)
VALUES ($job_id, $artifact_id, 'produced', now());
6.2 Storage URI Format
file://<node-id>/<relative-path-from-nfs-root>
Examples:
file://umbra-audio-01/mnt/umbra-data/audio/proj123/es/chapters/ch-001/track-001.wav
file://umbra-format-01/mnt/umbra-data/artifacts/org1/proj123/ab12cd34.epub
6.3 Reading Artifacts
# Via storage_uri from artifact row
storage_uri = artifact_row["storage_uri"] # "file://umbra-audio-01/mnt/umbra-data/..."
# Parse and read
if storage_uri.startswith("file://"):
# Extract path after hostname
path = storage_uri.split("/", 3)[-1] # "/mnt/umbra-data/..."
with open(path, "rb") as f:
data = f.read()
7. Compute Node Registration
7.1 Node Registration (at startup)
-- Register compute node
INSERT INTO public.compute_nodes (
id, organization_id, name, is_controller, trust_domain,
locality, transport, status, agent_version, agent_started_at, created_at
) VALUES (
$node_id, $org_id, $hostname, false, 'remote',
$datacenter, 'https', 'provisioning', $version, now(), now()
)
ON CONFLICT (organization_id, name) DO UPDATE SET
status = 'provisioning',
agent_version = EXCLUDED.agent_version,
agent_started_at = EXCLUDED.agent_started_at,
updated_at = now()
RETURNING id;
7.2 Device Registration
-- Register GPUs/CPUs
INSERT INTO public.compute_devices (
id, compute_node_id, kind, label, vram_total_mb, vram_free_mb,
ram_total_mb, ram_free_mb, compute_units, throughput_factor, supports
) VALUES (
gen_random_uuid(), $node_id, 'gpu_discrete', 'gpu:0',
24576, 24576, 0, 0, 10496, 1.0, '{"cuda": "12.4"}'
)
ON CONFLICT (compute_node_id, label) DO UPDATE SET
vram_total_mb = EXCLUDED.vram_total_mb,
vram_free_mb = EXCLUDED.vram_free_mb,
updated_at = now();
7.3 Heartbeat (every 30 seconds)
-- Update heartbeat (overwrites previous)
INSERT INTO public.node_heartbeats (
compute_node_id, reported_at, cpu_busy_pct, ram_free_mb,
vram_free_mb, vram_total_mb, disk_free_mb, load_avg_1m,
resident_models, active_jobs, agent_version, raw
) VALUES (
$node_id, now(), $cpu_pct, $ram_free, $vram_free, $vram_total,
$disk_free, $load_avg, $resident_models_json, $active_jobs, $version, $raw_json
)
ON CONFLICT (compute_node_id) DO UPDATE SET
reported_at = EXCLUDED.reported_at,
cpu_busy_pct = EXCLUDED.cpu_busy_pct,
ram_free_mb = EXCLUDED.ram_free_mb,
vram_free_mb = EXCLUDED.vram_free_mb,
vram_total_mb = EXCLUDED.vram_total_mb,
disk_free_mb = EXCLUDED.disk_free_mb,
load_avg_1m = EXCLUDED.load_avg_1m,
resident_models = EXCLUDED.resident_models,
active_jobs = EXCLUDED.active_jobs,
agent_version = EXCLUDED.agent_version,
raw = EXCLUDED.raw;
7.4 Status Transitions
provisioning → online → draining → offline → retired
↘️ (heartbeat stops, >2min) ↗️
- online: Heartbeat received, accepting jobs
- draining: Finishing current jobs, not accepting new
- offline: No heartbeat > 2 minutes
- retired: Decommissioned permanently
8. Model Registry
8.1 Model Registration (by controller)
INSERT INTO public.model_registry (
id, slug, kind, vram_min_mb, vram_preferred_mb, ram_min_mb,
quantisation, context_tokens, requires_accelerators,
reference_throughput, load_seconds, capabilities, source_uri, license
) VALUES (
gen_random_uuid(), 'xtts-v2', 'tts', 6144, 8192, 2048,
'fp16', 4096, ARRAY['cuda'], 1.0, 30,
'{"languages": ["en","es","fr","de","it","pt","pl","tr","ru","nl","cs","ar","zh","ja","hu","ko"]}',
'https://github.com/coqui-ai/TTS', 'CPML'
)
ON CONFLICT (slug) DO NOTHING;
8.2 Model Residency (worker reports)
-- When model loaded
INSERT INTO public.node_models (
compute_node_id, model_id, device_id, state, loaded_at, vram_held_mb
) VALUES ($node_id, $model_id, $device_id, 'loaded', now(), 7168)
ON CONFLICT (compute_node_id, model_id) DO UPDATE SET
state = EXCLUDED.state,
loaded_at = EXCLUDED.loaded_at,
vram_held_mb = EXCLUDED.vram_held_mb,
updated_at = now();
-- When model unloaded
UPDATE public.node_models SET state = 'available', vram_held_mb = 0
WHERE compute_node_id = $node_id AND model_id = $model_id;
9. Audio Import (UmbraAudio → UmbraTrans)
9.1 Bundle Export Format
UmbraAudio exports a bundle directory:
/mnt/umbra/productions/<project>-<lang>/bundle/
├── bundle.json # Envelope with metadata
└── manifest.jsonl # Line-delimited JSON records
bundle.json:
{
"bundle_format_version": 1,
"content_hash": "sha256-of-manifest",
"source": {
"system": "umbraaudio",
"node_id": "umbra-audio-01",
"product_version": "1.2.0"
},
"scope": {
"books_full_detail": ["book-dir-name"]
}
}
manifest.jsonl records: book, chapter, speaker, utterance, render
9.2 Import Trigger
UmbraTrans imports via CLI (runs as CronJob or manual):
python -m umbratrans_web.cli import-productions \
--organization umbra \
--allow-production \
--confirm
This reads bundles from /mnt/umbra/productions/*/bundle/ and populates:
project_source_refstranslation_chapterstranslation_speakerstranslation_utterancesartifacts(audio renders)translation_imports(ledger)
10. Format Metadata API (UmbraFormat ↔ UmbraTrans)
10.1 Get Format Metadata
GET /api/projects/{project_id}/format-metadata?language_tag=es
Authorization: Bearer <token>
Response:
{
"project_id": "uuid",
"language_tag": "es",
"title": "El Cazador de Ofertas",
"isbn13": "9788412345678",
"cover_front_asset_id": "asset-uuid",
"binding": "paperback",
"trim_w_in": 6.0,
"trim_h_in": 9.0,
"page_count_total": 312,
"font_primary": "Source Serif Pro",
"font_primary_size_pt": 11.0,
"line_height_pt": 14.5,
"margin_inside_in": 0.75,
"margin_outside_in": 0.625,
"margin_top_in": 0.875,
"margin_bottom_in": 0.875
}
10.2 Save Format Metadata
PUT /api/projects/{project_id}/format-metadata
Authorization: Bearer <token>
Content-Type: application/json
{
"language_tag": "es",
"isbn13": "9788412345678",
"cover_front_asset_id": "asset-uuid",
"binding": "paperback",
"trim_w_in": 6.0,
"trim_h_in": 9.0,
"page_count_total": 312,
"font_primary": "Source Serif Pro",
"font_primary_size_pt": 11.0
}
10.3 Cover Upload
POST /api/projects/{project_id}/cover?language_tag=es
Authorization: Bearer <token>
Content-Type: multipart/form-data
file: <cover-image.png>
11. Webhooks / Event Notifications
11.1 Job Completion Webhook
UmbraTrans can POST to external services on job completion:
POST https://umbra-audio.internal/webhooks/job-complete
Content-Type: application/json
X-Umbra-Signature: sha256=...
{
"event": "job.completed",
"job_id": "uuid",
"kind": "tts",
"project_id": "uuid",
"organization_id": "uuid",
"status": "complete",
"result": { ... },
"artifacts": [
{ "artifact_id": "uuid", "kind": "audio", "storage_uri": "..." }
],
"completed_at": "2026-10-31T12:00:00Z"
}
11.2 Configure Webhook URLs
-- In organization or project settings
UPDATE public.organizations
SET webhook_urls = '{"job_complete": "https://umbra-audio.internal/webhooks/job-complete"}'
WHERE id = $org_id;
12. Credentials & Secrets
12.1 Service Accounts (Kubernetes)
# umbra-audio namespace on remote cluster
apiVersion: v1
kind: Secret
metadata:
name: umbratrans-credentials
namespace: umbra-audio
type: Opaque
stringData:
DATABASE_DSN: "postgresql://umbraudio:PASS@192.168.123.14:6432/umbratrans?sslmode=require"
NFS_SERVER: "192.168.123.14"
NFS_PRODUCTIONS_PATH: "/mnt/umbra-data/productions"
NFS_ARTIFACTS_PATH: "/mnt/umbra-data/artifacts"
NODE_CREDENTIAL_PREFIX: "umbra-audio-"
12.2 Node Credentials (for job queue auth)
-- Created by UmbraTrans admin
INSERT INTO public.node_credentials (
id, compute_node_id, key_prefix, key_hash, agent_name, scopes, issued_at
) VALUES (
gen_random_uuid(), $node_id, 'umbra-audio-01-',
'$argon2id$v=19$m=65536,t=3,p=4$salt$hash',
'umbra-audio-worker', ARRAY['queue:claim', 'queue:complete', 'artifacts:write'],
now()
);
Worker uses this credential to authenticate job queue operations.
13. Health Checks & Monitoring
13.1 Required Health Endpoints
Each service exposes:
GET /health → { "status": "ok", "service": "umbra-audio", "version": "1.2.0" }
GET /metrics → Prometheus metrics
13.2 Key Metrics to Export
| Metric | Type | Labels |
|---|---|---|
umbra_jobs_claimed_total |
Counter | service, kind, status |
umbra_job_duration_seconds |
Histogram | service, kind |
umbra_artifact_bytes_written |
Counter | service, kind |
umbra_node_heartbeat_age_seconds |
Gauge | node_id |
umbra_gpu_utilization |
Gauge | node_id, device_id |
umbra_queue_depth |
Gauge | kind, status |
14. Deployment Checklist for Remote Services
UmbraAudio
- PostgreSQL role
umbraudiocreated with correct grants - Node credentials inserted in
node_credentials - NFS mounts configured in
/etc/fstab - Compute node registered in
compute_nodes - Devices registered in
compute_devices - Models registered in
model_registry - Heartbeat cron running (every 30s)
- Job poller running for
tts,asr,assemble,encode,master - Artifact writer writing to
/mnt/umbra/artifacts/+artifactstable - Webhook endpoint for job completion (optional)
UmbraFormat
- PostgreSQL role
umbraformatcreated with correct grants - NFS mounts configured in
/etc/fstab - Job poller running for
build_epub,build_pdf,cover_generate,metadata_validate - Format metadata API integrated (GET/PUT
/format-metadata) - Cover upload integrated (POST
/cover) - Artifact writer for EPUB/PDF to
/mnt/umbra/artifacts/ - Webhook endpoint for job completion (optional)
15. Troubleshooting
| Issue | Diagnosis | Resolution |
|---|---|---|
| Job claim returns 0 rows | No queued jobs, or wrong organization_id |
Check SELECT * FROM jobs WHERE status='queued' AND organization_id=... |
| Lease expires mid-job | Job takes >30min | Increase lease_expires_at on claim, or heartbeat extends it |
| NFS mount hangs | Network partition, server down | Use hard,intr options; check rpcinfo -p 192.168.123.14 |
| SSL connection failed | Cert mismatch | Use sslmode=require with valid CA, or sslmode=verify-ca |
| Artifact conflict | Duplicate content hash | ON CONFLICT DO NOTHING - safe to ignore |
| Heartbeat not updating | Clock skew | Ensure NTP sync on all nodes |
16. Contact & Escalation
| Role | Contact | Channel |
|---|---|---|
| UmbraTrans Platform | platform@umbra.example.com | Slack #umbra-platform |
| Database Admin | dba@umbra.example.com | PagerDuty |
| Network/Infra | infra@umbra.example.com | Slack #umbra-infra |
Appendix: Quick Reference Commands
# Test DB connectivity
psql "postgresql://umbraudio:PASS@192.168.123.14:6432/umbratrans?sslmode=require" -c "SELECT now();"
# Test NFS mount
mount -t nfs4 192.168.123.14:/mnt/umbra-data/productions /mnt/test && ls /mnt/test
# Check job queue
psql $DSN -c "SELECT kind, status, count(*) FROM jobs GROUP BY kind, status;"
# Check node status
psql $DSN -c "SELECT id, name, status, last_heartbeat_at FROM compute_nodes;"
# View recent artifacts
psql $DSN -c "SELECT kind, count(*), sum(byte_size) FROM artifacts GROUP BY kind;"