【Python】英会話トレーニングアプリを作ってみよう

【Python】英会話トレーニングアプリを作ってみよう
Photo by Flipsnack / Unsplash

AIが発達したおかげで、比較的簡単に英会話の練習ができるアプリを自分で作ることができるようになりました。

今回は、
(1) 日本語で話すと、自然なアメリカ英会話に翻訳して英語で話し、
(2) 英語で話すと、アメリカ英会話として不自然だったり誤っていた場合は、英語を訂正してくれて、さらに、話の続きを英会話として返答してくれる
アプリを作ってみました。

1. システム構成

  1. 音声入力(ASR)
    • Enterキーを押した後に、マイクから音声をキャプチャ。
      Enterキーを再度押すまで音声を記録。
    • OpenAI Whisper(API / ローカルモデル)で文字起こし
  2. 言語判定 & 処理
    • 文字起こし結果の言語を判定(Whisper が返す language を使用)
    • 日本語 → ChatGPT API に「自然なアメリカ英会話に翻訳して」もらう
    • 英 語 → ChatGPT API に「不自然なら訂正し、流れに合う英会話として返答して」もらう
  3. 音声合成(TTS)
    • Google Cloud Text-to-Speechで「女性/流暢なアメリカ英語音声」を生成
  4. 音声再生
    • 生成された音声をスピーカーから再生
  5. ループ
    • ユーザーが止めるまで続ける

2. 必要なライブラリと環境

Python3.12は、Coqui TTSが対応していなかったので、Python3.11を使用しました。
結局Coqui TTSは使わず、Google Cloud TTSを使用したので、Python3.12のままでも良かったかもしれません・・・

sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.11 python3.11-venv python3.11-dev

venv環境の作成

mkdir english_conversation
cd english_conversation
python3.11 -m venv .venv
source ./.venv/bin/activate
pip install --upgrade pip

ASR(ローカル Whisper)+ OpenAI API

pip install openai-whisper openai

音声入出力

pip install sounddevice soundfile

Google Cloud TTS(要 GCP プロジェクト&認証ファイル)

pip install google-cloud-texttospeech

.envファイルの読み込み

pip install python-dotenv

環境変数の設定

OpenAI API キーは環境変数 OPENAI_API_KEY に、
GCP 認証は GOOGLE_APPLICATION_CREDENTIALS に JSON ファイルパスを設定します。
今回は.envファイルに上記の設定を記述しました。

記述例(.envファイル)

GOOGLE_APPLICATION_CREDENTIALS=/mnt/gsdata/Study/python/403502-abcdefg.json
OPENAI_API_KEY=sk-proj-V6S19XTxibDxAWEH_N...76T5u9XATvTN1gFb

3. サンプルコード(main.py)

import os
import whisper
import sounddevice as sd
import soundfile as sf
from openai import OpenAI
from google.cloud import texttospeech
from dotenv import load_dotenv
import numpy as np

load_dotenv()

# ── 初期化 ─────────────────────────────
client = OpenAI(
    api_key = os.getenv("OPENAI_API_KEY")    
)

whisper_model = whisper.load_model("small")  # small/medium
tts_client = texttospeech.TextToSpeechClient()

# ── 音声録音 ────────────────────────────
def record_audio(filename="input.wav", fs=16000):
    # 1)開始トリガー
    input("▶ 録音を始めるには Enter を押してください…")
    print("🔴 録音中…停止するには Enter を押してください")

    # 2)コールバックでデータをバッファに溜める
    buf = []
    def callback(indata, frames, time, status):
        buf.append(indata.copy())

    # 3)InputStream を開いて、Enter が押されるまで録音
    with sd.InputStream(samplerate=fs, channels=1, callback=callback):
        input()

    # 4)NumPy 配列にまとめて WAV に書き出し
    audio = np.concatenate(buf, axis=0)
    sf.write(filename, audio, fs)
    print(f"✅ 録音を停止しました。ファイル: {filename}")
    return filename

# ── Whisper で文字起こし ──────────────────
def transcribe(file_path):
    res = whisper_model.transcribe(file_path)
    text = res["text"].strip()
    lang = res["language"]  # 'ja' or 'en' など
    print(f"📝 認識結果({lang}): {text}")
    return text, lang

# ── ChatGPT 処理 ─────────────────────────
messages_ja = [
    {"role":"system",
     "content":(
            "あなたはアメリカ日常会話の翻訳者です。"
            "ユーザーの日本語発話を自然なアメリカ英会話に翻訳してください。"
     )
    }
]

messages_en = [
    {"role":"system",
     "content":(
            "あなたはアメリカ英会話のパートナーです。"
            "ユーザーの英語発話が不自然で間違っていたなら訂正し、その後会話の流れに合う返答を英語でしてください。"
            "ユーザーの英語発話に問題がなければ、そのままユーザーの発話に応じて自然に会話してください。"
     )
    }
]


def chat_process(text, lang):
    print("▶ chat_process start")
    print(f"  input text: {text!r}, lang: {lang}")
    
    if lang.startswith("ja"):
        messages = messages_ja
    else:
        messages = messages_en
    
    messages.append({"role": "user", "content": text})
    resp = client.chat.completions.create(
        model="gpt-4.1-mini",
        messages=messages,
        temperature=0.7,
    )
    
    print("  raw resp:", resp)
    
    reply = resp.choices[0].message.content.strip()
    messages.append({"role":"assistant", "content": reply})
    
    print(f"💬 ChatGPT: {reply}")
    return reply

# ── 音声合成 ────────────────────────────
def synthesize_speech(text: str, filename="output.wav") -> str | None:
    
    if not text or not text.strip():
        print("テキストが空のため、TTS をスキップします。")
        return None

    synthesis_input = texttospeech.SynthesisInput(text=text)
    voice = texttospeech.VoiceSelectionParams(
        language_code="en-US",
        name="en-US-Wavenet-F",
        ssml_gender=texttospeech.SsmlVoiceGender.FEMALE,
    )
    audio_config = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.LINEAR16)
    response = tts_client.synthesize_speech(
        input=synthesis_input, voice=voice, audio_config=audio_config
    )
    with open(filename, "wb") as f:
        f.write(response.audio_content)
    return filename

# ── 音声再生 ────────────────────────────
def play_audio(file_path):
    data, fs = sf.read(file_path, dtype="float32")
    sd.play(data, fs)
    sd.wait()

# ── メインループ ─────────────────────────
if __name__ == "__main__":
    print("====== 英会話トレーニング開始 ======")
    try:
        while True:
            wav = record_audio()
            txt, lang = transcribe(wav)
            reply = chat_process(txt, lang)
            out_wav = synthesize_speech(reply)
            play_audio(out_wav)
    except KeyboardInterrupt:
        print("\n👋 終了します。お疲れ様でした!")

4. コード解説

def record_audio(filename="input.wav", fs=16000):
    # 1)開始トリガー
    input("▶ 録音を始めるには Enter を押してください…")
    print("🔴 録音中…停止するには Enter を押してください")

    # 2)コールバックでデータをバッファに溜める
    buf = []
    def callback(indata, frames, time, status):
        buf.append(indata.copy())

    # 3)InputStream を開いて、Enter が押されるまで録音
    with sd.InputStream(samplerate=fs, channels=1, callback=callback):
        input()

sd.InputStream(...) は「オーディオ入力ストリーム」を開くオブジェクト。

samplerate=fs:サンプリング周波数(例えば16 000 Hz)
channels=1:モノラル録音
callback=callback:一定サイズごとに呼ばれる関数を登録

sd.InputStreamは、あらかじめ設定したブロックサイズ(フレーム数。明示していない場合は PortAudio 側のデフォルト)に合わせて、デバイスから一定量ずつオーディオデータを読み込みます。読み込まれる量(フレーム数)が溜まるたびに、必ずコールバックが呼ばれます。

呼び出されたcallback関数は、indata内にマイクから取り込まれた音声データが入っているので、それをbuf(リスト)に追加しています。

Enterを押下すると、input() が終了し、with のスコープを出ます。
その瞬間に sd.InputStream のクリーンアップ処理が走り、ストリームがクローズされ、コールバックも停止します。

audio = np.concatenate(buf, axis=0)

sd.InputStream のコールバックでは小さなチャンク(indata)が都度 NumPy 配列で渡されるので、それを Python のリスト (buf) に貯めておいて、最後に
audio = np.concatenate(buf, axis=0)
でまとめて大きな 1 次元の NumPy 配列にしています。

Numpy配列にすることにより下記のメリットがあります。

  • メモリの連続性
    NumPy 配列はメモリ上に連続して配置されるため、後続の処理(フィルタリングや加工、ファイル書き出しなど)を行う際にアクセスが高速になります。

  • ベクトル化された演算
    無音検知や正規化、窓関数の適用など、配列全体に同じ演算を一括でかけるとき、NumPy のベクトル化された関数を使うと Python のループより圧倒的に速く処理できます。

  • I/O ライブラリとの親和性
    soundfile.write() などのライブラリは NumPy 配列をそのまま受け取って効率的にファイル書き出しを行うよう最適化されているものが多いです。

  • メモリ効率
    Python のリストは要素ごとにオブジェクトヘッダを持つため、総チャンク数が多いとリストだけでメモリオーバーヘッドがかさみます。チャンクを一時的にリストに貯めたあとはまとめて連結し、以降は大きな 1 つの配列として扱うほうが効率的です。

5. 実行例

$ python main.py
====== 英会話トレーニング開始 ======
▶ 録音を始めるには Enter を押してください… (←Enterキーを押す)
🔴 録音中…停止するには Enter を押してください (← 「Hello, what's up?」と話し、Enterキーを押す)

✅ 録音を停止しました。ファイル: input.wav
📝 認識結果(en): Hello, what's up?

💬 ChatGPT: Hello! Not much, just here to chat with you. How about you? What's new?
▶ 録音を始めるには Enter を押してください… (←Enterキーを押す)
🔴 録音中…停止するには Enter を押してください (← 「Nothing is special, but I went to the Sushi restaurant.」と話し、Enterキーを押す)

✅ 録音を停止しました。ファイル: input.wav
📝 認識結果(en): Nothing is special, but I went to the Sushi restaurant.
💬 ChatGPT: A more natural way to say that would be: "Nothing special, but I went to a sushi restaurant."

That sounds great! What kind of sushi did you try?

▶ 録音を始めるには Enter を押してください… (←Enterキーを押す)
🔴 録音中…停止するには Enter を押してください (← 「寿司の種類は、マグロ、納豆、サーモンです。」と話し、Enterキーを押す)

✅ 録音を停止しました。ファイル: input.wav
📝 認識結果(ja): スシートの種類は、マグロ、納豆、サーモンです。

💬 ChatGPT: The types of sushi rolls are tuna, natto, and salmon.
▶ 録音を始めるには Enter を押してください… (← Ctrl+Cを押す)
👋 終了します。お疲れ様でした!

良い感じに動いてくれました。
みなさんも、ぜひやってみてください。
ただし、modelの選択を間違えないようにしましょう。
はじめ、gpt-4.1を使っていたら、数回試しただけで、$10かかってしまいました。1回入力しただけで、$2かかっていたみたいです(泣)。 
gpt-4.1-miniなら1回入力$0.4、gpt-4.1-nanoなら1回入力$0.1なので、比較的安心して使えます。

それでは!