Skip to content

Commit 553708c

Browse files
authored
Merge pull request #584 from Freeesia/copilot/add-translation-module-plugin
翻訳モジュールとしてGitHub Copilotの対応
2 parents 26f1529 + 7287511 commit 553708c

31 files changed

Lines changed: 1374 additions & 5 deletions

ColorThief/ColorThief/ColorThief.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ unsafe private static void GetPixelsFast(SoftwareBitmap bmp, Rectangle rect, int
8484
var numUsedPixels = 0;
8585

8686
// 元画像の範囲を超えないようにクリップ
87-
var top = Math.Clamp(rect.Top, 0, bmp.PixelWidth);
87+
var top = Math.Clamp(rect.Top, 0, bmp.PixelHeight);
8888
var bottom = Math.Clamp(rect.Bottom, 0, bmp.PixelHeight);
8989
var left = Math.Clamp(rect.Left, 0, bmp.PixelWidth);
9090
var width = Math.Clamp(rect.Right - left, 0, bmp.PixelWidth - left);

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<PackageVersion Include="CsvHelper" Version="33.1.0" />
1212
<PackageVersion Include="DeepL.net" Version="1.15.0" />
1313
<PackageVersion Include="Emoji.Wpf" Version="0.3.4" />
14+
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.2.0" />
1415
<PackageVersion Include="Google.Apis.Auth" Version="1.73.0" />
1516
<PackageVersion Include="Google.Apis.Script.v1" Version="1.68.0.3294" />
1617
<PackageVersion Include="Google.Cloud.Storage.V1" Version="4.14.0" />
@@ -22,7 +23,7 @@
2223
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.102" />
2324
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
2425
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
25-
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
26+
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
2627
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" />
2728
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
2829
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using PropertyTools.DataAnnotations;
3+
using WindowTranslator.ComponentModel;
4+
using WindowTranslator.Plugin.GitHubCopilotPlugin.Properties;
5+
6+
namespace WindowTranslator.Plugin.GitHubCopilotPlugin;
7+
8+
public class GitHubCopilotOptions : IPluginParam
9+
{
10+
[LocalizedDescription(typeof(Resources), $"{nameof(Model)}_Desc")]
11+
public string Model { get; set; } = "gpt-5-mini";
12+
13+
[Height(120)]
14+
[DataType(DataType.MultilineText)]
15+
public string? TranslateContext { get; set; }
16+
17+
[FileExtensions(Extensions = ".csv")]
18+
[InputFilePath(".csv", "CSV (.csv)|*.csv")]
19+
public string? GlossaryPath { get; set; }
20+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
using System.Globalization;
2+
using System.Text;
3+
using System.Text.Json;
4+
using CsvHelper;
5+
using CsvHelper.Configuration;
6+
using GitHub.Copilot.SDK;
7+
using Microsoft.Extensions.Options;
8+
using ValueTaskSupplement;
9+
using WindowTranslator.Modules;
10+
11+
namespace WindowTranslator.Plugin.GitHubCopilotPlugin;
12+
13+
public class GitHubCopilotTranslator : ITranslateModule, IAsyncDisposable
14+
{
15+
private static readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web)
16+
{
17+
AllowTrailingCommas = true,
18+
};
19+
20+
private readonly string preSystem;
21+
private readonly string? userContext;
22+
private readonly string postSystem;
23+
private readonly string model;
24+
private readonly IDictionary<string, string> glossary = new Dictionary<string, string>();
25+
private readonly CopilotClient client;
26+
private readonly AsyncLazy<CopilotSession> session;
27+
private IReadOnlyList<string> common = [];
28+
private string? context;
29+
private volatile bool sessionStarted;
30+
31+
public string Name => $"{nameof(GitHubCopilotTranslator)}: {this.model}";
32+
33+
public GitHubCopilotTranslator(IOptionsSnapshot<LanguageOptions> langOptions, IOptionsSnapshot<GitHubCopilotOptions> options)
34+
{
35+
var srcLang = CultureInfo.GetCultureInfo(langOptions.Value.Source).DisplayName;
36+
var targetLang = CultureInfo.GetCultureInfo(langOptions.Value.Target).DisplayName;
37+
this.model = options.Value.Model;
38+
this.userContext = options.Value.TranslateContext;
39+
40+
this.preSystem = $$"""
41+
あなたは{{srcLang}}から{{targetLang}}へ翻訳するの専門家です。
42+
入力テキストは{{srcLang}}のテキストであり、翻訳が必要です。
43+
渡されたテキストを{{targetLang}}へ翻訳して出力してください。
44+
""";
45+
this.postSystem = """
46+
入力テキストは以下のJsonフォーマットになっています。
47+
各textの内容はペアとなるcontextの文脈を考慮して翻訳してください。
48+
contextに一人称が指定されている場合は、漢字、ひらがな、カタカナの表記を変更せずに一人称をそのまま使ってください。
49+
翻訳対象のテキストが判別できない場合は、翻訳を行わずにそのままの表記を利用してください。
50+
<入力テキストのJsonフォーマット>
51+
[{"text":"翻訳対象のテキスト1", "context": "翻訳対象のテキスト1の文脈"}, {"text":"翻訳対象のテキスト2", "context": "翻訳対象のテキスト2の文脈"}]
52+
</入力テキストのJsonフォーマット>
53+
54+
出力は以下の文字列型の配列を持ったJsonフォーマットです。
55+
入力されたテキストの順序を維持して翻訳したテキストを出力してください。
56+
<出力テキストのJsonフォーマット>
57+
{"translated": ["翻訳したテキスト1", "翻訳したテキスト2"]}
58+
</出力テキストのJsonフォーマット>
59+
""";
60+
61+
this.client = new CopilotClient(new() { CliPath = Utility.GetBundledCliPath() });
62+
63+
this.session = new(this.CreateSessionAsync);
64+
65+
if (File.Exists(options.Value.GlossaryPath))
66+
{
67+
using var reader = new StreamReader(options.Value.GlossaryPath);
68+
using var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = false });
69+
foreach (var (src, dst) in csv.GetRecords<Glossary>())
70+
{
71+
this.glossary[src] = dst;
72+
}
73+
}
74+
}
75+
76+
private record Glossary(string Source, string Target);
77+
78+
private record Response(string[] Translated);
79+
80+
private async ValueTask<CopilotSession> CreateSessionAsync()
81+
{
82+
var system = string.Join(Environment.NewLine, [this.preSystem, this.context, this.userContext, this.postSystem]);
83+
var s = await this.client.CreateSessionAsync(new()
84+
{
85+
Model = this.model,
86+
SystemMessage = new()
87+
{
88+
Mode = SystemMessageMode.Replace,
89+
Content = system,
90+
},
91+
OnPermissionRequest = static (_, _) => Task.FromResult(new PermissionRequestResult() { Kind = PermissionRequestResultKind.NoResult }),
92+
ReasoningEffort = "low",
93+
}).ConfigureAwait(false);
94+
this.sessionStarted = true;
95+
return s;
96+
}
97+
98+
public async ValueTask<string[]> TranslateAsync(TextInfo[] srcTexts)
99+
{
100+
var glossary = this.glossary.Where(kv => srcTexts.Any(s => s.SourceText.Contains(kv.Key))).ToArray();
101+
var common = this.common.Where(c => srcTexts.Any(s => s.SourceText.Contains(c))).ToArray();
102+
var sb = new StringBuilder();
103+
if (glossary.Length > 0)
104+
{
105+
sb.AppendLine($"""
106+
翻訳する際に以下の用語集を参照して、一貫した翻訳を行ってください。
107+
<用語集>
108+
{string.Join(Environment.NewLine, glossary.Select(kv => $"<用語>{kv.Key}</用語><翻訳>{kv.Value}</翻訳>"))}
109+
</用語集>
110+
111+
""");
112+
}
113+
if (common.Length > 0)
114+
{
115+
sb.AppendLine($"""
116+
翻訳するテキストに以下の共通の用語が含まれている場合は、その用語のみは必ず翻訳せずにそのままの表記を利用してください。
117+
<共通の用語>
118+
{string.Join(Environment.NewLine, common)}
119+
</共通の用語>
120+
121+
""");
122+
}
123+
124+
var jsonData = JsonSerializer.Serialize(srcTexts.Select(s => new { text = s.SourceText, context = s.Context }).ToArray(), jsonOptions);
125+
var content = sb.Length > 0 ? sb.Append(jsonData).ToString() : jsonData;
126+
127+
var session = await this.session.AsValueTask().ConfigureAwait(false);
128+
var response = await session.SendAndWaitAsync(content).ConfigureAwait(false);
129+
var json = response?.Data?.Content?.Trim() ?? string.Empty;
130+
var res = JsonSerializer.Deserialize<Response>(json, jsonOptions);
131+
return res?.Translated ?? [];
132+
}
133+
134+
public ValueTask RegisterGlossaryAsync(IReadOnlyDictionary<string, string> glossary)
135+
{
136+
this.common = glossary.Where(kv => kv.Key == kv.Value).Select(kv => kv.Key.ReplaceLineEndings(string.Empty)).ToArray();
137+
foreach (var (key, value) in glossary.Where(kv => kv.Key != kv.Value))
138+
{
139+
this.glossary.TryAdd(key.ReplaceLineEndings(string.Empty), value.ReplaceLineEndings(string.Empty));
140+
}
141+
return default;
142+
}
143+
144+
public void RegisterContext(string context)
145+
=> this.context = $"""
146+
翻訳するテキストは全体を通して、以下の背景や文脈があるものして翻訳してください。
147+
<背景>
148+
{context}
149+
</背景>
150+
151+
""";
152+
153+
public async ValueTask DisposeAsync()
154+
{
155+
if (this.sessionStarted)
156+
{
157+
await (await this.session.AsValueTask().ConfigureAwait(false)).DisposeAsync().ConfigureAwait(false);
158+
}
159+
await this.client.DisposeAsync().ConfigureAwait(false);
160+
GC.SuppressFinalize(this);
161+
}
162+
}
163+
164+
file static class CopilotClientExtensions
165+
{
166+
public static async Task<AssistantMessageEvent?> SendAndWaitAsync(this CopilotSession session, string prompt)
167+
{
168+
var effectiveTimeout = TimeSpan.FromSeconds(60);
169+
var tcs = new TaskCompletionSource<AssistantMessageEvent?>(TaskCreationOptions.RunContinuationsAsynchronously);
170+
AssistantMessageEvent? lastAssistantMessage = null;
171+
172+
using var subscription = session.On(evt =>
173+
{
174+
switch (evt)
175+
{
176+
case AssistantMessageEvent assistantMessage:
177+
lastAssistantMessage = assistantMessage;
178+
break;
179+
180+
case SessionIdleEvent:
181+
tcs.TrySetResult(lastAssistantMessage);
182+
break;
183+
184+
case SessionErrorEvent { Data: var err }:
185+
tcs.TrySetException(new AppUserException($"""
186+
## {err.ErrorType}: {err.StatusCode}
187+
188+
{err.Message}
189+
({err.Url})
190+
191+
```
192+
{err.Stack}
193+
```
194+
"""));
195+
break;
196+
}
197+
});
198+
199+
await session.SendAsync(new() { Prompt = prompt });
200+
201+
using var cts = new CancellationTokenSource();
202+
cts.CancelAfter(effectiveTimeout);
203+
204+
using var registration = cts.Token.Register(() => tcs.TrySetException(new TimeoutException($"SendAndWaitAsync timed out after {effectiveTimeout}")));
205+
return await tcs.Task;
206+
}
207+
}

Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/Properties/Resources.Designer.cs

Lines changed: 99 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<#@ template debug="false" hostspecific="true" language="C#" #>
2+
<#@ include file="$(SolutionDir)Resources.Designer.tt" #>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<root>
3+
<resheader name="resmimetype">
4+
<value>text/microsoft-resx</value>
5+
</resheader>
6+
<resheader name="version">
7+
<value>2.0</value>
8+
</resheader>
9+
<resheader name="reader">
10+
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
11+
</resheader>
12+
<resheader name="writer">
13+
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
14+
</resheader>
15+
<data name="GitHubCopilotOptions" xml:space="preserve">
16+
<value>إعدادات GitHub Copilot</value>
17+
</data>
18+
<data name="GitHubCopilotTranslator" xml:space="preserve">
19+
<value>ترجمة GitHub Copilot</value>
20+
</data>
21+
<data name="GlossaryPath" xml:space="preserve">
22+
<value>مسار المسرد</value>
23+
</data>
24+
<data name="InvalidOptions" xml:space="preserve">
25+
<value>تم تحديد 「ترجمة GitHub Copilot」كوحدة ترجمة.
26+
27+
لاستخدام ترجمة GitHub Copilot، يجب تثبيت GitHub Copilot CLI وتسجيل الدخول.
28+
29+
لتثبيت GitHub Copilot CLI، يُرجى الرجوع إلى [وثائق GitHub Copilot CLI](https://docs.github.com/ar/copilot/github-copilot-in-the-cli).</value>
30+
</data>
31+
<data name="Model" xml:space="preserve">
32+
<value>النموذج المراد استخدامه</value>
33+
</data>
34+
<data name="Model_Desc" xml:space="preserve">
35+
<value>معرف النموذج المتاح على GitHub Copilot (مثال: gpt-4o، claude-sonnet-4.5)</value>
36+
</data>
37+
<data name="TranslateContext" xml:space="preserve">
38+
<value>معلومات السياق المستخدمة أثناء الترجمة</value>
39+
</data>
40+
</root>

0 commit comments

Comments
 (0)