diff --git a/src/libse/AutoTranslate/AnthropicTranslate.cs b/src/libse/AutoTranslate/AnthropicTranslate.cs index 80167a1e0f..76b6465f0a 100644 --- a/src/libse/AutoTranslate/AnthropicTranslate.cs +++ b/src/libse/AutoTranslate/AnthropicTranslate.cs @@ -12,7 +12,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class AnthropicTranslate : IAutoTranslator, IDisposable + public class AnthropicTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private HttpClient _httpClient; @@ -62,6 +62,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = Configuration.Settings.Tools.AnthropicApiModel; if (string.IsNullOrEmpty(model)) @@ -74,7 +79,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.AnthropicPrompt = new ToolsSettings().AnthropicPrompt; } - var prompt = string.Format(Json.EncodeJsonText(Configuration.Settings.Tools.AnthropicPrompt), sourceLanguageCode, targetLanguageCode); + var prompt = Json.EncodeJsonText(string.Format(Configuration.Settings.Tools.AnthropicPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText)); var input = "{ \"model\": \"" + model + "\", \"max_tokens\": 1024, \"messages\": [{ \"role\": \"user\", \"content\": \"" + prompt + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\" }]}"; var content = new StringContent(input, Encoding.UTF8); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); diff --git a/src/libse/AutoTranslate/AvalAi.cs b/src/libse/AutoTranslate/AvalAi.cs index a847d18107..1b9b52ad32 100644 --- a/src/libse/AutoTranslate/AvalAi.cs +++ b/src/libse/AutoTranslate/AvalAi.cs @@ -12,7 +12,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class AvalAi : IAutoTranslator, IDisposable + public class AvalAi : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private HttpClient _httpClient; @@ -99,6 +99,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = Configuration.Settings.Tools.AvalAiModel; if (string.IsNullOrEmpty(model)) @@ -111,7 +116,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.AvalAiPrompt = new ToolsSettings().AvalAiPrompt; } - var prompt = string.Format(Json.EncodeJsonText(Configuration.Settings.Tools.AvalAiPrompt), sourceLanguageCode, targetLanguageCode); + var prompt = Json.EncodeJsonText(string.Format(Configuration.Settings.Tools.AvalAiPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText)); var input = ""; var input2 = "{\"model\": \"" + model + "\",\"messages\": [{ \"role\": \"user\", \"content\": \"" + prompt + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\" }]}"; var modelJson = "\"" + model + "\""; diff --git a/src/libse/AutoTranslate/ChatGptTranslate.cs b/src/libse/AutoTranslate/ChatGptTranslate.cs index 6bd8078c4e..4fb37f55a4 100644 --- a/src/libse/AutoTranslate/ChatGptTranslate.cs +++ b/src/libse/AutoTranslate/ChatGptTranslate.cs @@ -13,7 +13,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class ChatGptTranslate : IAutoTranslator, IDisposable + public class ChatGptTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private static readonly Regex UnicodeRegex = new Regex(@"\\u([0-9a-fA-F]{4})", RegexOptions.Compiled); @@ -97,6 +97,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = Configuration.Settings.Tools.ChatGptModel; if (string.IsNullOrEmpty(model)) @@ -109,7 +114,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.ChatGptPrompt = new ToolsSettings().ChatGptPrompt; } - var prompt = string.Format(Configuration.Settings.Tools.ChatGptPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.ChatGptPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = "{\"model\": \"" + Json.EncodeJsonText(model) + "\",\"messages\": [{ \"role\": \"user\", \"content\": \"" + Json.EncodeJsonText(prompt) + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\" }]}"; var content = new StringContent(input, Encoding.UTF8); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); diff --git a/src/libse/AutoTranslate/DeepSeekTranslate.cs b/src/libse/AutoTranslate/DeepSeekTranslate.cs index 6a9033904c..5e93530062 100644 --- a/src/libse/AutoTranslate/DeepSeekTranslate.cs +++ b/src/libse/AutoTranslate/DeepSeekTranslate.cs @@ -12,7 +12,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class DeepSeekTranslate : IAutoTranslator, IDisposable + public class DeepSeekTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private HttpClient _httpClient; @@ -58,6 +58,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = Configuration.Settings.Tools.DeepSeekModel; if (string.IsNullOrEmpty(model)) @@ -70,7 +75,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.DeepSeekPrompt = new ToolsSettings().DeepSeekPrompt; } - var prompt = string.Format(Configuration.Settings.Tools.DeepSeekPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.DeepSeekPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = "{\"model\": \"" + model + "\",\"messages\": [{ \"role\": \"user\", \"content\": \"" + Json.EncodeJsonText(prompt) + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\" }]}"; int[] retryDelays = { 2555, 5007, 9013 }; diff --git a/src/libse/AutoTranslate/GeminiTranslate.cs b/src/libse/AutoTranslate/GeminiTranslate.cs index c7886692f4..adcf1410c5 100644 --- a/src/libse/AutoTranslate/GeminiTranslate.cs +++ b/src/libse/AutoTranslate/GeminiTranslate.cs @@ -12,7 +12,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class GeminiTranslate : IAutoTranslator, IDisposable + public class GeminiTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { public static string StaticName { get; set; } = "Google Gemini"; public override string ToString() => StaticName; @@ -82,6 +82,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { int[] retryDelays = { 555, 3007, 7013 }; HttpResponseMessage result = null; @@ -89,7 +94,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri var switchedBaseUrl = false; for (var attempt = 0; attempt <= retryDelays.Length; attempt++) { - var content = MakeContent(text, sourceLanguageCode, targetLanguageCode); + var content = MakeContent(text, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); result = await _httpClient.PostAsync(_baseUrl, content, cancellationToken); if (result.StatusCode == System.Net.HttpStatusCode.NotFound && !switchedBaseUrl) @@ -147,9 +152,9 @@ public async Task Translate(string text, string sourceLanguageCode, stri return outputText; } - private HttpContent MakeContent(string text, string sourceLanguageCode, string targetLanguageCode) + private HttpContent MakeContent(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText) { - var prompt = string.Format(Configuration.Settings.Tools.GeminiPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.GeminiPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = "{ \"contents\": [ { \"role\": \"user\", \"parts\": [{ \"text\": \"" + Json.EncodeJsonText(prompt) + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\" }]}]}"; var content = new StringContent(input, Encoding.UTF8); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); diff --git a/src/libse/AutoTranslate/GroqTranslate.cs b/src/libse/AutoTranslate/GroqTranslate.cs index 2d945c81c8..b9c05a1fd8 100644 --- a/src/libse/AutoTranslate/GroqTranslate.cs +++ b/src/libse/AutoTranslate/GroqTranslate.cs @@ -12,7 +12,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class GroqTranslate : IAutoTranslator, IDisposable + public class GroqTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private HttpClient _httpClient; @@ -76,6 +76,11 @@ public List GetSupportedTargetLanguages() //-d '{"messages": [{"role": "user", "content": "Explain the importance of fast language models"}], "model": "llama3-8b-8192"} public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = Configuration.Settings.Tools.GroqModel; if (string.IsNullOrEmpty(model)) @@ -88,7 +93,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.GroqPrompt = new ToolsSettings().GroqPrompt; } - var prompt = string.Format(Configuration.Settings.Tools.GroqPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.GroqPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = "{\"model\": \"" + model + "\",\"messages\": [{ \"role\": \"user\", \"content\": \"" + Json.EncodeJsonText(prompt) + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\" }]}"; var content = new StringContent(input, Encoding.UTF8); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); diff --git a/src/libse/AutoTranslate/IAutoTranslatorWithContext.cs b/src/libse/AutoTranslate/IAutoTranslatorWithContext.cs new file mode 100644 index 0000000000..f8733cb28b --- /dev/null +++ b/src/libse/AutoTranslate/IAutoTranslatorWithContext.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Nikse.SubtitleEdit.Core.AutoTranslate +{ + /// + /// Optional extension for implementations that can use the + /// surrounding subtitle lines (previous/next) as extra context for the translation. + /// + public interface IAutoTranslatorWithContext + { + /// + /// Translates the given text, providing the neighboring subtitle lines as extra context. + /// + /// The text to be translated. + /// The language code of the source language. + /// The language code of the target language. + /// The subtitle line right before , or empty if none. + /// The subtitle line right after , or empty if none. + /// The cancellation token to cancel the translation process. + Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken); + } +} diff --git a/src/libse/AutoTranslate/KoboldCppTranslate.cs b/src/libse/AutoTranslate/KoboldCppTranslate.cs index 0e0b9ede62..ce2384e96c 100644 --- a/src/libse/AutoTranslate/KoboldCppTranslate.cs +++ b/src/libse/AutoTranslate/KoboldCppTranslate.cs @@ -12,7 +12,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class KoboldCppTranslate : IAutoTranslator, IDisposable + public class KoboldCppTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private HttpClient _httpClient; @@ -44,6 +44,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { // var model = Configuration.Settings.Tools.KoboldCppModel; var modelJson = string.Empty; @@ -63,7 +68,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.KoboldCppPrompt = new ToolsSettings().KoboldCppPrompt; } - var prompt = string.Format(Configuration.Settings.Tools.KoboldCppPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.KoboldCppPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = "{ " + modelJson + temperatureJson + " \"prompt\": \"" + Json.EncodeJsonText(prompt) + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\", \"stream\": false }"; var content = new StringContent(input, Encoding.UTF8); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); diff --git a/src/libse/AutoTranslate/LlamaCppTranslate.cs b/src/libse/AutoTranslate/LlamaCppTranslate.cs index b374c0bf38..dda7c9c96a 100644 --- a/src/libse/AutoTranslate/LlamaCppTranslate.cs +++ b/src/libse/AutoTranslate/LlamaCppTranslate.cs @@ -12,7 +12,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class LlamaCppTranslate : IAutoTranslator, IDisposable + public class LlamaCppTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private HttpClient _httpClient; @@ -44,6 +44,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = string.Empty; var modelJson = string.Empty; @@ -57,7 +62,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.LlamaCppPrompt = new ToolsSettings().LlamaCppPrompt; } - var prompt = string.Format(Configuration.Settings.Tools.LlamaCppPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.LlamaCppPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = "{ " + modelJson + " \"messages\": [{ \"role\": \"user\", \"content\": \"" + Json.EncodeJsonText(prompt) + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\" }]}"; var content = new StringContent(input, Encoding.UTF8); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); diff --git a/src/libse/AutoTranslate/LmStudioTranslate.cs b/src/libse/AutoTranslate/LmStudioTranslate.cs index f89e582830..bee3c2891d 100644 --- a/src/libse/AutoTranslate/LmStudioTranslate.cs +++ b/src/libse/AutoTranslate/LmStudioTranslate.cs @@ -12,7 +12,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class LmStudioTranslate : IAutoTranslator, IDisposable + public class LmStudioTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private HttpClient _httpClient; @@ -44,6 +44,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = Configuration.Settings.Tools.LmStudioModel; var modelJson = string.Empty; @@ -57,7 +62,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.LmStudioPrompt = new ToolsSettings().LmStudioPrompt; } - var prompt = string.Format(Configuration.Settings.Tools.LmStudioPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.LmStudioPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = "{ " + modelJson + " \"messages\": [{ \"role\": \"user\", \"content\": \"" + Json.EncodeJsonText(prompt) + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\" }]}"; var content = new StringContent(input, Encoding.UTF8); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); diff --git a/src/libse/AutoTranslate/MistralTranslate.cs b/src/libse/AutoTranslate/MistralTranslate.cs index c2e448bfcf..3e15cadfca 100644 --- a/src/libse/AutoTranslate/MistralTranslate.cs +++ b/src/libse/AutoTranslate/MistralTranslate.cs @@ -15,7 +15,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate /// /// Mistral AI translator - see https://mistral.ai/en /// - public class MistralTranslate : IAutoTranslator, IDisposable + public class MistralTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private string _apiKey; private string _apiUrl; @@ -66,6 +66,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = Configuration.Settings.Tools.AutoTranslateMistralModel; if (string.IsNullOrEmpty(model)) @@ -78,7 +83,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.AutoTranslateMistralPrompt = new ToolsSettings().AutoTranslateMistralPrompt; } - var prompt = string.Format(Configuration.Settings.Tools.AutoTranslateMistralPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.AutoTranslateMistralPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = "{\"model\": \"" + model + "\",\"messages\": [{ \"role\": \"user\", \"content\": \"" + Json.EncodeJsonText(prompt) + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\" }]}"; var content = new StringContent(input, Encoding.UTF8); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); diff --git a/src/libse/AutoTranslate/NvidiaTranslate.cs b/src/libse/AutoTranslate/NvidiaTranslate.cs index cbca0f450e..461e5a787c 100644 --- a/src/libse/AutoTranslate/NvidiaTranslate.cs +++ b/src/libse/AutoTranslate/NvidiaTranslate.cs @@ -12,7 +12,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class NvidiaTranslate : IAutoTranslator, IDisposable + public class NvidiaTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private HttpClient _httpClient; @@ -107,6 +107,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = Configuration.Settings.Tools.NvidiaModel; if (string.IsNullOrEmpty(model)) @@ -119,7 +124,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.NvidiaPrompt = new ToolsSettings().NvidiaPrompt; } - var prompt = string.Format(Configuration.Settings.Tools.NvidiaPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.NvidiaPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = "{\"model\": \"" + model + "\",\"messages\": [{ \"role\": \"user\", \"content\": \"" + Json.EncodeJsonText(prompt) + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\" }]}"; int[] retryDelays = { 2555, 5007, 9013 }; diff --git a/src/libse/AutoTranslate/OllamaTranslate.cs b/src/libse/AutoTranslate/OllamaTranslate.cs index 750367ddab..de1d92e8c5 100644 --- a/src/libse/AutoTranslate/OllamaTranslate.cs +++ b/src/libse/AutoTranslate/OllamaTranslate.cs @@ -12,7 +12,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class OllamaTranslate : IAutoTranslator, IDisposable + public class OllamaTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private HttpClient _httpClient; @@ -44,6 +44,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = Configuration.Settings.Tools.OllamaModel; var modelJson = string.Empty; @@ -57,7 +62,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.OllamaPrompt = new ToolsSettings().OllamaPrompt; } - var prompt = string.Format(Configuration.Settings.Tools.OllamaPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.OllamaPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = "{ " + modelJson + " \"prompt\": \"" + Json.EncodeJsonText(prompt) + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\", \"stream\": false }"; if (Configuration.Settings.Tools.OllamaApiUrl.TrimEnd('/').EndsWith("v1/chat/completions")) { diff --git a/src/libse/AutoTranslate/OpenRouterTranslate.cs b/src/libse/AutoTranslate/OpenRouterTranslate.cs index b3bed89486..4b6e936c25 100644 --- a/src/libse/AutoTranslate/OpenRouterTranslate.cs +++ b/src/libse/AutoTranslate/OpenRouterTranslate.cs @@ -12,7 +12,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class OpenRouterTranslate : IAutoTranslator, IDisposable + public class OpenRouterTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private HttpClient _httpClient; @@ -80,6 +80,11 @@ public List GetSupportedTargetLanguages() } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = Configuration.Settings.Tools.OpenRouterModel; if (string.IsNullOrEmpty(model)) @@ -92,7 +97,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri { Configuration.Settings.Tools.OpenRouterPrompt = new ToolsSettings().OpenRouterPrompt; } - var prompt = string.Format(Configuration.Settings.Tools.OpenRouterPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.OpenRouterPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = "{\"model\": \"" + model + "\",\"messages\": [{ \"role\": \"user\", \"content\": \"" + Json.EncodeJsonText(prompt) + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\" }]}"; var content = new StringContent(input, Encoding.UTF8); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); diff --git a/src/libse/AutoTranslate/PerplexityTranslate.cs b/src/libse/AutoTranslate/PerplexityTranslate.cs index 8ab3062afc..833babd8b7 100644 --- a/src/libse/AutoTranslate/PerplexityTranslate.cs +++ b/src/libse/AutoTranslate/PerplexityTranslate.cs @@ -11,7 +11,7 @@ namespace Nikse.SubtitleEdit.Core.AutoTranslate { - public class PerplexityTranslate : IAutoTranslator, IDisposable + public class PerplexityTranslate : IAutoTranslator, IAutoTranslatorWithContext, IDisposable { private HttpClient _httpClient; @@ -103,6 +103,11 @@ public List GetSupportedTargetLanguages() // } public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + return await Translate(text, sourceLanguageCode, targetLanguageCode, string.Empty, string.Empty, cancellationToken); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, string previousLineText, string nextLineText, CancellationToken cancellationToken) { var model = Configuration.Settings.Tools.PerplexityModel; if (string.IsNullOrEmpty(model)) @@ -116,7 +121,7 @@ public async Task Translate(string text, string sourceLanguageCode, stri Configuration.Settings.Tools.PerplexityPrompt = new ToolsSettings().PerplexityPrompt; } - var prompt = string.Format(Configuration.Settings.Tools.PerplexityPrompt, sourceLanguageCode, targetLanguageCode); + var prompt = string.Format(Configuration.Settings.Tools.PerplexityPrompt, sourceLanguageCode, targetLanguageCode, previousLineText, nextLineText); var input = prompt + Environment.NewLine + Environment.NewLine + text; // Build JSON request body according to Perplexity API diff --git a/src/libse/Settings/ToolsSettings.cs b/src/libse/Settings/ToolsSettings.cs index d29f230d65..b0b6cb1661 100644 --- a/src/libse/Settings/ToolsSettings.cs +++ b/src/libse/Settings/ToolsSettings.cs @@ -380,40 +380,41 @@ public ToolsSettings() AutoTranslateDeepLXUrl = "http://localhost:1188"; AutoTranslateMistralUrl = "https://api.mistral.ai/v1/chat/completions"; AutoTranslateMistralModel = MistralTranslate.Models[0]; - AutoTranslateMistralPrompt = "Translate from {0} to {1}, keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; + AutoTranslateMistralPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; ChatGptUrl = "https://api.openai.com/v1/chat/completions"; - ChatGptPrompt = "Translate from {0} to {1}, keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; + ChatGptPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; ChatGptModel = ChatGptTranslate.DefaultModel; GroqUrl = "https://api.groq.com/openai/v1/chat/completions"; - GroqPrompt = "Translate from {0} to {1}, keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; + GroqPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; GroqModel = GroqTranslate.Models[0]; DeepSeekUrl = "https://api.deepseek.com/chat/completions"; - DeepSeekPrompt = "Translate from {0} to {1}, keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; + DeepSeekPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; DeepSeekModel = DeepSeekTranslate.Models[0]; NvidiaUrl = "https://integrate.api.nvidia.com/v1/chat/completions"; - NvidiaPrompt = "Translate from {0} to {1}, keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; + NvidiaPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; NvidiaModel = NvidiaTranslate.Models[0]; AvalAiUrl = "https://api.avalai.ir/v1/chat/completions"; - AvalAiPrompt = "Translate from {0} to {1}, keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; + AvalAiPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; AvalAiModel = AvalAi.Models[0]; OpenRouterUrl = "https://openrouter.ai/api/v1/chat/completions"; - OpenRouterPrompt = "Translate from {0} to {1}, keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; + OpenRouterPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; OpenRouterModel = OpenRouterTranslate.Models[0]; - LmStudioPrompt = "Translate from {0} to {1}, keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; - LlamaCppPrompt = "Translate from {0} to {1}, keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; + LmStudioPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; + LlamaCppPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; OllamaApiUrl = "http://localhost:11434/api/generate"; OllamaModels = "llama3.2,llama3.2:1b,phi3,gemma2,qwen2,mistral"; OllamaModel = "llama3.2"; - OllamaPrompt = "Translate from {0} to {1}, keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments or notes:"; + OllamaPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments or notes:"; KoboldCppUrl = "http://localhost:5001/api/generate/"; - KoboldCppPrompt = "Translate from {0} to {1}, keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments or notes:"; + KoboldCppPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments or notes:"; KoboldCppTemperature = 0.4m; AnthropicApiUrl = "https://api.anthropic.com/v1/messages"; - AnthropicPrompt = "Translate from {0} to {1}, keep sentences in {1} as they are, do not censor the translation, give only the output without comments:"; + AnthropicPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep sentences in {1} as they are, do not censor the translation, give only the output without comments:"; AnthropicApiModel = AnthropicTranslate.Models[0]; BaiduUrl = "https://fanyi-api.baidu.com"; GeminiModel = GeminiTranslate.Models[0]; - GeminiPrompt = "Please translate the following text from {0} to {1}, keep line breaks exactly the same, do not censor the translation, only write the result:"; + GeminiPrompt = "Please translate the following text from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep line breaks exactly the same, do not censor the translation, only write the result:"; + PerplexityPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; AutoTranslateMaxBytes = 2000; CheckOneLetterWords = true; ListViewSyntaxColorWideLines = false; diff --git a/src/ui/Features/Translate/MergeAndSplitHelper.cs b/src/ui/Features/Translate/MergeAndSplitHelper.cs index 507992ceaa..a8362ece9d 100644 --- a/src/ui/Features/Translate/MergeAndSplitHelper.cs +++ b/src/ui/Features/Translate/MergeAndSplitHelper.cs @@ -43,7 +43,18 @@ public static async Task MergeAndTranslateIfPossible( return 0; } - var mergedTranslation = await autoTranslator.Translate(mergeResult.Text, source.Code, target.Code, cancellationToken); + string mergedTranslation; + if (autoTranslator is IAutoTranslatorWithContext contextAutoTranslator) + { + var previousLineText = index > 0 ? tempSubtitle[index - 1].Text : string.Empty; + var nextLineIndex = index + mergeResult.ParagraphCount; + var nextLineText = nextLineIndex < tempSubtitle.Length ? tempSubtitle[nextLineIndex].Text : string.Empty; + mergedTranslation = await contextAutoTranslator.Translate(mergeResult.Text, source.Code, target.Code, previousLineText, nextLineText, cancellationToken); + } + else + { + mergedTranslation = await autoTranslator.Translate(mergeResult.Text, source.Code, target.Code, cancellationToken); + } if (forceSingleLineMode || mergeResult.ParagraphCount == 1) { diff --git a/src/ui/Features/Translate/TranslateSettingsViewModel.cs b/src/ui/Features/Translate/TranslateSettingsViewModel.cs index 9cc645ba1b..eb4d10cc09 100644 --- a/src/ui/Features/Translate/TranslateSettingsViewModel.cs +++ b/src/ui/Features/Translate/TranslateSettingsViewModel.cs @@ -50,7 +50,7 @@ private async Task Ok() } var engineType = AutoTranslator.GetType(); - if (engineType == typeof(ChatGptTranslate) || + var supportsLineContext = engineType == typeof(ChatGptTranslate) || engineType == typeof(OllamaTranslate) || engineType == typeof(LmStudioTranslate) || engineType == typeof(AnthropicTranslate) || @@ -61,7 +61,9 @@ private async Task Ok() engineType == typeof(MistralTranslate) || engineType == typeof(GeminiTranslate) || engineType == typeof(DeepSeekTranslate) || - engineType == typeof(LlamaCppTranslate)) + engineType == typeof(LlamaCppTranslate); + + if (supportsLineContext) { if (!PromptText.Contains("{0}") || !PromptText.Contains("{1}")) { @@ -71,15 +73,25 @@ await MessageBox.Show(Window!, "Error", } } - if (PromptText.Replace("{0}", string.Empty).Replace("{1}", string.Empty).Contains('{')) + var allowedPlaceholders = supportsLineContext + ? "'{0}', '{1}', '{2}' and '{3}'" + : "'{0}' and '{1}'"; + + var sanitizedPromptText = PromptText.Replace("{0}", string.Empty).Replace("{1}", string.Empty); + if (supportsLineContext) + { + sanitizedPromptText = sanitizedPromptText.Replace("{2}", string.Empty).Replace("{3}", string.Empty); + } + + if (sanitizedPromptText.Contains('{')) { - await MessageBox.Show(Window!, "Error", "Character not allowed in prompt: '{' (besides '{0}' and '{1}')", MessageBoxButtons.OK, MessageBoxIcon.Error); + await MessageBox.Show(Window!, "Error", $"Character not allowed in prompt: '{{' (besides {allowedPlaceholders})", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } - if (PromptText.Replace("{0}", string.Empty).Replace("{1}", string.Empty).Contains('}')) + if (sanitizedPromptText.Contains('}')) { - await MessageBox.Show(Window!, "Error", "Character not allowed in prompt: '}' (besides '{0}' and '{1}')", MessageBoxButtons.OK, MessageBoxIcon.Error); + await MessageBox.Show(Window!, "Error", $"Character not allowed in prompt: '}}' (besides {allowedPlaceholders})", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } diff --git a/src/ui/Logic/Config/SeAutoTranslate.cs b/src/ui/Logic/Config/SeAutoTranslate.cs index 610c19f633..ee4e3c108d 100644 --- a/src/ui/Logic/Config/SeAutoTranslate.cs +++ b/src/ui/Logic/Config/SeAutoTranslate.cs @@ -113,21 +113,21 @@ public SeAutoTranslate() AnthropicApiKey = string.Empty; AnthropicApiModel = AnthropicTranslate.Models[0]; AnthropicApiUrl = "https://api.anthropic.com/v1/messages"; - AnthropicPrompt = "Translate from {0} to {1}, keep sentences in {1} as they are, do not censor the translation, give only the output without comments:"; + AnthropicPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep sentences in {1} as they are, do not censor the translation, give only the output without comments:"; AvalAiApiKey = string.Empty; AvalAiModel = AvalAi.Models[0]; - AvalAiPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments:"; + AvalAiPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; AvalAiUrl = "https://api.avalai.ir/v1/chat/completions"; PerplexityApiKey = string.Empty; PerplexityModel = PerplexityTranslate.Models[0]; - PerplexityPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments:"; + PerplexityPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; PerplexityUrl = "https://api.perplexity.ai/v1/responses"; LaraUrl = "https://api.laratranslate.com"; BaiduApiKey = string.Empty; BaiduUrl = "https://fanyi-api.baidu.com"; ChatGptApiKey = string.Empty; ChatGptModel = ChatGptTranslate.DefaultModel; - ChatGptPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments:"; + ChatGptPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; ChatGptUrl = "https://api.openai.com/v1/chat/completions"; CopyPasteLineSeparator = "(...)"; CopyPasteMaxBlockSize = 5000; @@ -137,21 +137,21 @@ public SeAutoTranslate() DeepLXUrl = "http://localhost:1188"; DeepSeekApiKey = string.Empty; DeepSeekModel = DeepSeekTranslate.Models[0]; - DeepSeekPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments:"; + DeepSeekPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; DeepSeekUrl = "https://api.deepseek.com/chat/completions"; NvidiaApiKey = string.Empty; NvidiaModel = NvidiaTranslate.Models[0]; - NvidiaPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments:"; + NvidiaPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; NvidiaUrl = "https://integrate.api.nvidia.com/v1/chat/completions"; GeminiModel = GeminiTranslate.Models[0]; GeminiProApiKey = string.Empty; - GeminiPrompt = "Please translate the following text from {0} to {1}, do not censor the translation, only write the result:"; + GeminiPrompt = "Please translate the following text from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep line breaks exactly the same, do not censor the translation, only write the result:"; GoogleApiV2Key = string.Empty; GroqApiKey = string.Empty; GroqModel = GroqTranslate.Models[0]; - GroqPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments:"; + GroqPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; GroqUrl = "https://api.groq.com/openai/v1/chat/completions"; - KoboldCppPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments or notes:"; + KoboldCppPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments or notes:"; KoboldCppTemperature = 0.4m; KoboldCppUrl = "http://localhost:5001/api/generate/"; LibreTranslateApiKey = string.Empty; @@ -159,17 +159,17 @@ public SeAutoTranslate() LibreTranslateUrl = "http://localhost:5000/"; LlamaCppApiUrl = "http://localhost:8080/v1/chat/completions"; LlamaCppModel = string.Empty; - LlamaCppPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments:"; + LlamaCppPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; LmStudioApiUrl = "http://localhost:1234/v1/chat/completions/"; LmStudioModel = string.Empty; - LmStudioPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments:"; + LmStudioPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; MicrosoftBingApiId = string.Empty; MicrosoftTranslatorApiKey = string.Empty; MicrosoftTranslatorCategory = string.Empty; MicrosoftTranslatorTokenEndpoint = "https://api.cognitive.microsoft.com/sts/v1.0/issueToken"; MistralApiKey = string.Empty; MistralModel = MistralTranslate.Models[0]; - MistralPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments:"; + MistralPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; MistralUrl = "https://api.mistral.ai/v1/chat/completions"; MyMemoryApiKey = string.Empty; NllbApiUrl = "http://localhost:7860/api/v4/"; @@ -181,11 +181,11 @@ public SeAutoTranslate() NnlbServeUrl = string.Empty; OllamaModel = string.Empty; OllamaModels = "llama3.2,llama3.2:1b,phi3,gemma2,qwen2,mistral"; - OllamaPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments or notes:"; + OllamaPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments or notes:"; OllamaUrl = "http://localhost:11434/api/generate"; OpenRouterApiKey = string.Empty; OpenRouterModel = OpenRouterTranslate.Models[0]; - OpenRouterPrompt = "Translate from {0} to {1}, keep punctuation as input, do not censor the translation, give only the output without comments:"; + OpenRouterPrompt = "Translate from {0} to {1}, the previous subtitle line was {2}, and the following is {3}. Keep punctuation as input, keep line breaks exactly the same, do not censor the translation, give only the output without comments:"; OpenRouterUrl = "https://openrouter.ai/api/v1/chat/completions"; PapagoApiKey = string.Empty; PapagoApiKeyId = string.Empty;