[+] Connect to source separation server
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
import { error } from 'console'
|
||||||
|
import { promises as fs } from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const API_URL = process.env.AUDIO_SEPARATOR_API
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separates a song into vocals and instrumental tracks using the Python API server.
|
||||||
|
* @param inputPath Absolute path to the input audio file.
|
||||||
|
* @param outputDir Directory where the output files (vocals.mp3, instrumental.mp3) will be saved.
|
||||||
|
*/
|
||||||
|
export async function separateSong(inputPath: string, outputDir: string) {
|
||||||
|
const vocalsPath = path.join(outputDir, 'vocals.opus')
|
||||||
|
const instrumentalPath = path.join(outputDir, 'instrumental.opus')
|
||||||
|
if (await fs.exists(vocalsPath) && await fs.exists(instrumentalPath)) return
|
||||||
|
|
||||||
|
// Read file and create FormData
|
||||||
|
const fileBuffer = await fs.readFile(inputPath)
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', new Blob([fileBuffer]), 'input.mp3')
|
||||||
|
|
||||||
|
const res = await fetch(`${API_URL}/separate`, { method: 'POST', body: formData })
|
||||||
|
if (!res.ok) throw error(500, '无法连接到分离服务')
|
||||||
|
const { task_id } = await res.json()
|
||||||
|
|
||||||
|
// Poll for completion
|
||||||
|
while (true) {
|
||||||
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
|
const statusRes = await fetch(`${API_URL}/status/${task_id}`)
|
||||||
|
const status = await statusRes.json()
|
||||||
|
|
||||||
|
if (status.status === 'completed') {
|
||||||
|
// Download results
|
||||||
|
const downloadStem = async (stem: string, dest: string) => {
|
||||||
|
const fileRes = await fetch(`${API_URL}/result/${task_id}/${stem}`)
|
||||||
|
if (!fileRes.ok) throw error(500, `无法下载 ${stem}`)
|
||||||
|
const buffer = await fileRes.arrayBuffer()
|
||||||
|
await fs.writeFile(dest, Buffer.from(buffer))
|
||||||
|
}
|
||||||
|
|
||||||
|
await downloadStem('vocals', vocalsPath)
|
||||||
|
await downloadStem('instrumental', instrumentalPath)
|
||||||
|
|
||||||
|
// Clean up task on server
|
||||||
|
try {
|
||||||
|
await fetch(`${API_URL}/task/${task_id}`, { method: 'DELETE' })
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`无法清理任务 ${task_id}:`, e)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (status.status === 'error') throw error(500, status.error || '分离失败')
|
||||||
|
if (status.status === 'not_found') throw error(500, '任务丢失')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkAudioSeparator() {
|
||||||
|
if (!API_URL) return console.warn('未设置音频分离服务地址,跳过连接性检查')
|
||||||
|
try {
|
||||||
|
const res = await fetch(API_URL)
|
||||||
|
if (res.ok) console.log(`音频分离服务连接成功 ${API_URL}`)
|
||||||
|
else console.warn(`音频分离服务返回状态 ${res.status} at ${API_URL}`)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`无法连接到音频分离服务 ${API_URL}:`, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-19
@@ -8,6 +8,8 @@ import type { ObjectId } from 'mongodb'
|
|||||||
import '../ext'
|
import '../ext'
|
||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { waitFor } from '../utils'
|
||||||
|
import { separateSong } from './separator'
|
||||||
|
|
||||||
const CACHE_DIR = path.resolve('static/audio')
|
const CACHE_DIR = path.resolve('static/audio')
|
||||||
|
|
||||||
@@ -171,26 +173,15 @@ export const prepareSong = async (songId: number) => {
|
|||||||
|
|
||||||
// 4. Source Separation
|
// 4. Source Separation
|
||||||
const taskSeparation = addTask('AI 人声分离')
|
const taskSeparation = addTask('AI 人声分离')
|
||||||
const vocalsPath = path.join(CACHE_DIR, `${songId}/vocals.mp3`)
|
const inputPath = path.join(CACHE_DIR, `${songId}/exhigh.mp3`)
|
||||||
|
const outputDir = path.join(CACHE_DIR, `${songId}`)
|
||||||
let retries = 0
|
|
||||||
// Wait up to 10 minutes (separation can be slow on CPU)
|
|
||||||
while (retries < 600) {
|
|
||||||
try {
|
|
||||||
await fs.access(vocalsPath)
|
|
||||||
taskSeparation.progress = 1
|
|
||||||
break
|
|
||||||
} catch {
|
|
||||||
// File doesn't exist yet
|
|
||||||
await new Promise(r => setTimeout(r, 1000))
|
|
||||||
retries++
|
|
||||||
// Update progress to show it's alive
|
|
||||||
if (retries % 5 === 0) taskSeparation.progress = Math.min(0.99, retries / 600)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskSeparation.progress < 1) {
|
try {
|
||||||
addTask('警告: AI 人声分离超时,请检查后台脚本').progress = -1
|
await separateSong(inputPath, outputDir)
|
||||||
|
taskSeparation.progress = 1
|
||||||
|
} catch (e: any) {
|
||||||
|
addTask(`错误: ${e.message}`).progress = -1
|
||||||
|
// Don't fail the whole process, just this step
|
||||||
}
|
}
|
||||||
|
|
||||||
state.status = 'done'
|
state.status = 'done'
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const totalProgress = state.items.reduce((acc: number, cur: any) => acc + Math.max(0, cur.progress), 0)
|
const totalProgress = state.items.reduce((acc: number, cur: any) => acc + Math.max(0, cur.progress), 0)
|
||||||
progressPercentage = Math.min(100, Math.round((totalProgress / 3) * 100))
|
progressPercentage = Math.min(100, Math.round((totalProgress / 4) * 100))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status === "done") {
|
if (state.status === "done") {
|
||||||
|
|||||||
Reference in New Issue
Block a user