feat: Abschreiben im Cluster mit Whisper-Image und Voice-CronJob (T7 L3 Task 13)
Backend Image / build-and-push (push) Successful in 5m49s
Frontend Image / build-and-push (push) Successful in 13m3s
Manifests / validate (push) Successful in 17s
Whisper Image / build-and-push (push) Successful in 1m56s

ApiCore-Whisper: transcribe.py liest das Manifest im RAM-Arbeitsordner,
dekodiert mit festem Demuxer je Endung, misst die Länge vor dem
Abschreiben (TOO_LONG > 240 s, DECODE_FAILED bei Länge 0), sperrt das
Netz vor dem Modell-Laden und schreibt nur Zähler auf stdout. Image:
python:3.12-slim per Digest, Nutzer 1000, alle Pakete mit Hash
(faster-whisper 1.2.1, av 18.1.0 wegen Inkompatibilität mit av 19),
Modell faster-whisper-small auf fester Revision mit SHA-256 je Datei.
CI-Workflow nur bei Änderungen unter ApiCore-Whisper/, Actions per SHA.

deploy/k8s/components/voice: CronJob alle 2 Minuten mit fetch, whisper,
ingest (gleiche UID, getrennte /tmp-Volumes, damit der Decoder dem
Container mit den Secrets nichts unterschieben kann; whisper ohne
Secrets und Umgebungsvariablen außer WHISPER_CPU_THREADS), Overlay mit
Platzhalter-Digest, ConfigMap-Werte für Sprache, Policy-Tests (55).
Die Anbindung des Anthropic-Schlüssels bleibt offen (Ruling 9).

Refs #27

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-16 11:12:59 +02:00
1 parent e614ded967
commit c53810b8a6
16 files changed
+1448 -5

No files matched your search

@@ -0,0 +1,165 @@
# Abschreiben im Cluster (Spec T7 6.3 und 11.1): alle 2 Minuten ein Pod mit
# drei Containern, die nacheinander laufen:
# fetch (initContainer) claimt Aufnahmen, entschluesselt, legt Ton nach /work/in
# whisper (initContainer) liest /work/in, schreibt /work/out; ohne Netz,
# ohne Schluessel, ohne Datenbank-Zugang
# ingest (Hauptcontainer) liest /work/out, verschluesselt, speichert, loescht Ton
#
# Der gemeinsame CronJob-Patch aus components/backend-roles trifft alle
# CronJobs mit app=apicore-job und wuerde diesem Pod einen Container `job`
# (Backend-Image, alle Secrets) hinzufuegen. Darum traegt das CronJob-Objekt
# app=apicore-voice-job; nur das Pod-Template traegt app=apicore-job (damit die
# NetworkPolicy apicore-jobs gilt: Egress nur Postgres und DNS). Haertung,
# Zeitzone und Forbid stehen deshalb hier vollstaendig.
#
# fetch, whisper und ingest laufen mit derselben uid 1000 und fsGroup 1000:
# /work/in und /work/out sind 0700, die Dateien 0600.
apiVersion: batch/v1
kind: CronJob
metadata:
name: apicore-job-voice-transcribe
labels:
app: apicore-voice-job
apicore.io/job: voice-transcribe
spec:
schedule: "*/2 * * * *"
timeZone: Europe/Berlin
concurrencyPolicy: Forbid
startingDeadlineSeconds: 120
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 0
activeDeadlineSeconds: 1500
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
app: apicore-job
apicore.io/job: voice-transcribe
db-client: "true"
spec:
restartPolicy: Never
automountServiceAccountToken: false
enableServiceLinks: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
volumes:
# Entschluesselter Ton und Abschriften liegen nur im RAM, nie auf
# einer Platte. Das Backend rechnet mit diesen 32Mi (work-dir.ts).
- name: work
emptyDir:
medium: Memory
sizeLimit: 32Mi
# /tmp von fetch und ingest (HOME=/tmp). Whisper bekommt ein eigenes
# Volume: ein gemeinsames /tmp liesse einen kompromittierten
# Decoder Code unter $HOME/.node_modules ablegen, den ingest mit
# allen Secrets laedt.
- name: tmp
emptyDir:
sizeLimit: 64Mi
- name: whisper-tmp
emptyDir:
medium: Memory
sizeLimit: 16Mi
initContainers:
- name: fetch
image: git.encry.de/nick.rosenloecher/apicore-backend
imagePullPolicy: IfNotPresent
command: ["node", "dist/cli.js", "voice-transcribe", "--phase=fetch"]
env:
- name: APICORE_ROLE
value: job
- name: HOME
value: /tmp
envFrom:
- configMapRef:
name: apicore-backend-config
- secretRef:
name: apicore-backend-secret
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: work
mountPath: /work
- name: tmp
mountPath: /tmp
resources:
requests:
cpu: 50m
memory: 192Mi
limits:
cpu: 500m
memory: 512Mi
# Kein envFrom, kein secretKeyRef, kein Service-Account-Token. Nur
# die Thread-Zahl kommt von aussen; alles andere steht im Image.
- name: whisper
image: git.encry.de/nick.rosenloecher/apicore-whisper
imagePullPolicy: IfNotPresent
env:
- name: WHISPER_CPU_THREADS
value: "2"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
volumeMounts:
# Der ganze Mount, Schreibrecht fuer /work/out.
- name: work
mountPath: /work
- name: whisper-tmp
mountPath: /tmp
resources:
requests:
cpu: 50m
memory: 1280Mi
limits:
cpu: 2000m
memory: 1280Mi
containers:
- name: ingest
image: git.encry.de/nick.rosenloecher/apicore-backend
imagePullPolicy: IfNotPresent
command: ["node", "dist/cli.js", "voice-transcribe", "--phase=ingest"]
env:
- name: APICORE_ROLE
value: job
- name: HOME
value: /tmp
envFrom:
- configMapRef:
name: apicore-backend-config
- secretRef:
name: apicore-backend-secret
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: work
mountPath: /work
- name: tmp
mountPath: /tmp
resources:
requests:
cpu: 50m
memory: 192Mi
limits:
cpu: 500m
memory: 512Mi