feat(app): Aufnahme verwerfen — Muelleimer neben dem Aufnahmeknopf

Bisher hiess "Stopp" immer "senden"; wer es sich anders ueberlegt hatte,
konnte die Aufnahme nicht zuruecknehmen.

- App: waehrend der Aufnahme 🗑 links neben dem roten Knopf. Verwirft ueber
  ariaStopRecording('user-discard') → cancelStreamingRecording; das leere
  Endpoint raeumt die Sprechblase ab, zurueck aufs Wake-Word.
- Fund dabei: "cancel" schickte der STT-Bridge nur stt_stream_end — die
  transkribierte trotzdem und der Text ging an ARIA (fiel nur nicht auf,
  weil cancel bisher bei Stille kam). Jetzt:
  - Whisper- und Voxtral-Bridge: stream_end mit reason "cancel:*" →
    discard, kein Transcribe, leerer Text (reason "cancelled").
  - aria-bridge merkt sich verworfene requestIds (2 min) und wirft einen
    trotzdem ankommenden Text weg — Schutz auch bei alter STT-Bridge.

Getestet: Whisper-Bridge mit nachgebautem Runner (Stopp → Text, Muelleimer
→ kein Transcribe, leer). App: TypeScript ohne Fehler; auf dem Handy noch
nicht ausprobiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 20:05:35 +01:00
co-authored by Claude Opus 5.5
parent 91619d9307
commit 9529bb0b16
5 changed files with 81 additions and 5 deletions
+13
View File
@@ -3449,6 +3449,16 @@ class ARIABridge:
# stt_endpoint-Handler popt selbst; hier nur ein verzoegerter Cleanup
# als Leak-Schutz, falls gar kein Endpoint mehr kommt.
req_id = payload.get("requestId", "") or ""
# Verworfen (Muelleimer in der App)? Dann darf ein trotzdem noch
# kommender Text NICHT an ARIA — auch wenn eine alte STT-Bridge ihn
# noch transkribiert.
if req_id and str(payload.get("reason", "")).startswith("cancel"):
if not hasattr(self, "_stt_cancelled"):
self._stt_cancelled = {}
self._stt_cancelled[req_id] = time.time()
for rid, t in list(self._stt_cancelled.items()):
if time.time() - t > 120:
self._stt_cancelled.pop(rid, None)
if req_id:
try:
loop = asyncio.get_event_loop()
@@ -3473,6 +3483,9 @@ class ARIABridge:
logger.info("[rvs] stt_endpoint mit leerem Text — ignoriert (reason=%s)",
payload.get("reason", ""))
return
if (payload.get("requestId") or "") in getattr(self, "_stt_cancelled", {}):
logger.info("[rvs] stt_endpoint einer verworfenen Aufnahme — ignoriert")
return
audio_request_id = payload.get("audioRequestId", "") or ""
voice = payload.get("voice", "") or ""
speed_raw = payload.get("speed")