メインコンテンツまでスキップ

「OpenAI」タグの記事が2件件あります

全てのタグを見る

· 約5分
森田 有貴

森田です。

先日OpenAIのWhisperを用いた文字起こしの実装を紹介しました。ですがこの文字起こしはブラウザ上で音声を録音し、それをapiに投げて文字起こししてもらうという実装でした。

しかしこれには2つの欠点があります。まず全て録音が終了するまでちゃんと文字起こしされるかわからないという点です。もう一つは録音ファイルの容量が大きくなると文字起こしに時間がかかるという点です。

今回はこの2点を解決する方法を試験的に実装できたので、そちらを紹介したいと思います。

やったこと

今回試したのは録音を特定の秒数で区切り、その都度文字起こしをかけるという方法です。 とりあえず実装してみましょう。

実装

warn

前回の記事で作成したwhipserHook.tsをそのまま使用します。

特定の秒数で区切るだけなので、whisperHook.tsにその処理を追加します。

/src/app/utils/whisperHook.ts
import { useEffect, useRef, useState } from "react";

type Hooks = {
startRecording: () => void;
stopRecording: () => void;
isAudio: boolean;
recording: boolean;
audioFile: File | null;
isLoading: boolean;
transcript: string;
};

export const useWhisperHook = (): Hooks => {
const mediaRecorder = useRef<MediaRecorder | null>(null);
const [audioFile, setAudioFile] = useState<File | null>(null);
const [isAudio, setIsAudio] = useState<boolean>(false);
const [recording, setRecording] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [transcript, setTranscript] = useState("")
const intervalRef = useRef<number | null>(null);


const handleDataAvailable = (event: BlobEvent) => {
// 音声ファイル生成
const file = new File([event.data], "audio.mp3", {
type: event.data.type,
lastModified: Date.now(),
});
setAudioFile(file);
};

const startRecording = async () => {
setAudioFile(null)
setRecording(true);
// 録音開始
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder.current = new MediaRecorder(stream);
mediaRecorder.current.start();
mediaRecorder.current.addEventListener("dataavailable", handleDataAvailable);
setIsAudio(true);

intervalRef.current = window.setInterval(() => {
mediaRecorder.current?.stop();
mediaRecorder.current?.start();
}, 5000); // 5秒ごとに録音を停止して新しい録音を開始
};

const stopRecording = () => {
setRecording(false);
// 録音停止
mediaRecorder.current?.stop();
setIsAudio(false);
};

useEffect(() => {
const uploadAudio = async () => {
if (!audioFile) return;
const endPoint = "https://api.openai.com/v1/audio/transcriptions";

const formData = new FormData();
// fileを指定
formData.append("file", audioFile, "audio.mp3");
// modelを指定
formData.append("model", "whisper-1");
// languageを指定
formData.append("language", "ja");
setIsLoading(true);
const response = await fetch(endPoint, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`,
},
body: formData,
});
const responseData = await response.json();
if (responseData.text) {
// 文字起こしされたテキスト
setTranscript(prev => prev + "\n" + responseData.text);
}
setIsLoading(false);
};
uploadAudio();
}, [audioFile]);

return {
startRecording,
stopRecording,
isAudio,
recording,
audioFile,
isLoading,
transcript
};
};

42〜45行目の部分で指定した秒数で録音を止めて開始するという処理を挟んでいます。今回は5秒で区切るようにしています。

intervalRef.current = window.setInterval(() => {
mediaRecorder.current?.stop();
mediaRecorder.current?.start();
}, 5000);

これで録音を開始してみましょう。すると...

video

定期的に区切られてリアルタイムっぽく文字起こしされてる!🎉🎉🎉

ちなみにこちらはSREチームリーダーの山田哲さんとの今日の夜ご飯何食べようねという会話を文字起こししました。

というわけで実装できました。

欠点

前回の記事でも述べたとおり、文字起こし可能な音声が録音されていない場合に変な文字列が返ってくるという不具合がかなり影響しまして、区切るタイミングを細かくするほど顕著に現れてしまいます。 また、話の途中で区切られるとうまく文章がつながらないということもあるので、精度的にも少し懸念があります。

終わり

というわけでWhisperの文字起こしをリアルタイムっぽく実装してみました。 改善の余地はたくさんありますが、ひとまず形になったので個人的には満足しています。 whisperのsdkとか出ないかなぁ。

ではまた。

· 約8分
森田 有貴

森田です。 以前 Azure Speech SDK を用いたリアルタイム文字起こしをご紹介しましたが、今回は OpenAi の Whisper を用いた文字起こしをご紹介して実際に実装までしてみたいと思います。 今回も Next js 用います。

実装

Azure の SDK を用いた文字起こしはリアルタイムでの実行が可能でしたが、Whisper では音声ファイルをアップロードし文字起こしされた文字列が返ってくるという形です。 なのでまずは録音機能から実装していきましょう。

録音機能

いつも通りサクッとプロジェクトを作成して...

$ npx create-next-app
✔ What is your project named? … whisper-sample-app
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes

whisperHook を作成しましょう。

/src/app/utils/whisperHook.ts
import { useRef, useState } from "react";

type Hooks = {
startRecording: () => void;
stopRecording: () => void;
isAudio: boolean;
recording: boolean;
audioFile: File | null;
};

export const useWhisperHook = (): Hooks => {
const mediaRecorder = useRef<MediaRecorder | null>(null);
const [audioFile, setAudioFile] = useState<File | null>(null);
const [isAudio, setIsAudio] = useState<boolean>(false);
const [recording, setRecording] = useState(false);

const handleDataAvailable = (event: BlobEvent) => {
// 音声ファイル生成
const file = new File([event.data], "audio.mp3", {
type: event.data.type,
lastModified: Date.now(),
});
setAudioFile(file);
};

const startRecording = async () => {
setAudioFile(null)
setRecording(true);
// 録音開始
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder.current = new MediaRecorder(stream);
mediaRecorder.current.start();
mediaRecorder.current.addEventListener("dataavailable", handleDataAvailable);
setIsAudio(true);
};

const stopRecording = () => {
setRecording(false);
// 録音停止
mediaRecorder.current?.stop();
setIsAudio(false);
};

return {
startRecording,
stopRecording,
isAudio,
recording,
audioFile
};
};

これでひとまず録音機能は完成です! 実際に使えるか試してみましょう。

適当なページに実装して...

/src/app/page.tsx
"use client";
import { useWhisperHook } from "./utils/whisperHook";

const Page = () => {
const { startRecording, stopRecording, recording, audioFile } = useWhisperHook();
return (
<div>
<button onClick={startRecording}>start</button>
<button onClick={stopRecording}>stop</button>
{audioFile && <audio src={URL.createObjectURL(audioFile)} controls />}
{recording && <div>recording...</div>}
</div>
);
};

export default Page;

スタートボタンとストップボタンだけの簡素な画面になればOK!

image1

スタートボタンを押すと「recording...」と表示されて録音が開始されます。

※マイク権限の許可を求められた場合は許可してあげてください。

image2

ストップボタンを押すと録音が停止されて録音した音声を聴くことができます。

image3

というわけで録音機能の実装は完了です!

文字起こし

では続いて先ほど作成したHookに文字起こしの機能を追加していきいましょう。

/src/app/utils/whisperHook.ts
import { useEffect, useRef, useState } from "react";

type Hooks = {
startRecording: () => void;
stopRecording: () => void;
isAudio: boolean;
recording: boolean;
audioFile: File | null;
isLoading: boolean;
transcript: string;
};

export const useWhisperHook = (): Hooks => {
const mediaRecorder = useRef<MediaRecorder | null>(null);
const [audioFile, setAudioFile] = useState<File | null>(null);
const [isAudio, setIsAudio] = useState<boolean>(false);
const [recording, setRecording] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [transcript, setTranscript] = useState("")

const handleDataAvailable = (event: BlobEvent) => {
// 音声ファイル生成
const file = new File([event.data], "audio.mp3", {
type: event.data.type,
lastModified: Date.now(),
});
setAudioFile(file);
};

const startRecording = async () => {
setAudioFile(null)
setRecording(true);
// 録音開始
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder.current = new MediaRecorder(stream);
mediaRecorder.current.start();
mediaRecorder.current.addEventListener("dataavailable", handleDataAvailable);
setIsAudio(true);
};

const stopRecording = () => {
setRecording(false);
// 録音停止
mediaRecorder.current?.stop();
setIsAudio(false);
};

useEffect(() => {
const uploadAudio = async () => {
if (!audioFile) return;
const endPoint = "https://api.openai.com/v1/audio/transcriptions";

const formData = new FormData();
// fileを指定
formData.append("file", audioFile, "audio.mp3");
// modelを指定
formData.append("model", "whisper-1");
// languageを指定
formData.append("language", "ja");
setIsLoading(true);
const response = await fetch(endPoint, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`,
},
body: formData,
});
const responseData = await response.json();
if (responseData.text) {
// 文字起こしされたテキスト
setTranscript(responseData.text);
}
setIsLoading(false);
};
uploadAudio();
}, [audioFile]);

return {
startRecording,
stopRecording,
isAudio,
recording,
audioFile,
isLoading,
transcript
};
};

useEffectの部分がWhisperを用いた文字起こしの機能になります。

忘れずにOpenAIのAPIキーも環境変数に設定しておきましょう。

/.env.local
NEXT_PUBLIC_OPENAI_API_KEY=

page.tsxの方も編集。

/src/app/page.tsx
"use client";
import { useWhisperHook } from "./utils/whisperHook";

const Page = () => {
const { startRecording, stopRecording, recording, audioFile, isLoading, transcript } = useWhisperHook();

return (
<div>
<button onClick={startRecording}>start</button>
<button onClick={stopRecording}>stop</button>
{audioFile && <audio src={URL.createObjectURL(audioFile)} controls />}
{recording && <div>recording...</div>}
{isLoading && <div>loading...</div>}
{transcript && <div>{transcript}</div>}
</div>
);
};

export default Page;

というわけで完成!

image4

ページ自体は先ほどと変わりませんが、録音してみると...

image5

「loading...」と表示され...

image6

文字起こしされた文章が表示されました!🎉🎉🎉🎉🎉🎉

というわけでWhisperを用いた文字起こし機能完成です!

欠点

ここまで紹介しておいて何なのですが、実はwhipserには重大な欠点があります。 それは会話が記録されていない音声ファイルを投げると変な文字列が返ってくるという点です。

実際に試していただくけるとわかると思うのですが、5秒ほどの特に会話など文字起こしする内容のない音声を録音して文字起こしをかけると「本日はご覧いただきありがとうございます。」や「ご視聴ありがとうございました。」などよくわからない文字列が返ってきてしまいます。

apiに投げる前に無音区間を削除する処理などをかませると改善するのかなぁなどと対策を考えている今日この頃です。

Whisperで文字起こしをする際には頭の片隅に覚えておいてください。

終わり

今回はNextjsでWhisperを用いた文字起こしの実装を紹介しました。

ここまで書いて思ったのですがこれNextjsでなくてReactで良かったですね。

Whisperの文字起こしの精度についてですが、他のサービスと比べてもかなり良い方だと思います。ただ複数人で話している状況ではもう一歩かなという感じもします。 先ほどあげた欠点のこともあるので、今後に期待!というところですね。

ではまた!