

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

# Amazon Lex V2 봇으로 대화 스트림 시작
<a name="start-stream-conversation"></a>

[StartConversation](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_StartConversation.html) 작업을 사용하여 애플리케이션에서 사용자와 Amazon Lex V2 봇 간의 스트림을 시작할 수 있습니다. 애플리케이션의 `POST` 요청에 따라 애플리케이션과 Amazon Lex V2 봇 간의 연결이 설정됩니다. 이렇게 하면 애플리케이션과 봇이 이벤트를 통해 서로 정보를 교환할 수 있습니다.

**참고**  
StartConversation을 사용할 때 발화 오디오 지속 시간이에 대해 구성된 값을 초과하면 `max-length-ms` Amazon Lex V2는 지정된 지속 시간에 오디오를 차단합니다.

`StartConversation` 작업은 다음 SDK에서만 지원됩니다.
+ [AWS SDK for C\$1\$1](https://docs.aws.amazon.com/goto/SdkForCpp/runtime.lex.v2-2020-08-07/StartConversation)
+ [AWS SDK for Java V2](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/lexruntimev2/LexRuntimeV2AsyncClient.html)
+ JavaScript용 AWS SDK v3[https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-lex-runtime-v2/index.html#aws-sdkclient-lex-runtime-v2](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-lex-runtime-v2/index.html#aws-sdkclient-lex-runtime-v2)
+ [AWS SDK for Ruby V3](https://docs.aws.amazon.com/goto/SdkForRubyV3/runtime.lex.v2-2020-08-07/StartConversation)

애플리케이션이 Amazon Lex V2 봇으로 전송해야 하는 첫 번째 이벤트는 [ConfigurationEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_ConfigurationEvent.html)입니다. 이 이벤트에는 응답 유형 형식과 같은 정보가 포함됩니다. 구성 이벤트에서 사용할 수 있는 파라미터는 다음과 같습니다.
+ **responseContentType** – 봇이 사용자 입력에 텍스트 또는 음성으로 응답할지 여부를 결정합니다.
+ **sessionState** – 미리 정해진 의도 또는 대화 상태 등 봇과의 스트리밍 세션과 관련된 정보입니다.
+ **WelcomeMessages** – 봇과의 대화를 시작할 때 사용자에게 재생되는 환영 메시지를 지정합니다. 이러한 메시지는 사용자가 입력을 하기 전에 재생됩니다. 환영 메시지를 활성화하려면 `sessionState` 및 `dialogAction` 파라미터에 대한 값도 지정해야 합니다.
+ **DisablePlayback** – 봇이 발신자 입력을 수신하기 전에 클라이언트의 신호를 기다려야 할지 여부를 결정합니다. 기본적으로 재생이 활성화되므로 이 필드의 값은 `false`입니다.
+ **requestAttributes** – 요청에 대한 추가 정보를 제공합니다.

이전 파라미터에 대한 값을 지정하는 방법에 대한 자세한 내용은 [StartConversation](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_StartConversation.html) 작업의 [ConfigurationEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_ConfigurationEvent.html) 데이터 유형을 참조하세요.

봇과 애플리케이션 간의 각 스트림에는 구성 이벤트가 하나만 있을 수 있습니다. 애플리케이션이 구성 이벤트를 보낸 후 봇은 애플리케이션으로부터 추가 통신을 받을 수 있습니다.

사용자가 오디오를 사용하여 Amazon Lex V2 봇과 통신하도록 지정한 경우 애플리케이션은 해당 대화 중에 봇에 다음 이벤트를 전송할 수 있습니다.
+ [AudioInputEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_AudioInputEvent.html) – 최대 크기가 320바이트인 오디오 청크를 포함합니다. 서버에서 봇으로 메시지를 보내려면 애플리케이션이 여러 개의 오디오 입력 이벤트를 사용해야 합니다. 스트림의 모든 오디오 입력 이벤트는 동일한 오디오 형식을 가져야 합니다.
+ [DTMFinputEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_DTMFInputEvent.html) – DTMF 입력을 봇에 보냅니다. 각 DTMF 키 누름은 단일 이벤트에 해당합니다.
+ [PlaybackCompletionEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_PlaybackCompletionEvent.html) – 사용자 입력의 응답이 재생되었음을 서버에 알립니다. 사용자에게 오디오 응답을 보내는 경우 재생 완료 이벤트를 사용해야 합니다. 구성 이벤트의 `disablePlayback`이 `true`이면 이 기능을 사용할 수 없습니다.
+ [DisconnectionEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_DTMFInputEvent.html) – 사용자가 대화에서 연결을 끊었다는 사실을 봇에게 알립니다.

사용자가 텍스트를 사용하여 봇과 통신하도록 지정한 경우 애플리케이션은 해당 대화 중에 봇에 다음 이벤트를 전송할 수 있습니다.
+ [TextInputEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_TextInputEvent.html) – 애플리케이션에서 봇으로 전송되는 텍스트입니다. 텍스트 입력 이벤트에는 최대 512자를 포함할 수 있습니다.
+ [PlaybackCompletionEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_PlaybackCompletionEvent.html) – 사용자 입력의 응답이 재생되었음을 서버에 알립니다. 사용자에게 오디오를 재생하려면 이 이벤트를 사용해야 합니다. 구성 이벤트의 `disablePlayback`이 `true`이면 이 기능을 사용할 수 없습니다.
+ [DisconnectionEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_DTMFInputEvent.html) – 사용자가 대화에서 연결을 끊었다는 사실을 봇에게 알립니다.

Amazon Lex V2 봇에 보내는 모든 이벤트를 올바른 형식으로 인코딩해야 합니다. 자세한 내용은 [이벤트 스트림 인코딩](event-stream-encoding.md) 단원을 참조하세요.

모든 이벤트에는 이벤트 ID가 있습니다. 스트림에서 발생할 수 있는 문제를 해결하는 데 도움이 되도록 각 입력 이벤트에 고유한 이벤트 ID를 할당하세요. 그런 다음 봇의 모든 처리 실패 문제를 해결할 수 있습니다.

또한 Amazon Lex V2는 각 이벤트에 타임스탬프를 사용합니다. 이벤트 ID와 함께 이러한 타임스탬프를 사용하여 네트워크 전송 문제를 해결할 수 있습니다.

사용자와 Amazon Lex V2 봇 간의 대화 중에 봇은 사용자에 대한 응답으로 다음과 같은 아웃바운드 이벤트를 전송할 수 있습니다.
+ [IntentResultEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_IntentResultEvent.html) – Amazon Lex V2가 사용자 발언으로부터 판단한 의도를 포함합니다. 각 내부 결과 이벤트에는 다음이 포함됩니다.
  + **InputMode** – 사용자 발화 유형입니다. 유효한 값은 `Speech`, `DTMF`또는 `Text`입니다.
  + **interpretations** – Amazon Lex V2가 사용자 발화로부터 결정하는 해석입니다.
  + **requestAttributes** – 람다 함수를 사용하여 요청 속성을 수정하지 않았다면, 이 속성은 대화 시작 시 전달된 것과 동일합니다.
  + **sessionId** – 대화에 사용되는 세션 식별자입니다.
  + **sessionState** – Amazon Lex V2를 사용한 사용자 세션의 상태입니다.
+ [TranscriptEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_TranscriptEvent.html) – 사용자가 애플리케이션에 입력을 제공하는 경우, 이 이벤트에는 사용자가 봇에게 말한 내용의 대화 기록이 포함됩니다. 사용자 입력이 없는 경우 애플리케이션은 `TranscriptEvent`를 수신하지 않습니다.

  애플리케이션으로 전송되는 트랜스크립트 이벤트의 값은 대화 모드로 오디오(음성 및 DMTF) 또는 텍스트를 지정했는지 여부에 따라 달라집니다.
  + 음성 입력 대화 기록 – 사용자가 봇과 대화하고 있는 경우 대화 기록 이벤트는 사용자 오디오의 트랜스크립션입니다. 이는 사용자가 말을 시작한 시점부터 말을 끝낼 때까지의 모든 음성 대화를 기록한 것입니다.
  + DTMF 입력의 대화 기록 – 사용자가 키패드로 타이핑하는 경우 대화 기록 이벤트에는 사용자가 입력에서 누른 모든 숫자가 포함됩니다.
  + 텍스트 입력의 대화 기록 – 사용자가 텍스트 입력을 제공하는 경우 대화 기록 이벤트에는 사용자 입력의 모든 텍스트가 포함됩니다.
+ [TextResponseEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_TextResponseEvent.html) – 봇 응답을 텍스트 형식으로 포함합니다. 기본적으로 텍스트 응답이 반환됩니다. Amazon Lex V2가 오디오 응답을 반환하도록 구성한 경우 이 텍스트는 오디오 응답을 생성하는 데 사용됩니다. 각 텍스트 응답 이벤트에는 봇이 사용자에게 반환하는 메시지 객체 배열이 포함됩니다.
+ [AudioResponseEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_AudioResponseEvent.html) – `TextResponseEvent`에서 생성된 텍스트에서 합성된 오디오 응답을 포함합니다. 오디오 응답 이벤트를 수신하려면 오디오 응답을 제공하도록 Amazon Lex V2를 구성해야 합니다. 모든 오디오 응답 이벤트는 동일한 오디오 형식을 갖습니다. 각 이벤트에는 100바이트 이하의 오디오 청크가 포함됩니다. Amazon Lex V2는 오디오 응답 이벤트가 종료되었음을 나타내는 `bytes` 필드가 `null`로 설정된 빈 오디오 청크를 애플리케이션에 전송합니다.
+ [PlaybackInterruptionEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_PlaybackInterruptionEvent.html) – 봇이 애플리케이션에 보낸 응답을 사용자가 중단하면 Amazon Lex V2가 이 이벤트를 트리거하여 응답 재생을 중지합니다.
+ [HeartbeatEvent](https://docs.aws.amazon.com/lexv2/latest/APIReference/API_runtime_HeartbeatEvent.html) – Amazon Lex V2는 애플리케이션과 봇 간의 연결 제한 시간이 초과되지 않도록 이 이벤트를 주기적으로 다시 전송합니다.

## Amazon Lex V2 봇 사용 시 오디오 대화의 이벤트 시간 순서
<a name="audio-conversation-sequence"></a>

다음 다이어그램은 사용자와 Amazon Lex V2 봇 간의 스트리밍 오디오 대화를 보여줍니다. 애플리케이션은 봇에 지속적으로 오디오를 스트리밍하고, 봇은 오디오에서 사용자 입력을 찾습니다. 이 예에서 사용자와 봇은 모두 음성을 사용하여 통신합니다. 각 다이어그램은 사용자 발화 및 해당 발화에 대한 봇의 반응에 해당합니다.

다음 다이어그램은 애플리케이션과 봇 간의 대화 시작을 보여줍니다. 스트림은 타임 0(t0)에 시작됩니다.

![\[Timeline showing audio input events from application and various response events from bot during a conversation.\]](http://docs.aws.amazon.com/ko_kr/lexv2/latest/dg/images/Streaming-Page-1.png)


다음 목록은 위 다이어그램의 이벤트에 대해 설명합니다.
+ t0: 애플리케이션이 봇에 구성 이벤트를 전송하여 스트림을 시작합니다.
+ t1: 애플리케이션이 오디오 데이터를 스트리밍합니다. 이 데이터는 애플리케이션에서 일련의 입력 이벤트로 나뉩니다.
+ t2: *사용자 발화 1*의 경우 사용자가 말하기 시작하면 봇이 오디오 입력 이벤트를 감지합니다.
+ t2: 사용자가 말하는 동안 봇은 연결을 유지하기 위해 하트비트 이벤트를 보냅니다. 연결이 시간 초과되지 않도록 이러한 이벤트를 간헐적으로 전송합니다.
+ t3: 봇은 사용자 발화의 끝을 감지합니다.
+ t4: 봇은 사용자 음성의 대화 기록이 포함된 대화 기록 이벤트를 애플리케이션에 다시 전송합니다. 이는 *사용자 발화 1에 대한 Bot의 응답*의 시작입니다.
+ t5: 봇은 의도 결과 이벤트를 전송하여 사용자가 수행하고자 하는 작업을 나타냅니다.
+ t6: 봇이 텍스트 응답 이벤트에서 응답을 텍스트로 제공하기 시작합니다.
+ t7: 봇은 일련의 오디오 응답 이벤트를 애플리케이션에 전송하여 사용자를 위해 재생합니다.
+ t8: 봇은 연결을 간헐적으로 유지하기 위해 또 다른 하트비트 이벤트를 보냅니다.

다음 다이어그램은 이전 다이어그램과 이어집니다. 애플리케이션이 봇에 재생 완료 이벤트를 전송하여 사용자에 대한 오디오 응답 재생을 중지했음을 알리는 것을 보여줍니다. 애플리케이션은 *사용자 발화 1에 대한 봇 응답*을 사용자에게 재생합니다. 사용자는 *사용자 발화 1에 대한 봇의 응답*에 *사용자 발화 2*로 응답합니다.

![\[Timeline of audio input events from user and response events from bot, showing interaction flow.\]](http://docs.aws.amazon.com/ko_kr/lexv2/latest/dg/images/Streaming-Page-2.png)


다음 목록은 위 다이어그램의 이벤트에 대해 설명합니다.
+ t10: 애플리케이션이 사용자에게 봇의 메시지 재생을 완료했음을 알리는 재생 완료 이벤트를 보냅니다.
+ t11: 애플리케이션은 사용자의 응답인 *사용자 발화 2*를 봇에 다시 보냅니다.
+ t12: *사용자 발화 2에 대한 봇 응답*의 경우, 봇은 사용자가 말을 멈출 때까지 기다린 다음 음성 응답을 제공하기 시작합니다.
+ t13: 봇이 *사용자 발화 2에 대한 봇 응답*을 애플리케이션에 보내는 동안 봇은 *사용자 발화 3*의 시작을 감지합니다. 봇은 *사용자 발화 2에 대한 봇의 응답*을 중지하고 재생 중단 이벤트를 보냅니다.
+ t14: 봇은 재생 중단 이벤트를 애플리케이션에 전송하여 사용자가 프롬프트를 중단했음을 알립니다.

다음 다이어그램은 *사용자 발화 3에 대한 봇의 응답*과 봇이 사용자 발화에 응답한 후에도 대화가 계속되는 것을 보여줍니다.

![\[Diagram showing events flow between application, bot, and user utterances over time.\]](http://docs.aws.amazon.com/ko_kr/lexv2/latest/dg/images/Streaming-Page-3.png)


# API를 사용하여 스트리밍 대화 시작
<a name="using-streaming-api"></a>

Amazon Lex V2 봇으로 스트리밍을 시작하면 다음 작업을 수행할 수 있습니다.

1. 서버에 대한 초기 연결을 생성합니다.

1. 보안 인증 정보와 봇 세부 정보를 구성합니다. 봇 세부 정보에는 봇이 DTMF 및 오디오 입력을 받는지 아니면 텍스트 입력을 받는지 여부가 포함됩니다.

1. 서버에 이벤트를 전송합니다. 이러한 이벤트는 사용자의 텍스트 데이터 또는 오디오 데이터입니다.

1. 서버에서 전송된 이벤트를 처리합니다. 이 단계에서는 봇 출력을 사용자에게 텍스트와 음성 중 어느 것으로 표시할지 결정합니다.

다음 코드 예시는 Amazon Lex V2 봇 및 로컬 컴퓨터와의 스트리밍 대화를 초기화합니다. 필요에 맞게 코드를 수정할 수 있습니다.

다음 코드는 AWS SDK for Java 를 사용하여 봇에 대한 연결을 시작하고 봇 세부 정보와 보안 인증 정보를 구성하는 요청의 예입니다.

```
package com.lex.streaming.sample;

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.lexruntimev2.LexRuntimeV2AsyncClient;
import software.amazon.awssdk.services.lexruntimev2.model.ConversationMode;
import software.amazon.awssdk.services.lexruntimev2.model.StartConversationRequest;

import java.net.URISyntaxException;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

/**
 * The following code creates a connection with the Amazon Lex bot and configures the bot details and credentials.  
 * Prerequisite: To use this example, you must be familiar with the Reactive streams programming model.
 * For more information, see
 * https://github.com/reactive-streams/reactive-streams-jvm.
 * This example uses AWS SDK for Java for Amazon Lex V2.
 * <p>
 * The following sample application interacts with an Amazon Lex bot with the streaming API. It uses the Audio
 * conversation mode to return audio responses to the user's input.
 * <p>
 * The code in this example accomplishes the following:
 * <p>
 * 1. Configure details about the conversation between the user and the Amazon Lex bot. These details include the conversation mode and the specific bot the user is speaking with.
 * 2. Create an events publisher that passes the audio events to the Amazon Lex bot after you establish the connection. The code we provide in this example tells your computer to pick up the audio from
 * your microphone and send that audio data to Amazon Lex.
 * 3. Create a response handler that handles the audio responses from the Amazon Lex bot and plays back the audio to you.
 */
public class LexBidirectionalStreamingExample {

    public static void main(String[] args) throws URISyntaxException, InterruptedException {
        String botId = "";
        String botAliasId = "";
        String localeId = "";
        String accessKey = "";
        String secretKey = "";
        String sessionId = UUID.randomUUID().toString();
        Region region = Region.region_name; // Choose an AWS Region where the Amazon Lex Streaming API is available.

        AwsCredentialsProvider awsCredentialsProvider = StaticCredentialsProvider
                .create(AwsBasicCredentials.create(accessKey, secretKey));

        // Create a new SDK client. You need to use an asynchronous client.
        System.out.println("step 1: creating a new Lex SDK client");
        LexRuntimeV2AsyncClient lexRuntimeServiceClient = LexRuntimeV2AsyncClient.builder()
                .region(region)
                .credentialsProvider(awsCredentialsProvider)
                .build();


        // Configure the bot, alias and locale that you'll use to have a conversation.
        System.out.println("step 2: configuring bot details");
        StartConversationRequest.Builder startConversationRequestBuilder = StartConversationRequest.builder()
                .botId(botId)
                .botAliasId(botAliasId)
                .localeId(localeId);

        // Configure the conversation mode of the bot. By default, the
        // conversation mode is audio.
        System.out.println("step 3: choosing conversation mode");
        startConversationRequestBuilder = startConversationRequestBuilder.conversationMode(ConversationMode.AUDIO);

        // Assign a unique identifier for the conversation.
        System.out.println("step 4: choosing a unique conversation identifier");
        startConversationRequestBuilder = startConversationRequestBuilder.sessionId(sessionId);

        // Start the initial request.
        StartConversationRequest startConversationRequest = startConversationRequestBuilder.build();

        // Create a stream of audio data to the Amazon Lex bot. The stream will start after the connection is established with the bot.
        EventsPublisher eventsPublisher = new EventsPublisher();

        // Create a class to handle responses from bot. After the server processes the user data you've streamed, the server responds
        // on another stream.
        BotResponseHandler botResponseHandler = new BotResponseHandler(eventsPublisher);

        // Start a connection and pass in the publisher that streams the audio and process the responses from the bot.
        System.out.println("step 5: starting the conversation ...");
        CompletableFuture<Void> conversation = lexRuntimeServiceClient.startConversation(
                startConversationRequest,
                eventsPublisher,
                botResponseHandler);

        // Wait until the conversation finishes. The conversation finishes if the dialog state reaches the "Closed" state.
        // The client stops the connection. If an exception occurs during the conversation, the
        // client sends a disconnection event.
        conversation.whenComplete((result, exception) -> {
            if (exception != null) {
                eventsPublisher.disconnect();
            }
        });

        // The conversation finishes when the dialog state is closed and last prompt has been played.
        while (!botResponseHandler.isConversationComplete()) {
            Thread.sleep(100);
        }

        // Randomly sleep for 100 milliseconds to prevent JVM from exiting.
        // You won't need this in your production code because your JVM is
        // likely to always run.
        // When the conversation finishes, the following code block stops publishing more data and informs the Amazon Lex bot that there is no more data to send.
        if (botResponseHandler.isConversationComplete()) {
            System.out.println("conversation is complete.");
            eventsPublisher.stop();
        }
    }
}
```

다음 코드는 AWS SDK for Java 를 사용하여 봇에 이벤트를 보내는 요청의 예입니다. 이 예시의 코드는 컴퓨터의 마이크를 사용하여 오디오 이벤트를 전송합니다.

```
package com.lex.streaming.sample;

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import software.amazon.awssdk.services.lexruntimev2.model.StartConversationRequestEventStream;

/**
 * You use the Events publisher to send events to the Amazon Lex bot. When you establish a connection, the bot uses the
 * subscribe() method and enables the events publisher starts sending events to
 * your computer. The bot uses the "request" method of the subscription to make more requests. For more information on the request method, see https://github.com/reactive-streams/reactive-streams-jvm. 
 */
public class EventsPublisher implements Publisher<StartConversationRequestEventStream> {

    private AudioEventsSubscription audioEventsSubscription;

    @Override
    public void subscribe(Subscriber<? super StartConversationRequestEventStream> subscriber) {
        if (audioEventsSubscription == null) {

            audioEventsSubscription = new AudioEventsSubscription(subscriber);
            subscriber.onSubscribe(audioEventsSubscription);

        } else {
            throw new IllegalStateException("received unexpected subscription request");
        }
    }

    public void disconnect() {
        if (audioEventsSubscription != null) {
            audioEventsSubscription.disconnect();
        }
    }

    public void stop() {
        if (audioEventsSubscription != null) {
            audioEventsSubscription.stop();
        }
    }

    public void playbackFinished() {
        if (audioEventsSubscription != null) {
            audioEventsSubscription.playbackFinished();
        }
    }
}
```

다음 코드는 AWS SDK for Java 를 사용하여 봇의 응답을 처리하는 요청의 예입니다. 이 예시의 코드는 Amazon Lex V2가 오디오 응답을 재생하도록 구성합니다.

```
package com.lex.streaming.sample;

import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.player.advanced.AdvancedPlayer;
import javazoom.jl.player.advanced.PlaybackEvent;
import javazoom.jl.player.advanced.PlaybackListener;
import software.amazon.awssdk.core.async.SdkPublisher;
import software.amazon.awssdk.services.lexruntimev2.model.AudioResponseEvent;
import software.amazon.awssdk.services.lexruntimev2.model.DialogActionType;
import software.amazon.awssdk.services.lexruntimev2.model.IntentResultEvent;
import software.amazon.awssdk.services.lexruntimev2.model.PlaybackInterruptionEvent;
import software.amazon.awssdk.services.lexruntimev2.model.StartConversationResponse;
import software.amazon.awssdk.services.lexruntimev2.model.StartConversationResponseEventStream;
import software.amazon.awssdk.services.lexruntimev2.model.StartConversationResponseHandler;
import software.amazon.awssdk.services.lexruntimev2.model.TextResponseEvent;
import software.amazon.awssdk.services.lexruntimev2.model.TranscriptEvent;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.concurrent.CompletableFuture;

/**
 * The following class is responsible for processing events sent from the Amazon Lex bot. The bot sends multiple audio events,
 * so the following code concatenates those audio events and uses a publicly available Java audio player to play out the message to
 * the user.
 */
public class BotResponseHandler implements StartConversationResponseHandler {

    private final EventsPublisher eventsPublisher;

    private boolean lastBotResponsePlayedBack;
    private boolean isDialogStateClosed;
    private AudioResponse audioResponse;


    public BotResponseHandler(EventsPublisher eventsPublisher) {
        this.eventsPublisher = eventsPublisher;
        this.lastBotResponsePlayedBack = false;// At the start, we have not played back last response from bot.
        this.isDialogStateClosed = false; // At the start, the dialog state is open.
    }

    @Override
    public void responseReceived(StartConversationResponse startConversationResponse) {
        System.out.println("successfully established the connection with server. request id:" + startConversationResponse.responseMetadata().requestId()); // would have 2XX, request id.
    }

    @Override
    public void onEventStream(SdkPublisher<StartConversationResponseEventStream> sdkPublisher) {

        sdkPublisher.subscribe(event -> {
            if (event instanceof PlaybackInterruptionEvent) {
                handle((PlaybackInterruptionEvent) event);
            } else if (event instanceof TranscriptEvent) {
                handle((TranscriptEvent) event);
            } else if (event instanceof IntentResultEvent) {
                handle((IntentResultEvent) event);
            } else if (event instanceof TextResponseEvent) {
                handle((TextResponseEvent) event);
            } else if (event instanceof AudioResponseEvent) {
                handle((AudioResponseEvent) event);
            }
        });
    }

    @Override
    public void exceptionOccurred(Throwable throwable) {
        System.err.println("got an exception:" + throwable);
    }

    @Override
    public void complete() {
        System.out.println("on complete");
    }

    private void handle(PlaybackInterruptionEvent event) {
        System.out.println("Got a PlaybackInterruptionEvent: " + event);
    }

    private void handle(TranscriptEvent event) {
        System.out.println("Got a TranscriptEvent: " + event);
    }


    private void handle(IntentResultEvent event) {
        System.out.println("Got an IntentResultEvent: " + event);
        isDialogStateClosed = DialogActionType.CLOSE.equals(event.sessionState().dialogAction().type());
    }

    private void handle(TextResponseEvent event) {
        System.out.println("Got an TextResponseEvent: " + event);
        event.messages().forEach(message -> {
            System.out.println("Message content type:" + message.contentType());
            System.out.println("Message content:" + message.content());
        });
    }

    private void handle(AudioResponseEvent event) {//Synthesize speech
        // System.out.println("Got a AudioResponseEvent: " + event);
        if (audioResponse == null) {
            audioResponse = new AudioResponse();
            //Start an audio player in a different thread.
            CompletableFuture.runAsync(() -> {
                try {
                    AdvancedPlayer audioPlayer = new AdvancedPlayer(audioResponse);

                    audioPlayer.setPlayBackListener(new PlaybackListener() {
                        @Override
                        public void playbackFinished(PlaybackEvent evt) {
                            super.playbackFinished(evt);

                            // Inform the Amazon Lex bot that the playback has finished.
                            eventsPublisher.playbackFinished();
                            if (isDialogStateClosed) {
                                lastBotResponsePlayedBack = true;
                            }
                        }
                    });
                    audioPlayer.play();
                } catch (JavaLayerException e) {
                    throw new RuntimeException("got an exception when using audio player", e);
                }
            });
        }

        if (event.audioChunk() != null) {
            audioResponse.write(event.audioChunk().asByteArray());
        } else {
            // The audio audio prompt has ended when the audio response has no
            // audio bytes.
            try {
                audioResponse.close();
                audioResponse = null;  // Prepare for the next audio prompt.
            } catch (IOException e) {
                throw new UncheckedIOException("got an exception when closing the audio response", e);
            }
        }
    }

    // The conversation with the Amazon Lex bot is complete when the bot marks the Dialog as DialogActionType.CLOSE
    // and any prompt playback is finished. For more information, see
    // https://docs.aws.amazon.com/lexv2/latest/dg/API_runtime_DialogAction.html.
    public boolean isConversationComplete() {
        return isDialogStateClosed && lastBotResponsePlayedBack;
    }

}
```

봇이 오디오와 함께 입력 이벤트에 응답하도록 구성하려면 먼저 Amazon Lex V2의 오디오 이벤트를 구독한 다음 사용자의 입력 이벤트에 오디오 응답을 제공하도록 봇을 구성해야 합니다.

다음 코드는 Amazon Lex V2에서 오디오 이벤트를 구독하는 AWS SDK for Java 예제입니다.

```
package com.lex.streaming.sample;

import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.lexruntimev2.model.AudioInputEvent;
import software.amazon.awssdk.services.lexruntimev2.model.ConfigurationEvent;
import software.amazon.awssdk.services.lexruntimev2.model.DisconnectionEvent;
import software.amazon.awssdk.services.lexruntimev2.model.PlaybackCompletionEvent;
import software.amazon.awssdk.services.lexruntimev2.model.StartConversationRequestEventStream;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.TargetDataLine;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;

public class AudioEventsSubscription implements Subscription {
    private static final AudioFormat MIC_FORMAT = new AudioFormat(8000, 16, 1, true, false);
    private static final String AUDIO_CONTENT_TYPE = "audio/lpcm; sample-rate=8000; sample-size-bits=16; channel-count=1; is-big-endian=false";
    //private static final String RESPONSE_TYPE = "audio/pcm; sample-rate=8000";
    private static final String RESPONSE_TYPE = "audio/mpeg";
    private static final int BYTES_IN_AUDIO_CHUNK = 320;
    private static final AtomicLong eventIdGenerator = new AtomicLong(0);

    private final AudioInputStream audioInputStream;
    private final Subscriber<? super StartConversationRequestEventStream> subscriber;
    private final EventWriter eventWriter;
    private CompletableFuture eventWriterFuture;


    public AudioEventsSubscription(Subscriber<? super StartConversationRequestEventStream> subscriber) {
        this.audioInputStream = getMicStream();
        this.subscriber = subscriber;
        this.eventWriter = new EventWriter(subscriber, audioInputStream);
        configureConversation();
    }

    private AudioInputStream getMicStream() {
        try {
            DataLine.Info dataLineInfo = new DataLine.Info(TargetDataLine.class, MIC_FORMAT);
            TargetDataLine targetDataLine = (TargetDataLine) AudioSystem.getLine(dataLineInfo);

            targetDataLine.open(MIC_FORMAT);
            targetDataLine.start();

            return new AudioInputStream(targetDataLine);
        } catch (LineUnavailableException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void request(long demand) {
        // If a thread to write events has not been started, start it.
        if (eventWriterFuture == null) {
            eventWriterFuture = CompletableFuture.runAsync(eventWriter);
        }
        eventWriter.addDemand(demand);
    }

    @Override
    public void cancel() {
        subscriber.onError(new RuntimeException("stream was cancelled"));
        try {
            audioInputStream.close();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void configureConversation() {
        String eventId = "ConfigurationEvent-" + String.valueOf(eventIdGenerator.incrementAndGet());

        ConfigurationEvent configurationEvent = StartConversationRequestEventStream
                .configurationEventBuilder()
                .eventId(eventId)
                .clientTimestampMillis(System.currentTimeMillis())
                .responseContentType(RESPONSE_TYPE)
                .build();

        System.out.println("writing config event");
        eventWriter.writeConfigurationEvent(configurationEvent);
    }

    public void disconnect() {

        String eventId = "DisconnectionEvent-" + String.valueOf(eventIdGenerator.incrementAndGet());

        DisconnectionEvent disconnectionEvent = StartConversationRequestEventStream
                .disconnectionEventBuilder()
                .eventId(eventId)
                .clientTimestampMillis(System.currentTimeMillis())
                .build();

        eventWriter.writeDisconnectEvent(disconnectionEvent);

        try {
            audioInputStream.close();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }

    }
    //Notify the subscriber that we've finished.
    public void stop() {
        subscriber.onComplete();
    }

    public void playbackFinished() {
        String eventId = "PlaybackCompletion-" + String.valueOf(eventIdGenerator.incrementAndGet());

        PlaybackCompletionEvent playbackCompletionEvent = StartConversationRequestEventStream
                .playbackCompletionEventBuilder()
                .eventId(eventId)
                .clientTimestampMillis(System.currentTimeMillis())
                .build();

        eventWriter.writePlaybackFinishedEvent(playbackCompletionEvent);
    }

    private static class EventWriter implements Runnable {
        private final BlockingQueue<StartConversationRequestEventStream> eventQueue;
        private final AudioInputStream audioInputStream;
        private final AtomicLong demand;
        private final Subscriber subscriber;

        private boolean conversationConfigured;

        public EventWriter(Subscriber subscriber, AudioInputStream audioInputStream) {
            this.eventQueue = new LinkedBlockingQueue<>();

            this.demand = new AtomicLong(0);
            this.subscriber = subscriber;
            this.audioInputStream = audioInputStream;
        }

        public void writeConfigurationEvent(ConfigurationEvent configurationEvent) {
            eventQueue.add(configurationEvent);
        }

        public void writeDisconnectEvent(DisconnectionEvent disconnectionEvent) {
            eventQueue.add(disconnectionEvent);
        }

        public void writePlaybackFinishedEvent(PlaybackCompletionEvent playbackCompletionEvent) {
            eventQueue.add(playbackCompletionEvent);
        }

        void addDemand(long l) {
            this.demand.addAndGet(l);
        }

        @Override
        public void run() {
            try {

                while (true) {
                    long currentDemand = demand.get();

                    if (currentDemand > 0) {
                        // Try to read from queue of events.
                        // If nothing is in queue at this point, read the audio events directly from audio stream.
                        for (long i = 0; i < currentDemand; i++) {

                            if (eventQueue.peek() != null) {
                                subscriber.onNext(eventQueue.take());
                                demand.decrementAndGet();
                            } else {
                                writeAudioEvent();
                            }
                        }
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException("interrupted when reading data to be sent to server");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        private void writeAudioEvent() {
            byte[] bytes = new byte[BYTES_IN_AUDIO_CHUNK];

            int numBytesRead = 0;
            try {
                numBytesRead = audioInputStream.read(bytes);
                if (numBytesRead != -1) {
                    byte[] byteArrayCopy = Arrays.copyOf(bytes, numBytesRead);

                    String eventId = "AudioEvent-" + String.valueOf(eventIdGenerator.incrementAndGet());

                    AudioInputEvent audioInputEvent = StartConversationRequestEventStream
                            .audioInputEventBuilder()
                            .audioChunk(SdkBytes.fromByteBuffer(ByteBuffer.wrap(byteArrayCopy)))
                            .contentType(AUDIO_CONTENT_TYPE)
                            .clientTimestampMillis(System.currentTimeMillis())
                            .eventId(eventId).build();

                    //System.out.println("sending audio event:" + audioInputEvent);
                    subscriber.onNext(audioInputEvent);
                    demand.decrementAndGet();
                    //System.out.println("sent audio event:" + audioInputEvent);
                } else {
                    subscriber.onComplete();
                    System.out.println("audio stream has ended");
                }

            } catch (IOException e) {
                System.out.println("got an exception when reading from audio stream");
                System.err.println(e);
                subscriber.onError(e);
            }
        }
    }
}
```

다음 AWS SDK for Java 예제에서는 입력 이벤트에 오디오 응답을 제공하도록 Amazon Lex V2 봇을 구성합니다.

```
package com.lex.streaming.sample;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Optional;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

public class AudioResponse extends InputStream{

    // Used to convert byte, which is signed in Java, to positive integer (unsigned)
    private static final int UNSIGNED_BYTE_MASK = 0xFF;
    private static final long POLL_INTERVAL_MS = 10;

    private final LinkedBlockingQueue<Integer> byteQueue = new LinkedBlockingQueue<>();

    private volatile boolean closed;

    @Override
    public int read() throws IOException {
        try {
            Optional<Integer> maybeInt;
            while (true) {
                maybeInt = Optional.ofNullable(this.byteQueue.poll(POLL_INTERVAL_MS, TimeUnit.MILLISECONDS));

                // If we get an integer from the queue, return it.
                if (maybeInt.isPresent()) {
                    return maybeInt.get();
                }

                // If the stream is closed and there is nothing queued up, return -1.
                if (this.closed) {
                    return -1;
                }
            }
        } catch (InterruptedException e) {
            throw new IOException(e);
        }
    }

    /**
     * Writes data into the stream to be offered on future read() calls.
     */
    public void write(byte[] byteArray) {
        // Don't write into the stream if it is already closed.
        if (this.closed) {
            throw new UncheckedIOException(new IOException("Stream already closed when attempting to write into it."));
        }

        for (byte b : byteArray) {
            this.byteQueue.add(b & UNSIGNED_BYTE_MASK);
        }
    }

    @Override
    public void close() throws IOException {
        this.closed = true;
        super.close();
    }
}
```