この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
1 はじめに
CX 事業本部のデリバリー部の平内(SIN)です。
今回は、gpt-3.5-turbo を使用して、音を判別してみました。
最初に動作している様子です。
「私のコップ(my_cup)」をスプーンで叩いた音と、「別のコップ(unknown)」の音を聞いてChatGPTが、どちらを鳴らしたのか判別しています。
2 コップの音
2種類のコップを叩いた時の波形です。見た目ですが、結構、違いが出ていたので、「これぐらいなら判別できるかもしれない!」と思ったのが、今回のスタートです。
my_cup
unknown
3 データの正規化及び圧縮
Audioデータは、そのままでは、ちょっと無理があると思ったので、正規化で、0 から 255 の整数へ変換し、64バイトまで圧縮しています。また、先頭の無音部分をトリミングして、音の立ち上がりを揃えました。
- 正規化 (0..255 の整数で表現する)
- -1 〜 1 への正規化
- 絶対値への変換 0..1 の表現に変換する
- 256 倍して、0..255 の表現に変換する
- 整数化 float -> int
- Compress 圧縮 1/256
- Trim 先頭の無音部分の削除
以下の図は、データを変換していく過程の波形です。 詳しくは、後述のコードで、 create_text(frames):をご参照ください。
なお、圧縮は、何種類かやってみたのですが、元の波形が認識できる程度ということで、1/256 としました。
4 文字列化
圧縮したデータは、プロンプトで使用できるよう 16 進数の文字列とし、コップを叩くと128バイトの文字列が生成されます。
# 16bit の int データを 00-FF の 2 バイトで表現
def to_string(array):
str = ""
for n in range(array.size):
str += "{:02X}".format(array[n])
return str
当初、「FF,FF,01,02,・・・」のように、カンマで区切って試していたのですが、トークンが 多くなってしまうので、データ間の区切りは、「無し」としました。「FFFF0102・・・」
5 プロンプト
ChatGPT に送るプロンプトは、以下のような形式となっています。
最初に、"system"で、データ形式と判定要領を定義し、その後は、事前にサンプリングしたデータで、"my_cup"と"unknown"を「判定例」を 3 回づつ繰り返しています。
「判定例」の繰り返しが、3回で充分というのは、ちょっと驚きでした。
[
{
"role": "system",
"content": "From the input that expresses the change in sound in a hexadecimal number of 2 byte, Look at similarity and reply with [my_cup] or [unknown]."
},
{ "role": "user", "content": "{既存のmy_cupのデータ}" },
{ "role": "assistant", "content": "my_cup" },
・・・略・・・
{ "role": "user", "content": "{既存のunknownのデータ}" },
{ "role": "assistant", "content": "unknown" },
・・・略・・・
{ "role": "user", "content": "{今回の録音データ}" }
]
6 実装
作成したコードです。
import os
import numpy as np
import pyaudio
import openai
openai.api_key = os.environ["OPENAI_API_KEY"]
# プロンプトの生成
def create_prompt(data):
order = "From the input that expresses the change in sound in a hexadecimal number of 2 byte, Look at similarity and reply with [my_cup] or [unknown]."
my_cup_list = [
"FFFFDBB4B7897C685C504C3F3D312D282620201B1A1817141512121010100F0E0E0D0C0C0B0C0B0B0A0A0909080908080707070706060606060605050505050505040404040404030303030303020302000000000000",
"FFFFFFC0B58B7A695C4E50403D302D2826211F1C1A1817161515121210100E0F0D0E0C0C0B0C0A0B0A0A0909090909080807070707070706060606050505050505040404040404030303030303030300000000000000",
"FFFFD1BCAE86807995725F4C4D3B3629281C201C1815150E120E0E0C0B0B0A09090908080706060605050505040404050304030402030203020302020202020202020102010201010101010101010000000000000000",
]
unknown_list = [
"FF722D130E070706050405030302010000000001000100000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"FF651D100A060404030203010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"FF8727150C050404040203010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
]
role = "role"
content = "content"
system = "system"
user = "user"
assistant = "assistant"
prompt = []
prompt.append({role: system, content: order})
for line in my_cup_list:
prompt.append({role: user, content: line})
prompt.append({role: assistant, content: "my_cup"})
for line in unknown_list:
prompt.append({role: user, content: line})
prompt.append({role: assistant, content: "unknown"})
prompt.append({role: user, content: data})
return prompt
# 音量が敷居値を超えているかどうかの判定
def is_valid(data, threshold):
np_array = np.frombuffer(data, dtype=np.int16) # データは、16bit単位で判定する
if np.amax(np_array) > threshold:
return True
return False
# データの圧縮
def compress(array, split):
result = np.empty(0, dtype=np.uint16)
size = np.size(array)
# データを分割する
sub_arrays = np.array_split(array, len(array) / split)
for i in range(int(size / split)):
# 分割した中で、最大値を使用する
max_val = np.max(sub_arrays[i])
result = np.append(result, max_val)
return result
# 先頭の無音部分の削除
def trim(array):
max = array.size
result = np.empty(0, dtype=np.uint16)
trim = True
for n in range(max):
if trim and array[n] > 10:
trim = False
if trim == False:
result = np.append(result, array[n])
for n in range(max - result.size):
result = np.append(result, 0)
return result
# 16進文字列への変換
def to_string(array):
str = ""
for n in range(array.size):
str += "{:02X}".format(array[n])
return str
# 録音データからテキストデータの生成する
def create_text(frames):
# numpy arrayへの変換
np_array = np.frombuffer(b"".join(frames), dtype=np.int16)
# -1 〜 1への正規化
np_array = (np_array) / (np.max(np_array) - np.mean(np_array))
# 絶対値への変換 0..1の表現に変換する
np_array = np.abs(np_array)
# 256倍して、0..256の表現に変換する
np_array = np_array * 256
# 範囲制限 データを規定範囲内(0..255)に収める
np_array = np.clip(np_array, 0, 255)
# 整数化 float -> int
np_array = np.array(np_array, dtype="int")
# データの圧縮 1/256
np_array = compress(np_array, 256)
# 先頭の無音部分の削除
np_array = trim(np_array)
# 16進文字列に変換
return to_string(np_array)
# Audio ストリームの初期化
def init_stream(py_audio, stream, CHUNK, SAMPLE_RATE, FORMAT, CHANNELS):
if stream is not None:
stream.stop_stream()
stream.close()
return py_audio.open(
format=FORMAT,
channels=CHANNELS,
rate=SAMPLE_RATE,
input=True,
frames_per_buffer=CHUNK,
)
def main():
FORMAT = pyaudio.paInt16
CHANNELS = 1
SAMPLE_RATE = 44100
CHUNK = int(SAMPLE_RATE / 40) # ループ毎の時間は、 SAMPLE_RATE/40 = 25ms
threshold = 2000 # MacOSのマイク音量に依存する
py_audio = pyaudio.PyAudio()
stream = None
stream = init_stream(py_audio, stream, CHUNK, SAMPLE_RATE, FORMAT, CHANNELS)
buffers = []
print("start.")
while True:
data = stream.read(CHUNK)
buffers.append(data)
# 一定の音量を超えたら、処理を開始する
if is_valid(data, threshold):
frames = []
# 25ms前のデータから使用する
frames.append(buffers[len(buffers) - 1])
for _ in range(14): # 25msec * 14 = 350msec 録音する
frames.append(stream.read(CHUNK))
# 録音データからテキストデータの生成する
str = create_text(frames)
print("gpt-3.5-turbo:", end=" ")
prompt = create_prompt(str)
try:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo", messages=prompt, temperature=0
)
print(response.choices[0]["message"]["content"].strip())
except Exception as e:
print("Exception:", e.args)
stream = init_stream(py_audio, stream, CHUNK, SAMPLE_RATE, FORMAT, CHANNELS)
buffers = []
if __name__ == "__main__":
main()
7 最後に
今回は、音の聞き分けを試してみましたが、正直なところ、想像以上に正確でビックリしました。
ディープラーニングでAudioデータの判定などを行おうとすると、それなりに多くのデータを用意する必要がありますが、今回試した例では、事前に用意したサンプリングデータは、それぞれ3個でした。
このような使い方が、「ChatGPTの利用方法としてどうなのか?」 という疑問は棚上げし・・・ 色々、幅広く利用範囲を模索できればな、と思っています。