# 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:v202604281434 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//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