solutions/databricks/solution-accelerator/00_config.py

00_config.py: full Python source from the solution-accelerator folder of Spatial9 Agent Skills. Free, MIT licensed code for immersive audio with the Spatial9...

Raw file · View on GitHub · Download every file as a zip

# Databricks notebook source
# MAGIC %md
# MAGIC # Spatial9 Solutions Accelerator: shared settings
# MAGIC Every other notebook starts with `%run ./00_config`. Change the widgets, not the code.

# COMMAND ----------

import json
import os
import re
import sys
from datetime import datetime, timezone

dbutils.widgets.text("catalog", "main")                    # noqa: F821 - provided by Databricks
dbutils.widgets.text("schema", "spatial9_accelerator")     # noqa: F821
dbutils.widgets.text("secret_scope", "spatial9")           # noqa: F821

CATALOG = dbutils.widgets.get("catalog")                   # noqa: F821
SCHEMA = dbutils.widgets.get("schema")                     # noqa: F821
SCOPE = dbutils.widgets.get("secret_scope")                # noqa: F821

# Catalog and schema names go into SQL as identifiers, so only allow plain names.
for name in (CATALOG, SCHEMA):
    if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]{0,127}", name):
        raise ValueError(f"Not a valid Unity Catalog name: {name!r}")

NS = f"{CATALOG}.{SCHEMA}"
QUEUE = f"{NS}.render_queue"          # one row per track version and route: the source of truth
EVENTS = f"{NS}.render_events"        # append-only audit trail
MASTERS = f"/Volumes/{CATALOG}/{SCHEMA}/masters"         # drop WAV, FLAC or AIFF masters here
STEMS = f"/Volumes/{CATALOG}/{SCHEMA}/stems"             # one folder per track for the REST API route
IMMERSIVE = f"/Volumes/{CATALOG}/{SCHEMA}/immersive"     # finished files land here
CHECKPOINTS = f"/Volumes/{CATALOG}/{SCHEMA}/checkpoints"

MAX_RUNNING = 3        # Spatial9 renders at once, shared by MCP and A2A
DAILY_LIMIT = 20       # renders per 24 hours

# The shared Python clients live two folders up in the Agent Skills repo (solutions/shared).
sys.path.append(os.path.abspath("../../shared"))


def secret(key: str) -> str:
    """Read from the secret scope. Databricks redacts the value in notebook output."""
    return dbutils.secrets.get(SCOPE, key)  # noqa: F821


def now():
    return datetime.now(timezone.utc)


def update(track_id: str, version: int, route: str, **cols) -> None:
    """Parameterised UPDATE, so titles or errors with quotes never break the statement."""
    cols["updated_at"] = now()
    sets = ", ".join(f"{k} = :{k}" for k in cols)
    spark.sql(  # noqa: F821
        f"UPDATE {QUEUE} SET {sets} WHERE track_id = :track_id AND version = :version AND route = :route",
        args={**cols, "track_id": track_id, "version": version, "route": route},
    )


def log(row, event: str, detail: dict | str = "", job_id: str | None = None) -> None:
    """One audit row per state change: who, what, which job, when."""
    spark.sql(  # noqa: F821
        f"INSERT INTO {EVENTS} VALUES (:track_id, :version, :route, :job_id, :event, :detail, current_timestamp())",
        args={
            "track_id": row.track_id, "version": row.version, "route": row.route,
            "job_id": job_id or row.job_id or "", "event": event,
            "detail": detail if isinstance(detail, str) else json.dumps(detail),
        },
    )


def free_slots() -> int:
    """How many new renders may start now, given the 3-at-once and 20-per-day limits."""
    running = spark.table(QUEUE).where("status = 'running' AND route IN ('mcp', 'a2a') AND skill <> 'preview_chorus'").count()  # noqa: F821
    today = spark.sql(  # noqa: F821
        f"SELECT count(*) n FROM {EVENTS} WHERE event = 'started' AND at > current_timestamp() - INTERVAL 24 HOURS"
    ).first().n
    return max(0, min(MAX_RUNNING - running, DAILY_LIMIT - today))


def save(url: str, target: str) -> str:
    """Stream a Spatial9 download link straight into a Volume."""
    import requests
    with requests.get(url, stream=True, timeout=900) as r:
        r.raise_for_status()
        with open(target, "wb") as out:
            for chunk in r.iter_content(1 << 20):
                out.write(chunk)
    return target