mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-03 14:20:50 +08:00
Merge pull request #477 from lekoOwO/feature/lyric-translation
This commit is contained in:
10
package.json
10
package.json
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -257,6 +257,12 @@ export default {
|
||||
lyricLines: '歌詞行数',
|
||||
mobileUnavailable: 'この設定はモバイルでのみ利用可能です'
|
||||
},
|
||||
// 歌詞翻訳エンジン
|
||||
translationEngine: '歌詞翻訳エンジン',
|
||||
translationEngineOptions: {
|
||||
none: 'オフ',
|
||||
opencc: 'OpenCC 繁体字化'
|
||||
},
|
||||
themeColor: {
|
||||
title: '歌詞テーマカラー',
|
||||
presetColors: 'プリセットカラー',
|
||||
|
||||
@@ -258,6 +258,12 @@ export default {
|
||||
lyricLines: '歌词行数',
|
||||
mobileUnavailable: '此设置仅在移动端可用'
|
||||
},
|
||||
// 歌词翻译引擎相关
|
||||
translationEngine: '歌詞翻譯引擎',
|
||||
translationEngineOptions: {
|
||||
none: '关闭',
|
||||
opencc: 'OpenCC 繁化'
|
||||
},
|
||||
themeColor: {
|
||||
title: '歌词主题色',
|
||||
presetColors: '预设颜色',
|
||||
|
||||
@@ -255,6 +255,12 @@ export default {
|
||||
},
|
||||
placeholder: '#1db954'
|
||||
},
|
||||
// 歌詞翻譯引擎相關
|
||||
translationEngine: '歌詞翻譯引擎',
|
||||
translationEngineOptions: {
|
||||
none: '關閉',
|
||||
opencc: 'OpenCC 繁化'
|
||||
},
|
||||
cookie: {
|
||||
title: 'Cookie設定',
|
||||
description: '請輸入網易雲音樂的Cookie:',
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"musicApiPort": 30488,
|
||||
"closeAction": "ask",
|
||||
"musicQuality": "higher",
|
||||
"lyricTranslationEngine": "none",
|
||||
"fontFamily": "system-ui",
|
||||
"fontScope": "global",
|
||||
"autoPlay": false,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
28
src/renderer/services/lyricTranslation.ts
Normal file
28
src/renderer/services/lyricTranslation.ts
Normal 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
|
||||
};
|
||||
3
src/renderer/services/translation-engines/index.ts
Normal file
3
src/renderer/services/translation-engines/index.ts
Normal 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';
|
||||
94
src/renderer/services/translation-engines/opencc.ts
Normal file
94
src/renderer/services/translation-engines/opencc.ts
Normal 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;
|
||||
}
|
||||
@@ -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
19
src/renderer/types/opencc-rust.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user