Skip to content

Commit

Permalink
Lyrics Translation and Romaji (Fulfill #732) [Translation Part] (#747)
Browse files Browse the repository at this point in the history
  • Loading branch information
ENDlezZenith authored Sep 24, 2024
1 parent e3946a9 commit 31492fa
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 46 deletions.
13 changes: 7 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,8 @@
"@tanstack/react-query-persist-client": "^4.32.1",
"@ts-rest/core": "^3.23.0",
"@xhayper/discord-rpc": "^1.0.24",
"auto-text-size": "^0.2.3",
"audiomotion-analyzer": "^4.5.0",
"auto-text-size": "^0.2.3",
"axios": "^1.6.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
Expand Down
3 changes: 2 additions & 1 deletion release/app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"trackNumber": "track",
"trackGain": "track gain",
"trackPeak": "track peak",
"translation": "translation",
"unknown": "unknown",
"version": "version",
"year": "year",
Expand Down Expand Up @@ -355,7 +356,8 @@
},
"lyrics": "lyrics",
"related": "related",
"upNext": "up next"
"upNext": "up next",
"visualizer": "visualizer"
},
"genreList": {
"showAlbums": "show $t(entity.genre_one) $t(entity.album_other)",
Expand Down Expand Up @@ -653,6 +655,12 @@
"transcodeBitrate_description": "selects the bitrate to transcode. 0 means let the server pick",
"transcodeFormat": "format to transcode",
"transcodeFormat_description": "selects the format to transcode. leave empty to let the server decide",
"translationApiProvider": "translation api provider",
"translationApiProvider_description": "api provider for translation",
"translationApiKey": "translation api key",
"translationApiKey_description": "api key for translation (Support global service endpoint only)",
"translationTargetLanguage": "translation target language",
"translationTargetLanguage_description": "target language for translation",
"trayEnabled": "show tray",
"trayEnabled_description": "show/hide tray icon/menu. if disabled, also disables minimize/exit to tray",
"useSystemTheme": "use system theme",
Expand Down
16 changes: 15 additions & 1 deletion src/renderer/features/lyrics/lyrics-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface LyricsActionsProps {
onRemoveLyric: () => void;
onResetLyric: () => void;
onSearchOverride: (params: LyricsOverride) => void;
onTranslateLyric: () => void;
setIndex: (idx: number) => void;
}

Expand All @@ -28,6 +29,7 @@ export const LyricsActions = ({
onRemoveLyric,
onResetLyric,
onSearchOverride,
onTranslateLyric,
setIndex,
}: LyricsActionsProps) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -120,7 +122,6 @@ export const LyricsActions = ({
{isDesktop && sources.length ? (
<Button
uppercase
color="red"
disabled={isActionsDisabled}
variant="subtle"
onClick={onRemoveLyric}
Expand All @@ -129,6 +130,19 @@ export const LyricsActions = ({
</Button>
) : null}
</Box>

<Box style={{ position: 'absolute', right: 0, top: -50 }}>
{isDesktop && sources.length ? (
<Button
uppercase
disabled={isActionsDisabled}
variant="subtle"
onClick={onTranslateLyric}
>
{t('common.translation', { postProcess: 'sentenceCase' })}
</Button>
) : null}
</Box>
</Box>
);
};
60 changes: 45 additions & 15 deletions src/renderer/features/lyrics/lyrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { ErrorBoundary } from 'react-error-boundary';
import { RiInformationFill } from 'react-icons/ri';
import styled from 'styled-components';
import { useSongLyricsByRemoteId, useSongLyricsBySong } from './queries/lyric-query';
import { translateLyrics } from './queries/lyric-translate';
import { SynchronizedLyrics, SynchronizedLyricsProps } from './synchronized-lyrics';
import { Spinner, TextTitle } from '/@/renderer/components';
import { ErrorFallback } from '/@/renderer/features/action-required';
import {
UnsynchronizedLyrics,
UnsynchronizedLyricsProps,
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
import { useCurrentSong, usePlayerStore } from '/@/renderer/store';
import { useCurrentSong, usePlayerStore, useLyricsSettings } from '/@/renderer/store';
import { FullLyricsMetadata, LyricSource, LyricsOverride } from '/@/renderer/api/types';
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
import { queryKeys } from '/@/renderer/api/query-keys';
Expand Down Expand Up @@ -84,7 +85,10 @@ const ScrollContainer = styled(motion.div)`

export const Lyrics = () => {
const currentSong = useCurrentSong();
const lyricsSettings = useLyricsSettings();
const [index, setIndex] = useState(0);
const [translatedLyrics, setTranslatedLyrics] = useState<string | null>(null);
const [showTranslation, setShowTranslation] = useState(false);

const { data, isInitialLoading } = useSongLyricsBySong(
{
Expand All @@ -96,6 +100,19 @@ export const Lyrics = () => {

const [override, setOverride] = useState<LyricsOverride | undefined>(undefined);

const [lyrics, synced] = useMemo(() => {
if (Array.isArray(data)) {
if (data.length > 0) {
const selectedLyric = data[Math.min(index, data.length)];
return [selectedLyric, selectedLyric.synced];
}
} else if (data?.lyrics) {
return [data, Array.isArray(data.lyrics)];
}

return [undefined, false];
}, [data, index]);

const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
setOverride(params);
}, []);
Expand Down Expand Up @@ -123,6 +140,27 @@ export const Lyrics = () => {
);
}, [currentSong?.id, currentSong?.serverId]);

const handleOnTranslateLyric = useCallback(async () => {
if (translatedLyrics) {
setShowTranslation(!showTranslation);
return;
}
if (!lyrics) return;
const originalLyrics = Array.isArray(lyrics.lyrics)
? lyrics.lyrics.map(([, line]) => line).join('\n')
: lyrics.lyrics;
const { translationApiKey, translationApiProvider, translationTargetLanguage } =
lyricsSettings;
const TranslatedText: string | null = await translateLyrics(
originalLyrics,
translationApiKey,
translationApiProvider,
translationTargetLanguage,
);
setTranslatedLyrics(TranslatedText);
setShowTranslation(true);
}, [lyrics, lyricsSettings, translatedLyrics, showTranslation]);

const { isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({
options: {
enabled: !!override,
Expand Down Expand Up @@ -150,19 +188,6 @@ export const Lyrics = () => {
};
}, []);

const [lyrics, synced] = useMemo(() => {
if (Array.isArray(data)) {
if (data.length > 0) {
const selectedLyric = data[Math.min(index, data.length)];
return [selectedLyric, selectedLyric.synced];
}
} else if (data?.lyrics) {
return [data, Array.isArray(data.lyrics)];
}

return [undefined, false];
}, [data, index]);

const languages = useMemo(() => {
if (Array.isArray(data)) {
return data.map((lyric, idx) => ({ label: lyric.lang, value: idx.toString() }));
Expand Down Expand Up @@ -203,10 +228,14 @@ export const Lyrics = () => {
transition={{ duration: 0.5 }}
>
{synced ? (
<SynchronizedLyrics {...(lyrics as SynchronizedLyricsProps)} />
<SynchronizedLyrics
{...(lyrics as SynchronizedLyricsProps)}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
) : (
<UnsynchronizedLyrics
{...(lyrics as UnsynchronizedLyricsProps)}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
)}
</ScrollContainer>
Expand All @@ -221,6 +250,7 @@ export const Lyrics = () => {
onRemoveLyric={handleOnRemoveLyric}
onResetLyric={handleOnResetLyric}
onSearchOverride={handleOnSearchOverride}
onTranslateLyric={handleOnTranslateLyric}
/>
</ActionsContainer>
</LyricsContainer>
Expand Down
50 changes: 50 additions & 0 deletions src/renderer/features/lyrics/queries/lyric-translate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import axios from 'axios';

export const translateLyrics = async (
originalLyrics: string,
translationApiKey: string,
translationApiProvider: string | null,
translationTargetLanguage: string | null,
) => {
let TranslatedText = '';
if (translationApiProvider === 'Microsoft Azure') {
try {
const response = await axios({
data: [
{
Text: originalLyrics,
},
],
headers: {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': translationApiKey,
},
method: 'post',
url: `https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${translationTargetLanguage as string}`,
});
TranslatedText = response.data[0].translations[0].text;
} catch (e) {
console.error('Microsoft Azure translate request got an error!', e);
return null;
}
} else if (translationApiProvider === 'Google Cloud') {
try {
const response = await axios({
data: {
format: 'text',
q: originalLyrics,
},
headers: {
'Content-Type': 'application/json',
},
method: 'post',
url: `https://translation.googleapis.com/language/translate/v2?target=${translationTargetLanguage as string}&key=${translationApiKey}`,
});
TranslatedText = response.data.data.translations[0].translatedText;
} catch (e) {
console.error('Google Cloud translate request got an error!', e);
return null;
}
}
return TranslatedText;
};
30 changes: 21 additions & 9 deletions src/renderer/features/lyrics/synchronized-lyrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const SynchronizedLyricsContainer = styled.div<{ $gap: number }>`

export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: SynchronizedLyricsArray;
translatedLyrics?: string | null;
}

export const SynchronizedLyrics = ({
Expand All @@ -63,6 +64,7 @@ export const SynchronizedLyrics = ({
name,
remote,
source,
translatedLyrics,
}: SynchronizedLyricsProps) => {
const playersRef = PlayersRef;
const status = useCurrentStatus();
Expand Down Expand Up @@ -364,15 +366,25 @@ export const SynchronizedLyrics = ({
/>
)}
{lyrics.map(([time, text], idx) => (
<LyricLine
key={idx}
alignment={settings.alignment}
className="lyric-line synchronized"
fontSize={settings.fontSize}
id={`lyric-${idx}`}
text={text}
onClick={() => handleSeek(time / 1000)}
/>
<div key={idx}>
<LyricLine
alignment={settings.alignment}
className="lyric-line synchronized"
fontSize={settings.fontSize}
id={`lyric-${idx}`}
text={text}
onClick={() => handleSeek(time / 1000)}
/>
{translatedLyrics && (
<LyricLine
alignment={settings.alignment}
className="lyric-line synchronized translation"
fontSize={settings.fontSize * 0.8}
text={translatedLyrics.split('\n')[idx]}
onClick={() => handleSeek(time / 1000)}
/>
)}
</div>
))}
</SynchronizedLyricsContainer>
);
Expand Down
Loading

0 comments on commit 31492fa

Please sign in to comment.