Compare commits

..
3 Commits
Author SHA1 Message Date
duffyduck 3693157210 release: bump version to 0.2.4.4 2026-08-16 19:57:14 +02:00
duffyduckandClaude Opus 4.8 64670cdd12 fix(voxtral): Repetition-Loop bremsen ('vergiss das'-Schleife)
Stefans Repro: eine echte Nachricht endete mit "...vergiss das, das ist nur..."
und Voxtral hat den Satz ~15x geloopt, bis zur Brain durch. Klassische
Repetition-Halluzination bei Stille/Rauschen am Ende.

Zwei Ebenen: (1) Generation bekommt no_repeat_ngram_size=4 + repetition_penalty
=1.15 → erste echte Nennung bleibt, exakte 4-Gramm-Wiederholung verboten, Loop
bricht an der Quelle ab. (2) Post-Detektor _collapse_repetitions kassiert einen
durchgerutschten Loop auf eine Kopie ein. Gegen den echten Loop + legitime
Doppelungen ('ja ja ja', 'sehr sehr') verifiziert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 19:42:22 +02:00
duffyduckandClaude Opus 4.8 c82616ebbd fix(voxtral): No-Speech-Timeout — Stille-Fenster schließt selbst
Stefans Repro: "die Stille-Ende wird nie erreicht, stop ich selbst ist es weg,
und geht automatisch auf lausche Computer". Ursache: der Endpoint feuert nur
wenn schon Stimme da war (last_voice_at>0). Bei totaler Stille bleibt
last_voice_at==0 → Endpoint feuert NIE → Fenster offen bis Hardcap/manuellem
Stop (→ stream_end → Phantom).

Fix: No-Speech-Timeout im _tick — wenn nach endpoint_ms (Stille-Toleranz) ab
Start noch KEINE Stimme kam, schließt der Bridge das Fenster selbst als
no-speech (leer, lautlos, zurück aufs Wake-Word). voiced_frames==0 →
_finalize verwirft ohne Transkript, also kein Phantom. Logik gegen totale
Stille / Sprache-dann-still / Dauer-Sprache verifiziert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 19:38:38 +02:00
3 changed files with 52 additions and 4 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 20403
versionName "0.2.4.3"
versionCode 20404
versionName "0.2.4.4"
// Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general'
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aria-cockpit",
"version": "0.2.4.3",
"version": "0.2.4.4",
"private": true,
"scripts": {
"android": "react-native run-android",
+49 -1
View File
@@ -94,6 +94,24 @@ _HALLUCINATION_RE = re.compile(
re.IGNORECASE,
)
# Kollabiert unmittelbar wiederholte Phrasen (Voxtral-Repetition-Loop) auf EINE
# Kopie. Zweites Netz hinter no_repeat_ngram in der Generation. Phrase 5-80 Zeichen,
# 3+ mal hintereinander → eine. Kurze legitime Doppelungen ('ja ja', 'sehr sehr')
# bleiben (Unit < 5 Zeichen bzw. < 3 Wiederholungen).
_REPEAT_RE = re.compile(r"(.{5,80}?)(?:\s*\1){2,}", re.IGNORECASE | re.DOTALL)
def _collapse_repetitions(text: str) -> str:
if not text:
return text
out = text
for _ in range(3): # mehrfach fuer verschachtelte/ungleiche Loops
new = _REPEAT_RE.sub(r"\1", out)
if new == out:
break
out = new
return out.strip()
# Speaker-ID Gating global an/aus. DEFAULT AUS (fail-open) — die "nur meine Stimme"-
# Pruefung ist ein BEWUSSTER Schalter, kein Automatismus: ein einziger schlechter
# Enroll darf nie die ganze STT lahmlegen (genau das ist passiert). Wird per config-
@@ -162,7 +180,20 @@ class VoxtralRunner:
with torch.no_grad():
# hoch genug fuer lange Diktate (stoppt eh am EOS); 512 hat
# mehrminutige Aufnahmen abgeschnitten.
outputs = model.generate(**inputs, max_new_tokens=4096)
# Repetition-Bremse: Voxtral kippt bei Stille/Rauschen am Ende
# gern in eine Schleife und wiederholt einen Satz zig-mal
# ("Vergiss das, das ist nur... Vergiss das, das ist nur..."
# x15). no_repeat_ngram_size=4 laesst die ERSTE echte Nennung
# durch, verbietet aber die exakte 4-Gramm-Wiederholung → Loop
# bricht ab; repetition_penalty daempft zusaetzlich. Beides mild,
# damit normale Sprache (auch mal ein doppeltes Wort) unberuehrt
# bleibt.
outputs = model.generate(
**inputs,
max_new_tokens=4096,
no_repeat_ngram_size=4,
repetition_penalty=1.15,
)
trimmed = outputs[:, inputs.input_ids.shape[1]:]
text = proc.batch_decode(trimmed, skip_special_tokens=True)
return (text[0] if text else "").strip()
@@ -428,6 +459,17 @@ class SessionManager:
})
else:
self._update_noise_floor(sess, rms)
# No-Speech-Timeout: wurde die GANZE Zeit KEINE Stimme erkannt
# (last_voice_at==0), feuert der normale Endpoint unten NIE — der braucht
# last_voice_at>0. Ohne das bleibt ein reines Stille-Fenster offen bis
# Hardcap/manuellem Stop → genau Stefans Repro: "die Stille-Ende wird nie
# erreicht, stop ich selbst ist es weg". Nach endpoint_ms Stille ab Start
# schliessen wir das Fenster selbst als no-speech (leer, lautlos, zurueck
# aufs Wake-Word). voiced_frames==0 → _finalize verwirft ohne Transkript,
# also KEIN Phantom.
if sess.last_voice_at == 0 and (now - sess.started_at) * 1000.0 >= sess.endpoint_ms:
await self._finalize(sess, "no_speech")
return
# Endpoint: hat der User schon gesprochen UND ist es seit endpoint_ms still?
if sess.last_voice_at > 0 and (now - sess.last_voice_at) * 1000.0 >= sess.endpoint_ms:
await self._finalize(sess, "endpoint")
@@ -474,6 +516,12 @@ class SessionManager:
final_text = sess.last_partial
stt_ms = int((time.time() - t0) * 1000)
duration_s = audio.size / 16000.0
# Repetition-Loop einkassieren, falls trotz no_repeat_ngram was durchkam.
_collapsed = _collapse_repetitions(final_text)
if _collapsed != final_text:
logger.info("Stream %s: Repetition-Loop kollabiert (%d%d Zeichen)",
sess.request_id[:8], len(final_text), len(_collapsed))
final_text = _collapsed
logger.info("Stream %s: FINAL (reason=%s, %.1fs, %dms): %r",
sess.request_id[:8], reason, duration_s, stt_ms, final_text[:120])