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.
- knowlsttclient-0.4.2/PKG-INFO +7 -0
- knowlsttclient-0.4.2/README.md +116 -0
- knowlsttclient-0.4.2/knowlsttclient/__init__.py +5 -0
- knowlsttclient-0.4.2/knowlsttclient/proto/__init__.py +4 -0
- knowlsttclient-0.4.2/knowlsttclient/proto/control_message_pb2.py +28 -0
- knowlsttclient-0.4.2/knowlsttclient/proto/transcription_event_pb2.py +27 -0
- knowlsttclient-0.4.2/knowlsttclient/streaming_client.py +368 -0
- knowlsttclient-0.4.2/knowlsttclient.egg-info/PKG-INFO +7 -0
- knowlsttclient-0.4.2/knowlsttclient.egg-info/SOURCES.txt +12 -0
- knowlsttclient-0.4.2/knowlsttclient.egg-info/dependency_links.txt +1 -0
- knowlsttclient-0.4.2/knowlsttclient.egg-info/requires.txt +3 -0
- knowlsttclient-0.4.2/knowlsttclient.egg-info/top_level.txt +1 -0
- knowlsttclient-0.4.2/pyproject.toml +13 -0
- knowlsttclient-0.4.2/setup.cfg +4 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -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*"]
|