fix(dashboard): stop infinite mock loop; add demo-mode warning banner

--dashboard defaulted to looping mock STT forever, so the status page
piled up thousands of fake "conversations" (all mock, 0ms, same reply)
that looked like real traffic. Now it plays 3 sample utterances then
idles; a loud 데모 모드 banner states the turns are mock samples, not
real STT/Brain/TTS. Continuous demo moved behind --dashboard-loop-demo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-08-16 23:06:30 +09:00
parent 6b0755e1ff
commit 9bac6d170a
3 changed files with 51 additions and 9 deletions

View File

@@ -2,12 +2,13 @@
python -m wsai # mock pipeline (no deps, no keys) — runs a demo
python -m wsai --voice # eyes-free voice loop demo (STT -> Brain -> TTS)
python -m wsai --dashboard # live status website + a continuous voice demo
python -m wsai --dashboard # live status website + a short voice demo, then idle
python -m wsai --live # capture this screen + Claude eyes/brain
python -m wsai --env # build from WSAI_* environment variables
The mock run is bounded (a few frames + a scripted conversation) so it exits on
its own; --dashboard/--live/--env run until Ctrl-C.
its own; --dashboard keeps the site open after a short demo; --live/--env run
until Ctrl-C.
"""
from __future__ import annotations
@@ -34,7 +35,14 @@ def _lan_ip() -> str:
return "127.0.0.1"
async def _run(settings: Settings, demo: bool, monitor: Monitor | None) -> None:
async def _run(
settings: Settings,
demo: bool,
monitor: Monitor | None,
*,
demo_loop: bool = False,
keep_dashboard_open: bool = False,
) -> None:
if demo:
# Bounded demo so CI / a quick check terminates.
from .backends.mock import MockFrameSource, MockSTT
@@ -43,11 +51,16 @@ async def _run(settings: Settings, demo: bool, monitor: Monitor | None) -> None:
if pipe.source is not None: # keep eyes-free configs eyes-free
pipe.source = MockFrameSource(interval=0.3, limit=4)
if pipe.stt is not None:
# When serving the dashboard, keep talking forever so there's always
# something live to watch; otherwise stay bounded so the demo exits.
loop = monitor is not None
pipe.stt = MockSTT(interval=2.0 if loop else 0.4, loop=loop)
pipe.stt = MockSTT(interval=2.0 if demo_loop else 0.4, loop=demo_loop)
await pipe.run()
if keep_dashboard_open:
# Keep the status page alive without generating fake conversations
# forever. The previous default looped mock STT indefinitely, which
# made the dashboard look like it had heard thousands of real users.
if monitor is not None:
monitor.set_status(running=True, listening=False)
monitor.log("info", "mock 데모 완료 — 실제 음성 파이프라인 연결 대기")
await asyncio.Event().wait()
return
await build(settings, monitor=monitor).run()
@@ -55,7 +68,9 @@ async def _run(settings: Settings, demo: bool, monitor: Monitor | None) -> None:
def main() -> None:
ap = argparse.ArgumentParser(prog="wsai")
ap.add_argument("--voice", action="store_true", help="eyes-free voice loop (STT -> Brain -> TTS)")
ap.add_argument("--dashboard", action="store_true", help="serve the live status website (voice demo loops)")
ap.add_argument("--dashboard", action="store_true", help="serve the live status website")
ap.add_argument("--dashboard-loop-demo", action="store_true",
help="keep generating mock demo utterances forever (off by default)")
ap.add_argument("--live", action="store_true", help="capture screen + Claude backends")
ap.add_argument("--env", action="store_true", help="build from WSAI_* env vars")
ap.add_argument("--port", type=int, default=int(os.environ.get("WSAI_DASHBOARD_PORT", "8787")),
@@ -96,7 +111,15 @@ def main() -> None:
print(f" (로컬: http://127.0.0.1:{args.port} )\n")
try:
asyncio.run(_run(settings, demo, monitor))
asyncio.run(
_run(
settings,
demo,
monitor,
demo_loop=args.dashboard and args.dashboard_loop_demo,
keep_dashboard_open=args.dashboard and demo and not args.dashboard_loop_demo,
)
)
except KeyboardInterrupt:
pass
finally:

View File

@@ -179,6 +179,9 @@ PAGE = r"""<!DOCTYPE html>
.ev.error{color:var(--err)}
.ev .et{flex:0 0 68px;color:#5f7488}
code{background:#0c1219;padding:1px 5px;border-radius:5px;border:1px solid var(--line)}
.demobar{background:#2a2210;border:1px solid #6b5417;color:var(--warn);border-radius:12px;
padding:11px 15px;margin:0 0 16px;font-size:13px;line-height:1.55}
.demobar b{color:#ffe08a}
</style>
</head>
<body>
@@ -196,6 +199,7 @@ PAGE = r"""<!DOCTYPE html>
</div>
</header>
<main>
<div class="demobar" id="demobar" style="display:none"></div>
<div class="comp" id="comp"></div>
<div id="turns"></div>
<div id="empty" class="empty">아직 대화가 없습니다. 사용자가 말하면 여기에 단계별로 나타납니다.</div>
@@ -232,6 +236,18 @@ function renderStatus(s){
$('dot').className = 'dot ' + (listening ? 'live' : 'off');
$('listen').textContent = listening ? '듣는 중' : (s.running ? '실행 중 (대기)' : '중지됨');
const comps = s.components || {};
// Demo banner: if the ears/brain/mouth are still mock, everything below is
// replayed sample data, not a real conversation. Say so loudly.
const mockParts = ['stt','brain','tts'].filter(k => comps[k]==='mock');
const bar = $('demobar');
if(mockParts.length){
bar.style.display='block';
bar.innerHTML = '⚠ <b>데모 모드</b> — 실제 음성/STT/두뇌/TTS가 아직 연결되지 않아, 아래 대화는 '
+ '실제로 들은 내용이 아니라 <b>목(mock) 예시 스크립트</b>입니다. '
+ '실제 엔진(faster-whisper·Claude·MeloTTS)을 붙이면 이 자리에 진짜 발화·지연·오류가 표시됩니다.';
} else {
bar.style.display='none';
}
const el = $('comp'); el.innerHTML = '';
const names = {source:'눈(소스)', vision:'시각', stt:'귀(STT)', brain:'두뇌', tts:'입(TTS)', text:'텍스트'};
for(const k of Object.keys(names)){