feat(synced-lyrics): Add Simplified/Traditional Chinese converter and improve Romanization to display tone (#4111)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Angelos Bouklis <me@arjix.dev>
Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
Hein Thant Maung Maung
2026-02-12 23:27:48 +07:00
committed by GitHub
parent e2f1ff50dd
commit 208e57bdd3
8 changed files with 154 additions and 20 deletions

View File

@ -89,6 +89,7 @@
"bgutils-js": "3.2.0",
"butterchurn": "3.0.0-beta.5",
"butterchurn-presets": "3.0.0-beta.4",
"chinese-conv": "^4.0.0",
"color": "5.0.3",
"conf": "15.0.2",
"custom-electron-prompt": "1.6.1",
@ -121,6 +122,7 @@
"node-html-parser": "7.0.2",
"node-id3": "0.2.9",
"peerjs": "1.5.5",
"pinyin-pro": "^3.27.0",
"semver": "7.7.3",
"serve": "14.2.5",
"socks": "2.8.7",
@ -129,7 +131,6 @@
"solid-js": "1.9.11",
"solid-styled-components": "0.28.5",
"solid-transition-group": "0.3.0",
"tiny-pinyin": "1.3.2",
"tinyld": "1.3.4",
"virtua": "0.48.5",
"vudio": "2.1.1",

31
pnpm-lock.yaml generated
View File

@ -114,6 +114,9 @@ importers:
butterchurn-presets:
specifier: 3.0.0-beta.4
version: 3.0.0-beta.4
chinese-conv:
specifier: ^4.0.0
version: 4.0.0
color:
specifier: 5.0.3
version: 5.0.3
@ -210,6 +213,9 @@ importers:
peerjs:
specifier: 1.5.5
version: 1.5.5
pinyin-pro:
specifier: ^3.27.0
version: 3.27.0
semver:
specifier: 7.7.3
version: 7.7.3
@ -234,9 +240,6 @@ importers:
solid-transition-group:
specifier: 0.3.0
version: 0.3.0(solid-js@1.9.11)
tiny-pinyin:
specifier: 1.3.2
version: 1.3.2
tinyld:
specifier: 1.3.4
version: 1.3.4
@ -1830,6 +1833,14 @@ packages:
resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
chinese-conv@4.0.0:
resolution: {integrity: sha512-PVBMzvv6CtX1cubaDXfxYscIdbOAHPuY/E2vnfJIzOACX+xIW4NRKRlNsZVI2p5KxGsXyUp7tVHfvQlqZ4yx/w==}
engines: {node: ^20.19.0 || >=22.12.0}
chownr@2.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
@ -3740,6 +3751,9 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
pinyin-pro@3.27.0:
resolution: {integrity: sha512-Osdgjwe7Rm17N2paDMM47yW+jUIUH3+0RGo8QP39ZTLpTaJVDK0T58hOLaMQJbcMmAebVuK2ePunTEVEx1clNQ==}
pixelmatch@5.3.0:
resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==}
hasBin: true
@ -4308,9 +4322,6 @@ packages:
tiny-async-pool@1.3.0:
resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==}
tiny-pinyin@1.3.2:
resolution: {integrity: sha512-uHNGu4evFt/8eNLldazeAM1M8JrMc1jshhJJfVRARTN3yT8HEEibofeQ7QETWQ5ISBjd6fKtTVBCC/+mGS6FpA==}
tiny-typed-emitter@2.1.0:
resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==}
@ -6359,6 +6370,10 @@ snapshots:
chalk@5.0.1: {}
chinese-conv@4.0.0: {}
chownr@2.0.0: {}
chownr@3.0.0: {}
chromium-pickle-js@0.2.0: {}
@ -8458,6 +8473,8 @@ snapshots:
picomatch@4.0.3: {}
pinyin-pro@3.27.0: {}
pixelmatch@5.3.0:
dependencies:
pngjs: 6.0.0
@ -9070,8 +9087,6 @@ snapshots:
dependencies:
semver: 5.7.2
tiny-pinyin@1.3.2: {}
tiny-typed-emitter@2.1.0: {}
tinycolor2@1.6.0: {}

View File

@ -891,6 +891,24 @@
"show-time-codes": {
"label": "Show time codes",
"tooltip": "Show the time codes next to the lyrics"
},
"convert-chinese-character": {
"label": "Convert Chinese character",
"submenu": {
"disabled": {
"label": "Disabled",
"tooltip": "Disable Chinese character conversion"
},
"simplified-to-traditional": {
"label": "Simplified to Traditional",
"tooltip": "Convert Simplified Chinese to Traditional Chinese"
},
"traditional-to-simplified": {
"label": "Traditional to Simplified",
"tooltip": "Convert Traditional Chinese to Simplified Chinese"
}
},
"tooltip": "Convert Chinese character to Traditional or Simplified"
}
},
"name": "Synced Lyrics",

View File

@ -153,6 +153,62 @@ export const menu = async (
});
},
},
{
label: t('plugins.synced-lyrics.menu.convert-chinese-character.label'),
toolTip: t(
'plugins.synced-lyrics.menu.convert-chinese-character.tooltip',
),
type: 'submenu',
submenu: [
{
label: t(
'plugins.synced-lyrics.menu.convert-chinese-character.submenu.disabled.label',
),
toolTip: t(
'plugins.synced-lyrics.menu.convert-chinese-character.submenu.disabled.tooltip',
),
type: 'radio',
checked:
config.convertChineseCharacter === 'disabled' ||
config.convertChineseCharacter === undefined,
click() {
ctx.setConfig({
convertChineseCharacter: 'disabled',
});
},
},
{
label: t(
'plugins.synced-lyrics.menu.convert-chinese-character.submenu.simplified-to-traditional.label',
),
toolTip: t(
'plugins.synced-lyrics.menu.convert-chinese-character.submenu.simplified-to-traditional.tooltip',
),
type: 'radio',
checked: config.convertChineseCharacter === 'simplifiedToTraditional',
click() {
ctx.setConfig({
convertChineseCharacter: 'simplifiedToTraditional',
});
},
},
{
label: t(
'plugins.synced-lyrics.menu.convert-chinese-character.submenu.traditional-to-simplified.label',
),
toolTip: t(
'plugins.synced-lyrics.menu.convert-chinese-character.submenu.traditional-to-simplified.tooltip',
),
type: 'radio',
checked: config.convertChineseCharacter === 'traditionalToSimplified',
click() {
ctx.setConfig({
convertChineseCharacter: 'traditionalToSimplified',
});
},
},
],
},
{
label: t('plugins.synced-lyrics.menu.show-time-codes.label'),
toolTip: t('plugins.synced-lyrics.menu.show-time-codes.tooltip'),

View File

@ -1,6 +1,11 @@
import { createEffect, createSignal, Show } from 'solid-js';
import { createEffect, createMemo, createSignal, Show } from 'solid-js';
import { canonicalize, romanize, simplifyUnicode } from '../utils';
import {
canonicalize,
convertChineseCharacter,
romanize,
simplifyUnicode,
} from '../utils';
import { config } from '../renderer';
interface PlainLyricsProps {
@ -9,11 +14,19 @@ interface PlainLyricsProps {
export const PlainLyrics = (props: PlainLyricsProps) => {
const [romanization, setRomanization] = createSignal('');
const text = createMemo(() => {
let line = props.line;
const convertChineseText = config()?.convertChineseCharacter;
if (convertChineseText && convertChineseText !== 'disabled') {
line = convertChineseCharacter(line, convertChineseText);
}
return line;
});
createEffect(() => {
if (!config()?.romanization) return;
const input = canonicalize(props.line);
const input = canonicalize(text());
romanize(input).then((result) => {
setRomanization(canonicalize(result));
});
@ -31,13 +44,13 @@ export const PlainLyrics = (props: PlainLyricsProps) => {
>
<yt-formatted-string
text={{
runs: [{ text: props.line }],
runs: [{ text: text() }],
}}
/>
<Show
when={
config()?.romanization &&
simplifyUnicode(props.line) !== simplifyUnicode(romanization())
simplifyUnicode(text()) !== simplifyUnicode(romanization())
}
>
<yt-formatted-string

View File

@ -7,7 +7,12 @@ import { type LineLyrics } from '@/plugins/synced-lyrics/types';
import { config, currentTime } from '../renderer';
import { _ytAPI } from '..';
import { canonicalize, romanize, simplifyUnicode } from '../utils';
import {
canonicalize,
convertChineseCharacter,
romanize,
simplifyUnicode,
} from '../utils';
interface SyncedLineProps {
scroller: VirtualizerHandle;
@ -81,7 +86,14 @@ const EmptyLine = (props: SyncedLineProps) => {
};
export const SyncedLine = (props: SyncedLineProps) => {
const text = createMemo(() => props.line.text.trim());
const text = createMemo(() => {
let line = props.line.text;
const convertChineseText = config()?.convertChineseCharacter;
if (convertChineseText && convertChineseText !== 'disabled') {
line = convertChineseCharacter(line, convertChineseText);
}
return line.trim();
});
const [romanization, setRomanization] = createSignal('');
createEffect(() => {

View File

@ -3,10 +3,11 @@ import KuromojiAnalyzer from 'kuroshiro-analyzer-kuromoji';
import Kuroshiro from 'kuroshiro';
import { romanize as esHangulRomanize } from 'es-hangul';
import hanja from 'hanja';
import * as pinyin from 'tiny-pinyin';
import { pinyin } from 'pinyin-pro';
import { romanize as romanizeThaiFrag } from '@dehoist/romanize-thai';
import { lazy } from 'lazy-var';
import { detect } from 'tinyld';
import { sify, tify } from 'chinese-conv';
import Sanscript from '@indic-transliteration/sanscript';
import { waitForElement } from '@/utils/wait-for-element';
@ -85,6 +86,20 @@ export const canonicalize = (text: string) => {
);
};
export const convertChineseCharacter = (
text: string,
mode: 'simplifiedToTraditional' | 'traditionalToSimplified',
) => {
if (!hasChinese([text])) return text;
switch (mode) {
case 'simplifiedToTraditional':
return tify(text);
case 'traditionalToSimplified':
return sify(text);
}
};
export const simplifyUnicode = (text?: string) =>
text
? text
@ -172,9 +187,9 @@ export const romanizeHangul = (line: string) =>
esHangulRomanize(hanja.translate(line, 'SUBSTITUTION'));
export const romanizeChinese = (line: string) => {
return line.replaceAll(/[\u4E00-\u9FFF]+/g, (match) =>
pinyin.convertToPinyin(match, ' ', true),
);
return line.replaceAll(/[\u4E00-\u9FFF]+/g, (match) => {
return pinyin(match, { separator: ' ' });
});
};
const thaiSegmenter = Intl.Segmenter.supportedLocalesOf('th').includes('th')

View File

@ -10,6 +10,10 @@ export type SyncedLyricsPluginConfig = {
showLyricsEvenIfInexact: boolean;
lineEffect: LineEffect;
romanization: boolean;
convertChineseCharacter?:
| 'simplifiedToTraditional'
| 'traditionalToSimplified'
| 'disabled';
};
export type LineLyricsStatus = 'previous' | 'current' | 'upcoming';