音声認識でブラウザゲーム FF14「唱えろ！アルテマ！！」を作った！

## 音声認識キャンペーンに参加！その２！
下記の投稿で参加した音声認識キャンペーンですが、私の周りの極一部のマニアの方にはとても好評でした！
それに気分を良くした私は、そのやり取りの中で得たヒントから、音声認識ゲームの作成に取り組んでみました！
というのが今回のお話です。

https://qiita.com/calcMCalcm/items/b715193536a2a8e9041d

https://qiita.com/official-events/23333339b694ca71810a

## 誰もが一度は口ずさむ FF14 アルテマ！
大好きなオンラインゲーム FF14で、その天丼効果により誰もが口ずさめるようになってしまった魔法アルテマの詠唱。
その詠唱をゲームにしたら楽しいんじゃないか？きっと楽しい！
という思い付きで、アルテマを詠唱するブラウザゲームを作る俺プロジェクトが始まりました。

## アプリと遊び方
こちらが実アプリになります。

https://ff14-ultima.streamlit.app/

遊び方は、マイクアイコンをポチして、マイクに向かって喋るだけです。
すると、音声がAmiVoiceAPIに送信され、音声認識され、テキストになり、詠唱の文言とどれだけ近いかを採点、点数によってWindows Copirotに生成してもらった大小のアルテマっぽい画像をランダムで表示します。

ゲーム音声もゲーム内で独りロケを行って、AmiVoiceAPIで音声認識したものを表示するようにしています。
当初は毎回送信していたのですが、音声認識の結果はいつも同じなので、結果のテキストをそれっぽく表示するだけにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679605/ee94aeae-82d2-44ec-d220-e7dc22bfde17.png)

また、音声認識の性能が良い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はこちら

https://github.com/calocenrieti/ff14_ultima

AmiVoiceAPIの部分は前回とほぼ同じなので、Streamlit部分について貼り付けます。

<details><summary>streamlit_app.py</summary>

```python: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)
```
</details>

## 参考にさせていただきました！

https://qiita.com/takashiuesaka/items/491b21e9afb34bbb6e6d

https://qiita.com/tamura__246/items/366b5581c03dd74f4508

https://qiita.com/KII437465127175/items/87cb81041ebe47b1a1da

https://qiita.com/tanuk1647/items/5a591da10e2ea5bedef6

https://qiita.com/tetsuro731/items/1bb1700b687d391c3c7b