Merge pull request #477 from lekoOwO/feature/lyric-translation

This commit is contained in:
Alger
2025-09-20 15:07:29 +08:00
committed by GitHub
13 changed files with 204 additions and 4 deletions

View File

@@ -56,7 +56,6 @@
"@electron-toolkit/tsconfig": "^1.0.1",
"@eslint/js": "^9.31.0",
"@rushstack/eslint-patch": "^1.10.3",
"@types/howler": "^2.2.12",
"@types/node": "^20.14.8",
"@types/tinycolor2": "^1.4.6",
@@ -84,8 +83,8 @@
"eslint-plugin-vue": "^10.3.0",
"eslint-plugin-vue-scoped-css": "^2.11.0",
"globals": "^16.3.0",
"lint-staged": "^15.2.10",
"howler": "^2.2.4",
"lint-staged": "^15.2.10",
"lodash": "^4.17.21",
"marked": "^15.0.4",
"naive-ui": "^2.41.0",
@@ -105,6 +104,7 @@
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "7.7.2",
"vue": "^3.5.13",
"vue-eslint-parser": "^10.2.0",
"vue-router": "^4.5.0",
"vue-tsc": "^2.0.22"
},
@@ -204,5 +204,11 @@
"shortcutName": "AlgerMusicPlayer",
"include": "build/installer.nsh"
}
},
"pnpm": {
"onlyBuiltDependencies": [
"electron",
"esbuild"
]
}
}

View File

@@ -258,6 +258,12 @@ export default {
lyricLines: 'Lyric Lines',
mobileUnavailable: 'This setting is only available on mobile devices'
},
// Lyric translation engine
translationEngine: 'Lyric Translation Engine',
translationEngineOptions: {
none: 'Off',
opencc: 'OpenCC Traditionalize'
},
themeColor: {
title: 'Lyric Theme Color',
presetColors: 'Preset Colors',

View File

@@ -257,6 +257,12 @@ export default {
lyricLines: '歌詞行数',
mobileUnavailable: 'この設定はモバイルでのみ利用可能です'
},
// 歌詞翻訳エンジン
translationEngine: '歌詞翻訳エンジン',
translationEngineOptions: {
none: 'オフ',
opencc: 'OpenCC 繁体字化'
},
themeColor: {
title: '歌詞テーマカラー',
presetColors: 'プリセットカラー',

View File

@@ -258,6 +258,12 @@ export default {
lyricLines: '歌词行数',
mobileUnavailable: '此设置仅在移动端可用'
},
// 歌词翻译引擎相关
translationEngine: '歌詞翻譯引擎',
translationEngineOptions: {
none: '关闭',
opencc: 'OpenCC 繁化'
},
themeColor: {
title: '歌词主题色',
presetColors: '预设颜色',

View File

@@ -255,6 +255,12 @@ export default {
},
placeholder: '#1db954'
},
// 歌詞翻譯引擎相關
translationEngine: '歌詞翻譯引擎',
translationEngineOptions: {
none: '關閉',
opencc: 'OpenCC 繁化'
},
cookie: {
title: 'Cookie設定',
description: '請輸入網易雲音樂的Cookie',

View File

@@ -15,6 +15,7 @@
"musicApiPort": 30488,
"closeAction": "ask",
"musicQuality": "higher",
"lyricTranslationEngine": "none",
"fontFamily": "system-ui",
"fontScope": "global",
"autoPlay": false,

View File

@@ -279,8 +279,15 @@ const setupMusicWatchers = () => {
nextTick(async () => {
console.log('歌曲切换,更新歌词数据');
// 更新歌词数据
lrcArray.value = playMusic.value.lyric?.lrcArray || [];
const rawLrc = playMusic.value.lyric?.lrcArray || [];
lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || [];
try {
const { translateLyrics } = await import('@/services/lyricTranslation');
lrcArray.value = await translateLyrics(rawLrc as any);
} catch (e) {
console.error('翻译歌词失败,使用原始歌词:', e);
lrcArray.value = rawLrc as any;
}
// 当歌词数据更新时,如果歌词窗口打开,则发送数据
if (isElectron && isLyricWindowOpen.value) {

View File

@@ -0,0 +1,28 @@
import { useSettingsStore } from '@/store/modules/settings';
import type { ILyricText } from '@/types/music';
/**
* Translate lyric lines according to selected engine.
* Supports runtime-loading `opencc-rust` from CDN and caches the converter.
*/
export async function translateLyrics(lines: ILyricText[] | undefined) {
if (!lines || lines.length === 0) return lines || [];
const settingsStore = useSettingsStore();
const engine = settingsStore.setData?.lyricTranslationEngine || 'none';
switch (engine) {
case 'opencc': {
const mod: any = await import('./translation-engines/opencc');
const engineMod = await mod.ensureOpenccConverter();
return engineMod.translateLines(lines);
}
default: {
return lines.map((l) => ({ ...l, trText: l.trText || '' }));
}
}
}
export default {
translateLyrics
};

View File

@@ -0,0 +1,3 @@
// Re-export available translation engines from this folder.
// Add more engines here as separate files and re-export them.
export * from './opencc';

View File

@@ -0,0 +1,94 @@
import type { ILyricText } from '@/types/music';
let _inited = false;
let _converter: any = null;
export async function init(): Promise<void> {
if (_inited) return;
const mod: any = await import('https://cdn.jsdelivr.net/npm/opencc-rust/dist/opencc-rust.mjs');
if (!mod?.initOpenccRust || !mod?.getConverter) {
throw new Error('opencc-rust module missing expected exports');
}
await mod.initOpenccRust();
_converter = mod.getConverter();
_inited = true;
}
export async function convert(text: string): Promise<string> {
await init();
if (!_converter) return text;
return _converter.convert(text);
}
export async function convertLines(lines: string[]) {
await init();
if (!_converter) return lines.slice();
const cjkRe = /[\u4e00-\u9fff]/;
const convertOne = async (s: string) => {
const src = (s || '').trim();
if (!src || !cjkRe.test(src)) return '';
try {
return await _converter.convert(src);
} catch (e) {
console.warn('opencc convertLines item failed:', e);
return '';
}
};
const results = await Promise.all(lines.map((s) => convertOne(s)));
return results;
}
export async function translateLines(lines: ILyricText[]) {
if (!lines || lines.length === 0) return lines.slice();
const srcInfo = lines.map((l) => {
const hasTr = !!(l.trText && l.trText.trim() !== '');
return {
src: hasTr ? (l.trText || '').trim() : (l.text || '').trim(),
targetIsTr: hasTr
};
});
const srcLines = srcInfo.map((s) => s.src);
const converted = await convertLines(srcLines);
return lines.map((l, i) => {
const { targetIsTr } = srcInfo[i];
const conv = converted[i] || '';
if (targetIsTr) {
const tr = conv || l.trText || l.text || '';
return { ...l, trText: tr };
}
const txt = conv || l.text || '';
return { ...l, text: txt };
});
}
export default {
init,
convert,
convertLines,
translateLines
};
// Ensure the engine is initialized and return public API.
let _ensurePromise: Promise<any> | null = null;
export async function ensureOpenccConverter() {
if (_ensurePromise) return _ensurePromise;
_ensurePromise = (async () => {
try {
await init();
} catch (e) {
console.warn('opencc ensureOpenccConverter init failed:', e);
}
return {
init,
convert,
convertLines,
translateLines
};
})();
return _ensurePromise;
}

View File

@@ -7,6 +7,7 @@ export interface LyricConfig {
showTranslation: boolean;
theme: 'default' | 'light' | 'dark';
hidePlayBar: boolean;
translationEngine?: 'none' | 'opencc';
pureModeEnabled: boolean;
hideMiniPlayBar: boolean;
hideLyrics: boolean;
@@ -31,7 +32,9 @@ export const DEFAULT_LYRIC_CONFIG: LyricConfig = {
// 移动端默认配置
mobileLayout: 'ios',
mobileCoverStyle: 'full',
mobileShowLyricLines: 3
mobileShowLyricLines: 3,
// 翻译引擎: 'none' or 'opencc'
translationEngine: 'none'
};
export interface ILyric {

19
src/renderer/types/opencc-rust.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
declare module 'https://cdn.jsdelivr.net/npm/opencc-rust/dist/opencc-rust.mjs' {
export function initOpenccRust(): Promise<void>;
export function getConverter(): {
convert: (text: string) => Promise<string>;
};
}
// Allow wildcard import if different CDN URL is used
declare module 'opencc-rust' {
export function initOpenccRust(): Promise<void>;
export function getConverter(): {
convert: (text: string) => Promise<string>;
};
}
declare module './translation-engines/opencc' {
export function init(): Promise<void>;
export function convert(text: string): Promise<string>;
}

View File

@@ -59,6 +59,21 @@
<language-switcher />
</div>
<div class="set-item">
<div>
<div class="set-item-title">{{ t('settings.translationEngine') }}</div>
<div class="set-item-content">{{ t('settings.translationEngine') }}</div>
</div>
<n-select
v-model:value="setData.lyricTranslationEngine"
:options="[
{ label: t('settings.translationEngineOptions.none'), value: 'none' },
{ label: t('settings.translationEngineOptions.opencc'), value: 'opencc' }
]"
style="width: 160px"
/>
</div>
<div class="set-item" v-if="isElectron">
<div>
<div class="set-item-title">{{ t('settings.basic.font') }}</div>