Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.3更新 #19

Merged
merged 10 commits into from
Feb 20, 2024
Merged
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
39 changes: 39 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"projectName": "ja-learner",
"projectOwner": "ks233",
"files": [
"README.md"
],
"commitType": "docs",
"commitConvention": "angular",
"contributorsPerLine": 7,
"contributors": [
{
"login": "ks233",
"name": "ks233",
"avatar_url": "https://avatars.githubusercontent.com/u/38981529?v=4",
"profile": "https://ks233.github.io/",
"contributions": [
"code"
]
},
{
"login": "shaka0919",
"name": "Harvey Wang",
"avatar_url": "https://avatars.githubusercontent.com/u/17539962?v=4",
"profile": "https://github.com/shaka0919",
"contributions": [
"code"
]
},
{
"login": "ly-nld",
"name": "跨性别",
"avatar_url": "https://avatars.githubusercontent.com/u/38471793?v=4",
"profile": "http://lgbt.sh",
"contributions": [
"infra"
]
}
]
}
120 changes: 98 additions & 22 deletions GUI/MainForm.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
using Microsoft.Web.WebView2.Core;
using System.Diagnostics;
using System.Runtime.InteropServices;
using MeCab;
using System.Text;
using System.Reflection.Metadata;
using OpenAI_API.Chat;
using Microsoft.VisualBasic;
using System.Windows.Forms.Design;
using System.Net.Http;

namespace ja_learner
{
Expand All @@ -16,9 +9,30 @@
DictForm dictForm;

TextAnalyzer textAnalyzer = new TextAnalyzer();
GptCaller gptCaller;
private string sentence = "";

private bool immersiveMode = false;
public bool ImmersiveMode
{
get { return immersiveMode; }
set
{
if (value)
{
webView.Parent = this;
tabControl.Hide();
panel1.Hide();
FormBorderStyle = FormBorderStyle.None;
}
else
{
webView.Parent = tabControl.TabPages[0];
tabControl.Show();
panel1.Show();
FormBorderStyle = FormBorderStyle.Sizable;
}
immersiveMode = value;
}
}
public string Sentence
{
get { return sentence; }
Expand All @@ -35,7 +49,7 @@
}
}

public MainForm()

Check warning on line 52 in GUI/MainForm.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable field 'dictForm' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.
{
InitializeComponent();
}
Expand All @@ -45,9 +59,9 @@
webView.CoreWebView2.Settings.IsStatusBarEnabled = false;
webView.CoreWebView2.Profile.PreferredColorScheme = CoreWebView2PreferredColorScheme.Light;
// 处理打开新窗口(用默认浏览器打开)
webView.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;

Check warning on line 62 in GUI/MainForm.cs

View workflow job for this annotation

GitHub Actions / build

Nullability of reference types in type of parameter 'sender' of 'void MainForm.CoreWebView2_NewWindowRequested(object sender, CoreWebView2NewWindowRequestedEventArgs e)' doesn't match the target delegate 'EventHandler<CoreWebView2NewWindowRequestedEventArgs>' (possibly because of nullability attributes).
// 接收js消息(查词典)
webView.CoreWebView2.WebMessageReceived += CoreWebView2_WebMessageReceived;

Check warning on line 64 in GUI/MainForm.cs

View workflow job for this annotation

GitHub Actions / build

Nullability of reference types in type of parameter 'sender' of 'void MainForm.CoreWebView2_WebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)' doesn't match the target delegate 'EventHandler<CoreWebView2WebMessageReceivedEventArgs>' (possibly because of nullability attributes).
}

private async void MainForm_Load(object sender, EventArgs e)
Expand Down Expand Up @@ -82,6 +96,20 @@
private void CoreWebView2_WebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)
{
string message = e.TryGetWebMessageAsString();
// 来自webview的特殊按键事件处理
if (message == "DBLCLICK")
{
ImmersiveMode = !ImmersiveMode;
return;
}
else if (message == "MOUSEDOWN")
{
// if (ImmersiveMode)
{
Drag();
}
return;
}
dictForm.SearchText(message);
dictForm.ShowAndFocus();
Focus();
Expand Down Expand Up @@ -111,16 +139,16 @@
{
if (timerWindowAttach.Enabled)
{
heightAfter = this.Height;
heightAfter = Height;
if (dictForm.Visible)
{
dictForm.Width = this.Right - WindowAttacher.TargetWindowRect.Right;
dictForm.Width = Right - WindowAttacher.TargetWindowRect.Right;
}
}
}


private System.Drawing.Point locationBefore; // 记录普通模式下窗口的位置
private Point locationBefore; // 记录普通模式下窗口的位置
private Size sizeBefore; // 记录普通模式下窗口的大小
private int heightAfter = 200; // 附着模式时,窗体通常会比较矮

Expand All @@ -131,17 +159,17 @@
if (checkBoxWindowAttach.Checked)
{
// 记录普通状态的窗口位置,切换到吸附状态下的窗口位置
sizeBefore = this.Size;
locationBefore = this.Location;
this.Height = heightAfter;
sizeBefore = Size;
locationBefore = Location;
Height = heightAfter;
timerWindowAttach.Enabled = true;
}
else
{
timerWindowAttach.Enabled = false;
heightAfter = this.Height;
this.Size = sizeBefore;
this.Location = locationBefore;
heightAfter = Height;
Size = sizeBefore;
Location = locationBefore;
}
}

Expand All @@ -151,7 +179,7 @@
{
WindowAttacher.AttachWindows(this, dictForm);
}
catch (Exception ex)
catch
{
WindowAttach = false;
}
Expand All @@ -176,6 +204,10 @@
private void checkBoxClipboardMode_CheckedChanged(object sender, EventArgs e)
{
ClipBoardMode = checkBoxClipboardMode.Checked;
if (ClipBoardMode)
{
Sentence = Clipboard.GetText(TextDataFormat.UnicodeText).Trim().Replace(" ", "");
}
}
private void timerGetClipboard_Tick(object sender, EventArgs e)
{
Expand All @@ -201,7 +233,7 @@

private void btnInputText_Click(object sender, EventArgs e)
{
Sentence = Microsoft.VisualBasic.Interaction.InputBox("手动输入", "输入句子", "", 0, 0);
Sentence = Interaction.InputBox("手动输入", "输入句子", "", 0, 0);
}

private void timerSelectWindow_Tick(object sender, EventArgs e)
Expand Down Expand Up @@ -250,7 +282,7 @@
string windowTitle = WindowAttacher.GetWindowTitle(hwnd);
checkBoxWindowAttach.Text = $"与【{windowTitle}】对齐";
// 判断窗口句柄是不是自己的
if (hwnd == this.Handle || hwnd == dictForm.Handle)
if (hwnd == Handle || hwnd == dictForm.Handle)
{
checkBoxWindowAttach.Enabled = false;
return;
Expand Down Expand Up @@ -340,7 +372,51 @@

async private void checkBoxTranslateKatakana_CheckedChanged(object sender, EventArgs e)
{
await webView.ExecuteScriptAsync($"setTranslateKatakana({checkBoxTranslateKatakana.Checked.ToString().ToLower()})");
string param = checkBoxTranslateKatakana.Checked ? "true" : "false";
await webView.ExecuteScriptAsync($"setTranslateKatakana({param})");
}

// https://stackoverflow.com/questions/31199437/borderless-and-resizable-form-c
// 从↑抄来的代码,无边框模式下可调整窗口大小
const int WM_NCHITTEST = 0x0084;
const int HTCLIENT = 1;
const int HTCAPTION = 2;
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
switch (m.Msg)
{
case WM_NCHITTEST:
if (m.Result == (IntPtr)HTCLIENT)
{
m.Result = (IntPtr)HTCAPTION;
}
break;
}
}
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.Style |= 0x40000;
return cp;
}
}

// 窗口拖拽
public const int WM_NCLBUTTONDOWN = 0xA1;
public const int HT_CAPTION = 0x2;

[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern bool ReleaseCapture();

private void Drag()
{
ReleaseCapture();
SendMessage(Handle, WM_NCLBUTTONDOWN, HT_CAPTION, 0);
}
}
}
82 changes: 58 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<h1 align="center">KS的日语学习工具 v0.2</h1>
<h1 align="center">KS的日语学习工具 v0.3</h1>
<div align="center">
<strong>📖 简易日语学习 / 视觉小说阅读辅助工具</strong>
<br />
Expand All @@ -14,6 +14,7 @@
</div>



## 功能介绍

* **语句分析**:用不同样式区分句子成分,为句子中的汉字注音
Expand All @@ -26,16 +27,25 @@

## 使用说明

### 基本的句子分析
### 分析与查词

* 读取剪贴板或手动输入句子
* 勾选“片假不留”可以把片假名单词翻译成英文
* 勾选“片假不留”把片假名单词翻译成英文
* 点击片假名单词上方的英文可以隐藏该单词的翻译,再点一下恢复显示,用于屏蔽错误的翻译结果
* 可以用 Ctrl + 滚轮调整分析界面的显示大小
* 本质浏览器套壳,你甚至可以按 F12 打开控制台(
* 在语句分析界面点击单词快速查词
* 左键点击单词会在词典窗口中显示 MOJi 辞書的搜索结果
* 中键点击单词会在浏览器中打开单词在 MOJi 辞書的搜索页面
* 左键点击单词:在词典窗口中显示 MOJi 辞書的搜索结果
* 中键点击单词:在浏览器中打开单词在 MOJi 辞書的搜索页面
* SHIFT + 左键点击单词:在搜索内容末尾追加该单词
* 词典窗口手动搜索
* 双击搜索框清空搜索内容
* 点击链接跳转至对应的词典网站


![demo](README/demo.gif)

搜索框与 SHIFT 追加搜索是 v0.3 新增的功能,MeCab 有时候会过度断句,比如把“二十四节气”当成了三个词,“四”的注音还标错了。用追加搜索功能就可以把拆开的词拼回来,使查词更加灵活。


### 窗口吸附
Expand All @@ -60,43 +70,67 @@

#### 使用 GPT(需要 API Key)

首先要配置 api key,在 `config.txt` 的第一行输入 api key,第二行输入 api url
首先要配置 api key,在 `appsettings.json` 中配置 ApiKey 与 ApiUrl

```
sk-xxxxxxxxx
https://api.openai.com/{0}/{1}
"ApiKey": "sk-xxx",
"ApiUrl": "https://api.openai.com/{0}/{1}",
```

如果你使用官方 API,第二行就不用修改,如果使用第三方反代,就要修改为相应的域名
如果你使用官方 API,就不用修改 ApiUrl,如果使用第三方反代,就要将其修改为相应的域名

配置好 api key 就可以使用 GPT 翻译和解说文本了。
配置好 ApiKey 就可以使用 ChatGPT 翻译和解说文本了。

![gpt](README/gpt.gif)

## 声明
### 沉浸模式

### 分析与翻译仅供参考
- 双击分析页面的背景,进入仅显示语句分析的沉浸模式
- CTRL + 左键拖动窗口
- 左键拖动窗口边缘调整窗口大小

本项目的分词与注音功能基于 MeCab,有时会犯一些低级错误,比如把「<ruby>身体<rt>からだ</rt></ruby>」注音为 しんたい、把「<ruby>二人<rt>ふたり</rt></ruby>」注音为 ににん,用词汇更丰富的 [UniDic](https://clrd.ninjal.ac.jp/unidic/) 词典替换 `dic` 文件夹中默认的 IPADIC 效果会稍好一些。
![immersive](README/immersive.gif)

翻译毕竟都是机翻,出错也很正常。谷歌翻译遇到复杂的句式和不规范的表达就容易翻车,ChatGPT 比谷歌懂更多俗语、流行语,但偶尔也会发癫,比如使用简体中文以外的语言回复、唐突的[塞氏翻译法](https://zh.moegirl.org.cn/zh-hans/塞氏翻译法)等等。建议把本软件当做一个精读工具而不是翻译器,把注意力放在日语原文上,只在不确定的时候使用翻译作为参考。
## 使用建议

外来语标注功能使用谷歌翻译将片假名单词翻译为英语,但不是所有片假名单词都是外来语,外来语也不一定来源于英语,还有像 supplies 和 surprise 这样的“同音词”也不好区分,因此也会出现标记错误的情况
本项目的分词与注音功能基于 MeCab,虽然整体准确率还算可以,但有时会犯一些低级错误,比如在某些语境下把<ruby>身体<rt>からだ</rt></ruby>注音为 しんたい、把<ruby>二人<rt>ふたり</rt></ruby>注音为 ににん,遇到读音特殊的人名也无法正确注音。用词汇更丰富的 [UniDic](https://clrd.ninjal.ac.jp/unidic/) 词典替换 `dic` 文件夹中默认的 IPADIC 效果会稍好一些

根据我个人的使用体验,整体准确率还可以接受,但还是不建议完全初学者使用,以免被误导。如果遇到可疑的注音或翻译,建议点击单词查看 MOJi 辞書的解释和注音,并对照不同引擎的翻译结果,或者使用 ChatGPT 的解说功能
翻译毕竟都是机翻,准确率有限。谷歌翻译遇到复杂的句式和不规范的表达就容易翻车,ChatGPT 比谷歌懂更多俗语、流行语,但比较不稳定,偶尔会使用简体中文以外的语言回复、唐突地使用[塞氏翻译法](https://zh.moegirl.org.cn/zh-hans/塞氏翻译法)。建议把本软件当做一个精读工具而不是翻译器,把注意力放在日语原文上,只在不确定的时候使用翻译作为参考

### 相关项目
外来语标注功能使用谷歌翻译将片假名单词翻译为英语,但不是所有片假名单词都是外来语,外来语也不一定来源于英语,还有像 supplies 和 surprise 这样的“同音词”也不好区分,因此也会出现标注错误的情况。

本项目的前端页面:[ks233/ja-learner-webview](https://github.com/ks233/ja-learner-webview)
根据作者自己的使用体验,整体准确率还可以接受,但还是不建议完全初学者使用,以免被误导。如果遇到可疑的注音或翻译,建议查询更权威的词典,比如 [Weblio 辞書](https://www.weblio.jp/)、大辞林、小学馆日中,网络用语可以查 [ニコニコ大百科](https://dic.nicovideo.jp/)。

开坑的想法主要来源于 [YUKI 翻译器](https://github.com/project-yuki/YUKI) 和 [Translation-Aggregator](https://github.com/Translation-Aggregator/Translation-Aggregator),前者支持了丰富的翻译接口,内置了文本提取功能,但使用起来比较复杂,且缺少快速查词的功能;后者虽然可以鼠标悬停查词,但只有日英词典、界面比较古老,而且翻译接口几乎炸完了,于是我决定搓一个更简单、更符合自己需求的工具。
## 相关项目

[taishi-i/awesome-japanese-nlp-resources](https://github.com/taishi-i/awesome-japanese-nlp-resources) 有非常多日语相关的工具和资源,对我开发这个项目帮助很大
开坑的想法主要来源于 [YUKI 翻译器](https://github.com/project-yuki/YUKI) 和 [Translation-Aggregator](https://github.com/Translation-Aggregator/Translation-Aggregator),前者支持了丰富的翻译接口,内置了文本提取功能,但使用起来比较复杂,且缺少快速查词的功能;后者虽然可以鼠标悬停查词,但只有日英词典、界面比较古老,而且翻译接口几乎炸完了,于是我决定搓一个更简单、更符合自己需求的工具

### 使用的第三方库与框架
使用的第三方工具与参考资料:

* 形态分析:[taku910/mecab](https://github.com/taku910/mecab) 的 .Net 移植版本 [kekyo/MeCab.DotNet](https://github.com/kekyo/MeCab.DotNet)
* 调用 ChatGPT:[OkGoDoIt/OpenAI-API-dotnet](https://github.com/OkGoDoIt/OpenAI-API-dotnet)
* 谷歌翻译:参考了 [FilipePS/Traduzir-paginas-web](https://github.com/FilipePS/Traduzir-paginas-web) 的 API 调用方式
* ChatGPT:[OkGoDoIt/OpenAI-API-dotnet](https://github.com/OkGoDoIt/OpenAI-API-dotnet)
* [前端页面](https://github.com/ks233/ja-learner-webview):[WebView2 控件](https://www.nuget.org/packages/Microsoft.Web.WebView2),Vite + Vue
* 单词搜索:[MOJi 辞書](https://www.mojidict.com/)
* Webview 页面:Vite + Vue
* 谷歌翻译:参考了 [FilipePS/Traduzir-paginas-web](https://github.com/FilipePS/Traduzir-paginas-web) 的 API 调用方式
* 其它参考资源:[taishi-i/awesome-japanese-nlp-resources](https://github.com/taishi-i/awesome-japanese-nlp-resources)

## 贡献者

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->

<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://ks233.github.io/"><img src="https://avatars.githubusercontent.com/u/38981529?v=4?s=100" width="100px;" alt="ks233"/><br /><sub><b>ks233</b></sub></a><br /><a href="#code-ks233" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shaka0919"><img src="https://avatars.githubusercontent.com/u/17539962?v=4?s=100" width="100px;" alt="Harvey Wang"/><br /><sub><b>Harvey Wang</b></sub></a><br /><a href="#code-shaka0919" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://lgbt.sh"><img src="https://avatars.githubusercontent.com/u/38471793?v=4?s=100" width="100px;" alt="跨性别"/><br /><sub><b>跨性别</b></sub></a><br /><a href="#infra-ly-nld" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
</tbody>
</table>

<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->
Binary file added README/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added README/immersive.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion TextAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct TextAnalyzerResult
public string Surface;
public string Pos;// 词性
public string Basic;// 基本型
public string Reading;// 渲染
public string Reading;// 读音
public readonly string ToJson()
{

Expand Down
1 change: 0 additions & 1 deletion appsettings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"SecretKey": "Secret key value",
"AppSetting": {
"ApiKey": "sk-xxx",
"ApiUrl": "https://api.openai.com/{0}/{1}",
Expand Down
Loading