113 lines
3.2 KiB
TypeScript
113 lines
3.2 KiB
TypeScript
import { yue2Configured } from '~/server/utils/yue2'
|
|
import { addStudioJob, kickStudioQueue, listStudioJobs, videoJobsBusy } from '~/server/utils/studioQueue'
|
|
import {
|
|
clampMusicDuration,
|
|
parseMusicEngine,
|
|
parseMusicSeed,
|
|
sanitizeMusicLyrics,
|
|
sanitizeMusicTags,
|
|
yueLyricsProblem
|
|
} from '~/utils/music'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const body = await readBody<{
|
|
tags?: unknown
|
|
lyrics?: unknown
|
|
duration?: unknown
|
|
seed?: unknown
|
|
folderId?: unknown
|
|
name?: unknown
|
|
engine?: unknown
|
|
}>(event).catch(() => ({} as Record<string, unknown>))
|
|
|
|
const engine = parseMusicEngine(body.engine)
|
|
if (engine !== 'yue2') {
|
|
throw createError({ statusCode: 400, statusMessage: 'Only YuE2 is supported for music generation.' })
|
|
}
|
|
if (!yue2Configured()) {
|
|
throw createError({ statusCode: 503, statusMessage: 'YuE2 host is not configured. Set COMFY_CONTROL_URL.' })
|
|
}
|
|
|
|
const tags = sanitizeMusicTags(body.tags)
|
|
const lyrics = sanitizeMusicLyrics(body.lyrics, false)
|
|
if (!tags) {
|
|
throw createError({ statusCode: 400, statusMessage: 'Genre and style tags are required' })
|
|
}
|
|
const lyricsProblem = yueLyricsProblem(lyrics)
|
|
if (lyricsProblem) {
|
|
throw createError({ statusCode: 400, statusMessage: lyricsProblem })
|
|
}
|
|
|
|
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)
|
|
|
|
const duration = clampMusicDuration(body.duration)
|
|
const seed = parseMusicSeed(body.seed)
|
|
const clipName = allocateOutputName(ownerKey, folderId, String(body.name || '').trim(), {
|
|
engine: 'yue2'
|
|
})
|
|
|
|
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: 0,
|
|
turbo: false,
|
|
seed,
|
|
cfg: 0,
|
|
fps: 0,
|
|
samplerName: 'euler',
|
|
scheduler: 'simple',
|
|
duration,
|
|
sound: true,
|
|
workflow: 'v1',
|
|
useIdentityRefs: false,
|
|
hideThumbnail: false,
|
|
folderLocked,
|
|
referenceStillIds: [null, null, null, null],
|
|
extensions: [],
|
|
queueAutoRun: false,
|
|
lyrics,
|
|
instrumental: false,
|
|
lyricsStrength: 0,
|
|
musicEngine: 'yue2'
|
|
}
|
|
})
|
|
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,
|
|
folderId,
|
|
folderLocked,
|
|
engine: 'yue2' as const,
|
|
name: clipName
|
|
}
|
|
})
|