はじめに
冒頭から私事で恐縮ですが、私は英語学習教材の一つとして「英語のハノン」を愛用しています。
英語のハノンで繰り返し練習することで、英語スピーキングテストアプリ「PROGOS」の「正確さ(Accuracy)」項目において当初「A2」レベルだった私が → 「B2」またはそれ以上をコンスタントにとれるまでになりました。
このような実体験としての効果から、私にとって英語のハノンは手放せない存在です。
(もちろんPROGOSにも感謝です!)
いつもはスマホの音楽再生アプリでやっているのですが、次項の「やりたいこと」がずっと気になっていました。
そんな折、株式会社アドバンスト・メディア様の「AmiVoice」の記事投稿キャンペーンが目に留まりました。
AmiVoiceは音声認識結果のテキストに加えて、単語単位で「信頼度」も返してくれるようです。
「これはやりたいことができそうだ!」ということで、やってみることにしました。
やりたいこと
- 「英語のハノン」をモバイルアプリ化する
- ★自分の発話/発音の正確性を確認したい
- 設問や正解音声が何を言っているのかテキストで確認したい(開本モード/閉本モード)
- 閉本で完璧にできるようになったUnitはスキップできるようにしたい
- 毎日やっているか確認したい
- ChatGPTを使用して同じ構文で異なる単語を使って飽きないようにしたい
- Pythonの勉強を兼ねる
今回はとりあえず★印のみの実現が目標です。
Pythonは始めたばかりですが、勉強を兼ねたいのでフレームワークとして「Flet」を使ってみることにしました。
開発環境
Windows10
Python3.11.9
PyCharm Community Edition
結論
思い立ってからの時間が少なく、最後までできませんでした。。。
とりあえずキャンペーンの締切なのでやったところまで公開します。
あと一歩のところまで来たとは思いますので、必ず完成させて全コードを公開したいと思います。
ご興味がおありの方は経過を見守っていただけますと幸いです。
やったこと
0. AmiVoide APPKEYの取得
「音声認識APIを使ってみよう!」キャンペーンの手順通り、AmiVoideの APPKEY
の取得を申し込みます。
APPKEY
はマイページの「接続情報」で確認できます。
1. AmiVoiceのPythonサンプルの動作確認
AmiVoice API クライアントライブラリ から一式ダウンロードします。
今回のケースでは長くても数秒の短い音声を認識させたいため、「HTTP音声認識API (Hrp
)」を使えば良いようです。
したがってHrp
フォルダ以下のpython
フォルダ以下のサンプルをベースにすれば良さそうです。
注意事項として
Windows プラットフォームでライブラリを使用する際、 サーバ証明書データベースファイルへのパスを環境変数 SSL_CERT_FILE に指定する必要があります。
とあるので、サンプル一式の直下にあるcurl-ca-bundle.crt
ファイルにパスを通します。
パスを通したら、Hrp\python以下のrun.bat
を実行します。
上記0項で取得したAPPKEY
をペーストしEnter
すると・・・
エラーになってしまいました。。
赤線のエラーメッセージを検索すると、Python3.12特有のエラーのようでした。
Python3.11.9をインストールし、Pathを通します。
C:\Users\$user\AppData\Local\Programs\Python\Python311\
2. PythonコードからAmiVoide APIを実行する
サンプルコードでは認識結果をprintするだけになっているため、コード(アプリ)から使いやすいように改造します。
サンプルのHrpSimpleTester.py
の名称をAmiVoice_Hrp.py
に変更し、次のようにしました。
(get_result
は無理に実装した感じです。良い方法がわかる方、ご教示ください!)
# encoding: UTF-8
import sys
import time
import json
# <!-- バイトコードキャッシュファイルの作成を抑制するために...
sys.dont_write_bytecode = True
# -->
import com.amivoice.hrp.Hrp
import com.amivoice.hrp.HrpListener
class HrpSimple_Sync(com.amivoice.hrp.HrpListener):
def __init__(self, appkey):
self.serverURL = "https://acp-api.amivoice.com/v1/recognize"
self.codec = "16K"
self.authorization = appkey
# HTTP 音声認識サーバイベントリスナの作成
self.listener = self
self.result = None
def run_stt(self, audioFileName, grammarFileNames):
# HTTP 音声認識サーバの初期化
hrp = com.amivoice.hrp.Hrp.construct()
hrp.setListener(self.listener)
hrp.setServerURL(self.serverURL)
hrp.setCodec(self.codec)
hrp.setGrammarFileNames(grammarFileNames)
hrp.setAuthorization(self.authorization)
# HTTP 音声認識サーバへの接続
if not hrp.connect():
print(hrp.getLastMessage())
print(u"HTTP 音声認識サーバ %s への接続に失敗しました。" % self.serverURL)
return
try:
# HTTP 音声認識サーバへの音声データの送信の開始
if not hrp.feedDataResume():
print(hrp.getLastMessage())
print(u"HTTP 音声認識サーバへの音声データの送信の開始に失敗しました。")
return
try:
with open(audioFileName, "rb") as audioStream:
# 音声データファイルからの音声データの読み込み
audioData = audioStream.read(4096)
while len(audioData) > 0:
# 微小時間のスリープ
hrp.sleep(1)
# HTTP 音声認識サーバへの音声データの送信
if not hrp.feedData(audioData, 0, len(audioData)):
print(hrp.getLastMessage())
print(u"HTTP 音声認識サーバへの音声データの送信に失敗しました。")
break
# 音声データファイルからの音声データの読み込み
audioData = audioStream.read(4096)
except:
print(u"音声データファイル %s の読み込みに失敗しました。" % audioFileName)
# HTTP 音声認識サーバへの音声データの送信の完了
if not hrp.feedDataPause():
print(hrp.getLastMessage())
print(u"HTTP 音声認識サーバへの音声データの送信の完了に失敗しました。")
return
finally:
# HTTP 音声認識サーバからの切断
hrp.disconnect()
def resultCreated(self, sessionId):
# print("C %s" % sessionId)
pass
def resultUpdated(self, result):
# print("U %s" % result)
pass
def resultFinalized(self, result):
self.result = result
# print("F %s" % result)
print(result)
text = self.text_(result)
if text != None:
print(" -> %s" % text)
def TRACE(self, message):
pass
def text_(self, result):
try:
return json.loads(result)["text"]
except:
return None
# 無理やり…
def get_result(self):
while True:
if self.result != None:
return self.result
time.sleep(0.1)
上記に合わせてmain.pyを次のようにしました。
英語を認識させるため、会話エンジンをサンプルの-a-general
から-a-general-en
に変更します。
from AmiVoice_Hrp import HrpSimple_Sync
if __name__ == "__main__":
hrp = HrpSimple_Sync("MY_APPKEY")
hrp.run_stt(r".\hanon2\unit1\chunk0.mp3", "-a-general-en")
print(hrp.get_result())
mainを実行すると、イイ感じで認識しているようです。
{"results":[{"tokens":[{"written":"I","confidence":1.00,"starttime":530,"endtime":690,"spoken":"I"},{"written":"happen","confidence":0.71,"starttime":690,"endtime":1010,"spoken":"happen"},{"written":"to","confidence":0.98,"starttime":1010,"endtime":1090,"spoken":"to"},{"written":"see","confidence":1.00,"starttime":1090,"endtime":1370,"spoken":"see"},{"written":"her","confidence":1.00,"starttime":1370,"endtime":1730,"spoken":"her"},{"written":"I","confidence":1.00,"starttime":2090,"endtime":2250,"spoken":"I"},{"written":"was","confidence":0.98,"starttime":2250,"endtime":2430,"spoken":"was"},{"written":"visiting","confidence":1.00,"starttime":2430,"endtime":2810,"spoken":"visiting"},{"written":"the","confidence":1.00,"starttime":2810,"endtime":2890,"spoken":"the"},{"written":"museum","confidence":0.92,"starttime":2890,"endtime":3690,"spoken":"museum"},{"written":"."}],"confidence":0.996,"starttime":250,"endtime":3690,"tags":[],"rulename":"","text":"I happen to see her I was visiting the museum."}],"utteranceid":"20240521/21/018f9b2a90b60a303f3b94c9_20240521_214009","text":"I happen to see her I was visiting the museum.","code":"","message":""}
3. 英語のハノンのMP3音声を無音部分で分割する
まず、英語のハノンサイトに公開されている音声教材ファイルをダウンロードします。
今回は実験的に 中級 のUnit1のみを使用します。
中級のUnit1のMP3ファイルを次のようにダウンロードします。
英語のハノンでは、構文練習単位として1ユニット5項目構成となっており、次のような音声構成になっています。
原文音声1
(発話)
原文音声1(リピート)
(発話)
キュー
(発話)
回答音声1
(発話)
回答音声1(リピート)
(若干の間)
原文音声2
(発話)
原文音声2(リピート)
(発話)
…
回答音声5(リピート)
(発話)
「(発話)」の部分は原文や回答音声の長さに応じた無音部分となっており、この無音部分の時間を使って学習者が発話します。
アプリでは以下の手順:
- 音声再生
- 音声認識
- 結果表示
を繰り返したいので、「音声再生」部分は項目ごとに分割する必要があります。
したがって、上記でダウンロードした中級Unit1のMP3ファイル「unit1_1.mp3」を無音部分で分割します。
無音部分で音声を分割するツールにはいろいろあるようですが、今回は以下を使用しました。
https://github.com/danesjenovdan/audio-splitter
(詳細は後日記載)
4. Fletのスケルトンを作成
公式の Audio および AudioRecorder のサンプルをベースに、main.pyを書き換えて簡単な画面を作成します。
(画面)
import flet as ft
import os
import time
from AmiVoice_Hrp import HrpSimple_Sync
async def main(page: ft.Page):
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.appbar = ft.AppBar(title=ft.Text("英語のハノン 中級 Unit1"), center_title=True)
rec_wav_path = "stt_result.wav"
async def handle_start_recording(e):
print(f"StartRecording: {rec_wav_path}")
await audio_rec.start_recording_async(rec_wav_path)
await toggle_Record_Button(True)
async def handle_stop_recording(e):
output_path = await audio_rec.stop_recording_async()
print(f"StopRecording: {output_path}")
if page.web and output_path is not None:
await page.launch_url_async(output_path)
await toggle_Record_Button(False)
async def toggle_Record_Button(rec_start):
Btn_start_Recording.visible = not rec_start
Btn_stop_Recording.visible = rec_start
await page.update_async()
# Unit1.1のファイルをリストアップ
ftAudio_chunks = []
for chunk in os.listdir(r".\hanon2\unit1"):
ftAudio_chunks.append(
ft.Audio(
src=os.path.join(r".\hanon2\unit1", chunk),
autoplay=False,
volume=1,
balance=0,
)
)
def play_audio_files(e):
# 今回の主要部分!
pass
Btn_Unit1 = ft.ElevatedButton("Unit1.1",
icon=ft.icons.PLAY_ARROW,
#on_click=lambda _: audio1.play())
on_click=play_audio_files)
async def handle_state_change(e):
print(f"State Changed: {e.data}")
audio_rec = ft.AudioRecorder(
audio_encoder=ft.AudioEncoder.WAV,
on_state_changed=handle_state_change,
)
page.overlay.append(audio_rec)
for ft_chunk in ftAudio_chunks:
page.overlay.append(ft_chunk)
page.update()
Btn_start_Recording = ft.IconButton(
icon=ft.icons.MIC,
icon_color="blue400",
icon_size=40,
tooltip="Start record",
on_click=handle_start_recording,
)
Btn_stop_Recording = ft.IconButton(
icon=ft.icons.STOP_CIRCLE,
icon_color="pink600",
icon_size=40,
tooltip="Stop record",
visible=False,
on_click=handle_stop_recording,
)
page.add(
Btn_Unit1,
Btn_start_Recording,
Btn_stop_Recording,
)
ft.app(target=main)
5. MP3再生→音声認識→結果表示を一気通貫させる
ここまでお読みいただいた方、申し訳ございません!
時間切れでできませんでした😇
進展があり出来次第本記事を修正していきたいと思います。
謝辞
このたびキャンペーンで無料APIを提供してくださった株式会社アドバンスト・メディア様に感謝申し上げます。
おかげさまで長年やりたかったことをやるきっかけになりました。