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