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.
Files changed (192) hide show
  1. audex/__init__.py +9 -0
  2. audex/__main__.py +7 -0
  3. audex/cli/__init__.py +189 -0
  4. audex/cli/apis/__init__.py +12 -0
  5. audex/cli/apis/init/__init__.py +34 -0
  6. audex/cli/apis/init/gencfg.py +130 -0
  7. audex/cli/apis/init/setup.py +330 -0
  8. audex/cli/apis/init/vprgroup.py +125 -0
  9. audex/cli/apis/serve.py +141 -0
  10. audex/cli/args.py +356 -0
  11. audex/cli/exceptions.py +44 -0
  12. audex/cli/helper/__init__.py +0 -0
  13. audex/cli/helper/ansi.py +193 -0
  14. audex/cli/helper/display.py +288 -0
  15. audex/config/__init__.py +64 -0
  16. audex/config/core/__init__.py +30 -0
  17. audex/config/core/app.py +29 -0
  18. audex/config/core/audio.py +45 -0
  19. audex/config/core/logging.py +163 -0
  20. audex/config/core/session.py +11 -0
  21. audex/config/helper/__init__.py +1 -0
  22. audex/config/helper/client/__init__.py +1 -0
  23. audex/config/helper/client/http.py +28 -0
  24. audex/config/helper/client/websocket.py +21 -0
  25. audex/config/helper/provider/__init__.py +1 -0
  26. audex/config/helper/provider/dashscope.py +13 -0
  27. audex/config/helper/provider/unisound.py +18 -0
  28. audex/config/helper/provider/xfyun.py +23 -0
  29. audex/config/infrastructure/__init__.py +31 -0
  30. audex/config/infrastructure/cache.py +51 -0
  31. audex/config/infrastructure/database.py +48 -0
  32. audex/config/infrastructure/recorder.py +32 -0
  33. audex/config/infrastructure/store.py +19 -0
  34. audex/config/provider/__init__.py +18 -0
  35. audex/config/provider/transcription.py +109 -0
  36. audex/config/provider/vpr.py +99 -0
  37. audex/container.py +40 -0
  38. audex/entity/__init__.py +468 -0
  39. audex/entity/doctor.py +109 -0
  40. audex/entity/doctor.pyi +51 -0
  41. audex/entity/fields.py +401 -0
  42. audex/entity/segment.py +115 -0
  43. audex/entity/segment.pyi +38 -0
  44. audex/entity/session.py +133 -0
  45. audex/entity/session.pyi +47 -0
  46. audex/entity/utterance.py +142 -0
  47. audex/entity/utterance.pyi +48 -0
  48. audex/entity/vp.py +68 -0
  49. audex/entity/vp.pyi +35 -0
  50. audex/exceptions.py +157 -0
  51. audex/filters/__init__.py +692 -0
  52. audex/filters/generated/__init__.py +21 -0
  53. audex/filters/generated/doctor.py +987 -0
  54. audex/filters/generated/segment.py +723 -0
  55. audex/filters/generated/session.py +978 -0
  56. audex/filters/generated/utterance.py +939 -0
  57. audex/filters/generated/vp.py +815 -0
  58. audex/helper/__init__.py +1 -0
  59. audex/helper/hash.py +33 -0
  60. audex/helper/mixin.py +65 -0
  61. audex/helper/net.py +19 -0
  62. audex/helper/settings/__init__.py +830 -0
  63. audex/helper/settings/fields.py +317 -0
  64. audex/helper/stream.py +153 -0
  65. audex/injectors/__init__.py +1 -0
  66. audex/injectors/config.py +12 -0
  67. audex/injectors/lifespan.py +7 -0
  68. audex/lib/__init__.py +1 -0
  69. audex/lib/cache/__init__.py +383 -0
  70. audex/lib/cache/inmemory.py +513 -0
  71. audex/lib/database/__init__.py +83 -0
  72. audex/lib/database/sqlite.py +406 -0
  73. audex/lib/exporter.py +189 -0
  74. audex/lib/injectors/__init__.py +1 -0
  75. audex/lib/injectors/cache.py +25 -0
  76. audex/lib/injectors/container.py +47 -0
  77. audex/lib/injectors/exporter.py +26 -0
  78. audex/lib/injectors/recorder.py +33 -0
  79. audex/lib/injectors/server.py +17 -0
  80. audex/lib/injectors/session.py +18 -0
  81. audex/lib/injectors/sqlite.py +24 -0
  82. audex/lib/injectors/store.py +13 -0
  83. audex/lib/injectors/transcription.py +42 -0
  84. audex/lib/injectors/usb.py +12 -0
  85. audex/lib/injectors/vpr.py +65 -0
  86. audex/lib/injectors/wifi.py +7 -0
  87. audex/lib/recorder.py +844 -0
  88. audex/lib/repos/__init__.py +149 -0
  89. audex/lib/repos/container.py +23 -0
  90. audex/lib/repos/database/__init__.py +1 -0
  91. audex/lib/repos/database/sqlite.py +672 -0
  92. audex/lib/repos/decorators.py +74 -0
  93. audex/lib/repos/doctor.py +286 -0
  94. audex/lib/repos/segment.py +302 -0
  95. audex/lib/repos/session.py +285 -0
  96. audex/lib/repos/tables/__init__.py +70 -0
  97. audex/lib/repos/tables/doctor.py +137 -0
  98. audex/lib/repos/tables/segment.py +113 -0
  99. audex/lib/repos/tables/session.py +140 -0
  100. audex/lib/repos/tables/utterance.py +131 -0
  101. audex/lib/repos/tables/vp.py +102 -0
  102. audex/lib/repos/utterance.py +288 -0
  103. audex/lib/repos/vp.py +286 -0
  104. audex/lib/restful.py +251 -0
  105. audex/lib/server/__init__.py +97 -0
  106. audex/lib/server/auth.py +98 -0
  107. audex/lib/server/handlers.py +248 -0
  108. audex/lib/server/templates/index.html.j2 +226 -0
  109. audex/lib/server/templates/login.html.j2 +111 -0
  110. audex/lib/server/templates/static/script.js +68 -0
  111. audex/lib/server/templates/static/style.css +579 -0
  112. audex/lib/server/types.py +123 -0
  113. audex/lib/session.py +503 -0
  114. audex/lib/store/__init__.py +238 -0
  115. audex/lib/store/localfile.py +411 -0
  116. audex/lib/transcription/__init__.py +33 -0
  117. audex/lib/transcription/dashscope.py +525 -0
  118. audex/lib/transcription/events.py +62 -0
  119. audex/lib/usb.py +554 -0
  120. audex/lib/vpr/__init__.py +38 -0
  121. audex/lib/vpr/unisound/__init__.py +185 -0
  122. audex/lib/vpr/unisound/types.py +469 -0
  123. audex/lib/vpr/xfyun/__init__.py +483 -0
  124. audex/lib/vpr/xfyun/types.py +679 -0
  125. audex/lib/websocket/__init__.py +8 -0
  126. audex/lib/websocket/connection.py +485 -0
  127. audex/lib/websocket/pool.py +991 -0
  128. audex/lib/wifi.py +1146 -0
  129. audex/lifespan.py +75 -0
  130. audex/service/__init__.py +27 -0
  131. audex/service/decorators.py +73 -0
  132. audex/service/doctor/__init__.py +652 -0
  133. audex/service/doctor/const.py +36 -0
  134. audex/service/doctor/exceptions.py +96 -0
  135. audex/service/doctor/types.py +54 -0
  136. audex/service/export/__init__.py +236 -0
  137. audex/service/export/const.py +17 -0
  138. audex/service/export/exceptions.py +34 -0
  139. audex/service/export/types.py +21 -0
  140. audex/service/injectors/__init__.py +1 -0
  141. audex/service/injectors/container.py +53 -0
  142. audex/service/injectors/doctor.py +34 -0
  143. audex/service/injectors/export.py +27 -0
  144. audex/service/injectors/session.py +49 -0
  145. audex/service/session/__init__.py +754 -0
  146. audex/service/session/const.py +34 -0
  147. audex/service/session/exceptions.py +67 -0
  148. audex/service/session/types.py +91 -0
  149. audex/types.py +39 -0
  150. audex/utils.py +287 -0
  151. audex/valueobj/__init__.py +81 -0
  152. audex/valueobj/common/__init__.py +1 -0
  153. audex/valueobj/common/auth.py +84 -0
  154. audex/valueobj/common/email.py +16 -0
  155. audex/valueobj/common/ops.py +22 -0
  156. audex/valueobj/common/phone.py +84 -0
  157. audex/valueobj/common/version.py +72 -0
  158. audex/valueobj/session.py +19 -0
  159. audex/valueobj/utterance.py +15 -0
  160. audex/view/__init__.py +51 -0
  161. audex/view/container.py +17 -0
  162. audex/view/decorators.py +303 -0
  163. audex/view/pages/__init__.py +1 -0
  164. audex/view/pages/dashboard/__init__.py +286 -0
  165. audex/view/pages/dashboard/wifi.py +407 -0
  166. audex/view/pages/login.py +110 -0
  167. audex/view/pages/recording.py +348 -0
  168. audex/view/pages/register.py +202 -0
  169. audex/view/pages/sessions/__init__.py +196 -0
  170. audex/view/pages/sessions/details.py +224 -0
  171. audex/view/pages/sessions/export.py +443 -0
  172. audex/view/pages/settings.py +374 -0
  173. audex/view/pages/voiceprint/__init__.py +1 -0
  174. audex/view/pages/voiceprint/enroll.py +195 -0
  175. audex/view/pages/voiceprint/update.py +195 -0
  176. audex/view/static/css/dashboard.css +452 -0
  177. audex/view/static/css/glass.css +22 -0
  178. audex/view/static/css/global.css +541 -0
  179. audex/view/static/css/login.css +386 -0
  180. audex/view/static/css/recording.css +439 -0
  181. audex/view/static/css/register.css +293 -0
  182. audex/view/static/css/sessions/styles.css +501 -0
  183. audex/view/static/css/settings.css +186 -0
  184. audex/view/static/css/voiceprint/enroll.css +43 -0
  185. audex/view/static/css/voiceprint/styles.css +209 -0
  186. audex/view/static/css/voiceprint/update.css +44 -0
  187. audex/view/static/images/logo.svg +95 -0
  188. audex/view/static/js/recording.js +42 -0
  189. audex-1.0.7a3.dist-info/METADATA +361 -0
  190. audex-1.0.7a3.dist-info/RECORD +192 -0
  191. audex-1.0.7a3.dist-info/WHEEL +4 -0
  192. 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