feat: integrate Chinese speech understanding and capability routing

This commit is contained in:
vee
2026-08-28 15:19:44 +08:00
parent 2529b4896a
commit aa12135b22
39 changed files with 6864 additions and 56 deletions
+88 -10
View File
@@ -12,28 +12,28 @@ import re
import socket
import threading
import time
from http.cookies import CookieError, SimpleCookie
from http import HTTPStatus
from http.cookies import CookieError, SimpleCookie
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import ParseResult, parse_qs, quote, unquote, urlparse
from cognitive_agent.config_loader import load_local_env
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
# Repository-local launch configuration is deliberate and must not be shadowed
# by stale variables inherited from an IDE or an earlier model experiment.
load_local_env(REPOSITORY_ROOT / ".env", override=True)
load_local_env(REPOSITORY_ROOT / ".env.auth", override=True)
from cognitive_agent.auth import AuthConfig, AuthRuntime, verify_password # noqa: E402
from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402
from cognitive_agent.application.gesture_bridge import GestureCapabilityBridge # noqa: E402
from cognitive_agent.application.calibration.service import ( # noqa: E402
ApiError as CalibrationApiError,
)
from cognitive_agent.application.calibration.service import (
CalibrationService,
)
from cognitive_agent.application.gesture_bridge import GestureCapabilityBridge # noqa: E402
from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402
from cognitive_agent.application.self_extension import ( # noqa: E402
ExtensionIntegrationError,
GatewayError,
@@ -42,16 +42,20 @@ from cognitive_agent.application.self_extension import ( # noqa: E402
resolve_active_peripheral_command,
)
from cognitive_agent.application.self_repair import SelfRepairService # noqa: E402
from cognitive_agent.auth import AuthConfig, AuthRuntime, verify_password # noqa: E402
from cognitive_agent.infrastructure.can.access import ( # noqa: E402
HardwareAccessRequiredError,
)
from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
HardwareBusyError,
HardwareSafetyError,
)
from cognitive_agent.infrastructure.can.access import ( # noqa: E402
HardwareAccessRequiredError,
)
from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentError # noqa: E402
from cognitive_agent.infrastructure.nfc import NfcReaderService # noqa: E402
from cognitive_agent.infrastructure.speech import ( # noqa: E402
SpeechRecognitionError,
SpeechTranscriber,
)
STATIC_ROOT = REPOSITORY_ROOT / "apps" / "web-console"
mimetypes.add_type("application/manifest+json", ".webmanifest")
@@ -176,6 +180,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
repository_root=REPOSITORY_ROOT,
)
repair = SelfRepairService()
speech = SpeechTranscriber(REPOSITORY_ROOT)
auth = AuthRuntime(AuthConfig.from_env())
vehicle_display_sync = VehicleDisplaySync()
server_version = "CognitiveAgentCabinDemo/2.0"
@@ -342,6 +347,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
return
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
if path == "/api/speech/status":
self._json(self.speech.status())
return
if path.startswith("/api/requests/"):
self._serve_request(path.removeprefix("/api/requests/"))
return
@@ -357,6 +365,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
return
if not self._authorize(path):
return
if path == "/api/speech/transcribe":
self._handle_speech_transcription()
return
if path in {
"/api/hardware/access/request",
"/api/hardware/access/release",
@@ -482,6 +493,32 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
payload.get("control_transport", "wired_can")
),
}
speech_utterance_id = str(
payload.get("speech_utterance_id", "")
).strip()
if speech_utterance_id:
if path != "/api/requests":
raise ValueError(
"speech semantic execution is only accepted by /api/requests"
)
speech_understanding = self.speech.cached_result(
speech_utterance_id
)
if str(speech_understanding.get("clean_text", "")).strip() != utterance:
raise SpeechRecognitionError(
"speech_text_changed",
"语音文本已被修改,请确认修改后的内容再执行。",
status=409,
)
if speech_understanding.get("execution_eligible") is not True:
raise SpeechRecognitionError(
"speech_not_executable",
"语音意图尚未通过能力或参数校验,已拒绝执行。",
status=422,
)
provider = "local"
common_arguments["provider"] = provider
common_arguments["_speech_understanding"] = speech_understanding
if (
path != "/api/can/commands"
and "application/x-ndjson" in self.headers.get("Accept", "")
@@ -596,6 +633,12 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
{"error": "extension_request_ambiguous", "message": str(exc)},
HTTPStatus.UNPROCESSABLE_ENTITY,
)
except SpeechRecognitionError as exc:
fail_display_sync(exc.code, exc)
self._json(
{"error": exc.code, "message": str(exc)},
HTTPStatus(exc.status),
)
except (ValueError, json.JSONDecodeError) as exc:
fail_display_sync("invalid_request", exc)
self._json({"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST)
@@ -1340,6 +1383,41 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
raise ValueError("request body must be a JSON object")
return payload
def _handle_speech_transcription(self) -> None:
content_type = self.headers.get("Content-Type", "").split(";", 1)[0].strip().lower()
if content_type not in {"audio/wav", "audio/wave", "audio/x-wav"}:
self._json(
{
"error": "unsupported_media_type",
"message": "语音听写接口仅接受 16 位 PCM WAV 音频。",
},
HTTPStatus.UNSUPPORTED_MEDIA_TYPE,
)
return
try:
content_length = int(self.headers.get("Content-Length", "0"))
if content_length <= 0 or content_length > 2 * 1024 * 1024:
raise SpeechRecognitionError(
"invalid_audio_size",
"录音数据为空或超过大小限制。",
status=413,
)
result = self.speech.transcribe_wav(
self.rfile.read(content_length),
utterance_id=self.headers.get("X-Speech-Utterance-Id"),
)
self._json(result)
except SpeechRecognitionError as exc:
self._json(
{"error": exc.code, "message": str(exc)},
HTTPStatus(exc.status),
)
except (TypeError, ValueError):
self._json(
{"error": "invalid_audio", "message": "录音请求格式无效。"},
HTTPStatus.BAD_REQUEST,
)
def _authorize(self, path: str) -> bool:
if not self.auth.config.enabled or path in PUBLIC_AUTH_PATHS:
return True
@@ -1612,7 +1690,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
self.send_header("Referrer-Policy", "no-referrer")
self.send_header(
"Permissions-Policy",
"camera=(self), microphone=(), geolocation=(), payment=(), usb=()",
"camera=(self), microphone=(self), geolocation=(), payment=(), usb=()",
)
self.send_header(
"Content-Security-Policy",