- ComfyUI custom nodes: BridgeInput (11 types) + BridgeOutput
- AnyType wildcard for universal output capture
- Custom server route: GET /comfybridge/results/{prompt_id}
- Python async client (ComfyBridge) + sync wrapper (ComfyBridgeSync)
- Workflow JSON parser and patcher (deepcopy, original unchanged)
- WebSocket completion detection
- BridgeResult container with PIL/bytes helpers
- Examples: basic_text_to_image.py, agent_loop.py
- Unit tests for workflow parser and client
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
624 lines
20 KiB
Markdown
624 lines
20 KiB
Markdown
# ComfyBridgeIO 아키텍처 설계
|
|
|
|
## 1. 프로젝트 개요
|
|
|
|
### 목적
|
|
ComfyBridgeIO는 ComfyUI 커스텀 노드 패키지와 Python 클라이언트 래퍼로 구성된다. AI Agent나 외부 스크립트가 ComfyUI 워크플로우를 **런타임에 동적으로 제어**할 수 있도록 표준화된 입출력 브리지를 제공한다.
|
|
|
|
### 핵심 동작 원리
|
|
|
|
```
|
|
[AI Agent / 외부 스크립트]
|
|
│
|
|
│ bridge.run("workflow.json", inputs={...})
|
|
▼
|
|
[ComfyBridge 클라이언트]
|
|
1. workflow.json 파싱 → BridgeInput 노드 탐색
|
|
2. 노드 inputs.value 필드 패치 (런타임 값 주입)
|
|
3. POST /prompt → ComfyUI 서버에 실행 요청
|
|
4. WebSocket 수신 → 실행 완료 대기
|
|
5. GET /history/{prompt_id} → BridgeOutput 노드 결과 수집
|
|
6. GET /view or GET /comfybridge/results/{prompt_id} → 바이너리/스칼라 회수
|
|
│
|
|
│ BridgeResult 객체 반환
|
|
▼
|
|
[AI Agent]
|
|
result.get("output_image") → PIL.Image / bytes
|
|
result.get("caption") → str
|
|
```
|
|
|
|
### 설계 원칙
|
|
- **YAGNI**: 현재 요구사항 범위 외 기능 추가 금지
|
|
- **ComfyUI 버전 호환성**: AnyType 트릭을 사용해 와일드카드 타입 안정성 확보
|
|
- **비동기 우선**: 클라이언트는 `asyncio` 기반, 동기 래퍼 별도 제공
|
|
- **단방향 패치**: 클라이언트가 워크플로우 JSON을 수정하되 원본 파일은 변경하지 않음
|
|
|
|
---
|
|
|
|
## 2. 디렉토리 구조
|
|
|
|
```
|
|
ComfyBridgeIO/
|
|
├── __init__.py # ComfyUI 노드 등록 진입점
|
|
├── nodes/
|
|
│ ├── __init__.py
|
|
│ ├── bridge_input.py # BridgeInput 노드 (타입별 팩토리 생성)
|
|
│ ├── bridge_output.py # BridgeOutput 노드 (타입별 팩토리 생성)
|
|
│ ├── any_type.py # AnyType 와일드카드 헬퍼
|
|
│ └── server_routes.py # 커스텀 API 라우트 (/comfybridge/*)
|
|
├── client/
|
|
│ ├── __init__.py # ComfyBridge, BridgeResult export
|
|
│ ├── bridge.py # ComfyBridge 메인 클래스 (async)
|
|
│ ├── bridge_sync.py # 동기 래퍼 (asyncio.run 기반)
|
|
│ ├── workflow.py # 워크플로우 JSON 파서 / 패처
|
|
│ ├── websocket_client.py # WebSocket 통신 및 완료 대기
|
|
│ ├── result.py # BridgeResult 클래스
|
|
│ └── types.py # 타입 상수, 변환 유틸리티
|
|
├── examples/
|
|
│ ├── basic_text_to_image.py # 기본 텍스트-이미지 예제
|
|
│ ├── agent_loop.py # AI Agent 루프 예제
|
|
│ └── workflows/
|
|
│ └── sdxl_bridge_template.json
|
|
├── tests/
|
|
│ ├── __init__.py
|
|
│ ├── test_workflow_parser.py
|
|
│ ├── test_bridge_client.py
|
|
│ └── fixtures/
|
|
│ └── sample_workflow.json
|
|
├── pyproject.toml
|
|
├── requirements.txt # 클라이언트 의존성
|
|
├── requirements-dev.txt
|
|
├── README.md
|
|
└── ARCHITECTURE.md
|
|
```
|
|
|
|
---
|
|
|
|
## 3. 커스텀 노드 설계
|
|
|
|
### 3-1. AnyType 와일드카드 (`nodes/any_type.py`)
|
|
|
|
ComfyUI의 타입 검증 시스템을 우회하기 위해 AnyType 클래스를 사용한다. `__ne__` 를 항상 `False` 로 반환하여 어떤 타입과도 동등 비교되도록 한다.
|
|
|
|
```python
|
|
class AnyType(str):
|
|
"""
|
|
ComfyUI 타입 검증을 통과하는 와일드카드 타입.
|
|
str을 상속하여 직렬화 호환성 유지.
|
|
"""
|
|
def __ne__(self, other: object) -> bool:
|
|
return False
|
|
|
|
ANY = AnyType("*")
|
|
```
|
|
|
|
이 타입은 BridgeOutput 노드의 입력부에 사용하며, 어떤 타입의 ComfyUI 출력도 연결 가능하게 한다.
|
|
|
|
---
|
|
|
|
### 3-2. BridgeInput 노드 (`nodes/bridge_input.py`)
|
|
|
|
#### 책임
|
|
워크플로우 안에서 **외부에서 주입 가능한 값의 입력 포인트** 역할을 한다. 클라이언트가 이 노드를 이름으로 탐색하여 `value` 위젯 값을 패치한다.
|
|
|
|
#### 타입별 팩토리 패턴
|
|
|
|
동일한 로직을 반복하지 않기 위해 팩토리 함수로 각 타입별 노드 클래스를 생성한다.
|
|
|
|
```python
|
|
SUPPORTED_TYPES = {
|
|
"STRING": ("STRING", {"default": "", "multiline": True}),
|
|
"INT": ("INT", {"default": 0, "min": -2**31, "max": 2**31}),
|
|
"FLOAT": ("FLOAT", {"default": 0.0, "min": -1e9, "max": 1e9}),
|
|
"BOOLEAN": ("BOOLEAN", {"default": False}),
|
|
"IMAGE": ("IMAGE", {}), # 위젯 없음, 입력 포트만
|
|
"MASK": ("MASK", {}),
|
|
"LATENT": ("LATENT", {}),
|
|
"MODEL": ("MODEL", {}),
|
|
"CLIP": ("CLIP", {}),
|
|
"VAE": ("VAE", {}),
|
|
"CONDITIONING": ("CONDITIONING", {}),
|
|
}
|
|
|
|
def make_bridge_input(type_name: str, comfy_type, widget_config: dict):
|
|
"""
|
|
타입별 BridgeInput 노드 클래스를 동적으로 생성한다.
|
|
"""
|
|
is_primitive = type_name in ("STRING", "INT", "FLOAT", "BOOLEAN")
|
|
|
|
class BridgeInputNode:
|
|
CATEGORY = "ComfyBridgeIO"
|
|
FUNCTION = "passthrough"
|
|
RETURN_TYPES = (comfy_type,)
|
|
RETURN_NAMES = ("value",)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
inputs = {
|
|
"required": {
|
|
"name": ("STRING", {"default": "input_name"}),
|
|
}
|
|
}
|
|
if is_primitive:
|
|
# 스칼라 타입: 위젯으로 기본값 설정
|
|
inputs["required"]["value"] = (comfy_type, widget_config)
|
|
else:
|
|
# 복합 타입(IMAGE 등): 선택적 연결 포트
|
|
inputs["optional"] = {
|
|
"value": (comfy_type, {})
|
|
}
|
|
return inputs
|
|
|
|
def passthrough(self, name: str, value=None):
|
|
return (value,)
|
|
|
|
BridgeInputNode.__name__ = f"BridgeInput{type_name}"
|
|
BridgeInputNode.__qualname__ = f"BridgeInput{type_name}"
|
|
return BridgeInputNode
|
|
```
|
|
|
|
#### 클라이언트와의 계약
|
|
|
|
| 노드 필드 | 역할 |
|
|
|-----------|------|
|
|
| `class_type` | `"BridgeInputSTRING"`, `"BridgeInputINT"` 등 (패턴: `BridgeInput*`) |
|
|
| `inputs.name` | 클라이언트 딕셔너리의 키와 매칭되는 식별자 |
|
|
| `inputs.value` | 클라이언트가 런타임에 패치하는 필드 |
|
|
|
|
---
|
|
|
|
### 3-3. BridgeOutput 노드 (`nodes/bridge_output.py`)
|
|
|
|
#### 책임
|
|
워크플로우 안에서 **결과를 이름으로 태깅하는 출력 포인트** 역할을 한다. `OUTPUT_NODE = True` 로 선언하여 ComfyUI가 이 노드의 실행을 보장하게 한다. 실행 후 결과를 서버 메모리에 저장하고 커스텀 API 라우트로 노출한다.
|
|
|
|
#### 설계
|
|
|
|
```python
|
|
from .any_type import ANY
|
|
from .server_routes import store_bridge_result
|
|
|
|
class BridgeOutput:
|
|
CATEGORY = "ComfyBridgeIO"
|
|
FUNCTION = "capture"
|
|
OUTPUT_NODE = True
|
|
RETURN_TYPES = () # 터미널 노드: 다운스트림 연결 없음
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"name": ("STRING", {"default": "output_name"}),
|
|
"value": (ANY, {}),
|
|
},
|
|
"hidden": {
|
|
"prompt_id": "PROMPT_ID", # ComfyUI가 런타임에 주입
|
|
"unique_id": "UNIQUE_ID",
|
|
}
|
|
}
|
|
|
|
def capture(self, name: str, value, prompt_id: str, unique_id: str):
|
|
store_bridge_result(prompt_id=prompt_id, name=name, value=value)
|
|
return {}
|
|
```
|
|
|
|
**IMAGE 타입 처리**: BridgeOutput이 IMAGE 값을 받으면 `store_bridge_result` 내부에서 PIL Image로 변환 후 메모리에 보관. 클라이언트는 `/comfybridge/results/{prompt_id}` 로 base64 인코딩된 PNG를 수신.
|
|
|
|
---
|
|
|
|
### 3-4. 서버 커스텀 라우트 (`nodes/server_routes.py`)
|
|
|
|
ComfyUI의 `PromptServer`에 라우트를 추가하여 BridgeOutput 결과를 외부로 노출한다.
|
|
|
|
```
|
|
GET /comfybridge/results/{prompt_id}
|
|
→ { "outputs": { "output_name": <value>, ... } }
|
|
→ IMAGE 타입은 base64 PNG 문자열로 직렬화
|
|
→ 스칼라 타입은 JSON 네이티브 값으로 직렬화
|
|
```
|
|
|
|
결과 저장소: 모듈 레벨 `dict[prompt_id → dict[name → value]]`. 최대 100개 prompt_id 보관 (LRU 방식으로 오래된 항목 제거).
|
|
|
|
---
|
|
|
|
### 3-5. 노드 등록 (`__init__.py`)
|
|
|
|
```python
|
|
NODE_CLASS_MAPPINGS = {}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {}
|
|
|
|
# BridgeInput 타입별 등록
|
|
for type_name, (comfy_type, widget_cfg) in SUPPORTED_TYPES.items():
|
|
cls = make_bridge_input(type_name, comfy_type, widget_cfg)
|
|
node_id = f"BridgeInput{type_name}"
|
|
NODE_CLASS_MAPPINGS[node_id] = cls
|
|
NODE_DISPLAY_NAME_MAPPINGS[node_id] = f"Bridge Input ({type_name})"
|
|
|
|
# BridgeOutput 등록
|
|
NODE_CLASS_MAPPINGS["BridgeOutput"] = BridgeOutput
|
|
NODE_DISPLAY_NAME_MAPPINGS["BridgeOutput"] = "Bridge Output"
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Python 클라이언트 설계
|
|
|
|
### 4-1. ComfyBridge 클래스 API (`client/bridge.py`)
|
|
|
|
```python
|
|
class ComfyBridge:
|
|
def __init__(
|
|
self,
|
|
host: str = "localhost",
|
|
port: int = 8188,
|
|
timeout: float = 300.0,
|
|
): ...
|
|
|
|
async def run(
|
|
self,
|
|
workflow: str | dict, # 파일 경로 또는 파싱된 dict
|
|
inputs: dict[str, Any] = {},
|
|
progress_callback: Callable[[float], None] | None = None,
|
|
) -> BridgeResult: ...
|
|
|
|
async def upload_image(
|
|
self,
|
|
image: bytes | str, # bytes 또는 파일 경로
|
|
filename: str = "input.png",
|
|
) -> str: ... # ComfyUI 서버 내 파일명 반환
|
|
|
|
async def get_queue_status(self) -> dict: ...
|
|
|
|
async def cancel(self, prompt_id: str) -> None: ...
|
|
```
|
|
|
|
#### 동기 래퍼 (`client/bridge_sync.py`)
|
|
|
|
```python
|
|
class ComfyBridgeSync:
|
|
"""asyncio가 없는 환경을 위한 동기 래퍼."""
|
|
|
|
def run(self, workflow, inputs={}, **kwargs) -> BridgeResult:
|
|
return asyncio.run(self._bridge.run(workflow, inputs, **kwargs))
|
|
```
|
|
|
|
---
|
|
|
|
### 4-2. 워크플로우 파서 / 패처 (`client/workflow.py`)
|
|
|
|
#### 파싱 전략
|
|
|
|
ComfyUI 워크플로우 JSON (API 포맷)은 노드 ID를 키로 하는 flat dict다.
|
|
|
|
```json
|
|
{
|
|
"1": { "class_type": "BridgeInputSTRING", "inputs": { "name": "prompt", "value": "default text" } },
|
|
"2": { "class_type": "KSampler", "inputs": { "text": ["1", 0], ... } },
|
|
"9": { "class_type": "BridgeOutput", "inputs": { "name": "output_image", "value": ["8", 0] } }
|
|
}
|
|
```
|
|
|
|
#### BridgeInput 노드 탐색
|
|
|
|
```python
|
|
BRIDGE_INPUT_PREFIX = "BridgeInput"
|
|
|
|
def find_bridge_inputs(workflow: dict) -> dict[str, tuple[str, str]]:
|
|
"""
|
|
Returns: { "input_name": (node_id, type_name) }
|
|
예: { "prompt": ("1", "STRING"), "steps": ("3", "INT") }
|
|
"""
|
|
result = {}
|
|
for node_id, node in workflow.items():
|
|
class_type = node.get("class_type", "")
|
|
if class_type.startswith(BRIDGE_INPUT_PREFIX):
|
|
name = node["inputs"].get("name")
|
|
type_name = class_type[len(BRIDGE_INPUT_PREFIX):]
|
|
if name:
|
|
result[name] = (node_id, type_name)
|
|
return result
|
|
```
|
|
|
|
#### 값 주입 (패치)
|
|
|
|
```python
|
|
def patch_inputs(workflow: dict, inputs: dict[str, Any]) -> dict:
|
|
"""
|
|
원본 workflow를 deepcopy 후 BridgeInput 노드의 value를 패치한다.
|
|
IMAGE 타입: 이미 upload_image()로 업로드된 파일명(str)을 주입.
|
|
"""
|
|
patched = copy.deepcopy(workflow)
|
|
bridge_map = find_bridge_inputs(patched)
|
|
|
|
for name, value in inputs.items():
|
|
if name not in bridge_map:
|
|
raise ValueError(f"BridgeInput '{name}' not found in workflow")
|
|
node_id, type_name = bridge_map[name]
|
|
converted = convert_value(value, type_name)
|
|
patched[node_id]["inputs"]["value"] = converted
|
|
|
|
return patched
|
|
```
|
|
|
|
#### 타입 변환 테이블 (`client/types.py`)
|
|
|
|
| type_name | Python 입력 타입 | 변환 |
|
|
|-----------|-----------------|------|
|
|
| STRING | str | 그대로 |
|
|
| INT | int / str | int() 캐스팅 |
|
|
| FLOAT | float / str | float() 캐스팅 |
|
|
| BOOLEAN | bool / int | bool() 캐스팅 |
|
|
| IMAGE | str (파일명) | 업로드 후 반환된 서버 파일명 |
|
|
| MASK, LATENT, MODEL, CLIP, VAE, CONDITIONING | 노드 링크 `[node_id, idx]` | 그대로 전달 |
|
|
|
|
---
|
|
|
|
### 4-3. WebSocket 통신 흐름 (`client/websocket_client.py`)
|
|
|
|
```
|
|
클라이언트 ComfyUI 서버
|
|
│ │
|
|
├─── GET /ws?clientId={uuid} ──────────►│ WebSocket 연결 수립
|
|
│ │
|
|
├─── POST /prompt ──────────────────────►│ { prompt, client_id, extra_data }
|
|
│◄── { prompt_id, number, ... } ─────────┤ 즉시 응답 (큐에 삽입)
|
|
│ │
|
|
│◄── { type: "execution_start" } ────────┤ 실행 시작
|
|
│◄── { type: "executing", │
|
|
│ data: { node: "1" } } ───────────┤ 노드별 진행 상황
|
|
│◄── { type: "progress", │
|
|
│ data: { value: 5, max: 20 } } ───┤ KSampler 스텝 진행률
|
|
│◄── { type: "executed", │
|
|
│ data: { node: "9", ... } } ──────┤ OUTPUT_NODE 결과 (SaveImage 등)
|
|
│◄── { type: "executing", │
|
|
│ data: { node: null } } ──────────┤ 완료 신호
|
|
│ │
|
|
├─── GET /history/{prompt_id} ──────────►│
|
|
│◄── { prompt_id: { outputs: {...} } } ──┤
|
|
│ │
|
|
├─── GET /comfybridge/results/{id} ─────►│ 비이미지 BridgeOutput 결과
|
|
│◄── { outputs: { "name": value } } ─────┤
|
|
```
|
|
|
|
#### 완료 감지 조건
|
|
|
|
```python
|
|
# WebSocket 메시지 처리 루프
|
|
async def wait_for_completion(ws, target_prompt_id: str) -> None:
|
|
async for message in ws:
|
|
data = json.loads(message)
|
|
msg_type = data.get("type")
|
|
msg_data = data.get("data", {})
|
|
|
|
# 다른 prompt의 메시지 무시
|
|
if msg_data.get("prompt_id") and msg_data["prompt_id"] != target_prompt_id:
|
|
continue
|
|
|
|
if msg_type == "executing" and msg_data.get("node") is None:
|
|
break # node가 null이면 이 prompt의 실행 완료
|
|
|
|
if msg_type == "execution_error":
|
|
raise ComfyBridgeError(msg_data.get("exception_message"))
|
|
```
|
|
|
|
---
|
|
|
|
### 4-4. 결과 회수 전략 (`client/result.py`)
|
|
|
|
#### BridgeResult 클래스
|
|
|
|
```python
|
|
class BridgeResult:
|
|
def __init__(self, raw_outputs: dict[str, Any]):
|
|
self._outputs = raw_outputs
|
|
|
|
def get(self, name: str, default=None) -> Any:
|
|
return self._outputs.get(name, default)
|
|
|
|
def keys(self) -> list[str]:
|
|
return list(self._outputs.keys())
|
|
|
|
def as_pil(self, name: str) -> "PIL.Image.Image":
|
|
"""IMAGE 타입 결과를 PIL Image로 반환."""
|
|
...
|
|
|
|
def as_bytes(self, name: str) -> bytes:
|
|
"""IMAGE 타입 결과를 PNG bytes로 반환."""
|
|
...
|
|
```
|
|
|
|
#### 결과 수집 흐름
|
|
|
|
```python
|
|
async def collect_results(
|
|
session: aiohttp.ClientSession,
|
|
base_url: str,
|
|
prompt_id: str,
|
|
workflow: dict,
|
|
) -> BridgeResult:
|
|
outputs = {}
|
|
|
|
# 1. /history/{prompt_id} 에서 SaveImage / PreviewImage 등 표준 노드 결과 수집
|
|
history = await fetch_history(session, base_url, prompt_id)
|
|
node_outputs = history[prompt_id]["outputs"]
|
|
|
|
for node_id, node_output in node_outputs.items():
|
|
if "images" in node_output:
|
|
for img_info in node_output["images"]:
|
|
# /view?filename=...&subfolder=...&type=... 로 다운로드
|
|
img_bytes = await fetch_image(session, base_url, img_info)
|
|
outputs[f"__node_{node_id}_image"] = img_bytes
|
|
|
|
# 2. /comfybridge/results/{prompt_id} 에서 BridgeOutput 노드 결과 수집
|
|
bridge_outputs = await fetch_bridge_results(session, base_url, prompt_id)
|
|
outputs.update(bridge_outputs)
|
|
|
|
return BridgeResult(outputs)
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 데이터 플로우 다이어그램
|
|
|
|
```
|
|
[AI Agent]
|
|
│
|
|
│ 1. bridge.run("workflow.json", inputs={"prompt": "a cat", "steps": 20})
|
|
▼
|
|
[ComfyBridge.run()]
|
|
│
|
|
├─ 2. workflow.py: JSON 로드 → BridgeInput 노드 탐색
|
|
├─ 3. workflow.py: inputs dict → 노드 value 필드 패치 (deepcopy, 원본 불변)
|
|
├─ 4. IMAGE 타입이 있으면: POST /upload/image → 서버 파일명 획득 후 패치
|
|
│
|
|
├─ 5. uuid4() → client_id 생성
|
|
├─ 6. WebSocket 연결 수립: ws://host:port/ws?clientId={client_id}
|
|
│
|
|
├─ 7. POST /prompt → { prompt: patched_workflow, client_id }
|
|
│ ← { prompt_id: "abc-123" }
|
|
│
|
|
├─ 8. WebSocket 수신 루프 (완료 대기)
|
|
│ progress_callback 호출 (선택)
|
|
│ execution_error 시 ComfyBridgeError raise
|
|
│
|
|
├─ 9. GET /history/{prompt_id} → 표준 노드 이미지 결과 수집
|
|
│ GET /view?filename=... → 이미지 바이너리 다운로드
|
|
│
|
|
├─ 10. GET /comfybridge/results/{prompt_id} → BridgeOutput 스칼라 결과 수집
|
|
│
|
|
└─ 11. BridgeResult 객체 생성 후 반환
|
|
│
|
|
▼
|
|
[AI Agent]
|
|
result.get("output_image") # bytes (PNG)
|
|
result.get("caption") # str
|
|
result.get("latent_code") # dict (LATENT)
|
|
```
|
|
|
|
---
|
|
|
|
## 6. 설치 및 사용법 개요
|
|
|
|
### 커스텀 노드 설치
|
|
|
|
```bash
|
|
# ComfyUI 커스텀 노드 디렉토리에 클론
|
|
cd /path/to/ComfyUI/custom_nodes
|
|
git clone https://github.com/your-org/ComfyBridgeIO
|
|
# ComfyUI 재시작
|
|
```
|
|
|
|
### Python 클라이언트 설치
|
|
|
|
```bash
|
|
pip install comfybridgeio
|
|
# 또는 로컬 설치
|
|
pip install -e /path/to/ComfyBridgeIO
|
|
```
|
|
|
|
### 워크플로우 준비
|
|
|
|
1. ComfyUI에서 워크플로우 편집
|
|
2. 외부에서 주입할 값의 위치에 **Bridge Input (STRING)** 등 노드 삽입, `name` 위젯에 식별자 입력
|
|
3. 외부로 회수할 결과에 **Bridge Output** 노드 연결, `name` 위젯에 식별자 입력
|
|
4. `Save (API Format)` 으로 JSON 내보내기
|
|
|
|
### 클라이언트 사용 예시
|
|
|
|
```python
|
|
import asyncio
|
|
from comfybridgeio import ComfyBridge
|
|
|
|
async def main():
|
|
bridge = ComfyBridge(host="localhost", port=8188)
|
|
|
|
result = await bridge.run(
|
|
workflow="workflow_api.json",
|
|
inputs={
|
|
"prompt": "a cat in space, photorealistic",
|
|
"negative_prompt": "blurry, low quality",
|
|
"steps": 20,
|
|
"cfg": 7.5,
|
|
"seed": 42,
|
|
},
|
|
progress_callback=lambda p: print(f"Progress: {p:.0%}"),
|
|
)
|
|
|
|
# 이미지 저장
|
|
with open("output.png", "wb") as f:
|
|
f.write(result.as_bytes("output_image"))
|
|
|
|
print("Done")
|
|
|
|
asyncio.run(main())
|
|
```
|
|
|
|
```python
|
|
# 동기 사용 예시
|
|
from comfybridgeio import ComfyBridgeSync
|
|
|
|
bridge = ComfyBridgeSync(host="localhost", port=8188)
|
|
result = bridge.run("workflow_api.json", inputs={"prompt": "a dog"})
|
|
image = result.as_pil("output_image")
|
|
image.show()
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 의존성
|
|
|
|
### requirements.txt (클라이언트)
|
|
|
|
```
|
|
aiohttp>=3.9.0
|
|
websockets>=12.0
|
|
Pillow>=10.0.0
|
|
```
|
|
|
|
### requirements-dev.txt
|
|
|
|
```
|
|
pytest>=8.0.0
|
|
pytest-asyncio>=0.23.0
|
|
pytest-mock>=3.12.0
|
|
aioresponses>=0.7.6
|
|
```
|
|
|
|
### pyproject.toml
|
|
|
|
```toml
|
|
[project]
|
|
name = "comfybridgeio"
|
|
version = "0.1.0"
|
|
description = "ComfyUI dynamic I/O bridge for AI agents"
|
|
requires-python = ">=3.10"
|
|
dependencies = [
|
|
"aiohttp>=3.9.0",
|
|
"websockets>=12.0",
|
|
"Pillow>=10.0.0",
|
|
]
|
|
|
|
[project.optional-dependencies]
|
|
dev = [
|
|
"pytest>=8.0.0",
|
|
"pytest-asyncio>=0.23.0",
|
|
"pytest-mock>=3.12.0",
|
|
"aioresponses>=0.7.6",
|
|
]
|
|
|
|
[build-system]
|
|
requires = ["hatchling"]
|
|
build-backend = "hatchling.build"
|
|
```
|
|
|
|
---
|
|
|
|
## 8. 미결 사항 (PM 승인 필요)
|
|
|
|
| # | 항목 | 선택지 |
|
|
|---|------|--------|
|
|
| 1 | MODEL / CLIP / VAE / CONDITIONING 타입의 BridgeOutput 직렬화 | (A) 미지원 (에러 반환) / (B) JSON-safe repr만 반환 |
|
|
| 2 | BridgeOutput 결과 서버 메모리 보관 최대 개수 | 기본 100개 LRU / 조정 가능 |
|
|
| 3 | 클라이언트 패키지 PyPI 배포 여부 | 배포 / GitHub only |
|
|
| 4 | ComfyUI Manager 연동 (`node_list.json` 포함 여부) | 포함 / 미포함 |
|