feat: support furigana and romaji generation on web (#2232)

* feat: support furigana and romaji generation on web
This commit is contained in:
York
2026-07-16 13:26:05 +08:00
committed by GitHub
parent 763e7d5638
commit 62c1ac04e4
12 changed files with 133 additions and 27 deletions
+14 -1
View File
@@ -17,6 +17,14 @@ export type RomajiToken = LyricTextToken & {
let kuroshiroInstance: any = null;
let initPromise: null | Promise<void> = null;
const getDictionaryPath = (): string | undefined => {
if (typeof document === 'undefined') {
return undefined;
}
return new URL('./assets/kuromoji/', document.baseURI).href;
};
const getKuroshiro = async () => {
if (initPromise) {
await initPromise;
@@ -26,8 +34,13 @@ const getKuroshiro = async () => {
if (kuroshiroInstance) return kuroshiroInstance;
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
const dictionaryPath = getDictionaryPath();
const analyzer = dictionaryPath
? new KuromojiAnalyzer({ dictPath: dictionaryPath })
: new KuromojiAnalyzer();
kuroshiroInstance = new KuroshiroClass();
initPromise = kuroshiroInstance.init(new KuromojiAnalyzer());
initPromise = kuroshiroInstance.init(analyzer);
await initPromise;
initPromise = null;
@@ -0,0 +1,13 @@
import isElectron from 'is-electron';
import * as browserLyricsApi from '../../../../main/features/core/lyrics/furigana';
const lyricsApi = isElectron() ? window.api.lyrics : browserLyricsApi;
export const {
convertFurigana,
convertFuriganaFragment,
convertRomaji,
convertRomajiTokens,
parseLyricsTextTokens,
} = lyricsApi;
@@ -0,0 +1,7 @@
export const {
convertFurigana,
convertFuriganaFragment,
convertRomaji,
convertRomajiTokens,
parseLyricsTextTokens,
} = window.api.lyrics;
@@ -372,7 +372,6 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
description: t('setting.enableFurigana', {
context: 'description',
}),
isHidden: !isElectron(),
title: t('setting.enableFurigana'),
},
{
@@ -386,7 +385,6 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
description: t('setting.enableRomaji', {
context: 'description',
}),
isHidden: !isElectron(),
title: t('setting.enableRomaji'),
},
{
@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import isElectron from 'is-electron';
import * as lyricsApi from '/@/lyrics-conversion-api';
import {
alignFuriganaToWordCues,
alignRomajiTokensToWordCues,
@@ -10,15 +10,9 @@ import {
import { normalizeLyrics } from '/@/renderer/features/lyrics/api/lyrics-utils';
import { LyricsResponse, SyncedCueLine, SynchronizedLyrics } from '/@/shared/types/domain-types';
const lyricsApi = isElectron() ? window.api.lyrics : null;
const convertSyncedLyricsFurigana = async (
lyrics: SynchronizedLyrics,
): Promise<SynchronizedLyrics> => {
if (!lyricsApi) {
return lyrics;
}
return Promise.all(
normalizeLyrics(lyrics).map(async (line) => ({
...line,
@@ -76,9 +70,9 @@ const convertSyncedLyricsRomaji = async (
export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, enabled: boolean) => {
return useQuery({
enabled: enabled && !!lyrics && !!lyricsApi,
enabled: enabled && !!lyrics,
queryFn: async () => {
if (!lyrics || !lyricsApi || !enabled) return lyrics;
if (!lyrics || !enabled) return lyrics;
if (typeof lyrics === 'string') {
return await lyricsApi.convertFurigana(lyrics);
@@ -97,9 +91,9 @@ export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, ena
export const useRomajiLyrics = (lyrics: LyricsResponse | null | undefined, enabled: boolean) => {
return useQuery({
enabled: enabled && !!lyrics && !!lyricsApi,
enabled: enabled && !!lyrics,
queryFn: async () => {
if (!lyrics || !lyricsApi || !enabled) return lyrics;
if (!lyrics || !enabled) return lyrics;
if (typeof lyrics === 'string') {
return await lyricsApi.convertRomaji(lyrics);
@@ -129,10 +123,6 @@ const buildSyncedRomajiLine = async (
continue;
}
if (!lyricsApi) {
return cueLines.map(() => null);
}
const tokens = (await lyricsApi.convertRomajiTokens(cueLine.value)) as RomajiToken[];
if (!tokens.length) {
romajiCueLines.push(null);
@@ -160,9 +150,9 @@ export const useSyncedRomajiLyrics = (
enabled: boolean,
) => {
return useQuery({
enabled: enabled && !!lyrics && !!lyricsApi,
enabled: enabled && !!lyrics,
queryFn: async (): Promise<null | SyncedRomajiLyrics> => {
if (!lyrics || !lyricsApi || !enabled) {
if (!lyrics || !enabled) {
return null;
}
@@ -104,7 +104,6 @@ export const LyricSettings = memo(() => {
description: t('setting.enableFurigana', {
context: 'description',
}),
isHidden: !isElectron(),
title: t('setting.enableFurigana'),
},
{
@@ -118,7 +117,6 @@ export const LyricSettings = memo(() => {
description: t('setting.enableRomaji', {
context: 'description',
}),
isHidden: !isElectron(),
title: t('setting.enableRomaji'),
},
{
+9
View File
@@ -0,0 +1,9 @@
export const join = (...parts: string[]): string =>
parts
.filter(Boolean)
.map((part, index) =>
index === 0 ? part.replace(/\/+$/u, '') : part.replace(/^\/+|\/+$/gu, ''),
)
.join('/');
export default { join };