Skip to content

Commit cd70da0

Browse files
authored
Added ControlNet ComfyUI workflow (#88)
* added controlnet workflow eg * updated folder stuct * readme to * fix * added blender example
1 parent 44c8ad1 commit cd70da0

7 files changed

Lines changed: 494 additions & 0 deletions

File tree

blender/.beamignore

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Beam SDK
2+
.beamignore
3+
pyproject.toml
4+
.git
5+
.idea
6+
.python-version
7+
.vscode
8+
.venv
9+
venv
10+
__pycache__
11+
.DS_Store
12+
.config
13+
drive/MyDrive
14+
.coverage
15+
.pytest_cache
16+
.ipynb
17+
.ruff_cache
18+
.dockerignore
19+
.ipynb_checkpoints
20+
.env.local
21+
.envrc
22+
**/__pycache__/
23+
**/.pytest_cache/
24+
**/node_modules/
25+
**/.venv/
26+
*.pyc
27+
.next/
28+
.circleci

blender/app.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
This script uses Blender to render a 3D scene. It downloads a .blend file, executes a Python script to render the scene,
3+
4+
To run this on Beam, you can run 'python app.py' in the terminal. Change the render_test.json and render.py files to your own use case.
5+
"""
6+
7+
from beam import function, Image, Output
8+
from pathlib import Path
9+
import subprocess
10+
11+
blender_image = (
12+
Image(python_version="python3.11")
13+
.add_python_packages(["bpy"])
14+
.add_commands(
15+
[
16+
"apt update && apt install -y xorg libxkbcommon0",
17+
"wget -q https://download.blender.org/release/Blender4.0/blender-4.0.2-linux-x64.tar.xz -O /tmp/blender.tar.xz",
18+
"tar -xf /tmp/blender.tar.xz -C /opt",
19+
"mv /opt/blender-* /opt/blender",
20+
"ln -s /opt/blender/blender /usr/local/bin/blender",
21+
]
22+
)
23+
)
24+
25+
26+
@function(image=blender_image, cpu=12, memory="32Gi", gpu="A10G")
27+
def render(script_content, json_content, output_name):
28+
blend_path = "/tmp/render_test.blend"
29+
script_path = "/tmp/render_alternative.py"
30+
json_path = "/tmp/render_test.json"
31+
output_path = f"/tmp/{output_name}"
32+
33+
subprocess.run(
34+
"wget https://vnyiodyihbjosm4n.public.blob.vercel-storage.com/Tree-y3VjlYkkByWNSOol56jE307zHqaNsp.blend -O /tmp/Tree.blend",
35+
shell=True,
36+
check=True,
37+
stdout=subprocess.DEVNULL,
38+
stderr=subprocess.DEVNULL,
39+
)
40+
41+
blend_bytes = Path("/tmp/Tree.blend").read_bytes()
42+
43+
Path(blend_path).write_bytes(blend_bytes)
44+
Path(script_path).write_text(script_content)
45+
Path(json_path).write_text(json_content)
46+
47+
cmd = ["blender", blend_path, "-b", "-P", script_path, "--", output_path, json_path]
48+
49+
subprocess.run(cmd, capture_output=True, text=True, cwd="/tmp")
50+
51+
output_file = Output(path=output_path)
52+
output_file.save()
53+
public_url = output_file.public_url(expires=400)
54+
return {"output_url": public_url}
55+
56+
57+
if __name__ == "__main__":
58+
output_url = render(
59+
script_content=Path("render.py").read_text(),
60+
json_content=Path("render_test.json").read_text(),
61+
output_name="output.png",
62+
)
63+
print(f"Output URL: {output_url}")

blender/render.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import bpy
2+
import sys
3+
import json
4+
5+
argv = sys.argv
6+
argv = argv[argv.index("--") + 1 :]
7+
8+
output_path = argv[0]
9+
json_path = argv[1]
10+
11+
with open(json_path, "r") as f:
12+
config = json.load(f)
13+
14+
15+
def create_material(name, color):
16+
if name in bpy.data.materials:
17+
mat = bpy.data.materials[name]
18+
else:
19+
mat = bpy.data.materials.new(name=name)
20+
21+
mat.use_nodes = True
22+
mat.node_tree.nodes.clear()
23+
24+
bsdf = mat.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
25+
bsdf.inputs["Base Color"].default_value = (*color, 1.0)
26+
27+
output = mat.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
28+
mat.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
29+
30+
return mat
31+
32+
33+
if "material_color" in config:
34+
color = config["material_color"]
35+
new_material = create_material("TreeMaterial", color)
36+
37+
for obj in bpy.data.objects:
38+
if obj.type == "MESH":
39+
obj_name_lower = obj.name.lower()
40+
41+
if any(
42+
keyword in obj_name_lower
43+
for keyword in ["tree", "leaf", "leaves", "foliage"]
44+
):
45+
obj.data.materials.clear()
46+
obj.data.materials.append(new_material)
47+
48+
if "object_to_modify" in config and "location" in config:
49+
obj = bpy.data.objects.get(config["object_to_modify"])
50+
if obj:
51+
obj.location = config["location"]
52+
53+
scene = bpy.context.scene
54+
scene.render.filepath = output_path
55+
scene.render.image_settings.file_format = "PNG"
56+
57+
try:
58+
scene.render.engine = "BLENDER_EEVEE"
59+
except:
60+
scene.render.engine = "CYCLES"
61+
scene.cycles.samples = 32
62+
63+
bpy.ops.render.render(write_still=True)

blender/render_test.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"object_to_modify": "tree",
3+
"location": [1.0, 2.0, 3.0],
4+
"material_color": [0.1, 0.6, 0.1]
5+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# ComfyUI API Example
2+
3+
An image generation API powered by ComfyUI, running ControlNet workflow
4+
5+
## Deployment
6+
7+
Deploy the app on Beam:
8+
9+
```
10+
beam deploy app.py:handler
11+
```
12+
13+
---
14+
15+
## API Usage
16+
17+
Send a `POST` request to the `/generate` endpoint with the following JSON body:
18+
19+
```json
20+
{
21+
"prompt": "Your prompt",
22+
"image_url": "https://your-image-url"
23+
}
24+
```
25+
26+
---
27+
28+
### Example Request:
29+
30+
```json
31+
{
32+
"prompt": "A photorealistic golden retriever sitting in a field of flowers, soft light, professional lens, background blur",
33+
"image_url": "https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcRHWNgtJvfMe4yIAOCPTpxRjPalxseEhGfMvW_B3Y3RLjhvwtCIHaYKt0D3K1h2qcWqqvvroakXVGKcAFnTDk7HhA"
34+
}
35+
```
36+
37+
---
38+
39+
### Example Response:
40+
41+
```json
42+
{
43+
"output_url": " https://app.beam.cloud/output/id/5cc90408-2c40-424f-bb3f-731268e7f100"
44+
}
45+
```
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
from beam import Image, asgi, Output
2+
import requests
3+
from pathlib import Path
4+
5+
image = (
6+
Image()
7+
.add_commands(["apt update && apt install git -y"])
8+
.add_python_packages(
9+
[
10+
"fastapi[standard]==0.115.4",
11+
"comfy-cli",
12+
"huggingface_hub[hf_transfer]==0.26.2",
13+
]
14+
)
15+
.add_commands(
16+
[
17+
"yes | comfy install --nvidia --version 0.3.10",
18+
"comfy node install was-node-suite-comfyui@1.0.2",
19+
"mkdir -p /root/comfy/ComfyUI/models/checkpoints/",
20+
"huggingface-cli download Comfy-Org/flux1-schnell flux1-schnell-fp8.safetensors --cache-dir /comfy-cache",
21+
"ln -s /comfy-cache/models--Comfy-Org--flux1-schnell/snapshots/f2808ab17fe9ff81dcf89ed0301cf644c281be0a/flux1-schnell-fp8.safetensors /root/comfy/ComfyUI/models/checkpoints/flux1-schnell-fp8.safetensors",
22+
]
23+
)
24+
)
25+
26+
27+
def download_image_from_url(url: str, filename: str = "input_image.png"):
28+
target_path = Path("/root/comfy/ComfyUI/input") / filename
29+
target_path.parent.mkdir(parents=True, exist_ok=True)
30+
31+
response = requests.get(url)
32+
if response.status_code != 200:
33+
raise Exception(f"Failed to download image: {response.status_code}")
34+
35+
with open(target_path, "wb") as f:
36+
f.write(response.content)
37+
38+
return target_path
39+
40+
41+
def hf_download():
42+
import subprocess
43+
from huggingface_hub import hf_hub_download
44+
45+
dream_model = hf_hub_download(
46+
repo_id="Bruhn/Lab_merge",
47+
filename="dreamCreationVirtual3DECommerce_v10.safetensors",
48+
cache_dir="/comfy-cache",
49+
)
50+
51+
vae_model = hf_hub_download(
52+
repo_id="stabilityai/sd-vae-ft-mse-original",
53+
filename="vae-ft-mse-840000-ema-pruned.safetensors",
54+
cache_dir="/comfy-cache",
55+
)
56+
57+
controlnet_model = hf_hub_download(
58+
repo_id="comfyanonymous/ControlNet-v1-1_fp16_safetensors",
59+
filename="control_v11p_sd15_scribble_fp16.safetensors",
60+
cache_dir="/comfy-cache",
61+
)
62+
63+
subprocess.run(
64+
"mkdir -p /root/comfy/ComfyUI/models/checkpoints/",
65+
shell=True,
66+
check=True,
67+
)
68+
subprocess.run(
69+
"mkdir -p /root/comfy/ComfyUI/models/vae/",
70+
shell=True,
71+
check=True,
72+
)
73+
subprocess.run(
74+
"mkdir -p /root/comfy/ComfyUI/models/controlnet/",
75+
shell=True,
76+
check=True,
77+
)
78+
79+
subprocess.run(
80+
f"ln -s {dream_model} /root/comfy/ComfyUI/models/checkpoints/dreamCreationVirtual3DECommerce_v10.safetensors",
81+
shell=True,
82+
check=True,
83+
)
84+
subprocess.run(
85+
f"ln -s {vae_model} /root/comfy/ComfyUI/models/vae/vae-ft-mse-840000-ema-pruned.safetensors",
86+
shell=True,
87+
check=True,
88+
)
89+
subprocess.run(
90+
f"ln -s {controlnet_model} /root/comfy/ComfyUI/models/controlnet/control_v11p_sd15_scribble_fp16.safetensors",
91+
shell=True,
92+
check=True,
93+
)
94+
95+
cmd = "comfy launch --background"
96+
subprocess.run(cmd, shell=True, check=True)
97+
98+
99+
@asgi(
100+
name="comfy",
101+
image=image,
102+
on_start=hf_download,
103+
cpu=8,
104+
memory="32Gi",
105+
gpu="A100-40",
106+
timeout=-1,
107+
)
108+
def handler():
109+
from fastapi import FastAPI, HTTPException
110+
import subprocess
111+
import json
112+
from pathlib import Path
113+
import uuid
114+
from typing import Dict
115+
116+
app = FastAPI()
117+
118+
# This is where you specify the path to your workflow file.
119+
# Make sure "workflow_api.json" exists in the same directory as this script.
120+
WORKFLOW_FILE = Path(__file__).parent / "workflow_api.json"
121+
OUTPUT_DIR = Path("/root/comfy/ComfyUI/output")
122+
123+
@app.post("/generate")
124+
async def generate(item: Dict):
125+
if not WORKFLOW_FILE.exists():
126+
raise HTTPException(status_code=500, detail="Workflow file not found.")
127+
128+
image_url = item.get("image_url")
129+
prompt = item.get("prompt")
130+
131+
if not image_url or not prompt:
132+
raise HTTPException(status_code=400, detail="Missing image_url or prompt")
133+
134+
downloaded_image_path = download_image_from_url(image_url, "input.jpg")
135+
request_id = uuid.uuid4().hex
136+
137+
workflow_data = json.loads(WORKFLOW_FILE.read_text())
138+
workflow_data["6"]["inputs"]["text"] = item["prompt"]
139+
workflow_data["11"]["inputs"]["image"] = str(downloaded_image_path)
140+
141+
new_workflow_file = Path(f"{request_id}.json")
142+
new_workflow_file.write_text(json.dumps(workflow_data, indent=4))
143+
144+
# Run inference
145+
cmd = (
146+
f"comfy run --workflow {new_workflow_file} --wait --timeout 1200 --verbose"
147+
)
148+
subprocess.run(cmd, shell=True, check=True)
149+
150+
image_files = list(OUTPUT_DIR.glob("*"))
151+
152+
# Find the latest image
153+
latest_image = max(
154+
(f for f in image_files if f.suffix.lower() in {".png", ".jpg", ".jpeg"}),
155+
key=lambda f: f.stat().st_mtime,
156+
default=None,
157+
)
158+
159+
if not latest_image:
160+
raise HTTPException(status_code=404, detail="No output image found.")
161+
162+
output_file = Output(path=latest_image)
163+
output_file.save()
164+
public_url = output_file.public_url(expires=-1)
165+
print(public_url)
166+
return {"output_url": public_url}
167+
168+
return app

0 commit comments

Comments
 (0)