Compare commits

..

6 Commits

Author SHA1 Message Date
duffyduck b27fba316b release: bump version to 0.2.0.1 2026-07-06 00:34:43 +02:00
duffyduck 648698d202 fix(voice): robusteres STT-Endpointing via akustische Stille (nicht nur semantisch)
Bug: die Spracherkennung bricht mal zu frueh ab (nach zwei Worten obwohl noch
gesprochen wird) und merkt mal gar nicht dass man aufgehoert hat.

Ursache: der Endpointer feuerte rein auf semantischer Stagnation (Whisper-
Transkript waechst endpoint_ms lang nicht). Das ist fragil — kurze Sprech-
Pausen oder beam_size=1-Instabilitaet → vorzeitiger Cut; Oszillation/
Halluzination → nie ein Endpoint.

Fix: akustische Stille als robustes Primaersignal. Jeder Tick (~200ms) misst
die RMS-Energie der letzten 300ms:
- Solange Sprach-Energie da ist, bleibt die Session am Leben (kein Cut mitten
  im Reden).
- Endpoint feuert wenn seit endpoint_ms keine Sprach-Energie mehr da ist.
- Semantischer Backstop (2x endpoint_ms) fuer laute Umgebungen (Auto), wo die
  Energie nie faellt — dort degradiert es sauber aufs bisherige Verhalten.

RMS-Threshold (0.012) gegen Stille/Sprache/Fahrgeraeusch verifiziert.
Konstanten oben in der Datei tunebar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 00:31:48 +02:00
duffyduck 3e88eecd9c fix(voice): Sprach-Nachricht landet im richtigen Projekt (Registry-Race)
Bug: bei manuellem Stop landete die Sprachnachricht (und ARIAs Antwort) im
Hauptchat statt im fokussierten Projekt — mal so, mal so, je nachdem ob der
Endpoint natuerlich kam oder man den Stop-Button drueckte.

Ursache: stt_stream_end popte die Focus-projectId-Registry SOFORT. Nach einem
manuellen Stop folgt aber noch der finale stt_endpoint (Whisper-Final-
Transcribe), der die projectId aus genau dieser Registry braucht. War sie schon
weg → focused_pid="" → Voice-Router faellt auf Hauptchat zurueck. Die
User-Bubble (lokal getaggt) blieb im Projekt, ARIAs Antwort kam aber mit
project_id="" → nur in EINEM Kontext sichtbar.

Fix: stt_stream_end raeumt die Registry nicht mehr sofort auf, sondern per
verzoegertem call_later(20s) als Leak-Schutz. Der stt_endpoint-Handler popt
selbst wenn er zuerst kommt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 00:31:48 +02:00
duffyduck e33d1c782f fix(diagnostic): ARIA-Datei-Bubbles (Bilder) tragen project_id — kein Ausblenden mehr
Regression aus dem chat_history-projectId-Fix: nach dem History-Reload laeuft
jetzt updateChatVisibilityByFocus(). addAriaFile() setzte aber kein
dataset.projectId → Datei-/Bild-Bubbles galten als Hauptchat und wurden
ausgeblendet, sobald der (aus localStorage wiederhergestellte) Focus auf einem
Projekt lag. In der App trat das nicht auf (datengetriebener Filter mit
korrektem projectId).

- addAriaFile(): setzt dataset.projectId aus p.projectId und respektiert beim
  Live-Anhaengen den aktuellen Focus (wie addChat via hiddenByFocus).
- chat_history-Renderer: reicht m.projectId an addAriaFile weiter.
- Bridge _process_core_response: haengt turn_pid als f["projectId"] an die
  file_from_aria-Payload, damit der Live-Pfad die Zuordnung kennt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:46:25 +02:00
duffyduck 1568c25ac4 fix(projects): Backfill v2 — robuster Match trotz GPS/Barge-In/FILE-Marker
v1 matchte text==content exakt und verfehlte damit Nachrichten, bei denen die
Bridge den Brain-Text anreichert oder cleant:
- User-Turns: _build_core_text PREPENDT [Hinweis: Barge-In]-/[GPS-Position]-
  Bloecke — die stehen in conversation.jsonl, nicht in chat_backup.
- Assistant-Turns: conversation enthaelt [FILE: /shared/uploads/...]-Marker,
  chat_backup hat sie schon rausgecleant.

_norm entfernt jetzt FILE-Marker + fuehrende [..]-Bloecke, kollabiert
Whitespace und vergleicht einen 120-Zeichen-Praefix. Neuer Marker (v2) →
laeuft einmal neu und fuellt die von v1 verbliebenen Luecken. Nicht-destruktiv
(eigene .pre-backfill-v2.bak), bestehende Tags bleiben unangetastet.

Mit Tests gegen GPS-/Barge-In-/FILE-Marker-Faelle verifiziert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:34:17 +02:00
duffyduck 97ee455ab4 fix(diagnostic): chat_history traegt project_id — getaggte Bubbles landen im Projekt
Der Reload-Pfad des Dashboards warf die Kontext-Zuordnung an ZWEI Stellen weg,
weshalb nach jedem Reload alles im Hauptchat lag (Projekte leer) — unabhaengig
davon ob chat_backup korrekt getaggt war:

- server.js (History-Builder): pushte {type,text,meta,ts} ohne project_id.
  Jetzt projectId aus obj.project_id fuer sent/received/aria_file mitgegeben.
- index.html (chat_history-Renderer): setzte dataset.ts aber nie
  dataset.projectId → jede Bubble galt als Hauptchat. Jetzt gesetzt, und nach
  dem Neuaufbau updateChatVisibilityByFocus() aufgerufen damit der aktuelle
  Kontext-Focus sofort greift.

Zusammen mit der Backfill-Migration (8b567e1) erscheinen alt-getaggte Turns
jetzt beim Dashboard-Reload im richtigen Projekt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:26:50 +02:00
7 changed files with 131 additions and 25 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit" applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 20000 versionCode 20001
versionName "0.2.0.0" versionName "0.2.0.1"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.2.0.0", "version": "0.2.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+23 -5
View File
@@ -25,6 +25,7 @@ from __future__ import annotations
import json import json
import logging import logging
import os import os
import re
from collections import defaultdict, deque from collections import defaultdict, deque
from pathlib import Path from pathlib import Path
@@ -32,13 +33,30 @@ logger = logging.getLogger("aria.migrate.backfill_projectid")
CONVERSATION_FILE = Path(os.environ.get("CONVERSATION_FILE", "/data/conversation.jsonl")) CONVERSATION_FILE = Path(os.environ.get("CONVERSATION_FILE", "/data/conversation.jsonl"))
CHAT_BACKUP_FILE = Path(os.environ.get("CHAT_BACKUP_FILE", "/shared/config/chat_backup.jsonl")) CHAT_BACKUP_FILE = Path(os.environ.get("CHAT_BACKUP_FILE", "/shared/config/chat_backup.jsonl"))
MARKER_FILE = Path("/shared/config/.chat_backup_projectid_backfill_v1") # v2: robusterer Match (Marker-Strip + Praefix). v1 verlangte exakte Gleichheit
# von text==content und verfehlte damit alle Nachrichten bei denen die Bridge
# den Brain-Text anreichert (GPS/Barge-In-Hints prepended) oder cleant
# (FILE-Marker entfernt). Neuer Marker → laeuft einmal neu, fuellt die Luecken.
MARKER_FILE = Path("/shared/config/.chat_backup_projectid_backfill_v2")
# _build_core_text (Bridge) PREPENDT bei User-Nachrichten Hinweis-/GPS-Bloecke
# in eckigen Klammern vor den eigentlichen Text; conversation.jsonl speichert
# diesen angereicherten Text, chat_backup nur den rohen. FILE-Marker stehen in
# conversation-Assistant-Turns, sind in chat_backup aber schon rausgecleant.
_FILE_MARKER_RE = re.compile(r"\[FILE:\s*/shared/uploads/[^\]]+\]", re.IGNORECASE)
_LEADING_BRACKET_RE = re.compile(r"^\s*(?:\[[^\]]*\]\s*)+")
_WS_RE = re.compile(r"\s+")
def _norm(text: str) -> str: def _norm(text: str) -> str:
"""Match-Key: getrimmt + auf 500 Zeichen begrenzt. Reicht um Turns eindeutig """Match-Key: FILE-Marker + fuehrende [Hinweis]/[GPS]-Bloecke entfernen,
zu unterscheiden, ist aber tolerant gegen minimale Trailing-Unterschiede.""" Whitespace kollabieren, auf 120-Zeichen-Praefix kuerzen. Toleriert damit
return (text or "").strip()[:500] die Anreicherungs-/Cleaning-Unterschiede zwischen conversation und backup,
bleibt durch den 120er-Praefix aber spezifisch genug gegen Fehl-Matches."""
t = _FILE_MARKER_RE.sub("", text or "")
t = _LEADING_BRACKET_RE.sub("", t)
t = _WS_RE.sub(" ", t).strip()
return t[:120]
def run() -> dict: def run() -> dict:
@@ -112,7 +130,7 @@ def run() -> dict:
# 4) Sicherung + atomarer Rewrite. # 4) Sicherung + atomarer Rewrite.
try: try:
bak = CHAT_BACKUP_FILE.with_suffix(".jsonl.pre-backfill-v1.bak") bak = CHAT_BACKUP_FILE.with_suffix(".jsonl.pre-backfill-v2.bak")
if not bak.exists(): if not bak.exists():
bak.write_bytes(CHAT_BACKUP_FILE.read_bytes()) bak.write_bytes(CHAT_BACKUP_FILE.read_bytes())
tmp = CHAT_BACKUP_FILE.with_suffix(".jsonl.tmp") tmp = CHAT_BACKUP_FILE.with_suffix(".jsonl.tmp")
+22 -2
View File
@@ -1228,6 +1228,10 @@ class ARIABridge:
server_path = f.get("serverPath") server_path = f.get("serverPath")
if turn_pid and server_path: if turn_pid and server_path:
self._tag_file_to_project(server_path, turn_pid) self._tag_file_to_project(server_path, turn_pid)
# projectId mitschicken, damit App+Diagnostic die Datei-Bubble dem
# richtigen Kontext zuordnen (sonst faellt sie im Diagnostic-Focus-
# Filter durch = wird nur im Hauptchat angezeigt).
f["projectId"] = turn_pid
await self._broadcast_aria_file(f) await self._broadcast_aria_file(f)
# Bei fehlenden Files: User informieren (sonst sieht er nur stille # Bei fehlenden Files: User informieren (sonst sieht er nur stille
# Verluste — ARIA hat den Marker hingeschrieben aber das File nicht # Verluste — ARIA hat den Marker hingeschrieben aber das File nicht
@@ -2807,9 +2811,25 @@ class ARIABridge:
return return
elif msg_type == "stt_stream_end": elif msg_type == "stt_stream_end":
# Session vorbei — Focus-Tracking fuer diese requestId aufraeumen. # Session vorbei — ABER nicht sofort aufraeumen: nach einem manuellen
# Stop folgt noch der finale stt_endpoint (Whisper-Final-Transcribe),
# der die Focus-projectId aus dieser Registry braucht. Wuerden wir hier
# sofort poppen, bekaeme der Endpoint "" → die Nachricht (und ARIA's
# Antwort) landet im Hauptchat statt im fokussierten Projekt. Der
# 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 "" req_id = payload.get("requestId", "") or ""
self._stt_stream_projects.pop(req_id, None) if req_id:
try:
loop = asyncio.get_event_loop()
loop.call_later(
20.0,
lambda rid=req_id: self._stt_stream_projects.pop(rid, None),
)
except Exception:
# Kein laufender Loop (sollte im ws-Handler nie passieren) —
# dann lieber gar nicht aufraeumen als crashen.
pass
return return
elif msg_type == "stt_endpoint": elif msg_type == "stt_endpoint":
+18 -1
View File
@@ -1910,7 +1910,7 @@
const m = msg.messages[mi]; const m = msg.messages[mi];
try { try {
if (m.type === 'aria_file') { if (m.type === 'aria_file') {
addAriaFile({ serverPath: m.serverPath, name: m.name, mimeType: m.mimeType, size: m.size, deleted: m.deleted }); addAriaFile({ serverPath: m.serverPath, name: m.name, mimeType: m.mimeType, size: m.size, deleted: m.deleted, projectId: m.projectId });
continue; continue;
} }
const cleaned = (m.text || '').replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim(); const cleaned = (m.text || '').replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim();
@@ -1928,6 +1928,11 @@
const el = document.createElement('div'); const el = document.createElement('div');
el.className = `chat-msg ${m.type}`; el.className = `chat-msg ${m.type}`;
if (m.ts) el.dataset.ts = String(m.ts); if (m.ts) el.dataset.ts = String(m.ts);
// Multi-Threading: Kontext-Zuordnung fuer den Focus-Filter.
// Ohne das landete beim Reload JEDE Bubble im Hauptchat
// (dataset.projectId undefined → '' → nur bei Hauptchat-Focus
// sichtbar), Projekte blieben leer.
el.dataset.projectId = m.projectId || '';
el.innerHTML = innerHtml; el.innerHTML = innerHtml;
b.appendChild(el); b.appendChild(el);
} }
@@ -1949,6 +1954,9 @@
} }
for (const b of boxes) b.scrollTop = b.scrollHeight; for (const b of boxes) b.scrollTop = b.scrollHeight;
} }
// Nach dem Neuaufbau den aktuellen Kontext-Focus anwenden: Bubbles
// die nicht zum fokussierten Projekt gehoeren ausblenden.
updateChatVisibilityByFocus();
if (errorCount > 0) { if (errorCount > 0) {
console.warn(`chat_history: ${errorCount} Bubble(s) konnten nicht gerendert werden`); console.warn(`chat_history: ${errorCount} Bubble(s) konnten nicht gerendert werden`);
} }
@@ -2335,6 +2343,15 @@
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'chat-msg received'; el.className = 'chat-msg received';
el.dataset.ariaFilePath = serverPath; el.dataset.ariaFilePath = serverPath;
// Kontext-Zuordnung fuer den Focus-Filter — ohne das wurde die
// Datei-Bubble beim Reload ausgeblendet wenn ein Projekt fokussiert war.
const filePid = p.projectId || '';
el.dataset.projectId = filePid;
// Beim Live-Anhaengen den aktuellen Focus respektieren (wie addChat),
// sonst blitzt eine Projekt-Datei kurz im Hauptchat auf.
if (typeof focusedContextId === 'string' && filePid !== focusedContextId) {
el.style.display = 'none';
}
if (deleted) el.dataset.deleted = '1'; if (deleted) el.dataset.deleted = '1';
el.innerHTML = html; el.innerHTML = html;
box.appendChild(el); box.appendChild(el);
+4 -2
View File
@@ -2746,8 +2746,9 @@ async function handleLoadChatHistory(clientWs) {
if (obj.role !== "user" && obj.role !== "assistant") continue; if (obj.role !== "user" && obj.role !== "assistant") continue;
const ts = obj.ts || 0; const ts = obj.ts || 0;
const text = String(obj.text || ""); const text = String(obj.text || "");
const projectId = String(obj.project_id || ""); // Multi-Threading: Kontext-Zuordnung
if (obj.role === "user") { if (obj.role === "user") {
if (text) messages.push({ type: "sent", text, meta: "Gateway direkt", ts }); if (text) messages.push({ type: "sent", text, meta: "Gateway direkt", ts, projectId });
continue; continue;
} }
// assistant: nach FILE-Markern scannen, eigene aria_file-Eintraege pro Datei // assistant: nach FILE-Markern scannen, eigene aria_file-Eintraege pro Datei
@@ -2769,9 +2770,10 @@ async function handleLoadChatHistory(clientWs) {
size, size,
ts, ts,
deleted: wasDeleted || !exists, deleted: wasDeleted || !exists,
projectId,
}); });
} }
if (text) messages.push({ type: "received", text, meta: "chat:final", ts }); if (text) messages.push({ type: "received", text, meta: "chat:final", ts, projectId });
} }
clientWs.send(JSON.stringify({ type: "chat_history", messages })); clientWs.send(JSON.stringify({ type: "chat_history", messages }));
+61 -12
View File
@@ -69,6 +69,18 @@ STREAM_DEFAULT_HARD_CAP_MS = 60000 # nach 60s Audio: harter Cut egal was
STREAM_MIN_AUDIO_MS = 600 # erst transkribieren wenn min 600ms Audio da STREAM_MIN_AUDIO_MS = 600 # erst transkribieren wenn min 600ms Audio da
STREAM_SESSION_TTL_S = 120 # tote Sessions nach 2 min aufraeumen STREAM_SESSION_TTL_S = 120 # tote Sessions nach 2 min aufraeumen
# Akustisches Endpointing (ergaenzt die rein-semantische Stagnation).
# Motivation: der reine „Transkript waechst nicht mehr"-Endpoint feuert zu
# frueh (kurze Sprech-Pausen, beam_size=1-Instabilitaet) oder gar nicht
# (Whisper oszilliert/halluziniert). Echte akustische Stille ist das robuste
# „User hat aufgehoert"-Signal.
STREAM_ENERGY_WINDOW_MS = 300 # RMS ueber die letzten 300ms Audio messen
STREAM_VOICE_RMS_THRESHOLD = 0.012 # RMS darueber = Sprache (haelt Session am Leben)
# Rein-semantischer Backstop: wenn die Energie NIE faellt (laute Umgebung,
# z.B. Auto), endpointen wir trotzdem — aber erst nach diesem Faktor x
# endpoint_ms, damit normales Sprechen mit Pausen nicht abgeschnitten wird.
STREAM_SEMANTIC_BACKUP_FACTOR = 2.0
class WhisperRunner: class WhisperRunner:
"""Haelt das Whisper-Modell. Hot-Swap bei Konfig-Wechsel via ensure_loaded().""" """Haelt das Whisper-Modell. Hot-Swap bei Konfig-Wechsel via ensure_loaded()."""
@@ -310,6 +322,7 @@ class StreamSession:
last_partial: str = "" last_partial: str = ""
last_growth_at: float = 0.0 last_growth_at: float = 0.0
last_transcribe_at: float = 0.0 last_transcribe_at: float = 0.0
last_voice_at: float = 0.0 # letzter Tick mit akustischer Sprach-Energie
closed: bool = False # nach stream_end gesetzt closed: bool = False # nach stream_end gesetzt
endpoint_sent: bool = False # Endpoint nur einmal feuern endpoint_sent: bool = False # Endpoint nur einmal feuern
# Speaker-ID Gating: bei aktiviertem Fingerprint pruefen wir die ersten # Speaker-ID Gating: bei aktiviertem Fingerprint pruefen wir die ersten
@@ -533,6 +546,35 @@ class SessionManager:
if audio_ms < STREAM_MIN_AUDIO_MS: if audio_ms < STREAM_MIN_AUDIO_MS:
return return
# Akustische Sprach-Aktivitaet JEDEN Tick (~200ms) messen — unabhaengig
# vom Transcribe-Throttle. Solange wirklich gesprochen wird, bleibt die
# Session am Leben, auch wenn Whisper gerade keinen neuen Text liefert.
if self._tail_rms(sess) >= STREAM_VOICE_RMS_THRESHOLD:
sess.last_voice_at = now
# Endpoint-Entscheidung JEDEN Tick, sobald ueberhaupt Text erkannt wurde:
# (a) akustisch: seit endpoint_ms keine Sprach-Energie mehr → User ist
# fertig. Das ist der robuste Primaerpfad gegen „hoert nach zwei
# Worten auf" (waehrend echten Sprechens ist Energie da → kein Cut).
# (b) semantisch (Backstop): Transkript stagniert deutlich laenger als
# endpoint_ms — fuer laute Umgebungen wo die Energie nie faellt.
if sess.last_growth_at > 0.0 and not sess.endpoint_sent:
acoustic_silence_ms = (now - sess.last_voice_at) * 1000.0 if sess.last_voice_at > 0 else 0.0
semantic_silence_ms = (now - sess.last_growth_at) * 1000.0
acoustic_done = sess.last_voice_at > 0 and acoustic_silence_ms >= sess.endpoint_ms
semantic_done = semantic_silence_ms >= sess.endpoint_ms * STREAM_SEMANTIC_BACKUP_FACTOR
if acoustic_done or semantic_done:
logger.info(
"Stream %s: Endpoint (%s) — akustisch %dms / semantisch %dms — Text=%r",
sess.request_id[:8],
"akustisch" if acoustic_done else "semantisch",
int(acoustic_silence_ms), int(semantic_silence_ms),
sess.last_partial[:80],
)
await self._finalize(sess, ws,
reason="endpoint" if acoustic_done else "endpoint_semantic")
return
# Transcribe-Throttling # Transcribe-Throttling
since_last = (now - sess.last_transcribe_at) * 1000.0 since_last = (now - sess.last_transcribe_at) * 1000.0
if since_last < STREAM_TRANSCRIBE_INTERVAL_MS: if since_last < STREAM_TRANSCRIBE_INTERVAL_MS:
@@ -568,18 +610,8 @@ class SessionManager:
}) })
await _debug_log(ws, "stream.partial", await _debug_log(ws, "stream.partial",
f"id={sess.request_id[:12]} text={text[:80]!r}") f"id={sess.request_id[:12]} text={text[:80]!r}")
else: # else: kein neuer Text — die Endpoint-Entscheidung (akustisch +
# Stagnation pruefen — Endpoint-Bedingung # semantischer Backstop) laeuft oben pro Tick, hier nichts mehr zu tun.
if sess.last_growth_at == 0.0:
# Noch gar kein Text erkannt. Wenn der User gar nichts sagt
# springt Brain irgendwann aus eigenem Conversation-Window-
# Timeout in der App raus; wir machen hier nix.
return
silence_ms = (now - sess.last_growth_at) * 1000.0
if silence_ms >= sess.endpoint_ms and not sess.endpoint_sent:
logger.info("Stream %s: Endpoint nach %dms ohne neuen Text — Text=%r",
sess.request_id[:8], int(silence_ms), sess.last_partial[:80])
await self._finalize(sess, ws, reason="endpoint")
def _buffer_duration_ms(self, sess: StreamSession) -> float: def _buffer_duration_ms(self, sess: StreamSession) -> float:
# 16-bit s16le mono → 2 bytes pro Sample # 16-bit s16le mono → 2 bytes pro Sample
@@ -588,6 +620,23 @@ class SessionManager:
return 0.0 return 0.0
return (samples / sess.sample_rate) * 1000.0 return (samples / sess.sample_rate) * 1000.0
def _tail_rms(self, sess: StreamSession) -> float:
"""RMS-Energie der letzten STREAM_ENERGY_WINDOW_MS des Audio-Buffers.
Dient als akustisches „redet noch / ist still"-Signal."""
win_bytes = int(sess.sample_rate * STREAM_ENERGY_WINDOW_MS / 1000) * 2
if win_bytes <= 0:
return 0.0
tail = sess.pcm_buffer[-win_bytes:]
if len(tail) < 2:
return 0.0
try:
arr = pcm_s16le_to_float32(bytes(tail))
except Exception:
return 0.0
if arr.size == 0:
return 0.0
return float(np.sqrt(np.mean(arr * arr)))
async def _finalize(self, sess: StreamSession, ws, reason: str) -> None: async def _finalize(self, sess: StreamSession, ws, reason: str) -> None:
"""Endgueltige Transkription auf dem vollen Buffer (beam_size=5), """Endgueltige Transkription auf dem vollen Buffer (beam_size=5),
feuert stt_endpoint + stt_stream_done, droppt Session.""" feuert stt_endpoint + stt_stream_done, droppt Session."""