Files
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

181 lines
6.0 KiB
Python

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Cluster configuration for UmbraAudio ↔ UmbraTrans integration."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
from pydantic_settings import BaseSettings, SettingsConfigDict
@dataclass(frozen=True)
class ClusterConfig:
"""Runtime configuration for cluster integration."""
# PostgreSQL (PgBouncer pooled)
pg_host: str = "192.168.123.14"
pg_port: int = 6432
pg_database: str = "umbratrans"
pg_user: str = "umbraudio"
pg_password: str = ""
pg_sslmode: Literal[
"disable", "allow", "prefer", "require", "verify-ca", "verify-full"
] = "require"
pg_app_name: str = "umbraudio-worker"
# Direct PostgreSQL (admin/backup)
pg_direct_port: int = 5432
# NFS
nfs_server: str = "192.168.123.14"
nfs_productions_path: str = "/mnt/umbra-data/productions"
nfs_artifacts_path: str = "/mnt/umbra-data/artifacts"
nfs_covers_path: str = "/mnt/umbra-data/covers"
nfs_audio_path: str = "/mnt/umbra-data/audio"
# Local mount points
local_mount_root: Path = Path("/mnt/umbra")
# Compute node
node_id: str = "umbra-audio-01"
organization_id: str = "umbra"
node_name: str = ""
datacenter: str = "local"
agent_version: str = "1.0.0"
# Credentials
node_credential_prefix: str = "umbra-audio-01-"
node_scopes: tuple[str, ...] = ("queue:claim", "queue:complete", "artifacts:write")
# Job polling
poll_interval_seconds: int = 10
lease_duration_minutes: int = 30
allowed_job_kinds: tuple[str, ...] = (
"tts", "asr", "assemble", "encode", "master", "speaker_profile",
)
# Heartbeat
heartbeat_interval_seconds: int = 30
heartbeat_timeout_minutes: int = 2
@property
def pg_dsn(self) -> str:
"""PostgreSQL DSN for psycopg/asyncpg."""
return (
f"postgresql://{self.pg_user}:{self.pg_password}@{self.pg_host}:{self.pg_port}/"
f"{self.pg_database}?sslmode={self.pg_sslmode}&application_name={self.pg_app_name}"
)
@property
def pg_direct_dsn(self) -> str:
"""Direct PostgreSQL DSN (port 5432)."""
return (
f"postgresql://{self.pg_user}:{self.pg_password}@{self.pg_host}:{self.pg_direct_port}/"
f"{self.pg_database}?sslmode={self.pg_sslmode}&application_name={self.pg_app_name}-direct"
)
@property
def nfs_mounts(self) -> dict[str, tuple[str, Path]]:
"""NFS exports → local mount points."""
server = self.nfs_server
root = self.local_mount_root
return {
"productions": (
f"{server}:{self.nfs_productions_path}",
root / "productions",
),
"artifacts": (
f"{server}:{self.nfs_artifacts_path}",
root / "artifacts",
),
"covers": (
f"{server}:{self.nfs_covers_path}",
root / "covers",
),
"audio": (
f"{server}:{self.nfs_audio_path}",
root / "audio",
),
}
class ClusterSettings(BaseSettings):
"""Settings loaded from environment (.env)."""
model_config = SettingsConfigDict(env_prefix="UMBRA_CLUSTER__", env_file=".env", extra="ignore")
pg_host: str = "192.168.123.14"
pg_port: int = 6432
pg_database: str = "umbratrans"
pg_user: str = "umbraudio"
pg_password: str = ""
pg_sslmode: str = "require"
pg_direct_port: int = 5432
nfs_server: str = "192.168.123.14"
nfs_productions_path: str = "/mnt/umbra-data/productions"
nfs_artifacts_path: str = "/mnt/umbra-data/artifacts"
nfs_covers_path: str = "/mnt/umbra-data/covers"
nfs_audio_path: str = "/mnt/umbra-data/audio"
local_mount_root: Path = Path("/mnt/umbra")
node_id: str = "umbra-audio-01"
organization_id: str = "umbra"
datacenter: str = "local"
agent_version: str = "1.0.0"
node_credential_prefix: str = "umbra-audio-01-"
node_scopes: str = "queue:claim,queue:complete,artifacts:write"
poll_interval_seconds: int = 10
lease_duration_minutes: int = 30
allowed_job_kinds: str = "tts,asr,assemble,encode,master,speaker_profile"
heartbeat_interval_seconds: int = 30
heartbeat_timeout_minutes: int = 2
def load_cluster_config(project_dir: Path | None = None) -> ClusterConfig:
"""Load cluster configuration from environment and defaults."""
settings = ClusterSettings()
# Parse comma-separated strings
scopes = tuple(s.strip() for s in settings.node_scopes.split(",") if s.strip())
job_kinds = tuple(k.strip() for k in settings.allowed_job_kinds.split(",") if k.strip())
# Node name defaults to hostname if not set
node_name = settings.node_id # Could be overridden
return ClusterConfig(
pg_host=settings.pg_host,
pg_port=settings.pg_port,
pg_database=settings.pg_database,
pg_user=settings.pg_user,
pg_password=settings.pg_password,
pg_sslmode=settings.pg_sslmode, # type: ignore[assignment]
pg_direct_port=settings.pg_direct_port,
nfs_server=settings.nfs_server,
nfs_productions_path=settings.nfs_productions_path,
nfs_artifacts_path=settings.nfs_artifacts_path,
nfs_covers_path=settings.nfs_covers_path,
nfs_audio_path=settings.nfs_audio_path,
local_mount_root=settings.local_mount_root,
node_id=settings.node_id,
organization_id=settings.organization_id,
node_name=node_name,
datacenter=settings.datacenter,
agent_version=settings.agent_version,
node_credential_prefix=settings.node_credential_prefix,
node_scopes=scopes,
poll_interval_seconds=settings.poll_interval_seconds,
lease_duration_minutes=settings.lease_duration_minutes,
allowed_job_kinds=job_kinds,
heartbeat_interval_seconds=settings.heartbeat_interval_seconds,
heartbeat_timeout_minutes=settings.heartbeat_timeout_minutes,
)