「Whisper」を使用して文字起こし
Whisper.net (ローカルライブラリ) を使用する: OpenAIのモデルをローカル環境で動かす。無料、高プライバシー、GPU(CUDA)推奨。
1.1 NuGetパッケージのインストール
Visual Studioの「NuGetパッケージマネージャー」またはターミナルから以下をインストールします。
※ GPUが使えない場合は、自動的にCPUが使用されます。
1.2 モデルファイルの準備
Whisperの学習済みモデル(.binファイル)が必要です。
HuggingFaceのWhisper.netリポジトリなどから、小~大(tiny, base, small, medium, large)のモデルをダウンロードします(例: ggml-base-q5_1.bin)。
実行画面


録音して解析します。CPU、GPUの能力に依存します。

結構時間が、かかります。精度は、辞書によってかなり異なります。
FrmWhisper




FrmWhisper.cs
using NAudio.Utils;
using NAudio.Wave;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Whisper.net;
using Whisper.net.Ggml;
using static System.Net.Mime.MediaTypeNames;
using static System.Windows.Forms.VisualStyles.VisualStyleElement;
namespace FrmWhisper
{
public partial class FrmWhisper : Form
{
//private WaveInEvent waveIn;
private MemoryStream recordingStream;
private bool isRecording = false;
// 1. クラスのメンバ変数(一番上)に置く
private WhisperFactory factory;
private WhisperProcessor processor;
// クラスのメンバ
private WaveInEvent waveIn;
private BufferedWaveProvider bufferedProvider;
private MediaFoundationResampler resampler;
private MemoryStream whisperBuffer = new MemoryStream();
private WaveFileWriter writer;
private string tempFile = "temp_recording.wav";
private long lastPosition = 0; // 解析済みの位置を記録
private long lastProcessedPosition = 0; // どこまで解析したかを保持
private bool isInsideTick = false; // クラスのメンバ変数
//control clsResize 画面表示を拡大縮小します。
clsResize _form_resize;
public FrmWhisper()
{
InitializeComponent();
//clsResize
_form_resize = new clsResize(this); //I put this after the initialize event to be sure that all controls are initialized properly
this.Load += new EventHandler(_Load); //This will be called after the initialization // form_load
this.Resize += new EventHandler(_Resize); //form_resize
//
InitializeWhisper();
}
//clsResize _Load
private void _Load(object sender, EventArgs e)
{
_form_resize._get_initial_size();
}
//clsResize _Resize
private void _Resize(object sender, EventArgs e)
{
_form_resize._resize();
}
private void 終了ToolStripMenuItem1_Click(object sender, EventArgs e)
{
this.Close();
}
private void 終了ToolStripMenuItem_Click(object sender, EventArgs e)
{
this.Close();
}
private void InitializeWhisper()
{
if (factory == null)
{
var options = new WhisperFactoryOptions { UseGpu = true };
factory = WhisperFactory.FromPath("ggml-medium.bin", options);
processor = factory.CreateBuilder()
.WithLanguage("ja")
.WithThreads(2)
.WithPrompt("ワード、エクセル。本日は晴天なり。")
.Build();
}
}
private void btnStart_Click(object sender, EventArgs e)
{
waveIn = new WaveInEvent { WaveFormat = new WaveFormat(48000, 1) };
writer = new WaveFileWriter(tempFile, waveIn.WaveFormat);
waveIn.DataAvailable += (s, a) => {
writer.Write(a.Buffer, 0, a.BytesRecorded);
writer.Flush();
};
// 1. 先に録音を開始してファイルにデータを書き込み始める
waveIn.StartRecording();
isRecording = true;
// 2. その後にタイマーを起動する
lastProcessedPosition = 0;
tmrRealtime.Start();
btnStart.Text = "録音中...";
}
private async void btnSelectFile_Click(object sender, EventArgs e)
{
var ofd = new OpenFileDialog { Filter = "Audio Files|*.wav;*.mp3" };
if (ofd.ShowDialog() != DialogResult.OK) return;
txtResult.Text = ofd.FileName;
try
{
// 録音データの総時間を取得(進捗計算用)
double totalSeconds;
// 1. Whisper用に音声を16kHz/Monoに変換してメモリに展開
using var resampledStream = new MemoryStream();
using (var reader = new AudioFileReader(ofd.FileName))
{
totalSeconds =reader.TotalTime.TotalSeconds;
var outFormat = new WaveFormat(16000, 16, 1); // 16kHz, 16bit, Mono
using var resampler = new MediaFoundationResampler(reader, outFormat);
WaveFileWriter.WriteWavFileToStream(resampledStream, resampler);
}
resampledStream.Position = 0;
/*
// 2. Whisperモデルの読み込み (事前にbinファイルを配置しておく)
var modelPath = "ggml-medium.bin";
if (!File.Exists(modelPath))
{
txtResult.Text = "モデルファイルが見つかりません。";
return;
}
using var factory = WhisperFactory.FromPath(modelPath);
using var processor = factory.CreateBuilder()
.WithLanguage("ja") // 日本語指定
.WithThreads(2) // CPU 100%対策(これだけは絶対に入れてください)
.Build();
*/
// 3. 文字起こし実行
txtRealtime.Clear();
progressBar1.Value = 0;
await foreach (var result in processor.ProcessAsync(resampledStream))
{
// 進捗率を計算してProgressBarに反映
if (totalSeconds > 0)
{
int progress = (int)((result.End.TotalSeconds / totalSeconds) * 100);
progressBar1.Value = Math.Min(progress, 100);
}
txtRealtime.AppendText($"{result.Text}{Environment.NewLine}");
}
SaveToTextFile(txtRealtime.Text);
progressBar1.Value = 100; // 完了
}
catch (Exception ex)
{
MessageBox.Show($"エラー: {ex.Message}");
}
}
private async void btnStartStop_Click(object sender, EventArgs e)
{
if (!isRecording)
{
// 1. マイクの本来の性能(48kHz)で録音を開始する
waveIn = new WaveInEvent { WaveFormat = new WaveFormat(48000, 1) };
recordingStream = new MemoryStream();
// WaveFileWriterは使わず、生データをそのままメモリストリームへ
waveIn.DataAvailable += (s, a) => {
recordingStream.Write(a.Buffer, 0, a.BytesRecorded);
};
waveIn.StartRecording();
isRecording = true;
btnStartStop.Text = "停止して解析";
txtRealtime.Text = "録音中...";
}
else
{
waveIn.StopRecording();
isRecording = false;
btnStartStop.Text = "マイク開始";
await ProcessAudio();
}
}
private async Task ProcessAudio_pre()
{
try
{
recordingStream.Position = 0;
using var resampledStream = new MemoryStream();
var rawSource = new RawSourceWaveStream(recordingStream, new WaveFormat(48000, 1));
// 録音データの総時間を取得(進捗計算用)
double totalSeconds = rawSource.TotalTime.TotalSeconds;
using (var resampler = new MediaFoundationResampler(rawSource, new WaveFormat(16000, 16, 1)))
{
WaveFileWriter.WriteWavFileToStream(resampledStream, resampler);
}
resampledStream.Position = 0;
/*
// GPU(OpenVINO)を使用するためのオプション
var options = new WhisperFactoryOptions { UseGpu = true };
// オプションを渡してFactoryを作成
using var factory = WhisperFactory.FromPath("ggml-medium.bin", options);
//using var factory = WhisperFactory.FromPath("ggml-small.bin");
using var processor = factory.CreateBuilder()
.WithLanguage("ja")
.WithPrompt("ワード、エクセル。本日は晴天なり。") // 成功したプロンプトを保持
.WithThreads(2) // CPU 100%対策(これだけは絶対に入れてください)
.Build();
*/
txtRealtime.Clear();
progressBar1.Value = 0;
await foreach (var result in processor.ProcessAsync(resampledStream))
{
// 進捗率を計算してProgressBarに反映
if (totalSeconds > 0)
{
int progress = (int)((result.End.TotalSeconds / totalSeconds) * 100);
progressBar1.Value = Math.Min(progress, 100);
}
txtRealtime.AppendText($"{result.Text}{Environment.NewLine}");
}
SaveToTextFile(txtRealtime.Text);
progressBar1.Value = 100; // 完了
}
catch (Exception ex)
{
MessageBox.Show("エラー: " + ex.Message);
}
finally
{
recordingStream?.Dispose();
waveIn?.Dispose();
}
}
private async Task ProcessAudio()
{
try
{
recordingStream.Position = 0;
// --- 追加:保存用ファイル名の生成(例:20260320_153005.wav) ---
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string backupWavPath = Path.Combine(System.Windows.Forms.Application.StartupPath, $"{timestamp}_recorded.wav");
// 録音生データをファイルとして書き出し
using (var fileWriter = new WaveFileWriter(backupWavPath, new WaveFormat(48000, 1)))
{
recordingStream.CopyTo(fileWriter);
}
// -----------------------------------------------------------
recordingStream.Position = 0; // ストリーム位置を戻す
using var resampledStream = new MemoryStream();
var rawSource = new RawSourceWaveStream(recordingStream, new WaveFormat(48000, 1));
double totalSeconds = rawSource.TotalTime.TotalSeconds;
// Whisper用に16kHzに変換
using (var resampler = new MediaFoundationResampler(rawSource, new WaveFormat(16000, 16, 1)))
{
WaveFileWriter.WriteWavFileToStream(resampledStream, resampler);
}
resampledStream.Position = 0;
// ※本来はここで factory や processor を初期化(省略)
txtRealtime.Clear();
txtRealtime.AppendText($"【ファイル保存済み: {Path.GetFileName(backupWavPath)}】{Environment.NewLine}");
progressBar1.Value = 0;
await foreach (var result in processor.ProcessAsync(resampledStream))
{
if (totalSeconds > 0)
{
int progress = (int)((result.End.TotalSeconds / totalSeconds) * 100);
progressBar1.Value = Math.Min(progress, 100);
}
txtRealtime.AppendText($"{result.Text}{Environment.NewLine}");
txtRealtime.ScrollToCaret(); // 常に最新を表示
}
// テキストも保存
string textFileName = Path.ChangeExtension(backupWavPath, ".txt");
File.WriteAllText(textFileName, txtRealtime.Text);
progressBar1.Value = 100;
MessageBox.Show($"解析完了!{Environment.NewLine}音声: {backupWavPath}{Environment.NewLine}テキスト: {textFileName}");
}
catch (Exception ex)
{
MessageBox.Show("エラー: " + ex.Message);
}
finally
{
recordingStream?.Dispose();
waveIn?.Dispose();
}
}
private void SaveToTextFile(string text)
{
try
{
// 実行ファイルと同じフォルダに「Log_日付.txt」で保存
string fileName = $"Transcription_{DateTime.Now:yyyyMMdd}.txt";
// 日時を添えて追記モードで書き込み
using (StreamWriter sw = new StreamWriter(fileName, true, System.Text.Encoding.UTF8))
{
sw.WriteLine($"[{DateTime.Now:HH:mm:ss}] {text}");
}
}
catch (Exception ex)
{
// 保存失敗時のエラー(書き込み権限など)
Console.WriteLine("保存エラー: " + ex.Message);
}
}
private async void btnStop_Click(object sender, EventArgs e)
{
if (waveIn != null)
{
tmrRealtime.Enabled = false; // 真っ先にタイマーを止める
// 1. 録音を止める
waveIn.StopRecording();
// 2. 書き込み用ライターを閉じる(重要:これでWAVヘッダーが確定します)
if (writer != null)
{
writer.Close();
writer.Dispose();
writer = null;
}
waveIn.Dispose();
waveIn = null;
isRecording = false;
btnStart.Text = "マイク開始";
// 3. 最後に残った音声データをすべて解析
txtRealtime.AppendText("--- 最終解析中 ---" + Environment.NewLine);
await ProcessAudioWithProgress(tempFile);
// 4. (任意) 使い終わった一時ファイルを削除
// File.Delete(tempFile);
}
}
private async Task ProcessAudioWithProgress(string filePath)
{
using var reader = new AudioFileReader(filePath);
double totalSeconds = reader.TotalTime.TotalSeconds; // 10分なら600秒
// 16kHz変換(省略せず確実に)
using var resampledStream = new MemoryStream();
using (var resampler = new MediaFoundationResampler(reader, new WaveFormat(16000, 16, 1)))
{
WaveFileWriter.WriteWavFileToStream(resampledStream, resampler);
}
resampledStream.Position = 0;
// --- UIの準備 ---
progressBar1.Value = 0;
lblStatus.Text = "解析開始...";
await foreach (var result in processor.ProcessAsync(resampledStream))
{
// result.End.TotalSeconds は「音声の何秒目まで解析したか」
double processedSeconds = result.End.TotalSeconds;
int percent = (int)((processedSeconds / totalSeconds) * 100);
// UIスレッドへの反映
this.Invoke(new Action(() => {
progressBar1.Value = Math.Min(percent, 100);
// 認識したテキストを追記
txtRealtime.AppendText($"{result.Text}{Environment.NewLine}");
}));
}
this.Invoke(new Action(() => {
SaveToTextFile(txtRealtime.Text);
progressBar1.Value = 100;
}));
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
// 録音中なら強制停止してファイルを閉じる
if (isRecording)
{
waveIn?.StopRecording();
writer?.Close();
}
}
private async void tmrRealtime_Tick(object sender, EventArgs e)
{
// 1. すでに解析中(前の回が終わっていない)なら、今回のタイマーは完全に無視する
if (isInsideTick) return;
// 2. 解析開始の「鍵」をかける
isInsideTick = true;
try
{
if (isRecording && writer != null && writer.Length > lastProcessedPosition)
{
// 3. 【重要】await を使うことで、解析が終わるまでここで「待機」する
// これにより、解析が終わるまで finally に進みません
await ProcessChunkAsync();
}
}
catch (Exception ex)
{
Debug.WriteLine("Tickエラー: " + ex.Message);
}
finally
{
// 4. 解析が完全に終わってから「鍵」を開ける
isInsideTick = false;
}
}
private async Task ProcessChunkAsync()
{
try
{
byte[] buffer;
lock (writer)
{
writer.Flush();
// 1. 未処理の「差分」だけを読み取る
long currentLength = writer.Length;
int bytesToRead = (int)(currentLength - lastProcessedPosition);
if (bytesToRead < 16000) return; // あまりに短い(0.5秒以下とか)なら次へ
buffer = new byte[bytesToRead];
using (var fs = new FileStream(tempFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
fs.Position = lastProcessedPosition;
fs.Read(buffer, 0, bytesToRead);
}
lastProcessedPosition = currentLength; // 読み取った分だけ位置を更新
}
// 2. この「差分だけ」を変換して解析する
using var rawStream = new MemoryStream(buffer);
using var reader = new RawSourceWaveStream(rawStream, new WaveFormat(48000, 1)); // マイクの形式
using var resampledStream = new MemoryStream();
var outFormat = new WaveFormat(16000, 16, 1);
using (var resampler = new MediaFoundationResampler(reader, outFormat))
{
WaveFileWriter.WriteWavFileToStream(resampledStream, resampler);
}
resampledStream.Position = 0;
await foreach (var result in processor.ProcessAsync(resampledStream))
{
string decodedText = result.Text.Trim();
if (string.IsNullOrEmpty(decodedText)) continue;
this.Invoke(new Action(() => {
txtRealtime.AppendText($"{decodedText} ");
SaveToTextFile(decodedText);
}));
}
}
catch (Exception ex)
{
Debug.WriteLine("Chunk解析エラー: " + ex.Message);
}
}
private void btnEnd_Click(object sender, EventArgs e)
{
this.Close();
}
}
}