audex 1.0.7a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- audex/__init__.py +9 -0
- audex/__main__.py +7 -0
- audex/cli/__init__.py +189 -0
- audex/cli/apis/__init__.py +12 -0
- audex/cli/apis/init/__init__.py +34 -0
- audex/cli/apis/init/gencfg.py +130 -0
- audex/cli/apis/init/setup.py +330 -0
- audex/cli/apis/init/vprgroup.py +125 -0
- audex/cli/apis/serve.py +141 -0
- audex/cli/args.py +356 -0
- audex/cli/exceptions.py +44 -0
- audex/cli/helper/__init__.py +0 -0
- audex/cli/helper/ansi.py +193 -0
- audex/cli/helper/display.py +288 -0
- audex/config/__init__.py +64 -0
- audex/config/core/__init__.py +30 -0
- audex/config/core/app.py +29 -0
- audex/config/core/audio.py +45 -0
- audex/config/core/logging.py +163 -0
- audex/config/core/session.py +11 -0
- audex/config/helper/__init__.py +1 -0
- audex/config/helper/client/__init__.py +1 -0
- audex/config/helper/client/http.py +28 -0
- audex/config/helper/client/websocket.py +21 -0
- audex/config/helper/provider/__init__.py +1 -0
- audex/config/helper/provider/dashscope.py +13 -0
- audex/config/helper/provider/unisound.py +18 -0
- audex/config/helper/provider/xfyun.py +23 -0
- audex/config/infrastructure/__init__.py +31 -0
- audex/config/infrastructure/cache.py +51 -0
- audex/config/infrastructure/database.py +48 -0
- audex/config/infrastructure/recorder.py +32 -0
- audex/config/infrastructure/store.py +19 -0
- audex/config/provider/__init__.py +18 -0
- audex/config/provider/transcription.py +109 -0
- audex/config/provider/vpr.py +99 -0
- audex/container.py +40 -0
- audex/entity/__init__.py +468 -0
- audex/entity/doctor.py +109 -0
- audex/entity/doctor.pyi +51 -0
- audex/entity/fields.py +401 -0
- audex/entity/segment.py +115 -0
- audex/entity/segment.pyi +38 -0
- audex/entity/session.py +133 -0
- audex/entity/session.pyi +47 -0
- audex/entity/utterance.py +142 -0
- audex/entity/utterance.pyi +48 -0
- audex/entity/vp.py +68 -0
- audex/entity/vp.pyi +35 -0
- audex/exceptions.py +157 -0
- audex/filters/__init__.py +692 -0
- audex/filters/generated/__init__.py +21 -0
- audex/filters/generated/doctor.py +987 -0
- audex/filters/generated/segment.py +723 -0
- audex/filters/generated/session.py +978 -0
- audex/filters/generated/utterance.py +939 -0
- audex/filters/generated/vp.py +815 -0
- audex/helper/__init__.py +1 -0
- audex/helper/hash.py +33 -0
- audex/helper/mixin.py +65 -0
- audex/helper/net.py +19 -0
- audex/helper/settings/__init__.py +830 -0
- audex/helper/settings/fields.py +317 -0
- audex/helper/stream.py +153 -0
- audex/injectors/__init__.py +1 -0
- audex/injectors/config.py +12 -0
- audex/injectors/lifespan.py +7 -0
- audex/lib/__init__.py +1 -0
- audex/lib/cache/__init__.py +383 -0
- audex/lib/cache/inmemory.py +513 -0
- audex/lib/database/__init__.py +83 -0
- audex/lib/database/sqlite.py +406 -0
- audex/lib/exporter.py +189 -0
- audex/lib/injectors/__init__.py +1 -0
- audex/lib/injectors/cache.py +25 -0
- audex/lib/injectors/container.py +47 -0
- audex/lib/injectors/exporter.py +26 -0
- audex/lib/injectors/recorder.py +33 -0
- audex/lib/injectors/server.py +17 -0
- audex/lib/injectors/session.py +18 -0
- audex/lib/injectors/sqlite.py +24 -0
- audex/lib/injectors/store.py +13 -0
- audex/lib/injectors/transcription.py +42 -0
- audex/lib/injectors/usb.py +12 -0
- audex/lib/injectors/vpr.py +65 -0
- audex/lib/injectors/wifi.py +7 -0
- audex/lib/recorder.py +844 -0
- audex/lib/repos/__init__.py +149 -0
- audex/lib/repos/container.py +23 -0
- audex/lib/repos/database/__init__.py +1 -0
- audex/lib/repos/database/sqlite.py +672 -0
- audex/lib/repos/decorators.py +74 -0
- audex/lib/repos/doctor.py +286 -0
- audex/lib/repos/segment.py +302 -0
- audex/lib/repos/session.py +285 -0
- audex/lib/repos/tables/__init__.py +70 -0
- audex/lib/repos/tables/doctor.py +137 -0
- audex/lib/repos/tables/segment.py +113 -0
- audex/lib/repos/tables/session.py +140 -0
- audex/lib/repos/tables/utterance.py +131 -0
- audex/lib/repos/tables/vp.py +102 -0
- audex/lib/repos/utterance.py +288 -0
- audex/lib/repos/vp.py +286 -0
- audex/lib/restful.py +251 -0
- audex/lib/server/__init__.py +97 -0
- audex/lib/server/auth.py +98 -0
- audex/lib/server/handlers.py +248 -0
- audex/lib/server/templates/index.html.j2 +226 -0
- audex/lib/server/templates/login.html.j2 +111 -0
- audex/lib/server/templates/static/script.js +68 -0
- audex/lib/server/templates/static/style.css +579 -0
- audex/lib/server/types.py +123 -0
- audex/lib/session.py +503 -0
- audex/lib/store/__init__.py +238 -0
- audex/lib/store/localfile.py +411 -0
- audex/lib/transcription/__init__.py +33 -0
- audex/lib/transcription/dashscope.py +525 -0
- audex/lib/transcription/events.py +62 -0
- audex/lib/usb.py +554 -0
- audex/lib/vpr/__init__.py +38 -0
- audex/lib/vpr/unisound/__init__.py +185 -0
- audex/lib/vpr/unisound/types.py +469 -0
- audex/lib/vpr/xfyun/__init__.py +483 -0
- audex/lib/vpr/xfyun/types.py +679 -0
- audex/lib/websocket/__init__.py +8 -0
- audex/lib/websocket/connection.py +485 -0
- audex/lib/websocket/pool.py +991 -0
- audex/lib/wifi.py +1146 -0
- audex/lifespan.py +75 -0
- audex/service/__init__.py +27 -0
- audex/service/decorators.py +73 -0
- audex/service/doctor/__init__.py +652 -0
- audex/service/doctor/const.py +36 -0
- audex/service/doctor/exceptions.py +96 -0
- audex/service/doctor/types.py +54 -0
- audex/service/export/__init__.py +236 -0
- audex/service/export/const.py +17 -0
- audex/service/export/exceptions.py +34 -0
- audex/service/export/types.py +21 -0
- audex/service/injectors/__init__.py +1 -0
- audex/service/injectors/container.py +53 -0
- audex/service/injectors/doctor.py +34 -0
- audex/service/injectors/export.py +27 -0
- audex/service/injectors/session.py +49 -0
- audex/service/session/__init__.py +754 -0
- audex/service/session/const.py +34 -0
- audex/service/session/exceptions.py +67 -0
- audex/service/session/types.py +91 -0
- audex/types.py +39 -0
- audex/utils.py +287 -0
- audex/valueobj/__init__.py +81 -0
- audex/valueobj/common/__init__.py +1 -0
- audex/valueobj/common/auth.py +84 -0
- audex/valueobj/common/email.py +16 -0
- audex/valueobj/common/ops.py +22 -0
- audex/valueobj/common/phone.py +84 -0
- audex/valueobj/common/version.py +72 -0
- audex/valueobj/session.py +19 -0
- audex/valueobj/utterance.py +15 -0
- audex/view/__init__.py +51 -0
- audex/view/container.py +17 -0
- audex/view/decorators.py +303 -0
- audex/view/pages/__init__.py +1 -0
- audex/view/pages/dashboard/__init__.py +286 -0
- audex/view/pages/dashboard/wifi.py +407 -0
- audex/view/pages/login.py +110 -0
- audex/view/pages/recording.py +348 -0
- audex/view/pages/register.py +202 -0
- audex/view/pages/sessions/__init__.py +196 -0
- audex/view/pages/sessions/details.py +224 -0
- audex/view/pages/sessions/export.py +443 -0
- audex/view/pages/settings.py +374 -0
- audex/view/pages/voiceprint/__init__.py +1 -0
- audex/view/pages/voiceprint/enroll.py +195 -0
- audex/view/pages/voiceprint/update.py +195 -0
- audex/view/static/css/dashboard.css +452 -0
- audex/view/static/css/glass.css +22 -0
- audex/view/static/css/global.css +541 -0
- audex/view/static/css/login.css +386 -0
- audex/view/static/css/recording.css +439 -0
- audex/view/static/css/register.css +293 -0
- audex/view/static/css/sessions/styles.css +501 -0
- audex/view/static/css/settings.css +186 -0
- audex/view/static/css/voiceprint/enroll.css +43 -0
- audex/view/static/css/voiceprint/styles.css +209 -0
- audex/view/static/css/voiceprint/update.css +44 -0
- audex/view/static/images/logo.svg +95 -0
- audex/view/static/js/recording.js +42 -0
- audex-1.0.7a3.dist-info/METADATA +361 -0
- audex-1.0.7a3.dist-info/RECORD +192 -0
- audex-1.0.7a3.dist-info/WHEEL +4 -0
- audex-1.0.7a3.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import email.utils as eut
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
import typing
|
|
9
|
+
import typing as t
|
|
10
|
+
import urllib.parse as urlparse
|
|
11
|
+
|
|
12
|
+
from httpx import URL
|
|
13
|
+
from httpx import AsyncClient
|
|
14
|
+
from httpx import Auth
|
|
15
|
+
from httpx import HTTPStatusError
|
|
16
|
+
from httpx import Request
|
|
17
|
+
from httpx import Response
|
|
18
|
+
from tenacity import RetryError
|
|
19
|
+
|
|
20
|
+
from audex import utils
|
|
21
|
+
from audex.helper.mixin import LoggingMixin
|
|
22
|
+
from audex.lib.restful import RESTfulMixin
|
|
23
|
+
from audex.lib.vpr import VPR
|
|
24
|
+
from audex.lib.vpr import GroupAlreadyExistsError
|
|
25
|
+
from audex.lib.vpr import GroupNotFoundError
|
|
26
|
+
from audex.lib.vpr import VPRError
|
|
27
|
+
from audex.lib.vpr.xfyun.types import AudioPayload
|
|
28
|
+
from audex.lib.vpr.xfyun.types import AudioResource
|
|
29
|
+
from audex.lib.vpr.xfyun.types import CreateFeatureParams
|
|
30
|
+
from audex.lib.vpr.xfyun.types import CreateFeatureRequest
|
|
31
|
+
from audex.lib.vpr.xfyun.types import CreateFeatureResponse
|
|
32
|
+
from audex.lib.vpr.xfyun.types import CreateFeatureResult
|
|
33
|
+
from audex.lib.vpr.xfyun.types import CreateGroupParams
|
|
34
|
+
from audex.lib.vpr.xfyun.types import CreateGroupRequest
|
|
35
|
+
from audex.lib.vpr.xfyun.types import CreateGroupResponse
|
|
36
|
+
from audex.lib.vpr.xfyun.types import CreateGroupResult
|
|
37
|
+
from audex.lib.vpr.xfyun.types import RequestHeader
|
|
38
|
+
from audex.lib.vpr.xfyun.types import S782b4996CreateFeatureParams
|
|
39
|
+
from audex.lib.vpr.xfyun.types import S782b4996CreateGroupParams
|
|
40
|
+
from audex.lib.vpr.xfyun.types import S782b4996SearchScoreFeaParams
|
|
41
|
+
from audex.lib.vpr.xfyun.types import S782b4996UpdateFeatureParams
|
|
42
|
+
from audex.lib.vpr.xfyun.types import SearchScoreFeaParams
|
|
43
|
+
from audex.lib.vpr.xfyun.types import SearchScoreFeaRequest
|
|
44
|
+
from audex.lib.vpr.xfyun.types import SearchScoreFeaResponse
|
|
45
|
+
from audex.lib.vpr.xfyun.types import SearchScoreFeaResult
|
|
46
|
+
from audex.lib.vpr.xfyun.types import UpdateFeatureParams
|
|
47
|
+
from audex.lib.vpr.xfyun.types import UpdateFeatureRequest
|
|
48
|
+
from audex.lib.vpr.xfyun.types import UpdateFeatureResponse
|
|
49
|
+
from audex.lib.vpr.xfyun.types import UpdateFeatureResult
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class XFYunAuth(LoggingMixin, Auth):
|
|
53
|
+
__logtag__ = "audex.lib.vpr.xfyun.auth"
|
|
54
|
+
|
|
55
|
+
def __init__(self, api_key: str, api_secret: str) -> None:
|
|
56
|
+
super().__init__()
|
|
57
|
+
self.api_key = api_key
|
|
58
|
+
self.api_secret = api_secret
|
|
59
|
+
self.logger.debug(f"Initialized XFYunAuth with api_key={api_key[:8]}*** (masked)")
|
|
60
|
+
|
|
61
|
+
def auth_flow(self, request: Request) -> t.Generator[Request, Response, None]:
|
|
62
|
+
self.logger.debug(f"Starting auth_flow for request: {request.method} {request.url}")
|
|
63
|
+
|
|
64
|
+
# Docs: https://www.xfyun.cn/doc/voiceservice/isv/API.html
|
|
65
|
+
# Generate RFC 1123 date
|
|
66
|
+
date = eut.formatdate(timeval=None, localtime=False, usegmt=True)
|
|
67
|
+
self.logger.debug(f"Generated RFC 1123 date: {date}")
|
|
68
|
+
|
|
69
|
+
# Parse URL components
|
|
70
|
+
parsed_url = urlparse.urlparse(str(request.url))
|
|
71
|
+
host = parsed_url.netloc
|
|
72
|
+
path = parsed_url.path
|
|
73
|
+
request_line = f"{request.method} {path} HTTP/1.1"
|
|
74
|
+
self.logger.debug("Parsed URL components:")
|
|
75
|
+
self.logger.debug(f" - Host: {host}")
|
|
76
|
+
self.logger.debug(f" - Path: {path}")
|
|
77
|
+
self.logger.debug(f" - Request-Line: {request_line}")
|
|
78
|
+
|
|
79
|
+
# Create the signature origin string
|
|
80
|
+
sig_origin = f"host: {host}\ndate: {date}\n{request_line}"
|
|
81
|
+
self.logger.debug("Signature origin string (for HMAC):")
|
|
82
|
+
self.logger.debug("\n" + sig_origin)
|
|
83
|
+
|
|
84
|
+
# Generate HMAC-SHA256 signature
|
|
85
|
+
self.logger.debug(
|
|
86
|
+
f"Computing HMAC-SHA256 with api_secret={self.api_secret[:8]}*** (masked)"
|
|
87
|
+
)
|
|
88
|
+
sig_sha = hmac.new(
|
|
89
|
+
self.api_secret.encode("utf-8"),
|
|
90
|
+
sig_origin.encode("utf-8"),
|
|
91
|
+
hashlib.sha256,
|
|
92
|
+
).digest()
|
|
93
|
+
self.logger.debug(f"HMAC-SHA256 raw digest (hex): {sig_sha.hex()}")
|
|
94
|
+
self.logger.debug(f"HMAC-SHA256 raw digest length: {len(sig_sha)} bytes")
|
|
95
|
+
|
|
96
|
+
# Base64 encode the signature
|
|
97
|
+
sig = base64.b64encode(sig_sha).decode("utf-8")
|
|
98
|
+
self.logger.debug(f"Base64-encoded signature: {sig}")
|
|
99
|
+
self.logger.debug(f"Base64-encoded signature length: {len(sig)} chars")
|
|
100
|
+
|
|
101
|
+
# Create the authorization origin string
|
|
102
|
+
auth_origin = (
|
|
103
|
+
f'api_key="{self.api_key}", '
|
|
104
|
+
f'algorithm="hmac-sha256", '
|
|
105
|
+
f'headers="host date request-line", '
|
|
106
|
+
f'signature="{sig}"'
|
|
107
|
+
)
|
|
108
|
+
self.logger.debug("Authorization origin string (before base64):")
|
|
109
|
+
self.logger.debug(auth_origin)
|
|
110
|
+
self.logger.debug(f"Authorization origin string length: {len(auth_origin)} chars")
|
|
111
|
+
|
|
112
|
+
# Base64 encode the authorization string
|
|
113
|
+
authorization = base64.b64encode(auth_origin.encode("utf-8")).decode("utf-8")
|
|
114
|
+
self.logger.debug(f"Base64-encoded authorization: {authorization}")
|
|
115
|
+
self.logger.debug(f"Base64-encoded authorization length: {len(authorization)} chars")
|
|
116
|
+
|
|
117
|
+
# Set params
|
|
118
|
+
auth_params = {
|
|
119
|
+
"authorization": authorization,
|
|
120
|
+
"host": host,
|
|
121
|
+
"date": date,
|
|
122
|
+
}
|
|
123
|
+
self.logger.debug("Auth parameters to append:")
|
|
124
|
+
for key, value in auth_params.items():
|
|
125
|
+
if key == "authorization":
|
|
126
|
+
self.logger.debug(f" - {key}: {value[:50]}... (truncated)")
|
|
127
|
+
else:
|
|
128
|
+
self.logger.debug(f" - {key}: {value}")
|
|
129
|
+
|
|
130
|
+
# Append auth params to URL
|
|
131
|
+
if parsed_url.query:
|
|
132
|
+
new_url = f"{request.url}&{urlparse.urlencode(auth_params)}"
|
|
133
|
+
self.logger.debug("Appending auth params to existing query string")
|
|
134
|
+
else:
|
|
135
|
+
new_url = f"{request.url}?{urlparse.urlencode(auth_params)}"
|
|
136
|
+
self.logger.debug("Adding auth params as new query string")
|
|
137
|
+
|
|
138
|
+
self.logger.debug(f"Final authenticated URL (truncated): {new_url[:100]}...")
|
|
139
|
+
|
|
140
|
+
# Update request URL
|
|
141
|
+
request.url = URL(new_url)
|
|
142
|
+
self.logger.debug("Successfully updated request URL with auth parameters")
|
|
143
|
+
|
|
144
|
+
yield request
|
|
145
|
+
|
|
146
|
+
async def async_auth_flow(self, request: Request) -> typing.AsyncGenerator[Request, Response]:
|
|
147
|
+
self.logger.debug(f"Starting async_auth_flow for request: {request.method} {request.url}")
|
|
148
|
+
# Reuse the synchronous auth_flow logic
|
|
149
|
+
auth_gen = self.auth_flow(request)
|
|
150
|
+
try:
|
|
151
|
+
while True:
|
|
152
|
+
req = next(auth_gen)
|
|
153
|
+
self.logger.debug("Yielding authenticated request in async_auth_flow")
|
|
154
|
+
yield req
|
|
155
|
+
except StopIteration:
|
|
156
|
+
self.logger.debug("Completed async_auth_flow")
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class XFYunVPR(RESTfulMixin, VPR):
|
|
161
|
+
__logtag__ = "audex.lib.vpr.xfyun"
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
*,
|
|
166
|
+
endpoint: str = "/v1/private/s782b4996",
|
|
167
|
+
app_id: str,
|
|
168
|
+
api_key: str,
|
|
169
|
+
api_secret: str,
|
|
170
|
+
group_id: str | None = None,
|
|
171
|
+
base_url: str = "https://api.xf-yun.com",
|
|
172
|
+
proxy: str | URL | None = None,
|
|
173
|
+
timeout: float = 10.0,
|
|
174
|
+
http_client: AsyncClient | None = None,
|
|
175
|
+
default_headers: dict[str, str] | None = None,
|
|
176
|
+
default_params: dict[str, t.Any] | None = None,
|
|
177
|
+
):
|
|
178
|
+
self.endpoint = endpoint
|
|
179
|
+
self.app_id = app_id
|
|
180
|
+
self.api_key = api_key
|
|
181
|
+
self.api_secret = api_secret
|
|
182
|
+
self.auth = XFYunAuth(api_key=api_key, api_secret=api_secret)
|
|
183
|
+
|
|
184
|
+
RESTfulMixin.__init__(
|
|
185
|
+
self,
|
|
186
|
+
base_url=base_url,
|
|
187
|
+
proxy=proxy,
|
|
188
|
+
auth=self.auth,
|
|
189
|
+
timeout=timeout,
|
|
190
|
+
http_client=http_client,
|
|
191
|
+
default_headers=default_headers,
|
|
192
|
+
default_params=default_params,
|
|
193
|
+
)
|
|
194
|
+
VPR.__init__(self, group_id=group_id)
|
|
195
|
+
self.logger.info("XFYunVPR client initialized successfully")
|
|
196
|
+
|
|
197
|
+
async def create_group(self, name: str, gid: str | None = None) -> str:
|
|
198
|
+
self.logger.info(f"Creating group with name='{name}', gid={gid or 'auto-generated'}")
|
|
199
|
+
|
|
200
|
+
if self.group_id:
|
|
201
|
+
error_msg = f"Group already exists (group_id={self.group_id}), cannot create a new one."
|
|
202
|
+
self.logger.error(error_msg)
|
|
203
|
+
raise GroupAlreadyExistsError(error_msg)
|
|
204
|
+
|
|
205
|
+
group_id = gid or utils.gen_id()
|
|
206
|
+
self.logger.debug(f"Using group_id: {group_id}")
|
|
207
|
+
|
|
208
|
+
request = CreateGroupRequest(
|
|
209
|
+
header=RequestHeader(app_id=self.app_id),
|
|
210
|
+
parameter=CreateGroupParams(
|
|
211
|
+
s782b4996=S782b4996CreateGroupParams(groupId=group_id, groupName=name)
|
|
212
|
+
),
|
|
213
|
+
).model_dump(exclude_none=True)
|
|
214
|
+
|
|
215
|
+
self.logger.debug("Request payload (JSON):")
|
|
216
|
+
self.logger.debug(json.dumps(request, indent=2, ensure_ascii=False))
|
|
217
|
+
|
|
218
|
+
self.logger.debug(f"Sending create_group request to {self.endpoint}")
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
response = await self.request(
|
|
222
|
+
endpoint=self.endpoint,
|
|
223
|
+
method="POST",
|
|
224
|
+
json=request,
|
|
225
|
+
cast_to=CreateGroupResponse,
|
|
226
|
+
strict=False,
|
|
227
|
+
)
|
|
228
|
+
except HTTPStatusError as e:
|
|
229
|
+
error_msg = f"HTTP error during create_group request: {e}"
|
|
230
|
+
self.logger.bind(request=e.request.content, response=e.response.text).error(error_msg)
|
|
231
|
+
raise VPRError(error_msg) from e
|
|
232
|
+
except RetryError as e:
|
|
233
|
+
error_msg = f"Retry error during verify request: {e}"
|
|
234
|
+
self.logger.error(error_msg)
|
|
235
|
+
raise VPRError(error_msg) from e
|
|
236
|
+
|
|
237
|
+
self.logger.debug(
|
|
238
|
+
f"Received response with code={response.header.code}, message='{response.header.message}'"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if response.header.code != 0:
|
|
242
|
+
error_msg = (
|
|
243
|
+
f"Failed to create group: {response.header.message} (code={response.header.code})"
|
|
244
|
+
)
|
|
245
|
+
self.logger.error(error_msg)
|
|
246
|
+
raise VPRError(error_msg)
|
|
247
|
+
|
|
248
|
+
text = response.payload.create_group_res.text
|
|
249
|
+
self.logger.debug(f"Response payload text (base64): {text[:100]}... (truncated)")
|
|
250
|
+
|
|
251
|
+
# Base-64 decode the model from text
|
|
252
|
+
obj_str = base64.b64decode(text).decode("utf-8")
|
|
253
|
+
self.logger.debug(f"Decoded payload (JSON): {obj_str}")
|
|
254
|
+
|
|
255
|
+
obj_json = json.loads(obj_str)
|
|
256
|
+
obj = CreateGroupResult.model_validate(obj_json)
|
|
257
|
+
self.logger.debug(f"Parsed CreateGroupResult: group_id={obj.group_id}")
|
|
258
|
+
|
|
259
|
+
if group_id != obj.group_id:
|
|
260
|
+
error_msg = f"Group ID mismatch after creation: expected={group_id}, got={obj.group_id}"
|
|
261
|
+
self.logger.error(error_msg)
|
|
262
|
+
raise VPRError(error_msg)
|
|
263
|
+
|
|
264
|
+
self.group_id = obj.group_id
|
|
265
|
+
self.logger.info(f"Group created successfully: group_id={self.group_id}, name='{name}'")
|
|
266
|
+
return self.group_id
|
|
267
|
+
|
|
268
|
+
async def enroll(self, data: bytes, sr: int, uid: str | None = None) -> str:
|
|
269
|
+
self.logger.info(
|
|
270
|
+
f"Enrolling feature with sr={sr}, uid={uid or 'auto-generated'}, data_size={len(data)} bytes"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if not self.group_id:
|
|
274
|
+
error_msg = "Group ID is not set. Cannot enroll feature. Please create a group first."
|
|
275
|
+
self.logger.error(error_msg)
|
|
276
|
+
raise GroupNotFoundError(error_msg)
|
|
277
|
+
|
|
278
|
+
uid = uid or utils.gen_id()
|
|
279
|
+
self.logger.debug(f"Using feature_id (uid): {uid}")
|
|
280
|
+
self.logger.debug(f"Target group_id: {self.group_id}")
|
|
281
|
+
|
|
282
|
+
audio_b64 = base64.b64encode(data).decode("utf-8")
|
|
283
|
+
self.logger.debug(f"Encoded audio data to base64: length={len(audio_b64)} chars")
|
|
284
|
+
|
|
285
|
+
request_payload = CreateFeatureRequest(
|
|
286
|
+
header=RequestHeader(app_id=self.app_id),
|
|
287
|
+
parameter=CreateFeatureParams(
|
|
288
|
+
s782b4996=S782b4996CreateFeatureParams(groupId=self.group_id, featureId=uid)
|
|
289
|
+
),
|
|
290
|
+
payload=AudioPayload(resource=AudioResource(audio=audio_b64, sample_rate=sr)),
|
|
291
|
+
).model_dump(exclude_none=True)
|
|
292
|
+
|
|
293
|
+
self.logger.debug(f"Sending enroll request to {self.endpoint}")
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
response = await self.request(
|
|
297
|
+
endpoint=self.endpoint,
|
|
298
|
+
method="POST",
|
|
299
|
+
json=request_payload,
|
|
300
|
+
cast_to=CreateFeatureResponse,
|
|
301
|
+
)
|
|
302
|
+
except HTTPStatusError as e:
|
|
303
|
+
error_msg = f"HTTP error during enroll request: {e}"
|
|
304
|
+
self.logger.bind(request=e.request.content, response=e.response.text).error(error_msg)
|
|
305
|
+
raise VPRError(error_msg) from e
|
|
306
|
+
except RetryError as e:
|
|
307
|
+
error_msg = f"Retry error during verify request: {e}"
|
|
308
|
+
self.logger.error(error_msg)
|
|
309
|
+
raise VPRError(error_msg) from e
|
|
310
|
+
|
|
311
|
+
self.logger.debug(
|
|
312
|
+
f"Received response with code={response.header.code}, message='{response.header.message}'"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if response.header.code != 0:
|
|
316
|
+
error_msg = (
|
|
317
|
+
f"Failed to enroll feature: {response.header.message} (code={response.header.code})"
|
|
318
|
+
)
|
|
319
|
+
self.logger.error(error_msg)
|
|
320
|
+
raise VPRError(error_msg)
|
|
321
|
+
|
|
322
|
+
text = response.payload.create_feature_res.text
|
|
323
|
+
self.logger.debug(f"Response payload text (base64): {text[:100]}... (truncated)")
|
|
324
|
+
|
|
325
|
+
# Base-64 decode the model from text
|
|
326
|
+
obj_str = base64.b64decode(text).decode("utf-8")
|
|
327
|
+
self.logger.debug(f"Decoded payload (JSON): {obj_str}")
|
|
328
|
+
|
|
329
|
+
obj_json = json.loads(obj_str)
|
|
330
|
+
obj = CreateFeatureResult.model_validate(obj_json)
|
|
331
|
+
self.logger.debug(f"Parsed CreateFeatureResult: feature_id={obj.feature_id}")
|
|
332
|
+
|
|
333
|
+
if uid != obj.feature_id:
|
|
334
|
+
error_msg = (
|
|
335
|
+
f"Feature ID mismatch after registration: expected={uid}, got={obj.feature_id}"
|
|
336
|
+
)
|
|
337
|
+
self.logger.error(error_msg)
|
|
338
|
+
raise VPRError(error_msg)
|
|
339
|
+
|
|
340
|
+
self.logger.info(
|
|
341
|
+
f"Feature enrolled successfully: feature_id={obj.feature_id}, group_id={self.group_id}"
|
|
342
|
+
)
|
|
343
|
+
return obj.feature_id
|
|
344
|
+
|
|
345
|
+
async def update(self, uid: str, data: bytes, sr: int) -> None:
|
|
346
|
+
self.logger.info(f"Updating feature: uid={uid}, sr={sr}, data_size={len(data)} bytes")
|
|
347
|
+
|
|
348
|
+
if not self.group_id:
|
|
349
|
+
error_msg = "Group ID is not set. Cannot update feature. Please create a group first."
|
|
350
|
+
self.logger.error(error_msg)
|
|
351
|
+
raise GroupNotFoundError(error_msg)
|
|
352
|
+
|
|
353
|
+
self.logger.debug(f"Target group_id: {self.group_id}")
|
|
354
|
+
|
|
355
|
+
audio_b64 = base64.b64encode(data).decode("utf-8")
|
|
356
|
+
self.logger.debug(f"Encoded audio data to base64: length={len(audio_b64)} chars")
|
|
357
|
+
|
|
358
|
+
request_payload = UpdateFeatureRequest(
|
|
359
|
+
header=RequestHeader(app_id=self.app_id),
|
|
360
|
+
parameter=UpdateFeatureParams(
|
|
361
|
+
s782b4996=S782b4996UpdateFeatureParams(groupId=self.group_id, featureId=uid)
|
|
362
|
+
),
|
|
363
|
+
payload=AudioPayload(resource=AudioResource(audio=audio_b64, sample_rate=sr)),
|
|
364
|
+
).model_dump(exclude_none=True)
|
|
365
|
+
|
|
366
|
+
self.logger.debug(f"Sending update request to {self.endpoint}")
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
response = await self.request(
|
|
370
|
+
endpoint=self.endpoint,
|
|
371
|
+
method="POST",
|
|
372
|
+
json=request_payload,
|
|
373
|
+
cast_to=UpdateFeatureResponse,
|
|
374
|
+
)
|
|
375
|
+
except HTTPStatusError as e:
|
|
376
|
+
error_msg = f"HTTP error during update request: {e}"
|
|
377
|
+
self.logger.bind(request=e.request.content, response=e.response.text).error(error_msg)
|
|
378
|
+
raise VPRError(error_msg) from e
|
|
379
|
+
except RetryError as e:
|
|
380
|
+
error_msg = f"Retry error during verify request: {e}"
|
|
381
|
+
self.logger.error(error_msg)
|
|
382
|
+
raise VPRError(error_msg) from e
|
|
383
|
+
|
|
384
|
+
self.logger.debug(
|
|
385
|
+
f"Received response with code={response.header.code}, message='{response.header.message}'"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if response.header.code != 0:
|
|
389
|
+
error_msg = (
|
|
390
|
+
f"Failed to update feature: {response.header.message} (code={response.header.code})"
|
|
391
|
+
)
|
|
392
|
+
self.logger.error(error_msg)
|
|
393
|
+
raise VPRError(error_msg)
|
|
394
|
+
|
|
395
|
+
text = response.payload.update_feature_res.text
|
|
396
|
+
self.logger.debug(f"Response payload text (base64): {text[:100]}... (truncated)")
|
|
397
|
+
|
|
398
|
+
# Base-64 decode the model from text
|
|
399
|
+
obj_str = base64.b64decode(text).decode("utf-8")
|
|
400
|
+
self.logger.debug(f"Decoded payload (JSON): {obj_str}")
|
|
401
|
+
|
|
402
|
+
obj_json = json.loads(obj_str)
|
|
403
|
+
obj = UpdateFeatureResult.model_validate(obj_json)
|
|
404
|
+
self.logger.debug(f"Parsed UpdateFeatureResult: msg={obj.msg}")
|
|
405
|
+
self.logger.info(
|
|
406
|
+
f"Feature updated successfully: feature_id={uid}, group_id={self.group_id}"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
async def verify(self, uid: str, data: bytes, sr: int) -> float:
|
|
410
|
+
self.logger.info(f"Verifying feature: uid={uid}, sr={sr}, data_size={len(data)} bytes")
|
|
411
|
+
|
|
412
|
+
if not self.group_id:
|
|
413
|
+
error_msg = "Group ID is not set. Cannot verify feature. Please create a group first."
|
|
414
|
+
self.logger.error(error_msg)
|
|
415
|
+
raise GroupNotFoundError(error_msg)
|
|
416
|
+
|
|
417
|
+
self.logger.debug(f"Target group_id: {self.group_id}")
|
|
418
|
+
self.logger.debug(f"Target dst_feature_id: {uid}")
|
|
419
|
+
|
|
420
|
+
audio_b64 = base64.b64encode(data).decode("utf-8")
|
|
421
|
+
self.logger.debug(f"Encoded audio data to base64: length={len(audio_b64)} chars")
|
|
422
|
+
|
|
423
|
+
request_payload = SearchScoreFeaRequest(
|
|
424
|
+
header=RequestHeader(app_id=self.app_id),
|
|
425
|
+
parameter=SearchScoreFeaParams(
|
|
426
|
+
s782b4996=S782b4996SearchScoreFeaParams(groupId=self.group_id, dstFeatureId=uid)
|
|
427
|
+
),
|
|
428
|
+
payload=AudioPayload(resource=AudioResource(audio=audio_b64, sample_rate=sr)),
|
|
429
|
+
).model_dump(exclude_none=True)
|
|
430
|
+
|
|
431
|
+
self.logger.debug(f"Sending verify request to {self.endpoint}")
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
response = await self.request(
|
|
435
|
+
endpoint=self.endpoint,
|
|
436
|
+
method="POST",
|
|
437
|
+
json=request_payload,
|
|
438
|
+
cast_to=SearchScoreFeaResponse,
|
|
439
|
+
)
|
|
440
|
+
except HTTPStatusError as e:
|
|
441
|
+
error_msg = f"HTTP error during verify request: {e}"
|
|
442
|
+
self.logger.bind(request=e.request.content, response=e.response.text).error(error_msg)
|
|
443
|
+
raise VPRError(error_msg) from e
|
|
444
|
+
except RetryError as e:
|
|
445
|
+
error_msg = f"Retry error during verify request: {e}"
|
|
446
|
+
self.logger.error(error_msg)
|
|
447
|
+
raise VPRError(error_msg) from e
|
|
448
|
+
|
|
449
|
+
self.logger.debug(
|
|
450
|
+
f"Received response with code={response.header.code}, message='{response.header.message}'"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
if response.header.code != 0:
|
|
454
|
+
error_msg = (
|
|
455
|
+
f"Failed to verify feature: {response.header.message} (code={response.header.code})"
|
|
456
|
+
)
|
|
457
|
+
self.logger.error(error_msg)
|
|
458
|
+
raise VPRError(error_msg)
|
|
459
|
+
|
|
460
|
+
text = response.payload.search_score_fea_res.text
|
|
461
|
+
self.logger.debug(f"Response payload text (base64): {text[:100]}... (truncated)")
|
|
462
|
+
|
|
463
|
+
# Base-64 decode the model from text
|
|
464
|
+
obj_str = base64.b64decode(text).decode("utf-8")
|
|
465
|
+
self.logger.debug(f"Decoded payload (JSON): {obj_str}")
|
|
466
|
+
|
|
467
|
+
obj_json = json.loads(obj_str)
|
|
468
|
+
obj = SearchScoreFeaResult.model_validate(obj_json)
|
|
469
|
+
self.logger.bind(feature_id=uid, score=obj.score).debug(
|
|
470
|
+
f"Parsed SearchScoreFeaResult: feature_id={obj.feature_id}, score={obj.score}"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if uid != obj.feature_id:
|
|
474
|
+
error_msg = (
|
|
475
|
+
f"Feature ID mismatch after verification: expected={uid}, got={obj.feature_id}"
|
|
476
|
+
)
|
|
477
|
+
self.logger.bind(expected=uid, got=obj.feature_id).error(error_msg)
|
|
478
|
+
raise VPRError(error_msg)
|
|
479
|
+
|
|
480
|
+
self.logger.bind(feature_id=uid, score=obj.score).info(
|
|
481
|
+
f"Feature verified successfully: feature_id={uid}, score={obj.score}"
|
|
482
|
+
)
|
|
483
|
+
return obj.score
|