OpenAI Whisper Large V3 Turboモデルでリアルタイム文字起こしを試してみた

OpenAI Whisper Large V3 Turboモデルでリアルタイム文字起こしを試してみた

Clock Icon2024.11.07

AWS事業本部コンサルティング部の石川です。OpenAIが最近リリースしたWhisper Large V3 Turboは、従来のWhisper Large V3モデルを最適化したバージョンです。本日は、OpenAI Whisper Large V3 Turboモデルでリアルタイム文字起こしに挑戦します。

https://dev.classmethod.jp/articles/openai-whisper-large-v3-turbo-faster-whisper/

先日、動画ファイルから文字起こしを試したブログを紹介した際に、私のMacBookのCPUでも300秒の動画を94秒で文字起こしできたため、これはニアリアルタイムに文字起こしできるのでは?と気になり、動くものができたのでその試行錯誤の過程をご紹介します。

OpenAI Whisper Large V3 Turboモデルは実際どうだったか?

まずは、前回の検証結果を簡単に解説します。Whisper Large V3 Turboは、Whisper Large V3と比較して約3.16倍高速に動作しました。これは、デコーダー層の削減による効果が顕著に表れています。

Whisper Large V3 Turboモデルは、処理速度と精度のバランスを優れた形で実現しています。特に、高速化による恩恵は大きく、リアルタイム音声認識や大量の音声データ処理など、幅広いアプリケーションでの活用が期待できます。一方で、わずかな精度の低下は存在するため、極めて高い精度が要求される特定のユースケースでは、従来のWhisper Large V3モデルの使用を検討する必要があるかもしれません。

本来、Whisperの使用にはGPUが推奨されますが、お手持ちのCPUでも300秒の動画を94秒で文字起こしできるという結果は、非常に魅力的と言えるでしょう。

検証環境

環境

MacBook(M1)、10CPU、32GB

今回は、CPUのみ利用します。前回は、Dockerでしたが、今回はマイクから直接音声を拾うため、MacBook本体でプログラムを動かします。

% system_profiler SPHardwareDataType
Hardware:
    Hardware Overview:
      Model Name: MacBook Pro
      Model Identifier: MacBookPro18,4
      Chip: Apple M1 Max
      Total Number of Cores: 10 (8 performance and 2 efficiency)
      Memory: 32 GB
      :

必要なパッケージのインストール

では、これに対してPythonのパッケージをインストールします。

  • numpy==1.26.2
  • onnxruntime==1.16.3
  • faster-whisper
% pip install numpy==1.26.2 onnxruntime==1.16.3 faster-whisper
Collecting numpy==1.26.2
  Downloading numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl.metadata (61 kB)
Collecting onnxruntime==1.16.3
  Downloading onnxruntime-1.16.3-cp310-cp310-macosx_11_0_arm64.whl.metadata (4.3 kB)
:
:
:
Downloading numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl (14.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14.0/14.0 MB 63.9 MB/s eta 0:00:00
Downloading onnxruntime-1.16.3-cp310-cp310-macosx_11_0_arm64.whl (6.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.2/6.2 MB 84.3 MB/s eta 0:00:00
Downloading faster_whisper-1.0.3-py3-none-any.whl (2.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.0/2.0 MB 74.5 MB/s eta 0:00:00
Installing collected packages: numpy, onnxruntime, faster-whisper
  Attempting uninstall: numpy
    Found existing installation: numpy 1.26.4
    Uninstalling numpy-1.26.4:
      Successfully uninstalled numpy-1.26.4
  Attempting uninstall: onnxruntime
    Found existing installation: onnxruntime 1.18.0
    Uninstalling onnxruntime-1.18.0:
      Successfully uninstalled onnxruntime-1.18.0
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
deepsearch-glm 0.26.1 requires numpy<2.0.0,>=1.26.4; python_version >= "3.9" and python_version < "3.13", but you have numpy 1.26.2 which is incompatible.
Successfully installed faster-whisper-1.0.3 numpy-1.26.2 onnxruntime-1.16.3

[notice] A new release of pip is available: 24.2 -> 24.3.1
[notice] To update, run: pip install --upgrade pip

一部エラーが出ていますが、影響がなさそうなので続行します。

音声関連(PyAudio)で必要なPortAudioライブラリ事前にインストールします。

  • portaudio
% brew install portaudio
==> Auto-updating Homebrew...
Adjust how often this is run with HOMEBREW_AUTO_UPDATE_SECS or disable with
HOMEBREW_NO_AUTO_UPDATE. Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
==> Auto-updated Homebrew!
Updated 3 taps (homebrew/bundle, homebrew/core and homebrew/cask).
==> New Formulae
decasify
==> New Casks
acronis-true-image-cleanup-tool           font-recursive-desktop                    viz
default-handler                           hyperconnect
font-eldur                                jet-pilot

You have 9 outdated formulae and 1 outdated cask installed.

==> Downloading https://ghcr.io/v2/homebrew/core/portaudio/manifests/19.7.0-1
####################################################################################################################### 100.0%
==> Fetching portaudio
==> Downloading https://ghcr.io/v2/homebrew/core/portaudio/blobs/sha256:e5f86790b92dc68b3e1770cffb14dcfa42ed8cb2496b1ae9fb30c2
####################################################################################################################### 100.0%
==> Pouring portaudio--19.7.0.arm64_sonoma.bottle.1.tar.gz
🍺  /opt/homebrew/Cellar/portaudio/19.7.0: 34 files, 545.7KB
==> Running `brew cleanup portaudio`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).

最後に、PythonのPyAudioパッケージをインストールします。

  • pyaudio

Pythonで音声入力を扱う方法はいくつかあるのですが、最もシンプルに他のプラットフォームやOSの移植性を考慮し、PyAudioというライブラリのみを利用することにしました。PyAudioをインストールする際には以下のオプションを指定しています。

  • --global-option='build_ext'

    • ビルド拡張モジュールを使用することを指定します。
  • --global-option='-I/opt/homebrew/include'

    • コンパイラに追加のインクルードディレクトリを指定します。
  • --global-option='-L/opt/homebrew/lib'

    • リンカーに追加のライブラリディレクトリを指定します。
% pip install --global-option='build_ext' --global-option='-I/opt/homebrew/include' --global-option='-L/opt/homebrew/lib' pyaudio
DEPRECATION: --build-option and --global-option are deprecated. pip 25.0 will enforce this behaviour change. A possible replacement is to use --config-settings. Discussion can be found at https://github.com/pypa/pip/issues/11859
WARNING: Implying --no-binary=:all: due to the presence of --build-option / --global-option.
Collecting pyaudio
  Using cached PyAudio-0.2.14.tar.gz (47 kB)
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing metadata (pyproject.toml) ... done
Building wheels for collected packages: pyaudio
  WARNING: Ignoring --global-option when building pyaudio using PEP 517
  Building wheel for pyaudio (pyproject.toml) ... done
  Created wheel for pyaudio: filename=PyAudio-0.2.14-cp310-cp310-macosx_14_0_arm64.whl size=25701 sha256=114f378c7cbf85f54da8628d7bf7894e43476564a3c06ea23435f5e3c0421e5a
  Stored in directory: /Users/ishikawa.satoru/Library/Caches/pip/wheels/d6/21/f4/0b51d41ba79e51b16295cbb096ec49f334792814d545b508c5
Successfully built pyaudio
Installing collected packages: pyaudio
Successfully installed pyaudio-0.2.14

試作プログラム(失敗)

最初に作成したのが、以下のプログラムです。マイクから音声を5秒間キャプチャで、model.transcribe()に渡して文字起こしします。実際に実行すると、バッファーオーバフローになってなります。その原因は、音声を文字起こししている最中に音声入力のバッファが溢れてしまうためです。

前回の検証で、model.transcribe()は遅延評価で動くので、勝手に並列実行するかと期待しましたがやはりだめでした。

import pyaudio
import wave
import numpy as np
from faster_whisper import WhisperModel

# Audio recording parameters
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
RECORD_SECONDS = 5

# Initialize Whisper model
model = WhisperModel(model_size_or_path="deepdml/faster-whisper-large-v3-turbo-ct2", device="cpu", compute_type="int8")

# Initialize PyAudio
p = pyaudio.PyAudio()

# Open stream
stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=RATE,
                input=True,
                frames_per_buffer=CHUNK)

print("* Recording")

while True:
    # Record audio for RECORD_SECONDS
    frames = []
    for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
        data = stream.read(CHUNK)
        frames.append(data)

    # Convert audio data to numpy array
    audio_data = np.frombuffer(b''.join(frames), dtype=np.int16)
    audio_data = audio_data.astype(np.float32) / 32768.0

    # Transcribe audio
    segments, _ = model.transcribe(audio_data, beam_size=5)

    # Print transcription
    for segment in segments:
        print(f"[{segment.start:.2f}s -> {segment.end:.2f}s] {segment.text}")

# Stop and close the stream
stream.stop_stream()
stream.close()
p.terminate()

試作プログラム(最終版)

そこで、音声入力と文字起こしを並列で実行できるようにスレッドを分けて、かつ入力した音声が録音した音声が順序通り文字起こしされるようにFIFOのQueueは介して処理するように見直しました。いわゆる、Producer-Consumerパターンです。ここまで、1時間程度でできたのですが、ここからが長かった。

Whisperは人の声は正しく文字起こしでできますが、マイクに入るノイズを誤って「ご視聴ありがとうございました」や「Thank you」などのフィラーワードが脈絡もなく入ることがわかりました。人の声を認識するロジックを追加したり、それでも防げなかった場合はフィラーワードの文字列を取り除く処理を追加しました。

import pyaudio
import numpy as np
from faster_whisper import WhisperModel
import threading
import queue
from scipy.signal import butter, lfilter
import re

# Audio recording parameters
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
RECORD_SECONDS = 5

# VAD Parameter
FRAME_SIZE = 1024
SILENCE_THRESHOLD = 0.01
SPEECH_THRESHOLD = 0.1
MIN_SPEECH_FRAMES = 5

# Filler words pattern
PATTERNS = [
    r'^Thank you(?:\.)?$',
    r'^Thanks for listening(?:\.)?$',
    r'^That\'s all(?:\.)?$',
    r'^That\'s it(?:\.)?$',
    r'^Goodbye(?:\.)?$',
    r'^See you(?:\.)?$',
    r'^ご視聴ありがとうございました(?:。)?$',
    r'^ご清聴ありがとうございました(?:。)?$',
    r'^ありがとうございました(?:。)?$',
    r'^以上です(?:。)?$',
    r'^これで終わります(?:。)?$',
    r'^お疲れ様でした(?:。)?$'
]

# Initialize Whisper model
model = WhisperModel(model_size_or_path="deepdml/faster-whisper-large-v3-turbo-ct2", device="cpu", compute_type="int8")

# Create a queue for audio frames
audio_queue = queue.Queue()

# Flag to signal threads to stop
stop_flag = threading.Event()

def butter_bandpass(lowcut, highcut, fs, order=5):
    nyq = 0.5 * fs
    low = lowcut / nyq
    high = highcut / nyq
    b, a = butter(order, [low, high], btype='band')
    return b, a

def butter_bandpass_filter(data, lowcut, highcut, fs, order=5):
    b, a = butter_bandpass(lowcut, highcut, fs, order=order)
    y = lfilter(b, a, data)
    return y

def is_speech(audio_data):
    # Apply Bandpass filter (300-3000 Hz)
    filtered = butter_bandpass_filter(audio_data, 300, 3000, RATE)

    # Frame energies
    frame_energies = np.array([
        np.sum(filtered[i:i+FRAME_SIZE]**2)
        for i in range(0, len(filtered), FRAME_SIZE)
    ])

    # Frame energies threshold
    speech_frames = frame_energies > SPEECH_THRESHOLD

    # Speech Frame Count
    speech_frame_count = np.sum(speech_frames)

    return speech_frame_count >= MIN_SPEECH_FRAMES

def audio_capture():
    p = pyaudio.PyAudio()
    stream = p.open(format=FORMAT,
                    channels=CHANNELS,
                    rate=RATE,
                    input=True,
                    frames_per_buffer=CHUNK)

    print("Recording...")

    while not stop_flag.is_set():
        frames = []
        for _ in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
            if stop_flag.is_set():
                break
            data = stream.read(CHUNK)
            frames.append(data)

        audio_data = np.frombuffer(b''.join(frames), dtype=np.int16)
        audio_data = audio_data.astype(np.float32) / 32768.0
        audio_queue.put(audio_data)

    stream.stop_stream()
    stream.close()
    p.terminate()

def transcribe_audio():
    while not stop_flag.is_set():
        try:
            audio_data = audio_queue.get(timeout=1)

            # Using VAD(Voice Activity Detection)
            if not is_speech(audio_data):
                # print("No speech detected, skipping transcription.")
                continue

            segments, info = model.transcribe(audio_data, beam_size=5)
            #segments, info = model.transcribe(audio_data, beam_size=5, language="ja")
            #segments, info = model.transcribe(audio_data, beam_size=5, language="en")
            #print("Transcribe Completed.")
            #print("Detected language '%s' with probability %f" % (info.language, info.language_probability))

            # Filtering short or filler segment
            filtered_segments = [
                segment for segment in segments
                if (segment.end - segment.start) >= 0.5 and  # 0.5 seconds
                not any(re.match(pattern, segment.text.strip()) for pattern in PATTERNS)
            ]

            for segment in filtered_segments:
                # print(f"[{segment.start:.2f}s -> {segment.end:.2f}s] {segment.text}")
                print(f"{segment.text}")

        except queue.Empty:
            continue

def main():
    # Create and start threads
    capture_thread = threading.Thread(target=audio_capture)
    transcribe_thread = threading.Thread(target=transcribe_audio)

    capture_thread.start()
    transcribe_thread.start()

    try:
        # Keep the main thread running
        while True:
            pass
    except KeyboardInterrupt:
        print("Stopping...")
        stop_flag.set()

    # Wait for threads to finish
    capture_thread.join()
    transcribe_thread.join()

    print("Program terminated.")

if __name__ == "__main__":
    main()

性能評価

検証用の動画

検証用の動画は、権利関係があると面倒なので自分の登壇動画の冒頭を再生して、それをこのプログラムで文字起こししています。

https://dev.classmethod.jp/articles/20200619-devio2020-connect-primer-of-dwh/

性能評価

音声を再生して、何秒で文字が出るかを計測しました。

  • 最短で7.5秒
  • 最長で13秒

5分毎に音声を文字起こししているのでだいたいそれくらいになります。14秒は体感ではもっと長く感じられっるため、少し前の会話が文字で現れるといった印象です。

以下が文字起こしした結果です。

% python l3turbort.py
Recording...
こんにちは 私はクラスメソッドでソリューションアーキテクトをしております
石川悟と言います。このセッションでは、データ分析基盤を指す
アサイル技術DWH再入門というタイトルでお話しします
簡単に自己紹介をさせていただきます
データ物質基盤の設計や開発 コンサルの 傍ら デベロティング
Developers.ioでデータ分析基盤関連の技術ブログを書いています。
AWSの認定エンジニア Snowflakeの認定エンジニアとして
そして本日はDWHについてお話しいたします
アジェンダです
本日のテーマは
DWHです。分かりそうで分からない。何のために導入して、どのために導入するのか。
どのようにデータを管理・蓄積するのか、どうやって利用するのか、普通のDBのデータを使っています。
と何が違ってアーキテクチャ はどうなっているかなどコンサル
の現場で
よく尋ねられる疑問について解説したいと思います
ここではDWHの概要と目的 導入の
背景について解説します
DWHとは何かを理解し、DWHの目的を確認したいと思います。
DWHはデータウェアハウスの略で
データの倉庫を意味します
つまりDWAの倉庫を意味します
Hは分析しやすく検索できるように 蓄積したデータベースのことを
:
:

なお、YouTubeの文字起こしは以下になります。

こんにちは。私はクラスメソッドでソリューションアーキテクトをしております、石川覚と言います。
このセッションでは、データ分析基盤を支える技術、DWH再入門というタイトルでお話します。
簡単に自己紹介をさせていただきます。普段はデータ分析基盤の設計や開発、コンサルの傍らDevelopersIOで、データ分析基盤関連の技術ブログを書いています。
AWSの認定エンジニア、Snowflakeの認定エンジニアとして、本日はDWHについてお話しいたします。
アジェンダです。 本日のテーマはDWHです。
わかりそうでわからない、何のために導入してどのようにデータを管理、蓄積するのか、どうやって利用するのか、
普通のDBと何が違って、アーキテクチャはどうなっているかなど、コンサルの現場でよく尋ねられる疑問について解説したいと思います。
ここでは、DWHの概要と目的、導入の背景について解説します。
DWHとは
DWHとは何かを理解し、DWHの目的を確認したいと思います。
DWHは、データウェアハウスの略で、直訳するとデータの倉庫を意味します。
つまり、DWHは分析しやすく検索できるように蓄積したデータベースのことを表します。

CPU使用率

音声のキャプチャと文字起こしを実行しているタイミングで、CPU使用率が波打っているのが確認できます。

openai-whisper-large-v3-turbo-realtime-1

最後に

このブログでは、OpenAI Whisper Large V3 Turboモデルを使用したリアルタイム文字起こしの実験と性能評価について解説しました。検証環境としてMacBook(M1)を使用し、Pythonスクリプトを実行しています。

実験では、文字起こしの精度に関して、5秒ごとに音声を区切って処理するため、不自然な区切りが生じています。この5秒の区切り(RECORD_SECONDS)を長くすることで不自然さは解消されますが、文字起こしの遅延が増すというトレードオフが発生します。しかし、海外のカンファレンスをリアルタイムで視聴する場合、単語の聞き落としなどを文字で確認できるのは有用です。音声をつなげて無音部分でチャンキングできれば、さらに精度が向上すると予想されます。実用的には10秒や15秒などの区切りが適しているかもしれません。

最後まで課題となったのは、脈絡なく挿入されるフィラーワードですが、PCやマイクを叩くなどの動作がなければ発生しないレベルまで改善しました。文字起こし時に言語の種類を自動認識しますが、これを事前に指定しても、フィラーワードがその言語に変わるだけでした。フィラーワードの出現は、モデルの学習データに起因する特性であり、不具合というよりは仕様に近いものと考えられます。

デバッグを兼ねて打ち合わせ中にこのプログラムを実行したところ、聞き取れなかった部分や少し前に話された内容を確認するのに意外と便利でした。この実験結果は、リアルタイム音声認識など、幅広いアプリケーションでの活用可能性を示唆しています。ぜひ試していただければと思います。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.