HEL-910: real-call retest window record — web text interrupt, wait chain, budgets, cleanup evidence

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
xiaomo
2026-10-29 20:24:52 +08:00
co-authored by multica-agent
parent 958c6962fb
commit 407fc8158c
42 changed files with 3162 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
{
"baseline_version": "1.12",
"read_commit": "958c6962fb1f2a4462a6df794d893aee1fcb0667",
"task": "HEL-910:按白栖知批准的《HEL-797-交谈修复复测安排》(主卡评论 01a18753-8c70-7181-9758-5bab4685ba9f「继续测」)对候选 958c696 做两次真实通话复测:真实网页原发送入口的文字打断(在播时机+final待播间隙)、python 同钟时间轴的等待链与回放打断、思考隔离真实端点检查;隔离部署 8360/8361/8363/8082 + 共享 TTS 9880 按快照临时恢复,全程批准上限内执行并清理恢复。",
"requirement_ids": [
"CG-006",
"CG-009",
"CG-010",
"CG-013",
"CG-014",
"CG-015",
"CG-016",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"AM-001",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "docs/rework/hel910/(window-record.md 窗口记录、window/ 逐通证据含 call2 同钟字幕/帧时间轴/回流音频/DB终态/ASR账本/前后快照/日志、web/ 真网页逐步截图、tools/ 本窗部署与测量脚本及配置);服务器证据包 /opt/cybergril-hel910/hel910-evidence-20261029.tar.gz(sha256 83363576…d7a3f0)。实测结论:真实网页发文字(真 Enter 键)在她语音在播时发送——旧声停止(浏览器能量 ≤~1.4s 持续静默、本地 hush 静音即时)、文字获完整回复并落库、旧轮不复活无幽灵行、被打断轮回执正确缺席、后续语音轮正常出声并完整回执(两次确认恢复);输入结束→实际第一声实测 9.53s(user_final 后 7.10s、生成开始后 3.44s,>5s 关注线,如实交回);全部真实回复零思考泄漏但真实端点未出现带 </think> 形状(覆盖范围如实限定,离线证据在 HEL-909);时机A(生成中被文字取消)因网页侧检测失误与 API 直发竞速反向未获正面样本(文字轮被更新语音轮正常取消:行 cancelled+SSE 幂等 chat.cancelled 终态帧,恰为 HEL-908 R1 修复路径的真实表现),随后语音轮撞 10/10 预算硬闸诚实 φ 失败;可闻旧声期间的回放打断样本因时机错过未取得(如实,与 HEL-907 同项均未取得)。用量:LLM 10/10 触线(30,398/40,000 tokens)、建房 4/4 触线、通话 2/2、TTS 6/20、ASR 350.63/720s(保守=确证、重试 0)、窗口 23.6/45 分、登记/上传/转写/试听/嵌入/付费全 0。清理:窗口端口与专属进程全停(精确 PID)、LiveKit 房间空、voices.json 哈希前后一致 cg- 零残留、共享 TTS 按快照停回、面板 8090 全程未启用、8300/8310/8320/8350/7880/10095 未动、GPU 回 747MiB。",
"acceptance": [
"部署核验:服务器源码树与 git archive 958c696 逐文件 sha256 一致(仅多 REVISION 标记);web/dist 含 cg.interrupt 接线;models.yaml 三档 reasoning: buffer_turn 与候选一致仅预算块不同;cap=0 fail-closed 预检(persona 拒绝、chat 诚实 error 零出站、voice 503 不建房)通过后才布防",
"真实网页链路:自签证书按用户姿态逐 origin 接受;语音通话从真实 UI 开启;文字从真实输入框+真实 Enter 键发送(hush→cg.interrupt 真实链路,未绕开网页修复);逐步截图 9 张留证",
"时机B(在播时发文字):旧声停止(浏览器能量探测 ~1.4s 内持续静默+本地静音即时)、T1 完整回复落库 sent、R1 不复活、后续语音轮正常出声并完整回执",
"final待播间隙(网页入口):U3 回复文本完整落库(音频截断、回执正确缺席)、T2 完整回复、再次语音轮正常出声回执",
"等待链(同钟毫秒):input_end→user_final 2.43s→生成开始 4.10s→实际第一声 9.53s→字幕全文/落库 12.10s→回执 ack;数字与 wav 能量对齐方法留证",
"思考隔离:18 条 companion 行/字幕/TTS 文本零思考;如实声明真实端点未出现带 closer 形状",
"上限与封账:全部类别在上限内或触线即停并如实记录;失败/取消均计入(call1 挂断后 U4 轮、call2 竞速取消轮与预算 φ 轮)",
"清理恢复:端口/房间/PID/声音列表/哈希/GPU 前后核对留证;隔离目录与证据包保留;未触碰任何既有实例与真实档案"
],
"deferred": [
"真实用户麦克风打断时延与可闻旧声期间的语音打断停声样本(两轮工程窗口均未取得,需真人同轴或下轮工程窗口以她实际有声为门控重试)",
"时机A(生成中文字取消)正面样本(需文字在语音轮服务端激活后、生成完成前到达;本轮竞速反向+预算封顶)",
"真实端点带 </think> 形状的分离复现(本轮真实回复未出现该形状;离线证据 HEL-909)",
"首声等待是否经常>5s 需多样本(本轮 1 精确样本+1 量级相近);停声时延的帧级精测(本轮为浏览器能量粗粒度 ≤~1.4s)",
"真机/手机麦克风、Safari(D3 用户动作)",
"R1–R7 完整功能未完成项继续按主卡承接;本轮不改变任何需求与验收状态"
],
"requirement_changes": [],
"verification": [
"python3 scripts/check_requirements.py --base-ref origin/main(及 --base-ref 958c696)+ --handoff docs/requirements/handoff-HEL-910.json 全部通过(结构检查,不代替原话核对与体验验收)",
"证据:docs/rework/hel910/window/(call2.subtitles.jsonl 逐事件毫秒时间戳、call2.frames.json 帧到达时间轴、call2.agent.wav 回流音频、db-final.json 全部 20 行含状态与回执账、asr-usage.jsonl、tts-window.log 6 段、api/worker 日志、前后快照);web/ 截图;tools/ 全套部署与测量脚本",
"未验证:真人麦克风、手机真机、真实端点思考形状、多样本首声统计——见 deferred,不冒充"
]
}
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# HEL-910 测试窗口:统一实例(8360 HTTP + 8361 HTTPS 同进程双入口)启动/停止
# 用法: bash api_ctl.sh start|stop|status (models.yaml 由调用方先摆好:failclosed 或 armed)
set -u
BASE=/opt/cybergril-hel910
CODE=$BASE/current
LOG=$BASE/logs/api.log
PIDF=$BASE/logs/api.pid
PAT='cybergril-hel910.*serve_dual_8360_8361'
stop() {
if [ -f "$PIDF" ]; then
P=$(cat "$PIDF")
if [ "$P" != "$$" ] && kill -0 "$P" 2>/dev/null; then kill "$P"; sleep 3; fi
if [ "$P" != "$$" ] && kill -0 "$P" 2>/dev/null; then kill -9 "$P"; fi
rm -f "$PIDF"
fi
for p in $(pgrep -f "$PAT"); do
[ "$p" = "$$" ] && continue
kill "$p" 2>/dev/null
done
echo "api stopped"
}
case "${1:-}" in
stop) stop; exit 0 ;;
status)
pgrep -af "$PAT" || echo "not running"
exit 0 ;;
esac
stop
cd "$CODE"
set -a
export CYBERGRIL_DB=$BASE/data/companion.db
export CYBERGRIL_SEMANTIC=stub
export CYBERGRIL_R2_ENABLED=1
export CYBERGRIL_MODELS=$BASE/etc/models.yaml
export CYBERGRIL_VOICE_CONFIG=$BASE/etc/voice.yaml
export CYBERGRIL_WEB=$CODE/web/dist
export PYTHONPATH=$CODE/src
set +a
nohup /opt/cybergril-hel879/current/.venv311/bin/python deploy/voice/serve_dual_8360_8361.py \
--http-port 8360 --https-port 8361 \
--cert /opt/cybergril-voice-certs/cybergril-voice.pem \
--key /opt/cybergril-voice-certs/cybergril-voice.key \
> "$LOG" 2>&1 &
echo $! > "$PIDF"
echo "api starting (pid $(cat $PIDF))"
sleep 6
curl -s --max-time 3 http://127.0.0.1:8360/api/health >/dev/null && echo "OK: 8360 healthy" || { echo "FAIL:"; tail -20 "$LOG"; exit 1; }
curl -sk --max-time 3 https://127.0.0.1:8361/api/health >/dev/null && echo "OK: 8361 healthy" || { echo "FAIL 8361:"; tail -20 "$LOG"; exit 1; }
+4
View File
@@ -0,0 +1,4 @@
# HEL-910 ASR 窗口布防(2026-10-29):新窗口 id,720 秒保守预记,每通重连≤2
CYBERGRIL_ASR_WINDOW_ID=hel910-20261029-a
CYBERGRIL_ASR_WINDOW_MAX_AUDIO_SECONDS=720
CYBERGRIL_ASR_MAX_CONNECT_RETRIES_PER_CALL=2
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# HEL-910 测试窗口:WSS TLS 桥 8363 -> 127.0.0.1:7880 启动/停止
set -u
BASE=/opt/cybergril-hel910
LOG=$BASE/logs/bridge.log
PIDF=$BASE/logs/bridge.pid
PAT='cybergril-hel910.*wss_bridge'
stop() {
if [ -f "$PIDF" ]; then
P=$(cat "$PIDF")
if [ "$P" != "$$" ] && kill -0 "$P" 2>/dev/null; then kill "$P"; sleep 2; fi
rm -f "$PIDF"
fi
for p in $(pgrep -f "$PAT"); do
[ "$p" = "$$" ] && continue
kill "$p" 2>/dev/null
done
echo "bridge stopped"
}
case "${1:-}" in
stop) stop; exit 0 ;;
status)
pgrep -af "$PAT" || echo "not running"
exit 0 ;;
esac
stop
nohup /usr/bin/python3 $BASE/current/deploy/voice/wss_bridge.example.py \
--listen 0.0.0.0:8363 --upstream 127.0.0.1:7880 \
--cert /opt/cybergril-voice-certs/cybergril-voice.pem \
--key /opt/cybergril-voice-certs/cybergril-voice.key \
> "$LOG" 2>&1 &
echo $! > "$PIDF"
echo "bridge starting (pid $(cat $PIDF))"
sleep 3
timeout 8 openssl s_client -connect 127.0.0.1:8363 -servername 192.168.200.12 </dev/null 2>/dev/null | grep -q "Protocol.*TLSv1.3\|New, TLSv1.3" && echo "OK: bridge TLS up" || { echo "WARN: TLS probe inconclusive, check $LOG"; tail -5 "$LOG"; }
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Build Chromium fake-audio-capture WAVs for HEL-910 (offline, workstation-local).
Pure stdlib (array/audioop-free) — resample by linear interpolation, mix to mono,
place utterances on a fixed schedule padded with silence; Chromium loops the file.
Usage: build_fake_wavs.py <outdir>
"""
from __future__ import annotations
import json
import sys
import wave
from pathlib import Path
RATE = 16000
def read_mono_16k(path: str) -> bytes:
with wave.open(path, "rb") as w:
raw = w.readframes(w.getnframes())
rate, ch, width = w.getframerate(), w.getnchannels(), w.getsampwidth()
assert width == 2, path
total = len(raw) // (2 * ch)
# de-interleave + average channels
acc = [0.0] * total
for c in range(ch):
off = c
for i in range(total):
s = raw[(i * ch + off) * 2] | (raw[(i * ch + off) * 2 + 1] << 8)
if s >= 32768:
s -= 65536
acc[i] += s
mono = [int(round(a / ch)) for a in acc]
if rate == RATE:
return b"".join(max(-32768, min(32767, s)).to_bytes(2, "little", signed=True) for s in mono)
n_out = int(len(mono) * RATE / rate)
out = bytearray()
step = rate / RATE
for j in range(n_out):
x = j * step
i = int(x)
frac = x - i
s = mono[i] * (1 - frac) + (mono[i + 1] if i + 1 < len(mono) else mono[i]) * frac
v = max(-32768, min(32767, int(round(s))))
out += v.to_bytes(2, "little", signed=True)
return bytes(out)
def trim_silence(pcm: bytes, thresh: float = 0.05) -> bytes:
# trim leading/trailing near-silence (50ms windows) — keep speech core
n = len(pcm) // 2
vals = [pcm[i * 2] | (pcm[i * 2 + 1] << 8) for i in range(n)]
vals = [v - 65536 if v >= 32768 else v for v in vals]
win = 800
nw = n // win
peak = 1
for w0 in range(nw):
m = max(abs(v) for v in vals[w0 * win:(w0 + 1) * win]) or 0
peak = max(peak, m)
t = peak * thresh
first, last = 0, nw - 1
for w0 in range(nw):
if max(abs(v) for v in vals[w0 * win:(w0 + 1) * win]) > t:
first = w0
break
for w0 in range(nw - 1, -1, -1):
if max(abs(v) for v in vals[w0 * win:(w0 + 1) * win]) > t:
last = w0
break
lo, hi = max(0, first - 2) * win, min(n, (last + 3) * win)
out = bytearray()
for v in vals[lo:hi]:
out += max(-32768, min(32767, v)).to_bytes(2, "little", signed=True)
return bytes(out)
def silence(seconds: float) -> bytes:
return b"\x00\x00" * int(RATE * seconds)
def write_wav(path: Path, pcm: bytes, rate: int = RATE) -> None:
with wave.open(str(path), "wb") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(rate)
w.writeframes(pcm)
def main() -> None:
outdir = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
outdir.mkdir(parents=True, exist_ok=True)
src = Path("/tmp/multica-task-2856608943/opencode")
u_hai = trim_silence(read_mono_16k(str(src / "haimianbaobao.wav")))
u_baj = trim_silence(read_mono_16k(str(src / "bajie.wav")))
u_xiao = trim_silence(read_mono_16k(str(src / "xiaoyun.wav")))
meta: dict[str, dict] = {}
secs = lambda p: len(p) / (2 * RATE)
# call1 browser schedule: U1@0 (reply R1), U2@60 (gen-cancel target),
# U3@120 (voice recovery). Cycle 240s so a 4th utterance never arrives inside
# the ~170s planned call.
c1 = bytearray()
c1 += u_hai
c1 += silence(60 - secs(u_hai))
c1 += u_baj
c1 += silence(120 - 60 - secs(u_baj))
c1 += u_hai
c1 += silence(240 - 120 - secs(u_hai))
write_wav(outdir / "hel910_call1_fake.wav", bytes(c1))
meta["call1"] = {
"u1_haimianbaobao_s": [0.0, round(secs(u_hai), 2)],
"u2_bajie_s": [60.0, round(60 + secs(u_baj), 2)],
"u3_haimianbaobao_s": [120.0, round(120 + secs(u_hai), 2)],
"cycle_s": 240,
}
# offline probe wav for the local fake-capture feasibility check: 3s speech, 8s silence
t = bytearray()
t += u_hai[: RATE * 3 * 2]
t += silence(8.0)
write_wav(outdir / "probe_fake.wav", bytes(t))
meta["probe"] = {"speech_s": [0.0, round(min(secs(u_hai), 3.0), 2)], "cycle_s": round(min(secs(u_hai), 3.0) + 8.0, 2)}
for name, pcm in (("u_haimianbaobao", u_hai), ("u_bajie", u_baj), ("u_xiaoyun", u_xiao)):
write_wav(outdir / f"{name}.trim.wav", pcm)
meta[name] = {"seconds": round(secs(pcm), 2)}
(outdir / "fake-wav-meta.json").write_text(json.dumps(meta, ensure_ascii=False, indent=1))
print(json.dumps(meta, ensure_ascii=False))
if __name__ == "__main__":
main()
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# HEL-910 窗口截止保护:45 分钟硬顶到点强制收尾(也供 watchdog 与异常路径调用)。
# 只动本窗专属句柄(pid 文件里的精确 PID)与本窗房间;绝不按程序名/端口扫杀,
# 绝不触碰 8300/8310/8320/8350、7880/7881、10095、8081 等共享服务。
set -u
BASE=/opt/cybergril-hel910
LOG=$BASE/logs/force_end.log
ts() { date -u +%FT%TZ; }
{
echo "[$(ts)] force_end begin"
# 1) 结清本窗房间(实例自己的登记账;DELETE 走统一实例所有者路径)
for sid in $(python3 - "$BASE/data" <<'EOF' 2>/dev/null
import json, sys, pathlib
p = pathlib.Path(sys.argv[1]) / "companion-voice-sessions.json"
try:
d = json.loads(p.read_text())
except Exception:
raise SystemExit
state = d.get("state", d)
def walk(o):
if isinstance(o, dict):
for k, v in o.items():
if k in ("sessions", "active") and isinstance(v, dict):
for sid, sess in v.items():
print(sid)
else:
walk(v)
elif isinstance(o, list):
for i in o:
walk(i)
walk(state)
EOF
); do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 -X DELETE "http://127.0.0.1:8360/api/voice/session/$sid" || echo err)
echo "[$(ts)] DELETE session $sid -> $code"
done
# 2) 停专属组件(每步都幂等)
bash $BASE/ctl/worker_ctl.sh stop 2>&1 | sed 's/^/ worker: /'
bash $BASE/ctl/api_ctl.sh stop 2>&1 | sed 's/^/ api: /'
bash $BASE/ctl/bridge_ctl.sh stop 2>&1 | sed 's/^/ bridge: /'
# 3) 共享 TTS 9880:只停我们启动的那个 PID
bash $BASE/ctl/tts_ctl.sh stop 2>&1 | sed 's/^/ tts: /'
echo "[$(ts)] force_end done"
} >> "$LOG" 2>&1
+274
View File
@@ -0,0 +1,274 @@
// HEL-910 call1: REAL web page drives the whole scenario (workstation-local Chromium, real HTTPS entry).
// - cert interstitials accepted exactly like a user (高级→继续) on 8361 AND 8363 (WSS bridge)
// - voice call started from the real UI (voice-button → 开始语音)
// - user voice = Chromium fake mic fed with the scheduled WAV (U1 haimianbaobao @0s,
// U2 bajie @60s, U3 haimianbaobao @120s; built offline from existing engineering samples)
// - TEXT interrupts are typed into the real composer textarea and sent with a REAL Enter key:
// T1 while her R1 answer is AUDIBLY playing (timing B, hush→cg.interrupt path)
// T2 while her R2 answer is STILL GENERATING (timing A, server turn-gate path)
// - in-page probes (single page clock): stream energy 50ms, el.muted transitions, voice phase,
// heard-final text, chat DOM rows, fetch network marks, MediaRecorder of the received stream
// Usage: node hel910_call1.mjs <chrome> <fakeWav> <outDir> <baseURL>
import { spawn } from 'node:child_process';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
const [chrome, fakeWav, out, baseArg] = process.argv.slice(2);
const base = baseArg || 'https://192.168.200.12:8361';
const wssOrigin = 'https://192.168.200.12:8363';
mkdirSync(out, { recursive: true });
const cdpPort = 9713;
const T1_TEXT = '先停下,我用打字跟你说:把你刚才那句还没说完的话作废,改用文字告诉我,你今天想拉我一起做的第一件事是什么?';
const T2_TEXT = '等一下,先别把刚才那段长回答说完,我打字补充:只用一句话告诉我你现在的心情,然后安静等我。';
const INIT = `(() => {
if (window.__cg) return;
const cg = window.__cg = { anchor: [Date.now(), performance.now()], energy: [], muted: [], phase: [], heard: [], chat: [], net: [], marks: {}, rec: { started: 0, bytes: 0 }, errs: [] };
const pnow = () => Math.round(performance.now());
// 1) fetch wrapper — exact page-clock network marks
const of = window.fetch.bind(window);
window.fetch = async (...a) => {
const url = typeof a[0] === 'string' ? a[0] : a[0]?.url || '';
const t0 = pnow();
try {
const r = await of(...a);
if (url.includes('/api/')) cg.net.push({ t: t0, url: url.slice(0, 80), method: (a[1] && a[1].method) || 'GET', status: r.status, tHeaders: pnow() });
return r;
} catch (e) { if (url.includes('/api/')) cg.net.push({ t: t0, url: url.slice(0, 80), method: (a[1] && a[1].method) || 'GET', status: 'err:' + String(e).slice(0, 40) }); throw e; }
};
// 2) energy + muted probe on her audio holder (50ms), starts whenever elements appear
let ctx = null, analysers = new Map(), lastMuted = null;
const ensureCtx = () => { if (!ctx) { try { ctx = new AudioContext(); } catch (e) { cg.errs.push('ctx:' + e); } } if (ctx && ctx.state === 'suspended') ctx.resume().catch(() => {}); return ctx; };
const hook = el => {
if (!el || !el.srcObject || analysers.has(el)) return;
const c = ensureCtx(); if (!c) return;
try {
const an = c.createAnalyser(); an.fftSize = 512;
c.createMediaStreamSource(el.srcObject).connect(an);
analysers.set(el, an);
if (!cg.rec.started) {
try {
const mr = new MediaRecorder(el.srcObject, { mimeType: 'audio/webm;codecs=opus' });
const chunks = []; mr.ondataavailable = ev => { chunks.push(ev.data); cg.rec.bytes += ev.data.size; };
mr.onstop = () => { cg.rec.chunks = chunks; };
mr.start(1000); cg.rec.started = pnow(); cg.rec.mr = mr;
} catch (e) { cg.errs.push('rec:' + e); }
}
} catch (e) { cg.errs.push('hook:' + e); }
};
setInterval(() => {
ensureCtx();
document.querySelectorAll("div[data-voice='her'] audio, div[data-voice='her'] video").forEach(hook);
let rms = 0;
const buf = new Uint8Array(512);
for (const an of analysers.values()) {
an.getByteTimeDomainData(buf);
let acc = 0; for (const v of buf) { const d = (v - 128) / 128; acc += d * d; }
rms = Math.max(rms, Math.sqrt(acc / buf.length));
}
const els = [...document.querySelectorAll("div[data-voice='her'] audio, div[data-voice='her'] video")];
const muted = els.length ? els.every(el => el.muted) : null;
if (muted !== lastMuted) { cg.muted.push([pnow(), muted]); lastMuted = muted; }
cg.energy.push([pnow(), Math.round(rms * 100000) / 100000, els.length]);
}, 50);
// 3) voice phase + heard text (100ms poll, log changes)
let lastPhase = null, lastHeard = null, lastSaid = null;
setInterval(() => {
const row = document.querySelector('.voice-row.real');
const phase = row ? row.dataset.phase : null;
const heard = document.querySelector('.voice-heard:not(.said)')?.textContent || '';
const said = document.querySelector('.voice-heard.said')?.textContent || '';
if (phase !== lastPhase) { cg.phase.push([pnow(), phase]); lastPhase = phase; }
if (heard !== lastHeard) { cg.heard.push([pnow(), 'draft', heard.slice(0, 60)]); lastHeard = heard; }
if (said !== lastSaid) { cg.heard.push([pnow(), 'final', said.slice(0, 60)]); lastSaid = said; }
}, 100);
// 4) chat DOM rows (conversation + record views)
const seen = new Set();
const scan = () => {
document.querySelectorAll('.message').forEach(m => {
const key = m.dataset.mid || (m.dataset.role + ':' + m.textContent.slice(0, 40));
if (seen.has(key)) return; seen.add(key);
cg.chat.push({ t: pnow(), role: m.dataset.role, mid: m.dataset.mid, text: (m.textContent || '').slice(0, 90), incomplete: !!m.querySelector('.incomplete') });
});
};
setInterval(scan, 120);
// 5) marks helper
window.__mark = (k) => { cg.marks[k] = pnow(); };
cg.mark = window.__mark;
})()`;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const browser = spawn(chrome, [
'--headless=new', `--remote-debugging-port=${cdpPort}`, `--user-data-dir=${join(tmpdir(), `hel910-call1-${Date.now()}`)}`,
'--no-first-run', '--hide-scrollbars', '--force-color-profile=srgb',
'--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream',
`--use-file-for-fake-audio-capture=${fakeWav}`,
'--autoplay-policy=no-user-gesture-required', 'about:blank',
], { stdio: 'ignore', env: { ...process.env, LD_LIBRARY_PATH: `${process.env.HOME}/chromium-libs/root/usr/lib/x86_64-linux-gnu:${process.env.HOME}/chromium-libs/root/usr/lib` } });
let targets;
for (let i = 0; i < 60 && !targets; i++) { try { targets = await (await fetch(`http://127.0.0.1:${cdpPort}/json`)).json(); } catch { await sleep(250); } }
if (!targets) { console.error('chromium did not start'); process.exit(1); }
const ws = new WebSocket(targets.find(t => t.type === 'page').webSocketDebuggerUrl);
await new Promise(r => ws.addEventListener('open', r, { once: true }));
let id = 0; const waiting = new Map(); const events = [];
ws.addEventListener('message', ({ data }) => {
const m = JSON.parse(data);
if (m.id && waiting.has(m.id)) { const { ok, ko } = waiting.get(m.id); waiting.delete(m.id); m.error ? ko(new Error(m.error.message)) : ok(m.result); }
else if (m.method) events.push(m);
});
const cdp = (method, params = {}) => new Promise((ok, ko) => { const n = ++id; waiting.set(n, { ok, ko }); ws.send(JSON.stringify({ id: n, method, params })); });
const js = async expr => { const r = await cdp('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description?.slice(0, 400) || 'js error'); return r.result.value; };
const shot = async name => { const { data } = await cdp('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false }); writeFileSync(join(out, `${name}.png`), Buffer.from(data, 'base64')); };
async function typeThisIsUnsafe() {
for (const ch of 'thisisunsafe') {
await cdp('Input.dispatchKeyEvent', { type: 'keyDown', key: ch, text: ch, windowsVirtualKeyCode: ch.charCodeAt(0), unmodifiedText: ch });
await cdp('Input.dispatchKeyEvent', { type: 'keyUp', key: ch, windowsVirtualKeyCode: ch.charCodeAt(0) });
await sleep(30);
}
}
async function goto(url) {
await cdp('Page.navigate', { url });
let privacySeen = 0;
for (let i = 0; i < 150; i++) {
await sleep(200);
const st = await js('document.readyState + "|" + (document.title || "") + "|" + (document.body ? document.body.textContent.slice(0, 40) : "")').catch(() => '');
if (!st.includes('Privacy') && !st.includes('not private') && st.startsWith('complete|')) return st;
// certificate interstitial: proceed through it exactly the way the interstitial itself accepts (its own key sequence)
if (st.includes('Privacy') || st.includes('not private')) {
privacySeen++;
await typeThisIsUnsafe();
}
}
return js('document.readyState + "|" + (document.title || "")').catch(() => 'unknown');
}
const click = async (selector, text = '') => js(`(() => { const el = [...document.querySelectorAll(${JSON.stringify(selector)})].find(e => e.offsetParent !== null && e.textContent.includes(${JSON.stringify(text)})); if (!el) return false; el.scrollIntoView({ block: 'center' }); el.click(); return true; })()`);
const until = async (fn, ms, every = 200) => { const end = Date.now() + ms; while (Date.now() < end) { const v = await fn(); if (v) return v; await sleep(every); } return null; };
const snap = () => js('JSON.stringify({e: window.__cg.energy.length ? window.__cg.energy.slice(-40) : [], ph: window.__cg.phase.slice(-6), hd: window.__cg.heard.slice(-6), mu: window.__cg.muted.slice(-6), ch: window.__cg.chat.slice(-8), mk: window.__cg.marks, rec: window.__cg.rec, err: window.__cg.errs, anchor: window.__cg.anchor})').then(JSON.parse);
const phaseNow = () => js(`document.querySelector('.voice-row.real')?.dataset.phase || null`);
const energyRecent = (n = 8) => js(`window.__cg.energy.slice(-${n})`).then(a => a || []);
async function sendText(label, text) {
await js(`(() => { window.__mark(${JSON.stringify(label + '_pre')}); const el = document.querySelector('.composer textarea'); if (!el) return false; el.focus(); const proto = HTMLTextAreaElement.prototype; Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, ${JSON.stringify(text)}); el.dispatchEvent(new Event('input', { bubbles: true })); return true; })()`);
await sleep(120);
await cdp('Input.dispatchKeyEvent', { type: 'rawKeyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
await cdp('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
await sleep(80);
await js(`window.__mark(${JSON.stringify(label + '_sent')})`);
}
const started = Date.now();
const R = { log: [], notes: [] };
const note = s => { R.log.push([Date.now() - started, s]); console.log(`[${((Date.now() - started) / 1000).toFixed(1)}] ${s}`); };
await cdp('Page.addScriptToEvaluateOnNewDocument', { source: INIT });
await cdp('Page.enable'); await cdp('Network.enable');
// 0) user-posture cert acceptance on both origins
note('accept cert 8361: ' + (await goto(base)).slice(0, 40));
note('accept cert 8363: ' + (await goto(wssOrigin)).slice(0, 40));
await goto(base);
await sleep(2500);
await shot('01-page-loaded');
note('page ready; companion=' + String(await js(`fetch('/api/companion').then(r => r.ok).catch(() => false)`)));
// enter the conversation view from the home window
const entered = await click('button', '和她说话');
note('clicked talk entry: ' + !!entered);
const composer = await until(async () => js(`!!document.querySelector('.composer textarea')`), 10000, 300);
if (!composer) {
note('FAIL: composer textarea not found — page state: ' + String(await js(`document.body.textContent.slice(0, 200)`)));
await finish('fail-nocomposer');
}
if (!(await js(`!!document.querySelector('.voice-button')`))) {
note('FAIL: voice button not found — page state: ' + String(await js(`document.body.textContent.slice(0, 200)`)));
await finish('fail-novoicebtn');
}
// 1) start the voice call from the real UI
if (!(await click('.voice-button'))) { note('FAIL: voice button not found'); await finish('fail-novoicebtn'); }
await sleep(600);
if (!(await click('.modal button', '开始语音'))) { note('FAIL: voice modal confirm not found'); await finish('fail-nomodal'); }
await js(`window.__mark('voice_start_click')`);
note('voice start clicked');
await until(async () => (await phaseNow()) !== null, 20000);
await until(async () => ['listening', 'thinking'].includes(await phaseNow()), 25000);
await shot('02-voice-listening');
note('voice row phase=' + (await phaseNow()));
// U1 @0-7.1s: she hears haimianbaobao → thinking → speaking (audible R1)
await until(async () => (await js(`window.__cg.heard.some(h => h[1] === 'final')`)), 30000);
note('U1 heard final');
const th1 = await until(async () => (await phaseNow()) === 'thinking', 40000);
note('phase thinking (R1 generating)');
// timing A rehearsal happens on U2; on U1 we wait for audible playback (timing B target)
const spk1 = await until(async () => {
const ph = await phaseNow(); if (ph !== 'speaking') return false;
const e = await energyRecent(12); return e.filter(x => x[1] > 0.02).length >= 8;
}, 60000);
note('R1 audible speaking: ' + !!spk1);
await sleep(1600); // let plenty of old audio remain
await shot('03-r1-speaking');
// T1 — timing B: text while old answer audibly playing (real composer + real Enter)
await sendText('T1', T1_TEXT);
note('T1 sent during playback');
await sleep(1200);
await shot('04-after-T1');
// watch old-voice stop + T1 text reply
await until(async () => { const e = await energyRecent(16); return e.length && e.filter(x => x[1] > 0.015).length === 0; }, 15000).then(v => note('stream quiet after T1: ' + !!v));
await until(async () => (await js(`window.__cg.chat.some(c => c.role === 'companion' && c.t > (window.__cg.marks.T1_sent || 0))`)), 90000, 300).then(v => note('T1 companion reply visible: ' + !!v));
await shot('05-T1-reply');
// U2 @60-73.3s: prefill T2, fire Enter the moment thinking starts for the bajie answer
await until(async () => (await js(`window.__cg.heard.some(h => h[1] === 'final' && h[2].includes('取经'))`)), 90000).then(v => note('U2 heard final (bajie): ' + !!v));
await js(`(() => { const el = document.querySelector('.composer textarea'); if (el) { el.focus(); const proto = HTMLTextAreaElement.prototype; Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, ${JSON.stringify(T2_TEXT)}); el.dispatchEvent(new Event('input', { bubbles: true })); } })()`);
const t2fired = await until(async () => (await phaseNow()) === 'thinking', 30000, 80);
if (t2fired) {
await js(`window.__mark('T2_thinking_seen')`);
await cdp('Input.dispatchKeyEvent', { type: 'rawKeyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
await cdp('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
await js(`window.__mark('T2_sent')`);
note('T2 sent during thinking (generation in flight)');
} else {
note('T2 NOT fired: thinking phase not observed in time');
}
await sleep(1500);
await shot('06-after-T2');
// R2 must never become audible: no energy while T2 reply streams as text
const quiet = await until(async () => { const e = await energyRecent(10); return e.length && e.every(x => x[1] <= 0.015); }, 20000);
note('no R2 audio after T2: ' + !!quiet);
await until(async () => (await js(`window.__cg.chat.filter(c => c.role === 'companion' && c.t > (window.__cg.marks.T2_sent || 0)).length >= 1`)), 90000, 300).then(v => note('T2 companion reply visible: ' + !!v));
await shot('07-T2-reply');
// U3 @120-127.1s: voice recovery — she must speak normally again
await until(async () => (await js(`window.__cg.heard.filter(h => h[1] === 'final').length >= 3`)), 90000).then(v => note('U3 heard final: ' + !!v));
const spk3 = await until(async () => {
const ph = await phaseNow(); if (ph !== 'speaking') return false;
const e = await energyRecent(12); return e.filter(x => x[1] > 0.02).length >= 8;
}, 90000);
note('R3 voice recovery audible: ' + !!spk3);
await shot('08-R3-speaking');
await until(async () => ['listening', 'thinking'].includes(await phaseNow()), 120000, 300).then(async v => note('R3 finished, back to ' + (await phaseNow())));
await shot('09-R3-done');
// end the call from the real UI
if (await click('.voice-end')) { await js(`window.__mark('voice_end_click')`); note('voice ended via UI'); }
await sleep(3000);
await finish('done');
async function finish(status) {
R.status = status;
try {
await js(`(() => { try { window.__cg.rec.mr && window.__cg.rec.mr.state !== 'inactive' && window.__cg.rec.mr.stop(); } catch (e) {} })()`);
await sleep(800);
const full = await js(`(async () => { const cg = window.__cg; let rec = null; try { if (cg.rec.chunks && cg.rec.chunks.length) { const blob = new Blob(cg.rec.chunks, { type: 'audio/webm' }); const buf = await blob.arrayBuffer(); const bytes = new Uint8Array(buf); let bin = ''; for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768)); rec = { bytes: blob.size, b64: bin }; } } catch (e) {} return { anchor: cg.anchor, energy: cg.energy, muted: cg.muted, phase: cg.phase, heard: cg.heard, chat: cg.chat, net: cg.net, marks: cg.marks, recMeta: { started: cg.rec.started, bytes: cg.rec.bytes }, errs: cg.errs, recB64: rec && rec.b64 }; })()`);
writeFileSync(join(out, 'call1-probe.json'), JSON.stringify({ ...R, probe: { ...full, recB64: undefined } }));
if (full.recB64) writeFileSync(join(out, 'call1-received.webm'), Buffer.from(full.recB64, 'base64'));
note('probe saved: energy=' + full.energy.length + ' chat=' + full.chat.length + ' recBytes=' + (full.recMeta.bytes || 0));
} catch (e) { note('probe save FAILED: ' + e); }
browser.kill();
process.exit(0);
}
+475
View File
@@ -0,0 +1,475 @@
#!/usr/bin/env python3
"""HEL-910 real-window voice test client (runs on .12, agent venv).
继承 HEL-907 hel907_client 的真链框架(预录音频回放经真实 WebRTC ->
LiveKit -> FunASR -> 统一 /api/chat(LLM) -> CosyVoice -> WebRTC 回流),
为本卡改造:dry(建房即删的挂断核验)、call2(稍长问题的首文/首声时延 +
回放打断停声同钟量测 + 思考泄漏取证)。所有时间戳同一客户端时钟(毫秒)。
文字打断的「真实网页原发送入口」场景在本机浏览器驱动脚本里,不在本文件。
Usage: hel910_client.py <phase> <outdir>
phases: dry | call2 | status
"""
from __future__ import annotations
import asyncio
import json
import sys
import time
import wave
from pathlib import Path
import httpx
from livekit import api, rtc
API = "http://127.0.0.1:8360"
LK_INTERNAL = "http://127.0.0.1:7880"
KEY = "devkey"
SECRET = "voiceagent-local-secret-2026"
SAMPLES = Path("/opt/voice-agent/cosyvoice/voice_samples")
TRIM = Path("/opt/cybergril-hel910/etc/trim")
DB = "/opt/cybergril-hel910/data/companion.db"
OUT = Path(sys.argv[2] if len(sys.argv) > 2 else "/opt/cybergril-hel910/evidence")
OUT.mkdir(parents=True, exist_ok=True)
T0 = time.time()
def log(msg: str) -> None:
print(f"[{time.time()-T0:8.2f}] {msg}", flush=True)
def now_ms() -> int:
return int(time.time() * 1000)
def load_wav_16k_mono(p: Path) -> bytes:
with wave.open(str(p), "rb") as w:
raw = w.readframes(w.getnframes())
rate, ch, width = w.getframerate(), w.getnchannels(), w.getsampwidth()
assert width == 2 and ch == 1 and rate == 16000, f"{p}: {rate}Hz ch{ch} w{width}"
return raw
async def list_rooms() -> list[str]:
lk = api.LiveKitAPI(url=LK_INTERNAL, api_key=KEY, api_secret=SECRET)
try:
res = await lk.room.list_rooms(api.ListRoomsRequest())
return [r.name for r in res.rooms]
finally:
await lk.aclose()
def safe_json(r: httpx.Response):
try:
return r.json()
except Exception:
return r.text[:200]
class Call:
"""One voice call through the real product API; full frame timeline kept."""
def __init__(self, tag: str):
self.tag = tag
self.http = httpx.AsyncClient(timeout=15)
self.room: rtc.Room | None = None
self.session: dict = {}
self.source: rtc.AudioSource | None = None
self.track: rtc.LocalAudioTrack | None = None
self.subtitles: list[dict] = []
self.frames: list[tuple[int, int]] = [] # (arrival_ms, bytes)
self.agent_audio = bytearray()
self.audio_rate = 48000
self.agent_joined_ms: int | None = None
self._speaking = False
self._keeper: asyncio.Task | None = None
self._first_pub_ms: int | None = None
async def open(self) -> dict:
r = await self.http.post(f"{API}/api/voice/session")
r.raise_for_status()
self.session = r.json()
log(f"[{self.tag}] session room={self.session['room']} voice={self.session.get('voice')} epoch={self.session['epoch']}")
return self.session
async def connect(self) -> None:
url = self.session["url"]
self.room = rtc.Room()
self.room.on("data_received", self._on_data)
self.room.on("track_subscribed", self._on_track)
self.room.on("participant_connected", self._on_participant)
await self.room.connect(url, self.session["token"])
self.source = rtc.AudioSource(16000, 1)
self.track = rtc.LocalAudioTrack.create_audio_track("mic", self.source)
opt = rtc.TrackPublishOptions()
opt.source = rtc.TrackSource.SOURCE_MICROPHONE
await self.room.local_participant.publish_track(self.track, opt)
log(f"[{self.tag}] connected via {url} mic published")
self._keeper = asyncio.create_task(self._keep_silent())
async def _keep_silent(self) -> None:
silence = b"\x00" * 3200
while self.source is not None:
if not self._speaking:
try:
await self.source.capture_frame(
rtc.AudioFrame(silence, 16000, 1, 1600))
except Exception:
return
await asyncio.sleep(0.1)
def _on_data(self, msg: rtc.DataPacket) -> None:
try:
payload = json.loads(msg.data.decode("utf-8"))
except Exception:
return
payload["_at_ms"] = now_ms()
self.subtitles.append(payload)
text = str(payload.get("text", ""))[:40]
log(f"[{self.tag}][sub] seq={payload.get('seq')} kind={payload.get('kind')} "
f"role={payload.get('role')} {text}")
def _on_participant(self, p: rtc.RemoteParticipant) -> None:
log(f"[{self.tag}] joined: {p.identity}")
if "agent" in p.identity:
self.agent_joined_ms = now_ms()
def _on_track(self, track: rtc.Track, pub, participant) -> None:
if track.kind != rtc.TrackKind.KIND_AUDIO:
return
log(f"[{self.tag}] agent audio track: {track.sid}")
async def read():
stream = rtc.AudioStream(track)
async for ev in stream:
frame = ev.frame
self.audio_rate = frame.sample_rate
self.frames.append((now_ms(), len(bytes(frame.data))))
self.agent_audio.extend(bytes(frame.data))
asyncio.create_task(read())
async def _publish_pcm(self, pcm: bytes, label: str) -> dict:
self._speaking = True
started = now_ms()
self._first_pub_ms = self._first_pub_ms or started
step = 3200
try:
for i in range(0, len(pcm), step):
chunk = pcm[i:i + step]
if len(chunk) < step:
chunk = chunk + b"\x00" * (step - len(chunk))
await self.source.capture_frame(rtc.AudioFrame(chunk, 16000, 1, 1600))
await asyncio.sleep(0.1)
finally:
self._speaking = False
ended = now_ms()
log(f"[{self.tag}] played {label} {len(pcm)/32000:.1f}s ({started}->{ended})")
return {"label": label, "start_ms": started, "end_ms": ended}
async def speak(self, pcm: bytes, label: str) -> dict:
return await self._publish_pcm(pcm, label)
async def wait_kind(self, kinds: tuple[str, ...], timeout: float, after_idx: int = 0) -> tuple[int, dict] | None:
deadline = time.time() + timeout
while time.time() < deadline:
await asyncio.sleep(0.05)
for i, s in enumerate(self.subtitles[after_idx:], start=after_idx):
if s.get("kind") in kinds:
return i, s
return None
async def wait_reply(self, timeout: float = 60.0) -> dict:
start_len = len(self.subtitles)
deadline = time.time() + timeout
seen_speech: str | None = None
while time.time() < deadline:
await asyncio.sleep(0.2)
for s in self.subtitles[start_len:]:
if s.get("kind") == "turn_start":
seen_speech = s.get("speech_id")
if s.get("kind") in ("turn_end", "turn_cancel") and (
seen_speech is None or s.get("speech_id") == seen_speech):
return {"kind": s["kind"], "speech_id": s.get("speech_id"),
"reason": s.get("reason")}
return {"kind": "timeout", "speech_id": seen_speech}
async def speak_and_wait(self, pcm: bytes, label: str, timeout: float = 60.0) -> dict:
play = asyncio.create_task(self._publish_pcm(pcm, label))
reply = await self.wait_reply(timeout)
await play
return {"play_label": label, "reply": reply}
async def wait_frames(self, min_seconds: float = 2.0, timeout: float = 40.0) -> int | None:
"""Wait until agent audio frames have been arriving for min_seconds."""
deadline = time.time() + timeout
while time.time() < deadline:
await asyncio.sleep(0.1)
if len(self.frames) >= 2:
cutoff = self.frames[-1][0] - int(min_seconds * 1000)
earliest_in_window = next((t for t, _ in reversed(self.frames) if t < cutoff), None)
if earliest_in_window is not None:
return now_ms()
# fewer than min_seconds of frames recorded at all
if self.frames[-1][0] - self.frames[0][0] >= int(min_seconds * 1000) - 300:
return now_ms()
return None
async def barge_interrupt(self, pcm: bytes) -> dict:
"""她长回复实际有声时,用预录响亮短句回放打断(真实 WebRTC 发布)。
T0 = 打断音频首帧发布;T1 = 旧语音最后一帧到达(≥1s 帧间隙分界)。
声源为预录 WAV 回放(工程输入),非真实用户麦克风。
"""
base_subs = len(self.subtitles)
ready = await self.wait_frames(min_seconds=3.0, timeout=45.0)
if ready is None:
return {"outcome": "no-audible-old-voice"}
barge = asyncio.create_task(self.speak(pcm, "barge-xiaoyun"))
# 打断首帧真正发布时刻由 speak 返回;此处先记发起
cancel_ms = None
deadline = time.time() + 25
while time.time() < deadline:
await asyncio.sleep(0.05)
for s in self.subtitles[base_subs:]:
if s.get("kind") == "turn_cancel":
cancel_ms = s["_at_ms"]
break
if cancel_ms:
break
await asyncio.sleep(14.0) # 观察旧声停止 + 新回复起播
await barge
timeline = [t for (t, _) in self.frames]
last_old = None
for i, t in enumerate(timeline):
if i + 1 < len(timeline) and timeline[i + 1] - t >= 1000:
last_old = t
break
if last_old is None and timeline:
last_old = timeline[-1]
return {
"outcome": "interrupted" if cancel_ms else "no-cancel",
"old_audio_last_frame_ms": last_old,
"cancel_ms": cancel_ms,
"frames_total": len(self.frames),
"source": "pre-recorded wav playback over real WebRTC (not a human microphone)",
}
async def hangup(self) -> dict:
sid = self.session.get("session_id")
self.source = None
frames_at_hangup = len(self.frames)
t_delete = now_ms()
r = await self.http.delete(f"{API}/api/voice/session/{sid}")
body = {"delete_status": r.status_code, "delete_body": safe_json(r),
"delete_ms": t_delete}
await asyncio.sleep(0.5)
if self.room is not None:
await self.room.disconnect()
await asyncio.sleep(2.0)
body["agent_frames_after_delete"] = len(self.frames) - frames_at_hangup
lk = api.LiveKitAPI(url=LK_INTERNAL, api_key=KEY, api_secret=SECRET)
try:
res = await lk.room.list_rooms(api.ListRoomsRequest(names=[self.session["room"]]))
body["room_removed"] = len(res.rooms) == 0
finally:
await lk.aclose()
log(f"[{self.tag}] hangup: {body}")
return body
def save(self, name: str) -> dict:
sub_path = OUT / f"{name}.subtitles.jsonl"
with sub_path.open("w", encoding="utf-8") as f:
for s in self.subtitles:
f.write(json.dumps(s, ensure_ascii=False) + "\n")
tl_path = OUT / f"{name}.frames.json"
tl_path.write_text(json.dumps(
{"first_pub_ms": self._first_pub_ms,
"frames": self.frames}, ensure_ascii=False))
audio_path = None
if self.agent_audio:
audio_path = OUT / f"{name}.agent.wav"
with wave.open(str(audio_path), "wb") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(self.audio_rate)
w.writeframes(bytes(self.agent_audio))
seqs = [s.get("seq") for s in self.subtitles if isinstance(s.get("seq"), int)]
return {
"tag": self.tag,
"session": self.session,
"session_voice": self.session.get("voice"),
"subtitle_count": len(self.subtitles),
"seq_monotonic": all(b > a for a, b in zip(seqs, seqs[1:])),
"agent_frames": len(self.frames),
"agent_audio_seconds": round(len(self.agent_audio) / 2 / self.audio_rate, 1),
"agent_audio_rate": self.audio_rate,
"agent_joined_ms": self.agent_joined_ms,
"subtitles_file": str(sub_path),
"frames_file": str(tl_path),
"agent_audio_file": str(audio_path) if audio_path else None,
}
def _db_query(sql: str, params: tuple = ()) -> list[dict]:
import sqlite3
conn = sqlite3.connect(f"file:{DB}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
try:
return [dict(r) for r in conn.execute(sql, params)]
finally:
conn.close()
def db_dump() -> dict:
out: dict = {}
try:
out["chat_message"] = _db_query(
"SELECT id, ts, role, turn_id, status, substr(text,1,120) AS text FROM chat_message ORDER BY id")
out["budget_total"] = _db_query("SELECT key, value FROM budget_total")
out["token_usage"] = _db_query("SELECT day, scene, used FROM token_usage")
out["request_usage"] = _db_query("SELECT * FROM request_usage")
except Exception as e:
out["error"] = str(e)
return out
def budget_snapshot() -> dict:
out: dict = {"at_ms": now_ms()}
try:
out["token_usage"] = _db_query("SELECT day, scene, used FROM token_usage")
out["request_usage"] = _db_query("SELECT * FROM request_usage")
out["budget_total"] = _db_query("SELECT key, value FROM budget_total")
except Exception as e:
out["error"] = str(e)
return out
def asr_ledger_summary() -> dict:
p = Path("/opt/cybergril-hel910/asr-usage.jsonl")
if not p.exists():
return {"exists": False}
events = [json.loads(l) for l in p.read_text().splitlines() if l.strip()]
return {"exists": True, "events": len(events), "raw": events[-8:]}
def worker_state() -> dict:
p = Path("/opt/cybergril-hel910/worker/voice_state_cybergril.json")
if not p.exists():
return {"exists": False}
try:
return {"exists": True, "content": json.loads(p.read_text())}
except Exception as e:
return {"exists": True, "error": str(e)}
def save_json(name: str, payload: dict) -> None:
path = OUT / f"{name}.json"
path.write_text(json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8")
log(f"saved {path}")
async def phase_dry() -> None:
"""建房即删:核验建房/派发/删除链路与房间清空;不发音频、无 LLM/TTS。"""
rooms_before = await list_rooms()
call = Call("dry")
await call.open()
await asyncio.sleep(10) # 给 worker 派发入房
rooms_mid = await list_rooms()
hangup = await call.hangup()
await asyncio.sleep(2)
rooms_after = await list_rooms()
save_json("dry", {"rooms_before": rooms_before, "rooms_mid": rooms_mid,
"hangup": hangup, "rooms_after": rooms_after,
"budget_after": budget_snapshot(),
"asr_ledger_after": asr_ledger_summary()})
async def phase_call2() -> None:
"""第二通:稍长问题(bajie)首文/首声时延 + 回放打断(xiaoyun)停声同钟量测。"""
call = Call("call2")
await call.open()
sess_voice = call.session.get("voice")
await call.connect()
marks: dict[str, int | None] = {}
base = len(call.subtitles)
await call.speak(load_wav_16k_mono(TRIM / "u_bajie.trim.wav"), "q-bajie")
# 输入结束 = 回放发完
marks["playback_end_ms"] = now_ms()
# user_final
got = await call.wait_kind(("user_final",), timeout=25)
marks["user_final_ms"] = got[1]["_at_ms"] if got else None
# 她的回复:turn_start(buffer_turn 下 interim 不先于全文)
got = await call.wait_kind(("turn_start",), timeout=60, after_idx=base)
marks["turn_start_ms"] = got[1]["_at_ms"] if got else None
got = await call.wait_kind(("assistant_final", "turn_cancel", "error"), timeout=90, after_idx=base)
marks["assistant_final_ms"] = got[1]["_at_ms"] if got else None
marks["assistant_final_kind"] = got[1].get("kind") if got else None
reply_text = (got[1].get("text") if got else "") or ""
# 首帧音频(回放发完之后到达的第一帧她的音频)
after = [t for (t, _) in call.frames if t > marks["playback_end_ms"]]
marks["first_agent_frame_ms"] = after[0] if after else None
# 等她实际有声再打断(不等 turn_end:那时音频已播完)
barge = await call.barge_interrupt(load_wav_16k_mono(TRIM / "u_xiaoyun.trim.wav"))
# 打断后的新回复
new = await call.wait_kind(("user_final",), timeout=20)
barge["new_user_final_ms"] = new[1]["_at_ms"] if new else None
new_reply = await call.wait_kind(("turn_end", "turn_cancel"), timeout=90)
barge["new_reply_end"] = {"kind": new_reply[1].get("kind"), "at_ms": new_reply[1]["_at_ms"]} if new_reply else None
# 时机A:她仍在生成时发文字(turn_start=生成开始;文字 POST 取消服务端活动轮)。
# 口径如实:本步文字走 API 直发(python 客户端),不是网页输入框;网页入口的
# 两种时机已在第一通完成(T1 在播时、T2 在 final→待播间隙)。
base2 = len(call.subtitles)
await call.speak(load_wav_16k_mono(TRIM / "u_bajie.trim.wav"), "q2-bajie")
got = await call.wait_kind(("turn_start",), timeout=40, after_idx=base2)
marks["q2_turn_start_ms"] = got[1]["_at_ms"] if got else None
a_out: dict = {"fired": False}
if got:
h = (await call.http.get(f"{API}/api/health")).json()
a_text = "先别把这句长回答生成完,我打字打断你:只用一句话告诉我你现在待在哪个角落。"
t_post = now_ms()
r = await call.http.post(f"{API}/api/chat", json={
"text": a_text, "turn_id": str(__import__("uuid").uuid4()), "epoch": h["epoch"]},
timeout=90)
a_out = {"fired": True, "post_ms": t_post, "status": r.status_code,
"body_head": r.text[:200]}
marks["A_post_ms"] = t_post
# 预期:语音轮 turn_cancel(生成被取消,零泄漏零复活),文字轮获完整回复
got2 = await call.wait_kind(("turn_cancel",), timeout=30, after_idx=base2)
a_out["voice_turn_cancel_ms"] = got2[1]["_at_ms"] if got2 else None
a_out["voice_turn_cancel_reason"] = got2[1].get("reason") if got2 else None
# 她此后不得再为 Q2 发声(观察窗口)
quiet_from = now_ms()
await asyncio.sleep(8.0)
a_out["q2_frames_after_cancel"] = sum(1 for (t, _) in call.frames if t >= quiet_from)
timing_a = a_out
await asyncio.sleep(3.0)
hangup = await call.hangup()
save_json("call2", {"session_voice": sess_voice, "marks": marks,
"reply_text_head": reply_text[:200],
"barge": barge, "timing_a": timing_a,
"hangup": hangup, "call": call.save("call2"),
"budget_after": budget_snapshot(),
"worker_state_after": worker_state(),
"db_dump": db_dump(),
"asr_ledger_after": asr_ledger_summary()})
async def phase_status() -> None:
rooms = await list_rooms()
save_json("status", {"rooms": rooms, "budget": budget_snapshot(),
"worker_state": worker_state(),
"asr_ledger": asr_ledger_summary(), "db": db_dump()})
async def main() -> None:
phase = sys.argv[1]
fn = {"dry": phase_dry, "call2": phase_call2, "status": phase_status}[phase]
await fn()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,88 @@
# HEL-910 一次性真实测试窗口部署配置(2026-10-29,服务器本地文件,不入仓库)
# 授权:白栖知主卡 HEL-797 评论 01a18753-8c70-7181-9758-5bab4685ba9f「继续测」,
# 批准《HEL-797-交谈修复复测安排.md》。LLM ≤10 请求、≤40000 tokens;嵌入 0
# (CYBERGRIL_SEMANTIC=stub);生活/后台/语义 0。失败/取消/重试均计入(发送前预占)。
# 窗口结束封账,余额不结转。tiers/routing/asr/tts/vad/turn/embedding/avatar 与
# 候选 958c696 的 config/models.yaml 逐字一致(含 HEL-908 reasoning: buffer_turn)。
tiers:
high:
provider: openai_compat
model: halogen-qwen3.8-flash-next
base_url: http://192.168.200.11:8731/v1
api_key_env: null
timeout: 60.0
temperature: 0.85
reasoning: buffer_turn
standard:
provider: openai_compat
model: halogen-qwen3.8-flash-next
base_url: http://192.168.200.11:8731/v1
api_key_env: null
timeout: 40.0
temperature: 0.7
reasoning: buffer_turn
low:
provider: openai_compat
model: halogen-qwen3.8-flash-next
base_url: http://192.168.200.11:8731/v1
api_key_env: null
timeout: 20.0
temperature: 0.4
reasoning: buffer_turn
routing:
dialogue_main: high
dialogue_followup: standard
proactive_message: high
crisis: high
life_event: low
memory_gist: low
fact_extract: low
output_guard: low
pattern_detect: low
filler: none
tick: none
reach_out: none
avatar_drive: none
budget:
daily_tokens: 40000
warn_ratio: 0.7
hard_stop_ratio: 0.95
daily_requests:
dialogue: 10
life: 0
background: 0
semantic: 0
total_requests: 10
total_tokens: 40000
asr:
provider: scripted
model: qwen3-asr-1.7b
language: zh
paralinguistic:
provider: scripted
model: sensevoice-small
window_ms: 1500
enabled: true
tts:
provider: silence
model: fun-cosyvoice3-0.5b
voice_ref: null
sample_rate: 16000
vad:
provider: energy
model: silero-v6
sample_rate: 16000
turn:
provider: heuristic
silence_min_ms: 150
silence_hard_end_ms: 800
embedding:
provider: openai_compat
model: qwen3-embedding-0.6b
base_url: null
api_key_env: null
dim: 1024
avatar:
route: vrm
renderer: three.js
lipsync: envelope
@@ -0,0 +1,88 @@
# HEL-910 一次性真实测试窗口部署配置(2026-10-29,服务器本地文件,不入仓库)
# 授权:白栖知主卡 HEL-797 评论 01a18753-8c70-7181-9758-5bab4685ba9f「继续测」,
# 批准《HEL-797-交谈修复复测安排.md》。LLM ≤10 请求、≤40000 tokens;嵌入 0
# (CYBERGRIL_SEMANTIC=stub);生活/后台/语义 0。失败/取消/重试均计入(发送前预占)。
# 窗口结束封账,余额不结转。tiers/routing/asr/tts/vad/turn/embedding/avatar 与
# 候选 958c696 的 config/models.yaml 逐字一致(含 HEL-908 reasoning: buffer_turn)。
tiers:
high:
provider: openai_compat
model: halogen-qwen3.8-flash-next
base_url: http://192.168.200.11:8731/v1
api_key_env: null
timeout: 60.0
temperature: 0.85
reasoning: buffer_turn
standard:
provider: openai_compat
model: halogen-qwen3.8-flash-next
base_url: http://192.168.200.11:8731/v1
api_key_env: null
timeout: 40.0
temperature: 0.7
reasoning: buffer_turn
low:
provider: openai_compat
model: halogen-qwen3.8-flash-next
base_url: http://192.168.200.11:8731/v1
api_key_env: null
timeout: 20.0
temperature: 0.4
reasoning: buffer_turn
routing:
dialogue_main: high
dialogue_followup: standard
proactive_message: high
crisis: high
life_event: low
memory_gist: low
fact_extract: low
output_guard: low
pattern_detect: low
filler: none
tick: none
reach_out: none
avatar_drive: none
budget:
daily_tokens: 0
warn_ratio: 0.7
hard_stop_ratio: 0.95
daily_requests:
dialogue: 0
life: 0
background: 0
semantic: 0
total_requests: 0
total_tokens: 0
asr:
provider: scripted
model: qwen3-asr-1.7b
language: zh
paralinguistic:
provider: scripted
model: sensevoice-small
window_ms: 1500
enabled: true
tts:
provider: silence
model: fun-cosyvoice3-0.5b
voice_ref: null
sample_rate: 16000
vad:
provider: energy
model: silero-v6
sample_rate: 16000
turn:
provider: heuristic
silence_min_ms: 150
silence_hard_end_ms: 800
embedding:
provider: openai_compat
model: qwen3-embedding-0.6b
base_url: null
api_key_env: null
dim: 1024
avatar:
route: vrm
renderer: three.js
lipsync: envelope
@@ -0,0 +1,59 @@
// HEL-910 offline feasibility probe (workstation-local, zero network beyond loopback):
// does Chromium --use-file-for-fake-audio-capture feed our schedule into getUserMedia,
// and does the AudioContext analyser probe see it at 50ms resolution?
// Usage: node probe_fake_capture.mjs <chrome> <wav> <outdir>
import { spawn } from 'node:child_process';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
const [chrome, wav, out] = process.argv.slice(2);
mkdirSync(out, { recursive: true });
const cdpPort = 9711, httpPort = 9712;
const page = `<!doctype html><meta charset=utf-8><body><script>
window.__log = [];
(async () => {
try {
const s = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } });
const ctx = new AudioContext(); await ctx.resume();
const an = ctx.createAnalyser(); an.fftSize = 512;
ctx.createMediaStreamSource(s).connect(an);
const buf = new Uint8Array(an.fftSize);
const t0 = performance.now();
window.__timer = setInterval(() => {
an.getByteTimeDomainData(buf);
let acc = 0; for (const v of buf) { const d = (v - 128) / 128; acc += d * d; }
window.__log.push([Math.round(performance.now() - t0), Math.round(Math.sqrt(acc / buf.length) * 100000) / 100000]);
}, 50);
window.__ok = true;
} catch (e) { window.__ok = false; window.__err = String(e); }
})();
</script></body>`;
writeFileSync(join(out, 'probe.html'), page);
const server = spawn('python3', ['-m', 'http.server', String(httpPort), '--bind', '127.0.0.1', '--directory', out], { stdio: 'ignore' });
const profile = join(tmpdir(), `hel910-probe-${Date.now()}`);
const browser = spawn(chrome, [
'--headless=new', `--remote-debugging-port=${cdpPort}`, `--user-data-dir=${profile}`, '--no-first-run',
'--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream',
`--use-file-for-fake-audio-capture=${wav}`,
'--autoplay-policy=no-user-gesture-required', 'about:blank',
], { stdio: 'ignore', env: { ...process.env, LD_LIBRARY_PATH: `${process.env.HOME}/chromium-libs/root/usr/lib/x86_64-linux-gnu:${process.env.HOME}/chromium-libs/root/usr/lib` } });
const sleep = ms => new Promise(r => setTimeout(r, ms));
let targets;
for (let i = 0; i < 60 && !targets; i++) { try { targets = await (await fetch(`http://127.0.0.1:${cdpPort}/json`)).json(); } catch { await sleep(250); } }
const ws = new WebSocket(targets.find(t => t.type === 'page').webSocketDebuggerUrl);
await new Promise(r => ws.addEventListener('open', r, { once: true }));
let id = 0; const waiting = new Map();
ws.addEventListener('message', ({ data }) => { const m = JSON.parse(data); if (m.id && waiting.has(m.id)) { const { ok } = waiting.get(m.id); waiting.delete(m.id); ok(m.result); } });
const cdp = (method, params = {}) => new Promise(ok => { const n = ++id; waiting.set(n, { ok }); ws.send(JSON.stringify({ id: n, method, params })); });
await cdp('Page.navigate', { url: `http://127.0.0.1:${httpPort}/probe.html` });
await sleep(12000);
const res = await cdp('Runtime.evaluate', { expression: 'JSON.stringify({ok: window.__ok, err: window.__err, samples: window.__log.length, log: window.__log})', returnByValue: true });
writeFileSync(join(out, 'probe-result.json'), res.result.value);
const parsed = JSON.parse(res.result.value);
const speaking = parsed.log.filter(([, e]) => e > 0.02);
console.log('gum ok:', parsed.ok, parsed.err || '', '| samples:', parsed.samples);
console.log('speaking windows (>0.02 rms):', speaking.length);
console.log('first/last speaking ms:', speaking[0]?.[0], speaking[speaking.length - 1]?.[0]);
console.log('max rms:', Math.max(...parsed.log.map(([, e]) => e)));
browser.kill(); server.kill();
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# HEL-910:窗口前/后状态快照(只读)
# 用法: snapshot.sh pre|post
set -u
BASE=/opt/cybergril-hel910
OUT=$BASE/snapshots/$1-$(date -u +%H%M%S).txt
{
echo "== HEL-910 $1 snapshot $(date -u +%FT%TZ) =="
echo "-- ports (83xx/80xx/98xx/788x/10095) --"
ss -ltnp | grep -E ':(8[03][0-9]{2}|98[0-9]{2}|788[0-9]|10095) ' || echo none
echo "-- our pids --"
for f in api bridge worker tts9880; do
p=$BASE/logs/$f.pid
if [ -f "$p" ]; then P=$(cat "$p"); if kill -0 "$P" 2>/dev/null; then echo "$f pid $P alive: $(tr '\0' ' ' < /proc/$P/cmdline | cut -c1-120)"; else echo "$f pid $P dead"; fi; else echo "$f no pidfile"; fi
done
echo "-- livekit rooms --"
curl -s --max-time 5 -H 'Authorization: Bearer skip' http://127.0.0.1:7880/twirp/livekit.RoomService/ListRooms -X POST -H 'Content-Type: application/json' -d '{}' 2>/dev/null | head -c 300 || true
echo
echo "-- voices.json hash + builtin count --"
sha256sum /opt/voice-agent/cosyvoice/voices.json
python3 - <<'EOF'
import json
try:
d = json.load(open('/opt/voice-agent/cosyvoice/voices.json'))
ids = list(d.keys()) if isinstance(d, dict) else [x.get('id') for x in d]
print('entries:', len(ids), 'ids:', ids, 'cg- entries:', sum(1 for i in ids if str(i).startswith('cg-')))
except Exception as e:
print('voices.json read error:', e)
EOF
echo "-- gpu --"
nvidia-smi --query-gpu=memory.used,memory.total --format=csv,noheader
echo "-- 8300/8310/8320/8350 untouched check --"
ss -ltn | grep -E ':(8300|8310|8320|8350) ' || echo 'WARN: expected demo ports missing'
echo "-- tts.log size --"
stat -c '%s bytes %y' /opt/voice-agent/cosyvoice/tts.log 2>/dev/null || echo no-log
} > "$OUT" 2>&1
cat "$OUT"
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# HEL-910 测试窗口:共享 CosyVoice TTS 9880 临时恢复/停回(按快照;精确 PID,禁 fuser -k)
# 用法: bash tts_ctl.sh start|stop|status|snapshot
set -u
BASE=/opt/cybergril-hel910
PIDF=$BASE/logs/tts9880.pid
SNAP=$BASE/snapshots/tts9880-before.txt
CC=/opt/voice-agent/cosyvoice
case "${1:-}" in
status)
if [ -f "$PIDF" ]; then P=$(cat "$PIDF"); kill -0 "$P" 2>/dev/null && echo "running pid $P (ours)" || echo "pid $P not alive"; else echo "not started by us"; fi
curl -s --max-time 3 http://127.0.0.1:9880/health && echo " <- health" || echo "health not answering"
exit 0 ;;
snapshot)
mkdir -p "$BASE/snapshots"
{ echo "ts=$(date -u +%FT%TZ)"; if curl -s --max-time 3 http://127.0.0.1:9880/health >/dev/null 2>&1; then echo "was=up"; else echo "was=down"; fi; ss -ltnp 2>/dev/null | grep ':9880 ' || echo "port9880=free"; } > "$SNAP"
cat "$SNAP" ;;
stop)
if [ -f "$PIDF" ]; then
P=$(cat "$PIDF")
kill "$P" 2>/dev/null; sleep 5
kill -0 "$P" 2>/dev/null && kill -9 "$P" 2>/dev/null
rm -f "$PIDF"
echo "tts stopped (pid $P)"
else
echo "not ours to stop (no pid file)"
fi
curl -s --max-time 3 http://127.0.0.1:9880/health >/dev/null 2>&1 && echo "WARN: 9880 still answering" || echo "9880 down" ;;
esac
# start (fallthrough)
if curl -s --max-time 3 http://127.0.0.1:9880/health >/dev/null 2>&1; then
echo "9880 already up — NOT ours, not touching"; exit 0
fi
if ss -ltn 2>/dev/null | grep -q ':9880 '; then echo "port 9880 occupied by non-healthy process — abort"; exit 1; fi
cd "$CC"
nohup "$CC/venv/bin/python" server.py >> "$CC/tts.log" 2>&1 &
echo $! > "$PIDF"
echo "tts starting (pid $(cat $PIDF)) — model load takes up to ~120s"
for i in $(seq 1 60); do
sleep 2
if curl -s --max-time 3 http://127.0.0.1:9880/health 2>/dev/null | grep -q '"ok"[[:space:]]*:[[:space:]]*true'; then
echo "OK: tts healthy after ~$((i*2))s"; exit 0
fi
kill -0 "$(cat $PIDF)" 2>/dev/null || { echo "FAIL: tts process died"; tail -10 "$CC/tts.log"; exit 1; }
done
echo "FAIL: tts not healthy in 120s"; tail -10 "$CC/tts.log"; exit 1
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# HEL-910:用量账快查(LLM 请求/tokens、TTS 段数、ASR 账本)——触线即停的现场读数
set -u
BASE=/opt/cybergril-hel910
echo "== budget tables ($(date -u +%FT%TZ)) =="
python3 - <<'EOF'
import json, sqlite3, pathlib
db = '/opt/cybergril-hel910/data/companion.db'
try:
conn = sqlite3.connect(f'file:{db}?mode=ro', uri=True); conn.row_factory = sqlite3.Row
for sql, label in [
("SELECT key,value FROM budget_total", 'budget_total'),
("SELECT day,scene,used FROM token_usage ORDER BY day,scene", 'token_usage'),
("SELECT * FROM request_usage", 'request_usage'),
]:
try:
print(label, json.dumps([dict(r) for r in conn.execute(sql)], ensure_ascii=False))
except Exception as e:
print(label, 'ERR', e)
conn.close()
except Exception as e:
print('db ERR', e)
p = pathlib.Path('/opt/cybergril-hel910/asr-usage.jsonl')
if p.exists():
lines = [l for l in p.read_text().splitlines() if l.strip()]
print('asr ledger events:', len(lines))
else:
print('asr ledger: (none)')
EOF
echo "== tts.log window segments =="
T0MARK=$BASE/snapshots/tts-log-start-byte
if [ -f "$T0MARK" ]; then
START=$(cat "$T0MARK")
else
START=0
fi
tail -c +$((START + 1)) /opt/voice-agent/cosyvoice/tts.log 2>/dev/null | grep -c "first chunk" || true
echo "(syntheses reaching first chunk since window start; cap 20)"
+15
View File
@@ -0,0 +1,15 @@
# HEL-910 一次性真实测试窗口语音配置(2026-10-29,服务器本地,不入仓库)
# livekit_url 走独立 8363 TLS 桥(wss),livekit_internal 保持本机明文 7880。
# 与 HEL-907 布局一致,仅状态/会话文件指到 /opt/cybergril-hel910。
livekit_url: wss://192.168.200.12:8363
livekit_internal: http://127.0.0.1:7880
api_key: devkey
api_secret: voiceagent-local-secret-2026
agent_name: cybergril-voice-agent
tts_health_url: http://127.0.0.1:9880/health
panel_url: https://127.0.0.1:8090
tts_url: http://127.0.0.1:9880
registry_file: /opt/voice-agent/cosyvoice/voices.json
worker_state_file: /opt/cybergril-hel910/worker/voice_state_cybergril.json
token_ttl_seconds: 300
session_ttl_seconds: 900
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# HEL-910 窗口墙钟看门狗:到点强制收尾。用法: watchdog.sh <deadline_epoch_seconds>
set -u
BASE=/opt/cybergril-hel910
LOG=$BASE/logs/watchdog.log
DL="${1:?deadline epoch seconds required}"
echo "[$(date -u +%FT%TZ)] watchdog start, deadline $(date -u -d @$DL +%FT%TZ) (pid $$)" >> "$LOG"
while [ "$(date +%s)" -lt "$DL" ]; do sleep 10; done
echo "[$(date -u +%FT%TZ)] DEADLINE REACHED — force end" >> "$LOG"
bash $BASE/ctl/force_end.sh
echo "[$(date -u +%FT%TZ)] watchdog done" >> "$LOG"
+10
View File
@@ -0,0 +1,10 @@
# HEL-910 测试窗口 worker 配置(2026-10-29,600 权限,不入仓库)
CYBERGRIL_API_BASE=http://127.0.0.1:8360
CYBERGRIL_AGENT_NAME=cybergril-voice-agent
LIVEKIT_URL=ws://127.0.0.1:7880
LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=voiceagent-local-secret-2026
CYBERGRIL_WORKER_HEALTH_PORT=8082
CYBERGRIL_VOICE_STATE=/opt/cybergril-hel910/worker/voice_state_cybergril.json
# ASR 用量账本独立(HEL-907/879 目录只读)
CYBERGRIL_ASR_USAGE_LEDGER=/opt/cybergril-hel910/asr-usage.jsonl
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# HEL-910 测试窗口:专属语音 worker(只按 PID 句柄停止;ASR 布防在 etc/asr-arm.env)
set -u
BASE=/opt/cybergril-hel910
LOG=$BASE/logs/worker.log
PIDF=$BASE/logs/worker.pid
VENV=/opt/voice-agent/agent/venv
stop() {
if [ -f "$PIDF" ]; then
P=$(cat "$PIDF")
if [ "$P" != "$$" ] && kill -0 "$P" 2>/dev/null; then kill "$P"; sleep 3; fi
if [ "$P" != "$$" ] && kill -0 "$P" 2>/dev/null; then kill -9 "$P"; fi
rm -f "$PIDF"
echo "worker stopped (pid $P)"
else
echo "no pid file; nothing stopped"
fi
}
case "${1:-}" in
stop) stop; exit 0 ;;
status)
if [ -f "$PIDF" ]; then P=$(cat "$PIDF"); kill -0 "$P" 2>/dev/null && echo "running pid $P" || echo "pid $P not alive"; fi
curl -s --max-time 3 "http://127.0.0.1:8082/" && echo " <- health" || echo "health port not answering"
exit 0 ;;
esac
cd "$BASE/worker"
set -a
source "$BASE/worker/.env"
source "$BASE/etc/asr-arm.env"
set +a
PYTHONPATH="$BASE/current/src" nohup "$VENV/bin/python" -m cybergril.voice.worker start > "$LOG" 2>&1 &
echo $! > "$PIDF"
echo "worker starting (pid $(cat $PIDF)) ASR window=$(grep -o 'hel910-20261029-a' $BASE/etc/asr-arm.env | head -1)"
sleep 18
grep -m1 "registered worker" "$LOG" >/dev/null && echo "OK: worker registered" \
|| { echo "FAIL:"; tail -8 "$LOG"; exit 1; }
Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

+85
View File
@@ -0,0 +1,85 @@
# HEL-910 · 真实通话复测窗口记录:发文字停声与新回复(2026-10-29)
> 授权:白栖知主卡 HEL-797 评论 `01a18753-8c70-7181-9758-5bab4685ba9f`「继续测」,批准
> 《HEL-797-交谈修复复测安排.md》(附件 `01a18755-110e-71c8-b12f-10d234e28b6a`)。
> 候选:`agent/2/15e79a2458bb` @ `958c6962fb1f2a4462a6df794d893aee1fcb0667`(父 b8ec832;部署前
> 对本地 `git archive 958c696` 逐文件 sha256 核对服务器 `/opt/cybergril-hel910/current` 完全一致,
> 仅多一个 REVISION 标记;web/dist 含 cg.interrupt 构建产物)。需求基准 v1.12/60 项;本卡无需求变更。
> 窗口(UTC):**布防后首个真实出站 11:53:12.96(dry 房 ASR connect)→ 封账 12:16:46 ≈ 23.6 分**
> (≤45 分);LLM 触线 10/10 即停。
## 0. 结论速览
| 问题(白栖知的原始问题) | 结果 |
| --- | --- |
| 用户发文字时她能否停下来 | ✅ 真实网页输入框发送(真 Enter 键):她在播的语音停止(浏览器能量探测 ≤~1.4s 内持续静默;本地 hush 静音在发送瞬间);第二通未取得可闻旧声期间的回放打断样本(时机错过,如实) |
| 文字是否得到回复 | ✅ 两次网页文字(T1 在播时、T2 在她想完待播时)都得到完整回复并落库(行 sent),旧轮不复活、无幽灵行;回执纪律正确(被打断轮零回执) |
| 她多久开口 | ⚠️ 实测(第二通,完整链):输入结束→识别终稿 2.43s→开始生成 +4.10s→**实际第一声 +9.53s**→字幕全文/落库 +12.10s。首声距输入结束 ~9.5s(>5s 关注线,主因 VAD 2.4s+整轮缓冲生成 3.4s+TTS ~0.5s;单样本如实交回) |
| 是否读内部思考 | ✅ 本轮全部真实回复(18 条 companion 行、字幕、TTS 文本)干净无思考;**但真实回复未出现带 `</think>` 形状**,故真实端点未复现"带 closer 分离";离线证据在 HEL-909 |
| 生成中被文字取消(时机A) | ⛔ 未取得正面样本:call1 网页侧 U2 检测失误未触发;call2 的 API 直发文字与 worker 语音轮竞速反向(文字先到被语音轮正常取消,行 cancelled+幂等 chat.cancelled 帧);随后语音轮撞 10/10 预算硬闸诚实 φ 失败。预算封顶不补测 |
| 语音打断后声音恢复 | ✅ 两次文字打断后,后续语音轮正常出声并完整回执(U2 轮、U4 轮 receipt ack) |
| 全部上限 | ✅ 全部在上限内或恰好触线即停(LLM 10/10 触线、建房 4/4 触线;tokens 30398/40000、TTS 6/20、ASR 350.63/720s、通话 2/2、窗口 23.6/45min、付费 0) |
| 清理与恢复 | ✅ 8360/8361/8363/8082/9880 全停;LiveKit 房间空;voices.json 哈希前后一致、11 支内置零变化、cg- 零残留;8300/8310/8320/8350/7880/10095 未动;GPU 回 747MiB |
## 1. 布防(先核环境,cap=0 fail-closed 验证后才开窗)
- 快照:端口/进程/GPU/voices.json(哈希 `7b2a5021…`,11 内置,cg- 0)/TTS 9880 停机 → `window/pre-window.txt`。
- 隔离部署 `/opt/cybergril-hel910/`(新预算、新 window_id `hel910-20261029-a`、新隔离档案「小测」;HEL-907/879 目录只读复用):
- 统一实例 8360/8361 同进程双入口(`tools/api_ctl.sh`,`.venv311` 只读复用,`CYBERGRIL_SEMANTIC=stub`、`R2_ENABLED=1` 与 HEL-907 一致);
- WSS 桥 8363、专属 worker 8082(agent venv 1.8.8);`etc/models.yaml` 三档 `reasoning: buffer_turn` 与候选 `config/models.yaml` 逐字一致,仅预算块改为本窗批准值(dialogue 10 / total 10 / 40000 tokens,其余 0)。
- **cap=0 fail-closed 预检**(`models-failclosed.yaml`):建档案 → persona_job=failed(background=0,预占前拒绝);POST /api/chat → 诚实 `chat.error`(φ 原句),`request_usage`/`budget_total` 全空(零出站);POST /api/voice/session 在 TTS 停机时 503 且不建房。之后整库丢弃换 armed。
- 共享 TTS 9880 按快照临时恢复(health ok,14s);**面板 8090 本轮未启用**(零登记/零试听,不需要)。墙钟看门狗(44 分钟硬顶,精确 PID 句柄)常备;`force_end.sh` 覆盖异常路径。
## 2. 第一通:真实网页入口的文字打断(浏览器=用户,Chromium 156 headless)
- 用户侧全部走真实页面 `https://192.168.200.12:8361`:自签证书按用户姿态逐 origin 过Interstitial(8361+8363);麦克风=Chromium fake capture 回放既有工程样本时刻表(`tools/build_fake_wavs.py`:U1 海绵宝宝@0s、U2 八戒@60s、U3 海绵宝宝@120s,240s 循环);文字=真实输入框 `textarea` + CDP 真实 Enter 键(非直接 POST)。页面探针(同页时钟):流能量 50ms、`el.muted` 转换、voice 相位、heard 文本、chat DOM、fetch 网络。截图 `web/01..09`。
- **T1(时机B:旧回答已生成、TTS 实际在播)**:她在播 R1(能量>0.02 持续、相位 speaking)时输入框发送 →
发送瞬间本地 hush 静音(同步);**~1.4s 内浏览器收流能量持续归零**(远端旧音轨停止;0.2s 轮询+静默窗口径,粗粒度如实);T1 获完整文字回复(行4 sent);R1 不复活(行2 已完、无后续 R1 音频、receipt 正确缺席);U2 语音轮随后正常出声(行5/6 sent,回执 ack)→ **文字打断后语音恢复正常**。
- **T2(网页入口,页面相位=thinking 时发送)**:实际落在 U3 回复已落库、TTS 待播/刚起播的间隙(HEL-909 评审 C 形状)→ U3 回复行8 sent 且文本完整保留(音频被截,receipt 正确缺席);T2 获完整回复(行10「平静。」);U4 语音轮再次正常出声且回执 ack(行11/12)→ 恢复正常第二次确认。
- **call1 缺陷/失误(如实)**:① 驱动脚本对 U2 heard-final 的检测条件未命中(原因无法定证——探针原始数据随浏览器被超时杀掉丢失),时机A 网页正面样本因此未执行;② 页面语音会话结束后文件循环带来一次 U4 轮(+257s,回复在挂断后落库、未出声)多耗 1 次 LLM——测量装置的时序代价,已计入账本;③ 浏览器探针原始 JSON/webm 因驱动进程超时被杀未存盘(截图与里程碑日志保留;第二通用 python 客户端独立取证帧级时间轴)。
## 3. 第二通:python 客户端(同钟毫秒时间轴,`window/call2.*`)
- **稍长问题(八戒)等待链**(call2.json marks + frames/wav 离线对齐):回放发完 → user_final **+2.43s**(VAD)→ turn_start(生成开始)**+4.10s** → **实际第一声 +9.53s**(wav 能量段起播;生成 3.44s+TTS 首包)→ assistant_final(字幕全文)**+12.10s** → 播放完整、回执 ack(+12.10s 处)。回复本体较短(「好,小十。去化缘…」14 字)——「稍长回复」目标部分达成,等待数字仍有效(整轮缓冲的主导项是 VAD+生成,不随回复长度显著变化)。
- **回放打断(xiaoyun,响亮短句 rms 4970 预检)**:⚠️ **未取得有效样本**——她的 Q1 音频在打断回放开始前 ~1.1s 已自然播完(wav 能量段 979988–982187 vs 回放起播 ~983.3k),`turn_cancel(interrupted)` 是其后新轮被同段回放第二次 final 打断(非停旧声)。真实取到的:VAD 中断链路真实工作(cancel=final 后 0.6s)、被打断轮回执 fail-closed 正确。HEL-907 同项亦未取得,两轮均如实不冒充。
- **时机A(API 直发文字取消生成中的语音轮)**:竞速反向——文字 POST 先于 worker 语音 POST 到达(turn_start 在适配器开流时即发,早于服务端收到),文字轮被更新的语音轮正常取消(**行19 cancelled + SSE 幂等 `chat.cancelled` 终态帧**,即 HEL-908 R1 修复路径的真实表现);随后语音轮 LLM 预占撞 10/10 硬闸 → 诚实 `chat.error` φ 原句念出(行20 failed,无幽灵 companion 行)。**正面"生成中被取消"样本未取得,预算封顶不补测**。
- 挂断:DELETE 200、房间真删、删后仅 1 帧在途。
## 4. 思考隔离检查(真实端点)
18 条 companion 落库行、全部字幕、`tts-window.log` 合成文本:零 `<think>`/`</think>`/思考前缀。
**但本轮真实回复均未出现带 closer 形状**(该端点隐式思考无标签时任何客户端模式都不可分离——HEL-908 已声明边界):真实端点未复现"带 `</think>` 分离",覆盖范围如实限定;离线 32 组形状证据见 HEL-909 附件。
## 5. 用量账(含失败/取消;封账后不重开)
| 类别 | 实际 | 上限 |
| --- | --- | --- |
| 时窗 | 23.6 分(11:53:12.96→12:16:46) | ≤45 分 |
| 通话 | 2 通(浏览器 ~4.4 分 / python ~1.5 分,串行) | ≤2、各≤5 分 |
| 建房 | 4(dry+call1+call2+一次 SSL 证书报错未连成的空房,房间已核实删除、房间清空) | ≤4(触线) |
| LLM | **10 请求 / 30,398 tokens(触线即停)**:call1 6(含挂断后 U4 轮)+ call2 4 | ≤10 / ≤40000 |
| 嵌入/生活/后台/语义 | 0(stub;persona/background=0 fail-closed 预检证实) | 0 |
| 通话 TTS | 6 段(达首包口径) | ≤20 |
| 连续 ASR | 保守=确证 **350.63s**(逐会话 0/264.24/0/86.38),重试 0 | ≤720s、每通重连≤2 |
| 新登记/上传/登记转写/试听合成 | 全 0(未开 8090;voices.json 前后哈希一致) | 0 |
| 新增付费 | 0 | 0 |
## 6. 发现的问题(交回,不现场改核心代码)
- 🟡 **首声等待**:输入结束→第一声 ~9.5s(user_final 后 ~7.1s;生成开始后 ~3.4s)。单样本+call1 量级相近,未达"经常"判定所需的多样本;按《复测安排》记录交回,不现场调参。构成:VAD 2.4s + 整轮缓冲生成 3.4s(buffer_turn 已知代价,换来零思考泄漏)+ TTS ~0.5s。
- 🟡 worker.py `_on_item` 对 `AgentHandoff` 项取 `role` 抛 AttributeError(SDK 事件内捕获,非致命日志噪音,两轮通话均出现)。
- 🟡 job 关停路径 `TypeError: object NoneType can't be used in 'await' expression`(livekit-agents shutdown 回调;非致命、挂断清理本身正常)。
- 🟡 本窗测量脚本自身:`tts_ctl.sh` snapshot/stop 分支穿透导致一次多余的 TTS 重启(随即按快照停回,零合成零预算影响);call1 驱动超时杀进程丢失页内探针数据。均已在 `tools/` 原样留档。
- 已知窄窗口(延迟 cg.interrupt 打断新回复):本轮未观察到该事件,不宣称排除(无正面样本)。
## 7. 清理与恢复(逐项核对,`window/post-window.txt`)
8360/8361/8363/8082/9880 全停(精确 PID);LiveKit `ListRooms` 空(含那次证书空房,已核实删除;隔离实例会话登记文件留有一条 TTL 未到期记录,房间实际已不存在——API 进程先于 TTL 停机所致,留证不追删);voices.json 哈希前后一致、cg- 0;8300/8310/8320/8350/7880/10095 原样;GPU 747MiB;8340/ComfyUI 保持停机未碰。看门狗已停。证据包 `/opt/cybergril-hel910/hel910-evidence-20261029.tar.gz` sha256 `83363576b922ff45a7b85706801ece5b790b2d22bf14a4e1dbf1aed303d7a3f0`;测试档案/账本留在 `/opt/cybergril-hel910/`(未触碰任何既有实例与真实档案)。
## 8. 未测 / 承接(如实)
- **真实用户麦克风打断时延**(含可闻旧声期间的语音打断停声):两轮(HEL-907/910)均未取得有效样本,需真人在同一时间轴发声+听声,或下轮工程窗口先用"她实际有声才触发"的门控重试。
- **时机A(生成中文字取消)正面样本**:需文字在语音轮服务端激活后、生成完成前到达(如等 chat 行落库再发);本轮竞速反向+预算封顶,未覆盖。
- **真实端点带 `</think>` 形状**:未出现,真实分离未复现(离线证据在 HEL-909)。
- 首声等待"经常>5s"与否:需多样本;本轮 1 精确样本 + 1 量级相近。
- 真机/手机麦克风、Safari:未测(D3 用户动作)。
+148
View File
@@ -0,0 +1,148 @@
INFO: Started server process [116699]
INFO: Started server process [116699]
INFO: Uvicorn running on http://0.0.0.0:8360 (Press CTRL+C to quit)
INFO: Uvicorn running on https://0.0.0.0:8361 (Press CTRL+C to quit)
INFO: 127.0.0.1:53518 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:43322 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:53530 - "POST /api/companion HTTP/1.1" 201 Created
INFO: 127.0.0.1:46880 - "POST /api/voice/session HTTP/1.1" 201 Created
INFO: 127.0.0.1:33042 - "DELETE /api/voice/session/1RDW7vfLqnpCjIK4SO_tSNzk HTTP/1.1" 200 OK
INFO: 192.168.200.13:34830 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.200.13:34830 - "GET /assets/index-Vm-EFPLb.js HTTP/1.1" 200 OK
INFO: 192.168.200.13:34844 - "GET /assets/index-B8TdHoQm.css HTTP/1.1" 200 OK
INFO: 192.168.200.13:34830 - "GET /assets/her-talk.jpg HTTP/1.1" 200 OK
INFO: 192.168.200.13:34844 - "GET /assets/her-room.jpg HTTP/1.1" 200 OK
INFO: 192.168.200.13:34844 - "GET /api/health HTTP/1.1" 200 OK
INFO: 192.168.200.13:34830 - "GET /favicon.ico HTTP/1.1" 404 Not Found
INFO: 192.168.200.13:34830 - "GET /api/companion HTTP/1.1" 200 OK
INFO: 192.168.200.13:34830 - "GET /api/messages?limit=200 HTTP/1.1" 200 OK
INFO: 192.168.200.13:34830 - "GET /assets/icons.svg HTTP/1.1" 200 OK
INFO: 192.168.200.13:34830 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.200.13:34830 - "GET /api/health HTTP/1.1" 200 OK
INFO: 192.168.200.13:34830 - "GET /api/companion HTTP/1.1" 200 OK
INFO: 192.168.200.13:34830 - "GET /api/messages?limit=200 HTTP/1.1" 200 OK
INFO: 192.168.200.13:34830 - "GET /api/companion HTTP/1.1" 200 OK
INFO: 192.168.200.13:48642 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.200.13:48642 - "GET /assets/index-B8TdHoQm.css HTTP/1.1" 200 OK
INFO: 192.168.200.13:48656 - "GET /assets/index-Vm-EFPLb.js HTTP/1.1" 200 OK
INFO: 192.168.200.13:48642 - "GET /assets/her-talk.jpg HTTP/1.1" 200 OK
INFO: 192.168.200.13:48656 - "GET /assets/her-room.jpg HTTP/1.1" 200 OK
INFO: 192.168.200.13:48656 - "GET /api/health HTTP/1.1" 200 OK
INFO: 192.168.200.13:48642 - "GET /favicon.ico HTTP/1.1" 404 Not Found
INFO: 192.168.200.13:48642 - "GET /api/companion HTTP/1.1" 200 OK
INFO: 192.168.200.13:48642 - "GET /api/messages?limit=200 HTTP/1.1" 200 OK
INFO: 192.168.200.13:48642 - "GET /assets/icons.svg HTTP/1.1" 200 OK
INFO: 192.168.200.13:35342 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.200.13:35342 - "GET /api/health HTTP/1.1" 200 OK
INFO: 192.168.200.13:35342 - "GET /api/companion HTTP/1.1" 200 OK
INFO: 192.168.200.13:35342 - "GET /api/messages?limit=200 HTTP/1.1" 200 OK
INFO: 192.168.200.13:35342 - "GET /api/companion HTTP/1.1" 200 OK
INFO: 192.168.200.13:35342 - "GET /api/voice/status HTTP/1.1" 200 OK
INFO: 192.168.200.13:35342 - "GET /api/voice/status HTTP/1.1" 200 OK
INFO: 192.168.200.13:35342 - "GET /api/voice/status HTTP/1.1" 200 OK
INFO: 192.168.200.13:35342 - "GET /vendor/livekit-client-2.22.4.umd.min.js HTTP/1.1" 200 OK
INFO: 192.168.200.13:35342 - "POST /api/voice/session HTTP/1.1" 201 Created
INFO: 192.168.200.13:35342 - "GET /api/voice/voices HTTP/1.1" 503 Service Unavailable
INFO: 127.0.0.1:52114 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:52114 - "POST /api/chat HTTP/1.1" 200 OK
INFO: 192.168.200.13:54432 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:51758 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:51758 - "GET /api/health HTTP/1.1" 200 OK
INFO: 192.168.200.13:51758 - "POST /api/chat HTTP/1.1" 200 OK
INFO: 192.168.200.13:51758 - "POST /api/chat/received HTTP/1.1" 200 OK
memory compression failed: memory_id=2
INFO: 192.168.200.13:48068 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:56264 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=2
INFO: 192.168.200.13:52390 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:52390 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 127.0.0.1:34364 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:34364 - "POST /api/chat HTTP/1.1" 200 OK
INFO: 127.0.0.1:40776 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:40776 - "POST /api/chat/received HTTP/1.1" 200 OK
INFO: 192.168.200.13:34404 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=2
INFO: 192.168.200.13:34404 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:55626 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=1
INFO: 127.0.0.1:48340 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:48340 - "POST /api/chat HTTP/1.1" 200 OK
INFO: 192.168.200.13:58740 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:58740 - "GET /api/health HTTP/1.1" 200 OK
INFO: 192.168.200.13:58740 - "POST /api/chat HTTP/1.1" 200 OK
INFO: 192.168.200.13:58740 - "POST /api/chat/received HTTP/1.1" 200 OK
INFO: 192.168.200.13:58740 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=1
INFO: 192.168.200.13:37600 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:60942 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=1
INFO: 192.168.200.13:51596 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=4
INFO: 192.168.200.13:51382 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:47722 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=4
INFO: 127.0.0.1:58234 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:58234 - "POST /api/chat HTTP/1.1" 200 OK
INFO: 192.168.200.13:57716 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:35614 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 127.0.0.1:37294 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:37294 - "POST /api/chat/received HTTP/1.1" 200 OK
INFO: 192.168.200.13:35644 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:35644 - "DELETE /api/voice/session/EkSPC7iZhHiqZmoCoZanDIPp HTTP/1.1" 200 OK
INFO: 192.168.200.13:35664 - "GET /assets/icons.svg HTTP/1.1" 304 Not Modified
INFO: 192.168.200.13:35664 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=4
INFO: 192.168.200.13:33818 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:49544 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=3
INFO: 192.168.200.13:40860 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=3
INFO: 192.168.200.13:52124 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
INFO: 192.168.200.13:35732 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=3
INFO: 192.168.200.13:46040 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=6
INFO: 192.168.200.13:50306 - "GET /api/messages?limit=50 HTTP/1.1" 200 OK
memory compression failed: memory_id=6
memory compression failed: memory_id=6
memory compression failed: memory_id=5
memory compression failed: memory_id=5
memory compression failed: memory_id=5
memory compression failed: memory_id=8
memory compression failed: memory_id=8
memory compression failed: memory_id=8
memory compression failed: memory_id=7
memory compression failed: memory_id=7
INFO: 127.0.0.1:50124 - "POST /api/voice/session HTTP/1.1" 201 Created
memory compression failed: memory_id=7
memory compression failed: memory_id=10
memory compression failed: memory_id=10
memory compression failed: memory_id=10
memory compression failed: memory_id=9
INFO: 127.0.0.1:39284 - "POST /api/voice/session HTTP/1.1" 201 Created
memory compression failed: memory_id=9
INFO: 127.0.0.1:40248 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:40248 - "POST /api/chat HTTP/1.1" 200 OK
INFO: 127.0.0.1:40248 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:40248 - "POST /api/chat/received HTTP/1.1" 200 OK
INFO: 127.0.0.1:40248 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:40248 - "POST /api/chat HTTP/1.1" 200 OK
INFO: 127.0.0.1:40248 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:40248 - "POST /api/chat HTTP/1.1" 200 OK
INFO: 127.0.0.1:40248 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:40248 - "POST /api/chat/received HTTP/1.1" 200 OK
memory compression failed: memory_id=9
INFO: 127.0.0.1:53290 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:53294 - "GET /api/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:53294 - "POST /api/chat HTTP/1.1" 200 OK
INFO: 127.0.0.1:53290 - "POST /api/chat HTTP/1.1" 200 OK
memory compression failed: memory_id=12
INFO: 127.0.0.1:60666 - "DELETE /api/voice/session/Br4leZ_1VqtwH_E4TIM9n8o5 HTTP/1.1" 200 OK
memory compression failed: memory_id=12
memory compression failed: memory_id=12
memory compression failed: memory_id=11
memory compression failed: memory_id=11
INFO: Shutting down
INFO: Finished server process [116699]
INFO: Shutting down
INFO: Finished server process [116699]
+81
View File
@@ -0,0 +1,81 @@
{"kind":"call_start","call":"cg-voice-9r5jjmcz0n","stream_seq":1,"window":"hel910-20261029-a","t":1793274792.96}
{"kind":"call_end","call":"cg-voice-9r5jjmcz0n","reason":"error","audio_seconds":0.0,"confirmed_audio_seconds":0.0,"connect_attempts":0,"connect_failures":0,"connect_retries":0,"window":"hel910-20261029-a","t":1793274804.372}
{"kind":"call_start","call":"cg-voice-fdk5izk2uz","stream_seq":1,"window":"hel910-20261029-a","t":1793275102.514}
{"kind":"connect_attempt","call":"cg-voice-fdk5izk2uz","seq":1,"ok":true,"retry":false,"error":"","window":"hel910-20261029-a","t":1793275102.631}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":4.990686,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275107.561}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056373,"confirmed_seconds":5.023499,"frames":101,"bytes":161804,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275112.611}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056311,"confirmed_seconds":5.056311,"frames":101,"bytes":161802,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275117.66}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056311,"confirmed_seconds":5.056373,"frames":101,"bytes":161802,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275122.711}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275127.761}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275132.761}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275137.812}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275142.811}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056373,"confirmed_seconds":5.056311,"frames":101,"bytes":161804,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275147.861}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275152.911}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275157.911}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275162.96}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275167.961}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056311,"confirmed_seconds":5.056311,"frames":101,"bytes":161802,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275173.011}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056373,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275178.061}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990624,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275183.061}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056373,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275188.111}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990624,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275193.111}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056311,"confirmed_seconds":5.056373,"frames":101,"bytes":161802,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275198.161}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275203.211}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275208.211}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275213.261}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275218.262}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056373,"confirmed_seconds":5.056311,"frames":101,"bytes":161804,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275223.312}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275228.36}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275233.362}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275238.411}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275243.411}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056311,"confirmed_seconds":5.056311,"frames":101,"bytes":161802,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275248.461}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056373,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275253.511}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990624,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275258.512}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056373,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275263.561}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990624,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275268.561}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056311,"confirmed_seconds":5.056373,"frames":101,"bytes":161802,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275273.611}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275278.661}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275283.661}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275288.711}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275293.711}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056373,"confirmed_seconds":5.056311,"frames":101,"bytes":161804,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275298.761}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275303.811}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275308.811}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275313.861}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275318.861}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275323.911}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275328.911}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056311,"confirmed_seconds":5.056311,"frames":101,"bytes":161802,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275333.961}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056373,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275339.011}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990624,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275344.011}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056373,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275349.062}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023498,"confirmed_seconds":4.990624,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275354.061}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.056311,"confirmed_seconds":5.056373,"frames":101,"bytes":161802,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275359.11}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275364.161}
{"kind":"audio","call":"cg-voice-fdk5izk2uz","seconds":2.626688,"confirmed_seconds":2.659501,"frames":13,"bytes":84054,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275366.316}
{"kind":"call_end","call":"cg-voice-fdk5izk2uz","reason":"error","audio_seconds":264.242609,"confirmed_audio_seconds":264.242609,"connect_attempts":1,"connect_failures":0,"connect_retries":0,"window":"hel910-20261029-a","t":1793275366.316}
{"kind":"call_start","call":"cg-voice-jq8q4zm9h0","stream_seq":1,"window":"hel910-20261029-a","t":1793275813.498}
{"kind":"call_start","call":"cg-voice-dlas9zmiox","stream_seq":1,"window":"hel910-20261029-a","t":1793275957.196}
{"kind":"connect_attempt","call":"cg-voice-dlas9zmiox","seq":1,"ok":true,"retry":false,"error":"","window":"hel910-20261029-a","t":1793275957.28}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023499,"confirmed_seconds":4.990686,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275962.248}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.056373,"confirmed_seconds":5.023499,"frames":101,"bytes":161804,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275967.298}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.056311,"confirmed_seconds":5.056311,"frames":101,"bytes":161802,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275972.348}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.056311,"confirmed_seconds":5.056373,"frames":101,"bytes":161802,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275977.398}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275982.448}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275987.449}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275992.499}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793275997.499}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.056373,"confirmed_seconds":5.056311,"frames":101,"bytes":161804,"sample_rate":16000,"window":"hel910-20261029-a","t":1793276002.548}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793276007.599}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793276012.598}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023499,"confirmed_seconds":5.056311,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793276017.648}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023498,"confirmed_seconds":4.990686,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793276022.651}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.056311,"confirmed_seconds":5.056311,"frames":101,"bytes":161802,"sample_rate":16000,"window":"hel910-20261029-a","t":1793276027.698}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023499,"confirmed_seconds":5.056373,"frames":101,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793276032.748}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023498,"confirmed_seconds":4.990624,"frames":100,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793276037.75}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":5.023499,"confirmed_seconds":4.892186,"frames":83,"bytes":160752,"sample_rate":16000,"window":"hel910-20261029-a","t":1793276041.653}
{"kind":"audio","call":"cg-voice-dlas9zmiox","seconds":0.820813,"confirmed_seconds":1.017813,"frames":4,"bytes":26266,"sample_rate":16000,"window":"hel910-20261029-a","t":1793276043.643}
{"kind":"call_end","call":"cg-voice-dlas9zmiox","reason":"error","audio_seconds":86.384475,"confirmed_audio_seconds":86.384475,"connect_attempts":1,"connect_failures":0,"connect_retries":0,"window":"hel910-20261029-a","t":1793276043.643}
{"kind":"call_end","call":"cg-voice-jq8q4zm9h0","reason":"error","audio_seconds":0.0,"confirmed_audio_seconds":0.0,"connect_attempts":0,"connect_failures":0,"connect_retries":0,"window":"hel910-20261029-a","t":1793276114.155}
+1
View File
@@ -0,0 +1 @@
wss-bridge(PROPOSAL) tls ('0.0.0.0', 8363) -> 127.0.0.1:7880
Binary file not shown.
File diff suppressed because one or more lines are too long
+384
View File
@@ -0,0 +1,384 @@
{
"session_voice": "rain",
"marks": {
"playback_end_ms": 1793275970454,
"user_final_ms": 1793275972886,
"turn_start_ms": 1793275976553,
"assistant_final_ms": 1793275982556,
"assistant_final_kind": "assistant_final",
"first_agent_frame_ms": 1793275970457,
"q2_turn_start_ms": 1793276022633,
"A_post_ms": 1793276022652
},
"reply_text_head": "好,小十。去化缘",
"barge": {
"outcome": "interrupted",
"old_audio_last_frame_ms": 1793276002937,
"cancel_ms": 1793275988927,
"frames_total": 4565,
"source": "pre-recorded wav playback over real WebRTC (not a human microphone)",
"new_user_final_ms": 1793275972886,
"new_reply_end": {
"kind": "turn_end",
"at_ms": 1793275982559
}
},
"timing_a": {
"fired": true,
"post_ms": 1793276022652,
"status": 200,
"body_head": "data: {\"t\": \"chat.cancelled\", \"epoch\": 1, \"turn_id\": \"58d03703-cf6b-4699-a4a5-d63a07f38bb4\"}\n\n",
"voice_turn_cancel_ms": 1793276030578,
"voice_turn_cancel_reason": "error_phi",
"q2_frames_after_cancel": 800
},
"hangup": {
"delete_status": 200,
"delete_body": {
"ended": true,
"room": "cg-voice-dlas9zmiox"
},
"delete_ms": 1793276041619,
"agent_frames_after_delete": 1,
"room_removed": true
},
"call": {
"tag": "call2",
"session": {
"url": "wss://192.168.200.12:8363",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2aWRlbyI6eyJyb29tSm9pbiI6dHJ1ZSwicm9vbSI6ImNnLXZvaWNlLWRsYXM5em1pb3giLCJjYW5QdWJsaXNoIjp0cnVlLCJjYW5TdWJzY3JpYmUiOnRydWUsImNhblB1Ymxpc2hEYXRhIjp0cnVlfSwic3ViIjoidXNlci1qb29ibmlzYiIsImlzcyI6ImRldmtleSIsIm5iZiI6MTc5MzI3NTk1NiwiZXhwIjoxNzkzMjc2MjU2LCJuYW1lIjoi55So5oi3In0.GeGFqnvFP_XUgV8eLn6T2ZIqxCd7A8V-dHZBxMXp7Yo",
"room": "cg-voice-dlas9zmiox",
"session_id": "Br4leZ_1VqtwH_E4TIM9n8o5",
"agent_name": "cybergril-voice-agent",
"epoch": 1,
"expires_at": 1793276856594,
"voice": "rain"
},
"session_voice": "rain",
"subtitle_count": 69,
"seq_monotonic": true,
"agent_frames": 8434,
"agent_audio_seconds": 84.3,
"agent_audio_rate": 48000,
"agent_joined_ms": 1793275957224,
"subtitles_file": "/opt/cybergril-hel910/evidence/call2.subtitles.jsonl",
"frames_file": "/opt/cybergril-hel910/evidence/call2.frames.json",
"agent_audio_file": "/opt/cybergril-hel910/evidence/call2.agent.wav"
},
"budget_after": {
"at_ms": 1793276044162,
"token_usage": [
{
"day": "2026-10-29",
"scene": "dialogue_main",
"used": 30398
}
],
"request_usage": [
{
"day": "2026-10-29",
"category": "dialogue",
"requests": 10
}
],
"budget_total": [
{
"key": "total_requests",
"value": 10
},
{
"key": "total_tokens",
"value": 30398
}
]
},
"worker_state_after": {
"exists": true,
"content": {
"voice": "rain"
}
},
"db_dump": {
"chat_message": [
{
"id": 1,
"ts": 1793275110974,
"role": "user",
"turn_id": "25a72390-7a14-4ea0-821c-01bc87ab684c",
"status": "sent",
"text": "我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天"
},
{
"id": 2,
"ts": 1793275113739,
"role": "companion",
"turn_id": "25a72390-7a14-4ea0-821c-01bc87ab684c",
"status": "sent",
"text": "好呀,小十。带上小网兜,慢一点也没关系,抓到一只算一只,抓不到就当风陪我们走一趟。"
},
{
"id": 3,
"ts": 1793275121122,
"role": "user",
"turn_id": "e6732ca0-c56b-4cd2-9e60-5f6b4603cdc6",
"status": "sent",
"text": "先停下,我用打字跟你说:把你刚才那句还没说完的话作废,改用文字告诉我,你今天想拉我一起做的第一件事是什么?"
},
{
"id": 4,
"ts": 1793275123508,
"role": "companion",
"turn_id": "e6732ca0-c56b-4cd2-9e60-5f6b4603cdc6",
"status": "sent",
"text": "今天想拉你一起做的第一件事,是找个安静的地方坐下,先一起晒一会儿太阳。"
},
{
"id": 5,
"ts": 1793275181313,
"role": "user",
"turn_id": "c726406d-5476-4c3a-8dac-49b92c86c43e",
"status": "sent",
"text": "师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去画个缘"
},
{
"id": 6,
"ts": 1793275183882,
"role": "companion",
"turn_id": "c726406d-5476-4c3a-8dac-49b92c86c43e",
"status": "sent",
"text": "行啊,小十。去画缘之前,先把那石龙素包子掏出来分我一半。"
},
{
"id": 7,
"ts": 1793275231037,
"role": "user",
"turn_id": "e52af134-b80b-4e40-a42f-8aeab4a3a91b",
"status": "sent",
"text": "没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天"
},
{
"id": 8,
"ts": 1793275233262,
"role": "companion",
"turn_id": "e52af134-b80b-4e40-a42f-8aeab4a3a91b",
"status": "sent",
"text": "好,小十。先坐下来歇口气,等力气攒够了,再一起去抓水母。"
},
{
"id": 9,
"ts": 1793275235147,
"role": "user",
"turn_id": "dc1f3c9d-33da-411f-ae1e-1a2559b150f4",
"status": "sent",
"text": "等一下,先别把刚才那段长回答说完,我打字补充:只用一句话告诉我你现在的心情,然后安静等我。"
},
{
"id": 10,
"ts": 1793275236443,
"role": "companion",
"turn_id": "dc1f3c9d-33da-411f-ae1e-1a2559b150f4",
"status": "sent",
"text": "平静。"
},
{
"id": 11,
"ts": 1793275351015,
"role": "user",
"turn_id": "188dec6c-e59c-49a5-bf56-ea5a071acf30",
"status": "sent",
"text": "没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天"
},
{
"id": 12,
"ts": 1793275357616,
"role": "companion",
"turn_id": "188dec6c-e59c-49a5-bf56-ea5a071acf30",
"status": "sent",
"text": "好,小十,那就一起。"
},
{
"id": 13,
"ts": 1793275976566,
"role": "user",
"turn_id": "faeccd22-7cbf-48f5-8195-ca34d952c078",
"status": "sent",
"text": "师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去化个缘"
},
{
"id": 14,
"ts": 1793275977965,
"role": "companion",
"turn_id": "faeccd22-7cbf-48f5-8195-ca34d952c078",
"status": "sent",
"text": "好,小十。去化缘"
},
{
"id": 15,
"ts": 1793275985818,
"role": "user",
"turn_id": "3100252b-25b4-4090-8ae9-05441b77d69a",
"status": "sent",
"text": "盯着,你看怎么啦"
},
{
"id": 16,
"ts": 1793275987410,
"role": "companion",
"turn_id": "3100252b-25b4-4090-8ae9-05441b77d69a",
"status": "sent",
"text": "我在"
},
{
"id": 17,
"ts": 1793275989727,
"role": "user",
"turn_id": "1cef4c0c-a35e-4864-b0b2-00b002bc5585",
"status": "sent",
"text": "难得看你这么有意思的样子吗"
},
{
"id": 18,
"ts": 1793275990925,
"role": "companion",
"turn_id": "1cef4c0c-a35e-4864-b0b2-00b002bc5585",
"status": "sent",
"text": "我在"
},
{
"id": 19,
"ts": 1793276022655,
"role": "user",
"turn_id": "58d03703-cf6b-4699-a4a5-d63a07f38bb4",
"status": "cancelled",
"text": "先别把这句长回答生成完,我打字打断你:只用一句话告诉我你现在待在哪个角落。"
},
{
"id": 20,
"ts": 1793276022662,
"role": "user",
"turn_id": "76a151eb-b2f0-46aa-a533-ae981ae53168",
"status": "failed",
"text": "师傅,哎,这取经路比高老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去化个缘"
}
],
"budget_total": [
{
"key": "total_requests",
"value": 10
},
{
"key": "total_tokens",
"value": 30398
}
],
"token_usage": [
{
"day": "2026-10-29",
"scene": "dialogue_main",
"used": 30398
}
],
"request_usage": [
{
"day": "2026-10-29",
"category": "dialogue",
"requests": 10
}
]
},
"asr_ledger_after": {
"exists": true,
"events": 80,
"raw": [
{
"kind": "audio",
"call": "cg-voice-dlas9zmiox",
"seconds": 5.023499,
"confirmed_seconds": 5.056311,
"frames": 101,
"bytes": 160752,
"sample_rate": 16000,
"window": "hel910-20261029-a",
"t": 1793276017.648
},
{
"kind": "audio",
"call": "cg-voice-dlas9zmiox",
"seconds": 5.023498,
"confirmed_seconds": 4.990686,
"frames": 100,
"bytes": 160752,
"sample_rate": 16000,
"window": "hel910-20261029-a",
"t": 1793276022.651
},
{
"kind": "audio",
"call": "cg-voice-dlas9zmiox",
"seconds": 5.056311,
"confirmed_seconds": 5.056311,
"frames": 101,
"bytes": 161802,
"sample_rate": 16000,
"window": "hel910-20261029-a",
"t": 1793276027.698
},
{
"kind": "audio",
"call": "cg-voice-dlas9zmiox",
"seconds": 5.023499,
"confirmed_seconds": 5.056373,
"frames": 101,
"bytes": 160752,
"sample_rate": 16000,
"window": "hel910-20261029-a",
"t": 1793276032.748
},
{
"kind": "audio",
"call": "cg-voice-dlas9zmiox",
"seconds": 5.023498,
"confirmed_seconds": 4.990624,
"frames": 100,
"bytes": 160752,
"sample_rate": 16000,
"window": "hel910-20261029-a",
"t": 1793276037.75
},
{
"kind": "audio",
"call": "cg-voice-dlas9zmiox",
"seconds": 5.023499,
"confirmed_seconds": 4.892186,
"frames": 83,
"bytes": 160752,
"sample_rate": 16000,
"window": "hel910-20261029-a",
"t": 1793276041.653
},
{
"kind": "audio",
"call": "cg-voice-dlas9zmiox",
"seconds": 0.820813,
"confirmed_seconds": 1.017813,
"frames": 4,
"bytes": 26266,
"sample_rate": 16000,
"window": "hel910-20261029-a",
"t": 1793276043.643
},
{
"kind": "call_end",
"call": "cg-voice-dlas9zmiox",
"reason": "error",
"audio_seconds": 86.384475,
"confirmed_audio_seconds": 86.384475,
"connect_attempts": 1,
"connect_failures": 0,
"connect_retries": 0,
"window": "hel910-20261029-a",
"t": 1793276043.643
}
]
}
}
@@ -0,0 +1,69 @@
{"v": 2, "seq": 1, "kind": "interim", "role": "user", "text": "师", "_at_ms": 1793275957911}
{"v": 2, "seq": 2, "kind": "interim", "role": "user", "text": "傅", "_at_ms": 1793275958505}
{"v": 2, "seq": 3, "kind": "interim", "role": "user", "text": "诶这", "_at_ms": 1793275959117}
{"v": 2, "seq": 4, "kind": "interim", "role": "user", "text": "取经", "_at_ms": 1793275959706}
{"v": 2, "seq": 5, "kind": "interim", "role": "user", "text": "路比", "_at_ms": 1793275960316}
{"v": 2, "seq": 6, "kind": "interim", "role": "user", "text": "高老", "_at_ms": 1793275960909}
{"v": 2, "seq": 7, "kind": "interim", "role": "user", "text": "庄种地", "_at_ms": 1793275961508}
{"v": 2, "seq": 8, "kind": "interim", "role": "user", "text": "还", "_at_ms": 1793275962118}
{"v": 2, "seq": 9, "kind": "interim", "role": "user", "text": "苦俺", "_at_ms": 1793275962704}
{"v": 2, "seq": 10, "kind": "interim", "role": "user", "text": "老中", "_at_ms": 1793275963311}
{"v": 2, "seq": 11, "kind": "interim", "role": "user", "text": "饿的能", "_at_ms": 1793275963913}
{"v": 2, "seq": 12, "kind": "interim", "role": "user", "text": "吞下", "_at_ms": 1793275964521}
{"v": 2, "seq": 13, "kind": "interim", "role": "user", "text": "石龙", "_at_ms": 1793275965114}
{"v": 2, "seq": 14, "kind": "interim", "role": "user", "text": "素包", "_at_ms": 1793275965714}
{"v": 2, "seq": 15, "kind": "interim", "role": "user", "text": "子", "_at_ms": 1793275966304}
{"v": 2, "seq": 16, "kind": "interim", "role": "user", "text": "猴哥", "_at_ms": 1793275966910}
{"v": 2, "seq": 17, "kind": "interim", "role": "user", "text": "你筋", "_at_ms": 1793275967506}
{"v": 2, "seq": 18, "kind": "interim", "role": "user", "text": "斗云", "_at_ms": 1793275968108}
{"v": 2, "seq": 19, "kind": "interim", "role": "user", "text": "块", "_at_ms": 1793275968707}
{"v": 2, "seq": 20, "kind": "interim", "role": "user", "text": "要不", "_at_ms": 1793275969313}
{"v": 2, "seq": 21, "kind": "interim", "role": "user", "text": "先", "_at_ms": 1793275969905}
{"v": 2, "seq": 22, "kind": "interim", "role": "user", "text": "去画个", "_at_ms": 1793275970514}
{"v": 2, "seq": 23, "kind": "user_final", "role": "user", "text": "师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去化个缘", "_at_ms": 1793275972886}
{"v": 2, "seq": 24, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_d7ad586b1cbe", "_at_ms": 1793275976553}
{"v": 2, "seq": 25, "kind": "assistant_interim", "role": "assistant", "text": "好,小十。去化缘", "speech_id": "speech_d7ad586b1cbe", "_at_ms": 1793275977968}
{"v": 2, "seq": 26, "kind": "assistant_final", "role": "assistant", "text": "好,小十。去化缘", "speech_id": "speech_d7ad586b1cbe", "turn_id": "faeccd22-7cbf-48f5-8195-ca34d952c078", "_at_ms": 1793275982556}
{"v": 2, "seq": 27, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_d7ad586b1cbe", "turn_id": "faeccd22-7cbf-48f5-8195-ca34d952c078", "_at_ms": 1793275982559}
{"v": 2, "seq": 28, "kind": "interim", "role": "user", "text": "盯着你看", "_at_ms": 1793275983928}
{"v": 2, "seq": 29, "kind": "interim", "role": "user", "text": "怎么啦", "_at_ms": 1793275984510}
{"v": 2, "seq": 30, "kind": "user_final", "role": "user", "text": "盯着,你看怎么啦", "_at_ms": 1793275985803}
{"v": 2, "seq": 31, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_43cf4c5b0e2f", "_at_ms": 1793275985811}
{"v": 2, "seq": 32, "kind": "interim", "role": "user", "text": "难得", "_at_ms": 1793275986014}
{"v": 2, "seq": 33, "kind": "interim", "role": "user", "text": "看你这么", "_at_ms": 1793275986611}
{"v": 2, "seq": 34, "kind": "interim", "role": "user", "text": "有意思的", "_at_ms": 1793275987209}
{"v": 2, "seq": 35, "kind": "assistant_interim", "role": "assistant", "text": "我在", "speech_id": "speech_43cf4c5b0e2f", "_at_ms": 1793275987414}
{"v": 2, "seq": 36, "kind": "interim", "role": "user", "text": "样子", "_at_ms": 1793275987812}
{"v": 2, "seq": 37, "kind": "user_final", "role": "user", "text": "难得看你这么有意思的样子吗", "_at_ms": 1793275988919}
{"v": 2, "seq": 38, "kind": "turn_cancel", "role": "assistant", "text": "", "speech_id": "speech_43cf4c5b0e2f", "reason": "interrupted", "_at_ms": 1793275988927}
{"v": 2, "seq": 39, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_115a5fca7071", "_at_ms": 1793275989724}
{"v": 2, "seq": 40, "kind": "assistant_interim", "role": "assistant", "text": "我在", "speech_id": "speech_115a5fca7071", "_at_ms": 1793275990928}
{"v": 2, "seq": 41, "kind": "assistant_final", "role": "assistant", "text": "我在", "speech_id": "speech_115a5fca7071", "turn_id": "1cef4c0c-a35e-4864-b0b2-00b002bc5585", "_at_ms": 1793275992765}
{"v": 2, "seq": 42, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_115a5fca7071", "turn_id": "1cef4c0c-a35e-4864-b0b2-00b002bc5585", "_at_ms": 1793275992768}
{"v": 2, "seq": 43, "kind": "interim", "role": "user", "text": "师", "_at_ms": 1793276004164}
{"v": 2, "seq": 44, "kind": "interim", "role": "user", "text": "傅诶", "_at_ms": 1793276004754}
{"v": 2, "seq": 45, "kind": "interim", "role": "user", "text": "这取", "_at_ms": 1793276005365}
{"v": 2, "seq": 46, "kind": "interim", "role": "user", "text": "经路", "_at_ms": 1793276005968}
{"v": 2, "seq": 47, "kind": "interim", "role": "user", "text": "比高", "_at_ms": 1793276006561}
{"v": 2, "seq": 48, "kind": "interim", "role": "user", "text": "老庄", "_at_ms": 1793276007158}
{"v": 2, "seq": 49, "kind": "interim", "role": "user", "text": "种地", "_at_ms": 1793276007759}
{"v": 2, "seq": 50, "kind": "interim", "role": "user", "text": "还", "_at_ms": 1793276008359}
{"v": 2, "seq": 51, "kind": "interim", "role": "user", "text": "苦俺", "_at_ms": 1793276008956}
{"v": 2, "seq": 52, "kind": "interim", "role": "user", "text": "老猪饿", "_at_ms": 1793276009557}
{"v": 2, "seq": 53, "kind": "interim", "role": "user", "text": "的能", "_at_ms": 1793276010158}
{"v": 2, "seq": 54, "kind": "interim", "role": "user", "text": "吞下石", "_at_ms": 1793276010760}
{"v": 2, "seq": 55, "kind": "interim", "role": "user", "text": "龙素", "_at_ms": 1793276011358}
{"v": 2, "seq": 56, "kind": "interim", "role": "user", "text": "包", "_at_ms": 1793276011954}
{"v": 2, "seq": 57, "kind": "interim", "role": "user", "text": "子猴", "_at_ms": 1793276012570}
{"v": 2, "seq": 58, "kind": "interim", "role": "user", "text": "哥", "_at_ms": 1793276013163}
{"v": 2, "seq": 59, "kind": "interim", "role": "user", "text": "你筋斗", "_at_ms": 1793276013764}
{"v": 2, "seq": 60, "kind": "interim", "role": "user", "text": "云", "_at_ms": 1793276014358}
{"v": 2, "seq": 61, "kind": "interim", "role": "user", "text": "快要", "_at_ms": 1793276014959}
{"v": 2, "seq": 62, "kind": "interim", "role": "user", "text": "不", "_at_ms": 1793276015560}
{"v": 2, "seq": 63, "kind": "interim", "role": "user", "text": "先去", "_at_ms": 1793276016157}
{"v": 2, "seq": 64, "kind": "interim", "role": "user", "text": "画个", "_at_ms": 1793276016762}
{"v": 2, "seq": 65, "kind": "user_final", "role": "user", "text": "师傅,哎,这取经路比高老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去化个缘", "_at_ms": 1793276018979}
{"v": 2, "seq": 66, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_6ab8dc1b67d0", "_at_ms": 1793276022633}
{"v": 2, "seq": 67, "kind": "assistant_interim", "role": "assistant", "text": "她这会儿没接上话,稍后再试试。你写的话还在。", "speech_id": "speech_6ab8dc1b67d0", "_at_ms": 1793276022670}
{"v": 2, "seq": 68, "kind": "error", "role": "assistant", "text": "她这会儿没接上话,稍后再试试。你写的话还在。", "speech_id": "speech_6ab8dc1b67d0", "_at_ms": 1793276030576}
{"v": 2, "seq": 69, "kind": "turn_cancel", "role": "assistant", "text": "", "speech_id": "speech_6ab8dc1b67d0", "reason": "error_phi", "_at_ms": 1793276030578}
+394
View File
@@ -0,0 +1,394 @@
{
"chat_message": [
{
"id": 1,
"ts": 1793275110974,
"role": "user",
"text": "我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天",
"turn_id": "25a72390-7a14-4ea0-821c-01bc87ab684c",
"status": "sent",
"proactive": 0
},
{
"id": 2,
"ts": 1793275113739,
"role": "companion",
"text": "好呀,小十。带上小网兜,慢一点也没关系,抓到一只算一只,抓不到就当风陪我们走一趟。",
"turn_id": "25a72390-7a14-4ea0-821c-01bc87ab684c",
"status": "sent",
"proactive": 0
},
{
"id": 3,
"ts": 1793275121122,
"role": "user",
"text": "先停下,我用打字跟你说:把你刚才那句还没说完的话作废,改用文字告诉我,你今天想拉我一起做的第一件事是什么?",
"turn_id": "e6732ca0-c56b-4cd2-9e60-5f6b4603cdc6",
"status": "sent",
"proactive": 0
},
{
"id": 4,
"ts": 1793275123508,
"role": "companion",
"text": "今天想拉你一起做的第一件事,是找个安静的地方坐下,先一起晒一会儿太阳。",
"turn_id": "e6732ca0-c56b-4cd2-9e60-5f6b4603cdc6",
"status": "sent",
"proactive": 0
},
{
"id": 5,
"ts": 1793275181313,
"role": "user",
"text": "师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去画个缘",
"turn_id": "c726406d-5476-4c3a-8dac-49b92c86c43e",
"status": "sent",
"proactive": 0
},
{
"id": 6,
"ts": 1793275183882,
"role": "companion",
"text": "行啊,小十。去画缘之前,先把那石龙素包子掏出来分我一半。",
"turn_id": "c726406d-5476-4c3a-8dac-49b92c86c43e",
"status": "sent",
"proactive": 0
},
{
"id": 7,
"ts": 1793275231037,
"role": "user",
"text": "没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天",
"turn_id": "e52af134-b80b-4e40-a42f-8aeab4a3a91b",
"status": "sent",
"proactive": 0
},
{
"id": 8,
"ts": 1793275233262,
"role": "companion",
"text": "好,小十。先坐下来歇口气,等力气攒够了,再一起去抓水母。",
"turn_id": "e52af134-b80b-4e40-a42f-8aeab4a3a91b",
"status": "sent",
"proactive": 0
},
{
"id": 9,
"ts": 1793275235147,
"role": "user",
"text": "等一下,先别把刚才那段长回答说完,我打字补充:只用一句话告诉我你现在的心情,然后安静等我。",
"turn_id": "dc1f3c9d-33da-411f-ae1e-1a2559b150f4",
"status": "sent",
"proactive": 0
},
{
"id": 10,
"ts": 1793275236443,
"role": "companion",
"text": "平静。",
"turn_id": "dc1f3c9d-33da-411f-ae1e-1a2559b150f4",
"status": "sent",
"proactive": 0
},
{
"id": 11,
"ts": 1793275351015,
"role": "user",
"text": "没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天",
"turn_id": "188dec6c-e59c-49a5-bf56-ea5a071acf30",
"status": "sent",
"proactive": 0
},
{
"id": 12,
"ts": 1793275357616,
"role": "companion",
"text": "好,小十,那就一起。",
"turn_id": "188dec6c-e59c-49a5-bf56-ea5a071acf30",
"status": "sent",
"proactive": 0
},
{
"id": 13,
"ts": 1793275976566,
"role": "user",
"text": "师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去化个缘",
"turn_id": "faeccd22-7cbf-48f5-8195-ca34d952c078",
"status": "sent",
"proactive": 0
},
{
"id": 14,
"ts": 1793275977965,
"role": "companion",
"text": "好,小十。去化缘",
"turn_id": "faeccd22-7cbf-48f5-8195-ca34d952c078",
"status": "sent",
"proactive": 0
},
{
"id": 15,
"ts": 1793275985818,
"role": "user",
"text": "盯着,你看怎么啦",
"turn_id": "3100252b-25b4-4090-8ae9-05441b77d69a",
"status": "sent",
"proactive": 0
},
{
"id": 16,
"ts": 1793275987410,
"role": "companion",
"text": "我在",
"turn_id": "3100252b-25b4-4090-8ae9-05441b77d69a",
"status": "sent",
"proactive": 0
},
{
"id": 17,
"ts": 1793275989727,
"role": "user",
"text": "难得看你这么有意思的样子吗",
"turn_id": "1cef4c0c-a35e-4864-b0b2-00b002bc5585",
"status": "sent",
"proactive": 0
},
{
"id": 18,
"ts": 1793275990925,
"role": "companion",
"text": "我在",
"turn_id": "1cef4c0c-a35e-4864-b0b2-00b002bc5585",
"status": "sent",
"proactive": 0
},
{
"id": 19,
"ts": 1793276022655,
"role": "user",
"text": "先别把这句长回答生成完,我打字打断你:只用一句话告诉我你现在待在哪个角落。",
"turn_id": "58d03703-cf6b-4699-a4a5-d63a07f38bb4",
"status": "cancelled",
"proactive": 0
},
{
"id": 20,
"ts": 1793276022662,
"role": "user",
"text": "师傅,哎,这取经路比高老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去化个缘",
"turn_id": "76a151eb-b2f0-46aa-a533-ae981ae53168",
"status": "failed",
"proactive": 0
}
],
"companion": [
{
"id": 1,
"identity_json": "{\"id\": \"1\", \"relation\": \"朋友\", \"nickname\": \"小十\", \"boundary\": \"尊重彼此的节奏\", \"name\": \"小测\", \"birth\": \"2001-06-15\", \"hour\": \"午时\", \"color\": {\"pillars\": {\"year\": \"辛巳\", \"month\": \"甲午\", \"day\": \"己酉\", \"time\": \"庚午\"}, \"traits\": \"话多、直接、不服管、爱纠正人、温和\", \"calendar\": {\"solar\": \"2001-06-15\", \"lunar\": \"二〇〇一年闰四月廿四\", \"leap_month\": true, \"hour\": \"午时\", \"hour_range\": \"11:00–12:59\", \"calculation_time\": \"12:00\", \"timezone\": \"Asia/Shanghai\", \"rule\": \"按北京时间和所选时辰内的偶数整点排盘;子时取本日00:00。月份按节气分界,未使用出生地或真太阳时。\"}, \"sections\": [{\"id\": \"expression\", \"title\": \"表达与社交节奏\", \"text\": \"熟悉的话题容易带动她主动展开,常借着说出来理清想法。这让交流有热度,但赶着表达时也可能挤掉对方的空间。发现节奏不同时,先复述对方的意思,再继续自己的观点。\", \"basis\": \"取自原档案的表达倾向;结合十神中食神、伤官对应的表达偏移。\"}, {\"id\": \"closeness\", \"title\": \"亲密、安全感与边界\", \"text\": \"她既需要被回应,也需要保留自己的安排。关系稳定时愿意靠近,信息过密时会想留一点空隙;这不是忽冷忽热的定论。把独处和下一次交流的期待说清楚,比反复猜测更有帮助。\", \"basis\": \"取自原档案的独处需求与自主倾向;日主己;五行侧重金、火,十神以食神较显著。\"}, {\"id\": \"repair\", \"title\": \"冲突时的反应与修复\", \"text\": \"发生分歧时,她可能先说明自己的理由,再观察对方是否愿意互相理解。有守住立场的能力,也可能在反复解释中消耗耐心。把事实、感受和希望怎样调整分开说,更容易找到可落实的修复。\", \"basis\": \"取自原档案的对抗、纠正与顺从倾向;没有把分歧当作已发生的记忆。\"}, {\"id\": \"rhythm\", \"title\": \"决策、行动与日常节律\", \"text\": \"她通常既看具体条件,也会给当下的感受留位置。事情明确时愿意推进,存在多种可能时会重新衡量;这种灵活有助于避开僵化,也可能拖延选择。给决定一个小范围和可调整的期限会更稳。\", \"basis\": \"取自原档案的纪律、日常规律、现实感与行动倾向。\"}, {\"id\": \"stress\", \"title\": \"压力下的变化与恢复\", \"text\": \"压力会影响她的回应速度和耐心,但影响不是每次都一样。环境清楚、有可预期的下一步时更容易回稳;反复追问可能增加负担。把刺激减少一点、留出休息,比要求马上恢复原样更合适。\", \"basis\": \"取自原档案由日主强弱形成的敏感度与恢复尺度;不是此刻情绪。\"}, {\"id\": \"growth\", \"title\": \"内在张力与成长空间\", \"text\": \"她更愿意自己判断,不轻易把决定交出去。这有助于保留独立和诚实,也可能让协商听起来像被限制。成长可以是把可商量的部分说清楚:保留立场,同时让对方真正参与,而不是靠退让或控制维持亲近。\", \"basis\": \"取自原档案的自主与顺从之间的组合;成长来自之后真实的相处,不由出生资料预写。\"}], \"interpretation\": \"这是以四柱、五行、十神和已存底色为依据的角色塑造说明,不是对真人性格的测定,也不是已经发生的经历。倾向不等于固定台词或此刻情绪。\"}}"
}
],
"bazi_core": [
{
"id": 1,
"birth_input": "{\"birth\": \"2001-06-15\", \"hour\": \"\\u5348\\u65f6\", \"tz\": \"Asia/Shanghai\"}",
"pillars": "{\"year\": \"辛巳\", \"month\": \"甲午\", \"day\": \"己酉\", \"time\": \"庚午\"}",
"day_master": "己",
"day_master_elem": "土",
"ten_gods": "{\"\\u98df\\u795e\": 0.23275862068965517, \"\\u6b63\\u5370\": 0.060344827586206885, \"\\u4f24\\u5b98\": 0.23275862068965517, \"\\u52ab\\u8d22\": 0.060344827586206885, \"\\u6b63\\u5b98\": 0.17241379310344826, \"\\u504f\\u5370\": 0.12068965517241377, \"\\u6bd4\\u80a9\": 0.12068965517241377}",
"element_scores": "{\"\\u6728\": 2.0, \"\\u706b\": 2.4, \"\\u571f\": 2.0, \"\\u91d1\": 2.9, \"\\u6c34\": 0.0}",
"strength": 0.3440860215053763,
"derived": "{\"autonomy\": 0.596551724137931, \"compliance\": 0.2655172413793103, \"expressiveness\": 0.8491379310344828, \"warmth\": 0.6163793103448276, \"anxiety\": 0.5, \"aggression\": 0.5, \"discipline\": 0.6379310344827587, \"correcting\": 0.6206896551724138, \"solitude_need\": 0.596551724137931, \"impulsivity\": 0.5422413793103448, \"sharing\": 0.4698275862068966, \"realism\": 0.5, \"romance\": 0.5, \"generosity\": 0.5, \"routine\": 0.5, \"caretaking\": 0.5422413793103448, \"initiative\": 0.4698275862068966, \"explaining\": 0.42758620689655175, \"sensitivity\": 1.1091397849462368, \"recovery_scale\": 1.1870967741935485, \"trust_floor\": 0.5, \"element_bias\": {\"\\u6728\": 0.2150537634408602, \"\\u706b\": 0.25806451612903225, \"\\u571f\": 0.2150537634408602, \"\\u91d1\": 0.31182795698924726, \"\\u6c34\": 0.0}, \"strength\": 0.3440860215053763}",
"version": 1,
"created_at": 1793274721137
}
],
"persona_job": [
{
"id": 1,
"state": "failed",
"owner": "497d8e3e4cf406995b1cade83f60a652",
"lease_until": 1793274761137,
"reason": "generation_failed",
"provenance": "created-R2"
}
],
"token_usage": [
{
"day": "2026-10-29",
"scene": "dialogue_main",
"used": 30398
}
],
"request_usage": [
{
"day": "2026-10-29",
"category": "dialogue",
"requests": 10
}
],
"budget_total": [
{
"key": "total_requests",
"value": 10
},
{
"key": "total_tokens",
"value": 30398
}
],
"sharing_attempt": [
{
"message_id": 2,
"turn_id": "25a72390-7a14-4ea0-821c-01bc87ab684c",
"epoch": 1,
"token": "hjEZheK3Eykyk8S_raw2Sft0mQs5sZxp7TooRh3__ys",
"text_hash": "c2ee89ce73cb6f55997b954f03cd91470f9a607ca354860532e3723e1bd5ece4",
"expires_at": 1793361513739,
"ack_at": null,
"candidates": "[]",
"state": "judged",
"selected": "[]",
"lease_until": 1793275175525,
"owner": "ae5b3da4d7b9b6b03d7ddfb95346f842",
"judgment_json": null,
"judged_at": 1793275135525,
"model": null
},
{
"message_id": 4,
"turn_id": "e6732ca0-c56b-4cd2-9e60-5f6b4603cdc6",
"epoch": 1,
"token": "JKtPmucXVDtidVATDkmd6ms_rtbW-D08yS7Y94XxXsc",
"text_hash": "a40e4cc46410034a74fc7431ca8d77cafb85e7d86a9fab5f0a6d29f5ed73e212",
"expires_at": 1793361523508,
"ack_at": 1793275123516,
"candidates": "[]",
"state": "judged",
"selected": "[]",
"lease_until": 1793275205529,
"owner": "c9bd39978bfbbbe59d5cce108b732313",
"judgment_json": null,
"judged_at": 1793275165529,
"model": null
},
{
"message_id": 6,
"turn_id": "c726406d-5476-4c3a-8dac-49b92c86c43e",
"epoch": 1,
"token": "StFEeQfEcbAYMvEOLUwGPh9YCEQ2qxDmATLsdQHN0u0",
"text_hash": "6f23aa4e1852ef2035dab15932d38d99980062d7a97234d9c3718cd0c17e08ba",
"expires_at": 1793361583882,
"ack_at": 1793275194491,
"candidates": "[]",
"state": "judged",
"selected": "[]",
"lease_until": 1793275235535,
"owner": "eba70539290f13f67ff79fde51314e78",
"judgment_json": null,
"judged_at": 1793275195535,
"model": null
},
{
"message_id": 8,
"turn_id": "e52af134-b80b-4e40-a42f-8aeab4a3a91b",
"epoch": 1,
"token": "1XGtAHqxEhUTeLhzdKNdgkkFbDoIcSD-frIktFwcU_Q",
"text_hash": "1e44e181ffd0c8bde7743734d753272842dd62341ae621524f2c99febf00f5a3",
"expires_at": 1793361633262,
"ack_at": null,
"candidates": "[]",
"state": "judged",
"selected": "[]",
"lease_until": 1793275295544,
"owner": "dd352e7cad5af1d483786589dcd03ce5",
"judgment_json": null,
"judged_at": 1793275255544,
"model": null
},
{
"message_id": 10,
"turn_id": "dc1f3c9d-33da-411f-ae1e-1a2559b150f4",
"epoch": 1,
"token": "bx99mmrkqfhUCyysWSR6j0wyfwFOpb6y6pyRpdjDPno",
"text_hash": "b7123f80eed52f0093c274ac380e9add3834a861915ac462de476bc57b8a1810",
"expires_at": 1793361636443,
"ack_at": 1793275236447,
"candidates": "[]",
"state": "judged",
"selected": "[]",
"lease_until": 1793275325548,
"owner": "e593a91da302170ae178db2f74ad92bb",
"judgment_json": null,
"judged_at": 1793275285548,
"model": null
},
{
"message_id": 12,
"turn_id": "188dec6c-e59c-49a5-bf56-ea5a071acf30",
"epoch": 1,
"token": "abmkHDipAE1oNFXP9EXQ7za8ukG34ND-OQqtRScdJtM",
"text_hash": "c494f738d3f4b90a713e6d0d847e6c318687dde177d411dc4d27041612870791",
"expires_at": 1793361757616,
"ack_at": 1793275363881,
"candidates": "[]",
"state": "judged",
"selected": "[]",
"lease_until": 1793275415560,
"owner": "a7e1e8951d50df107584bc6d8dd28617",
"judgment_json": null,
"judged_at": 1793275375560,
"model": null
},
{
"message_id": 14,
"turn_id": "faeccd22-7cbf-48f5-8195-ca34d952c078",
"epoch": 1,
"token": "I246ZrcVFbxsH4TyybQj6m_y7z89z2x0wKrZHU9TWkA",
"text_hash": "9c0869ba62f61e3aa7fbdb3796d7e4105b1ce2b0c67411ad67529c9135a8d9a9",
"expires_at": 1793362377965,
"ack_at": 1793275982559,
"candidates": "[]",
"state": "judged",
"selected": "[]",
"lease_until": 1793276045622,
"owner": "d7332f384d265be5e2fbdf8a3a57809f",
"judgment_json": null,
"judged_at": 1793276005622,
"model": null
},
{
"message_id": 16,
"turn_id": "3100252b-25b4-4090-8ae9-05441b77d69a",
"epoch": 1,
"token": "maAFv_MreHy8ZXt-zkJCUjpXer0ZBCvfYiZvlwfXpHg",
"text_hash": "33f40475f82bfe083f44e6744917f76acfa69cc8c3fba89c3a03c09c9388cfbd",
"expires_at": 1793362387411,
"ack_at": null,
"candidates": "[]",
"state": "judged",
"selected": "[]",
"lease_until": 1793276075626,
"owner": "17317b2213cade6cb7c57dea1282d655",
"judgment_json": null,
"judged_at": 1793276035626,
"model": null
},
{
"message_id": 18,
"turn_id": "1cef4c0c-a35e-4864-b0b2-00b002bc5585",
"epoch": 1,
"token": "hc3uDqDF0lxSxjCWZmBgCW-F-co9UofhwhpXf20UrX4",
"text_hash": "33f40475f82bfe083f44e6744917f76acfa69cc8c3fba89c3a03c09c9388cfbd",
"expires_at": 1793362390925,
"ack_at": 1793275992766,
"candidates": "[]",
"state": "judged",
"selected": "[]",
"lease_until": 1793276105630,
"owner": "05a9a9737f9240da765207a0645e0182",
"judgment_json": null,
"judged_at": 1793276065631,
"model": null
}
]
}
+48
View File
@@ -0,0 +1,48 @@
{
"rooms_before": [],
"rooms_mid": [
"cg-voice-9r5jjmcz0n"
],
"hangup": {
"delete_status": 200,
"delete_body": {
"ended": true,
"room": "cg-voice-9r5jjmcz0n"
},
"delete_ms": 1793274802357,
"agent_frames_after_delete": 0,
"room_removed": true
},
"rooms_after": [],
"budget_after": {
"at_ms": 1793274806882,
"token_usage": [],
"request_usage": [],
"budget_total": []
},
"asr_ledger_after": {
"exists": true,
"events": 2,
"raw": [
{
"kind": "call_start",
"call": "cg-voice-9r5jjmcz0n",
"stream_seq": 1,
"window": "hel910-20261029-a",
"t": 1793274792.96
},
{
"kind": "call_end",
"call": "cg-voice-9r5jjmcz0n",
"reason": "error",
"audio_seconds": 0.0,
"confirmed_audio_seconds": 0.0,
"connect_attempts": 0,
"connect_failures": 0,
"connect_retries": 0,
"window": "hel910-20261029-a",
"t": 1793274804.372
}
]
}
}
+29
View File
@@ -0,0 +1,29 @@
== HEL-910 post snapshot 2026-10-29T12:16:46Z ==
-- ports (83xx/80xx/98xx/788x/10095) --
LISTEN 0 2048 0.0.0.0:8310 0.0.0.0:* users:(("python",pid=1981,fd=9))
LISTEN 0 5 0.0.0.0:8300 0.0.0.0:* users:(("python3",pid=1437,fd=3))
LISTEN 0 2048 0.0.0.0:8350 0.0.0.0:* users:(("python",pid=1984,fd=9))
LISTEN 0 2048 0.0.0.0:8320 0.0.0.0:* users:(("python",pid=1982,fd=9))
LISTEN 0 4096 0.0.0.0:10095 0.0.0.0:* users:(("docker-proxy",pid=2542,fd=8))
LISTEN 0 4096 [::]:10095 [::]:* users:(("docker-proxy",pid=2548,fd=8))
LISTEN 0 4096 *:7881 *:* users:(("livekit-server",pid=2435,fd=4))
LISTEN 0 4096 *:7880 *:* users:(("livekit-server",pid=2435,fd=7))
-- our pids --
api no pidfile
bridge no pidfile
worker no pidfile
tts9880 no pidfile
-- livekit rooms --
invalid authorization token
-- voices.json hash + builtin count --
7b2a50213312c3d0b517a3047434a29890501937178217815812809c3cac4970 /opt/voice-agent/cosyvoice/voices.json
entries: 11 ids: ['default', 'sunwukong', 'nezha', 'bajie', 'haimianbaobao', 'female_soft', 'male_warm', 'xiaoyun', 'rain', 'jiaojiao', 'bailu'] cg- entries: 0
-- gpu --
747 MiB, 32607 MiB
-- 8300/8310/8320/8350 untouched check --
LISTEN 0 2048 0.0.0.0:8310 0.0.0.0:*
LISTEN 0 5 0.0.0.0:8300 0.0.0.0:*
LISTEN 0 2048 0.0.0.0:8350 0.0.0.0:*
LISTEN 0 2048 0.0.0.0:8320 0.0.0.0:*
-- tts.log size --
27302 bytes 2026-10-29 20:16:37.339409311 +0800
+29
View File
@@ -0,0 +1,29 @@
== HEL-910 pre snapshot 2026-10-29T11:51:06Z ==
-- ports (83xx/80xx/98xx/788x/10095) --
LISTEN 0 2048 0.0.0.0:8310 0.0.0.0:* users:(("python",pid=1981,fd=9))
LISTEN 0 5 0.0.0.0:8300 0.0.0.0:* users:(("python3",pid=1437,fd=3))
LISTEN 0 2048 0.0.0.0:8350 0.0.0.0:* users:(("python",pid=1984,fd=9))
LISTEN 0 2048 0.0.0.0:8320 0.0.0.0:* users:(("python",pid=1982,fd=9))
LISTEN 0 4096 0.0.0.0:10095 0.0.0.0:* users:(("docker-proxy",pid=2542,fd=8))
LISTEN 0 4096 [::]:10095 [::]:* users:(("docker-proxy",pid=2548,fd=8))
LISTEN 0 4096 *:7881 *:* users:(("livekit-server",pid=2435,fd=4))
LISTEN 0 4096 *:7880 *:* users:(("livekit-server",pid=2435,fd=7))
-- our pids --
api no pidfile
bridge no pidfile
worker no pidfile
tts9880 no pidfile
-- livekit rooms --
invalid authorization token
-- voices.json hash + builtin count --
7b2a50213312c3d0b517a3047434a29890501937178217815812809c3cac4970 /opt/voice-agent/cosyvoice/voices.json
entries: 11 ids: ['default', 'sunwukong', 'nezha', 'bajie', 'haimianbaobao', 'female_soft', 'male_warm', 'xiaoyun', 'rain', 'jiaojiao', 'bailu'] cg- entries: 0
-- gpu --
747 MiB, 32607 MiB
-- 8300/8310/8320/8350 untouched check --
LISTEN 0 2048 0.0.0.0:8310 0.0.0.0:*
LISTEN 0 5 0.0.0.0:8300 0.0.0.0:*
LISTEN 0 2048 0.0.0.0:8350 0.0.0.0:*
LISTEN 0 2048 0.0.0.0:8320 0.0.0.0:*
-- tts.log size --
16311 bytes 2026-10-28 20:26:46.181514865 +0800
+91
View File
@@ -0,0 +1,91 @@
INFO: 127.0.0.1:60586 - "GET /health HTTP/1.1" 200 OK
INFO: 127.0.0.1:42846 - "GET /health HTTP/1.1" 200 OK
INFO: 127.0.0.1:52016 - "POST /tts HTTP/1.1" 200 OK
0%| | 0/1 [00:00<?, ?it/s]/opt/voice-agent/cosyvoice/venv/lib/python3.12/site-packages/torchaudio/_backend/utils.py:213: UserWarning: In 2.9, this function's implementation will be changed to use torchaudio.load_with_torchcodec` under the hood. Some parameters like ``normalize``, ``format``, ``buffer_size``, and ``backend`` will be ignored. We recommend that you port your code to rely directly on TorchCodec's decoder instead: https://docs.pytorch.org/torchcodec/stable/generated/torchcodec.decoders.AudioDecoder.html#torchcodec.decoders.AudioDecoder.
warnings.warn(
2026-10-29 19:58:35,530 INFO synthesis text 好呀,小十。带上小网兜,慢一点也没关系,抓到一只算一只,抓不到就当风陪我们走一趟。
/opt/voice-agent/cosyvoice/CosyVoice/cosyvoice/cli/model.py:103: FutureWarning: `torch.cuda.amp.autocast(args...)` is deprecated. Please use `torch.amp.autocast('cuda', args...)` instead.
with self.llm_context, torch.cuda.amp.autocast(self.fp16 is True and hasattr(self.llm, 'vllm') is False):
/opt/voice-agent/cosyvoice/CosyVoice/cosyvoice/cli/model.py:426: FutureWarning: `torch.cuda.amp.autocast(args...)` is deprecated. Please use `torch.amp.autocast('cuda', args...)` instead.
with torch.cuda.amp.autocast(self.fp16):
2026-10-29 19:58:37,189 INFO yield speech len 1.48, rtf 1.1213744008863296
[cosyvoice] first chunk after 2.139s (好呀,小十。带上小网兜,慢一点也没关系,...)
2026-10-29 19:58:37,765 INFO yield speech len 2.0, rtf 0.28624629974365234
2026-10-29 19:58:39,023 INFO yield speech len 4.0, rtf 0.3143606185913086
2026-10-29 19:58:39,892 INFO yield speech len 3.08, rtf 0.28152744491378984
100%|██████████| 1/1 [00:04<00:00, 4.84s/it]
100%|██████████| 1/1 [00:04<00:00, 4.84s/it]
INFO: 127.0.0.1:51570 - "POST /tts HTTP/1.1" 200 OK
0%| | 0/1 [00:00<?, ?it/s]2026-10-29 19:59:44,345 INFO synthesis text 行啊,小十。去画缘之前,先把那石龙素包子掏出来分我一半。
2026-10-29 19:59:46,395 INFO yield speech len 5.48, rtf 0.3741068996652199
[cosyvoice] first chunk after 2.499s (行啊,小十。去画缘之前,先把那石龙素包子...)
2026-10-29 19:59:47,161 INFO yield speech len 2.56, rtf 0.29581449925899506
100%|██████████| 1/1 [00:03<00:00, 3.27s/it]
100%|██████████| 1/1 [00:03<00:00, 3.27s/it]
INFO: 127.0.0.1:38442 - "POST /tts HTTP/1.1" 200 OK
0%| | 0/1 [00:00<?, ?it/s]2026-10-29 20:00:35,508 INFO synthesis text 好,小十。先坐下来歇口气,等力气攒够了,再一起去抓水母。
2026-10-29 20:00:37,504 INFO yield speech len 5.48, rtf 0.3641228171160621
0%| | 0/1 [00:02<?, ?it/s]
INFO: 127.0.0.1:42530 - "POST /tts HTTP/1.1" 200 OK
0%| | 0/1 [00:00<?, ?it/s]2026-10-29 20:02:37,629 WARNING synthesis text 好,小十,那就一起。 too short than prompt text You are a helpful assistant.<|endofprompt|>你是不是早就想睡我了呀, this may lead to bad performance
2026-10-29 20:02:38,073 INFO synthesis text 好,小十,那就一起。
2026-10-29 20:02:39,630 INFO yield speech len 4.2, rtf 0.3707294804709298
[cosyvoice] first chunk after 2.003s (好,小十,那就一起。...)
100%|██████████| 1/1 [00:02<00:00, 2.01s/it]
100%|██████████| 1/1 [00:02<00:00, 2.01s/it]
INFO: 127.0.0.1:39972 - "GET /health HTTP/1.1" 200 OK
INFO: 127.0.0.1:52960 - "GET /health HTTP/1.1" 200 OK
INFO: 127.0.0.1:47530 - "POST /tts HTTP/1.1" 200 OK
0%| | 0/1 [00:00<?, ?it/s]2026-10-29 20:12:57,987 WARNING synthesis text 好,小十。去化缘。 too short than prompt text You are a helpful assistant.<|endofprompt|>你是不是早就想睡我了呀, this may lead to bad performance
2026-10-29 20:12:58,439 INFO synthesis text 好,小十。去化缘。
2026-10-29 20:12:59,596 INFO yield speech len 2.92, rtf 0.3961743557289855
[cosyvoice] first chunk after 1.610s (好,小十。去化缘...)
100%|██████████| 1/1 [00:01<00:00, 1.62s/it]
100%|██████████| 1/1 [00:01<00:00, 1.62s/it]
INFO: 127.0.0.1:58028 - "POST /tts HTTP/1.1" 200 OK
0%| | 0/1 [00:00<?, ?it/s]2026-10-29 20:13:07,997 WARNING synthesis text 我在。 too short than prompt text You are a helpful assistant.<|endofprompt|>你是不是早就想睡我了呀, this may lead to bad performance
2026-10-29 20:13:08,478 INFO synthesis text 我在。
2026-10-29 20:13:09,046 INFO yield speech len 0.84, rtf 0.6765649432227725
INFO: 127.0.0.1:45692 - "POST /tts HTTP/1.1" 200 OK
0%| | 0/1 [00:00<?, ?it/s]2026-10-29 20:13:10,937 WARNING synthesis text 我在。 too short than prompt text You are a helpful assistant.<|endofprompt|>你是不是早就想睡我了呀, this may lead to bad performance
2026-10-29 20:13:11,393 INFO synthesis text 我在。
2026-10-29 20:13:11,888 INFO yield speech len 0.84, rtf 0.5890837737492153
[cosyvoice] first chunk after 0.952s (我在...)
100%|██████████| 1/1 [00:00<00:00, 1.04it/s]
100%|██████████| 1/1 [00:00<00:00, 1.04it/s]
INFO: 127.0.0.1:59420 - "POST /tts HTTP/1.1" 200 OK
0%| | 0/1 [00:00<?, ?it/s]2026-10-29 20:13:42,684 WARNING synthesis text 她这会儿没接上话,稍后再试试。你写的话还在。 too short than prompt text You are a helpful assistant.<|endofprompt|>你是不是早就想睡我了呀, this may lead to bad performance
2026-10-29 20:13:43,127 INFO synthesis text 她这会儿没接上话,稍后再试试。你写的话还在。
0%| | 0/1 [00:35<?, ?it/s]
2026-10-29 20:13:45,094 INFO yield speech len 5.44, rtf 0.36164346863241753
[cosyvoice] first chunk after 2.412s (她这会儿没接上话,稍后再试试。你写的话还...)
100%|██████████| 1/1 [00:02<00:00, 2.42s/it]
100%|██████████| 1/1 [00:02<00:00, 2.42s/it]
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [116771]
/opt/voice-agent/cosyvoice/server.py:71: DeprecationWarning:
on_event is deprecated, use lifespan event handlers instead.
Read more about it in the
+1
View File
@@ -0,0 +1 @@
[2026-10-29T11:53:04Z] watchdog start, deadline 2026-10-29T12:37:04Z (pid 117201)
+298
View File
@@ -0,0 +1,298 @@
{"message": "starting worker", "level": "INFO", "name": "livekit.agents", "version": "1.8.8", "rtc-version": "1.1.20", "pid": 117034, "timestamp": "2026-10-29T11:52:40.284241+00:00"}
{"message": "plugin registered", "level": "INFO", "name": "livekit.agents", "plugin": "livekit.plugins.silero", "version": "1.8.8", "pid": 117034, "timestamp": "2026-10-29T11:52:40.284377+00:00"}
{"message": "preloading plugins", "level": "INFO", "name": "livekit.agents", "packages": ["livekit.plugins.silero", "livekit.agents.ipc._preload", "livekit.agents.ipc._preload_freeze"], "pid": 117034, "timestamp": "2026-10-29T11:52:40.284454+00:00"}
{"message": "HTTP server listening on :8082", "level": "INFO", "name": "livekit.agents", "pid": 117034, "timestamp": "2026-10-29T11:52:40.287181+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 117117, "timestamp": "2026-10-29T11:52:41.505412+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 117119, "timestamp": "2026-10-29T11:52:41.508439+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 117121, "timestamp": "2026-10-29T11:52:41.511120+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 117123, "timestamp": "2026-10-29T11:52:41.514240+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 117117, "elapsed_time": 0.12, "timestamp": "2026-10-29T11:52:41.621852+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 117121, "elapsed_time": 0.14, "timestamp": "2026-10-29T11:52:41.647905+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 117119, "elapsed_time": 0.14, "timestamp": "2026-10-29T11:52:41.648976+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 117123, "elapsed_time": 0.15, "timestamp": "2026-10-29T11:52:41.664619+00:00"}
/opt/voice-agent/agent/venv/lib/python3.12/site-packages/jwt/api_jwt.py:149: InsecureKeyLengthWarning: The HMAC key is 28 bytes long, which is below the minimum recommended length of 32 bytes for SHA256. See RFC 7518 Section 3.2.
return self._jws.encode(
{"message": "registered worker", "level": "INFO", "name": "livekit.agents", "agent_name": "cybergril-voice-agent", "deployment": "", "id": "AW_EmnniB7z9YMD", "url": "ws://127.0.0.1:7880", "region": "", "protocol": 17, "pid": 117034, "timestamp": "2026-10-29T11:52:41.685510+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_ZpuB6R3tdZak", "dispatch_id": "AD_M4z7UnYivdNG", "room": "cg-voice-9r5jjmcz0n", "room_id": "RM_uBLCU6RTkyYD", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 117034, "timestamp": "2026-10-29T11:53:12.543121+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 117340, "timestamp": "2026-10-29T11:53:12.614337+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 117340, "elapsed_time": 0.11, "timestamp": "2026-10-29T11:53:12.727122+00:00"}
Nvidia Decoder is supported.
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 117117, "job_id": "AJ_ZpuB6R3tdZak", "room": "cg-voice-9r5jjmcz0n", "room_id": "RM_uBLCU6RTkyYD", "timestamp": "2026-10-29T11:53:12.950095+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/cybergril-hel910/current/src/cybergril/voice/worker.py\", line 203, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/cybergril-hel910/current/src/cybergril/voice/worker.py\", line 203, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 117117, "job_id": "AJ_ZpuB6R3tdZak", "room": "cg-voice-9r5jjmcz0n", "room_id": "RM_uBLCU6RTkyYD", "timestamp": "2026-10-29T11:53:12.950937+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=rain, agent_name=cybergril-voice-agent, asr_window=(window=hel910-20261029-a audio=0.000/720.000s confirmed=0.000s retries<= 2/call ledger=/opt/cybergril-hel910/asr-usage.jsonl))
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "parent process shutdown", "pid": 117117, "job_id": "AJ_ZpuB6R3tdZak", "room": "cg-voice-9r5jjmcz0n", "room_id": "RM_uBLCU6RTkyYD", "timestamp": "2026-10-29T11:53:24.378953+00:00"}
{"message": "error while shutting down the job\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 504, in _shutdown_job\n await asyncio.gather(*shutdown_tasks)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 493, in _traced_shutdown_callback\n await callback(shutdown_info.reason)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/job.py\", line 637, in wrapper\n await callback() # type: ignore\n ^^^^^^^^^^^^^^^^\nTypeError: object NoneType can't be used in 'await' expression", "level": "ERROR", "name": "livekit.agents", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 504, in _shutdown_job\n await asyncio.gather(*shutdown_tasks)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 493, in _traced_shutdown_callback\n await callback(shutdown_info.reason)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/job.py\", line 637, in wrapper\n await callback() # type: ignore\n ^^^^^^^^^^^^^^^^\nTypeError: object NoneType can't be used in 'await' expression", "pid": 117117, "job_id": "AJ_ZpuB6R3tdZak", "room": "cg-voice-9r5jjmcz0n", "room_id": "RM_uBLCU6RTkyYD", "timestamp": "2026-10-29T11:53:24.379160+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_6ER6CLfrDgjj", "dispatch_id": "AD_G4bRhnJp3HgV", "room": "cg-voice-fdk5izk2uz", "room_id": "RM_kbpdRUqex2LN", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 117034, "timestamp": "2026-10-29T11:58:22.089500+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 117672, "timestamp": "2026-10-29T11:58:22.176516+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 117672, "elapsed_time": 0.11, "timestamp": "2026-10-29T11:58:22.289729+00:00"}
Nvidia Decoder is supported.
{"message": "RoomIO linked to participant", "level": "INFO", "name": "livekit.agents", "lk.pii.participant_identity": "user-wnjxjdyz", "lk.pii.room_name": "cg-voice-fdk5izk2uz", "pid": 117121, "job_id": "AJ_6ER6CLfrDgjj", "room": "cg-voice-fdk5izk2uz", "room_id": "RM_kbpdRUqex2LN", "timestamp": "2026-10-29T11:58:22.502347+00:00"}
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 117121, "job_id": "AJ_6ER6CLfrDgjj", "room": "cg-voice-fdk5izk2uz", "room_id": "RM_kbpdRUqex2LN", "timestamp": "2026-10-29T11:58:22.503217+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/cybergril-hel910/current/src/cybergril/voice/worker.py\", line 203, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/cybergril-hel910/current/src/cybergril/voice/worker.py\", line 203, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 117121, "job_id": "AJ_6ER6CLfrDgjj", "room": "cg-voice-fdk5izk2uz", "room_id": "RM_kbpdRUqex2LN", "timestamp": "2026-10-29T11:58:22.503948+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=rain, agent_name=cybergril-voice-agent, asr_window=(window=hel910-20261029-a audio=0.000/720.000s confirmed=0.000s retries<= 2/call ledger=/opt/cybergril-hel910/asr-usage.jsonl))
[stt] utterance open, frame sr=24000
[stt] interim: 我就是
[stt->session] interim_transcript: 我就是
[stt] interim: 一个笨
[stt->session] interim_transcript: 一个笨
[stt] interim: 蛋派
[stt->session] interim_transcript: 蛋派
[stt] interim: 大星
[stt->session] interim_transcript: 大星
[stt] interim: 我们一起
[stt->session] interim_transcript: 我们一起
[stt] interim: 去抓水
[stt->session] interim_transcript: 去抓水
[stt] interim: 母
[stt->session] interim_transcript: 母
[stt] interim: 吧我们一
[stt->session] interim_transcript: 吧我们一
[stt] interim: 起度过快
[stt->session] interim_transcript: 起度过快
[stt] interim: 乐的一
[stt->session] interim_transcript: 乐的一
[stt] FINAL(merged): 我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天
[stt->session] final_transcript: 我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天
[tts] first audio after 2.147s (好呀,小十。带上小网兜,慢一点也没关系,...)
[stt] interim: 师
[stt->session] interim_transcript: 师
[stt] interim: 傅
[stt->session] interim_transcript: 傅
[stt] interim: 诶这取
[stt->session] interim_transcript: 诶这取
[stt] interim: 经
[stt->session] interim_transcript: 经
[stt] interim: 路比高
[stt->session] interim_transcript: 路比高
[stt] interim: 老庄
[stt->session] interim_transcript: 老庄
[stt] interim: 种地
[stt->session] interim_transcript: 种地
[stt] interim: 还
[stt->session] interim_transcript: 还
[stt] interim: 苦俺老
[stt->session] interim_transcript: 苦俺老
[stt] interim: 猪饿
[stt->session] interim_transcript: 猪饿
[stt] interim: 的能吞
[stt->session] interim_transcript: 的能吞
[stt] interim: 下石
[stt->session] interim_transcript: 下石
[stt] interim: 龙素
[stt->session] interim_transcript: 龙素
[stt] interim: 包
[stt->session] interim_transcript: 包
[stt] interim: 子猴
[stt->session] interim_transcript: 子猴
[stt] interim: 哥你
[stt->session] interim_transcript: 哥你
[stt] interim: 筋斗
[stt->session] interim_transcript: 筋斗
[stt] interim: 云
[stt->session] interim_transcript: 云
[stt] interim: 快要
[stt->session] interim_transcript: 快要
[stt] interim: 不
[stt->session] interim_transcript: 不
[stt] interim: 先去
[stt->session] interim_transcript: 先去
[stt] interim: 画个
[stt->session] interim_transcript: 画个
[stt] FINAL(merged): 师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去画个缘
[stt->session] final_transcript: 师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云
[tts] first audio after 2.514s (行啊,小十。去画缘之前,先把那石龙素包子...)
[stt] interim: 没
[stt->session] interim_transcript: 没
[stt] interim: 错我
[stt->session] interim_transcript: 错我
[stt] interim: 就是个笨
[stt->session] interim_transcript: 就是个笨
[stt] interim: 蛋
[stt->session] interim_transcript: 蛋
[stt] interim: 派大
[stt->session] interim_transcript: 派大
[stt] interim: 星我们一
[stt->session] interim_transcript: 星我们一
[stt] interim: 起去抓
[stt->session] interim_transcript: 起去抓
[stt] interim: 水母
[stt->session] interim_transcript: 水母
[stt] interim: 吧我们
[stt->session] interim_transcript: 吧我们
[stt] interim: 一起度过
[stt->session] interim_transcript: 一起度过
[stt] interim: 快乐的一
[stt->session] interim_transcript: 快乐的一
[stt] interim: 天
[stt->session] interim_transcript: 天
[stt] FINAL(merged): 没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天
[stt->session] final_transcript: 没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天
[stt] interim: 没
[stt->session] interim_transcript: 没
[stt] interim: 错我
[stt->session] interim_transcript: 错我
[stt] interim: 就是个笨
[stt->session] interim_transcript: 就是个笨
[stt] interim: 蛋
[stt->session] interim_transcript: 蛋
[stt] interim: 派大
[stt->session] interim_transcript: 派大
[stt] interim: 星我们一
[stt->session] interim_transcript: 星我们一
[stt] interim: 起去抓
[stt->session] interim_transcript: 起去抓
[stt] interim: 水母
[stt->session] interim_transcript: 水母
[stt] interim: 吧我们
[stt->session] interim_transcript: 吧我们
[stt] interim: 一起度过
[stt->session] interim_transcript: 一起度过
[stt] interim: 快乐的一
[stt->session] interim_transcript: 快乐的一
[stt] FINAL(merged): 没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天
[stt->session] final_transcript: 没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天
[tts] first audio after 2.008s (好,小十,那就一起。...)
{"message": "closing agent session due to participant disconnect (disable via `RoomInputOptions.close_on_disconnect=False`)", "level": "INFO", "name": "livekit.agents", "room": "cg-voice-fdk5izk2uz", "participant": "user-wnjxjdyz", "reason": "CLIENT_INITIATED", "pid": 117121, "job_id": "AJ_6ER6CLfrDgjj", "room_id": "RM_kbpdRUqex2LN", "timestamp": "2026-10-29T12:02:44.305190+00:00"}
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "room disconnected", "pid": 117121, "job_id": "AJ_6ER6CLfrDgjj", "room": "cg-voice-fdk5izk2uz", "room_id": "RM_kbpdRUqex2LN", "timestamp": "2026-10-29T12:02:46.322112+00:00"}
{"message": "error while shutting down the job\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 504, in _shutdown_job\n await asyncio.gather(*shutdown_tasks)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 493, in _traced_shutdown_callback\n await callback(shutdown_info.reason)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/job.py\", line 637, in wrapper\n await callback() # type: ignore\n ^^^^^^^^^^^^^^^^\nTypeError: object NoneType can't be used in 'await' expression", "level": "ERROR", "name": "livekit.agents", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 504, in _shutdown_job\n await asyncio.gather(*shutdown_tasks)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 493, in _traced_shutdown_callback\n await callback(shutdown_info.reason)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/job.py\", line 637, in wrapper\n await callback() # type: ignore\n ^^^^^^^^^^^^^^^^\nTypeError: object NoneType can't be used in 'await' expression", "pid": 117121, "job_id": "AJ_6ER6CLfrDgjj", "room": "cg-voice-fdk5izk2uz", "room_id": "RM_kbpdRUqex2LN", "timestamp": "2026-10-29T12:02:46.322288+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_5Uk4XbETMfVr", "dispatch_id": "AD_i9twJRAXhgHd", "room": "cg-voice-jq8q4zm9h0", "room_id": "RM_XARewyWHLk4R", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 117034, "timestamp": "2026-10-29T12:10:13.069565+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 118637, "timestamp": "2026-10-29T12:10:13.151002+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 118637, "elapsed_time": 0.13, "timestamp": "2026-10-29T12:10:13.284159+00:00"}
Nvidia Decoder is supported.
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 117119, "job_id": "AJ_5Uk4XbETMfVr", "room": "cg-voice-jq8q4zm9h0", "room_id": "RM_XARewyWHLk4R", "timestamp": "2026-10-29T12:10:13.485573+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/cybergril-hel910/current/src/cybergril/voice/worker.py\", line 203, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/cybergril-hel910/current/src/cybergril/voice/worker.py\", line 203, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 117119, "job_id": "AJ_5Uk4XbETMfVr", "room": "cg-voice-jq8q4zm9h0", "room_id": "RM_XARewyWHLk4R", "timestamp": "2026-10-29T12:10:13.487053+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=rain, agent_name=cybergril-voice-agent, asr_window=(window=hel910-20261029-a audio=264.243/720.000s confirmed=264.243s retries<= 2/call ledger=/opt/cybergril-hel910/asr-usage.jsonl))
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_kS9dDvnAMCtu", "dispatch_id": "AD_rpzro2wKCVWM", "room": "cg-voice-dlas9zmiox", "room_id": "RM_yzXp5MvjnLxV", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 117034, "timestamp": "2026-10-29T12:12:36.795242+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 119984, "timestamp": "2026-10-29T12:12:36.837621+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 119984, "elapsed_time": 0.11, "timestamp": "2026-10-29T12:12:36.952751+00:00"}
Nvidia Decoder is supported.
{"message": "RoomIO linked to participant", "level": "INFO", "name": "livekit.agents", "lk.pii.participant_identity": "user-joobnisb", "lk.pii.room_name": "cg-voice-dlas9zmiox", "pid": 117123, "job_id": "AJ_kS9dDvnAMCtu", "room": "cg-voice-dlas9zmiox", "room_id": "RM_yzXp5MvjnLxV", "timestamp": "2026-10-29T12:12:37.185284+00:00"}
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 117123, "job_id": "AJ_kS9dDvnAMCtu", "room": "cg-voice-dlas9zmiox", "room_id": "RM_yzXp5MvjnLxV", "timestamp": "2026-10-29T12:12:37.186027+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/cybergril-hel910/current/src/cybergril/voice/worker.py\", line 203, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/cybergril-hel910/current/src/cybergril/voice/worker.py\", line 203, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 117123, "job_id": "AJ_kS9dDvnAMCtu", "room": "cg-voice-dlas9zmiox", "room_id": "RM_yzXp5MvjnLxV", "timestamp": "2026-10-29T12:12:37.186855+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=rain, agent_name=cybergril-voice-agent, asr_window=(window=hel910-20261029-a audio=264.243/720.000s confirmed=264.243s retries<= 2/call ledger=/opt/cybergril-hel910/asr-usage.jsonl))
[stt] utterance open, frame sr=24000
[stt] interim: 师
[stt->session] interim_transcript: 师
[stt] interim: 傅
[stt->session] interim_transcript: 傅
[stt] interim: 诶这
[stt->session] interim_transcript: 诶这
[stt] interim: 取经
[stt->session] interim_transcript: 取经
[stt] interim: 路比
[stt->session] interim_transcript: 路比
[stt] interim: 高老
[stt->session] interim_transcript: 高老
[stt] interim: 庄种地
[stt->session] interim_transcript: 庄种地
[stt] interim: 还
[stt->session] interim_transcript: 还
[stt] interim: 苦俺
[stt->session] interim_transcript: 苦俺
[stt] interim: 老中
[stt->session] interim_transcript: 老中
[stt] interim: 饿的能
[stt->session] interim_transcript: 饿的能
[stt] interim: 吞下
[stt->session] interim_transcript: 吞下
[stt] interim: 石龙
[stt->session] interim_transcript: 石龙
[stt] interim: 素包
[stt->session] interim_transcript: 素包
[stt] interim: 子
[stt->session] interim_transcript: 子
[stt] interim: 猴哥
[stt->session] interim_transcript: 猴哥
[stt] interim: 你筋
[stt->session] interim_transcript: 你筋
[stt] interim: 斗云
[stt->session] interim_transcript: 斗云
[stt] interim: 块
[stt->session] interim_transcript: 块
[stt] interim: 要不
[stt->session] interim_transcript: 要不
[stt] interim: 先
[stt->session] interim_transcript: 先
[stt] interim: 去画个
[stt->session] interim_transcript: 去画个
[stt] FINAL(merged): 师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去化个缘
[stt->session] final_transcript: 师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云
[tts] first audio after 1.614s (好,小十。去化缘...)
[stt] interim: 盯着你看
[stt->session] interim_transcript: 盯着你看
[stt] interim: 怎么啦
[stt->session] interim_transcript: 怎么啦
[stt] FINAL(merged): 盯着,你看怎么啦
[stt->session] final_transcript: 盯着,你看怎么啦
[stt] interim: 难得
[stt->session] interim_transcript: 难得
[stt] interim: 看你这么
[stt->session] interim_transcript: 看你这么
[stt] interim: 有意思的
[stt->session] interim_transcript: 有意思的
[stt] interim: 样子
[stt->session] interim_transcript: 样子
[stt] FINAL(merged): 难得看你这么有意思的样子吗
[stt->session] final_transcript: 难得看你这么有意思的样子吗
{"message": "transcript arrives after turn has been committed. consider raising `min_delay` in the endpointing options to accommodate a slow stt. subsequent occurrences will log at debug level.", "level": "WARNING", "name": "livekit.agents", "pid": 117123, "job_id": "AJ_kS9dDvnAMCtu", "room": "cg-voice-dlas9zmiox", "room_id": "RM_yzXp5MvjnLxV", "timestamp": "2026-10-29T12:13:08.918069+00:00"}
[tts] first audio after 0.957s (我在...)
[stt] interim: 师
[stt->session] interim_transcript: 师
[stt] interim: 傅诶
[stt->session] interim_transcript: 傅诶
[stt] interim: 这取
[stt->session] interim_transcript: 这取
[stt] interim: 经路
[stt->session] interim_transcript: 经路
[stt] interim: 比高
[stt->session] interim_transcript: 比高
[stt] interim: 老庄
[stt->session] interim_transcript: 老庄
[stt] interim: 种地
[stt->session] interim_transcript: 种地
[stt] interim: 还
[stt->session] interim_transcript: 还
[stt] interim: 苦俺
[stt->session] interim_transcript: 苦俺
[stt] interim: 老猪饿
[stt->session] interim_transcript: 老猪饿
[stt] interim: 的能
[stt->session] interim_transcript: 的能
[stt] interim: 吞下石
[stt->session] interim_transcript: 吞下石
[stt] interim: 龙素
[stt->session] interim_transcript: 龙素
[stt] interim: 包
[stt->session] interim_transcript: 包
[stt] interim: 子猴
[stt->session] interim_transcript: 子猴
[stt] interim: 哥
[stt->session] interim_transcript: 哥
[stt] interim: 你筋斗
[stt->session] interim_transcript: 你筋斗
[stt] interim: 云
[stt->session] interim_transcript: 云
[stt] interim: 快要
[stt->session] interim_transcript: 快要
[stt] interim: 不
[stt->session] interim_transcript: 不
[stt] interim: 先去
[stt->session] interim_transcript: 先去
[stt] interim: 画个
[stt->session] interim_transcript: 画个
[stt] FINAL(merged): 师傅,哎,这取经路比高老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去化个缘
[stt->session] final_transcript: 师傅,哎,这取经路比高老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块
[tts] first audio after 2.419s (她这会儿没接上话,稍后再试试。你写的话还...)
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_reliable' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 117123, "job_id": "AJ_kS9dDvnAMCtu", "room": "cg-voice-dlas9zmiox", "room_id": "RM_yzXp5MvjnLxV", "timestamp": "2026-10-29T12:14:01.641609+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_lossy' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 117123, "job_id": "AJ_kS9dDvnAMCtu", "room": "cg-voice-dlas9zmiox", "room_id": "RM_yzXp5MvjnLxV", "timestamp": "2026-10-29T12:14:01.642771+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_data_track' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 117123, "job_id": "AJ_kS9dDvnAMCtu", "room": "cg-voice-dlas9zmiox", "room_id": "RM_yzXp5MvjnLxV", "timestamp": "2026-10-29T12:14:01.644140+00:00"}
{"message": "closing agent session due to participant disconnect (disable via `RoomInputOptions.close_on_disconnect=False`)", "level": "INFO", "name": "livekit.agents", "room": "cg-voice-dlas9zmiox", "participant": "user-joobnisb", "reason": "ROOM_DELETED", "pid": 117123, "job_id": "AJ_kS9dDvnAMCtu", "room_id": "RM_yzXp5MvjnLxV", "timestamp": "2026-10-29T12:14:01.658858+00:00"}
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "parent process shutdown", "pid": 117123, "job_id": "AJ_kS9dDvnAMCtu", "room": "cg-voice-dlas9zmiox", "room_id": "RM_yzXp5MvjnLxV", "timestamp": "2026-10-29T12:14:03.649239+00:00"}
{"message": "error while shutting down the job\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 504, in _shutdown_job\n await asyncio.gather(*shutdown_tasks)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 493, in _traced_shutdown_callback\n await callback(shutdown_info.reason)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/job.py\", line 637, in wrapper\n await callback() # type: ignore\n ^^^^^^^^^^^^^^^^\nTypeError: object NoneType can't be used in 'await' expression", "level": "ERROR", "name": "livekit.agents", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 504, in _shutdown_job\n await asyncio.gather(*shutdown_tasks)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 493, in _traced_shutdown_callback\n await callback(shutdown_info.reason)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/job.py\", line 637, in wrapper\n await callback() # type: ignore\n ^^^^^^^^^^^^^^^^\nTypeError: object NoneType can't be used in 'await' expression", "pid": 117123, "job_id": "AJ_kS9dDvnAMCtu", "room": "cg-voice-dlas9zmiox", "room_id": "RM_yzXp5MvjnLxV", "timestamp": "2026-10-29T12:14:03.649526+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_reliable' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 117119, "job_id": "AJ_5Uk4XbETMfVr", "room": "cg-voice-jq8q4zm9h0", "room_id": "RM_XARewyWHLk4R", "timestamp": "2026-10-29T12:15:12.157732+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_lossy' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 117119, "job_id": "AJ_5Uk4XbETMfVr", "room": "cg-voice-jq8q4zm9h0", "room_id": "RM_XARewyWHLk4R", "timestamp": "2026-10-29T12:15:12.158251+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_data_track' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 117119, "job_id": "AJ_5Uk4XbETMfVr", "room": "cg-voice-jq8q4zm9h0", "room_id": "RM_XARewyWHLk4R", "timestamp": "2026-10-29T12:15:12.158737+00:00"}
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "parent process shutdown", "pid": 117119, "job_id": "AJ_5Uk4XbETMfVr", "room": "cg-voice-jq8q4zm9h0", "room_id": "RM_XARewyWHLk4R", "timestamp": "2026-10-29T12:15:14.162493+00:00"}
{"message": "error while shutting down the job\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 504, in _shutdown_job\n await asyncio.gather(*shutdown_tasks)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 493, in _traced_shutdown_callback\n await callback(shutdown_info.reason)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/job.py\", line 637, in wrapper\n await callback() # type: ignore\n ^^^^^^^^^^^^^^^^\nTypeError: object NoneType can't be used in 'await' expression", "level": "ERROR", "name": "livekit.agents", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 504, in _shutdown_job\n await asyncio.gather(*shutdown_tasks)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/ipc/job_proc_lazy_main.py\", line 493, in _traced_shutdown_callback\n await callback(shutdown_info.reason)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/job.py\", line 637, in wrapper\n await callback() # type: ignore\n ^^^^^^^^^^^^^^^^\nTypeError: object NoneType can't be used in 'await' expression", "pid": 117119, "job_id": "AJ_5Uk4XbETMfVr", "room": "cg-voice-jq8q4zm9h0", "room_id": "RM_XARewyWHLk4R", "timestamp": "2026-10-29T12:15:14.162511+00:00"}
{"message": "draining worker", "level": "INFO", "name": "livekit.agents", "id": "AW_EmnniB7z9YMD", "timeout": 3600, "pid": 117034, "timestamp": "2026-10-29T12:16:00.200546+00:00"}
{"message": "shutting down worker", "level": "INFO", "name": "livekit.agents", "id": "AW_EmnniB7z9YMD", "pid": 117034, "timestamp": "2026-10-29T12:16:00.200808+00:00"}