Skip to content

Commit 8cc7175

Browse files
authored
Merge pull request #474 from Freeesia/copilot/fix-e6886732-3984-473e-97ae-6605ea89cd20
PLaMo翻訳プラグインの実装 (Implement PLaMo Translation Plugin)
2 parents 38414e5 + e2022b5 commit 8cc7175

26 files changed

Lines changed: 3513 additions & 6 deletions

.github/workflows/dotnet-desktop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ jobs:
122122
versionSpec: "6.x"
123123
- id: gitversion
124124
uses: gittools/actions/gitversion/execute@v4.1.0
125+
- uses: Jimver/cuda-toolkit@v0.2.24
126+
with:
127+
cuda: '12.9.0'
125128
- env:
126129
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
127130
run: |

Directory.Packages.props

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project>
1+
<Project>
22
<PropertyGroup>
33
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
44
</PropertyGroup>
@@ -15,6 +15,7 @@
1515
<PackageVersion Include="Google_GenerativeAI" Version="3.3.0" />
1616
<PackageVersion Include="Kamishibai.Hosting" Version="3.1.0" />
1717
<PackageVersion Include="ksemenenko.ColorThief" Version="1.1.1.4" />
18+
<PackageVersion Include="LLamaSharp.Backend.Cuda12.Windows" Version="0.25.0" />
1819
<PackageVersion Include="MdXaml" Version="1.27.0" />
1920
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
2021
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.9" />
@@ -53,5 +54,7 @@
5354
<PackageVersion Include="xunit" Version="2.9.3" />
5455
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
5556
<PackageVersion Include="OpenAI" Version="2.5.0" />
57+
<PackageVersion Include="LLamaSharp" Version="0.25.0" />
58+
<PackageVersion Include="LLamaSharp.Backend.Cpu" Version="0.25.0" />
5659
</ItemGroup>
5760
</Project>

Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/BergamotTranslator.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Globalization;
44
using System.IO.Compression;
55
using BergamotTranslatorSharp;
6+
using Microsoft.Extensions.Logging;
67
using Microsoft.Extensions.Options;
78
using Octokit;
89
using WindowTranslator.ComponentModel;
@@ -58,11 +59,12 @@ private string[] Translate(TextInfo[] srcTexts)
5859
}
5960
}
6061

61-
public class BergamotValidator(IGitHubClient client) : ITargetSettingsValidator
62+
public class BergamotValidator(IGitHubClient client, ILogger<BergamotValidator> logger) : ITargetSettingsValidator
6263
{
6364
private const string RepoOwner = "mozilla";
6465
private const string RepoName = "firefox-translations-models";
6566
private readonly IGitHubClient client = client;
67+
private readonly ILogger<BergamotValidator> logger = logger;
6668

6769
public async ValueTask<ValidateResult> Validate(TargetSettings settings)
6870
{
@@ -162,7 +164,9 @@ private async ValueTask<List<string>> DownloadAndExtractFiles(IReadOnlyList<Repo
162164
foreach (var content in contents)
163165
{
164166
var tmpPath = Path.Combine(tmpDir, content.Name);
167+
this.logger.LogInformation("Downloading {FileName}...", content.Name);
165168
await this.client.DownloadFileAsync(RepoOwner, RepoName, content, tmpPath).ConfigureAwait(false);
169+
this.logger.LogInformation("Downloaded {FileName}", content.Name);
166170

167171
var fileName = await ExtractFileIfNeeded(tmpPath, modelDir);
168172
files.Add(fileName);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using LLama.Abstractions;
2+
using LLama.Native;
3+
4+
namespace WindowTranslator.Plugin.PLaMoPlugin;
5+
public class LLamaSharpNativeLibrarySelectingPolicy : INativeLibrarySelectingPolicy
6+
{
7+
public static LLamaSharpNativeLibrarySelectingPolicy Instance { get; } = new();
8+
9+
public IEnumerable<INativeLibrary> Apply(NativeLibraryConfig.Description description, SystemInfo systemInfo, NativeLogConfig.LLamaLogCallback? logCallback = null)
10+
{
11+
Log(description.ToString(), LLamaLogLevel.Info, logCallback);
12+
yield return new NativeLibraryWithCuda(12, description.Library, description.AvxLevel, description.SkipCheck);
13+
yield return new NativeLibraryWithAvx(description.Library, description.AvxLevel, description.SkipCheck);
14+
}
15+
16+
private static void Log(string message, LLamaLogLevel level, NativeLogConfig.LLamaLogCallback? logCallback)
17+
{
18+
if (!message.EndsWith('\n'))
19+
message += "\n";
20+
21+
logCallback?.Invoke(level, message);
22+
}
23+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using Microsoft.Extensions.Logging;
3+
using PropertyTools.DataAnnotations;
4+
using WindowTranslator.ComponentModel;
5+
using WindowTranslator.Extensions;
6+
using WindowTranslator.Modules;
7+
using WindowTranslator.Plugin.PLaMoPlugin.Properties;
8+
9+
namespace WindowTranslator.Plugin.PLaMoPlugin;
10+
11+
public class PLaMoOptions : IPluginParam
12+
{
13+
public const string ModelFileName = "plamo-2-translate-Q4_K_S.gguf";
14+
public const string ModelUrl = $"https://huggingface.co/mmnga/plamo-2-translate-gguf/resolve/main/{ModelFileName}";
15+
16+
public static string ModelPath => Path.Combine(PathUtility.UserDir, "plamo", ModelFileName);
17+
18+
[LocalizedDescription(typeof(Resources), $"{nameof(ContextSize)}_Desc")]
19+
[Range(512, 32768)]
20+
[Slidable(512, 32768, 16, 128, true, 0.1)]
21+
public int ContextSize { get; set; } = 2048;
22+
23+
[Range(-1, 6)]
24+
[Spinnable(Minimum = -1, Maximum = 6)]
25+
[LocalizedDescription(typeof(Resources), $"{nameof(VRAM)}_Desc")]
26+
public int VRAM { get; set; } = -1;
27+
}
28+
29+
public class PLaMoValidator(ILogger<PLaMoValidator> logger) : ITargetSettingsValidator
30+
{
31+
private readonly ILogger<PLaMoValidator> logger = logger;
32+
33+
public async ValueTask<ValidateResult> Validate(TargetSettings settings)
34+
{
35+
// 翻訳モジュールで利用しない場合は無条件で有効
36+
if (settings.SelectedPlugins[nameof(ITranslateModule)] != nameof(PLaMoTranslator))
37+
{
38+
return ValidateResult.Valid;
39+
}
40+
41+
try
42+
{
43+
await DownloadModelIfNotExists().ConfigureAwait(false);
44+
return ValidateResult.Valid;
45+
}
46+
catch (Exception ex)
47+
{
48+
return ValidateResult.Invalid("PLaMo", string.Format(Resources.DownloadFailed, ex.Message));
49+
}
50+
}
51+
52+
private async ValueTask DownloadModelIfNotExists()
53+
{
54+
var modelPath = PLaMoOptions.ModelPath;
55+
// すでにモデルファイルが存在する場合は処理をスキップ
56+
if (File.Exists(modelPath))
57+
return;
58+
59+
var modelDir = Path.GetDirectoryName(PLaMoOptions.ModelPath)!;
60+
Directory.CreateDirectory(modelDir);
61+
62+
// モデルファイルをダウンロード
63+
using var httpClient = new HttpClient();
64+
this.logger.LogInformation("Downloading PLaMo model...");
65+
await httpClient.DownloadFile(PLaMoOptions.ModelUrl, modelPath, p => this.logger.LogInformation($"Downloading PLaMo model: {p:P2}")).ConfigureAwait(false);
66+
}
67+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System.Globalization;
2+
using System.Text;
3+
using LLama;
4+
using LLama.Common;
5+
using LLama.Native;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.Extensions.Options;
8+
using WindowTranslator.Modules;
9+
using WindowTranslator.Plugin.PLaMoPlugin.Properties;
10+
11+
namespace WindowTranslator.Plugin.PLaMoPlugin;
12+
13+
public sealed class PLaMoTranslator : ITranslateModule, IDisposable
14+
{
15+
private readonly string sourceLang;
16+
private readonly string targetLang;
17+
private readonly LLamaWeights? weights;
18+
private readonly ModelParams? modelParams;
19+
private readonly ILogger<PLaMoTranslator> logger;
20+
private readonly InferenceParams inferenceParams;
21+
22+
static PLaMoTranslator()
23+
=> NativeLibraryConfig.LLama.WithSelectingPolicy(LLamaSharpNativeLibrarySelectingPolicy.Instance);
24+
25+
public PLaMoTranslator(IOptionsSnapshot<PLaMoOptions> plamoOptions, IOptionsSnapshot<LanguageOptions> langOptions, ILogger<PLaMoTranslator> logger)
26+
{
27+
var options = plamoOptions.Value;
28+
29+
// PLaMoモデル用の言語名を取得
30+
this.sourceLang = GetLanguageName(langOptions.Value.Source);
31+
this.targetLang = GetLanguageName(langOptions.Value.Target);
32+
33+
// ダウンロードされたモデルのパスを取得
34+
var modelPath = PLaMoOptions.ModelPath;
35+
36+
if (!File.Exists(modelPath))
37+
{
38+
throw new AppUserException(Resources.ModelFileNotFound);
39+
}
40+
41+
if (!NativeLibraryConfig.LLama.LibraryHasLoaded)
42+
{
43+
NativeLibraryConfig.LLama.WithLogCallback(logger);
44+
}
45+
46+
this.modelParams = new ModelParams(modelPath)
47+
{
48+
ContextSize = (uint)options.ContextSize,
49+
GpuLayerCount = CalcGpuLayersFromGB(options.VRAM, options.ContextSize),
50+
};
51+
this.inferenceParams = new InferenceParams
52+
{
53+
MaxTokens = options.ContextSize / 2,
54+
AntiPrompts = ["<|plamo:op|>"],
55+
};
56+
57+
this.weights = LLamaWeights.LoadFromFile(this.modelParams);
58+
this.logger = logger;
59+
}
60+
61+
private static string GetLanguageName(string cultureCode)
62+
{
63+
// PLaMoモデルが認識する言語名に変換
64+
var culture = CultureInfo.GetCultureInfo(cultureCode);
65+
return culture.TwoLetterISOLanguageName switch
66+
{
67+
"ja" => "Japanese",
68+
"en" => "English",
69+
"zh" => "Chinese",
70+
"ko" => "Korean",
71+
"de" => "German",
72+
"fr" => "French",
73+
"es" => "Spanish",
74+
"it" => "Italian",
75+
"pt" => "Portuguese",
76+
"ru" => "Russian",
77+
"ar" => "Arabic",
78+
"vi" => "Vietnamese",
79+
_ => culture.EnglishName.Split(' ')[0] // フォールバック: 英語名の最初の単語
80+
};
81+
}
82+
83+
static int CalcGpuLayersFromGB(int vramGB, int ctx, double L = 0.17, double W = 1.30)
84+
{
85+
if (vramGB < 0) return -1;
86+
if (vramGB == 0) return 0;
87+
var kvGB = Math.Ceiling(40.0 * ctx / 1024.0) / 1024.0; // 40KB/token, safe
88+
var n = (int)Math.Floor((vramGB - W - kvGB) / L);
89+
if (n < 0) return 0;
90+
if (n > 32) return -1;
91+
return n;
92+
}
93+
94+
public async ValueTask<string[]> TranslateAsync(TextInfo[] srcTexts)
95+
{
96+
if (this.weights is null || this.modelParams is null)
97+
{
98+
throw new InvalidOperationException(Resources.ModelNotInitialized);
99+
}
100+
101+
102+
103+
using var context = this.weights.CreateContext(this.modelParams, this.logger);
104+
var executor = new StatelessExecutor(this.weights, this.modelParams);
105+
var responseBuilder = new StringBuilder();
106+
var responses = new List<string>();
107+
108+
foreach (var text in srcTexts)
109+
{
110+
// PLaMo専用のプロンプトフォーマット
111+
var prompt = $"""
112+
<|plamo:op|>dataset
113+
translation
114+
<|plamo:op|>input lang={this.sourceLang}
115+
{text.SourceText}
116+
<|plamo:op|>output lang={this.targetLang}
117+
118+
""".ReplaceLineEndings("\n");
119+
responseBuilder.Clear();
120+
await foreach (var token in executor.InferAsync(prompt, this.inferenceParams))
121+
{
122+
responseBuilder.Append(token);
123+
}
124+
var response = responseBuilder.ToString().Trim();
125+
responses.Add(response);
126+
this.logger.LogDebug("PLaMo translated: {Original} => {Translated}", text.SourceText, response);
127+
}
128+
129+
return [.. responses];
130+
}
131+
132+
// PLaMoモデルは用語集をサポートしないため、何もしない
133+
public ValueTask RegisterGlossaryAsync(IReadOnlyDictionary<string, string> glossary)
134+
=> default;
135+
136+
// PLaMoモデルはコンテキストをサポートしないため、何もしない
137+
public void RegisterContext(string context)
138+
{
139+
}
140+
141+
public void Dispose()
142+
{
143+
this.weights?.Dispose();
144+
}
145+
}

0 commit comments

Comments
 (0)