windows

「Whisper」を使用して文字起こし

「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の能力に依存します。

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

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();
        }
    }
}
       
        

-windows

PAGE TOP