ローカルLLMをCeVIO AIでしゃべらせる
今までVoiceVoxを使っていたのですが発音が少し気になったのでCeVIO AIの「夏色花梨」を購入しました。CeVIO AIを選んだ理由は、APIに対応していること、比較的新しいことです。夏色花梨は2.0にアップグレードされてより声が鮮明になってるようです。
APIに対応してるから安心していたのですが、これが結構大変でした。.NET Framewaork対応でcomオブジェクトを使っているんですよね。無理やり.NET8.0からcom参照で呼び出してもエラーになります。結局下記コーディングで話せるようになりました。
参考サイト:CeVIOユーザー互助会
変数をdynamicにするとVisualStudioでエラーになりません。
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());
}
}
色々試してみたのですが、残念ながら上記のやり方だと感情パラメータが触れないことがわかりました。まあ、AIと会話中に動的に感情パラメータをイジることはないのでソフト側の既定のプリセットを設定すれば変更できるかなと思います。
既定プリセット設定方法
感情パラメータをいじったあと新しいプリセットを登録します。
例)嬉しい50+普通50+哀しみ50⇒普通2
ツールのオプションで既定のプリセットに登録したプリセットを設定します
ChatHistoryDBクラスを少し変更しました。Switchを使ってAutherRoleとStringを変換していましたがDictionaryに変更しました。
kvキャッシュを理解できていませんので、この仕組みが正しいのか怪しいです。なので標準のLoadSessionとSaveSessionを使った方が無難かもしれません。
履歴が多くなるとエラーで落ちるかも知れませんのでProgram.cs内のModelParamsのContextSizeを大きめに取ると回避できると思います。
ChatHistoryDB.cs
using LLama.Common;
using System.Data.SQLite;
namespace ChatProgram
{
class ChatHistoryDB
{
ChatHistory? chtHis;
string strDbpath;
Dictionary<string, AuthorRole>? Roles = new Dictionary<string, AuthorRole> { { "System" ,AuthorRole.System}, { "User", AuthorRole.User }, { "Assistant", AuthorRole.Assistant } };
public ChatHistoryDB(string strDbpath, ChatHistory chtHis){
this.chtHis= chtHis;
this.strDbpath= strDbpath;
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 ch(" +
"sq INTEGER PRIMARY KEY," +
"dt TEXT NOT NULL," +
"id TEXT NOT NULL," +
"msg TEXT)";
cmd.ExecuteNonQuery();
cmd.CommandText="select * from ch order by sq";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
if (chtHis is null) chtHis=new ChatHistory();
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 ch(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);
}
}
}
}
LLMは、「Shadows-MoE」を使用しています。今のところ一番会話がしっくりきています。それとエロくていい感じです。
※引き続きVoiceVoxを使用したい場合は、MainAsyncの下の方のコメントをさわってください
Nuget情報
Program.cs
using LLama.Common;
using LLama;
using System.Media;
using System.Net.Http.Headers;
namespace ChatProgram
{
public class Program
{
static void Main(string[] args)
{
//コンソールアプリケーションからAsyncを呼び出す
Task task = MainAsync();
//終了を待つ
task.Wait();
}
public static async Task MainAsync()
{
bool Op = true;
string strModelPath = Environment.GetEnvironmentVariable("LLMPATH", System.EnvironmentVariableTarget.User) + @"mradermacher\Shadows-MoE-GGUF\Shadows-MoE.Q8_0.gguf";
try
{
string strChatlogPath = Environment.GetEnvironmentVariable("CHATDB", System.EnvironmentVariableTarget.User) + @"ChatDB.db";
ChatHistoryDB chtDB;
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 = new ChatHistory();
chtDB = new ChatHistoryDB(strChatlogPath, chtHis);
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,
};
// ユーザーからの質問
Console.ForegroundColor = ConsoleColor.White;
Console.Write("\nUser: ");
string strUserInput = "";
strUserInput = Console.ReadLine() ?? string.Empty;
while (strUserInput != "exit") //チャットはexitで終了します
{
ChatHistory.Message msgText = new(AuthorRole.User, "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();
Console.WriteLine(strSndmsg);
if (Op) chtDB.WriteHistory(AuthorRole.Assistant, strMsg.Replace("User:", "").Trim());
//VoiceVoxを仕様する場合は、下の2行のコメントを外してCevioAIをコメントアウトしてください
//Task task = Voicevox(strSndmsg);
//task.Wait();
CevioAI(strSndmsg);
// ユーザーからの質問
Console.ForegroundColor = ConsoleColor.White;
Console.Write("\nUser: ");
strUserInput = Console.ReadLine() ?? string.Empty;
}
}
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 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());
}
}
}
}