stt-service — business features

Speech to text for the creation-chat mic button · ← platform hub · entry points verified against tools/feature-docs/out/stt-service.json

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 itauthoring-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.
RuntimeFastAPI, single module (app/main.py) · Deployment stt-service, replicas: 1, 4 CPU limit / ~1.8 GiB request (k8s/deployment.yaml:22, :29, :96-100)
DatabaseNone — stateless.
RedisNone.
NATS streamsNone. It neither consumes nor publishes.
External APIsNone. faster-whisper small, int8, weights baked into the image (README.md:27).
Entry points3 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

internal

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

internal

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

background

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_LOAD is 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

MethodPathAuthFeatureCallersVerdict
POST/v1/internal/transcriptionsX-Oper-Key (skipped when no key is configured, i.e. local dev)Transcribeauthoring-service-v2/app/core/http.py:750live
GET/health/livenoneHealthkubelet — k8s/deployment.yaml:65live
GET/health/readynoneHealthkubelet — k8s/deployment.yaml:75live

5. Async contracts

Consumes

SubjectStreamDurablePublished byFeatureVerdict
None. stt-service has no NATS client at all.

Publishes

SubjectConsumed byFeatureVerdict
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

JobScheduleWhat it doesVerdict
_load_model
app/main.py:55
Once, at pod startLoads the faster-whisper weights on a worker thread; readiness flips when it completeslive

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 pointKindVerdictEvidence
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