import workflowAceStep from '../assets/workflow_ace_step.json' import workflowAceStep15 from '../assets/workflow_ace_step_15.json' import workflowYue from '../assets/workflow_yue.json' import type { MusicEngine } from '~/utils/music' import { parseMusicBpm } from '~/utils/music' export type MusicWorkflowParams = { tags: string lyrics: string duration: number steps: number seed: number cfg: number lyricsStrength: number samplerName?: string scheduler?: string filenamePrefix?: string checkpoint?: string engine?: MusicEngine vocalMultiplier?: number bpm?: number } type WorkflowNode = { class_type: string; inputs: Record; _meta?: { title?: string } } type WorkflowGraph = Record 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_NODE = 'YUE_Stage_A_Loader' export function musicFilenamePrefix() { return String(useRuntimeConfig().comfyAudioPrefix || process.env.COMFY_AUDIO_PREFIX || 'audio/AIGen') } function yueStageARepo() { return String(process.env.YUE_STAGE_A || useRuntimeConfig().yueStageA || YUE_S1_DEFAULT) } function yueStageBRepo() { return String(process.env.YUE_STAGE_B || useRuntimeConfig().yueStageB || YUE_S2_DEFAULT) } function applyPrefix(graph: WorkflowGraph, prefix: string) { for (const node of Object.values(graph)) { if ((node.class_type === 'SaveAudio' || node.class_type === 'SaveAudioMP3') && 'filename_prefix' in node.inputs) { node.inputs.filename_prefix = prefix } } } function buildAceV1Workflow(params: MusicWorkflowParams): WorkflowGraph { const graph = structuredClone(workflowAceStep as WorkflowGraph) const prefix = params.filenamePrefix || musicFilenamePrefix() const sampler = params.samplerName || 'euler' const scheduler = params.scheduler || 'simple' for (const node of Object.values(graph)) { if (node.class_type === 'CheckpointLoaderSimple' && 'ckpt_name' in node.inputs) { node.inputs.ckpt_name = params.checkpoint || ACE_V1_CKPT } if (node.class_type === 'TextEncodeAceStepAudio') { node.inputs.tags = params.tags node.inputs.lyrics = params.lyrics node.inputs.lyrics_strength = params.lyricsStrength } if (node.class_type === 'EmptyAceStepLatentAudio' && 'seconds' in node.inputs) { node.inputs.seconds = params.duration } if (node.class_type === 'KSampler') { node.inputs.seed = params.seed node.inputs.steps = params.steps node.inputs.cfg = params.cfg node.inputs.sampler_name = sampler node.inputs.scheduler = scheduler } } applyPrefix(graph, prefix) return graph } function buildAce15Workflow(params: MusicWorkflowParams): WorkflowGraph { const graph = structuredClone(workflowAceStep15 as WorkflowGraph) const prefix = params.filenamePrefix || musicFilenamePrefix() const sampler = params.samplerName || 'euler' const scheduler = params.scheduler || 'simple' const bpm = params.bpm || parseMusicBpm(params.tags) for (const node of Object.values(graph)) { if (node.class_type === 'CheckpointLoaderSimple' && 'ckpt_name' in node.inputs) { node.inputs.ckpt_name = params.checkpoint || ACE_15_CKPT } if (node.class_type === 'TextEncodeAceStepAudio1.5') { node.inputs.tags = params.tags node.inputs.lyrics = params.lyrics node.inputs.seed = params.seed node.inputs.bpm = bpm node.inputs.duration = params.duration } if (node.class_type === 'EmptyAceStep1.5LatentAudio' && 'seconds' in node.inputs) { node.inputs.seconds = params.duration } if (node.class_type === 'KSampler') { node.inputs.seed = params.seed node.inputs.steps = params.steps node.inputs.cfg = params.cfg node.inputs.sampler_name = sampler node.inputs.scheduler = scheduler } } applyPrefix(graph, prefix) return graph } 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' // Beast 5080 16GB recipe (keep in sync with workflow_yue.json): // - mmgp profile 2 only (3–5 enable quanto and crash under Comfy InferenceMode) // - 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 // - 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() node.inputs.quantization_model = 'fp16' node.inputs.use_mmgp = true node.inputs.stage1_cache_size = 8192 node.inputs.mmgp_profile = 2 } if (node.class_type === 'YUE_Stage_B_Loader') { node.inputs.stage_B_repo = yueStageBRepo() node.inputs.use_mmgp = true node.inputs.stage2_cache_size = 4096 node.inputs.stage2_batch_size = 1 } if (node.class_type === 'YUE_Stage_A_Sampler') { // 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, 30) node.inputs.run_n_segment = segments node.inputs.max_new_tokens = 2944 node.inputs.use_dual_tracks_prompt = false node.inputs.use_audio_prompt = false node.inputs.offload_model = true node.inputs.stage1_no_guidance = true } } applyPrefix(graph, prefix) return graph } export function buildMusicWorkflow(params: MusicWorkflowParams): WorkflowGraph { const engine = params.engine || 'ace-step' if (engine === 'yue') return buildYueWorkflow(params) if (engine === 'ace-step-1.5') return buildAce15Workflow(params) return buildAceV1Workflow(params) } export async function assertMusicEngineNodes(engine: MusicEngine | undefined) { const { comfyHasClassType } = await import('~/server/utils/comfy') if (engine === 'yue') { const present = await comfyHasClassType(YUE_NODE) if (present === false) { throw createError({ statusCode: 400, statusMessage: 'YuE is not installed on Beast. Clone ComfyUI_YuE into custom_nodes and restart Comfy, then try again.' }) } } if (engine === 'ace-step-1.5') { const present = await comfyHasClassType('TextEncodeAceStepAudio1.5') if (present === false) { throw createError({ statusCode: 400, statusMessage: 'This ComfyUI build does not have ACE-Step 1.5 nodes. Update Comfy Desktop, or turn the 1.5 toggle off.' }) } } } export const MUSIC_NODE_LABELS: Record = Object.fromEntries([ ...Object.entries(workflowAceStep as WorkflowGraph).map(([id, node]) => [id, node._meta?.title || `Node ${id}`]), ...Object.entries(workflowAceStep15 as WorkflowGraph).map(([id, node]) => [id, node._meta?.title || `Node ${id}`]), ...Object.entries(workflowYue as WorkflowGraph).map(([id, node]) => [id, node._meta?.title || `Node ${id}`]) ])