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 json
import os
import re
import tempfile
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@ -23,6 +24,44 @@ MAX_AUDIO_BYTES = 30 * 1024 * 1024
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:
body = json.dumps(payload).encode("utf-8")
handler.send_response(status)
@ -104,9 +143,12 @@ class SpeechHandler(BaseHTTPRequestHandler):
fp16=torch.cuda.is_available(),
condition_on_previous_text=False,
temperature=0,
compression_ratio_threshold=2.0,
logprob_threshold=-0.8,
no_speech_threshold=0.5,
verbose=False,
)
transcript = str(result.get("text") or "").strip()
transcript = _clean_transcript(result)
_json(self, 200, {"text": transcript, "model": MODEL_NAME})
except Exception as exc:
print(f"[stt] transcription failed: {exc}", flush=True)

View File

@ -108,24 +108,42 @@
setState('listening');
try{
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;}
stream=capture;
const Context=window.AudioContext||window.webkitAudioContext;
audioContext=new Context();
const analyser=audioContext.createAnalyser();
analyser.fftSize=1024;
audioContext.createMediaStreamSource(stream).connect(analyser);
analyser.fftSize=2048;
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 mimeTypes=['audio/webm;codecs=opus','audio/ogg;codecs=opus','audio/webm'];
const mime=mimeTypes.find(function(value){return MediaRecorder.isTypeSupported(value);})||'';
const chunks=[];
const preRoll=[];
let heardSpeech=false;
let voiceFrames=0;
let noiseFloor=0.008;
let lastSpeech=Date.now();
const started=Date.now();
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(){
if(vadTimer){clearInterval(vadTimer);vadTimer=null;}
const recordedStream=stream;
@ -149,7 +167,17 @@
}
const rms=Math.sqrt(energy/samples.length);
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 timedOut=now-started>=90000;
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/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/config-rev: "20260810-worker-spaces-permissions"
ai.bstein.dev/config-rev: "20260810-detached-worker-spaces"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: hermes-agent
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
@ -350,7 +350,7 @@ spec:
requests: {cpu: 250m, memory: 512Mi}
limits: {cpu: "2", memory: 4Gi}
- name: webui
image: registry.bstein.dev/bstein/hermes-webui@sha256:6eb16f76b236e10b210a31e69c7e68103cbf921892d813561d63f8aab0a98c32
image: registry.bstein.dev/bstein/hermes-webui@sha256:c109e6faec1d6b86859a182bc845a2e35d64260459dda3892c0510db1dc7272d
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:

View File

@ -28,7 +28,7 @@ spec:
ai.bstein.dev/role: isolated-user-chat
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/config-rev: "20260810-natural-jetson-voice"
ai.bstein.dev/config-rev: "20260810-noise-gated-voice"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: hermes-chat
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
@ -226,7 +226,7 @@ spec:
requests: {cpu: 250m, memory: 512Mi}
limits: {cpu: "1", memory: 2Gi}
- name: webui
image: registry.bstein.dev/bstein/hermes-webui@sha256:6eb16f76b236e10b210a31e69c7e68103cbf921892d813561d63f8aab0a98c32
image: registry.bstein.dev/bstein/hermes-webui@sha256:c109e6faec1d6b86859a182bc845a2e35d64260459dda3892c0510db1dc7272d
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
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/role: testing-triage
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
# created Secret. The role is declared in
# services/vault/scripts/vault_k8s_auth_configure.sh and bound to
@ -345,7 +345,7 @@ spec:
cpu: "2"
memory: 4Gi
- name: webui
image: registry.bstein.dev/bstein/hermes-webui@sha256:6eb16f76b236e10b210a31e69c7e68103cbf921892d813561d63f8aab0a98c32
image: registry.bstein.dev/bstein/hermes-webui@sha256:c109e6faec1d6b86859a182bc845a2e35d64260459dda3892c0510db1dc7272d
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:

View File

@ -133,6 +133,94 @@ def route_unmanaged_tabs(
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:
"""Continuously reconcile Agent tabs while allowing Herdr to own workers."""
while True:
@ -141,6 +229,8 @@ def run_loop(label: str, interval: float) -> None:
if workspace_id:
for pane_id in route_unmanaged_tabs(workspace_id):
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:
print(f"Agent tab routing retry: {type(error).__name__}: {error}", flush=True)
time.sleep(interval)

View File

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

View File

@ -3,6 +3,8 @@
from __future__ import annotations
import importlib.util
import sys
from types import SimpleNamespace
from pathlib import Path
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 "pending=fetchSpeech(chunks[index+1])" 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():
@ -238,6 +281,10 @@ def test_voice_workloads_have_deliberate_xavier_placement():
assert stt["runtimeClassName"] == "nvidia"
assert stt["securityContext"]["supplementalGroups"] == [44]
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["limits"]["nvidia.com/gpu.shared"] == 1
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"]]
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():
deployment = yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
containers = deployment["spec"]["template"]["spec"]["containers"]