관심분야/클라우드

#7 Lambda를 활용한 Polly 호출 - AWS 음성지원 서비스를 활용한 신문 읽어주는 프로젝트

뱅노 2019. 11. 23. 15:09

이제부터 이 프로젝트의 최종 목표인 Polly Service를 호출해서 TTS 변환을 하고 mp3파일을 S3에 적재하는 진행 하려고 한다.

빨간 네모칸 중 Lambda에서 Polly로 넘어가고 다시 Polly에서 Lambda로 돌아오는 구간을 진행하려고 한다. 먼저 Polly를 호출하기 전에 Lambda에 S3 접근 권한이 존재해야 한다. 우선 서비스 검색에서 S3라고 검색을 한다. 그리고는 아래의 화면이 나타나게 된다.

좌측 상단의 [버킷 만들기] 버튼을 클릭해서 Bucket를 새로 만든다. S3는 Simple Storage Service의 약자로서 Web Storage이다. S3에 파일을 업로드하면은 해당 파일에 접근할 수 있는 URL을 제공하게 된다. Public권한으로 Object가 S3에 업로드될 경우 모든 불특정 다수의 사용자가 접근이 가능하게 된다. 따라서 S3를 사용하려고 할 경우에는 각별히 ACL(Access Control List : 접근 통제 목록)에 신경을 써주어야 한다.

 

S3의 Bucket을 생성하고 나서 Lambda(setNewsInfo_test)의 IAM를 조정해 준다. 아래와 같이 S3의 GetObject와 PutObject권한을 넣어준다. 진행하려고 하는 목표에서는 GetObject는 필요는 없다. PutObject에 대한 권한만 존재하고 있으면 된다.

다음에는 Polly로 동일하게 필요한 권한들을 설정한다. 그 후 아래의 소스 코드를 작성해 준다.

import json
import os
import boto3
from contextlib import closing
from botocore.exceptions import BotoCoreError, ClientError
import uuid

def lambda_handler(event, context):
    # TODO implement
    
    print('DynamoDB Insert trigger')
    print('event {}'.format(event))
    
    for idx, val in enumerate(event['Records']) :
        
        event_name = event['Records'][idx]['eventName']
        print('debug eventName {}'.format(event_name))
        if 'INSERT' in event_name :
            print('#### start polly ####')
            
            update_reg_dt = event['Records'][idx]['dynamodb']['Keys']['REG_DT']['S']
            update_reg_srno = event['Records'][idx]['dynamodb']['Keys']['REG_SRNO']['N']
            
            print('reg_dt : {}, reg_srno : {}'.format(update_reg_dt, update_reg_srno))
            
            # DynamoDB에서 들어오는 데이터를 가공시킨다.
            polly_newimage_data = event['Records'][idx]['dynamodb']
            
            # content 영역의 key가 old인지 new인지 확인한다.
            image_key = 'OldImage'
            if 'NewImage' in polly_newimage_data :
                image_key = 'NewImage'
            
            print('#### image_key {}'.format(image_key))    
            
            polly_newimage_data = polly_newimage_data[image_key]
            
            polly_data = polly_newimage_data['CONTENT']['S']
            
            print('polly_data {}'.format(polly_data))
            
            # polly로 음성변환 할 데이터를 보낸다.
            print('### polly request ###')
            
            try :
                polly = boto3.client('polly')
                polly_response = polly.synthesize_speech(Text=polly_data, OutputFormat='mp3', VoiceId='Seoyeon')
                
                print('polly_response {}'.format(polly_response))
                
            except (BotoCoreError, ClientError) as error :
                # The service returned an error, exit gracefully
                print(error)
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

DynamoDB에 적재된 내용을 가지고 와서 필요한 데이터를 추출하고 Polly를 호출해서 mp3 변환 요청을 하는 것이다.

Polly에게 mp3 변환 요청을 할 때 synthesize_speech 함수를 호출하는데 이 함수를 사용할 권한이 없다는 내용이다. 당황할 필요 없다. 이제부터 사용권한이 없다고 나오는 메시지는 모두 IAM에서 처리를 하면된다.

역시나 Polly에 대한 IAM 역할을 확인해 보니 권한이 없었다. 쓸 때 없이 GetLexicon, GetSpeechSynthesisTask에 대한 권한이 존재한다. 불필요한 권한은 삭제를 한다. AWS 보안의 가장 기본은 권한을 최소화시키는 것이다.

synthesize_speech 함수에 대한 사용권한을 부여하고 다시 테스트를 해보니깐 synthesize_speech에서 정상적으로 return값이 나왔다. mp3에 대한 return값은 AudioStream Key의 Value에 존재하고 있다. AudioStream에서 mp3 바이너리를 추출해야 한다.

if 'AudioStream' in polly_response :
	with closing(polly_response['AudioStream']) as stream :
    	output = os.path.join('/tmp/', post_id)
        with open(output, 'wb') as file :
        	file.write(stream.read())
    
    # S3에 polly로 만든 mp3파일 넣기 
    s3 = boto3.client('s3')
    s3.upload_file('/tmp/' + post_id, os.environ['BUCKET_NAME'], post_id + '.mp3')
                    
    # S3 ACL 정책 변경
    s3.put_object_acl(ACL='public-read', Bucket=os.environ['BUCKET_NAME'], Key=post_id + '.mp3')
                    
    # S3 위치 가져오기
    location = s3.get_bucket_location(Bucket=os.environ['BUCKET_NAME'])
    region = location['LocationConstraint']        

Polly의 respone 결괏값 다음에 위 소스를 작성한다. (전체 소스는 마지막에 작성해 놓을 예정이다.)

os.eviron['BUCKET_NAME']이라는 것이 눈에 보일 것이다. 환경변수의 값을 불러오는 소스이다. Lambda 소스 작성 영역 아래로 내려보면 환경변수 영역이 존재한다. 환경변수에 내가 mp3파일을 넣고 싶은 Bucket명을 작성해서 넣는다. 그리고는 os.eviron를 사용해서 가져올 수 있다. 환경변수까지 설정을 하고 난 후 다시 테스트를 진행해 본다.

역시 오류가 발생된다. 한 번에 되면 재미가 없나 보다. 내용은 put_object_acl 사용권한이 없다는 내용이다. 기본적으로 Bucket에 PUT 하는 Object는 private권한으로 설정된다. 그러나 Polly를 활용한 음성변환 mp3의 경우 public권한을 강제로 부여하고 싶어서 put_object_acl 함수를 사용하였다. 그래서 IAM 권한을 살펴보았다. 그런데 put_object_acl 함수에 대한 접근 권한이 존재하였다. 침착하게 S3를 확인해 보니깐 퍼블릭 액세스를 설정을 차단하는 설정이 되어 있었다. 퍼블릭 설정은 할 수 있도록 설정을 풀어준다.

get_bucket_location 함수 접근 권한 차단 내용이다. IAM권한 설정에서 다시 설정해 준다. 이제 마지막으로 DynamoDB에 업데이트된 MP3 URL 정보를 update를 해준다. 아래의 전체 소스를 작성했다.

import json
import os
import boto3
from contextlib import closing
from botocore.exceptions import BotoCoreError, ClientError
import uuid

def lambda_handler(event, context):
    # TODO implement
    
    print('DynamoDB Insert trigger')
    print('event {}'.format(event))
    
    for idx, element in enumerate(event['Records']) :
        
        event_name = element['eventName']
        print('debug eventName {}'.format(event_name))
        
        # Insert 된 값만 처리한다.
        if 'INSERT' == event_name :
            print('#### start polly ####')
            
            update_reg_dt = element['dynamodb']['Keys']['REG_DT']['S']
            update_reg_srno = element['dynamodb']['Keys']['REG_SRNO']['N']
            
            print('reg_dt : {}, reg_srno : {}'.format(update_reg_dt, update_reg_srno))
            
            # DynamoDB에서 들어오는 데이터를 가공시킨다.
            polly_newimage_data = element['dynamodb']
            
            # content 영역의 key가 old인지 new인지 확인한다.
            image_key = 'OldImage'
            if 'NewImage' in polly_newimage_data :
                image_key = 'NewImage'
            
            print('#### image_key {}'.format(image_key))    
            
            polly_newimage_data = polly_newimage_data[image_key]
            
            polly_data = polly_newimage_data['CONTENT']['S']
            
            print('polly_data {}'.format(polly_data))
            
            # polly로 음성변환 할 데이터를 보낸다.
            print('### polly request ###')
            
            try :
                polly = boto3.client('polly')
                polly_response = polly.synthesize_speech(Text=polly_data, OutputFormat='mp3', VoiceId='Seoyeon')
                
                post_id = str(uuid.uuid4())
                
                print('post_id {}'.format(post_id))
                
                if 'AudioStream' in polly_response :
                    with closing(polly_response['AudioStream']) as stream :
                        output = os.path.join('/tmp/', post_id)
                        print('output {}'.format(output))
                        with open(output, 'wb') as file :
                            file.write(stream.read())
                    
                    # S3에 polly로 만든 mp3파일 넣기 
                    # print('bucket {} '.format(os.environ['BUCKET_NAME']))
                    s3 = boto3.client('s3')
                    s3.upload_file('/tmp/' + post_id, os.environ['BUCKET_NAME'], post_id + '.mp3')
                    
                    # S3 ACL 정책 변경
                    s3.put_object_acl(ACL='public-read', Bucket=os.environ['BUCKET_NAME'], Key=post_id + '.mp3')
                    
                    # S3 위치 가져오기
                    location = s3.get_bucket_location(Bucket=os.environ['BUCKET_NAME'])
                    region = location['LocationConstraint']
                    
                    if region is None:
                        url_begining = "https://s3.amazonaws.com/"
                    else:
                        url_begining = "https://s3-" + str(region) + ".amazonaws.com/" \
                    
                    url = url_begining \
                            + str('polly-mp3-save') \
                            + "/" \
                            + str(post_id) \
                            + ".mp3"
                    
                    print('url {}'.format(url))
                    
                    # DynamoDB에 S3 mp3 위치 update 하기
                    dynamodb = boto3.resource('dynamodb')
                    table = dynamodb.Table(os.environ['TABLE_NAME'])
                    
                    update_data_key = {
                        'REG_DT' : update_reg_dt,
                        'REG_SRNO' : int(update_reg_srno),
                    }
                    
                    update_data = {
                        ':MP3_URL' : url
                    }
                    
                    response = table.update_item(Key=update_data_key
                    , UpdateExpression='SET MP3_URL = :MP3_URL'
                    , ExpressionAttributeValues=update_data)
                    
                    print('response {}'.format(response))
                
            except (BotoCoreError, ClientError) as error :
                # The service returned an error, exit gracefully
                print(error)
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

DynamoDB의 경우 update_item 함수 사용권한만 추가해 주고 끝났다. 테스트해보니 DynamoDB를 통해서 들어온 데이터를 mp3 파일로 만들고 S3에 mp3파일을 PUT 하고 DynamoDB에 URL 업데이트까지 되었다. 아래 변환된 mp3 내용을 올려놓는다. 생각보다 음성인식이 잘 되는 것 같다. 참고로 해당 내용은 유아교육신문 사이트에 있는 내용이다.

 

다음에는 S3에 간단한 웹 페이지를 올리고 Lambda를 통해서 MP3 목록을 조회하도록 하겠다.