#!/usr/bin/env python3 import rclpy, subprocess, os, wave from rclpy.node import Node from std_msgs.msg import String from piper import PiperVoice from piper.config import SynthesisConfig MODEL_PATH = '/home/sunrise/tts_model/zh_CN-huayan-medium.onnx' class TTSNode(Node): def __init__(self): super().__init__('tts_node') self.declare_parameter('audio_sink', 'alsa_output.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.analog-stereo') self.declare_parameter('result_topic', '/vlm_result') self.declare_parameter('tts_speed', 1.5) self.audio_sink = self.get_parameter('audio_sink').value result_topic = self.get_parameter('result_topic').value tts_speed = self.get_parameter('tts_speed').value self.length_scale = 1.0 / tts_speed self.espeak_speed = int(175 * tts_speed) # espeak default=175wpm, scale with tts_speed self.sub = self.create_subscription(String, result_topic, self.callback, 10) self.get_logger().info(f'Piper TTS started | voice=zh_CN-huayan | speed={tts_speed}x | sink={self.audio_sink}') self.get_logger().info(f'Loading model: {MODEL_PATH}') self.voice = PiperVoice.load(MODEL_PATH) self.get_logger().info('Model loaded OK') def callback(self, msg): text = msg.data.strip() if not text: return self.get_logger().info(f'TTS: {text}') try: syn_config = SynthesisConfig(length_scale=self.length_scale) wav_path = '/tmp/tts_out.wav' with wave.open(wav_path, 'wb') as wf: self.voice.synthesize_wav(text, wf, syn_config=syn_config) subprocess.Popen( ['paplay', f'--device={self.audio_sink}', wav_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except Exception as e: self.get_logger().error(f'Piper TTS failed, fallback espeak: {e}') subprocess.Popen( ['espeak-ng', '-v', 'cmn', '-s', str(self.espeak_speed), text], env={**os.environ, 'PULSE_SINK': self.audio_sink}, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def main(args=None): rclpy.init(args=args) node = TTSNode() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()