knowlsttclient 0.4.2__tar.gz

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.
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: knowlsttclient
3
+ Version: 0.4.2
4
+ Summary: Python client for real-time streaming speech-to-text
5
+ Requires-Dist: protobuf
6
+ Requires-Dist: requests
7
+ Requires-Dist: websockets
@@ -0,0 +1,116 @@
1
+ # Parakeet Hindi ASR - Triton Inference Server
2
+
3
+ This repository contains the Triton Inference Server configuration for serving the `parakeet-knowl-hindi.nemo` ASR model.
4
+
5
+ ## Model Repository Structure
6
+
7
+ ```
8
+ model_repository/
9
+ └── parakeet_hindi/
10
+ ├── config.pbtxt # Triton configuration
11
+ └── 1/
12
+ └── model.py # Python backend that loads the .nemo file
13
+ ```
14
+
15
+ ## Run Triton Server
16
+
17
+ ```bash
18
+ docker run --gpus all --rm -it \
19
+ -p 8000:8000 -p 8001:8001 -p 8002:8002 \
20
+ -v /home/ubuntu/triton_work/model_repository:/models \
21
+ -v /home/ubuntu/hf_models:/home/ubuntu/hf_models:ro \
22
+ triton-parakeet-server:latest \
23
+ tritonserver --model-repository=/models
24
+ ```
25
+
26
+ ### Ports
27
+
28
+ - `8000`: HTTP endpoint
29
+ - `8001`: gRPC endpoint
30
+ - `8002`: Metrics endpoint
31
+
32
+
33
+ ## Input/Output Specification
34
+
35
+ ### Inputs
36
+
37
+ | Name | Data Type | Shape | Description |
38
+ |------|-----------|-------|-------------|
39
+ | AUDIO_SIGNAL | FP32 | [-1] | 1D array of audio samples (16 kHz mono, normalized) |
40
+ | AUDIO_LENGTH | INT32 | [1] | Number of valid audio samples |
41
+
42
+ ### Outputs
43
+
44
+ | Name | Data Type | Shape | Description |
45
+ |------|-----------|-------|-------------|
46
+ | TRANSCRIPT | STRING | [1] | Transcribed text |
47
+
48
+ # Setting up VM instance
49
+ ## Create VM Instance
50
+ ```
51
+ VM_INSTANCE_NAME='knowl-stt-inference'
52
+ ZONE='asia-south1-c'
53
+ gcloud compute instances create $VM_INSTANCE_NAME --project=tribal-datum-361011 --zone=$ZONE --machine-type=g2-standard-8 --network-interface=network-tier=PREMIUM,stack-type=IPV4_ONLY,subnet=default --metadata=enable-osconfig=TRUE --maintenance-policy=TERMINATE --provisioning-model=STANDARD --service-account=44392650498-compute@developer.gserviceaccount.com --scopes=https://www.googleapis.com/auth/devstorage.read_only,https://www.googleapis.com/auth/logging.write,https://www.googleapis.com/auth/monitoring.write,https://www.googleapis.com/auth/service.management.readonly,https://www.googleapis.com/auth/servicecontrol,https://www.googleapis.com/auth/trace.append --accelerator=count=1,type=nvidia-l4 --create-disk=auto-delete=yes,boot=yes,device-name=knowl-stt-inference,disk-resource-policy=projects/tribal-datum-361011/regions/asia-south1/resourcePolicies/default-schedule-1,image=projects/ubuntu-os-cloud/global/images/ubuntu-minimal-2204-jammy-v20251217,mode=rw,size=50,type=pd-balanced --no-shielded-secure-boot --shielded-vtpm --shielded-integrity-monitoring --labels=goog-ops-agent-policy=v2-x86-template-1-4-0,goog-ec-src=vm_add-gcloud --reservation-affinity=any && printf 'agentsRule:\n packageState: installed\n version: latest\ninstanceFilter:\n inclusionLabels:\n - labels:\n goog-ops-agent-policy: v2-x86-template-1-4-0\n' > config.yaml && gcloud compute instances ops-agents policies create goog-ops-agent-v2-x86-template-1-4-0-$ZONE --project=tribal-datum-361011 --zone=$ZONE --file=config.yam
54
+ ```
55
+
56
+ ## setup git on the instance
57
+ ```
58
+ sudo apt update
59
+ sudo apt install -y git
60
+ USER='bhups@knowl.io'
61
+ ssh-keygen -t ed25519 -C "$USER"
62
+ mkdir -p ~/.ssh && printf "Host github.com\n AddKeysToAgent yes\n IdentityFile ~/.ssh/id_ed25519\n" > ~/.ssh/config && chmod 600 ~/.ssh/config
63
+ cat .ssh/id_ed25519.pub # copy content to clipboard and [follow instructions from here](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account).
64
+ ssh-keyscan -t ed25519 github.com >> ~/.ssh/known_hosts
65
+ git clone git@github.com:knowl-doc/knowl-stt-inference-server.git
66
+ ```
67
+
68
+ ## install drivers
69
+ ```
70
+ sudo apt install -y ubuntu-drivers-common
71
+ sudo ubuntu-drivers install
72
+ sudo reboot
73
+ ```
74
+ ## install cuda-toolkit
75
+ sudo apt install nvidia-cuda-toolkit
76
+ sudo apt install nvtop
77
+
78
+ ## install nvidia container toolkit
79
+ distribution=ubuntu22.04
80
+ curl -fsSL https://nvidia.github.io/nvidia-docker/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
81
+ curl -fsSL https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | sudo tee /etc/apt/sources.list.d/nvidia-docker.list
82
+ sudo apt update
83
+ sudo apt install -y nvidia-container-toolkit
84
+
85
+ ## install python venv
86
+ sudo apt install python3.10-venv
87
+
88
+ ## install Docker
89
+ sudo install -m 0755 -d /etc/apt/keyrings
90
+ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
91
+ sudo chmod a+r /etc/apt/keyrings/docker.gpg
92
+
93
+ echo \
94
+ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
95
+ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
96
+ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
97
+
98
+ sudo apt update
99
+ sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
100
+
101
+ sudo usermod -aG docker $USER
102
+ newgrp docker
103
+
104
+ ## create triton server docker with nemo toolkit
105
+ ```docker build -f Dockerfile.triton-nemo-toolkit -t triton-parakeet-server:latest .```
106
+
107
+ ## Create virtual env
108
+ python3 -m venv knowl-env
109
+ pip install -r requirements.txt
110
+
111
+ ## download model
112
+ mkdir -p /home/ubuntu/models
113
+ gsutil cp gs://knowl-models/parakeet-knowl-hindi.nemo /home/ubuntu/models
114
+
115
+ ## run triton server on docker
116
+ docker run --gpus all --rm -it -p 8000:8000 -p 8001:8001 -p 8002:8002 -v /home/ubuntu/knowl-stt-inference-server/model_repository:/models -v /home/ubuntu/models:/home/ubuntu/models:ro triton-parakeet-server:latest tritonserver --model-repository=/models
@@ -0,0 +1,5 @@
1
+ from .streaming_client import StreamingClient, TritonTranscriptionClient
2
+
3
+ # StreamingClient is the public name; TritonTranscriptionClient is a deprecated
4
+ # alias kept for backwards compatibility (to be removed once all callers migrate).
5
+ __all__ = ["StreamingClient", "TritonTranscriptionClient"]
@@ -0,0 +1,4 @@
1
+ from .transcription_event_pb2 import TranscriptionEvent
2
+ from .control_message_pb2 import ControlMessage
3
+
4
+ __all__ = ["TranscriptionEvent", "ControlMessage"]
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: control_message.proto
4
+ # Protobuf Python Version: 4.25.3
5
+ """Generated protocol buffer code."""
6
+ from google.protobuf import descriptor as _descriptor
7
+ from google.protobuf import descriptor_pool as _descriptor_pool
8
+ from google.protobuf import symbol_database as _symbol_database
9
+ from google.protobuf.internal import builder as _builder
10
+ # @@protoc_insertion_point(imports)
11
+
12
+ _sym_db = _symbol_database.Default()
13
+
14
+
15
+
16
+
17
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63ontrol_message.proto\"\x9c\x01\n\x0e\x43ontrolMessage\x12)\n\x04type\x18\x01 \x01(\x0e\x32\x1b.ControlMessage.MessageType\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x10\n\x08metadata\x18\x03 \x01(\t\"?\n\x0bMessageType\x12\t\n\x05MEDIA\x10\x00\x12\x0c\n\x08METADATA\x10\x01\x12\t\n\x05\x43LOSE\x10\x02\x12\x0c\n\x08\x46INALIZE\x10\x03\x62\x06proto3')
18
+
19
+ _globals = globals()
20
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
21
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'control_message_pb2', _globals)
22
+ if _descriptor._USE_C_DESCRIPTORS == False:
23
+ DESCRIPTOR._options = None
24
+ _globals['_CONTROLMESSAGE']._serialized_start=26
25
+ _globals['_CONTROLMESSAGE']._serialized_end=182
26
+ _globals['_CONTROLMESSAGE_MESSAGETYPE']._serialized_start=119
27
+ _globals['_CONTROLMESSAGE_MESSAGETYPE']._serialized_end=182
28
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: transcription_event.proto
4
+ """Generated protocol buffer code."""
5
+ from google.protobuf.internal import builder as _builder
6
+ from google.protobuf import descriptor as _descriptor
7
+ from google.protobuf import descriptor_pool as _descriptor_pool
8
+ from google.protobuf import symbol_database as _symbol_database
9
+ # @@protoc_insertion_point(imports)
10
+
11
+ _sym_db = _symbol_database.Default()
12
+
13
+
14
+
15
+
16
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19transcription_event.proto\"\xb3\x02\n\x12TranscriptionEvent\x12+\n\x04type\x18\x01 \x01(\x0e\x32\x1d.TranscriptionEvent.EventType\x12\x12\n\ntranscript\x18\x02 \x01(\t\x12\x12\n\nstart_time\x18\x03 \x01(\x02\x12\x10\n\x08\x65nd_time\x18\x04 \x01(\x02\x12\x1d\n\x15vad_speech_start_time\x18\x05 \x01(\x02\x12\x19\n\x11speech_start_time\x18\x06 \x01(\x02\x12\x17\n\x0fspeech_end_time\x18\x07 \x01(\x02\x12\x10\n\x08is_final\x18\x08 \x01(\x08\"Q\n\tEventType\x12\x0e\n\nTRANSCRIPT\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x12\x14\n\x10VAD_SPEECH_START\x10\x02\x12\x12\n\x0eVAD_SPEECH_END\x10\x03\x62\x06proto3')
17
+
18
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
19
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'transcription_event_pb2', globals())
20
+ if _descriptor._USE_C_DESCRIPTORS == False:
21
+
22
+ DESCRIPTOR._options = None
23
+ _TRANSCRIPTIONEVENT._serialized_start=30
24
+ _TRANSCRIPTIONEVENT._serialized_end=337
25
+ _TRANSCRIPTIONEVENT_EVENTTYPE._serialized_start=256
26
+ _TRANSCRIPTIONEVENT_EVENTTYPE._serialized_end=337
27
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,368 @@
1
+ """
2
+ Client library for connecting to Triton Inference Server WebSocket for real-time audio transcription.
3
+
4
+ This library is designed for applications that have their own WebSocket connections to VOIP services
5
+ and receive 8k mono telephony packets. They can use this library to transcribe audio in real-time.
6
+
7
+ Example usage:
8
+ import asyncio
9
+ from knowlsttclient import StreamingClient
10
+
11
+ async def on_transcript(transcript: str):
12
+ print(f"Transcript: {transcript}")
13
+
14
+ async def main():
15
+ client = StreamingClient(
16
+ server_url="ws://localhost:8765",
17
+ on_transcript=on_transcript
18
+ )
19
+
20
+ await client.connect()
21
+
22
+ # Send audio data (8k mono 16-bit PCM)
23
+ audio_data = b"..." # Your audio bytes
24
+ await client.send_audio(audio_data)
25
+
26
+ # Keep connection alive and process transcripts
27
+ await client.wait_for_transcripts()
28
+
29
+ asyncio.run(main())
30
+ """
31
+
32
+ import asyncio
33
+ import json
34
+ import logging
35
+ from typing import Optional, Callable, Awaitable
36
+ import websockets
37
+ from websockets import WebSocketClientProtocol
38
+ from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
39
+ from knowlsttclient.proto import TranscriptionEvent
40
+ from knowlsttclient.proto import ControlMessage
41
+ import uuid
42
+
43
+ logging.basicConfig(
44
+ level=logging.INFO,
45
+ format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
46
+ )
47
+ logger = logging.getLogger("knowlsttclient")
48
+
49
+
50
+ class StreamingClient:
51
+ """
52
+ Client for real-time streaming speech-to-text over a WebSocket.
53
+
54
+ The client expects:
55
+ - Audio format: Mono 16-bit signed PCM (little-endian), 8 kHz sample rate
56
+ - Audio packets: Typically 20ms chunks (160 samples = 320 bytes)
57
+
58
+ The server will:
59
+ - Accumulate audio and run VAD (Voice Activity Detection)
60
+ - Send transcripts as text messages when speech is detected (minimum 1 second of audio)
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ server_url: str = "ws://localhost:8765",
66
+ on_transcript: Optional[Callable[[str], Awaitable[None]]] = None,
67
+ on_vad_event: Optional[Callable[[str], Awaitable[None]]] = None,
68
+ on_error: Optional[Callable[[Exception], Awaitable[None]]] = None,
69
+ on_close: Optional[Callable[[], Awaitable[None]]] = None,
70
+ auto_reconnect: bool = True,
71
+ reconnect_delay: float = 5.0,
72
+ stream_id: Optional[str] = None,
73
+ stop_history_ms: Optional[int] = None,
74
+ endpointing: Optional[dict] = None,
75
+ api_key: Optional[str] = None,
76
+ sample_rate: int = 8000,
77
+ ):
78
+ """
79
+ Initialize the Triton Transcription Client.
80
+
81
+ Args:
82
+ server_url: WebSocket URL of the Triton inference server (default: ws://localhost:8765)
83
+ on_transcript: Async callback function called when a transcript is received.
84
+ Signature: async def on_transcript(transcript: str) -> None
85
+ on_error: Async callback function called when an error occurs.
86
+ Signature: async def on_error(error: Exception) -> None
87
+ on_close: Async callback function called when the connection is closed.
88
+ Signature: async def on_close() -> None
89
+ auto_reconnect: Whether to automatically reconnect on connection loss (default: True)
90
+ reconnect_delay: Delay in seconds before attempting to reconnect (default: 5.0)
91
+ stream_id: Optional stream ID to identify the stream.
92
+ stop_history_ms: Optional per-call endpointing duration — trailing
93
+ silence (ms) before a segment is finalized. Lower = faster finals
94
+ (lower latency) at the risk of cutting on natural pauses. Sweet
95
+ spot for telephony is ~300-500 ms. None = use the server default
96
+ (~800 ms baked into the model). Only honoured by the Riva backend.
97
+ endpointing: Optional dict for advanced endpointing knobs, any of:
98
+ stop_history_ms, stop_history_eou_ms, start_history_ms (ints, ms),
99
+ stop_threshold, stop_threshold_eou, start_threshold (floats, 0..1).
100
+ ``stop_history_ms`` (the kwarg) takes precedence over this dict.
101
+ api_key: Optional API key for the gated external endpoint
102
+ (e.g. wss://voice.knowl.io/stt/v1). Sent as
103
+ ``Authorization: Bearer <key>`` on the WebSocket handshake.
104
+ Leave unset for the internal endpoint (no auth).
105
+ sample_rate: Input PCM sample rate in Hz (default 8000). 8000 is
106
+ upsampled to 16 kHz server-side; 16000 is sent to the model
107
+ as-is (no upsampling). Audio must be mono 16-bit PCM at this rate.
108
+ """
109
+ self.server_url = server_url
110
+ self.on_transcript = on_transcript
111
+ self.on_vad_event = on_vad_event
112
+ self.on_error = on_error
113
+ self.on_close = on_close
114
+ self.auto_reconnect = auto_reconnect
115
+ self.reconnect_delay = reconnect_delay
116
+ self.stream_id = stream_id if stream_id else str(uuid.uuid4())
117
+ self.api_key = api_key
118
+ self.served_by = None # serving host (X-Served-By header), set on connect
119
+ # Input PCM rate. 8000 (default) is upsampled to 16 kHz server-side;
120
+ # 16000 is sent to the model as-is (no upsampling). Sent in METADATA.
121
+ self.sample_rate = sample_rate
122
+ # Assemble per-call endpointing overrides (sent in the METADATA message).
123
+ self.endpointing: dict = dict(endpointing or {})
124
+ if stop_history_ms is not None:
125
+ self.endpointing["stop_history_ms"] = stop_history_ms
126
+ self.websocket: Optional[WebSocketClientProtocol] = None
127
+ self._receive_task: Optional[asyncio.Task] = None
128
+ self._connected = False
129
+ self._should_reconnect = True
130
+
131
+ def _build_metadata(self) -> str:
132
+ """Build the METADATA payload. Plain stream_id when there are no
133
+ endpointing overrides (back-compatible); a JSON object otherwise."""
134
+ meta = dict(self.endpointing)
135
+ if self.sample_rate != 8000:
136
+ meta["sample_rate"] = self.sample_rate
137
+ if not meta:
138
+ return self.stream_id
139
+ return json.dumps({"stream_id": self.stream_id, **meta})
140
+
141
+ async def connect(self) -> None:
142
+ """
143
+ Connect to the Triton inference server WebSocket.
144
+
145
+ Raises:
146
+ Exception: If connection fails
147
+ """
148
+ try:
149
+ logger.info(f"Connecting to {self.server_url}...")
150
+ connect_kwargs = {"max_size": None, "max_queue": None}
151
+ # Only attach an auth header when an API key is set (the internal
152
+ # fleet is keyless). websockets >= 14 renamed `extra_headers` to
153
+ # `additional_headers`; pick the kwarg the INSTALLED version accepts
154
+ # so the client works against both — callers (e.g. the call-manager)
155
+ # may pin an older websockets that rejects `additional_headers`.
156
+ if self.api_key:
157
+ try:
158
+ _ws_major = int(websockets.__version__.split(".")[0])
159
+ except Exception:
160
+ _ws_major = 0
161
+ header_kwarg = "additional_headers" if _ws_major >= 14 else "extra_headers"
162
+ connect_kwargs[header_kwarg] = {"Authorization": f"Bearer {self.api_key}"}
163
+ self.websocket = await websockets.connect(self.server_url, **connect_kwargs)
164
+ self._connected = True
165
+ # Record the serving host from the handshake response (X-Served-By).
166
+ # websockets >= 14 exposes .response.headers; < 14 uses .response_headers.
167
+ try:
168
+ resp = getattr(self.websocket, "response", None)
169
+ hdrs = resp.headers if resp is not None else getattr(self.websocket, "response_headers", None)
170
+ self.served_by = hdrs.get("X-Served-By") if hdrs else None
171
+ except Exception:
172
+ self.served_by = None
173
+ logger.info("Connected to Triton inference server (served_by=%s)", self.served_by)
174
+ await self.websocket.send(ControlMessage(
175
+ type=ControlMessage.MessageType.METADATA,
176
+ metadata=self._build_metadata(),
177
+ ).SerializeToString())
178
+
179
+ # Start receiving transcripts
180
+ self._receive_task = asyncio.create_task(self._receive_loop())
181
+
182
+ except Exception as e:
183
+ self._connected = False
184
+ logger.error(f"Failed to connect to server: {e}")
185
+ if self.on_error:
186
+ await self.on_error(e)
187
+ raise
188
+
189
+ async def _receive_loop(self) -> None:
190
+ """Internal loop to receive transcripts from the server."""
191
+ try:
192
+ while self._connected and self.websocket:
193
+ try:
194
+ message = await self.websocket.recv()
195
+ transcription_event = TranscriptionEvent()
196
+ # TODO: Handle bad message
197
+ transcription_event.ParseFromString(message)
198
+ match transcription_event.type:
199
+ case TranscriptionEvent.EventType.TRANSCRIPT:
200
+ if self.on_transcript:
201
+ try:
202
+ await self.on_transcript(transcription_event)
203
+ except Exception as e:
204
+ logger.error(f"Error in on_transcript callback: {e}")
205
+ if self.on_error:
206
+ await self.on_error(e)
207
+ case TranscriptionEvent.EventType.VAD_SPEECH_START | TranscriptionEvent.EventType.VAD_SPEECH_END:
208
+ if self.on_vad_event:
209
+ try:
210
+ await self.on_vad_event(transcription_event)
211
+ except Exception as e:
212
+ logger.error(f"Error in on_vad_event callback: {e}")
213
+ if self.on_error:
214
+ await self.on_error(e)
215
+ case TranscriptionEvent.EventType.CLOSED:
216
+ logger.info("Server closed connection")
217
+ self._connected = False
218
+ if self.on_close:
219
+ await self.on_close()
220
+ break
221
+ case _:
222
+ logger.warning("Received unexpected transcription event type")
223
+
224
+ except ConnectionClosedOK:
225
+ logger.info("Server closed connection cleanly")
226
+ self._connected = False
227
+ break
228
+ except ConnectionClosedError as e:
229
+ logger.warning(f"Server connection error: {e}")
230
+ self._connected = False
231
+ break
232
+ except Exception as e:
233
+ logger.error(f"Error receiving message: {e}")
234
+ if self.on_error:
235
+ await self.on_error(e)
236
+ self._connected = False
237
+ break
238
+
239
+ except asyncio.CancelledError:
240
+ logger.debug("Receive loop cancelled")
241
+ except Exception as e:
242
+ logger.error(f"Unexpected error in receive loop: {e}")
243
+ if self.on_error:
244
+ await self.on_error(e)
245
+ finally:
246
+ self._connected = False
247
+ if self.on_close:
248
+ await self.on_close()
249
+
250
+ async def send_audio(self, audio_data: bytes) -> None:
251
+ """
252
+ Send audio data to the server for transcription.
253
+
254
+ Args:
255
+ audio_data: Raw 8 kHz mono 16-bit PCM audio bytes (little-endian)
256
+
257
+ Raises:
258
+ RuntimeError: If not connected to the server
259
+ Exception: If sending fails
260
+ """
261
+ if not self._connected or not self.websocket:
262
+ raise RuntimeError("Not connected to server. Call connect() first.")
263
+
264
+ try:
265
+ await self.websocket.send(ControlMessage(type=ControlMessage.MessageType.MEDIA, data=audio_data).SerializeToString())
266
+ logger.debug(f"Sent {len(audio_data)} bytes of audio data")
267
+ except ConnectionClosedOK:
268
+ logger.warning("Connection closed while sending audio")
269
+ self._connected = False
270
+ if self.auto_reconnect and self._should_reconnect:
271
+ await self._reconnect()
272
+ raise
273
+ except ConnectionClosedError as e:
274
+ logger.warning(f"Connection error while sending audio: {e}")
275
+ self._connected = False
276
+ if self.auto_reconnect and self._should_reconnect:
277
+ await self._reconnect()
278
+ raise
279
+ except Exception as e:
280
+ logger.error(f"Error sending audio: {e}")
281
+ if self.on_error:
282
+ await self.on_error(e)
283
+ raise
284
+
285
+ async def send_close_message(self) -> None:
286
+ """
287
+ Send a close message to the server.
288
+
289
+ Raises:
290
+ RuntimeError: If not connected to the server
291
+ Exception: If sending fails
292
+ """
293
+ if not self._connected or not self.websocket:
294
+ raise RuntimeError("Not connected to server. Call connect() first.")
295
+ try:
296
+ await self.websocket.send(ControlMessage(type=ControlMessage.MessageType.CLOSE).SerializeToString())
297
+ logger.debug(f"Sent close message")
298
+ except Exception as e:
299
+ logger.error(f"Error sending close message: {e}")
300
+ if self.on_error:
301
+ await self.on_error(e)
302
+ raise
303
+
304
+ async def _reconnect(self) -> None:
305
+ """Attempt to reconnect to the server."""
306
+ logger.info(f"Attempting to reconnect in {self.reconnect_delay} seconds...")
307
+ await asyncio.sleep(self.reconnect_delay)
308
+
309
+ try:
310
+ await self.connect()
311
+ except Exception as e:
312
+ logger.error(f"Reconnection failed: {e}")
313
+ if self.auto_reconnect and self._should_reconnect:
314
+ # Schedule another reconnection attempt
315
+ asyncio.create_task(self._reconnect())
316
+
317
+ async def wait_for_transcripts(self) -> None:
318
+ """
319
+ Wait for transcripts to be received. This will block until the connection is closed.
320
+ Useful for keeping the connection alive while processing transcripts.
321
+ """
322
+ if self._receive_task:
323
+ try:
324
+ await self._receive_task
325
+ except asyncio.CancelledError:
326
+ pass
327
+
328
+ def is_connected(self) -> bool:
329
+ """Check if currently connected to the server."""
330
+ return self._connected and self.websocket is not None
331
+
332
+ async def disconnect(self) -> None:
333
+ """Disconnect from the server and clean up resources."""
334
+ logger.info("Disconnecting from server...")
335
+ self._should_reconnect = False
336
+ self._connected = False
337
+
338
+ if self._receive_task:
339
+ self._receive_task.cancel()
340
+ try:
341
+ await self._receive_task
342
+ except asyncio.CancelledError:
343
+ pass
344
+
345
+ if self.websocket:
346
+ try:
347
+ await self.websocket.close()
348
+ except Exception as e:
349
+ logger.warning(f"Error closing websocket: {e}")
350
+
351
+ logger.info("Disconnected from server")
352
+
353
+ async def __aenter__(self):
354
+ """Async context manager entry."""
355
+ await self.connect()
356
+ return self
357
+
358
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
359
+ """Async context manager exit."""
360
+ await self.disconnect()
361
+
362
+
363
+ # Backwards-compatible alias. `StreamingClient` is the public name; older callers
364
+ # (and the current prod fleet) import `TritonTranscriptionClient`. Both refer to
365
+ # the same class so existing code keeps working. The alias will be removed once
366
+ # all consumers have migrated to `StreamingClient`.
367
+ TritonTranscriptionClient = StreamingClient
368
+
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: knowlsttclient
3
+ Version: 0.4.2
4
+ Summary: Python client for real-time streaming speech-to-text
5
+ Requires-Dist: protobuf
6
+ Requires-Dist: requests
7
+ Requires-Dist: websockets
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ knowlsttclient/__init__.py
4
+ knowlsttclient/streaming_client.py
5
+ knowlsttclient.egg-info/PKG-INFO
6
+ knowlsttclient.egg-info/SOURCES.txt
7
+ knowlsttclient.egg-info/dependency_links.txt
8
+ knowlsttclient.egg-info/requires.txt
9
+ knowlsttclient.egg-info/top_level.txt
10
+ knowlsttclient/proto/__init__.py
11
+ knowlsttclient/proto/control_message_pb2.py
12
+ knowlsttclient/proto/transcription_event_pb2.py
@@ -0,0 +1,3 @@
1
+ protobuf
2
+ requests
3
+ websockets
@@ -0,0 +1 @@
1
+ knowlsttclient
@@ -0,0 +1,13 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "knowlsttclient"
7
+ version = "0.4.2"
8
+ description = "Python client for real-time streaming speech-to-text"
9
+ dependencies = ["protobuf", "requests", "websockets"]
10
+
11
+ [tool.setuptools.packages.find]
12
+ include = ["knowlsttclient*"]
13
+ exclude = ["server*", "proto*", "model_repository*", "sample-wav*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+