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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd82c8cd31aaabd131bb14b35a9e067b9f3f8a4e2f9a43e9c68f343976fbc90c
4
- data.tar.gz: c5c6c20bc874085965b6833d175af91e56ea847f58d59bb1ea2fdaf879492a89
3
+ metadata.gz: f156eac95b70864a77dfaeb5e8db6538f52eaf60d1184e3a4c08164115307a20
4
+ data.tar.gz: 191552e0d38c629677b3def057d01f314743af6f354799741d225acdb9d2ce0d
5
5
  SHA512:
6
- metadata.gz: 6787a88a9c70d2bc6e28c5cef770b26a578f46af3fe251486ef05b54e51f3c2c9a2ea25c98255afc66d2c3e486ed8d5509b15d555b8345cc5566ae7490a1f231
7
- data.tar.gz: 8419fed41b3a914da9e880d390da87814e8210784cc7ca7a84d40ec56b4c22d3123b89d07a151d0f3c93d5cb862ed25eb1362064b840a3e593cc629e2bb528bb
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
@@ -24,7 +24,7 @@ To use this gem, install via Bundler by adding the following to your application
24
24
  <!-- x-release-please-start-version -->
25
25
 
26
26
  ```ruby
27
- gem "telnyx", "~> 5.66.1"
27
+ gem "telnyx", "~> 5.67.0"
28
28
  ```
29
29
 
30
30
  <!-- x-release-please-end -->
@@ -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