Files
aigen/server/utils/musicWorkflow.ts
T
2026-09-14 18:45:57 -05:00

128 lines
4.8 KiB
TypeScript

import workflowAceStep from '../assets/workflow_ace_step.json'
import workflowAceStep15 from '../assets/workflow_ace_step_15.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<string, unknown>; _meta?: { title?: string } }
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'
export function musicFilenamePrefix() {
return String(useRuntimeConfig().comfyAudioPrefix || process.env.COMFY_AUDIO_PREFIX || 'audio/AIGen')
}
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
}
export function buildMusicWorkflow(params: MusicWorkflowParams): WorkflowGraph {
const engine = params.engine || 'ace-step'
if (engine === 'yue') throw new Error('YuE requires the standalone YuEGP backend.')
if (engine === 'yue2') throw new Error('YuE2 requires the standalone YuE2 backend.')
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') throw new Error('YuE cannot run on Comfy.')
if (engine === 'yue2') throw new Error('YuE2 cannot run on Comfy.')
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<string, string> = 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}`])
])