diff --git a/src/main/character-dictionary-runtime/constants.ts b/src/main/character-dictionary-runtime/constants.ts index 3180e2b6..646ff455 100644 --- a/src/main/character-dictionary-runtime/constants.ts +++ b/src/main/character-dictionary-runtime/constants.ts @@ -1,7 +1,7 @@ export const ANILIST_GRAPHQL_URL = 'https://graphql.anilist.co'; export const ANILIST_REQUEST_DELAY_MS = 2000; export const CHARACTER_IMAGE_DOWNLOAD_DELAY_MS = 250; -export const CHARACTER_DICTIONARY_FORMAT_VERSION = 19; +export const CHARACTER_DICTIONARY_FORMAT_VERSION = 20; export const CHARACTER_DICTIONARY_MERGED_TITLE = 'SubMiner Character Dictionary'; export const HONORIFIC_SUFFIXES = [ diff --git a/src/main/character-dictionary-runtime/term-building.test.ts b/src/main/character-dictionary-runtime/term-building.test.ts index 26eef728..8f7dfc71 100644 --- a/src/main/character-dictionary-runtime/term-building.test.ts +++ b/src/main/character-dictionary-runtime/term-building.test.ts @@ -36,3 +36,35 @@ test('buildNameTerms adds surname honorifics from Japanese localized aliases', ( assert.ok(terms.includes('馬渕さん')); assert.ok(!terms.includes('송치')); }); + +test('buildNameTerms drops the disambiguator letter of a mob character name', () => { + const terms = buildNameTerms( + characterRecord({ + firstNameHint: '', + lastNameHint: '', + fullName: 'Joshi A', + nativeName: '女子A', + }), + ); + + // ア would match every あ〜 in the subtitles; the letter is a disambiguator + // (Girl A / Girl B), not a name. + assert.ok(!terms.includes('ア')); + assert.ok(!terms.includes('アさん')); + assert.ok(terms.includes('女子A')); + assert.ok(terms.includes('ジョシア')); +}); + +test('buildNameTerms keeps a single-kanji name part', () => { + const terms = buildNameTerms( + characterRecord({ + firstNameHint: 'Sora', + lastNameHint: 'Yamada', + fullName: 'Sora Yamada', + nativeName: '山田 空', + }), + ); + + assert.ok(terms.includes('山田')); + assert.ok(terms.includes('空')); +}); diff --git a/src/main/character-dictionary-runtime/term-building.ts b/src/main/character-dictionary-runtime/term-building.ts index 43f99147..f0f3c2ee 100644 --- a/src/main/character-dictionary-runtime/term-building.ts +++ b/src/main/character-dictionary-runtime/term-building.ts @@ -42,6 +42,15 @@ export function expandRawNameVariants(rawName: string): string[] { return [...variants]; } +// AniList disambiguates unnamed mob characters with a trailing letter (女子A / +// "Joshi A"), and a single letter romanizes into a single-kana alias (A → ア) +// that collides with interjections (あ〜 matching ア). A one-character form is +// only a real lookup target when it is kanji, so every other single-character +// form is dropped before it can become a term. +function isUsableNameTerm(name: string): boolean { + return [...name].length > 1 || containsKanji(name); +} + export function isJapaneseNameSplitCandidate(name: string): boolean { const compact = name.replace(/[\s\u3000・・·•]/g, ''); return ( @@ -97,8 +106,11 @@ export function buildNameTerms( const split = name.split(/[\s\u3000]+/).filter((part) => part.trim().length > 0); if (split.length === 2) { - target.add(split[0]!); - target.add(split[1]!); + for (const part of split) { + if (isUsableNameTerm(part)) { + target.add(part); + } + } } const splitByMiddleDot = name @@ -107,7 +119,9 @@ export function buildNameTerms( .filter((part) => part.length > 0); if (splitByMiddleDot.length >= 2) { for (const part of splitByMiddleDot) { - target.add(part); + if (isUsableNameTerm(part)) { + target.add(part); + } } } @@ -136,6 +150,7 @@ export function buildNameTerms( const withHonorifics = new Set(); for (const entry of base) { + if (!isUsableNameTerm(entry)) continue; withHonorifics.add(entry); for (const suffix of HONORIFIC_SUFFIXES) { withHonorifics.add(`${entry}${suffix.term}`);