Skip to content

Commit a9f4913

Browse files
committed
fix: route comfy music through shared tool
1 parent 9dfa4db commit a9f4913

11 files changed

Lines changed: 242 additions & 79 deletions

docs/providers/comfy.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ OpenClaw ships a bundled `comfy` plugin for workflow-driven ComfyUI runs.
1313

1414
- Provider: `comfy`
1515
- Models: `comfy/workflow`
16-
- Shared surfaces: `image_generate`, `video_generate`
17-
- Plugin tool: `music_generate`
16+
- Shared surfaces: `image_generate`, `video_generate`, `music_generate`
1817
- Auth: none for local ComfyUI; `COMFY_API_KEY` or `COMFY_CLOUD_API_KEY` for Comfy Cloud
1918
- API: ComfyUI `/prompt` / `/history` / `/view` and Comfy Cloud `/api/*`
2019

@@ -24,7 +23,7 @@ OpenClaw ships a bundled `comfy` plugin for workflow-driven ComfyUI runs.
2423
- Image editing with 1 uploaded reference image
2524
- Video generation from a workflow JSON
2625
- Video generation with 1 uploaded reference image
27-
- Music or audio generation through the bundled `music_generate` tool
26+
- Music or audio generation through the shared `music_generate` tool
2827
- Output download from a configured node or all matching output nodes
2928

3029
The bundled plugin is workflow-driven, so OpenClaw does not try to map generic
@@ -162,8 +161,8 @@ the configured graph. OpenClaw does not pass input videos into Comfy workflows.
162161

163162
## Music workflows
164163

165-
The bundled plugin registers a `music_generate` tool for workflow-defined audio
166-
or music outputs:
164+
The bundled plugin registers a music-generation provider for workflow-defined
165+
audio or music outputs, surfaced through the shared `music_generate` tool:
167166

168167
```text
169168
/tool music_generate prompt="Warm ambient synth loop with soft tape texture"

docs/tools/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ Plugins can register additional tools. Some examples:
106106

107107
- [Lobster](/tools/lobster) — typed workflow runtime with resumable approvals
108108
- [LLM Task](/tools/llm-task) — JSON-only LLM step for structured output
109-
- [Music Generation](/tools/music-generation) — shared `music_generate` tool plus plugin-provided workflow variants
109+
- [Music Generation](/tools/music-generation) — shared `music_generate` tool with workflow-backed providers
110110
- [Diffs](/tools/diffs) — diff viewer and renderer
111111
- [OpenProse](/prose) — markdown-first workflow orchestration
112112

docs/tools/music-generation.md

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
summary: "Generate music with shared providers or plugin-provided workflows"
2+
summary: "Generate music with shared providers, including workflow-backed plugins"
33
read_when:
44
- Generating music or audio via the agent
55
- Configuring music generation providers and models
@@ -9,10 +9,9 @@ title: "Music Generation"
99

1010
# Music Generation
1111

12-
The `music_generate` tool lets the agent create music or audio through either:
13-
14-
- the shared music-generation capability with configured providers such as Google and MiniMax
15-
- plugin-provided tool surfaces such as a workflow-configured ComfyUI graph
12+
The `music_generate` tool lets the agent create music or audio through the
13+
shared music-generation capability with configured providers such as Google,
14+
MiniMax, and workflow-configured ComfyUI.
1615

1716
For shared provider-backed agent sessions, OpenClaw starts music generation as a
1817
background task, tracks it in the task ledger, then wakes the agent again when
@@ -23,10 +22,6 @@ original channel.
2322
The built-in shared tool only appears when at least one music-generation provider is available. If you don't see `music_generate` in your agent's tools, configure `agents.defaults.musicGenerationModel` or set up a provider API key.
2423
</Note>
2524

26-
<Note>
27-
Plugin-provided `music_generate` implementations can expose different parameters or runtime behavior. The async task/status flow below applies to the built-in shared provider-backed path.
28-
</Note>
29-
3025
## Quick start
3126

3227
### Shared provider-backed generation
@@ -66,10 +61,10 @@ Generate a cinematic piano track with soft strings and no vocals.
6661
Generate an energetic chiptune loop about launching a rocket at sunrise.
6762
```
6863

69-
### Workflow-driven plugin generation
64+
### Workflow-driven Comfy generation
7065

71-
The bundled `comfy` plugin can also provide `music_generate` using a
72-
workflow-configured ComfyUI graph.
66+
The bundled `comfy` plugin plugs into the shared `music_generate` tool through
67+
the music-generation provider registry.
7368

7469
1. Configure `models.providers.comfy.music` with a workflow JSON and
7570
prompt/output nodes.
@@ -84,16 +79,11 @@ Example:
8479

8580
## Shared bundled provider support
8681

87-
| Provider | Default model | Reference inputs | Supported controls | API key |
88-
| -------- | ---------------------- | ---------------- | --------------------------------------------------------- | ---------------------------------- |
89-
| Google | `lyria-3-clip-preview` | Up to 10 images | `lyrics`, `instrumental`, `format` | `GEMINI_API_KEY`, `GOOGLE_API_KEY` |
90-
| MiniMax | `music-2.5+` | None | `lyrics`, `instrumental`, `durationSeconds`, `format=mp3` | `MINIMAX_API_KEY` |
91-
92-
## Plugin-provided support
93-
94-
| Provider | Model | Notes |
95-
| -------- | ---------- | ------------------------------- |
96-
| ComfyUI | `workflow` | Workflow-defined music or audio |
82+
| Provider | Default model | Reference inputs | Supported controls | API key |
83+
| -------- | ---------------------- | ---------------- | --------------------------------------------------------- | -------------------------------------- |
84+
| ComfyUI | `workflow` | Up to 1 image | Workflow-defined music or audio | `COMFY_API_KEY`, `COMFY_CLOUD_API_KEY` |
85+
| Google | `lyria-3-clip-preview` | Up to 10 images | `lyrics`, `instrumental`, `format` | `GEMINI_API_KEY`, `GOOGLE_API_KEY` |
86+
| MiniMax | `music-2.5+` | None | `lyrics`, `instrumental`, `durationSeconds`, `format=mp3` | `MINIMAX_API_KEY` |
9787

9888
Use `action: "list"` to inspect available shared providers and models at
9989
runtime:
@@ -129,8 +119,8 @@ Direct generation example:
129119
| `format` | string | Output format hint (`mp3` or `wav`) when the provider supports it |
130120
| `filename` | string | Output filename hint |
131121

132-
Not all providers or plugins support all parameters. The shared built-in tool
133-
validates provider capability limits before it submits the request.
122+
Not all providers support all parameters. The shared tool validates provider
123+
capability limits before it submits the request.
134124

135125
## Async behavior for the shared provider-backed path
136126

extensions/comfy/comfy.live.test.ts

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeAll, describe, expect, it, vi } from "vitest";
1+
import { beforeAll, describe, expect, it } from "vitest";
22
import { resolveOpenClawAgentDir } from "../../src/agents/agent-paths.js";
33
import { isLiveTestEnabled } from "../../src/agents/live-test-helpers.js";
44
import { loadConfig } from "../../src/config/config.js";
@@ -10,15 +10,10 @@ const LIVE =
1010
isLiveTestEnabled(["COMFY_LIVE_TEST"]) && (process.env.COMFY_LIVE_TEST ?? "").trim() === "1";
1111
const describeLive = LIVE ? describe : describe.skip;
1212

13-
type RegisteredTool = {
14-
name: string;
15-
execute: (
16-
id: string,
17-
params: Record<string, unknown>,
18-
) => Promise<{
19-
content: Array<{ type: string; text?: string }>;
20-
details?: unknown;
21-
}>;
13+
type RegisteredMusicProvider = {
14+
id: string;
15+
generateMusic: Function;
16+
isConfigured?: Function;
2217
};
2318

2419
function withPluginsEnabled<T>(cfg: T): T {
@@ -40,46 +35,25 @@ describeLive("comfy live", () => {
4035
let agentDir = "";
4136
const imageProviders: Array<{ id: string; generateImage: Function; isConfigured?: Function }> =
4237
[];
38+
const musicProviders: RegisteredMusicProvider[] = [];
4339
const videoProviders: Array<{ id: string; generateVideo: Function; isConfigured?: Function }> =
4440
[];
45-
const tools: RegisteredTool[] = [];
46-
const saveMediaBuffer = vi.fn(
47-
async (
48-
_buffer: Buffer,
49-
_mimeType: string,
50-
_subdir?: string,
51-
_maxBytes?: number,
52-
originalFilename?: string,
53-
) => ({
54-
path: `/tmp/${originalFilename ?? "generated.bin"}`,
55-
id: "saved-1",
56-
mimeType: _mimeType,
57-
bytes: _buffer.byteLength,
58-
}),
59-
);
6041

6142
beforeAll(async () => {
6243
cfg = withPluginsEnabled(loadConfig());
6344
agentDir = resolveOpenClawAgentDir();
6445
await plugin.register(
6546
createTestPluginApi({
6647
config: cfg as never,
67-
runtime: {
68-
channel: {
69-
media: {
70-
saveMediaBuffer,
71-
},
72-
},
73-
} as never,
7448
registerImageGenerationProvider(provider) {
7549
imageProviders.push(provider as never);
7650
},
51+
registerMusicGenerationProvider(provider) {
52+
musicProviders.push(provider as never);
53+
},
7754
registerVideoGenerationProvider(provider) {
7855
videoProviders.push(provider as never);
7956
},
80-
registerTool(tool) {
81-
tools.push(tool as RegisteredTool);
82-
},
8357
}),
8458
);
8559
});
@@ -123,17 +97,20 @@ describeLive("comfy live", () => {
12397
);
12498

12599
it.skipIf(!isComfyCapabilityConfigured({ cfg: cfg as never, agentDir, capability: "music" }))(
126-
"runs a music workflow tool",
100+
"runs a music workflow",
127101
async () => {
128-
const tool = tools.find((entry) => entry.name === "music_generate");
129-
expect(tool).toBeDefined();
130-
const result = await tool!.execute("music-live", {
102+
const provider = musicProviders.find((entry) => entry.id === "comfy");
103+
expect(provider).toBeDefined();
104+
const result = await provider!.generateMusic({
105+
provider: "comfy",
106+
model: "workflow",
131107
prompt: "A gentle ambient synth loop with warm analog pads.",
132-
filename: "comfy-live.mp3",
108+
cfg: cfg as never,
109+
agentDir,
133110
});
134-
const text = result.content.find((entry) => entry.type === "text")?.text ?? "";
135-
expect(text).toContain("MEDIA:/tmp/comfy-live.mp3");
136-
expect(saveMediaBuffer).toHaveBeenCalled();
111+
expect(result.tracks.length).toBeGreaterThan(0);
112+
expect(result.tracks[0]?.mimeType.startsWith("audio/")).toBe(true);
113+
expect(result.tracks[0]?.buffer.byteLength).toBeGreaterThan(512);
137114
},
138115
180_000,
139116
);

extensions/comfy/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
22
import { buildComfyImageGenerationProvider } from "./image-generation-provider.js";
3-
import { createComfyMusicGenerateTool } from "./music-generate-tool.js";
3+
import { buildComfyMusicGenerationProvider } from "./music-generation-provider.js";
44
import { buildComfyVideoGenerationProvider } from "./video-generation-provider.js";
55

66
const PROVIDER_ID = "comfy";
@@ -18,7 +18,7 @@ export default definePluginEntry({
1818
auth: [],
1919
});
2020
api.registerImageGenerationProvider(buildComfyImageGenerationProvider());
21+
api.registerMusicGenerationProvider(buildComfyMusicGenerationProvider());
2122
api.registerVideoGenerationProvider(buildComfyVideoGenerationProvider());
22-
api.registerTool(createComfyMusicGenerateTool(api));
2323
},
2424
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { buildComfyMusicGenerationProvider } from "./music-generation-provider.js";
3+
import { _setComfyFetchGuardForTesting } from "./workflow-runtime.js";
4+
5+
const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({
6+
fetchWithSsrFGuardMock: vi.fn(),
7+
}));
8+
9+
describe("comfy music-generation provider", () => {
10+
it("registers the workflow model", () => {
11+
const provider = buildComfyMusicGenerationProvider();
12+
13+
expect(provider.defaultModel).toBe("workflow");
14+
expect(provider.models).toEqual(["workflow"]);
15+
expect(provider.capabilities.maxInputImages).toBe(1);
16+
});
17+
18+
it("runs a music workflow and returns audio outputs", async () => {
19+
_setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
20+
fetchWithSsrFGuardMock
21+
.mockResolvedValueOnce({
22+
response: new Response(JSON.stringify({ prompt_id: "music-job-1" }), {
23+
status: 200,
24+
headers: { "content-type": "application/json" },
25+
}),
26+
release: vi.fn(async () => {}),
27+
})
28+
.mockResolvedValueOnce({
29+
response: new Response(
30+
JSON.stringify({
31+
"music-job-1": {
32+
outputs: {
33+
"9": {
34+
audio: [{ filename: "song.mp3", subfolder: "", type: "output" }],
35+
},
36+
},
37+
},
38+
}),
39+
{
40+
status: 200,
41+
headers: { "content-type": "application/json" },
42+
},
43+
),
44+
release: vi.fn(async () => {}),
45+
})
46+
.mockResolvedValueOnce({
47+
response: new Response(Buffer.from("music-bytes"), {
48+
status: 200,
49+
headers: { "content-type": "audio/mpeg" },
50+
}),
51+
release: vi.fn(async () => {}),
52+
});
53+
54+
const provider = buildComfyMusicGenerationProvider();
55+
const result = await provider.generateMusic({
56+
provider: "comfy",
57+
model: "workflow",
58+
prompt: "gentle ambient synth loop",
59+
cfg: {
60+
models: {
61+
providers: {
62+
comfy: {
63+
music: {
64+
workflow: {
65+
"6": { inputs: { text: "" } },
66+
"9": { inputs: {} },
67+
},
68+
promptNodeId: "6",
69+
outputNodeId: "9",
70+
},
71+
},
72+
},
73+
},
74+
} as never,
75+
});
76+
77+
expect(result).toMatchObject({
78+
model: "workflow",
79+
tracks: [
80+
{
81+
mimeType: "audio/mpeg",
82+
fileName: "song.mp3",
83+
},
84+
],
85+
metadata: {
86+
promptId: "music-job-1",
87+
outputNodeIds: ["9"],
88+
inputImageCount: 0,
89+
},
90+
});
91+
expect(result.tracks[0]?.buffer).toEqual(Buffer.from("music-bytes"));
92+
});
93+
});

0 commit comments

Comments
 (0)