diff --git a/STranslateDLL/LocalMode.cs b/STranslateDLL/LocalMode.cs index 81259c3..58ee34d 100644 --- a/STranslateDLL/LocalMode.cs +++ b/STranslateDLL/LocalMode.cs @@ -1,7 +1,9 @@ using System.IO.Compression; +using System.Net; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace STranslateDLL; @@ -10,7 +12,20 @@ public static class LocalMode { private static long _nextId; - private static bool _hasInit = false; + private static bool _hasInit; + + private static readonly Dictionary TextTypeDic = new() + { + { true, "richtext" }, + { false, "plaintext" } + }; + + private static JsonSerializerOptions GetOptions => + new() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault + }; private static void Initial() { @@ -23,39 +38,122 @@ public static class LocalMode public static async Task ExecuteAsync( string content, - string? sourceLang = null, + string sourceLang = "auto", string targetLang = "ZH", CancellationToken? token = null ) { - if (!_hasInit) + if (!_hasInit) Initial(); + var getToken = token ?? CancellationToken.None; + var splitResult = await SplitTextAsync(content, getToken); + //TODO: Error handling + + var splitObj = JsonSerializer.Deserialize(splitResult); + if (sourceLang.Equals("auto", StringComparison.CurrentCultureIgnoreCase) || sourceLang.Equals("")) { - Initial(); + sourceLang = splitObj?["result"]?["lang"]?["detected"]?.ToString()?.ToUpper() ?? "auto"; } - var getToken = token ?? CancellationToken.None; - var timeSpan = GenerateTimestamp(content); + + var jobs = new List(); + var chunks = splitObj?["result"]?["texts"]?[0]?["chunks"]; + if (chunks is JsonArray chunkArray) + { + for (var i = 0; i < chunkArray.Count; i++) + { + var sentence = chunkArray[i]?["sentences"]?[0]; + var contextBefore = Array.Empty(); + var contextAfter = Array.Empty(); + if (i > 0) + { + contextBefore = [chunkArray[i - 1]?["sentences"]?[0]?["text"]?.ToString() ?? ""]; + } + + if (i < chunkArray.Count - 1) + { + contextAfter = [chunkArray[i + 1]?["sentences"]?[0]?["text"]?.ToString() ?? ""]; + } + + var job = new Job + { + Kind = "default", + PreferredNumBeams = 4, + RawEnContextBefore = contextBefore, + RawEnContextAfter = contextAfter, + Sentences = + [ + new Sentence + { + Prefix = sentence?["prefix"]?.ToString() ?? "", + Text = sentence?["text"]?.ToString() ?? "", + Id = i + 1, + } + ] + }; + jobs.Add(job); + } + } + + var hasRegionalVariant = false; + var targetLangCode = targetLang; + var targetLangParts = targetLang.Split("-"); + if (targetLangParts.Length > 1) + { + targetLangCode = targetLangParts[0]; + hasRegionalVariant = true; + } + var id = CreateId(); - var reqStr = GenerateRequestStr(content, sourceLang, targetLang, timeSpan, id); + var reqData = new DeepLRequest + { + Jsonrpc = "2.0", + Method = "LMT_handle_jobs", + Id = id, + Params = new ReqParams + { + CommonJobParams = new ReqParamsCommonJobParams + { + Mode = "translate", + RegionalVariant = hasRegionalVariant ? targetLang : null + }, + Lang = new ReqParamsLang + { + // SourceLangUserSelected = sourceLang, + SourceLangComputed = sourceLang.ToUpper(), + TargetLang = targetLangCode.ToUpper() + }, + Jobs = jobs.ToArray(), + Priority = 1, + Timestamp = GenerateTimestamp(content) + } + }; + + var json = JsonSerializer.Serialize(reqData, GetOptions); + json = AdjustJsonContent(json, id); + using var client = new HttpClient(); - var request = new HttpRequestMessage(HttpMethod.Post, "https://www2.deepl.com/jsonrpc") + var request = new HttpRequestMessage(HttpMethod.Post, + "https://www2.deepl.com/jsonrpc?client=chrome-extension,1.28.0&method=LMT_handle_jobs") { - Content = new StringContent(reqStr, Encoding.UTF8, "application/json") + Content = new StringContent(json, Encoding.UTF8, "application/json") }; - // Add headers to the request - request.Headers.Add("User-Agent", "DeepL-iOS/2.4.0 iOS 15.7.1 (iPhone14,2)"); request.Headers.Add("Accept", "*/*"); - request.Headers.Add("x-app-os-name", "IOS"); - request.Headers.Add("x-app-os-version", "15.7.1"); - request.Headers.Add("Accept-Language", "en-US,en;q=0.9"); - request.Headers.Add("Accept-Encoding", "gzip,deflate,br"); - //request.Headers.Add("Content-Type", "application/json"); - request.Headers.Add("x-app-device", "iPhone14,2"); - request.Headers.Add("x-app-build", "353"); - request.Headers.Add("x-app-version", "2.4"); + request.Headers.Add("Accept-Language", "en-US,en;q=0.9,zh-CN;q=0.8,zh-TW;q=0.7,zh-HK;q=0.6,zh;q=0.5"); + request.Headers.Add("Authorization", "None"); + request.Headers.Add("Cache-Control", "no-cache"); + // request.Headers.Add("Content-Type", "application/json"); + request.Headers.Add("DNT", "1"); + request.Headers.Add("Origin", "chrome-extension://cofdbpoegempjloogbagkncekinflcnj"); + request.Headers.Add("Pragma", "no-cache"); + request.Headers.Add("Priority", "u=1, i"); request.Headers.Add("Referer", "https://www.deepl.com/"); - request.Headers.Add("Connection", "keep-alive"); + request.Headers.Add("Sec-Fetch-Dest", "empty"); + request.Headers.Add("Sec-Fetch-Mode", "cors"); + request.Headers.Add("Sec-Fetch-Site", "none"); + request.Headers.Add("Sec-GPC", "1"); + request.Headers.Add("User-Agent", + "DeepLBrowserExtension/1.28.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"); var resp = await client.SendAsync(request, getToken); //resp.EnsureSuccessStatusCode(); @@ -76,28 +174,124 @@ public static class LocalMode { responseBody = await resp.Content.ReadAsStringAsync(getToken); } + + var jNode = JsonNode.Parse(responseBody); + var data = jNode?["result"]?["translations"]?[0]?["beams"]?[0]?["sentences"]?[0]?["text"]?.ToString(); + // data = UnicodeToString(data); - var deeplResp = JsonSerializer.Deserialize(responseBody); + var errorMsg = jNode?["error"]?["message"]?.ToString(); + var detailsMsg = jNode?["error"]?["data"]?["what"]?.ToString(); + var error = $"Error: {errorMsg}\nDetails: {detailsMsg}"; - var response = new Response { Code = resp.StatusCode.GetHashCode() }; - - string defaultMessage = - resp.StatusCode == System.Net.HttpStatusCode.OK - ? "Empty Result" - : "Empty Error Message"; - response.Data = - deeplResp?.Result?.Texts?.FirstOrDefault()?.Text - ?? deeplResp?.Error?.Message - ?? defaultMessage; + var response = new Response + { + Code = resp.StatusCode.GetHashCode(), + Data = data ?? error + }; return JsonSerializer.Serialize(response, GetOptions); } + private static async Task SplitTextAsync(string text, CancellationToken? token) + { + var id = CreateId(); + var getToken = token ?? CancellationToken.None; + var requestData = new DeepLRequest + { + Jsonrpc = "2.0", + Method = "LMT_split_text", + Id = id, + Params = new ReqParams + { + CommonJobParams = new ReqParamsCommonJobParams + { + Mode = "translate" + }, + Lang = new ReqParamsLang + { + SourceLangUserSelected = "auto" + }, + Texts = [text], + TextType = TextTypeDic[ /*tagHandling*/false || IsRichText(text)] + } + }; + + var json = JsonSerializer.Serialize(requestData, GetOptions); + json = AdjustJsonContent(json, id); + using var client = new HttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, + "https://www2.deepl.com/jsonrpc?client=chrome-extension,1.28.0&method=LMT_split_text") + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + // Add headers to the request + request.Headers.Add("Accept", "*/*"); + request.Headers.Add("Accept-Language", "en-US,en;q=0.9,zh-CN;q=0.8,zh-TW;q=0.7,zh-HK;q=0.6,zh;q=0.5"); + request.Headers.Add("Authorization", "None"); + request.Headers.Add("Cache-Control", "no-cache"); + // request.Headers.Add("Content-Type", "application/json"); + request.Headers.Add("DNT", "1"); + request.Headers.Add("Origin", "chrome-extension://cofdbpoegempjloogbagkncekinflcnj"); + request.Headers.Add("Pragma", "no-cache"); + request.Headers.Add("Priority", "u=1, i"); + request.Headers.Add("Referer", "https://www.deepl.com/"); + request.Headers.Add("Sec-Fetch-Dest", "empty"); + request.Headers.Add("Sec-Fetch-Mode", "cors"); + request.Headers.Add("Sec-Fetch-Site", "none"); + request.Headers.Add("Sec-GPC", "1"); + request.Headers.Add("User-Agent", + "DeepLBrowserExtension/1.28.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"); + + var resp = await client.SendAsync(request, getToken); + string responseBody; + + if (resp.Content.Headers.ContentEncoding.Contains("br")) + { + await using var responseStream = await resp.Content.ReadAsStreamAsync(getToken); + await using var decompressionStream = new BrotliStream( + responseStream, + CompressionMode.Decompress + ); + using var streamReader = new StreamReader(decompressionStream); + responseBody = await streamReader.ReadToEndAsync(getToken); + } + else + { + responseBody = await resp.Content.ReadAsStringAsync(getToken); + } + + return responseBody; + } + + private static string? UnicodeToString(string? srcText) + { + if (srcText == null) return default; + var dst = ""; + var src = srcText; + var len = srcText.Length / 6; + for (var i = 0; i <= len - 1; i++) + { + var str = ""; + str = src[..6][2..]; + src = src[6..]; + var bytes = new byte[2]; + bytes[1] = byte.Parse(int.Parse(str[..2], System.Globalization.NumberStyles.HexNumber).ToString()); + bytes[0] = byte.Parse(int.Parse(str.Substring(2, 2), System.Globalization.NumberStyles.HexNumber).ToString()); + dst += Encoding.Unicode.GetString(bytes); + } + return dst; + } + + private static bool IsRichText(string text) + { + return text.Contains('<') && text.Contains('>'); + } + private static long GenerateTimestamp(string texts) { long iCount = texts.Split('i').Length - 1; var ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - return iCount != 0 ? ts - ts % (iCount + 1) + (iCount + 1) : ts; + return iCount != 0 ? ts - ts % (iCount + 1) + iCount + 1 : ts; } private static long CreateId() @@ -109,62 +303,11 @@ public static class LocalMode { string method; if ((id + 3) % 13 == 0 || (id + 5) % 29 == 0) - { method = "\"method\" : \""; - } else - { method = "\"method\": \""; - } return json.Replace("\"method\":\"", method); } - - private static JsonSerializerOptions GetOptions => - new() - { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - private static string GenerateRequestStr( - string text, - string? sourceLang, - string targetLang, - long timeSpan, - long id - ) - { - var req = new DeepLRequest - { - Jsonrpc = "2.0", - Method = "LMT_handle_texts", - Params = new ReqParams - { - Texts = [new ReqParamsTexts { Text = text, RequestAlternatives = 0 }], - Splitting = "newlines", - Lang = new ReqParamsLang - { - SourceLangUserSelected = sourceLang, - TargetLang = targetLang - }, - Timestamp = timeSpan, - CommonJobParams = new ReqParamsCommonJobParams - { - WasSpoken = false, - TranscribeAS = "" - } - }, - Id = id - }; - - var json = JsonSerializer.Serialize(req, GetOptions); - - var count = json.Length > 300 ? 0 : 3; - req.Params.Texts.First().RequestAlternatives = count; - json = JsonSerializer.Serialize(req, GetOptions); - json = AdjustJsonContent(json, id); - return json; - } } public class Response @@ -173,46 +316,34 @@ public class Response public string Data { get; set; } = ""; } +#region Request + public class DeepLRequest { - [JsonPropertyName("jsonrpc")] - public string Jsonrpc { get; set; } = ""; + [JsonPropertyName("jsonrpc")] public string Jsonrpc { get; set; } = ""; - [JsonPropertyName("method")] - public string Method { get; set; } = ""; + [JsonPropertyName("method")] public string Method { get; set; } = ""; - [JsonPropertyName("params")] - public ReqParams? Params { get; set; } + [JsonPropertyName("params")] public ReqParams? Params { get; set; } - [JsonPropertyName("id")] - public long Id { get; set; } + [JsonPropertyName("id")] public long Id { get; set; } } public class ReqParams { - [JsonPropertyName("texts")] - public ReqParamsTexts[]? Texts { get; set; } + [JsonPropertyName("commonJobParams")] public ReqParamsCommonJobParams? CommonJobParams { get; set; } - [JsonPropertyName("splitting")] - public string Splitting { get; set; } = ""; + [JsonPropertyName("lang")] public ReqParamsLang? Lang { get; set; } - [JsonPropertyName("lang")] - public ReqParamsLang? Lang { get; set; } + [JsonPropertyName("texts")] public string[]? Texts { get; set; } - [JsonPropertyName("timestamp")] - public long Timestamp { get; set; } + [JsonPropertyName("textType")] public string? TextType { get; set; } - [JsonPropertyName("commonJobParams")] - public ReqParamsCommonJobParams? CommonJobParams { get; set; } -} + [JsonPropertyName("jobs")] public Job[]? Jobs { get; set; } -public class ReqParamsTexts -{ - [JsonPropertyName("text")] - public string Text { get; set; } = ""; + [JsonPropertyName("priority")] public int Priority { get; set; } - [JsonPropertyName("requestAlternatives")] - public int RequestAlternatives { get; set; } + [JsonPropertyName("timestamp")] public long Timestamp { get; set; } } public class ReqParamsLang @@ -220,94 +351,50 @@ public class ReqParamsLang [JsonPropertyName("source_lang_user_selected")] public string? SourceLangUserSelected { get; set; } - [JsonPropertyName("target_lang")] - public string TargetLang { get; set; } = ""; + [JsonPropertyName("source_lang_computed")] + public string? SourceLangComputed { get; set; } + + [JsonPropertyName("target_lang")] public string TargetLang { get; set; } = ""; } public class ReqParamsCommonJobParams { - [JsonPropertyName("wasSpoken")] - public bool WasSpoken { get; set; } + [JsonPropertyName("mode")] public string Mode { get; set; } = ""; - [JsonPropertyName("transcribe_as")] - public string? TranscribeAS { get; set; } + [JsonPropertyName("regionalVariant")] public string? RegionalVariant { get; set; } } -public class DeepLResponse +public class Job { - [JsonPropertyName("jsonrpc")] - public string Jsonrpc { get; set; } = ""; + [JsonPropertyName("kind")] public string Kind { get; set; } = ""; + + [JsonPropertyName("preferred_num_beams")] + public int PreferredNumBeams { get; set; } - [JsonPropertyName("id")] - public long Id { get; set; } + [JsonPropertyName("raw_en_context_before")] + public string[]? RawEnContextBefore { get; set; } - [JsonPropertyName("result")] - public RespResult? Result { get; set; } + [JsonPropertyName("raw_en_context_after")] + public string[]? RawEnContextAfter { get; set; } - [JsonPropertyName("error")] - public RespError? Error { get; set; } + [JsonPropertyName("sentences")] public Sentence[]? Sentences { get; set; } } -public class RespResult +public class Sentence { - [JsonPropertyName("texts")] - public RespResultText[]? Texts { get; set; } + [JsonPropertyName("id")] public int Id { get; set; } - [JsonPropertyName("lang")] - public string Lang { get; set; } = ""; + [JsonPropertyName("prefix")] public string Prefix { get; set; } = ""; - [JsonPropertyName("lang_is_confident")] - public bool LangIsConfident { get; set; } - - [JsonPropertyName("detectedLanguages")] - public RespResultDetectedLanguages? DetectedLanguages { get; set; } + [JsonPropertyName("text")] public string Text { get; set; } = ""; } -public class RespResultText -{ - [JsonPropertyName("alternatives")] - public object[]? Alternatives { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } = ""; -} +#endregion -public class RespResultDetectedLanguages -{ - public float EN { get; set; } - public float DE { get; set; } - public float FR { get; set; } - public float ES { get; set; } - public float PT { get; set; } - public float IT { get; set; } - public float NL { get; set; } - public float PL { get; set; } - public float RU { get; set; } - public float ZH { get; set; } - public float JA { get; set; } - public float BG { get; set; } - public float CS { get; set; } - public float DA { get; set; } - public float EL { get; set; } - public float ET { get; set; } - public float FI { get; set; } - public float HU { get; set; } - public float LT { get; set; } - public float LV { get; set; } - public float RO { get; set; } - public float SK { get; set; } - public float SL { get; set; } - public float SV { get; set; } - public float TR { get; set; } - public float ID { get; set; } - public float Unsupported { get; set; } -} public class RespError { - [JsonPropertyName("code")] - public int Code { get; set; } + [JsonPropertyName("code")] public int Code { get; set; } - [JsonPropertyName("message")] - public string Message { get; set; } = ""; -} + [JsonPropertyName("message")] public string Message { get; set; } = ""; +} \ No newline at end of file diff --git a/STranslateDLLTests/LocalModeTests.cs b/STranslateDLLTests/LocalModeTests.cs index ef8fb85..3b5a860 100644 --- a/STranslateDLLTests/LocalModeTests.cs +++ b/STranslateDLLTests/LocalModeTests.cs @@ -1,9 +1,11 @@ using System.Diagnostics; +using STranslateDLL; using Xunit; +using Xunit.Abstractions; -namespace STranslateDLL.Tests; +namespace STranslateDLLTests; -public class LocalModeTests +public class LocalModeTests(ITestOutputHelper testOutputHelper) { [Fact()] public async Task ExecuteAsyncTestAsync() @@ -11,8 +13,8 @@ public class LocalModeTests try { var cts = new CancellationTokenSource(); - var ret = await LocalMode.ExecuteAsync("Hello World", "auto", "ZH", cts.Token); - Debug.WriteLine(ret); + var ret = await LocalMode.ExecuteAsync("Hello World", "EN", "ZH", cts.Token); + testOutputHelper.WriteLine(ret); } catch (Exception ex) {