Hotfix nach Realtest: Turn-Hold konservativ + Smart-Turn-Vorwaermen (3,3s -> 0,16s Kaltstart)
User-Feedback 'sehr langsam' diagnostiziert (voice_metrics): (1) zwei App-Instanzen liefen parallel (Dev-Instanz vergessen — beendet), (2) Semantik-Hold hielt echte Saetze auf. - useVAD: warten NUR noch bei sehr sicherem 'unfertig' (P(fertig) < 0.15 statt < 0.5), max. eine Warterunde pro Aeusserung, HOLD 1,8s -> 1,2s, Not-Aus localStorage lucy_turncheck=0, Entscheidung wird bei lucy_perf=1 geloggt (fuer echtes Nachtunen) - voice_service: Smart Turn beim Start vorwaermen (Probe-Inferenz auf Stille) — der 3,3s-Kaltstart traf sonst den ersten gesprochenen Satz; deployt + verifiziert (0,16s) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,11 +20,16 @@ const REDEMPTION_MS = 450 // so lange Stille -> Äußerung zu Ende (war 900
|
|||||||
const PRE_SPEECH_PAD_MS = 320 // Vorlauf mitschneiden (erster Wortanfang nicht abschneiden)
|
const PRE_SPEECH_PAD_MS = 320 // Vorlauf mitschneiden (erster Wortanfang nicht abschneiden)
|
||||||
const MIN_SPEECH_MS = 160 // kürzer = Klick/Räuspern -> verwerfen
|
const MIN_SPEECH_MS = 160 // kürzer = Klick/Räuspern -> verwerfen
|
||||||
const COOLDOWN_MS = 450 // nach Lucys Antwort kurz taub (Echo/Lautsprecher abklingen lassen)
|
const COOLDOWN_MS = 450 // nach Lucys Antwort kurz taub (Echo/Lautsprecher abklingen lassen)
|
||||||
// Semantische Turn-Detection (Smart Turn v3 auf der Box): meldet sie 'incomplete' (User denkt
|
// Semantische Turn-Detection (Smart Turn v3 auf der Box): meldet sie 'unfertig' (User denkt
|
||||||
// mitten im Satz nach), warten wir bis zu HOLD_MS auf die Fortsetzung und hängen sie an,
|
// mitten im Satz nach), warten wir bis zu HOLD_MS auf die Fortsetzung und hängen sie an,
|
||||||
// statt mitten im Gedanken zu antworten. Hart begrenzt, damit Lucy nie ewig schweigt.
|
// statt mitten im Gedanken zu antworten. Hart begrenzt, damit Lucy nie ewig schweigt.
|
||||||
const HOLD_MS = 1800
|
// KONSERVATIV seit 02.07. abends: Nur bei SEHR sicherem 'unfertig' warten (P(fertig) < 0.15) —
|
||||||
|
// beim ersten Realtest hielt der Check zu viele echte Sätze auf (+1,8s je Turn, „sehr langsam").
|
||||||
|
// Not-Aus ohne Rebuild: localStorage lucy_turncheck = "0".
|
||||||
|
const HOLD_MS = 1200
|
||||||
|
const HOLD_BELOW_P = 0.15 // nur unter dieser P(fertig) wird gewartet
|
||||||
const MAX_UTTERANCE_S = 30 // Sicherheitsdeckel fürs Zusammenhängen
|
const MAX_UTTERANCE_S = 30 // Sicherheitsdeckel fürs Zusammenhängen
|
||||||
|
const TURNCHECK_ENABLED = typeof localStorage === "undefined" || localStorage.getItem("lucy_turncheck") !== "0"
|
||||||
|
|
||||||
// Float32-Samples (16 kHz mono) -> WAV-Blob (PCM16). Ersetzt den MediaRecorder-webm-Umweg:
|
// Float32-Samples (16 kHz mono) -> WAV-Blob (PCM16). Ersetzt den MediaRecorder-webm-Umweg:
|
||||||
// die Box muss kein Opus mehr dekodieren, Parakeet/Whisper bekommen direkt sauberes WAV.
|
// die Box muss kein Opus mehr dekodieren, Parakeet/Whisper bekommen direkt sauberes WAV.
|
||||||
@@ -44,16 +49,17 @@ function toWavBlob(samples: Float32Array, sampleRate = 16000): Blob {
|
|||||||
return new Blob([buf], { type: "audio/wav" })
|
return new Blob([buf], { type: "audio/wav" })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fragt die Box, ob die Äußerung semantisch fertig ist. Fehler => true (nie blockieren).
|
// Fragt die Box nach P(fertig) der Äußerung. Fehler => 1.0 (nie blockieren, sofort senden).
|
||||||
async function isTurnComplete(audio: Float32Array): Promise<boolean> {
|
async function turnCompleteness(audio: Float32Array): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
fd.append("audio", toWavBlob(audio.subarray(Math.max(0, audio.length - 8 * 16000))), "rec.wav")
|
fd.append("audio", toWavBlob(audio.subarray(Math.max(0, audio.length - 8 * 16000))), "rec.wav")
|
||||||
const r = await fetch(`${BOX_URL}/api/voice/turn`, { method: "POST", body: fd })
|
const r = await fetch(`${BOX_URL}/api/voice/turn`, { method: "POST", body: fd })
|
||||||
if (!r.ok) return true
|
if (!r.ok) return 1.0
|
||||||
return (await r.json()).complete !== false
|
const j = await r.json()
|
||||||
|
return typeof j.probability === "number" ? j.probability : 1.0
|
||||||
} catch {
|
} catch {
|
||||||
return true
|
return 1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,10 +124,16 @@ export function useVAD({ enabled, paused, onUtterance, onListening }: VADOptions
|
|||||||
if (combined.length > MAX_UTTERANCE_S * 16000) {
|
if (combined.length > MAX_UTTERANCE_S * 16000) {
|
||||||
combined = combined.subarray(combined.length - MAX_UTTERANCE_S * 16000)
|
combined = combined.subarray(combined.length - MAX_UTTERANCE_S * 16000)
|
||||||
}
|
}
|
||||||
const complete = await isTurnComplete(combined)
|
// Bereits einmal gehalten? Dann JETZT senden — maximal eine Warterunde pro Äußerung.
|
||||||
|
if (!TURNCHECK_ENABLED || pending) { emit(combined); return }
|
||||||
|
const t0 = performance.now()
|
||||||
|
const pComplete = await turnCompleteness(combined)
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
if (complete) { emit(combined); return }
|
if (localStorage.getItem("lucy_perf") === "1") {
|
||||||
// Mitten im Gedanken pausiert: kurz auf die Fortsetzung warten, dann notfalls doch senden.
|
console.log(`[lucy-perf] Turn-Check P(fertig)=${pComplete} in ${Math.round(performance.now() - t0)}ms -> ${pComplete < HOLD_BELOW_P ? "HALTEN" : "senden"}`)
|
||||||
|
}
|
||||||
|
if (pComplete >= HOLD_BELOW_P) { emit(combined); return }
|
||||||
|
// SEHR sicher mitten im Gedanken: kurz auf die Fortsetzung warten, dann notfalls senden.
|
||||||
pending = combined
|
pending = combined
|
||||||
if (holdTimer) clearTimeout(holdTimer)
|
if (holdTimer) clearTimeout(holdTimer)
|
||||||
holdTimer = setTimeout(() => { if (!cancelled && pending) emit(pending) }, HOLD_MS)
|
holdTimer = setTimeout(() => { if (!cancelled && pending) emit(pending) }, HOLD_MS)
|
||||||
|
|||||||
@@ -494,6 +494,18 @@ async def lifespan(_app: FastAPI):
|
|||||||
else:
|
else:
|
||||||
stt_model()
|
stt_model()
|
||||||
log.info("Voice-Sidecar bereit (STT whisper '%s', Piper-Dir %s).", STT_MODEL, VOICES_DIR)
|
log.info("Voice-Sidecar bereit (STT whisper '%s', Piper-Dir %s).", STT_MODEL, VOICES_DIR)
|
||||||
|
# Smart Turn vorwärmen (Modell + transformers-Import + 1 Probe-Inferenz): der erste
|
||||||
|
# /turn-Aufruf kostete sonst ~3,3 s — das traf beim Realtest den ersten gesprochenen Satz.
|
||||||
|
try:
|
||||||
|
import io as _io
|
||||||
|
import numpy as _np
|
||||||
|
import soundfile as _sf
|
||||||
|
buf = _io.BytesIO()
|
||||||
|
_sf.write(buf, _np.zeros(16000, dtype="float32"), 16000, format="WAV", subtype="PCM_16")
|
||||||
|
check_turn(buf.getvalue(), ".wav")
|
||||||
|
log.info("Smart Turn vorgewärmt.")
|
||||||
|
except Exception:
|
||||||
|
log.warning("Smart-Turn-Vorwärmen fehlgeschlagen (Feature bleibt lazy).", exc_info=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("STT-Vorwärmen fehlgeschlagen (Dienst läuft, /health meldet Detail).")
|
log.exception("STT-Vorwärmen fehlgeschlagen (Dienst läuft, /health meldet Detail).")
|
||||||
yield
|
yield
|
||||||
|
|||||||
Reference in New Issue
Block a user