Skip to content

Commit b1b6227

Browse files
许君山许君山
authored andcommitted
fix: enforce Chinese localization in AI output and implement romaji-to-kanji auto-conversion via Jisho API
1 parent 5f4e341 commit b1b6227

2 files changed

Lines changed: 113 additions & 19 deletions

File tree

backend/server.js

Lines changed: 108 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,35 @@ const __dirname = path.dirname(__filename);
1515
// 读取动词库
1616
const commonVerbs = JSON.parse(fs.readFileSync(path.join(__dirname, 'common-verbs.json'), 'utf8'));
1717

18-
// 调用 Jisho API 获取更多动词补充
18+
// 调用 Jisho API 获取汉字(如果 kuromoji 没有汉字的话)
19+
function getKanjiFromJisho(verb) {
20+
return new Promise((resolve) => {
21+
https.get(`https://jisho.org/api/v1/search/words?keyword=${encodeURIComponent(verb)}`, (res) => {
22+
let data = '';
23+
res.on('data', chunk => data += chunk);
24+
res.on('end', () => {
25+
try {
26+
const parsed = JSON.parse(data);
27+
if (parsed.data && parsed.data.length > 0) {
28+
for (const item of parsed.data) {
29+
if (item.japanese && item.japanese.length > 0) {
30+
// 检查这个词是否能匹配我们输入的读音
31+
const reading = item.japanese[0].reading;
32+
const word = item.japanese[0].word;
33+
if (reading === verb && word) {
34+
return resolve(word);
35+
}
36+
}
37+
}
38+
}
39+
resolve(verb); // 没找到合适的汉字,返回原词
40+
} catch (e) {
41+
resolve(verb);
42+
}
43+
});
44+
}).on('error', () => resolve(verb));
45+
});
46+
}
1947
function searchJisho(keyword) {
2048
return new Promise((resolve, reject) => {
2149
https.get(`https://jisho.org/api/v1/search/words?keyword=${encodeURIComponent(keyword)}`, (res) => {
@@ -90,10 +118,27 @@ function detectVerbType(verb) {
90118

91119
// 特殊情况硬编码:カ变动词(来る / くる)
92120
if (hiraganaVerb === 'くる' || hiraganaVerb === '来る') {
93-
return 'KURU';
121+
return { type: 'KURU', basicForm: '来る' };
122+
}
123+
// 特殊情况硬编码:サ变动词(する)
124+
if (hiraganaVerb === 'する') {
125+
return { type: 'SURU', basicForm: 'する' };
126+
}
127+
128+
// 尝试分词,首先用转换后的平假名
129+
let tokens = tokenizer.tokenize(hiraganaVerb);
130+
131+
// 如果输入包含汉字且能被正确分词,则使用原输入以保留汉字
132+
// 但我们需要确保它是一个有效的动词
133+
const originalTokens = tokenizer.tokenize(verb);
134+
if (originalTokens.length > 0) {
135+
const originalVerbToken = originalTokens.slice().reverse().find(t => t.pos === '動詞');
136+
if (originalVerbToken && originalVerbToken.conjugated_form === '基本形') {
137+
// 如果原输入(可能包含汉字)能被正确解析为基本形动词,则优先使用它
138+
tokens = originalTokens;
139+
}
94140
}
95141

96-
const tokens = tokenizer.tokenize(hiraganaVerb);
97142
if (tokens.length === 0) return null;
98143

99144
// 对于像 勉強する 这样的词,动词部分在最后
@@ -103,23 +148,51 @@ function detectVerbType(verb) {
103148
if (!verbToken) return null;
104149

105150
// 严格匹配:确保输入的整个词就是一个动词,或者是以动词结尾的复合词(如勉強する)
106-
// 避免像 "tebe" 这种无意义的词被拆分成助词,或者被错误地当作动词的一部分
107-
// 检查提取出的动词原形(basic_form)是否能和输入的词(或其后缀)对得上
108-
// 因为像 `tabe` 提取出来 basic_form 是 `たべる`,如果输入只有 `tabe` 就不完整
109-
// 如果是复合动词如 `勉強する`,verbToken.surface_form 会是 `する`
110-
if (!hiraganaVerb.endsWith(verbToken.surface_form)) {
151+
// 注意:如果是复合动词,basic_form 可能只包含动词部分(如 する),需要特殊处理
152+
const surfaceMatches = verb.endsWith(verbToken.surface_form) || hiraganaVerb.endsWith(verbToken.surface_form);
153+
154+
if (!surfaceMatches) {
111155
return null;
112156
}
157+
113158
// 还需要检查提取出的动词是否是一个完整的字典形(基本形)
114159
if (verbToken.conjugated_form !== '基本形') {
115160
return null;
116161
}
117162

118163
const cType = verbToken.conjugated_type;
119-
if (cType.includes('一段')) return 'ICHIDAN';
120-
if (cType.includes('五段')) return 'GODAN';
121-
if (cType.includes('サ変')) return 'SURU';
122-
if (cType.includes('カ変')) return 'KURU';
164+
165+
// 构建包含汉字的完整基本形
166+
// 如果是复合动词(如 勉強する),需要把前面的名词部分拼起来
167+
let fullBasicForm = verbToken.basic_form;
168+
if (tokens.length > 1) {
169+
// 找到动词前的名词部分
170+
const nounTokens = tokens.slice(0, tokens.indexOf(verbToken));
171+
const prefix = nounTokens.map(t => t.surface_form).join('');
172+
// 只有当输入的原始字符串包含这个前缀时,才拼起来
173+
if (verb.startsWith(prefix) || hiraganaVerb.startsWith(wanakana.toHiragana(prefix))) {
174+
// 如果原输入是以汉字开头的(如 勉強),就用原输入的汉字部分
175+
const originalPrefix = verb.substring(0, prefix.length);
176+
fullBasicForm = originalPrefix + verbToken.basic_form;
177+
}
178+
} else if (verbToken.surface_form === verbToken.basic_form) {
179+
// 如果 surface_form 和 basic_form 一样,尽量使用输入的表面形式(如果输入是汉字的话)
180+
// 比如输入 食べる,verbToken.basic_form 可能是 食べる,也可能是 たべる
181+
// 我们倾向于保留用户输入的汉字
182+
if (wanakana.toHiragana(verb) === wanakana.toHiragana(verbToken.basic_form)) {
183+
fullBasicForm = verb;
184+
}
185+
}
186+
187+
let type = null;
188+
if (cType.includes('一段')) type = 'ICHIDAN';
189+
else if (cType.includes('五段')) type = 'GODAN';
190+
else if (cType.includes('サ変')) type = 'SURU';
191+
else if (cType.includes('カ変')) type = 'KURU';
192+
193+
if (type) {
194+
return { type, basicForm: fullBasicForm };
195+
}
123196

124197
return null;
125198
}
@@ -182,7 +255,8 @@ ${JSON.stringify(conjugationResult, null, 2)}
182255
}
183256
\`\`\`
184257
185-
第二步:在 JSON 代码块之后,用中文简明扼要地解释该动词的含义,并提供2个实用的日常例句(必须包含日文原文、平假名注音和精准的中文翻译)。支持使用 Markdown 格式加粗、高亮。`;
258+
第二步:在 JSON 代码块之后,用中文简明扼要地解释该动词的含义,并提供2个实用的日常例句(必须包含日文原文、平假名注音和精准的中文翻译)。支持使用 Markdown 格式加粗、高亮。
259+
注意:解释动词类型时,请使用中文习惯的称呼(如“五段动词”、“一段动词”、“サ变动词”、“カ变动词”),不要使用英文(如 Godan、Ichidan、Group 1、Group 2)。`;
186260

187261
res.setHeader('Content-Type', 'text/event-stream');
188262
res.setHeader('Cache-Control', 'no-cache');
@@ -255,7 +329,7 @@ app.get('/api/suggest', async (req, res) => {
255329
});
256330

257331
// 动词活用 API
258-
app.get('/api/conjugate', (req, res) => {
332+
app.get('/api/conjugate', async (req, res) => {
259333
try {
260334
let { verb, type } = req.query;
261335

@@ -271,22 +345,39 @@ app.get('/api/conjugate', (req, res) => {
271345
const processedVerb = wanakana.toHiragana(verb);
272346

273347
// 如果前端没有传 type,就用 kuromoji 自动推断
348+
let finalVerb = processedVerb;
349+
274350
if (!type) {
275351
if (!tokenizer) {
276352
return res.status(503).json({ error: 'Dictionary is initializing, please try again later.' });
277353
}
278-
type = detectVerbType(processedVerb);
279-
if (!type) {
354+
const detectResult = detectVerbType(verb);
355+
if (!detectResult) {
280356
return res.status(400).json({
281357
error: `无法自动识别 "${verb}" (解析为 "${processedVerb}") 的动词类型。请确保输入的是正确的日语动词原形(如:食べる、飲む、勉強する)。`
282358
});
283359
}
360+
type = detectResult.type;
361+
finalVerb = detectResult.basicForm;
362+
}
363+
364+
// 检查字符串是否完全没有汉字(wanakana.isKanji 检查是否只包含汉字,所以要手写正则)
365+
const hasKanji = (str) => /[\u4e00-\u9faf]/.test(str);
366+
367+
// 如果推断出来的还是全平假名,尝试用 Jisho 转换成带汉字的常用形式
368+
// 只有当输入不包含任何汉字(即只有平假名或罗马音)时才尝试转换
369+
if (!hasKanji(verb) && !hasKanji(finalVerb)) {
370+
const kanjiVerb = await getKanjiFromJisho(finalVerb);
371+
if (kanjiVerb) {
372+
finalVerb = kanjiVerb;
373+
}
284374
}
285375

286-
const result = conjugate(processedVerb, type);
376+
const result = conjugate(finalVerb, type);
287377
// 如果转换后有变化,可以在返回结果里告诉前端这是基于罗马音解析的
288378
res.json({
289379
...result,
380+
dictionaryForm: finalVerb, // 覆盖为带汉字的原形
290381
originalInput: verb,
291382
parsedAs: processedVerb
292383
});

frontend/src/App.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,17 +333,20 @@ const conjugate = async () => {
333333
return;
334334
}
335335
336+
// 记录原始输入,用于后续判断是否需要更新输入框
337+
const originalInput = form.value.verb.trim();
338+
336339
loading.value = true;
337340
try {
338341
const response = await axios.get('/api/conjugate', {
339342
params: {
340-
verb: form.value.verb
343+
verb: originalInput
341344
}
342345
});
343346
result.value = response.data;
344347
345348
// 如果返回了合法的 dictionaryForm,将其同步回输入框(包含汉字转换)
346-
if (result.value.dictionaryForm) {
349+
if (result.value.dictionaryForm && originalInput !== result.value.dictionaryForm) {
347350
form.value.verb = result.value.dictionaryForm;
348351
}
349352

0 commit comments

Comments
 (0)