forked from zbw/yiliao2026
62 lines
2.4 KiB
Python
62 lines
2.4 KiB
Python
#!/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()
|