Files
ApiCore/docs/superpowers/specs/2026-10-16-beute-teil4-sprach-durchsicht-design.md
T
2026-10-16 09:12:04 +02:00

904 lines
76 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.
# Beute Teil 4: Sprach-Durchsicht — Design
Stand: 2026-10-16 · Betrifft: ApiCore-Frontend (Sitzung, Spracherkennung, Parser, Rückfragen), ApiCore-Backend (Sitzungen, KI-Schleife, Hive-Operationen, Durchsicht), Whisper-Image aus T7 (Segmente) · Grundlage: Inhaber-Anforderungen `2026-10-16-beute-teil4-sprach-durchsicht-anforderungen.md` (verbindlich), T7 `2026-10-14-t7-sprache-design.md` (Grundlage, Datenschutz, Aufbewahrung, Prüfregeln), Beute-Spec `2026-10-15-beute-3d-design.md` §5 (Datenmodell), §6 (Operationen), §7 (Durchsicht), Quelle `AI_SUGGESTION`, Code in `ApiCore-Backend/src/hive` und `ApiCore-Frontend/src/lib/hive` (Stand heute: L1 und L2 fertig)
## 1. Ziel
Der Imker öffnet eine Durchsicht und tippt auf „Sprache an“. Das Handy steckt in der Brusttasche. Er arbeitet mit beiden Händen und spricht frei:
> „Zarge eins, von hinten. Rähmchen rechts eins sieht gut aus, ist eine Futterwabe, die daneben ist eine Pollenwabe, beide ca. 25 %, die dritte ist eine Brutwabe, wenig Brut, Futterkranz gering.“
Die App ordnet jede Angabe genau einer Zarge und einem Rähmchen zu. Sie fragt sofort nach, wenn etwas logisch nicht stimmt, und sonst nicht. Am Ende liest sie eine kurze Zusammenfassung vor. Mit „übernehmen“ (oder Tipp) entstehen ein Entwurf der Durchsicht und die Änderungen am Aufbau als Vorschlag. Gespeichert wird erst mit dem Knopf „Durchsicht speichern“.
Kern der Lösung: Ein **eigener, deterministischer Parser im Browser** versteht die Imker-Grammatik. Nur was er nicht versteht, geht als einzelner Satz mit kurzem Zeiger-Zustand an ein kleines Claude-Modell.
Messbar fertig ist Teil 4, wenn:
1. Der Beispielsatz oben ergibt genau drei Rähmchen-Befunde (Zarge 1, Plätze 10, 9, 8 mit Art und Anteilen, Abschnitt 5.9 Beispiele 1–5). Der Fixture-Test beweist das im Frontend und im Backend.
2. Eine Sitzung läuft auf Android Chrome 20 Minuten im Taschenmodus. Fünf Minuten Stille beenden sie nicht. Sie endet nur durch „Durchsicht beendet“ oder den Knopf (Test mit gefälschter Erkennung, dazu manuell am Stand).
3. Bei den Gold-Aufnahmen des Inhabers (5 Durchsichten) versteht der Parser mindestens 80 % der Sätze ohne KI. Eine typische Durchsicht von 15 Minuten braucht höchstens 20 KI-Aufrufe und kostet mit `claude-haiku-5-5` unter 1 Cent (gemessen über die Token-Zähler).
4. Sofort-Rückfragen kommen nur aus der Regelliste in 6.1 und höchstens 5 je Sitzung (plus höchstens eine Zählrichtungs-Frage je Zarge). Alles andere steht in der Zusammenfassung.
5. Nichts wird ohne ausdrückliche Bestätigung übernommen und nichts ohne Tipp auf „Durchsicht speichern“ gespeichert. Durchsicht und `HiveChange` (Quelle `AI_SUGGESTION`) entstehen in einer Transaktion.
6. Ohne Netz entsteht eine Aufnahme-Durchsicht (T7-Weg). Ihre spätere Auswertung liefert für dieselben Sätze dieselben Befunde wie live (gleiche Fixtures).
7. Vom Gerät geht nur der Text unverstandener Sätze an den Server, nie Ton. Kein Text steht in Logs (Log-Mitschnitt-Test wie T7).
### Entscheidungen
| Frage | Entscheidung |
|---|---|
| Spracherkennung live | **Web Speech API im Browser** (`SpeechRecognition`, `continuous`, `de-DE`), bevorzugt auf dem Gerät (`processLocally`), sonst Browser-Cloud nur mit Einwilligung (T7 Frage 2). Begründung in 4.1. Kein Streaming-STT-Dienst. |
| Rückfall ohne Live-Erkennung | **Aufnahme-Durchsicht:** T7-Aufnahme in Stücken zu höchstens 180 s, im Cluster mit Whisper abgeschrieben, danach derselbe Parser und dieselbe KI (Abschnitt 10). |
| Wo läuft der Parser? | **Im Browser** für live (keine Latenz, kein Netz, kein Token). **Im Worker** für Aufnahme-Durchsichten. Zwei Fassungen gegen dieselben JSON-Fixtures, Muster `hive-ops` (Beute §6.1). |
| Zeiger | **Zustandsmaschine** mit Zarge, Zählweise je Zarge, Kante, Platz und Gruppe (5.3). Jede Angabe bindet an genau einen Platz oder an die Durchsicht. Ohne Bezug wird nichts geraten. |
| Zarge eins | **Unterste Zarge mit Rähmchen-Plätzen**, nach oben gezählt. Leerzargen (0 Plätze) zählen nicht (Annahme B1). |
| Zählweise | **Je Zarge gesprochen und gemerkt:** „von hinten (Flugloch vorn)“ oder „von der linken/rechten Seite“. Unbekannt und nötig: eine Frage je Zarge, mit Angebot der vorigen Zarge (5.4). |
| Prozentangaben | **Anteil der Wabenfläche** mit dem Inhalt, über beide Seiten gemittelt (Anforderung 7). Ganze Zahl 0–100. Ohne Inhaltswort gilt der Inhalt der Rähmchen-Art. |
| Datenmodell Rähmchen | **`HiveFrame` bekommt `fill` und `note`.** `setFrame` wird additiv erweitert (`type` optional, `fill`, `note`). Kein neues Modell an der Durchsicht. Der Rähmchen-Befund einer Durchsicht ist ihre `HiveChange` (7.1). |
| KI | **Nur für unverstandene Sätze.** `claude-haiku-5-5`, `effort: low`, strukturierte Ausgabe, fester System-Teil im Prompt-Cache (1 h), je Aufruf nur Satz und Zeiger-Zustand (Abschnitt 8). |
| Wo läuft der KI-Aufruf? | **Worker-Schleife `voice-live`**, geweckt per `pg_notify`. Der API-Schlüssel bleibt nur im Worker (T7-Entscheidung). Das Gerät fragt das Ergebnis alle 1,5 s ab, solange etwas offen ist. |
| Rückfragen | **Sprachausgabe (`speechSynthesis`) nur bei Widerspruch oder blockierender Unklarheit**, Budget 5 je Sitzung. Abweichung von T7 §2.2 „Sprachausgabe nicht gewünscht“: Der Inhaber will sie seit 2026-10-16 für Rückfragen und die Zusammenfassung. |
| Ende | **Nur „Durchsicht beendet“ oder Knopf.** Stille, Erkennungsfehler, Netzverlust und Bildschirm-Aus beenden nie (3.2). |
| Bestätigung | **Zusammenfassung vorlesen, dann „übernehmen“ oder Tipp.** Das füllt den Entwurf (Felder „aus Sprache“, Beute-Änderungen in der Liste). Speichern nur per Tipp (offene Frage 1). |
| Quelle der Beute-Änderung | **`AI_SUGGESTION`**, nur zusammen mit einer gültigen `voiceSessionId`. Der Server nimmt sie im Durchsicht-Weg an, nicht in `POST …/hive/changes`. |
| Netz | Live-Start nur online (Anforderung 4). Netzverlust während der Sitzung: bei Erkennung auf dem Gerät Parser weiter, KI-Sätze warten; bei Browser-Cloud-Erkennung Wechsel auf Aufnahme (10.2). |
| iOS Safari | **Kein Live-Modus in dieser Lieferung.** iOS bekommt die Aufnahme-Durchsicht. Gründe in 3.4. |
### Abhängigkeiten
- **T7** (noch nicht gebaut): Modul `voice`, Recht `voice-captures:use`, Schlüsselbund-Zweck `voice`, `VoiceCapture`, Whisper-CronJob, Worker-Schleife, `voice-vocabulary.ts`, `VoiceSuggestion`, Datenschutz-Seite. Teil 4 nutzt diese Bausteine und erweitert sie additiv.
- **Beute L1/L2** (fertig): `applyHiveOps`, Validator, `deriveHiveFacts`, Fixtures. **Beute L5** (offen): Abschnitt „Beute“ im Durchsicht-Formular und `hiveChange` in der Offline-Warteschlange. Teil 4 füllt diesen Abschnitt.
- Rückgrat: `DomainEvent`, `PollingLoop`, `outbox-claims.ts`, `retry-policy.ts`, `@Idempotent()`.
## 2. Umfang
### 2.1 Im Umfang
| Baustein | Inhalt |
|---|---|
| Sitzung | Start „Sprache an“, Ende nur gesprochen oder per Knopf, Pause per Knopf, Wake Lock, Taschenmodus, Journal im Gerät, Fortsetzen nach Unterbrechung. |
| Erkennung | Web-Speech-Schleife mit Neustart, Alternativen, Fachwort-Gewichtung, Hörfehler-Tabelle. |
| Parser | Normalisierung, Satzteile, Navigation, Zählweise, Angaben, Befehle, Rest-Text. Zwei Fassungen, ein Fixture-Satz. |
| Rückfragen | Regeln, Budget, Antwort-Grammatik, Sprachausgabe, gesammelte Punkte. |
| KI | Sitzungen und Sätze im Backend, Schleife `voice-live`, Prompt, Schema, Prüfung, Kostenbremse, Zähler. |
| Abschluss | Zusammenfassung (gesprochen und sichtbar), Bestätigung, Entwurf, Beute-Operationen, Speichern mit `voiceSessionId`. |
| Datenmodell | `HiveFrame.fill`, `HiveFrame.note`, erweitertes `setFrame`, Facts, `VoiceSession`, `VoiceUtterance`, Felder an `VoiceCapture`. |
| Offline | Aufnahme-Durchsicht in Stücken, Whisper-Segmente, Auswertung im Worker, Prüfansicht. |
### 2.2 Nicht im Umfang
| Thema | Grund |
|---|---|
| Live auf iOS Safari | Erkennung im Dauerbetrieb und bei gesperrtem Bildschirm nicht verlässlich (3.4). Wird nach einer Messung neu bewertet. |
| Streaming-STT im Cluster oder bei einem Dienst | 4.1: Dauer-Deployment (T7 verworfen) bzw. Ton bei Dritten (T7 verboten). |
| Umbau per Sprache über den kleinen Satz in 5.6 hinaus | Futterstände, Verdunster, Gitter usw. kommen als offener Punkt „Umbau gesagt: … bitte in der Karte eintragen“. Eigene spätere Stufe. |
| Fütterung, Behandlung, Aufgaben aus Sprache | Wie T7 §2.2. Die Beute-Folgevorschläge (Beute §8) gelten wie bei jeder Änderung. |
| Mehrere Völker in einer Sitzung | Eine Sitzung gehört zu einem Volk. „Weiter mit B4“ startet eine neue (Standrunde). |
| Mehrere Sprecher, Mundart-Training | Wie T7. |
| Speichern per Sprache | Offene Frage 1. Bis zur Antwort nur per Tipp. |
## 3. Sitzung
### 3.1 Zustände
```
„Sprache an“ (Tipp)
┌─────────────────────────────────────────┐
▼ │
HÖRT ◀──── Antwort / „weiter“ ──── FRAGT │
│ ▲ Widerspruch (6.1) ─────────────▲ │
│ │ │
│ └── Knopf „Weiter“ ── PAUSE ◀── Knopf „Pause“
│
├── Netz weg + Browser-Cloud ──▶ AUFNAHME ──(Netz da)──▶ HÖRT
├── Seite verdeckt ──▶ UNTERBROCHEN ──(sichtbar)──▶ HÖRT
│
└── „Durchsicht beendet“ / Knopf ──▶ ZUSAMMENFASSUNG
├── „weiter“ ──▶ HÖRT
├── „übernehmen“ / Tipp ──▶ ÜBERNOMMEN (Entwurf, Sitzung CLOSED)
└── „verwerfen“ + „ja, verwerfen“ ──▶ VERWORFEN
```
- **Start:** nur per Tipp auf „Sprache an“ im Durchsicht-Formular (mit gewähltem Volk). Der Tipp erzeugt auch den `AudioContext` für die Töne und erlaubt `speechSynthesis` (Browser verlangen eine Nutzergeste). Online legt die App `POST /voice-sessions` an; ohne Antwort binnen 5 s startet sie trotzdem, die Sitzungs-Id kommt nach.
- **PAUSE:** Die Erkennung läuft weiter, aber nur „weiter“ wird ausgewertet. Alles andere wird verworfen und nicht gespeichert (Gespräch mit Nachbarn). Kein gesprochenes „Pause“, weil das Wort im Alltag zu oft fällt.
- **Fortsetzen:** Das Journal (3.5) überlebt Neuladen und Absturz. Öffnet der Nutzer die Durchsicht des Volks erneut, steht oben „Sprach-Durchsicht von 10:05 fortsetzen“.
### 3.2 Kein Ende bei Stille
Chrome beendet eine Erkennung nach Stille, Netzfehler oder nach einiger Zeit von selbst (`end`, Fehler `no-speech`, `network`, `aborted`). Die App behandelt das nie als Ende:
| Ereignis | Verhalten |
|---|---|
| `end` ohne Fehler, `no-speech` | Sofort neu starten (250 ms). Kein Zähler, keine Meldung. |
| `network` | Netzprüfung; online: Neustart mit Wartezeit 1 s, 2 s, 5 s, dann alle 5 s. Offline: 10.2. |
| `audio-capture` | Drei Neustarts, dann Hinweis „Mikrofon nicht verfügbar“ mit Ton, Sitzung bleibt offen. |
| `not-allowed`, `service-not-allowed` | Hinweis mit Schritt-für-Schritt-Text je Browser, Sitzung bleibt offen, „Erneut versuchen“. |
| kein `audiostart` 5 s nach Neustart | Wächter: Instanz verwerfen, neu anlegen. |
| Seite verdeckt (`visibilitychange`) | UNTERBROCHEN. Zeit merken. Beim Sichtbarwerden Neustart und Ansage „Weiter. Unterbrochen von 10:12 bis 10:14.“ |
Schwache Aufnahme führt höchstens zu weniger Sätzen, nie zum Ende. Die Oberfläche zeigt „Hört zu“ und die Zeit seit dem letzten erkannten Satz.
### 3.3 Bildschirm und Tasche
- **Wake Lock:** `navigator.wakeLock.request('screen')` beim Start, erneut bei jedem `visibilitychange` auf sichtbar. Ohne Unterstützung: Hinweis „Bildschirm-Zeitlimit hochsetzen, sonst stoppt die Erkennung“.
- **Taschenmodus:** Knopf „Taschenmodus“ oder gesprochen „Taschenmodus“. Schwarzer Vollbild-Schirm (spart auf OLED fast so viel wie ein ausgeschalteter Bildschirm), eine kleine Statuszeile in gedämpfter Schrift. Berührungen werden verschluckt. Entsperren: Tipp auf „Entsperren“, dann binnen 3 s Tipp auf „Bestätigen“ an anderer Stelle, oder gesprochen „Taschenmodus aus“. Kein Halten, kein Wischen (WCAG 2.5.1, 2.5.7).
- **Bildschirm aus (Ein/Aus-Taste):** Die Seite wird verdeckt, Chrome stoppt die Erkennung. Die Sitzung bleibt offen (UNTERBROCHEN). Ehrlich: Mit ausgeschaltetem Bildschirm gibt es keine Live-Erkennung im Browser. Die App sagt das beim ersten Start einmal: „Lass den Bildschirm an. Ich schalte ihn schwarz, wenn du ‚Taschenmodus‘ sagst.“
- **Töne:** Kurzer heller Ton für „verstanden“, zwei tiefe Töne für „nicht verstanden, prüfe ich“ (Satz geht an die KI), drei Töne bei Unterbrechung nach dem Fortsetzen. Abschaltbar. Keine Töne bei laufender Sprachausgabe.
### 3.4 Was Browser erlauben
| Punkt | Android Chrome (Ziel) | iOS Safari |
|---|---|---|
| Web Speech API | ja; `continuous` endet trotzdem nach Stille, daher Neustart-Schleife (3.2) | `webkitSpeechRecognition` vorhanden, Dauerbetrieb unzuverlässig, in der installierten PWA lange nicht verfügbar |
| Erkennung auf dem Gerät | `processLocally`, `available()`, `install()` sind experimentell; Verfügbarkeit je Version per Erkennung prüfen (Annahme B4) | nicht steuerbar |
| Fachwort-Gewichtung | `phrases` (Kontext-Biasing) experimentell, nur nutzen, wenn vorhanden | nein |
| Wake Lock | ja | ab 16.4; in der installierten PWA erst ab 18.4 verlässlich |
| Erkennung bei Bildschirm aus / im Hintergrund | nein | nein |
| Mikrofon-Erlaubnis | bleibt je Herkunft gespeichert, außer „Nur dieses Mal“ | wird oft je Sitzung neu gefragt |
| Signalton beim Start der Erkennung | auf manchen Geräten bei jedem Neustart (Risiko, Messung M3) | – |
Daraus folgt: Live nur auf Android Chrome (und Chromium-Browsern, die die Prüfung in 4.3 bestehen). Alle anderen bekommen die Aufnahme-Durchsicht (Abschnitt 10). Die Entscheidung trifft die App per Fähigkeitsprüfung, nicht per Browser-Kennung.
### 3.5 Journal im Gerät
Neuer Speicher `voice-sessions` in `db.ts` (nächste freie Version nach T7 und Beute L5; Version 4 ist heute durch `pending-phenology-reports` belegt). Ein Eintrag je Sitzung:
```ts
interface VoiceSessionJournal {
id: string; // lokale Id = Idempotency-Key für POST /voice-sessions
serverId: string | null;
ownerUserId: string;
colonyId: string;
inspectionDate: string; // Berliner Tag
hiveVersion: number | null; // Stand des Aufbaus beim Start
state: SessionState; // 3.1
cursor: Cursor; // 5.3
utterances: JournalUtterance[]; // seq, Text, Alternativen, Ergebnis (geparst / KI offen / KI fertig / verworfen)
findings: Finding[]; // 5.7, mit seq und Zitat
deferred: OpenPoint[]; // 6.4
questionsAsked: number;
createdAt: string;
}
```
- Gelöscht bei „übernehmen“ (nach dem Füllen des Entwurfs), „verwerfen“, Abmelden (Rückfrage wie T3) und spätestens nach 7 Tagen.
- Fremde Konten sehen den Eintrag nie (`ownerUserId`, Muster T7 6.1).
## 4. Spracherkennung
### 4.1 Vergleich
| Kriterium | Web Speech, auf dem Gerät | Web Speech, Browser-Cloud (Google) | Streaming-STT im Cluster (z. B. faster-whisper-Streaming, sherpa-onnx) | Streaming-STT-Dienst (Cloud) |
|---|---|---|---|---|
| Qualität Fachwörter | mittel; Biasing per `phrases`, wo vorhanden; Alternativen helfen | gut für Alltagsdeutsch, Fachwörter mittel bis gut | Whisper `small` laut T7 mittel; Streaming verschlechtert, `medium` braucht ~2 GiB | gut, mit Vokabel-Listen sehr gut |
| Datenschutz | Ton bleibt auf dem Gerät | Ton geht an Google (Einwilligung, T7 Frage 2) | Ton bleibt im Cluster | Ton bei Dritten, **verletzt T7** („Ton verlässt den Cluster nicht“) |
| Kosten | 0 | 0 | Dauer-Deployment ~1–2 GiB RAM während der Saison, CPU-Last bei 1 vCPU-Bursts | pro Minute Ton, laufend |
| Latenz | < 0,5 s | 0,5–1 s | 1–3 s je Satz auf CPU | 0,3–1 s |
| Offline | ja | nein | nein (braucht Netz zum Cluster) | nein |
| Aufwand | gering (Frontend) | gering | hoch (WebSocket, Deployment, Skalierung) | mittel plus Vertrag/AVV |
**Entscheidung:** Web Speech API, bevorzugt auf dem Gerät. Browser-Cloud nur nach Einwilligung (Schalter „Browser-Cloud-Erkennung erlauben“ aus T7 8.4, Standard aus). Streaming-STT wird nicht gebaut: Im Cluster fehlt die Rechenleistung für einen stets bereiten Dienst (T7 6.3 hat das Dauer-Deployment verworfen), ein Dienst verletzt die T7-Zusage zum Ton. Der Rückfall ist die Aufnahme-Durchsicht mit Whisper im Cluster (Abschnitt 10). Ob die Erkennung auf dem Gerät für die Fachwörter reicht, entscheidet die Messung M1 (Abschnitt 15.5). Reicht sie nicht, ist die Browser-Cloud mit Einwilligung der Live-Weg; ohne Einwilligung bleibt die Aufnahme-Durchsicht.
### 4.2 Erkennungs-Schleife
- `lang = 'de-DE'`, `continuous = true`, `interimResults = true` (nur für die Anzeige), `maxAlternatives = 3`.
- `processLocally = true`, wenn `SpeechRecognition.available({ langs: ['de-DE'], processLocally: true })` „available“ meldet. Bei „downloadable“ bietet die App „Sprachpaket laden (einmalig, über WLAN)“ an (`install()`, Nutzergeste).
- `phrases`: die Fachwörter aus `voice-vocabulary.ts` (T7, eine Quelle) mit Gewicht, nur wenn die Eigenschaft existiert.
- Jedes Endergebnis (`isFinal`) wird ein **Satz** mit fortlaufender `seq` und bis zu drei Alternativen. Der Parser versucht die Alternativen der Reihe nach, wenn die erste nichts Bindbares ergibt (5.8). Das fängt Hörfehler, bevor die KI etwas kostet.
- Während der Sprachausgabe ist die Erkennung angehalten (`abort()`), damit die App sich nicht selbst hört. Nach `onend` der Ausgabe Neustart, Sicherheitszeit 15 s.
### 4.3 Fähigkeitsprüfung
Live-Modus wird angeboten, wenn: `SpeechRecognition` oder `webkitSpeechRecognition` existiert, `continuous` nach einem Testlauf von 3 s nicht mit `service-not-allowed` endet, und entweder Erkennung auf dem Gerät verfügbar ist oder die Cloud-Einwilligung vorliegt. Sonst zeigt der Knopf „Sprache an“ den Untertitel „Aufnahme, Auswertung später“ und startet die Aufnahme-Durchsicht.
## 5. Grammatik und Parser
### 5.1 Aufbau
Reine Funktionen, keine Abhängigkeit von React oder Nest:
```
lib/voice-grammar/ (Frontend) src/voice/grammar/ (Backend, gleiche Dateien und Semantik)
normalize.ts Kleinschreibung, NFKC, Zahlwörter, Hörfehler, Komposita
lexicon.ts Wörter → Klassen (Navigation, Art, Inhalt, Menge, Feld, Befehl, Füllwort)
clauses.ts Satz → Satzteile an Ankern
rules.ts Satzteil → Ereignisse (BOX, STANCE, FRAME, STEP, GROUP, RANGE, ATTR, FIELD, CMD)
cursor.ts Zustandsmaschine: Ereignisse → Bindungen
conflicts.ts Regeln aus 6.1 und 6.3
parse.ts parseUtterance(text, alternatives, state, hive) → ParseResult
```
```ts
interface ParseResult {
findings: Finding[]; // gebunden, mit Zitat (Wortlaut-Ausschnitt)
commands: Command[]; // 5.7
issues: Issue[]; // Widersprüche und Unklarheiten, Art und Text
rest: string; // nicht verwendeter Text nach Abzug der Füllwörter
restKind: 'NONE' | 'FILLER' | 'FRAME_NOTE' | 'NEEDS_AI' | 'UNUSED';
nextState: Cursor; // nie Veränderung des Eingangs
}
```
`hive` ist eine kleine Sicht auf den Aufbau beim Start: Zargen mit Rähmchen-Plätzen von unten (Id, Anzahl Plätze, Arten). Ohne Aufbau gilt 5.10.
### 5.2 Normalisierung
| Schritt | Beispiel |
|---|---|
| NFKC, Kleinschreibung, Satzzeichen bleiben als Anker | „Rähmchen Rechts 1.“ → „rähmchen rechts 1 .“ |
| Zahlwörter → Ziffern, auch zusammengesetzt | „fünfundzwanzig“ → 25, „zehn“ → 10 |
| Ordinalzahlen markiert | „die dritte“ → `ORD:3`, „erste“ → `ORD:1`, „letzte“ → `ORD:LAST` |
| Bruchwörter → Prozent | „halb“ 50, „ein Viertel“ 25, „ein Drittel“ 33, „drei Viertel“ 75 |
| Prozent | „25 %“, „25%“, „25 Prozent“ → `PCT:25` |
| Ungefähr-Wörter entfernt, Befund markiert `≈` | „ca.“, „circa“, „etwa“, „ungefähr“, „so“ (vor Zahl) |
| Komposita zusammengezogen | „pollen wabe“ → „pollenwabe“, „futter kranz“ → „futterkranz“ |
| Hörfehler-Tabelle (`voice-mishears.ts`, wächst mit Messung M1) | „rämchen“, „rehmchen“, „rahmen“ → „rähmchen“; „weisel zelle“, „weise zellen“ → „weiselzelle“; „zage“ → „zarge“ |
| „ein/eine“ ist nur nach „zarge“ oder „rähmchen“ eine Zahl | „ist eine futterwabe“ bleibt Artikel |
### 5.3 Zeiger-Zustand
```ts
type Stance = 'REAR' | 'SIDE_LEFT' | 'SIDE_RIGHT';
type Edge = 'L' | 'R';
interface Cursor {
boxNo: number | null; // 1 = unterste Zarge mit Plätzen
slot: number | null; // Platz im Aufbau-JSON (1 = links mit Blick auf das Flugloch, Beute A4)
edge: Edge | null; // Kante, von der in dieser Zarge zuletzt gezählt wurde
group: number[]; // zuletzt genannte Plätze dieser Zarge, höchstens 4, neueste zuletzt
stanceByBox: Record<number, Stance>; // gemerkt je Zarge für die ganze Sitzung
edgeByBox: Record<number, Edge>;
pending: PendingQuestion | null; // höchstens eine offene Frage
}
```
Übergänge (Auszug, vollständig in `cursor.ts` und den Fixtures):
| Ereignis | Wirkung |
|---|---|
| `BOX(n)` | `boxNo = n`, `slot = null`, `group = []`, `edge = edgeByBox[n] ?? null`. Gibt es Zarge n nicht: Widerspruch W1. |
| `STANCE(s)` | `stanceByBox[boxNo] = s`. Ohne Zarge: gilt für die nächste genannte Zarge. |
| `FRAME(edge?, n)` | Platz aus Zählweise (5.4), `slot` setzen, `edge` setzen, Platz an `group` hängen. Außerhalb 1…Plätze: Widerspruch W2. |
| `STEP(+1)` („daneben“, „die nächste“) | `slot` einen Platz weiter, weg von `edge`. Ohne `slot`: Unklarheit U1. |
| `STEP(-1)` („die davor“, „die vorige“) | einen Platz zurück, zur `edge` hin. |
| `ORD(n)` ohne Nomen („die dritte“) | wie `FRAME(edge, n)` mit der aktuellen Kante. |
| `ORD(LAST)` („die letzte“) | Platz an der Gegenkante. |
| `RANGE(a, b)` („drei bis sechs“) | `group = [a…b]` (über die Kante umgerechnet), `slot = b`. |
| `GROUP(k)` („beide“, „alle drei“) | Ziel sind die letzten k Plätze aus `group`. Weniger vorhanden: Unklarheit U1. |
| `ATTR(...)` | bindet an die Platz-Angabe desselben Satzteils, sonst an `group`-Ziel, sonst an `slot`. Nichts davon: Unklarheit U1. |
| `FIELD(...)` | bindet an die Durchsicht, braucht keinen Zeiger. |
Jeder Satzteil hat höchstens eine Platz-Angabe. Ein Zargen-Wechsel leert Platz und Gruppe. So kann eine Angabe nie in eine andere Zarge rutschen.
### 5.4 Zählweise
Grundlage ist die Platz-Nummer im Aufbau: von links, mit Blick vom Imker hinter der Beute auf das Flugloch (Beute A4, Rähmchen quer zum Flugloch, Annahme B2).
| Gesagt | Zählweise | „links N“ | „rechts N“ | „Rähmchen N“, „die N-te“ |
|---|---|---|---|---|
| „von hinten“, „Flugloch vorn“, „von hinten gezählt“ | `REAR` | Platz N | Platz S+1−N | von der zuletzt benutzten Kante dieser Zarge, sonst von links (Platz N) |
| „von der linken Seite“, „von links (Seite)“ | `SIDE_LEFT` | Widerspruch W5 | Widerspruch W5 | N-tes vom Imker aus = Platz N |
| „von der rechten Seite“ | `SIDE_RIGHT` | Widerspruch W5 | Widerspruch W5 | N-tes vom Imker aus = Platz S+1−N |
| „von der Seite“ ohne Seite | – | Frage F2: „Von welcher Seite, links oder rechts, von hinten gesehen?“ |
S = Anzahl Plätze der Zarge. „Links“ und „rechts“ einer Seite meinen die Seitenwände, gesehen vom Imker hinter der Beute.
**Unbekannte Zählweise:** Fällt in einer Zarge die erste Rähmchen-Angabe ohne bekannte Zählweise, hält der Parser diesen Satzteil zurück und fragt einmal (F1): „Zarge zwei: Zählst du wie bei Zarge eins von hinten?“ (ohne vorige Zarge: „Zählst du von hinten oder von der Seite?“). „Ja“ übernimmt das Angebot. Die zurückgehaltenen Satzteile werden danach gebunden. Diese Frage zählt nicht zum Budget, gilt aber nur einmal je Zarge. Bleibt sie unbeantwortet, gilt `REAR` und die Zarge bekommt den offenen Punkt „Zählweise angenommen: von hinten“.
### 5.5 Angaben und Felder
**Je Rähmchen** (in `HiveFrame`, Abschnitt 7.1):
| Gesagt | Ergebnis |
|---|---|
| „Brutwabe“, „Drohnenwabe“, „Honigwabe“, „Pollenwabe“, „Futterwabe“, „Mittelwand“, „Leerwabe“, „Baurahmen“/„Drohnenrahmen“, „Schied“, „Lücke“/„kein Rähmchen“ | `type` = `BROOD`, `DRONE`, `HONEY`, `POLLEN`, `FEED`, `FOUNDATION`, `EMPTY_COMB`, `DRONE_FRAME`, `DIVIDER`, `NONE` (Wörter aus `FRAME_TYPE_LABELS`) |
| „25 % Futter“, „Futter 25 %“, „25 % Honig“ | `fill.feedPct = 25` (Honig zählt als Futter) |
| „30 % Pollen“ | `fill.pollenPct = 30` |
| „40 % Brut“ | `fill.broodPct = 40` |
| „25 %“ ohne Inhaltswort | Inhalt aus der Art des Ziel-Rähmchens: `FEED`/`HONEY` → Futter, `POLLEN` → Pollen, `BROOD`/`DRONE` → Brut. Andere Art oder unbekannt: offener Punkt „25 % wovon?“ |
| „auf einer Seite 50 % Brut“ | `broodPct = 25` (Mittel beider Seiten), Bemerkung „eine Seite“ |
| „wenig/etwas/kaum Brut“, „mittel Brut“, „viel Brut“, „voll Brut“ | `fill.broodAmount = LOW`/`MEDIUM`/`HIGH` (nur ohne Prozentzahl) |
| „keine Brut“ | `fill.broodPct = 0` |
| „Futterkranz gering/schmal/wenig“, „mittel/normal“, „breit/kräftig/gut“, „kein Futterkranz“ | `fill.feedRim = LOW`/`MEDIUM`/`HIGH`/`NONE` |
| „sieht gut aus“, „schön“, „prima“, „schlecht“ (Liste `voice-remarks.ts`) | `note` wörtlich, höchstens 120 Zeichen |
| „zwei Weiselzellen unten am Rand“ | `note` „zwei Weiselzellen unten am Rand“ **und** Durchsicht `queenCellsPresent = true` |
| „Spielnäpfchen“ | nur `note` (sind keine Weiselzellen) |
| „Königin auf dieser Wabe“ | `note` „Königin“ **und** `queenSeen = true` |
**Je Durchsicht** (Felder von `CreateInspectionDto`, Grenzen wie dort; Skalen-Richtung aus dem Formular, „5 = gut“ bzw. bei Schwarmstimmung 5 = stark):
| Gesagt | Feld und Wert |
|---|---|
| „Königin gesehen“ / „Königin nicht gesehen“ | `queenSeen` true / false |
| „Stifte da“, „Stifte gesehen“ / „keine Stifte“ | `eggsPresent` true / false |
| „offene Brut“ / „keine offene Brut“ | `openBroodPresent` |
| „verdeckelte Brut“, „gut verdeckelt“ | `cappedBroodPresent = true` |
| „Weiselzellen“ / „keine Weiselzellen“ | `queenCellsPresent` |
| „Brutbild 4“, „Brutbild geschlossen“ (5), „lückenhaft“ (1) | `broodPattern` |
| „Sanftmut 4“, „sehr sanft“ (5), „stechlustig“ (1) | `gentleness` |
| „Wabensitz 4“, „sitzt ruhig“ (5), „laufen weg“ (1) | `combBehavior` |
| „Schwarmstimmung 2“, „keine Schwarmstimmung“ (1), „starke Schwarmstimmung“ (5) | `swarmMood` |
| „ruhig“ allein | offener Punkt: „Ruhig: Sanftmut, Wabensitz oder Schwarmstimmung?“ (Ankerwort in drei Skalen, gegensätzlich) |
| „Futter ausreichend/reichlich“, „Futter niedrig/knapp“, „Futter kritisch“ | `feedStatus` `GOOD` / `WEAK` / `CRITICAL` |
| „sitzt auf acht Waben“, „acht Waben besetzt“, „Volksstärke 8“ | `colonyStrength = 8` (0–40) |
| „vier Brutwaben“ (Zahl genannt, ohne Platz-Bezug) | `broodFrames = 4` |
| „Varroa …“, „Honigraum …“, „Maßnahme …“, „Notiz …“ | Rest des Satzteils wörtlich in `varroaIssues`, `honeySuperStatus`, `measures`, `notes` (angehängt, höchstens 500 Zeichen) |
| Krankheitswörter („Kalkbrut“, „Sackbrut“, „Faulbrut“, „Räuberei“, Liste) | Satzteil wörtlich in `healthIssues`; „Faulbrut“ zusätzlich oben in der Zusammenfassung (anzeigepflichtige Seuche, Hinweis auf Amtstierarzt wie T6) |
Nichts wird aus anderen Angaben geschlossen (T7 Regel 1). Futterkranz sagt nichts über `feedStatus`. `broodFrames` aus den Rähmchen erscheint nur als Hinweis „Laut Rähmchen: 4 Brutwaben“ mit „Übernehmen“ (Beute §7).
### 5.6 Umbau in kleinen Sätzen
Nur diese Sätze erzeugen Aufbau-Operationen. Alles andere zum Aufbau wird ein offener Punkt.
| Gesagt | Operation (Vorschlag) |
|---|---|
| „Zarge aufgesetzt“, „Honigraum aufgesetzt“ | `addPart` einer Zarge vom Typ der obersten Zarge mit allen Plätzen `FOUNDATION`, direkt über der obersten Zarge (unter Folie und Deckel). Die neue Zarge wird für die Sitzung die nächste Nummer. |
| „Bausperre raus“ / „Bausperre rein“ | `removePart` bzw. `addInsert` am Boden (Beute §4.1) |
| „Rähmchen N raus“, „Lücke bei N“ | `setFrame` mit `type: NONE` |
### 5.7 Befehle
Erkannt nur als eigener Satz oder am Satzende, mit kurzer Wortliste:
| Gesagt | Wirkung |
|---|---|
| „Durchsicht beendet“, „Durchsicht fertig“ | ZUSAMMENFASSUNG |
| „Streich das“, „Rückgängig“, „das war falsch“ | nimmt die Befunde des letzten Satzes zurück, Ansage „Gestrichen: Rähmchen 8, Brutwabe.“ |
| „Nein, …“ direkt nach einer Angabe | ersetzt den letzten Wert desselben Ziels, keine Rückfrage, Befund `corrected` |
| „Wo bin ich?“ | Ansage „Zarge eins, von hinten, Rähmchen acht.“ |
| „Wiederhole“, „Was hast du?“ | Ansage des letzten Befunds |
| „Taschenmodus“, „Taschenmodus aus“ | 3.3 |
| „weiter“ | aus PAUSE oder ZUSAMMENFASSUNG zurück nach HÖRT |
| „übernehmen“, „passt“ | nur in ZUSAMMENFASSUNG: Bestätigung (Abschnitt 9) |
| „verwerfen“ | nur in ZUSAMMENFASSUNG, mit Rückfrage „Wirklich alles verwerfen? Sag ‚ja, verwerfen‘.“ |
### 5.8 Rest-Text und Weg zur KI
Nach dem Parsen bleibt ein Rest. Er wird so behandelt (token-arm, nichts verschwindet still):
| Rest | Art | Weg |
|---|---|---|
| leer oder nur Füllwörter („so“, „also“, „na“, „mal sehen“, Flüche, Liste) | `FILLER` | verworfen |
| kurze Wertung aus `voice-remarks.ts`, Platz-Zeiger vorhanden | `FRAME_NOTE` | Bemerkung am Rähmchen |
| enthält ein Fachwort, eine Zahl oder ein Bezugswort und keine Alternative ergab eine Bindung | `NEEDS_AI` | Satz an die KI (Abschnitt 8) |
| sonst | `UNUSED` | grau in der Mitschrift, in der Zusammenfassung unter „Nicht verwendet (3)“ sichtbar |
Ein Satz geht nur einmal an die KI, als Ganzes, mit dem Zeiger-Zustand **vor** dem Satz. Was der Parser im selben Satz schon gebunden hat, wird mitgeschickt, damit die KI es nicht doppelt liefert (8.3).
### 5.9 Beispiele mit erwarteter Auswertung
Aufbau: Zarge 1 und Zarge 2 mit je 10 Plätzen, Zählweise unbekannt. Jede Zeile ist ein Fixture-Fall (Abschnitt 15.1).
| Nr. | Gesagt | Erwartet |
|---|---|---|
| 1 | „Zarge ganz unten“ | `BOX(1)`; Zeiger Zarge 1, kein Platz |
| 2 | „von hinten, Flugloch vorn“ | Zarge 1 `REAR` |
| 3 | „Rähmchen rechts eins sieht gut aus, ist eine Futterwabe“ | Z1 P10: `type FEED`, `note "sieht gut aus"`; Kante R, Gruppe [10] |
| 4 | „die daneben ist eine Pollenwabe, beide ca. 25 %“ | Z1 P9: `type POLLEN`; Gruppe [10, 9]; „beide“ → P10 `feedPct 25 ≈`, P9 `pollenPct 25 ≈` |
| 5 | „die dritte ist eine Brutwabe, wenig Brut, Futterkranz gering“ | `ORD(3)` von R → Z1 P8: `type BROOD`, `broodAmount LOW`, `feedRim LOW` |
| 6 | „Königin gesehen, Stifte da, Sanftmut vier“ | Durchsicht: `queenSeen true`, `eggsPresent true`, `gentleness 4` |
| 7 | „Rähmchen drei bis sechs Brutwaben, gut verdeckelt“ | REAR, Kante R → Plätze 8…5 (S+1−3 … S+1−6) `BROOD`; `cappedBroodPresent true` |
| 8 | „Rähmchen elf“ | Widerspruch W2, Frage: „Zarge eins hat zehn Plätze. Welches Rähmchen?“ |
| 9 | „Zarge zwei. Rähmchen drei ist eine Mittelwand“ | `BOX(2)`; Zählweise unbekannt → F1: „Zarge zwei: Zählst du wie bei Zarge eins von hinten?“ → „ja“ → Z2 P3 `FOUNDATION` |
| 10 | „von der rechten Seite. Die erste ist eine Leerwabe, die nächste auch“ | Z2 `SIDE_RIGHT`; P10 `EMPTY_COMB`; „die nächste auch“ → P9 `EMPTY_COMB` |
| 11 | „Rähmchen links zwei“ (in `SIDE_RIGHT`) | Widerspruch W5: „Du zählst von der rechten Seite. Meinst du das zweite von dir aus?“ |
| 12 | „Futter 60 %, Brut 50 %“ (auf P8) | Widerspruch W3: „Rähmchen acht: Futter 60 und Brut 50 sind über 100 Prozent. Welcher Wert stimmt nicht?“ |
| 13 | „ist eine Futterwabe … nein, Pollenwabe“ | `type POLLEN`, `corrected`, keine Frage |
| 14 | „Futterwabe, 70 % Brut“ | Befunde gesetzt; offener Punkt „Rähmchen acht: Futterwabe mit 70 % Brut?“ (Hinweis H1) |
| 15 | „ruhig“ | offener Punkt „Ruhig: Sanftmut, Wabensitz oder Schwarmstimmung?“ |
| 16 | „Leerwabe 30 %“ | offener Punkt „30 % wovon?“ |
| 17 | „die Bienen sind heute nervös wegen dem Gewitter“ | kein Treffer, Fachwort „Bienen“ → `NEEDS_AI`; erwartete KI-Antwort: `notes` „Bienen nervös (Gewitter)“, keine Skala (T7 Regel 3) |
| 18 | „Honigraum aufgesetzt“ | `addPart` Zarge 3 über Zarge 2, alle Plätze `FOUNDATION` |
| 19 | „streich das“ | Befunde aus Satz 18 entfernt, Ansage |
| 20 | „Durchsicht beendet“ | ZUSAMMENFASSUNG |
| 21 | „die daneben“ als erster Rähmchen-Bezug einer Zarge | Unklarheit U1, Frage: „Neben welchem Rähmchen?“ |
| 22 | „Zarge drei“ (Beute hat zwei Zargen, nichts aufgesetzt) | Widerspruch W1: „Die Beute hat laut App zwei Zargen. Ist eine dazugekommen?“ „ja“ → Vorschlag wie Nr. 18; „nein“ → „Welche Zarge meinst du?“ |
### 5.10 Ohne Aufbau
Hat das Volk keinen Aufbau, startet die Sitzung trotzdem. Zargen und Plätze werden ohne Grenzprüfung gezählt (höchstens 30). Rähmchen-Befunde landen nicht als Operation, sondern als Textblock in `notes` („Zarge 1, R10: Futterwabe, 25 % Futter“). Die Zusammenfassung sagt: „Diese Beute ist noch nicht angelegt. Die Rähmchen stehen in den Notizen.“ Fehlt dem Nutzer `colonies:update`, gilt dasselbe (Beute §7).
## 6. Widersprüche und Rückfragen
### 6.1 Sofort fragen nur bei
| Code | Fall | Frage (Beispiel) |
|---|---|---|
| W1 | Zarge gibt es nicht | „Die Beute hat laut App zwei Zargen. Ist eine dazugekommen?“ |
| W2 | Platz außerhalb der Zarge | „Zarge eins hat zehn Plätze. Welches Rähmchen?“ |
| W3 | Anteile eines Rähmchens über 100 % | „Rähmchen acht: Futter 60 und Brut 50 sind über 100 Prozent. Welcher Wert stimmt nicht?“ |
| W4 | Dasselbe Ziel bekommt in derselben Sitzung zwei verschiedene Arten oder Werte, ohne „nein“ | „Rähmchen acht: erst Futterwabe, jetzt Brutwabe. Was stimmt?“ |
| W5 | „links/rechts“ bei Zählweise von der Seite | „Du zählst von der rechten Seite. Meinst du das zweite von dir aus?“ |
| U1 | Rähmchen-Angabe ohne Bezug („daneben“ ohne Platz, „beide“ ohne zwei Plätze, Art ohne Zeiger) | „Für welches Rähmchen?“ |
| U2 | KI meldet einen Navigationssatz, den sie nicht sicher binden kann (8.4) | „Wo bist du gerade? Sag zum Beispiel: Zarge zwei, Rähmchen drei.“ |
| F1, F2 | Zählweise nötig und unbekannt (5.4) | eigene Regel, nicht im Budget |
Begründung der Auswahl: Diese Fälle betreffen das Rähmchen, das der Imker gerade in der Hand hat. Später weiß er es nicht mehr. Alles andere kann warten.
### 6.2 Budget und Ablauf
- Höchstens **5 Sofort-Fragen** je Sitzung (`VOICE_QUESTION_BUDGET`), dazu F1/F2 höchstens einmal je Zarge.
- Höchstens eine Frage offen. Zwischen zwei Fragen liegt mindestens ein normaler Satz.
- Dieselbe Frage-Art wird in derselben Zarge nicht zweimal gestellt.
- Ist das Budget aufgebraucht, wird jeder weitere Fall ein offener Punkt, ohne Ansage.
- Die Frage ist kurz (höchstens 12 Wörter), Stimme `de-DE`, Tempo 1,05.
- Bis zur Antwort hält der Parser nur die betroffenen Satzteile zurück. Andere Angaben im selben Satz werden gebunden.
### 6.3 Antworten
Der nächste Satz wird zuerst mit der kleinen Antwort-Grammatik geprüft: „ja“, „genau“, „stimmt“, „nein“, Zahlen, „von hinten“, „von der linken/rechten Seite“, „links“, „rechts“, Namen der angebotenen Möglichkeiten, „weiß nicht“, „später“. Passt der Satz nicht, ist er ein normaler Satz, und die Frage wird still ein offener Punkt. Gleiches gilt nach 20 Sekunden ohne Satz. Keine Wiederholung der Frage.
### 6.4 Gesammelte Punkte (für die Zusammenfassung)
| Code | Fall |
|---|---|
| H1 | Art und Anteile passen nicht zusammen (Futterwabe mit mehr Brut als Futter, Brutwabe ohne Brut) |
| H2 | Prozent ohne Inhalt bei einer Art ohne festen Inhalt |
| H3 | Mehrdeutiges Ankerwort („ruhig“) |
| H4 | KI-Ergebnis mit `unclear` |
| H5 | Umbau gesagt, aber nicht in 5.6 |
| H6 | Fragen über dem Budget, unbeantwortete Fragen |
| H7 | Zählweise angenommen |
| H8 | Satz nicht ausgewertet (KI-Fehler, Sitzungsgrenze) mit Wortlaut |
## 7. Datenmodell
Alles additiv. Der Validator lehnt heute unbekannte Schlüssel ab, darum wird das Backend vor dem Frontend ausgerollt.
### 7.1 Rähmchen: `fill` und `note`
```ts
interface HiveFrame {
slot: number;
type: FrameType;
name?: string; // wie bisher, max. 40
fill?: FrameFill; // neu
note?: string; // neu, max. 120 Zeichen
}
interface FrameFill {
feedPct?: number; // ganze Zahl 0–100, Futter inkl. Honig
pollenPct?: number; // ganze Zahl 0–100
broodPct?: number; // ganze Zahl 0–100
broodAmount?: 'LOW' | 'MEDIUM' | 'HIGH'; // nur ohne broodPct
feedRim?: 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH';
checkedOn: string; // Berliner Tag YYYY-MM-DD, setzt der Server
}
```
- Prozent bedeutet Anteil der Wabenfläche, beide Seiten gemittelt.
- **Neue Validator-Regeln** (Backend `hive-validate.ts`, Frontend `validate.ts`): `FRAME_KEYS` um `fill`, `note`; Schlüssel in `fill` nur aus der Liste (`UNKNOWN_FIELD`); Prozent ganze Zahl 0–100 (`STATE_OUT_OF_RANGE`); `feedPct + pollenPct + broodPct ≤ 100` (neuer Grund `RULE_FILL_SUM`); `broodAmount` nicht zusammen mit `broodPct` (`RULE_FILL_SUM`); `note` höchstens 120 Zeichen (`LIMIT_TEXT`); `fill` nur bei Art außer `NONE` und `DIVIDER`.
- `schemaVersion` bleibt 1: Die Felder sind optional, alte Stände bleiben gültig.
### 7.2 `setFrame` erweitert
```ts
| { op: 'setFrame'; boxId: string; slot: number;
type?: FrameType; // fehlt = bleibt (neu: optional)
name?: string | null; // wie bisher
fill?: Omit<FrameFill, 'checkedOn'> | null; // fehlt = bleibt, null = löschen
note?: string | null } // fehlt = bleibt, null = löschen
```
- `applySetFrame` behält heute nur `slot`, `type`, `name` und würde neue Felder verlieren. Neu: Es übernimmt das bestehende Rähmchen und ändert nur genannte Felder. Mindestens eines von `type`, `name`, `fill`, `note` muss da sein.
- `fill` ersetzt das ganze Objekt: Ein Befund ist der Stand dieses Tages. Nicht genannte Anteile von früher gelten nicht weiter. `checkedOn` setzt der Server auf `occurredOn` der Änderung (ein Wert vom Client wird überschrieben).
- `setFrames` (Umbauen) übernimmt `fill` und `note` unverändert, wenn der Client sie mitschickt. Der Frontend-Editor gibt sie weiter.
- DTO `HiveOpDto`: `type` wird `@IsOptional()`, neue Felder `fill` (`@IsObject`, tiefe Prüfung im Validator) und `note` (`@MaxLength(120)`).
- Höchstens 100 Operationen je Anfrage bleiben. Teil 4 erzeugt eine `setFrame` je berührtes Rähmchen; zwei volle Zargen sind 20 Operationen.
- Fixtures in `test/fixtures/hive-ops/`: `set-frame-keeps-fill.json`, `set-frame-fill-replaces.json`, `set-frame-type-optional.json`, `fill-sum-over-100.json`, `fill-on-none-rejected.json`.
### 7.3 Abgeleitete Werte
`deriveHiveFacts` bekommt `fillTotals: { feedFrames, pollenFrames, broodFrames, checkedOn }`: Summe der Anteile in Waben-Äquivalenten (z. B. 3,5 Waben Futter), nur über Rähmchen mit `fill`, mit dem ältesten `checkedOn`. Die Karte zeigt „≈ 3,5 Waben Futter (Stand 12.10.)“. `describeHiveOps` beschreibt `setFrame` mit Anteilen („R8 Brutwabe, wenig Brut, Futterkranz gering“). Nichts davon schreibt in `Colony` oder `Inspection`.
### 7.4 Sitzungen im Backend
```prisma
enum VoiceSessionMode { LIVE RECORDED }
enum VoiceSessionStatus { OPEN CLOSED READY CONFIRMED DISCARDED FAILED EXPIRED }
enum VoiceRecognizer { DEVICE BROWSER_CLOUD WHISPER }
// Eine Sprach-Durchsicht. Hält keinen Text außer den kurzlebigen Sätzen in
// VoiceUtterance und dem Ergebnis einer Aufnahme-Durchsicht (resultEnc).
model VoiceSession {
id String @id @default(cuid())
ownerUserId String
owner User @relation(fields: [ownerUserId], references: [id])
colonyId String
colony Colony @relation(fields: [colonyId], references: [id])
inspectionDate DateTime @db.Date
mode VoiceSessionMode
status VoiceSessionStatus @default(OPEN)
recognizer VoiceRecognizer?
resultEnc String? // nur RECORDED: geprüftes Ergebnis (10.3), Schlüsselbund „voice“
llmCalls Int @default(0)
inputTokens Int @default(0)
cacheReadTokens Int @default(0)
cacheWriteTokens Int @default(0)
outputTokens Int @default(0)
model String?
utteranceCount Int? // vom Client beim Schließen gemeldet, nur Zahl
parsedCount Int?
questionsAsked Int?
inspectionId String? // gesetzt bei CONFIRMED
createdAt DateTime @default(now())
closedAt DateTime?
contentExpiresAt DateTime // LIVE: createdAt + 24 h, RECORDED: + 7 Tage
contentDeletedAt DateTime?
updatedAt DateTime @updatedAt
utterances VoiceUtterance[]
captures VoiceCapture[]
@@index([ownerUserId, status])
@@index([contentExpiresAt])
}
enum VoiceUtteranceStatus { QUEUED PROCESSING DONE FAILED }
// Ein unverstandener Satz auf dem Weg zur KI. Text und Ergebnis leben nur
// bis zur Abholung durch das Gerät, höchstens 24 h.
model VoiceUtterance {
id String @id @default(cuid())
sessionId String
session VoiceSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
seq Int
textEnc String? // Satz, Schlüsselbund „voice“
contextEnc String? // Zeiger-Zustand und schon Gebundenes (8.3)
resultEnc String? // geprüftes KI-Ergebnis
status VoiceUtteranceStatus @default(QUEUED)
attempts Int @default(0)
availableAt DateTime @default(now())
claimedAt DateTime?
claimToken String?
lastErrorCode String?
inputTokens Int?
outputTokens Int?
createdAt DateTime @default(now())
@@unique([sessionId, seq])
@@index([status, availableAt])
}
```
- **CHECK-Constraints:** `("status" = 'CONFIRMED') = ("inspectionId" IS NOT NULL)`; `"mode" = 'RECORDED' OR "resultEnc" IS NULL`; `"seq" BETWEEN 0 AND 5000`.
- **`VoiceCapture` (T7) bekommt:** `sessionId String?` (Relation), `chunkIndex Int?`, `isLastChunk Boolean @default(false)`, `contextEnc String?` (Zeiger beim Beginn des Stücks, 10.2). CHECK: `"sessionId" IS NULL OR "chunkIndex" IS NOT NULL`. Die Whisper-Ausgabe (T7 6.3) bekommt `segments: [{ start, end, text }]`.
- **`CreateInspectionDto`:** `voiceSessionId?: string` (cuid-Zeichensatz, nicht gespeichert) und `hiveChange.source?: 'INSPECTION' | 'AI_SUGGESTION'` (Standard `INSPECTION`). `AI_SUGGESTION` ist nur mit gültiger `voiceSessionId` erlaubt, sonst 400. `POST /colonies/:id/hive/changes` lehnt `AI_SUGGESTION` weiter ab (Beute §6.2, bis Teil 3).
- **Ereignisse:** `VoiceSessionReady` (`{ sessionId, colonyId }`, nur RECORDED) und `VoiceSessionFailed` (`{ sessionId, code }`), ohne Text, keine Webhooks. `HiveChanged` trägt wie bisher `source`.
- **Rechte:** wie T7: `voice-captures:use` und `inspections:create`; Beute-Operationen zusätzlich `colonies:update` (Beute §7). Kein neues Recht.
- **Backup:** `VoiceSession`, `VoiceUtterance` in `MODELS_EXCLUDED_FROM_BACKUP` (flüchtig, personenbezogen). `HiveFrame.fill/note` sind Teil des Aufbau-JSON und damit gesichert.
- **Audit:** Statuswechsel von `VoiceSession` mit `CREATE`/`UPDATE`, ohne Inhalt (T7 §3).
## 8. KI für unverstandene Sätze
### 8.1 Wann
Nur bei `restKind = NEEDS_AI` (5.8). Grenzen: höchstens 40 Aufrufe je Sitzung (`VOICE_LIVE_MAX_CALLS`), 30 je Minute und Nutzer, 400 je Tag im ganzen Betrieb (`VOICE_LIVE_DAILY_CALLS`). Darüber: offener Punkt H8 mit Wortlaut, keine Meldung während der Sitzung.
### 8.2 Modell
| Modell | Eignung laut Anthropic | Latenz | Preis Eingabe / Ausgabe je 1 Mio. Tokens | Cache-Lesen | Cache-Schreiben 1 h |
|---|---|---|---|---|---|
| **`claude-haiku-5-5`** | „high-volume, latency-sensitive tasks such as classification, extraction, and routing“ | „Fastest“ | 0,10 $ / 0,50 $ (bis 100 000 Tokens Prompt) | 0,01 $ | 0,20 $ |
| `claude-sonnet-5-5` | „best combination of speed and intelligence“ | „Fast“ | 2,00 $ / 10,00 $ | 0,10 $ | 4,00 $ |
Quelle: Anthropic-Doku, Seiten „Pricing“ (`https://platform.claude.com/docs/en/about-claude/pricing`), „Models overview“ (`…/about-claude/models/overview`) und „Prompt caching“ (`…/build-with-claude/prompt-caching`), abgerufen am 2026-10-16. Mindestlänge für den Cache: 512 Tokens bei beiden Modellen.
**Entscheidung:** `claude-haiku-5-5`, `effort: 'low'`, über `VOICE_LIVE_MODEL`. Die Aufgabe ist ein einzelner Satz mit festem Schema, also Extraktion, genau der Einsatz, für den Haiku 5.5 gedacht ist. Haiku antwortet am schnellsten, und das zählt live mehr als bei T7. Die Sicherheit kommt aus Schema, Zitat-Prüfung und Bestätigung, nicht aus der Modellgröße (wie T7 7.1). Ist eine Antwort nach der Serverprüfung ungültig (`EXTRACT_INVALID`), gibt es einen zweiten Versuch mit `VOICE_LIVE_FALLBACK_MODEL` (Standard `claude-sonnet-5-5`). Die Gold-Menge (15.4) entscheidet vor dem Rollout. Erreicht Haiku die Ziele nicht, wird Sonnet der Standard (eine Umgebungsvariable). Modellkennungen stehen nur in `voice-config.ts`.
### 8.3 Anfrage
- **System (im Cache, `cache_control: { type: 'ephemeral', ttl: '1h' }` am letzten System-Block):** Rolle, Regeln aus T7 6.4 (nichts schließen, Verneinung ist ein Wert, Zahlen nur wenn gesagt, Skalen nur mit Zahl oder Ankerwort, keine Namen, kein Wetter), die Grammatik aus 5.4–5.6 in Kurzform, Fachwortliste, Felder mit Wertebereichen, Ziel-Regeln (nur Plätze aus dem Zustand oder ausdrücklich genannte), 8 kurze Beispiele. Rund 2 500 Tokens. Der Text ist fest und versioniert (`voice-live-prompt.ts`), damit der Cache trifft. Nichts Veränderliches (Datum, Volk, Nutzer) steht darin.
- **Ausgabe:** `output_config: { effort: 'low', format: { type: 'json_schema', schema } }`, Schema fest (8.4). `max_tokens` 600, nicht streamend, Zeitlimit 20 s, SDK-Wiederholungen 1. Kein `tool_choice`, keine Sampling-Parameter (T7).
- **Nutzernachricht (nicht im Cache, rund 200 Tokens):**
```
<zustand>{"z":1,"zargen":2,"plaetze":[10,10],"zw":"REAR","kante":"R","platz":8,"gruppe":[10,9,8],
"arten":{"10":"FEED","9":"POLLEN","8":"BROOD"},"schon":["P8 type BROOD"]}</zustand>
<satz>die Bienen sind heute nervös wegen dem Gewitter</satz>
```
Nur wenn der Satz mit einem Bezugswort beginnt („die“, „das“, „auch“, „dort“), kommt der vorige Satz als `<vorher>` dazu (rund 30 Tokens). Nie die ganze Mitschrift.
- Inhalte in `<satz>` sind Daten, nie Anweisung (T7 6.4). Die KI hat keine Werkzeuge.
### 8.4 Schema und Prüfung
```json
{
"type": "object", "additionalProperties": false,
"required": ["findings", "navigation", "unclear"],
"properties": {
"findings": { "type": "array", "maxItems": 8, "items": {
"type": "object", "additionalProperties": false,
"required": ["target", "box", "slot", "field", "value", "quote"],
"properties": {
"target": { "type": "string", "enum": ["FRAME", "INSPECTION"] },
"box": { "type": ["integer", "null"] },
"slot": { "type": ["integer", "null"] },
"field": { "type": "string", "enum": [
"type", "feedPct", "pollenPct", "broodPct", "broodAmount", "feedRim", "note",
"colonyStrength", "broodFrames", "broodPattern", "openBroodPresent", "cappedBroodPresent",
"eggsPresent", "queenSeen", "queenCellsPresent", "swarmMood", "gentleness", "combBehavior",
"feedStatus", "honeySuperStatus", "healthIssues", "varroaIssues", "measures", "notes" ] },
"value": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "boolean" } ] },
"quote": { "type": "string" } } } },
"navigation": { "type": "boolean" },
"unclear": { "type": ["string", "null"] }
}
}
```
**Serverprüfung** (`validateLiveResult`, rein, vor dem Speichern in `resultEnc`):
1. Typ und Grenzen je Feld aus einer Tabelle `FIELD_SPECS`, abgeleitet aus `CreateInspectionDto` und 7.1 (Volksstärke 0–40, Skalen 1–5, Prozent 0–100, Aufzählungen genau). Falsch: verwerfen.
2. `target = FRAME` braucht `box` und `slot` innerhalb von `zargen`/`plaetze` aus dem Kontext, und der Platz muss im Kontext vorkommen (`platz`, `gruppe`) **oder** als Zahl im Satz stehen. Sonst verwerfen. So kann die KI den Zeiger nicht verschieben.
3. Zitat-Prüfung wie T7 6.5 (normalisiertes Zitat im normalisierten Satz). Sonst verwerfen.
4. Doppelt zu `schon`: verwerfen.
5. Freitext: Steuerzeichen und `<`/`>` entfernt, gekürzt (Felder 500, `note` 120).
6. `navigation = true` ohne verwertbaren Befund ergibt im Gerät U2 (6.1), wenn Budget und Alter (< 30 s) passen, sonst H4.
### 8.5 Reihenfolge
Jedes KI-Ergebnis trägt die `seq` seines Satzes und gebundene Ziele. Das Gerät wendet es mit dieser `seq` an: Hat ein späterer Satz dasselbe Ziel schon anders belegt, gewinnt der spätere, das KI-Ergebnis steht als „Zuvor genannt“ dabei. Die Ankunftszeit spielt keine Rolle.
### 8.6 Token-Budget und Kosten je Durchsicht
Annahmen für eine typische Durchsicht von 15 Minuten: zwei Zargen, rund 70 erkannte Sätze, Parser-Quote 80 % → **15 KI-Aufrufe**. Je Aufruf: 2 500 Tokens fester Teil (Cache), 200 Tokens neu, 300 Tokens Ausgabe (JSON rund 120, dazu Denken bei `effort: low`). Haiku 5.5 und Sonnet 5.5 nutzen den neuen Tokenizer (rund 30 % mehr Tokens als ältere Modelle), die Zahlen sind damit gerechnet.
| Posten | `claude-haiku-5-5` | `claude-sonnet-5-5` |
|---|---|---|
| Cache schreiben (1 × 2 500, 1 h) | 0,00050 $ | 0,01000 $ |
| Cache lesen (14 × 2 500) | 0,00035 $ | 0,00350 $ |
| Neue Eingabe (15 × 200) | 0,00030 $ | 0,00600 $ |
| Ausgabe (15 × 300) | 0,00225 $ | 0,04500 $ |
| **Summe je Durchsicht** | **≈ 0,0034 $ (0,3 Cent)** | **≈ 0,065 $ (6,5 Cent)** |
| Ohne Cache | ≈ 0,0063 $ | ≈ 0,10 $ |
| Obergrenze 40 Aufrufe (mit Cache) | ≈ 0,0083 $ | ≈ 0,17 $ |
| Jahr bei 4 000 Durchsichten (Annahme T7 7.1) | ≈ 14 $ | ≈ 260 $ |
Der Cache halbiert die Kosten bei Haiku und spart rund ein Drittel bei Sonnet. Den größten Hebel hat der Parser: Ohne ihn gingen alle 70 Sätze an die KI, also rund das Fünffache. Die Ausgabe ist der größte Posten; darum ist das Schema knapp und `maxItems` klein. Die Batch-API (50 % Rabatt) passt nicht zu live und lohnt bei Aufnahme-Durchsichten den Tag Verzug nicht.
Gezählt werden je Satz und Sitzung `inputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `outputTokens` und `model`. Die System-Seite zeigt den Monatswert (T7 7.2). Ausgabenlimit in der Anthropic-Konsole wie T7 (Vorschlag 10 $ im Monat reicht weiter).
## 9. Abschluss
### 9.1 Zusammenfassung
Nach „Durchsicht beendet“ wartet die App höchstens 10 s auf offene KI-Ergebnisse (Anzeige „Werte noch 2 Sätze aus“). Dann spricht sie eine kurze, feste Zusammenfassung (Vorlage in `voice-summary.ts`, kein KI-Aufruf):
> „Zusammenfassung B3. Zarge eins: zehn Rähmchen, vier Brutwaben, zwei Futterwaben, eine Pollenwabe. Zarge zwei: drei Rähmchen. Königin gesehen, Stifte da, Sanftmut vier. Zwei Punkte offen. Erstens: ‚ruhig‘, meinst du Sanftmut, Wabensitz oder Schwarmstimmung?“
- Offene Punkte werden **einzeln** gefragt, höchstens 5, ohne Budget (die Sitzung endet ja). Antwort mit der Antwort-Grammatik, „weiter“ überspringt. Übrige Punkte stehen nur auf dem Bildschirm.
- Danach: „Sag ‚übernehmen‘, ‚weiter‘ zum Fortsetzen oder ‚verwerfen‘. Oder tippe.“
- Auf dem Bildschirm steht dieselbe Zusammenfassung ausführlich: je Zarge die Rähmchen mit Art, Anteilen, Bemerkung und Zitat; die Felder der Durchsicht; offene Punkte mit Knöpfen (z. B. „Sanftmut“, „Wabensitz“, „Schwarmstimmung“, „Ignorieren“); „Nicht verwendet (3)“ aufklappbar.
### 9.2 Bestätigen und Entwurf
„übernehmen“ oder Tipp auf „Übernehmen“ ist die ausdrückliche Bestätigung (wie „Alle übernehmen“ mit Rückfrage in T7 6.6, die vorgelesene Zusammenfassung ist die Rückfrage):
- Die Felder der Durchsicht werden im Formular gesetzt und bleiben sichtbar „aus Sprache“ markiert, bis der Nutzer sie ändert oder speichert (T7 8.2). Gesprochen, aber nicht gebunden heißt: Feld bleibt leer.
- Die Beute-Operationen stehen im Abschnitt „Beute“ (Beute §7) als Liste „Änderungen an der Beute (14) · aus Sprache“ mit „Rückgängig“ je Eintrag. Der Aufbau zeigt die Vorschau.
- `broodFrames` aus den Rähmchen erscheint nur als Hinweis „Laut Rähmchen: 4“ mit „Übernehmen“.
- Gespeichert wird **nichts**. Der Entwurf liegt im Gerät (Entwurfs-Speicher des Formulars). Das Journal wird gelöscht. Online schickt das Gerät `POST /voice-sessions/:id/close` mit den Zählern.
**Operationen bauen** (`buildVoiceHiveOps`, rein): Je berührtes Rähmchen eine `setFrame` mit `type` (nur wenn anders als beim Start), `fill` (nur wenn etwas gesagt wurde, ersetzt) und `note` (nur wenn gesagt). Aufgesetzte Zargen zuerst als `addPart` mit Ids aus dem Gerät, dann ihre Rähmchen. Mehr als 100 Operationen (unrealistisch, über vier volle Zargen) teilt die App nicht auf, sondern zeigt „Zu viele Änderungen für eine Durchsicht“ und lässt den Nutzer Zargen abwählen.
### 9.3 Speichern
Tipp auf „Durchsicht speichern“ schickt `POST /inspections` mit `voiceSessionId` und `hiveChange: { ops, source: 'AI_SUGGESTION' }`. Der Server prüft in **einer** Transaktion (Muster T7 6.6 und Beute §7): Sitzung gehört dem Aufrufer, gleiches Volk, Status `OPEN`, `CLOSED` oder `READY`; dann Durchsicht, `HiveChange` mit Quelle `AI_SUGGESTION` und `inspectionId`, `HiveChanged`, `InspectionRecorded`, Sitzung `CONFIRMED`, Löschen aller Sätze, Ergebnisse und `resultEnc`, `contentDeletedAt = now()`.
- Sitzung abgelaufen oder schon bestätigt: 409 `VOICE_SESSION_GONE`. Der Client speichert erneut ohne `voiceSessionId` und mit `source: 'INSPECTION'`; die Eingaben bleiben (Muster T7 `VOICE_CAPTURE_GONE`).
- `HIVE_CONFLICT`: wie Beute §7 („Ohne Beute-Änderung senden“).
- Offline gespeichert: Die Durchsicht geht mit `voiceSessionId` in die Warteschlange (Beute L5, `drainHiveTimeline`). Live-Sitzungen ohne Server-Id (Start offline nicht erlaubt, aber Id kam nie an) werden ohne `voiceSessionId` gesendet; die Quelle wird dann `INSPECTION`.
## 10. Offline und Aufnahme-Durchsicht
### 10.1 Aufnahme-Durchsicht
Gilt ohne Netz beim Start, ohne Live-Fähigkeit (iOS, Firefox) und nach Netzverlust mit Browser-Cloud-Erkennung. Gleiche Sitzungs-Oberfläche, aber Status „Nimmt auf, Auswertung später“, keine Fragen, keine Befunde während der Aufnahme.
- Aufnahme mit `MediaRecorder` wie T7 6.1, in **Stücken zu höchstens 180 s** (T7-Grenze bleibt). Am Stückende beginnt nahtlos das nächste (Überlappung 1 s gegen abgeschnittene Wörter; Dopplungen erkennt der Parser nicht, die Überlappung wird beim Zusammenfügen über die Segment-Zeiten entfernt).
- Jedes Stück ist ein `pending-voice`-Eintrag (T7) mit `sessionId`, `chunkIndex`, `isLastChunk` und beim ersten Stück nach einem Wechsel `context` (Zeiger-Zustand bei Netzverlust).
- Ende wie live: „Durchsicht beendet“ wird hier nicht erkannt (keine Live-Erkennung). Darum Knopf „Durchsicht beendet“; das gesprochene Ende erkennt erst die Auswertung und schneidet dort ab.
- Höchstens 40 Stücke (2 Stunden) je Sitzung.
### 10.2 Netzverlust während einer Live-Sitzung
| Erkennung | Verhalten |
|---|---|
| auf dem Gerät | Live geht weiter: Parser, Zeiger, Rückfragen. KI-Sätze warten im Journal und gehen bei Netz raus (in `seq`-Reihenfolge). Ansage einmal: „Kein Netz. Ich verstehe weiter, prüfe Unklares später.“ |
| Browser-Cloud | Erkennung geht nicht mehr. Wechsel auf Aufnahme (10.1) mit dem aktuellen Zeiger als `context`. Ansage: „Kein Netz. Ich nehme jetzt auf und werte später aus.“ Bei Netz zurück: „Netz wieder da. Live geht weiter.“ |
Beides beendet die Sitzung nicht. Teile aus Aufnahme und Live landen im selben Entwurf, geordnet nach Zeit.
### 10.3 Spätere Auswertung
- Whisper (T7-CronJob) schreibt jedes Stück ab und liefert `segments`.
- Sind alle Stücke einer Sitzung bis `isLastChunk` `TRANSCRIBED`, übernimmt die Worker-Schleife `voice-live` die Sitzung: Stücke zusammenfügen, Sätze an Pausen ≥ 0,7 s oder Satzende trennen, Backend-Parser mit dem Start-Zeiger aus `context` (sonst leer), KI-Aufrufe für `NEEDS_AI` nacheinander mit demselben Prompt, Prüfung wie 8.4.
- Fragen gehen nicht. Jeder Fall aus 6.1 wird ein offener Punkt mit Antwort-Knöpfen (W2 „Welches Rähmchen?“ mit Platzauswahl, F1 „Von hinten / Linke Seite / Rechte Seite“). Was von einer offenen Antwort abhängt, bleibt bis zur Antwort ungebunden und erscheint dort.
- Ergebnis (Befunde, offene Punkte, Mitschrift-Abschnitte für Zitate) verschlüsselt in `VoiceSession.resultEnc`, Status `READY`, Ereignis `VoiceSessionReady`, Meldung „Sprach-Durchsicht für B3 ist ausgewertet“. Die Stücke folgen den T7-Fristen (Ton gelöscht nach Abschrift, Text in der Sitzung bis Bestätigung, höchstens 7 Tage).
- Der Parser ist derselbe wie live (gleiche Fixtures). Darum liefert die spätere Auswertung für dieselben Sätze dieselben Befunde (Fertig-Kriterium 6).
### 10.4 Darstellung
Die Meldung öffnet die Durchsicht mit `?voiceSession=<id>`. Oben dieselbe Zusammenfassung wie in 9.1 (nur sichtbar), offene Punkte mit Knöpfen, darunter „Alle übernehmen“ (Rückfrage mit Anzahl: „12 Felder und 14 Rähmchen übernehmen?“) und „Verwerfen“. Je Feld der T7-Hinweis `VoiceSuggestion` mit Zitat. Im Abschnitt „Beute“ je Zarge die Rähmchen-Vorschläge mit Vorher/Nachher und Zitat, „Übernehmen“ je Rähmchen. Speichern wie 9.3. Gab es einen Live-Teil, sind dessen Befunde schon im Entwurf; die neuen kommen als Vorschläge dazu, bei Zielkonflikt mit Hinweis „Live gesagt: …“.
## 11. API
Alle Pfade unter `/api/v1`, Recht `voice-captures:use`, Filter `ownerUserId = Aufrufer`, fremde Ids 404, `Cache-Control: no-store` (T7 §5).
| Methode | Pfad | Zweck |
|---|---|---|
| POST | `/voice-sessions` | `{ colonyId, inspectionDate, mode, recognizer }`, `@Idempotent()`. Volk lebend, Datum höchstens 7 Tage zurück. Antwort `{ id, limits: { maxCalls, questionBudget } }`. Höchstens 3 offene Sitzungen je Nutzer. |
| POST | `/voice-sessions/:id/utterances` | `{ seq, text, context }`; `text` 1–500 Zeichen, NFKC, ohne Steuerzeichen; `context` per DTO streng geprüft (Zahlen 1–30, Aufzählungen, `schon` höchstens 12 Einträge zu je 40 Zeichen). Doppelte `seq`: 200 mit bestehender Id. Antwort 202. Rate-Limit 30/min. |
| GET | `/voice-sessions/:id/results?afterSeq=` | Ergebnisse `[{ seq, status, result?, code? }]` ab `afterSeq`. Setzt nichts. |
| POST | `/voice-sessions/:id/ack` | `{ upToSeq }`: löscht `textEnc`, `contextEnc`, `resultEnc` bis dahin. |
| POST | `/voice-sessions/:id/close` | Zähler ohne Inhalt; Status `CLOSED`. |
| POST | `/voice-sessions/:id/discard` | Löscht alle Inhalte, Status `DISCARDED`. Idempotent. |
| GET | `/voice-sessions/:id` | Status; bei `READY` (RECORDED) das Ergebnis. |
| POST | `/voice-captures` (T7) | zusätzlich `sessionId`, `chunkIndex`, `isLastChunk`, `context`. |
| POST | `/inspections` | zusätzlich `voiceSessionId`, `hiveChange.source` (7.4, 9.3). |
| GET | `/voice-sessions/stats` | Zähler und Kosten der letzten 30 Tage ohne Nutzerbezug (`system:manage`). |
## 12. Oberfläche
Grundsätze aus T7 §8 (Handschuhe: Ziele ≥ 56 px, Hauptknopf 72 px; WCAG 2.2 AA; Status nie nur über Farbe).
### 12.1 Knopf und Sitzungsleiste
- Im Durchsicht-Formular oben neben dem Volk: **„Sprache an“** (Mikrofon-Symbol), Untertitel je nach Fähigkeit „Live“ oder „Aufnahme, Auswertung später“. Beim ersten Mal ein Hinweis (T7 9.5 erweitert): „Sprich nur selbst. Der Ton bleibt auf dem Handy. Nur Sätze, die die App nicht versteht, gehen als Text zur Auswertung an Anthropic.“ (bei Browser-Cloud: „Der Ton geht zur Erkennung an Google.“).
- Laufende Sitzung: feste Leiste über dem Formular (verdeckt keinen Fokus, WCAG 2.4.11) mit Status in Worten („Hört zu“, „Frage“, „Pause“, „Nimmt auf“, „Unterbrochen“), Zeiger „Zarge 1 · von hinten · Rähmchen 8 (Brutwabe)“, letzte Zeile „Verstanden: R8 Brutwabe, wenig Brut, Futterkranz gering“, Knöpfe „Durchsicht beendet“ (Hauptaktion), „Pause“, „Taschenmodus“.
- Aufklappbar „Mitschrift“: jeder Satz mit Zustand (verstanden, „wird geprüft“, „nicht verwendet“ grau), „Streichen“ je Satz.
- Live-Region (`aria-live="polite"`) meldet Befunde gebündelt höchstens alle 5 s; Fragen `assertive`.
- Keine Bewegung außer einer ruhigen Pegel-Anzeige; bei `prefers-reduced-motion` statisch.
### 12.2 Zusammenfassung
Eigene Ansicht im Formular (kein Modal): Kopf „Zusammenfassung B3 · 14.10.“, Abschnitte je Zarge (Tabelle Platz, Art, Anteile, Bemerkung, Zitat), Felder der Durchsicht, offene Punkte, „Nicht verwendet“. Unten „Übernehmen“ (Hauptaktion), „Weiter sprechen“, „Verwerfen“ (Rückfrage).
### 12.3 Beute
Die 3D-Karte (L3) und die Aufbau-Liste zeigen `fill` im Fokus einer Zarge: Etikett „R8 · Brutwabe · Brut wenig · Futterkranz gering · 12.10.“, im SVG und in der Liste als Text. Während einer Sitzung hebt die Karte das Rähmchen am Zeiger hervor (Hover-Zustand aus `useHiveView`), damit man auch ohne Ton sieht, wo die App gerade ist.
## 13. Datenschutz und Aufbewahrung
| Datum | Wo | Wie lange | Wohin |
|---|---|---|---|
| Ton live | nur im Browser | nie gespeichert | bei Browser-Cloud an Google (Einwilligung) |
| Ton Aufnahme-Durchsicht | Gerät, Datenbank verschlüsselt, RAM im Whisper-Pod | wie T7 (bis Abschrift, höchstens 24 h) | bleibt im Cluster |
| Mitschrift und Befunde live | Journal im Gerät | bis Übernahme/Verwerfen, höchstens 7 Tage | – |
| Unverstandene Sätze und KI-Ergebnis | Datenbank verschlüsselt | bis Abholung (`ack`), höchstens 24 h | Satz an Anthropic |
| Ergebnis Aufnahme-Durchsicht | Datenbank verschlüsselt | bis Bestätigung/Verwerfen, höchstens 7 Tage | unverstandene Sätze an Anthropic |
| Zähler, Tokens, Status | Datenbank | 90 Tage | – |
| Rähmchen-Befunde nach dem Speichern | Aufbau-JSON, `HiveChange` | wie alle Stammdaten | – |
- Gegenüber T7 geht weniger an Anthropic: nur Sätze, die der Parser nicht verstanden hat (Ziel ≤ 20 %), nicht die ganze Mitschrift.
- Alle Maßnahmen aus T7 9.2 gelten: Schlüsselbund `voice`, keine Inhalte in Logs, Ereignissen, Audit, Metriken (`loggableError()`), SDK-Debug aus, Schlüssel nur im Worker, Prompt-Injection-Schutz durch Schema und Prüfung.
- `cleanup` (T7) wird erweitert: Sätze und Ergebnisse älter als 24 h, Sitzungs-Inhalte nach `contentExpiresAt` (`EXPIRED`), Zeilen nach 90 Tagen.
- Mikrofon wird nur nach Tipp auf „Sprache an“ angefragt und bei Ende, Pause über 10 Minuten (dann Neustart beim nächsten „weiter“ per Knopf) und Verlassen der Seite freigegeben.
- Die Datenschutz-Seite (T7 8.4) bekommt den Abschnitt „Sprach-Durchsicht“. DPA und Drittland wie T7 9.4.
## 14. Fehlerfälle
| Fall | Code | Verhalten |
|---|---|---|
| Kein Live möglich | – | Aufnahme-Durchsicht (10.1), Hinweis mit Grund in Worten. |
| Mikrofon verweigert | – | Hinweis je Browser, „Stattdessen tippen“. Sitzung bleibt im Journal. |
| Erkennung endet (Stille, Netz, Fehler) | – | Neustart (3.2). Nie Ende. |
| Seite verdeckt | – | UNTERBROCHEN, Fortsetzen mit Ansage. |
| Sprachausgabe nicht verfügbar | – | Fragen als Ton (drei Töne) plus Text; gesprochene Antwort geht weiter. |
| Sprachpaket fehlt, keine Cloud-Einwilligung | – | Aufnahme-Durchsicht, Hinweis „Sprachpaket laden“ oder Einstellung. |
| KI-Schlüssel fehlt | `EXTRACT_DISABLED` | Satz bleibt `QUEUED`; Gerät macht nach 30 s einen offenen Punkt H8. Meldung an `system:manage` (T7). |
| Anthropic 429/5xx/Zeit | `EXTRACT_RETRY` | 2 s, 5 s, dann `FAILED/EXTRACT_FAILED`, im Gerät H8 mit Wortlaut. |
| Antwort ungültig | `EXTRACT_INVALID` | einmal mit Ersatzmodell, dann H8. |
| Ablehnung | `REFUSED` | H8. |
| Grenzen (Sitzung, Minute, Tag) | `LIVE_LIMIT` | H8, keine Ansage. |
| Sitzung abgelaufen beim Speichern | `VOICE_SESSION_GONE` | Speichern ohne Sitzung, Quelle `INSPECTION`. |
| Beute geändert seit Start | `HIVE_CONFLICT` | Beute §7/§11. |
| Fill-Summe > 100 trotz Rückfrage | `RULE_FILL_SUM` | Kann nur über Bearbeitung entstehen; die Liste zeigt das Rähmchen rot mit Wort „über 100 %“ und blockiert Speichern der Beute-Änderung bis zur Korrektur. |
| Aufbau fehlt / kein `colonies:update` | – | 5.10. |
| Stücke fehlen (Aufnahme-Durchsicht) | `CHUNK_MISSING` | Nach 24 h ohne letztes Stück: Auswertung mit dem, was da ist, Hinweis „Ein Teil der Aufnahme fehlt“. |
| Zwei Geräte, dieselbe Sitzung | – | Erste Speicherung gewinnt, zweite 409 → ohne Sitzung speichern (T7). |
## 15. Tests
TDD wie im Rückgrat-Plan: erst der fehlschlagende Test.
### 15.1 Parser-Fixtures (Kern)
Ort: `ApiCore-Backend/test/fixtures/voice-grammar/*.json`, gelesen von Backend (Jest) und Frontend (Vitest, `skipIf`, wenn die Datei im Image-Build fehlt; Muster `hive-ops`). Format:
```json
{
"name": "owner-example-right-one-neighbour-third",
"hive": { "boxes": [ { "id": "b1", "slots": 10 }, { "id": "b2", "slots": 10 } ] },
"start": { "stanceByBox": { "1": "REAR" }, "boxNo": 1 },
"utterances": [
{ "text": "Rähmchen rechts eins sieht gut aus, ist eine Futterwabe", "answer": null },
{ "text": "die daneben ist eine Pollenwabe, beide ca. 25 %" },
{ "text": "die dritte ist eine Brutwabe, wenig Brut, Futterkranz gering" }
],
"expect": {
"findings": [
{ "box": 1, "slot": 10, "type": "FEED", "note": "sieht gut aus", "fill": { "feedPct": 25 } },
{ "box": 1, "slot": 9, "type": "POLLEN", "fill": { "pollenPct": 25 } },
{ "box": 1, "slot": 8, "type": "BROOD", "fill": { "broodAmount": "LOW", "feedRim": "LOW" } }
],
"questions": [], "openPoints": [], "aiUtterances": [],
"cursor": { "boxNo": 1, "slot": 8, "edge": "R" }
}
}
```
Pflicht-Fälle (mindestens 60 Dateien zum Start, wachsen mit M1): alle 22 Beispiele aus 5.9; Zahlwörter 1–30 und Ordinalzahlen; Bruchwörter; „eine Seite“; Kante bei `REAR` mit und ohne Wechsel; `SIDE_LEFT`/`SIDE_RIGHT` mit „die nächste“ und „die letzte“; Zargen-Wechsel leert Platz und Gruppe; „beide“ und „alle drei“ mit zu kleiner Gruppe; Bereich über die Kante; jede Art und jedes Inhaltswort; Futterkranz-Stufen; jedes Durchsicht-Feld mit Ja/Nein und Skalenwörtern; „ruhig“; Weiselzellen gegen Spielnäpfchen; Befehle in der Satzmitte (nicht erkannt) und am Ende (erkannt); „nein, …“; „streich das“ nach einem Satz mit mehreren Befunden; Hörfehler-Tabelle; Alternativen (erste ohne Bindung, zweite mit); Füllwort-Sätze ohne KI; Rest mit Fachwort → KI; ohne Aufbau (5.10); Antwort-Grammatik für jede Frage-Art; unbeantwortete Frage wird offener Punkt; Budget erschöpft.
### 15.2 Unit
- Frontend `cursor.spec.ts`: `never binds a finding to a box other than the current one`, `clears slot and group on box change`, `asks the stance once per box and offers the previous box`, `does not count the stance question against the budget`.
- `conflicts.spec.ts`: `asks on a fill sum over 100`, `asks on two different types for one frame without nein`, `collects a type/fill mismatch instead of asking`, `turns questions into open points when the budget is used up`.
- `recognition-loop.spec.ts` (gefälschte `SpeechRecognition`): `restarts on end and no-speech without ending the session`, `never ends on 5 minutes of silence`, `pauses recognition while speaking and resumes after`, `recreates the instance when audiostart does not come`, `ends only on the spoken phrase or the button`.
- `wake-lock.spec.ts`: `re-acquires on visible`, `shows the hint without support`. `pocket-mode.spec.tsx`: `swallows touches`, `unlocks with two distinct taps`, `unlocks by voice`, `axe finds nothing`.
- `journal.spec.ts` (`fake-indexeddb`): `survives a reload and resumes`, `deletes on übernehmen and discard`, `never shows another user's session`, DB-Upgrade behält alle Warteschlangen.
- `summary.spec.ts`: `speaks counts per box and fields in a fixed order`, `asks at most five open points`. `build-ops.spec.ts`: `one setFrame per touched frame`, `type only when changed`, `fill replaces`, `addPart before the frames of a new box`, `refuses more than 100 ops`.
- Backend `hive-ops.spec.ts`/`hive-validate.spec.ts` (Erweiterung): neue Fixtures aus 7.2, `RULE_FILL_SUM`, `checkedOn set from occurredOn`, `setFrame without any field is rejected`. Frontend-Port gegen dieselben Fixtures.
- `voice-live-result.spec.ts`: `drops a frame target outside the context`, `drops a slot neither in the context nor in the sentence`, `drops a missing quote`, `drops duplicates of already bound items`, `clamps free text`.
- `voice-live.service.spec.ts` (Port gemockt): `marks the system block for a 1h cache`, `sends only the sentence and the compact context`, `counts cache read and write tokens`, `retries once with the fallback model on invalid output`, `respects session, minute and day limits`, `never logs the sentence`.
- `inspections.service.spec.ts` (Erweiterung): `accepts AI_SUGGESTION only with a valid voice session`, `confirms the session and wipes its content in the same transaction`, `409 VOICE_SESSION_GONE`, `rolls back the hive change and the confirmation together`.
- `voice-cleanup.spec.ts` (Erweiterung): Sätze nach 24 h, Sitzungen nach Frist, Zeilen nach 90 Tagen.
### 15.3 e2e
- `test/voice-sessions.e2e-spec.ts`: Sitzung anlegen; Satz senden → Schleife mit Ersatz-Modell → Ergebnis abholen → `ack` löscht Inhalte; `POST /inspections` mit `voiceSessionId` und `AI_SUGGESTION` → `HiveChange.source = AI_SUGGESTION`, Sitzung `CONFIRMED`, keine Inhalte mehr; fremder Nutzer 404; READONLY und API_CLIENT 403; STAFF ohne `colonies:update` bekommt 403 nur für den Beute-Teil (Beute §7).
- `test/voice-recorded.e2e-spec.ts`: drei Stücke mit Ersatz-Whisper-Ausgabe → Sitzung `READY` → Ergebnis gleich dem Fixture-Ergebnis derselben Sätze.
- Playwright: Formular, „Sprache an“ mit gefälschter Erkennung (Init-Skript), Beispielsatz, Frage F1 per gefälschter Antwort, „Durchsicht beendet“, „übernehmen“, Entwurf mit Feldern „aus Sprache“ und Beute-Liste, Speichern.
### 15.4 Gold-Menge KI
40 erfundene, anonyme unverstandene Sätze mit Kontext und erwarteter Ausgabe (`npm run eval:voice-live`, kostet Geld, nie in der CI, nur nach Freigabe). Ziele: kein einziger Befund, der nicht gesagt wurde; Ziel-Rähmchen 100 % richtig oder verworfen; Trefferquote ≥ 80 %. Haiku zuerst, Sonnet zum Vergleich.
### 15.5 Messung vor der Freigabe (mit dem Inhaber, am Stand)
| Nr. | Was | Ziel |
|---|---|---|
| M1 | 5 echte Durchsichten auf dem Android-Handy des Inhabers, Erkennung auf dem Gerät und (mit Einwilligung) Browser-Cloud im Vergleich; Mitschriften werden anonymisiert Fixtures | Fachwörter ≥ 90 % richtig erkannt (inkl. Alternativen und Hörfehler-Tabelle); Parser-Quote ≥ 80 % |
| M2 | 20 Minuten Taschenmodus mit Wind und Summen | keine ungewollte Beendigung, Akku-Verbrauch notiert |
| M3 | Signalton bei jedem Neustart | Anzahl je Minute notiert; stört er, Bericht an den Inhaber (Web-Apps können ihn nicht abschalten) |
| M4 | Token-Zähler der 5 Durchsichten | ≤ 20 KI-Aufrufe und < 1 Cent je Durchsicht |
| M5 | iPhone (falls vorhanden): Live-Versuch | Ergebnis entscheidet über eine spätere iOS-Stufe |
**Manuell (MP):** 390 px mit Handschuhen; Tastatur und Screenreader für Zusammenfassung und Prüfansicht; `prefers-reduced-motion`; Fehler- und Leerzustände.
## 16. Lieferungen und Aufwand
Jede Lieferung ist für sich lauffähig und wird einzeln ausgerollt (nach Freigabe). Reihenfolge nach T7 und Beute L5; S1 hängt an nichts und darf vorher.
| Nr. | Lieferung | Voraussetzung | PT |
|---|---|---|---|
| S1 | **Grammatik und Datenmodell:** Parser (beide Fassungen), Fixtures, `HiveFrame.fill/note`, erweitertes `setFrame`, Validator, Facts, `describeHiveOps`, Anzeige der Anteile in Liste/SVG. Ohne Sprache nutzbar: Anteile in der Karte setzen. | Beute L2 (fertig) | 3,0 |
| S2 | **Live-Sitzung ohne KI:** Erkennungs-Schleife, Fähigkeitsprüfung, Wake Lock, Taschenmodus, Journal, Zeiger-Leiste, Rückfragen per Sprachausgabe, Zusammenfassung, Übernahme in den Entwurf. Unverstandenes wird offener Punkt. | T7 Stufe 1 (Einwilligung), Beute L5 | 3,5 |
| S3 | **KI für Unverstandenes:** `VoiceSession`, `VoiceUtterance`, Endpunkte, Schleife `voice-live`, Prompt, Schema, Prüfung, Cache, Zähler, Grenzen, Aufräumen. | T7 Stufe 2 (Schlüssel, Modul, Worker) | 2,0 |
| S4 | **Speichern:** `voiceSessionId`, `hiveChange.source = AI_SUGGESTION`, Transaktion, 409-Wege, Warteschlange. | S2, S3 | 1,0 |
| S5 | **Aufnahme-Durchsicht:** Stücke, Whisper-Segmente, Auswertung im Worker, Netzverlust-Wechsel, Prüfansicht `?voiceSession=`. | T7 Stufe 2, S3 | 2,0 |
| S6 | **Messung und Doku:** M1–M5, Gold-Menge, Hörfehler-Tabelle aus M1, ARCHITECTURE (nächste freie Annahme-Nummern), TASKS, Datenschutz-Seite. | S1–S5 | 1,0 |
| | **Summe** | | **12,5** |
Rollout wie T7 §14: Backend mit Migration zuerst (KI ohne Schlüssel aus), dann Frontend. Rückweg: Knopf „Sprache an“ per Schalter ausblenden; Sitzungen laufen über `cleanup` ab; `fill`/`note` bleiben im Aufbau (additiv, schadet nicht).
## 17. Annahmen
| Nr. | Annahme | Prüfung |
|---|---|---|
| B1 | „Zarge eins“ ist die unterste Zarge mit Rähmchen-Plätzen; Leerzargen zählen nicht. | Inhaber (offene Frage 2). |
| B2 | Rähmchen stehen quer zum Flugloch (Kaltbau); „von hinten“ sieht man die Reihe links nach rechts, „von der Seite“ von nah nach fern. Passt zu Beute A4. | Inhaber (offene Frage 2). |
| B3 | Honig zählt als Futter-Anteil; Anteile sind ganze Prozent. | Inhaber. |
| B4 | Chrome für Android bietet `processLocally`/`available()` und ggf. `phrases` für `de-DE`; sonst Browser-Cloud mit Einwilligung. Die Schnittstellen sind experimentell (MDN). | Fähigkeitsprüfung im Code, Messung M1; Bau prüft die MDN-Kompatibilitätstabelle. |
| B5 | Der Signalton beim Neustart der Erkennung ist erträglich oder auf dem Gerät des Inhabers nicht vorhanden. | M3. |
| B6 | Das feste JSON-Schema der strukturierten Ausgabe wird zwischen Aufrufen wiederverwendet und verteuert die Aufrufe nicht nennenswert. | Token-Zähler in M4. |
| B7 | `output_config.effort` und adaptives Denken verhalten sich bei Haiku 5.5 wie in der Doku (Standard `medium`, hier `low`). | Bau liest die aktuelle Doku nach. |
| B8 | 15 KI-Aufrufe je Durchsicht bei 80 % Parser-Quote. | M4. |
## 18. Offene Fragen an den Inhaber
1. **Speichern per Sprache:** Soll nach „übernehmen“ zusätzlich ein gesprochenes „speichern“ die Durchsicht speichern dürfen, oder bleibt Speichern ein Tipp (z. B. am Auto ohne Handschuhe)? Ändert den Bau wenig (ein Befehl, eine zweite Rückfrage). **Empfehlung:** Tipp. Die Rähmchen-Befunde ändern den Aufbau, und ein Blick auf die Liste vor dem Speichern kostet zehn Sekunden.
2. **Zählung und Lage:** Ist „Zarge eins“ immer die unterste Zarge mit Rähmchen (Leerzarge mit Verdunster zählt nicht mit), und stehen deine Rähmchen quer zum Flugloch, sodass man „von hinten“ links nach rechts zählt und „von der Seite“ von nah nach fern? **Empfehlung:** ja zu beidem, wie in B1/B2. Steht eine Beute anders, braucht das Beutensystem ein Feld für die Lage.
3. **Offline live:** Erkennt dein Handy Deutsch auf dem Gerät, könnte die Sitzung auch **ohne Netz** live laufen: Parser und Rückfragen sofort, nur Unklares wird später geprüft. Das weicht von Anforderung 4 („nur mit Internet“) ab. Erlauben, oder ohne Netz immer Aufnahme? **Empfehlung:** erlauben, wenn M1 die Erkennung auf dem Gerät als gut genug zeigt. Bis dahin gilt Anforderung 4.