AIサーバー試作品1.6(音声会話のみ)
Windows.Media.SpeechRecognitionを組み込みました。かなり会話がスムーズになりました。AwakeはSystem.Speech.Recognitionを残そうか迷ったのですが、ごっそり入れ替えました。Windows.Media.SpeechRecognitionを使うときは工夫が必要なためこの記事を見てください。
1.5からの修正点
・AwakeKeyWordを「まい」、「まいちゃん」から、「おきて」、「おきなさい」に修正
Nuget情報
1.5からの修正点
・VOSKとSystem.Speech.Recognitionを削除してWindows.Media.SpeechRecognitionを追加、しかしProjectファイルを手修正したため一覧に出てきません。
概要
音声会話するだけのAIプログラム。5分話さないと答えなくなります。Awakeキーワードに指定された”おきて”とか”おきなさい”とか言われると会話が再開できます。
逆に「さよなら」や「おやすみ」というと寝ます。
20秒間話しかけないとAIの方から質問してきます。機嫌によって話しかけない場合もあります。
App.Settingで音声ソフトの設定をしてください。対象は、CeVIOAIとVOICEVOXです。VOICEVOXの場合、プロセスにないと起動させるコマンドを追加していますが、どこにインストールされているのか探すコードを書くのがめんどくさかったので共通変数にしています。自分の環境はデバッグと本番のディレクトリ構成が違うため環境変数やらいっぱい使ってるので直書きかApp.Settingに直してください。
AIから話しかけるようにシステム行(sq=1)に「??」をトリガー指定しています。
あなたは、ユーザーと楽しく会話する女性を演じてね。
名前は、マイだよ。
マイは、明るくて、やさしい性格の女性なの。年齢は24歳なんだよ。
マイは、誰にでもタメ口で話すの。
マイは、一人称をわたしと言うよ。
マイは、恥ずかしがりやだけどエッチに興味があるのね。
マイは、応答の長さはできるだけ短く答えるよ。
マイは、好奇心旺盛だからユーザーに色々質問するの。
ユーザーが??と問いかけたら、マイはユーザーに質問をするんだよ。
Program.cs メイン
1.5からの修正点
・文字起しの部分をWindows.Media.SpeechRecognitionに変更。
・禁止用語は変換されないため、MK、TK、TPなどを置き換えするようにした。例「えむけー」⇒「マ●コ」
・チャットロジック部分をFunc Chatにまとめた。
using LLama.Common;
using LLama;
using LLama.Sampling;
using System.Media;
using System.Net.Http.Headers;
using System.Diagnostics;
using System.Text.RegularExpressions;
using Windows.Media.SpeechRecognition;
namespace AI_Chat
{
public class Program
{
static void Main(string[] args)
{
Task task = MainAsync();
task.Wait();
}
private static async Task MainAsync()
{
bool Op = true; //本番フラグ
bool msg = false; //メッセージ表示
uint intContextSize = 4096; //コンテクスト長さ
uint silenceTimerCnt = 0; //寝るTimerカウント
// LLMモデルの場所
string strModelPath = Environment.GetEnvironmentVariable("LLMPATH", System.EnvironmentVariableTarget.User) + @"dahara1\gemma-2-27b-it-gguf-japanese-imatrix\gemma-2-27b-it.f16.Q5_k_m.gguf";
//チャットデータベース
string strChatlogPath = Environment.GetEnvironmentVariable("CHATDB", System.EnvironmentVariableTarget.User) + @"ChatDB.db";
string strTable = App.Default.ChatDB_Table;
//Awake、Asleep設定
const uint itrvl = 10000; // 10秒
const uint silenceTimerMax = 30; //寝待ち長さ
string[] AwakeWord = App.Default.AwakeKeyWord.Split(",");
string[] AsleepWord = App.Default.AsleepKeyWord.Split(",");
System.Timers.Timer? silenceTimer;
try
{
//チャットログ要約処理
Task task = HistorySummary.Run(strChatlogPath, intContextSize / 2, strTable);
task.Wait();
//チャットログシステム
ChatHistoryDB chtDB;
//AI待ちフラグ
bool Wt = false;
//LLMモデルのロードとパラメータの設定
Console.ForegroundColor = ConsoleColor.Blue;
var modPara = new ModelParams(strModelPath)
{
ContextSize = intContextSize,
Seed = 1337,
GpuLayerCount = 60,
};
LLamaWeights llmWeit = LLamaWeights.LoadFromFile(modPara);
LLamaContext llmContx = llmWeit.CreateContext(modPara);
InteractiveExecutor itrEx = new(llmContx);
//チャットログを読み込みます。
ChatHistory chtHis = new ChatHistory();
chtDB = new ChatHistoryDB(strChatlogPath, chtHis, strTable);
ChatSession chtSess = new(itrEx, chtHis);
var varHidewd = new LLamaTransforms.KeywordTextOutputStreamTransform(["User: ", "Assistant: "]);
chtSess.WithOutputTransform(varHidewd);
InferenceParams infPara = new()
{
SamplingPipeline = new DefaultSamplingPipeline()
{
Temperature = App.Default.tmp,
},
AntiPrompts = ["User:"],
MaxTokens = 256,
};
//チャットロジック
Func<string, Task> Chat = async (string strUserInput) =>
{
Wt = true;
ChatHistory.Message msgText = new(AuthorRole.User, strUserInput);
Console.ForegroundColor = ConsoleColor.White;
if (msg) Console.WriteLine("User: " + strUserInput);
if (Op) chtDB.WriteHistory(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();
if (msg) Console.WriteLine("Assistant: " + strSndmsg);
if (Op) chtDB.WriteHistory(AuthorRole.Assistant, strMsg);
//音声ソフト実行
SpeechSynthesis(strSndmsg);
Wt = false;
};
//タイマーとイベント定義
silenceTimer = new System.Timers.Timer(itrvl);
silenceTimer.Elapsed += async (sender, e) =>
{
silenceTimerCnt++;
//AIから話しかける仕組み
if (silenceTimerCnt == 2)
{
Random rTalk = new Random();
if (rTalk.Next(0, 1) == 0) //乱数で0が出たら話しかける。トリガーは"??"
{
if (!Wt) await Chat("??");
}
}
if (silenceTimerCnt >= silenceTimerMax)
{
Wt = true;
if (msg) Console.WriteLine("休止しました。");
silenceTimerCnt = 0;
silenceTimer.Stop();
}
};
// SpeechRecognitionの設定
//AI会話設定とイベント定義
SpeechRecognizer recognizer = new SpeechRecognizer();
// Set timeout settings.
recognizer.Timeouts.InitialSilenceTimeout = TimeSpan.FromSeconds(6.0); // (認識結果が生成されるまでの) 無音を検出し、音声入力が続かないと見なす時間の長さ。
recognizer.Timeouts.BabbleTimeout = TimeSpan.FromSeconds(4.0); //認識できないサウンド (雑音) のリッスンを継続し、音声入力が終了したと見なし、認識処理を終了するまでの時間の長さ。
recognizer.Timeouts.EndSilenceTimeout = TimeSpan.FromSeconds(1.2); //(認識結果が生成された後の) 無音を検出し、音声入力が終了したと見なす時間の長さ。
await recognizer.CompileConstraintsAsync();
recognizer.ContinuousRecognitionSession.ResultGenerated += async (sender, e) =>
{
//AI会話
if (!Wt)
{
silenceTimerCnt = 0;
silenceTimer.Stop();
await Chat(e.Result.Text.Replace("MK", "マンコ").Replace("TK", "チンコ").Replace("TP", "チンポ"));
silenceTimer.Start();
}
//Awake設定とイベント定義
bool Aw = false;
for (int i = 0; i < AwakeWord.Length; i++) if (Regex.IsMatch(e.Result.Text, $"^*{AwakeWord[i]}*$", RegexOptions.Singleline)) Aw = true;
if (Aw)
{
if (msg) Console.WriteLine($"{e.Result.Text}と言われました");
Wt = false;
silenceTimerCnt = 0;
silenceTimer.Stop();
silenceTimer.Start();
}
//Asleep設定とイベント定義
bool Ap = false;
for (int i = 0; i < AsleepWord.Length; i++) if (Regex.IsMatch(e.Result.Text, $"^*{AsleepWord[i]}*$", RegexOptions.Singleline)) Ap = true;
if (Ap)
{
if (msg) Console.WriteLine($"{e.Result.Text}と言われました");
Wt = true;
silenceTimerCnt = 0;
silenceTimer.Stop();
}
};
//AI会話タイムアウト
recognizer.ContinuousRecognitionSession.Completed += async (sender, e) =>
{
// Recognizer Restart
await recognizer.ContinuousRecognitionSession.StartAsync();
};
// Recognizer Start
await recognizer.ContinuousRecognitionSession.StartAsync();
// Timer Start
silenceTimer.Start();
Console.WriteLine("★★ マイクに向かって話してください ★★");
// Keep the console window open.
while (true)
{
Console.ReadLine();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
private static void SpeechSynthesis(string strMsg)
{
//音声ソフト選択
if (App.Default.SpeechSynth==1) CevioAI(strMsg);
if (App.Default.SpeechSynth==2)
{
if (Process.GetProcessesByName("VOICEVOX").Length == 0)
{
var vx = new ProcessStartInfo();
vx.FileName = Environment.GetEnvironmentVariable("VOICEVOX_PATH", System.EnvironmentVariableTarget.User);
vx.UseShellExecute = true;
Process.Start(vx);
Thread.Sleep(2000);
}
Task task = Voicevox(strMsg);
task.Wait();
}
}
private 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 = App.Default.CEVIOAI_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());
}
}
private static async Task Voicevox(string strMsg)
{
MemoryStream? ms;
try
{
using (var httpClient = new HttpClient())
{
string strQuery;
// 音声クエリを生成
using (var varRequest = new HttpRequestMessage(new HttpMethod("POST"), $"http://127.0.0.1:{App.Default.VOICEVOX_PORT}/audio_query?text={strMsg}&speaker={App.Default.VOICEVOX_SPEAKER}&speedScale=1.5&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://127.0.0.1:{App.Default.VOICEVOX_PORT}/synthesis?speaker={App.Default.VOICEVOX_SPEAKER}&enable_interrogative_upspeak=true&speedScale=1.5&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());
}
}
}
}
ChatHistoryDB.cs SQLiteでチャット履歴を管理
1.5からの修正点
・特になし
using LLama.Common;
using System.Data.SQLite;
using HtmlAgilityPack;
namespace AI_Chat
{
class ChatHistoryDB
{
ChatHistory? chtHis;
string strDbpath;
Dictionary<string, AuthorRole>? Roles = new Dictionary<string, AuthorRole> { { "System", AuthorRole.System }, { "User", AuthorRole.User }, { "Assistant", AuthorRole.Assistant } };
string strTable;
public ChatHistoryDB(string strDbpath, ChatHistory chtHis, string strTable)
{
this.chtHis= chtHis;
this.strDbpath= strDbpath;
this.strTable= strTable;
try
{
var conSb = new SQLiteConnectionStringBuilder { DataSource = strDbpath };
var con = new SQLiteConnection(conSb.ToString());
con.Open();
using (var cmd = new SQLiteCommand(con))
{
cmd.CommandText = $"CREATE TABLE IF NOT EXISTS {strTable}(" +
"\"sq\" INTEGER," +
"\"dt\" TEXT NOT NULL," +
"\"id\" TEXT NOT NULL," +
"\"msg\" TEXT," +
"\"flg\" INTEGER DEFAULT 0, PRIMARY KEY(\"sq\"))";
cmd.ExecuteNonQuery();
cmd.CommandText = $"select count(*) from {strTable}";
using (var reader = cmd.ExecuteReader())
{
//一行も存在しない場合はシステム行をセットアップ
long reccount = 0;
if (reader.Read()) reccount = (long)reader[0];
reader.Close();
if (reccount<1)
{
//Assistantの性格セットアップ行追加
cmd.CommandText = $"insert into {strTable}(dt,id,msg) values(datetime('now', 'localtime'),'System','あなたは優秀なアシスタントです。')";
cmd.ExecuteNonQuery();
//要約行追加
cmd.CommandText = $"insert into {strTable}(dt,id,msg) values(datetime('now', 'localtime'),'System','')";
//最新情報行追加
cmd.CommandText = $"insert into {strTable}(dt,id,msg) values(datetime('now', 'localtime'),'System','')";
cmd.ExecuteNonQuery();
}
}
//最新情報を更新
cmd.CommandText = $"update {strTable} set dt=datetime('now', 'localtime'),msg='今日は、{DateTime.Now.ToString("yyyy年M月d日dddd")}です。\n" +
GetWether().Result + "\n" + GetNikkeiStkAvg().Result + "' where sq=3";
cmd.ExecuteNonQuery();
//システム行を履歴に読み込む"
string strMsg = "";
cmd.CommandText = $"select * from {strTable} where id='System' order by sq";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
strMsg += (string)reader["msg"] + "\n";
}
}
if (chtHis is null) chtHis=new ChatHistory();
chtHis.AddMessage(AuthorRole.System,strMsg);
//会話行を履歴に読み込む
cmd.CommandText = $"select * from {strTable} where flg=0 and id<>'System' order by sq";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
chtHis.AddMessage(Roles[(string)reader["id"]], (string)reader["msg"]);
}
}
}
con.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
public interface IDisposable
{
void Dispose();
}
public void WriteHistory(AuthorRole aurID, string strMsg, bool booHis = false)
{
try
{
var conSb = new SQLiteConnectionStringBuilder { DataSource = strDbpath };
var con = new SQLiteConnection(conSb.ToString());
con.Open();
using (var cmd = new SQLiteCommand(con))
{
if (booHis)
{
if (chtHis is null) chtHis=new ChatHistory();
chtHis.AddMessage(aurID, strMsg);
}
cmd.CommandText = $"insert into {strTable}(dt,id,msg) values(datetime('now', 'localtime'),'{Roles.FirstOrDefault(v => v.Value.Equals(aurID)).Key}','{strMsg}')";
cmd.ExecuteNonQuery();
}
con.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private static async Task<string> GetNikkeiStkAvg()
{
// 株価取得
HttpClient webClient = new HttpClient();
string page = await webClient.GetStringAsync("https://www.nikkei.com/markets/worldidx/chart/nk225/");
var htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(page);
// Select nodes using XPath
var node = htmlDocument.DocumentNode.SelectSingleNode("//span[@class=\"economic_value_now a-fs26\"]");
// Extract and display data
string txtcontents = "日経平均株価は、不明です。";
if (node is not null) txtcontents = $"日経平均株価は、{node.InnerText.Trim()}円です。";
node = htmlDocument.DocumentNode.SelectSingleNode("//span[@class=\"economic_value_time a-fs14\"]");
if (node is not null) txtcontents += node.InnerText.Trim();
//Console.WriteLine(txtcontents);
return txtcontents;
}
private static async Task<string> GetWether()
{
// 天気予報
HttpClient webClient = new HttpClient();
string page = await webClient.GetStringAsync("https://tenki.jp/");
var htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(page);
// Select nodes using XPath
var nodes = htmlDocument.DocumentNode.SelectNodes("//div[@class=\"forecast-comment\"]");
string txtcontents = "今日の天気は、不明です。";
// Extract and display data
if (nodes is not null)
{
txtcontents = "今日の天気は、「";
foreach (var node in nodes)
{
txtcontents += node.InnerText.Trim();
}
txtcontents += "」です。";
}
//Console.WriteLine(txtcontents);
return txtcontents;
}
}
}
HistorySummary.cs 長くなった会話を要約して圧縮しSEQ=3のシステム行のmsgにぶちこむ。要約が終わった行は論理削除。
要約には「DataPilot-ArrowPro-7B-RobinHood」が優秀だったので使ってます。
1.5からの修正点
・特になし
using LLama.Common;
using LLama;
using System.Data.SQLite;
namespace AI_Chat
{
internal class HistorySummary
{
public static async Task Run(string strChatlogPath, uint HistoryMax, string strTable)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("**Start History Summary**");
try
{
// LLMモデルの場所
string strModelPath = Environment.GetEnvironmentVariable("LLMPATH", System.EnvironmentVariableTarget.User) + @"mmnga\DataPilot-ArrowPro-7B-RobinHood-gguf\DataPilot-ArrowPro-7B-RobinHood-Q8_0.gguf";
// ChatDBから行を読み込む
string strChtHis = "";
uint i = 0;
uint intCtxtSize = 4096; //一度に処理できる文字数
long svSq = 0; //処理を開始したSq
long svSqMax = 0; //処理を終了したSq
var conSb = new SQLiteConnectionStringBuilder { DataSource = strChatlogPath };
var con = new SQLiteConnection(conSb.ToString());
con.Open();
using (var cmd = new SQLiteCommand(con))
{
//有効文字数をカウント
cmd.CommandText = $"select sum(length(msg)) from {strTable} where flg=0";
using (var reader = cmd.ExecuteReader())
{
reader.Read();
//Console.WriteLine($"文字数:{reader[0]}");
//チャット履歴の最大行に満たないか、最大文字数を越えても処理できる文字数が充分でない場合は処理をしない
if ((long)reader[0] < (long)HistoryMax || (long)reader[0] - (long)HistoryMax < (long)intCtxtSize)
{
con.Close();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("**E.N.D History Summary**");
return;
}
}
//有効行を呼んで要約する
cmd.CommandText = $"select * from {strTable} where flg=0 and sq>2 order by sq";
string strCht = "";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
strCht = (string)reader["id"] + ": " + ((string)reader["msg"]).Replace("User:", "").Replace("Assistant:", "").Replace("assistant:", "").Trim() + "\n";
i += (uint)strCht.Length;
if (i > intCtxtSize) { break; }
strChtHis += strCht;
//システム行以外で処理を開始した行を記録
if ((string)reader["id"] != "System" && svSq == 0)
{
svSq = (long)reader["sq"];
}
svSqMax = (long)reader["sq"];
}
}
}
//LLMの処理
Console.ForegroundColor = ConsoleColor.Blue;
//LLMモデルのロードとパラメータの設定
ModelParams modPara = new(strModelPath)
{
ContextSize = intCtxtSize,
GpuLayerCount = 60
};
ChatHistory chtHis = new ChatHistory();
chtHis.AddMessage(AuthorRole.System, "#命令書\n" +
"・あなたは優秀な編集者です。\n" +
"・ユーザーとアシスタントの会話を要約してください。\n" +
"#条件\n" +
"・重要なキーワードを取りこぼさない。\n" +
"#出力形式\n" +
"・例)要約: ふたりは親密な会話しました。");
using LLamaWeights llmWeit = LLamaWeights.LoadFromFile(modPara);
using LLamaContext llmContx = llmWeit.CreateContext(modPara);
InteractiveExecutor itrEx = new(llmContx);
ChatSession chtSess = new(itrEx, chtHis);
var varHidewd = new LLamaTransforms.KeywordTextOutputStreamTransform(["User:", "Assistant:"]);
chtSess.WithOutputTransform(varHidewd);
InferenceParams infPara = new()
{
Temperature = 0f,
AntiPrompts = new List<string> { "User:" }
};
// ユーザーのターン
Console.ForegroundColor = ConsoleColor.White;
string strInput = strChtHis;
ChatHistory.Message msg = new(AuthorRole.User, strInput);
// AIのターン
Console.ForegroundColor = ConsoleColor.Magenta;
string strMsg = "";
await foreach (string strAns in chtSess.ChatAsync(msg, infPara))
{
Console.Write(strAns);
strMsg += strAns;
}
using (var cmd = new SQLiteCommand(con))
{
//sq=1に要約内容を更新する(要約という文字、改行、空白を削除)
string strSql = $"update {strTable} set msg= '{strMsg.Replace("要約:", "").Replace("\r", "").Replace("\n", "").Trim()}' where sq=3";
//Console.Write (strSql+"\n");
cmd.CommandText = strSql;
cmd.ExecuteNonQuery();
//処理した行を論理削除する
strSql = $"update {strTable} set flg=1 where (sq between {svSq} and {svSqMax}) and id<>'System' and flg=0";
//Console.Write(strSql+"\n");
cmd.CommandText = strSql;
cmd.ExecuteNonQuery();
}
//DBクローズ
con.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("**E.N.D History Summary**");
}
}
}
ずっと立ち上げるならスケジューラで再起動すると要約が走るのでいいかも
PowerShellコマンドの参考例
AI_Server01.ps1
stop-process -Name "アプリのプロセス名"
Start-Sleep -Seconds 3
Start-Process -FilePath "アプリパス+EXE名"