Files
2025-10-17 17:34:17 -05:00

413 lines
16 KiB
Python

import time
from aiohttp import web
import os
import json
import folder_paths
import server # Import server for node_info
import uuid # For generating unique filenames
import aiohttp
HIDDEN_FOLDERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".hidden_folders")
def read_hidden_folders():
if not os.path.exists(HIDDEN_FOLDERS_FILE):
return []
with open(HIDDEN_FOLDERS_FILE, 'r') as f:
return [line.strip() for line in f.readlines()]
def write_hidden_folders(hidden_folders):
with open(HIDDEN_FOLDERS_FILE, 'w') as f:
f.write('\n'.join(hidden_folders))
async def get_hello(request: web.Request) -> web.Response:
return web.json_response({"status": "success", "message": "Hello from the CozyGen API!"})
async def toggle_folder_visibility(request: web.Request) -> web.Response:
data = await request.json()
folder_to_toggle = data.get('folder')
if not folder_to_toggle:
return web.json_response({"error": "Missing 'folder' in request body"}, status=400)
hidden_folders = read_hidden_folders()
if folder_to_toggle in hidden_folders:
hidden_folders.remove(folder_to_toggle)
else:
hidden_folders.append(folder_to_toggle)
write_hidden_folders(hidden_folders)
return web.json_response({"status": "success", "hidden_folders": hidden_folders})
async def get_gallery_files(request: web.Request) -> web.Response:
subfolder = request.rel_url.query.get('subfolder', '')
show_hidden_str = request.rel_url.query.get('show_hidden', 'false')
show_hidden = show_hidden_str.lower() == 'true'
try:
page = int(request.rel_url.query.get('page', '1'))
per_page = int(request.rel_url.query.get('per_page', '20'))
except ValueError:
return web.json_response({"error": "Invalid page or per_page parameter"}, status=400)
output_directory = folder_paths.get_output_directory()
hidden_folders = read_hidden_folders()
# Security: Prevent directory traversal
gallery_path = os.path.normpath(os.path.join(output_directory, subfolder))
if not gallery_path.startswith(output_directory):
return web.json_response({"error": "Unauthorized path"}, status=403)
if not os.path.exists(gallery_path) or not os.path.isdir(gallery_path):
return web.json_response({"error": "Gallery directory not found"}, status=404)
items = os.listdir(gallery_path)
gallery_items = []
for item_name in items:
item_path = os.path.join(gallery_path, item_name)
relative_item_path = os.path.join(subfolder, item_name)
if not show_hidden and relative_item_path in hidden_folders:
continue
if os.path.isdir(item_path):
mod_time = os.path.getmtime(item_path)
gallery_items.append({
"filename": item_name,
"type": "directory",
"subfolder": os.path.join(subfolder, item_name),
"mod_time": mod_time,
"hidden": relative_item_path in hidden_folders
})
elif item_name.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.mp4', '.webm', '.mp3', '.wav', '.flac')):
mod_time = os.path.getmtime(item_path)
gallery_items.append({
"filename": item_name,
"type": "output",
"subfolder": subfolder,
"mod_time": mod_time
})
# Sort items: directories first, then by modification time
gallery_items.sort(key=lambda x: (x['type'] == 'directory', x.get('mod_time', 0)), reverse=True)
# Pagination
total_items = len(gallery_items)
total_pages = (total_items + per_page - 1) // per_page
start_index = (page - 1) * per_page
end_index = start_index + per_page
paginated_items = gallery_items[start_index:end_index]
# Remove mod_time before sending
for item in paginated_items:
if 'mod_time' in item:
del item['mod_time']
return web.json_response({
"items": paginated_items,
"page": page,
"per_page": per_page,
"total_pages": total_pages,
"total_items": total_items
})
async def upload_image(request: web.Request) -> web.Response:
reader = await request.multipart()
field = await reader.next()
if field.name != 'image':
return web.json_response({"error": "Expected field 'image'"}, status=400)
filename = field.filename
if not filename:
return web.json_response({"error": "No filename provided"}, status=400)
# Generate a unique filename to prevent collisions
unique_filename = f"{uuid.uuid4()}_{filename}"
# Save to the input directory of ComfyUI
input_dir = folder_paths.get_input_directory()
save_path = os.path.join(input_dir, unique_filename)
size = 0
with open(save_path, 'wb') as f:
while True:
chunk = await field.read_chunk()
if not chunk:
break
f.write(chunk)
size += len(chunk)
return web.json_response({"filename": unique_filename, "size": size})
async def get_workflow_list(request: web.Request) -> web.Response:
workflows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "workflows")
if not os.path.exists(workflows_dir):
return web.json_response({"error": "Workflows directory not found"}, status=404)
workflow_files = [f for f in os.listdir(workflows_dir) if f.endswith('.json')]
return web.json_response({"workflows": workflow_files})
async def get_workflow_file(request: web.Request) -> web.Response:
filename = request.match_info.get('filename', '')
workflows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "workflows")
workflow_path = os.path.join(workflows_dir, filename)
if not os.path.exists(workflow_path):
return web.json_response({"error": f"Workflow file '{filename}' not found"}, status=404)
try:
with open(workflow_path, 'r', encoding='utf-8') as f:
workflow_content = json.load(f)
return web.json_response(workflow_content)
except json.JSONDecodeError:
return web.json_response({"error": f"Invalid JSON in workflow file '{filename}'"}, status=400)
except Exception:
return web.json_response({"error": f"Error reading workflow file: {filename}"}, status=500)
import comfy.samplers
# Get all valid model folder types from ComfyUI itself
valid_model_types = folder_paths.folder_names_and_paths.keys()
# A map for aliases to official folder_paths names
alias_map = {
"unet": "unet_gguf"
}
async def get_choices(request: web.Request) -> web.Response:
choice_type = request.rel_url.query.get('type', '')
if not choice_type:
return web.json_response({"error": "Missing 'type' query parameter"}, status=400)
# Alias map for backward compatibility
alias_map = {
"samplers_list": "sampler",
"schedulers_list": "scheduler",
"unet": "unet_gguf" # Example of another potential alias
}
resolved_choice_type = alias_map.get(choice_type, choice_type)
choices = []
if resolved_choice_type == "scheduler":
choices = comfy.samplers.KSampler.SCHEDULERS
elif resolved_choice_type == "sampler":
choices = comfy.samplers.KSampler.SAMPLERS
elif resolved_choice_type in valid_model_types:
choices = folder_paths.get_filename_list(resolved_choice_type)
else:
return web.json_response({"error": f"Invalid choice type: {choice_type}"}, status=400)
return web.json_response({"choices": choices})
import asyncio
from collections import deque
# Global queue to hold generation jobs
# Each job is a dict: {'id': str, 'status': str, 'data': dict, 'workflowName': str}
server_queue = deque()
queue_lock = asyncio.Lock()
async def submit_to_queue(request: web.Request) -> web.Response:
"""Adds a new workflow generation job to the server-side queue."""
try:
payload = await request.json()
prompt = payload.get('prompt')
workflow_name = payload.get('workflowName', 'Unknown')
if not prompt:
return web.json_response({"error": "Missing 'prompt' in request body"}, status=400)
job_id = str(uuid.uuid4())
job = {
"id": job_id,
"status": "queued",
"data": prompt,
"workflowName": workflow_name,
"timestamp": time.time(),
"thumbnailUrl": None, # Will be populated on completion
"comfy_prompt_id": None, # Will be populated by the worker
}
async with queue_lock:
server_queue.append(job)
return web.json_response({"status": "success", "job_id": job_id})
except json.JSONDecodeError:
return web.json_response({"error": "Invalid JSON in request body"}, status=400)
except Exception:
return web.json_response({"error": "An internal server error occurred"}, status=500)
async def get_queue_status(request: web.Request) -> web.Response:
"""Returns the current state of the generation queue."""
async with queue_lock:
# Simple pruning: if queue is long, trim oldest completed
while len(server_queue) > 50:
try:
oldest_completed = next(j for j in server_queue if j['status'] in ['completed', 'error'])
server_queue.remove(oldest_completed)
except StopIteration:
break # No completed jobs to prune
queue_list = sorted(list(server_queue), key=lambda j: j.get('timestamp', 0), reverse=True)
return web.json_response({"queue": queue_list})
async def queue_worker():
"""The background worker that processes the server-side queue."""
prompt_url = "http://127.0.0.1:8188/prompt"
async with aiohttp.ClientSession() as session:
while True:
job_to_run = None
async with queue_lock:
queued_job = next((job for job in server_queue if job['status'] == 'queued'), None)
if queued_job:
queued_job['status'] = 'running'
job_to_run = queued_job
if job_to_run:
try:
job_id = job_to_run['id']
prompt = job_to_run['data']
# Dynamically inject the job_id into the workflow
output_node_id = None
for node_id, node_data in prompt.items():
if node_data["class_type"] in ["CozyGenOutput", "CozyGenVideoOutput"]:
output_node_id = node_id
break
if output_node_id:
# Find a unique ID for our new node
job_id_node_id = str(max([int(k) for k in prompt.keys() if k.isdigit()] + [0]) + 1)
# Add the new CozyGenJobID node
prompt[job_id_node_id] = {
"inputs": {"job_id": job_id},
"class_type": "CozyGenJobID",
}
# Link the output node to our new job_id node
prompt[output_node_id]["inputs"]["job_id"] = [job_id_node_id, 0]
# Prepare the payload for the /prompt endpoint (no longer needs extra_data)
payload = {"prompt": prompt}
async with session.post(prompt_url, json=payload) as response:
if response.status == 200:
response_data = await response.json()
comfy_prompt_id = response_data.get('prompt_id')
async with queue_lock:
job_to_run['comfy_prompt_id'] = comfy_prompt_id
else:
async with queue_lock:
job_to_run['status'] = 'queued'
except Exception:
async with queue_lock:
job_to_run['status'] = 'queued'
await asyncio.sleep(1)
async def cozygen_startup(app):
"""Starts the CozyGen background worker on server startup."""
asyncio.create_task(queue_worker())
async def get_presets(request: web.Request) -> web.Response:
workflow_name = request.rel_url.query.get('workflow', '')
if not workflow_name:
return web.json_response({"error": "Missing 'workflow' query parameter"}, status=400)
presets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "presets")
workflow_presets_dir = os.path.join(presets_dir, os.path.splitext(workflow_name)[0])
if not os.path.exists(workflow_presets_dir):
return web.json_response({"presets": []})
preset_files = [f for f in os.listdir(workflow_presets_dir) if f.endswith('.json')]
return web.json_response({"presets": preset_files})
async def get_preset_file(request: web.Request) -> web.Response:
workflow_name = request.match_info.get('workflow_name', '')
preset_name = request.match_info.get('preset_name', '')
presets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "presets")
preset_path = os.path.join(presets_dir, os.path.splitext(workflow_name)[0], preset_name)
if not os.path.exists(preset_path):
return web.json_response({"error": f"Preset file '{preset_name}' not found"}, status=404)
try:
with open(preset_path, 'r', encoding='utf-8') as f:
preset_content = json.load(f)
return web.json_response(preset_content)
except Exception as e:
return web.json_response({"error": f"Error reading preset file: {e}"}, status=500)
async def save_preset(request: web.Request) -> web.Response:
data = await request.json()
workflow_name = data.get('workflow')
preset_name = data.get('preset_name')
form_data = data.get('form_data')
if not all([workflow_name, preset_name, form_data]):
return web.json_response({"error": "Missing required fields"}, status=400)
presets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "presets")
workflow_presets_dir = os.path.join(presets_dir, os.path.splitext(workflow_name)[0])
os.makedirs(workflow_presets_dir, exist_ok=True)
preset_path = os.path.join(workflow_presets_dir, f"{preset_name}.json")
try:
with open(preset_path, 'w', encoding='utf-8') as f:
json.dump(form_data, f, indent=4)
return web.json_response({"status": "success"})
except Exception as e:
return web.json_response({"error": f"Error saving preset file: {e}"}, status=500)
async def delete_preset(request: web.Request) -> web.Response:
data = await request.json()
workflow_name = data.get('workflow')
preset_name = data.get('preset_name')
if not all([workflow_name, preset_name]):
return web.json_response({"error": "Missing required fields"}, status=400)
presets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "presets")
preset_path = os.path.join(presets_dir, os.path.splitext(workflow_name)[0], preset_name)
if not os.path.exists(preset_path):
return web.json_response({"error": "Preset not found"}, status=404)
try:
os.remove(preset_path)
return web.json_response({"status": "success"})
except Exception as e:
return web.json_response({"error": f"Error deleting preset: {e}"}, status=500)
routes = [
web.get('/cozygen/hello', get_hello),
web.get('/cozygen/gallery', get_gallery_files),
web.post('/cozygen/toggle_folder_visibility', toggle_folder_visibility),
web.post('/cozygen/upload_image', upload_image),
web.get('/cozygen/workflows', get_workflow_list),
web.get('/cozygen/workflows/{filename}', get_workflow_file),
web.get('/cozygen/get_choices', get_choices),
web.post('/cozygen/submit', submit_to_queue),
web.get('/cozygen/queue', get_queue_status),
web.get('/cozygen/presets', get_presets),
web.get('/cozygen/presets/{workflow_name}/{preset_name}', get_preset_file),
web.post('/cozygen/presets', save_preset),
web.delete('/cozygen/presets', delete_preset),
]
# Register the startup function correctly
server.PromptServer.instance.app.on_startup.append(cozygen_startup)