deploy/mainnet: add PiperTTS (private) + supplier-tts (audio.synthesize.piper.v1)
Mirrors deploy/testnet/docker-compose.supplier-tts.yml and the simulation PiperTTS stack, co-located on vector-marketplace as a private internal service on marketplace-mainnet-net (no Traefik labels, no public DNS for Piper itself). Supplier reaches Piper at http://marketplace-mainnet-tts-piper:8000. Voice: en_US-lessac-high. New supplier wallet + on-chain advert are published out-of-band by the operator per the runbook header. Files: - deploy/mainnet/docker-compose.tts-piper.yml openedai-speech-min, private - deploy/mainnet/docker-compose.supplier-tts.yml CAPABILITY_KIND=tts supplier - deploy/mainnet/install-voice-extras.sh voice download + yaml mapping - deploy/mainnet/voice-mappings.py idempotent yaml editor - supplier/.env.tts.example operator env template - .gitignore whitelist .env.*.example Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
81
deploy/mainnet/docker-compose.supplier-tts.yml
Normal file
81
deploy/mainnet/docker-compose.supplier-tts.yml
Normal file
@@ -0,0 +1,81 @@
|
||||
# deploy/mainnet/docker-compose.supplier-tts.yml — TTS Supplier — MAINNET
|
||||
#
|
||||
# Third mainnet supplier, dedicated to capability `audio.synthesize.piper.v1`.
|
||||
# Reaches the colocated PRIVATE PiperTTS container at the docker-DNS name
|
||||
# `marketplace-mainnet-tts-piper:8000`. Traefik publishes it at
|
||||
# `mp-suppliers-tts.vector.apexfusion.org`. Coexists with the ollama-backed
|
||||
# (docker-compose.supplier.yml) and ChatGPT-backed (docker-compose.supplier-gpt.yml)
|
||||
# suppliers.
|
||||
#
|
||||
# Pre-flight networks (must already exist):
|
||||
# docker network create marketplace-mainnet-net # one-shot
|
||||
# # traefik-net is created by the Traefik project
|
||||
#
|
||||
# Pre-flight services:
|
||||
# docker compose -f docker-compose.tts-piper.yml up -d
|
||||
# bash deploy/mainnet/install-voice-extras.sh # one-shot voice install
|
||||
#
|
||||
# Bring up:
|
||||
# docker compose -f deploy/mainnet/docker-compose.supplier-tts.yml up -d --build
|
||||
|
||||
name: marketplace-mainnet-supplier-tts
|
||||
|
||||
services:
|
||||
supplier:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: supplier/Dockerfile
|
||||
container_name: marketplace-mainnet-supplier-tts
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
# Self-contained env for the TTS variant — kept separate from the
|
||||
# canonical ../../supplier/.env (ollama-backed) and ../../supplier/.env.gpt
|
||||
# (ChatGPT-backed). Holds this supplier's distinct SUPPLIER_PRIV_KEY_HEX,
|
||||
# ADVERT_REF, ADVERT_*, and the shared mainnet plumbing duplicated
|
||||
# in full (NETWORK_ID, OGMIOS_URL, VECTOR_ZERO_TIME_MS, ref-script
|
||||
# UTxOs, LIVE_CHAIN). No env_file ordering / override magic.
|
||||
- path: ../../supplier/.env.tts
|
||||
required: false
|
||||
environment:
|
||||
CAPABILITY_KIND: "tts"
|
||||
# Internal docker-DNS — Piper is private on marketplace-mainnet-net,
|
||||
# no public hostname for it.
|
||||
PIPER_URL: "http://marketplace-mainnet-tts-piper:8000"
|
||||
PORT: "8080"
|
||||
# Enable LiveOgmiosProvider so submitTx/awaitTx actually broadcast.
|
||||
# Mirrors the other mainnet supplier composes; without this the
|
||||
# supplier silently falls back to ReadOnlyOgmiosProvider and
|
||||
# Claim/Submit never reach chain.
|
||||
LIVE_CHAIN: "1"
|
||||
# NETWORK_ID and OGMIOS_URL intentionally not pinned here — they MUST
|
||||
# come from the operator-provided supplier/.env.tts on a mainnet host.
|
||||
# Setting them via compose would be a footgun: a stray re-up on
|
||||
# testnet hardware could silently re-tag the supplier as mainnet.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- marketplace-mainnet-net
|
||||
- traefik-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-net"
|
||||
- "traefik.http.routers.mp-suppliers-tts-mainnet.entrypoints=web"
|
||||
- "traefik.http.routers.mp-suppliers-tts-mainnet.rule=Host(`mp-suppliers-tts.vector.apexfusion.org`)"
|
||||
- "traefik.http.services.mp-suppliers-tts-mainnet.loadbalancer.server.port=8080"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
marketplace-mainnet-net:
|
||||
external: true
|
||||
name: marketplace-mainnet-net
|
||||
traefik-net:
|
||||
external: true
|
||||
name: traefik-net
|
||||
61
deploy/mainnet/docker-compose.tts-piper.yml
Normal file
61
deploy/mainnet/docker-compose.tts-piper.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
# deploy/mainnet/docker-compose.tts-piper.yml — PiperTTS (PRIVATE) — MAINNET
|
||||
#
|
||||
# OpenAI-shape /v1/audio/speech served by openedai-speech-min, reachable
|
||||
# ONLY from inside marketplace-mainnet-net. No Traefik labels, no public
|
||||
# DNS — the colocated marketplace-mainnet-supplier-tts container hits it at
|
||||
# http://marketplace-mainnet-tts-piper:8000
|
||||
#
|
||||
# Mirrors deploy/simulation/docker-compose.tts-piper.yml, but private:
|
||||
# the simulation host exposes Piper publicly via Traefik+Cloudflare;
|
||||
# mainnet keeps it on-net only since only the colocated TTS supplier
|
||||
# consumes it.
|
||||
#
|
||||
# Pre-flight networks (must already exist):
|
||||
# docker network create marketplace-mainnet-net # one-shot
|
||||
#
|
||||
# Bring up:
|
||||
# docker compose -f deploy/mainnet/docker-compose.tts-piper.yml up -d
|
||||
# bash deploy/mainnet/install-voice-extras.sh # download voices + yaml mapping
|
||||
|
||||
name: marketplace-mainnet-tts-piper
|
||||
|
||||
services:
|
||||
tts-piper:
|
||||
image: ghcr.io/matatonic/openedai-speech-min:latest
|
||||
container_name: marketplace-mainnet-tts-piper
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Pre-load Piper's highest-quality American-English voice so first
|
||||
# /v1/audio/speech doesn't pay the model-download tax. Other voices
|
||||
# can be added via install-voice-extras.sh + voice-mappings.py.
|
||||
PRELOAD_MODEL: en_US-lessac-high
|
||||
PORT: "8000"
|
||||
volumes:
|
||||
# Host bind mounts (not Docker named volumes) so the operator can
|
||||
# ls/du/back up the downloaded Piper voices directly. Paths relative
|
||||
# to THIS compose file, i.e. deploy/mainnet/data/tts-piper/{voices,config}
|
||||
# from the repo root. install-voice-extras.sh creates them.
|
||||
- ./data/tts-piper/voices:/app/voices
|
||||
- ./data/tts-piper/config:/app/config
|
||||
healthcheck:
|
||||
# /v1/models is the OpenAI-shape model list; a 200 here proves the
|
||||
# server is up and at least one voice is loaded. Image ships with
|
||||
# curl (and python3) but NOT wget — using wget here marks the
|
||||
# container unhealthy on every probe.
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost:8000/v1/models >/dev/null || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
networks:
|
||||
- marketplace-mainnet-net # PRIVATE — no traefik-net join
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
marketplace-mainnet-net:
|
||||
external: true
|
||||
name: marketplace-mainnet-net
|
||||
74
deploy/mainnet/install-voice-extras.sh
Executable file
74
deploy/mainnet/install-voice-extras.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# install-voice-extras.sh — runs ON THE HOST (vector-marketplace, as root)
|
||||
# after `docker compose -f deploy/mainnet/docker-compose.tts-piper.yml up -d`
|
||||
# to install Piper voices and yaml mappings beyond what the
|
||||
# openedai-speech-min image ships with.
|
||||
#
|
||||
# Idempotent: every step is a no-op if it already finished previously.
|
||||
# Safe to re-run after a host wipe — a fresh `data/` dir gets the same
|
||||
# voices back.
|
||||
#
|
||||
# Mirrors deploy/simulation/install-voice-extras.sh; differs only in:
|
||||
# - CONTAINER name (mainnet-prefixed)
|
||||
# - resolves VOICES_DIR + voice-mappings.py relative to deploy/mainnet/
|
||||
#
|
||||
# What it does (in order):
|
||||
# 1. Ensure the bind-mount voice files are present (curl -sSL with `[ -s ]` guard).
|
||||
# 2. Copy voice-mappings.py into the container and execute it as root.
|
||||
# The script edits /app/config/voice_to_speaker.yaml (host bind-mount) to
|
||||
# add custom voice aliases. It prints "modified" or "unchanged" — we
|
||||
# restart only on the former.
|
||||
# 3. Restart the container if either step changed something.
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/mainnet/install-voice-extras.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MAINNET_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
VOICES_DIR="$MAINNET_DIR/data/tts-piper/voices"
|
||||
CONTAINER="marketplace-mainnet-tts-piper"
|
||||
|
||||
mkdir -p "$VOICES_DIR"
|
||||
changes=0
|
||||
|
||||
# --- 1. Voice files to install (filename → URL) ---
|
||||
# Add entries here when you want another voice baked into the deploy.
|
||||
# Each .onnx pairs with a .onnx.json — both must be downloaded.
|
||||
voices=(
|
||||
"en_US-lessac-high.onnx|https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/high/en_US-lessac-high.onnx"
|
||||
"en_US-lessac-high.onnx.json|https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/high/en_US-lessac-high.onnx.json"
|
||||
)
|
||||
|
||||
for entry in "${voices[@]}"; do
|
||||
fn="${entry%%|*}"
|
||||
url="${entry##*|}"
|
||||
if [ -s "$VOICES_DIR/$fn" ]; then
|
||||
echo "voice: already installed — $fn"
|
||||
continue
|
||||
fi
|
||||
echo "voice: downloading $fn"
|
||||
# `--fail` so HTTP 4xx/5xx aborts (HF returns html on 404 by default).
|
||||
# Download to a tmp file then mv, so a partial download from a network
|
||||
# blip doesn't leave a half-file that the next run treats as installed.
|
||||
tmp="$VOICES_DIR/.${fn}.partial"
|
||||
curl -sSL --fail --output "$tmp" "$url"
|
||||
mv "$tmp" "$VOICES_DIR/$fn"
|
||||
changes=1
|
||||
done
|
||||
|
||||
# --- 2. Yaml mapping additions inside the container ---
|
||||
docker cp "$MAINNET_DIR/voice-mappings.py" "$CONTAINER:/tmp/voice-mappings.py" >/dev/null
|
||||
mapping_output="$(docker exec "$CONTAINER" python3 /tmp/voice-mappings.py)"
|
||||
echo "mapping: $mapping_output"
|
||||
if [[ "$mapping_output" == modified* ]]; then
|
||||
changes=1
|
||||
fi
|
||||
|
||||
# --- 3. Restart only if anything changed ---
|
||||
if [ "$changes" -eq 1 ]; then
|
||||
echo "→ restarting $CONTAINER to pick up new voices/mappings"
|
||||
docker restart "$CONTAINER" >/dev/null
|
||||
else
|
||||
echo "✓ voice extras already up to date — no restart needed"
|
||||
fi
|
||||
60
deploy/mainnet/voice-mappings.py
Normal file
60
deploy/mainnet/voice-mappings.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
# voice-mappings.py — idempotently apply project-specific voice aliases to
|
||||
# /app/config/voice_to_speaker.yaml inside the marketplace-mainnet-tts-piper container.
|
||||
#
|
||||
# Why this exists:
|
||||
# The openedai-speech-min image populates voice_to_speaker.yaml on first
|
||||
# start with its OWN defaults (alloy/echo/fable/onyx/nova/shimmer mapped
|
||||
# onto the bundled LibriTTS + northern-english-male voices). Voices we
|
||||
# install ourselves (e.g. en_US-lessac-high) need a name in that yaml so
|
||||
# the OpenAI-shape `voice` field can resolve to them. This script adds
|
||||
# those entries — and only those — without disturbing the defaults.
|
||||
#
|
||||
# Idempotency contract:
|
||||
# Reads the yaml, compares each desired entry against what's there, only
|
||||
# rewrites if at least one entry is missing or stale. Prints "modified"
|
||||
# on write, "unchanged" otherwise — install-voice-extras.sh greps for
|
||||
# "modified" to decide whether to restart the container.
|
||||
#
|
||||
# Run via:
|
||||
# docker cp voice-mappings.py marketplace-mainnet-tts-piper:/tmp/voice-mappings.py
|
||||
# docker exec marketplace-mainnet-tts-piper python3 /tmp/voice-mappings.py
|
||||
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
CONFIG_PATH = "/app/config/voice_to_speaker.yaml"
|
||||
|
||||
# Mappings to ensure exist under `tts-1`. Adding more voices later is a
|
||||
# matter of dropping the .onnx + .onnx.json into voices/ (handled by
|
||||
# install-voice-extras.sh) and adding an entry here.
|
||||
DESIRED_TTS1 = {
|
||||
# en_US-lessac-high — high-quality American female single-speaker voice.
|
||||
# Single-speaker model, so no `speaker:` key.
|
||||
"lessac": {"model": "voices/en_US-lessac-high.onnx"},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with open(CONFIG_PATH) as f:
|
||||
cfg = yaml.safe_load(f) or {}
|
||||
|
||||
cfg.setdefault("tts-1", {})
|
||||
|
||||
modified_keys = []
|
||||
for name, mapping in DESIRED_TTS1.items():
|
||||
if cfg["tts-1"].get(name) != mapping:
|
||||
cfg["tts-1"][name] = mapping
|
||||
modified_keys.append(name)
|
||||
|
||||
if modified_keys:
|
||||
with open(CONFIG_PATH, "w") as f:
|
||||
yaml.safe_dump(cfg, f, sort_keys=False)
|
||||
print(f"modified — applied: {', '.join(modified_keys)}")
|
||||
else:
|
||||
print("unchanged — all mappings already present")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user