1. What it is
When an author is building a training module and would rather talk than type, they hold the mic button in the creation chat and speak; stt-service turns that recording into text for the prompt box. It runs a Whisper model on ordinary CPU inside the cluster, so the audio never leaves Oper's infrastructure and a dictation costs nothing per call.
| Who uses it | authoring-service-v2 only, on behalf of an admin author. No client talks to it directly — the browser posts to authoring-v2's /v1/admin/authoring/transcriptions, which proxies here. |
| Runtime | FastAPI, single module (app/main.py) · Deployment stt-service, replicas: 1, 4 CPU limit / ~1.8 GiB request (k8s/deployment.yaml:22, :29, :96-100) |
| Database | None — stateless. |
| Redis | None. |
| NATS streams | None. It neither consumes nor publishes. |
| External APIs | None. faster-whisper small, int8, weights baked into the image (README.md:27). |
| Entry points | 3 HTTP routes · 0 NATS consumers · 1 background task (the model load) |
2. Feature map
flowchart LR AUTHOR["Admin author (mic button)"] --> FE["authoring-v2 /v1/admin/authoring/transcriptions"] FE --> T["Transcribe an utterance"] T --> R1["POST /v1/internal/transcriptions"] K8S["kubelet probes"] --> H["Report health"] H --> R2["GET /health/live"] H --> R3["GET /health/ready"] BOOT["Pod start"] --> LOAD["Load the Whisper model"] LOAD --> R3
3. Features
Internal (other services)
Turn a spoken utterance into text live
The author speaks a sentence or two into the creation chat; the recording arrives here as raw bytes and comes back as a transcript, with the detected language and the clip length. A typical 20-second dictation lands in about three seconds. Clips that are too long, too large or not decodable audio are refused with a clear verdict the author sees, rather than a generic error. A second recording waits its turn instead of halving the speed of both.
- Entry points
POST /v1/internal/transcriptions(optional?language=hint;X-Oper-Key)- Touches
- Nothing persistent — audio is decoded in memory and discarded
- Related
- Caps:
STT_MAX_DURATION_SECONDS=120,STT_MAX_BODY_BYTES=26214400,STT_MAX_CONCURRENCY=2(k8s/configmap.yaml) - Evidence
- app/main.py:104 · caller authoring-service-v2/app/core/http.py:750, reached from authoring-service-v2/app/routes/transcription_routes.py:36 (
POST /v1/admin/authoring/transcriptions, mounted at authoring-service-v2/app/main.py:255)
Withhold traffic until the model is actually loaded live
Weights take a while to become resident. Liveness answers immediately so the container is never killed while warming up; readiness stays negative until the model is in memory, so Kubernetes keeps the pod out of the load balancer and no author ever hits a half-started service. A model that fails to load leaves the pod permanently not-ready with the reason in the logs, instead of crash-looping.
- Entry points
GET /health/live·GET /health/ready- Touches
- In-process model state only
- Related
README.md:57("Ready = weights resident")- Evidence
- app/main.py:89, :94 · probes wired at k8s/deployment.yaml:65 (live) and :75 (ready)
Admin
None — no admin-facing route. The FE-facing surface belongs to authoring-service-v2.
Employee (mobile)
None.
Background
Load the Whisper model at startup live
A one-shot task started when the pod boots: it pulls the baked-in weights into memory on a worker thread so the event loop stays responsive, then flips readiness. It is cancelled if the pod shuts down before it finishes.
- Entry points
_load_model- Touches
- Process memory (~1.7 GiB warm at 4 threads,
README.md:35-40) - Related
- Skipped entirely when
STT_SKIP_MODEL_LOADis set, which is how the unit tests run - Evidence
- app/main.py:55, started from the lifespan at app/main.py:68
4. API reference
| Method | Path | Auth | Feature | Callers | Verdict |
|---|---|---|---|---|---|
| POST | /v1/internal/transcriptions | X-Oper-Key (skipped when no key is configured, i.e. local dev) | Transcribe | authoring-service-v2/app/core/http.py:750 | live |
| GET | /health/live | none | Health | kubelet — k8s/deployment.yaml:65 | live |
| GET | /health/ready | none | Health | kubelet — k8s/deployment.yaml:75 | live |
5. Async contracts
Consumes
| Subject | Stream | Durable | Published by | Feature | Verdict |
|---|---|---|---|---|---|
| None. stt-service has no NATS client at all. | |||||
Publishes
| Subject | Consumed by | Feature | Verdict |
|---|---|---|---|
None. Transcriptions write no usage events and debit no credits — the model is self-hosted, so there is no per-call cost to meter (authoring-service-v2/app/core/config.py:71). | |||
Background jobs
| Job | Schedule | What it does | Verdict |
|---|---|---|---|
_load_modelapp/main.py:55 | Once, at pod start | Loads the faster-whisper weights on a worker thread; readiness flips when it completes | live |
6. Data it owns
None — stt-service is stateless. No Postgres database, no Redis, no bucket, no local persistence. Audio is decoded in memory and discarded once the transcript is returned; the only file on disk is the model baked into the container image. Nothing else in the platform reads or writes state on its behalf.
7. Dependencies
flowchart LR FE["Admin browser (mic button)"] --> ASV2["authoring-service-v2"] ASV2 --> STT["stt-service"] K8S["kubelet probes"] --> STT STT --> MODEL["faster-whisper small int8 (in image)"]
No outbound calls. Not listed in ARCHITECTURE.md §3.2's 27 HTTP edges — that table
predates this service; the edge is authoring-v2 → stt-service via
STT_SERVICE_URL (authoring-service-v2/app/core/config.py:80, default
http://stt-service with no port, deliberately: the Service is port 80 → targetPort
8000).
8. Dead-code verdicts
Every entry point with no in-repo caller. Deleting is a separate decision — see the hub roll-up.
| Entry point | Kind | Verdict | Evidence |
|---|---|---|---|
| None. All three routes and the one background task have a named caller: the business route is called by authoring-service-v2, both probes are wired in the Deployment, and the model load is started by the app lifespan. | |||
9. Sources
stt-service/README.md— endpoint contract, error codes, model sizing and the measured thread-scaling curve that fixes the CPU limit.stt-service/app/main.py— the whole service; module docstring states the caller and the stateless design.stt-service/k8s/— Deployment (probes, resources), Service, ConfigMap (model and cap knobs).- Caller verification:
authoring-service-v2/app/core/http.py:715-775,authoring-service-v2/app/routes/transcription_routes.py,authoring-service-v2/app/core/config.py:69-80.