Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Current supported plugins include:
- Probably the most popular LLM that has the highest quality but is not Free
- [Ollama Models](https://ollama.com/)
- Ollama is a local hosting option for LLMs. You are able to run one or more llms on your local machine of varying size. This option is free but will require you to engineer your prompts dependant on the model and/or language.
- [LM Studio](https://lmstudio.ai/)
- LM Studio is a local hosting option for OpenAI-compatible chat completion APIs. Start the local server in LM Studio and use the model id shown by LM Studio in `LmStudio.yaml`.

# Why use this instead of the [Custom] endpoint?

Expand All @@ -33,6 +35,7 @@ To configure your LLM you will need to follow the following steps:
2. Open the config for the LLMTranslator you wish to use
- If OpenaI: `OpenAi.Yml`
- If a local Olama LLM: `Ollama.Yml`
- If a local LM Studio LLM: `LmStudio.Yml`
3. Update your config with any API keys, custom urls, glossaries and system prompts.
4. Finally update your AutoTranslator INI file with your translate service
- ```
Expand All @@ -42,6 +45,32 @@ To configure your LLM you will need to follow the following steps:
```
- If OpenAi: `OpenAiTranslate`
- If a local Olama LLM: `OllamaTranslate`
- If a local LM Studio LLM: `LmStudioTranslate`

## LM Studio

To use LM Studio, start the local server in LM Studio and load the model you want to use for translation. The default LM Studio OpenAI-compatible chat completion URL is:

```yaml
url: "http://localhost:1234/v1/chat/completions"
```

Copy `LmStudio.yaml` into your AutoTranslator config folder, then update the `model` value to the model identifier shown by LM Studio:

```yaml
apiKey: "None"
apiKeyRequired: false
url: "http://localhost:1234/v1/chat/completions"
model: "model-identifier"
```

Finally, set the translator endpoint in `Config.ini`:

```ini
[Service]
Endpoint=LmStudioTranslate
FallbackEndpoint=
```

## Global API Key

Expand All @@ -55,11 +84,11 @@ We also use global environment variables so you can just set your API Key once a
We have seperate files that can be override any config you have loaded in your config file. This makes it easier to publish game specific prompts, glossaries or just make it easier to use multi line prompts without having to worry about YAML formatting.

These files are:
- `OpenAi-SystemPrompt.txt` or `Ollama-SystemPrompt.txt`
- `OpenAi-SystemPrompt.txt`, `Ollama-SystemPrompt.txt` or `LmStudio-SystemPrompt.txt`
- Use this file to update your system prompt
- `OpenAi-GlossaryPrompt.txt` or `Ollama-GlossaryPrompt.txt`
- `OpenAi-GlossaryPrompt.txt`, `Ollama-GlossaryPrompt.txt` or `LmStudio-GlossaryPrompt.txt`
- Use this file to update your glossary prompt
- `OpenAi-ApiKey.txt` or `Ollama-ApiKey.txt`
- `OpenAi-ApiKey.txt`, `Ollama-ApiKey.txt` or `LmStudio-ApiKey.txt`
- Use this file to update your API Key

# Glossary
Expand Down Expand Up @@ -98,4 +127,4 @@ A test project is included with the project. The [PromptTests](./XUnity.AutoTran

# Packages

The assemblies included are the Dev versions of XUnity.AutoTranslator. Feel free to star/fork this repo however you like.
The assemblies included are the Dev versions of XUnity.AutoTranslator. Feel free to star/fork this repo however you like.
32 changes: 32 additions & 0 deletions XUnity.AutoTranslator.LlmTranslators.Tests/ConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,36 @@ public void TestDefaultConfig()

Assert.True(config.SystemPrompt!.Split("\n").Length > 1);
}

[Fact]
public void TestLmStudioConfig()
{
var config = Configuration.GetConfiguration($"{sampleDirectory}/LmStudio.yaml");

Assert.Equal("http://localhost:1234/v1/chat/completions", config.Url);
Assert.Equal("model-identifier", config.Model);
Assert.False(config.ApiKeyRequired);
Assert.True(config.SystemPrompt!.Split("\n").Length > 1);
}

[Fact]
public void TestGlossaryPromptOverride()
{
var config = new LlmConfig { SystemPrompt = "System prompt", GlossaryPrompt = "Glossary prompt" };
var file = Path.GetTempFileName();

try
{
File.WriteAllText(file, "Override glossary prompt");

Configuration.LoadGlossaryPrompt(config, file);

Assert.Equal("System prompt", config.SystemPrompt);
Assert.Equal("Override glossary prompt", config.GlossaryPrompt);
}
finally
{
File.Delete(file);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public static void LoadGlossaryPrompt(LlmConfig config, string file)
if (!File.Exists(file))
return;

config.SystemPrompt = File.ReadAllText(file, Encoding.UTF8);
config.GlossaryPrompt = File.ReadAllText(file, Encoding.UTF8);
}

public static void LoadApiKey(LlmConfig config, string file)
Expand Down
16 changes: 16 additions & 0 deletions XUnity.AutoTranslator.LlmTranslators/ILRepack.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="ILRepacker" AfterTargets="Build" Condition="$(Configuration) == 'Release'">
<ItemGroup>
<InputAssemblies Include="$(TargetPath)" />
<InputAssemblies Include="$(TargetDir)YamlDotNet.dll" />
</ItemGroup>

<Message Text="MERGING: YamlDotNet into $(TargetFileName)" Importance="High" />

<ILRepack
Internalize="true"
InputAssemblies="@(InputAssemblies)"
OutputFile="$(TargetPath)"
TargetKind="Dll" />
</Target>
</Project>
59 changes: 59 additions & 0 deletions XUnity.AutoTranslator.LlmTranslators/LmStudioTranslatorEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using SimpleJSON;
using System.Net;
using XUnity.AutoTranslator.LlmTranslators.Behavior;
using XUnity.AutoTranslator.LlmTranslators.Config;
using XUnity.AutoTranslator.Plugin.Core.Endpoints;
using XUnity.AutoTranslator.Plugin.Core.Endpoints.Http;
using XUnity.AutoTranslator.Plugin.Core.Web;

public class LmStudioTranslatorEndpoint : HttpEndpoint
{
public override string Id => "LmStudioTranslate";
public override string FriendlyName => "LM Studio Translate";
public override int MaxTranslationsPerRequest => 1;

// Local models can saturate quickly; keep this closer to Ollama's default.
public override int MaxConcurrency => 5;

private LlmConfig _config = new();

public override void Initialize(IInitializationContext context)
{
string folder = Configuration.CalculateConfigFolder();
var file = Path.Combine(folder, "LmStudio.yaml");
_config = Configuration.GetConfiguration(file);
Configuration.LoadGlossary(_config, "LmStudio-Glossary.yaml");

// Remove artificial delays
context.SetTranslationDelay(0.1f);
context.DisableSpamChecks();

if (string.IsNullOrEmpty(_config.ApiKey) && _config.ApiKeyRequired)
throw new Exception("The endpoint requires an API key which has not been provided.");
}

public override void OnCreateRequest(IHttpRequestCreationContext context)
{
var requestData = BaseEndpointBehavior.GetRequestData(_config, context.UntranslatedText);

var request = new XUnityWebRequest("POST", _config.Url, requestData);
request.Headers[HttpRequestHeader.ContentType] = "application/json";

if (_config.ApiKeyRequired)
request.Headers[HttpRequestHeader.Authorization] = $"Bearer {_config.ApiKey}";

context.Complete(request);
}

public override void OnExtractTranslation(IHttpTranslationExtractionContext context)
{
var data = context.Response.Data;

var jsonResponse = JSON.Parse(data);
var result = jsonResponse["choices"]?[0]?["message"]?["content"]?.ToString() ?? string.Empty;
result = BaseEndpointBehavior.ValidateAndCleanupTranslation(context.UntranslatedText, result, _config);

if (MaxTranslationsPerRequest == 1)
context.Complete(result);
}
}
58 changes: 58 additions & 0 deletions XUnity.AutoTranslator.LlmTranslators/SampleConfig/LmStudio.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
apiKey: "None"
apiKeyRequired: false
url: "http://localhost:1234/v1/chat/completions"
model: "model-identifier"
modelParams:
temperature: 0.2
max_tokens: 4096
top_p: 0.9
top_k: 40
frequency_penalty: 0
presence_penalty: 0
systemPrompt: |
Your task is to accurately translate Chinese strings provided into fluent English, maintaining the original tone and meaning without adding explanations or embellishments.

#### Guidelines:

**Output**
- Do not include additional HTML or markdown.

**Gender-Neutral Language:**
- Default to gender-neutral language and pronouns ("they/them"), unless specified by the text.

**Character References:**
- Refer to characters by their names or roles, avoiding gendered descriptors.
- Use gender-specific terms only if clearly identified in the text or necessary for clarity.

**Names:**
- Use Pinyin for Chinese names.

**Nicknames:**
- Translate into culturally appropriate English equivalents, maintaining meaning and sentiment.

**Title Conversion:**
- Convert titles such as "Doctor," "Lord," or "Master" to their English equivalents.
- Precede names with these titles (e.g., "Brother Li").
- Use glossary terms for titles as provided, with flexible capitalization but no changes to wording based on context
- Preserve the intended meaning and connotations of each title.

**Cultural Sensitivity:**
- Incorporate relevant Wuxia or traditional terms as needed.

**Idioms:**
- Convey the meaning, cultural context, and implications in English.
- Avoid direct transliteration; use literal translations within the text to maintain intended message and tone.

**Contextual Adaptations:**
- Resolve ambiguities using broader context for clarity.
- Select translations fitting the context and guidelines.

**Language Refinement:**
- Ensure clarity and accuracy with minimal grammatical changes.

**Capitalization:**
- Follow standard English grammar rules.
glossaryPrompt: |
#Glossary for Consistent Translations
Prioritise and use the translation when an exact match with the original text is found.
## Terms
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<Xyzj2GameDir>G:\SteamLibrary\steamapps\common\下一站江湖Ⅱ\下一站江湖Ⅱ\下一站江湖Ⅱ_Data\Managed\Translators</Xyzj2GameDir>
<LoMGameDir>C:\Program Files (x86)\Steam\steamapps\common\LegendOfMortal\Mortal_Data\Managed\Translators</LoMGameDir>
<BaseFolder>\..\..\..\</BaseFolder>
<DeployToGameDirs>false</DeployToGameDirs>
<Configurations>Debug;Release</Configurations>
<UserSecretsId>f11951a3-bacd-43f2-a9fa-0187cf947674</UserSecretsId>
</PropertyGroup>
Expand All @@ -30,31 +31,28 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="ILRepack.MSBuild.Task" Version="2.0.13" PrivateAssets="All" />
<PackageReference Include="ILRepack.Lib.MSBuild.Task" Version="2.0.45" PrivateAssets="All" />
</ItemGroup>

<Target Name="ILRepack" AfterTargets="Build" Condition="$(Configuration) == 'Release'">
<Target Name="PostBuild" AfterTargets="ILRepacker" Condition="$(Configuration) == 'Release'">
<PropertyGroup>
<WorkingDirectory>$(MSBuildThisFileDirectory)bin\$(Configuration)\$(TargetFramework)</WorkingDirectory>
<ReleaseFolder>$(MSBuildThisFileDirectory)..\Release\</ReleaseFolder>
<ReleaseTranslatorsFolder>$(ReleaseFolder)Translators\</ReleaseTranslatorsFolder>
<ReleaseSampleConfigFolder>$(ReleaseFolder)SampleConfig\</ReleaseSampleConfigFolder>
</PropertyGroup>

<ItemGroup>
<InputAssemblies Include="YamlDotNet.dll" />
<SampleConfigFiles Include="$(MSBuildThisFileDirectory)SampleConfig\**\*" />
</ItemGroup>

<!-- Log message for merging assemblies -->
<Message Text="MERGING: @(InputAssemblies->'%(Filename)') into $(OutputAssembly)" Importance="High" />

<!-- ILRepack task to merge assemblies -->
<ILRepack OutputType="$(OutputType)" MainAssembly="$(AssemblyName).dll" OutputAssembly="$(AssemblyName).dll" InputAssemblies="@(InputAssemblies)" InternalizeExcludeAssemblies="@(InternalizeExcludeAssemblies)" WorkingDirectory="$(WorkingDirectory)" />
</Target>

<Target Name="PostBuild" AfterTargets="ILRepack" Condition="$(Configuration) == 'Release'">
<Exec Command="XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(PoKGameDir)&quot;" />
<Exec Command="XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(Xyzj2GameDir)&quot;" />
<Exec Command="XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(LoMGameDir)&quot;" />
<MakeDir Directories="$(ReleaseTranslatorsFolder);$(ReleaseSampleConfigFolder)" />
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(ReleaseTranslatorsFolder)" />
<Copy SourceFiles="@(SampleConfigFiles)" DestinationFiles="@(SampleConfigFiles->'$(ReleaseSampleConfigFolder)%(RecursiveDir)%(Filename)%(Extension)')" />

<Exec Command="XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(TargetDir)$(BaseFolder)\Release&quot;" />
<Exec Command="XCOPY /Y /I &quot;$(TargetDir)$(BaseFolder)\SampleConfig&quot; &quot;$(TargetDir)$(BaseFolder)\Release&quot;" />
<MakeDir Directories="$(PoKGameDir);$(Xyzj2GameDir);$(LoMGameDir)" Condition="'$(DeployToGameDirs)' == 'true'" />
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PoKGameDir)" Condition="'$(DeployToGameDirs)' == 'true'" />
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(Xyzj2GameDir)" Condition="'$(DeployToGameDirs)' == 'true'" />
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(LoMGameDir)" Condition="'$(DeployToGameDirs)' == 'true'" />
</Target>

</Project>