BenjaminPittsley commited on
Commit
6ddff10
·
1 Parent(s): da69e87

Configure Space to run ComfyUI with custom nodes and auto-GPU/CPU detection

Browse files
Files changed (2) hide show
  1. Dockerfile +46 -42
  2. app.py +44 -250
Dockerfile CHANGED
@@ -1,42 +1,46 @@
1
- FROM python:3.10-slim
2
-
3
- # Install system dependencies
4
- RUN apt-get update && apt-get install -y \
5
- git \
6
- wget \
7
- libgl1 \
8
- libglib2.0-0 \
9
- && rm -rf /var/lib/apt/lists/*
10
-
11
- # Set working directory
12
- WORKDIR /app
13
-
14
- # Install Python packages
15
- RUN pip install --no-cache-dir \
16
- gradio \
17
- torch \
18
- torchvision \
19
- diffusers \
20
- transformers \
21
- accelerate \
22
- safetensors \
23
- pillow \
24
- spaces
25
-
26
- # Copy application
27
- COPY app.py /app/
28
- COPY custom_nodes/ /app/custom_nodes/
29
- COPY workflows/ /app/workflows/
30
-
31
- # Create necessary directories
32
- RUN mkdir -p /app/models
33
-
34
- # Expose port
35
- EXPOSE 7860
36
-
37
- # Set environment for Zero GPU
38
- ENV PYTHONUNBUFFERED=1
39
- ENV HF_HOME=/app/models
40
-
41
- # Run the app
42
- CMD ["python", "app.py"]
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Install system dependencies
4
+ RUN apt-get update && apt-get install -y \
5
+ git \
6
+ wget \
7
+ curl \
8
+ libgl1 \
9
+ libglib2.0-0 \
10
+ build-essential \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Set working directory
14
+ WORKDIR /app
15
+
16
+ # Clone ComfyUI into /app
17
+ RUN git clone https://github.com/comfyanonymous/ComfyUI.git .
18
+
19
+ # Install ComfyUI requirements
20
+ RUN pip install --no-cache-dir torch torchvision --extra-index-url https://download.pytorch.org/whl/cu121
21
+ RUN pip install --no-cache-dir -r requirements.txt
22
+
23
+ # Install extra requirements for custom nodes
24
+ RUN pip install --no-cache-dir \
25
+ gradio \
26
+ spaces \
27
+ diffusers \
28
+ transformers \
29
+ accelerate \
30
+ safetensors \
31
+ pillow \
32
+ opencv-python-headless \
33
+ scipy
34
+
35
+ # Copy custom launcher script
36
+ COPY app.py /app/app.py
37
+
38
+ # Copy custom nodes and workflows
39
+ COPY custom_nodes/ /app/custom_nodes/
40
+ COPY workflows/ /app/workflows/
41
+
42
+ # Expose port
43
+ EXPOSE 7860
44
+
45
+ # Run the launcher app
46
+ CMD ["python", "app.py"]
app.py CHANGED
@@ -1,250 +1,44 @@
1
- """
2
- DGG ComfyUI API Wrapper for HuggingFace Spaces (Zero GPU)
3
- Provides Gradio interface and API endpoints for NWN character enhancement.
4
- Uses Zero GPU - GPU is only allocated during inference.
5
- """
6
- import os
7
- import subprocess
8
- import threading
9
- import time
10
- import json
11
- import base64
12
- from pathlib import Path
13
- from io import BytesIO
14
- import random
15
-
16
- import gradio as gr
17
- from PIL import Image
18
- import numpy as np
19
- try:
20
- import cv2
21
- except ImportError:
22
- print("CV2 not found, installing headless...")
23
- import subprocess
24
- subprocess.check_call(["pip", "install", "opencv-python-headless"])
25
- import cv2
26
- import spaces # HuggingFace Zero GPU
27
-
28
- try:
29
- import torch
30
- from diffusers import StableDiffusionImg2ImgPipeline
31
- DIFFUSERS_AVAILABLE = True
32
- except ImportError:
33
- DIFFUSERS_AVAILABLE = False
34
- print("Diffusers not available, will use fallback")
35
-
36
- # Global pipeline (loaded on first use)
37
- _pipeline = None
38
- _pipeline_lock = threading.Lock()
39
-
40
- def get_pipeline():
41
- """Get or create the Stable Diffusion pipeline."""
42
- global _pipeline
43
- if _pipeline is not None:
44
- return _pipeline
45
-
46
- with _pipeline_lock:
47
- if _pipeline is not None:
48
- return _pipeline
49
-
50
- # Use v1-5 for general purpose (Terrain + Char)
51
- model_id = "runwayml/stable-diffusion-v1-5"
52
-
53
- _pipeline = StableDiffusionImg2ImgPipeline.from_pretrained(
54
- model_id,
55
- torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
56
- safety_checker=None,
57
- requires_safety_checker=False
58
- )
59
- if torch.cuda.is_available():
60
- _pipeline = _pipeline.to("cuda")
61
- return _pipeline
62
-
63
- @spaces.GPU(duration=60)
64
- def enhance_image_gpu(
65
- image: Image.Image,
66
- prompt: str,
67
- negative_prompt: str,
68
- strength: float = 0.65,
69
- guidance_scale: float = 7.5,
70
- num_inference_steps: int = 25,
71
- seed: int = -1
72
- ) -> Image.Image:
73
- if not DIFFUSERS_AVAILABLE:
74
- return image
75
-
76
- pipe = get_pipeline()
77
- if torch.cuda.is_available():
78
- pipe = pipe.to("cuda")
79
-
80
- if image.mode != "RGB":
81
- image = image.convert("RGB")
82
-
83
- # Resize Logic (Maintain aspect, Power of 8)
84
- w, h = image.size
85
- w = (w // 8) * 8
86
- h = (h // 8) * 8
87
- image = image.resize((w, h), Image.Resampling.LANCZOS)
88
-
89
- generator = None
90
- if seed >= 0:
91
- generator = torch.Generator(device="cuda" if torch.cuda.is_available() else "cpu")
92
- generator.manual_seed(seed)
93
-
94
- result = pipe(
95
- prompt=prompt,
96
- image=image,
97
- strength=strength,
98
- guidance_scale=guidance_scale,
99
- num_inference_steps=num_inference_steps,
100
- negative_prompt=negative_prompt,
101
- generator=generator
102
- ).images[0]
103
-
104
- return result
105
-
106
- # --- CHARACTER LOGIC ---
107
- def enhance_nwn_character(input_image, character_type, denoise, steps, seed):
108
- if input_image is None: return None
109
- prompt = f"photorealistic {character_type}, highly detailed, 8k, cinematic lighting"
110
- neg = "blurry, low quality, low poly, bad anatomy, watermark, text"
111
- return enhance_image_gpu(input_image, prompt, neg, denoise, 7.5, steps, seed)
112
-
113
- CHARACTER_PRESETS = [
114
- "female elf paladin in ornate silver armor",
115
- "male human warrior in plate armor",
116
- "female human mage in flowing robes"
117
- ]
118
-
119
- # --- TERRAIN LOGIC ---
120
- def generate_noise_map(resolution=512, seed=-1):
121
- if seed >= 0:
122
- np.random.seed(seed)
123
- # Simple fractal noise approximation
124
- noise = np.random.rand(resolution, resolution).astype(np.float32)
125
- # Blur to create "hills"
126
- noise = cv2.GaussianBlur(noise, (101, 101), 0)
127
- noise = (noise - noise.min()) / (noise.max() - noise.min())
128
- return noise
129
-
130
- def erosion_sim(heightmap, iterations=10):
131
- # Fast blur-based erosion
132
- for _ in range(iterations):
133
- blurred = cv2.GaussianBlur(heightmap, (3, 3), 0)
134
- # Mix: Enhance valleys, sharpen peaks?
135
- # Simple: H_new = H - (H - Blur) * strength
136
- heightmap = heightmap - (heightmap - blurred) * 0.1
137
- return heightmap
138
-
139
- def generate_terrain(seed, erosion_steps, ai_strength):
140
- # 1. Base Noise
141
- res = 512
142
- h_map = generate_noise_map(res, seed)
143
-
144
- # 2. Convert to Image for AI
145
- img_pil = Image.fromarray((h_map * 255).astype(np.uint8)).convert("RGB")
146
-
147
- # 3. AI Enhancement (Hallucinate details)
148
- prompt = "high altitude aerial view of realistic mountain terrain heightmap, grayscale, erosion, geological details, 8k"
149
- neg = "color, trees, water, buildings, roads, text, map overlay"
150
-
151
- enhanced = enhance_image_gpu(
152
- img_pil, prompt, neg, strength=ai_strength, seed=seed
153
- )
154
-
155
- # 4. Post-Process (16-bit conversion)
156
- enhanced_np = np.array(enhanced.convert("L")).astype(np.float32) / 255.0
157
-
158
- # 5. Erosion on AI result
159
- eroded = erosion_sim(enhanced_np, erosion_steps)
160
-
161
- # 6. Save as 16-bit
162
- h_16 = (eroded * 65535).clip(0, 65535).astype(np.uint16)
163
-
164
- out_path = "output_terrain.png"
165
- cv2.imwrite(out_path, h_16)
166
-
167
- # Return 8-bit preview and file path
168
- preview = (eroded * 255).astype(np.uint8)
169
- return Image.fromarray(preview), out_path
170
-
171
- def generate_courtyard_blueprint(seed, style, detail_level):
172
- # 1. Create a base layout (Top-down blueprint style)
173
- res = 512
174
- # Create white canvas
175
- canvas = np.ones((res, res, 3), dtype=np.uint8) * 255
176
-
177
- if seed >= 0:
178
- np.random.seed(seed)
179
-
180
- # Draw a basic courtyard rectangle in center
181
- cx, cy = res // 2, res // 2
182
- w, h = 300, 200
183
- cv2.rectangle(canvas, (cx - w//2, cy - h//2), (cx + w//2, cy + h//2), (0, 0, 0), 2)
184
- # Draw a gate gap at south
185
- cv2.rectangle(canvas, (cx - 30, cy + h//2 - 5), (cx + 30, cy + h//2 + 5), (255, 255, 255), -1)
186
- # Draw central feature circle
187
- cv2.circle(canvas, (cx, cy), 20, (50, 50, 50), -1)
188
-
189
- # Add some "blueprint" details via OpenCV
190
- # Grid lines
191
- for i in range(0, res, 50):
192
- cv2.line(canvas, (i, 0), (i, res), (220, 220, 220), 1)
193
- cv2.line(canvas, (0, i), (res, i), (220, 220, 220), 1)
194
-
195
- # Text labels
196
- cv2.putText(canvas, f"STYLE: {style.upper()}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
197
- cv2.putText(canvas, f"SEED: {seed}", (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
198
- cv2.putText(canvas, "TARGET: COURTYARD RECONSTRUCTION", (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
199
-
200
- return Image.fromarray(canvas)
201
-
202
- # --- APP UI ---
203
- with gr.Blocks(title="DGG Suite (Zero GPU)", theme=gr.themes.Soft()) as demo:
204
- gr.Markdown("# 🛠️ DGG Content Suite")
205
-
206
- with gr.Tabs():
207
- # TAB 1: CHARACTERS
208
- with gr.Tab("Character Enhancer"):
209
- with gr.Row():
210
- with gr.Column():
211
- c_in = gr.Image(type="pil", label="Input")
212
- c_type = gr.Dropdown(CHARACTER_PRESETS, label="Type", value=CHARACTER_PRESETS[0], allow_custom_value=True)
213
- c_str = gr.Slider(0.3, 1.0, 0.65, label="Strength")
214
- c_seed = gr.Number(-1, label="Seed")
215
- c_btn = gr.Button("Enhance", variant="primary")
216
- with gr.Column():
217
- c_out = gr.Image(label="Result")
218
- c_btn.click(enhance_nwn_character, [c_in, c_type, c_str, gr.Number(25, visible=False), c_seed], c_out)
219
-
220
- # TAB 2: TERRAIN
221
- with gr.Tab("Terrain Builder"):
222
- gr.Markdown("Generate 16-bit Heightmaps for UE5")
223
- with gr.Row():
224
- with gr.Column():
225
- t_seed = gr.Number(-1, label="Seed")
226
- t_iter = gr.Slider(0, 50, 10, label="Erosion Steps")
227
- t_ai = gr.Slider(0.0, 1.0, 0.5, label="AI Upscale Strength")
228
- t_btn = gr.Button("Generate Heightmap", variant="primary")
229
- with gr.Column():
230
- t_prev = gr.Image(label="Preview (8-bit)")
231
- t_file = gr.File(label="Download 16-bit PNG")
232
-
233
- t_btn.click(generate_terrain, [t_seed, t_iter, t_ai], [t_prev, t_file])
234
-
235
- # TAB 3: COURTYARD
236
- with gr.Tab("Courtyard Architect"):
237
- gr.Markdown("Generate blueprint reference for AI construction")
238
- with gr.Row():
239
- with gr.Column():
240
- cy_seed = gr.Number(-1, label="Seed")
241
- cy_style = gr.Dropdown(["Gothic", "Ancient", "Cybernetic", "Natural"], label="Style", value="Gothic")
242
- cy_detail = gr.Radio(["Low", "Medium", "High"], label="Detail Level", value="Medium")
243
- cy_btn = gr.Button("Generate Blueprint", variant="primary")
244
- with gr.Column():
245
- cy_out = gr.Image(label="Blueprint Reference")
246
-
247
- cy_btn.click(generate_courtyard_blueprint, [cy_seed, cy_style, cy_detail], cy_out, api_name="generate_courtyard_blueprint")
248
-
249
- if __name__ == "__main__":
250
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ import os
2
+ import subprocess
3
+ import urllib.request
4
+ import torch
5
+
6
+ def download_file(url, dest):
7
+ print(f"Downloading {url} to {dest}...")
8
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
9
+
10
+ # Simple console progress log
11
+ last_reported = -10
12
+ def progress(count, block_size, total_size):
13
+ nonlocal last_reported
14
+ if total_size <= 0:
15
+ return
16
+ percent = int(count * block_size * 100 / total_size)
17
+ if percent >= last_reported + 10:
18
+ print(f"Download progress: {percent}%")
19
+ last_reported = percent
20
+
21
+ urllib.request.urlretrieve(url, dest, reporthook=progress)
22
+ print("Download complete!")
23
+
24
+ # Check/download SD 1.5 model if missing
25
+ checkpoint_path = "models/checkpoints/v1-5-pruned-emaonly.safetensors"
26
+ if not os.path.exists(checkpoint_path):
27
+ sd15_url = "https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.safetensors"
28
+ try:
29
+ download_file(sd15_url, checkpoint_path)
30
+ except Exception as e:
31
+ print(f"Error downloading checkpoint: {e}")
32
+
33
+ # Detect CPU vs GPU
34
+ cuda_available = torch.cuda.is_available()
35
+ cmd = ["python", "main.py", "--listen", "0.0.0.0", "--port", "7860"]
36
+ if not cuda_available:
37
+ print("CUDA not available. Running ComfyUI in CPU mode...")
38
+ cmd.append("--cpu")
39
+ else:
40
+ print("CUDA available. Running ComfyUI with GPU acceleration!")
41
+
42
+ # Start ComfyUI
43
+ print(f"Launching ComfyUI: {' '.join(cmd)}")
44
+ subprocess.run(cmd)