mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-24 16:27:23 +08:00
Merge pull request #477 from lekoOwO/feature/lyric-translation
This commit is contained in:
+8
-2
@@ -56,7 +56,6 @@
|
|||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
"@eslint/js": "^9.31.0",
|
"@eslint/js": "^9.31.0",
|
||||||
"@rushstack/eslint-patch": "^1.10.3",
|
"@rushstack/eslint-patch": "^1.10.3",
|
||||||
|
|
||||||
"@types/howler": "^2.2.12",
|
"@types/howler": "^2.2.12",
|
||||||
"@types/node": "^20.14.8",
|
"@types/node": "^20.14.8",
|
||||||
"@types/tinycolor2": "^1.4.6",
|
"@types/tinycolor2": "^1.4.6",
|
||||||
@@ -84,8 +83,8 @@
|
|||||||
"eslint-plugin-vue": "^10.3.0",
|
"eslint-plugin-vue": "^10.3.0",
|
||||||
"eslint-plugin-vue-scoped-css": "^2.11.0",
|
"eslint-plugin-vue-scoped-css": "^2.11.0",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"lint-staged": "^15.2.10",
|
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
|
"lint-staged": "^15.2.10",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "^15.0.4",
|
"marked": "^15.0.4",
|
||||||
"naive-ui": "^2.41.0",
|
"naive-ui": "^2.41.0",
|
||||||
@@ -105,6 +104,7 @@
|
|||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-vue-devtools": "7.7.2",
|
"vite-plugin-vue-devtools": "7.7.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-eslint-parser": "^10.2.0",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
"vue-tsc": "^2.0.22"
|
"vue-tsc": "^2.0.22"
|
||||||
},
|
},
|
||||||
@@ -204,5 +204,11 @@
|
|||||||
"shortcutName": "AlgerMusicPlayer",
|
"shortcutName": "AlgerMusicPlayer",
|
||||||
"include": "build/installer.nsh"
|
"include": "build/installer.nsh"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"electron",
|
||||||
|
"esbuild"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,6 +258,12 @@ export default {
|
|||||||
lyricLines: 'Lyric Lines',
|
lyricLines: 'Lyric Lines',
|
||||||
mobileUnavailable: 'This setting is only available on mobile devices'
|
mobileUnavailable: 'This setting is only available on mobile devices'
|
||||||
},
|
},
|
||||||
|
// Lyric translation engine
|
||||||
|
translationEngine: 'Lyric Translation Engine',
|
||||||
|
translationEngineOptions: {
|
||||||
|
none: 'Off',
|
||||||
|
opencc: 'OpenCC Traditionalize'
|
||||||
|
},
|
||||||
themeColor: {
|
themeColor: {
|
||||||
title: 'Lyric Theme Color',
|
title: 'Lyric Theme Color',
|
||||||
presetColors: 'Preset Colors',
|
presetColors: 'Preset Colors',
|
||||||
|
|||||||
@@ -257,6 +257,12 @@ export default {
|
|||||||
lyricLines: '歌詞行数',
|
lyricLines: '歌詞行数',
|
||||||
mobileUnavailable: 'この設定はモバイルでのみ利用可能です'
|
mobileUnavailable: 'この設定はモバイルでのみ利用可能です'
|
||||||
},
|
},
|
||||||
|
// 歌詞翻訳エンジン
|
||||||
|
translationEngine: '歌詞翻訳エンジン',
|
||||||
|
translationEngineOptions: {
|
||||||
|
none: 'オフ',
|
||||||
|
opencc: 'OpenCC 繁体字化'
|
||||||
|
},
|
||||||
themeColor: {
|
themeColor: {
|
||||||
title: '歌詞テーマカラー',
|
title: '歌詞テーマカラー',
|
||||||
presetColors: 'プリセットカラー',
|
presetColors: 'プリセットカラー',
|
||||||
|
|||||||
@@ -258,6 +258,12 @@ export default {
|
|||||||
lyricLines: '歌词行数',
|
lyricLines: '歌词行数',
|
||||||
mobileUnavailable: '此设置仅在移动端可用'
|
mobileUnavailable: '此设置仅在移动端可用'
|
||||||
},
|
},
|
||||||
|
// 歌词翻译引擎相关
|
||||||
|
translationEngine: '歌詞翻譯引擎',
|
||||||
|
translationEngineOptions: {
|
||||||
|
none: '关闭',
|
||||||
|
opencc: 'OpenCC 繁化'
|
||||||
|
},
|
||||||
themeColor: {
|
themeColor: {
|
||||||
title: '歌词主题色',
|
title: '歌词主题色',
|
||||||
presetColors: '预设颜色',
|
presetColors: '预设颜色',
|
||||||
|
|||||||
@@ -255,6 +255,12 @@ export default {
|
|||||||
},
|
},
|
||||||
placeholder: '#1db954'
|
placeholder: '#1db954'
|
||||||
},
|
},
|
||||||
|
// 歌詞翻譯引擎相關
|
||||||
|
translationEngine: '歌詞翻譯引擎',
|
||||||
|
translationEngineOptions: {
|
||||||
|
none: '關閉',
|
||||||
|
opencc: 'OpenCC 繁化'
|
||||||
|
},
|
||||||
cookie: {
|
cookie: {
|
||||||
title: 'Cookie設定',
|
title: 'Cookie設定',
|
||||||
description: '請輸入網易雲音樂的Cookie:',
|
description: '請輸入網易雲音樂的Cookie:',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"musicApiPort": 30488,
|
"musicApiPort": 30488,
|
||||||
"closeAction": "ask",
|
"closeAction": "ask",
|
||||||
"musicQuality": "higher",
|
"musicQuality": "higher",
|
||||||
|
"lyricTranslationEngine": "none",
|
||||||
"fontFamily": "system-ui",
|
"fontFamily": "system-ui",
|
||||||
"fontScope": "global",
|
"fontScope": "global",
|
||||||
"autoPlay": false,
|
"autoPlay": false,
|
||||||
|
|||||||
@@ -279,8 +279,15 @@ const setupMusicWatchers = () => {
|
|||||||
nextTick(async () => {
|
nextTick(async () => {
|
||||||
console.log('歌曲切换,更新歌词数据');
|
console.log('歌曲切换,更新歌词数据');
|
||||||
// 更新歌词数据
|
// 更新歌词数据
|
||||||
lrcArray.value = playMusic.value.lyric?.lrcArray || [];
|
const rawLrc = playMusic.value.lyric?.lrcArray || [];
|
||||||
lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || [];
|
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) {
|
if (isElectron && isLyricWindowOpen.value) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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';
|
||||||
@@ -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;
|
showTranslation: boolean;
|
||||||
theme: 'default' | 'light' | 'dark';
|
theme: 'default' | 'light' | 'dark';
|
||||||
hidePlayBar: boolean;
|
hidePlayBar: boolean;
|
||||||
|
translationEngine?: 'none' | 'opencc';
|
||||||
pureModeEnabled: boolean;
|
pureModeEnabled: boolean;
|
||||||
hideMiniPlayBar: boolean;
|
hideMiniPlayBar: boolean;
|
||||||
hideLyrics: boolean;
|
hideLyrics: boolean;
|
||||||
@@ -31,7 +32,9 @@ export const DEFAULT_LYRIC_CONFIG: LyricConfig = {
|
|||||||
// 移动端默认配置
|
// 移动端默认配置
|
||||||
mobileLayout: 'ios',
|
mobileLayout: 'ios',
|
||||||
mobileCoverStyle: 'full',
|
mobileCoverStyle: 'full',
|
||||||
mobileShowLyricLines: 3
|
mobileShowLyricLines: 3,
|
||||||
|
// 翻译引擎: 'none' or 'opencc'
|
||||||
|
translationEngine: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ILyric {
|
export interface ILyric {
|
||||||
|
|||||||
Vendored
+19
@@ -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 />
|
<language-switcher />
|
||||||
</div>
|
</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 class="set-item" v-if="isElectron">
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">{{ t('settings.basic.font') }}</div>
|
<div class="set-item-title">{{ t('settings.basic.font') }}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user