mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
103 lines
3.8 KiB
Python
103 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from dataclasses import dataclass
|
|
from urllib.parse import urlsplit
|
|
|
|
import httpx
|
|
|
|
|
|
class VoicePrintIntegrationError(RuntimeError):
|
|
def __init__(self, code: int, message: str | None = None, params: tuple[object, ...] = ()):
|
|
super().__init__(message or str(code))
|
|
self.code = code
|
|
self.message = message
|
|
self.params = params
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class VoicePrintEndpoint:
|
|
base_url: str
|
|
authorization: str
|
|
|
|
@classmethod
|
|
def parse(cls, configured_url: str | None) -> VoicePrintEndpoint:
|
|
if configured_url is None:
|
|
raise VoicePrintIntegrationError(10084)
|
|
parsed = urlsplit(configured_url)
|
|
if not parsed.scheme or not parsed.hostname:
|
|
raise VoicePrintIntegrationError(10084)
|
|
marker = "key="
|
|
marker_index = parsed.query.find(marker)
|
|
key = parsed.query[marker_index + len(marker) :] if marker_index >= 0 else parsed.query[3:]
|
|
port = f":{parsed.port}" if parsed.port is not None else ""
|
|
return cls(f"{parsed.scheme}://{parsed.hostname}{port}", f"Bearer {key}")
|
|
|
|
|
|
class VoicePrintClient:
|
|
def __init__(self, configured_url: str, *, timeout: float = 10.0, client: httpx.AsyncClient | None = None):
|
|
self.endpoint = VoicePrintEndpoint.parse(configured_url)
|
|
self.timeout = timeout
|
|
self._client = client
|
|
|
|
async def _request(
|
|
self,
|
|
method: str,
|
|
path: str,
|
|
*,
|
|
data: Mapping[str, str] | None = None,
|
|
files: Mapping[str, tuple[str, bytes, str]] | None = None,
|
|
) -> httpx.Response:
|
|
headers = {"Authorization": self.endpoint.authorization}
|
|
if self._client is not None:
|
|
return await self._client.request(
|
|
method, f"{self.endpoint.base_url}{path}", headers=headers, data=data, files=files
|
|
)
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
return await client.request(
|
|
method, f"{self.endpoint.base_url}{path}", headers=headers, data=data, files=files
|
|
)
|
|
|
|
async def identify(self, speaker_ids: list[str], audio: bytes) -> tuple[str | None, float | None] | None:
|
|
if not speaker_ids:
|
|
return None
|
|
response = await self._request(
|
|
"POST",
|
|
"/voiceprint/identify",
|
|
data={"speaker_ids": ",".join(speaker_ids)},
|
|
files={"file": ("VoicePrint.WAV", audio, "application/octet-stream")},
|
|
)
|
|
if response.status_code != 200:
|
|
raise VoicePrintIntegrationError(10091)
|
|
try:
|
|
payload = response.json()
|
|
except ValueError as exc:
|
|
raise VoicePrintIntegrationError(10091) from exc
|
|
if not isinstance(payload, dict):
|
|
return None
|
|
speaker_id = payload.get("speaker_id")
|
|
score = payload.get("score")
|
|
return (
|
|
str(speaker_id) if speaker_id is not None else None,
|
|
float(score) if isinstance(score, int | float) else None,
|
|
)
|
|
|
|
async def register(self, speaker_id: str, audio: bytes) -> None:
|
|
response = await self._request(
|
|
"POST",
|
|
"/voiceprint/register",
|
|
data={"speaker_id": speaker_id},
|
|
files={"file": ("VoicePrint.WAV", audio, "application/octet-stream")},
|
|
)
|
|
if response.status_code != 200:
|
|
raise VoicePrintIntegrationError(10087)
|
|
if "true" not in response.text:
|
|
raise VoicePrintIntegrationError(10088)
|
|
|
|
async def cancel(self, speaker_id: str) -> None:
|
|
response = await self._request("DELETE", f"/voiceprint/{speaker_id}")
|
|
if response.status_code != 200:
|
|
raise VoicePrintIntegrationError(10089)
|
|
if "true" not in response.text:
|
|
raise VoicePrintIntegrationError(10090)
|