ROS 2 Humble과 TurtleBot3 Burger에서 Google Cloud Text-to-Speech로 문자열 음성 출력하기 #4

41) 부모 destroy_node 호출

return super().destroy_node()

작업 스레드 종료 처리를 마친 후 부모 클래스의 destroy_node()를 호출합니다.

이 과정에서 ROS 2 구독자, 타이머, 서비스, 노드 핸들 등의 자원이 정리됩니다.

42) main 함수

def main(args=None) -> None:

ROS 2 노드 실행 진입점입니다.

Python 패키지의 setup.py 또는 setup.cfg에서 콘솔 스크립트로 등록하면 ros2 run 명령으로 실행할 수 있습니다.

a. ROS 2 초기화
rclpy.init(args=args)

ROS 2 Python 클라이언트 라이브러리를 초기화합니다.

명령행으로 전달된 ROS 2 인자와 파라미터 재정의 옵션도 여기서 처리됩니다.

b. node 변수 초기화
node = None

노드 생성 중 예외가 발생하더라도 finally 블록에서 안전하게 상태를 확인할 수 있도록 초기값을 None으로 설정합니다.

이 코드가 없다면 노드 생성 전에 예외가 발생했을 때 node 변수를 참조하면서 또 다른 오류가 발생할 수 있습니다.

c. 노드 생성과 spin 실행
try:
    node = GoogleTtsNode()
    rclpy.spin(node)

GoogleTtsNode 객체를 생성한 후 rclpy.spin()으로 콜백 처리를 시작합니다.

rclpy.spin()은 노드가 종료될 때까지 실행되며 다음 이벤트를 처리합니다.

  1. 토픽 메시지 수신
  2. 서비스 요청
  3. 타이머 콜백
  4. 기타 ROS 2 이벤트

현재 노드는 주로 /tts 토픽 메시지를 처리합니다.

d. Ctrl+C 처리
except KeyboardInterrupt:
    pass

터미널에서 Ctrl+C를 누르면 KeyboardInterrupt 예외가 발생합니다.

이를 정상 종료 상황으로 처리하기 위해 별도의 오류 로그 없이 넘어갑니다.

e. 노드 정리
finally:
    if node is not None:
        node.destroy_node()

정상 종료, 키보드 인터럽트 또는 실행 중 예외가 발생한 경우에도 destroy_node()를 호출합니다.

이를 통해 작업 스레드 종료와 ROS 2 노드 자원 정리가 수행됩니다.

f. ROS 2 종료
if rclpy.ok():
    rclpy.shutdown()

ROS 2가 아직 활성 상태라면 rclpy.shutdown()을 호출합니다.

rclpy.ok()를 먼저 확인하므로 이미 종료된 상태에서 다시 종료 함수를 호출하는 문제를 방지합니다.

43) Python 직접 실행 처리

if __name__ == "__main__":
    main()

해당 Python 파일을 직접 실행했을 때 main() 함수를 호출합니다.

예를 들면 다음과 같습니다.

python3 google_tts_node.py

ROS 2 패키지에 등록되어 있다면 일반적으로 다음과 같이 실행합니다.

ros2 run google_tts_ros2 google_tts_node

11. 재생 작업을 별도 스레드로 처리하는 이유

Google Cloud API 호출과 음성 재생은 즉시 끝나는 작업이 아닙니다.

토픽 콜백 안에서 직접 다음 작업을 실행하면 문제가 생길 수 있습니다.

def callback(self, msg):
    self.engine.speak(msg.data)

이 방식에서는 음성이 끝날 때까지 ROS 2 콜백이 점유됩니다. 긴 문장을 읽는 동안 다른 콜백 처리가 지연될 수 있습니다.

이번 코드에서는 토픽 콜백이 문자열을 대기열에 넣고 즉시 반환합니다.

토픽 콜백
→ Queue에 문자열 추가
→ 즉시 반환

별도의 Worker Thread가 다음 작업을 처리합니다.

Queue에서 문자열 꺼내기
→ Google Cloud TTS 요청
→ 음성 재생
→ 다음 문자열 처리

따라서 여러 문자열이 연속으로 들어오더라도 순서대로 재생할 수 있습니다.

대기열이 무한히 증가하면 메모리와 API 사용량이 증가할 수 있으므로 최대 크기를 제한했습니다.

speech_queue_size = 10

대기열이 가득 차면 새로운 문자열은 버리고 경고 로그를 출력합니다.

12. Launch 파일 작성

launch/tts.launch.py 파일을 작성합니다.

from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    return LaunchDescription([
        Node(
            package="google_tts_ros2",
            executable="tts_node",
            name="google_tts_node",
            output="screen",
            parameters=[
                {
                    "topic_name": "/tts",
                    "language_code": "ko-KR",
                    "voice_name": "ko-KR-Standard-A",
                    "speaking_rate": 1.0,
                    "pitch": 0.0,
                    "volume_gain_db": 0.0,
                    "audio_device": "default",
                    "speech_queue_size": 10,
                    "startup_text": "",
                }
            ],
        )
    ])

USB 오디오 장치를 직접 지정하려면 다음과 같이 변경합니다.

"audio_device": "plughw:CARD=Device,DEV=0",

노드가 시작될 때 안내 음성을 출력하려면 startup_text를 설정합니다.

"startup_text": "터틀봇 음성 안내 시스템을 시작합니다.",

13. package.xml 작성

package.xml 파일을 다음과 같이 작성합니다.

<?xml version="1.0"?>
<?xml-model
  href="http://download.ros.org/schema/package_format3.xsd"
  schematypens="http://www.w3.org/2001/XMLSchema"?>

<package format="3">
  <name>google_tts_ros2</name>
  <version>0.0.1</version>

  <description>
    Google Cloud Text-to-Speech node for ROS 2 Humble
  </description>

  <maintainer email="your_email@example.com">
    Your Name
  </maintainer>

  <license>Apache-2.0</license>

  <buildtool_depend>ament_python</buildtool_depend>

  <exec_depend>rclpy</exec_depend>
  <exec_depend>std_msgs</exec_depend>
  <exec_depend>launch</exec_depend>
  <exec_depend>launch_ros</exec_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

export 영역의 다음 설정을 빠뜨리지 않아야 합니다.

<export>
  <build_type>ament_python</build_type>
</export>

이 값은 해당 패키지가 ament_python 빌드 형식임을 ROS 2 도구에 알려 줍니다.

14. setup.py 작성

setup.py 파일을 작성합니다.

import os
from glob import glob

from setuptools import find_packages
from setuptools import setup


package_name = "google_tts_ros2"


setup(
    name=package_name,
    version="0.0.1",
    packages=find_packages(
        exclude=["test"]
    ),
    data_files=[
        (
            "share/ament_index/resource_index/packages",
            [
                "resource/" + package_name
            ],
        ),
        (
            "share/" + package_name,
            [
                "package.xml"
            ],
        ),
        (
            os.path.join(
                "share",
                package_name,
                "launch",
            ),
            glob("launch/*.launch.py"),
        ),
    ],
    install_requires=[
        "setuptools",
        "google-cloud-texttospeech",
    ],
    zip_safe=True,
    maintainer="Your Name",
    maintainer_email="your_email@example.com",
    description=(
        "Google Cloud Text-to-Speech "
        "node for ROS 2"
    ),
    license="Apache-2.0",
    entry_points={
        "console_scripts": [
            (
                "tts_node = "
                "google_tts_ros2.tts_node:main"
            ),
            (
                "tts_say = "
                "google_tts_ros2.say:main"
            ),
        ],
    },
)

console_scripts에는 두 개의 실행 파일을 등록했습니다.

tts_node : /tts 토픽 구독 노드
tts_say  : 문자열 즉시 재생 명령

15. 패키지 빌드

작업 공간으로 이동합니다.

cd ~/tutlebot3_ws

ROS 2 의존성을 확인합니다.

rosdep install --from-paths src --ignore-src --rosdistro humble -r -y

패키지를 빌드합니다.

colcon build --packages-select google_tts_ros2

환경을 적용합니다.

source /opt/ros/humble/setup.bash
source ~/turtlebot3_ws/install/setup.bash

패키지 실행 파일을 확인합니다.

ros2 pkg executables google_tts_ros2

다음과 같이 표시되어야 합니다.

google_tts_ros2 tts_node
google_tts_ros2 tts_say

16. 문자열을 직접 음성으로 출력하기

먼저 Google Cloud 인증 환경변수를 확인합니다.

echo $GOOGLE_APPLICATION_CREDENTIALS

스피커의 기본 재생 상태도 확인합니다.

aplay /usr/share/sounds/alsa/Front_Center.wav

문자열을 음성으로 출력합니다.

ros2 run google_tts_ros2 tts_say "안녕하세요. 휴로 입니다."

USB 오디오 장치를 직접 지정하려면 다음과 같이 실행합니다.

ros2 run google_tts_ros2 tts_say "USB 스피커 출력 테스트입니다." \
  --audio-device "plughw:CARD=UACDemo10,DEV=0"

남성 음성으로 변경하려면 다음과 같이 실행할 수 있습니다.

ros2 run google_tts_ros2 tts_say "남성 음성 출력 테스트입니다." \
  --voice-name "ko-KR-Standard-C"

음성 속도를 느리게 설정합니다.

ros2 run google_tts_ros2 tts_say "천천히 안내 음성을 출력합니다." \
  --speaking-rate 0.85

음성 높이를 낮춥니다.

ros2 run google_tts_ros2 tts_say "음성 높이 조정 테스트입니다." --pitch -2.0

17. ROS 2 토픽으로 문자열 전달하기

TTS 노드를 실행합니다.

ros2 run google_tts_ros2 tts_node

정상적으로 실행되면 다음과 같은 로그가 출력됩니다.

[INFO] [google_tts_node]:
Google Cloud TTS 준비 완료: topic=/tts

새로운 터미널을 열고 ROS 2 환경을 적용합니다.

source /opt/ros/humble/setup.bash
source ~/turtlebot3_ws/install/setup.bash

/tts 토픽으로 문자열을 발행합니다.

ros2 topic pub --once /tts \
  std_msgs/msg/String "{data: '안녕하세요. 토픽으로 전달된 문자열입니다.'}"

TTS 노드에서 다음과 같은 로그가 출력됩니다.

[INFO] [google_tts_node]:
TTS 대기열 추가: 안녕하세요. 토픽으로 전달된 문자열입니다.

[INFO] [google_tts_node]:
음성 합성 및 재생 시작: 안녕하세요. 토픽으로 전달된 문자열입니다.

스피커에서는 입력한 문자열이 한국어 음성으로 재생됩니다.

18. Launch 파일로 실행하기

다음 명령으로 실행합니다.

ros2 launch google_tts_ros2 tts.launch.py

토픽을 발행합니다.

ros2 topic pub --once /tts \
  std_msgs/msg/String  "{data: '터틀봇 음성 안내 시스템이 정상적으로 동작합니다.'}"

19. ROS 2 파라미터로 설정 변경하기

실행할 때 음성을 변경할 수 있습니다.

ros2 run google_tts_ros2 tts_node \
  --ros-args \
  -p voice_name:="ko-KR-Neural2-C" \
  -p speaking_rate:=0.95 \
  -p pitch:=-1.0 \
  -p volume_gain_db:=2.0

USB 오디오 장치를 지정합니다.

ros2 run google_tts_ros2 tts_node \
  --ros-args \
  -p audio_device:="plughw:CARD=UACDemo10,DEV=0"

토픽 이름을 변경합니다.

ros2 run google_tts_ros2 tts_node \
  --ros-args -p topic_name:="/robot/speech"

변경된 토픽으로 문자열을 발행합니다.

ros2 topic pub --once /robot/speech \
  std_msgs/msg/String "{data: '로봇 음성 토픽 테스트입니다.'}"

시작 음성을 지정합니다.

ros2 run google_tts_ros2 tts_node \
  --ros-args -p startup_text:="휴로 시스템을 시작합니다."

현재 파라미터를 확인합니다.

ros2 param list /google_tts_node

특정 파라미터 값을 확인합니다.

ros2 param get /google_tts_node voice_name

20. 사용할 수 있는 한국어 음성

Google Cloud Text-to-Speech는 한국어 ko-KR에 대해 Standard, WaveNet, Neural2, Chirp3-HD 계열 음성을 제공합니다. 사용 가능한 음성은 Google Cloud 서비스 업데이트에 따라 변경될 수 있습니다.

대표적인 한국어 음성은 다음과 같습니다.

ko-KR-Standard-A   여성
ko-KR-Standard-B   여성
ko-KR-Standard-C   남성
ko-KR-Standard-D   남성

ko-KR-Wavenet-A    여성
ko-KR-Wavenet-B    여성
ko-KR-Wavenet-C    남성
ko-KR-Wavenet-D    남성

ko-KR-Neural2-A    여성
ko-KR-Neural2-B    여성
ko-KR-Neural2-C    남성

기본 테스트에서는 다음 음성을 권장합니다.

ko-KR-Standard-A

자연스러운 안내 음성이 필요한 경우 다음과 같은 음성을 테스트할 수 있습니다.

ko-KR-Neural2-A
ko-KR-Neural2-C

음성 계열에 따라 비용이 다를 수 있으므로 실제 로봇에 적용하기 전에 Google Cloud의 현재 가격 정책과 사용량 제한을 확인해야 합니다.

21. 다른 ROS 2 노드에서 음성 출력 요청하기

다른 Python ROS 2 노드에서 /tts Publisher를 생성하면 간단하게 음성 출력을 요청할 수 있습니다.

from std_msgs.msg import String


self.tts_publisher = self.create_publisher(
    String,
    "/tts",
    10,
)

음성 안내를 출력하는 함수를 작성합니다.

def speak(self, text: str) -> None:
    message = String()
    message.data = text

    self.tts_publisher.publish(message)

    self.get_logger().info(
        f"TTS 요청: {text}"
    )

사용 예시는 다음과 같습니다.

self.speak("목적지에 도착했습니다.")

장애물을 감지했을 때 사용할 수 있습니다.

if obstacle_detected:
    self.speak(
        "전방에 장애물이 있습니다."
    )

배터리가 부족할 때 사용할 수 있습니다.

if battery_percentage < 20.0:
    self.speak(
        "배터리가 부족합니다. "
        "충전이 필요합니다."
    )

자율주행이 완료되었을 때 사용할 수 있습니다.

if navigation_completed:
    self.speak(
        "목적지 이동을 완료했습니다."
    )

22. TurtleBot3 Navigation2와 연동하는 예

Navigation2 액션 결과가 성공했을 때 다음과 같이 TTS 메시지를 발행할 수 있습니다.

from action_msgs.msg import GoalStatus
from std_msgs.msg import String


def navigation_result_callback(
    self,
    future,
) -> None:
    result = future.result()

    message = String()

    if (
        result.status
        == GoalStatus.STATUS_SUCCEEDED
    ):
        message.data = (
            "목적지에 도착했습니다."
        )

    elif (
        result.status
        == GoalStatus.STATUS_CANCELED
    ):
        message.data = (
            "이동 명령이 취소되었습니다."
        )

    else:
        message.data = (
            "목적지 이동에 실패했습니다."
        )

    self.tts_publisher.publish(message)

이 구조를 사용하면 Navigation2 노드가 Google Cloud API를 직접 알 필요가 없습니다.

Navigation2 노드는 /tts 토픽에 문자열만 발행하고, 음성 합성과 스피커 출력은 TTS 노드가 담당합니다.

이처럼 기능을 분리하면 다음과 같은 장점이 있습니다.

  • 자율주행 코드와 음성 코드를 분리할 수 있음
  • 음성 엔진을 다른 서비스로 교체하기 쉬움
  • 여러 노드가 하나의 TTS 노드를 공유할 수 있음
  • 음성 출력 순서를 중앙에서 관리할 수 있음
  • 테스트할 때 ros2 topic pub 명령을 사용할 수 있음

23. 자주 발생하는 오류와 해결 방법

1) DefaultCredentialsError

오류 예시는 다음과 같습니다.

google.auth.exceptions.DefaultCredentialsError:
Could not automatically determine credentials

환경변수를 확인합니다.

echo $GOOGLE_APPLICATION_CREDENTIALS

파일 존재 여부를 확인합니다.

ls -l $GOOGLE_APPLICATION_CREDENTIALS

현재 터미널에 다시 적용합니다.

export GOOGLE_APPLICATION_CREDENTIALS="$HOME/.config/google/turtlebot3-tts.json"

ROS 2 노드를 sudo로 실행하면 일반 사용자 환경변수가 전달되지 않을 수 있습니다.

다음과 같이 실행하지 않는 것이 좋습니다.

sudo ros2 run google_tts_ros2 tts_node

일반 사용자 권한으로 실행합니다.

ros2 run google_tts_ros2 tts_node

2) 403 Permission Denied

오류 예시는 다음과 같습니다.

403 PermissionDenied

다음 항목을 확인합니다.

  • Cloud Text-to-Speech API가 활성화되어 있는지
  • 결제 계정이 연결되어 있는지
  • JSON 키가 올바른 프로젝트에서 생성되었는지
  • 서비스 계정이 삭제되거나 비활성화되지 않았는지
  • Service Usage Consumer 권한이 있는지
  • API 사용 한도를 초과하지 않았는지

3) aplay를 찾을 수 없음

오류 예시는 다음과 같습니다.

aplay를 찾을 수 없습니다

다음 패키지를 설치합니다.

sudo apt install -y alsa-utils

4) 소리가 출력되지 않음

오디오 장치를 확인합니다.

aplay -l
aplay -L

직접 재생 테스트를 합니다.

aplay -D plughw:1,0 \
  /usr/share/sounds/alsa/Front_Center.wav

ROS 2 노드에도 같은 장치를 설정합니다.

ros2 run google_tts_ros2 tts_node \
  --ros-args \
  -p audio_device:="plughw:1,0"

5) Device or resource busy

오류 예시는 다음과 같습니다.

audio open error:
Device or resource busy

다른 프로그램이 오디오 장치를 사용하고 있는지 확인합니다.

fuser -v /dev/snd/*

필요하다면 해당 프로세스를 종료합니다.

kill <PID>

여러 프로세스가 하나의 장치를 공유해야 한다면 hw 대신 default, plughw, dmix 장치를 사용합니다.

6) 음성이 너무 빠르거나 느림

속도를 조절합니다.

ros2 run google_tts_ros2 tts_node \
  --ros-args \
  -p speaking_rate:=0.9

대표적인 값은 다음과 같습니다.

0.8 : 느린 안내
1.0 : 기본 속도
1.2 : 빠른 안내

7) 토픽을 발행했지만 재생되지 않음

토픽 이름을 확인합니다.

ros2 topic list

토픽 형식을 확인합니다.

ros2 topic type /tts

정상 출력은 다음과 같습니다.

std_msgs/msg/String

TTS 노드가 실제로 구독하고 있는지 확인합니다.

ros2 topic info /tts

메시지가 발행되는지 확인합니다.

ros2 topic echo /tts

ROS_DOMAIN_ID도 확인합니다.

echo $ROS_DOMAIN_ID

Publisher와 Subscriber의 ROS_DOMAIN_ID가 다르면 서로 통신하지 못합니다.

8) Python 모듈을 찾을 수 없음

오류 예시는 다음과 같습니다.

ModuleNotFoundError:
No module named 'google.cloud'

현재 사용자의 Python 환경에 다시 설치합니다.

python3 -m pip install \
  --user \
  --upgrade \
  google-cloud-texttospeech

설치 위치를 확인합니다.

python3 -m pip show \
  google-cloud-texttospeech

24. 실제 로봇에 적용할 때 고려할 점

1) 인터넷 연결이 필요함

Google Cloud Text-to-Speech는 클라우드 API이므로 인터넷 연결이 필요합니다.

네트워크가 끊기면 새로운 문자열을 음성으로 합성할 수 없습니다.

다음과 같은 안전 관련 안내를 클라우드 TTS에만 의존하면 안 됩니다.

긴급 정지
모터 과전류
배터리 위험
화재 또는 충돌 경고

긴급 경고는 로컬 비프음, LED, 저장된 WAV 파일과 함께 사용하는 것이 안전합니다.

2) 자주 사용하는 문장은 캐시하는 것이 좋음

다음 문장은 로봇에서 반복적으로 사용될 가능성이 높습니다.

목적지에 도착했습니다.
배터리가 부족합니다.
충전을 시작합니다.
전방에 장애물이 있습니다.
작업을 완료했습니다.

이 문장들을 매번 API로 합성하면 네트워크 지연과 API 사용량이 증가합니다.

실제 운영에서는 한 번 생성한 음성 데이터를 파일로 저장하고, 같은 문장이 들어오면 저장된 파일을 재생하는 캐시 구조가 효율적입니다.

3) 긴급 안내와 일반 안내를 분리해야 함

현재 구현은 입력된 순서대로 모든 문자열을 재생합니다.

긴 문장을 재생하는 동안 긴급 메시지가 들어오면 기존 문장이 끝날 때까지 기다려야 합니다.

실제 서비스 로봇에서는 다음 기능을 추가할 수 있습니다.

  • 메시지 우선순위
  • 현재 음성 재생 중단
  • 긴급 메시지 즉시 재생
  • 중복 메시지 제거
  • 일정 시간 내 동일 메시지 재생 제한

예를 들어 메시지를 다음과 같이 확장할 수 있습니다.

text
priority
interrupt
repeat_count

이 경우 std_msgs/msg/String 대신 사용자 정의 메시지를 만드는 것이 좋습니다.

4) 서비스 계정 키 관리

서비스 계정 JSON 파일은 로봇 저장장치에 평문으로 존재합니다.

다음 보안 대책이 필요합니다.

  • 파일 권한을 600으로 제한
  • ROS 2 패키지와 별도 위치에 저장
  • Git 저장소에 추가하지 않기
  • 로봇 분실 시 즉시 키 폐기
  • 개발용과 운영용 서비스 계정 분리
  • 주기적으로 키 교체
  • 불필요한 IAM 권한 제거

5) 비용과 사용량 제한

Cloud Text-to-Speech는 음성 모델과 처리한 문자 수에 따라 비용이 발생할 수 있습니다.

무한 반복이나 센서 오류로 같은 메시지가 계속 발행되면 예상보다 많은 API 요청이 발생할 수 있습니다.

다음 제한을 추가하는 것이 좋습니다.

  • 동일 문장 재생 간격 제한
  • 분당 최대 요청 수 제한
  • 최대 문자열 길이 제한
  • 대기열 최대 크기 제한
  • Google Cloud 예산 알림 설정

현재 코드는 대기열 최대 크기를 제한하지만 동일 문자열의 반복은 제한하지 않습니다.

25. 최종 테스트 순서

전체 시스템은 다음 순서로 확인하면 됩니다.

오디오 장치를 확인합니다.

aplay -l

스피커를 테스트합니다.

aplay /usr/share/sounds/alsa/Front_Center.wav

Google Cloud 인증 정보를 확인합니다.

echo $GOOGLE_APPLICATION_CREDENTIALS

Python 라이브러리를 확인합니다.

python3 -c \
  "from google.cloud import texttospeech; print('OK')"

문자열 직접 재생을 테스트합니다.

ros2 run google_tts_ros2 tts_say \
  "직접 음성 출력 테스트입니다."

토픽 구독 노드를 실행합니다.

ros2 launch google_tts_ros2 tts.launch.py

토픽으로 문자열을 전달합니다.

ros2 topic pub \
  --once \
  /tts \
  std_msgs/msg/String \
  "{data: '토픽 음성 출력 테스트입니다.'}"

모든 단계가 정상이라면 TurtleBot3 Burger에서 Google Cloud Text-to-Speech를 이용한 ROS 2 음성 안내 시스템이 완성된 것입니다.

Leave a Comment