volcengine-python-sdk-aicc 0.0.41__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 (60) hide show
  1. bytedance/__init__.py +4 -0
  2. bytedance/jeddak_secure_channel/__init__.py +28 -0
  3. bytedance/jeddak_secure_channel/client.py +305 -0
  4. bytedance/jeddak_secure_channel/config.py +171 -0
  5. bytedance/jeddak_secure_channel/consts.py +5 -0
  6. bytedance/jeddak_secure_channel/crypto/__init__.py +14 -0
  7. bytedance/jeddak_secure_channel/crypto/aes.py +174 -0
  8. bytedance/jeddak_secure_channel/crypto/envelope.py +395 -0
  9. bytedance/jeddak_secure_channel/crypto/rsa.py +164 -0
  10. bytedance/jeddak_secure_channel/error.py +131 -0
  11. bytedance/jeddak_secure_channel/log/__init__.py +23 -0
  12. bytedance/jeddak_secure_channel/log/config.py +31 -0
  13. bytedance/jeddak_secure_channel/log/logger.py +88 -0
  14. bytedance/jeddak_secure_channel/logger_factory.py +134 -0
  15. bytedance/jeddak_secure_channel/ra.py +772 -0
  16. bytedance/jeddak_secure_channel/server.py +745 -0
  17. bytedance/jeddak_secure_channel/service/__init__.py +16 -0
  18. bytedance/jeddak_secure_channel/service/eps.py +133 -0
  19. bytedance/jeddak_secure_channel/service/eps_models.py +47 -0
  20. bytedance/jeddak_secure_channel/service/ras.py +88 -0
  21. bytedance/jeddak_secure_channel/service/tks.py +51 -0
  22. bytedance/jeddak_secure_channel/service/tks_models.py +334 -0
  23. bytedance/jeddak_secure_channel/utils.py +469 -0
  24. bytedance/jeddak_secure_model/model_automation_tool.py +181 -0
  25. bytedance/jeddak_secure_model/model_encryption.py +416 -0
  26. bytedance/jeddak_secure_model/model_hub_admin.py +116 -0
  27. bytedance/jeddak_secure_model/pcc_utils.py +279 -0
  28. bytedance/jeddak_secure_model/rds_utils.py +63 -0
  29. bytedance/tks/attester.py +41 -0
  30. bytedance/tks/client.py +629 -0
  31. bytedance/tks/consts.py +122 -0
  32. bytedance/tks/crypto/aes.py +47 -0
  33. bytedance/tks/crypto/ec.py +39 -0
  34. bytedance/tks/exceptions.py +22 -0
  35. bytedance/volcengine_aicc_sdk/__init__.py +14 -0
  36. bytedance/volcengine_aicc_sdk/aicc_client.py +277 -0
  37. bytedance/volcengine_aicc_sdk/aicc_inference.py +64 -0
  38. bytedance/volcengine_aicc_sdk/ark_inference_client.py +343 -0
  39. bytedance/volcengine_aicc_sdk/asr_inference_client.py +158 -0
  40. bytedance/volcengine_aicc_sdk/asr_websocket_client.py +705 -0
  41. bytedance/volcengine_aicc_sdk/auc_inference_client.py +305 -0
  42. bytedance/volcengine_aicc_sdk/common/__init__.py +0 -0
  43. bytedance/volcengine_aicc_sdk/common/aes.py +136 -0
  44. bytedance/volcengine_aicc_sdk/common/constant.py +4 -0
  45. bytedance/volcengine_aicc_sdk/common/error.py +21 -0
  46. bytedance/volcengine_aicc_sdk/common/log/__init__.py +24 -0
  47. bytedance/volcengine_aicc_sdk/common/log/config.py +31 -0
  48. bytedance/volcengine_aicc_sdk/common/log/logger.py +88 -0
  49. bytedance/volcengine_aicc_sdk/common/tos_utils.py +341 -0
  50. bytedance/volcengine_aicc_sdk/common/utils.py +45 -0
  51. bytedance/volcengine_aicc_sdk/cu_inference_client.py +292 -0
  52. bytedance/volcengine_aicc_sdk/deepseek_inference_client.py +501 -0
  53. bytedance/volcengine_aicc_sdk/secure_http_client.py +406 -0
  54. bytedance/volcengine_aicc_sdk/seedance_inference_client.py +500 -0
  55. bytedance/volcengine_aicc_sdk/seedream_inference_client.py +213 -0
  56. volcengine_python_sdk_aicc-0.0.41.dist-info/METADATA +79 -0
  57. volcengine_python_sdk_aicc-0.0.41.dist-info/RECORD +60 -0
  58. volcengine_python_sdk_aicc-0.0.41.dist-info/WHEEL +5 -0
  59. volcengine_python_sdk_aicc-0.0.41.dist-info/licenses/LICENSE +202 -0
  60. volcengine_python_sdk_aicc-0.0.41.dist-info/top_level.txt +1 -0
@@ -0,0 +1,705 @@
1
+ """
2
+ 流式语音识别大模型机密推理客户端
3
+ """
4
+ __all__ = ["AsrWebSocketClient"]
5
+
6
+ from ..jeddak_secure_channel.crypto import AesKey
7
+
8
+ """
9
+ SAUC WebSocket demo client.
10
+
11
+ Usage:
12
+ 1. Minimal run:
13
+ python3 tests/sauc_websocket_demo.py --file tests/test-sound.mp3 --stop-on-first-result
14
+
15
+ 2. Override any request fields with inline JSON:
16
+ python3 tests/sauc_websocket_demo.py --file tests/test-sound.mp3 \
17
+ --request-json '{"request":{"enable_nonstream":true,"show_utterances":true}}'
18
+
19
+ 3. Override any request fields with a JSON file:
20
+ python3 tests/sauc_websocket_demo.py --file tests/test-sound.mp3 \
21
+ --request-json-file /path/to/sauc_request.json
22
+
23
+ Notes:
24
+ - CLI keeps only commonly needed options.
25
+ - Any SAUC full client request fields can still be passed through JSON overrides.
26
+ """
27
+
28
+ import io
29
+ import json
30
+ import gzip
31
+ import subprocess
32
+ import struct
33
+ import threading
34
+ import time
35
+ import uuid
36
+ import wave
37
+ from dataclasses import dataclass
38
+ from typing import Any, Dict, Iterator, List, Optional, Tuple
39
+
40
+ try:
41
+ from websocket import WebSocket, WebSocketConnectionClosedException, WebSocketTimeoutException, create_connection
42
+ except ImportError:
43
+ try:
44
+ from websocket._core import WebSocket
45
+ from websocket._exceptions import WebSocketConnectionClosedException, WebSocketTimeoutException
46
+ from websocket._app import create_connection
47
+ except ImportError:
48
+ WebSocket = object
49
+ WebSocketConnectionClosedException = ConnectionError
50
+ WebSocketTimeoutException = TimeoutError
51
+ create_connection = None
52
+ import bytedance.jeddak_secure_channel as jsc
53
+ from .common.log import logger
54
+ from .secure_http_client import AICCConfig
55
+
56
+ # 常量定义
57
+ DEFAULT_SAMPLE_RATE = 16000
58
+ DEFAULT_MODEL_NAME = "bigmodel"
59
+ INTER_PACKET_DRAIN_TIMEOUT_SECONDS = 0.02
60
+ APP_PING = "__aicc_ws_ping__"
61
+ APP_PONG = "__aicc_ws_pong__"
62
+ DEFAULT_FULL_CLIENT_REQUEST = {
63
+ "audio": {
64
+ "format": "wav",
65
+ "codec": "raw",
66
+ "rate": DEFAULT_SAMPLE_RATE,
67
+ "bits": 16,
68
+ "channel": 1,
69
+ },
70
+ "request": {
71
+ "model_name": DEFAULT_MODEL_NAME,
72
+ },
73
+ }
74
+
75
+
76
+ class ProtocolVersion:
77
+ V1 = 0b0001
78
+
79
+
80
+ class MessageType:
81
+ CLIENT_FULL_REQUEST = 0b0001
82
+ CLIENT_AUDIO_ONLY_REQUEST = 0b0010
83
+ SERVER_FULL_RESPONSE = 0b1001
84
+ SERVER_ERROR_RESPONSE = 0b1111
85
+
86
+
87
+ class MessageTypeSpecificFlags:
88
+ NO_SEQUENCE = 0b0000
89
+ POS_SEQUENCE = 0b0001
90
+ NEG_SEQUENCE = 0b0010
91
+ NEG_WITH_SEQUENCE = 0b0011
92
+
93
+
94
+ class SerializationType:
95
+ NO_SERIALIZATION = 0b0000
96
+ JSON = 0b0001
97
+
98
+
99
+ class CompressionType:
100
+ GZIP = 0b0001
101
+
102
+
103
+ class AsrRequestHeader:
104
+ def __init__(self):
105
+ self.message_type = MessageType.CLIENT_FULL_REQUEST
106
+ self.message_type_specific_flags = MessageTypeSpecificFlags.POS_SEQUENCE
107
+ self.serialization_type = SerializationType.JSON
108
+ self.compression_type = CompressionType.GZIP
109
+ self.reserved_data = bytes([0x00])
110
+
111
+ def with_message_type(self, message_type: int) -> "AsrRequestHeader":
112
+ self.message_type = message_type
113
+ return self
114
+
115
+ def with_message_type_specific_flags(self, flags: int) -> "AsrRequestHeader":
116
+ self.message_type_specific_flags = flags
117
+ return self
118
+
119
+ def with_serialization_type(self, serialization_type: int) -> "AsrRequestHeader":
120
+ self.serialization_type = serialization_type
121
+ return self
122
+
123
+ def with_compression_type(self, compression_type: int) -> "AsrRequestHeader":
124
+ self.compression_type = compression_type
125
+ return self
126
+
127
+ def with_reserved_data(self, reserved_data: bytes) -> "AsrRequestHeader":
128
+ self.reserved_data = reserved_data
129
+ return self
130
+
131
+ def to_bytes(self) -> bytes:
132
+ header = bytearray()
133
+ header.append((ProtocolVersion.V1 << 4) | 1)
134
+ header.append((self.message_type << 4) | self.message_type_specific_flags)
135
+ header.append((self.serialization_type << 4) | self.compression_type)
136
+ header.extend(self.reserved_data)
137
+ return bytes(header)
138
+
139
+ @staticmethod
140
+ def default_header() -> "AsrRequestHeader":
141
+ return AsrRequestHeader()
142
+
143
+
144
+ @dataclass
145
+ class AsrResponse:
146
+ code: int = 0
147
+ event: int = 0
148
+ is_last_package: bool = False
149
+ payload_sequence: int = 0
150
+ payload_size: int = 0
151
+ payload_msg: Optional[Dict[str, Any]] = None
152
+
153
+ def to_dict(self) -> Dict[str, Any]:
154
+ return {
155
+ "code": self.code,
156
+ "event": self.event,
157
+ "is_last_package": self.is_last_package,
158
+ "payload_sequence": self.payload_sequence,
159
+ "payload_size": self.payload_size,
160
+ "payload_msg": self.payload_msg,
161
+ }
162
+
163
+ def is_terminal(self) -> bool:
164
+ return self.is_last_package or self.code != 0
165
+
166
+
167
+ class _SessionContext:
168
+ """Holds per-execution state to support concurrent access."""
169
+
170
+ def __init__(self, client: "AsrWebSocketClient"):
171
+ self.client = client
172
+ self.conn: Optional[WebSocket] = None
173
+ self.seq: int = 1
174
+ self.encrypt_key = None
175
+ self.aes_key = AesKey.generate() if client.jsc_client else None
176
+
177
+ def connect(self) -> None:
178
+ headers = self.client._build_auth_headers(
179
+ self.client.api_key, self.client.resource_id, self.client._build_connection_headers()
180
+ )
181
+ self.conn = create_connection(
182
+ self.client.base_url,
183
+ header=[f"{key}: {value}" for key, value in headers.items()],
184
+ timeout=self.client.connect_timeout,
185
+ )
186
+ self.conn.settimeout(self.client.receive_timeout)
187
+ logger.info("Connected to %s", self.client.base_url)
188
+
189
+ def close(self) -> None:
190
+ if self.conn is not None:
191
+ self.conn.close()
192
+ self.conn = None
193
+
194
+ def encode_outbound_frame(self, payload: bytes) -> bytes:
195
+ if self.client.jsc_client:
196
+ msg, encrypt_key = self.client.jsc_client.encrypt_with_response(payload, self.aes_key)
197
+ if encrypt_key:
198
+ self.encrypt_key = encrypt_key
199
+ if isinstance(msg, str):
200
+ msg = msg.encode("utf-8")
201
+ return msg
202
+ else:
203
+ return payload
204
+
205
+ def decode_inbound_frame(self, payload: bytes) -> bytes:
206
+ if self.encrypt_key:
207
+ msg = self.encrypt_key.decrypt(payload)
208
+ if isinstance(msg, str):
209
+ msg = msg.encode("utf-8")
210
+ return msg
211
+ else:
212
+ return payload
213
+
214
+ def send_binary(self, payload: bytes) -> None:
215
+ if self.conn is None:
216
+ raise RuntimeError("WebSocket connection is not established")
217
+ self.conn.send_binary(self.encode_outbound_frame(payload))
218
+
219
+ def send_text(self, payload: str) -> None:
220
+ if self.conn is None:
221
+ raise RuntimeError("WebSocket connection is not established")
222
+ self.conn.send(payload)
223
+
224
+ def send_app_pong(self) -> None:
225
+ payload = json.dumps({"type": APP_PONG}, separators=(",", ":"))
226
+ if self.client.jsc_client:
227
+ self.send_binary(payload.encode("utf-8"))
228
+ logger.info("[WS_HEARTBEAT] client sent encrypted app pong")
229
+ return
230
+ self.send_text(payload)
231
+ logger.info("[WS_HEARTBEAT] client sent app pong")
232
+
233
+ def _maybe_handle_control_payload(self, payload) -> bool:
234
+ text: str
235
+ if isinstance(payload, bytes):
236
+ try:
237
+ text = payload.decode("utf-8")
238
+ except UnicodeDecodeError:
239
+ return False
240
+ else:
241
+ text = payload
242
+
243
+ try:
244
+ payload_obj = json.loads(text)
245
+ except (TypeError, json.JSONDecodeError):
246
+ return False
247
+
248
+ if not isinstance(payload_obj, dict):
249
+ return False
250
+
251
+ message_type = payload_obj.get("type")
252
+ if message_type == APP_PING:
253
+ logger.info("[WS_HEARTBEAT] client received app ping")
254
+ self.send_app_pong()
255
+ return True
256
+ if message_type == APP_PONG:
257
+ logger.info("[WS_HEARTBEAT] client received app pong")
258
+ return True
259
+ return False
260
+
261
+ def recv_one(self, timeout: float) -> Optional[AsrResponse]:
262
+ if self.conn is None:
263
+ raise RuntimeError("WebSocket connection is not established")
264
+
265
+ deadline = time.monotonic() + max(timeout, 0.0)
266
+ while True:
267
+ remaining = deadline - time.monotonic()
268
+ if remaining <= 0:
269
+ return None
270
+
271
+ self.conn.settimeout(remaining)
272
+ try:
273
+ message = self.conn.recv()
274
+ except WebSocketTimeoutException:
275
+ return None
276
+ except WebSocketConnectionClosedException as exc:
277
+ raise RuntimeError("WebSocket connection closed unexpectedly") from exc
278
+ finally:
279
+ self.conn.settimeout(self.client.receive_timeout)
280
+
281
+ if isinstance(message, str):
282
+ if self._maybe_handle_control_payload(message):
283
+ continue
284
+ logger.info("Received text message: %s", message)
285
+ continue
286
+
287
+ decoded_message = self.decode_inbound_frame(message)
288
+ if self._maybe_handle_control_payload(decoded_message):
289
+ continue
290
+ return self.client._parse_response(decoded_message)
291
+
292
+ def drain_messages(self, timeout: float) -> Iterator[AsrResponse]:
293
+ while True:
294
+ response = self.recv_one(timeout)
295
+ if response is None:
296
+ return
297
+ yield response
298
+ timeout = 0.1
299
+
300
+ def send_full_client_request(self) -> Iterator[AsrResponse]:
301
+ self.send_binary(
302
+ self.client._build_full_client_request(
303
+ self.seq,
304
+ model_name=self.client.model_name,
305
+ enable_nonstream=self.client.enable_nonstream,
306
+ enable_itn=self.client.enable_itn,
307
+ enable_punc=self.client.enable_punc,
308
+ enable_ddc=self.client.enable_ddc,
309
+ show_utterances=self.client.show_utterances,
310
+ result_type=self.client.result_type,
311
+ enable_accelerate_text=self.client.enable_accelerate_text,
312
+ vad_segment_duration=self.client.vad_segment_duration,
313
+ full_client_request_overrides=self.client.full_client_request_overrides,
314
+ )
315
+ )
316
+ logger.info(f"Sent full client request with seq: {self.seq}")
317
+ self.seq += 1
318
+ yield from self.drain_messages(self.client.connect_timeout)
319
+
320
+ def send_audio_stream(self, content: bytes, segment_size: int) -> Iterator[AsrResponse]:
321
+ audio_segments = self.client._split_audio(content, segment_size)
322
+ total_segments = len(audio_segments)
323
+
324
+ for index, segment in enumerate(audio_segments):
325
+ is_last = index == total_segments - 1
326
+ request = self.client._build_audio_only_request(self.seq, segment, is_last=is_last)
327
+ self.send_binary(request)
328
+ logger.debug(f"Sent audio segment with seq: {self.seq} (last: {is_last})")
329
+ if not is_last:
330
+ self.seq += 1
331
+
332
+ if self.client.segment_duration > 0:
333
+ time.sleep(self.client.segment_duration / 1000)
334
+
335
+ yield from self.drain_messages(INTER_PACKET_DRAIN_TIMEOUT_SECONDS)
336
+
337
+ def receive_final_messages(self) -> Iterator[AsrResponse]:
338
+ deadline = time.monotonic() + self.client.final_wait_seconds
339
+ while time.monotonic() < deadline:
340
+ response = self.recv_one(min(self.client.receive_timeout, deadline - time.monotonic()))
341
+ if response is None:
342
+ continue
343
+ yield response
344
+ if response.is_terminal():
345
+ return
346
+
347
+
348
+ class AsrWebSocketClient:
349
+ _jsc_client_cache: Dict[str, Any] = {}
350
+ _jsc_client_lock = threading.Lock()
351
+
352
+ def __init__(
353
+ self,
354
+ base_url: str = "",
355
+ api_key: str = "",
356
+ endpoint: str = "",
357
+ aicc_config: AICCConfig = None,
358
+ **kwargs
359
+ ):
360
+ self.base_url = base_url
361
+ self.api_key = api_key
362
+ self.endpoint = endpoint
363
+ self.resource_id = self.endpoint
364
+ self.aicc_config = aicc_config
365
+ self.kwargs = kwargs
366
+
367
+ self.seq = self.kwargs.get("seq", 1)
368
+ self.segment_duration = self.kwargs.get("segment_duration", 200)
369
+
370
+ self.model_name = self.kwargs.get("model_name", None)
371
+ self.enable_nonstream = self.kwargs.get("enable_nonstream", None)
372
+ self.enable_itn = self.kwargs.get("enable_itn", None)
373
+ self.enable_punc = self.kwargs.get("enable_punc", None)
374
+ self.enable_ddc = self.kwargs.get("enable_ddc", None)
375
+ self.show_utterances = self.kwargs.get("show_utterances", None)
376
+ self.result_type = self.kwargs.get("result_type", None)
377
+ self.enable_accelerate_text = self.kwargs.get("enable_accelerate_text", None)
378
+ self.vad_segment_duration = self.kwargs.get("vad_segment_duration", None)
379
+ self.full_client_request_overrides = self.kwargs.get("full_client_request_overrides", {})
380
+ self.extra_headers = self.kwargs.get("extra_headers", {})
381
+ self.connect_timeout = self.kwargs.get("connect_timeout", 10.0)
382
+ self.receive_timeout = self.kwargs.get("receive_timeout", 1.0)
383
+ self.final_wait_seconds = self.kwargs.get("final_wait_seconds", 15.0)
384
+ self.is_secure = self.kwargs.get("is_secure", True)
385
+ self.enable_app_heartbeat = self.kwargs.get("enable_app_heartbeat", True)
386
+
387
+ # 将http地址转成wss地址
388
+ if self.base_url.startswith("https://"):
389
+ self.base_url = self.base_url.replace("https", "wss")
390
+ elif self.base_url.startswith("http://"):
391
+ self.base_url = self.base_url.replace("http", "wss")
392
+
393
+ asr_suffix = self.kwargs.get("asr_suffix", "/sauc/bigmodel_async")
394
+ if not self.base_url.endswith(asr_suffix):
395
+ self.base_url = self.base_url + asr_suffix
396
+
397
+ self.jsc_client = None
398
+ if not self.aicc_config:
399
+ if self.is_secure:
400
+ self.aicc_config = AICCConfig(ra_url=base_url, auth_token=api_key)
401
+ if self.aicc_config:
402
+ self.jsc_client = self._get_or_create_jsc_client(aicc_config=self.aicc_config)
403
+
404
+ self.AICC_SDK_VERSION = "0.1.0"
405
+
406
+ @classmethod
407
+ def _get_or_create_jsc_client(cls, config_dict: Dict[str, Any] = None, aicc_config: AICCConfig = None) -> Any:
408
+ if aicc_config is not None:
409
+ config_dict = aicc_config.get_jsc_config()
410
+ cache_key = json.dumps(config_dict, sort_keys=True)
411
+ if cache_key in cls._jsc_client_cache:
412
+ return cls._jsc_client_cache[cache_key]
413
+ with cls._jsc_client_lock:
414
+ if cache_key in cls._jsc_client_cache:
415
+ return cls._jsc_client_cache[cache_key]
416
+ secure_channel_config = jsc.ClientConfig.from_dict(config_dict)
417
+ client = jsc.Client(secure_channel_config)
418
+ cls._jsc_client_cache[cache_key] = client
419
+ return client
420
+
421
+ def _build_auth_headers(
422
+ self,
423
+ api_key: str,
424
+ resource_id: str,
425
+ extra_headers: Optional[Dict[str, str]] = None,
426
+ ) -> Dict[str, str]:
427
+ headers = {
428
+ "X-Api-Resource-Id": resource_id,
429
+ "X-Api-Request-Id": str(uuid.uuid4()),
430
+ "X-Api-Connect-Id": str(uuid.uuid4()),
431
+ "X-Api-Sequence": "-1",
432
+ "X-Api-Key": api_key,
433
+ "Authorization": f"Bearer {api_key}",
434
+ }
435
+ if extra_headers:
436
+ headers.update(extra_headers)
437
+ return headers
438
+
439
+ def _convert_audio_to_wav_bytes(self, audio_path: str, sample_rate: int = DEFAULT_SAMPLE_RATE) -> bytes:
440
+ cmd = [
441
+ "ffmpeg",
442
+ "-v",
443
+ "quiet",
444
+ "-y",
445
+ "-i",
446
+ audio_path,
447
+ "-acodec",
448
+ "pcm_s16le",
449
+ "-ac",
450
+ "1",
451
+ "-ar",
452
+ str(sample_rate),
453
+ "-f",
454
+ "wav",
455
+ "-",
456
+ ]
457
+ try:
458
+ result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
459
+ except subprocess.CalledProcessError as exc:
460
+ message = exc.stderr.decode("utf-8", errors="replace")
461
+ raise RuntimeError(f"Audio conversion failed: {message}") from exc
462
+
463
+ try:
464
+ with open("converted.wav", "wb") as file_handle:
465
+ file_handle.write(result.stdout)
466
+ except OSError as exc:
467
+ logger.warning("Failed to save converted.wav: %s", exc)
468
+
469
+ return result.stdout
470
+
471
+ def _is_wav_file(self, data: bytes) -> bool:
472
+ if len(data) < 12:
473
+ return False
474
+ return data[:4] == b"RIFF" and data[8:12] == b"WAVE"
475
+
476
+ def _read_audio_data(self, file_path: str) -> bytes:
477
+ with open(file_path, "rb") as file_handle:
478
+ content = file_handle.read()
479
+
480
+ if self._is_wav_file(content):
481
+ return content
482
+
483
+ logger.info("Converting audio to WAV format...")
484
+ return self._convert_audio_to_wav_bytes(file_path, DEFAULT_SAMPLE_RATE)
485
+
486
+ def _read_wav_info(self, data: bytes) -> Tuple[int, int, int, int]:
487
+ try:
488
+ with wave.open(io.BytesIO(data), "rb") as wav_file:
489
+ channel_num = wav_file.getnchannels()
490
+ samp_width = wav_file.getsampwidth()
491
+ frame_rate = wav_file.getframerate()
492
+ frame_count = wav_file.getnframes()
493
+ except wave.Error as exc:
494
+ raise ValueError(f"Invalid WAV file: {exc}") from exc
495
+
496
+ return channel_num, samp_width, frame_rate, frame_count
497
+
498
+ def _get_segment_size(self, content: bytes) -> int:
499
+ channel_num, samp_width, frame_rate, _ = self._read_wav_info(content)
500
+ size_per_second = channel_num * samp_width * frame_rate
501
+ return size_per_second * self.segment_duration // 1000
502
+
503
+ def _build_connection_headers(self) -> Dict[str, str]:
504
+ headers = dict(self.extra_headers)
505
+ if self.enable_app_heartbeat:
506
+ headers["X-AICC-App-Heartbeat"] = "true"
507
+ if self.aicc_config:
508
+ headers["X-AICC-Encryption-Enable"] = "true"
509
+ headers["X-AICC-Encryption-SDK"] = "aicc"
510
+ headers["X-AICC-Encryption-Version"] = self.AICC_SDK_VERSION
511
+ return headers
512
+
513
+ def _gzip_compress(self, data: bytes) -> bytes:
514
+ return gzip.compress(data)
515
+
516
+ def _gzip_decompress(self, data: bytes) -> bytes:
517
+ return gzip.decompress(data)
518
+
519
+ def _parse_response(self, message: bytes) -> AsrResponse:
520
+ if not message:
521
+ raise ValueError("Cannot parse an empty websocket frame")
522
+
523
+ response = AsrResponse()
524
+ header_size = message[0] & 0x0F
525
+ message_type = message[1] >> 4
526
+ message_type_specific_flags = message[1] & 0x0F
527
+ serialization_method = message[2] >> 4
528
+ message_compression = message[2] & 0x0F
529
+ payload = message[header_size * 4:]
530
+
531
+ if message_type_specific_flags & 0x01:
532
+ response.payload_sequence = struct.unpack(">i", payload[:4])[0]
533
+ payload = payload[4:]
534
+ if message_type_specific_flags & 0x02:
535
+ response.is_last_package = True
536
+ if message_type_specific_flags & 0x04:
537
+ response.event = struct.unpack(">i", payload[:4])[0]
538
+ payload = payload[4:]
539
+
540
+ if message_type == MessageType.SERVER_FULL_RESPONSE:
541
+ response.payload_size = struct.unpack(">I", payload[:4])[0]
542
+ payload = payload[4:]
543
+ elif message_type == MessageType.SERVER_ERROR_RESPONSE:
544
+ response.code = struct.unpack(">i", payload[:4])[0]
545
+ response.payload_size = struct.unpack(">I", payload[4:8])[0]
546
+ payload = payload[8:]
547
+
548
+ if not payload:
549
+ return response
550
+
551
+ if message_compression == CompressionType.GZIP:
552
+ try:
553
+ payload = self._gzip_decompress(payload)
554
+ except Exception as exc:
555
+ logger.error("Failed to decompress payload: %s", exc)
556
+ return response
557
+
558
+ try:
559
+ if serialization_method == SerializationType.JSON:
560
+ response.payload_msg = json.loads(payload.decode("utf-8"))
561
+ except Exception as exc:
562
+ logger.error("Failed to parse payload: %s", exc)
563
+
564
+ return response
565
+
566
+ def _deep_merge_dict(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
567
+ merged = dict(base)
568
+ for key, value in override.items():
569
+ if isinstance(value, dict) and isinstance(merged.get(key), dict):
570
+ merged[key] = self._deep_merge_dict(merged[key], value)
571
+ else:
572
+ merged[key] = value
573
+ return merged
574
+
575
+ def _build_full_request_payload(
576
+ self,
577
+ *,
578
+ model_name: Optional[str] = None,
579
+ enable_nonstream: Optional[bool] = None,
580
+ enable_itn: Optional[bool] = None,
581
+ enable_punc: Optional[bool] = None,
582
+ enable_ddc: Optional[bool] = None,
583
+ show_utterances: Optional[bool] = None,
584
+ result_type: Optional[str] = None,
585
+ enable_accelerate_text: Optional[bool] = None,
586
+ vad_segment_duration: Optional[int] = None,
587
+ full_client_request_overrides: Optional[Dict[str, Any]] = None,
588
+ ) -> Dict[str, Any]:
589
+ payload = self._deep_merge_dict(DEFAULT_FULL_CLIENT_REQUEST, full_client_request_overrides or {})
590
+ request_payload = dict(payload.get("request", {}))
591
+
592
+ if model_name is not None:
593
+ request_payload["model_name"] = model_name
594
+ if enable_nonstream is not None:
595
+ request_payload["enable_nonstream"] = enable_nonstream
596
+ if enable_itn is not None:
597
+ request_payload["enable_itn"] = enable_itn
598
+ if enable_punc is not None:
599
+ request_payload["enable_punc"] = enable_punc
600
+ if enable_ddc is not None:
601
+ request_payload["enable_ddc"] = enable_ddc
602
+ if show_utterances is not None:
603
+ request_payload["show_utterances"] = show_utterances
604
+ if result_type is not None:
605
+ request_payload["result_type"] = result_type
606
+ if enable_accelerate_text is not None:
607
+ request_payload["enable_accelerate_text"] = enable_accelerate_text
608
+ if vad_segment_duration is not None:
609
+ request_payload["vad_segment_duration"] = vad_segment_duration
610
+
611
+ payload["request"] = request_payload
612
+ return payload
613
+
614
+ def _build_full_client_request(
615
+ self,
616
+ seq: int,
617
+ *,
618
+ model_name: Optional[str] = None,
619
+ enable_nonstream: Optional[bool] = None,
620
+ enable_itn: Optional[bool] = None,
621
+ enable_punc: Optional[bool] = None,
622
+ enable_ddc: Optional[bool] = None,
623
+ show_utterances: Optional[bool] = None,
624
+ result_type: Optional[str] = None,
625
+ enable_accelerate_text: Optional[bool] = None,
626
+ vad_segment_duration: Optional[int] = None,
627
+ full_client_request_overrides: Optional[Dict[str, Any]] = None,
628
+ ) -> bytes:
629
+ header = AsrRequestHeader.default_header().with_message_type_specific_flags(MessageTypeSpecificFlags.POS_SEQUENCE)
630
+ payload = self._build_full_request_payload(
631
+ model_name=model_name,
632
+ enable_nonstream=enable_nonstream,
633
+ enable_itn=enable_itn,
634
+ enable_punc=enable_punc,
635
+ enable_ddc=enable_ddc,
636
+ show_utterances=show_utterances,
637
+ result_type=result_type,
638
+ enable_accelerate_text=enable_accelerate_text,
639
+ vad_segment_duration=vad_segment_duration,
640
+ full_client_request_overrides=full_client_request_overrides,
641
+ )
642
+ payload_bytes = json.dumps(payload).encode("utf-8")
643
+ compressed_payload = self._gzip_compress(payload_bytes)
644
+
645
+ request = bytearray()
646
+ request.extend(header.to_bytes())
647
+ request.extend(struct.pack(">i", seq))
648
+ request.extend(struct.pack(">I", len(compressed_payload)))
649
+ request.extend(compressed_payload)
650
+ return bytes(request)
651
+
652
+ def _split_audio(self, data: bytes, segment_size: int) -> List[bytes]:
653
+ if segment_size <= 0:
654
+ return []
655
+ return [data[index:index + segment_size] for index in range(0, len(data), segment_size)]
656
+
657
+ def _build_audio_only_request(self, seq: int, segment: bytes, is_last: bool = False) -> bytes:
658
+ header = AsrRequestHeader.default_header()
659
+ if is_last:
660
+ header.with_message_type_specific_flags(MessageTypeSpecificFlags.NEG_WITH_SEQUENCE)
661
+ seq = -seq
662
+ else:
663
+ header.with_message_type_specific_flags(MessageTypeSpecificFlags.POS_SEQUENCE)
664
+ header.with_message_type(MessageType.CLIENT_AUDIO_ONLY_REQUEST)
665
+
666
+ request = bytearray()
667
+ request.extend(header.to_bytes())
668
+ request.extend(struct.pack(">i", seq))
669
+
670
+ compressed_segment = self._gzip_compress(segment)
671
+ request.extend(struct.pack(">I", len(compressed_segment)))
672
+ request.extend(compressed_segment)
673
+ return bytes(request)
674
+
675
+ def execute(self, file_path: str) -> Iterator[AsrResponse]:
676
+ if not file_path:
677
+ raise ValueError("File path is empty")
678
+ if not self.base_url or not (self.base_url.startswith("wss://") or self.base_url.startswith("ws://")):
679
+ raise ValueError("Base URL is empty or invalid (must start with wss:// or ws://)")
680
+ if not self.endpoint:
681
+ raise ValueError("Endpoint is empty")
682
+ if not self.api_key:
683
+ raise ValueError("API key is empty")
684
+
685
+ session = _SessionContext(self)
686
+
687
+ content = self._read_audio_data(file_path)
688
+ segment_size = self._get_segment_size(content)
689
+
690
+ try:
691
+ session.connect()
692
+
693
+ for response in session.send_full_client_request():
694
+ yield response
695
+ if response.is_terminal():
696
+ return
697
+
698
+ for response in session.send_audio_stream(content, segment_size):
699
+ yield response
700
+ if response.is_terminal():
701
+ return
702
+
703
+ yield from session.receive_final_messages()
704
+ finally:
705
+ session.close()