VoskでLLMと音声会話する

前回記事でVoskは処理が早いので会話に使えそうなのがわかりました。そこでLLMを組み込んで音声会話してみました。

Nuget情報
System.Windows.Extensionsは、VOICEVOX音声を再生するために組み込んでいます。



概要
System.SpeachのSpeechRecognizedイベントを使いVoskで文字起ししてLLMに渡し回答は、VOICEVOXまたはCevioAIまたはVoicePeakで音声再生する。
LLMは、回答速度と会話に優れたVecteus2を使っています。

using LLama.Common;
using LLama;
using System.Media;
using System.Net.Http.Headers;
using System.Diagnostics;
using System.Speech.Recognition;
using System.Text.Json;
using Vosk;

namespace ChatProgram
{
    public class Program
    {
        static void Main(string[] args)
        {
            //コンソールアプリケーションからAsyncを呼び出す
            Task task = MainAsync();
            //終了を待つ
            task.Wait();
        }

        public static async Task MainAsync()
        {
            // LLMモデルの場所
            string strModelPath = Environment.GetEnvironmentVariable("LLMPATH", System.EnvironmentVariableTarget.User) + @"Local-Novel-LLM-project\Vecteus2-gguf\ggml-model-Q8_0.gguf"; 
            //Vosk設定
            Vosk.Vosk.SetLogLevel(-1); //LogメッセージOFF
            Model model = new Model(Environment.GetEnvironmentVariable("LLMPATH", System.EnvironmentVariableTarget.User)+@"vosk\vosk-model-ja-0.22");

            try
            {
                Console.ForegroundColor = ConsoleColor.Blue;
                //LLMモデルのロードとパラメータの設定
                var modPara = new ModelParams(strModelPath)
                {
                    ContextSize = 4096,
                    Seed = 1337,
                    GpuLayerCount = 24
                };
                LLamaWeights llmWeit = LLamaWeights.LoadFromFile(modPara);
                LLamaContext llmContx = llmWeit.CreateContext(modPara);
                InteractiveExecutor itrEx = new(llmContx);
                // 初期はチャットログにキャラ設定と方向性を示す会話を打ち込む
                ChatHistory chtHis;
                chtHis = new ChatHistory();
                chtHis.AddMessage(AuthorRole.System, "あなたは、ユーザーと楽しく会話する女性を演じてね。\n" +
                                                    "名前は、マイだよ。\n" +
                                                    "マイは、明るくて、やさしい性格の女性なの。年齢は24歳なんだよ。\n" +
                                                    "マイは、誰にでもタメ口で話すの。\n" +
                                                    "マイは、一人称をわたしと言うよ。\n" +
                                                    "マイは、恥ずかしがりやだけどエッチに興味があるのね。\n" +
                                                    "マイは、応答の長さはできるだけ短く答えるよ。\n" +
                                                    "マイは、好奇心旺盛だからユーザーに色々質問するの。");
                chtHis.AddMessage(AuthorRole.User, "はじめまして、オレの名前は犬マンマです。呼びにくいからマー君って呼んでええよ。");
                chtHis.AddMessage(AuthorRole.Assistant, "はじめまして、わたしの名前はマイです。");
                chtHis.AddMessage(AuthorRole.User, "マイ、オレのことマー君と呼んでくれ。それと敬語じゃなくてタメ口でええよ。");
                chtHis.AddMessage(AuthorRole.Assistant, "わかったマー君、フレンドリーにタメ口で話すね。");

                ChatSession chtSess = new(itrEx, chtHis);
                var varHidewd = new LLamaTransforms.KeywordTextOutputStreamTransform(["User: ", "Assistant: "]);
                chtSess.WithOutputTransform(varHidewd);
                InferenceParams infPara = new()
                {
                    Temperature = 0.8f,
                    AntiPrompts = ["User:"],
                    //AntiPrompts = ["User:", "<|eot_id|>"],  //Llama3用
                    MaxTokens = 256,
                };

                // SpeechRecognitionの設定  
                using (SpeechRecognitionEngine recognizer =
                  new SpeechRecognitionEngine(new System.Globalization.CultureInfo("ja-JP")))
                {
                    recognizer.LoadGrammar(new DictationGrammar());

                    // ▼▼▼ ここからSpeechRecognizedイベント定義 開始 ▼▼▼  
                    recognizer.SpeechRecognized += async(sender, e) =>
                    {
                        //RecognizedしたwaveをMemoryStreamに書き込み
                        MemoryStream st = new MemoryStream();
                        e.Result.Audio.WriteToWaveStream(st);
                        st.Position = 0;

                        // byte buffer
                        VoskRecognizer rec = new VoskRecognizer(model, 16000.0f);
                        rec.SetMaxAlternatives(0);
                        byte[] buffer = new byte[4096];
                        int bytesRead;
                        while ((bytesRead = st.Read(buffer, 0, buffer.Length)) > 0)
                            rec.AcceptWaveform(buffer, bytesRead);

                        // Json形式のResultからテキストを抽出
                        string jsontext = rec.FinalResult();
                        string strUserInput = "";
                        var jsondoc = JsonDocument.Parse(jsontext);
                        if (jsondoc.RootElement.TryGetProperty("text", out var element))
                        {
                            strUserInput = element.GetString() ?? "";
                        }
                        strUserInput = strUserInput.Replace(" ", "");
                        Console.ForegroundColor = ConsoleColor.White;
                        Console.WriteLine($"User: {strUserInput}");

                        ChatHistory.Message msgText = new(AuthorRole.User, "User: "+ strUserInput);
                        // 回答の表示
                        Console.ForegroundColor = ConsoleColor.Yellow;
                        string strMsg = "";
                        await foreach (string strText in chtSess.ChatAsync(msgText, infPara))
                        {
                            strMsg += strText;
                        }
                        //発信するときは「User:」や「Assistant:」を抜く
                        string strSndmsg = strMsg.Replace("User:", "").Replace("Assistant:", "").Replace("assistant:", "").Trim();
                        Console.WriteLine(strSndmsg);
                        //Voicevox以外を使用したい場合、音声ソフトのコメントを外して下の2行をコメントアウトしてください
                        Task task = Voicevox(strSndmsg);
                        task.Wait();
                        //CevioAI(strSndmsg);
                        //VoicePeak(strSndmsg);
                        //MemoryStream破棄
                        st.Close();
                        st.Dispose();
                    };
                    // ▲▲▲ ここでSpeechRecognizedイベント定義 終了 ▲▲▲  

                    // Configure input to the speech recognizer.  
                    recognizer.SetInputToDefaultAudioDevice();

                    // Start asynchronous, continuous speech recognition.  
                    recognizer.RecognizeAsync(RecognizeMode.Multiple);

                    Console.WriteLine("★★ マイクに向かって話してください ★★");

                    // Keep the console window open.  
                    while (true)
                    {
                        Console.ReadLine();
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }

        }
//▼▼▼ここから下は、音声ソフトの再生処理です。いらないところは削除してください。▼▼▼
        public static void CevioAI(string strMsg)
        {
            try
            {
                dynamic service = Activator.CreateInstance(Type.GetTypeFromProgID("CeVIO.Talk.RemoteService2.ServiceControl2V40"));
                service.StartHost(false);

                dynamic talker = Activator.CreateInstance(Type.GetTypeFromProgID("CeVIO.Talk.RemoteService2.Talker2V40"));
                talker.Cast = "双葉湊音";
                dynamic result = talker.Speak(strMsg);
                result.Wait();

                //開放忘れるとメモリリーク
                System.Runtime.InteropServices.Marshal.ReleaseComObject(talker);
                System.Runtime.InteropServices.Marshal.ReleaseComObject(service);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }

        public static void VoicePeak(string strMsg)
        {
            string wavFileName = Environment.GetEnvironmentVariable("TESTDATA", System.EnvironmentVariableTarget.User) + @"voicepeak.wav";
            try
            {
                var processSI = new ProcessStartInfo
                {
                    FileName = @"E:\Program Files\VOICEPEAK\voicepeak.exe",
                    Arguments = $"-s \"{strMsg}\" -o \"{wavFileName}\"",
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    CreateNoWindow = true
                };
                using (var process = Process.Start(processSI))
                {
                    process.WaitForExit();
                };

                var player = new SoundPlayer(wavFileName);
                //再生する
                player.PlaySync();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }

        public static async Task Voicevox(string strMsg)
        {
            MemoryStream? ms;

            try
            {
                using (var httpClient = new HttpClient())
                {
                    string strQuery;
                    int intSpeaker = 8; //春日部つむぎ

                    // 音声クエリを生成
                    using (var varRequest = new HttpRequestMessage(new HttpMethod("POST"), $"http://localhost:50021/audio_query?text={strMsg}&speaker={intSpeaker}&speedScale=1.1&prePhonemeLength=0&postPhonemeLength=0&intonationScale=1.16&enable_interrogative_upspeak=true"))
                    {
                        varRequest.Headers.TryAddWithoutValidation("accept", "application/json");

                        varRequest.Content = new StringContent("");
                        varRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");

                        var response = await httpClient.SendAsync(varRequest);

                        strQuery = response.Content.ReadAsStringAsync().Result;
                    }

                    // 音声クエリから音声合成
                    using (var request = new HttpRequestMessage(new HttpMethod("POST"), $"http://localhost:50021/synthesis?speaker={intSpeaker}&enable_interrogative_upspeak=true&speedScale=1.1&prePhonemeLength=0&postPhonemeLength=0&intonationScale=1.16"))
                    {
                        request.Headers.TryAddWithoutValidation("accept", "audio/wav");

                        request.Content = new StringContent(strQuery);
                        request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");

                        var response = await httpClient.SendAsync(request);

                        // 音声を保存
                        using (ms = new MemoryStream())
                        {
                            using (var httpStream = await response.Content.ReadAsStreamAsync())
                            {
                                httpStream.CopyTo(ms);
                                ms.Flush();
                            }
                        }
                    }
                }
                ms = new MemoryStream(ms.ToArray());
                //読み込む
                var player = new SoundPlayer(ms);
                //再生する
                player.PlaySync();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
    }
}



結果
ところどころ聞き間違いがありますが、わりとスムーズに会話ができました。
少しはcotomoに近づけたかなあ・・・
(誤)前は何してたの ⇒ (正)マイは何してたの

Follow me!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です