my_voice_assistant_v3_jamulix/BEDIENUNGSANLEITUNG.md
Dieter Schlüter 70c7e2ec0c docs: Top-Konstellation (all-cloud OpenRouter) reproduzierbar + grobe Kosten
- Bedienungsanleitung Teil C4: bewaehrte Konstellation (whisper-large-v3 +
  gemini-3.1-flash-lite + gemini-flash-tts/Zephyr, alle remote), exakte .env-Werte
  und Start-Befehl zum Reproduzieren
- grobe Kostenschaetzung pro Sprech-Runde (~1-2 ct; Treiber = Audio/TTS) mit
  klarem Hinweis: am OpenRouter-Dashboard verifizieren, Preise aendern sich

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 16:10:00 +02:00

453 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Bedienungsanleitung — Voice Assistant Gateway
Schritt-für-Schritt-Anleitung zum Ausprobieren: **mit dem Assistenten sprechen**,
**Einstellungen ändern** und **Praxis-Tests mit Reaktionszeiten**. Technische
Hintergründe: [Architektur-Dokument](Docs/voice-assistant-architecture.md),
Kurzüberblick: [README](README.md).
> **Tipp:** Alle Befehle, die JSON liefern, enden hier auf `| jq` (hübsche, lesbare
> Ausgabe). Dafür `jq` installieren: `sudo apt install jq`. Befehle, die **Audio**
> liefern, schreiben in eine Datei und spielen sie ab (kein `jq`).
> In den Beispielen wird die Adresse als Variable genutzt — einmal setzen, dann überall
> einsetzbar (Port aus deiner `.env`, hier `8003`):
> ```bash
> export URL=http://localhost:8003
> ```
---
# Einrichtung
## 1. Voraussetzungen
- **Python 3.11+** (`python3 --version`)
- **jq** für lesbare JSON-Ausgabe (`sudo apt install jq`)
- Für den Sprech-Loop: **`arecord`** (Paket `alsa-utils`) und ein Player
(`ffplay`/`aplay`/`paplay`) — auf den meisten Linux-Desktops vorhanden
- **OpenRouter-API-Key** — nötig für Profile mit Cloud-KI (`hybrid`, `cloud`);
für rein lokalen Betrieb (`local-dev`) nicht
- Für **lokales STT** (Provider `faster-whisper`): einmalig `pip install -e .[local]`
(lädt beim ersten Lauf ein Whisper-Modell). Für **lokales LLM**: ein laufender
Ollama-Server (`http://127.0.0.1:11434`) mit einem Modell (`ollama pull llama3.2`)
- Optional: Docker
## 2. Installation
```bash
cd voice-assistant-scaffold
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -e .[test]
cp config/voice-assistant.example.toml config/voice-assistant.toml
```
## 3. API-Key hinterlegen (für Cloud/Hybrid)
Der Schlüssel wird **aus der Umgebung** gelesen, nie aus einer Datei:
```bash
echo 'export OPENROUTER_API_KEY=sk-or-v1-DEIN_KEY' >> ~/.bashrc
chmod 600 ~/.bashrc
source ~/.bashrc
echo ${OPENROUTER_API_KEY:0:8} # zeigt nur den Anfang zur Kontrolle
```
> **Sicherheit:** Key nie in `.env`/`config/*.toml`. Bei Leak im OpenRouter-Dashboard
> löschen (= widerrufen) und neu erzeugen.
## 4. Profil (Betriebsart) wählen
| Profil | Bedeutung | Key nötig? |
|-------------|-----------------------------------------|------------|
| `local-dev` | alles lokal (eigene KI) | nein |
| `hybrid` | STT/TTS Cloud, Haupt-LLM lokal | ja |
| `cloud` | alles über OpenRouter (Standard) | ja |
Dauerhaft in `.env`: `VA_PROFILE=cloud` — oder einmalig: `VA_PROFILE=cloud make run`.
## 5. Starten und Stoppen
```bash
make run # startet im Vordergrund (Port aus .env, hier 8003)
```
Beenden mit **Strg + C**. Schnelltest in einem zweiten Terminal:
```bash
curl -s $URL/health | jq
curl -s $URL/api/config | jq
```
Im Hintergrund (Logs in Datei):
```bash
nohup make run > server.log 2>&1 & # starten
pkill -f "uvicorn app.main:app" # stoppen
```
Docker: `export OPENROUTER_API_KEY=…; docker compose up --build`.
Port ändern: `PORT=8005 make run` (einmalig) bzw. `PORT=` in `.env` (dauerhaft).
---
# Teil A — Mit dem Assistenten sprechen
## A1. Sprech-Loop: sprechen → hören → erneut sprechen (empfohlen)
Der mitgelieferte Helfer nimmt vom Mikrofon auf, schickt die Aufnahme an das Gateway
und spielt die Antwort ab — fortlaufend, mit Gedächtnis:
```bash
source .venv/bin/activate
python scripts/voice_loop.py --session mein-gespraech
```
Ablauf je Runde:
1. **[Enter]** drücken → **sprechen** (z. B. „Guten Tag, wie heißt du?")
2. **[Enter]** drücken → Aufnahme stoppt; der Assistent **antwortet hörbar**
3. wieder **[Enter]** → **erneut sprechen**; der Verlauf bleibt erhalten
4. **Strg + C** → Loop beenden
Nützliche Optionen:
```bash
python scripts/voice_loop.py --recorder pw-record # PipeWire-Aufnahme (Standard bei 'auto')
python scripts/voice_loop.py --recorder arecord --device hw:1,0 # ALSA, bestimmtes Mikrofon
python scripts/voice_loop.py --llm-provider openrouter --tts-provider openrouter
python scripts/voice_loop.py --token "$TOKEN" # falls AUTH_ENABLED=true
python scripts/voice_loop.py --file frage.wav # ohne Mikrofon: WAV senden (Test)
```
> **Aufnahmewerkzeug:** `--recorder auto` (Standard) bevorzugt **PipeWire** (`pw-record`),
> sonst `parecord`/`arecord`. **Falls die Aufnahme scheitert** (z. B.
> *„pw_context_connect() failed"* oder *„Fehler beim Öffnen des Gerätes"*), ist der
> verlässlichste Weg ein **direktes ALSA-Hardware-Gerät**:
> ```bash
> arecord -l # Kartennummern der Mikrofone
> python scripts/voice_loop.py --recorder arecord --device plughw:2,0
> ```
> (`plughw:KARTE,GERÄT` aus `arecord -l`; z. B. onboard oft Karte 2, USB-Webcam Karte 4.)
## A2. Nur tippen → Antwort hören
```bash
python chat_client.py "Erzähl mir bitte einen guten Morgen-Spruch"
```
Spielt die gesprochene Antwort ab (erwartet Port **8003**).
## A3. Einzelschritte verstehen (manueller Loop)
Pro Gesprächsrunde drei Schritte — gut, um die Pipeline zu verstehen:
```bash
# 1) Aufnehmen (Strg+C zum Stoppen)
arecord -f S16_LE -r 16000 -c 1 frage.wav
# 2) Transkribieren (Audio rein -> Text raus)
curl -s -X POST $URL/api/transcribe \
-F "file=@frage.wav" -F "language=de" -F "stt_provider=openrouter" | jq
# 3) Antwort erzeugen (Text rein -> Audio raus) und abspielen
curl -s -X POST "$URL/api/chat?session_id=loop" \
-H 'Content-Type: application/json' \
-d '{"text":"Guten Tag, wie heißt du?"}' --output antwort.pcm
ffplay -loglevel quiet -nodisp -autoexit -f s16le -ar 24000 -ac 1 antwort.pcm
# alternativ: aplay -f S16_LE -r 24000 -c 1 antwort.pcm
```
## A4. Einzelne Bausteine direkt aufrufen
```bash
# Nur Sprachausgabe (Text -> Audio):
curl -s -X POST $URL/api/speak \
-H 'Content-Type: application/json' \
-d '{"text":"Guten Morgen, wie geht es Ihnen?"}' --output gruss.pcm
ffplay -loglevel quiet -nodisp -autoexit -f s16le -ar 24000 -ac 1 gruss.pcm
# Chat als Text-Trace (ohne Audio), schön lesbar:
curl -s -X POST "$URL/api/chat?debug=true" \
-H 'Content-Type: application/json' \
-d '{"text":"Wie wird das Wetter morgen?"}' | jq
```
## A5. Weitere Features
- **Fortlaufendes Gespräch (Gedächtnis):** `?session_id=name` anhängen — der Verlauf
fließt in die nächste Antwort. Ohne `session_id` ist jeder Aufruf eigenständig.
Wie viele Nachrichten einfließen, steuert `HISTORY_MAX_MESSAGES` (Standard 10).
- **Echtzeit-Streaming:** WebSocket `/ws/chat` mit `{"text":"…","stream":true}` liefert
die Antwort wortweise; `"audio_stream":true` zusätzlich das Audio satzweise.
- **Unterbrechen (Barge-in):** während der Assistent spricht `{"type":"interrupt"}`
senden → laufende Antwort wird abgebrochen.
- **Automatische Sprechpausen-Erkennung (VAD):** im Start-Frame von `/ws/voice`
`{"type":"start","vad":true,"format":"pcm","sample_rate":16000}` → kein manuelles Ende nötig.
- **Notfall-Erkennung:** Bei Notlagen-Signalen („Schmerzen in der Brust", „gestürzt"…)
wird eskaliert (Details siehe Teil 9). ⚠️ Nur Heuristik, kein Notruf-Ersatz.
---
# Teil B — Einstellungen ändern (User / Entwickler / Admin)
## B1. Software / KI wechseln (lokal ↔ remote) — wirkt sofort
Welche KI (STT/LLM/TTS, lokal oder über die Cloud) genutzt wird, lässt sich auf
mehreren Ebenen festlegen. **Höhere Ebene gewinnt:**
| Ebene | Wer | Wie | Beispiel |
|-------|-----|-----|----------|
| Profil/Global | Admin/Entwickler | `VA_PROFILE` bzw. `.env` | `VA_PROFILE=hybrid` |
| Fallback | Admin | `*_FALLBACK` in `.env` | `LLM_FALLBACK=local-openai-compatible` |
| Pro Nutzer | User/Admin | `PUT /api/me/prefs` | `{"llm_provider":"openrouter"}` |
| Pro Session | User | `POST /api/sessions/{id}/route` | `{"tts_provider":"piper"}` |
| Pro Aufruf | User | Felder im Request-Body | `{"text":"…","llm_provider":"openrouter"}` |
```bash
# Verfügbare Provider + aktuell aufgelöste Auswahl ansehen:
curl -s $URL/api/config | jq '{profile, default_route, available}'
# Pro Aufruf umschalten (hier: lokales TTS statt Cloud):
curl -s -X POST "$URL/api/chat?debug=true" \
-H 'Content-Type: application/json' \
-d '{"text":"Test","llm_provider":"openrouter","tts_provider":"piper"}' | jq '.route'
# Pro Session dauerhaft (gilt für alle Aufrufe mit dieser session_id):
curl -s -X POST $URL/api/sessions/oma-anna/route \
-H 'Content-Type: application/json' \
-d '{"llm_provider":"openrouter","language":"de"}' | jq
```
Profil global umschalten (Entwickler/Admin): `VA_PROFILE=local-dev make run`.
**Hybrid-Beispiel** (Aufnahme + STT + LLM **lokal**, nur TTS **remote**):
```bash
# einmalig: lokales STT installieren
pip install -e .[local]
# Server mit kleinem lokalem Ollama-Modell (muss in 'ollama list' stehen):
echo 'LOCAL_LLM_MODEL=llama3.2:latest' >> .env
make run
# in Terminal 2 — Sprech-Loop mit der Hybrid-Kombi (MOTU = plughw:5,0):
python scripts/voice_loop.py --recorder arecord --device plughw:5,0 --session hybrid \
--stt-provider faster-whisper \
--llm-provider local-openai-compatible \
--tts-provider openrouter
```
Erster Turn ist langsamer (Whisper- und Ollama-Modell laden), danach zügig. STT-Modell
und Gerät steuern `FASTER_WHISPER_MODEL`/`FASTER_WHISPER_DEVICE` in `.env`.
## B2. Soundquelle & Ausgabe-Gerät wechseln (Mikrofon, Lautsprecher, Bluetooth, Handy)
> **Wichtig — aktueller Stand:** Die Geräte-Endpunkte **im Gateway**
> (`input_endpoint`/`output_endpoint`) sind die **Auswahl-/Routing-Ebene** (sie werden
> validiert und in `/api/devices` aufgelistet), aber die eigentlichen **Gerätetreiber
> sind noch Platzhalter** — es fließt also noch **kein echtes Geräte-Audio durch das
> Gateway**. Welches Mikrofon/welcher Lautsprecher/welches Bluetooth-Gerät tatsächlich
> genutzt wird, steuerst du **heute auf Betriebssystem-Ebene** (bei Aufnahme/Wiedergabe).
**Welche Geräte gibt es?**
```bash
arecord -L # Eingabegeräte (Mikrofone)
aplay -L # Ausgabegeräte (Lautsprecher/Kopfhörer)
```
**Mikrofon (Quelle) wählen:**
```bash
python scripts/voice_loop.py --device hw:1,0 # Helfer mit bestimmtem Mikrofon
arecord -D hw:1,0 -f S16_LE -r 16000 -c 1 frage.wav # manuell
```
**Lautsprecher/Kopfhörer (Ausgabe) wählen:**
```bash
aplay -D hw:0,0 -f S16_LE -r 24000 -c 1 antwort.pcm
```
**Bluetooth / Standardgerät (PipeWire/PulseAudio):** Gerät am System koppeln und als
Standard setzen — dann nutzen `arecord`/`aplay`/`voice_loop.py` automatisch dieses
Gerät. Grafisch mit `pavucontrol`, per Kommandozeile z. B. mit `wpctl status` /
`wpctl set-default <id>`.
**Gateway-Endpunkt-Auswahl (Routing-Ebene, vorbereitet):**
```bash
curl -s $URL/api/devices | jq '{inputs:[.inputs[].kind], outputs:[.outputs[].kind]}'
# Auswahl mitgeben (wird validiert; echtes Geräte-Audio folgt erst mit echten Treibern):
curl -s -X POST $URL/api/sessions/oma-anna/route \
-H 'Content-Type: application/json' \
-d '{"input_endpoint":"bluetooth","output_endpoint":"local-default"}' | jq
```
Ein unbekannter Endpunkt führt zu `HTTP 422`. *Roadmap: echte Geräte-Endpunkte
(PipeWire/Bluetooth/Handy) sind der nächste Ausbauschritt.*
## B3. Sprache wechseln
Global `DEFAULT_LANGUAGE=de` in `.env`, pro Nutzer via `PUT /api/me/prefs`, pro Session
via Route, oder pro Aufruf `{"text":"…","language":"en"}`.
---
# Teil C — Praxis-Tests & Reaktionszeiten
## C1. Funktioniert alles? (echter Live-Check)
```bash
make smoke
```
Prüft LLM, TTS und STT **live** gegen OpenRouter (geringe Kosten) und meldet pro Modul
`[OK]`/`[FAIL]` — inkl. TTS→STT-Round-Trip. Braucht `OPENROUTER_API_KEY`.
## C2. Reaktionszeiten messen
Pro Aufruf die Gesamtzeit anzeigen (`curl -w`):
```bash
# Sprachausgabe (TTS):
curl -s -o gruss.pcm -w "TTS: %{time_total}s, %{size_download} Bytes\n" \
-X POST $URL/api/speak -H 'Content-Type: application/json' \
-d '{"text":"Guten Tag, wie kann ich Ihnen helfen?"}'
# Transkription (STT):
curl -s -o /dev/null -w "STT: %{time_total}s\n" \
-X POST $URL/api/transcribe -F "file=@frage.wav" -F "language=de"
# Chat-Antworttext (LLM, TTS auf Stub isoliert):
curl -s -o /dev/null -w "LLM: %{time_total}s\n" \
-X POST "$URL/api/chat?debug=true" -H 'Content-Type: application/json' \
-d '{"text":"Sag einen kurzen Gruss.","tts_provider":"piper"}'
```
Durchschnitt der Pipeline-Stufen serverseitig:
```bash
curl -s $URL/api/metrics | jq '.timers | to_entries
| map(select(.key|test("stage_duration")))
| map({stufe:.key, sekunden:.value.avg})'
```
**Gemessene Richtwerte** (Profil `cloud`, gegen OpenRouter, Stand 2026-06-17):
| Stufe | Modell | ~Zeit |
|-------|--------|-------|
| STT | `whisper-large-v3` | ~1,2 s |
| LLM | `gemini-3.1-flash-lite` | ~0,7 s |
| TTS | `gemini-3.1-flash-tts` | ~1,9 s |
| **Sprach-Round-Trip** (STT→LLM→TTS) | — | **~4 s** |
> Werte schwanken mit Netz, Textlänge, Modell und Region; der erste Aufruf ist oft
> langsamer (Verbindungsaufbau). Mit `stream`/`audio_stream` (Teil A5) sinkt die
> **wahrgenommene** Wartezeit deutlich, weil schon vor Fertigstellung Text/Audio kommt.
## C3. Welche Konstellation für welchen Use-Case?
| Use-Case | Empfehlung | Begründung |
|----------|------------|------------|
| Senioren-Standard (kein KI-Rechner zuhause) | **Profil `cloud`** | beste Qualität/Latenz ohne lokale Hardware (~4 s Round-Trip) |
| Datenschutz / offline | `local-dev` | alles lokal — benötigt echte lokale Modelle (heute Platzhalter) |
| Kosten/Ausfallsicherheit | `hybrid` + `*_FALLBACK` | teure Teile lokal, Rest Cloud; automatischer Fallback |
Empfehlung für den Einstieg: **`cloud`** verwenden, Antwortzeiten mit C2 prüfen, dann
bei Bedarf einzelne Module umstellen (Teil B1).
### C4. Empfohlene Top-Konstellation (reproduzierbar)
Bewährte Konstellation mit sehr guter Sprachqualität (beherrscht u. a. **Plattdeutsch**) —
**alles remote über OpenRouter** (Profil `cloud`), nichts lokal:
| Stufe | Modell | Anbieter |
|------|--------|----------|
| STT | `openai/whisper-large-v3` | OpenRouter (remote) |
| LLM | `google/gemini-3.1-flash-lite` | OpenRouter (remote) |
| TTS | `google/gemini-3.1-flash-tts-preview` (Stimme `Zephyr`) | OpenRouter (remote) |
So reproduzierst du sie — in `.env`:
```
VA_PROFILE=cloud
OPENROUTER_STT_MODEL=openai/whisper-large-v3
OPENROUTER_LLM_MODEL=google/gemini-3.1-flash-lite
OPENROUTER_TTS_MODEL=google/gemini-3.1-flash-tts-preview
OPENROUTER_TTS_VOICE=Zephyr
```
Key via Umgebung (`OPENROUTER_API_KEY`). Dann ohne Provider-Overrides starten:
```bash
make run
python scripts/voice_loop.py --recorder arecord --device plughw:5,0 --session test
```
**Grobe Kosten (Daumenwert, am OpenRouter-Dashboard verifizieren — Preise ändern sich):**
| Stufe | Annahme | ~Kosten/Runde |
|------|---------|---------------|
| STT (Whisper) | ~$0,006/Audio-Min, ~15 s | ~0,15 ¢ |
| LLM (Flash-Lite) | ~800 in / ~150 out Tokens | ~0,01 ¢ (vernachlässigbar) |
| TTS (Flash-TTS) | ~$0,010,03/Audio-Min, ~30 s | ~0,51,5 ¢ |
| **Summe** | | **≈ 12 ¢ pro Sprech-Runde** |
→ grob **~2040 ¢ pro 10-Minuten-Gespräch**, **~12 $/Stunde**. Kostentreiber ist das
**Audio (TTS, dann STT)**; das LLM ist nahezu kostenlos. Verlässliche Zahlen liefert das
**OpenRouter-Dashboard** (Activity/Usage).
---
# Betrieb & Verwaltung
## 9. Authentifizierung, Kontingent & Notfall
**Auth (Mehrbenutzer):** Standard `AUTH_ENABLED=true` → geschützte Endpunkte brauchen
ein **Bearer-Token pro Nutzer**. (In der mitgelieferten `.env` ist es für die
Entwicklung auf `false` — dann ohne Token.)
```bash
export ADMIN_API_KEY=ein-langes-geheimnis # Server muss damit laufen
# Nutzer anlegen (Token erscheint NUR einmal):
curl -s -X POST $URL/api/admin/users \
-H "X-Admin-Key: $ADMIN_API_KEY" \
-H 'Content-Type: application/json' -d '{"display_name":"Oma Anna"}' | jq
# Mit Token nutzen:
TOKEN=<token-von-oben>
curl -s $URL/api/me -H "Authorization: Bearer $TOKEN" | jq
```
**Langzeit-Erinnerungen** (dauerhafte Fakten, gelten über alle Gespräche):
```bash
curl -s -X POST $URL/api/me/memories -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"content":"Mag morgens Kamillentee."}' | jq
curl -s $URL/api/me/memories -H "Authorization: Bearer $TOKEN" | jq
```
**Tageskontingent** (Kostenbremse) in `.env`: `DAILY_REQUEST_LIMIT=200` (0 = unbegrenzt).
Überschreitung → `HTTP 429`. Notfall-Eingaben werden nie blockiert.
**Notfall-Eskalation:** Erkennt der Dienst ein Notlagen-Signal, protokolliert er es,
macht es sichtbar (`X-Emergency` / `emergency`-Event) und ruft optional einen Webhook
auf (`EMERGENCY_WEBHOOK_URL`). ⚠️ Nur eine **Heuristik** — kein Ersatz für einen echten
Notruf; erkannte Texte sind sensibel (Datenschutz/Einwilligung beachten).
## 10. Resilienz & Metriken
```bash
# Fallback je Modul (in .env): Provider fällt aus -> nächster übernimmt
LLM_FALLBACK=local-openai-compatible
# Metriken (Requests, Latenzen, Stufen, Fallback/Fehler):
curl -s $URL/api/metrics | jq
curl -s "$URL/api/metrics?format=prometheus" # Prometheus-Text (kein jq)
```
## 11. Fehlerbehebung
| Symptom | Ursache | Lösung |
|---|---|---|
| `OPENROUTER_API_KEY is empty` | Key nicht in der Umgebung | `export OPENROUTER_API_KEY=…`, neues Terminal / `source ~/.bashrc` |
| HTTP **401** (Bearer/Invalid token) | Auth an, Token fehlt/falsch | Token im Header, oder `AUTH_ENABLED=false` für dev |
| HTTP **401** bei `/api/admin/users` | falscher/fehlender Admin-Key | `X-Admin-Key` = `ADMIN_API_KEY` |
| HTTP **403** bei `?session_id=…` | Session gehört anderem Nutzer | eigene `session_id` verwenden |
| HTTP **429** | Tageskontingent erreicht | `DAILY_REQUEST_LIMIT` erhöhen / Folgetag |
| HTTP **422** „Unbekannter Provider/Endpunkt" | Tippfehler | gültige Werte via `curl -s $URL/api/config \| jq` |
| HTTP **502** bei STT/TTS | Cloud-Fehler/Format | `make smoke` ausführen; Modellnamen in `.env` prüfen |
| `VA_PROFILE` wirkt nicht | `DEFAULT_*_PROVIDER` in `.env` überschreibt | diese Zeilen in `.env` auskommentieren |
| `Address already in use` | Port belegt | anderen `PORT` setzen |
| `pw_context_connect() failed` / `arecord: Fehler beim Öffnen des Gerätes` | PipeWire-Client- bzw. ALSA-`default`-Pfad gestört | direktes Gerät nehmen: `arecord -l`, dann `--recorder arecord --device plughw:2,0` |
| Keine Aufnahme/Wiedergabe | Werkzeug/Gerät fehlt | `arecord -L` / `aplay -L`; Pakete `pipewire`/`alsa-utils`/`ffmpeg`; Default via `wpctl status` |
| Profil greift nicht | `config/voice-assistant.toml` fehlt | aus `*.example.toml` kopieren (Abschnitt 2) |
Logs erscheinen im Terminal von `make run`; mehr Details mit `LOG_LEVEL=debug` in `.env`.
## 12. Automatisierte Tests
```bash
make test # offline (mit Platzhaltern) — schnell, kostenlos
make smoke # echter Live-Check gegen OpenRouter (LLM/TTS/STT)
```