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:
@@ -327,6 +327,7 @@ class StreamSession:
|
||||
last_speech_check_at: float = 0.0 # Drossel fuer den Silero-Speech-Endpoint
|
||||
noise_floor: float = 0.0
|
||||
closed: bool = False
|
||||
discard: bool = False # stream_end mit reason "cancel:*" → NICHT transkribieren
|
||||
endpoint_sent: bool = False
|
||||
# Einmaliges "Sprache erkannt"-Signal an die App gesendet? Voxtral schickt
|
||||
# keine Live-Partials, aber der App-No-Speech-Watchdog wartet auf ein
|
||||
@@ -396,10 +397,12 @@ class SessionManager:
|
||||
sess.last_chunk_at = time.time()
|
||||
return True
|
||||
|
||||
def end_session(self, request_id: str) -> None:
|
||||
def end_session(self, request_id: str, reason: str = "") -> None:
|
||||
sess = self._sessions.get(request_id)
|
||||
if sess is not None:
|
||||
sess.closed = True
|
||||
if str(reason).startswith("cancel"):
|
||||
sess.discard = True # Muelleimer in der App: verwerfen
|
||||
|
||||
def drop(self, request_id: str) -> None:
|
||||
self._sessions.pop(request_id, None)
|
||||
@@ -620,6 +623,9 @@ class SessionManager:
|
||||
# Schwelle (voiced==0 = gar keine Stimme), damit ein kurzes bewusstes Wort
|
||||
# ('ja', 'stopp') am Aufnahme-Button noch durchgeht, echte Stille aber nicht.
|
||||
_min_voiced = STREAM_MIN_VOICED_FRAMES if reason != "stream_end" else 1
|
||||
if sess.discard:
|
||||
reason = "cancelled"
|
||||
_min_voiced = 10 ** 9 # verworfen: nie transkribieren, leerer Text
|
||||
if sess.voiced_frames < _min_voiced:
|
||||
logger.info("Stream %s: no-speech (voiced_frames=%d<%d, reason=%s) — leeres Endpoint",
|
||||
sess.request_id[:8], sess.voiced_frames, _min_voiced, reason)
|
||||
@@ -810,7 +816,7 @@ async def run_loop(sessions: SessionManager) -> None:
|
||||
elif mtype == "stt_audio_chunk":
|
||||
sessions.feed_chunk(payload)
|
||||
elif mtype == "stt_stream_end":
|
||||
sessions.end_session(payload.get("requestId", ""))
|
||||
sessions.end_session(payload.get("requestId", ""), payload.get("reason", ""))
|
||||
elif mtype == "stt_transcribe_blob":
|
||||
# One-Shot-Transkription eines PCM-Schnipsels (kein Live-
|
||||
# Stream) — fuer die Wake-Wort-Bestaetigung: die App schickt
|
||||
|
||||
+11
-3
@@ -390,6 +390,7 @@ class StreamSession:
|
||||
voice_rms_min: float = STREAM_VOICE_RMS_MIN
|
||||
voice_rms_max: float = STREAM_VOICE_RMS_MAX
|
||||
closed: bool = False # nach stream_end gesetzt
|
||||
discard: bool = False # stream_end mit reason "cancel:*" → NICHT transkribieren
|
||||
endpoint_sent: bool = False # Endpoint nur einmal feuern
|
||||
# Speaker-ID Gating: bei aktiviertem Fingerprint pruefen wir die ersten
|
||||
# ~1.5s der Aufnahme. Bei mismatch wird die Session sofort beendet mit
|
||||
@@ -483,13 +484,16 @@ class SessionManager:
|
||||
session.last_chunk_at = time.time()
|
||||
return True
|
||||
|
||||
def end_session(self, request_id: str) -> Optional[StreamSession]:
|
||||
def end_session(self, request_id: str, reason: str = "") -> Optional[StreamSession]:
|
||||
"""Markiert Session als geschlossen. Der Endpointer-Loop macht das
|
||||
Final-Transcribe + Cleanup."""
|
||||
Final-Transcribe + Cleanup. reason "cancel:*" (z.B. Muelleimer in der
|
||||
App) = verwerfen: kein Transcribe, leerer Text — nichts geht an ARIA."""
|
||||
session = self._sessions.get(request_id)
|
||||
if session is None:
|
||||
return None
|
||||
session.closed = True
|
||||
if str(reason).startswith("cancel"):
|
||||
session.discard = True
|
||||
return session
|
||||
|
||||
def drop(self, request_id: str) -> None:
|
||||
@@ -807,6 +811,10 @@ class SessionManager:
|
||||
if sess.partial_task is not None and not sess.partial_task.done():
|
||||
sess.partial_task.cancel()
|
||||
audio = pcm_s16le_to_float32(bytes(sess.pcm_buffer))
|
||||
if sess.discard:
|
||||
logger.info("Stream %s: verworfen (cancel) — kein Transcribe", sess.request_id[:8])
|
||||
audio = audio[:0]
|
||||
reason = "cancelled"
|
||||
if audio.size == 0:
|
||||
logger.info("Stream %s: leere Audio-Daten — final text leer", sess.request_id[:8])
|
||||
final_text = ""
|
||||
@@ -1080,7 +1088,7 @@ async def run_loop(runner: WhisperRunner, sessions: SessionManager) -> None:
|
||||
req_id[:8], payload.get("reason", ""))
|
||||
await _debug_log(ws, "stream.end",
|
||||
f"received id={req_id[:12]} reason={payload.get('reason', '')}")
|
||||
sessions.end_session(req_id)
|
||||
sessions.end_session(req_id, payload.get("reason", ""))
|
||||
|
||||
elif mtype == "voice_id_status_request":
|
||||
req_id = payload.get("requestId", "")
|
||||
|
||||
Reference in New Issue
Block a user