2015년 8월 18일 화요일

자비스에게 한글로 말하고 듣게 하다

[수정 : 2016/03/15] 음성인식은 annyang 으로 바꿨습니다.
일전에 TTS(Text To Speech) 와 STT (Speak To Text)에 대해서 소개 했었는데요.
저만의 자비스를 만들기 위해 TTS, STT 의 개념을 이해하고 이를 활용할 수 있는 최적의 솔루션을 찾기 위해 정리할 필요가 있을 것 같습니다.

간단하게 제가 '자비스!' 라고 부르면 '네, 주인님' 이라고 답을 하기 까지의 과정을 설명하면,

  1. 말 하는 내용을 마이크로 녹음 해서 ==> 레코딩
  2. 녹음된 음성을 글자로 변환하고 ==> STT
  3. 변환된 글자가 '자비스' 와 같을 때 ==> 비교
  4. '네, 주인님' 이라는 글자에서 사람이 알아 들을 수 있는 음성으로 바꾸고 ==> TTS
  5. 스피커로 출력 합니다. ==> 출력

5가지의 기술로 요약할 수 있을 것 같습니다.
레코딩/비교/출력은 라즈베리파이(리눅스) 에서는 ALSA(혹은 PulseAudio) 를 사용할 수 있기때문에 쉽다고 판단되지만, STT, TTS 는 복잡한 알고리즘을 요구하므로 별도의 솔루션을 찾아야 하는 결론에 다다릅니다. 더불어 단순히 '자비스' 라는 이름만 인식 하는게 아니라 '지금 시간이 몇시야?' 라고 하는 문맥을 이해 하고 '지금은 오후 12시 입니다." 라고 Q&A 를 할 수 있어야 하는 요구사항이 있으므로 좀 더 머리가 복잡해 집니다.

위 고민과 관련한 현존하는 기술의 명칭은 '음성인식' 과 'IPA (Intelligent Personal Assistant)' 가 있습니다. 두 가지 용어가 음성인식 < IPA 정도의 포함 관계라고 생각할 수 있겠는데요.

현존하는 IPA 는 구글의 'Now', 애플의 '시리', 마소의 '코타나'가 상용 서비스의 대표선수라면 오픈소스 월드에서는 시리우스, 제스퍼, 보이스커맨드 정도가 눈에 띄는 것 같습니다.
라즈베리 파이에서는 오픈소스를 사용해야 하기 때문에 각각의 솔루션의 장단점을 비교 선택해야 합니다. 위에서도 언급 되었지만 IPA 의 핵심 기술은 STT, TTS 인데, 이를 비교할 때 가장 중요한 부분이 '한국어 지원' '인식률' 입니다.

보이스 커맨드는 '구글 API' 만을 사용하기 때문에 제외시켜 두고 시리우스와 제스퍼를 보면 시리우스는 STT 에  Kaldi, Pocketsphinx, Sphinx4 라는 3가지 플러그인을 사용할 수 있고 TTS 에는 espeak 만 사용합니다. 반면 제스퍼는 STT 에 Sphinx 시리즈 외에도 추가로 Google STT, Wai.ai, Julius, AT&T STT 를 사용할 수 있습니다. TTS 역시 espeak 외에도 Google TTS, Festival, Flite, SVOX Pico, Inova, Mary, MAX OS X 등의 다양한 솔루션이 사용 가능합니다.

확장성만 따지고 보면 제스퍼가 단연 1등이지만, 지원 하는 솔루션의 개수가 중요한 것이 아니라 '한국어 지원' 과 '인식률' 을 봐야 하기 때문에 다시 한번 살펴 보게 됩니다.
사실 논란의 여지가 없이 저 많은 솔루션들은 듣보잡이고 (유수 대학의 오랜 시간 연구를 평가절하 하려는 의도가 아니라 일반인들에게 알려진 솔루션이 없기 때문입니다 ㅠㅠ) 넘사벽 킹왕짱 구글의 STT, TTS 에게 손을 들어 줄 수 밖에 없습니다.

이유는 STT 는 고사하고 TTS 만 비교를 해도 '안녕하세요' 라는 것은 espeak 로 출력해 보면 '안녕하세요' 라는 자막과 같이 보지 않으면 이게 정말 '안녕?' 하다고 하는 것인지 알아챌 수 없습니다. 실제 들어 보고 싶으신 분들을 위해 간단히 테스트 방법을 적어보면,
최신 버젼의 espeak 를 받아서 빌드합니다.
$ wget http://espeak.sourceforge.net/test/espeak-1.48.15.zip
$ unzip espeak-1.48.15.zip
$ cd espeak-1.48.15/src
$ sudo apt-get install hardening-wrapper libportaudiocpp0 libsonic-dev portaudio19-dev
$ vi Makefile # 라즈베리 파이 상의 설치 경로를 수정 합니다
  ++ #DATADIR=/usr/share/espeak-data
  ++ DATADIR=/usr/lib/arm-linux-gnueabihf/espeak-data
$ make -j4
$ sudo make install
$ espeak -v ko "안녕하세요" 

당연히 '한국어'를 인식 할리 만무하죠. 물론 '한국어'를 인식할 수 있게 트레이닝 시켜 주면 많은 contribution 과 시간이 지난 후에 가능하게 되겠지만, 이미 넘사벽 수준으로 전세계 사람들로 부터 트레이닝된 구글의 STT / TTS 솔루션을 따라 갈 만한 게 없습니다.

설명이 길었지만, 결론은 '시리우스, 제스퍼, 보이스커맨드 중 어느 것을 사용해도 무방하고 다만 백앤드 솔루션으로 구글을 사용해야 한다' 입니다.
이 결론 대로라면 제스퍼, 보이스커맨드 가 후보가 되는데 현재 오픈소스 Activity 가 활발한 제스퍼로 선택하게 되었습니다. 보이스커맨드는 링크의 블로그 날짜가 2013년 5월 인 것과 실제 코드량이 많지 않고 조악한 부분을 발견 할 수 있습니다. 그렇다고 제스퍼가 훌륭하다고 할 수는 없는 이유가,

  1. 영어만을 위해 디자인 되었습니다.
  2. ALSA 만 사용가능한 PyAudio 를 사용해서 유선 마이크만 사용 가능합니다.


일전에 살짝 맛뵈기로 Ubuntu 에서 테스트 해 본 제스퍼를 라즈베리로 옮기고 위 2가지 부족한 부분을 patch 해서 '한국어' 를 알아 듣고 말할 수 있는 놈으로 바꿔서 이름을 '자비스' 라고 불러 보겠습니다.

제스퍼의 기본 설치 과정은 홈페이지 로 대체 하기로 하고, 첫번째 숙제인 '영어만을 위한' 부분을 해결하려면,

1. python 의 default encoding 을 utf-8 로 수정 합니다.
 - 제가 python 에 익숙치 않아 정답 여부는 전문가 조언을 따르겠습니다.
$ diff site.py site.py.org
493c493
<     encoding = "utf-8" # Default value set by _PyUnicode_Init()
---
>     encoding = "ascii" # Default value set by _PyUnicode_Init()

2. 기본적인 대화를 영어에서 한국어로 변경 합니다.
$ git status
# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified:   client/app_utils.py
# modified:   client/brain.py
# modified:   client/conversation.py
# modified:   jasper.py

$ git diff client/app_utils.py
diff --git a/client/app_utils.py b/client/app_utils.py
index edc8467..0b48386 100644
--- a/client/app_utils.py
+++ b/client/app_utils.py
@@ -125,4 +125,4 @@ def isPositive(phrase):
         Arguments:
         phrase -- the input phrase to-be evaluated
     """
-    return bool(re.search(r'\b(sure|yes|yeah|go)\b', phrase, re.IGNORECASE))
+    return bool(re.search(ur'어|응|그래|예쓰|고', phrase, re.UNICODE))

$ git diff client/brain.py
diff --git a/client/brain.py b/client/brain.py
index 593ca39..ffaea9c 100644
--- a/client/brain.py
+++ b/client/brain.py
@@ -74,8 +74,8 @@ class Brain(object):
                     except:
                         self._logger.error('Failed to execute module',
                                            exc_info=True)
-                        self.mic.say("I'm sorry. I had some trouble with " +
-                                     "that operation. Please try again later.")
+                        self.mic.say("죄송합니다. 문제가 생겼습니다. " +
+                                     "나중에 다시 시도해 주세요.")

                     else:
                         self._logger.debug("Handling of phrase '%s' by " +
                                            "module '%s' completed", text,

$ git diff client/conversation.py
diff --git a/client/conversation.py b/client/conversation.py
index 6b2fab1..51f5e4a 100644
--- a/client/conversation.py
+++ b/client/conversation.py
@@ -46,4 +46,4 @@ class Conversation(object):
             if input:
                 self.brain.query(input)
             else:
-                self.mic.say("Pardon?")
+                self.mic.say("다시 말씀해 주세요")

 $ git diff jasper.py
diff --git a/jasper.py b/jasper.py
index 4e7f28e..5f9b740 100755
--- a/jasper.py
+++ b/jasper.py
@@ -108,13 +108,12 @@ class Jasper(object):

     def run(self):
         if 'first_name' in self.config:
-            salutation = ("How can I be of service, %s?"
-                          % self.config["first_name"])
+            salutation = ("시스템을 정상적으로 시작했습니다.");

         else:
             salutation = "How can I be of service?"
         self.mic.say(salutation)

-        conversation = Conversation("JASPER", self.mic, self.config)
+        conversation = Conversation("자비스", self.mic, self.config)

         conversation.handleForever()

 if __name__ == "__main__":

3. 구글 STT 기본 언어를 한국어로 변경 합니다.
$ git diff client/stt.py
diff --git a/client/stt.py b/client/stt.py
index a486960..3a70dc7 100644
--- a/client/stt.py
+++ b/client/stt.py
@@ -16,7 +16,6 @@ import jasperpath
 import diagnose
 import vocabcompiler

-
 class AbstractSTTEngine(object):
     """
     Generic parent class for all STT engines
@@ -301,7 +300,7 @@ class GoogleSTT(AbstractSTTEngine):

     SLUG = 'google'

-    def __init__(self, api_key=None, language='en-us'): 
+    def __init__(self, api_key=None, language='ko'):         # FIXME: get init args from config
여기 까지만 하고 실행 해 보면, 놀랍게도 한국어를 알아 듣고, 한국어로 대답합니다.

이제 두번째 숙제인 'ALSA 를 벗어나기' 에 대해 적어보면.
사실 이것을 해야 하는 이유가 유선 마이크/스피커 를 없애기 위해서 입니다.
블루투스 핸즈프리를 사용하면 (정확히 얘기 하자면 HSP 프로파일) 거추장스러운 선들과 장비들을 깔끔하게 없앨 수 있습니다. 이게 왜 필요 하냐를 설명하기 위해서 '백문이 불여일견!'


사진과 같이 라즈베리파이가 마이크 단자를 제공해 주지 않기 때문에 USB Audio 를 사용해서 마이크와 스피커 (혹은 이어폰) 을 사용해야 합니다. 이것만 해도 거추장 스러운데...


무선랜, 아두이노(와 센서들), 배터리팩 까지 연결하게 되면 아수라장이 따로 없습니다.
레고와 합체는 커넝 데스크탑 PC 보다 지저분한 상황이 발생 합니다.

이를 위한 해결책은 ~~~~~~~ 마법의 블루투스!


Wifi 와 HSP 를 사용하기 위한 동글과


아두이노와 라즈베리를 무선으로 연결하기 위한 블루투스



그리고 마지막으로 '자비스' 의 뽀대를 위해 특별히 주문한 'Moto hint'

이렇게 모든 유선 장비들을 다 걷어내고 블루투스 조합으로 정리하면 이렇게 됩니다.


깔끔하죠? 다시한번 비교해 보겠습니다. Before & After

그런데 이런 깔끔함을 얻으려면 엄청난 고통이 수반 됩니다.
이유는 라즈베리파이 에는 PulseAudio 가 제공 되지 않기 때문에 빌드해서 설치를 해야 합니다.

이 과정은 복잡해서 링크로 대체 하겠습니다.
절차를 따라 하시되 주의 하실 점은!! 이 가이드는 A2DP 를 위한 가이드 이기 때문에 (휴대폰에서 음악을 재생하고 라즈베리 파이에서 듣기 위함) 따라하지 말아야 하는 부분이 두 가지 있습니다.

1. Bluetooth 를 업그레이드 하지 마세요
2. PulseAudio 는 다운로드 하신 다음에 Ubuntu 15.04 의 patch 를 적용해 주세요

이것만 유의 한다면 'Motorolla Hint' 를 귀에 꽂고 '자비스' 를 외칠 수 있습니다.

현재까지 구현하고 테스트한 기능은 아래와 같습니다.

'자비스'
 - '지금 몇시야?' (시스템 time 사용)
   >> 지금은 오후 10시 12분 입니다.

 - '온도가 몇도야?' (아두이노 온도 센서 사용)
  >> 31 도 입니다. 에어컨이 필요하세요?
  >>> 어
  >>>> 에어컨을 켭니다. (아두이노 IR Transmitter 사용)

 - 'TV 좀 켜줄래?' (아두이노 IR Transmitter 사용)
  >> 네, TV 를 켭니다.

  - '거실에 불이 켜져 있니?" (아두이노 조도 센서 사용)
   >> 네, 끌까요?
   >>> 그래
   >>>> 네, 불을 끕니다. (홈비타 서버 사용)

  - '보일러좀 켜줘" (홈비타 서버 사용)
   >> 네, 보일러를 켜겠습니다.

  - '음악좀 들려줄래?' (mplayer playlist 사용)
   >> 네, 음악을 재생 합니다.

이번 글이 너무 길어 지쳐서 자비스가 하는 세부 동작의 소스 코드 설명이나 시연을 다음으로 기약 해야 되겠습니다. 홈비타 제어도 재미 있는 부분이어서 다뤄 보겠습니다. 

이제 자비스와 사무라이를 합체 해야 하는 것만 남았네요. 사무라이 합체가 끝나면 시연 동영상을 올려 보도록 하겠습니다.

그럼 다음 기회에....

댓글 10개:

Myoa Promaeus :

좋은 정보 잘 보고 갑니다.
혹시 관련해서 더 여쭤봐도 될까요?

brian Park :

궁금하신점 있으시면 글 남겨주세요, 메일 주셔도 되구요. 1년전 글이라서 지금은 상황이 많이 바뀌긴 했지만 제가 도움이 될 수 있는게 있다면 답변 드리도록 하겠습니다.

young hwan Jung :

안녕하세요
1시간짜리 음성녹음파일인데, 이걸 텍스트로 자동 변환해주는 프로그램은 없을까요?

brian Park :

이런건 어떨까요.. https://vagabond-voyage.blogspot.kr/2016/09/blog-post.html

김수석 :

안녕하세요. 어쩌다 스마트미러를 보게 되어 따라 만들어 보려고 이것저것 만지고 있습니다.
감사하게도 블로그 포스팅 해주신 내용 덕분에 많은 부분을 참고하고 있습니다. :)

글을 올리는 이유는 다름이 아니라 지금 막힌 부분에서 진도를 못나가고 있어서 혹시 도움을 얻을수 있을까 싶어서 입니다 ㅠ
왠지 잘 아실것 같아서요 ^^;

현재 미러에 올리기 위한 소스는 github에 공개된 오픈소스를 받아 기본 세팅을 다 하였는데요.
문제는 음성 관련된 쪽으로 에러가 나는데 원인을 모르겠습니다.
우선 장비는 라즈베리파이3 + USB 사운드카드 + 마이크로 구성이 되어있습니다.

프로그램 시작을 하면 아래와 같은 에러가 나구요.

Traceback (most recent call last):
File "speech/kws.py", line 32, in
detector = snowboydecoder.HotwordDetector(model, sensitivity=detectionSensitivity)
File "/home/pi/smart-mirror/smart-mirror/speech/snowboydecoder.py", line 115, in __init__
stream_callback=audio_callback)
File "/usr/lib/python2.7/dist-packages/pyaudio.py", line 747, in open
stream = Stream(self, *args, **kwargs)
File "/usr/lib/python2.7/dist-packages/pyaudio.py", line 442, in __init__
self._stream = pa.open(**arguments)
IOError: [Errno Invalid sample rate] -9997

혹시나 아시는 내용이시면 염치불구하고 도움을 요청드립니다 ㅠ

brian Park :

제가 지금 여행중이라서 소스코드 볼 여건이 안되지만 에러 내용을 보고 유추해 보면 'Invalid sample rate' 이 힌트가 될것 같습니다. 어렴풋이 라즈베리는 고정 sample rate 만 지원했던 것으로 기억하는데요, pyaudio python 코드를 열어 보셔서 sample rate 설정 하는 부분에서 라즈베리가 지원하는 sample rate 으로 하드코딩을 해 보시면 되지 않을까 싶습니다. 도움이 될지 모르겠네요 ㅠㅜ

김준수 :

make -4j를 하게되면
g++ -o speak speak.o compiledict.o dictionary.o intonation.o readclause.o setlengths.o numbers.o synth_mbrola.o synthdata.o synthesize.o translate.o mbrowrap.o tr_languages.o voices.o wavegen.o phonemelist.o klatt.o sonic.o -lstdc++ -lportaudio -lpthread
g++ -o espeak espeak.o -lstdc++ -L . -lespeak
./libespeak.so: undefined reference to `Pa_StreamActive'
./libespeak.so: undefined reference to `Pa_GetDefaultOutputDeviceID'
collect2: error: ld returned 1 exit status
Makefile:108: recipe for target 'espeak' failed
make: *** [espeak] Error 1
make: *** Waiting for unfinished jobs....
wavegen.o: In function `WavegenOpenSound()':
wavegen.cpp:(.text+0x46c): undefined reference to `Pa_StreamActive'
wavegen.o: In function `WavegenCloseSound()':
wavegen.cpp:(.text+0x558): undefined reference to `Pa_StreamActive'
collect2: error: ld returned 1 exit status
Makefile:105: recipe for target 'speak' failed
make: *** [speak] Error

이러한 오류가 뜨면서 설치가 안됩니다 ㅠㅠ

토리 :

늦은 질문이지만..
google 서버로 음성데이터를 보내고 response를 받는 구조인가요? 아니면 단말 단독으로 처리 가능한 구조인가요?

gogogo :

안녕하세요
혹시 윈도우에서는 어떻게 설정해야 할지 알려주시면 감사하겠습니다.
윈도우에서는 "2. 위의 마이크 버튼을 누르고 볼륨 컨트롤에서 크롬 레코딩 소스를 'Monitor of' 로 바꾼다."이부분에서 볼륨 컨트롤/크롬 레코딩 소스를 도저히 찾을수가 없어서요.
알려주시면 감사하겠습니다.^^;

익명 :

안녕하세요. 좋은 정보 감사합니다~!
제가 요새 Pocketsphinx에 대해 r&d를 해보고 있는데 막히는 부분이 있어서 글을 남기게 되었습니다 ㅠ_ㅠ
혹시 Pocketsphinx를 사용해서 특정 단어를 검색하는 기능이 아니라 말하는대로 Text로 변환하는 STT기능만을 사용하는 게 가능할까요?