From 9ac9420978a1ed09b127f46b5c95dd63a4736197 Mon Sep 17 00:00:00 2001 From: zggsong Date: Thu, 18 Jan 2024 12:25:23 +0800 Subject: [PATCH] feat: Adapt gemini translation --- STranslate.Model/Enums.cs | 4 +- STranslate.Style/Resources/gemini.png | Bin 0 -> 2760 bytes STranslate.Style/STranslate.Style.csproj | 3 + STranslate.Style/Styles/IconStyle.xaml | 1 + STranslate/Helper/ConfigHelper.cs | 1 + STranslate/ViewModels/InputViewModel.cs | 4 + .../History/HistoryContentViewModel.cs | 1 + .../ViewModels/Preference/ServiceViewModel.cs | 3 + .../Preference/Services/TranslatorBaidu.cs | 11 +- .../Preference/Services/TranslatorBing.cs | 9 +- .../Preference/Services/TranslatorGemini.cs | 198 ++++++++++++++++++ .../Preference/Services/TranslatorOpenAI.cs | 5 +- STranslate/ViewModels/ServiceHandler.cs | 117 +++++++++-- .../Service/TextGeminiServicesPage.xaml | 101 +++++++++ .../Service/TextGeminiServicesPage.xaml.cs | 35 ++++ 15 files changed, 468 insertions(+), 25 deletions(-) create mode 100644 STranslate.Style/Resources/gemini.png create mode 100644 STranslate/ViewModels/Preference/Services/TranslatorGemini.cs create mode 100644 STranslate/Views/Preference/Service/TextGeminiServicesPage.xaml create mode 100644 STranslate/Views/Preference/Service/TextGeminiServicesPage.xaml.cs diff --git a/STranslate.Model/Enums.cs b/STranslate.Model/Enums.cs index b395114c..91559b1d 100644 --- a/STranslate.Model/Enums.cs +++ b/STranslate.Model/Enums.cs @@ -102,7 +102,8 @@ public enum ServiceType ApiService = 0, BaiduService, BingService, - OpenAIService + OpenAIService, + GeminiService, } /// @@ -118,6 +119,7 @@ public enum IconType Youdao, Bing, OpenAI, + Gemini, } /// diff --git a/STranslate.Style/Resources/gemini.png b/STranslate.Style/Resources/gemini.png new file mode 100644 index 0000000000000000000000000000000000000000..0855e34bb37eb40a380e7a14c25f5a1bf55b4031 GIT binary patch literal 2760 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 O0000 Never + + Never + Never diff --git a/STranslate.Style/Styles/IconStyle.xaml b/STranslate.Style/Styles/IconStyle.xaml index 2b7cdfbe..a1422d54 100644 --- a/STranslate.Style/Styles/IconStyle.xaml +++ b/STranslate.Style/Styles/IconStyle.xaml @@ -9,4 +9,5 @@ + \ No newline at end of file diff --git a/STranslate/Helper/ConfigHelper.cs b/STranslate/Helper/ConfigHelper.cs index ac914c23..000ca321 100644 --- a/STranslate/Helper/ConfigHelper.cs +++ b/STranslate/Helper/ConfigHelper.cs @@ -309,6 +309,7 @@ JsonSerializer serializer (int)ServiceType.BaiduService => new TranslatorBaidu(), (int)ServiceType.BingService => new TranslatorBing(), (int)ServiceType.OpenAIService => new TranslatorOpenAI(), + (int)ServiceType.GeminiService => new TranslatorGemini(), //TODO: 新接口需要适配 _ => throw new NotSupportedException($"Unsupported ServiceType: {type}") }; diff --git a/STranslate/ViewModels/InputViewModel.cs b/STranslate/ViewModels/InputViewModel.cs index 499c4c50..4642a2c5 100644 --- a/STranslate/ViewModels/InputViewModel.cs +++ b/STranslate/ViewModels/InputViewModel.cs @@ -193,6 +193,9 @@ await Parallel.ForEachAsync( case ServiceType.OpenAIService: await ServiceHandler.OpenAIHandlerAsync(service, InputContent, sourceStr, targetStr, token); break; + case ServiceType.GeminiService: + await ServiceHandler.GeminiHandlerAsync(service, InputContent, sourceStr, targetStr, token); + break; default: break; } @@ -400,6 +403,7 @@ public class CurrentTranslatorConverter : JsonConverter (int)ServiceType.BaiduService => new TranslatorBaidu(), (int)ServiceType.BingService => new TranslatorBing(), (int)ServiceType.OpenAIService => new TranslatorOpenAI(), + (int)ServiceType.GeminiService => new TranslatorGemini(), _ => new TranslatorApi(), }; diff --git a/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs b/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs index 243f5717..76e2e729 100644 --- a/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs +++ b/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs @@ -92,6 +92,7 @@ JsonSerializer serializer (int)ServiceType.BaiduService => new TranslatorBaidu(), (int)ServiceType.BingService => new TranslatorBing(), (int)ServiceType.OpenAIService => new TranslatorOpenAI(), + (int)ServiceType.GeminiService => new TranslatorGemini(), //TODO: 新接口需要适配 _ => throw new NotSupportedException($"Unsupported ServiceType: {type}") }; diff --git a/STranslate/ViewModels/Preference/ServiceViewModel.cs b/STranslate/ViewModels/Preference/ServiceViewModel.cs index dd4f42a3..814d9256 100644 --- a/STranslate/ViewModels/Preference/ServiceViewModel.cs +++ b/STranslate/ViewModels/Preference/ServiceViewModel.cs @@ -26,6 +26,7 @@ public ServiceViewModel() TransServices.Add(new TranslatorBaidu()); TransServices.Add(new TranslatorBing()); TransServices.Add(new TranslatorOpenAI()); + TransServices.Add(new TranslatorGemini()); ResetView(); } @@ -87,6 +88,7 @@ private void TogglePage(ITranslator service) ServiceType.BaiduService => string.Format("{0}TextBaiduServicesPage", head), ServiceType.BingService => string.Format("{0}TextBingServicesPage", head), ServiceType.OpenAIService => string.Format("{0}TextOpenAIServicesPage", head), + ServiceType.GeminiService => string.Format("{0}TextGeminiServicesPage", head), _ => string.Format("{0}TextApiServicePage", head) }; @@ -111,6 +113,7 @@ private void Add(List list) TranslatorBaidu baidu => baidu.DeepClone(), TranslatorBing bing => bing.DeepClone(), TranslatorOpenAI openAI => openAI.DeepClone(), + TranslatorGemini gemini => gemini.DeepClone(), _ => throw new InvalidOperationException($"Unsupported service type: {service.GetType().Name}") }); diff --git a/STranslate/ViewModels/Preference/Services/TranslatorBaidu.cs b/STranslate/ViewModels/Preference/Services/TranslatorBaidu.cs index d43c4b61..4bce3f28 100644 --- a/STranslate/ViewModels/Preference/Services/TranslatorBaidu.cs +++ b/STranslate/ViewModels/Preference/Services/TranslatorBaidu.cs @@ -126,10 +126,11 @@ public bool KeyHide } } } - - [RelayCommand] - private void ShowEncryptInfo(string obj) + + private void ShowEncryptInfo(string? obj) { + if (obj == null) return; + if (obj.Equals(nameof(AppID))) { IdHide = !IdHide; @@ -140,6 +141,10 @@ private void ShowEncryptInfo(string obj) } } + private RelayCommand? showEncryptInfoCommand; + [JsonIgnore] + public IRelayCommand ShowEncryptInfoCommand => showEncryptInfoCommand ??= new RelayCommand(new Action(ShowEncryptInfo)); + #endregion Show/Hide Encrypt Info public async Task TranslateAsync(object request, CancellationToken token) diff --git a/STranslate/ViewModels/Preference/Services/TranslatorBing.cs b/STranslate/ViewModels/Preference/Services/TranslatorBing.cs index ad5e7930..0a41c328 100644 --- a/STranslate/ViewModels/Preference/Services/TranslatorBing.cs +++ b/STranslate/ViewModels/Preference/Services/TranslatorBing.cs @@ -131,9 +131,10 @@ public bool KeyHide } } - [RelayCommand] - private void ShowEncryptInfo(string obj) + private void ShowEncryptInfo(string? obj) { + if (obj == null) return; + if (obj.Equals(nameof(AppID))) { IdHide = !IdHide; @@ -144,6 +145,10 @@ private void ShowEncryptInfo(string obj) } } + private RelayCommand? showEncryptInfoCommand; + [JsonIgnore] + public IRelayCommand ShowEncryptInfoCommand => showEncryptInfoCommand ??= new RelayCommand(new Action(ShowEncryptInfo)); + #endregion Show/Hide Encrypt Info public async Task TranslateAsync(object request, CancellationToken token) diff --git a/STranslate/ViewModels/Preference/Services/TranslatorGemini.cs b/STranslate/ViewModels/Preference/Services/TranslatorGemini.cs new file mode 100644 index 00000000..114fc5ce --- /dev/null +++ b/STranslate/ViewModels/Preference/Services/TranslatorGemini.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Security.Policy; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using STranslate.Model; +using STranslate.Util; + +namespace STranslate.ViewModels.Preference.Services +{ + public partial class TranslatorGemini : ObservableObject, ITranslator + { + public TranslatorGemini() + : this(Guid.NewGuid(), "https://generativelanguage.googleapis.com", "Gemini") { } + + public TranslatorGemini( + Guid guid, + string url, + string name = "", + IconType icon = IconType.Gemini, + string appID = "", + string appKey = "", + bool isEnabled = true, + ServiceType type = ServiceType.GeminiService + ) + { + Identify = guid; + Url = url; + Name = name; + Icon = icon; + AppID = appID; + AppKey = appKey; + IsEnabled = isEnabled; + Type = type; + } + + [ObservableProperty] + private Guid _identify = Guid.Empty; + + [JsonIgnore] + [ObservableProperty] + private ServiceType _type = 0; + + [JsonIgnore] + [ObservableProperty] + public bool _isEnabled = true; + + [JsonIgnore] + [ObservableProperty] + private string _name = string.Empty; + + [JsonIgnore] + [ObservableProperty] + private IconType _icon = IconType.Gemini; + + [JsonIgnore] + [ObservableProperty] + public string _url = string.Empty; + + [JsonIgnore] + [ObservableProperty] + public string _AppID = string.Empty; + + [JsonIgnore] + [ObservableProperty] + public string _appKey = string.Empty; + + [JsonIgnore] + public object _data = string.Empty; + + [JsonIgnore] + public object Data + { + get => _data; + set + { + if (_data != value) + { + OnPropertyChanging(nameof(Data)); + _data = value; + OnPropertyChanged(nameof(Data)); + } + } + } + + [JsonIgnore] + public List Icons { get; private set; } = Enum.GetValues(typeof(IconType)).OfType().ToList(); + + #region Show/Hide Encrypt Info + + [JsonIgnore] + private bool _keyHide = true; + + [JsonIgnore] + public bool KeyHide + { + get => _keyHide; + set + { + if (_keyHide != value) + { + OnPropertyChanging(nameof(KeyHide)); + _keyHide = value; + OnPropertyChanged(nameof(KeyHide)); + } + } + } + + + private void ShowEncryptInfo() => KeyHide = !KeyHide; + + private RelayCommand? showEncryptInfoCommand; + [JsonIgnore] + public IRelayCommand ShowEncryptInfoCommand => showEncryptInfoCommand ??= new RelayCommand(new Action(ShowEncryptInfo)); + + #endregion Show/Hide Encrypt Info + + [Obsolete] + public async Task TranslateAsync(object request, CancellationToken token) + { + try + { + if (string.IsNullOrEmpty(Url) || string.IsNullOrEmpty(AppKey)) + throw new Exception("请先完善配置"); + + if (!Url.EndsWith("completions")) + { + Url = Url.TrimEnd('/') + "/completions"; + } + + if (request != null) + { + var jsonData = JsonConvert.SerializeObject(request); + + // 构建请求 + var client = new HttpClient(new SocketsHttpHandler()); + var req = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri(Url), + Content = new StringContent(jsonData, Encoding.UTF8, "application/json") + }; + req.Headers.Add("Authorization", $"Bearer {AppKey}"); + + // 发送请求 + using var response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, token); + // 获取响应流 + using var responseStream = await response.Content.ReadAsStreamAsync(token); + using var reader = new System.IO.StreamReader(responseStream); + // 逐行读取并输出结果 + while (!reader.EndOfStream || token.IsCancellationRequested) + { + var line = await reader.ReadLineAsync(token); + + if (string.IsNullOrEmpty(line?.Trim())) + continue; + + var preprocessString = line.Replace("data:", "").Trim(); + + // 结束标记 + if (preprocessString.Equals("[DONE]")) + break; + + // 解析JSON数据 + var parsedData = JsonConvert.DeserializeObject(preprocessString); + + if (parsedData is null) + continue; + + // 提取content的值 + var contentValue = parsedData["choices"]?.FirstOrDefault()?["delta"]?["content"]?.ToString(); + + if (string.IsNullOrEmpty(contentValue)) + continue; + + // 输出 + Data += contentValue; + //Debug.Write(contentValue); + } + } + } + catch (Exception ex) + { + Data = ex.Message; + } + + return Task.FromResult(null); + } + } +} diff --git a/STranslate/ViewModels/Preference/Services/TranslatorOpenAI.cs b/STranslate/ViewModels/Preference/Services/TranslatorOpenAI.cs index 0103ffde..47b16f7f 100644 --- a/STranslate/ViewModels/Preference/Services/TranslatorOpenAI.cs +++ b/STranslate/ViewModels/Preference/Services/TranslatorOpenAI.cs @@ -114,9 +114,12 @@ public bool KeyHide } } - [RelayCommand] private void ShowEncryptInfo() => KeyHide = !KeyHide; + private RelayCommand? showEncryptInfoCommand; + [JsonIgnore] + public IRelayCommand ShowEncryptInfoCommand => showEncryptInfoCommand ??= new RelayCommand(new Action(ShowEncryptInfo)); + #endregion Show/Hide Encrypt Info [Obsolete] diff --git a/STranslate/ViewModels/ServiceHandler.cs b/STranslate/ViewModels/ServiceHandler.cs index d801055f..5a11b972 100644 --- a/STranslate/ViewModels/ServiceHandler.cs +++ b/STranslate/ViewModels/ServiceHandler.cs @@ -1,13 +1,14 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using STranslate.Model; -using STranslate.Util; -using System; +using System; using System.Linq; using System.Net.Http; +using System.Security.Policy; using System.Text; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using STranslate.Model; +using STranslate.Util; namespace STranslate.ViewModels { @@ -113,18 +114,19 @@ public static async Task OpenAIHandlerAsync(ITranslator service, string content, if (string.IsNullOrEmpty(service.Url) || string.IsNullOrEmpty(service.AppKey)) throw new Exception("请先完善配置"); - if (!service.Url.EndsWith("/v1/completions")) + UriBuilder uriBuilder = new(service.Url); + + if (!uriBuilder.Path.EndsWith("/v1/completions")) { - service.Url = service.Url.TrimEnd('/') + "/v1/completions"; + uriBuilder.Path = uriBuilder.Path.TrimEnd('/') + "/v1/completions"; } + // 构建请求数据 var reqData = new { model = "gpt-3.5-turbo", - messages = new[] { - new { role = "user", content = $"Translate the following text to {target}: {content}" } - }, - temperature = 0, + messages = new[] { new { role = "user", content = $"Translate the following text to {target}: {content}" } }, + temperature = 1.0, stream = true }; @@ -136,7 +138,7 @@ public static async Task OpenAIHandlerAsync(ITranslator service, string content, var req = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri(service.Url), + RequestUri = uriBuilder.Uri, Content = new StringContent(jsonData, Encoding.UTF8, "application/json") }; req.Headers.Add("Authorization", $"Bearer {service.AppKey}"); @@ -146,31 +148,110 @@ public static async Task OpenAIHandlerAsync(ITranslator service, string content, // 获取响应流 using var responseStream = await response.Content.ReadAsStreamAsync(token); using var reader = new System.IO.StreamReader(responseStream); + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); // 逐行读取并输出结果 while (!reader.EndOfStream || token.IsCancellationRequested) { var line = await reader.ReadLineAsync(token); - if (string.IsNullOrEmpty(line?.Trim())) continue; + if (string.IsNullOrEmpty(line?.Trim())) + continue; var preprocessString = line.Replace("data:", "").Trim(); // 结束标记 - if (preprocessString.Equals("[DONE]")) break; + if (preprocessString.Equals("[DONE]")) + break; // 解析JSON数据 var parsedData = JsonConvert.DeserializeObject(preprocessString); - if (parsedData is null) continue; + if (parsedData is null) + continue; // 提取content的值 var contentValue = parsedData["choices"]?.FirstOrDefault()?["delta"]?["content"]?.ToString(); - if (string.IsNullOrEmpty(contentValue)) continue; + if (string.IsNullOrEmpty(contentValue)) + continue; // 输出 - service.Data += contentValue; + lock (service) + { + service.Data += contentValue; + } + } + + if (string.IsNullOrEmpty(service.Data?.ToString())) + service.Data = "未获取到内容"; + } + catch (Exception ex) + { + service.Data = ex.Message; + } + } + + /// + /// Gemini + /// + /// + /// + /// + /// + /// + /// + public static async Task GeminiHandlerAsync(ITranslator service, string content, string source, string target, CancellationToken token) + { + try + { + if (string.IsNullOrEmpty(service.Url) || string.IsNullOrEmpty(service.AppKey)) + throw new Exception("请先完善配置"); + + UriBuilder uriBuilder = new(service.Url); + + if (!uriBuilder.Path.EndsWith("/v1beta/models/gemini-pro:streamGenerateContent")) + { + uriBuilder.Path = uriBuilder.Path.TrimEnd('/') + "/v1beta/models/gemini-pro:streamGenerateContent"; + } + + uriBuilder.Query = $"key={service.AppKey}"; + + // 构建请求数据 + var reqData = new { contents = new[] { new { parts = new[] { new { text = $"Translate the following text to {target}: {content}" } } } } }; + + // 为了流式输出与MVVM还是放这里吧 + var jsonData = JsonConvert.SerializeObject(reqData); + + // 构建请求 + var client = new HttpClient(new SocketsHttpHandler()); + var req = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = uriBuilder.Uri, + Content = new StringContent(jsonData, Encoding.UTF8, "application/json") + }; + + // 发送请求 + using var response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, token); + // 获取响应流 + using var responseStream = await response.Content.ReadAsStreamAsync(token); + using var reader = new System.IO.StreamReader(responseStream); + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + // 逐行读取并输出结果 + while (!reader.EndOfStream) + { + string line = await reader.ReadLineAsync(token) ?? ""; + line = line.Trim(); + if (line.StartsWith("\"text\":")) + { + service.Data += line.Replace("\"text\": ", "").Replace("\"", ""); + } } + + if (string.IsNullOrEmpty(service.Data?.ToString())) + service.Data = "未获取到内容"; } catch (Exception ex) { @@ -178,4 +259,4 @@ public static async Task OpenAIHandlerAsync(ITranslator service, string content, } } } -} \ No newline at end of file +} diff --git a/STranslate/Views/Preference/Service/TextGeminiServicesPage.xaml b/STranslate/Views/Preference/Service/TextGeminiServicesPage.xaml new file mode 100644 index 00000000..9aecc3cc --- /dev/null +++ b/STranslate/Views/Preference/Service/TextGeminiServicesPage.xaml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +