mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-22 10:26:33 +02:00
feat: support furigana and romaji generation on web (#2232)
* feat: support furigana and romaji generation on web
This commit is contained in:
+16
-5
@@ -1,15 +1,16 @@
|
|||||||
import { externalizeDepsPlugin, UserConfig } from 'electron-vite';
|
import { defineConfig, externalizeDepsPlugin, UserConfig } from 'electron-vite';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import conditionalImportPlugin from 'vite-plugin-conditional-import';
|
import conditionalImportPlugin from 'vite-plugin-conditional-import';
|
||||||
import dynamicImportPlugin from 'vite-plugin-dynamic-import';
|
import dynamicImportPlugin from 'vite-plugin-dynamic-import';
|
||||||
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
||||||
|
|
||||||
|
import { kuromojiDictionaryPlugin } from './vite.kuromoji-plugin';
|
||||||
import { createReactPlugin } from './vite.react-plugin';
|
import { createReactPlugin } from './vite.react-plugin';
|
||||||
|
|
||||||
const currentOSEnv = process.platform;
|
const currentOSEnv = process.platform;
|
||||||
const electronRendererTarget = 'chrome87';
|
const electronRendererTarget = 'chrome87';
|
||||||
|
|
||||||
const config: UserConfig = {
|
const createConfig = (isDevelopment: boolean): UserConfig => ({
|
||||||
main: {
|
main: {
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
@@ -65,16 +66,26 @@ const config: UserConfig = {
|
|||||||
localsConvention: 'camelCase',
|
localsConvention: 'camelCase',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [createReactPlugin(), ViteEjsPlugin({ web: false })],
|
plugins: [
|
||||||
|
createReactPlugin(),
|
||||||
|
...(isDevelopment ? [kuromojiDictionaryPlugin({ emitDictionary: false })] : []),
|
||||||
|
ViteEjsPlugin({ web: false }),
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'/@/i18n': resolve('src/i18n'),
|
'/@/i18n': resolve('src/i18n'),
|
||||||
|
'/@/lyrics-conversion-api': resolve(
|
||||||
|
isDevelopment
|
||||||
|
? 'src/renderer/features/lyrics/api/development-lyrics-conversion-api.ts'
|
||||||
|
: 'src/renderer/features/lyrics/api/electron-lyrics-conversion-api.ts',
|
||||||
|
),
|
||||||
'/@/remote': resolve('src/remote'),
|
'/@/remote': resolve('src/remote'),
|
||||||
'/@/renderer': resolve('src/renderer'),
|
'/@/renderer': resolve('src/renderer'),
|
||||||
'/@/shared': resolve('src/shared'),
|
'/@/shared': resolve('src/shared'),
|
||||||
|
...(isDevelopment ? { path: resolve('src/renderer/shims/path.ts') } : {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
export default config;
|
export default defineConfig(({ command }) => createConfig(command === 'serve'));
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ export type RomajiToken = LyricTextToken & {
|
|||||||
let kuroshiroInstance: any = null;
|
let kuroshiroInstance: any = null;
|
||||||
let initPromise: null | Promise<void> = 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 () => {
|
const getKuroshiro = async () => {
|
||||||
if (initPromise) {
|
if (initPromise) {
|
||||||
await initPromise;
|
await initPromise;
|
||||||
@@ -26,8 +34,13 @@ const getKuroshiro = async () => {
|
|||||||
if (kuroshiroInstance) return kuroshiroInstance;
|
if (kuroshiroInstance) return kuroshiroInstance;
|
||||||
|
|
||||||
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
|
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
|
||||||
|
const dictionaryPath = getDictionaryPath();
|
||||||
|
const analyzer = dictionaryPath
|
||||||
|
? new KuromojiAnalyzer({ dictPath: dictionaryPath })
|
||||||
|
: new KuromojiAnalyzer();
|
||||||
|
|
||||||
kuroshiroInstance = new KuroshiroClass();
|
kuroshiroInstance = new KuroshiroClass();
|
||||||
initPromise = kuroshiroInstance.init(new KuromojiAnalyzer());
|
initPromise = kuroshiroInstance.init(analyzer);
|
||||||
await initPromise;
|
await initPromise;
|
||||||
|
|
||||||
initPromise = null;
|
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', {
|
description: t('setting.enableFurigana', {
|
||||||
context: 'description',
|
context: 'description',
|
||||||
}),
|
}),
|
||||||
isHidden: !isElectron(),
|
|
||||||
title: t('setting.enableFurigana'),
|
title: t('setting.enableFurigana'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -386,7 +385,6 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
|
|||||||
description: t('setting.enableRomaji', {
|
description: t('setting.enableRomaji', {
|
||||||
context: 'description',
|
context: 'description',
|
||||||
}),
|
}),
|
||||||
isHidden: !isElectron(),
|
|
||||||
title: t('setting.enableRomaji'),
|
title: t('setting.enableRomaji'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import isElectron from 'is-electron';
|
|
||||||
|
|
||||||
|
import * as lyricsApi from '/@/lyrics-conversion-api';
|
||||||
import {
|
import {
|
||||||
alignFuriganaToWordCues,
|
alignFuriganaToWordCues,
|
||||||
alignRomajiTokensToWordCues,
|
alignRomajiTokensToWordCues,
|
||||||
@@ -10,15 +10,9 @@ import {
|
|||||||
import { normalizeLyrics } from '/@/renderer/features/lyrics/api/lyrics-utils';
|
import { normalizeLyrics } from '/@/renderer/features/lyrics/api/lyrics-utils';
|
||||||
import { LyricsResponse, SyncedCueLine, SynchronizedLyrics } from '/@/shared/types/domain-types';
|
import { LyricsResponse, SyncedCueLine, SynchronizedLyrics } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const lyricsApi = isElectron() ? window.api.lyrics : null;
|
|
||||||
|
|
||||||
const convertSyncedLyricsFurigana = async (
|
const convertSyncedLyricsFurigana = async (
|
||||||
lyrics: SynchronizedLyrics,
|
lyrics: SynchronizedLyrics,
|
||||||
): Promise<SynchronizedLyrics> => {
|
): Promise<SynchronizedLyrics> => {
|
||||||
if (!lyricsApi) {
|
|
||||||
return lyrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
normalizeLyrics(lyrics).map(async (line) => ({
|
normalizeLyrics(lyrics).map(async (line) => ({
|
||||||
...line,
|
...line,
|
||||||
@@ -76,9 +70,9 @@ const convertSyncedLyricsRomaji = async (
|
|||||||
|
|
||||||
export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, enabled: boolean) => {
|
export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, enabled: boolean) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: enabled && !!lyrics && !!lyricsApi,
|
enabled: enabled && !!lyrics,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!lyrics || !lyricsApi || !enabled) return lyrics;
|
if (!lyrics || !enabled) return lyrics;
|
||||||
|
|
||||||
if (typeof lyrics === 'string') {
|
if (typeof lyrics === 'string') {
|
||||||
return await lyricsApi.convertFurigana(lyrics);
|
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) => {
|
export const useRomajiLyrics = (lyrics: LyricsResponse | null | undefined, enabled: boolean) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: enabled && !!lyrics && !!lyricsApi,
|
enabled: enabled && !!lyrics,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!lyrics || !lyricsApi || !enabled) return lyrics;
|
if (!lyrics || !enabled) return lyrics;
|
||||||
|
|
||||||
if (typeof lyrics === 'string') {
|
if (typeof lyrics === 'string') {
|
||||||
return await lyricsApi.convertRomaji(lyrics);
|
return await lyricsApi.convertRomaji(lyrics);
|
||||||
@@ -129,10 +123,6 @@ const buildSyncedRomajiLine = async (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lyricsApi) {
|
|
||||||
return cueLines.map(() => null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens = (await lyricsApi.convertRomajiTokens(cueLine.value)) as RomajiToken[];
|
const tokens = (await lyricsApi.convertRomajiTokens(cueLine.value)) as RomajiToken[];
|
||||||
if (!tokens.length) {
|
if (!tokens.length) {
|
||||||
romajiCueLines.push(null);
|
romajiCueLines.push(null);
|
||||||
@@ -160,9 +150,9 @@ export const useSyncedRomajiLyrics = (
|
|||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
) => {
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: enabled && !!lyrics && !!lyricsApi,
|
enabled: enabled && !!lyrics,
|
||||||
queryFn: async (): Promise<null | SyncedRomajiLyrics> => {
|
queryFn: async (): Promise<null | SyncedRomajiLyrics> => {
|
||||||
if (!lyrics || !lyricsApi || !enabled) {
|
if (!lyrics || !enabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ export const LyricSettings = memo(() => {
|
|||||||
description: t('setting.enableFurigana', {
|
description: t('setting.enableFurigana', {
|
||||||
context: 'description',
|
context: 'description',
|
||||||
}),
|
}),
|
||||||
isHidden: !isElectron(),
|
|
||||||
title: t('setting.enableFurigana'),
|
title: t('setting.enableFurigana'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -118,7 +117,6 @@ export const LyricSettings = memo(() => {
|
|||||||
description: t('setting.enableRomaji', {
|
description: t('setting.enableRomaji', {
|
||||||
context: 'description',
|
context: 'description',
|
||||||
}),
|
}),
|
||||||
isHidden: !isElectron(),
|
|
||||||
title: t('setting.enableRomaji'),
|
title: t('setting.enableRomaji'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||||
"include": [
|
"include": [
|
||||||
"electron.vite.config.*",
|
"electron.vite.config.*",
|
||||||
|
"vite.kuromoji-plugin.ts",
|
||||||
"vite.react-plugin.ts",
|
"vite.react-plugin.ts",
|
||||||
"src/main/**/*",
|
"src/main/**/*",
|
||||||
"src/preload/**/*",
|
"src/preload/**/*",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"src/renderer/**/*.tsx",
|
"src/renderer/**/*.tsx",
|
||||||
"src/preload/*.d.ts",
|
"src/preload/*.d.ts",
|
||||||
"src/i18n/**/*",
|
"src/i18n/**/*",
|
||||||
|
"src/main/features/core/lyrics/furigana.ts",
|
||||||
"src/shared/**/*",
|
"src/shared/**/*",
|
||||||
"src/remote/**/*",
|
"src/remote/**/*",
|
||||||
"package.json"
|
"package.json"
|
||||||
@@ -18,6 +19,9 @@
|
|||||||
"/@/renderer/*": [
|
"/@/renderer/*": [
|
||||||
"src/renderer/*"
|
"src/renderer/*"
|
||||||
],
|
],
|
||||||
|
"/@/lyrics-conversion-api": [
|
||||||
|
"src/main/features/core/lyrics/furigana.ts"
|
||||||
|
],
|
||||||
"/@/shared/*": [
|
"/@/shared/*": [
|
||||||
"src/shared/*"
|
"src/shared/*"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Connect, Plugin } from 'vite';
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const analyzerRequire = createRequire(require.resolve('kuroshiro-analyzer-kuromoji/package.json'));
|
||||||
|
const dictionaryDirectory = path.join(
|
||||||
|
path.dirname(analyzerRequire.resolve('kuromoji/package.json')),
|
||||||
|
'dict',
|
||||||
|
);
|
||||||
|
const dictionaryFiles = fs
|
||||||
|
.readdirSync(dictionaryDirectory)
|
||||||
|
.filter((fileName) => fileName.endsWith('.dat.gz'));
|
||||||
|
|
||||||
|
const serveDictionary: Connect.NextHandleFunction = (request, response, next) => {
|
||||||
|
const fileName = path.basename(new URL(request.url ?? '/', 'http://localhost').pathname);
|
||||||
|
|
||||||
|
if (!dictionaryFiles.includes(fileName)) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.setHeader('Content-Type', 'application/gzip');
|
||||||
|
fs.createReadStream(path.join(dictionaryDirectory, fileName)).pipe(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
type KuromojiDictionaryPluginOptions = {
|
||||||
|
emitDictionary?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const kuromojiDictionaryPlugin = ({
|
||||||
|
emitDictionary = true,
|
||||||
|
}: KuromojiDictionaryPluginOptions = {}): Plugin => ({
|
||||||
|
configurePreviewServer(server) {
|
||||||
|
server.middlewares.use('/assets/kuromoji', serveDictionary);
|
||||||
|
},
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use('/assets/kuromoji', serveDictionary);
|
||||||
|
},
|
||||||
|
generateBundle() {
|
||||||
|
if (!emitDictionary) return;
|
||||||
|
|
||||||
|
for (const fileName of dictionaryFiles) {
|
||||||
|
this.emitFile({
|
||||||
|
fileName: `assets/kuromoji/${fileName}`,
|
||||||
|
source: fs.readFileSync(path.join(dictionaryDirectory, fileName)),
|
||||||
|
type: 'asset',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: 'kuromoji-dictionary',
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@ import { defineConfig, normalizePath } from 'vite';
|
|||||||
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
|
import { kuromojiDictionaryPlugin } from './vite.kuromoji-plugin';
|
||||||
import { createReactPlugin } from './vite.react-plugin';
|
import { createReactPlugin } from './vite.react-plugin';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -65,6 +66,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
createReactPlugin(),
|
createReactPlugin(),
|
||||||
|
kuromojiDictionaryPlugin(),
|
||||||
ViteEjsPlugin({
|
ViteEjsPlugin({
|
||||||
root: normalizePath(path.resolve(__dirname, './src/renderer')),
|
root: normalizePath(path.resolve(__dirname, './src/renderer')),
|
||||||
web: true,
|
web: true,
|
||||||
@@ -134,6 +136,7 @@ export default defineConfig({
|
|||||||
workbox: {
|
workbox: {
|
||||||
cleanupOutdatedCaches: true,
|
cleanupOutdatedCaches: true,
|
||||||
clientsClaim: true,
|
clientsClaim: true,
|
||||||
|
globIgnores: ['**/kuromoji/**'],
|
||||||
maximumFileSizeToCacheInBytes: 1000000 * 5, // 5 MB
|
maximumFileSizeToCacheInBytes: 1000000 * 5, // 5 MB
|
||||||
skipWaiting: true,
|
skipWaiting: true,
|
||||||
},
|
},
|
||||||
@@ -142,9 +145,14 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'/@/i18n': path.resolve(__dirname, './src/i18n'),
|
'/@/i18n': path.resolve(__dirname, './src/i18n'),
|
||||||
|
'/@/lyrics-conversion-api': path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'./src/main/features/core/lyrics/furigana.ts',
|
||||||
|
),
|
||||||
'/@/remote': path.resolve(__dirname, './src/remote'),
|
'/@/remote': path.resolve(__dirname, './src/remote'),
|
||||||
'/@/renderer': path.resolve(__dirname, './src/renderer'),
|
'/@/renderer': path.resolve(__dirname, './src/renderer'),
|
||||||
'/@/shared': path.resolve(__dirname, './src/shared'),
|
'/@/shared': path.resolve(__dirname, './src/shared'),
|
||||||
|
path: path.resolve(__dirname, './src/renderer/shims/path.ts'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
root: path.resolve(__dirname, './src/renderer'),
|
root: path.resolve(__dirname, './src/renderer'),
|
||||||
|
|||||||
Reference in New Issue
Block a user