音声認識キャンペーンに参加!その2!
下記の投稿で参加した音声認識キャンペーンですが、私の周りの極一部のマニアの方にはとても好評でした!
それに気分を良くした私は、そのやり取りの中で得たヒントから、音声認識ゲームの作成に取り組んでみました!
というのが今回のお話です。
誰もが一度は口ずさむ FF14 アルテマ!
大好きなオンラインゲーム FF14で、その天丼効果により誰もが口ずさめるようになってしまった魔法アルテマの詠唱。
その詠唱をゲームにしたら楽しいんじゃないか?きっと楽しい!
という思い付きで、アルテマを詠唱するブラウザゲームを作る俺プロジェクトが始まりました。
アプリと遊び方
こちらが実アプリになります。
遊び方は、マイクアイコンをポチして、マイクに向かって喋るだけです。
すると、音声がAmiVoiceAPIに送信され、音声認識され、テキストになり、詠唱の文言とどれだけ近いかを採点、点数によってWindows Copirotに生成してもらった大小のアルテマっぽい画像をランダムで表示します。
ゲーム音声もゲーム内で独りロケを行って、AmiVoiceAPIで音声認識したものを表示するようにしています。
当初は毎回送信していたのですが、音声認識の結果はいつも同じなので、結果のテキストをそれっぽく表示するだけにしています。
また、音声認識の性能が良いAmiVoiceさんには大変申し訳ないのですが、前回同様、音声認識率がやや低い同期版を利用させていただいています。
変な認識となった方がゲームとしては面白いのではないか?と考えたからです。
(こう話したら?モノマネしたら?これならどう?という、話し方の試行錯誤がしたかった)
早速作ってみる!
次のような環境で構築しました。
- 音声認識は前回に引き続き、AmiVoiceAPIを利用
- ブラウザでの音声入力として、Streamlit、audio_recorder_streamlit
- テキストの評価として、Levenshtein
- アプリのデプロイ先として、Azure AppService
工夫したところ、苦労したところ、感想など
- AmiVoiceAPIが受け付けられるwavファイルはサンプリングレート16KHzだったので、Streamlitでも16KHzを指定しています。
- 複数のアクセスがあった場合に、ユニークなwavファイルとなるよう、uuid、タイムスタンプを利用してwavファイルのファイル名をつけるようにしています。また送信した音声は即削除する漢らしい仕様です。
- Streamlitが各ボタン実行毎に、全て再実行されてしまう、というのが微妙に気にはなりましたが、シビアなものを作っているわけではないので(session_stateやformなどで解決できそうな気もしましたが)諦めました。
- Streamlitの公式でも公開できたのですが、手元に置いておきたい気持ちから、Azure AppServiceにデプロイすることにしました。当初はコードがイマイチだったこともあって、作った環境を消したりしていたところ、AppServiceの環境自体が作れなくなって数日いろいろやってました。結論的には従量課金にすることで再作成できるようになりました。一見さんは1回きりよ、っていうことかもしれませんね。
- pipでstreamlitをインストールしていたのですが、write_streamが認識されなかったり、マイクがすぐに出なかったり、動作が妙な感じでした。stream hubでは正しく動作したので、versionを確認すると1.2くらいの古いものでした。最新の1.34を導入することでOKとなりました。
- 公開してしまうつもりではいたものの、もしかすると悪の組織が私を破産させようとアルテマを唱え続けるかもしれない!Amivoiceはキャンペーンもあって大きな無料枠もあるが、保険として広告が貼れないか?とかなり調べたが、結論的にはStreamLitへの広告の埋め込みはかなりハードルが高そうで無理そうだった。
幸いあまり利用されていないから助かっている
今後できたらやりたいこと
- 音声認識の結果と点数などをDBに記録は、練習としてはやってみたい(ゲームとしてはどっちでもいいかな)
- FF14のレイドコンテンツは、ノーマルとハード(零式)があるので、今回のAmiVoiceAPIの同期版を難度が高い零式と位置付けて、ノーマル向けとして非同期版を実装したらどうか。(ただ日常会話ではないので微妙な認識率になるかもしれない?)
- Azureの触り方が少しわかったので、今後も活用していきたい
コード紹介
githubはこちら
AmiVoiceAPIの部分は前回とほぼ同じなので、Streamlit部分について貼り付けます。
streamlit_app.py
import streamlit as st
from audio_recorder_streamlit import audio_recorder
from send_voice import send_voice
import Levenshtein
import time
import os
import random
import uuid
def generate_unique_filename():
# セッション ID やタイムスタンプを組み合わせて一意のファイル名を生成
session_id = str(uuid.uuid4())
timestamp = int(time.time())
unique_filename = f"{session_id}_{timestamp}.wav"
return unique_filename
def stream_data(text):
for word in list(text):
yield word + " "
time.sleep(0.08)
def saiten(text):
st.write_stream(stream_data(text))
ultima_2=ULTIMA.replace('、','')
text_2=text.replace('、','')
jw=int(Levenshtein.jaro_winkler(ultima_2,text_2)*100)
st.write(str(jw)+'点!')
return jw
def rnd_image(image_directory):
# random.seed(0)
image_files = [f for f in os.listdir(image_directory) if os.path.isfile(os.path.join(image_directory, f))]
selected_image = random.choice(image_files)
return selected_image
ULTIMA='渦なす生命の色、七つの扉開き、力の塔の天に至らん'
LAHABREA='./wav/Lahabrea.wav'
ATHENA='./wav/Athena.wav'
st.title("唱えろ!アルテマ!!")
st.header(" ")
st.write("マイクを通して唱えろ!")
st.write("「__"+ULTIMA+"__!!」")
st.write('制限時間は10秒です')
st.write('© SQUARE ENIX')
audio_bytes = audio_recorder( energy_threshold=(-1.0, 1.0),pause_threshold=10.0,sample_rate=16000)
if audio_bytes:
unique_filename = generate_unique_filename()
with open(unique_filename, "wb") as f:
f.write(audio_bytes)
st.success("Recording saved & Voice sending...")
text=send_voice(unique_filename)
os.remove(unique_filename)
st.write('___')
jw=saiten(text)
if jw > 80:
file_path="./image/80_/"
elif jw > 70:
file_path="./image/70_/"
elif jw > 60:
file_path="./image/60_/"
else:
file_path="./image/_59/"
img_file=rnd_image(file_path)
st.image(file_path+img_file)
st.write('___')
if st.button("ラハブレア"):
text_LAHABREA="宇品生命の色を七つの扉開き力の党の店に行ったら、"
if st.audio(LAHABREA,format="audio/wav"):
time.sleep(10)
saiten(text_LAHABREA)
if st.button("アテナ"):
text_ATHENA="次生命の色七つの扉開き力の店に行ったら"
if st.audio(ATHENA,format="audio/wav"):
time.sleep(12)
saiten(text_ATHENA)
参考にさせていただきました!