From 2b65adceb9bd957ae6a979a4d60f879892974fdc Mon Sep 17 00:00:00 2001 From: herring101 <81513223+herring101@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:00:28 +0900 Subject: [PATCH 01/21] no --- Epub/KoeBook.Epub/Services/AnalyzerService.cs | 19 +-- .../Contracts/Services/IClaudeService.cs | 8 + .../Contracts/Services/ILlmAnalyzerService.cs | 2 +- KoeBook.Core/EbookException.cs | 6 + KoeBook.Core/KoeBook.Core.csproj | 1 + .../Services/ChatGptAnalyzerService.cs | 14 +- .../Services/ClaudeAnalyzerService.cs | 159 ++++++++++++++++++ KoeBook.Core/Services/ClaudeService.cs | 31 ++++ 8 files changed, 221 insertions(+), 19 deletions(-) create mode 100644 KoeBook.Core/Contracts/Services/IClaudeService.cs create mode 100644 KoeBook.Core/Services/ClaudeAnalyzerService.cs create mode 100644 KoeBook.Core/Services/ClaudeService.cs diff --git a/Epub/KoeBook.Epub/Services/AnalyzerService.cs b/Epub/KoeBook.Epub/Services/AnalyzerService.cs index b8fa5cb..4c536db 100644 --- a/Epub/KoeBook.Epub/Services/AnalyzerService.cs +++ b/Epub/KoeBook.Epub/Services/AnalyzerService.cs @@ -1,5 +1,4 @@ -using System.Text; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using KoeBook.Core; using KoeBook.Core.Contracts.Services; using KoeBook.Core.Models; @@ -65,21 +64,7 @@ public async ValueTask AnalyzeAsync(BookProperties bookProperties, } } - // 800文字以上になったら1チャンクに分ける - var chunks = new List(); - var chunk = new StringBuilder(); - foreach (var line in scriptLines) - { - if (chunk.Length + line.Text.Length > 800) - { - chunks.Add(chunk.ToString()); - chunk.Clear(); - } - chunk.AppendLine(line.Text); - } - if (chunk.Length > 0) chunks.Add(chunk.ToString()); - - // GPT4による話者、スタイル解析 + // LLMによる話者、スタイル解析 var bookScripts = await _llmAnalyzerService.LlmAnalyzeScriptLinesAsync(bookProperties, scriptLines, chunks, cancellationToken); return bookScripts; diff --git a/KoeBook.Core/Contracts/Services/IClaudeService.cs b/KoeBook.Core/Contracts/Services/IClaudeService.cs new file mode 100644 index 0000000..d5cc5ea --- /dev/null +++ b/KoeBook.Core/Contracts/Services/IClaudeService.cs @@ -0,0 +1,8 @@ +using Claudia; + +namespace KoeBook.Core.Contracts.Services; + +public interface IClaudeService +{ + IMessages? Messages { get; } +} diff --git a/KoeBook.Core/Contracts/Services/ILlmAnalyzerService.cs b/KoeBook.Core/Contracts/Services/ILlmAnalyzerService.cs index ddf948b..02d2098 100644 --- a/KoeBook.Core/Contracts/Services/ILlmAnalyzerService.cs +++ b/KoeBook.Core/Contracts/Services/ILlmAnalyzerService.cs @@ -4,5 +4,5 @@ namespace KoeBook.Core.Contracts.Services; public interface ILlmAnalyzerService { - ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bookProperties, List scriptLines, List chunks, CancellationToken cancellationToken); + ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bookProperties, List scriptLines, CancellationToken cancellationToken); } diff --git a/KoeBook.Core/EbookException.cs b/KoeBook.Core/EbookException.cs index e8590e8..9ca421f 100644 --- a/KoeBook.Core/EbookException.cs +++ b/KoeBook.Core/EbookException.cs @@ -44,6 +44,12 @@ public enum ExceptionType [EnumMember(Value = "GPT4による話者・スタイル設定に失敗しました")] Gpt4TalkerAndStyleSettingFailed, + [EnumMember(Value = "APIキーが設定されていません")] + ApiKeyNotSet, + + [EnumMember(Value = "Claudeによる話者・スタイル設定に失敗しました")] + ClaudeTalkerAndStyleSettingFailed, + [EnumMember(Value = "webページの解析に失敗しました")] WebScrapingFailed } diff --git a/KoeBook.Core/KoeBook.Core.csproj b/KoeBook.Core/KoeBook.Core.csproj index 3963f59..cb6e17d 100644 --- a/KoeBook.Core/KoeBook.Core.csproj +++ b/KoeBook.Core/KoeBook.Core.csproj @@ -10,6 +10,7 @@ + diff --git a/KoeBook.Core/Services/ChatGptAnalyzerService.cs b/KoeBook.Core/Services/ChatGptAnalyzerService.cs index dd0d84e..747a3a7 100644 --- a/KoeBook.Core/Services/ChatGptAnalyzerService.cs +++ b/KoeBook.Core/Services/ChatGptAnalyzerService.cs @@ -13,8 +13,20 @@ public partial class ChatGptAnalyzerService(IOpenAIService openAIService, IDispl private readonly IOpenAIService _openAiService = openAIService; private readonly IDisplayStateChangeService _displayStateChangeService = displayStateChangeService; - public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bookProperties, List scriptLines, List chunks, CancellationToken cancellationToken) + public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bookProperties, List scriptLines, CancellationToken cancellationToken) { + var chunks = new List(); + var chunk = new StringBuilder(); + foreach (var line in scriptLines) + { + if (chunk.Length + line.Text.Length > 800) + { + chunks.Add(chunk.ToString()); + chunk.Clear(); + } + chunk.AppendLine(line.Text); + } + if (chunk.Length > 0) chunks.Add(chunk.ToString()); var progress = _displayStateChangeService.ResetProgress(bookProperties, GenerationState.Analyzing, chunks.Count); Queue summaryList = new(); Queue characterList = new(); diff --git a/KoeBook.Core/Services/ClaudeAnalyzerService.cs b/KoeBook.Core/Services/ClaudeAnalyzerService.cs new file mode 100644 index 0000000..c6f8298 --- /dev/null +++ b/KoeBook.Core/Services/ClaudeAnalyzerService.cs @@ -0,0 +1,159 @@ +using KoeBook.Core.Contracts.Services; +using KoeBook.Core.Models; + +namespace KoeBook.Core.Services; + +public partial class ClaudeAnalyzerService(IClaudeService claudeService, IDisplayStateChangeService displayStateChangeService) : ILlmAnalyzerService +{ + private readonly IClaudeService _claudeService = claudeService; + private readonly IDisplayStateChangeService _displayStateChangeService = displayStateChangeService; + + public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bookProperties, List scriptLines, CancellationToken cancellationToken) + { + var lineNumberingText = LineNumbering(scriptLines); + if (_claudeService.Messages is null) + { + throw new EbookException(ExceptionType.ApiKeyNotSet); + } + try + { + var message1 = await _claudeService.Messages.CreateAsync(new() + { + Model = "claude-3-opus-20240229", // you can use Claudia.Models.Claude3Opus string constant + MaxTokens = 4000, + Messages = [new() { + Role = "user", + Content = CreatePrompt1(lineNumberingText) + }] + }, + cancellationToken: cancellationToken + ); + var (characterList, voiceIds) = ExtractCharacterListAndVoiceIds(message1.ToString()); + + var message2 = await _claudeService.Messages.CreateAsync(new() + { + Model = "claude-3-opus-20240229", // you can use Claudia.Models.Claude3Opus string constant + MaxTokens = 4000, + Messages = [new() { + Role = "user", + Content = CreatePrompt2(voiceList) + }] + }, + cancellationToken: cancellationToken + ); + + var characterVoiceMapping = ExtractCharacterVoiceMapping(message2.ToString()); + } + catch (OperationCanceledException) + { + throw; + } + catch + { + throw new EbookException(ExceptionType.ClaudeTalkerAndStyleSettingFailed); + } + } + + private string CreatePrompt1(string lineNumberingText) + { + return $$""" + {{lineNumberingText}} + + Notes: + - For narration parts, which are not enclosed in quotation marks, select the narrator. + - For dialogues enclosed in quotation marks, assign a voice other than the narrator. + - In the character description, include the appropriate voice characteristics. + Tasks: Based on the notes above, perform the following two tasks: + - List of characters Objective: To understand the characters appearing in the text, list the character ID, name, and description for all characters who speak at least one line. + - Output the speaking character or narrator for each line Objective: To identify which character is speaking in each line of the text, output the speaking character or narrator for all lines. Carefully recognize the context and output with attention. Select only one character_id per line. + - Revise CHARACTER LIST and VOICE ID (in the event that the CHARACTER LIST is incomplete) + Output Format: + [CHARACTER LIST] + c0. ナレーター: The person who speaks the narration parts. A calm-toned male voice. + c1. {character_name}: {character_description} + {character_id}. {character_name}: {character_description} + ... + [VOICE ID] + 1. {character_id} {narration|dialogue} + 2. {character_id} {narration|dialogue} + 3. {character_id} {narration|dialogue} + ... + [REVISE CHARACTER LIST] + c0. ナレーター: The person who speaks the narration parts. A calm-toned male voice. + c1. {character_name}: {character_description} + {character_id}. {character_name}: {character_description} + ... + [REVISE VOICE ID] + 1. {character_id} {narration|dialogue} + 2. {character_id} {narration|dialogue} + 3. {character_id} {narration|dialogue} + ... + """; + } + + private string CreatePrompt2(List scriptLines, List characterList, List voiceIds) + { + return $$""" + + } + + private string LineNumbering(List scriptLines) + { + var sb = new StringBuilder(); + foreach (var (index, scriptLine) in scriptLines.Select((x, i) => (i, x))) + { + sb.AppendLine($"{index + 1}. {scriptLine.Text}"); + } + return sb.ToString(); + } + + (List, List) ExtractCharacterListAndVoiceIds(string response) + { + var characterList = new List(); + var voiceIds = new List(); + var lines = response.Split("\n"); + var characterListStartIndex = 0; + var characterListEndIndex = 0; + for (var i = 0; i < lines.Length; i++) + { + if (lines[i].StartsWith("[REVISE CHARACTER LIST]")) + { + characterListStartIndex = i + 1; + } + + if (lines[i].StartsWith("[REVISE VOICE ID]")) + { + characterListEndIndex = i; + } + } + for (var i = characterListStartIndex; i < characterListEndIndex; i++) + { + var line = lines[i]; + if (line.StartsWith('c')) + { + var characterId = line[1..line.IndexOf('.')]; + var characterName = line[(line.IndexOf('.') + 2)..line.IndexOf(':')]; + var characterDescription = line[(line.IndexOf(':') + 2)..]; + characterList.Add(new Character(characterId, characterName, characterDescription)); + } + } + + { + var dest = (stackalloc Range[4]); + for (var i = characterListEndIndex + 1; i < lines.Length; i++) + { + var line = lines[i].AsSpan(); + if (line.Length > 0 && line.Contains('c')) + { + if (line.Split(dest, ' ') > 3) + throw new EbookException(ExceptionType.ClaudeTalkerAndStyleSettingFailed); + var characterId = line[dest[1]]; + var narrationOrDialogue = line[dest[2]]; + } + } + } + return (characterList, voiceIds); + } + + private record Character(string Id, string Name, string Description); +} diff --git a/KoeBook.Core/Services/ClaudeService.cs b/KoeBook.Core/Services/ClaudeService.cs new file mode 100644 index 0000000..d06db0d --- /dev/null +++ b/KoeBook.Core/Services/ClaudeService.cs @@ -0,0 +1,31 @@ +using Claudia; +using KoeBook.Core.Contracts.Services; + +namespace KoeBook.Core.Services; + +public class ClaudeService(ISecretSettingsService secretSettingsService) : IClaudeService +{ + private readonly ISecretSettingsService _secretSettingsService = secretSettingsService; + + private string? _apiKey; + private Anthropic? _anthropic; + + public IMessages? Messages => GetAnthropic()?.Messages; + + + private Anthropic? GetAnthropic() + { + if (_apiKey != _secretSettingsService.ApiKey) + { + if (string.IsNullOrEmpty(_secretSettingsService.ApiKey)) + { + _apiKey = _secretSettingsService.ApiKey; + return null; + } + + _anthropic = new Anthropic { ApiKey = _secretSettingsService.ApiKey }; + _apiKey = _secretSettingsService.ApiKey; + } + return _anthropic; + } +} From bae10e2bfa501b0394d3570be31f64e1cb0e586d Mon Sep 17 00:00:00 2001 From: aiueo-1234 <130837816+aiueo-1234@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:06:53 +0900 Subject: [PATCH 02/21] =?UTF-8?q?#25=20=E7=B0=A1=E5=8D=98=E3=81=AA?= =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=89=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Epub/KoeBook.Epub/Services/AnalyzerService.cs | 8 ++-- .../Services/ChatGptAnalyzerService.cs | 6 +-- .../Services/ClaudeAnalyzerService.cs | 39 ++++++++++++------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/Epub/KoeBook.Epub/Services/AnalyzerService.cs b/Epub/KoeBook.Epub/Services/AnalyzerService.cs index 4c536db..08b3a74 100644 --- a/Epub/KoeBook.Epub/Services/AnalyzerService.cs +++ b/Epub/KoeBook.Epub/Services/AnalyzerService.cs @@ -12,10 +12,10 @@ public partial class AnalyzerService(IScraperSelectorService scrapingService, IE private readonly IScraperSelectorService _scrapingService = scrapingService; private readonly IEpubDocumentStoreService _epubDocumentStoreService = epubDocumentStoreService; private readonly ILlmAnalyzerService _llmAnalyzerService = llmAnalyzerService; - private Dictionary _rubyReplacements = new Dictionary(); public async ValueTask AnalyzeAsync(BookProperties bookProperties, string tempDirectory, string coverFilePath, CancellationToken cancellationToken) { + var _rubyReplacements = new Dictionary(); coverFilePath = Path.Combine(tempDirectory, "Cover.png"); using var fs = File.Create(coverFilePath); await fs.WriteAsync(CoverFile.ToArray(), cancellationToken); @@ -42,8 +42,8 @@ public async ValueTask AnalyzeAsync(BookProperties bookProperties, { if (element is Paragraph paragraph) { - var line = paragraph.Text; - // rubyタグがあればルビのdictionaryに登録 + var line = paragraph.Text ?? ""; + // rubyタグがあればルビの dictionary に登録 var rubyDict = ExtractRuby(line); foreach (var ruby in rubyDict) @@ -65,7 +65,7 @@ public async ValueTask AnalyzeAsync(BookProperties bookProperties, } // LLMによる話者、スタイル解析 - var bookScripts = await _llmAnalyzerService.LlmAnalyzeScriptLinesAsync(bookProperties, scriptLines, chunks, cancellationToken); + var bookScripts = await _llmAnalyzerService.LlmAnalyzeScriptLinesAsync(bookProperties, scriptLines, cancellationToken)!; return bookScripts; } diff --git a/KoeBook.Core/Services/ChatGptAnalyzerService.cs b/KoeBook.Core/Services/ChatGptAnalyzerService.cs index 747a3a7..96a06eb 100644 --- a/KoeBook.Core/Services/ChatGptAnalyzerService.cs +++ b/KoeBook.Core/Services/ChatGptAnalyzerService.cs @@ -159,7 +159,7 @@ 3. Target Sentence }, Model = OpenAI.ObjectModels.Models.Gpt_4_turbo_preview, MaxTokens = 4000 - }); + }, cancellationToken: cancellationToken); if (completionResult.Successful) { var result = completionResult.Choices.First().Message.Content; @@ -273,7 +273,7 @@ 3. Story }, Model = OpenAI.ObjectModels.Models.Gpt_4_turbo_preview, MaxTokens = 4000 - }); + }, cancellationToken: cancellationToken); if (completionResult.Successful) { var result = completionResult.Choices.First().Message.Content; @@ -368,7 +368,7 @@ Make a table of character names and voices }, Model = OpenAI.ObjectModels.Models.Gpt_4_turbo_preview, MaxTokens = 4000 - }); + }, cancellationToken: cancellationToken); if (completionResult.Successful) { var result = completionResult.Choices.First().Message.Content; diff --git a/KoeBook.Core/Services/ClaudeAnalyzerService.cs b/KoeBook.Core/Services/ClaudeAnalyzerService.cs index c6f8298..e2d47d5 100644 --- a/KoeBook.Core/Services/ClaudeAnalyzerService.cs +++ b/KoeBook.Core/Services/ClaudeAnalyzerService.cs @@ -1,4 +1,5 @@ -using KoeBook.Core.Contracts.Services; +using System.Text; +using KoeBook.Core.Contracts.Services; using KoeBook.Core.Models; namespace KoeBook.Core.Services; @@ -21,10 +22,11 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo { Model = "claude-3-opus-20240229", // you can use Claudia.Models.Claude3Opus string constant MaxTokens = 4000, - Messages = [new() { - Role = "user", - Content = CreatePrompt1(lineNumberingText) - }] + Messages = [new() + { + Role = "user", + Content = CreatePrompt1(lineNumberingText) + }] }, cancellationToken: cancellationToken ); @@ -34,15 +36,18 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo { Model = "claude-3-opus-20240229", // you can use Claudia.Models.Claude3Opus string constant MaxTokens = 4000, - Messages = [new() { - Role = "user", - Content = CreatePrompt2(voiceList) - }] + Messages = [new() + { + Role = "user", + Content = CreatePrompt2(characterList, voiceIds) + }] }, cancellationToken: cancellationToken ); var characterVoiceMapping = ExtractCharacterVoiceMapping(message2.ToString()); + + return new(bookProperties, new(characterVoiceMapping)) { ScriptLines = scriptLines }; } catch (OperationCanceledException) { @@ -54,7 +59,12 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo } } - private string CreatePrompt1(string lineNumberingText) + private Dictionary ExtractCharacterVoiceMapping(string response) + { + throw new NotImplementedException(); + } + + private static string CreatePrompt1(string lineNumberingText) { return $$""" {{lineNumberingText}} @@ -91,13 +101,12 @@ [REVISE VOICE ID] """; } - private string CreatePrompt2(List scriptLines, List characterList, List voiceIds) + private static string CreatePrompt2(List characterList, List voiceIds) { - return $$""" - + throw new NotImplementedException(); } - private string LineNumbering(List scriptLines) + private static string LineNumbering(List scriptLines) { var sb = new StringBuilder(); foreach (var (index, scriptLine) in scriptLines.Select((x, i) => (i, x))) @@ -107,7 +116,7 @@ private string LineNumbering(List scriptLines) return sb.ToString(); } - (List, List) ExtractCharacterListAndVoiceIds(string response) + private static (List, List) ExtractCharacterListAndVoiceIds(string response) { var characterList = new List(); var voiceIds = new List(); From c5b1ab2261d294822be0209c48bd98e3bcf40f38 Mon Sep 17 00:00:00 2001 From: herring101 <81513223+herring101@users.noreply.github.com> Date: Sat, 13 Apr 2024 15:32:09 +0900 Subject: [PATCH 03/21] =?UTF-8?q?character=E8=A7=A3=E6=9E=90=20(=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E4=BB=98=E3=81=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/ClaudeAnalyzerService.cs | 103 +++++++++++++++--- KoeBook/App.xaml.cs | 4 +- 2 files changed, 87 insertions(+), 20 deletions(-) diff --git a/KoeBook.Core/Services/ClaudeAnalyzerService.cs b/KoeBook.Core/Services/ClaudeAnalyzerService.cs index e2d47d5..a9d21e6 100644 --- a/KoeBook.Core/Services/ClaudeAnalyzerService.cs +++ b/KoeBook.Core/Services/ClaudeAnalyzerService.cs @@ -4,10 +4,11 @@ namespace KoeBook.Core.Services; -public partial class ClaudeAnalyzerService(IClaudeService claudeService, IDisplayStateChangeService displayStateChangeService) : ILlmAnalyzerService +public partial class ClaudeAnalyzerService(IClaudeService claudeService, IDisplayStateChangeService displayStateChangeService, ISoundGenerationSelectorService soundGenerationSelectorService) : ILlmAnalyzerService { private readonly IClaudeService _claudeService = claudeService; private readonly IDisplayStateChangeService _displayStateChangeService = displayStateChangeService; + private readonly ISoundGenerationSelectorService _soundGenerationSelectorService = soundGenerationSelectorService; public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bookProperties, List scriptLines, CancellationToken cancellationToken) { @@ -30,7 +31,7 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo }, cancellationToken: cancellationToken ); - var (characterList, voiceIds) = ExtractCharacterListAndVoiceIds(message1.ToString()); + var characterList = ExtractCharacterList(message1.ToString()); var message2 = await _claudeService.Messages.CreateAsync(new() { @@ -39,13 +40,13 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo Messages = [new() { Role = "user", - Content = CreatePrompt2(characterList, voiceIds) + Content = CreatePrompt2(characterList) }] }, cancellationToken: cancellationToken ); - var characterVoiceMapping = ExtractCharacterVoiceMapping(message2.ToString()); + var characterVoiceMapping = ExtractCharacterVoiceMapping(message2.ToString(), characterList); return new(bookProperties, new(characterVoiceMapping)) { ScriptLines = scriptLines }; } @@ -59,9 +60,39 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo } } - private Dictionary ExtractCharacterVoiceMapping(string response) + private Dictionary ExtractCharacterVoiceMapping(string response, List characterList) { - throw new NotImplementedException(); + var characterVoiceMapping = new Dictionary(); + var lines = response.Split("\n"); + var characterListStartIndex = 0; + for (var i = 0; i < lines.Length; i++) + { + if (lines[i].StartsWith("[Assign Voices]")) + { + characterListStartIndex = i + 1; + } + } + + for (var i = characterListStartIndex; i < lines.Length; i++) + { + var line = lines[i]; + if (line.StartsWith('c')) + { + var characterId = line[1..line.IndexOf('.')]; + var voiceType = line[(line.IndexOf(':') + 2)..]; + // voiceTypeが_soundGenerationSelectorService.Modelsに含まれているかチェック + if (_soundGenerationSelectorService.Models.Any(x => x.Name == voiceType)) + { + characterVoiceMapping.Add(characterId, voiceType); + } + else + { + characterVoiceMapping.Add(characterId, string.Empty); + } + } + } + + return characterVoiceMapping; } private static string CreatePrompt1(string lineNumberingText) @@ -79,9 +110,9 @@ private static string CreatePrompt1(string lineNumberingText) - Revise CHARACTER LIST and VOICE ID (in the event that the CHARACTER LIST is incomplete) Output Format: [CHARACTER LIST] - c0. ナレーター: The person who speaks the narration parts. A calm-toned male voice. - c1. {character_name}: {character_description} - {character_id}. {character_name}: {character_description} + c0. ナレーター: {character_and_voice_description, example:"The person who speaks the narration parts. A calm-toned male voice."} + c1. {character_name}: {character_and_voice_description} + {character_id}. {character_name}: {character_and_voice_description} ... [VOICE ID] 1. {character_id} {narration|dialogue} @@ -89,9 +120,9 @@ [VOICE ID] 3. {character_id} {narration|dialogue} ... [REVISE CHARACTER LIST] - c0. ナレーター: The person who speaks the narration parts. A calm-toned male voice. - c1. {character_name}: {character_description} - {character_id}. {character_name}: {character_description} + c0. ナレーター: {character_and_voice_description} + c1. {character_name}: {character_and_voice_description} + {character_id}. {character_name}: {character_and_voice_description} ... [REVISE VOICE ID] 1. {character_id} {narration|dialogue} @@ -101,9 +132,34 @@ [REVISE VOICE ID] """; } - private static string CreatePrompt2(List characterList, List voiceIds) + private string CreatePrompt2(List characterList) { - throw new NotImplementedException(); + StringBuilder sb = new StringBuilder(); + foreach (var character in characterList) + { + sb.AppendLine($"c{character.Id}. {character.Name}: {character.Description}"); + } + + StringBuilder sb2 = new StringBuilder(); + foreach (var voiceType in _soundGenerationSelectorService.Models) + { + sb2.Append($"{voiceType.Name},"); + } + + return $$""" + Assign the most fitting voice type to each character from the provided list, ensuring the chosen voice aligns with their role and attributes in the story. Only select from the available voice types. + + Characters: + {{sb}} + + Voice Types: + {{sb2}} + + Output Format: + [Assign Voices] + c0. {character_name}: {voice_type} + c1. {character_name}: {voice_type} + """; } private static string LineNumbering(List scriptLines) @@ -116,10 +172,9 @@ private static string LineNumbering(List scriptLines) return sb.ToString(); } - private static (List, List) ExtractCharacterListAndVoiceIds(string response) + private static List ExtractCharacterList(string response) { var characterList = new List(); - var voiceIds = new List(); var lines = response.Split("\n"); var characterListStartIndex = 0; var characterListEndIndex = 0; @@ -161,8 +216,20 @@ private static (List, List) ExtractCharacterListAndVoiceIds(s } } } - return (characterList, voiceIds); + return characterList; } - private record Character(string Id, string Name, string Description); + private class Character + { + public string Id { get; } + public string Name { get; } + public string Description { get; } + public string VoiceType { get; set; } = string.Empty; + public Character(string id, string name, string description) + { + Id = id; + Name = name; + Description = description; + } + } } diff --git a/KoeBook/App.xaml.cs b/KoeBook/App.xaml.cs index 10b0523..f54e231 100644 --- a/KoeBook/App.xaml.cs +++ b/KoeBook/App.xaml.cs @@ -6,7 +6,6 @@ using KoeBook.Core.Contracts.Services; using KoeBook.Core.Services; using KoeBook.Core.Services.Mocks; -using KoeBook.Epub; using KoeBook.Epub.Contracts.Services; using KoeBook.Epub.Services; using KoeBook.Models; @@ -99,7 +98,8 @@ public App() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); // Epub Services From 7247602dba8114b0aca4e824ec07b6c8bff05fd0 Mon Sep 17 00:00:00 2001 From: herring101 <81513223+herring101@users.noreply.github.com> Date: Sat, 13 Apr 2024 15:38:16 +0900 Subject: [PATCH 04/21] openai to claude --- KoeBook/Startup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/KoeBook/Startup.cs b/KoeBook/Startup.cs index 1a546b5..ecf3feb 100644 --- a/KoeBook/Startup.cs +++ b/KoeBook/Startup.cs @@ -33,8 +33,8 @@ public static IHostBuilder UseCoreStartup(this IHostBuilder builder) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // Epub Services services From 4722f6c6b95d8d7a46ea76e1f5e9eba5a038ea73 Mon Sep 17 00:00:00 2001 From: aiueo-1234 <130837816+aiueo-1234@users.noreply.github.com> Date: Sat, 27 Apr 2024 18:55:14 +0900 Subject: [PATCH 05/21] =?UTF-8?q?#25=20=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/ClaudeAnalyzerService.cs | 73 +++++-------------- 1 file changed, 20 insertions(+), 53 deletions(-) diff --git a/KoeBook.Core/Services/ClaudeAnalyzerService.cs b/KoeBook.Core/Services/ClaudeAnalyzerService.cs index a9d21e6..77a2e67 100644 --- a/KoeBook.Core/Services/ClaudeAnalyzerService.cs +++ b/KoeBook.Core/Services/ClaudeAnalyzerService.cs @@ -26,7 +26,7 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo Messages = [new() { Role = "user", - Content = CreatePrompt1(lineNumberingText) + Content = CreateCharaterGuessPrompt(lineNumberingText) }] }, cancellationToken: cancellationToken @@ -40,7 +40,7 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo Messages = [new() { Role = "user", - Content = CreatePrompt2(characterList) + Content = CreateVoiceTypeAnalyzePrompt(characterList) }] }, cancellationToken: cancellationToken @@ -50,10 +50,7 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo return new(bookProperties, new(characterVoiceMapping)) { ScriptLines = scriptLines }; } - catch (OperationCanceledException) - { - throw; - } + catch (OperationCanceledException) { throw; } catch { throw new EbookException(ExceptionType.ClaudeTalkerAndStyleSettingFailed); @@ -62,40 +59,21 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo private Dictionary ExtractCharacterVoiceMapping(string response, List characterList) { - var characterVoiceMapping = new Dictionary(); - var lines = response.Split("\n"); - var characterListStartIndex = 0; - for (var i = 0; i < lines.Length; i++) - { - if (lines[i].StartsWith("[Assign Voices]")) - { - characterListStartIndex = i + 1; - } - } - - for (var i = characterListStartIndex; i < lines.Length; i++) - { - var line = lines[i]; - if (line.StartsWith('c')) - { - var characterId = line[1..line.IndexOf('.')]; - var voiceType = line[(line.IndexOf(':') + 2)..]; - // voiceTypeが_soundGenerationSelectorService.Modelsに含まれているかチェック - if (_soundGenerationSelectorService.Models.Any(x => x.Name == voiceType)) - { - characterVoiceMapping.Add(characterId, voiceType); - } - else - { - characterVoiceMapping.Add(characterId, string.Empty); - } - } - } - - return characterVoiceMapping; + return response.Split("\n") + .SkipWhile(l => !l.StartsWith("[Assign Voices]")) + .Where(l => l.StartsWith('c')) + .Select(l => + { + var characterId = l[1..l.IndexOf('.')]; + var voiceType = l[(l.IndexOf(':') + 2)..]; + // voiceTypeが_soundGenerationSelectorService.Modelsに含まれているかチェック + return _soundGenerationSelectorService.Models.Any(x => x.Name == voiceType) + ? (characterId, voiceType) + : (characterId, string.Empty); + }).ToDictionary(); } - private static string CreatePrompt1(string lineNumberingText) + private static string CreateCharaterGuessPrompt(string lineNumberingText) { return $$""" {{lineNumberingText}} @@ -132,28 +110,16 @@ [REVISE VOICE ID] """; } - private string CreatePrompt2(List characterList) + private string CreateVoiceTypeAnalyzePrompt(List characterList) { - StringBuilder sb = new StringBuilder(); - foreach (var character in characterList) - { - sb.AppendLine($"c{character.Id}. {character.Name}: {character.Description}"); - } - - StringBuilder sb2 = new StringBuilder(); - foreach (var voiceType in _soundGenerationSelectorService.Models) - { - sb2.Append($"{voiceType.Name},"); - } - return $$""" Assign the most fitting voice type to each character from the provided list, ensuring the chosen voice aligns with their role and attributes in the story. Only select from the available voice types. Characters: - {{sb}} + {{string.Join("\n", characterList.Select(character => $"c{character.Id}. {character.Name}: {character.Description}"))}} Voice Types: - {{sb2}} + {{string.Join(",", _soundGenerationSelectorService.Models.Select(m => m.Name))}} Output Format: [Assign Voices] @@ -188,6 +154,7 @@ private static List ExtractCharacterList(string response) if (lines[i].StartsWith("[REVISE VOICE ID]")) { characterListEndIndex = i; + break; } } for (var i = characterListStartIndex; i < characterListEndIndex; i++) From 47b0c5f24a5e7259a73ec27fbaab1c48a7520047 Mon Sep 17 00:00:00 2001 From: aiueo-1234 <130837816+aiueo-1234@users.noreply.github.com> Date: Sat, 27 Apr 2024 19:00:06 +0900 Subject: [PATCH 06/21] =?UTF-8?q?#25=20claudia=E3=81=A8chatgpt=E3=81=AE?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=96=E3=83=A9=E3=83=AA=E3=83=90=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=83=A7=E3=83=B3=E3=81=AE=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KoeBook.Core/KoeBook.Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/KoeBook.Core/KoeBook.Core.csproj b/KoeBook.Core/KoeBook.Core.csproj index cb6e17d..ea979ab 100644 --- a/KoeBook.Core/KoeBook.Core.csproj +++ b/KoeBook.Core/KoeBook.Core.csproj @@ -9,8 +9,8 @@ - - + + From ee25951a2831b969cbc0eb1eaac2ac2236c94048 Mon Sep 17 00:00:00 2001 From: aiueo-1234 <130837816+aiueo-1234@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:56:06 +0900 Subject: [PATCH 07/21] =?UTF-8?q?#25=20ExtractCharacterList=E3=81=AE?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/ClaudeAnalyzerService.cs | 35 +++++++++---------- KoeBook.Core/Services/MyOpenAiService.cs | 2 ++ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/KoeBook.Core/Services/ClaudeAnalyzerService.cs b/KoeBook.Core/Services/ClaudeAnalyzerService.cs index 77a2e67..b5642ed 100644 --- a/KoeBook.Core/Services/ClaudeAnalyzerService.cs +++ b/KoeBook.Core/Services/ClaudeAnalyzerService.cs @@ -21,21 +21,21 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo { var message1 = await _claudeService.Messages.CreateAsync(new() { - Model = "claude-3-opus-20240229", // you can use Claudia.Models.Claude3Opus string constant + Model = Claudia.Models.Claude3Opus, MaxTokens = 4000, Messages = [new() { Role = "user", - Content = CreateCharaterGuessPrompt(lineNumberingText) + Content = CreateCharacterGuessPrompt(lineNumberingText) }] }, cancellationToken: cancellationToken ); - var characterList = ExtractCharacterList(message1.ToString()); + var characterList = ExtractCharacterList(message1.ToString(), scriptLines); var message2 = await _claudeService.Messages.CreateAsync(new() { - Model = "claude-3-opus-20240229", // you can use Claudia.Models.Claude3Opus string constant + Model = Claudia.Models.Claude3Opus, MaxTokens = 4000, Messages = [new() { @@ -51,9 +51,9 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo return new(bookProperties, new(characterVoiceMapping)) { ScriptLines = scriptLines }; } catch (OperationCanceledException) { throw; } - catch + catch (Exception e) { - throw new EbookException(ExceptionType.ClaudeTalkerAndStyleSettingFailed); + throw new EbookException(ExceptionType.ClaudeTalkerAndStyleSettingFailed, innerException: e); } } @@ -73,7 +73,7 @@ private Dictionary ExtractCharacterVoiceMapping(string response, }).ToDictionary(); } - private static string CreateCharaterGuessPrompt(string lineNumberingText) + private static string CreateCharacterGuessPrompt(string lineNumberingText) { return $$""" {{lineNumberingText}} @@ -138,7 +138,7 @@ private static string LineNumbering(List scriptLines) return sb.ToString(); } - private static List ExtractCharacterList(string response) + private static List ExtractCharacterList(string response, List scriptLines) { var characterList = new List(); var lines = response.Split("\n"); @@ -169,18 +169,17 @@ private static List ExtractCharacterList(string response) } } + var dic = characterList.Select(x => KeyValuePair.Create(x.Id, x.Name)).ToDictionary(); + var lines2 = lines.AsSpan()[(characterListEndIndex + 1)..]; + + for (var i = 0; i < lines2.Length; i++) { - var dest = (stackalloc Range[4]); - for (var i = characterListEndIndex + 1; i < lines.Length; i++) + var line = lines2[i].AsSpan().TrimEnd(); + line = line[(line.IndexOf(' ') + 2)..];//cまで無視 + line = line[..line.IndexOf(' ')]; + if (dic.TryGetValue(line.ToString(), out var characterName)) { - var line = lines[i].AsSpan(); - if (line.Length > 0 && line.Contains('c')) - { - if (line.Split(dest, ' ') > 3) - throw new EbookException(ExceptionType.ClaudeTalkerAndStyleSettingFailed); - var characterId = line[dest[1]]; - var narrationOrDialogue = line[dest[2]]; - } + scriptLines[i].Character = characterName; } } return characterList; diff --git a/KoeBook.Core/Services/MyOpenAiService.cs b/KoeBook.Core/Services/MyOpenAiService.cs index e8c4793..c5f6229 100644 --- a/KoeBook.Core/Services/MyOpenAiService.cs +++ b/KoeBook.Core/Services/MyOpenAiService.cs @@ -36,6 +36,8 @@ public class MyOpenAiService(ISecretSettingsService secretSettingsService, IHttp public IAudioService Audio => GetOpenAiService()?.Audio!; + public IBatchService Batch => GetOpenAiService()?.Batch!; + public void SetDefaultModelId(string modelId) { GetOpenAiService()?.SetDefaultModelId(modelId); From 79893cfa5f5fb890506bae9d06c12636a14831d7 Mon Sep 17 00:00:00 2001 From: aiueo-1234 <130837816+aiueo-1234@users.noreply.github.com> Date: Mon, 29 Apr 2024 01:56:19 +0900 Subject: [PATCH 08/21] =?UTF-8?q?#25=20StyleBertVirs=E3=81=AEUrl=E7=94=9F?= =?UTF-8?q?=E6=88=90=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KoeBook.Core/Services/StyleBertVitsClientService.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/KoeBook.Core/Services/StyleBertVitsClientService.cs b/KoeBook.Core/Services/StyleBertVitsClientService.cs index 2d45c9a..92d6a6c 100644 --- a/KoeBook.Core/Services/StyleBertVitsClientService.cs +++ b/KoeBook.Core/Services/StyleBertVitsClientService.cs @@ -1,4 +1,5 @@ -using System.Net.Http.Json; +using System; +using System.Net.Http.Json; using KoeBook.Core.Contracts.Services; namespace KoeBook.Core.Services; @@ -28,8 +29,10 @@ private async ValueTask GetAsync(string path, ExceptionType excepti var root = _apiRootSelectorService.StyleBertVitsRoot; if (string.IsNullOrEmpty(root)) throw new EbookException(ExceptionType.UnknownStyleBertVitsRoot); + var baseUri = new Uri(root); + var requestUri = new Uri(baseUri, path); var response = await _httpClientFactory.CreateClient() - .GetAsync($"{root}{path}", cancellationToken) + .GetAsync(requestUri, cancellationToken) .ConfigureAwait(false) ?? throw new EbookException(exceptionType); if (!response.IsSuccessStatusCode) From 153e3deb4cbae38f97e4cfe72c13e02a1dcfc4e8 Mon Sep 17 00:00:00 2001 From: aiueo-1234 <130837816+aiueo-1234@users.noreply.github.com> Date: Mon, 29 Apr 2024 01:56:47 +0900 Subject: [PATCH 09/21] =?UTF-8?q?#25=20SoundGenerationSelectorMock?= =?UTF-8?q?=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Mocks/SoundGenerationSelectorServiceMock.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/KoeBook.Core/Services/Mocks/SoundGenerationSelectorServiceMock.cs b/KoeBook.Core/Services/Mocks/SoundGenerationSelectorServiceMock.cs index 1f8c81d..3fe9b9d 100644 --- a/KoeBook.Core/Services/Mocks/SoundGenerationSelectorServiceMock.cs +++ b/KoeBook.Core/Services/Mocks/SoundGenerationSelectorServiceMock.cs @@ -11,12 +11,16 @@ public async ValueTask InitializeAsync(CancellationToken cancellationToken) { await Task.Delay(1000, cancellationToken).ConfigureAwait(false); Models = [ - new SoundModel("0", "青年1", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]), - new SoundModel("1", "青年2", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]), - new SoundModel("2", "女性1", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]), - new SoundModel("3", "女性2", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]), - new SoundModel("4", "ナレーション (男性)", ["narration"]), - new SoundModel("5", "ナレーション (女性)", ["narration"]), + new SoundModel("0", "MaleNarrator", ["narration"]), + new SoundModel("1", "ElementarySchoolBoy", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]), + new SoundModel("2", "MiddleHighSchoolBoy", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]), + new SoundModel("3", "AdultMan", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]), + new SoundModel("4", "ElderlyMan", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]), + new SoundModel("5", "FemaleNarrator", ["narration"]), + new SoundModel("6", "ElementarySchoolGirl", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]), + new SoundModel("7", "MiddleHighSchoolGirl", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]), + new SoundModel("8", "AdultWoman", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]), + new SoundModel("9", "ElderlyWoman", ["neutral", "laughing", "happy", "sad", "cry", "surprised", "angry"]) ]; } } From a47d535833ca396bc481ada6cd5bf675aec03336 Mon Sep 17 00:00:00 2001 From: aiueo-1234 <130837816+aiueo-1234@users.noreply.github.com> Date: Mon, 29 Apr 2024 02:10:25 +0900 Subject: [PATCH 10/21] =?UTF-8?q?#25=20=EF=BC=88=E3=82=AD=E3=83=A3?= =?UTF-8?q?=E3=83=A9=E3=82=AF=E3=82=BF=E3=83=BC=E5=90=8D,=20=E3=83=9C?= =?UTF-8?q?=E3=82=A4=E3=82=B9=E3=83=A2=E3=83=87=E3=83=AB=E5=90=8D=EF=BC=89?= =?UTF-8?q?=E3=81=AE=E3=83=9E=E3=83=83=E3=83=94=E3=83=B3=E3=82=B0=E3=81=AB?= =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/ClaudeAnalyzerService.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/KoeBook.Core/Services/ClaudeAnalyzerService.cs b/KoeBook.Core/Services/ClaudeAnalyzerService.cs index b5642ed..ed3ca12 100644 --- a/KoeBook.Core/Services/ClaudeAnalyzerService.cs +++ b/KoeBook.Core/Services/ClaudeAnalyzerService.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Buffers; +using System.Text; using KoeBook.Core.Contracts.Services; using KoeBook.Core.Models; @@ -9,6 +10,7 @@ public partial class ClaudeAnalyzerService(IClaudeService claudeService, IDispla private readonly IClaudeService _claudeService = claudeService; private readonly IDisplayStateChangeService _displayStateChangeService = displayStateChangeService; private readonly ISoundGenerationSelectorService _soundGenerationSelectorService = soundGenerationSelectorService; + private static readonly SearchValues _searchValues = SearchValues.Create(", "); public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bookProperties, List scriptLines, CancellationToken cancellationToken) { @@ -31,7 +33,7 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo }, cancellationToken: cancellationToken ); - var characterList = ExtractCharacterList(message1.ToString(), scriptLines); + (var characterList, var characterIdNameDic) = ExtractCharacterList(message1.ToString(), scriptLines); var message2 = await _claudeService.Messages.CreateAsync(new() { @@ -46,7 +48,7 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo cancellationToken: cancellationToken ); - var characterVoiceMapping = ExtractCharacterVoiceMapping(message2.ToString(), characterList); + var characterVoiceMapping = ExtractCharacterVoiceMapping(message2.ToString(), characterIdNameDic); return new(bookProperties, new(characterVoiceMapping)) { ScriptLines = scriptLines }; } @@ -57,7 +59,7 @@ public async ValueTask LlmAnalyzeScriptLinesAsync(BookProperties bo } } - private Dictionary ExtractCharacterVoiceMapping(string response, List characterList) + private Dictionary ExtractCharacterVoiceMapping(string response, Dictionary characterIdDic) { return response.Split("\n") .SkipWhile(l => !l.StartsWith("[Assign Voices]")) @@ -65,11 +67,17 @@ private Dictionary ExtractCharacterVoiceMapping(string response, .Select(l => { var characterId = l[1..l.IndexOf('.')]; - var voiceType = l[(l.IndexOf(':') + 2)..]; + var voiceTypeSpan = l[(l.IndexOf(':') + 2)..].AsSpan(); + // ボイス割り当てが複数あたったときに先頭のものを使う(例:群衆 AdultMan, AdultWoman) + if (voiceTypeSpan.IndexOfAny(_searchValues) > 0) + { + voiceTypeSpan = voiceTypeSpan[..voiceTypeSpan.IndexOfAny(_searchValues)]; + } // voiceTypeが_soundGenerationSelectorService.Modelsに含まれているかチェック + var voiceType = voiceTypeSpan.ToString(); return _soundGenerationSelectorService.Models.Any(x => x.Name == voiceType) - ? (characterId, voiceType) - : (characterId, string.Empty); + ? (characterIdDic[characterId], voiceType) + : (characterIdDic[characterId], string.Empty); }).ToDictionary(); } @@ -138,7 +146,7 @@ private static string LineNumbering(List scriptLines) return sb.ToString(); } - private static List ExtractCharacterList(string response, List scriptLines) + private static (List, Dictionary) ExtractCharacterList(string response, List scriptLines) { var characterList = new List(); var lines = response.Split("\n"); @@ -174,15 +182,15 @@ private static List ExtractCharacterList(string response, List