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.
- bytedance/__init__.py +4 -0
- bytedance/jeddak_secure_channel/__init__.py +28 -0
- bytedance/jeddak_secure_channel/client.py +305 -0
- bytedance/jeddak_secure_channel/config.py +171 -0
- bytedance/jeddak_secure_channel/consts.py +5 -0
- bytedance/jeddak_secure_channel/crypto/__init__.py +14 -0
- bytedance/jeddak_secure_channel/crypto/aes.py +174 -0
- bytedance/jeddak_secure_channel/crypto/envelope.py +395 -0
- bytedance/jeddak_secure_channel/crypto/rsa.py +164 -0
- bytedance/jeddak_secure_channel/error.py +131 -0
- bytedance/jeddak_secure_channel/log/__init__.py +23 -0
- bytedance/jeddak_secure_channel/log/config.py +31 -0
- bytedance/jeddak_secure_channel/log/logger.py +88 -0
- bytedance/jeddak_secure_channel/logger_factory.py +134 -0
- bytedance/jeddak_secure_channel/ra.py +772 -0
- bytedance/jeddak_secure_channel/server.py +745 -0
- bytedance/jeddak_secure_channel/service/__init__.py +16 -0
- bytedance/jeddak_secure_channel/service/eps.py +133 -0
- bytedance/jeddak_secure_channel/service/eps_models.py +47 -0
- bytedance/jeddak_secure_channel/service/ras.py +88 -0
- bytedance/jeddak_secure_channel/service/tks.py +51 -0
- bytedance/jeddak_secure_channel/service/tks_models.py +334 -0
- bytedance/jeddak_secure_channel/utils.py +469 -0
- bytedance/jeddak_secure_model/model_automation_tool.py +181 -0
- bytedance/jeddak_secure_model/model_encryption.py +416 -0
- bytedance/jeddak_secure_model/model_hub_admin.py +116 -0
- bytedance/jeddak_secure_model/pcc_utils.py +279 -0
- bytedance/jeddak_secure_model/rds_utils.py +63 -0
- bytedance/tks/attester.py +41 -0
- bytedance/tks/client.py +629 -0
- bytedance/tks/consts.py +122 -0
- bytedance/tks/crypto/aes.py +47 -0
- bytedance/tks/crypto/ec.py +39 -0
- bytedance/tks/exceptions.py +22 -0
- bytedance/volcengine_aicc_sdk/__init__.py +14 -0
- bytedance/volcengine_aicc_sdk/aicc_client.py +277 -0
- bytedance/volcengine_aicc_sdk/aicc_inference.py +64 -0
- bytedance/volcengine_aicc_sdk/ark_inference_client.py +343 -0
- bytedance/volcengine_aicc_sdk/asr_inference_client.py +158 -0
- bytedance/volcengine_aicc_sdk/asr_websocket_client.py +705 -0
- bytedance/volcengine_aicc_sdk/auc_inference_client.py +305 -0
- bytedance/volcengine_aicc_sdk/common/__init__.py +0 -0
- bytedance/volcengine_aicc_sdk/common/aes.py +136 -0
- bytedance/volcengine_aicc_sdk/common/constant.py +4 -0
- bytedance/volcengine_aicc_sdk/common/error.py +21 -0
- bytedance/volcengine_aicc_sdk/common/log/__init__.py +24 -0
- bytedance/volcengine_aicc_sdk/common/log/config.py +31 -0
- bytedance/volcengine_aicc_sdk/common/log/logger.py +88 -0
- bytedance/volcengine_aicc_sdk/common/tos_utils.py +341 -0
- bytedance/volcengine_aicc_sdk/common/utils.py +45 -0
- bytedance/volcengine_aicc_sdk/cu_inference_client.py +292 -0
- bytedance/volcengine_aicc_sdk/deepseek_inference_client.py +501 -0
- bytedance/volcengine_aicc_sdk/secure_http_client.py +406 -0
- bytedance/volcengine_aicc_sdk/seedance_inference_client.py +500 -0
- bytedance/volcengine_aicc_sdk/seedream_inference_client.py +213 -0
- volcengine_python_sdk_aicc-0.0.41.dist-info/METADATA +79 -0
- volcengine_python_sdk_aicc-0.0.41.dist-info/RECORD +60 -0
- volcengine_python_sdk_aicc-0.0.41.dist-info/WHEEL +5 -0
- volcengine_python_sdk_aicc-0.0.41.dist-info/licenses/LICENSE +202 -0
- 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()
|