telnyx 5.66.0 → 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.
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Telnyx
4
+ module Lib
5
+ module WebSocket
6
+ # Parameters for configuring Text-to-Speech 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 = TextToSpeechStreamParams.new(
14
+ # voice: "telnyx.NaturalHD.Alloy",
15
+ # output_format: "mp3"
16
+ # )
17
+ # url = params.to_query_string
18
+ #
19
+ class TextToSpeechStreamParams
20
+ # @return [String, nil] The voice to use (e.g., "telnyx.NaturalHD.Alloy")
21
+ attr_accessor :voice
22
+
23
+ # @return [String, nil] The audio output format (e.g., "mp3", "pcm", "wav")
24
+ attr_accessor :output_format
25
+
26
+ # @return [Integer, nil] Sample rate in Hz (e.g., 22050, 24000)
27
+ attr_accessor :sample_rate
28
+
29
+ # @return [String, nil] Language code (e.g., "en-US")
30
+ attr_accessor :language
31
+
32
+ # @return [Float, nil] Speaking rate/speed multiplier (0.5 to 2.0)
33
+ attr_accessor :speed
34
+
35
+ # @return [Float, nil] Pitch adjustment (-20.0 to 20.0)
36
+ attr_accessor :pitch
37
+
38
+ # @return [String, nil] TTS model to use
39
+ attr_accessor :model
40
+
41
+ # @return [String, nil] Client reference identifier
42
+ attr_accessor :client_ref
43
+
44
+ # Create params from a hash
45
+ #
46
+ # @param options [Hash] The parameter options
47
+ # @return [TextToSpeechStreamParams]
48
+ def self.from_hash(options)
49
+ params = new
50
+ params.voice = options[:voice] || options["voice"]
51
+ params.output_format = options[:output_format] || options["output_format"]
52
+ params.sample_rate = options[:sample_rate] || options["sample_rate"]
53
+ params.language = options[:language] || options["language"]
54
+ params.speed = options[:speed] || options["speed"]
55
+ params.pitch = options[:pitch] || options["pitch"]
56
+ params.model = options[:model] || options["model"]
57
+ params.client_ref = options[:client_ref] || options["client_ref"]
58
+ params
59
+ end
60
+
61
+ # Convert to a hash for URL encoding
62
+ #
63
+ # @return [Hash] The params as a hash with string keys
64
+ def to_hash
65
+ hash = {}
66
+ hash["voice"] = voice if voice
67
+ hash["output_format"] = output_format if output_format
68
+ hash["sample_rate"] = sample_rate.to_s if sample_rate
69
+ hash["language"] = language if language
70
+ hash["speed"] = speed.to_s if speed
71
+ hash["pitch"] = pitch.to_s if pitch
72
+ hash["model"] = model if model
73
+ hash["client_ref"] = client_ref if client_ref
74
+ hash
75
+ end
76
+
77
+ # Convert to URL query string
78
+ #
79
+ # @return [String]
80
+ def to_query_string
81
+ require("cgi")
82
+ to_hash.map { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v.to_s)}" }.join("&")
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "base"
5
+ require_relative "tts_server_event"
6
+ require_relative "text_to_speech_stream_params"
7
+ require_relative "websocket_error"
8
+
9
+ module Telnyx
10
+ module Lib
11
+ module WebSocket
12
+ # WebSocket stream message types for async iteration.
13
+ #
14
+ # @api private
15
+ StreamMessage = Struct.new(:type, :message, :error)
16
+
17
+ # WebSocket client for Text-to-Speech (TTS) streaming synthesis.
18
+ #
19
+ # This client establishes a WebSocket connection to the Telnyx TTS API
20
+ # for real-time speech synthesis. Text input events are sent as JSON
21
+ # and audio chunks are received as events.
22
+ #
23
+ # Example usage with event callbacks:
24
+ #
25
+ # client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"])
26
+ #
27
+ # ws = Telnyx::Lib::WebSocket::TextToSpeechWS.new(client, {
28
+ # voice: "telnyx.NaturalHD.Alloy"
29
+ # })
30
+ #
31
+ # ws.on(:audio_chunk) do |event|
32
+ # audio = event.decode_audio
33
+ # # Process audio chunk
34
+ # end
35
+ #
36
+ # ws.on(:final) do |event|
37
+ # puts "Synthesis complete"
38
+ # end
39
+ #
40
+ # ws.wait_for_open
41
+ # ws.send({ type: "text", text: "Hello, world!" })
42
+ # ws.send({ type: "flush" })
43
+ #
44
+ # Example usage with Enumerable pattern:
45
+ #
46
+ # ws.each do |msg|
47
+ # case msg.type
48
+ # when :message
49
+ # event = msg.message
50
+ # if event.audio_chunk?
51
+ # # Process audio
52
+ # end
53
+ # when :error
54
+ # puts "Error: #{msg.error.message}"
55
+ # when :close
56
+ # break
57
+ # end
58
+ # end
59
+ #
60
+ class TextToSpeechWS < Base
61
+ include Enumerable
62
+
63
+ # The WebSocket API path for TTS
64
+ API_PATH = "/v2/text-to-speech/speech"
65
+
66
+ # @return [Telnyx::Client] The Telnyx client
67
+ attr_reader :client
68
+
69
+ # @return [TextToSpeechStreamParams, nil] The stream parameters
70
+ attr_reader :params
71
+
72
+ # Create a new TTS WebSocket connection.
73
+ #
74
+ # @param client [Telnyx::Client] The Telnyx client
75
+ # @param params [Hash, TextToSpeechStreamParams, nil] Stream configuration parameters
76
+ # @param options [Hash] Additional WebSocket options
77
+ # @option options [Hash] :headers Additional HTTP headers
78
+ def initialize(client, params = nil, options = {})
79
+ super()
80
+ @client = client
81
+ @params = normalize_params(params)
82
+ @options = options
83
+ @stream_queue = nil # Will be a ::Queue
84
+ @streaming = false
85
+
86
+ @url = build_url(client, API_PATH, @params&.to_hash)
87
+ connect
88
+ end
89
+
90
+ # Send a text input event to the server for synthesis.
91
+ #
92
+ # @param event [Hash] The event to send
93
+ # @option event [String] :type Event type ("text", "flush", "close")
94
+ # @option event [String] :text The text to synthesize (for type: "text")
95
+ # @return [void]
96
+ # @raise [WebSocketError] If send fails
97
+ #
98
+ # Event types:
99
+ # - { type: "text", text: "Hello" } - Add text to synthesize
100
+ # - { type: "flush" } - Flush buffered text and generate audio
101
+ # - { type: "close" } - Signal end of input
102
+ def send(event)
103
+ unless open?
104
+ raise WebSocketError.new("Cannot send: WebSocket is not open")
105
+ end
106
+
107
+ begin
108
+ json_data = event.is_a?(String) ? event : JSON.generate(event)
109
+ @socket.send(json_data, type: :text)
110
+ rescue StandardError => e
111
+ emit_error(nil, "could not send data", e)
112
+ end
113
+ end
114
+
115
+ # Iterate over WebSocket events using the Enumerable pattern.
116
+ #
117
+ # This provides an alternative to the callback-based `.on()` API.
118
+ # The iterator yields StreamMessage structs with :type, :message, and :error.
119
+ #
120
+ # @yield [StreamMessage] Each event as a StreamMessage
121
+ # @yieldparam msg [StreamMessage] The stream message
122
+ # @yieldparam msg.type [Symbol] One of :connecting, :open, :closing, :close, :message, :error
123
+ # @yieldparam msg.message [TtsServerEvent, nil] The parsed event (for :message type)
124
+ # @yieldparam msg.error [WebSocketError, nil] The error (for :error type)
125
+ # @return [Enumerator] If no block given
126
+ #
127
+ # Example:
128
+ #
129
+ # ws.each do |msg|
130
+ # case msg.type
131
+ # when :message
132
+ # puts msg.message.type
133
+ # when :close
134
+ # break
135
+ # end
136
+ # end
137
+ # rubocop:disable Lint/UnusedMethodArgument
138
+ def each(&block)
139
+ return to_enum(:each) unless block_given?
140
+ # rubocop:enable Lint/UnusedMethodArgument
141
+
142
+ @stream_queue = ::Queue.new
143
+ @streaming = true
144
+
145
+ # Set up event forwarding
146
+ setup_streaming
147
+
148
+ # Yield initial state
149
+ case @ready_state
150
+ when CONNECTING
151
+ yield(StreamMessage.new(type: :connecting))
152
+ when OPEN
153
+ yield(StreamMessage.new(type: :open))
154
+ when CLOSING
155
+ yield(StreamMessage.new(type: :closing))
156
+ when CLOSED
157
+ yield(StreamMessage.new(type: :close))
158
+ return
159
+ end
160
+
161
+ # Process events from the queue
162
+ loop do
163
+ msg = @stream_queue.pop
164
+ break if msg.nil? || (msg.type == :close)
165
+
166
+ yield(msg)
167
+
168
+ break if msg.type == :close
169
+ end
170
+ ensure
171
+ @streaming = false
172
+ @stream_queue = nil
173
+ end
174
+
175
+ # Alias for each - provides async iterator semantics
176
+ #
177
+ # @return [Enumerator]
178
+ def stream
179
+ each
180
+ end
181
+
182
+ private
183
+
184
+ def normalize_params(params)
185
+ case params
186
+ when nil
187
+ nil
188
+ when TextToSpeechStreamParams
189
+ params
190
+ when Hash
191
+ TextToSpeechStreamParams.from_hash(params)
192
+ else
193
+ raise ArgumentError, "params must be a Hash or TextToSpeechStreamParams"
194
+ end
195
+ end
196
+
197
+ def connect
198
+ require("websocket-client-simple")
199
+
200
+ headers = auth_headers(@client).merge(@options[:headers] || {})
201
+
202
+ ws_self = self
203
+ @socket = ::WebSocket::Client::Simple.connect(@url.to_s, headers: headers)
204
+
205
+ @socket.on(:open) do
206
+ ws_self.send(:handle_open)
207
+ end
208
+
209
+ @socket.on(:message) do |msg|
210
+ ws_self.send(:handle_message, msg)
211
+ end
212
+
213
+ @socket.on(:error) do |e|
214
+ ws_self.send(:handle_socket_error, e)
215
+ end
216
+
217
+ @socket.on(:close) do |e|
218
+ code = begin
219
+ e&.code
220
+ rescue StandardError
221
+ nil
222
+ end
223
+ reason = begin
224
+ e&.reason
225
+ rescue StandardError
226
+ nil
227
+ end
228
+ ws_self.send(:handle_close, code, reason)
229
+ end
230
+ end
231
+
232
+ def handle_open
233
+ mark_open
234
+ push_stream_message(StreamMessage.new(type: :open)) if @streaming
235
+ end
236
+
237
+ def handle_message(msg)
238
+ # Parse the JSON message
239
+ event = parse_event(msg)
240
+ return unless event
241
+
242
+ # Emit generic event
243
+ emit(:event, event)
244
+
245
+ # Push to stream queue if streaming
246
+ push_stream_message(StreamMessage.new(type: :message, message: event)) if @streaming
247
+
248
+ # Emit type-specific event
249
+ if event.error?
250
+ error = WebSocketError.new(event.error || "Server error", error: event.raw)
251
+ emit(:error, error)
252
+ push_stream_message(StreamMessage.new(type: :error, error: error)) if @streaming
253
+ elsif event.type
254
+ emit(event.type.to_sym, event)
255
+ end
256
+ end
257
+
258
+ def parse_event(msg)
259
+ data = JSON.parse(msg.data)
260
+ TtsServerEvent.from_hash(data)
261
+ rescue JSON::ParserError => e
262
+ emit_error(nil, "could not parse websocket event", e)
263
+ nil
264
+ end
265
+
266
+ def handle_socket_error(e)
267
+ emit_error(nil, e.message, e)
268
+ return unless @streaming
269
+ error = WebSocketError.new(e.message, cause: e)
270
+ push_stream_message(StreamMessage.new(type: :error, error: error))
271
+ end
272
+
273
+ def handle_close(code, reason)
274
+ mark_closed(code, reason)
275
+ push_stream_message(StreamMessage.new(type: :close)) if @streaming
276
+ end
277
+
278
+ def push_stream_message(msg)
279
+ @stream_queue&.push(msg)
280
+ end
281
+
282
+ def setup_streaming
283
+ # The streaming is already set up via handle_* methods
284
+ # This method exists for potential future enhancements
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Telnyx
4
+ module Lib
5
+ module WebSocket
6
+ # Server event types for Text-to-Speech WebSocket streaming.
7
+ #
8
+ # These events are received from the Telnyx TTS WebSocket API
9
+ # during real-time speech synthesis.
10
+ #
11
+ # Example usage:
12
+ #
13
+ # ws.on(:audio_chunk) do |event|
14
+ # audio_data = Base64.decode64(event.audio)
15
+ # # Process audio chunk
16
+ # end
17
+ #
18
+ # ws.on(:final) do |event|
19
+ # puts "Stream complete"
20
+ # end
21
+ #
22
+ class TtsServerEvent
23
+ # @return [String] The event type ("audio_chunk", "final", or "error")
24
+ attr_accessor :type
25
+
26
+ # @return [String, nil] Base64-encoded audio data
27
+ attr_accessor :audio
28
+
29
+ # @return [String, nil] The text being synthesized
30
+ attr_accessor :text
31
+
32
+ # @return [Boolean, nil] Whether this is the final event
33
+ attr_accessor :is_final
34
+
35
+ # @return [Boolean, nil] Whether the response was served from cache
36
+ attr_accessor :cached
37
+
38
+ # @return [Integer, nil] Time in milliseconds to first audio frame
39
+ attr_accessor :time_to_first_audio_frame_ms
40
+
41
+ # @return [String, nil] Error message if type is "error"
42
+ attr_accessor :error
43
+
44
+ # @return [String, nil] Audio encoding format (e.g., "mp3", "pcm")
45
+ attr_accessor :encoding
46
+
47
+ # @return [Integer, nil] Sample rate in Hz
48
+ attr_accessor :sample_rate
49
+
50
+ # @return [Integer, nil] Sequence number for ordering
51
+ attr_accessor :sequence
52
+
53
+ # @return [Hash, nil] Original raw event data
54
+ attr_accessor :raw
55
+
56
+ # Create a TtsServerEvent from a parsed JSON hash
57
+ #
58
+ # @param data [Hash] The parsed JSON event data
59
+ # @return [TtsServerEvent]
60
+ def self.from_hash(data)
61
+ event = new
62
+ event.raw = data
63
+ event.type = data["type"]
64
+ event.audio = data["audio"]
65
+ event.text = data["text"]
66
+ event.is_final = data["is_final"]
67
+ event.cached = data["cached"]
68
+ event.time_to_first_audio_frame_ms = data["time_to_first_audio_frame_ms"]
69
+ event.error = data["error"]
70
+ event.encoding = data["encoding"]
71
+ event.sample_rate = data["sample_rate"]
72
+ event.sequence = data["sequence"]
73
+ event
74
+ end
75
+
76
+ # Check if this is the final event
77
+ #
78
+ # @return [Boolean]
79
+ def final?
80
+ type == "final" || is_final == true
81
+ end
82
+
83
+ # Check if this is an error event
84
+ #
85
+ # @return [Boolean]
86
+ def error?
87
+ type == "error"
88
+ end
89
+
90
+ # Check if this is an audio chunk
91
+ #
92
+ # @return [Boolean]
93
+ def audio_chunk?
94
+ type == "audio_chunk"
95
+ end
96
+
97
+ # Decode the audio data from Base64
98
+ #
99
+ # @return [String, nil] Raw audio bytes, or nil if no audio data
100
+ def decode_audio
101
+ return nil unless audio
102
+ require("base64")
103
+ Base64.decode64(audio)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Telnyx
4
+ module Lib
5
+ module WebSocket
6
+ # Error class for WebSocket-related errors in STT/TTS streaming.
7
+ #
8
+ # This error is raised when WebSocket connections encounter issues,
9
+ # including connection failures, message parsing errors, and
10
+ # server-side error events.
11
+ #
12
+ # Example usage:
13
+ #
14
+ # ws.on(:error) do |error|
15
+ # puts "WebSocket error: #{error.message}"
16
+ # puts "Server error: #{error.error}" if error.error
17
+ # end
18
+ #
19
+ class WebSocketError < Telnyx::Errors::Error
20
+ # @return [Hash, nil] The error data sent by the server in an error event
21
+ attr_reader :error
22
+
23
+ # @return [StandardError, nil] The underlying cause of this error
24
+ attr_reader :cause
25
+
26
+ # @api private
27
+ #
28
+ # @param message [String] Human-readable error message
29
+ # @param error [Hash, nil] Server error event data
30
+ # @param cause [StandardError, nil] The underlying exception that caused this error
31
+ def initialize(message, error: nil, cause: nil)
32
+ @error = error
33
+ @cause = cause
34
+ super(message)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # WebSocket support for Telnyx Speech-to-Text (STT) and Text-to-Speech (TTS) streaming.
4
+ #
5
+ # This module provides WebSocket clients for real-time audio transcription
6
+ # and speech synthesis using the Telnyx API.
7
+ #
8
+ # Example - Speech-to-Text:
9
+ #
10
+ # require "telnyx"
11
+ # require "telnyx/lib/websocket"
12
+ #
13
+ # client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"])
14
+ #
15
+ # ws = Telnyx::Lib::WebSocket::SpeechToTextWS.new(client, {
16
+ # transcription_engine: "Deepgram",
17
+ # language: "en-US"
18
+ # })
19
+ #
20
+ # ws.on(:transcript) do |event|
21
+ # puts event.transcript if event.is_final
22
+ # end
23
+ #
24
+ # ws.wait_for_open
25
+ # ws.send(audio_data)
26
+ # ws.close
27
+ #
28
+ # Example - Text-to-Speech:
29
+ #
30
+ # ws = Telnyx::Lib::WebSocket::TextToSpeechWS.new(client, {
31
+ # voice: "telnyx.NaturalHD.Alloy"
32
+ # })
33
+ #
34
+ # ws.each do |msg|
35
+ # case msg.type
36
+ # when :message
37
+ # audio = msg.message.decode_audio if msg.message.audio_chunk?
38
+ # when :close
39
+ # break
40
+ # end
41
+ # end
42
+ #
43
+
44
+ require_relative "websocket/websocket_error"
45
+ require_relative "websocket/stt_server_event"
46
+ require_relative "websocket/tts_server_event"
47
+ require_relative "websocket/speech_to_text_stream_params"
48
+ require_relative "websocket/text_to_speech_stream_params"
49
+ require_relative "websocket/base"
50
+ require_relative "websocket/speech_to_text_ws"
51
+ require_relative "websocket/text_to_speech_ws"
@@ -21,7 +21,7 @@ module Telnyx
21
21
  def agree(params = {})
22
22
  @client.request(
23
23
  method: :post,
24
- path: "terms-of-service/number-reputation/agree",
24
+ path: "terms_of_service/number_reputation/agree",
25
25
  model: NilClass,
26
26
  options: params[:request_options]
27
27
  )
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Telnyx
4
- VERSION = "5.66.0"
4
+ VERSION = "5.67.0"
5
5
  end
@@ -0,0 +1,55 @@
1
+ # typed: strong
2
+
3
+ module Telnyx
4
+ module Lib
5
+ module WebSocket
6
+ class SpeechToTextStreamParams
7
+ sig { returns(String) }
8
+ attr_accessor :transcription_engine
9
+
10
+ sig { returns(String) }
11
+ attr_accessor :input_format
12
+
13
+ sig { returns(String) }
14
+ attr_accessor :language
15
+
16
+ sig { returns(T.nilable(Integer)) }
17
+ attr_accessor :sample_rate
18
+
19
+ sig { returns(T.nilable(T::Boolean)) }
20
+ attr_accessor :interim_results
21
+
22
+ sig { returns(T.nilable(String)) }
23
+ attr_accessor :client_ref
24
+
25
+ sig do
26
+ params(
27
+ transcription_engine: String,
28
+ input_format: String,
29
+ language: String,
30
+ sample_rate: T.nilable(Integer),
31
+ interim_results: T.nilable(T::Boolean),
32
+ client_ref: T.nilable(String)
33
+ ).void
34
+ end
35
+ def initialize(
36
+ transcription_engine: "Deepgram",
37
+ input_format: "wav",
38
+ language: "en-US",
39
+ sample_rate: nil,
40
+ interim_results: nil,
41
+ client_ref: nil
42
+ )
43
+ end
44
+
45
+ sig { returns(T::Hash[String, String]) }
46
+ def to_query_params
47
+ end
48
+
49
+ sig { returns(String) }
50
+ def to_query_string
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,43 @@
1
+ # typed: strong
2
+
3
+ module Telnyx
4
+ module Lib
5
+ module WebSocket
6
+ class SpeechToTextWS
7
+ sig { returns(Telnyx::Client) }
8
+ attr_reader :client
9
+
10
+ sig { returns(SpeechToTextStreamParams) }
11
+ attr_reader :params
12
+
13
+ sig { returns(T::Boolean) }
14
+ attr_reader :connected
15
+
16
+ sig do
17
+ params(
18
+ client: Telnyx::Client,
19
+ params: SpeechToTextStreamParams
20
+ ).void
21
+ end
22
+ def initialize(client, params)
23
+ end
24
+
25
+ sig { params(timeout: Integer).void }
26
+ def wait_for_open(timeout: 30)
27
+ end
28
+
29
+ sig { params(audio_data: String).void }
30
+ def send(audio_data)
31
+ end
32
+
33
+ sig { void }
34
+ def close
35
+ end
36
+
37
+ sig { params(event: String, block: T.proc.void).void }
38
+ def on(event, &block)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end