Add voice state tags, SuperTTS configs, and demo WS log groups.
Parse leading <state> tags from LLM replies and emit response.state over the product websocket while stripping tags from TTS/text streams. Add FastGPT+Xfyun voice configs (including state-enabled preset), SuperTTS support, and context sync for interrupted turns. Refresh the voice demo with a state indicator and collapsible audio delta websocket log groups. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -26,6 +26,9 @@ def resolve_voice_config_path() -> Path:
|
||||
|
||||
DEFAULT_VOICE_CONFIG = resolve_voice_config_path()
|
||||
|
||||
SUPPORTED_LLM_PROVIDERS = frozenset({"openai", "fastgpt"})
|
||||
_LLM_PROVIDER_ALIASES = {"llm": "openai", "openai": "openai", "fastgpt": "fastgpt"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ServerConfig:
|
||||
@@ -93,11 +96,20 @@ class TurnConfig:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResponseStateConfig:
|
||||
enabled: bool = False
|
||||
tag: str = "state"
|
||||
event_type: str = "response.state"
|
||||
max_prefix_chars: int = 256
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentConfig:
|
||||
system_prompt: str = "You are a helpful, friendly voice assistant."
|
||||
greeting: str | None = None
|
||||
greeting_mode: str = "generated"
|
||||
response_state: ResponseStateConfig = field(default_factory=ResponseStateConfig)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -106,6 +118,7 @@ class LLMConfig:
|
||||
api_key: str = ""
|
||||
base_url: str | None = None
|
||||
model: str = "gpt-4o-mini"
|
||||
app_id: str | None = None
|
||||
temperature: float | None = 0.7
|
||||
chat_id: str | None = None
|
||||
variables: dict[str, str] = field(default_factory=dict)
|
||||
@@ -113,6 +126,19 @@ class LLMConfig:
|
||||
timeout_sec: float = 60.0
|
||||
send_system_prompt: bool = False
|
||||
|
||||
@property
|
||||
def is_fastgpt(self) -> bool:
|
||||
return self.provider == "fastgpt"
|
||||
|
||||
@property
|
||||
def is_openai(self) -> bool:
|
||||
return self.provider == "openai"
|
||||
|
||||
@property
|
||||
def uses_local_context_history(self) -> bool:
|
||||
"""Whether the pipeline should seed and maintain local LLM context history."""
|
||||
return not self.is_fastgpt or self.send_system_prompt
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class STTConfig:
|
||||
@@ -147,6 +173,8 @@ class TTSConfig:
|
||||
pitch: int = 50
|
||||
timeout_sec: float = 30.0
|
||||
source_sample_rate_hz: int | None = None
|
||||
oral_level: str = "mid"
|
||||
text_aggregation_mode: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -183,14 +211,24 @@ def config_from_dict(data: dict) -> EngineConfig:
|
||||
agent["greeting"] = None
|
||||
if agent.get("greeting_mode") not in (None, "generated", "fixed", "off"):
|
||||
raise ValueError("agent.greeting_mode must be one of: generated, fixed, off")
|
||||
response_state = ResponseStateConfig(**_dict(agent.pop("response_state")))
|
||||
if response_state.max_prefix_chars < 1:
|
||||
raise ValueError("agent.response_state.max_prefix_chars must be greater than 0")
|
||||
if not response_state.tag:
|
||||
raise ValueError("agent.response_state.tag must not be empty")
|
||||
if not response_state.event_type:
|
||||
raise ValueError("agent.response_state.event_type must not be empty")
|
||||
|
||||
stt = _dict(services.get("stt") or services.get("asr"))
|
||||
if stt.get("language") == "":
|
||||
stt["language"] = None
|
||||
|
||||
llm = _dict(services.get("llm"))
|
||||
llm["provider"] = _normalize_llm_provider(llm.get("provider", LLMConfig().provider))
|
||||
if llm.get("chat_id") == "":
|
||||
llm["chat_id"] = None
|
||||
if llm.get("app_id") == "":
|
||||
llm["app_id"] = None
|
||||
if not isinstance(llm.get("variables"), dict):
|
||||
llm["variables"] = {}
|
||||
|
||||
@@ -219,7 +257,7 @@ def config_from_dict(data: dict) -> EngineConfig:
|
||||
)
|
||||
),
|
||||
),
|
||||
agent=AgentConfig(**agent),
|
||||
agent=AgentConfig(**agent, response_state=response_state),
|
||||
services=ServicesConfig(
|
||||
llm=LLMConfig(**llm),
|
||||
stt=STTConfig(**stt),
|
||||
@@ -230,3 +268,14 @@ def config_from_dict(data: dict) -> EngineConfig:
|
||||
|
||||
def _dict(value: object) -> dict:
|
||||
return dict(value) if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def _normalize_llm_provider(value: object) -> str:
|
||||
provider = str(value or LLMConfig().provider).strip().lower()
|
||||
normalized = _LLM_PROVIDER_ALIASES.get(provider)
|
||||
if normalized is None:
|
||||
supported = ", ".join(sorted(SUPPORTED_LLM_PROVIDERS | {"llm"}))
|
||||
raise ValueError(
|
||||
f"services.llm.provider must be one of: {supported}; got {value!r}"
|
||||
)
|
||||
return normalized
|
||||
|
||||
Reference in New Issue
Block a user