feat(voice): bracketed emotion tags, [잡음] for noise, spoken emotion, concise replies

- Brain persona now prefixes every reply with one bracketed emotion tag
  (e.g. [반가움], [궁금]) and keeps replies to one or two short sentences.
- Empty/unrecognised audio (silence/noise) is reported as reply "[잡음]" with
  no TTS playback instead of an empty reply.
- TTS speaks the bracketed emotion too: "[힘차게] 안녕!" is synthesised as
  "힘차게, 안녕!" via a leading-tag -> spoken-word transform.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-08-22 01:44:25 +09:00
parent 2c4c9ad82c
commit f585ed7b76
2 changed files with 25 additions and 4 deletions

View File

@@ -127,6 +127,9 @@ class ClaudeBrain:
"화면 설명을 참고해 자연스러운 반말/존댓말은 사용자에 맞추고, 짧고 대화하듯 답해. "
"화면을 못 봤으면 솔직히 말해. "
"네 답변은 음성으로 읽히니 마크다운·코드블록·백틱 같은 서식 없이 평범한 말로만 답해. "
"답변은 반드시 대괄호 감정 표시 한 개로 시작해. 예: [힘차게], [궁금], [반가움], [차분하게], [웃으며]. "
"감정 태그는 답변 맨 앞에 딱 한 번만 붙이고, 그 뒤에 실제 답을 이어써. "
"답변은 최대한 짧고 간결하게, 한두 문장 이내로 해."
)
def __init__(self, *, model: str = "claude-sonnet-4-5", api_key: str | None = None) -> None:

View File

@@ -18,6 +18,7 @@ from __future__ import annotations
import json
import logging
import queue
import re
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@@ -25,6 +26,21 @@ from .monitor import Monitor
log = logging.getLogger("wsai.dashboard")
_EMOTION_RE = re.compile(r"^\s*\[([^\]]+)\]\s*(.*)", re.S)
def _speech_text(reply: str) -> str:
"""Turn a reply like ``[힘차게] 안녕!`` into what the TTS should actually say.
The bracketed emotion is spoken too (per user request), so the leading tag
becomes a natural spoken word: ``[힘차게] 안녕!`` -> ``힘차게, 안녕!``. Replies
without a tag are spoken as-is."""
m = _EMOTION_RE.match(reply or "")
if not m:
return reply
emotion, rest = m.group(1).strip(), m.group(2).strip()
return f"{emotion}, {rest}" if rest else emotion
def _make_handler(dash: "Dashboard"):
monitor = dash.monitor
@@ -259,12 +275,14 @@ class Dashboard:
heard = (self._submit(self.stt.transcribe(wav)) or "").strip()
turn.heard(heard or "(빈 결과)")
if not heard:
# Nothing recognised (silence/noise): skip the turn, tell the bot.
# Nothing recognised (silence/noise): mark it as [잡음] and skip
# the brain/TTS so the bot plays nothing back.
turn.replied("[잡음]")
turn.finish()
return {"heard": heard, "reply": "", "wav": b""}
return {"heard": heard, "reply": "[잡음]", "wav": b""}
reply_text = self._think(heard)
turn.replied(reply_text)
out_path = self._submit(self.tts.synth(reply_text))
out_path = self._submit(self.tts.synth(_speech_text(reply_text)))
with open(out_path, "rb") as f:
reply_wav = f.read()
ms = int((time.monotonic() - t0) * 1000)