11#!/usr/bin/env python3
2+
23# BSD 3-Clause License
34#
45# Copyright (c) 2024, Intelligent Robotics Lab
2930# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
3031# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3132
32- #!/usr/bin/env python3
33-
3433import rclpy
3534from rclpy .node import Node
36- from std_msgs .msg import String
3735from go2_tts_msgs .msg import TTSRequest
3836from unitree_api .msg import Request
3937import requests
4644import base64
4745import time
4846
47+
4948class TTSNode (Node ):
5049 def __init__ (self ):
51- super ().__init__ (' go2_tts_node' )
52-
50+ super ().__init__ (" go2_tts_node" )
51+
5352 # Initialize parameters
54- self .declare_parameter (' elevenlabs_api_key' , '' )
55- self .declare_parameter (' local_playback' , False ) # Default to robot playback
56-
57- self .api_key = self .get_parameter (' elevenlabs_api_key' ).value
58- self .local_playback = self .get_parameter (' local_playback' ).value
59-
53+ self .declare_parameter (" elevenlabs_api_key" , "" )
54+ self .declare_parameter (" local_playback" , False ) # Default to robot playback
55+
56+ self .api_key = self .get_parameter (" elevenlabs_api_key" ).value
57+ self .local_playback = self .get_parameter (" local_playback" ).value
58+
6059 if not self .api_key :
61- self .get_logger ().error (' ElevenLabs API key not provided!' )
60+ self .get_logger ().error (" ElevenLabs API key not provided!" )
6261 return
6362
6463 # Create subscription for TTS requests
6564 self .subscription = self .create_subscription (
66- TTSRequest ,
67- '/tts' ,
68- self .tts_callback ,
69- 10
65+ TTSRequest , "/tts" , self .tts_callback , 10
7066 )
71-
67+
7268 # Create publisher for robot audio hub requests
73- self .audio_pub = self .create_publisher (
74- Request ,
75- '/api/audiohub/request' ,
76- 10
77- )
78-
69+ self .audio_pub = self .create_publisher (Request , "/api/audiohub/request" , 10 )
70+
7971 # Create output directory for wave files
80- self .output_dir = ' tts_output'
72+ self .output_dir = " tts_output"
8173 os .makedirs (self .output_dir , exist_ok = True )
82-
83- self .get_logger ().info (f'TTS Node initialized ({ "local" if self .local_playback else "robot" } playback)' )
74+
75+ self .get_logger ().info (
76+ f'TTS Node initialized ({ "local" if self .local_playback else "robot" } playback)'
77+ )
8478
8579 def tts_callback (self , msg ):
8680 """Handle incoming TTS requests"""
8781 try :
88- self .get_logger ().info (f'Received TTS request: "{ msg .text } " with voice: { msg .voice_name } ' )
89-
82+ self .get_logger ().info (
83+ f'Received TTS request: "{ msg .text } " with voice: { msg .voice_name } '
84+ )
85+
9086 # Call ElevenLabs API
9187 audio_data = self .generate_speech (msg .text , msg .voice_name )
92-
88+
9389 if audio_data :
9490 # Save to WAV file
9591 timestamp = datetime .now ().strftime ("%Y%m%d_%H%M%S" )
9692 filename = f"{ self .output_dir } /tts_{ timestamp } .wav"
9793 wav_data = self .save_wav (audio_data , filename )
98-
94+
9995 if self .local_playback :
10096 # Play locally
10197 self .play_audio (audio_data )
10298 else :
10399 # Send to robot
104100 self .play_on_robot (wav_data )
105-
106- self .get_logger ().info (f'Successfully processed TTS request. Saved to { filename } ' )
101+
102+ self .get_logger ().info (
103+ f"Successfully processed TTS request. Saved to { filename } "
104+ )
107105 else :
108- self .get_logger ().error (' Failed to generate speech' )
109-
106+ self .get_logger ().error (" Failed to generate speech" )
107+
110108 except Exception as e :
111- self .get_logger ().error (f' Error processing TTS request: { str (e )} ' )
109+ self .get_logger ().error (f" Error processing TTS request: { str (e )} " )
112110
113111 def generate_speech (self , text , voice_name ):
114112 """Generate speech using ElevenLabs API"""
115113 url = f"https://api.elevenlabs.io/v1/text-to-speech/{ voice_name } "
116-
114+
117115 headers = {
118116 "Accept" : "audio/mpeg" ,
119117 "Content-Type" : "application/json" ,
120- "xi-api-key" : self .api_key
118+ "xi-api-key" : self .api_key ,
121119 }
122-
120+
123121 data = {
124122 "text" : text ,
125123 "model_id" : "eleven_turbo_v2_5" ,
126- "voice_settings" : {
127- "stability" : 0.5 ,
128- "similarity_boost" : 0.5
129- }
124+ "voice_settings" : {"stability" : 0.5 , "similarity_boost" : 0.5 },
130125 }
131-
126+
132127 try :
133128 response = requests .post (url , json = data , headers = headers )
134129 response .raise_for_status ()
135130 return response .content
136-
131+
137132 except requests .exceptions .RequestException as e :
138- self .get_logger ().error (f' API request failed: { str (e )} ' )
133+ self .get_logger ().error (f" API request failed: { str (e )} " )
139134 return None
140135
141136 def save_wav (self , audio_data , filename ):
142137 """Save audio data to WAV file and return the WAV data"""
143138 try :
144139 # Convert MP3 to WAV
145140 audio = AudioSegment .from_mp3 (io .BytesIO (audio_data ))
146-
141+
147142 # Export to file
148143 audio .export (filename , format = "wav" )
149- self .get_logger ().info (f' Saved WAV file: { filename } ' )
150-
144+ self .get_logger ().info (f" Saved WAV file: { filename } " )
145+
151146 # Return WAV data
152147 wav_io = io .BytesIO ()
153148 audio .export (wav_io , format = "wav" )
154149 return wav_io .getvalue ()
155-
150+
156151 except Exception as e :
157- self .get_logger ().error (f' Error saving WAV file: { str (e )} ' )
152+ self .get_logger ().error (f" Error saving WAV file: { str (e )} " )
158153 return None
159154
160155 def play_audio (self , audio_data ):
@@ -163,35 +158,35 @@ def play_audio(self, audio_data):
163158 audio = AudioSegment .from_mp3 (io .BytesIO (audio_data ))
164159 play (audio )
165160 except Exception as e :
166- self .get_logger ().error (f' Error playing audio: { str (e )} ' )
161+ self .get_logger ().error (f" Error playing audio: { str (e )} " )
167162
168- def split_into_chunks (self , data , chunk_size = 256 * 1024 ):
163+ def split_into_chunks (self , data , chunk_size = 256 * 1024 ):
169164 """Split data into chunks of specified size"""
170- return [data [i : i + chunk_size ] for i in range (0 , len (data ), chunk_size )]
165+ return [data [i : i + chunk_size ] for i in range (0 , len (data ), chunk_size )] # noqa: E203
171166
172167 def play_on_robot (self , wav_data ):
173168 """Send audio to robot's audio hub in chunks"""
174169 try :
175170 identity = int (time .time ())
176171 chunks = self .split_into_chunks (wav_data )
177172 total_chunks = len (chunks )
178-
179- self .get_logger ().info (f' Sending audio in { total_chunks } chunks' )
180-
173+
174+ self .get_logger ().info (f" Sending audio in { total_chunks } chunks" )
175+
181176 # Start audio
182177 start_req = Request ()
183178 start_req .header .identity .id = identity
184179 start_req .header .identity .api_id = 4001
185180 start_req .header .lease .id = 0
186181 start_req .header .policy .priority = 0
187182 start_req .header .policy .noreply = True
188- start_req .parameter = ''
183+ start_req .parameter = ""
189184 start_req .binary = []
190-
185+
191186 self .audio_pub .publish (start_req )
192187
193188 time .sleep (1 )
194-
189+
195190 # Send WAV data in chunks
196191 for chunk_idx , chunk in enumerate (chunks , 1 ):
197192 wav_req = Request ()
@@ -200,18 +195,20 @@ def play_on_robot(self, wav_data):
200195 wav_req .header .lease .id = 0
201196 wav_req .header .policy .priority = 0
202197 wav_req .header .policy .noreply = True
203-
198+
204199 audio_block = {
205200 "current_block_index" : chunk_idx ,
206201 "total_block_number" : total_chunks ,
207- "block_content" : base64 .b64encode (chunk ).decode (' utf-8' )
202+ "block_content" : base64 .b64encode (chunk ).decode (" utf-8" ),
208203 }
209204 wav_req .parameter = json .dumps (audio_block )
210205 wav_req .binary = []
211206
212207 self .audio_pub .publish (wav_req )
213- self .get_logger ().info (f'Sent chunk { chunk_idx } /{ total_chunks } ({ len (chunk )} bytes)' )
214-
208+ self .get_logger ().info (
209+ f"Sent chunk { chunk_idx } /{ total_chunks } ({ len (chunk )} bytes)"
210+ )
211+
215212 # Add a small delay between chunks to prevent flooding
216213 time .sleep (0.01 )
217214
@@ -220,31 +217,33 @@ def play_on_robot(self, wav_data):
220217 duration_ms = len (audio )
221218 duration_s = duration_ms / 1000.0
222219
223- self .get_logger ().info (f'Waiting for audio playback ({ duration_s :.2f} seconds)...' )
220+ self .get_logger ().info (
221+ f"Waiting for audio playback ({ duration_s :.2f} seconds)..."
222+ )
224223 time .sleep (duration_s )
225224
226-
227225 # End audio
228226 end_req = Request ()
229227 end_req .header .identity .id = identity
230228 end_req .header .identity .api_id = 4002
231229 end_req .header .lease .id = 0
232230 end_req .header .policy .priority = 0
233231 end_req .header .policy .noreply = True
234- end_req .parameter = ''
232+ end_req .parameter = ""
235233 end_req .binary = []
236-
234+
237235 self .audio_pub .publish (end_req )
238-
239- self .get_logger ().info (' Completed sending audio to robot' )
240-
236+
237+ self .get_logger ().info (" Completed sending audio to robot" )
238+
241239 except Exception as e :
242- self .get_logger ().error (f'Error sending audio to robot: { str (e )} ' )
240+ self .get_logger ().error (f"Error sending audio to robot: { str (e )} " )
241+
243242
244243def main (args = None ):
245244 rclpy .init (args = args )
246245 node = TTSNode ()
247-
246+
248247 try :
249248 rclpy .spin (node )
250249 except KeyboardInterrupt :
@@ -253,5 +252,6 @@ def main(args=None):
253252 node .destroy_node ()
254253 rclpy .shutdown ()
255254
256- if __name__ == '__main__' :
255+
256+ if __name__ == "__main__" :
257257 main ()
0 commit comments