deploy: add apex-simulation host stack (Traefik + PiperTTS + voice extras)

Stand up a second deploy target on apex-simulation dedicated to
"simulation"-style supplier capabilities, starting with CPU-only PiperTTS
served via openedai-speech-min behind Traefik on :80.

- deploy/simulation/docker-compose.traefik.yml: Traefik latest with
  introspection API, settles on Docker daemon 29.x API negotiation
- deploy/simulation/docker-compose.tts-piper.yml: openedai-speech-min
  with curl-based healthcheck (image lacks wget, which Docker would have
  marked unhealthy and Traefik would filter from the docker provider)
- deploy/simulation/deploy.sh: rsync-based deploy mirroring the
  apex-dashboard pattern; selective --traefik-only / --tts-only flags
- deploy/simulation/install-voice-extras.sh + voice-mappings.py:
  idempotent post-deploy step that downloads en_US-lessac-high voice
  files into the bind-mount and registers a "lessac" alias in
  voice_to_speaker.yaml; survives a host wipe without manual steps
- deploy/simulation/README.md: full host runbook
- .gitignore: exclude data/ recursively, *.log, .vitest-cache, .DS_Store,
  __pycache__/, *.pyc, docs/*.zip
This commit is contained in:
jelisaveta-talbot
2026-05-06 18:59:20 +02:00
parent cbab81efbd
commit 3f3bc72680
7 changed files with 531 additions and 1 deletions

View 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-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-tts-piper:/tmp/voice-mappings.py
# docker exec marketplace-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())