[项目边界]:完成全双工Agent入口和配置冻结,包含run-agent-live入口、配置校验和迁移文档
This commit is contained in:
@@ -38,6 +38,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||
subparsers.add_parser("device-check", help="Validate local microphone and speaker availability")
|
||||
live = subparsers.add_parser("run-live", help="Run real repeated live voice conversation")
|
||||
live.add_argument("--once", action="store_true", help="Run one completed live turn and exit")
|
||||
agent_live = subparsers.add_parser(
|
||||
"run-agent-live",
|
||||
help="Run planned full-duplex Agent voice assistant entry point",
|
||||
)
|
||||
agent_live.add_argument("--check-config", action="store_true", help="Validate and print full-duplex Agent config")
|
||||
simulate = subparsers.add_parser("simulate-live", help="Run live pipeline with simulated microphone frames")
|
||||
simulate.add_argument("--turns", type=int, default=2, help="Number of simulated turns. Default: 2")
|
||||
simulate.add_argument("--fixture", default=None, help="Replay simulated microphone frames from JSONL")
|
||||
@@ -58,6 +63,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"assistant_mode": config.assistant_mode,
|
||||
"wake_word": config.wake_word,
|
||||
"sample_rate": config.sample_rate,
|
||||
"channels": config.channels,
|
||||
@@ -113,6 +119,29 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"end_chime_file": str(config.end_chime_file),
|
||||
"end_chime_frequency_hz": config.end_chime_frequency_hz,
|
||||
"end_chime_duration_ms": config.end_chime_duration_ms,
|
||||
"audio_apm_provider": config.audio_apm_provider,
|
||||
"audio_aec_enabled": config.audio_aec_enabled,
|
||||
"audio_ns_enabled": config.audio_ns_enabled,
|
||||
"audio_agc_enabled": config.audio_agc_enabled,
|
||||
"audio_apm_required": config.audio_apm_required,
|
||||
"audio_frame_ms": config.audio_frame_ms,
|
||||
"audio_ring_buffer_ms": config.audio_ring_buffer_ms,
|
||||
"interrupt_enabled": config.interrupt_enabled,
|
||||
"interrupt_target_latency_ms": config.interrupt_target_latency_ms,
|
||||
"streaming_stt_provider": config.streaming_stt_provider,
|
||||
"streaming_stt_product_candidate": config.streaming_stt_product_candidate,
|
||||
"streaming_tts_provider": config.streaming_tts_provider,
|
||||
"memory_enabled": config.memory_enabled,
|
||||
"memory_provider": config.memory_provider,
|
||||
"memory_top_k": config.memory_top_k,
|
||||
"memory_auto_save_sensitive": config.memory_auto_save_sensitive,
|
||||
"tool_router_enabled": config.tool_router_enabled,
|
||||
"tool_max_calls_per_turn": config.tool_max_calls_per_turn,
|
||||
"tool_timeout_ms": config.tool_timeout_ms,
|
||||
"openinterpreter_enabled": config.openinterpreter_enabled,
|
||||
"openinterpreter_command": config.openinterpreter_command,
|
||||
"browser_playwright_enabled": config.browser_playwright_enabled,
|
||||
"computer_control_enabled": config.computer_control_enabled,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
@@ -171,6 +200,47 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return 1
|
||||
return 0 if summary.completed_turns > 0 or summary.interrupted else 1
|
||||
|
||||
if args.command == "run-agent-live":
|
||||
config = replace(AppConfig.from_dotenv(args.env_file), assistant_mode="full_duplex_agent")
|
||||
errors = config.validate_basic()
|
||||
if errors:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"command": "run-agent-live",
|
||||
"errors": [str(error) for error in errors],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 1
|
||||
data = {
|
||||
"success": bool(args.check_config),
|
||||
"command": "run-agent-live",
|
||||
"assistant_mode": config.assistant_mode,
|
||||
"audio_apm_provider": config.audio_apm_provider,
|
||||
"streaming_stt_provider": config.streaming_stt_provider,
|
||||
"streaming_tts_provider": config.streaming_tts_provider,
|
||||
"memory_provider": config.memory_provider,
|
||||
"memory_enabled": config.memory_enabled,
|
||||
"tool_router_enabled": config.tool_router_enabled,
|
||||
"openinterpreter_enabled": config.openinterpreter_enabled,
|
||||
"browser_playwright_enabled": config.browser_playwright_enabled,
|
||||
"computer_control_enabled": config.computer_control_enabled,
|
||||
"turn_based_entry": "run-live",
|
||||
"full_duplex_runtime_ready": False,
|
||||
}
|
||||
if not args.check_config:
|
||||
data["code"] = "FULL_DUPLEX_RUNTIME_NOT_IMPLEMENTED"
|
||||
data["message"] = (
|
||||
"run-agent-live is the reserved full-duplex Agent entry point; "
|
||||
"runtime wiring will be implemented by later OpenSpec tasks."
|
||||
)
|
||||
print(json.dumps(data, ensure_ascii=False, sort_keys=True))
|
||||
return 0 if args.check_config else 1
|
||||
|
||||
if args.command == "simulate-live":
|
||||
try:
|
||||
data = run_simulated_live(
|
||||
|
||||
@@ -8,6 +8,7 @@ from .models import ErrorCode, ProviderError
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AppConfig:
|
||||
assistant_mode: str = "turn_based_voice_pet"
|
||||
wake_word: str = "小杰小杰"
|
||||
sample_rate: int = 16000
|
||||
channels: int = 1
|
||||
@@ -68,6 +69,29 @@ class AppConfig:
|
||||
end_chime_file: Path = Path("assets/sounds/codex-notification.wav")
|
||||
end_chime_frequency_hz: int = 880
|
||||
end_chime_duration_ms: int = 140
|
||||
audio_apm_provider: str = "webrtc"
|
||||
audio_aec_enabled: bool = True
|
||||
audio_ns_enabled: bool = True
|
||||
audio_agc_enabled: bool = True
|
||||
audio_apm_required: bool = True
|
||||
audio_frame_ms: int = 20
|
||||
audio_ring_buffer_ms: int = 3000
|
||||
interrupt_enabled: bool = True
|
||||
interrupt_target_latency_ms: int = 200
|
||||
streaming_stt_provider: str = "faster_whisper"
|
||||
streaming_stt_product_candidate: str = "sensevoice"
|
||||
streaming_tts_provider: str = "cosyvoice"
|
||||
memory_enabled: bool = False
|
||||
memory_provider: str = "faiss_sqlite"
|
||||
memory_top_k: int = 5
|
||||
memory_auto_save_sensitive: bool = False
|
||||
tool_router_enabled: bool = False
|
||||
tool_max_calls_per_turn: int = 5
|
||||
tool_timeout_ms: int = 30000
|
||||
openinterpreter_enabled: bool = False
|
||||
openinterpreter_command: str = "openinterpreter"
|
||||
browser_playwright_enabled: bool = False
|
||||
computer_control_enabled: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_dotenv(cls, path: str | Path = ".env", prefix: str = "OWNER_") -> "AppConfig":
|
||||
@@ -77,7 +101,13 @@ class AppConfig:
|
||||
value = values.get(f"{prefix}{name}")
|
||||
return default if value is None or value == "" else value
|
||||
|
||||
def get_bool(name: str, default: str = "0") -> bool:
|
||||
return (get(name, default) or default).lower() not in {"0", "false", "no"}
|
||||
|
||||
return cls(
|
||||
assistant_mode=(
|
||||
get("ASSISTANT_MODE", "turn_based_voice_pet") or "turn_based_voice_pet"
|
||||
).lower(),
|
||||
wake_word=get("WAKE_WORD", "小杰小杰") or "小杰小杰",
|
||||
sample_rate=int(get("SAMPLE_RATE", "16000") or "16000"),
|
||||
channels=int(get("CHANNELS", "1") or "1"),
|
||||
@@ -156,6 +186,37 @@ class AppConfig:
|
||||
),
|
||||
end_chime_frequency_hz=int(get("END_CHIME_FREQUENCY_HZ", "880") or "880"),
|
||||
end_chime_duration_ms=int(get("END_CHIME_DURATION_MS", "140") or "140"),
|
||||
audio_apm_provider=(get("AUDIO_APM_PROVIDER", "webrtc") or "webrtc").lower(),
|
||||
audio_aec_enabled=get_bool("AUDIO_AEC_ENABLED", "1"),
|
||||
audio_ns_enabled=get_bool("AUDIO_NS_ENABLED", "1"),
|
||||
audio_agc_enabled=get_bool("AUDIO_AGC_ENABLED", "1"),
|
||||
audio_apm_required=get_bool("AUDIO_APM_REQUIRED", "1"),
|
||||
audio_frame_ms=int(get("AUDIO_FRAME_MS", "20") or "20"),
|
||||
audio_ring_buffer_ms=int(get("AUDIO_RING_BUFFER_MS", "3000") or "3000"),
|
||||
interrupt_enabled=get_bool("INTERRUPT_ENABLED", "1"),
|
||||
interrupt_target_latency_ms=int(get("INTERRUPT_TARGET_LATENCY_MS", "200") or "200"),
|
||||
streaming_stt_provider=(
|
||||
get("STREAMING_STT_PROVIDER", "faster_whisper") or "faster_whisper"
|
||||
).lower(),
|
||||
streaming_stt_product_candidate=(
|
||||
get("STREAMING_STT_PRODUCT_CANDIDATE", "sensevoice") or "sensevoice"
|
||||
).lower(),
|
||||
streaming_tts_provider=(
|
||||
get("STREAMING_TTS_PROVIDER", "cosyvoice") or "cosyvoice"
|
||||
).lower(),
|
||||
memory_enabled=get_bool("MEMORY_ENABLED", "0"),
|
||||
memory_provider=(get("MEMORY_PROVIDER", "faiss_sqlite") or "faiss_sqlite").lower(),
|
||||
memory_top_k=int(get("MEMORY_TOP_K", "5") or "5"),
|
||||
memory_auto_save_sensitive=get_bool("MEMORY_AUTO_SAVE_SENSITIVE", "0"),
|
||||
tool_router_enabled=get_bool("TOOL_ROUTER_ENABLED", "0"),
|
||||
tool_max_calls_per_turn=int(get("TOOL_MAX_CALLS_PER_TURN", "5") or "5"),
|
||||
tool_timeout_ms=int(get("TOOL_TIMEOUT_MS", "30000") or "30000"),
|
||||
openinterpreter_enabled=get_bool("OPENINTERPRETER_ENABLED", "0"),
|
||||
openinterpreter_command=(
|
||||
get("OPENINTERPRETER_COMMAND", "openinterpreter") or "openinterpreter"
|
||||
),
|
||||
browser_playwright_enabled=get_bool("BROWSER_PLAYWRIGHT_ENABLED", "0"),
|
||||
computer_control_enabled=get_bool("COMPUTER_CONTROL_ENABLED", "0"),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -174,6 +235,16 @@ class AppConfig:
|
||||
|
||||
def validate_basic(self) -> list[ProviderError]:
|
||||
errors: list[ProviderError] = []
|
||||
if self.assistant_mode not in {"turn_based_voice_pet", "full_duplex_agent"}:
|
||||
errors.append(
|
||||
ProviderError(
|
||||
ErrorCode.CONFIG_MISSING_VALUE,
|
||||
"OWNER_ASSISTANT_MODE must be turn_based_voice_pet or full_duplex_agent",
|
||||
False,
|
||||
"config",
|
||||
"startup",
|
||||
)
|
||||
)
|
||||
if self.sample_rate <= 0:
|
||||
errors.append(
|
||||
ProviderError(
|
||||
@@ -451,6 +522,12 @@ class AppConfig:
|
||||
for name, value in {
|
||||
"OWNER_END_CHIME_FREQUENCY_HZ": self.end_chime_frequency_hz,
|
||||
"OWNER_END_CHIME_DURATION_MS": self.end_chime_duration_ms,
|
||||
"OWNER_AUDIO_FRAME_MS": self.audio_frame_ms,
|
||||
"OWNER_AUDIO_RING_BUFFER_MS": self.audio_ring_buffer_ms,
|
||||
"OWNER_INTERRUPT_TARGET_LATENCY_MS": self.interrupt_target_latency_ms,
|
||||
"OWNER_MEMORY_TOP_K": self.memory_top_k,
|
||||
"OWNER_TOOL_MAX_CALLS_PER_TURN": self.tool_max_calls_per_turn,
|
||||
"OWNER_TOOL_TIMEOUT_MS": self.tool_timeout_ms,
|
||||
}.items():
|
||||
if value <= 0:
|
||||
errors.append(
|
||||
@@ -462,6 +539,66 @@ class AppConfig:
|
||||
"startup",
|
||||
)
|
||||
)
|
||||
if self.audio_apm_provider not in {"webrtc", "fake", "disabled"}:
|
||||
errors.append(
|
||||
ProviderError(
|
||||
ErrorCode.CONFIG_MISSING_VALUE,
|
||||
"OWNER_AUDIO_APM_PROVIDER must be webrtc, fake, or disabled",
|
||||
False,
|
||||
"config",
|
||||
"startup",
|
||||
)
|
||||
)
|
||||
if self.streaming_stt_provider not in {"faster_whisper", "sensevoice", "sherpa_onnx"}:
|
||||
errors.append(
|
||||
ProviderError(
|
||||
ErrorCode.CONFIG_MISSING_VALUE,
|
||||
"OWNER_STREAMING_STT_PROVIDER must be faster_whisper, sensevoice, or sherpa_onnx",
|
||||
False,
|
||||
"config",
|
||||
"startup",
|
||||
)
|
||||
)
|
||||
if self.streaming_stt_product_candidate not in {"sensevoice", "faster_whisper", "sherpa_onnx"}:
|
||||
errors.append(
|
||||
ProviderError(
|
||||
ErrorCode.CONFIG_MISSING_VALUE,
|
||||
"OWNER_STREAMING_STT_PRODUCT_CANDIDATE must be sensevoice, faster_whisper, or sherpa_onnx",
|
||||
False,
|
||||
"config",
|
||||
"startup",
|
||||
)
|
||||
)
|
||||
if self.streaming_tts_provider not in {"cosyvoice", "macos_say"}:
|
||||
errors.append(
|
||||
ProviderError(
|
||||
ErrorCode.CONFIG_MISSING_VALUE,
|
||||
"OWNER_STREAMING_TTS_PROVIDER must be cosyvoice or macos_say",
|
||||
False,
|
||||
"config",
|
||||
"startup",
|
||||
)
|
||||
)
|
||||
if self.memory_provider not in {"faiss_sqlite"}:
|
||||
errors.append(
|
||||
ProviderError(
|
||||
ErrorCode.CONFIG_MISSING_VALUE,
|
||||
"OWNER_MEMORY_PROVIDER must be faiss_sqlite",
|
||||
False,
|
||||
"config",
|
||||
"startup",
|
||||
)
|
||||
)
|
||||
if self.openinterpreter_enabled and not self.openinterpreter_command.strip():
|
||||
errors.append(
|
||||
ProviderError(
|
||||
ErrorCode.CONFIG_MISSING_VALUE,
|
||||
"OWNER_OPENINTERPRETER_COMMAND must be non-empty when Open Interpreter is enabled",
|
||||
False,
|
||||
"config",
|
||||
"startup",
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
||||
def api_url(self, path: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user