feat: SaluteSpeech STT for the pilot + no foreign STT outside the demo (ADR-016)
CI / ruff · pytest (pull_request) Successful in 2m16s

Refs #121

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 17:11:44 +03:00
co-authored by Claude Opus 5.5
parent b70911e182
commit f19ea33937
6 changed files with 317 additions and 13 deletions
+20 -3
View File
@@ -42,12 +42,18 @@ class Settings(BaseSettings):
chat_agent_timeout_s: float = 25.0
chat_agent_max_steps: int = 5
# Voice input (#116): speech-to-text for the patient's messages. `auto` = OpenRouter if a key is set, else mock.
# OpenRouter STT is synthetic data only (ADR-014): the audio leaves the RF and cannot be scrubbed.
stt_provider: Literal["auto", "off", "mock", "openrouter"] = "auto"
# Voice input (#116, #121): speech-to-text for the patient's messages. `auto` = SaluteSpeech if credentials are
# set; otherwise, in the demo only, OpenRouter if a key is set, else mock. OpenRouter STT is synthetic data only
# (ADR-014): the audio leaves the RF and cannot be scrubbed — forbidden with DEMO_CLOCK=false (validator below).
stt_provider: Literal["auto", "off", "mock", "openrouter", "salute"] = "auto"
stt_model: str = "google/gemini-2.5-flash" # needs audio input
stt_timeout_s: float = 30.0
stt_max_seconds: int = Field(default=60, ge=5, le=300)
# SaluteSpeech (Sber, RF contour) — the pilot provider (#121, ADR-016). Credentials = the base64 authorization key.
salute_speech_credentials: SecretStr = SecretStr("")
salute_speech_scope: str = "SALUTE_SPEECH_PERS"
# Russian Trusted Root CA (Минцифры) is not in certifi: point to the .crt/.pem to verify Sber's TLS chain.
salute_speech_ca: Path | None = None
# Telegram is used only for the prototype on synthetic data; prod channel is MAX / Gosuslugi.
tg_bot_token: str = ""
@@ -65,6 +71,17 @@ class Settings(BaseSettings):
max_ca_bundle: Path | None = None
max_link_ttl_seconds: int = Field(default=600, ge=60, le=1800)
@model_validator(mode="after")
def validate_stt(self) -> "Settings":
if self.stt_provider == "openrouter" and not self.demo_clock:
raise ValueError(
"STT_PROVIDER=openrouter is for synthetic data only (ADR-014) and is forbidden with DEMO_CLOCK=false: "
"use STT_PROVIDER=salute"
)
if self.salute_speech_ca is not None and not self.salute_speech_ca.is_file():
raise ValueError("SALUTE_SPEECH_CA must point to an existing CA certificate file")
return self
@model_validator(mode="after")
def validate_max(self) -> "Settings":
if not self.max_enabled: