telnyx 5.66.1 → 5.67.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +1 -1
- data/lib/telnyx/lib/websocket/base.rb +260 -0
- data/lib/telnyx/lib/websocket/speech_to_text_stream_params.rb +108 -0
- data/lib/telnyx/lib/websocket/speech_to_text_ws.rb +166 -0
- data/lib/telnyx/lib/websocket/stt_server_event.rb +91 -0
- data/lib/telnyx/lib/websocket/text_to_speech_stream_params.rb +87 -0
- data/lib/telnyx/lib/websocket/text_to_speech_ws.rb +289 -0
- data/lib/telnyx/lib/websocket/tts_server_event.rb +108 -0
- data/lib/telnyx/lib/websocket/websocket_error.rb +39 -0
- data/lib/telnyx/lib/websocket.rb +51 -0
- data/lib/telnyx/version.rb +1 -1
- data/rbi/telnyx/lib/websocket/speech_to_text_stream_params.rbi +55 -0
- data/rbi/telnyx/lib/websocket/speech_to_text_ws.rbi +43 -0
- data/rbi/telnyx/lib/websocket/stt_server_event.rbi +43 -0
- data/rbi/telnyx/lib/websocket/text_to_speech_stream_params.rbi +60 -0
- data/rbi/telnyx/lib/websocket/text_to_speech_ws.rbi +47 -0
- data/rbi/telnyx/lib/websocket/tts_server_event.rbi +57 -0
- data/rbi/telnyx/lib/websocket/websocket_error.rbi +25 -0
- data/rbi/telnyx/lib/websocket.rbi +8 -0
- metadata +33 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f156eac95b70864a77dfaeb5e8db6538f52eaf60d1184e3a4c08164115307a20
|
|
4
|
+
data.tar.gz: 191552e0d38c629677b3def057d01f314743af6f354799741d225acdb9d2ce0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a8471f199d00dec4d4c8d4222b89dc870075ff07605396e13ad77b0b379b171ff2ba6ea1011804e57b6882d98223bb1278138151eeff0fa5e0ac76ce07d2a15
|
|
7
|
+
data.tar.gz: 3a4fa036e4c042ddea3c68fa8687ca51040730dd6695d192782e9928b482ce70b18ab94f7f4b3db2815e4a404ebe12001c23214d730ac90cf2f9445ccc4f6f64
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.67.0 (2026-03-26)
|
|
4
|
+
|
|
5
|
+
Full Changelog: [v5.66.1...v5.67.0](https://github.com/team-telnyx/telnyx-ruby/compare/v5.66.1...v5.67.0)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **websocket:** add STT/TTS WebSocket streaming support ([34b7363](https://github.com/team-telnyx/telnyx-ruby/commit/34b73633e8b5c86b17b046746e798365628c2a66))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **websocket:** add Sorbet RBI type definitions ([efe7e55](https://github.com/team-telnyx/telnyx-ruby/commit/efe7e550f9f781453cf69827fe8bfca9b23d9a87))
|
|
15
|
+
|
|
3
16
|
## 5.66.1 (2026-03-25)
|
|
4
17
|
|
|
5
18
|
Full Changelog: [v5.66.0...v5.66.1](https://github.com/team-telnyx/telnyx-ruby/compare/v5.66.0...v5.66.1)
|
data/README.md
CHANGED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "cgi"
|
|
6
|
+
|
|
7
|
+
module Telnyx
|
|
8
|
+
module Lib
|
|
9
|
+
module WebSocket
|
|
10
|
+
# Base class for WebSocket connections.
|
|
11
|
+
#
|
|
12
|
+
# This class provides the foundation for STT and TTS WebSocket clients,
|
|
13
|
+
# including event handling, connection state management, and error handling.
|
|
14
|
+
#
|
|
15
|
+
# The class uses a simple event emitter pattern with block callbacks
|
|
16
|
+
# for handling WebSocket events.
|
|
17
|
+
#
|
|
18
|
+
# Example usage:
|
|
19
|
+
#
|
|
20
|
+
# class MyWS < Base
|
|
21
|
+
# def initialize(client)
|
|
22
|
+
# super()
|
|
23
|
+
# @client = client
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# ws = MyWS.new(client)
|
|
28
|
+
# ws.on(:message) { |data| puts data }
|
|
29
|
+
# ws.on(:error) { |error| puts error.message }
|
|
30
|
+
#
|
|
31
|
+
class Base
|
|
32
|
+
# WebSocket ready states (matching WebSocket spec)
|
|
33
|
+
CONNECTING = 0
|
|
34
|
+
OPEN = 1
|
|
35
|
+
CLOSING = 2
|
|
36
|
+
CLOSED = 3
|
|
37
|
+
|
|
38
|
+
# @return [Integer] Current connection state
|
|
39
|
+
attr_reader :ready_state
|
|
40
|
+
|
|
41
|
+
# @return [URI] The WebSocket URL
|
|
42
|
+
attr_reader :url
|
|
43
|
+
|
|
44
|
+
# @api private
|
|
45
|
+
def initialize
|
|
46
|
+
@listeners = Hash.new { |h, k| h[k] = [] }
|
|
47
|
+
@ready_state = CONNECTING
|
|
48
|
+
@socket = nil
|
|
49
|
+
@mutex = Mutex.new
|
|
50
|
+
@open_queue = ::Queue.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Register an event listener.
|
|
54
|
+
#
|
|
55
|
+
# @param event [Symbol, String] The event name to listen for
|
|
56
|
+
# @yield [Object] Block to call when event is emitted
|
|
57
|
+
# @return [self]
|
|
58
|
+
#
|
|
59
|
+
# Available events:
|
|
60
|
+
# - :open - Connection established
|
|
61
|
+
# - :message - Raw message received
|
|
62
|
+
# - :event - Parsed event received
|
|
63
|
+
# - :error - Error occurred
|
|
64
|
+
# - :close - Connection closed
|
|
65
|
+
# - Event-specific types (e.g., :transcript, :audio_chunk)
|
|
66
|
+
def on(event, &block)
|
|
67
|
+
@listeners[event.to_sym] << block
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Remove an event listener.
|
|
72
|
+
#
|
|
73
|
+
# @param event [Symbol, String] The event name
|
|
74
|
+
# @param block [Proc, nil] The specific block to remove, or nil to remove all
|
|
75
|
+
# @return [self]
|
|
76
|
+
def off(event, block = nil)
|
|
77
|
+
if block
|
|
78
|
+
@listeners[event.to_sym].delete(block)
|
|
79
|
+
else
|
|
80
|
+
@listeners.delete(event.to_sym)
|
|
81
|
+
end
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if the connection is open.
|
|
86
|
+
#
|
|
87
|
+
# @return [Boolean]
|
|
88
|
+
def open?
|
|
89
|
+
@ready_state == OPEN
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Check if the connection is connecting.
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
def connecting?
|
|
96
|
+
@ready_state == CONNECTING
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Check if the connection is closed.
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
def closed?
|
|
103
|
+
@ready_state == CLOSED || @ready_state == CLOSING
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Wait for the connection to be established.
|
|
107
|
+
#
|
|
108
|
+
# @param timeout [Numeric, nil] Maximum time to wait in seconds
|
|
109
|
+
# @raise [WebSocketError] If connection fails or times out
|
|
110
|
+
# @return [self]
|
|
111
|
+
def wait_for_open(timeout: nil)
|
|
112
|
+
return self if open?
|
|
113
|
+
|
|
114
|
+
result = if timeout
|
|
115
|
+
@open_queue.pop(timeout: timeout)
|
|
116
|
+
else
|
|
117
|
+
@open_queue.pop
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if result.is_a?(Exception)
|
|
121
|
+
raise result
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
self
|
|
125
|
+
rescue ThreadError
|
|
126
|
+
raise WebSocketError.new("Connection timed out")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Close the WebSocket connection.
|
|
130
|
+
#
|
|
131
|
+
# @param code [Integer] Close status code (default: 1000 normal closure)
|
|
132
|
+
# @param reason [String] Close reason
|
|
133
|
+
# @return [void]
|
|
134
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
135
|
+
def close(code: 1000, reason: "OK")
|
|
136
|
+
return if closed?
|
|
137
|
+
|
|
138
|
+
@ready_state = CLOSING
|
|
139
|
+
begin
|
|
140
|
+
@socket&.close
|
|
141
|
+
rescue StandardError => e
|
|
142
|
+
emit_error(nil, "could not close the connection", e)
|
|
143
|
+
end
|
|
144
|
+
@ready_state = CLOSED
|
|
145
|
+
end
|
|
146
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
147
|
+
|
|
148
|
+
protected
|
|
149
|
+
|
|
150
|
+
# Emit an event to all registered listeners.
|
|
151
|
+
#
|
|
152
|
+
# @param event [Symbol] The event name
|
|
153
|
+
# @param args [Array] Arguments to pass to listeners
|
|
154
|
+
# @return [void]
|
|
155
|
+
def emit(event, *args)
|
|
156
|
+
@listeners[event.to_sym].each do |listener|
|
|
157
|
+
listener.call(*args)
|
|
158
|
+
rescue StandardError => e
|
|
159
|
+
# Don't let listener errors crash the WebSocket
|
|
160
|
+
warn("WebSocket listener error for #{event}: #{e.message}")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Check if there are listeners for an event.
|
|
165
|
+
#
|
|
166
|
+
# @param event [Symbol] The event name
|
|
167
|
+
# @return [Boolean]
|
|
168
|
+
def listener?(event)
|
|
169
|
+
@listeners[event.to_sym].any?
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Emit an error event, handling missing error handlers.
|
|
173
|
+
#
|
|
174
|
+
# @param event_data [Hash, nil] Server error event data
|
|
175
|
+
# @param message [String, nil] Error message
|
|
176
|
+
# @param cause [StandardError, nil] Underlying exception
|
|
177
|
+
# @return [void]
|
|
178
|
+
def emit_error(event_data, message = nil, cause = nil)
|
|
179
|
+
message ||= safe_json_stringify(event_data) || "unknown error"
|
|
180
|
+
|
|
181
|
+
unless listener?(:error)
|
|
182
|
+
error = WebSocketError.new(
|
|
183
|
+
"#{message}\n\nTo handle errors, bind an `error` callback: ws.on(:error) { |e| ... }",
|
|
184
|
+
error: event_data,
|
|
185
|
+
cause: cause
|
|
186
|
+
)
|
|
187
|
+
warn("Unhandled WebSocket error: #{error.message}")
|
|
188
|
+
return
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
error = WebSocketError.new(message, error: event_data, cause: cause)
|
|
192
|
+
emit(:error, error)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Mark the connection as open.
|
|
196
|
+
#
|
|
197
|
+
# @api private
|
|
198
|
+
def mark_open
|
|
199
|
+
@ready_state = OPEN
|
|
200
|
+
emit(:open)
|
|
201
|
+
@open_queue << :ok
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Mark the connection as closed.
|
|
205
|
+
#
|
|
206
|
+
# @api private
|
|
207
|
+
def mark_closed(code = nil, reason = nil)
|
|
208
|
+
@ready_state = CLOSED
|
|
209
|
+
emit(:close, code, reason)
|
|
210
|
+
@open_queue << :closed
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Build the WebSocket URL.
|
|
214
|
+
#
|
|
215
|
+
# @param client [Telnyx::Client] The Telnyx client
|
|
216
|
+
# @param path [String] The API path
|
|
217
|
+
# @param params [Hash, nil] Query parameters
|
|
218
|
+
# @return [URI]
|
|
219
|
+
def build_url(client, path, params = nil)
|
|
220
|
+
base_url = client.base_url
|
|
221
|
+
url = URI.parse(base_url + (base_url.end_with?("/") ? path[1..] : path))
|
|
222
|
+
|
|
223
|
+
if params && !params.empty?
|
|
224
|
+
query = params.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&")
|
|
225
|
+
url.query = query
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Convert to WebSocket protocol
|
|
229
|
+
url.scheme = url.scheme == "http" ? "ws" : "wss"
|
|
230
|
+
url
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Build authorization headers.
|
|
234
|
+
#
|
|
235
|
+
# @param client [Telnyx::Client] The Telnyx client
|
|
236
|
+
# @return [Hash]
|
|
237
|
+
def auth_headers(client)
|
|
238
|
+
if client.api_key
|
|
239
|
+
{"Authorization" => "Bearer #{client.api_key}"}
|
|
240
|
+
else
|
|
241
|
+
{}
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
private
|
|
246
|
+
|
|
247
|
+
# Safely stringify a value to JSON.
|
|
248
|
+
#
|
|
249
|
+
# @param value [Object] The value to stringify
|
|
250
|
+
# @return [String, nil]
|
|
251
|
+
def safe_json_stringify(value)
|
|
252
|
+
return nil if value.nil?
|
|
253
|
+
JSON.generate(value)
|
|
254
|
+
rescue StandardError
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Telnyx
|
|
4
|
+
module Lib
|
|
5
|
+
module WebSocket
|
|
6
|
+
# Parameters for configuring Speech-to-Text WebSocket streaming.
|
|
7
|
+
#
|
|
8
|
+
# These parameters are passed as query string parameters when
|
|
9
|
+
# establishing the WebSocket connection.
|
|
10
|
+
#
|
|
11
|
+
# Example usage:
|
|
12
|
+
#
|
|
13
|
+
# params = SpeechToTextStreamParams.new(
|
|
14
|
+
# transcription_engine: "Deepgram",
|
|
15
|
+
# language: "en-US",
|
|
16
|
+
# input_format: "wav"
|
|
17
|
+
# )
|
|
18
|
+
# url = params.to_query_string
|
|
19
|
+
#
|
|
20
|
+
class SpeechToTextStreamParams
|
|
21
|
+
# @return [String, nil] The transcription engine to use (e.g., "Deepgram", "Google", "Telnyx")
|
|
22
|
+
attr_accessor :transcription_engine
|
|
23
|
+
|
|
24
|
+
# @return [String, nil] The language code (e.g., "en-US", "es-ES")
|
|
25
|
+
attr_accessor :language
|
|
26
|
+
|
|
27
|
+
# @return [String, nil] The audio input format (e.g., "wav", "mp3", "raw")
|
|
28
|
+
attr_accessor :input_format
|
|
29
|
+
|
|
30
|
+
# @return [Integer, nil] Sample rate in Hz (e.g., 8000, 16000)
|
|
31
|
+
attr_accessor :sample_rate
|
|
32
|
+
|
|
33
|
+
# @return [Boolean, nil] Enable interim/partial results
|
|
34
|
+
attr_accessor :interim_results
|
|
35
|
+
|
|
36
|
+
# @return [Boolean, nil] Enable automatic punctuation
|
|
37
|
+
attr_accessor :punctuate
|
|
38
|
+
|
|
39
|
+
# @return [Boolean, nil] Enable profanity filtering
|
|
40
|
+
attr_accessor :profanity_filter
|
|
41
|
+
|
|
42
|
+
# @return [Boolean, nil] Enable speaker diarization
|
|
43
|
+
attr_accessor :diarize
|
|
44
|
+
|
|
45
|
+
# @return [Integer, nil] Number of speakers for diarization
|
|
46
|
+
attr_accessor :diarize_speakers
|
|
47
|
+
|
|
48
|
+
# @return [String, nil] Custom vocabulary or model name
|
|
49
|
+
attr_accessor :model
|
|
50
|
+
|
|
51
|
+
# @return [Array<String>, nil] Keywords to boost recognition
|
|
52
|
+
attr_accessor :keywords
|
|
53
|
+
|
|
54
|
+
# @return [String, nil] Client reference identifier
|
|
55
|
+
attr_accessor :client_ref
|
|
56
|
+
|
|
57
|
+
# Create params from a hash
|
|
58
|
+
#
|
|
59
|
+
# @param options [Hash] The parameter options
|
|
60
|
+
# @return [SpeechToTextStreamParams]
|
|
61
|
+
def self.from_hash(options)
|
|
62
|
+
params = new
|
|
63
|
+
params.transcription_engine = options[:transcription_engine] || options["transcription_engine"]
|
|
64
|
+
params.language = options[:language] || options["language"]
|
|
65
|
+
params.input_format = options[:input_format] || options["input_format"]
|
|
66
|
+
params.sample_rate = options[:sample_rate] || options["sample_rate"]
|
|
67
|
+
params.interim_results = options[:interim_results] || options["interim_results"]
|
|
68
|
+
params.punctuate = options[:punctuate] || options["punctuate"]
|
|
69
|
+
params.profanity_filter = options[:profanity_filter] || options["profanity_filter"]
|
|
70
|
+
params.diarize = options[:diarize] || options["diarize"]
|
|
71
|
+
params.diarize_speakers = options[:diarize_speakers] || options["diarize_speakers"]
|
|
72
|
+
params.model = options[:model] || options["model"]
|
|
73
|
+
params.keywords = options[:keywords] || options["keywords"]
|
|
74
|
+
params.client_ref = options[:client_ref] || options["client_ref"]
|
|
75
|
+
params
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Convert to a hash for URL encoding
|
|
79
|
+
#
|
|
80
|
+
# @return [Hash] The params as a hash with string keys
|
|
81
|
+
def to_hash
|
|
82
|
+
hash = {}
|
|
83
|
+
hash["transcription_engine"] = transcription_engine if transcription_engine
|
|
84
|
+
hash["language"] = language if language
|
|
85
|
+
hash["input_format"] = input_format if input_format
|
|
86
|
+
hash["sample_rate"] = sample_rate.to_s if sample_rate
|
|
87
|
+
hash["interim_results"] = interim_results.to_s unless interim_results.nil?
|
|
88
|
+
hash["punctuate"] = punctuate.to_s unless punctuate.nil?
|
|
89
|
+
hash["profanity_filter"] = profanity_filter.to_s unless profanity_filter.nil?
|
|
90
|
+
hash["diarize"] = diarize.to_s unless diarize.nil?
|
|
91
|
+
hash["diarize_speakers"] = diarize_speakers.to_s if diarize_speakers
|
|
92
|
+
hash["model"] = model if model
|
|
93
|
+
hash["keywords"] = keywords.join(",") if keywords && !keywords.empty?
|
|
94
|
+
hash["client_ref"] = client_ref if client_ref
|
|
95
|
+
hash
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Convert to URL query string
|
|
99
|
+
#
|
|
100
|
+
# @return [String]
|
|
101
|
+
def to_query_string
|
|
102
|
+
require("cgi")
|
|
103
|
+
to_hash.map { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v.to_s)}" }.join("&")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
require_relative "stt_server_event"
|
|
6
|
+
require_relative "speech_to_text_stream_params"
|
|
7
|
+
require_relative "websocket_error"
|
|
8
|
+
|
|
9
|
+
module Telnyx
|
|
10
|
+
module Lib
|
|
11
|
+
module WebSocket
|
|
12
|
+
# WebSocket client for Speech-to-Text (STT) streaming transcription.
|
|
13
|
+
#
|
|
14
|
+
# This client establishes a WebSocket connection to the Telnyx STT API
|
|
15
|
+
# for real-time audio transcription. Audio data is sent as binary frames
|
|
16
|
+
# and transcription results are received as JSON events.
|
|
17
|
+
#
|
|
18
|
+
# Example usage:
|
|
19
|
+
#
|
|
20
|
+
# client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"])
|
|
21
|
+
#
|
|
22
|
+
# ws = Telnyx::Lib::WebSocket::SpeechToTextWS.new(client, {
|
|
23
|
+
# transcription_engine: "Deepgram",
|
|
24
|
+
# language: "en-US",
|
|
25
|
+
# input_format: "wav"
|
|
26
|
+
# })
|
|
27
|
+
#
|
|
28
|
+
# ws.on(:transcript) do |event|
|
|
29
|
+
# puts "Transcript: #{event.transcript}" if event.is_final
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# ws.on(:error) do |error|
|
|
33
|
+
# puts "Error: #{error.message}"
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# ws.wait_for_open
|
|
37
|
+
#
|
|
38
|
+
# # Send audio data
|
|
39
|
+
# File.open("audio.wav", "rb") do |f|
|
|
40
|
+
# while (chunk = f.read(4096))
|
|
41
|
+
# ws.send(chunk)
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# ws.close
|
|
46
|
+
#
|
|
47
|
+
class SpeechToTextWS < Base
|
|
48
|
+
# The WebSocket API path for STT
|
|
49
|
+
API_PATH = "/v2/speech-to-text/transcription"
|
|
50
|
+
|
|
51
|
+
# @return [Telnyx::Client] The Telnyx client
|
|
52
|
+
attr_reader :client
|
|
53
|
+
|
|
54
|
+
# @return [SpeechToTextStreamParams] The stream parameters
|
|
55
|
+
attr_reader :params
|
|
56
|
+
|
|
57
|
+
# Create a new STT WebSocket connection.
|
|
58
|
+
#
|
|
59
|
+
# @param client [Telnyx::Client] The Telnyx client
|
|
60
|
+
# @param params [Hash, SpeechToTextStreamParams] Stream configuration parameters
|
|
61
|
+
# @param options [Hash] Additional WebSocket options
|
|
62
|
+
# @option options [Hash] :headers Additional HTTP headers
|
|
63
|
+
def initialize(client, params = nil, options = {})
|
|
64
|
+
super()
|
|
65
|
+
@client = client
|
|
66
|
+
@params = normalize_params(params)
|
|
67
|
+
@options = options
|
|
68
|
+
|
|
69
|
+
@url = build_url(client, API_PATH, @params&.to_hash)
|
|
70
|
+
connect
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Send binary audio data to the server for transcription.
|
|
74
|
+
#
|
|
75
|
+
# @param data [String] Raw audio bytes (mp3, wav, or raw format)
|
|
76
|
+
# @return [void]
|
|
77
|
+
# @raise [WebSocketError] If send fails
|
|
78
|
+
def send(data)
|
|
79
|
+
unless open?
|
|
80
|
+
raise WebSocketError.new("Cannot send: WebSocket is not open")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
@socket.send(data, type: :binary)
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
emit_error(nil, "could not send audio data", e)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def normalize_params(params)
|
|
93
|
+
case params
|
|
94
|
+
when nil
|
|
95
|
+
nil
|
|
96
|
+
when SpeechToTextStreamParams
|
|
97
|
+
params
|
|
98
|
+
when Hash
|
|
99
|
+
SpeechToTextStreamParams.from_hash(params)
|
|
100
|
+
else
|
|
101
|
+
raise ArgumentError, "params must be a Hash or SpeechToTextStreamParams"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def connect
|
|
106
|
+
require("websocket-client-simple")
|
|
107
|
+
|
|
108
|
+
headers = auth_headers(@client).merge(@options[:headers] || {})
|
|
109
|
+
|
|
110
|
+
ws_self = self
|
|
111
|
+
@socket = ::WebSocket::Client::Simple.connect(@url.to_s, headers: headers)
|
|
112
|
+
|
|
113
|
+
@socket.on(:open) do
|
|
114
|
+
ws_self.send(:mark_open)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@socket.on(:message) do |msg|
|
|
118
|
+
ws_self.send(:handle_message, msg)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@socket.on(:error) do |e|
|
|
122
|
+
ws_self.send(:emit_error, nil, e.message, e)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
@socket.on(:close) do |e|
|
|
126
|
+
code = begin
|
|
127
|
+
e&.code
|
|
128
|
+
rescue StandardError
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
reason = begin
|
|
132
|
+
e&.reason
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
ws_self.send(:mark_closed, code, reason)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def handle_message(msg)
|
|
141
|
+
# Parse the JSON message
|
|
142
|
+
event = parse_event(msg)
|
|
143
|
+
return unless event
|
|
144
|
+
|
|
145
|
+
# Emit generic event
|
|
146
|
+
emit(:event, event)
|
|
147
|
+
|
|
148
|
+
# Emit type-specific event
|
|
149
|
+
if event.error?
|
|
150
|
+
emit_error(event.raw)
|
|
151
|
+
elsif event.type
|
|
152
|
+
emit(event.type.to_sym, event)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def parse_event(msg)
|
|
157
|
+
data = JSON.parse(msg.data)
|
|
158
|
+
SttServerEvent.from_hash(data)
|
|
159
|
+
rescue JSON::ParserError => e
|
|
160
|
+
emit_error(nil, "could not parse websocket event", e)
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Telnyx
|
|
4
|
+
module Lib
|
|
5
|
+
module WebSocket
|
|
6
|
+
# Server event types for Speech-to-Text WebSocket streaming.
|
|
7
|
+
#
|
|
8
|
+
# These events are received from the Telnyx STT WebSocket API
|
|
9
|
+
# during real-time audio transcription.
|
|
10
|
+
#
|
|
11
|
+
# Example usage:
|
|
12
|
+
#
|
|
13
|
+
# ws.on(:transcript) do |event|
|
|
14
|
+
# puts "Transcript: #{event.transcript}" if event.is_final
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# ws.on(:error) do |event|
|
|
18
|
+
# puts "Error: #{event.error}"
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
class SttServerEvent
|
|
22
|
+
# @return [String] The event type ("transcript" or "error")
|
|
23
|
+
attr_accessor :type
|
|
24
|
+
|
|
25
|
+
# @return [String, nil] The transcribed text
|
|
26
|
+
attr_accessor :transcript
|
|
27
|
+
|
|
28
|
+
# @return [Boolean, nil] Whether this is a final transcript
|
|
29
|
+
attr_accessor :is_final
|
|
30
|
+
|
|
31
|
+
# @return [Float, nil] Confidence score (0.0 to 1.0)
|
|
32
|
+
attr_accessor :confidence
|
|
33
|
+
|
|
34
|
+
# @return [String, nil] Error message if type is "error"
|
|
35
|
+
attr_accessor :error
|
|
36
|
+
|
|
37
|
+
# @return [Float, nil] Start time offset in seconds
|
|
38
|
+
attr_accessor :start
|
|
39
|
+
|
|
40
|
+
# @return [Float, nil] Duration in seconds
|
|
41
|
+
attr_accessor :duration
|
|
42
|
+
|
|
43
|
+
# @return [Array<Hash>, nil] Word-level timing information
|
|
44
|
+
attr_accessor :words
|
|
45
|
+
|
|
46
|
+
# @return [String, nil] Detected language code
|
|
47
|
+
attr_accessor :language
|
|
48
|
+
|
|
49
|
+
# @return [String, nil] Speaker identifier for diarization
|
|
50
|
+
attr_accessor :speaker
|
|
51
|
+
|
|
52
|
+
# @return [Hash, nil] Original raw event data
|
|
53
|
+
attr_accessor :raw
|
|
54
|
+
|
|
55
|
+
# Create an SttServerEvent from a parsed JSON hash
|
|
56
|
+
#
|
|
57
|
+
# @param data [Hash] The parsed JSON event data
|
|
58
|
+
# @return [SttServerEvent]
|
|
59
|
+
def self.from_hash(data)
|
|
60
|
+
event = new
|
|
61
|
+
event.raw = data
|
|
62
|
+
event.type = data["type"]
|
|
63
|
+
event.transcript = data["transcript"]
|
|
64
|
+
event.is_final = data["is_final"]
|
|
65
|
+
event.confidence = data["confidence"]
|
|
66
|
+
event.error = data["error"]
|
|
67
|
+
event.start = data["start"]
|
|
68
|
+
event.duration = data["duration"]
|
|
69
|
+
event.words = data["words"]
|
|
70
|
+
event.language = data["language"]
|
|
71
|
+
event.speaker = data["speaker"]
|
|
72
|
+
event
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if this is a final transcript
|
|
76
|
+
#
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def final?
|
|
79
|
+
is_final == true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check if this is an error event
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def error?
|
|
86
|
+
type == "error"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|