fix(hermes): isolate workers and gate noisy speech

This commit is contained in:
jenkins 2026-08-10 01:47:51 -03:00
parent 4f986b6ccb
commit 17e2ecfcf6
9 changed files with 306 additions and 14 deletions

View File

@ -6,6 +6,7 @@ from __future__ import annotations
import cgi import cgi
import json import json
import os import os
import re
import tempfile import tempfile
import threading import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@ -23,6 +24,44 @@ MAX_AUDIO_BYTES = 30 * 1024 * 1024
MODEL_LOCK = threading.Lock() MODEL_LOCK = threading.Lock()
def _repetitive_token(token: str) -> bool:
"""Identify long periodic Whisper hallucinations caused by steady noise."""
letters = "".join(re.findall(r"[a-z]+", token.lower()))
if len(letters) < 10:
return False
for period in range(1, 5):
pattern = letters[:period]
matches = sum(
character == pattern[index % period]
for index, character in enumerate(letters)
)
if matches / len(letters) >= 0.86:
return True
return max(letters.count(character) for character in set(letters)) / len(letters) >= 0.78
def _clean_transcript(result: dict) -> str:
"""Drop noise-only segments and repetitive tokens while retaining speech."""
segments = result.get("segments")
if not isinstance(segments, list):
segments = [{"text": result.get("text") or ""}]
kept: list[str] = []
for segment in segments:
if not isinstance(segment, dict):
continue
text = str(segment.get("text") or "").strip()
if not text:
continue
no_speech = float(segment.get("no_speech_prob") or 0.0)
average_logprob = float(segment.get("avg_logprob") or 0.0)
if no_speech >= 0.55 and average_logprob <= -0.55:
continue
words = [word for word in text.split() if not _repetitive_token(word)]
if words:
kept.append(" ".join(words))
return " ".join(kept).strip()
def _json(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None: def _json(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None:
body = json.dumps(payload).encode("utf-8") body = json.dumps(payload).encode("utf-8")
handler.send_response(status) handler.send_response(status)
@ -104,9 +143,12 @@ class SpeechHandler(BaseHTTPRequestHandler):
fp16=torch.cuda.is_available(), fp16=torch.cuda.is_available(),
condition_on_previous_text=False, condition_on_previous_text=False,
temperature=0, temperature=0,
compression_ratio_threshold=2.0,
logprob_threshold=-0.8,
no_speech_threshold=0.5,
verbose=False, verbose=False,
) )
transcript = str(result.get("text") or "").strip() transcript = _clean_transcript(result)
_json(self, 200, {"text": transcript, "model": MODEL_NAME}) _json(self, 200, {"text": transcript, "model": MODEL_NAME})
except Exception as exc: except Exception as exc:
print(f"[stt] transcription failed: {exc}", flush=True) print(f"[stt] transcription failed: {exc}", flush=True)

View File

@ -108,24 +108,42 @@
setState('listening'); setState('listening');
try{ try{
const capture=await navigator.mediaDevices.getUserMedia({ const capture=await navigator.mediaDevices.getUserMedia({
audio:{echoCancellation:true,noiseSuppression:true,autoGainControl:true}, audio:(function(){
const constraints={echoCancellation:true,noiseSuppression:true,autoGainControl:true};
const supported=navigator.mediaDevices.getSupportedConstraints?navigator.mediaDevices.getSupportedConstraints():{};
if(supported.voiceIsolation) constraints.voiceIsolation=true;
return constraints;
})(),
}); });
if(!active||token!==generation){capture.getTracks().forEach(function(track){track.stop();});return;} if(!active||token!==generation){capture.getTracks().forEach(function(track){track.stop();});return;}
stream=capture; stream=capture;
const Context=window.AudioContext||window.webkitAudioContext; const Context=window.AudioContext||window.webkitAudioContext;
audioContext=new Context(); audioContext=new Context();
const analyser=audioContext.createAnalyser(); const analyser=audioContext.createAnalyser();
analyser.fftSize=1024; analyser.fftSize=2048;
audioContext.createMediaStreamSource(stream).connect(analyser); const highpass=audioContext.createBiquadFilter();
highpass.type='highpass';
highpass.frequency.value=140;
highpass.Q.value=0.7;
audioContext.createMediaStreamSource(stream).connect(highpass);
highpass.connect(analyser);
const samples=new Uint8Array(analyser.fftSize); const samples=new Uint8Array(analyser.fftSize);
const mimeTypes=['audio/webm;codecs=opus','audio/ogg;codecs=opus','audio/webm']; const mimeTypes=['audio/webm;codecs=opus','audio/ogg;codecs=opus','audio/webm'];
const mime=mimeTypes.find(function(value){return MediaRecorder.isTypeSupported(value);})||''; const mime=mimeTypes.find(function(value){return MediaRecorder.isTypeSupported(value);})||'';
const chunks=[]; const chunks=[];
const preRoll=[];
let heardSpeech=false; let heardSpeech=false;
let voiceFrames=0;
let noiseFloor=0.008;
let lastSpeech=Date.now(); let lastSpeech=Date.now();
const started=Date.now(); const started=Date.now();
recorder=new MediaRecorder(stream,mime?{mimeType:mime}:undefined); recorder=new MediaRecorder(stream,mime?{mimeType:mime}:undefined);
recorder.ondataavailable=function(event){if(event.data&&event.data.size) chunks.push(event.data);}; recorder.ondataavailable=function(event){
if(!event.data||!event.data.size) return;
if(heardSpeech){chunks.push(event.data);return;}
preRoll.push(event.data);
while(preRoll.length>3) preRoll.shift();
};
recorder.onstop=function(){ recorder.onstop=function(){
if(vadTimer){clearInterval(vadTimer);vadTimer=null;} if(vadTimer){clearInterval(vadTimer);vadTimer=null;}
const recordedStream=stream; const recordedStream=stream;
@ -149,7 +167,17 @@
} }
const rms=Math.sqrt(energy/samples.length); const rms=Math.sqrt(energy/samples.length);
const now=Date.now(); const now=Date.now();
if(rms>0.025){heardSpeech=true;lastSpeech=now;} const speechThreshold=Math.max(0.04,noiseFloor*2.4+0.006);
const voiceNow=rms>speechThreshold;
if(!heardSpeech&&!voiceNow){noiseFloor=(noiseFloor*0.94)+(rms*0.06);}
voiceFrames=voiceNow?Math.min(voiceFrames+1,5):Math.max(voiceFrames-1,0);
if(!heardSpeech&&voiceFrames>=3){
heardSpeech=true;
lastSpeech=now;
while(preRoll.length) chunks.push(preRoll.shift());
}else if(heardSpeech&&voiceNow){
lastSpeech=now;
}
const finished=heardSpeech&&(now-lastSpeech)>=silenceMs; const finished=heardSpeech&&(now-lastSpeech)>=silenceMs;
const timedOut=now-started>=90000; const timedOut=now-started>=90000;
const idle=(!heardSpeech)&&(now-started)>=20000; const idle=(!heardSpeech)&&(now-started)>=20000;

View File

@ -24,7 +24,7 @@ spec:
ai.bstein.dev/execution: Herdr-supervised Codex and Claude Code ai.bstein.dev/execution: Herdr-supervised Codex and Claude Code
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback
ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available
ai.bstein.dev/config-rev: "20260810-worker-spaces-permissions" ai.bstein.dev/config-rev: "20260810-detached-worker-spaces"
vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: hermes-agent vault.hashicorp.com/role: hermes-agent
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
@ -350,7 +350,7 @@ spec:
requests: {cpu: 250m, memory: 512Mi} requests: {cpu: 250m, memory: 512Mi}
limits: {cpu: "2", memory: 4Gi} limits: {cpu: "2", memory: 4Gi}
- name: webui - name: webui
image: registry.bstein.dev/bstein/hermes-webui@sha256:6eb16f76b236e10b210a31e69c7e68103cbf921892d813561d63f8aab0a98c32 image: registry.bstein.dev/bstein/hermes-webui@sha256:c109e6faec1d6b86859a182bc845a2e35d64260459dda3892c0510db1dc7272d
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec] command: [/bin/sh, -ec]
args: args:

View File

@ -28,7 +28,7 @@ spec:
ai.bstein.dev/role: isolated-user-chat ai.bstein.dev/role: isolated-user-chat
ai.bstein.dev/isolation: one Hermes process and PVC per Keycloak subject ai.bstein.dev/isolation: one Hermes process and PVC per Keycloak subject
ai.bstein.dev/model-policy: uniform automatic policy with per-user overrides ai.bstein.dev/model-policy: uniform automatic policy with per-user overrides
ai.bstein.dev/config-rev: "20260810-natural-jetson-voice" ai.bstein.dev/config-rev: "20260810-noise-gated-voice"
vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: hermes-chat vault.hashicorp.com/role: hermes-chat
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
@ -226,7 +226,7 @@ spec:
requests: {cpu: 250m, memory: 512Mi} requests: {cpu: 250m, memory: 512Mi}
limits: {cpu: "1", memory: 2Gi} limits: {cpu: "1", memory: 2Gi}
- name: webui - name: webui
image: registry.bstein.dev/bstein/hermes-webui@sha256:6eb16f76b236e10b210a31e69c7e68103cbf921892d813561d63f8aab0a98c32 image: registry.bstein.dev/bstein/hermes-webui@sha256:c109e6faec1d6b86859a182bc845a2e35d64260459dda3892c0510db1dc7272d
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec] command: [/bin/sh, -ec]
args: args:

View File

@ -24,7 +24,7 @@ spec:
ai.bstein.dev/model: anthropic/claude-opus-5, falling back to openai-codex/gpt-5.6-terra then local gpt-oss:20b ai.bstein.dev/model: anthropic/claude-opus-5, falling back to openai-codex/gpt-5.6-terra then local gpt-oss:20b
ai.bstein.dev/role: testing-triage ai.bstein.dev/role: testing-triage
ai.bstein.dev/placement: titan-21 preferred, Jetson preferred, arm64 fallback ai.bstein.dev/placement: titan-21 preferred, Jetson preferred, arm64 fallback
ai.bstein.dev/config-rev: "20260808-dedicated-triage" ai.bstein.dev/config-rev: "20260810-noise-gated-webui"
# The Anthropic credential comes from Vault rather than a manually # The Anthropic credential comes from Vault rather than a manually
# created Secret. The role is declared in # created Secret. The role is declared in
# services/vault/scripts/vault_k8s_auth_configure.sh and bound to # services/vault/scripts/vault_k8s_auth_configure.sh and bound to
@ -345,7 +345,7 @@ spec:
cpu: "2" cpu: "2"
memory: 4Gi memory: 4Gi
- name: webui - name: webui
image: registry.bstein.dev/bstein/hermes-webui@sha256:6eb16f76b236e10b210a31e69c7e68103cbf921892d813561d63f8aab0a98c32 image: registry.bstein.dev/bstein/hermes-webui@sha256:c109e6faec1d6b86859a182bc845a2e35d64260459dda3892c0510db1dc7272d
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec] command: [/bin/sh, -ec]
args: args:

View File

@ -133,6 +133,94 @@ def route_unmanaged_tabs(
return started return started
def _pane_number(pane_id: str) -> int:
"""Return a stable numeric sort key for Herdr pane identifiers."""
match = re.search(r":p(\d+)$", pane_id)
return int(match.group(1)) if match else 2**31 - 1
def _worker_label(
tab_label: str,
pane: dict[str, Any],
agent_names: dict[str, str],
) -> str:
"""Describe a detached worker using its parent tab, provider, and role."""
pane_id = str(pane.get("pane_id") or "")
parent = _slug(tab_label)
raw_name = agent_names.get(pane_id, "").strip()
name = _slug(raw_name) if raw_name else ""
provider = _slug(str(pane.get("agent") or "worker"))
suffix = _slug(pane_id.split(":")[-1])
if name and name not in {parent, f"{parent}-{suffix}"}:
label = name if name.startswith(f"{parent}-") else f"{parent}-{name}"
else:
label = f"{parent}-{provider}-{suffix}"
return label[:32].rstrip("-")
def isolate_worker_panes(
workspace_id: str,
run: Callable[[list[str]], dict[str, Any]] = _run_json,
) -> list[str]:
"""Keep one coordinator pane per tab and detach every worker workspace."""
tabs_payload = run([str(HERDR_BIN), "tab", "list"])
panes_payload = run(
[str(HERDR_BIN), "pane", "list", "--workspace", workspace_id]
)
agents_payload = run([str(HERDR_BIN), "agent", "list"])
tab_labels = {
str(item.get("tab_id") or ""): str(item.get("label") or "session")
for item in _result_list(tabs_payload, "tabs")
if item.get("workspace_id") == workspace_id
}
agent_names = {
str(item.get("pane_id") or ""): str(item.get("name") or "")
for item in _result_list(agents_payload, "agents")
if item.get("workspace_id") == workspace_id
}
panes_by_tab: dict[str, list[dict[str, Any]]] = {}
for pane in _result_list(panes_payload, "panes"):
if pane.get("workspace_id") != workspace_id:
continue
tab_id = str(pane.get("tab_id") or "")
if tab_id:
panes_by_tab.setdefault(tab_id, []).append(pane)
moved: list[str] = []
for tab_id, panes in panes_by_tab.items():
if len(panes) <= 1:
continue
# The oldest Hermes pane is the tab's stable coordinator. Directly
# spawned Codex/Claude workers and later Hermes helpers are detached.
primary = min(
panes,
key=lambda pane: (
0 if pane.get("agent") == "hermes" else 1,
_pane_number(str(pane.get("pane_id") or "")),
),
)
for pane in panes:
if pane is primary:
continue
pane_id = str(pane.get("pane_id") or "")
if not pane_id:
continue
run(
[
str(HERDR_BIN),
"pane",
"move",
pane_id,
"--new-workspace",
"--label",
_worker_label(tab_labels.get(tab_id, "session"), pane, agent_names),
"--no-focus",
]
)
moved.append(pane_id)
return moved
def run_loop(label: str, interval: float) -> None: def run_loop(label: str, interval: float) -> None:
"""Continuously reconcile Agent tabs while allowing Herdr to own workers.""" """Continuously reconcile Agent tabs while allowing Herdr to own workers."""
while True: while True:
@ -141,6 +229,8 @@ def run_loop(label: str, interval: float) -> None:
if workspace_id: if workspace_id:
for pane_id in route_unmanaged_tabs(workspace_id): for pane_id in route_unmanaged_tabs(workspace_id):
print(f"Started routed Hermes session in {pane_id}", flush=True) print(f"Started routed Hermes session in {pane_id}", flush=True)
for pane_id in isolate_worker_panes(workspace_id):
print(f"Detached worker pane {pane_id}", flush=True)
except Exception as error: except Exception as error:
print(f"Agent tab routing retry: {type(error).__name__}: {error}", flush=True) print(f"Agent tab routing retry: {type(error).__name__}: {error}", flush=True)
time.sleep(interval) time.sleep(interval)

View File

@ -31,7 +31,7 @@ spec:
kubernetes.io/hostname: titan-21 kubernetes.io/hostname: titan-21
containers: containers:
- name: stt - name: stt
image: registry.bstein.dev/bstein/hermes-jetson-stt@sha256:341fd5c93d202d7368bbd74bd392f207c66e6d3968cc39f2d1684ec87a9d3899 image: registry.bstein.dev/bstein/hermes-jetson-stt@sha256:dfb0b0788dcf63747d8c761c6d3ea6a7459bcd8d0a5c92bcd4f6f434a760249f
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
ports: ports:
- {name: http, containerPort: 9000, protocol: TCP} - {name: http, containerPort: 9000, protocol: TCP}
@ -42,7 +42,7 @@ spec:
- {name: HERMES_STT_MODEL, value: small} - {name: HERMES_STT_MODEL, value: small}
- {name: HERMES_STT_CACHE, value: /opt/models/whisper} - {name: HERMES_STT_CACHE, value: /opt/models/whisper}
- {name: NVIDIA_VISIBLE_DEVICES, value: all} - {name: NVIDIA_VISIBLE_DEVICES, value: all}
- {name: NVIDIA_DRIVER_CAPABILITIES, value: compute,utility} - {name: NVIDIA_DRIVER_CAPABILITIES, value: "compute,utility"}
- {name: JETSON_JETPACK, value: "5"} - {name: JETSON_JETPACK, value: "5"}
startupProbe: startupProbe:
httpGet: {path: /health, port: http} httpGet: {path: /health, port: http}

View File

@ -3,6 +3,8 @@
from __future__ import annotations from __future__ import annotations
import importlib.util import importlib.util
import sys
from types import SimpleNamespace
from pathlib import Path from pathlib import Path
import yaml import yaml
@ -185,6 +187,47 @@ def test_chat_voice_uses_private_jetson_services_and_shared_auto_route():
assert "window._splitForTTS(text,280)" in voice_script assert "window._splitForTTS(text,280)" in voice_script
assert "pending=fetchSpeech(chunks[index+1])" in voice_script assert "pending=fetchSpeech(chunks[index+1])" in voice_script
assert "restartSoon(token,450)" in voice_script assert "restartSoon(token,450)" in voice_script
assert "constraints.voiceIsolation=true" in voice_script
assert "highpass.frequency.value=140" in voice_script
assert "Math.max(0.04,noiseFloor*2.4+0.006)" in voice_script
assert "while(preRoll.length>3) preRoll.shift()" in voice_script
stt_server = (ROOT / "dockerfiles" / "hermes-jetson-stt-server.py").read_text()
assert "def _repetitive_token" in stt_server
assert "compression_ratio_threshold=2.0" in stt_server
assert "no_speech_threshold=0.5" in stt_server
def test_voice_transcript_filter_removes_fan_hallucinations(monkeypatch):
server_path = ROOT / "dockerfiles" / "hermes-jetson-stt-server.py"
spec = importlib.util.spec_from_file_location("hermes_jetson_stt_server", server_path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, "cgi", SimpleNamespace())
monkeypatch.setitem(
sys.modules,
"torch",
SimpleNamespace(cuda=SimpleNamespace(is_available=lambda: False)),
)
monkeypatch.setitem(sys.modules, "whisper", SimpleNamespace())
spec.loader.exec_module(module)
result = {
"segments": [
{
"text": " ththththththththth Testing.",
"no_speech_prob": 0.12,
"avg_logprob": -0.2,
},
{
"text": " background hum",
"no_speech_prob": 0.82,
"avg_logprob": -0.9,
},
]
}
assert module._clean_transcript(result) == "Testing."
def test_voice_models_are_baked_and_runtime_has_no_public_egress(): def test_voice_models_are_baked_and_runtime_has_no_public_egress():
@ -238,6 +281,10 @@ def test_voice_workloads_have_deliberate_xavier_placement():
assert stt["runtimeClassName"] == "nvidia" assert stt["runtimeClassName"] == "nvidia"
assert stt["securityContext"]["supplementalGroups"] == [44] assert stt["securityContext"]["supplementalGroups"] == [44]
stt_resources = stt["containers"][0]["resources"] stt_resources = stt["containers"][0]["resources"]
stt_env = {
item["name"]: item["value"] for item in stt["containers"][0]["env"]
}
assert stt_env["NVIDIA_DRIVER_CAPABILITIES"] == "compute,utility"
assert stt_resources["requests"]["nvidia.com/gpu.shared"] == 1 assert stt_resources["requests"]["nvidia.com/gpu.shared"] == 1
assert stt_resources["limits"]["nvidia.com/gpu.shared"] == 1 assert stt_resources["limits"]["nvidia.com/gpu.shared"] == 1
assert "nvidia.com/gpu.shared" not in tts["containers"][0]["resources"]["requests"] assert "nvidia.com/gpu.shared" not in tts["containers"][0]["resources"]["requests"]

View File

@ -295,6 +295,91 @@ def test_agent_tab_router_only_targets_coordinator_workspace():
assert calls == [[str(tab_router.HERDR_BIN), "workspace", "list"]] assert calls == [[str(tab_router.HERDR_BIN), "workspace", "list"]]
def test_agent_tab_router_detaches_workers_without_closing_sessions():
calls = []
def fake_run(command):
calls.append(command)
if command[1:3] == ["tab", "list"]:
return {
"result": {
"tabs": [
{
"workspace_id": "w2",
"tab_id": "w2:t2",
"label": "cassandra",
}
]
}
}
if command[1:3] == ["pane", "list"]:
return {
"result": {
"panes": [
{
"workspace_id": "w2",
"tab_id": "w2:t2",
"pane_id": "w2:p2",
"agent": "hermes",
},
{
"workspace_id": "w2",
"tab_id": "w2:t2",
"pane_id": "w2:p8",
"agent": "claude",
},
{
"workspace_id": "w2",
"tab_id": "w2:t2",
"pane_id": "w2:p6",
"agent": "codex",
},
{
"workspace_id": "w2",
"tab_id": "w2:t2",
"pane_id": "w2:p7",
"agent": "hermes",
},
]
}
}
if command[1:3] == ["agent", "list"]:
return {
"result": {
"agents": [
{"workspace_id": "w2", "pane_id": "w2:p2", "name": "cassandra-p2"},
{"workspace_id": "w2", "pane_id": "w2:p8", "name": "cassandra-claude-review"},
{"workspace_id": "w2", "pane_id": "w2:p6", "name": "cassandra-codex-review"},
{"workspace_id": "w2", "pane_id": "w2:p7", "name": "cassandra-p7"},
]
}
}
return {"result": {"ok": True}}
moved = tab_router.isolate_worker_panes("w2", fake_run)
assert moved == ["w2:p8", "w2:p6", "w2:p7"]
move_calls = calls[-3:]
assert move_calls[0][1:] == [
"pane",
"move",
"w2:p8",
"--new-workspace",
"--label",
"cassandra-claude-review",
"--no-focus",
]
assert move_calls[1][-2:] == ["cassandra-codex-review", "--no-focus"]
assert move_calls[2][-2:] == ["cassandra-hermes-p7", "--no-focus"]
assert not any("close" in command for command in move_calls)
def test_detached_worker_label_falls_back_to_parent_provider_and_pane():
pane = {"pane_id": "w4:p9", "agent": "codex"}
assert tab_router._worker_label("Work", pane, {}) == "work-codex-p9"
def test_agent_ttyd_defers_identity_to_owner_only_oauth_boundary(): def test_agent_ttyd_defers_identity_to_owner_only_oauth_boundary():
deployment = yaml.safe_load((HERMES / "agent-deployment.yaml").read_text()) deployment = yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
containers = deployment["spec"]["template"]["spec"]["containers"] containers = deployment["spec"]["template"]["spec"]["containers"]