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

TurtleBot3 Burger에 스피커를 연결하면 단순한 비프음뿐만 아니라 현재 상태, 경고, 작업 결과를 실제 음성으로 안내할 수 있습니다.

예를 들어 다음과 같은 상황에서 음성 안내를 활용할 수 있습니다.

  • 로봇이 출발하거나 목적지에 도착했을 때
  • 장애물을 감지했을 때
  • 배터리 잔량이 부족할 때
  • 자율주행이나 작업이 완료되었을 때
  • 사용자의 명령을 정상적으로 처리했을 때
  • 센서나 모터 오류가 발생했을 때

이 글에서는 ROS 2 Humble을 사용하는 TurtleBot3 Burger에서 Google Cloud Text-to-Speech API를 이용해 다음 기능을 구현합니다.

  1. 터미널에서 입력한 문자열을 즉시 음성으로 출력
  2. ROS 2 std_msgs/msg/String 토픽으로 문자열을 받아 음성 출력
  3. 음성 종류, 속도, 높이, 볼륨을 ROS 2 파라미터로 설정
  4. 여러 문자열이 연속으로 들어올 때 순서대로 재생
  5. USB 스피커와 ALSA 오디오 장치 선택

Google Cloud Text-to-Speech는 일반 텍스트 또는 SSML을 입력받아 음성 데이터를 생성하며, Python 클라이언트 라이브러리는 Application Default Credentials, ADC 방식으로 인증할 수 있습니다.

1. 전체 시스템 구성

이번에 구현할 시스템의 흐름은 다음과 같습니다.

ROS 2 노드 또는 ros2 topic pub
            │
            ▼
     /tts 토픽
 std_msgs/msg/String
            │
            ▼
  google_tts_node
            │
            ▼
Google Cloud Text-to-Speech API
            │
            ▼
   LINEAR16 음성 데이터
            │
            ▼
        aplay
            │
            ▼
 USB 스피커 또는 오디오 장치

ROS 2 노드는 /tts 토픽을 구독합니다. 문자열 메시지를 받으면 Google Cloud Text-to-Speech API로 전송하고, 반환된 음성 데이터를 Linux의 aplay 명령으로 재생합니다.

ROS 2에서 Publisher와 Subscriber가 통신하려면 토픽 이름과 메시지 형식이 일치해야 합니다. 이번 구현에서는 기본 문자열 메시지인 std_msgs/msg/String을 사용합니다.

2. 개발 환경

이 글에서 사용하는 환경은 다음과 같습니다.

Robot        : TurtleBot3 Burger
Computer     : Raspberry Pi 또는 TurtleBot3 SBC
OS           : Ubuntu 22.04
ROS          : ROS 2 Humble
Language     : Python 3
Audio        : USB 오디오 동글 또는 USB 스피커
Cloud API    : Google Cloud Text-to-Speech

ROS 2 Humble의 대표 지원 환경은 Ubuntu 22.04이며 Python과 C++ 클라이언트를 사용할 수 있습니다.

TurtleBot3 Burger에는 기본적으로 일반 PC와 같은 내장 스피커가 없기 때문에 별도의 USB 오디오 장치나 USB 스피커를 연결하는 것이 가장 간단합니다.

블루투스 스피커도 사용할 수 있지만, 부팅 후 자동 연결과 PulseAudio 또는 PipeWire 설정이 추가로 필요하므로 로봇에서는 USB 오디오 장치를 권장합니다.

3. Google Cloud 프로젝트 준비 및 인증 파일을 TurtleBot3로 복사

아래의 사이트를 참조하세요

4. Google Cloud Python 라이브러리 설치

Python 패키지 관리 도구를 설치합니다.

sudo apt install -y python3-pip

Google Cloud Text-to-Speech 클라이언트 라이브러리를 설치합니다.

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

설치를 확인합니다.

python3 -c "from google.cloud import texttospeech; print('Google Cloud TTS library OK')"

다음 메시지가 출력되면 정상입니다.

Google Cloud TTS library OK

Google 공식 Python 예제에서도 google.cloud.texttospeech 모듈의 TextToSpeechClient를 생성하고 synthesize_speech()를 호출하는 방식을 사용합니다.

sudo pip install은 시스템 Python 환경과 ROS 2 패키지 환경을 손상시킬 수 있으므로 사용하지 않는 편이 좋습니다.

7. ROS 2 Python 패키지 생성

ROS 2 Python 패키지를 생성합니다.

cd ~/turtlebot3_ws/src

ros2 pkg create --build-type ament_python \
  --license Apache-2.0 google_tts_ros2 --dependencies rclpy std_msgs

ROS 2에서는 ament_python 빌드 형식으로 Python 패키지를 만들 수 있으며 rclpystd_msgs를 실행 의존성으로 사용합니다.

필요한 디렉터리와 파일을 생성합니다.

cd ~/turtlebot3_ws/src/google_tts_ros2

mkdir -p launch

touch google_tts_ros2/tts_engine.py
touch google_tts_ros2/tts_node.py
touch google_tts_ros2/say.py
touch launch/tts.launch.py

최종 디렉터리 구조는 다음과 같습니다.

google_tts_ros2/
├── google_tts_ros2/
│   ├── __init__.py
│   ├── tts_engine.py
│   └── tts_node.py
├── launch/
│   └── tts.launch.py
├── resource/
│   └── google_tts_ros2
├── package.xml
├── setup.cfg
└── setup.py

8. Google Cloud TTS 처리 클래스 작성

google_tts_ros2/tts_engine.py 파일을 작성합니다.

import subprocess

from google.cloud import texttospeech


class GoogleTtsEngine:
    def __init__(
        self,
        language_code: str = "ko-KR",
        voice_name: str = "ko-KR-Standard-A",
        speaking_rate: float = 1.0,
        pitch: float = 0.0,
        volume_gain_db: float = 0.0,
        audio_device: str = "default",
    ) -> None:
        self.language_code = language_code
        self.voice_name = voice_name
        self.speaking_rate = speaking_rate
        self.pitch = pitch
        self.volume_gain_db = volume_gain_db
        self.audio_device = audio_device

        self.client = texttospeech.TextToSpeechClient()

    def synthesize(self, text: str) -> bytes:
        clean_text = text.strip()

        if not clean_text:
            raise ValueError("음성으로 변환할 문자열이 비어 있습니다.")

        synthesis_input = texttospeech.SynthesisInput(
            text=clean_text
        )

        voice_options = {
            "language_code": self.language_code
        }

        if self.voice_name:
            voice_options["name"] = self.voice_name

        voice = texttospeech.VoiceSelectionParams(
            **voice_options
        )

        audio_config = texttospeech.AudioConfig(
            audio_encoding=texttospeech.AudioEncoding.LINEAR16,
            speaking_rate=self.speaking_rate,
            pitch=self.pitch,
            volume_gain_db=self.volume_gain_db,
        )

        response = self.client.synthesize_speech(
            input=synthesis_input,
            voice=voice,
            audio_config=audio_config,
        )

        return response.audio_content

    def play(self, audio_content: bytes) -> None:
        command = ["aplay", "-q"]

        if self.audio_device and self.audio_device != "default":
            command.extend([
                "-D",
                self.audio_device,
            ])

        result = subprocess.run(
            command,
            input=audio_content,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.PIPE,
            check=False,
        )

        if result.returncode != 0:
            error_message = result.stderr.decode(
                "utf-8",
                errors="replace",
            ).strip()

            raise RuntimeError(
                "aplay 실행 실패"
                f"(return code={result.returncode}): "
                f"{error_message}"
            )

    def speak(self, text: str) -> None:
        audio_content = self.synthesize(text)
        self.play(audio_content)

1) subprocess 모듈의 역할

소스 코드의 첫 번째 줄에서는 Python 표준 라이브러리인 subprocess를 가져옵니다.

import subprocess

subprocess 모듈은 Python 프로그램에서 외부 명령어나 다른 프로그램을 실행할 때 사용합니다.

이 코드에서는 다음 Linux 명령어를 실행하기 위해 사용합니다.

aplay -q

aplay는 WAV 형식의 오디오 데이터를 재생하는 ALSA 기반 명령어입니다.

Python 코드에서는 음성 데이터를 파일로 저장하지 않고 aplay 프로세스의 표준 입력으로 직접 전달합니다.

이 방식의 장점은 다음과 같습니다.

  1. 임시 WAV 파일을 만들 필요가 없습니다.
  2. 파일 삭제 처리가 필요하지 않습니다.
  3. 저장 장치에 불필요한 쓰기가 발생하지 않습니다.
  4. 음성 생성 후 바로 재생할 수 있습니다.
  5. 반복 음성 출력 시스템을 단순하게 구현할 수 있습니다.

2) Google Text-to-Speech 모듈 가져오기

다음 코드는 Google Cloud Text-to-Speech 라이브러리를 가져옵니다.

from google.cloud import texttospeech

이 모듈에서는 다음 기능을 사용합니다.

  1. TextToSpeechClient
  2. SynthesisInput
  3. VoiceSelectionParams
  4. AudioConfig
  5. AudioEncoding

각 객체는 음성 생성 요청의 특정 설정을 담당합니다.

3) GoogleTtsEngine 클래스

class GoogleTtsEngine:

GoogleTtsEngine 클래스는 음성 합성에 필요한 설정과 동작을 하나의 객체로 관리합니다.

클래스 내부 기능은 크게 다음과 같이 나뉩니다.

  1. 초기 설정과 클라이언트 생성
  2. 문자열을 음성 데이터로 변환
  3. 음성 데이터 재생
  4. 합성과 재생을 한 번에 실행

이러한 구조를 사용하면 다른 Python 프로그램이나 ROS 2 노드에서 쉽게 재사용할 수 있습니다.

예를 들어 다음처럼 객체를 한 번 생성한 뒤 여러 문장을 출력할 수 있습니다.

tts = GoogleTtsEngine()

tts.speak("시스템을 시작합니다.")
tts.speak("배터리 상태가 정상입니다.")
tts.speak("임무를 시작합니다.")

4) 생성자 메서드 구조

클래스의 생성자는 다음과 같습니다.

def __init__(
    self,
    language_code: str = "ko-KR",
    voice_name: str = "ko-KR-Standard-A",
    speaking_rate: float = 1.0,
    pitch: float = 0.0,
    volume_gain_db: float = 0.0,
    audio_device: str = "default",
) -> None:

생성자는 음성 합성에 사용할 기본 옵션을 전달받습니다.

Python 타입 힌트를 사용하여 각 인수의 예상 자료형도 명확하게 표시하고 있습니다.

language_code: str
speaking_rate: float

-> None은 생성자 메서드가 별도의 값을 반환하지 않는다는 의미입니다.

5) language_code 설정

language_code: str = "ko-KR"

language_code는 음성을 합성할 언어와 지역을 지정합니다.

기본값인 ko-KR은 대한민국 한국어를 의미합니다.

대표적인 언어 코드는 다음과 같습니다.

  1. ko-KR: 한국어
  2. en-US: 미국 영어
  3. en-GB: 영국 영어
  4. ja-JP: 일본어
  5. zh-CN: 중국어 간체
  6. de-DE: 독일어
  7. fr-FR: 프랑스어

한국어 문장을 출력할 때는 일반적으로 ko-KR을 사용합니다.

tts = GoogleTtsEngine(
    language_code="ko-KR"
)

6) voice_name 설정

voice_name: str = "ko-KR-Standard-A"

voice_name은 실제로 사용할 음성 모델을 지정합니다.

언어 코드만 지정하면 Google Cloud가 해당 언어에 맞는 기본 음성을 선택할 수 있습니다. 하지만 음성 이름까지 지정하면 원하는 목소리를 명확하게 선택할 수 있습니다.

이 코드에서는 기본값으로 다음 음성을 사용합니다.

ko-KR-Standard-A

음성 모델에 따라 다음 요소가 달라질 수 있습니다.

  1. 남성 또는 여성 음성
  2. 목소리의 높낮이
  3. 발음의 자연스러움
  4. 음성 생성 비용
  5. 지원되는 기능
  6. 응답 속도

음성 이름을 빈 문자열로 전달하면 언어 코드만 사용하도록 설계되어 있습니다.

tts = GoogleTtsEngine(
    voice_name=""
)

7) speaking_rate 설정

speaking_rate: float = 1.0

speaking_rate는 음성 재생 속도를 조절합니다.

기본값 1.0은 원래 속도를 의미합니다.

speaking_rate=0.8

위 설정은 기본보다 느리게 읽도록 합니다.

speaking_rate=1.2

위 설정은 기본보다 빠르게 읽도록 합니다.

안내 방송이나 경고 메시지는 너무 빠르게 출력하면 사용자가 내용을 놓칠 수 있습니다. 따라서 장비 상태 안내에는 일반적으로 0.9에서 1.1 정도의 값이 무난합니다.

예제는 다음과 같습니다.

tts = GoogleTtsEngine(
    speaking_rate=0.95
)

8) pitch 설정

pitch: float = 0.0

pitch는 음성의 높낮이를 조절합니다.

기본값 0.0은 원래 음높이를 의미합니다.

양수 값을 사용하면 목소리가 높아지고, 음수 값을 사용하면 목소리가 낮아집니다.

tts = GoogleTtsEngine(
    pitch=-2.0
)

위 설정은 기본보다 낮은 음성으로 출력합니다.

tts = GoogleTtsEngine(
    pitch=2.0
)

위 설정은 기본보다 높은 음성으로 출력합니다.

장비 경고음이나 시스템 안내음은 지나치게 높은 목소리보다 중립적이거나 약간 낮은 목소리가 듣기 편한 경우가 많습니다.

9) volume_gain_db 설정

volume_gain_db: float = 0.0

volume_gain_db는 생성되는 음성의 볼륨 증폭 값을 데시벨 단위로 설정합니다.

기본값 0.0은 볼륨을 추가로 증폭하거나 감소시키지 않는다는 의미입니다.

volume_gain_db=3.0

위 설정은 음량을 증가시킵니다.

volume_gain_db=-3.0

위 설정은 음량을 감소시킵니다.

다만 음성 합성 단계의 볼륨 조절과 실제 운영체제의 스피커 볼륨은 서로 다른 개념입니다.

최종 출력 크기는 다음 요소의 영향을 받습니다.

  1. Google TTS의 volume_gain_db
  2. Linux ALSA 믹서 볼륨
  3. USB 오디오 장치의 출력 수준
  4. 앰프의 증폭률
  5. 스피커 자체 출력

ALSA 볼륨 상태는 다음 명령어로 확인할 수 있습니다.

alsamixer

10) audio_device 설정

audio_device: str = "default"

audio_deviceaplay가 사용할 ALSA 출력 장치를 지정합니다.

기본값 default를 사용하면 시스템의 기본 오디오 장치로 재생합니다.

USB 오디오 장치나 HDMI 오디오 장치를 직접 선택해야 하는 경우 장치 이름을 설정할 수 있습니다.

사용 가능한 ALSA 장치는 다음 명령어로 확인합니다.

aplay -L

하드웨어 장치 목록은 다음 명령어로 확인할 수 있습니다.

aplay -l

예를 들어 장치 이름이 plughw:2,0이라면 다음처럼 객체를 생성할 수 있습니다.

tts = GoogleTtsEngine(
    audio_device="plughw:2,0"
)

Jetson이나 Raspberry Pi처럼 여러 오디오 출력 장치가 연결된 시스템에서는 장치를 명시적으로 지정하는 편이 안정적입니다.

11) 인스턴스 변수 저장

생성자로 전달받은 값은 다음과 같이 인스턴스 변수에 저장됩니다.

self.language_code = language_code
self.voice_name = voice_name
self.speaking_rate = speaking_rate
self.pitch = pitch
self.volume_gain_db = volume_gain_db
self.audio_device = audio_device

self는 현재 생성된 GoogleTtsEngine 객체 자신을 의미합니다.

예를 들어 다음 객체를 생성하면

tts = GoogleTtsEngine(
    speaking_rate=1.1
)

객체 내부에는 다음 값이 저장됩니다.

tts.speaking_rate

결과는 다음과 같습니다.

1.1

12) Google Cloud 클라이언트 생성

self.client = texttospeech.TextToSpeechClient()

이 코드는 Google Cloud Text-to-Speech API와 통신하기 위한 클라이언트 객체를 생성합니다.

클라이언트는 이후 다음 작업을 수행합니다.

  1. 인증 정보 확인
  2. Google Cloud 서버 연결
  3. 음성 합성 요청 전송
  4. 응답 데이터 수신
  5. API 오류 전달

클라이언트를 생성자에서 한 번만 생성하기 때문에 speak()를 호출할 때마다 새로운 클라이언트를 만들 필요가 없습니다.

반복적으로 음성을 출력하는 시스템에서는 이런 방식이 더 효율적입니다.

13) synthesize 메서드의 역할

def synthesize(self, text: str) -> bytes:

synthesize() 메서드는 문자열을 받아 음성 데이터로 변환합니다.

입력값은 문자열이고 반환값은 bytes 형식입니다.

text: str

입력 문장이 문자열임을 나타냅니다.

-> bytes

음성 데이터가 바이트 배열로 반환된다는 의미입니다.

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

audio_data = tts.synthesize(
    "안녕하세요."
)

이 단계에서는 음성이 재생되지 않습니다. Google Cloud가 생성한 오디오 데이터만 메모리에 반환됩니다.

14) 입력 문자열 정리

clean_text = text.strip()

strip()은 문자열 앞뒤의 공백과 줄바꿈 문자를 제거합니다.

예를 들어 다음 문자열이 전달되었다고 가정할 수 있습니다.

text = "   시스템을 시작합니다.   "

strip() 처리 후에는 다음과 같이 됩니다.

시스템을 시작합니다.

사용자가 입력한 문자열에 불필요한 공백이 있더라도 정상적으로 처리할 수 있습니다.

15) 빈 문자열 검사

if not clean_text:
    raise ValueError("음성으로 변환할 문자열이 비어 있습니다.")

공백을 제거한 결과 문자열이 비어 있다면 음성 합성을 진행하지 않습니다.

예를 들어 다음 입력은 모두 오류 처리됩니다.

tts.speak("")
tts.speak("   ")
tts.speak("\n\t")

빈 문자열을 Google API로 보내기 전에 로컬에서 검사하므로 불필요한 API 요청을 방지할 수 있습니다.

ValueError를 사용하는 이유는 메서드에 전달된 값이 올바르지 않기 때문입니다.

호출하는 쪽에서는 다음과 같이 예외를 처리할 수 있습니다.

try:
    tts.speak("   ")
except ValueError as error:
    print(error)

16) SynthesisInput 생성

synthesis_input = texttospeech.SynthesisInput(
    text=clean_text
)

SynthesisInput은 Google Cloud에 전달할 입력 문장을 정의합니다.

현재 코드에서는 일반 문자열을 사용합니다.

text=clean_text

Google Cloud Text-to-Speech는 일반 텍스트 외에도 SSML 방식의 입력을 지원할 수 있습니다.

SSML(Speech Synthesis Markup Language)은 TTS(Text-to-Speech)에 일반 문장 대신 XML 형식의 태그를 넣어 발음, 속도, 높낮이, 쉼, 강조 등을 제어하는 입력 방식입니다.

SSML을 사용하면 다음과 같은 세부 제어가 가능합니다.

  1. 문장 사이의 정지 시간
  2. 특정 단어의 강조
  3. 숫자 읽기 방식
  4. 날짜 읽기 방식
  5. 약어 발음 방식
  6. 말하기 속도 일부 변경
  7. 음높이 일부 변경

현재 클래스는 단순성과 범용성을 위해 일반 텍스트 입력만 사용합니다.

17) voice_options 딕셔너리 생성

voice_options = {
    "language_code": self.language_code
}

음성 선택 옵션을 딕셔너리 형태로 구성합니다.

최초에는 언어 코드만 포함합니다.

예를 들어 기본 설정에서는 다음과 같은 딕셔너리가 만들어집니다.

{
    "language_code": "ko-KR"
}

18) 음성 이름 조건부 추가

if self.voice_name:
    voice_options["name"] = self.voice_name

voice_name 값이 존재하는 경우에만 음성 이름을 딕셔너리에 추가합니다.

기본 설정에서는 결과가 다음과 같습니다.

{
    "language_code": "ko-KR",
    "name": "ko-KR-Standard-A"
}

반대로 다음처럼 빈 문자열을 전달하면

tts = GoogleTtsEngine(
    voice_name=""
)

name 항목은 추가되지 않습니다.

이 구조를 사용하면 음성 이름을 반드시 지정하지 않아도 됩니다.

19) VoiceSelectionParams 생성

voice = texttospeech.VoiceSelectionParams(
    **voice_options
)

VoiceSelectionParams는 사용할 언어와 음성 모델을 정의합니다.

여기서 **voice_options는 딕셔너리에 저장된 값을 키워드 인수로 펼쳐 전달하는 Python 문법입니다.

다음 코드는

voice_options = {
    "language_code": "ko-KR",
    "name": "ko-KR-Standard-A",
}

실질적으로 다음 코드와 같은 의미입니다.

voice = texttospeech.VoiceSelectionParams(
    language_code="ko-KR",
    name="ko-KR-Standard-A",
)

딕셔너리를 사용한 이유는 voice_name을 선택적으로 전달하기 위해서입니다.

20) AudioConfig 생성

audio_config = texttospeech.AudioConfig(
    audio_encoding=texttospeech.AudioEncoding.LINEAR16,
    speaking_rate=self.speaking_rate,
    pitch=self.pitch,
    volume_gain_db=self.volume_gain_db,
)

AudioConfig는 생성될 오디오 데이터의 형식과 음성 출력 속성을 정의합니다.

설정 항목은 다음과 같습니다.

  1. 오디오 인코딩 형식
  2. 말하기 속도
  3. 음높이
  4. 볼륨 증폭값

21) LINEAR16 오디오 형식

audio_encoding=texttospeech.AudioEncoding.LINEAR16

LINEAR16은 16비트 PCM 기반의 비압축 오디오 형식입니다.

일반적으로 WAV 재생 환경에서 다루기 쉬운 형식입니다.

이 예제에서 LINEAR16을 선택한 가장 큰 이유는 aplay와 직접 연결하기 편하기 때문입니다.

MP3처럼 압축된 형식을 사용하면 환경에 따라 별도의 디코더가 필요할 수 있습니다.

LINEAR16 방식의 특징은 다음과 같습니다.

  1. 음질 손실이 적습니다.
  2. 디코딩 과정이 단순합니다.
  3. Linux ALSA 환경에서 재생하기 쉽습니다.
  4. MP3보다 데이터 크기가 큽니다.
  5. 실시간 시스템에서 처리 구조가 단순합니다.

로봇이나 드론의 상태 안내처럼 짧은 문장을 출력하는 용도라면 데이터 크기가 크게 문제가 되지 않습니다.

22) 음성 합성 API 호출

response = self.client.synthesize_speech(
    input=synthesis_input,
    voice=voice,
    audio_config=audio_config,
)

이 부분에서 실제 Google Cloud Text-to-Speech API 요청이 발생합니다.

전달되는 정보는 다음과 같습니다.

  1. input: 읽을 문자열
  2. voice: 사용할 언어와 음성
  3. audio_config: 오디오 형식과 속도, 음높이, 볼륨

API 호출이 정상적으로 처리되면 응답 객체가 반환됩니다.

네트워크가 연결되지 않았거나 인증 정보가 잘못된 경우에는 이 단계에서 예외가 발생합니다.

주요 오류 원인은 다음과 같습니다.

  1. 인터넷 연결 실패
  2. 서비스 계정 인증 실패
  3. Text-to-Speech API 비활성화
  4. 프로젝트 결제 설정 문제
  5. 사용량 제한 초과
  6. 잘못된 음성 이름
  7. 지원되지 않는 언어 코드
  8. 잘못된 속도 또는 음높이 값

23) 오디오 데이터 반환

return response.audio_content

Google Cloud 응답에서 실제 오디오 데이터만 추출하여 반환합니다.

response.audio_content의 자료형은 bytes입니다.

이 데이터를 파일로 저장하면 WAV 파일로 사용할 수 있습니다.

audio_data = tts.synthesize(
    "파일로 저장할 음성입니다."
)

with open("output.wav", "wb") as file:
    file.write(audio_data)

현재 클래스는 파일 저장이 목적이 아니라 즉시 재생이 목적이므로, 반환된 데이터를 play() 메서드로 전달합니다.

24) play 메서드의 역할

def play(self, audio_content: bytes) -> None:

play() 메서드는 바이트 형식의 오디오 데이터를 받아 Linux 스피커로 출력합니다.

입력값은 다음과 같습니다.

audio_content: bytes

반환값은 없습니다.

-> None

이 메서드는 Google Cloud API를 호출하지 않습니다. 이미 생성된 오디오 데이터만 재생합니다.

따라서 같은 음성 데이터를 여러 번 재생할 수도 있습니다.

audio_data = tts.synthesize(
    "경고합니다."
)

tts.play(audio_data)
tts.play(audio_data)
tts.play(audio_data)

25) 기본 aplay 명령어 생성

command = ["aplay", "-q"]

외부 명령어는 문자열 하나가 아니라 리스트 형태로 정의합니다.

실제로 실행되는 명령어는 다음과 같습니다.

aplay -q

-q 옵션은 quiet 모드를 의미합니다.

이 옵션을 사용하면 aplay가 재생 중에 출력하는 불필요한 메시지를 줄일 수 있습니다.

리스트 형태로 명령어를 전달하는 방식은 쉘 문자열을 직접 실행하는 것보다 안전합니다.

예를 들어 다음처럼 구성하지 않습니다.

command = f"aplay -q -D {self.audio_device}"

대신 각 인수를 독립적인 리스트 항목으로 전달합니다.

[
    "aplay",
    "-q",
    "-D",
    "plughw:2,0",
]

26) 출력 장치 조건부 설정

if self.audio_device and self.audio_device != "default":

오디오 장치 이름이 존재하고 값이 default가 아닌 경우에만 -D 옵션을 추가합니다.

기본 장치를 사용할 때는 다음 명령어가 실행됩니다.

aplay -q

특정 장치를 설정한 경우에는 다음과 같은 명령어가 실행됩니다.

aplay -q -D plughw:2,0

장치 옵션은 다음 코드로 추가됩니다.

command.extend([
    "-D",
    self.audio_device,
])

extend()는 리스트에 여러 항목을 추가합니다.

초기 리스트가 다음과 같다면

["aplay", "-q"]

실행 후에는 다음과 같이 됩니다.

[
    "aplay",
    "-q",
    "-D",
    "plughw:2,0",
]

27) subprocess.run 실행

result = subprocess.run(
    command,
    input=audio_content,
    stdout=subprocess.DEVNULL,
    stderr=subprocess.PIPE,
    check=False,
)

이 코드가 실제로 aplay 프로그램을 실행합니다.

각 옵션의 역할을 하나씩 살펴보겠습니다.

a. command 인수
command

실행할 프로그램과 옵션이 담긴 리스트입니다.

예를 들어 다음 형태입니다.

["aplay", "-q"]

또는 다음 형태가 될 수 있습니다.

["aplay", "-q", "-D", "plughw:2,0"]
b. input 인수
input=audio_content

audio_contentaplay 프로세스의 표준 입력으로 전달합니다.

일반적으로 파일을 재생할 때는 다음처럼 실행합니다.

aplay output.wav

현재 코드는 파일 경로를 전달하지 않습니다.

대신 Python 메모리에 저장된 WAV 데이터를 aplay에 직접 전달합니다.

개념적으로는 다음 명령어와 비슷한 동작입니다.

cat output.wav | aplay

하지만 실제 구현에서는 중간 파일 없이 Python이 직접 데이터를 전달합니다.

c. stdout 설정
stdout=subprocess.DEVNULL

aplay가 표준 출력으로 보내는 내용을 버립니다.

DEVNULL은 Linux의 /dev/null과 같은 역할을 합니다.

음성 출력만 필요하고 터미널 메시지는 필요하지 않기 때문에 표준 출력을 숨깁니다.

d. stderr 설정
stderr=subprocess.PIPE

오류 메시지는 버리지 않고 Python 프로그램으로 가져옵니다.

aplay 실행에 실패하면 오류 내용이 result.stderr에 저장됩니다.

이 값을 이용해 다음과 같은 오류를 확인할 수 있습니다.

  1. 오디오 장치를 찾을 수 없음
  2. 장치가 다른 프로그램에서 사용 중임
  3. ALSA 설정 오류
  4. 지원되지 않는 오디오 형식
  5. 권한 부족
  6. aplay 실행 파일 없음
e. check 설정
check=False

check=False는 외부 프로그램이 실패해도 subprocess.run() 자체에서 즉시 예외를 발생시키지 않도록 합니다.

대신 실행 결과의 반환 코드를 직접 검사합니다.

if result.returncode != 0:

이 방식은 오류 메시지를 직접 가공하여 더 자세한 예외를 발생시킬 수 있다는 장점이 있습니다.

28) 반환 코드 검사

if result.returncode != 0:

Linux 프로그램은 일반적으로 정상 종료 시 0을 반환합니다.

실패하면 0이 아닌 값을 반환합니다.

따라서 다음 조건은 aplay 실행 실패를 의미합니다.

result.returncode != 0

예를 들어 오디오 장치가 존재하지 않으면 aplay는 오류 메시지와 함께 비정상 종료 코드를 반환할 수 있습니다.

29) 오류 메시지 디코딩

error_message = result.stderr.decode(
    "utf-8",
    errors="replace",
).strip()

result.stderr는 문자열이 아니라 bytes 형식입니다.

사람이 읽을 수 있는 문자열로 바꾸기 위해 decode()를 사용합니다.

.decode("utf-8")

오류 출력에 UTF-8로 해석할 수 없는 데이터가 포함된 경우를 대비해 다음 옵션을 사용합니다.

errors="replace"

해석할 수 없는 문자는 대체 문자로 변환되므로 디코딩 과정에서 추가 예외가 발생하는 것을 막을 수 있습니다.

마지막의 strip()은 오류 메시지 앞뒤의 공백과 줄바꿈을 제거합니다.

30) RuntimeError 발생

raise RuntimeError(
    "aplay 실행 실패"
    f"(return code={result.returncode}): "
    f"{error_message}"
)

aplay 실행이 실패하면 RuntimeError 예외를 발생시킵니다.

출력되는 오류에는 다음 정보가 포함됩니다.

  1. aplay 실행 실패 여부
  2. 프로세스 반환 코드
  3. ALSA 또는 aplay가 출력한 상세 오류 메시지

예상되는 메시지 형태는 다음과 같습니다.

aplay 실행 실패(return code=1): audio open error: No such file or directory

단순히 실패했다고만 출력하는 것보다 실제 오류 원인을 확인하기 쉽습니다.

31) speak 메서드의 역할

def speak(self, text: str) -> None:
    audio_content = self.synthesize(text)
    self.play(audio_content)

speak() 메서드는 음성 합성과 재생을 한 번에 처리하는 편의 메서드입니다.

내부 동작 순서는 다음과 같습니다.

  1. synthesize()로 문자열을 음성 데이터로 변환합니다.
  2. 반환된 음성 데이터를 play()로 전달합니다.
  3. aplay를 통해 스피커에서 음성을 출력합니다.

따라서 사용자는 다음 한 줄만 호출하면 됩니다.

tts.speak("시스템을 시작합니다.")

이는 내부적으로 다음 코드와 같습니다.

audio_content = tts.synthesize(
    "시스템을 시작합니다."
)

tts.play(audio_content)

a. 말하기 속도와 음높이 변경 예제
tts = GoogleTtsEngine(
    speaking_rate=0.9,
    pitch=-1.5,
)

tts.speak(
    "현재 배터리 잔량은 30퍼센트입니다."
)

이 설정은 기본보다 약간 느리고 낮은 음성으로 출력합니다.

긴급 경고에서는 조금 빠른 속도와 높은 볼륨을 사용할 수 있습니다.

warning_tts = GoogleTtsEngine(
    speaking_rate=1.1,
    pitch=0.0,
    volume_gain_db=4.0,
)

warning_tts.speak(
    "경고합니다. 장애물이 감지되었습니다."
)
b. 특정 ALSA 장치 사용 예제

먼저 출력 장치를 확인합니다.

aplay -l

출력 예시는 다음과 같습니다.

card 2: Device [USB Audio Device], device 0: USB Audio

이 경우 다음과 같이 설정할 수 있습니다.

tts = GoogleTtsEngine(
    audio_device="plughw:2,0"
)

tts.speak(
    "USB 스피커 출력 테스트입니다."
)

장치 번호는 시스템 재부팅이나 USB 연결 순서에 따라 달라질 수 있습니다.

제품이나 현장 시스템에서는 카드 번호보다 고정된 ALSA 장치 이름을 설정하는 편이 안전합니다.

32) 예외 처리 사용 예제

실제 시스템에서는 네트워크, 인증, 오디오 장치 문제로 음성 출력이 실패할 수 있습니다.

따라서 다음과 같이 예외를 처리하는 것이 좋습니다.

def main() -> None:
    try:
        tts = GoogleTtsEngine()
        tts.speak("시스템을 시작합니다.")

    except ValueError as error:
        print(f"입력 오류: {error}")

    except RuntimeError as error:
        print(f"오디오 재생 오류: {error}")

    except Exception as error:
        print(f"음성 합성 오류: {error}")


if __name__ == "__main__":
    main()

ValueError는 빈 문자열 입력처럼 잘못된 인수에서 발생합니다.

RuntimeErroraplay 실행 실패 시 발생합니다.

Google Cloud API 또는 인증 관련 오류는 다른 예외 형태로 발생할 수 있으므로 마지막에 일반 예외 처리도 추가할 수 있습니다.

9. 문자열을 즉시 재생하는 실행 파일 작성

google_tts_ros2/say.py 파일을 작성합니다.

import argparse
import sys

from google_tts_ros2.tts_engine import GoogleTtsEngine


def main() -> int:
    parser = argparse.ArgumentParser(
        description=(
            "Google Cloud Text-to-Speech로 "
            "문자열을 즉시 재생합니다."
        )
    )

    parser.add_argument(
        "text",
        nargs="+",
        help="음성으로 출력할 문자열",
    )

    parser.add_argument(
        "--language-code",
        default="ko-KR",
    )

    parser.add_argument(
        "--voice-name",
        default="ko-KR-Standard-A",
    )

    parser.add_argument(
        "--speaking-rate",
        type=float,
        default=1.0,
    )

    parser.add_argument(
        "--pitch",
        type=float,
        default=0.0,
    )

    parser.add_argument(
        "--volume-gain-db",
        type=float,
        default=0.0,
    )

    parser.add_argument(
        "--audio-device",
        default="default",
    )

    args = parser.parse_args()

    engine = GoogleTtsEngine(
        language_code=args.language_code,
        voice_name=args.voice_name,
        speaking_rate=args.speaking_rate,
        pitch=args.pitch,
        volume_gain_db=args.volume_gain_db,
        audio_device=args.audio_device,
    )

    try:
        engine.speak(" ".join(args.text))

    except Exception as error:
        print(
            f"TTS 실행 실패: {error}",
            file=sys.stderr,
        )
        return 1

    return 0


if __name__ == "__main__":
    raise SystemExit(main())

이 실행 파일은 ROS 2 토픽을 사용하지 않고 명령행에서 전달한 문자열을 바로 음성으로 출력합니다.

1) argparse 모듈 가져오기

import argparse

argparse는 Python 표준 라이브러리에 포함된 명령줄 인수 분석 모듈입니다.

별도의 패키지를 설치하지 않아도 사용할 수 있습니다.

argparse를 사용하면 다음 기능을 쉽게 구현할 수 있습니다.

  1. 필수 인수 정의
  2. 선택 옵션 정의
  3. 기본값 설정
  4. 문자열을 숫자로 변환
  5. 도움말 자동 생성
  6. 잘못된 인수 검사
  7. 사용법 자동 출력

예를 들어 프로그램에 --help 옵션을 전달하면 argparse가 자동으로 도움말을 출력합니다.

python3 say.py --help

출력 형태는 다음과 비슷합니다.

usage: say.py [-h]
                  [--language-code LANGUAGE_CODE]
                  [--voice-name VOICE_NAME]
                  [--speaking-rate SPEAKING_RATE]
                  [--pitch PITCH]
                  [--volume-gain-db VOLUME_GAIN_DB]
                  [--audio-device AUDIO_DEVICE]
                  text [text ...]

Google Cloud Text-to-Speech로 문자열을 즉시 재생합니다.

직접 도움말 처리 코드를 작성하지 않아도 된다는 것이 argparse의 큰 장점입니다.

2) sys 모듈 가져오기

import sys

sys 역시 Python 표준 라이브러리입니다.

이번 코드에서는 다음 목적으로 사용합니다.

  1. 오류 메시지를 표준 오류 출력으로 전송
  2. 프로그램 종료 상태 관리

실제 사용 부분은 다음과 같습니다.

file=sys.stderr

sys.stderr는 표준 오류 출력 스트림입니다.

일반 출력과 오류 출력을 분리하면 쉘 스크립트나 로그 수집 시스템에서 성공 메시지와 실패 메시지를 구분하기 쉬워집니다.

3) GoogleTtsEngine 클래스 가져오기

from google_tts_ros2.tts_engine import GoogleTtsEngine

이 코드는 google_tts_ros2 Python 패키지 안에 있는 tts_engine.py 모듈에서 GoogleTtsEngine 클래스를 가져옵니다.

패키지 구조는 일반적으로 다음과 같이 구성할 수 있습니다.

google_tts_ros2/
├── google_tts_ros2/
│   ├── __init__.py
│   ├── tts_engine.py
│   └── say.py
├── package.xml
├── resource/
├── setup.cfg
└── setup.py

각 파일의 역할은 다음과 같습니다.

  1. tts_engine.py: Google Cloud TTS 음성 합성과 재생 기능
  2. say.py: 명령줄 인수를 받아 TTS 엔진 실행
  3. setup.py: Python 패키지와 실행 엔트리 포인트 설정
  4. package.xml: ROS 2 패키지 정보와 의존성 정의
  5. setup.cfg: ROS 2 실행 파일 설치 위치 설정

GoogleTtsEngine은 실제 음성 합성과 재생을 담당하고, 현재 소스는 사용자 입력과 실행 흐름만 담당합니다.

이처럼 기능을 분리하면 유지보수가 쉬워집니다.

4) main 함수 정의

def main() -> int:

main() 함수는 프로그램의 핵심 실행 흐름을 담당합니다.

반환 자료형은 int로 지정되어 있습니다.

-> int

반환값은 프로그램의 종료 코드로 사용됩니다.

일반적인 종료 코드 의미는 다음과 같습니다.

  1. 0: 정상 종료
  2. 1: 일반적인 실행 오류
  3. 2: 명령줄 인수 오류
  4. 그 외 값: 프로그램에서 정의한 특정 오류

현재 코드에서는 성공 시 0, TTS 실행 실패 시 1을 반환합니다.

5) ArgumentParser 객체 생성

parser = argparse.ArgumentParser(
    description=(
        "Google Cloud Text-to-Speech로 "
        "문자열을 즉시 재생합니다."
    )
)

ArgumentParser는 명령줄 인수를 관리하는 핵심 객체입니다.

이 객체를 이용해 프로그램이 받을 인수와 옵션을 등록합니다.

description은 프로그램의 목적을 설명하는 문자열입니다.

사용자가 다음 명령을 실행하면 설명이 표시됩니다.

python3 say.py --help

문자열을 괄호 안에서 두 줄로 나눠 작성했지만 Python에서는 자동으로 하나의 문자열로 연결됩니다.

description=(
    "Google Cloud Text-to-Speech로 "
    "문자열을 즉시 재생합니다."
)

실제 문자열은 다음과 같습니다.

Google Cloud Text-to-Speech로 문자열을 즉시 재생합니다.

이 방식은 한 줄이 지나치게 길어지는 것을 방지하면서도 문자열 연결 연산자를 사용하지 않아도 된다는 장점이 있습니다.

6) 필수 위치 인수 text 등록

parser.add_argument(
    "text",
    nargs="+",
    help="음성으로 출력할 문자열",
)

text는 음성으로 출력할 문장을 받는 필수 위치 인수입니다.

위치 인수는 옵션 이름 없이 입력합니다.

python3 say.py 안녕하세요

여기서 안녕하세요text 인수에 저장됩니다.

여러 단어를 입력할 수도 있습니다.

python3 say.py 로봇 시스템을 시작합니다

이 경우 각 단어는 리스트의 개별 항목으로 저장됩니다.

args.text

결과는 다음과 비슷합니다.

[
    "로봇",
    "시스템을",
    "시작합니다",
]

7) nargs 옵션의 의미

nargs="+"

nargs는 해당 인수가 몇 개의 값을 받을 수 있는지 지정합니다.

"+"는 하나 이상의 값을 반드시 받아야 한다는 의미입니다.

따라서 다음 명령은 정상입니다.

python3 say.py 안녕하세요

다음 명령도 정상입니다.

python3 say.py 시스템 점검을 시작합니다

하지만 문자열 없이 실행하면 오류가 발생합니다.

python3 say.py

이 경우 argparse가 자동으로 오류 메시지를 출력합니다.

error: the following arguments are required: text

그리고 일반적으로 종료 코드 2로 프로그램을 종료합니다.

nargs="+"를 사용한 이유는 쉘에서 공백으로 분리된 여러 단어를 모두 입력받기 위해서입니다.

8) text 도움말 설정

help="음성으로 출력할 문자열"

help--help 실행 시 해당 인수에 표시되는 설명입니다.

출력 예시는 다음과 같습니다.

positional arguments:
  text    음성으로 출력할 문자열

명령줄 프로그램은 사용자 인터페이스가 단순하기 때문에 도움말 문구가 중요합니다.

9) language-code 옵션 등록

parser.add_argument(
    "--language-code",
    default="ko-KR",
)

--language-code는 음성 합성 언어를 지정하는 선택 옵션입니다.

기본값은 다음과 같습니다.

ko-KR

ko-KR은 대한민국 한국어를 의미합니다.

옵션을 생략하면 기본값이 사용됩니다.

python3 say.py 안녕하세요

위 명령은 내부적으로 다음 설정을 사용합니다.

language_code="ko-KR"

다른 언어를 사용하려면 다음과 같이 입력할 수 있습니다.

python3 say.py Hello world \
    --language-code en-US

일본어를 사용하려면 다음과 같이 지정할 수 있습니다.

python3 say.py こんにちは \
    --language-code ja-JP

대표적인 언어 코드는 다음과 같습니다.

  1. ko-KR: 한국어
  2. en-US: 미국 영어
  3. en-GB: 영국 영어
  4. ja-JP: 일본어
  5. zh-CN: 중국어 간체
  6. de-DE: 독일어
  7. fr-FR: 프랑스어

언어 코드와 음성 이름은 서로 일치해야 합니다.

예를 들어 language-codeen-US로 지정하면서 한국어 음성 이름을 그대로 사용하면 API 오류가 발생할 수 있습니다.

10) voice-name 옵션 등록

parser.add_argument(
    "--voice-name",
    default="ko-KR-Standard-A",
)

--voice-name은 Google Cloud Text-to-Speech에서 사용할 음성 모델을 지정합니다.

기본값은 다음과 같습니다.

ko-KR-Standard-A

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

python3 say.py 안내 방송을 시작합니다 \
    --voice-name ko-KR-Standard-A

다른 음성 모델을 사용하려면 설치된 라이브러리와 Google Cloud에서 지원하는 정확한 음성 이름을 지정해야 합니다.

음성 이름은 다음 요소에 영향을 줍니다.

  1. 목소리 특성
  2. 남성 또는 여성 음성
  3. 자연스러움
  4. 음색
  5. 사용 요금
  6. 지원 기능

언어 코드를 변경하는 경우 음성 이름도 함께 변경하는 것이 안전합니다.

python3 say.py Hello --language-code en-US --voice-name en-US-Standard-A

11) speaking-rate 옵션 등록

parser.add_argument(
    "--speaking-rate",
    type=float,
    default=1.0,
)

--speaking-rate는 말하기 속도를 지정합니다.

기본값은 1.0입니다.

1.0

1.0은 기본 속도를 의미합니다.

느리게 읽게 하려면 다음과 같이 지정할 수 있습니다.

python3 say.py 천천히 안내합니다 --speaking-rate 0.8

빠르게 읽게 하려면 다음과 같이 지정할 수 있습니다.

python3 say.py 빠르게 안내합니다 --speaking-rate 1.2

type=float가 지정되어 있으므로 사용자가 입력한 문자열은 자동으로 실수형으로 변환됩니다.

type=float

명령줄에서 입력되는 모든 값은 기본적으로 문자열입니다.

예를 들어 다음 값을 입력하면

--speaking-rate 0.9

argparse가 다음처럼 변환합니다.

args.speaking_rate == 0.9

잘못된 문자열을 입력하면 자동으로 오류가 발생합니다.

python3 say.py 테스트 --speaking-rate fast

출력 예시는 다음과 같습니다.

error: argument --speaking-rate: invalid float value: 'fast'

12) pitch 옵션 등록

parser.add_argument(
    "--pitch",
    type=float,
    default=0.0,
)

--pitch는 음성의 높낮이를 조절합니다.

기본값은 0.0입니다.

0.0

양수 값을 사용하면 음성이 높아지고, 음수 값을 사용하면 음성이 낮아집니다.

낮은 목소리를 사용하려면 다음과 같이 실행합니다.

python3 say.py 시스템 점검을 시작합니다 --pitch -2.0

높은 목소리를 사용하려면 다음과 같이 실행합니다.

python3 say.py 알림이 도착했습니다 --pitch 2.0

pitch 역시 실수형 값이므로 type=float가 지정되어 있습니다.

13) volume-gain-db 옵션 등록

parser.add_argument(
    "--volume-gain-db",
    type=float,
    default=0.0,
)

--volume-gain-db는 음성 합성 단계에서 적용할 볼륨 증폭값을 데시벨 단위로 지정합니다.

기본값은 0.0입니다.

음량을 증가시키려면 다음과 같이 사용합니다.

python3 say.py 경고합니다 --volume-gain-db 3.0

음량을 감소시키려면 다음과 같이 사용합니다.

python3 say.py 조용한 안내입니다 --volume-gain-db -3.0

이 옵션은 운영체제의 마스터 볼륨을 직접 변경하지 않습니다.

실제 출력 음량은 다음 요소에 함께 영향을 받습니다.

  1. Google TTS의 볼륨 설정
  2. ALSA 믹서 볼륨
  3. PulseAudio 또는 PipeWire 설정
  4. USB 오디오 장치 출력
  5. 외부 앰프의 증폭률
  6. 스피커 자체 볼륨

Linux 시스템의 오디오 볼륨은 다음 명령으로 확인할 수 있습니다.

alsamixer

14) audio-device 옵션 등록

parser.add_argument(
    "--audio-device",
    default="default",
)

--audio-deviceaplay가 사용할 ALSA 출력 장치를 지정합니다.

기본값은 다음과 같습니다.

default

기본 장치를 사용하면 다음과 같이 실행합니다.

python3 say.py 기본 스피커 테스트입니다

특정 장치를 사용하려면 다음과 같이 지정합니다.

python3 say.py USB 오디오 출력 테스트입니다 --audio-device plughw:2,0

사용 가능한 장치는 다음 명령으로 확인할 수 있습니다.

aplay -l

논리적인 ALSA 장치 이름은 다음 명령으로 확인할 수 있습니다.

aplay -L

Jetson, Raspberry Pi 또는 산업용 Ubuntu 시스템에서는 HDMI, USB 오디오, 내장 오디오 장치가 동시에 존재할 수 있습니다.

이런 환경에서는 장치를 명시적으로 지정하는 것이 안정적입니다.

15) 명령줄 인수 분석

args = parser.parse_args()

parse_args()는 사용자가 입력한 명령줄 인수를 분석합니다.

예를 들어 다음 명령을 실행한다고 가정할 수 있습니다.

python3 say.py 로봇 시스템을 시작합니다 --speaking-rate 0.9 \
    --pitch -1.0 --audio-device plughw:2,0

분석 결과는 개념적으로 다음과 같습니다.

args.text = [
    "로봇",
    "시스템을",
    "시작합니다",
]

args.language_code = "ko-KR"
args.voice_name = "ko-KR-Standard-A"
args.speaking_rate = 0.9
args.pitch = -1.0
args.volume_gain_db = 0.0
args.audio_device = "plughw:2,0"

옵션 이름의 하이픈은 Python 속성에서 밑줄로 변경됩니다.

예를 들어 명령줄 옵션은 다음과 같습니다.

--language-code

Python에서는 다음처럼 접근합니다.

args.language_code

다음 옵션도 같은 방식으로 변환됩니다.

--speaking-rate
args.speaking_rate

16) GoogleTtsEngine 객체 생성

engine = GoogleTtsEngine(
    language_code=args.language_code,
    voice_name=args.voice_name,
    speaking_rate=args.speaking_rate,
    pitch=args.pitch,
    volume_gain_db=args.volume_gain_db,
    audio_device=args.audio_device,
)

분석된 명령줄 옵션을 이용해 GoogleTtsEngine 객체를 생성합니다.

각 값은 다음과 같이 전달됩니다.

  1. args.language_code는 음성 언어로 전달됩니다.
  2. args.voice_name은 음성 모델 이름으로 전달됩니다.
  3. args.speaking_rate는 말하기 속도로 전달됩니다.
  4. args.pitch는 음높이로 전달됩니다.
  5. args.volume_gain_db는 볼륨 증폭값으로 전달됩니다.
  6. args.audio_device는 ALSA 출력 장치로 전달됩니다.

기본 옵션으로 실행하면 내부적으로 다음과 비슷한 객체가 생성됩니다.

engine = GoogleTtsEngine(
    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",
)

명령줄 옵션과 클래스 생성자 인수 이름이 동일하게 구성되어 있어 코드를 이해하기 쉽습니다.

17) try 문을 이용한 오류 처리 시작

try:
    engine.speak(" ".join(args.text))

TTS 실행 과정에서는 여러 종류의 오류가 발생할 수 있습니다.

예상 가능한 오류는 다음과 같습니다.

  1. Google Cloud 인증 실패
  2. 인터넷 연결 실패
  3. Text-to-Speech API 비활성화
  4. 서비스 계정 권한 부족
  5. 잘못된 언어 코드
  6. 잘못된 음성 이름
  7. 음성 합성 제한값 초과
  8. aplay 설치 누락
  9. ALSA 장치 탐색 실패
  10. 오디오 장치 사용 중
  11. 스피커 권한 문제
  12. 잘못된 오디오 장치 이름

이런 오류가 프로그램 전체의 Python 예외 추적 메시지로 출력되는 대신, 사용자에게 필요한 핵심 오류만 보여주도록 tryexcept를 사용합니다.

18) 여러 단어를 하나의 문장으로 결합

" ".join(args.text)

args.text는 문자열이 아니라 문자열 리스트입니다.

예를 들어 다음 명령을 실행하면

python3 say.py 로봇 시스템을 시작합니다

args.text는 다음과 같습니다.

[
    "로봇",
    "시스템을",
    "시작합니다",
]

join()은 리스트의 각 문자열 사이에 공백을 넣어 하나의 문자열로 결합합니다.

" ".join(args.text)

결과는 다음과 같습니다.

로봇 시스템을 시작합니다

이 결과가 engine.speak()에 전달됩니다.

19) 따옴표로 문장 입력하기

현재 코드는 nargs="+"를 사용하기 때문에 문장을 따옴표 없이 입력해도 됩니다.

python3 say.py 로봇 시스템을 시작합니다

따옴표를 사용해 하나의 인수로 전달해도 정상 동작합니다.

python3 say.py "로봇 시스템을 시작합니다"

첫 번째 방식에서 args.text는 여러 항목으로 구성됩니다.

[
    "로봇",
    "시스템을",
    "시작합니다",
]

두 번째 방식에서는 한 항목으로 구성됩니다.

[
    "로봇 시스템을 시작합니다",
]

두 경우 모두 " ".join(args.text) 결과는 동일합니다.

20) speak 메서드 호출

engine.speak(" ".join(args.text))

speak() 메서드는 입력 문자열을 음성으로 합성하고 즉시 재생합니다.

내부적으로 다음 작업이 수행됩니다.

  1. 문자열 앞뒤 공백 제거
  2. 빈 문자열 검사
  3. Google Cloud TTS 요청 생성
  4. 음성 모델 설정
  5. 음성 속도와 높낮이 설정
  6. Google 서버에 합성 요청
  7. WAV 형식 음성 데이터 수신
  8. aplay 프로세스 실행
  9. 오디오 데이터를 표준 입력으로 전달
  10. 스피커에서 음성 재생
  11. 재생 결과 확인

현재 프로그램은 동기 방식으로 실행됩니다.

따라서 음성 재생이 끝날 때까지 프로그램이 종료되지 않습니다.

21) 예외 처리

except Exception as error:

engine.speak() 실행 중 발생한 예외를 처리합니다.

현재 코드는 특정 예외만 선택하지 않고 모든 일반 예외를 처리합니다.

예를 들어 다음 예외가 포함될 수 있습니다.

  1. ValueError
  2. RuntimeError
  3. Google Cloud 인증 예외
  4. Google API 호출 예외
  5. 네트워크 관련 예외
  6. 파일 또는 프로세스 실행 예외

CLI 도구에서는 예외 종류를 사용자에게 모두 노출하기보다, 실행 실패 메시지와 원인을 간단히 표시하는 방식이 실용적입니다.

22) 오류 메시지 출력

print(
    f"TTS 실행 실패: {error}",
    file=sys.stderr,
)

오류가 발생하면 다음 형식으로 메시지를 출력합니다.

TTS 실행 실패: 오류 원인

예를 들어 인증 정보가 없으면 다음과 비슷한 메시지가 출력될 수 있습니다.

TTS 실행 실패: Your default credentials were not found

오디오 장치가 없으면 다음과 같은 형태가 될 수 있습니다.

TTS 실행 실패: aplay 실행 실패(return code=1): audio open error

file=sys.stderr가 지정되어 있으므로 일반 출력이 아니라 표준 오류 출력으로 전달됩니다.

23) 표준 출력과 표준 오류 출력의 차이

프로그램에는 일반적으로 다음 두 출력 스트림이 있습니다.

  1. 표준 출력인 stdout
  2. 표준 오류 출력인 stderr

일반적인 정보는 표준 출력으로 보냅니다.

print("실행 완료")

오류 정보는 표준 오류 출력으로 보냅니다.

print(
    "실행 실패",
    file=sys.stderr,
)

두 출력을 분리하면 쉘에서 각각 다르게 처리할 수 있습니다.

정상 출력만 파일에 저장하려면 다음처럼 실행합니다.

python3 say.py 안녕하세요 > output.log

오류 출력만 파일에 저장하려면 다음과 같이 실행합니다.

python3 say.py 안녕하세요 2> error.log

모든 출력을 하나의 파일에 저장하려면 다음과 같이 실행합니다.

python3 say.py 안녕하세요 > all.log 2>&1

시스템 서비스나 자동화 스크립트에서는 표준 출력과 오류 출력을 구분하는 것이 중요합니다.

24) 실패 시 종료 코드 반환

return 1

TTS 실행에 실패하면 main() 함수는 1을 반환합니다.

쉘에서는 프로그램의 종료 코드를 이용해 성공과 실패를 판단할 수 있습니다.

실행 직후 다음 명령으로 종료 코드를 확인할 수 있습니다.

echo $?

TTS 실행에 실패했다면 다음이 출력됩니다.

1

종료 코드는 쉘 스크립트에서 유용합니다.

python3 say.py 시스템 시작

if [ $? -ne 0 ]; then
    echo "음성 출력에 실패했습니다."
fi

더 간단하게 다음처럼 작성할 수도 있습니다.

if ! python3 say.py 시스템 시작; then
    echo "음성 출력에 실패했습니다."
fi

25) 정상 종료 코드 반환

return 0

오류 없이 음성 재생이 완료되면 0을 반환합니다.

쉘에서 확인하면 다음과 같습니다.

echo $?

결과는 다음과 같습니다.

0

종료 코드 0은 일반적으로 정상 실행을 의미합니다.

26) 직접 실행 여부 확인

if __name__ == "__main__":

이 조건문은 현재 Python 파일이 직접 실행되었는지 확인합니다.

다음과 같이 실행하면 조건은 참이 됩니다.

python3 say.py 안녕하세요

반대로 다른 Python 파일에서 모듈로 가져오면 조건은 거짓이 됩니다.

import google_tts_ros2.tts_cli

이 구조를 사용하면 모듈을 가져오는 순간 프로그램이 자동으로 실행되는 것을 방지할 수 있습니다.

27) SystemExit를 이용한 프로그램 종료

raise SystemExit(main())

main() 함수의 반환값을 운영체제 종료 코드로 전달합니다.

정상 실행 시 다음과 같습니다.

main()

반환값은 0입니다.

raise SystemExit(0)

프로그램은 정상 종료합니다.

오류 발생 시에는 다음과 같습니다.

raise SystemExit(1)

프로그램은 실패 상태로 종료합니다.

다음 코드도 비슷한 동작을 합니다.

sys.exit(main())

하지만 현재 코드는 SystemExit를 명시적으로 발생시키는 방식을 사용합니다.

두 방식 모두 일반적인 Python CLI 프로그램에서 사용됩니다.

28) 가장 기본적인 실행 예제

기본 한국어 음성으로 출력하려면 다음과 같이 실행합니다.

python3 say.py 안녕하세요

여러 단어를 입력할 수 있습니다.

python3 say.py 로봇 시스템이 정상적으로 시작되었습니다

따옴표로 묶어도 됩니다.

python3 say.py "로봇 시스템이 정상적으로 시작되었습니다"

29) 말하기 속도 변경 예제

기본보다 느리게 읽으려면 다음과 같이 실행합니다.

python3 say.py 배터리 잔량을 확인합니다 --speaking-rate 0.8

기본보다 빠르게 읽으려면 다음과 같이 실행합니다.

python3 say.py 임무를 시작합니다 --speaking-rate 1.2

로봇 안내 음성은 너무 빠르면 내용을 이해하기 어렵습니다.

일반적인 안내 문장에는 0.9에서 1.1 정도가 적당합니다.

30) 음높이 변경 예제

낮은 목소리로 출력하려면 다음과 같이 실행합니다.

python3 say.py 시스템 경고입니다 --pitch -2.0

높은 목소리로 출력하려면 다음과 같이 실행합니다.

python3 tts_cli.py 새로운 메시지가 있습니다 --pitch 2.0

음높이는 음성 모델에 따라 체감 차이가 다를 수 있습니다.

Leave a Comment