diff --git a/MisakaTranslator-WPF/IAppSettings.cs b/MisakaTranslator-WPF/IAppSettings.cs index c0a58748..8ccf870a 100644 --- a/MisakaTranslator-WPF/IAppSettings.cs +++ b/MisakaTranslator-WPF/IAppSettings.cs @@ -128,6 +128,20 @@ string ChatGPTapiUrl set; } + [Option(Alias = "AzureOpenAITranslator.apiKey", DefaultValue = "")] + string AzureOpenAIApiKey + { + get; + set; + } + + [Option(Alias = "AzureOpenAITranslator.apiUrl", DefaultValue = "https://XXX.openai.azure.com/openai/deployments/YYY/chat/completions")] + string AzureOpenAIApiUrl + { + get; + set; + } + [Option(Alias = "XiaoniuTranslator.xiaoniuApiKey", DefaultValue = "")] string xiaoniuApiKey { diff --git a/MisakaTranslator-WPF/MisakaTranslator-WPF.csproj b/MisakaTranslator-WPF/MisakaTranslator-WPF.csproj index 95a181be..44a60143 100644 --- a/MisakaTranslator-WPF/MisakaTranslator-WPF.csproj +++ b/MisakaTranslator-WPF/MisakaTranslator-WPF.csproj @@ -60,6 +60,14 @@ + + + MSBuild:Compile + Wpf + Designer + + + diff --git a/MisakaTranslator-WPF/SettingsPages/TranslatorPages/AzureOpenAITransSettingsPage.xaml b/MisakaTranslator-WPF/SettingsPages/TranslatorPages/AzureOpenAITransSettingsPage.xaml new file mode 100644 index 00000000..e810f462 --- /dev/null +++ b/MisakaTranslator-WPF/SettingsPages/TranslatorPages/AzureOpenAITransSettingsPage.xaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MisakaTranslator-WPF/SettingsPages/TranslatorPages/AzureOpenAITransSettingsPage.xaml.cs b/MisakaTranslator-WPF/SettingsPages/TranslatorPages/AzureOpenAITransSettingsPage.xaml.cs new file mode 100644 index 00000000..766753eb --- /dev/null +++ b/MisakaTranslator-WPF/SettingsPages/TranslatorPages/AzureOpenAITransSettingsPage.xaml.cs @@ -0,0 +1,69 @@ +using System.Windows; +using System.Windows.Controls; +using TranslatorLibrary; + +namespace MisakaTranslator_WPF.SettingsPages.TranslatorPages +{ + /// + /// AzureOpenAITransSettingsPage.xaml 的交互逻辑 + /// + public partial class AzureOpenAITransSettingsPage : Page + { + public AzureOpenAITransSettingsPage() + { + InitializeComponent(); + AzureOpenAITransSecretKeyBox.Text = Common.appSettings.AzureOpenAIApiKey; + AzureOpenAITransUrlBox.Text = Common.appSettings.AzureOpenAIApiUrl; + } + + private async void AuthTestBtn_Click(object sender, RoutedEventArgs e) + { + Common.appSettings.AzureOpenAIApiKey = AzureOpenAITransSecretKeyBox.Text; + Common.appSettings.AzureOpenAIApiUrl = AzureOpenAITransUrlBox.Text; + + ITranslator trans = new AzureOpenAITranslator(); + trans.TranslatorInit(AzureOpenAITransSecretKeyBox.Text, AzureOpenAITransUrlBox.Text); + + if (await trans.TranslateAsync("apple", "zh", "en") != null) + { + HandyControl.Controls.Growl.Success($"Azure OpenAI {Application.Current.Resources["APITest_Success_Hint"]}"); + } + else + { + HandyControl.Controls.Growl.Error($"Azure OpenAI {Application.Current.Resources["APITest_Error_Hint"]}\n{trans.GetLastError()}"); + } + } + + private void ApplyBtn_Click(object sender, RoutedEventArgs e) + { + System.Diagnostics.Process.Start("https://azure.microsoft.com/en-us/solutions/ai/"); + } + + private void DocBtn_Click(object sender, RoutedEventArgs e) + { + System.Diagnostics.Process.Start("https://learn.microsoft.com/en-us/azure/ai-services/cognitive-services-support-options"); + } + + private void BillBtn_Click(object sender, RoutedEventArgs e) + { + // System.Diagnostics.Process.Start(ChatGPTTranslator.BILL_URL); + } + + private async void TransTestBtn_Click(object sender, RoutedEventArgs e) + { + ITranslator trans = new AzureOpenAITranslator(); + trans.TranslatorInit(AzureOpenAITransSecretKeyBox.Text, AzureOpenAITransUrlBox.Text); + string res = await trans.TranslateAsync(TestSrcText.Text, TestDstLang.Text, TestSrcLang.Text); + + if (res != null) + { + HandyControl.Controls.MessageBox.Show(res, Application.Current.Resources["MessageBox_Result"].ToString()); + } + else + { + HandyControl.Controls.Growl.Error( + $"Azure OpenAI {Application.Current.Resources["APITest_Error_Hint"]}\n{trans.GetLastError()}"); + } + } + } +} \ No newline at end of file diff --git a/MisakaTranslator-WPF/SettingsWindow.xaml b/MisakaTranslator-WPF/SettingsWindow.xaml index 48543194..7e96b4b9 100644 --- a/MisakaTranslator-WPF/SettingsWindow.xaml +++ b/MisakaTranslator-WPF/SettingsWindow.xaml @@ -42,6 +42,7 @@ + diff --git a/MisakaTranslator-WPF/SettingsWindow.xaml.cs b/MisakaTranslator-WPF/SettingsWindow.xaml.cs index b3a5b52b..d4bd424e 100644 --- a/MisakaTranslator-WPF/SettingsWindow.xaml.cs +++ b/MisakaTranslator-WPF/SettingsWindow.xaml.cs @@ -56,6 +56,11 @@ private void Item_ChatGPTTrans_Selected(object sender, RoutedEventArgs e) this.SettingFrame.Navigate(new Uri("SettingsPages/TranslatorPages/ChatGPTTransSettingsPage.xaml", UriKind.Relative)); } + private void Item_AzureOpenAITrans_Selected(object sender, RoutedEventArgs e) + { + this.SettingFrame.Navigate(new Uri("SettingsPages/TranslatorPages/AzureOpenAITransSettingsPage.xaml", UriKind.Relative)); + } + private void Item_FYJTrans_Selected(object sender, RoutedEventArgs e) { this.SettingFrame.Navigate(new Uri("SettingsPages/TranslatorPages/TencentFYJTransSettingsPage.xaml", UriKind.Relative)); diff --git a/MisakaTranslator-WPF/TranslateWindow.xaml.cs b/MisakaTranslator-WPF/TranslateWindow.xaml.cs index 4495bc56..8fe59e5e 100644 --- a/MisakaTranslator-WPF/TranslateWindow.xaml.cs +++ b/MisakaTranslator-WPF/TranslateWindow.xaml.cs @@ -266,6 +266,10 @@ public static ITranslator TranslatorAuto(string translator) ChatGPTTranslator chatgpt = new ChatGPTTranslator(); chatgpt.TranslatorInit(Common.appSettings.ChatGPTapiKey, Common.appSettings.ChatGPTapiUrl); return chatgpt; + case nameof(AzureOpenAITranslator): + AzureOpenAITranslator azureOpenAI = new AzureOpenAITranslator(); + azureOpenAI.TranslatorInit(Common.appSettings.AzureOpenAIApiKey, Common.appSettings.AzureOpenAIApiUrl); + return azureOpenAI; case "ArtificialTranslator": ArtificialTranslator at = new ArtificialTranslator(); at.TranslatorInit(Common.appSettings.ArtificialPatchPath); diff --git a/MisakaTranslator-WPF/lang/en-US.xaml b/MisakaTranslator-WPF/lang/en-US.xaml index 2145f885..cb2ea47c 100644 --- a/MisakaTranslator-WPF/lang/en-US.xaml +++ b/MisakaTranslator-WPF/lang/en-US.xaml @@ -66,6 +66,7 @@ 小学馆日中 Shogakukan ja-zh dictionary DeepL ChatGPT + Azure OpenAI IBM Yandex @@ -97,6 +98,10 @@ ChatGPT authentication key ChatGPT API URL + Azure OpenAI (network required) provides AI Translat, basically consistent with ChatGPT + Deployment Key + Deployment target URL + Tencent TMT API (腾讯云翻译, network required) supports multilingual translation with better support for Japanese and Chinese. You need to apply for the API (on the website in Chinese), get access to the SecretId and SecretKey, and then fill in below before testing. If the API is tested as invalid, you can refer to official Tencent documentations with the error codes. Previous version of API is likely to incur charges, "Check API quota" will lead you to the dashboard of Tencent TMT. SecretId diff --git a/MisakaTranslator-WPF/lang/zh-CN.xaml b/MisakaTranslator-WPF/lang/zh-CN.xaml index 296f271f..29ded079 100644 --- a/MisakaTranslator-WPF/lang/zh-CN.xaml +++ b/MisakaTranslator-WPF/lang/zh-CN.xaml @@ -64,6 +64,7 @@ 小学馆日中 DeepL ChatGPT + Azure OpenAI IBM Yandex @@ -95,6 +96,10 @@ ChatGPT API 密钥 ChatGPT API URL + Azure OpenAI人工智能翻译,基本和ChatGPT一致 + Deployment 密钥 + Deployment 目标URL + 腾讯翻译(私人)API支持多种语言互译,对日语、汉语的Galgame互译支持较好,需要联网使用;您需要先申请该API并创建一个密钥,获取SecretId和SecretKey后填入下方相应位置并进行测试认证;如果认证出现错误,请根据错误码至腾讯翻译API官方文档获取解决办法;腾讯旧版翻译API部分版本可能会产生某些费用,您可以使用额度查询按钮进入控制台确认。 腾讯云API SecretId 腾讯云API SecretKey diff --git a/TranslatorLibrary/AzureOpenAITranslator.cs b/TranslatorLibrary/AzureOpenAITranslator.cs new file mode 100644 index 00000000..ebc98bd2 --- /dev/null +++ b/TranslatorLibrary/AzureOpenAITranslator.cs @@ -0,0 +1,193 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +// ReSharper disable ClassNeverInstantiated.Global + +namespace TranslatorLibrary +{ + // ReSharper disable once InconsistentNaming + public class AzureOpenAITranslator : ITranslator + { + private const string ROLE_SYSTEM = "system"; + private const string ROLE_USER = "user"; + private const string ROLE_ASSISTANT = "assistant"; + + private static readonly string systemPromptTemplate = "You are a professional translator, translating a novel from {0} to {1}. Output the translated text directly."; + + private string? key = string.Empty; + private string? url = string.Empty; + private string errorInfo = string.Empty; + + public void TranslatorInit(string param1, string param2) + { + key = param1; + url = param2; + } + + public async Task TranslateAsync(string sourceText, string desLang, string srcLang) + { + if (string.IsNullOrEmpty(sourceText) || string.IsNullOrEmpty(desLang) || string.IsNullOrEmpty(srcLang)) + { + errorInfo = "Param Missing"; + return null; + } + + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + errorInfo = "Invalid Api Url"; + return null; + } + + var httpClient = CommonFunction.GetHttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, uri); + var completionRequest = CreateCompletionRequest(sourceText, desLang, srcLang); + + request.Headers.Add("api-key", key); + request.Content = new StringContent(JsonSerializer.Serialize(completionRequest), Encoding.UTF8, "application/json"); + HttpResponseMessage response; + try + { + response = await httpClient.SendAsync(request); + var responseStream = await response.Content.ReadAsStreamAsync(); + if (response.StatusCode != HttpStatusCode.OK) + { + var error = await JsonSerializer.DeserializeAsync(responseStream); + errorInfo = error == null ? $"http status code is {(int)response.StatusCode}({response.StatusCode})" : error.Error.Message; + return null; + } + + var completion = await JsonSerializer.DeserializeAsync(responseStream); + if (completion is not { Choices.Count: > 0 }) + { + errorInfo = "No completion choices"; + return null; + } + + return completion.Choices[0].Message.Content; + } + catch (Exception ex) when (ex is TaskCanceledException or HttpRequestException or JsonException) + { + errorInfo = ex.Message; + return null; + } + catch (Exception ex) + { + errorInfo = ex.Message; + throw; + } + } + + public string GetLastError() + { + return errorInfo; + } + + private static CompletionRequest CreateCompletionRequest(string sourceText, string desLang, string srcLang) + { + var messages = new List + { + new() + { + Role = ROLE_SYSTEM, + Content = new List() + { + new() + { + Type = "text", + Text = string.Format(systemPromptTemplate, srcLang, desLang) + } + } + }, + new() + { + Role = ROLE_USER, + Content = new List + { + new() + { + Type = "text", + Text = sourceText + } + } + } + }; + return new CompletionRequest + { + Messages = messages, + Stream = false + }; + } + + #region DataModel + + public class CompletionRequest + { + [JsonPropertyName("messages")] public List Messages { get; set; } = new(); + [JsonPropertyName("stream")] public bool Stream { get; set; } = false; + // [JsonPropertyName("temperature")] public double Temperature { get; set; } = 0.7; + // [JsonPropertyName("top_p")] public double TopP { get; set; } = 0.95; + // [JsonPropertyName("max_tokens")] public int MaxTokens { get; set; } = 800; + } + + public class CompletionResponse + { + [JsonPropertyName("choices")] public List Choices { get; set; } = new(); + // [JsonPropertyName("created")] public ulong Created { get; set; } + // [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + // [JsonPropertyName("model")] public string Model { get; set; } = string.Empty; + // [JsonPropertyName("object")] public string Object { get; set; } = string.Empty; + // [JsonPropertyName("system_fingerprint")] public string SystemFingerprint { get; set; } = string.Empty; + // [JsonPropertyName("usage")] public CompletionUsage Usage { get; set; } = new(); + } + + public class CompletionErrorResponse + { + [JsonPropertyName("error")] public CompletionError Error { get; set; } = new(); + } + + public class CompletionError + { + [JsonPropertyName("code")] public string Code { get; set; } = string.Empty; + [JsonPropertyName("message")] public string Message { get; set; } = string.Empty; + } + + public class ChatChoice + { + [JsonPropertyName("message")] public MessageResponse Message { get; set; } = new(); + [JsonPropertyName("finish_reason")] public string FinishReason { get; set; } = "stop"; + [JsonPropertyName("index")] public int Index { get; set; } + } + + public class MessageRequest + { + [JsonPropertyName("role")] public string Role { get; set; } = ROLE_SYSTEM; + [JsonPropertyName("content")] public List Content { get; set; } = new(); + } + + public class MessageResponse + { + [JsonPropertyName("role")] public string Role { get; set; } = ROLE_ASSISTANT; + [JsonPropertyName("content")] public string Content { get; set; } = string.Empty; + } + + public class MessageContent + { + [JsonPropertyName("type")] public string Type { get; set; } = "text"; + [JsonPropertyName("text")] public string Text { get; set; } = string.Empty; + } + + public class CompletionUsage + { + [JsonPropertyName("completion_tokens")] public int CompletionTokens { get; set; } + [JsonPropertyName("prompt_tokens")] public int PromptTokens { get; set; } + [JsonPropertyName("total_tokens")] public int TotalTokens { get; set; } + } + #endregion + } +} \ No newline at end of file diff --git a/TranslatorLibrary/CommonFunction.cs b/TranslatorLibrary/CommonFunction.cs index 74e7cb77..6ebf8cc0 100644 --- a/TranslatorLibrary/CommonFunction.cs +++ b/TranslatorLibrary/CommonFunction.cs @@ -38,6 +38,7 @@ public static class CommonFunction { "译典通", "Dreye"}, { "DeepL", "DeepLTranslator"}, {"ChatGPT","ChatGPTTranslator" }, + { "Azure OpenAI", nameof(AzureOpenAITranslator) }, { "本地人工翻译(见说明)" , "ArtificialTranslator"} };