ComfyUI API 批量出歌踩坑实录串行队列、失败重试与输出归档的血泪经验【免费下载链接】Yue2项目地址: https://ai.gitcode.com/hf_mirrors/Comfy-Org/Yue2YuE2 的走红几乎是今年开源音乐生成领域最确定的事港科大 M-A-P 团队开源、AR–NAR 混合 Transformer 架构、先输出可编辑的 ABC 乐谱再渲染 48kHz 立体声音频在 WildSongBench 上 best-of-8 全局平均分 6.96、专家评测音质对齐 Suno v5。社区实测 4090 上约一分钟出一首歌71 秒即可产出 3.6 分钟的完整成品。ComfyUI 也在 v0.36.0 更新中把 Yue2 音乐工作流收进了官方模板库这个仓库就是 Comfy-Org 为 ComfyUI 重新打包的模型文件见 README.md含 yue2_3b_bf16.safetensors、yue2_3b_int8_convrot.safetensors 与 sheetsage2_bf16.safetensors。于是问题来了当你要的不是出一首歌而是出一百首歌——批量给歌词配风格、批量做翻唱、批量验证提示词组合时靠前端点按钮逐条跑显然不现实。ComfyUI 原生提供了/prompt、/queue、/history这套 HTTP API但把它们拼成一条可靠的批量流水线中间全是坑队列是串行的、失败不会自动重试、history 只增不减、输出目录一个月就能吃满磁盘。本文把这些坑逐个拆开给出源码级的依据和可直接落地的方案。/prompt、/queue、/history三个接口的正确姿势先明确分工/prompt负责提交任务与查询剩余队列/queue反映任务的实时状态运行中/排队中/history是任务的最终档案成功或失败都留痕。三者配合才能构成完整的批量闭环。POST /prompt提交与幂等提交的核心 payload 就两个字段prompt工作流图即把 JSON 工作流里所有节点的widgets_values换成你本次要传的值和client_id一个 UUID 字符串用于把执行进度事件路由到你的 WebSocket 会话。服务端校验通过后返回{prompt_id: 5f3c..., number: 12, node_errors: {}}一个常被忽略的细节prompt_id可以由客户端自己指定服务端会校验它必须是合法 UUID不传才由服务端生成。这给批量场景带来了幂等能力——你在本地生成的prompt_id就是这次出歌的唯一业务主键后续查 history、取消任务、定位输出文件全都用它即使网络闪断重发也始终能对上同一条记录。import json, uuid, requests BASE http://127.0.0.1:8188 def submit(workflow, client_id): payload { prompt: workflow, client_id: client_id, prompt_id: str(uuid.uuid4()), } r requests.post(f{BASE}/prompt, jsonpayload) r.raise_for_status() return r.json()[prompt_id]GET /queue看清你的队列提交之后立刻能查的就是队列状态。GET /queue返回两个数组queue_running正在执行和queue_pending排队等待数组里的元组形如(number, prompt_id, prompt, extra_data, outputs_to_execute)——注意服务端在返回前会主动剥掉其中的敏感字段这也是批量场景里不要依赖返回体携带完整 prompt 的原因字段结构随版本演进。更轻量的探针是GET /prompt只返回{exec_info: {queue_remaining: 47}}一个数字就能判断我提交的活儿干到哪了轮询开销极低适合做批量任务的进度条def queue_remaining(): info requests.get(f{BASE}/prompt).json() return info[exec_info][queue_remaining]GET /history任务的最终归宿任务无论成功失败都会进入 history按prompt_id索引。GET /history/{prompt_id}精确查单条GET /history?max_items100offset0支持分页。单条记录的核心结构是{ prompt_id: { prompt: [...], outputs: {10: {audio: [{filename: 00001_YuE2.flac, subfolder: audio/YuE2, type: output}]}}, status: {status_str: success, completed: true} } }这里有两个关键点。第一判完成必须以status.status_str为唯一依据success和error二选一不存在第三态第二输出文件信息藏在outputs里、以节点 ID 为键——Text to Music 工作流里负责落盘的是SaveAudioAdvanced节点它产出的音频就挂在outputs[节点id][audio]下取出来是filename/subfolder/type三元组再拼上GET /view?filename...subfolderaudio/YuE2typeoutput就能把 flac 拉下来归档。别忽略新 API/api/jobs较新版本的 ComfyUI 还提供了/api/jobs与/api/jobs/{job_id}支持按statuspending、in_progress、completed、failed过滤、按时间排序分页。如果你的批量任务量级到了几千条用它做失败清单的离线统计比逐条翻 history 干净得多——它把运行中、排队中、历史记录统一成了同一个 job 视图省掉了你手工合并三个数据源的逻辑。串行调度与失败重试那些最深的坑坑一执行引擎天生串行并发提交只是排队ComfyUI 的执行引擎在服务端是单 worker 循环同一时刻只能执行一个 prompt其余全部进入queue_pending排队。服务端源码里队列项就是(number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive)这种顺序元组worker 逐个取出执行。这意味着——并发 50 个 POST /prompt 不会带来任何并行加速只会产生一条 50 深的队列而队列里的每个任务都携带完整的 prompt JSON几十首歌词挤在内存里中途想精准撤销某一首还得遍历删除。正确姿势是窗口化流水线不要一次性全量入队而是维护一个固定窗口比如同时最多 5 个 pending每完成一首就补投一首。这样队列始终短、内存占用稳定、单任务失败时影响面可控。执行虽然是串行的但你的调度器从失控的长队变成了看得见的传送带。坑二没有自动重试失败必须自己兜底服务端对失败任务的处理非常朴素写进 history、标status_str error然后继续下一个。没有任何内置的重试、退避或告警。批量跑音乐生成失败是常态而不是意外常见根因有三个OOMYuE2GenerateMusic 的max_duration默认 360 秒一首 3.6 分钟的歌在 24GB 显存的消费级显卡上已经是极限工况队列里连续几首长歌极易在采样阶段爆显存而且失败后显存不会立刻完全回收服务抖动长任务运行几分钟期间服务端日志被刷屏或 WebSocket 断连客户端容易误判参数问题歌词与max_duration不匹配歌词太长、时长太短导致的节点报错这种失败是确定性的重试一百次也没用。所以重试策略要分两层对 OOM 和服务抖动用指数退避 冷却重试间隔 5 秒起步、逐次翻倍给显存和内存喘息时间对参数类错误把 history 里status.messages中的异常信息捞出来做一次分类命中必失败特征的任务直接标记失败、不重投省下无意义的算力。另一个重试细节与 seed 相关。官方 Text to Music 工作流里YuE2GenerateABC 和 YuE2GenerateMusic 的 seed 都挂在 SeedNode 上、默认randomize。批量场景建议在提交前把工作流 JSON 里的 seed 改成你外部生成的固定值并关闭control_after_generate fixed——这样同一首歌失败重试时重投相同的 seed 理论上能得到可复现的输出便于对照是参数问题还是偶发 OOM而想要多样性时就显式地每首分配不同 seed 再提交。把随机性放进你的调度器手里而不是丢给工作流内部。坑三取消与清空要分清语义批量跑的时候经常遇到这批参数不行全停。三个接口的语义差异很大POST /interrupt中断正在执行的任务新版支持{prompt_id: ...}定向中断被中断的任务会在 history 里留一条 error 记录POST /queue管理待执行队列{delete: [prompt_id, ...]}按 ID 精准删除{clear: true}则清空全部 pending——注意 clear 会连你下一批想跑的任务一起清掉别在窗口化调度里误用POST /history的{clear: true}/{delete: [...]}只是清理历史记录不会删除磁盘上的音频文件只释放 history 占用的内存与落盘 JSON。批量管线的标准取消流程是先delete掉所有 pending再interrupt正在跑的那个最后选择性清理 history。顺序反了会出现任务还在跑、新任务却已入队的窗口期。坑四history 只增不减每条 history 记录都完整保留了prompt含整段歌词和风格描述与 outputs 元数据批量跑几千首之后history 会成为内存和磁盘上的隐形炸弹还会拖慢每次查询。批量任务跑完一个批次就应该对已归档且不再需要的记录执行POST /history {clear: true}让系统回到轻量状态再开始下一批。输出归档与磁盘管理的实操建议音频文件的管理要从工作流源头设计而不是事后补救。目录前缀即归档分区。Text to Music 与 Music Cover 两个官方工作流的SaveAudioAdvanced节点默认前缀分别是audio/YuE2与audio/YuE2_cover、格式均为 flac——前缀中的斜杠会被自动转换为输出目录层级最终落盘在output/audio/YuE2/00001_YuE2.flac这类路径下。批量时建议把前缀升级为批次维度例如audio/YuE2/batch_20261010让输出天然按批次分区。同时注意重名文件会被自动追加序号所以同一首歌重试两次会得到两个不同文件名的成品归档时务必以prompt_id为唯一键做去重别让重试产物污染结果集。算清楚磁盘账。48kHz 立体声 flac3.6 分钟一首大约 15–25MB视音乐复杂度而定100 首就是 1.5–2.5GB一个月跑几万首的规模足以吃满一块普通数据盘。归档策略就三条按批次把output/audio/YuE2/下的文件移动到独立的冷存储目录NAS 或对象存储移动前先把该批次的prompt_id → 文件名映射写成 JSON 清单随批保存它是你日后回溯哪首歌唱了什么词的唯一索引移动后清理对应 history 与源目录。这样热盘上永远只保留最近一个批次的产物磁盘水位可控。def archive(batch_dir, manifest, records): # records: [{prompt_id, filename, subfolder}...] os.makedirs(batch_dir, exist_okTrue) moved [] for rec in records: src Path(output) / rec[subfolder] / rec[filename] dst Path(batch_dir) / rec[filename] if src.exists(): shutil.move(str(src), str(dst)) moved.append(rec) (Path(batch_dir) / manifest.json).write_text( json.dumps(moved, ensure_asciiFalse, indent2)) requests.post(f{BASE}/history, json{clear: True})一份可落地的批量调度参考实现把上面的要点串起来一个最小可用的批量出歌调度器长这样固定窗口提交、成功即归档、失败按类型退避重试、每批次结束清 history。import json, shutil, time, uuid from pathlib import Path import requests BASE http://127.0.0.1:8188 CLIENT_ID str(uuid.uuid4()) def submit(workflow): pid str(uuid.uuid4()) requests.post(f{BASE}/prompt, json{ prompt: workflow, client_id: CLIENT_ID, prompt_id: pid, }).raise_for_status() return pid def wait_done(pid, timeout2400): deadline time.time() timeout while time.time() deadline: rec requests.get(f{BASE}/history/{pid}).json().get(pid) if rec: return rec[status][status_str], rec time.sleep(2) return timeout, None def pending_count(): return requests.get(f{BASE}/prompt).json()[exec_info][queue_remaining] def run_batch(items, window5, max_retries3): results, inflight [], {} # items: [{workflow: {...}, meta: {...}}, ...] it iter(items) while True: while len(inflight) window: try: item next(it) except StopIteration: break inflight[submit(item[workflow])] {item: item, tries: 0} if not inflight: break for pid in list(inflight): status, rec wait_done(pid, timeout30) # 快速探测 if status in (success, error, timeout): if status success: results.append({pid: pid, meta: inflight[pid][item][meta], rec: rec}) del inflight[pid] else: inflight[pid][tries] 1 if inflight[pid][tries] max_retries: results.append({pid: pid, failed: True}) del inflight[pid] else: # 退避后重投换新 prompt_id time.sleep(5 * inflight[pid][tries]) new_pid submit(inflight[pid][item][workflow]) inflight[new_pid] inflight.pop(pid) time.sleep(1) return results这段代码刻意保持了精简真正的工程化还需要补三件事失败原因分类解析 history 中的 messages、种子管理提交前覆写工作流 JSON 的 seed、以及每批结束后的归档与history clear——这三点恰好就是前面踩过的四个坑的答案。结语ComfyUI 的这三个 API 设计得足够克制提交、排队、归档各司其职没有任何多余的魔法。但克制也意味着批量场景里的串行限制、失败兜底、状态清理全部要由调用方负责。把队列当传送带而不是停车场、把重试分成可重试与必失败两类、把输出目录当冷热分离的存储来设计——这三条原则到位之后从一首一首点按钮到一夜跑完一千首差的就只是一段几十行的调度脚本了。YuE2 把音乐生成从抽卡变成了可编辑的工程而可靠的批量管线才是把它变成真正生产力的最后一公里。【免费下载链接】Yue2项目地址: https://ai.gitcode.com/hf_mirrors/Comfy-Org/Yue2创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考