관리 메뉴

A seeker after truth

FlaskAsk 라이브러리를 이용해 Amazon Alexa 음성 인식 게임 만들기 본문

Projects

FlaskAsk 라이브러리를 이용해 Amazon Alexa 음성 인식 게임 만들기

dr.meteor 2019. 10. 9. 17:34

*본문은 경희대학교 소프트웨어융합대학 <웹파이썬 프로그래밍> 과목(2019년 상반기 수업)의 프로젝트 과제로 제출한 작품입니다. 교수님께서 우수 작품 중 하나로 선정해주셔서 종강하던 날 발표를 했던 바가 있습니다. 블로그에 올리기엔 스스로 너무 부끄럽다고 느끼는 수준의 프로젝트지만, 처음으로 어플을 개발해 본 경험이었던만큼 많은 정성을 쏟았기에 보고서 전문을 기재합니다.

 

[Tongue Twister Game 개발]

 

1.     필요한 지식 공부

1)    Flask-ask

  • 요청 처리하는 법

사용자의 발언은 서버의 적절한 동작으로 매핑이 된다. 이 때, 알렉사는 이 말을 JSON 형태로 바꿔 어플로 전달한다.

또한, ‘슬롯이라는 파라미터가 스킬 상에서 정의되어, 사용자가 말을 하면 항상 파싱되어 알렉사에게 전달된다. 예를 들면, 사용자가 “Alexa, Tell HelloApp to say hi to John” 이라고 말하면 ‘John’이란 이름은 firstname이라 명명된 슬롯으로 전달되어 아마존에서 정의한 슬롯 유형 타입 중 하나인 AMAZON.US_FIRST_NAME으로 인식된다.

 

  • 알렉사의 요청을 함수(기능)로 맵핑하는 데코레이터의 종류

-      Launch: 세션을 시작하는 데코레이터

@ask.launch
def launched():
    return question('Welcome to Foo')

-      Intent: launch를 제외한 다른 intent들을 전달할 때 쓰이는 데코레이터

@ask.intent('HelloWorldIntent')
def hello():
    return statement('Hello, world')

-      Session_ended: 말 그대로 세션(스킬)을 끝내는 요청을 처리하는 데코레이터

@ask.session_ended
def session_ended():
    return "{}", 200

-      on_session_started:  launchintent 데코레이터 하의 코드가 동일할 경우 중복을 피하기 위해 쓰는 데코레이터

 

 

  • 응답을 처리하는 함수의 종류

-      Statement: 세션을 끝내는 함수

@ask.session_ended
def session_ended():
    return "{}", 200

-      Question: 세션을 여는 함수

@ask.intent('AppointmentIntent')
def make_appointment():
    return question("What day would you like to make an appointment for?")

-      Reprompt: 세션이 열려 있는 시간 동안 사용자가 아무 말도 하지 않았을 때, 다시 question 하는 함수

@ask.intent('AppointmentIntent')
def make_appointment():
return question("What day would you like to make an appointment for?") \
.reprompt("I didn't get that. When would you like to be seen?")

 

 

 

2)    SSML 태그

음성 합성 마크 업 언어(SSML: Speech Synthesis Markup Language)는 스킬이 사용자의 요청에 대한 응답을 반환할 때 Alexa 서비스가 음성으로 변환하는 텍스트를 제공한다. Alexa는 마침표 뒤에 일시적으로 중지하거나 물음표로 끝나는 문장을 질문으로 말하는 것과 같은 일반적인 구두점을 자동으로 처리한다.

 

그러나 어떤 경우에는 Alexa가 응답 텍스트의 음성을 생성하는 방법에 대한 추가 제어가 필요할 수 있다. 음성 내에서 더 긴 시간 동안 일시 중지를 원하거나, 숫자의 문자열을 표준 전화 번호로 읽고 싶은 기능 등이 그 예다. Alexa 기술 키트는 SSML 지원을 통해 이러한 유형의 제어를 가능하게 해주는 것이다. Alexa 기술 키트는 SSML 사양에 정의된 태그의 하위 집합을 지원한다.

 알렉사 SDK AWS 람다를 이용하는 것이 아닌 경우에는 pip를 통해 pyssml을 설치하여 진행해야 한다.

 

 

 

2.     English Tongue Twister Mission Game이란?

 Tongue Twister, 소리 내어 읽기에 비슷한 모음이나 받침자가 많아서 술술 읽기 힘들게 만든 문장들을 일컫는 단어다. 실제로 발음 향상에 상당한 도움이 된다고 한다. Alexa가 사용자의 말을 제대로 인식하기 위해선 사용자가 한 호흡 안에 정확한 발음 및 속도로 발설해야 한다는 특징과, 파이썬이 다양한 문자열 관련 내장 함수를 지원한다는 점 2가지에 착안하여 언어학적인 게임을 만드려는 아이디어를 고안하던 중 어렸을 때 학교나 학원에서 종종 접했던 Tongue twister가 떠올랐고, 이를 음성 게임에 접목시켰다.

 본 팀이 고안한 음성 게임의 컨셉 개요는 ‘mission’voice game이다. 이 게임에서 알렉사가 랜덤으로 하나의 tongue twister 문장을 제시해주면 플레이어들은 이 문장을 외워야 하고, 여러 명의 플레이어가 각자 한 번씩 이 문장을 한 호흡 만에 정확한 발음과 속도로 발설해야 한다. 모두가 성공하면 미션을 clear한 것이고, 한 명이라도 실패하면 미션은 실패한 것이다.

 

 

 

3.     English Tongue Twister Mission Game의 상호 작용 모델

              

<Invoke & Launch 단계>

“Start (Invocation Name)”

“Welcome to the world of Tongue Twisters!”

(환호하는 Audio 효과)


<Player 수를 물어보는 단계>

“Let me ask you something. How many players want to join this game?”

“{input number}”


<게임 룰을 설명하는 단계>

“Ok, I got it.”

“From now, I'm gonna tell you guys the answer sentence once, and all of you have to remember this.

Then each of you has to say this sentence clearly at once in rotation.

If you are ready for listening to the answer sentence, say

‘Alexa, tell me the answer’.”

“Alexa, tell me the answer’”


<alexa가 답을 말해주는 단계>

“Ok. The answer sentence is, (answer sentence).”

“If you want to start the game, say 'Alexa, Let’s start!'.

Unless, say 'Tell me the answer one more time'.”

* 사용자가 'Tell me the answer one more time'이라 말할 경우 answer를 다시 말해줌

'Alexa, Let’s start!'


<본격적으로 play하는 단계>

“Ok. Please start to speak the answer!”

(플레이어들이 번갈아 가며 문장을 한 번씩 말한다.)

(한 문장이 끝날 때마다 알렉사는 내부적으로 성공/실패 여부를 판단하여 저장해둔다)

--------------------------------------------------------------------------------------------------------------

<AlexaMission Clear 여부를 말해주는 단계>

(성공한 경우)”(박수 음향 효과) Congratuations! You guys perfectly clear the hard mission!”

(실패한 경우)”(부정적인 음향 효과) Oh my Gosh, You guys are failed! Try again!”

 

 

 

4.     각 단계를 Intent로 나누어 정의하기

 알렉사의 커스텀 스킬은 사용자의 대답이 데코레이터로 정의된 한 개의 함수로 매핑되면 함수의 반환값으로 알렉사의 응답을 보내주는 구조로 이뤄져 있다. 따라서 데코레이터로 정의된 한 개의 함수를 하나의 ‘Intent’로 정의하여 알렉사 개발 콘솔에 정의해주어야 한다.

 우리는 3번에서 기숧한 각 단계를 LaunchIntent, CalculateIntent, AnswerIntent & RepeatIntent, YesIntent, PlayIntent로 정의하고, 알렉사 개발 콘솔에서 다음과 같이 설정해주었다.

 

 

5.     코드 작성

다음으로, intent에 해당하는 함수를 코드로 작성하였다. 코드는 주석과 함께 첨부했다.

import flask
import flask_ask
import random
import datacollection
import pyssml


app = flask.Flask(__name__) # create a instant of flask
ask = flask_ask.Ask(app, "/") # create a instant of flask_ask
playerNum = 0 # global variable that stores the number of players
answerTwister = "" # global variable that stores the answer sentence
success = [] # global variable that stores the result of the game


# choose the answer sentence from the list in the datacollection.py in random
def ChooseAnswer():
    index = random.randint(0, 33)
    answer = datacollection.list[index]
    return answer


# examine the accordance
def isCorrect(str1, str2):
    if (str1.lower().strip(',').strip('.').strip('?') == str2.lower().strip(',').strip('.').strip('?')):
        return True
    else:
        return False



@ask.launch
def launch_game():
    msg = "<speak> <voice name='Matthew'>Welcome to the world of tongue twisters!</voice>" + \
    "<audio src='soundbank://soundlibrary/human/amzn_sfx_large_crowd_cheer_02'/>" + \
    "<prosody pitch = '+12%'>Let me ask you something. How many players want to join this game?</prosody></speak>"
    repeat_msg = "Please tell me the number of players who want to join."
    return flask_ask.question(msg).reprompt(repeat_msg)


@ask.intent("CalculateIntent", convert={'inputNum': int})
def PlayerNum(inputNum):
    global playerNum
    playerNum = inputNum
    print(playerNum)
    global answerTwister
    answerTwister = ChooseAnswer()

    msg = "<speak>Ok, I got it. " + \
            "<voice name='Matthew'>From now, I'm gonna tell you guys the answer sentence once, and all of you have to remember this. " + \
            "Then each of you has to say this sentence clearly at once in rotation. " + \
            "If you are ready for listening to the answer sentence, say</voice> " + \
            "<break time='0.6s'/><prosody rate='slow'>'Alexa, tell me the answer'.</prosody></speak>"

    return flask_ask.question(msg)


@ask.intent("AnswerIntent")
def SpeakAnswer():
    msg1 = "<speak><voice name='Matthew'>Ok. The answer sentence is, " + answerTwister
    msg2 = ". <break time='0.8s'/>If you want to start the game, say</voice><break time = '0.6s'/><prosody rate='slow'>'Alexa, Lets start!'</prosody>" + \
    "<break time='1s'/><voice name='Matthew'>Unless, say </voice><break time = '0.6s'/><prosody rate='slow'>'Tell me the answer one more time'.</prosody></speak>"
    totalMsg = msg1 + msg2

    return flask_ask.question(totalMsg)


@ask.intent("RepeatIntent")
def RepeatAnswer():
    msg1 = "<speak><voice name='Matthew'>Ok. The answer sentence is, " + answerTwister
    msg2 = ". <break time='1s'/>If you want to start the game, say</voice><break time = '0.6s'/><prosody rate='slow'>'Alexa, Lets start!'</prosody>" + \
    "<break time='1s'/><voice name='Matthew'>Unless, say </voice><break time = '0.6s'/><prosody rate='slow'>'Tell me the answer one more time'.</prosody></speak>"
    totalMsg = msg1 + msg2

    return flask_ask.question(totalMsg)


@ask.intent("YesIntent")
def Yes():
    msg = "<speak>Ok. <prosody pitch='+20%'>Please start to speak the answer!</prosody></speak>"
    return flask_ask.question(msg)


@ask.intent("PlayIntent", convert={'userstc': str})
def Play(userstc):
    correct = isCorrect(answerTwister, userstc)
    global success
    success.append(correct)
    print(len(success), correct)
    lastmsg = "<speak><audio src='soundbank://soundlibrary/human/amzn_sfx_crowd_applause_03'/>" + \
        "<prosody pitch='x-high'>Congratuations!</prosody> You guys <prosody pitch='+25%'>perfectly</prosody> clear the hard mission!</speak>"

    if (len(success) != playerNum):
        msg = "<speak><audio src='soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_neutral_response_01'/></speak>"
        return flask_ask.question(msg)
    else:
        i = 0
        while(i != len(success)):
            if(success[i] == False):
                lastmsg = "<speak><audio src='soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_negative_response_03'/>" + \
                "<prosody pitch='low'>Oh my Gosh,</prosody> You guys are failed! <prosody pitch='high'>Try again!</prosody></speak>"
                break
            else:
                i += 1

        return flask_ask.statement(lastmsg)




if __name__ == '__main__':
    app.run(debug=True)

 

6.     알렉사 개발 콘솔에서 커스텀 스킬 추가하기

 우선, 이 알렉사 스킬을 호출할 때 인식하는 고유한 이름인 Invocation Name을 설정해주었다. 다른 다양한 스킬들 사이에서도 이 스킬이 구별되어야 했기 때문에 최대한 unique한 이름으로 설정해주었다.

 

 

7.     슬롯 추가 및 슬롯 유형 설정

 사용자의 발설 내용으로부터 특별히 구분하여 모듈 내에서 변수로 활용하는 슬롯은 플레이어 수(playerNum)와 사용자가 발설한 정답 문장이다(userstc). 이 때, 전자의 경우 아마존에 이미 정의된 슬롯 유형 타입 중 NUMBER 타입을 쓸 수 있었다. 그러나, 후자의 경우 그냥 plain text인데 알렉사의 슬롯 유형은 특수한 케이스들로 분류가 되어 있었으므로 이렇게 평이한 타입은 존재하지 않았다.

 따라서 alexa skill kit 기술 문서를 통해 plain text를 구분하는 방법을 찾고 공부해보았는데, JSON, XML Alexa SDK for python에 대한 내용밖에 없었다. 하지만 우리 팀은 파이썬SDK로 개발을 하지 않았기 때문에 기술 문서에 나와있는 내용은 따라할 수 없었고, JSON을 오리지널 파이썬 내에서 다루는 방법은 공부를 해도 이해가 잘 되지 않았다. XML에 대한 내용 역시 마찬가지였다. 결국, 사용자가 직접 슬롯 유형 타입을 정의할 수 있는 사용자 지정 슬롯 타입기능을 이용하여 슬롯 유형을 직접 정의하였다.

 

8.     메인 코드에 ssml 태그 추가

우리가 이용한 ssml 태그는 다음과 같다.

- Audio 태그: 응답을 렌더링하는 동안 알렉사 서비스가 재생할 있는 MP3 파일의 URL 제공할 있다. 사용 예는 다음과 같다.

<speak>

<audio src="soundbank://soundlibrary/transportation/amzn_sfx_car_accelerate_01" />

    You can order a ride, or request a fare estimate.

</speak>

 

- Break 태그: 말을 중간에 멈추는 기능을 제공하며, 그 종류에는 strength 속성과 time 속성이 있다. 그 중에서도 time 속성을 이용했으며, 사용 예는 다음과 같다.

<speak>

    There is a three second pause here <break time="3s"/>

    then the speech continues.

</speak>

 

- Strength: 말의 속도나 볼륨을 변화시키므로써 단어나 구를 강조할 수 있다. 강조의 세기가 강할수록 더 크고 느리게 말한다. 그 종류로는 strong, moderate, reduced가 있다.

<speak>

    I already told you I

    <emphasis level="strong">really like</emphasis>

    that person.

</speak>

 

- prosody태그: 볼륨, 음조, 속도를 조절할 수 있다. 이 중에서도 음조(pitch), 속도(rate) 태그를 사용하였으며 둘 다 숫자 혹은 지정값으로 조정이 가능하다.

<speak>

    Normal volume for the first sentence.

    <prosody volume="x-loud">Louder volume for the second sentence</prosody>.

    When I wake up, <prosody rate="x-slow">I speak quite slowly</prosody>.

    I can speak with my normal pitch,

    <prosody pitch="x-high"> but also with a much higher pitch </prosody>,

    and also <prosody pitch="low">with a lower pitch</prosody>.

</speak>

 

- voice 태그: 음색을 조절할 수 있다. 미국 영어로는 Ivy, Joanna , Joey , Justin , Kendra , Kimberly , Matthew , Salli 의 목소리가 지원된다.

<speak>

    I want to tell you a secret.

    <voice name=’Matthew’>I am not a real human.</voice>.

    Can you believe it?

</speak>

 

 

9.     최종 테스트

1)    성공한 경우

터미널 화면은 다음과 같다. 가장 처음에 출력된 숫자는 플레이어 수(two)가 터미널 창에 출력된 모습이고, 숫자와 True가 같이 출력된 부분은 N번째 플레이어의 정답 여부를 나타낸다.

 

2)    실패한 경우

터미널 화면의 모습은 다음과 같다.

 

 

10.  개인적인 소감 

1)    Agile 원칙의 의의를 절감하다

 처음에 팀원이 7명이었을 때, 이 팀플에 열의를 보이는 팀원과 그렇지 않은 팀원들 간에 극명한 태도 차이가 있었다. 그 열의는 팀플의 분위기와 팀워크에 지대한 영향을 미쳤다. 최종적으로 열의가 뛰어났던 팀원들만 최종팀으로 남고 나니, 팀워크와 분위기에 매우 긍정적인 영향을 미쳤다.

2)    프로젝트 규모에 맞는 적절한 팀원 수의 중요성을 느끼다

 처음에 내가 나의 팀플 계획에 합류하길 원했던 모든 사람들을 다 팀에 합류시켰던 이유는 사람이 많으면 많을수록 서로 배우는 것도 많고, 노동력 확충 및 보다 효과적인 아이데이션이 가능하다고 생각했기 때문이었다. 하지만 인원이 너무 많으니 팀원 한 명이 느끼는 책임감이 상당히 해이해지는 것을 느꼈다. 주제의 난이도를 낮추기 위해 주제를 수정함에 따라 팀원도 조정이 되고 나니, 팀워크가 훨씬 수월함을 느꼈다.

3)    과도한 욕심은 금물

 나는 2년 간 학업, 즉 학점에만 충실하게 공부를 하면서 도대체 내가 역량을 쌓기 위해 어떤 일을 해야 하는지 감을 잡지 못했다. 그것을 깨닫기 위해 동일 분야의 뉴스를 보고, 팟캐스트를 듣고, 관련 분야의 책을 다독하고, 최대한 인터넷 및 게시판 정보를 입수하여 활동을 해보아도 마찬가지였다. 그게 아무것도 아닌 것은 아니었는지, 소프트웨어융합학과로 진학한 뒤 들은 수업들이 그동안 내가 쌓아온 배경지식들과 화려한 지적 상호 작용을 일으키며 드디어 개발 커리어를 쌓는 일을 어떻게 하면 되는지 깨닫게 됐다. 그 깨달음에 눈이 멀어 나는 그 어느 때보다 드높은 지적 이상을 바라보게 되었고, 이 텀프로젝트가 10점짜리 텀프로젝트임을 간과한 채 그동안 켜켜이 쌓인 개발에 대한 나의 강렬한 갈증을 해소하는데 급급했다. 중간고사 이후 내 시간의 7~80%는 텀프로젝트 및 실용적인 개발에 대한 배경 지식을 쌓는데 투입됐다. 사실 내가 가장 원하는 것이 바로 이 배경 지식들을 공부하여 app을 개발해보는 것이었기 때문에, 결과물 자체는 다소 미미할지라도 내가 원하는 바는 제대로 이룬 셈이다. 하지만 기존 주제가 내가 현재 가진 능력, 그리고 주어진 시간에 비해 상당히 비현실적인 목표였기 때문에 하필 다른 할 일도 많은 학기 중의 시간을 불태워버림에 따라 다른 일들을 놓치기도 했다. 그리고 매우 지쳤다. 목표가 버거우면 시너지가 나지 않는다는 사실을 뼈에 사무치게 깨달았다. 앞으로 모든 일은 나의 능력과 주어진 시간을 고려해서 현실적으로 계획해야 한다는 큰 깨달음을 얻었다.

4)    배경 지식이 없는 것에 대해 기술 문서를 읽고 개발해보는 경험을 통해 러닝 커브란 개념을 배우다

원래 개발자란 사람은 이렇게 한 번도 개발해보지 않은 것에 대해서도 빨리 공부를 해서 개발을 할 줄 아는 사람이 진가를 가진 게 아닌가 생각한다. 실제로 이렇게 일을 하는 날도 많다고 알고 있었다. 그런데, 영어로 된 방대한 양의 기술 문서를 읽고 필요한 정보를 골라내어 공부를 하는 일이 생각보다 어렵게 느껴졌다. 실제로 이런 능력을 통상 러닝커브라고 일컫는다는 것을 이번에 알게 되었다. 앞으로 개발 경험을 더 늘려 포트폴리오에서 내가 러닝 커브가 좋은 개발 지망생임을 어필하겠노라고 목표를 잡았다.

5)    사람들과 함께 뭔가를 만들어 나가는 일만큼 의미 있는 일은 없다

팀워크가 잘 맞는 팀원들과 함께 공부를 하고 고민을 하는 과정이 내내 즐거웠다. 내가 혼자 개발을 할 때보다 배의 기쁨을 느낄 수 있었다.

6)    공부할 게 정말로 많다

 일단 이 텀프로젝트의 배경 지식을 쌓는 데에 굉장히 많은 시간을 들이면서, 그동안 궁금했던 모든 부분들이 다 해결되었다. 백엔드 개발자는 무엇을 준비하면 되는가, 함수형/메타 프로그래밍이 무엇인가, JSON/SSML/XML이 무엇인가, 가상환경은 무엇인가, pip는 어떻게 활용하는 것인가, 웹 및 서버의 구동 원리는 무엇인가…. 이렇게 사소한 텀프로젝트 하나만 해도 그 과정에서 얻는 것이 많으니, 직접 개발해보는 일의 중요성을 절감할 수 있었다. 그리고 지식이 늘어감에 따라 역설적이게도 공부할 것은 점점 더 늘어나며, 그 지적 이상도 날로 커짐을 느꼈다. 무엇보다도 알고리즘. 알고리즘이 충분히 연습되어있지 않으니 머리가 충분히 돌아가지 않았다. 따라서 알고리즘 분야의 학습 및 연습이 가장 간절해졌다. 아니, 간절을 넘어 염원에 가까워졌다.

7)    결과가 너무 아쉽다

 사실 필요한 지식을 습득하는 데에 더 많은 노력과 시간이 들어가는 바람에 결과물이 너무나 미미해졌다. 사실 여기부터 정교함을 요하는 과정까지 뚫고 가는 것이 진정한 개발이라고 생각하기 때문에, 후에 기존의 코드를 리팩토링하는 과정을 거치려고 한다.

 

 

[배운 점]

- json, 데코레이터의 정의 및 용도

- 함수형 프로그래밍의 정의

- 외부 라이브러리 사용법(flask, flaskask)

- ssml 태그의 정의 및 용도

- ngrok 라는 프로그램의 용도 및 사용법

 

[참고 자료]

https://flask-ask.readthedocs.io/en/latest/getting_started.html (flask-ask 관련 안내 문서)

https://developer.amazon.com/docs/custom-skills/understanding-custom-skills.html(아마존 알렉사 서비스 관련 안내 사이트)

https://flask-docs-kr.readthedocs.io/ko/latest/installation.html#id3 (flask 관련 문서)

https://developer.amazon.com/blogs/post/Tx14R0IYYGH3SKT/Flask-Ask-A-New-Python-Framework-for-Rapid-Alexa-Skills-Kit-Development

<빠르게 활용하는 파이썬3.6 프로그래밍>(위키북스, 2017)

<깔끔한 파이썬 탄탄한 백엔드>(비제이퍼블릭, 2019)