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

76 KiB
Raw Permalink Blame History

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:

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

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

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

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

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

{
  "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:

{
  "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.