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)) 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 } })