Compare commits
5
Commits
v0.2.4.2
...
3693157210
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3693157210 | ||
|
|
64670cdd12 | ||
|
|
c82616ebbd | ||
|
|
b226e1da11 | ||
|
|
0b7ed241b4 |
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 20402
|
||||
versionName "0.2.4.2"
|
||||
versionCode 20404
|
||||
versionName "0.2.4.4"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.2.4.2",
|
||||
"version": "0.2.4.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
+70
-11
@@ -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")
|
||||
@@ -438,14 +480,21 @@ class SessionManager:
|
||||
sess.endpoint_sent = True
|
||||
# Halluzinations-Guard: zu wenig echte Stimme (Stille / kurzer Blip im
|
||||
# Passiv-/Wake-Fenster) → NICHT transkribieren. Voxtral (wie Whisper) baut
|
||||
# aus Fast-Nichts gern einen Fuellsatz ("keine Ahnung" o.ae.), der dann als
|
||||
# PHANTOM-Nachricht ans Brain geht und das Gespraech entgleisen laesst
|
||||
# (Stefans Repro: "kam Nachricht von mir, obwohl ich nichts sagte"). Leeres
|
||||
# Endpoint = no-speech → App re-armt still. Der manuelle Stop (stream_end)
|
||||
# ist ausgenommen: dort hat der User bewusst gesprochen (kurze Woerter ok).
|
||||
if reason != "stream_end" and sess.voiced_frames < STREAM_MIN_VOICED_FRAMES:
|
||||
# aus Fast-Nichts gern einen Fuellsatz ("Die Stadt hat eine Flaeche von
|
||||
# 1,5 km2"), der dann als PHANTOM-Nachricht ans Brain geht und das Gespraech
|
||||
# entgleisen laesst (Stefans Repro: "kam Nachricht von mir, obwohl ich
|
||||
# nichts sagte"). Leeres Endpoint = no-speech → App re-armt still.
|
||||
#
|
||||
# WICHTIG (aus dem ai-box-Log gelernt): die Phantome kommen mit
|
||||
# reason=stream_end — Passiv-/Wake-Fenster enden AUCH per stream_end, wenn
|
||||
# sie auf Stille zumachen. stream_end ist also NICHT gleich "manueller Stop".
|
||||
# Deshalb greift der Guard jetzt auch bei stream_end, aber mit niedrigerer
|
||||
# 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.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, STREAM_MIN_VOICED_FRAMES, reason)
|
||||
sess.request_id[:8], sess.voiced_frames, _min_voiced, reason)
|
||||
if self._ws is not None:
|
||||
nospeech = {"requestId": sess.request_id,
|
||||
"audioRequestId": sess.audio_request_id,
|
||||
@@ -467,17 +516,27 @@ 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])
|
||||
|
||||
# Halluzinations-Filter (2. Netz): leeres/Artefakt-Transkript im borderline-
|
||||
# Band → als no-speech verwerfen statt ein Phantom ("Die Stadt hat eine
|
||||
# Flaeche von 1,5 km2") ans Brain zu schicken. Manueller Stop (stream_end)
|
||||
# ist ausgenommen (bewusst gesprochen, auch kurze Woerter zaehlen).
|
||||
# Flaeche von 1,5 km2") ans Brain zu schicken. Gilt fuer ALLE reasons inkl.
|
||||
# stream_end (dort kamen die realen Phantome!) — aber das borderline-Band
|
||||
# (wenig voiced_frames) schuetzt echte, klar gesprochene Eingaben: eine echte
|
||||
# Geografie-FRAGE hat normale Stimm-Energie (voiced_frames >> Schwelle) und
|
||||
# geht durch; das Phantom aus Stille hat ~0 und wird verworfen. Ein leeres
|
||||
# Transkript wird immer verworfen (nichts gesagt = nichts senden).
|
||||
_clean = final_text.strip(" .,!?…-\t\n\r")
|
||||
_borderline = sess.voiced_frames < STREAM_HALLUC_GUARD_FRAMES
|
||||
_is_phantom = (not _clean) or (_borderline and bool(_HALLUCINATION_RE.search(final_text)))
|
||||
if reason != "stream_end" and _is_phantom:
|
||||
if _is_phantom:
|
||||
logger.info("Stream %s: Halluzination verworfen (voiced_frames=%d<%d, %.1fs, text=%r)",
|
||||
sess.request_id[:8], sess.voiced_frames, STREAM_HALLUC_GUARD_FRAMES,
|
||||
duration_s, final_text[:80])
|
||||
|
||||
Reference in New Issue
Block a user