Tighten YuE lyrics validation and Comfy music workflow wiring.
Require section tags before queueing YuE and keep the ACE/YuE path changes that were already in progress. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -72,7 +72,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
return {
|
||||
jobId: video?.jobId || edit?.jobId || music?.jobId || null,
|
||||
jobId: video?.jobId || edit?.jobId || null,
|
||||
video,
|
||||
edit,
|
||||
music
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
parseMusicEngine,
|
||||
parseMusicSeed,
|
||||
sanitizeMusicLyrics,
|
||||
sanitizeMusicTags
|
||||
sanitizeMusicTags,
|
||||
yueLyricsProblem
|
||||
} from '~/utils/music'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -62,6 +63,10 @@ export default defineEventHandler(async (event) => {
|
||||
const folderLocked = Boolean(destFolder?.protected && !destFolder.unlocked)
|
||||
|
||||
const engine = parseMusicEngine(body.engine)
|
||||
if (engine === 'yue' && !instrumental) {
|
||||
const problem = yueLyricsProblem(lyrics)
|
||||
if (problem) throw createError({ statusCode: 400, statusMessage: problem })
|
||||
}
|
||||
const duration = clampMusicDuration(body.duration)
|
||||
const steps = clampMusicSteps(
|
||||
body.steps,
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
"genres_prompt": "inspiring female uplifting pop airy vocal electronic bright",
|
||||
"lyrics_prompt": "[verse]\n\n[chorus]\n",
|
||||
"seed": 0,
|
||||
"run_n_segment": 2,
|
||||
"run_n_segment": 1,
|
||||
"repetition_penalty": 1.1,
|
||||
"prompt_start_time": 0,
|
||||
"prompt_end_time": 45,
|
||||
"prompt_end_time": 30,
|
||||
"max_new_tokens": 2944,
|
||||
"use_dual_tracks_prompt": false,
|
||||
"use_audio_prompt": false,
|
||||
|
||||
@@ -195,6 +195,16 @@ export async function interruptComfy() {
|
||||
return Boolean(res?.ok)
|
||||
}
|
||||
|
||||
/** Ask Comfy to drop cached models so a heavy YuE job starts with a clean 16GB card. */
|
||||
export async function freeComfyVram() {
|
||||
await comfyFetch('/free', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ unload_models: true, free_memory: true }),
|
||||
signal: AbortSignal.timeout(15000)
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
export async function fetchHistory(promptId: string) {
|
||||
const res = await comfyFetch(`/history/${encodeURIComponent(promptId)}`)
|
||||
if (!res.ok) return null
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createJob, emitJob, type Job } from '~/server/utils/jobs'
|
||||
import { extractAudio, fetchHistory, fetchHistoryAll, findHistoryAudio, purgeComfyArtifacts, queuePrompt } from '~/server/utils/comfy'
|
||||
import { extractAudio, fetchHistory, fetchHistoryAll, findHistoryAudio, freeComfyVram, purgeComfyArtifacts, queuePrompt } from '~/server/utils/comfy'
|
||||
import { comfyWsUrl } from '~/server/utils/comfy'
|
||||
import { ensureComfyReady } from '~/server/utils/comfyLifecycle'
|
||||
import { downloadComfyAudio, saveTrack } from '~/server/utils/library'
|
||||
@@ -33,7 +33,7 @@ function sleep(ms: number) {
|
||||
function classifyError(message: string) {
|
||||
const lower = message.toLowerCase()
|
||||
if (lower.includes('out of memory') || lower.includes('exceed allowed memory') || (lower.includes('cuda') && lower.includes('alloc')) || lower.includes('vram')) {
|
||||
return 'ComfyUI ran out of VRAM. For YuE use shorter lyrics (fewer sections) or ACE-Step instead.'
|
||||
return 'ComfyUI ran out of VRAM on YuE. Sleep→Poke Comfy, don’t run video/image first, use one short [Chorus] only — or use ACE-Step.'
|
||||
}
|
||||
if (lower.includes('econnrefused') || lower.includes('unreachable') || lower.includes('fetch failed')) {
|
||||
return 'ComfyUI host connection dropped. Confirm the desktop instance is running.'
|
||||
@@ -372,6 +372,10 @@ export async function startMusicJob(params: MusicJobParams) {
|
||||
emitJob(job, { type: 'status', message: `Queueing ${engineLabel}...`, progress: 6 })
|
||||
await waitForSocket(job, 4000)
|
||||
await assertMusicEngineNodes(params.engine)
|
||||
if (params.engine === 'yue') {
|
||||
emitJob(job, { type: 'status', message: 'Freeing Comfy VRAM for YuE…', progress: 5 })
|
||||
await freeComfyVram()
|
||||
}
|
||||
const graph = buildMusicWorkflow({
|
||||
tags: params.tags,
|
||||
lyrics: params.instrumental ? '' : params.lyrics,
|
||||
|
||||
@@ -26,8 +26,8 @@ type WorkflowGraph = Record<string, WorkflowNode>
|
||||
|
||||
const ACE_V1_CKPT = 'ace_step_v1_3.5b.safetensors'
|
||||
const ACE_15_CKPT = 'ace_step_1.5_turbo_aio.safetensors'
|
||||
const YUE_S1_DEFAULT = 'C:\\Users\\ianjm\\AppData\\Local\\Comfy-Desktop\\ComfyUI-Shared\\models\\YuE\\YuE-s1-7B-anneal-en-cot'
|
||||
const YUE_S2_DEFAULT = 'C:\\Users\\ianjm\\AppData\\Local\\Comfy-Desktop\\ComfyUI-Shared\\models\\YuE\\YuE-s2-1B-general'
|
||||
const YUE_S1_DEFAULT = 'C:\\Users\\ianjm\\AppData\\Local\\Comfy-Desktop\\ComfyUI-Shared\\models\\yue\\YuE-s1-7B-anneal-en-cot'
|
||||
const YUE_S2_DEFAULT = 'C:\\Users\\ianjm\\AppData\\Local\\Comfy-Desktop\\ComfyUI-Shared\\models\\yue\\YuE-s2-1B-general'
|
||||
const YUE_NODE = 'YUE_Stage_A_Loader'
|
||||
|
||||
export function musicFilenamePrefix() {
|
||||
@@ -115,14 +115,14 @@ function buildYueWorkflow(params: MusicWorkflowParams): WorkflowGraph {
|
||||
const graph = structuredClone(workflowYue as WorkflowGraph)
|
||||
const prefix = params.filenamePrefix || musicFilenamePrefix()
|
||||
const tags = params.tags
|
||||
const lyrics = params.lyrics || '[instrumental]\n'
|
||||
const lyrics = params.lyrics || '[Instrumental]\n'
|
||||
// Beast 5080 16GB recipe (keep in sync with workflow_yue.json):
|
||||
// - mmgp profile 2 only (3–5 enable quanto and crash under Comfy InferenceMode)
|
||||
// - max 2 Stage-A segments (3+ OOMs)
|
||||
// - exactly 1 Stage-A lyric section (2+ still OOMs beside xcodec)
|
||||
// - max_new_tokens floor is 2944 (node min); do not go lower
|
||||
// - Triton/torch.compile off on Windows
|
||||
const sections = lyrics.split(/\[[^\]]+\]/g).filter(part => part.trim()).length || 2
|
||||
const segments = Math.max(1, Math.min(2, sections))
|
||||
// - xcodec stays on CPU during Stage A (node patch)
|
||||
const segments = 1
|
||||
for (const node of Object.values(graph)) {
|
||||
if (node.class_type === 'YUE_Stage_A_Loader') {
|
||||
node.inputs.stage_A_repo = yueStageARepo()
|
||||
@@ -138,10 +138,11 @@ function buildYueWorkflow(params: MusicWorkflowParams): WorkflowGraph {
|
||||
node.inputs.stage2_batch_size = 1
|
||||
}
|
||||
if (node.class_type === 'YUE_Stage_A_Sampler') {
|
||||
node.inputs.genres_prompt = tags.replace(/,/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
// Genres go to the model prompt only. Stage-A npy filenames are short (Windows MAX_PATH).
|
||||
node.inputs.genres_prompt = tags.replace(/,/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 400)
|
||||
node.inputs.lyrics_prompt = lyrics
|
||||
node.inputs.seed = params.seed
|
||||
node.inputs.prompt_end_time = Math.min(params.duration, 45)
|
||||
node.inputs.prompt_end_time = Math.min(params.duration, 30)
|
||||
node.inputs.run_n_segment = segments
|
||||
node.inputs.max_new_tokens = 2944
|
||||
node.inputs.use_dual_tracks_prompt = false
|
||||
|
||||
Reference in New Issue
Block a user