🔊

GPT-4o-Transcribe(Whisper後継)で音声入力してみた

に公開


React(フロント)で録音し、FastAPI(バック)で受け取り、OpenAIの音声認識モデルで文字起こしするまでの一連の流れを実装する方法をまとめました。
この記事を読めば、音声入力機能をWebアプリに組み込むのに必要な流れがすべて理解できるようになっています。

1.録音機能(React / MediaRecorder)

ブラウザの MediaRecorder API を使って音声を録音します。
録音 → 停止 の流れで音声データを Blob として収集し、それを File に変換してバックエンドに送ります。

import { useRef, useState } from "react";
import axios from "axios";

export default function Recorder({ onTranscribed }) {
  const recorderRef = useRef(null);
  const bufferRef = useRef([]);
  const [recording, setRecording] = useState(false);

  const start = async () => {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    recorderRef.current = new MediaRecorder(stream);
    bufferRef.current = [];

    recorderRef.current.ondataavailable = (e) => bufferRef.current.push(e.data);
    recorderRef.current.onstop = async () => handleStop();

    recorderRef.current.start();
    setRecording(true);
  };

  const stop = () => {
    recorderRef.current.stop();
    setRecording(false);
  };

  const handleStop = async () => {
    const blob = new Blob(bufferRef.current, { type: "audio/webm" });
    const file = new File([blob], "audio.webm", { type: "audio/webm" });

    const form = new FormData();
    form.append("audio_file", file);

    const res = await axios.post("http://localhost:8000/api/stt", form);
    onTranscribed(res.data.text);
  };

  return (
    <div>
      {recording ? (
        <button onClick={stop}>停止</button>
      ) : (
        <button onClick={start}>録音開始</button>
      )}
    </div>
  );
}

ポイント
・音声データは Blob で返るため File に変換する
・「録音停止」したタイミングでデータが確定する
・audio/webm はブラウザ互換性が高く扱いやすい

2.音声データの送信(FormData)

録音した File を FormData に入れて POST します。

const form = new FormData();
form.append("audio_file", file);

await axios.post("http://localhost:8000/api/stt", form);

Axios に任せれば multipart/form-data は自動挿入されます。

3.音声ファイルの受け取り(FastAPI / UploadFile)

FastAPI 側では UploadFile で音声を受け取ります。

from fastapi import FastAPI, UploadFile, File, HTTPException

app = FastAPI()

@app.post("/api/stt")
async def stt(audio_file: UploadFile = File(...)):
    audio_bytes = await audio_file.read()
    if not audio_bytes:
        raise HTTPException(status_code=400, detail="音声が空です")

4.OpenAI での文字起こし処理(GPT-4o-Transcribe)

受け取った音声データを OpenAI に渡して文字起こしします。
今回はWhisperの後継モデルであるGPT-4o-Transcribeを使います。

from openai import OpenAI

client = OpenAI()
STT_MODEL = "gpt-4o-transcribe"

transcription = client.audio.transcriptions.create(
    model=STT_MODEL,
    file=(
        audio_file.filename or "audio.webm",
        audio_bytes,
        audio_file.content_type or "audio/webm"
    ),
)

return {"text": transcription.text}

特長
・Whisper より高速
・精度が高く句読点も自然
・そのまま UI に表示できる品質

Congrats!


これで 録音 → 送信 → 文字起こし がすべて動作します。
音声入力はこのシンプルな構成で十分実用レベルになりますし、ここを起点に長時間録音やストリーミング処理にも拡張できます。

⚠CORS設定

React と FastAPI が別ポートの場合、CORS を開けておきます。

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware(
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
)

⚠よくあるハマりどころ

  1. マイク権限がブラウザで拒否されている
    getUserMedia が例外を出します。Chrome の権限を確認してください。
  2. Axios でヘッダーを自前で指定してしまう
    multipart/form-data を自前で設定すると逆に壊れます。Axios に任せるのが正解です。
  3. UploadFile の代わりに bytes を使う
    bytes は大きい音声に弱いです。必ず UploadFile を使ってください。

おまけ:全体コードまとめ

初心者がつまずかないように、フロントとバックのまとめコードを置いておきます。

▼ フロントエンド(React)まとめコード

import { useRef, useState } from "react";
import axios from "axios";

export default function Recorder({ onTranscribed }) {
  const recorderRef = useRef(null);
  const bufferRef = useRef([]);
  const [recording, setRecording] = useState(false);

  const start = async () => {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    recorderRef.current = new MediaRecorder(stream);
    bufferRef.current = [];

    recorderRef.current.ondataavailable = (e) => bufferRef.current.push(e.data);
    recorderRef.current.onstop = async () => handleStop();

    recorderRef.current.start();
    setRecording(true);
  };

  const stop = () => {
    recorderRef.current.stop();
    setRecording(false);
  };

  const handleStop = async () => {
    const blob = new Blob(bufferRef.current, { type: "audio/webm" });
    const file = new File([blob], "audio.webm", { type: "audio/webm" });

    const form = new FormData();
    form.append("audio_file", file);

    const res = await axios.post("http://localhost:8000/api/stt", form);
    onTranscribed(res.data.text);
  };

  return (
    <div>
      {recording ? (
        <button onClick={stop}>停止</button>
      ) : (
        <button onClick={start}>録音開始</button>
      )}
    </div>
  );
}

▼ バックエンド(FastAPI)まとめコード

from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from openai import OpenAI

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"]
)

client = OpenAI()
STT_MODEL = "gpt-4o-mini-transcribe"

@app.post("/api/stt")
async def stt(audio_file: UploadFile = File(...)):
    audio_bytes = await audio_file.read()
    if not audio_bytes:
        raise HTTPException(status_code=400, detail="音声が空です")

    transcription = client.audio.transcriptions.create(
        model=STT_MODEL,
        file=(
            audio_file.filename or "audio.webm",
            audio_bytes,
            audio_file.content_type or "audio/webm"
        ),
    )

    return {"text": transcription.text}
ネイバーズ東京

Discussion