Files
aigen/server/api/generate/music.post.ts
T

156 lines
5.4 KiB
TypeScript

import { yueGpConfigured } from '~/server/utils/yueGp'
import { yue2Configured } from '~/server/utils/yue2'
import { addStudioJob, kickStudioQueue, listStudioJobs, videoJobsBusy } from '~/server/utils/studioQueue'
import { comfyConfigured } from '~/server/utils/comfy'
import {
clampLyricsStrength,
clampMusicCfg,
clampMusicDuration,
clampMusicSteps,
MUSIC_CFG_DEFAULT,
MUSIC_CFG_DEFAULT_15,
MUSIC_STEPS_DEFAULT,
MUSIC_STEPS_DEFAULT_15,
parseMusicEngine,
parseMusicSeed,
sanitizeMusicLyrics,
sanitizeMusicTags,
yueLyricsProblem
} from '~/utils/music'
export default defineEventHandler(async (event) => {
const body = await readBody<{
tags?: unknown
lyrics?: unknown
duration?: unknown
steps?: unknown
seed?: unknown
cfg?: unknown
lyricsStrength?: unknown
instrumental?: unknown
folderId?: unknown
name?: unknown
yueProfile?: unknown
engine?: unknown
sampler_name?: unknown
scheduler?: unknown
}>(event).catch(() => ({} as Record<string, unknown>))
const engine = parseMusicEngine(body.engine)
if (engine === 'yue' && !yueGpConfigured()) throw createError({ statusCode: 503, statusMessage: 'YuEGP host is not configured. Set COMFY_CONTROL_URL.' })
if (engine === 'yue2' && !yue2Configured()) throw createError({ statusCode: 503, statusMessage: 'YuE2 host is not configured. Set COMFY_CONTROL_URL.' })
if (engine !== 'yue' && engine !== 'yue2' && !comfyConfigured()) {
throw createError({
statusCode: 503,
statusMessage: 'Beast ComfyUI is not configured. Set COMFY_HOST.'
})
}
const instrumental = body.instrumental === true || body.instrumental === 'true'
const tags = sanitizeMusicTags(body.tags)
const lyrics = sanitizeMusicLyrics(body.lyrics, instrumental)
if (!tags) {
throw createError({ statusCode: 400, statusMessage: 'Genre and style tags are required' })
}
if (!instrumental && !lyrics) {
throw createError({ statusCode: 400, statusMessage: 'Lyrics are required unless Instrumental is on' })
}
const ownerKey = libraryOwnerKey(event)
const library = publicLibrary(event)
const folderId = library.folders.some(folder => folder.id === String(body.folderId || ''))
? String(body.folderId)
: library.folders[0]?.id
if (!folderId) {
throw createError({ statusCode: 400, statusMessage: 'Create a library folder before generating' })
}
assertFolderExists(event, folderId)
const destFolder = library.folders.find(folder => folder.id === folderId)
const folderLocked = Boolean(destFolder?.protected && !destFolder.unlocked)
if ((engine === 'yue' || engine === 'yue2') && instrumental) throw createError({ statusCode: 400, statusMessage: 'YuE currently requires lyrics. Use ACE for instrumental music.' })
const yueProfile = body.yueProfile ?? 1
if (engine === 'yue' && yueProfile !== 1 && yueProfile !== 3) throw createError({ statusCode: 400, statusMessage: 'Choose YuEGP profile 1 or manual fallback 3.' })
if (engine === 'yue' && !instrumental) {
const problem = yueLyricsProblem(lyrics)
if (problem) throw createError({ statusCode: 400, statusMessage: problem })
}
if (engine === 'yue2' && !instrumental && !lyrics) {
throw createError({ statusCode: 400, statusMessage: 'YuE2 needs lyrics.' })
}
const duration = clampMusicDuration(body.duration)
if ((engine === 'yue' || engine === 'yue2') && duration > 150) throw createError({ statusCode: 400, statusMessage: 'YuE supports up to 150 seconds per section.' })
const steps = clampMusicSteps(
body.steps,
engine === 'ace-step-1.5' ? MUSIC_STEPS_DEFAULT_15 : MUSIC_STEPS_DEFAULT
)
const cfg = clampMusicCfg(
body.cfg,
engine === 'ace-step-1.5' ? MUSIC_CFG_DEFAULT_15 : MUSIC_CFG_DEFAULT
)
const lyricsStrength = clampLyricsStrength(body.lyricsStrength)
const seed = parseMusicSeed(body.seed)
const clipName = allocateOutputName(ownerKey, folderId, String(body.name || '').trim(), {
engine: engine === 'ace-step-1.5' ? 'ace15' : engine === 'yue' ? 'yue' : engine === 'yue2' ? 'yue2' : 'ace'
})
const samplerName = String(body.sampler_name || 'euler')
const scheduler = String(body.scheduler || 'simple')
const familyId = crypto.randomUUID()
const studio = await addStudioJob({
ownerKey,
familyId,
kind: 'music',
payload: {
prompt: tags,
name: clipName,
folderId,
aspect: 'audio',
width: 0,
height: 0,
steps,
turbo: false,
seed,
cfg,
fps: 0,
samplerName,
scheduler,
duration,
sound: !instrumental,
workflow: 'v1',
useIdentityRefs: false,
hideThumbnail: false,
folderLocked,
referenceStillIds: [null, null, null, null],
extensions: [],
queueAutoRun: false,
lyrics,
instrumental,
lyricsStrength,
musicEngine: engine,
...(engine === 'yue' ? { yueProfile: yueProfile as 1 | 3 } : {})
}
})
await kickStudioQueue()
let latest = listStudioJobs(ownerKey).find(item => item.id === studio.id)
if (!latest?.liveJobId && !(await videoJobsBusy())) {
await kickStudioQueue()
latest = listStudioJobs(ownerKey).find(item => item.id === studio.id)
}
const liveJobId = latest?.liveJobId || ''
return {
jobId: liveJobId || studio.id,
studioJobId: studio.id,
queued: !liveJobId,
seed,
duration,
steps,
cfg,
folderId,
folderLocked,
engine,
name: clipName
}
})