From d3d37a15ecf3c2e8059ca435fc73006b472561c2 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 12 Jun 2026 00:25:35 +0000 Subject: [PATCH] =?UTF-8?q?tts:=20GPU-gated=20live=20narration=20=E2=80=94?= =?UTF-8?q?=20demand-gate=20CronJob=20+=20all-day=20VRAM=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Viktor asked 'can't we make it live? why the cronjob?' — the overnight window guaranteed VRAM room on the shared T4, but immich/frigate models idle-unload during the day so the card often has room (measured 10.3 GiB free at 01:20). New 'demand' action every 3 min: scale Chatterbox up when tripit's audio queue is non-empty AND free VRAM >= floor; idle it back to 0 when the queue empties (also frees the card early inside the nightly window). Failed metrics scrape fail-safes to no-scale-up, same as the window preflight. The guard moves to all-day */5 — live synthesis can hold the card at any hour, so the yield-on-pressure watchdog must watch at any hour. tripit exposes the unauthenticated in-cluster queue count; a 404 from an older image reads as queued=0 (no-op). The 02:00 window-up stays as the guaranteed nightly catch-up. Co-Authored-By: Claude Fable 5 --- stacks/tts/main.tf | 50 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/stacks/tts/main.tf b/stacks/tts/main.tf index 609e49b1..0f0bd215 100644 --- a/stacks/tts/main.tf +++ b/stacks/tts/main.tf @@ -44,9 +44,12 @@ variable "offpeak_window_down_schedule" { } variable "offpeak_guard_schedule" { - type = string - default = "*/5 2-5 * * *" # every 5 min inside the 02:00–06:00 window - description = "Cron schedule for the mid-window guard that yields the card if free VRAM drops." + type = string + # ALL-DAY since the demand gate (2026-06-12): live synthesis can hold the + # card at any hour, so the yield-on-VRAM-pressure guard must watch at any + # hour too. A guard tick while replicas=0 is a no-op. + default = "*/5 * * * *" + description = "Cron schedule for the guard that yields the card if free VRAM drops below the floor." } locals { @@ -106,8 +109,8 @@ locals { # treat conservatively (skip scale-up). if ! BODY="$(curl -sf -m 10 "$${METRICS_URL}")"; then echo "WARN: could not scrape $${METRICS_URL}" - if [ "$${ACTION}" = "up" ]; then - echo "preflight: scrape failed -> NOT scaling up (fail-safe)"; exit 0 + if [ "$${ACTION}" = "up" ] || [ "$${ACTION}" = "demand" ]; then + echo "$${ACTION}: scrape failed -> NOT scaling up (fail-safe)"; exit 0 fi # For down/guard a failed scrape must NOT block yielding the card. BODY="" @@ -139,6 +142,29 @@ locals { echo "window end -> scaling chatterbox-tts to 0" kubectl -n tts scale deploy/chatterbox-tts --replicas=0 ;; + demand) + # GPU-gated LIVE narration (tripit#24 amendment, 2026-06-12): scale up + # whenever tripit has audio waiting AND the card has room; idle back + # down when the queue empties (even inside the nightly window — done is + # done, free the card early). The 02:00 window-up stays the guaranteed + # nightly catch-up for days the daytime card never had room. + QUEUED="$(curl -sf -m 10 "$${QUEUE_URL}" \ + | sed -n 's/.*"queued"[^0-9]*\([0-9][0-9]*\).*/\1/p')" || QUEUED="" + QUEUED="$${QUEUED:-0}" + REPLICAS="$(kubectl -n tts get deploy/chatterbox-tts -o jsonpath='{.spec.replicas}')" + echo "demand: queued=$${QUEUED} replicas=$${REPLICAS}" + if [ "$${QUEUED}" -gt 0 ] && [ "$${REPLICAS}" = "0" ]; then + if [ "$${FREE}" -ge "$${FLOOR}" ]; then + echo "demand: audio waiting + room on the card -> scaling chatterbox-tts to 1" + kubectl -n tts scale deploy/chatterbox-tts --replicas=1 + else + echo "demand: audio waiting but free < floor -> staying down (nightly window catches up)" + fi + elif [ "$${QUEUED}" -eq 0 ] && [ "$${REPLICAS}" != "0" ]; then + echo "demand: queue empty -> idling chatterbox-tts back to 0" + kubectl -n tts scale deploy/chatterbox-tts --replicas=0 + fi + ;; esac EOT @@ -159,7 +185,17 @@ locals { schedule = var.offpeak_guard_schedule action = "guard" } + # GPU-gated live narration: every 3 min, scale up when tripit's audio queue + # is non-empty and the VRAM preflight passes; idle down when it empties. + chatterbox-demand-gate = { + schedule = "*/3 * * * *" + action = "demand" + } } + + # tripit's unauthenticated in-cluster queue probe (count only, non-sensitive). + # A 404 from an older tripit image yields QUEUED=0 -> the gate no-ops. + tripit_queue_url = "http://tripit.tripit.svc.cluster.local:8080/api/tour/tts-queue" } resource "kubernetes_namespace" "tts" { @@ -463,6 +499,10 @@ resource "kubernetes_cron_job_v1" "offpeak" { name = "GPU_TOTAL" value = tostring(var.gpu_total_bytes) } + env { + name = "QUEUE_URL" + value = local.tripit_queue_url + } resources { requests = { cpu = "20m", memory = "64Mi" } limits = { memory = "128Mi" }