わざわざWhisperとGPT-4とFunction callingでエアコンを消す

# はじめに
触ろう触ろうと思ってWhisperを放置し続けていたのですがようやく触りました。
使い方を知るのが目的の手順なのでタイトルの「エアコンを消す」というゴールを達成するのに不要な内容も含まれます。
また、この手順では「リモートでエアコンを操作できる」APIがあるていで進めます。エアコンじゃなくてもなんでもいいです。

内容は、`①Whisperで音声をテキスト化`→`②GPT-4でテキストを整える`→`③Function callingで実行する処理を決定`という流れになります。

`②GPT-4でテキストを整える`に関しては手順としては不要ですが、Whisperでテキスト化した内容を加工（具体的には文調の変更、補足説明の追加、カナ変換など）したいことは結構あるのではと思い、手順を入れました。

# 手順
## 事前準備
1. OpenAIにてAPIキーを発行しておく

https://platform.openai.com/api-keys

## 1. Whisperで音声をテキスト化
今回音声はこちらを使わせていただきます。　
[「エアコンを消します」](https://amitaro.conohawing.com/download/voice/606_iot/airconwokeshimasu_01.wav) [あみたろの声素材工房](https://amitaro.net)

内容は一言「エアコンを消します」という音声が入っているシンプルなものです。

こちらがWhisper APIを使う際のPyhonのサンプルプログラムです。ここではやっていることの理解しやすさと他の言語で使う際の汎用性のためOpenAIのモジュールを使わず書いてみています。
```python
import requests

def create_transcription():
    whisper_endpoint = "https://api.openai.com/v1/audio/transcriptions"
    headers = {
        "Authorization": "Bearer <ご自身のOpenAI API Key>"
    }
    files = {
        "file": ("airconwokeshimasu_01.wav", open("airconwokeshimasu_01.wav", "rb")),
        "model": (None, "whisper-1")
    }
    response = requests.post(whisper_endpoint, headers=headers, files=files)
    return response.json()

if __name__ == '__main__':
    transcription = create_transcription()
    print(transcription)

```
**実行結果**
```
{'text': 'エアコンを消します。'}
```
短いですがちゃんとテキストにできてます。

## 2. GPT-4で整える
「エアコンを消します。」だけなので整えるも何もないのですが、加工の練習ということでやってみます。GPT-4でも3.5でもいいです。
先ほどのサンプルに加工を担う`transform_transcription()`を追加してみました。

日本語のニュアンスをより具体的な指示に変換するということはありそうだなと思い、今回は例として`テキストに動詞が入っていた場合より具体的な指示に変換してください`というプロンプトにしてみました。ここをお好みの加工内容に変えてみてください。

```python
import requests

def create_transcription():
    -- 省略 --

def transform_transcription(transcription):
    chat_endpoint = "https://api.openai.com/v1/chat/completions"
    headers = {
        "Authorization": "Bearer <ご自身のOpenAI API Key>"
    }
    messages = [
        {
            "role": "system",
            "content": "与えられたテキストを指示通りに加工してください。"
        },
        {
            "role": "user",
            "content": "テキストに動詞が入っていた場合、より具体的な指示に変換してください: " + transcription["text"]
        }
    ]
    body = {
        "model": "gpt-4",
        "messages": messages
    }
    response = requests.post(chat_endpoint, headers=headers, json=body)
    return response.json()

if __name__ == '__main__':
    transcription = create_transcription()
    transformed_transcription = transform_transcription(transcription)
    print(transformed_transcription["choices"][0]["message"]["content"])

```

**実行結果**
```
エアコンの電源を切ってください。
```
`エアコンを消します。`が`エアコンの電源を切ってください。`に変換されました。シンプルすぎる例なので地味ですが、ちょっと曖昧な日本語を、Function callingしやすくしたいとき、別途プロンプトに組み込む時、英語に翻訳する時などに使えるかもと思いました。

## 3. 処理内容を決める（Function calling）
それでは`エアコンの電源を切ってください。`という指示まで取得できたのでこれを実際の処理に繋げるため、Function callingを行う処理を追記してみます。

```python
import requests

# 重複する内容を出しました。
OPENAI_API_KEY = "your-openai-apikey" # ご自身のAPIキーを入れてください。
WHISPER_ENDPOINT = "https://api.openai.com/v1/audio/transcriptions"
CHAT_ENDPOINT = "https://api.openai.com/v1/chat/completions"

def create_transcription():
    -- 省略 -- 

def transform_transcription(transcription):
    -- 省略 --

### 選択肢となる関数 ###
def operate_air_conditioner():
    ### ↓　架空の処理です。イメージです。
    # sample_endpoint = "https://sample-smart-air-conditionar.com"
    # headers = {
    #     "Authorization": "xxxxyyyyzzz"
    # }
    # body = {
    #     "operation": "turn-off"
    # }
    # response = requests.post(sample_endpoint, headers=headers, json=body)
    # return response.json()
    print("success: Air conditioner is turned off.")

def operate_floor_heating():
    ###　こちらは処理イメージは割愛します。
    print("success: floor N, floor heating is turned off.")

### 処理を選択する関数 ###
def select_function(instruction):
    headers = {
        "Authorization": "Bearer " + OPENAI_API_KEY
    }
    messages = [
        {
            "role": "system",
            "content": "あなたはスマート家電のオペレーターです。様々な家電に関する操作指示を受けます。"
        },
        {
            "role": "user",
            "content": instruction
        }
    ]
    functions = [
        {
            "name": "operate_air_conditioner",
            "description": "エアコンの操作を行う"
        },
        {
            "name": "operate_floor_heating",
            "description": "床暖房の操作を行う"
        }    
    ]
    function_call = "auto"
    body = {
        "model": "gpt-4",
        "messages": messages,
        "functions": functions,
        "function_call": function_call
    }
    response = requests.post(CHAT_ENDPOINT, headers=headers, json=body)
    return response.json()

if __name__ == '__main__':
    transcription = create_transcription()
    transformed_transcription = transform_transcription(transcription)
    instruction = transformed_transcription["choices"][0]["message"]["content"]
    selected_function = select_function(instruction)
    print(selected_function)
```
`select_function()`で入力されたプロンプトに対して与えられた選択肢である`operate_air_conditioner`と`operate_floor_heating`から処理を選ぶということです。（指示がこの2つで対応できない内容の場合はその旨を返す）

ちなみにFunction calling自体の解説は別記事がありますので興味がある方は是非。
今回のサンプルは簡略化のため引数がない関数を選択肢にしてますが、普通はあると思います。こちらにはその辺り書いてあります。

https://qiita.com/ysv/items/cbdbb994dac790e26489

**実行結果**
```json
{
	'id': 'chatcmpl-8iXvbcXMhxxxxxxxx',
	 'object': 'chat.completion',
	 'created': 1705627543,
	 'model': 'gpt-4-0613',
	 'choices': [
		{
			'index': 0,
			'message': {
				'role': 'assistant',
				'content': None,
				'function_call': {
					'name': 'operate_air_conditioner',
					'arguments': '{\n  "operation": "turn_off"\n}'
				}
			},
			'logprobs': None,
			'finish_reason': 'function_call'
		}
	],
	'usage': {
		'prompt_tokens': 122,
		'completion_tokens': 19,
		'total_tokens': 141
	},
	system_fingerprint': None
}
```
`choices`のバリューを確認すると、
```json
'function_call': {
    'name': 'operate_air_conditioner',
    ...
}
```
となっており、`operate_air_conditioner`が選択されていることがわかります。
あとは実行すればいいですね。
```python
function = selected_function["choices"][0]["message"]["function_call"]["name"]
if function == "operate_air_conditioner":
    operate_air_conditioner()
```

↑これを追記した省略なしのサンプルです。
```python
import requests

OPENAI_API_KEY = "sk-xxxxxxxxxxxxxxxxxxxxx"
WHISPER_ENDPOINT = "https://api.openai.com/v1/audio/transcriptions"
CHAT_ENDPOINT = "https://api.openai.com/v1/chat/completions"

def create_transcription():
    headers = {
        "Authorization": "Bearer " + OPENAI_API_KEY
    }
    files = {
        "file": ("airconwokeshimasu_01.wav", open("airconwokeshimasu_01.wav", "rb")),
        "model": (None, "whisper-1")
    }
    response = requests.post(WHISPER_ENDPOINT, headers=headers, files=files)
    return response.json()

def transform_transcription(transcription):
    headers = {
        "Authorization": "Bearer " + OPENAI_API_KEY
    }
    messages = [
        {
            "role": "system",
            "content": "与えられたテキストを指示通りに加工してください。"
        },
        {
            "role": "user",
            "content": "テキストに動詞が入っていた場合、より具体的な指示に変換してください: " + transcription["text"]
        }
    ]
    body = {
        "model": "gpt-4",
        "messages": messages
    }
    response = requests.post(CHAT_ENDPOINT, headers=headers, json=body)
    return response.json()

### 選択肢となる関数 ###
def operate_air_conditioner():
    ### ↓　架空の処理です。イメージです。
    # sample_endpoint = "https://sample-smart-air-conditionar.com"
    # headers = {
    #     "Authorization": "xxxxyyyyzzz"
    # }
    # body = {
    #     "operation": "turn-off"
    # }
    # response = requests.post(sample_endpoint, headers=headers, json=body)
    # return response.json()
    print("success: Air conditioner is turned off.")

def operate_floor_heating():
    ###　こちらは処理イメージは割愛します。
    print("success: floor N, floor heating is turned off.")

### 処理を選択する関数 ###
def select_function(instruction):
    headers = {
        "Authorization": "Bearer " + OPENAI_API_KEY
    }
    messages = [
        {
            "role": "system",
            "content": "あなたはスマート家電のオペレーターです。様々な家電に関する操作指示を受けます。"
        },
        {
            "role": "user",
            "content": instruction
        }
    ]
    functions = [
        {
            "name": "operate_air_conditioner",
            "description": "エアコンの操作を行う"
        },
        {
            "name": "operate_floor_heating",
            "description": "床暖房の操作を行う"
        }    
    ]
    function_call = "auto"
    body = {
        "model": "gpt-4",
        "messages": messages,
        "functions": functions,
        "function_call": function_call
    }
    response = requests.post(CHAT_ENDPOINT, headers=headers, json=body)
    return response.json()

if __name__ == '__main__':
    # 音声をテキスト化
    transcription = create_transcription()
    # テキストを加工
    transformed_transcription = transform_transcription(transcription)
    instruction = transformed_transcription["choices"][0]["message"]["content"]
    # 処理を選択
    selected_function = select_function(instruction)
    # 処理実行
    function = selected_function["choices"][0]["message"]["function_call"]["name"]
    if function == "operate_air_conditioner":
        operate_air_conditioner()
```
**実行結果**
```
success: Air conditioner is turned off.
```

以上です。よしなにお好みの処理に置き換えて遊んでみてください。

# 参考

https://platform.openai.com/docs/api-reference/audio/createTranscription