Files
bluejay-infra/apps/fc-ttsreader/fc-ttsreader.yaml

731 lines
22 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FlowerCore TTS Reader — Text-to-speech book reader service
---
apiVersion: v1
kind: Namespace
metadata:
name: fc-ttsreader
labels:
app.kubernetes.io/part-of: flowercore
---
# 1Password -> K8s Secret sync for TTS Reader API keys
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: ttsreader-secrets
namespace: fc-ttsreader
spec:
itemPath: "vaults/IAmWorkin/items/FlowerCore TTS Reader"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ttsreader-piper
namespace: fc-ttsreader
labels:
app.kubernetes.io/name: ttsreader-piper
app.kubernetes.io/part-of: flowercore
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: ttsreader-piper
template:
metadata:
labels:
app.kubernetes.io/name: ttsreader-piper
app.kubernetes.io/part-of: flowercore
spec:
initContainers:
- name: seed-voices
image: rhasspy/wyoming-piper:latest
command:
- python3
- -c
args:
- |
import shutil
import ssl
from pathlib import Path
from urllib.request import urlopen
ssl._create_default_https_context = ssl._create_unverified_context
files = {
"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",
"en_US-lessac-medium.onnx": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx",
"en_US-lessac-medium.onnx.json": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json",
"en_US-amy-medium.onnx": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/amy/medium/en_US-amy-medium.onnx",
"en_US-amy-medium.onnx.json": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/amy/medium/en_US-amy-medium.onnx.json",
"en_US-john-medium.onnx": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/john/medium/en_US-john-medium.onnx",
"en_US-john-medium.onnx.json": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/john/medium/en_US-john-medium.onnx.json",
"en_GB-cori-high.onnx": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_GB/cori/high/en_GB-cori-high.onnx",
"en_GB-cori-high.onnx.json": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_GB/cori/high/en_GB-cori-high.onnx.json",
}
target = Path("/data")
target.mkdir(parents=True, exist_ok=True)
for name, url in files.items():
path = target / name
if path.exists() and path.stat().st_size > 0:
print(f"cached {name}", flush=True)
continue
print(f"downloading {name}", flush=True)
with urlopen(url, timeout=180) as response, open(path, "wb") as download_file:
shutil.copyfileobj(response, download_file)
print(f"ready {name}", flush=True)
volumeMounts:
- name: data
mountPath: /data
containers:
- name: piper
image: rhasspy/wyoming-piper:latest
env:
- name: PYTHONHTTPSVERIFY
value: "0"
args:
- "--voice"
- "en_US-lessac-high"
- "--data-dir"
- "/data"
- "--download-dir"
- "/data"
ports:
- containerPort: 10200
name: wyoming
# Memory bumped after observed OOMKills during real chapter
# renders 2026-04-25. Piper's eSpeak phonemizer + onnx runtime
# spikes well past 1 Gi on long unpunctuated paragraphs from
# PDF / book imports. 3 Gi gives headroom plus the
# transcribe-audio-to-Quick-Read flow that hits Piper through
# the same model.
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 2000m
memory: 3Gi
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: ttsreader-piper-data
---
# fc-speech-align — cluster-native faster-whisper wrapper.
# Exposes POST /align (fc-align contract used by FlowerCore.Shared.Speech) AND
# POST /transcribe (audio-file-in feature). CPU model = base.en, int8 compute.
# Source: bluejay-infra/apps/fc-ttsreader/speech-align/ (Dockerfile + app.py).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ttsreader-align-models
namespace: fc-ttsreader
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ttsreader-align
namespace: fc-ttsreader
labels:
app.kubernetes.io/name: ttsreader-align
app.kubernetes.io/part-of: flowercore
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: ttsreader-align
template:
metadata:
labels:
app.kubernetes.io/name: ttsreader-align
app.kubernetes.io/part-of: flowercore
spec:
# Bypass CoreDNS's *.iamworkin.lan template hijack on public hosts
# (huggingface.co model download at first boot would otherwise resolve
# to Traefik VIP via search expansion). Drops the iamworkin.lan suffix.
dnsPolicy: None
dnsConfig:
nameservers:
- 10.43.0.10
searches:
- fc-ttsreader.svc.cluster.local
- svc.cluster.local
- cluster.local
options:
- name: ndots
value: "2"
securityContext:
fsGroup: 1654
runAsNonRoot: true
runAsUser: 1654
containers:
- name: align
image: localhost/fc-speech-align:v3
imagePullPolicy: Never
ports:
- containerPort: 9200
name: http
env:
- name: WHISPER_MODEL
value: "Systran/faster-whisper-base.en"
- name: WHISPER_DEVICE
value: "cpu"
- name: WHISPER_COMPUTE_TYPE
value: "int8"
- name: WHISPER_CACHE_DIR
value: "/models"
- name: DEFAULT_LANGUAGE
value: "en"
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
volumeMounts:
- name: models
mountPath: /models
readinessProbe:
httpGet:
path: /health
port: 9200
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 18
livenessProbe:
httpGet:
path: /health
port: 9200
initialDelaySeconds: 180
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
volumes:
- name: models
persistentVolumeClaim:
claimName: ttsreader-align-models
---
apiVersion: v1
kind: Service
metadata:
name: ttsreader-align
namespace: fc-ttsreader
spec:
selector:
app.kubernetes.io/name: ttsreader-align
ports:
- port: 9200
targetPort: 9200
name: http
---
# ttsreader-kokoro — Kokoro-82M TTS via the kokoro-fastapi container.
# Provides high-quality English voices alongside Piper for the TtsReader
# render pipeline AND for AiStation when it talks to the cluster TTS plane
# (instead of pointing back at BLUEJAY-WS:10401). Model + voices ship
# inside the container image, so no PVC is needed.
apiVersion: apps/v1
kind: Deployment
metadata:
name: ttsreader-kokoro
namespace: fc-ttsreader
labels:
app.kubernetes.io/name: ttsreader-kokoro
app.kubernetes.io/part-of: flowercore
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: ttsreader-kokoro
template:
metadata:
labels:
app.kubernetes.io/name: ttsreader-kokoro
app.kubernetes.io/part-of: flowercore
spec:
# Same DNS bypass as ttsreader-align — without it, the *.iamworkin.lan
# CoreDNS template would hijack hexgrad/Kokoro-82M's HuggingFace-style
# repo lookups during model warmup.
dnsPolicy: None
dnsConfig:
nameservers:
- 10.43.0.10
searches:
- fc-ttsreader.svc.cluster.local
- svc.cluster.local
- cluster.local
options:
- name: ndots
value: "2"
containers:
- name: kokoro
image: ghcr.io/remsky/kokoro-fastapi-cpu:latest
ports:
- containerPort: 8880
name: http
resources:
requests:
cpu: 250m
memory: 1Gi
limits:
cpu: 2000m
memory: 3Gi
readinessProbe:
httpGet:
path: /v1/audio/voices
port: 8880
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 18
# Sprint E Phase 1a (kokoro stability) — 4 restarts in 2d6h with
# exit 143 traced to liveness probe `context deadline exceeded` while
# kokoro was busy synthesizing. /v1/audio/voices shares the FastAPI
# worker pool with /v1/audio/speech, so a long synth can starve the
# probe out within the prior 5s × 3 = 15s window. Bump timeoutSeconds
# 5 → 15 and failureThreshold 3 → 5 → 75s grace before kubelet kills
# the pod. The TtsCircuitBreaker on the synthesizer side (Phase 1b)
# backs this up so the FC backend stops slamming kokoro during
# recovery.
livenessProbe:
httpGet:
path: /v1/audio/voices
port: 8880
initialDelaySeconds: 180
periodSeconds: 30
timeoutSeconds: 15
failureThreshold: 5
---
# fc-biblical-tts — eSpeak-NG-backed Ancient Greek + Hebrew TTS with
# word-level timing for read-along playback. Companion to ttsreader-kokoro
# (modern English) and ttsreader-piper (English narrator); operators pick
# whichever engine matches the source text. Source:
# bluejay-infra/apps/fc-ttsreader/biblical-tts/
apiVersion: apps/v1
kind: Deployment
metadata:
name: ttsreader-biblical
namespace: fc-ttsreader
labels:
app.kubernetes.io/name: ttsreader-biblical
app.kubernetes.io/part-of: flowercore
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: ttsreader-biblical
template:
metadata:
labels:
app.kubernetes.io/name: ttsreader-biblical
app.kubernetes.io/part-of: flowercore
spec:
securityContext:
fsGroup: 1654
runAsNonRoot: true
runAsUser: 1654
containers:
- name: biblical-tts
image: localhost/fc-biblical-tts:v1
imagePullPolicy: Never
ports:
- containerPort: 10402
name: http
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1000m
memory: 512Mi
readinessProbe:
httpGet:
path: /health
port: 10402
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
httpGet:
path: /health
port: 10402
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: ttsreader-biblical
namespace: fc-ttsreader
spec:
selector:
app.kubernetes.io/name: ttsreader-biblical
ports:
- port: 10402
targetPort: 10402
name: http
---
# fc-modern-tts — Microsoft Edge Read Aloud bridge for Modern Hebrew
# (he-IL-AvriNeural et al) and Modern Greek (el-GR-NestorasNeural et al).
# Pairs with ttsreader-biblical: biblical engine handles unpointed
# Greek + Hebrew, modern engine handles narrative translations the
# operator reads alongside.
apiVersion: apps/v1
kind: Deployment
metadata:
name: ttsreader-modern
namespace: fc-ttsreader
labels:
app.kubernetes.io/name: ttsreader-modern
app.kubernetes.io/part-of: flowercore
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: ttsreader-modern
template:
metadata:
labels:
app.kubernetes.io/name: ttsreader-modern
app.kubernetes.io/part-of: flowercore
spec:
# edge-tts needs egress to *.tts.speech.microsoft.com — bypass the
# iamworkin.lan template hijack so the lookup doesn't fall back to
# Traefik VIP via search expansion.
dnsPolicy: None
dnsConfig:
nameservers:
- 10.43.0.10
searches:
- fc-ttsreader.svc.cluster.local
- svc.cluster.local
- cluster.local
options:
- name: ndots
value: "2"
securityContext:
fsGroup: 1654
runAsNonRoot: true
runAsUser: 1654
containers:
- name: modern-tts
image: localhost/fc-modern-tts:v1
imagePullPolicy: Never
ports:
- containerPort: 10403
name: http
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1000m
memory: 512Mi
readinessProbe:
httpGet:
path: /health
port: 10403
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
httpGet:
path: /health
port: 10403
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: ttsreader-modern
namespace: fc-ttsreader
spec:
selector:
app.kubernetes.io/name: ttsreader-modern
ports:
- port: 10403
targetPort: 10403
name: http
---
apiVersion: v1
kind: Service
metadata:
name: ttsreader-kokoro
namespace: fc-ttsreader
spec:
selector:
app.kubernetes.io/name: ttsreader-kokoro
ports:
- port: 8880
targetPort: 8880
name: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ttsreader-web
namespace: fc-ttsreader
labels:
app.kubernetes.io/name: ttsreader-web
app.kubernetes.io/part-of: flowercore
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: ttsreader-web
template:
metadata:
labels:
app.kubernetes.io/name: ttsreader-web
app.kubernetes.io/part-of: flowercore
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "5217"
prometheus.io/path: "/metrics"
spec:
securityContext:
fsGroup: 1654
fsGroupChangePolicy: OnRootMismatch
containers:
- name: web
image: localhost/fc-ttsreader-web:v202604281444
imagePullPolicy: Never
ports:
- containerPort: 5217
name: http
env:
- name: ASPNETCORE_URLS
value: "http://+:5217"
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: FlowerCore__Database__ConnectionStrings__Sqlite
value: "Data Source=/data/ttsreader.db"
- name: TtsReader__Audio__OutputRoot
value: "/data/audio"
- name: TtsReader__Audio__FfmpegPath
value: "/usr/bin/ffmpeg"
- name: TtsReader__Bible__CorpusRoot
value: "/data/corpus-cache/world-english-bible/eng/usx"
- name: TtsReader__Jobs__Root
value: "/data/jobs"
- name: TtsReader__Piper__Host
value: "ttsreader-piper.fc-ttsreader.svc.cluster.local."
- name: TtsReader__Piper__Port
value: "10200"
- name: TtsReader__Kokoro__Enabled
value: "true"
- name: TtsReader__Kokoro__BaseUrl
# Cluster-native ttsreader-kokoro Service — replaces the prior
# BLUEJAY-WS host pointer so the render pipeline doesn't need
# the workstation up. AiStation can still hit its local
# http://localhost:8880 instance.
value: "http://ttsreader-kokoro.fc-ttsreader.svc.cluster.local.:8880"
- name: TtsReader__Kokoro__TimeoutSeconds
value: "120"
- name: Speech__Alignment__Enabled
# Cluster-native faster-whisper (Lane F, 2026-04-25). The
# ttsreader-align deployment in this manifest wraps
# SYSTRAN/faster-whisper with a /align endpoint matching the
# FlowerCore.Shared.Speech master contract.
value: "true"
- name: Speech__Alignment__BaseUrl
value: "http://ttsreader-align.fc-ttsreader.svc.cluster.local.:9200"
- name: Speech__Alignment__TimeoutSeconds
value: "120"
# Cluster-native transcription endpoint shares the same pod
# (POST /transcribe). Lane G consumes this from the
# FlowerCore.TtsReader.Web AudioImport feature.
- name: TtsReader__Transcription__Enabled
value: "true"
- name: TtsReader__Transcription__BaseUrl
value: "http://ttsreader-align.fc-ttsreader.svc.cluster.local.:9200"
- name: TtsReader__Transcription__TimeoutSeconds
value: "300"
- name: TtsReader__Ollama__BaseUrl
value: "http://10.0.57.17:11434"
- name: TtsReader__Ollama__DefaultModel
value: "gemma3:4b"
- name: TtsReader__Ollama__TimeoutSeconds
value: "45"
- name: TtsReader__Runtime__LogsRoot
value: "/data/logs"
- name: TtsReader__Runtime__SmokeStatePath
value: "/data/ops/smoke-status.json"
# Sprint E Day 8 voice-preview disk cache — writes WAVs under
# this directory. Default "data/voice-previews" resolves to
# the read-only $HOME path under runAsNonRoot=true. Pin to
# the writable PVC mount.
- name: TtsReader__Preview__CacheDirectory
value: "/data/voice-previews"
# Sprint E XXL Phase 4γ — content-addressed CDN bundle dir for
# POST /api/v1/render. Default "wwwroot/cdn" resolves under the
# read-only app filesystem, so pin to the writable PVC mount
# alongside other TtsReader runtime data. Manifests + cue audio
# land at /data/cdn/sha256/<hash>/manifest.json + cues/.
- name: TtsReader__Render__CdnDirectory
value: "/data/cdn"
- name: Auth__ApiKey
valueFrom:
secretKeyRef:
name: ttsreader-secrets
key: Auth__ApiKey
optional: true
- name: Auth__AdminApiKey
valueFrom:
secretKeyRef:
name: ttsreader-secrets
key: Auth__AdminApiKey
optional: true
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: data
mountPath: /data
- name: tmp
mountPath: /tmp
securityContext:
runAsNonRoot: true
runAsUser: 1654
runAsGroup: 1654
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
readinessProbe:
httpGet:
path: /health
port: 5217
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 5217
initialDelaySeconds: 15
periodSeconds: 30
volumes:
- name: data
persistentVolumeClaim:
claimName: ttsreader-data
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: ttsreader-piper
namespace: fc-ttsreader
spec:
selector:
app.kubernetes.io/name: ttsreader-piper
ports:
- port: 10200
targetPort: 10200
name: wyoming
---
apiVersion: v1
kind: Service
metadata:
name: ttsreader-web
namespace: fc-ttsreader
spec:
selector:
app.kubernetes.io/name: ttsreader-web
ports:
- port: 5217
targetPort: 5217
name: http
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ttsreader-piper-data
namespace: fc-ttsreader
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ttsreader-data
namespace: fc-ttsreader
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 5Gi
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: ttsreader-cert
namespace: fc-ttsreader
spec:
secretName: ttsreader-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- ttsreader.iamworkin.lan
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: ttsreader-web
namespace: fc-ttsreader
spec:
entryPoints:
- websecure
routes:
- match: Host(`ttsreader.iamworkin.lan`)
kind: Rule
services:
- name: ttsreader-web
port: 5217
tls:
secretName: ttsreader-tls