🦁

OpenAI の Speech to Text を試す

2024/06/19に公開

はじめに

この記事では OpenAI の Speech to Text を試してみます。

https://platform.openai.com/docs/guides/speech-to-text

Speech to Text とは

Speech to Text とは音声をテキストに変換する技術です。略して STT などと呼ばれたり、音声認識と呼ばれることがあります。

Speech to Text は、Google の Speech-to-Text や AWS の Transcribe などが有名です。本記事では、OpenAI の Speech to Text を利用して音声をテキストに変換してみます。

OpenAI の Speach to Text は音声をテキストに変換する API です。2つの機能を提供しています。

  • transcriptions
    • 音声ファイルをテキストに変換します。
  • translations
    • 音声ファイルを翻訳します。

音声認識させるためにはファイルアップロードが必要です。ファイルのアップロードは現在 25 MB に制限されており、サポートされているファイルはこちらです。

  • mp4
  • mpeg
  • mpga
  • m4a
  • wav
  • webm

サポートされている言語が記載されています。

https://github.com/openai/whisper#available-models-and-languages

作業環境の準備

以降利用する作業環境を構築しておきます。

長いので折りたたんでおきます。

package.json を作成

package.json を作成します。

$ mkdir -p node-stt-sample
$ cd node-stt-sample
$ pnpm init

package.json を変更します。

package.json
{
  "name": "mute-sample",
  "version": "1.0.0",
  "description": "",
- "main": "index.js",
- "scripts": {
-   "test": "echo \"Error: no test specified\" && exit 1"
- },
- "keywords": [],
- "author": "",
- "license": "ISC"
+ "main": "index.ts",
+ "scripts": {
+   "typecheck": "tsc --noEmit",
+   "dev": "vite-node index.ts",
+   "build": "tsc"
+ },
+ "keywords": [],
+ "author": "",
}

TypeScript & vite-node をインストール

TypeScript と vite-node をインストールします。補足としてこちらの理由のため ts-node ではなく vite-node を利用します。

$ pnpm install -D typescript vite-node @types/node

TypeScriptの設定ファイルを作成

tsconfig.json を作成します。

$ npx tsc --init

tsconfig.json を上書きします。

tsconfig.json
{
  "compilerOptions": {
    /* Base Options: */
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "ES2022",
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "isolatedModules": true,

    /* Strictness */
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "checkJs": true,

    /* Bundled projects */
    "noEmit": true,
    "outDir": "dist",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "preserve",
    "incremental": true,
    "sourceMap": true,
  },
  "include": ["**/*.ts", "**/*.js"],
  "exclude": ["node_modules", "dist"]
}

git を初期化します。

$ git init

.gitignore を作成します。

$ touch .gitignore
.gitignore
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build
dist/

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

動作確認コードを作成

動作を確認するためのコードを作成します。

$ touch index.ts
index.ts
console.log('Hello, World');

型チェック

型チェックします。

$ pnpm run typecheck

動作確認

動作確認を実施します。

$ pnpm run dev

Hello, World

コミットします。

$ git add .
$ git commit -m "作業環境構築"

OpenAI API キーを取得

OpenAI API キーの取得方法はこちらを参照してください。

https://zenn.dev/hayato94087/articles/85378e1f7bc0e5#openai-の-apiキーの取得

環境変数の設定

環境変数に OpenAI キーを追加します。<your-api-key> に自身の API キーを設定してください。

$ touch .env
.env
# OPENAI_API_KEY は OpenAI の API キーです。
OPENAI_API_KEY='<your-api-key>'

Node.js で環境変数を利用するために dotenv をインストールします。

$ pnpm i -D dotenv

コミットします。

$ touch .env.example
.env.example
# OPENAI_API_KEY は OpenAI の API キーです。
OPENAI_API_KEY='<your-api-key>'
$ git add .
$ git commit -m "環境変数を設定"

OpenAI のパッケージをインストール

openai パッケージをインストールします。

$ pnpm install -D openai

コミットします。

$ git add .
$ git commit -m "パッケージをインストール"

Transcriptions

Transcriptions API は入力された音声ファイルをテキストに変換します。

簡易的な利用

ここでは簡易的に Speech to Text を利用します。

$ touch transcriptions-demo01.ts
transcriptions-demo01.ts
import fs from "fs";
import OpenAI from "openai";

const openai = new OpenAI();

async function main() {
  const transcription = await openai.audio.transcriptions.create({
    file: fs.createReadStream("audio/demo1.mp3"),
    model: "whisper-1",
  });

  console.log(transcription);
}
main();

音声ファイルを audio/demo1.mp3 に保存しておきます。

動作確認のための音声は自身で録音してもよいですし、こちらから取ってきても良いです。

https://soundeffect-lab.info/sound/voice/game.html

今回は「軽くひねってあげましょう」という音声を利用します。

$ mkdir audio

動作確認します。しっかり認識しています。

$ pnpm vite-node transcriptions-demo01.ts

{ text: '軽くひねってあげましょう' }

パラメータの追加

さらにパラメータを追加してみます。

  • response_format は出力形式を指定します。デフォルトは json です。今回は verbose_json を設定し詳細データを吐き出すようにします。

  • temperature はランダム性を指定します。デフォルトは 0 です。1.0 に近づくほどランダム性が増します。

$ touch transcriptions-demo02.ts
transcriptions-demo02.ts
import fs from "fs";
import OpenAI from "openai";

const openai = new OpenAI();

async function main() {
  const transcription = await openai.audio.transcriptions.create({
    file: fs.createReadStream("audio/demo1.mp3"),
    model: "whisper-1",
    response_format: "verbose_json", // デフォルトはJSON。
    temperature: 0.8 // デフォルトは0。1.0に近づくほどランダム性が増す
  });

  console.log(transcription);
}
main();

動作確認します。

  • verbose_json により詳細データが出力されています。
  • temperature0.8 になっていますが、今回は内容が簡単なため変化は見えません。
$ pnpm vite-node transcriptions-demo02.ts

{
  task: 'transcribe',
  language: 'japanese',
  duration: 1.7400000095367432,
  text: '軽くひねってあげましょう',
  segments: [
    {
      id: 0,
      seek: 0,
      start: 0,
      end: 2,
      text: '軽くひねってあげましょう',
      tokens: [Array],
      temperature: 0.800000011920929,
      avg_logprob: -0.22753798961639404,
      compression_ratio: 0.7659574747085571,
      no_speech_prob: 0.07101698219776154
    }
  ]
}

更に別のパラメーtimestamp_granularities を追加してみます。すると、時間単位での情報が追加されています。

$ touch transcriptions-demo03.ts
transcriptions-demo03.ts
import fs from "fs";
import OpenAI from "openai";

const openai = new OpenAI();

async function main() {
  const transcription = await openai.audio.transcriptions.create({
    file: fs.createReadStream("audio/demo1.mp3"),
    model: "whisper-1",
    response_format: "verbose_json", // デフォルトはJSON。
    temperature: 0.8, // デフォルトは0。1.0に近づくほどランダム性が増す
    timestamp_granularities: ["word"]
  });

  console.log(transcription);
}
main();

動作確認すると、単語単位での時間情報が追加されています。

pnpm vite-node transcriptions-demo03.ts

{
  task: 'transcribe',
  language: 'japanese',
  duration: 1.7400000095367432,
  text: '軽くひねってあげましょう',
  words: [
    { word: '軽', start: 0, end: 0.2800000011920929 },
    { word: 'く', start: 0.2800000011920929, end: 0.5400000214576721 },
    { word: 'ひ', start: 0.5400000214576721, end: 0.7200000286102295 },
    { word: 'ね', start: 0.7200000286102295, end: 0.8999999761581421 },
    { word: 'って', start: 0.8999999761581421, end: 1.059999942779541 },
    { word: 'あ', start: 1.059999942779541, end: 1.1799999475479126 },
    { word: 'げ', start: 1.1799999475479126, end: 1.3200000524520874 },
    { word: 'ましょう', start: 1.3200000524520874, end: 1.659999966621399 }
  ]
}

API リファレンス

Transcriptions API の利用についてはこちらに詳しく記載されています。

https://platform.openai.com/docs/api-reference/audio/createTranscription

コミットします。

誤って音声データアップしないように .gitignore に追加します。

.gitignore
audio/*
$ git add .
$ git commit -m "Transcriptions API を試す"

Translations

Translations API は音声ファイルを翻訳します。現状では英語にしか翻訳できないようです。

使ってみる

ここでは実際に使ってみます。

$ touch translations-demo01.ts
translations-demo01.ts
import fs from "fs";
import OpenAI from "openai";

const openai = new OpenAI();

async function main() {
    const translation = await openai.audio.translations.create({
      file: fs.createReadStream("audio/demo1.mp3"),
      model: "whisper-1",
      response_format: "verbose_json", // デフォルトはJSON。
    });

    console.log(translation);
}
main();

動作確認します。「I'll give you a light twist.」に翻訳されました。あってますね。

$ pnpm vite-node translations-demo01.ts  

{
  task: 'translate',
  language: 'english',
  duration: 1.7400000095367432,
  text: "I'll give you a light twist.",
  segments: [
    {
      id: 0,
      seek: 0,
      start: 0,
      end: 2,
      text: " I'll give you a light twist.",
      tokens: [Array],
      temperature: 0,
      avg_logprob: -0.6929762959480286,
      compression_ratio: 0.7777777910232544,
      no_speech_prob: 0.07101698219776154
    }
  ]
}

Chat Completion API を利用した翻訳方法

翻訳する方法としてChat Completion APIを利用する方法もあります。処理の流れはこちらです。

  • Transcriptions API を利用し音声データをテキスト化し音声認識する
  • 音声認識の結果を Chat Completion API により指定した言語で翻訳

このポイントのメリットは、Chat Completion API により後処理を行うことで柔軟な処理ができます。デメリットは複数回 API を実行することになるため、処理時間がかかることです。

では、コードで実装してみます。

$ touch translations-demo02.ts
translations-demo01.ts
import fs from "fs";
import OpenAI from "openai";

const openai = new OpenAI();

async function main() {
  const transcription = await openai.audio.transcriptions.create({
    file: fs.createReadStream("audio/demo1.mp3"),
    model: "whisper-1",
    response_format: "verbose_json", // デフォルトはJSON。
  });

  const language = "英語";

  console.log('音声認識結果\n', transcription?.text ?? "");

  // 専門用語を追加
  const systemPrompt = `あなたは有能なアシスタントです。あなたの任務は入力されたテキストを${language}に翻訳してください。翻訳結果のみを出力死てください。`;

  const completion = await openai.chat.completions.create({
    model: "gpt-4o",
    temperature: 0,
    messages: [
      {
        role: "system",
        content: systemPrompt
      },
      {
        role: "user",
        content: transcription.text
      }
    ]
  });

  console.log('翻訳結果\n', completion?.choices[0]?.message.content);
}
main();

動作確認します。「I'll give you a light twist.」に翻訳されました。あってますね。

$ pnpm vite-node translations-demo02.ts

音声認識結果
 軽くひねってあげましょう
翻訳結果
 Let's give it a little twist.

API リファレンス

Translations API の利用についてはこちらに詳しく記載されています。

https://platform.openai.com/docs/api-reference/audio/createTranslation

コミットします。

$ git add .
$ git commit -m "Translations API を試す"

専門用語の追加

Whisper がデフォルトでテキストに変換できない専門用語を認識できるようにする方法があります。

prompt を利用し修正

1 つ目の方法は prompt を活用する方法です。prompt の引数を追加することで最大 244 トークンまでですが、専門用語を追加できます。例えば以下のような言葉を読み上げて音声データを作成します。音声データの中身は意味の無い内容です。

出版会社の株式会社ホゲホゲMAXが灼熱でぃあぼろすという本を出版しました。この本では魔界の王であるディアボロスが登場し、勇者ポンぽこポコぽんと戦いながら人間界を支配していく物語です。

ここで追加したい専門用語はこちらです。

  • 株式会社ホゲホゲ MAX
  • 灼熱でぃあぼろす
  • 勇者ポンぽこポコぽん

では、実際に prompt を利用し専門用語を追加して音声認識させてみます。

$ touch correction-demo01.ts
correction-demo01.ts
import fs from "fs";
import OpenAI from "openai";

const openai = new OpenAI();

async function main() {
  const transcription = await openai.audio.transcriptions.create({
    file: fs.createReadStream("audio/demo2.wav"),
    model: "whisper-1",
    response_format: "verbose_json", // デフォルトはJSON。
    prompt:"株式会社ホゲホゲMAX, 灼熱でぃあぼろす, 勇者ポンぽこポコぽん", // 専門用語を追加
  });

  console.log(transcription);
}
main();

動作確認をします。専門用語をしっかり認識しています。

$ pnpm vite-node correction-demo01.ts

{
  task: 'transcribe',
  language: 'japanese',
  duration: 14.399999618530273,
  text: '出版会社の株式会社ホゲホゲMAXが 灼熱でぃあぼろすという本を出版しました。 この本では魔界の王であるでぃあぼろすが登場し、 勇者ポンぽこポコポンと戦いながら人間界を支配していく物語です。',
  segments: [
    {
      id: 0,
      seek: 0,
      start: 0,
      end: 6,
      text: '出版会社の株式会社ホゲホゲMAXが 灼熱でぃあぼろすという本を出版しました。',
      tokens: [Array],
      temperature: 0,
      avg_logprob: -0.1343606412410736,
      compression_ratio: 1.4010416269302368,
      no_speech_prob: 0.007006216328591108
    },
    {
      id: 1,
      seek: 0,
      start: 6,
      end: 16,
      text: 'この本では魔界の王であるでぃあぼろすが登場し、 勇者ポンぽこポコポンと戦いながら人間界を支配していく物語です。',
      tokens: [Array],
      temperature: 0,
      avg_logprob: -0.1343606412410736,
      compression_ratio: 1.4010416269302368,
      no_speech_prob: 0.007006216328591108
    }
  ]
}

ちなみに、専門用語を追加しない場合の認識結果も確認します。

$ touch correction-demo02.ts
correction-demo02.ts
import fs from "fs";
import OpenAI from "openai";

const openai = new OpenAI();

async function main() {
  const transcription = await openai.audio.transcriptions.create({
    file: fs.createReadStream("audio/demo2.wav"),
    model: "whisper-1",
    response_format: "verbose_json", // デフォルトはJSON。
  });

  console.log(transcription);
}
main();

専門用語を認識してくれません。

$ pnpm vite-node correction-demo02.ts

{
  task: 'transcribe',
  language: 'japanese',
  duration: 14.399999618530273,
  text: '出版会社の株式会社ホゲホゲマックスが 灼熱ディアブロスという本を出版しました この本では魔界の王であるディアブロスが登場し 勇者ポンポコポコポンと戦いながら人間界を支配していく物語です',
  segments: [
    {
      id: 0,
      seek: 0,
      start: 0,
      end: 6.119999885559082,
      text: '出版会社の株式会社ホゲホゲマックスが 灼熱ディアブロスという本を出版しました',
      tokens: [Array],
      temperature: 0,
      avg_logprob: -0.11454080790281296,
      compression_ratio: 1.4308511018753052,
      no_speech_prob: 0.007779653649777174
    },
    {
      id: 1,
      seek: 0,
      start: 6.119999885559082,
      end: 16.040000915527344,
      text: 'この本では魔界の王であるディアブロスが登場し 勇者ポンポコポコポンと戦いながら人間界を支配していく物語です',
      tokens: [Array],
      temperature: 0,
      avg_logprob: -0.11454080790281296,
      compression_ratio: 1.4308511018753052,
      no_speech_prob: 0.007779653649777174
    }
  ]
}

Chat Completion API による後処理

2 つ目の方法は、Chat Completion API を利用し、後処理を行う形で認識結果を修正する方法です。

$ touch correction-demo03.ts
correction-demo03.ts
import fs from "fs";
import OpenAI from "openai";

const openai = new OpenAI();


async function main() {
  const transcription = await openai.audio.transcriptions.create({
    file: fs.createReadStream("audio/demo2.wav"),
    model: "whisper-1",
    response_format: "verbose_json", // デフォルトはJSON。
  });


  console.log('音声認識結果(補正前)\n', transcription?.text ?? "");

  // 専門用語を追加
  const systemPrompt = "あなたは株式会社ほげほげMAXの有能なアシスタントです。あなたの任務は作成されたテキストのスペルの不一致を修正することです。次の言葉が正しく綴られていることを確認してください。株式会社ホゲホゲMAX, 灼熱でぃあぼろす, 勇者ポンぽこポコぽん。必要な句読点(ピリオド、カンマ、大文字など)のみを追加し、提供された文脈のみを使用してください。";

  const completion = await openai.chat.completions.create({
    model: "gpt-4o",
    temperature: 0,
    messages: [
      {
        role: "system",
        content: systemPrompt
      },
      {
        role: "user",
        content: transcription.text
      }
    ]
  });

  console.log('音声認識結果(補正後)\n', completion?.choices[0]?.message.content);
}
main();
$ pnpm vite-node correction-demo03.ts

音声認識結果(補正前)
 出版会社の株式会社ホゲホゲマックスが 灼熱ディアブロスという本を出版しました この本では魔界の王であるディアブロスが登場し 勇者ポンポコポコポンと戦いながら人間界を支配していく物語です

音声認識結果(補正後)
 出版会社の株式会社ホゲホゲMAXが『灼熱でぃあぼろす』という本を出版しました。この本では魔界の王であるディアブロスが登場し、勇者ポンぽこポコぽんと戦いながら人間界を支配していく物語です。

コミットします。

$ git add .
$ git commit -m "専門用語を追加"

まとめ

この記事では OpenAI の Speech to Text を試してみました。

作業リポジトリ

https://github.com/hayato94087/node-stt-sample

Discussion