feat: integrate Chinese speech understanding and capability routing
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user