Files
cbpark84 1c919f1106 feat: batch/chain/schema/cache/preview/MCP/seed
- run_batch(): sequential|concurrent|auto
- chain(): 파이프라인 체이닝
- schema(): 스키마 자동 추출
- ResultCache: LRU+TTL 캐시
- preview_callback: 실시간 프리뷰
- upload_file/audio/video()
- BridgeInputSEED: random/increment/fixed
- MCPServer: MCP Tool 자동 생성
2026-07-16 15:39:45 +09:00

204 lines
7.8 KiB
Python

"""BridgeResult — 워크플로우 실행 결과 컨테이너."""
from __future__ import annotations
import base64
import io
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
logger = logging.getLogger(__name__)
class BridgeResult:
"""
워크플로우 실행 결과 컨테이너.
BridgeOutput 노드에 지정한 이름으로 값을 회수한다.
동기 메서드 (inline base64 있을 때):
result.get("caption") → str
result.as_pil("small_image") → PIL.Image (2MB 미만 이미지)
result.as_bytes("small_image") → bytes
비동기 메서드 (모든 크기, URL 스트리밍 폴백):
await result.as_pil_async("4k_image") → PIL.Image
await result.as_bytes_async("audio") → bytes
await result.download("video") → bytes
await result.save("video", "out.mp4") → Path
"""
def __init__(
self,
outputs: Dict[str, Any],
base_url: str = "",
timeout: float = 300.0,
):
self._outputs = outputs
self._base_url = base_url.rstrip("/")
self._timeout = timeout
# ── 기본 접근 ────────────────────────────────────────────
def get(self, name: str, default: Any = None) -> Any:
"""이름으로 결과 메타데이터를 반환한다."""
return self._outputs.get(name, default)
def keys(self) -> List[str]:
"""사용 가능한 출력 이름 목록."""
return list(self._outputs.keys())
def type_of(self, name: str) -> Optional[str]:
"""출력 타입 반환 ('image', 'audio', 'video', 'latent', 'str', ...)"""
value = self._outputs.get(name)
if value is None:
return None
if isinstance(value, dict):
return value.get("__type__", "unknown")
return type(value).__name__
def has_inline(self, name: str) -> bool:
"""inline base64 데이터가 있는지 확인 (sync as_bytes 사용 가능 여부)."""
value = self._outputs.get(name)
return isinstance(value, dict) and "data" in value
def has_url(self, name: str) -> bool:
"""스트리밍 URL이 있는지 확인."""
value = self._outputs.get(name)
return isinstance(value, dict) and "url" in value
# ── 동기 메서드 (inline base64 only) ─────────────────────
def as_pil(self, name: str):
"""
이미지를 PIL Image로 반환한다 (동기, inline base64만 지원).
4K 등 대형 이미지는 as_pil_async() 사용.
"""
from PIL import Image
data = self._get_inline_bytes(name)
return Image.open(io.BytesIO(data))
def as_bytes(self, name: str) -> bytes:
"""
결과를 bytes로 반환한다 (동기, inline base64만 지원).
대형 파일은 await result.download() 또는 as_bytes_async() 사용.
"""
return self._get_inline_bytes(name)
def _get_inline_bytes(self, name: str) -> bytes:
value = self._outputs.get(name)
if value is None:
raise KeyError(f"Output '{name}' not found. Available: {self.keys()}")
if isinstance(value, bytes):
return value
if isinstance(value, dict):
if "data" in value:
return base64.b64decode(value["data"])
if "url" in value:
t = self.type_of(name)
raise RuntimeError(
f"'{name}' ({t}) is too large for inline transfer. "
f"Use: `await result.download('{name}')` or `await result.as_bytes_async('{name}')`"
)
raise TypeError(f"'{name}' is not a binary type: {type(value)}")
# ── 비동기 메서드 (URL 스트리밍 폴백) ────────────────────
async def as_pil_async(self, name: str):
"""이미지를 PIL Image로 반환한다 (비동기, 모든 크기 지원)."""
from PIL import Image
data = await self.as_bytes_async(name)
return Image.open(io.BytesIO(data))
async def as_bytes_async(self, name: str) -> bytes:
"""결과를 bytes로 반환한다 (비동기, 모든 크기 지원)."""
value = self._outputs.get(name)
if value is None:
raise KeyError(f"Output '{name}' not found. Available: {self.keys()}")
if isinstance(value, bytes):
return value
if isinstance(value, dict):
# inline base64 우선
if "data" in value:
return base64.b64decode(value["data"])
# URL 스트리밍 폴백
if "url" in value:
return await self.download(name)
raise TypeError(f"'{name}' is not a binary type: {type(value)}")
async def download(self, name: str) -> bytes:
"""
바이너리 결과를 서버 URL에서 스트리밍 다운로드한다.
이미지(4K 포함) / 오디오 / 영상 모두 지원.
"""
value = self._outputs.get(name)
if value is None:
raise KeyError(f"Output '{name}' not found. Available: {self.keys()}")
# 이미 bytes라면 바로 반환
if isinstance(value, bytes):
return value
# inline base64 우선 (이미 캐시됨)
if isinstance(value, dict) and "data" in value:
return base64.b64decode(value["data"])
# URL 스트리밍
if isinstance(value, dict) and "url" in value:
url = f"{self._base_url}{value['url']}"
return await self._fetch_url(url)
raise ValueError(f"'{name}' has no downloadable data: {value}")
async def save(self, name: str, path: Union[str, Path]) -> Path:
"""
결과를 파일로 저장한다.
사용 예::
await result.save("result_video", "output.mp4")
await result.save("result_audio", "output.wav")
await result.save("result_image", "4k_output.png")
"""
data = await self.download(name)
out = Path(path)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(data)
logger.info(f"[ComfyBridgeIO] Saved '{name}' → {out} ({len(data):,} bytes)")
return out
async def _fetch_url(self, url: str) -> bytes:
"""URL에서 바이너리를 다운로드한다."""
import aiohttp
timeout = aiohttp.ClientTimeout(total=self._timeout)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url) as resp:
if resp.status == 404:
raise FileNotFoundError(f"Server returned 404 for: {url}")
resp.raise_for_status()
return await resp.read()
# ── 편의 메서드 ──────────────────────────────────────────
def info(self, name: str) -> Dict:
"""출력의 메타데이터를 반환한다 (크기, 포맷, URL 등)."""
value = self._outputs.get(name)
if isinstance(value, dict):
# data 필드는 크므로 제외
return {k: v for k, v in value.items() if k != "data"}
return {"value": value, "__type__": type(value).__name__}
def summary(self) -> Dict:
"""모든 출력의 요약 정보를 반환한다."""
return {name: self.info(name) for name in self.keys()}
def __repr__(self) -> str:
parts = []
for name in self.keys():
t = self.type_of(name)
inline = " [inline]" if self.has_inline(name) else ""
url = " [url]" if self.has_url(name) else ""
parts.append(f" {name}: {t}{inline}{url}")
return "BridgeResult(\n" + "\n".join(parts) + "\n)"