Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
![Trinity Assistant Banner](assets/banner.png)
> [!NOTE]
> **Aktuelle Highlights:**
> - **v0.17.10:** Die G2-Spracherkennung reagiert schneller und robuster: Digitale Stille, unsichere No-Speech-Segmente und bekannte Whisper-Untertitelartefakte wie `Copyright WDR`, `Amara.org` oder erfundene Schnellsession-Domains werden verworfen. Der bisher zu starke Fachwort-Bias ist auf das eigentliche Wakeword `Trinity` reduziert.
> - **v0.17.9:** Eine Trinity-Instanz in einer Windows-VM kann Eve-STT und Eve-TTS jetzt von einem privaten Ubuntu-NVIDIA-Host beziehen. Windows bleibt Control Plane mit Sessions, Memory, Agenten und UI; Ubuntu uebernimmt nur GPU-Inferenz. Dafuer gibt es die Profile `eve-windows-remote` und `eve-linux-gpu-server`, getrennte Tokens, Diagnosepruefungen und Installationsskripte.
> - **v0.17.8:** ClassicUI, iPhone und iPad zeigen nur noch einen zentralen Lautsprecherknopf. Aktiviert ein Client seine Ausgabe, wird er Bridge-weit zum einzigen Sprecher und alle anderen verbundenen Clients zeigen den durchgestrichenen Lautsprecher. Ein erneuter Klick auf dem aktiven Geraet schaltet Trinity ueberall stumm; auch Eves direkter Desktop- und Companion-Audiopfad folgt dieser Auswahl.
> - **v0.17.7:** Die Even G2 erhaelt einen kompakten, zwischengespeicherten Umgebungskontext: die neueste n-tv-Schlagzeile, Wetter und Temperatur der naechsten Stunde sowie optional den naechsten Kalendertermin. iPhone/iPad koennen Standort und Kalender einzeln und widerrufbar an die private Bridge melden; ohne Freigabe gilt der in der G2 gewaehlte Ersatzort.
> - Die vollstaendige Historie steht in **[RELEASES.md](RELEASES.md)** und in den detaillierten **[Release Notes](docs/release_notes/)**.

> [!IMPORTANT]
Expand Down
1 change: 1 addition & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Einzelnotizen liegen unter [docs/release_notes](docs/release_notes/).

## Aktuelle Highlights

- **v0.17.10:** Die G2-Audiobridge nutzt einen neutralen deutschen Erkennungskontext mit `Trinity` als einzigem Hotword. Digitale Stille, unsichere No-Speech-Segmente und typische Whisper-Untertitelhalluzinationen wie `Copyright WDR`, `Amara.org` oder erfundene Schnellsession-Domains werden vor dem Routing verworfen. Kleinere Beam-Suchen senken die G2-Latenz, ohne Desktop-, Companion- oder Eve-Sprachpfade zu veraendern.
- **v0.17.9:** Windows kann als Trinity-Control-Plane in einer VM laufen, waehrend ein privater Ubuntu-Host mit NVIDIA-GPU Parakeet-STT, Qwen3-TTS/Eve und das konfigurierte OpenAI-kompatible LLM bereitstellt. Die neuen Profile `eve-windows-remote` und `eve-linux-gpu-server`, getrennte Voice-/Core-Tokens, Remote-Diagnosen und Installationsskripte vermeiden unnoetiges GPU-Passthrough. Native Mac- und Windows-Eve-Profile sowie Legacy STT/TTS bleiben unveraendert waehlbar.
- **v0.17.8:** ClassicUI, iPhone und iPad verwenden einen einzigen Bridge-weiten Lautsprecherknopf. Das ausgewaehlte Geraet ist exklusiv; alle anderen Clients werden samt direkter Eve-Wiedergabe sofort stumm und zeigen den durchgestrichenen Lautsprecher. Ein zweiter Klick auf dem aktiven Geraet setzt die Ausgabe explizit auf `Stumm`, statt sie unbemerkt an einen anderen Client weiterzureichen.
- **v0.17.7:** Even G2 zeigt eine neue n-tv-Schlagzeile kurz als Laufhinweis sowie Wetter und Temperatur der naechsten Stunde im kompakten Statusfeld. Optional meldet der Companion den aktuellen Standort und den naechsten Kalendertermin an die private Bridge; beides bleibt nur im Arbeitsspeicher und wird beim Abschalten entfernt. Ohne Freigabe verwendet Trinity den in G2 konfigurierten Ersatzort.
Expand Down
51 changes: 40 additions & 11 deletions core/bridge_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@

import base64
import binascii
import re
import threading


G2_SAMPLE_RATE = 16_000
MAX_AUDIO_SECONDS = 20
TRINITY_VOCABULARY = (
"Trinity. Deutsche Vorlesung und natuerliche Konversation. "
"Kommandos: Trinity, Hilfe, Modus Zuruf, Zurufmodus, "
"Konversationsmodus, Wakeword, Arbeitsraum, "
"Schnellsession, Nash-Gleichgewicht, Gefangenendilemma und Kooperation."
"Natuerliche deutsche Sprache. Trinity ist der Name der Assistentin."
)
TRINITY_HOTWORDS = (
"Trinity Hilfe Zuruf Zurufmodus Konversationsmodus Wakeword "
"Arbeitsraum Schnellsession Nash-Gleichgewicht "
"Gefangenendilemma Kooperation"
TRINITY_HOTWORDS = "Trinity"

_KNOWN_SUBTITLE_HALLUCINATIONS = (
re.compile(r"\bcopyright\b.*\b(?:ard|zdf|wdr|ndr|swr|br|mdr|rbb)\b", re.IGNORECASE),
re.compile(r"\buntertitel\b.*\b(?:auftrag|community|amara)\b", re.IGNORECASE),
re.compile(r"\bamara\s*\.\s*org\b", re.IGNORECASE),
re.compile(r"\b(?:www\s*\.\s*)?schnellsessions?\s*\.\s*com\b", re.IGNORECASE),
)


Expand Down Expand Up @@ -61,6 +62,10 @@ def _ensure_model(self):

def transcribe(self, audio_base64, *, sample_rate=G2_SAMPLE_RATE, language="de", quality="balanced"):
audio = self.decode_pcm(audio_base64, sample_rate=sample_rate)
import numpy as np

if float(np.sqrt(np.mean(np.square(audio), dtype=np.float64))) < 0.0015:
return {"text": "", "language": str(language or ""), "language_probability": 0.0}
selected_language = str(language or "de").strip().lower()
if selected_language in {"", "auto"}:
selected_language = None
Expand All @@ -70,7 +75,7 @@ def transcribe(self, audio_base64, *, sample_rate=G2_SAMPLE_RATE, language="de",
quality = str(quality or "balanced").strip().lower()
if quality not in {"fast", "balanced", "precise"}:
raise ValueError("Erkennungsqualitaet muss fast, balanced oder precise sein.")
beam_size = {"fast": 1, "balanced": 3, "precise": 5}[quality]
beam_size = {"fast": 1, "balanced": 2, "precise": 4}[quality]

with self._lock:
segments, info = self._ensure_model().transcribe(
Expand All @@ -80,13 +85,37 @@ def transcribe(self, audio_base64, *, sample_rate=G2_SAMPLE_RATE, language="de",
hotwords=TRINITY_HOTWORDS,
condition_on_previous_text=False,
vad_filter=True,
vad_parameters={
"threshold": 0.5,
"min_speech_duration_ms": 180,
"min_silence_duration_ms": 250,
"speech_pad_ms": 120,
},
beam_size=beam_size,
best_of=beam_size,
best_of=1,
temperature=0.0,
no_speech_threshold=0.55,
log_prob_threshold=-0.9,
)
text = " ".join(str(segment.text or "").strip() for segment in segments).strip()
accepted = []
for segment in segments:
text = str(segment.text or "").strip()
no_speech = float(getattr(segment, "no_speech_prob", 0.0) or 0.0)
average_log_probability = float(getattr(segment, "avg_logprob", 0.0) or 0.0)
if no_speech > 0.65 and average_log_probability < -0.7:
continue
if text and not self.is_known_hallucination(text):
accepted.append(text)
text = " ".join(accepted).strip()
if self.is_known_hallucination(text):
text = ""
return {
"text": text,
"language": str(getattr(info, "language", selected_language or "") or ""),
"language_probability": float(getattr(info, "language_probability", 0.0) or 0.0),
}

@staticmethod
def is_known_hallucination(text):
normalized = " ".join(str(text or "").split())
return bool(normalized) and any(pattern.search(normalized) for pattern in _KNOWN_SUBTITLE_HALLUCINATIONS)
6 changes: 6 additions & 0 deletions docs/release_notes/v0.17.10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Trinity v0.17.10 - G2 Speech Reliability

- Replaces the long G2 vocabulary bias with a neutral German recognition prompt and the single `Trinity` hotword.
- Rejects digital silence, low-confidence no-speech segments, and known Whisper subtitle hallucinations before they can reach Trinity.
- Uses smaller balanced and precise beam searches to reduce G2 latency without changing Desktop, Companion, or Eve speech paths.
- Adds regression coverage for `Copyright WDR`, `Amara.org`, fake Schnellsession domains, silence, and uncertain speech segments.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "trinity-assistant"
version = "0.17.8"
version = "0.17.10"
description = "Local-first academic personal concierge for macOS, Windows 11, and Linux servers"
readme = "README.md"
requires-python = ">=3.10,<3.15"
Expand Down
53 changes: 48 additions & 5 deletions tests/test_bridge_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,19 @@ def transcribe(self, _audio, **kwargs):

transcriber = BridgeAudioTranscriber()
transcriber._model = FakeModel()
encoded = base64.b64encode(b"\x00\x00" * 1600).decode("ascii")
encoded = base64.b64encode(np.full(1600, 1200, dtype="<i2").tobytes()).decode("ascii")

result = transcriber.transcribe(encoded)

assert result["text"] == "Trinity, Modus Zuruf"
assert calls[0]["condition_on_previous_text"] is False
assert calls[0]["beam_size"] == 3
assert "Zuruf" in calls[0]["hotwords"]
assert "Nash-Gleichgewicht" in calls[0]["initial_prompt"]
assert calls[0]["beam_size"] == 2
assert calls[0]["hotwords"] == "Trinity"
assert "Schnellsession" not in calls[0]["initial_prompt"]
assert calls[0]["best_of"] == 1

transcriber.transcribe(encoded, quality="precise")
assert calls[1]["beam_size"] == 5
assert calls[1]["beam_size"] == 4

with pytest.raises(ValueError, match="Erkennungsqualitaet"):
transcriber.transcribe(encoded, quality="maximum")
Expand All @@ -79,6 +80,48 @@ def test_bridge_audio_prompt_does_not_bias_spontaneous_checklist_hallucinations(
assert "stichwoerter" not in prompt


@pytest.mark.parametrize(
"text",
[
"Copyright WDR 2024",
"Untertitel im Auftrag des ZDF",
"Untertitel der Amara.org Community",
"www.schnellsessions.com",
],
)
def test_bridge_audio_filters_known_subtitle_and_domain_hallucinations(text):
assert BridgeAudioTranscriber.is_known_hallucination(text)


def test_bridge_audio_drops_silent_audio_without_loading_the_model():
transcriber = BridgeAudioTranscriber()
transcriber._ensure_model = lambda: (_ for _ in ()).throw(AssertionError("model must stay unloaded"))
encoded = base64.b64encode(b"\x00\x00" * 1600).decode("ascii")

assert transcriber.transcribe(encoded)["text"] == ""


def test_bridge_audio_drops_low_confidence_no_speech_segments():
class Segment:
text = " Copyright WDR "
no_speech_prob = 0.91
avg_logprob = -1.2

class Info:
language = "de"
language_probability = 0.99

class FakeModel:
def transcribe(self, _audio, **_kwargs):
return [Segment()], Info()

transcriber = BridgeAudioTranscriber()
transcriber._model = FakeModel()
encoded = base64.b64encode(np.full(1600, 1200, dtype="<i2").tobytes()).decode("ascii")

assert transcriber.transcribe(encoded)["text"] == ""


def test_audio_transcription_http_endpoint_accepts_authenticated_g2_request(tmp_path):
(tmp_path / "core").mkdir()
(tmp_path / "memory").mkdir()
Expand Down
Loading