|
|
@@ -37,12 +37,13 @@ from audio_processor import AudioBuffer
|
|
|
from qwen_engine import QwenASREngine
|
|
|
|
|
|
try:
|
|
|
- from voiceprint import get_store
|
|
|
+ from voiceprint import get_store, save_conversation, load_conversation_context
|
|
|
_VP_ENABLED = True
|
|
|
except ImportError:
|
|
|
_VP_ENABLED = False
|
|
|
- def get_store():
|
|
|
- return None
|
|
|
+ def get_store(): return None
|
|
|
+ def save_conversation(*a): pass
|
|
|
+ def load_conversation_context(*a): return ""
|
|
|
|
|
|
logger = logging.getLogger("worker")
|
|
|
|
|
|
@@ -65,7 +66,7 @@ LLM_TEMPERATURE = 0.7
|
|
|
LLM_TIMEOUT = 25
|
|
|
|
|
|
SYSTEM_PROMPT = (
|
|
|
- "你是友好的中文语音助手,名字叫小智。"
|
|
|
+ "你是狄诺尼试验员,一名专业的工程试验助手。"
|
|
|
"回答简洁自然,2-3句即可,用口语化中文。"
|
|
|
"不要使用 Markdown、代码块、表格或特殊符号。"
|
|
|
"不要输出括号注释、不要使用英文缩写。"
|
|
|
@@ -97,21 +98,25 @@ def _token(room, ident):
|
|
|
# LLM (streaming, <think> filtering)
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
-async def _llm_stream(prompt: str, hist: list[dict], speaker: str | None = None):
|
|
|
+async def _llm_stream(prompt: str, hist: list[dict], speaker: str | None = None, speaker_id: str | None = None):
|
|
|
"""Stream LLM tokens, dispatching based on LLM_PROVIDER env."""
|
|
|
if LLM_PROVIDER == "mimo":
|
|
|
- async for x in _llm_stream_mimo(prompt, hist, speaker):
|
|
|
+ async for x in _llm_stream_mimo(prompt, hist, speaker, speaker_id):
|
|
|
yield x
|
|
|
return
|
|
|
- async for x in _llm_stream_vllm(prompt, hist, speaker):
|
|
|
+ async for x in _llm_stream_vllm(prompt, hist, speaker, speaker_id):
|
|
|
yield x
|
|
|
|
|
|
|
|
|
-async def _llm_stream_vllm(prompt: str, hist: list[dict], speaker: str | None = None):
|
|
|
+async def _llm_stream_vllm(prompt: str, hist: list[dict], speaker: str | None = None, speaker_id: str | None = None):
|
|
|
"""vLLM streaming via OpenAI-compatible SSE."""
|
|
|
msgs = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
|
if speaker:
|
|
|
msgs.append({"role": "system", "content": f"[内部上下文] 当前说话人: {speaker}。请根据对方身份自然地调整回应风格,但不要在回复中主动提及说话人的名字。"})
|
|
|
+ # Conversation memory context
|
|
|
+ mem_ctx = load_conversation_context(speaker_id, speaker) if speaker_id else ""
|
|
|
+ if mem_ctx:
|
|
|
+ msgs.append({"role": "system", "content": mem_ctx})
|
|
|
msgs.extend(hist)
|
|
|
msgs.append({"role": "user", "content": prompt})
|
|
|
|
|
|
@@ -147,7 +152,7 @@ async def _llm_stream_vllm(prompt: str, hist: list[dict], speaker: str | None =
|
|
|
yield "", True
|
|
|
|
|
|
|
|
|
-async def _llm_stream_mimo(prompt: str, hist: list[dict], speaker: str | None = None):
|
|
|
+async def _llm_stream_mimo(prompt: str, hist: list[dict], speaker: str | None = None, speaker_id: str | None = None):
|
|
|
"""Mimo v2.5 LLM streaming via OpenAI-compatible SSE."""
|
|
|
if not MIMO_KEY:
|
|
|
yield "", True
|
|
|
@@ -155,6 +160,9 @@ async def _llm_stream_mimo(prompt: str, hist: list[dict], speaker: str | None =
|
|
|
msgs = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
|
if speaker:
|
|
|
msgs.append({"role": "system", "content": f"[内部上下文] 当前说话人: {speaker}。请根据对方身份自然地调整回应风格,但不要在回复中主动提及说话人的名字。"})
|
|
|
+ mem_ctx = load_conversation_context(speaker_id, speaker) if speaker_id else ""
|
|
|
+ if mem_ctx:
|
|
|
+ msgs.append({"role": "system", "content": mem_ctx})
|
|
|
msgs.extend(hist)
|
|
|
msgs.append({"role": "user", "content": prompt})
|
|
|
|
|
|
@@ -631,21 +639,21 @@ class Worker:
|
|
|
|
|
|
# ── Voiceprint: identify or auto-register ──
|
|
|
speaker_name = None
|
|
|
+ speaker_id = None
|
|
|
if _VP_ENABLED:
|
|
|
store = get_store()
|
|
|
if store and store.enabled:
|
|
|
loop = asyncio.get_running_loop()
|
|
|
- _id, speaker_name, _sim = await loop.run_in_executor(None, store.identify, all_audio)
|
|
|
+ speaker_id, speaker_name, _sim = await loop.run_in_executor(None, store.identify, all_audio)
|
|
|
if speaker_name:
|
|
|
logger.info("VP: identified %s (sim=%.3f)", speaker_name, _sim)
|
|
|
await self._send({"type": "speaker", "name": speaker_name, "confidence": round(_sim, 3)})
|
|
|
else:
|
|
|
- # Auto-register: first-time speaker
|
|
|
auto_id = f"user_{int(time.time() * 1000) % 1000000:06d}"
|
|
|
auto_name = f"用户{auto_id[-3:]}"
|
|
|
ok = await loop.run_in_executor(None, store.register, auto_id, auto_name, all_audio)
|
|
|
if ok:
|
|
|
- speaker_name = auto_name
|
|
|
+ speaker_id, speaker_name = auto_id, auto_name
|
|
|
logger.info("VP: auto-registered %s", auto_name)
|
|
|
await self._send({"type": "vp_registered", "name": auto_name, "user_id": auto_id})
|
|
|
|
|
|
@@ -655,7 +663,7 @@ class Worker:
|
|
|
tts_segments: list[str] = []
|
|
|
|
|
|
try:
|
|
|
- async for delta, is_final in _llm_stream(txt, self.hist, speaker_name):
|
|
|
+ async for delta, is_final in _llm_stream(txt, self.hist, speaker_name, speaker_id):
|
|
|
if delta:
|
|
|
reply_full += delta
|
|
|
await self._send({"type": "reply_partial", "text": reply_full, "seq": 0})
|
|
|
@@ -693,6 +701,10 @@ class Worker:
|
|
|
{"role": "assistant", "content": reply_full},
|
|
|
])
|
|
|
self.hist[:] = self.hist[-20:]
|
|
|
+ # Save conversation to persistent memory
|
|
|
+ if speaker_id and _VP_ENABLED:
|
|
|
+ loop = asyncio.get_running_loop()
|
|
|
+ await loop.run_in_executor(None, save_conversation, speaker_id, speaker_name or "unknown", txt, reply_full)
|
|
|
|
|
|
async def _send(self, msg: dict):
|
|
|
try:
|