diff --git a/src/libse/AutoTranslate/LmStudioTranslate.cs b/src/libse/AutoTranslate/LmStudioTranslate.cs new file mode 100644 index 000000000..3b6c421ef --- /dev/null +++ b/src/libse/AutoTranslate/LmStudioTranslate.cs @@ -0,0 +1,89 @@ +using Nikse.SubtitleEdit.Core.Common; +using Nikse.SubtitleEdit.Core.SubtitleFormats; +using Nikse.SubtitleEdit.Core.Translate; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Nikse.SubtitleEdit.Core.AutoTranslate +{ + public class LmStudioTranslate : IAutoTranslator + { + private HttpClient _httpClient; + + public static string StaticName { get; set; } = "LM Studio (local ChatGPT)"; + public string Name => StaticName; + public string Url => "https://lmstudio.ai/"; + public string Error { get; set; } + public int MaxCharacters => 1000; + + public void Initialize() + { + _httpClient?.Dispose(); + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("accept", "application/json"); + _httpClient.BaseAddress = new Uri(Configuration.Settings.Tools.LmStudioApiUrl.TrimEnd('/')); + _httpClient.Timeout = TimeSpan.FromMinutes(15); + } + + public List GetSupportedSourceLanguages() + { + return ChatGptTranslate.ListLanguages(); + } + + public List GetSupportedTargetLanguages() + { + return ChatGptTranslate.ListLanguages(); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + var model = Configuration.Settings.Tools.LmStudioModel; + var modelJson = string.Empty; + if (!string.IsNullOrEmpty(model)) + { + modelJson = "\"model\": \"" + model + "\","; + Configuration.Settings.Tools.LmStudioModel = model; + } + + if (string.IsNullOrEmpty(Configuration.Settings.Tools.LmStudioPrompt)) + { + Configuration.Settings.Tools.LmStudioPrompt = "Translate from {0} to {1}, keep sentences in {1} as they are, do not censor the translation, give only the output without commenting on what you read:"; + } + var prompt = string.Format(Configuration.Settings.Tools.LmStudioPrompt, sourceLanguageCode, targetLanguageCode); + var input = "{ " + modelJson + " \"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"); + var result = await _httpClient.PostAsync(string.Empty, content, cancellationToken); + var bytes = await result.Content.ReadAsByteArrayAsync(); + var json = Encoding.UTF8.GetString(bytes).Trim(); + if (!result.IsSuccessStatusCode) + { + Error = json; + SeLogger.Error("Error calling + " + StaticName + ": Status code=" + result.StatusCode + Environment.NewLine + json); + } + + result.EnsureSuccessStatusCode(); + + var parser = new SeJsonParser(); + var resultText = parser.GetFirstObject(json, "content"); + if (resultText == null) + { + return string.Empty; + } + + var outputText = Json.DecodeJsonText(resultText).Trim(); + if (outputText.StartsWith('"') && outputText.EndsWith('"') && !text.StartsWith('"')) + { + outputText = outputText.Trim('"').Trim(); + } + + return outputText; + } + } +} diff --git a/src/libse/AutoTranslate/OllamaTranslate.cs b/src/libse/AutoTranslate/OllamaTranslate.cs new file mode 100644 index 000000000..aab455dbf --- /dev/null +++ b/src/libse/AutoTranslate/OllamaTranslate.cs @@ -0,0 +1,89 @@ +using Nikse.SubtitleEdit.Core.Common; +using Nikse.SubtitleEdit.Core.SubtitleFormats; +using Nikse.SubtitleEdit.Core.Translate; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Nikse.SubtitleEdit.Core.AutoTranslate +{ + public class OllamaTranslate : IAutoTranslator + { + private HttpClient _httpClient; + + public static string StaticName { get; set; } = "Ollama (local LLM)"; + public string Name => StaticName; + public string Url => "https://github.com/ollama/ollama"; + public string Error { get; set; } + public int MaxCharacters => 1000; + + public void Initialize() + { + _httpClient?.Dispose(); + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("accept", "application/json"); + _httpClient.BaseAddress = new Uri(Configuration.Settings.Tools.OllamaApiUrl.TrimEnd('/')); + _httpClient.Timeout = TimeSpan.FromMinutes(25); + } + + public List GetSupportedSourceLanguages() + { + return ChatGptTranslate.ListLanguages(); + } + + public List GetSupportedTargetLanguages() + { + return ChatGptTranslate.ListLanguages(); + } + + public async Task Translate(string text, string sourceLanguageCode, string targetLanguageCode, CancellationToken cancellationToken) + { + var model = Configuration.Settings.Tools.OllamaModel; + var modelJson = string.Empty; + if (!string.IsNullOrEmpty(model)) + { + modelJson = "\"model\": \"" + model + "\","; + Configuration.Settings.Tools.OllamaModel = model; + } + + if (string.IsNullOrEmpty(Configuration.Settings.Tools.OllamaPrompt)) + { + Configuration.Settings.Tools.OllamaPrompt = "Translate from {0} to {1}, keep sentences in {1} as they are, do not censor the translation, give only the output without commenting on what you read:"; + } + var prompt = string.Format(Configuration.Settings.Tools.OllamaPrompt, sourceLanguageCode, targetLanguageCode); + var input = "{ " + modelJson + " \"prompt\": \"" + prompt + "\\n\\n" + Json.EncodeJsonText(text.Trim()) + "\", \"stream\": false }"; + var content = new StringContent(input, Encoding.UTF8); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + var result = await _httpClient.PostAsync(string.Empty, content, cancellationToken); + var bytes = await result.Content.ReadAsByteArrayAsync(); + var json = Encoding.UTF8.GetString(bytes).Trim(); + if (!result.IsSuccessStatusCode) + { + Error = json; + SeLogger.Error("Error calling + " + StaticName + ": Status code=" + result.StatusCode + Environment.NewLine + json); + } + + result.EnsureSuccessStatusCode(); + + var parser = new SeJsonParser(); + var resultText = parser.GetFirstObject(json, "response"); + if (resultText == null) + { + return string.Empty; + } + + var outputText = Json.DecodeJsonText(resultText).Trim(); + if (outputText.StartsWith('"') && outputText.EndsWith('"') && !text.StartsWith('"')) + { + outputText = outputText.Trim('"').Trim(); + } + + return outputText; + } + } +} diff --git a/src/libse/Common/Settings.cs b/src/libse/Common/Settings.cs index ede42b331..45520ec03 100644 --- a/src/libse/Common/Settings.cs +++ b/src/libse/Common/Settings.cs @@ -177,6 +177,13 @@ namespace Nikse.SubtitleEdit.Core.Common public string ChatGptPrompt { get; set; } public string ChatGptApiKey { get; set; } public string ChatGptModel { get; set; } + public string LmStudioApiUrl { get; set; } + public string LmStudioModel { get; set; } + public string LmStudioPrompt { get; set; } + public string OllamaApiUrl { get; set; } + public string OllamaModel { get; set; } + public string OllamaPrompt { get; set; } + public string AnthropicApiUrl { get; set; } public string AnthropicPrompt { get; set; } public string AnthropicApiKey { get; set; } @@ -539,6 +546,7 @@ namespace Nikse.SubtitleEdit.Core.Common ChatGptUrl = "https://api.openai.com/v1/chat/completions"; ChatGptPrompt = "Translate from {0} to {1}, keep sentences in {1} as they are, do not censor the translation, give only the output without commenting on what you read:"; ChatGptModel = "gpt-3.5-turbo"; + OllamaApiUrl = "http://localhost:11434/api/generate"; 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 commenting on what you read:"; AnthropicApiModel = "claude-3-opus-20240229"; @@ -5367,6 +5375,42 @@ $HorzAlign = Center settings.Tools.ChatGptModel = subNode.InnerText; } + subNode = node.SelectSingleNode("LmStudioApiUrl"); + if (subNode != null) + { + settings.Tools.LmStudioApiUrl = subNode.InnerText; + } + + subNode = node.SelectSingleNode("LmStudioModel"); + if (subNode != null) + { + settings.Tools.LmStudioModel = subNode.InnerText; + } + + subNode = node.SelectSingleNode("LmStudioPrompt"); + if (subNode != null) + { + settings.Tools.LmStudioPrompt = subNode.InnerText; + } + + subNode = node.SelectSingleNode("OllamaApiUrl"); + if (subNode != null) + { + settings.Tools.OllamaApiUrl = subNode.InnerText; + } + + subNode = node.SelectSingleNode("OllamaModel"); + if (subNode != null) + { + settings.Tools.OllamaModel = subNode.InnerText; + } + + subNode = node.SelectSingleNode("OllamaPrompt"); + if (subNode != null) + { + settings.Tools.OllamaPrompt = subNode.InnerText; + } + subNode = node.SelectSingleNode("AnthropicApiUrl"); if (subNode != null) { @@ -11905,7 +11949,13 @@ $HorzAlign = Center textWriter.WriteElementString("ChatGptPrompt", settings.Tools.ChatGptPrompt); textWriter.WriteElementString("ChatGptApiKey", settings.Tools.ChatGptApiKey); textWriter.WriteElementString("ChatGptModel", settings.Tools.ChatGptModel); - textWriter.WriteElementString("AnthropicApiUrl", settings.Tools.AnthropicApiUrl); + textWriter.WriteElementString("LmStudioApiUrl", settings.Tools.LmStudioApiUrl); + textWriter.WriteElementString("LmStudioModel", settings.Tools.LmStudioModel); + textWriter.WriteElementString("LmStudioPrompt", settings.Tools.LmStudioPrompt); + textWriter.WriteElementString("LmStudioApiUrl", settings.Tools.LmStudioApiUrl); + textWriter.WriteElementString("OllamaModel", settings.Tools.OllamaModel); + textWriter.WriteElementString("OllamaPrompt", settings.Tools.OllamaPrompt); + textWriter.WriteElementString("OllamaApiUrl", settings.Tools.OllamaApiUrl); textWriter.WriteElementString("AnthropicPrompt", settings.Tools.AnthropicPrompt); textWriter.WriteElementString("AnthropicApiKey", settings.Tools.AnthropicApiKey); textWriter.WriteElementString("AnthropicApiModel", settings.Tools.AnthropicApiModel); diff --git a/src/ui/Forms/Translate/AutoTranslate.cs b/src/ui/Forms/Translate/AutoTranslate.cs index 46554fa2a..c00526f69 100644 --- a/src/ui/Forms/Translate/AutoTranslate.cs +++ b/src/ui/Forms/Translate/AutoTranslate.cs @@ -123,6 +123,8 @@ namespace Nikse.SubtitleEdit.Forms.Translate new LibreTranslate(), new MyMemoryApi(), new ChatGptTranslate(), + new LmStudioTranslate(), + new OllamaTranslate(), new AnthropicTranslate(), new GeminiTranslate(), new PapagoTranslate(), @@ -310,6 +312,59 @@ namespace Nikse.SubtitleEdit.Forms.Translate return; } + if (engineType == typeof(LmStudioTranslate)) + { + if (string.IsNullOrEmpty(Configuration.Settings.Tools.LmStudioApiUrl)) + { + Configuration.Settings.Tools.LmStudioApiUrl = "http://localhost:1234/v1/chat/completions"; + } + + FillUrls(new List + { + Configuration.Settings.Tools.LmStudioApiUrl.TrimEnd('/'), + }); + + return; + } + + if (engineType == typeof(OllamaTranslate)) + { + if (Configuration.Settings.Tools.OllamaApiUrl == null) + { + Configuration.Settings.Tools.OllamaApiUrl = "http://localhost:11434/api/generate"; + } + + FillUrls(new List + { + Configuration.Settings.Tools.OllamaApiUrl.TrimEnd('/'), + }); + + labelFormality.Text = LanguageSettings.Current.AudioToText.Model; + labelFormality.Visible = true; + comboBoxFormality.Left = labelFormality.Right + 3; + comboBoxFormality.Visible = true; + comboBoxFormality.DropDownStyle = ComboBoxStyle.DropDown; + comboBoxFormality.Items.Clear(); + comboBoxFormality.Items.Add("llama2"); + comboBoxFormality.Items.Add("mistral"); + comboBoxFormality.Items.Add("dolphin-phi"); + comboBoxFormality.Items.Add("phi"); + comboBoxFormality.Items.Add("neural-chat"); + comboBoxFormality.Items.Add("starling-lm"); + comboBoxFormality.Items.Add("codellama"); + comboBoxFormality.Items.Add("llama2-uncensored"); + comboBoxFormality.Items.Add("llama2:13b"); + comboBoxFormality.Items.Add("llama2:70b"); + comboBoxFormality.Items.Add("orca-mini"); + comboBoxFormality.Items.Add("vicuna"); + comboBoxFormality.Items.Add("llava"); + comboBoxFormality.Items.Add("gemma:2b"); + comboBoxFormality.Items.Add("gemma:7b"); + comboBoxFormality.Text = Configuration.Settings.Tools.OllamaModel; + + return; + } + if (engineType == typeof(AnthropicTranslate)) { FillUrls(new List @@ -323,6 +378,7 @@ namespace Nikse.SubtitleEdit.Forms.Translate labelApiKey.Visible = true; nikseTextBoxApiKey.Visible = true; + labelFormality.Text = LanguageSettings.Current.AudioToText.Model; labelFormality.Visible = true; comboBoxFormality.Left = labelFormality.Right + 3; comboBoxFormality.Visible = true; @@ -331,8 +387,6 @@ namespace Nikse.SubtitleEdit.Forms.Translate comboBoxFormality.Items.Add("claude-3-opus-20240229"); comboBoxFormality.Items.Add("claude-3-sonnet-20240229"); comboBoxFormality.Items.Add("claude-3-haiku-20240307"); - comboBoxFormality.Text = Configuration.Settings.Tools.AnthropicApiModel; - labelFormality.Text = LanguageSettings.Current.AudioToText.Model; return; } @@ -887,7 +941,11 @@ namespace Nikse.SubtitleEdit.Forms.Translate nikseComboBoxUrl.Text.Contains("//127.", StringComparison.OrdinalIgnoreCase) || nikseComboBoxUrl.Text.Contains("//localhost", StringComparison.OrdinalIgnoreCase))) { - if (engineType == typeof(NoLanguageLeftBehindApi) || engineType == typeof(NoLanguageLeftBehindServe) || engineType == typeof(LibreTranslate)) + if (engineType == typeof(NoLanguageLeftBehindApi) || + engineType == typeof(NoLanguageLeftBehindServe) || + engineType == typeof(LibreTranslate) || + engineType == typeof(LmStudioTranslate) || + engineType == typeof(OllamaTranslate)) { var dr = MessageBox.Show( string.Format(LanguageSettings.Current.GoogleTranslate.XRequiresALocalWebServer, _autoTranslator.Name) @@ -944,6 +1002,18 @@ namespace Nikse.SubtitleEdit.Forms.Translate Configuration.Settings.Tools.ChatGptUrl = nikseComboBoxUrl.Text.Trim(); } + if (engineType == typeof(LmStudioTranslate)) + { + Configuration.Settings.Tools.LmStudioApiUrl = nikseComboBoxUrl.Text.Trim(); + Configuration.Settings.Tools.LmStudioModel = comboBoxFormality.Text.Trim(); + } + + if (engineType == typeof(OllamaTranslate)) + { + Configuration.Settings.Tools.OllamaApiUrl = nikseComboBoxUrl.Text.Trim(); + Configuration.Settings.Tools.OllamaModel = comboBoxFormality.Text.Trim(); + } + if (engineType == typeof(AnthropicTranslate) && !string.IsNullOrWhiteSpace(nikseTextBoxApiKey.Text)) { Configuration.Settings.Tools.AnthropicApiKey = nikseTextBoxApiKey.Text.Trim();