Power AppsとPower AutomateでAzure AIのテキスト読み上げアバターを使ってみよう！ 

# はじめに 
パブリック プレビュー段階の機能ですが、[Azure AI サービスの Speech Studioにて、テキスト読み上げアバターの作成が実現できるようになりました。](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/text-to-speech-avatar/what-is-text-to-speech-avatar ) 

:::note info
この記事は2024年5月12日の内容です。 
:::

[Speech Studio](https://speech.microsoft.com/portal)で早速使ってみると、本当に生きているようなアバターと会話を試すことができます！ 

https://speech.microsoft.com/portal

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/e54cdfd3-e475-d37c-1bad-000f833615a6.png)

**非常に面白いサービスで心が踊ります。**

https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/text-to-speech-avatar/what-is-text-to-speech-avatar 

:::note warn
 
テキスト読み上げアバターは、パブリック プレビュー段階です。
このプレビュー版はサービス レベル アグリーメントなしで提供されています。 
あくまで趣味で使うという認識で使ってみてください。 
:::

**今回は`Azure AI`でリソースをデプロイし、`Power Apps`を画面に、`Power Automate`から、[テキスト読み上げアバター動画をバッチ合成](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/text-to-speech-avatar/batch-synthesis-avatar)、対話を実現するアプリを作成してみました！**

https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/text-to-speech-avatar/batch-synthesis-avatar

# 作成したもの

このようなPower Appsを作成しています。

![スクリーンショット 2024-05-12 104313.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/375e5dde-c3a1-8f2b-e55e-d6edb6d9c752.png)

**■ アプリケーションの流れ**

1. `Power Apps`のテキスト入力からテキストを入力 
1. ボタンコントロールからテキストを`Power Automate`に渡す 
1. Azure OpenAIのgpt3.5に結果を渡し、アバターが読み上げるテキストを作成する 
1. テキスト読み上げアバターの動画をバッチ合成する 
1. 結果をPower Appsに返し、ビデオコントロールで投影する 

## 1. Azureでリソースの作成
作成するリソースは下記の通りです。
 - **Azure OpenAI**
     - リージョン: `Japan East`
     - モデル名: `gpt-35-turbo`
     - モデル バージョン: `0613`
 - **音声サービス**
     - リージョン: `West US 2`

:::note ifo
テキスト読み上げアバター機能を使用できるリージョンは限られています。
 - 米国西部 2　*West US 2*
 - 西ヨーロッパ *West Europe*
 - 東南アジア *Southeast Asia*
:::

https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/text-to-speech-avatar/what-is-text-to-speech-avatar

## 2. Power Automate
*Power Automate*は`HTTP要求`を、作成したリソースに渡し、結果を受け取り、*Power Apps*に返します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/f158c714-157c-8d29-4cd5-4a151728ca62.png)

1. `PowerApps (V2) トリガー`で下記の値を定義
    1. `テキスト読み上げアバター`のために、`Job ID（SynthesisId）`
    1. `Open AI`のために、`Prompt`
1. `作成`アクションで下記を設定
    1. *Azure OpenAI*の`URI`と`エンドポイント`、システム プロンプト
    1. *Speech Service*の`URI`と`エンドポイント`
1. `HTTP`で`Azure OpenAI`に要求を送信し、テキストを生成
1. (3)で作成したテキストを含めた[音声合成マークアップ言語 (SSML)](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/speech-synthesis-markup-structure)を定義
1. [テキスト読み上げアバターのバッチ合成要求を作成](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/text-to-speech-avatar/batch-synthesis-avatar#create-a-batch-synthesis-request)
1. 変数でステータス（進捗状況 確認用）を宣言
1. 繰り返し処理 `Do until`:`status`が`Succeeded`になるまで
    1. [テキスト読み上げアバターのバッチ合成を取得](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/text-to-speech-avatar/batch-synthesis-avatar#get-batch-synthesis)
1. [(7)で取得した値から`output.result`を抽出し、HTTP要求を送信し、バッチ合成の結果ファイルを取得する](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/text-to-speech-avatar/batch-synthesis-avatar#get-batch-synthesis-results-file)
1. *Power Apps*に、**Azure OpenAIによる生成テキスト**と、**バッチ合成の結果ファイルの`$content`** を返す


### 3. `HTTP`で`Azure OpenAI`に要求を送信し、テキストを生成
チャットの履歴を無視した方法です。

```plaintext:URI
https://{AZURE_OPENAI_ENDPOINT}/openai/deployments/{YOUR-DEPLOYMENT_NAME_HERE}/chat/completions?api-version=2024-02-15-preview 
```

`Method` : `POST`

`Header` :

| キー | 値 |
|:-:|:-:|
|api-key|AZURE OPENAIのAPIキー |
|Content-Type|application/json|

```json:Header
{
  "api-key": "@{outputs('OpenAIAPIKey')}",
  "Content-Type": "application/json"
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/cc33b01e-2fb5-152e-2c30-98ce63e584ee.png)

戻り値は下記のようになります。

```json:本文
{
  "choices": [
    {
      "content_filter_results": {
        "hate": {
          "filtered": false,
          "severity": "safe"
        },
        "self_harm": {
          "filtered": false,
          "severity": "safe"
        },
        "sexual": {
          "filtered": false,
          "severity": "safe"
        },
        "violence": {
          "filtered": false,
          "severity": "safe"
        }
      },
      "finish_reason": "stop",
      "index": 0,
      "logprobs": null,
      "message": {
        "content": "OPEN AIサービスで生成されたテキスト",
        "role": "assistant"
      }
    }
  ],
  "created": 11111111,
  "id": "chatcmpl-hogehoge",
  "model": "gpt-35-turbo",
  "object": "chat.completion",
  "prompt_filter_results": [
    {
      "prompt_index": 0,
      "content_filter_results": {
        "hate": {
          "filtered": false,
          "severity": "safe"
        },
        "self_harm": {
          "filtered": false,
          "severity": "safe"
        },
        "sexual": {
          "filtered": false,
          "severity": "safe"
        },
        "violence": {
          "filtered": false,
          "severity": "safe"
        }
      }
    }
  ],
  "system_fingerprint": null,
  "usage": {
    "completion_tokens": 49,
    "prompt_tokens": 60,
    "total_tokens": 109
  }
}
```

https://learn.microsoft.com/ja-jp/azure/ai-services/openai/quickstart?tabs=command-line%2Cpython-new&pivots=rest-api

生成されたテキストを直接取得するには、下記を指定します。

```plaintext:OpenAIで生成されたテキストを取得
body('OpenAIのアクション名')?['choices'][0]?['message']?['content']
```

### 4. (3)で作成したテキストを含めた[音声合成マークアップ言語 (SSML)](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/speech-synthesis-markup-structure)を定義

`SSML`は、Speech serviceで使用する言語です。

https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/speech-synthesis-markup-structure

読み上げる`言語`と`モデル`を選択します。
`日本語`で、モデル`ja-JP-MayuNeural`を使用する場合、下記の例になります。

```xml:SSML
<speak version='1.0' xml:lang='ja-JP'>
    <voice name='ja-JP-MayuNeural'>
        読み上げるテキスト
    </voice>
</speak>
```

:::note info
今回は、`作成`アクションで、SSMLの定義を行いました。
モデルによる声質の違いは、`Speech Studio`で試すことをお勧めします。
:::

### 5. [テキスト読み上げアバターのバッチ合成要求を作成](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/text-to-speech-avatar/batch-synthesis-avatar#create-a-batch-synthesis-request)

まず**新しいバッチ合成ジョブを作成します。**
動画を作成するために、10秒ほど時間がかかる認識です。

```plaintext:URI
https://{リージョン}.api.cognitive.microsoft.com/avatar/batchsyntheses/{Job ID（SynthesisId）}?api-version=2024-04-15-preview
```

`Method` : `PUT`

`Header` :

| キー | 値 |
|:-:|:-:|
|Ocp-Apim-Subscription-Key|音声サービスのAPIキー|
|Content-Type|application/json|

```json:Header
{
  "Ocp-Apim-Subscription-Key": "@{outputs('AvatarAPIKEY')}",
  "Content-Type": "application/json"
}
```

```json:Body
{
  "inputKind": "SSML",
  "inputs": [
    {
      "content": "@{outputs('SSML')}"
    }
  ],
  "avatarConfig": {
    "talkingAvatarCharacter": "lisa",
    "talkingAvatarStyle": "graceful-sitting"
  }
}
```

:::note info
Bodyの中でアバターの`モデル`や`振る舞い`を定義しています。
SSMLを含めて、`Body`に書くことが手間であったことから、SSMLは`作成`アクションで定義しています。

ジョブIDが、すでに存在する場合、このリクエストは通りません。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/2f36f3f0-6e3a-a03d-81ea-58ecc6f53632.png)

```json:戻り値のSchema
{
    "type": "object",
    "properties": {
        "id": {
            "type": "string"
        },
        "Status": {
            "type": "string"
        },
        "createdDateTime": {
            "type": "string"
        },
        "lastActionDateTime": {
            "type": "string"
        },
        "inputKind": {
            "type": "string"
        },
        "customVoices": {
            "type": "object",
            "properties": {}
        },
        "properties": {
            "type": "object",
            "properties": {
                "timeToLiveInHours": {
                    "type": "integer"
                }
            }
        },
        "avatarConfig": {
            "type": "object",
            "properties": {
                "talkingAvatarCharacter": {
                    "type": "string"
                },
                "talkingAvatarStyle": {
                    "type": "string"
                },
                "videoFormat": {
                    "type": "string"
                },
                "videoCodec": {
                    "type": "string"
                },
                "subtitleType": {
                    "type": "string"
                },
                "bitrateKbps": {
                    "type": "integer"
                },
                "customized": {
                    "type": "boolean"
                }
            }
        }
    }
}
```

### 7. 繰り返し処理 `Do until`:`status`が`Succeeded`になるまで[テキスト読み上げアバターのバッチ合成を取得](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/text-to-speech-avatar/batch-synthesis-avatar#get-batch-synthesis)

バッチ合成には、時間を要するため、HTTP要求で進捗状況を確認し、完了（）まで待機します。

```plaintext:URI
https://{リージョン}.api.cognitive.microsoft.com/avatar/batchsyntheses/{Job ID（SynthesisId）}?api-version=2024-04-15-preview
```

`Method` : `GET`

`Header` :

| キー | 値 |
|:-:|:-:|
|Ocp-Apim-Subscription-Key|音声サービスのAPIキー|

```json:Header
{
  "Ocp-Apim-Subscription-Key": "@{outputs('AvatarAPIKEY')}",
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/01651aa7-fdc3-f596-0cde-c360855ed194.png)

ステータスのチェックは、変数で行っています。
具体的には、`GET`要求の戻り値の`status`が`Succeeded`か、[equals関数](https://learn.microsoft.com/ja-jp/azure/logic-apps/workflow-definition-language-functions-reference#equals)で確認します。

```Update Status
equals(body('GET_Avatar')?['status'],'Succeeded')
```

```json:戻り値のSchema
{
    "type": "object",
    "properties": {
        "id": {
            "type": "string"
        },
        "Status": {
            "type": "string"
        },
        "createdDateTime": {
            "type": "string"
        },
        "lastActionDateTime": {
            "type": "string"
        },
        "inputKind": {
            "type": "string"
        },
        "customVoices": {
            "type": "object",
            "properties": {}
        },
        "properties": {
            "type": "object",
            "properties": {
                "timeToLiveInHours": {
                    "type": "integer"
                },
                "sizeInBytes": {
                    "type": "integer"
                },
                "durationInMilliseconds": {
                    "type": "integer"
                },
                "succeededCount": {
                    "type": "integer"
                },
                "failedCount": {
                    "type": "integer"
                },
                "billingDetails": {
                    "type": "object",
                    "properties": {
                        "neuralCharacters": {
                            "type": "integer"
                        },
                        "talkingAvatarDurationSeconds": {
                            "type": "integer"
                        }
                    }
                }
            }
        },
        "avatarConfig": {
            "type": "object",
            "properties": {
                "talkingAvatarCharacter": {
                    "type": "string"
                },
                "talkingAvatarStyle": {
                    "type": "string"
                },
                "videoFormat": {
                    "type": "string"
                },
                "videoCodec": {
                    "type": "string"
                },
                "subtitleType": {
                    "type": "string"
                },
                "bitrateKbps": {
                    "type": "integer"
                },
                "customized": {
                    "type": "boolean"
                }
            }
        },
        "outputs": {
            "type": "object",
            "properties": {
                "result": {
                    "type": "string"
                },
                "summary": {
                    "type": "string"
                }
            }
        }
    }
}
```

https://learn.microsoft.com/ja-jp/azure/logic-apps/workflow-definition-language-functions-reference#equals

### 8. [(7)で取得した値から`output.result`を抽出し、HTTP要求を送信し、バッチ合成の結果ファイルを取得する](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/text-to-speech-avatar/batch-synthesis-avatar#get-batch-synthesis-results-file)
`GET`要求の戻り値の`status`が`Succeeded`になった場合、`output`の`result`属性から、結果ファイルのURLを取得できます。

`output`の`result`属性の値に、`GET`要求を送信することで、結果ファイルのコンテンツを取得することができます。

```plaintext:URI
@{body('Get_Avatar_Result')?['outputs']?['result']}
```

`Method` : `GET`

`Header` :

| キー | 値 |
|:-:|:-:|
|Ocp-Apim-Subscription-Key|音声サービスのAPIキー|

```json:Header
{
  "Ocp-Apim-Subscription-Key": "@{outputs('AvatarAPIKEY')}",
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/47af4bf3-3084-7e4a-fe7d-dd9d3656fc73.png)

ここまでで、*Power Automate*の準備は完了です！

## 3. Power Apps

ほとんど`テンプレート`です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/ca397d8a-01e9-972d-e168-a778ae19bcb3.png)

`ギャラリー`コントロールに`チャットの履歴`、`ボタン`コントロールで機能を集約しています。
`ボタン`コントロールの`OnSelect`に下記の式を設定します。

```plaintext:OnSelect
/*
* Power AutomateにJob IDとプロンプトを送信
*/
With(
    {
        Whisper:
            TextToSpeechAvatar.Run(
                IdInput.Value,
                PromptInput.Value
            )
    },
    UpdateContext(
        {
            Result:Whisper.result,
            Answer:Whisper.content,
            IDNumber: IDNumber + 1
        }
    )
);
/*
* colChatというコレクションに、文字列を設定
*/
Collect(colChat,Table(
    {Speaker: "Me", content: PromptInput.Value, role: "user"},
    {Speaker: "Avatar", content: Answer, role: "assistant"}
    )
);
/*
* colChatというコレクションに、文字列を設定
*/
Collect(colJob,{id: IdInput.Value});
/*
* Prompt入力用のテキスト入力コントロールをリセット
*/
Reset(PromptInput);
```

`ビデオ`コントロールの`Media`属性が、バッチ合成された動画を再生する部品です。
*Power Automate*からの戻り値は`base64`で取得されるため、`Media`属性を下記の通り設定します。

```plaintext:Media
"data:audio/wav;base64," & Result
```

これで機能はそろいました！

# 実践
実際に使ってみると、下記のように動きます！
近未来的で、超興奮しました！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Whisperを省くと、安定する！<br>Open AI GPT 3.5で、Power Apps &amp; Power Automate、Azure AIによる対話アプリ！テキスト読み上げアバターがしゃべる！！娘をほめてもらいました！<a href="https://twitter.com/hashtag/PowerApps?src=hash&amp;ref_src=twsrc%5Etfw">#PowerApps</a> <a href="https://twitter.com/hashtag/PowerAutomate?src=hash&amp;ref_src=twsrc%5Etfw">#PowerAutomate</a> <a href="https://twitter.com/hashtag/Azure?src=hash&amp;ref_src=twsrc%5Etfw">#Azure</a> <a href="https://twitter.com/hashtag/OpenAI?src=hash&amp;ref_src=twsrc%5Etfw">#OpenAI</a><a href="https://t.co/2Nq8ibOThR">https://t.co/2Nq8ibOThR</a> <a href="https://t.co/ASon06ROON">pic.twitter.com/ASon06ROON</a></p>&mdash; 出戻りガツオ🐟 Microsoft MVP (@DemodoriGatsuo) <a href="https://twitter.com/DemodoriGatsuo/status/1789470635049316553?ref_src=twsrc%5Etfw">May 12, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

`Whisper`を使って、マイクを使った対話も作成中ですが・・・、課題ありです。

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">プレビュー機能のライブアバターチャットを使って、Power Apps &amp; Power Automate、Azure AIによる対話アプリが、それっぽくはなった！！<br>Whisperでコケる・・・<br><br>ノンエンジニアでもこれまで作れる時代！楽しすぎる！<a href="https://t.co/DGXeOFA4Ul">https://t.co/DGXeOFA4Ul</a><a href="https://twitter.com/hashtag/PowerApps?src=hash&amp;ref_src=twsrc%5Etfw">#PowerApps</a> <a href="https://twitter.com/hashtag/PowerAutomate?src=hash&amp;ref_src=twsrc%5Etfw">#PowerAutomate</a> <a href="https://twitter.com/hashtag/Azure?src=hash&amp;ref_src=twsrc%5Etfw">#Azure</a> <a href="https://twitter.com/hashtag/OpenAI?src=hash&amp;ref_src=twsrc%5Etfw">#OpenAI</a> <a href="https://t.co/sAJyc3xjNV">pic.twitter.com/sAJyc3xjNV</a></p>&mdash; 出戻りガツオ🐟 Microsoft MVP (@DemodoriGatsuo) <a href="https://twitter.com/DemodoriGatsuo/status/1789294291418038357?ref_src=twsrc%5Etfw">May 11, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

非常に楽しい機能です！
是非試してみてください！！！