4 Commits

Author SHA1 Message Date
azalea 460566c792 Update README.md 2025-11-23 01:25:42 +08:00
azalea 3ae05f27da [+] Step-by-step buttons 2025-11-23 01:20:31 +08:00
azalea fc22edd3d9 [O] don't repeat code 2025-11-23 01:15:14 +08:00
azalea 36651c0f6a [+] Singing mode button 2025-11-23 01:12:33 +08:00
8 changed files with 44 additions and 21 deletions
+2 -1
View File
@@ -23,10 +23,11 @@ Practice Japanese Karaoke lyrics reading and typing at the same time with KaraDa
* [ ] 音域分析(自动推荐升降调幅度) * [ ] 音域分析(自动推荐升降调幅度)
* [ ] 电视模式 * [ ] 电视模式
* [ ] 和手机配对、用手机点歌 * [ ] 和手机配对、用手机点歌
* [ ] 从网易云搜索歌曲
## Technical Tasks ## Technical Tasks
* [ ] i18n * [x] i18n
* [ ] 404 page * [ ] 404 page
* [ ] Update an existing playlist * [ ] Update an existing playlist
* [ ] Allow users to correct lyric pronunciations through correction feedback * [ ] Allow users to correct lyric pronunciations through correction feedback
+2 -1
View File
@@ -86,7 +86,8 @@ export default {
song: { song: {
mode: { mode: {
typing: 'Typing Mode', typing: 'Typing Mode',
music: 'Music Mode' music: 'Music Mode',
karaoke: 'Singing Mode'
}, },
karaoke: { karaoke: {
noVocals: 'No vocal separation track detected, cannot adjust vocal volume. Please process in song details page first.' noVocals: 'No vocal separation track detected, cannot adjust vocal volume. Please process in song details page first.'
+2 -1
View File
@@ -86,7 +86,8 @@ export default {
song: { song: {
mode: { mode: {
typing: 'タイピングモード', typing: 'タイピングモード',
music: '音楽モード' music: '音楽モード',
karaoke: '歌うモード'
}, },
karaoke: { karaoke: {
noVocals: 'ボーカル分離トラックが検出されないため、ボーカル音量を調整できません。まず曲の詳細ページで処理してください。' noVocals: 'ボーカル分離トラックが検出されないため、ボーカル音量を調整できません。まず曲の詳細ページで処理してください。'
+2 -1
View File
@@ -86,7 +86,8 @@ export default {
song: { song: {
mode: { mode: {
typing: '打字模式', typing: '打字模式',
music: '音乐模式' music: '音乐模式',
karaoke: '唱歌模式'
}, },
karaoke: { karaoke: {
noVocals: '未检测到人声分离音轨,无法调节人声音量。请先在歌曲详情页进行处理。' noVocals: '未检测到人声分离音轨,无法调节人声音量。请先在歌曲详情页进行处理。'
+9 -9
View File
@@ -140,7 +140,7 @@ export const getSongUrl = async (id: number | string) => {
// ///////////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////////
// API for Song Preparation // API for Song Preparation
export interface ProgressItem { task: string, progress: number } export interface ProgressItem { id: string, task: string, progress: number }
export interface SongProcessState { items: ProgressItem[], status: 'running' | 'done' | 'error' } export interface SongProcessState { items: ProgressItem[], status: 'running' | 'done' | 'error' }
const songProcessingStatus = new Map<number, SongProcessState>() const songProcessingStatus = new Map<number, SongProcessState>()
@@ -152,20 +152,20 @@ export const prepareSong = async (songId: number) => {
const state: SongProcessState = { items: [], status: 'running' } const state: SongProcessState = { items: [], status: 'running' }
songProcessingStatus.set(songId, state) songProcessingStatus.set(songId, state)
const addTask = (task: string) => ({ task, progress: 0 }).also(it => state.items.push(it)) const addTask = (id: string, task: string) => ({ id, task, progress: 0 }).also(it => state.items.push(it))
try { try {
// 1. Get Lyrics // 1. Get Lyrics
const taskLyrics = addTask('从网易云获取歌词') const taskLyrics = addTask('lyrics', '从网易云获取歌词')
const raw = await getLyricsRaw(songId) const raw = await getLyricsRaw(songId)
taskLyrics.progress = 1 taskLyrics.progress = 1
if (raw.lang !== 'jpn') { if (raw.lang !== 'jpn') {
addTask('错误: 不是日语歌曲').progress = -1 addTask('error', '错误: 不是日语歌曲').progress = -1
return state.status = 'error' return state.status = 'error'
} }
// 2. AI Process // 2. AI Process
const taskAI = addTask('AI 标注歌词读音') const taskAI = addTask('ai', 'AI 标注歌词读音')
// Check cache // Check cache
if (await checkLyricsProcessed(songId)) taskAI.progress = 1 if (await checkLyricsProcessed(songId)) taskAI.progress = 1
@@ -176,12 +176,12 @@ export const prepareSong = async (songId: number) => {
} }
// 3. Audio // 3. Audio
const taskAudio = addTask('从网易云获取音乐') const taskAudio = addTask('music', '从网易云获取音乐')
await getSongUrl(songId) await getSongUrl(songId)
taskAudio.progress = 1 taskAudio.progress = 1
// 4. Source Separation // 4. Source Separation
const taskSeparation = addTask('AI 人声分离') const taskSeparation = addTask('separation', 'AI 人声分离')
const inputPath = path.join(CACHE_DIR, `${songId}/exhigh.mp3`) const inputPath = path.join(CACHE_DIR, `${songId}/exhigh.mp3`)
const outputDir = path.join(CACHE_DIR, `${songId}`) const outputDir = path.join(CACHE_DIR, `${songId}`)
@@ -189,14 +189,14 @@ export const prepareSong = async (songId: number) => {
await separateSong(inputPath, outputDir) await separateSong(inputPath, outputDir)
taskSeparation.progress = 1 taskSeparation.progress = 1
} catch (e: any) { } catch (e: any) {
addTask(`错误: ${e.message}`).progress = -1 addTask('error', `错误: ${e.message}`).progress = -1
// Don't fail the whole process, just this step // Don't fail the whole process, just this step
} }
state.status = 'done' state.status = 'done'
} catch (e) { } catch (e) {
addTask(`错误: ${eToString(e)}`).progress = -1 addTask('error', `错误: ${eToString(e)}`).progress = -1
state.status = 'error' state.status = 'error'
} }
} }
+27 -6
View File
@@ -11,6 +11,19 @@
const t = getI18n().song.mode const t = getI18n().song.mode
let { data } = $props() let { data } = $props()
let taskStatus = $state({
lyrics: false,
ai: false,
music: false,
separation: false
})
let modes = $derived([
{ icon: "i-material-symbols:keyboard-rounded", label: t.typing, url: `/song/${data.song.id}/play`, disabled: !taskStatus.lyrics || !taskStatus.ai },
{ icon: "i-material-symbols:music-note-rounded", label: t.music, url: `/song/${data.song.id}/play?music=true`, disabled: !taskStatus.music },
{ icon: "i-material-symbols:mic-rounded", label: t.karaoke, url: `/song/${data.song.id}/karaoke`, disabled: !taskStatus.separation },
])
let loadStatus = $state<"idle" | "loading" | "done">("idle") let loadStatus = $state<"idle" | "loading" | "done">("idle")
let progressItems = $state<any[]>([]) let progressItems = $state<any[]>([])
let progressPercentage = $state(0) let progressPercentage = $state(0)
@@ -27,6 +40,15 @@
const state = res.status const state = res.status
if (state && state.items) { if (state && state.items) {
// Update task status
for (const item of state.items) {
if (item.progress !== 1) continue
if (item.id === 'lyrics') taskStatus.lyrics = true
if (item.id === 'ai') taskStatus.ai = true
if (item.id === 'music') taskStatus.music = true
if (item.id === 'separation') taskStatus.separation = true
}
progressItems = state.items.map((item: any) => ({ progressItems = state.items.map((item: any) => ({
title: item.task + (item.progress > 0 && item.progress < 1 ? ` (${Math.round(item.progress * 100)}%)` : ''), title: item.task + (item.progress > 0 && item.progress < 1 ? ` (${Math.round(item.progress * 100)}%)` : ''),
icon: item.progress === 1 ? 'i-material-symbols:check text-green-500' : icon: item.progress === 1 ? 'i-material-symbols:check text-green-500' :
@@ -53,9 +75,8 @@
<ProgressList percentage={progressPercentage} items={progressItems} /> <ProgressList percentage={progressPercentage} items={progressItems} />
{#if loadStatus === "done"} <div class="hbox gap-4 p-16px flex-wrap">
<div class="hbox gap-4 p-16px"> {#each modes as mode}
<Button big icon="i-material-symbols:keyboard-rounded" onclick={() => goto(`/song/${data.song.id}/play`)}>{t.typing}</Button> <Button big icon={mode.icon} onclick={() => goto(mode.url)} disabled={mode.disabled} class="!w-auto !min-w-[calc(50%-8px)] grow disabled:opacity-50 disabled:cursor-not-allowed">{mode.label}</Button>
<Button big icon="i-material-symbols:music-note-rounded" onclick={() => goto(`/song/${data.song.id}/play?music=true`)}>{t.music}</Button> {/each}
</div> </div>
{/if}
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { PageProps } from "./$types" import type { PageProps } from "./$types"
import { LinearProgress } from "m3-svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import { typingSettingsDefault } from "$lib/types" import { typingSettingsDefault } from "$lib/types"
import { processLrcLine, dedupLines, type ProcLrcLine } from "$lib/ui/player/IMEHelper" import { processLrcLine, dedupLines, type ProcLrcLine } from "$lib/ui/player/IMEHelper"
-1
View File
@@ -8,7 +8,6 @@
import "$lib/ext.ts" import "$lib/ext.ts"
import { API } from "$lib/client.ts" import { API } from "$lib/client.ts"
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
import { artistAndAlbum } from "$lib/utils.ts"
import { MusicControl } from "$lib/ui/player/MusicControl.ts" import { MusicControl } from "$lib/ui/player/MusicControl.ts"
import Lyrics from "$lib/ui/player/Lyrics.svelte" import Lyrics from "$lib/ui/player/Lyrics.svelte"
import PlayerAppBar from "$lib/ui/player/PlayerAppBar.svelte" import PlayerAppBar from "$lib/ui/player/PlayerAppBar.svelte"