OpenAI Whisperによる日本語の音声・動画ファイルやマイクの文字起こしツール
【概要】
OpenAI Whisperで音声・動画やマイクの音声をテキスト化する。Windows環境での実行手順、コード、実験アイデアを含む。【その他情報】
現在,声の大きさやトーンを解析し議論が白熱した「熱量の高い部分」の発言を自動で赤字強調表示する機能はなくしています(調整中)。
【目次】
【関連する外部ページ】
https://www.kkaneko.jp/cc/dev/aiassist.html(NVIDIA CUDA、Visual Studio、Cursor等の追加インストールの解説)
【サイト内の関連情報】
https://www.kkaneko.jp/ai/labo/index.html(人工知能のプログラム実行体験と探求)
プログラム利用ガイド
1. このプログラムの利用シーン
このツールは、音声や動画ファイルやマイクの日本語文字起こし,字幕作成を行う。講義の文字起こし、動画コンテンツの字幕作成、会議の議事録作成など、音声をテキスト化する様々な場面で活用できる。
2. 主な機能
- 音声・動画ファイルの日本語文字起こし: 複数のファイルを選択して文字起こしを実行できる
- リアルタイムマイク入力認識: マイクからの音声をリアルタイムで日本語に変換し、画面に表示する
- 複数の出力形式対応: SRT(字幕ファイル)、TXT(プレーンテキスト)、CSV(表形式)の3つの形式で保存可能
- 日本語認識: Faster-Whisperによる日本語音声の認識
3. 基本的な使い方
- ファイル文字起こし:
「ファイル選択」または「フォルダ選択」ボタンでファイルを選択し、出力形式(SRT/TXT/CSV)を指定して「処理を開始して保存...」ボタンを押す。保存先フォルダを選択すると自動的に文字起こしが開始される。
- リアルタイム認識:
「マイク入力開始」ボタンを押すと、マイクからの音声がリアルタイムで文字に変換され、画面下部のテキストエリアに表示される。「停止」ボタンで終了する。
- 結果の確認:
文字起こし結果は画面下部のテキストエリアに表示され、右クリックでコピーや全選択が可能である。
4. 便利な機能
- モデル選択: 用途に応じて音声認識モデル(tiny/base/small/medium/large-v3/large-v3-turbo)を選択可能。精度重視なら「large-v3」、速度と精度の両立なら「large-v3-turbo」、速度重視なら「small」を推奨
- バッチ処理: フォルダ選択により、フォルダ内の対応ファイルを一括で処理できる
- 処理中断機能: 長時間の処理中でも「中断」ボタンでいつでも処理を停止可能
- 音声活動検出: 無音区間を自動検出
事前準備
ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.html で詳しく解説しているので、必要に応じて参照する。
Python 3.12 のインストール
Pythonのインストールを行い、Pythonのプログラムを実行する環境を整える。扱う環境は、Windows搭載パソコンである。金子研究室では、Python 3.12.10を推奨する。
[Windows での Python 3.12 のインストール手順を見るには、ここをクリック]
Windows での Python 3.12 のインストール
以下のいずれかの方法でPython 3.12をインストールする。Pythonがインストール済みの場合、この手順は不要である。
方法 1:winget によるインストール
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。
--scope machine を指定することで、システム全体(全ユーザー向け)にインストールされる。このオプションの実行には管理者権限が必要である。インストール完了後、コマンドプロンプトを再起動するとPATHが反映される。
REM Python 3.12 をシステム領域にインストール
winget install --id Python.Python.3.12 -e --scope machine --silent --accept-source-agreements --accept-package-agreements --override "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0 Include_pip=1 Include_launcher=1 InstallLauncherAllUsers=1 TargetDir=\"C:\Program Files\Python312\""
REM Python と Scripts を PATH 先頭に追加
powershell -NoProfile -Command "$p='C:\Program Files\Python312'; $s=\"$p\Scripts\"; $c=[Environment]::GetEnvironmentVariable('Path','Machine'); if((Test-Path $p) -and (';'+$c+';' -notlike \"*;$p;*\") -and (';'+$c+';' -notlike \"*;$s;*\")){[Environment]::SetEnvironmentVariable('Path',\"$p;$s;$c\",'Machine')}"
方法 2:インストーラーによるインストール
- Python公式サイト(https://www.python.org/downloads/)にアクセスし、「Download Python 3.x.x」ボタンからWindows用インストーラーをダウンロードする。
- ダウンロードしたインストーラーを実行する。
- 初期画面の下部に表示される「Add python.exe to PATH」にチェックを入れてから「Customize installation」を選択する。このチェックを入れ忘れると、コマンドプロンプトから
pythonコマンドを実行できない。 - 「Install Python 3.xx for all users」にチェックを入れ、「Install」をクリックする。
インストールの確認
コマンドプロンプトで以下を実行する。
python --version
バージョン番号(例:Python 3.12.x)が表示されればインストール成功である。「'python' は、内部コマンドまたは外部コマンドとして認識されていません。」と表示される場合は、インストールが正常に完了していない。
Python の開発環境 Visual Studio Code のインストールと Python 用の設定
Python の開発環境Visual Studio Code(プログラムを編集するソフトウェア。以下、VS Code)を整える。
[Windows での Visual Studio Code のインストールと Python 用の設定手順を見るには、ここをクリック]
Windows での Visual Studio Code のインストールと Python 用の設定手順
1. VS Code と拡張機能のインストール
以下のコマンドにより,既存の VS Code を削除し,全ユーザー共有の設定で再インストールしたうえで,拡張機能(VS Code に機能を追加するソフトウェア)をまとめて導入する.
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして,コマンド全体をコマンドプロンプトにコピー&ペーストする。
インストールコマンド
REM ============================================================
REM Microsoft Visual Studio Code
REM ============================================================
winget uninstall -e --id Microsoft.VisualStudioCode --silent --disable-interactivity --accept-source-agreements
rmdir /s /q C:\ProgramData\vscode-extensions 2>nul
rmdir /s /q "%APPDATA%\Code" 2>nul
rmdir /s /q "%USERPROFILE%\.vscode" 2>nul
rmdir /s /q "%LOCALAPPDATA%\Microsoft\vscode-update" 2>nul
REM VS Code をシステム領域に新規インストール
winget install --scope machine --id Microsoft.VisualStudioCode -e --silent --accept-source-agreements --accept-package-agreements
REM 全ユーザー共有の拡張機能フォルダ
mkdir C:\ProgramData\vscode-extensions 2>nul
icacls "C:\ProgramData\vscode-extensions" /grant "Everyone:(OI)(CI)M" /T
REM スタートメニューのショートカットを --extensions-dir 付きで再作成
rmdir /s /q "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code" 2>nul
del "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk" 2>nul
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); $lnk.TargetPath='C:\Program Files\Microsoft VS Code\Code.exe'; $lnk.Arguments='--extensions-dir \"C:\ProgramData\vscode-extensions\"'; $lnk.Save()"
REM ショートカットの検証
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); Write-Host 'TargetPath:' $lnk.TargetPath; Write-Host 'Arguments:' $lnk.Arguments"
REM ファイル / フォルダ右クリックの「Code で開く」を登録
reg add "HKLM\SOFTWARE\Classes\*\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\Background\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%V\"" /f
REM --extensions-dir 付きで起動する code.cmd ラッパを作成
REM (%* を echo で書くと対話的 cmd で失われるため、PowerShell で [char]37+'*' を書き出す)
powershell -NoProfile -Command "$pct=[char]37; $q=[char]34; $c='@echo off'+[char]13+[char]10+$q+'C:\Program Files\Microsoft VS Code\bin\code.cmd'+$q+' --extensions-dir '+$q+'C:\ProgramData\vscode-extensions'+$q+' '+$pct+'*'+[char]13+[char]10; [IO.File]::WriteAllText('C:\ProgramData\vscode-extensions\vscode.cmd',$c,[Text.Encoding]::ASCII)"
REM 拡張機能のインストール
set "CODE=C:\Program Files\Microsoft VS Code\bin\code.cmd"
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot-chat
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.python
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.vscode-pylance
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.debugpy
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension MS-CEINTL.vscode-language-pack-ja
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension saoudrizwan.claude-dev
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension rust-lang.rust-analyzer
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension tamasfe.even-better-toml
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension anthropic.claude-code
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension almenon.arepl
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --list-extensions --show-versions
echo === セットアップ完了 ===
2. Python インタプリタの選択
同一マシンに複数の Python がインストールされている場合,VS Code で使用する Python 本体(インタプリタ:Python プログラムを解釈・実行するソフトウェア)を選択する必要がある.
- コマンドパレット(コマンド名で機能を呼び出す VS Code の入力欄)を開く(
Ctrl+Shift+P) Python: Select Interpreterと入力する
- 表示される一覧から,使用する Python(例:
C:\Program Files\Python312\python.exe)を選択する.
Build Tools for Visual Studio 2022 のインストール(Windows 上)
pyaudioやwebrtcvadのインストール時にC拡張のビルドが必要になるため、あらかじめインストールしておく。
管理者権限でコマンドプロンプトを起動する
(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。
以下を実行する。
REM VC++ ランタイム
winget install --scope machine --id Microsoft.VCRedist.2015+.x64 -e --silent --disable-interactivity --force --accept-source-agreements --accept-package-agreements --override "/quiet /norestart"
REM Build Tools + Desktop development with C++(VCTools)+ 追加コンポーネント(一括)
winget install --id Microsoft.VisualStudio.2022.BuildTools --accept-source-agreements --accept-package-agreements ^
--override "--passive --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --add Microsoft.VisualStudio.ComponentGroup.ClangCL --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.Windows11SDK.26100"
--add で追加されるコンポーネント
上記のコマンドでは,まず Build Tools 本体と Visual C++ 再頒布可能パッケージをインストールし,次に setup.exe を用いて以下のコンポーネントを追加している。
VCTools:C++ デスクトップ開発ワークロード(--includeRecommendedにより、MSVC コンパイラ、C++ AddressSanitizer、vcpkg、CMake ツール、Windows 11 SDK 等の推奨コンポーネントが含まれる)VC.Llvm.Clang:Windows 向け C++ Clang コンパイラClangCL:clang-cl ツールセットを含むコンポーネントグループ(MSBuild から Clang を使用するために必要)VC.CMake.Project:Windows 向け C++ CMake ツールWindows11SDK.26100:Windows 11 SDK(ビルド 10.0.26100)
インストール完了の確認
winget list Microsoft.VisualStudio.2022.BuildTools
上記以外の追加のコンポーネントが必要になった場合は Visual Studio Installer で個別にインストールできる。
Windows での FFmpeg インストール手順(手動インストール)
公式ビルド版を使用
-
ダウンロード
- https://www.gyan.dev/ffmpeg/builds/ にアクセス
- 「release builds」セクションの「full」版をダウンロード
- ファイル名例:
ffmpeg-release-full.7z - essentials版ではなくfull版を選択(ffprobeも含まれる)
- ファイル名例:
-
解凍
- ダウンロードした7zファイルを右クリック
- 7-Zip等で解凍(Windows標準では7z非対応のため、7-Zipが必要)
- 7-Zipダウンロード: https://www.7-zip.org/
- 解凍先を
C:\ffmpegにする(推奨)- フォルダ構造:
C:\ffmpeg\bin\ffmpeg.exeとなるように配置
- フォルダ構造:
-
環境変数PATHの設定
- Windowsキー + R → 「sysdm.cpl」と入力してEnter
- 「詳細設定」タブ → 「環境変数」ボタンをクリック
- 「システム環境変数」の「Path」を選択 → 「編集」
- 「新規」をクリック →
C:\ffmpeg\binを追加 - 「OK」を3回クリックして設定を保存
-
動作確認
- コマンドプロンプトを新規で開く(既存のものは閉じる)
- 以下のコマンドを実行:
ffmpeg -version ffprobe -version - バージョン情報が表示されれば成功
必要なライブラリのインストール
管理者権限でコマンドプロンプトを起動する
(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。
以下を実行する。
REM PyTorch をインストール(GPU対応版)
set "CUDA_TAG=cu128"
set "PYTHON_PATH=C:\Program Files\Python312"
"%PYTHON_PATH%\Scripts\pip" install --no-user -U numpy torch torchvision torchaudio --index-url https://download.pytorch.org/whl/%CUDA_TAG%
"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
set DISTUTILS_USE_SDK=1
pip install --no-user -U setuptools wheel
pip install --no-user faster-whisper pyaudio webrtcvad numpy
プログラムコード
概要
このプログラムは、音声・動画ファイルやリアルタイムマイク入力の文字起こし,字幕作成を行う。Faster-Whisper[1]を用いる。音声・動画ファイルはバッチ処理、マイク入力はリアルタイム処理である。結果をSRT、TXT、CSV形式(選択可能)で出力する。
主要技術
Faster-Whisper
OpenAI Whisperモデル[3]のCTranslate2による実装である[1]。従来のopenai-whisperと比較して高速化とメモリ使用量の削減を行う[2]。
CTranslate2
Transformerモデルの推論エンジンである[2]。重み量子化、レイヤー融合、バッチ並び替えを行う。
WebRTC VAD
Googleが開発した音声活動検出技術である[4][5]。ガウシアン混合モデルを使用して音声・無音を分類する。
技術的特徴
- 16kHzサンプリングレート
- VAD感度の動的調整機能(積極性レベル0-3の自動調整)
- 長時間発話時の無音判定フレーム数自動短縮(450msから210msへ)
実装の特色
リアルタイム音声認識では、発話時間に基づくVAD感度の動的調整を実装している。7秒以上の長時間発話を検出した場合、無音判定フレーム数を自動的に短縮し、認識精度を向上させる。
参考文献
[1] SYSTRAN. (2023). faster-whisper: Faster Whisper transcription with CTranslate2. GitHub. https://github.com/SYSTRAN/faster-whisper
[2] OpenNMT. (2024). CTranslate2: A C++ and Python library for efficient inference with Transformer models. https://opennmt.net/CTranslate2/python/ctranslate2.models.Whisper.html
[3] Radford, A., Kim, J. W., Xu, T., Brockman, G., McLeavey, C., & Sutskever, I. (2022). Robust Speech Recognition via Large-Scale Weak Supervision. arXiv preprint arXiv:2212.04356.
[4] Wiseman, J. (2016). py-webrtcvad: Python interface to the WebRTC Voice Activity Detector. GitHub. https://github.com/wiseman/py-webrtcvad
[5] Google. (2025). WebRTC Voice Activity Detection. Chromium Source. https://chromium.googlesource.com/external/webrtc/
ソースコード
# 音声・動画ファイル文字起こしツール
# 特徴技術名: Faster-Whisper
# 出典: SYSTRAN. (2023). faster-whisper: Faster Whisper transcription with CTranslate2. GitHub. https://github.com/SYSTRAN/faster-whisper
# 特徴機能: Faster Whisper batching - 複数の音声ストリームを同時に処理し、スループットを向上させる機能。大規模な音声データセットを必要とするアプリケーションに高いスケーラビリティを提供
# 学習済みモデル: OpenAI Whisper large-v3, large-v3-turbo, medium等のCTranslate2変換モデル(Hugging Face Hub自動ダウンロード対応)。高精度な多言語音声認識を実現し、日本語音声の文字起こしに最適化
# 特徴技術および学習済みモデルの利用制限: MIT License - 学術研究・商用利用可能
# 方式設計:
# 関連利用技術:
# - WebRTC VAD: Googleが開発した音声活動検出技術。リアルタイム音声処理において無音区間を検出し、効率的な音声認識を実現
# - PyAudio: クロスプラットフォーム音声入出力ライブラリ。リアルタイムマイク入力の取得に使用
# - Tkinter: Python標準GUIライブラリ。ユーザーインターフェースの構築に使用
# - PyTorch: 深層学習フレームワーク。GPU/CPU自動選択による最適化された推論処理
# 入力と出力: 入力: 音声・動画ファイル、リアルタイムマイク入力、出力: 文字起こし結果(SRT/TXT/CSV形式)
# 処理手順:
# 1. 音声入力(ファイルまたはマイク)の取得
# 2. VADによる音声活動検出とセグメンテーション
# 3. Faster-Whisperによるバッチ処理での音声認識
# 4. 結果の整形と指定形式での出力
# 前処理、後処理:
# 前処理: 16kHzサンプリングレート正規化、VADによる無音区間除去
# 後処理: タイムスタンプ付与、SRT形式変換、文字列整形
# 追加処理:
# - 長時間発話時のVAD感度動的調整: 7秒以上の発話時に無音判定フレーム数を450msから210msに短縮し、認識精度を向上
# - GPU/CPU自動選択: PyTorchによるデバイス自動検出により最適な計算環境を選択
# 調整を必要とする設定値:
# - RT_VAD_AGGRESSIVENESS(現在値:3): VAD積極性レベル(0-3)。値が大きいほど音声検出が厳格
# - RT_LONG_SPEECH_THRESHOLD_S(現在値:7.0): 長時間発話判定閾値(秒)。この値を超えるとVAD感度が向上
# 将来方策: VAD積極性の動的調整機能 - 環境ノイズレベルに応じてRT_VAD_AGGRESSIVENESSを自動調整する機能の実装
# その他の重要事項: Windows環境対応、リアルタイム処理とバッチ処理の両方をサポート
# 前準備:
# pip install --no-user -U numpy torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
# pip install --no-user faster-whisper pyaudio webrtcvad numpy
import os
import threading
import time
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
import torch
from faster_whisper import WhisperModel
import csv
import queue
import subprocess
import tempfile
import pyaudio
import webrtcvad
import numpy as np
# 対応ファイル形式
VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.flv', '.m4v'}
AUDIO_EXTENSIONS = {'.wav', '.mp3', '.aac', '.ogg', '.wma', '.flac', '.webm', '.3gp'}
SUPPORTED_EXTENSIONS = VIDEO_EXTENSIONS | AUDIO_EXTENSIONS
# リアルタイム処理用の定数
RT_CHUNK_SAMPLES = 480
RT_VAD_AGGRESSIVENESS = 3
RT_SAMPLERATE = 16000
# VAD感度を動的に変更するための定数
RT_LONG_SPEECH_THRESHOLD_S = 7.0 # 長時間発話とみなす閾値(秒)
RT_PADDING_FRAMES_NORMAL = 15 # 通常時の無音判定フレーム数 (15 * 30ms = 450ms)
RT_PADDING_FRAMES_SENSITIVE = 7 # 長時間発話時の無音判定フレーム数 (7 * 30ms = 210ms)
def detect_best_device():
"""最適なデバイスを自動検出"""
if torch.cuda.is_available():
return "cuda", "float16"
else:
return "cpu", "int8"
def format_srt_timestamp(seconds: float) -> str:
"""SRT形式のタイムスタンプに変換(丸め誤差に強い)"""
total_ms = int(round(seconds * 1000))
hours, rem = divmod(total_ms, 3600 * 1000)
minutes, rem = divmod(rem, 60 * 1000)
secs, millis = divmod(rem, 1000)
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
class TranscriptionGUI:
def __init__(self, root):
self.root = root
self.root.title("音声・動画ファイル文字起こしツール")
self.root.geometry("800x600")
self.auto_device, self.auto_compute_type = detect_best_device()
self.model = None
self.files_to_process = []
self.worker_thread = None
self.cancel_requested = threading.Event()
self.audio_queue = queue.Queue()
self.is_recording = threading.Event()
self.audio_thread = None
self.setup_gui()
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def setup_gui(self):
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
model_frame = ttk.LabelFrame(main_frame, text="設定", padding="5")
model_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(model_frame, text="モデル:").pack(side=tk.LEFT)
self.model_var = tk.StringVar(value="medium")
self.model_combobox = ttk.Combobox(
model_frame, textvariable=self.model_var,
values=["tiny", "base", "small", "medium", "large-v3", "large-v3-turbo"],
state="readonly", width=15
)
self.model_combobox.pack(side=tk.LEFT, padx=(5, 20))
ttk.Label(model_frame, text="デバイス:").pack(side=tk.LEFT)
device_text = "GPU (CUDA)" if self.auto_device == "cuda" else "CPU"
ttk.Label(model_frame, text=device_text, foreground="blue").pack(side=tk.LEFT, padx=(5, 20))
file_frame = ttk.LabelFrame(main_frame, text="ファイルまたはフォルダ選択", padding="5")
file_frame.pack(fill=tk.X, pady=(0, 10))
self.file_path_var = tk.StringVar()
ttk.Entry(file_frame, textvariable=self.file_path_var, width=60).pack(side=tk.LEFT, padx=(0, 5), fill=tk.X, expand=True)
ttk.Button(file_frame, text="ファイル選択", command=self.select_files).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(file_frame, text="フォルダ選択", command=self.select_folder).pack(side=tk.LEFT)
action_frame = ttk.Frame(main_frame)
action_frame.pack(fill=tk.X, pady=(5, 10))
file_action_frame = ttk.Frame(action_frame)
file_action_frame.pack(side=tk.LEFT)
self.transcribe_button = ttk.Button(file_action_frame, text="処理を開始して保存...", command=self.start_file_transcription)
self.transcribe_button.pack(side=tk.LEFT)
self.save_format_var = tk.StringVar(value="SRT")
self.save_format_combobox = ttk.Combobox(
file_action_frame, textvariable=self.save_format_var,
values=["SRT", "TXT", "CSV"],
state="readonly", width=10
)
self.save_format_combobox.pack(side=tk.LEFT, padx=(5, 0))
self.stop_file_button = ttk.Button(file_action_frame, text="中断", command=self.stop_transcription, state=tk.DISABLED)
self.stop_file_button.pack(side=tk.LEFT, padx=(5, 0))
mic_action_frame = ttk.Frame(action_frame)
mic_action_frame.pack(side=tk.RIGHT)
self.mic_start_button = ttk.Button(mic_action_frame, text="マイク入力開始", command=self.start_mic_transcription)
self.mic_start_button.pack(side=tk.LEFT)
self.mic_stop_button = ttk.Button(mic_action_frame, text="停止", command=self.stop_mic_transcription, state=tk.DISABLED)
self.mic_stop_button.pack(side=tk.LEFT, padx=(5, 0))
self.progress_var = tk.DoubleVar()
self.progressbar = ttk.Progressbar(main_frame, variable=self.progress_var)
self.progressbar.pack(fill=tk.X, pady=(0, 10))
self.result_text = scrolledtext.ScrolledText(main_frame, height=20, wrap=tk.WORD, font=("Consolas", 10))
self.result_text.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
self.context_menu = tk.Menu(self.result_text, tearoff=0)
self.context_menu.add_command(label="コピー", command=lambda: self.result_text.event_generate("<<Copy>>"))
self.context_menu.add_separator()
self.context_menu.add_command(label="すべて選択", command=lambda: self.result_text.tag_add("sel", "1.0", "end"))
self.result_text.bind("<Button-3>", self.show_context_menu)
status_frame = ttk.Frame(main_frame)
status_frame.pack(fill=tk.X)
self.status_var = tk.StringVar()
self.status_label = ttk.Label(status_frame, textvariable=self.status_var, anchor=tk.W)
self.status_label.pack(fill=tk.X)
def set_file_controls_state(self, state):
"""ファイル処理関連のコントロールの状態を一括で変更する"""
self.transcribe_button.config(state=state)
self.save_format_combobox.config(state="readonly" if state == tk.NORMAL else tk.DISABLED)
self.stop_file_button.config(state=tk.DISABLED if state == tk.NORMAL else tk.NORMAL)
self.mic_start_button.config(state=state)
def set_mic_controls_state(self, state):
"""マイク処理関連のコントロールの状態を一括で変更する"""
self.mic_start_button.config(state=state)
self.mic_stop_button.config(state=tk.DISABLED if state == tk.NORMAL else tk.NORMAL)
self.transcribe_button.config(state=state)
self.model_combobox.config(state="readonly" if state == tk.NORMAL else tk.DISABLED)
self.save_format_combobox.config(state=tk.DISABLED)
def show_context_menu(self, event):
"""右クリックメニューを表示し、選択状態に応じて「コピー」を有効/無効化"""
try:
self.result_text.get("sel.first", "sel.last")
self.context_menu.entryconfig(0, state=tk.NORMAL)
except tk.TclError:
self.context_menu.entryconfig(0, state=tk.DISABLED)
self.context_menu.tk_popup(event.x_root, event.y_root)
def select_files(self):
filenames = filedialog.askopenfilenames(
title="音声・動画ファイルを選択(複数可)",
filetypes=[("対応ファイル", " ".join(f"*{ext}" for ext in SUPPORTED_EXTENSIONS)), ("すべて", "*.*")]
)
if filenames:
self.files_to_process = list(filenames)
self.file_path_var.set(f"{len(self.files_to_process)}個のファイルを選択しました")
self.update_status(f"{len(self.files_to_process)}個のファイルを選択しました")
def select_folder(self):
folder = filedialog.askdirectory(title="処理対象のフォルダを選択")
if folder:
self.files_to_process = [
os.path.join(folder, f) for f in os.listdir(folder)
if Path(f).suffix.lower() in SUPPORTED_EXTENSIONS
]
if self.files_to_process:
self.file_path_var.set(f"{len(self.files_to_process)}個のファイルをフォルダから選択しました")
self.update_status(f"{len(self.files_to_process)}個のファイルをフォルダから選択しました")
else:
self.file_path_var.set("フォルダ内に対応ファイルがありません")
self.update_status("フォルダ内に対応ファイルがありません")
def update_status(self, text: str):
self.root.after(0, lambda: self.status_var.set(text))
def update_result(self, text: str):
def _update():
self.result_text.insert(tk.END, text)
self.result_text.see(tk.END)
self.root.after(0, _update)
def start_file_transcription(self):
if not self.files_to_process:
messagebox.showerror("エラー", "ファイルまたはフォルダを選択してください")
return
output_dir = filedialog.askdirectory(title="保存先のフォルダを選択してください")
if not output_dir:
messagebox.showwarning("キャンセル", "保存先が選択されなかったため、処理を中止しました。")
return
self.set_file_controls_state(tk.DISABLED)
self.cancel_requested.clear()
self.progressbar.config(maximum=len(self.files_to_process), value=0)
save_format = self.save_format_var.get()
self.worker_thread = threading.Thread(
target=self.batch_transcribe_worker,
args=(output_dir, save_format),
daemon=True
)
self.worker_thread.start()
def stop_transcription(self):
self.update_status("処理の中断を要求しました...")
self.cancel_requested.set()
self.stop_file_button.config(state=tk.DISABLED)
def batch_transcribe_worker(self, output_dir, save_format):
self.update_status(f"一括処理を開始... 対象: {len(self.files_to_process)}ファイル")
processed_count = 0
for i, file_path in enumerate(self.files_to_process):
if self.cancel_requested.is_set():
self.update_status("処理がユーザーによって中断されました。")
break
self.root.after(0, lambda: self.result_text.delete(1.0, tk.END))
self.update_status(f"処理中 ({i+1}/{len(self.files_to_process)}): {Path(file_path).name}")
result_segments = self.transcribe_single_file(file_path)
if result_segments:
self.save_single_result(result_segments, file_path, output_dir, save_format)
processed_count += 1
self.root.after(0, lambda i=i: self.progressbar.config(value=i + 1))
if not self.cancel_requested.is_set():
self.update_status(f"すべての処理が完了しました。 {processed_count}個のファイルを保存しました。")
self.root.after(0, self.file_transcription_finished)
def transcribe_single_file(self, file_path):
temp_wav_file = None
try:
file_ext = Path(file_path).suffix.lower()
if file_ext in VIDEO_EXTENSIONS:
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpfile:
temp_wav_file = tmpfile.name
command = ["ffmpeg", "-y", "-i", file_path, "-vn", "-ar", "16000", "-ac", "1", "-f", "wav", temp_wav_file]
proc = subprocess.run(command, capture_output=True, text=True, encoding='utf-8', errors='ignore')
if proc.returncode != 0:
self.update_status(f"音声抽出に失敗: {proc.stderr or 'ffmpeg error'}")
return None
audio_file_to_process = temp_wav_file
else:
audio_file_to_process = file_path
if not self.model:
model_size = self.model_var.get()
self.model = WhisperModel(model_size, device=self.auto_device, compute_type=self.auto_compute_type)
segments_gen, _ = self.model.transcribe(
audio_file_to_process, language="ja", beam_size=5, temperature=0.0,
condition_on_previous_text=True,
initial_prompt="以下は日本語の音声である。文は意味のまとまりで区切り、適切に句読点「、」「。」を付ける。",
vad_filter=True,
vad_parameters=dict(min_silence_duration_ms=2800, speech_pad_ms=400, max_speech_duration_s=3600.0),
)
result_segments = []
srt_index = 0
for segment in segments_gen:
if self.cancel_requested.is_set():
break
srt_index += 1
srt_entry = f"{srt_index}\n{format_srt_timestamp(segment.start)} --> {format_srt_timestamp(segment.end)}\n{segment.text.strip()}\n\n"
self.update_result(srt_entry)
result_segments.append({'start': segment.start, 'end': segment.end, 'text': segment.text.strip()})
return result_segments
except Exception as e:
self.update_status(f"エラーが発生: {e}")
return None
finally:
if temp_wav_file and os.path.exists(temp_wav_file):
try:
os.remove(temp_wav_file)
except OSError as e:
self.update_status(f"一時ファイル削除エラー: {e}")
def save_single_result(self, segments, original_path, output_dir, save_format):
base_name = Path(original_path).stem
try:
if save_format == "SRT":
output_path = os.path.join(output_dir, f"{base_name}.srt")
with open(output_path, 'w', encoding='utf-8') as f:
for i, seg in enumerate(segments):
f.write(f"{i+1}\n{format_srt_timestamp(seg['start'])} --> {format_srt_timestamp(seg['end'])}\n{seg['text']}\n\n")
elif save_format == "TXT":
output_path = os.path.join(output_dir, f"{base_name}.txt")
with open(output_path, 'w', encoding='utf-8') as f:
for seg in segments:
f.write(f"{seg['text']}\n")
elif save_format == "CSV":
output_path = os.path.join(output_dir, f"{base_name}.csv")
with open(output_path, 'w', encoding='utf-8', newline='') as f:
writer = csv.writer(f)
writer.writerow(["start_time", "end_time", "text"])
for seg in segments:
writer.writerow([f"{seg['start']:.3f}", f"{seg['end']:.3f}", seg['text']])
except Exception as e:
self.update_status(f"保存エラー: {e}")
def file_transcription_finished(self):
self.set_file_controls_state(tk.NORMAL)
self.progressbar.config(value=0)
def start_mic_transcription(self):
try:
# PyAudio初期化テスト
p = pyaudio.PyAudio()
p.terminate()
except Exception as e:
messagebox.showerror("マイクエラー", f"マイクの初期化に失敗しました: {e}")
return
self.set_mic_controls_state(tk.DISABLED)
self.result_text.delete(1.0, tk.END)
self.update_status("マイク入力を開始します...")
self.is_recording.set()
self.audio_thread = threading.Thread(target=self.record_audio, daemon=True)
self.audio_thread.start()
self.worker_thread = threading.Thread(target=self.mic_transcribe_worker, daemon=True)
self.worker_thread.start()
def stop_mic_transcription(self):
self.update_status("マイク入力を停止しています...")
self.is_recording.clear()
self.mic_stop_button.config(state=tk.DISABLED)
def record_audio(self):
p = pyaudio.PyAudio()
try:
stream = p.open(format=pyaudio.paInt16, channels=1, rate=RT_SAMPLERATE,
input=True, frames_per_buffer=RT_CHUNK_SAMPLES)
while self.is_recording.is_set():
try:
data = stream.read(RT_CHUNK_SAMPLES, exception_on_overflow=False)
self.audio_queue.put(data)
except Exception as e:
self.update_status(f"音声読み取りエラー: {e}")
break
stream.stop_stream()
stream.close()
except Exception as e:
self.update_status(f"マイクエラー: {e}")
finally:
p.terminate()
def mic_transcribe_worker(self):
if not self.model:
model_size = self.model_var.get()
self.model = WhisperModel(model_size, device=self.auto_device, compute_type=self.auto_compute_type)
vad = webrtcvad.Vad(RT_VAD_AGGRESSIVENESS)
frames = []
ring_buffer_size = RT_PADDING_FRAMES_NORMAL
ring_buffer = []
triggered = False
speech_start_time = None
while self.is_recording.is_set() or not self.audio_queue.empty():
try:
chunk = self.audio_queue.get(timeout=0.1)
# 空データをスキップ
if len(chunk) != RT_CHUNK_SAMPLES * 2: # 16bit = 2bytes
continue
try:
is_speech = vad.is_speech(chunk, RT_SAMPLERATE)
except Exception:
# VADエラー時は無音として扱う
is_speech = False
if not triggered:
if len(ring_buffer) >= ring_buffer_size:
ring_buffer.pop(0)
ring_buffer.append(chunk)
if is_speech:
triggered = True
speech_start_time = time.time()
frames.extend(ring_buffer)
self.update_status("音声検出...")
else:
frames.append(chunk)
elapsed_time = time.time() - speech_start_time if speech_start_time else 0
if elapsed_time > RT_LONG_SPEECH_THRESHOLD_S:
current_padding_frames = RT_PADDING_FRAMES_SENSITIVE
else:
current_padding_frames = RT_PADDING_FRAMES_NORMAL
if len(ring_buffer) >= current_padding_frames:
ring_buffer.pop(0)
ring_buffer.append(chunk)
if not is_speech and len(ring_buffer) >= current_padding_frames:
recent_silence = all(
not vad.is_speech(f, RT_SAMPLERATE)
if len(f) == RT_CHUNK_SAMPLES * 2 else True
for f in ring_buffer[-current_padding_frames:]
)
if recent_silence:
self.update_status("認識中...")
audio_data = b''.join(frames)
frames.clear()
triggered = False
speech_start_time = None
ring_buffer.clear()
if len(audio_data) > 0:
audio_np = np.frombuffer(audio_data, dtype=np.int16).astype(np.float32) / 32768.0
try:
segments, _ = self.model.transcribe(audio_np, language="ja", beam_size=5)
transcribed_text = "".join(seg.text for seg in segments).strip()
if transcribed_text:
self.update_result(transcribed_text + " ")
self.update_status("待機中...")
except Exception as e:
self.update_status(f"認識エラー: {e}")
except queue.Empty:
continue
except Exception as e:
self.update_status(f"処理エラー: {e}")
# ループを抜けた後にフレームが残っている場合、最後の発話を処理する
if frames:
self.update_status("最後の音声を認識中...")
audio_data = b''.join(frames)
if len(audio_data) > 0:
audio_np = np.frombuffer(audio_data, dtype=np.int16).astype(np.float32) / 32768.0
try:
segments, _ = self.model.transcribe(audio_np, language="ja", beam_size=5)
transcribed_text = "".join(seg.text for seg in segments).strip()
if transcribed_text:
self.update_result(transcribed_text + " ")
except Exception as e:
self.update_status(f"最終認識エラー: {e}")
self.root.after(0, self.mic_transcription_finished)
def mic_transcription_finished(self):
self.set_mic_controls_state(tk.NORMAL)
self.update_status("マイク入力を停止しました。")
def on_closing(self):
if self.worker_thread and self.worker_thread.is_alive():
if self.is_recording.is_set():
self.stop_mic_transcription()
self.wait_for_thread_and_close()
else:
if messagebox.askyesno("確認", "処理が実行中です。中断して終了しますか?"):
self.cancel_requested.set()
self.wait_for_thread_and_close()
else:
self.root.destroy()
def wait_for_thread_and_close(self):
max_wait_time = 5.0 # 最大5秒待機
start_time = time.time()
def check_and_close():
elapsed_time = time.time() - start_time
if (not self.worker_thread or not self.worker_thread.is_alive()) and \
(not self.audio_thread or not self.audio_thread.is_alive()):
self.root.destroy()
elif elapsed_time >= max_wait_time:
# 強制終了
self.root.destroy()
else:
self.root.after(100, check_and_close)
check_and_close()
def main():
root = tk.Tk()
app = TranscriptionGUI(root)
root.mainloop()
if __name__ == "__main__":
main()
使用方法
- 上記のコードを whisper_transcribe.py として保存する
- コマンドラインで実行する:
python whisper_transcribe.py
体験・実験のアイデア
- 異なるモデルサイズで、結果を比較する
- 雑音のある環境での音声認識性能を検証する
- 早口や方言での認識能力を実験する
- ファイルの一括処理とリアルタイムマイク入力で、認識精度や応答性の違いを比較する