Add LM Studio and Ollama as auto-translators

Fix #7980
This commit is contained in:
Nikolaj Olsson 2024-04-03 16:38:27 +02:00
parent 31ed11f60a
commit f4f61f20ba
4 changed files with 302 additions and 4 deletions

View File

@ -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<TranslationPair> GetSupportedSourceLanguages()
{
return ChatGptTranslate.ListLanguages();
}
public List<TranslationPair> GetSupportedTargetLanguages()
{
return ChatGptTranslate.ListLanguages();
}
public async Task<string> 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;
}
}
}

View File

@ -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<TranslationPair> GetSupportedSourceLanguages()
{
return ChatGptTranslate.ListLanguages();
}
public List<TranslationPair> GetSupportedTargetLanguages()
{
return ChatGptTranslate.ListLanguages();
}
public async Task<string> 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;
}
}
}

View File

@ -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);

View File

@ -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<string>
{
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<string>
{
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<string>
@ -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();