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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -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/resources/terms_of_service/number_reputation.rb +1 -1
- 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
|
@@ -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"
|
data/lib/telnyx/version.rb
CHANGED
|
@@ -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
|