worker.ps1이 하는 일
worker.ps1은 이미지 생성 파이프라인의 핵심입니다. 이 스크립트 하나가 처음부터 끝까지 전부 처리합니다.
- job.json을 읽어서 프롬프트를 꺼냅니다.
- comfy_workflow.json을 읽어서 프롬프트를 교체합니다.
- ComfyUI API에 POST 요청으로 이미지 생성을 시작합니다.
- 생성이 완료될 때까지 폴링합니다 (최대 600초).
- 완료되면 이미지를 output 폴더에 저장하고 result.json을 씁니다.
전체 코드
$ErrorActionPreference = "Stop"
$BASE = "D:\016_CardNew"
$JOB_PATH = Join-Path $BASE "iojob.json"
$WF_PATH = Join-Path $BASE "iocomfy_workflow.json"
$OUT_DIR = Join-Path $BASE "output"
$RES_PATH = Join-Path $BASE "io
esult.json"
$COMFY = "http://127.0.0.1:8188"
$POS_NODE_ID = "6"
$NEG_NODE_ID = "7"
if (!(Test-Path $JOB_PATH)) {
'{"status":"wait","message":"job.json not found"}' | Set-Content -Encoding UTF8 $RES_PATH
exit 0
}
if (!(Test-Path $WF_PATH)) {
'{"status":"error","message":"workflow not found"}' | Set-Content -Encoding UTF8 $RES_PATH
exit 1
}
$job = Get-Content $JOB_PATH -Raw | ConvertFrom-Json
$wf = Get-Content $WF_PATH -Raw | ConvertFrom-Json
$wf.$POS_NODE_ID.inputs.text = [string]$job.image_prompt
if ($job.negative_prompt) {
$wf.$NEG_NODE_ID.inputs.text = [string]$job.negative_prompt
} else {
$wf.$NEG_NODE_ID.inputs.text = "blurry, low quality, watermark"
}
$body = @{ prompt = $wf } | ConvertTo-Json -Depth 100 -Compress
$q = $null
for ($try=1; $try -le 3; $try++) {
try {
$q = Invoke-RestMethod -Method Post -Uri "$COMFY/prompt" -ContentType "application/json" -Body $body
break
} catch {
if ($try -eq 3) { throw }
Start-Sleep -Seconds (2 * $try)
}
}
$promptId = [string]$q.prompt_id
Write-Host "[INFO] promptId=$promptId"
$imageMeta = $null
for ($i=0; $i -lt 300; $i++) {
Start-Sleep -Seconds 2
$h = Invoke-RestMethod -Method Get -Uri "$COMFY/history/$promptId"
if ($h.PSObject.Properties.Name -contains $promptId) {
$outputs = $h.$promptId.outputs
foreach ($p in $outputs.PSObject.Properties) {
if ($p.Value.images -and $p.Value.images.Count -gt 0) {
$imageMeta = $p.Value.images[0]
break
}
}
}
if ($imageMeta) { break }
}
if (-not $imageMeta) {
'{"status":"error","message":"timeout"}' | Set-Content -Encoding UTF8 $RES_PATH
exit 1
}
$fn = [uri]::EscapeDataString([string]$imageMeta.filename)
$sf = [uri]::EscapeDataString([string]$imageMeta.subfolder)
$tp = [uri]::EscapeDataString([string]$imageMeta.type)
$viewUrl = "$COMFY/view?filename=$fn&subfolder=$sf&type=$tp"
if (!(Test-Path $OUT_DIR)) { New-Item -ItemType Directory -Path $OUT_DIR | Out-Null }
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
$outPath = Join-Path $OUT_DIR ("result_" + $ts + ".jpg")
Invoke-WebRequest -Uri $viewUrl -OutFile $outPath -UseBasicParsing
@{
status = "ok"
prompt_id = $promptId
output_image = $outPath
} | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 $RES_PATH
Write-Host "[OK] saved: $outPath"
블록별 분석
블록 1: $ErrorActionPreference = "Stop"
$ErrorActionPreference = "Stop"
PowerShell에서 기본 에러 처리는 에러가 나도 계속 실행되는 것입니다. 예를 들어 파일을 못 찾아도 경고만 출력하고 다음 줄로 넘어갑니다. "Stop"으로 설정하면 에러가 나는 즉시 스크립트가 중단됩니다. 이미지 생성 파이프라인에서는 중간에 실패했는데 계속 진행하면 더 큰 문제가 생길 수 있습니다. 명시적으로 Stop을 설정하는 것이 안전합니다.
블록 2: 경로 변수와 노드 ID
$BASE = "D:\016_CardNew"
$JOB_PATH = Join-Path $BASE "iojob.json"
$WF_PATH = Join-Path $BASE "iocomfy_workflow.json"
$OUT_DIR = Join-Path $BASE "output"
$RES_PATH = Join-Path $BASE "io
esult.json"
$COMFY = "http://127.0.0.1:8188"
$POS_NODE_ID = "6"
$NEG_NODE_ID = "7"
경로를 변수로 묶는 이유는 나중에 폴더를 옮길 때 $BASE만 바꾸면 되기 때문입니다.
Join-Path는 OS에 맞는 경로 구분자를 자동으로 씁니다. Windows에서는 백슬래시를 씁니다. 문자열 연결보다 안전합니다.
$POS_NODE_ID = "6"과 $NEG_NODE_ID = "7"은 comfy_workflow.json에서 확인한 노드 ID입니다. 워크플로우가 바뀌어서 노드 ID가 달라지면 이 두 줄만 바꾸면 됩니다.
블록 3: 파일 존재 확인
if (!(Test-Path $JOB_PATH)) {
'{"status":"wait","message":"job.json not found"}' | Set-Content -Encoding UTF8 $RES_PATH
exit 0
}
if (!(Test-Path $WF_PATH)) {
'{"status":"error","message":"workflow not found"}' | Set-Content -Encoding UTF8 $RES_PATH
exit 1
}
job.json이 없으면 status: "wait"를 result.json에 쓰고 exit 0으로 정상 종료합니다. exit 0은 에러 없이 종료했다는 신호입니다. Linux에서 SSH로 이 스크립트를 실행했을 때 exit code가 0이면 성공으로 인식합니다. job.json이 아직 안 도착한 것은 오류 상황이 아니기 때문입니다.
comfy_workflow.json이 없으면 status: "error"와 exit 1입니다. 워크플로우 파일은 사전에 배치되어 있어야 합니다. 없다는 것은 설정 오류이므로 실제 에러입니다.
블록 4: JSON 파싱
$job = Get-Content $JOB_PATH -Raw | ConvertFrom-Json
$wf = Get-Content $WF_PATH -Raw | ConvertFrom-Json
-Raw 옵션이 중요합니다. 이것 없이 Get-Content를 쓰면 파일 내용을 줄 단위 배열로 읽습니다. -Raw를 쓰면 파일 전체를 하나의 문자열로 읽습니다. ConvertFrom-Json은 문자열을 받아야 하기 때문에 -Raw가 없으면 작동하지 않습니다.
ConvertFrom-Json이 성공하면 $job.image_prompt처럼 점으로 필드에 접근할 수 있습니다.
블록 5: 프롬프트 주입
$wf.$POS_NODE_ID.inputs.text = [string]$job.image_prompt
if ($job.negative_prompt) {
$wf.$NEG_NODE_ID.inputs.text = [string]$job.negative_prompt
} else {
$wf.$NEG_NODE_ID.inputs.text = "blurry, low quality, watermark"
}
$wf.$POS_NODE_ID에서 $POS_NODE_ID는 문자열 "6"입니다. 따라서 $wf."6".inputs.text와 동일합니다. PowerShell에서 객체 프로퍼티 이름을 변수로 참조하는 방법입니다.
[string] 캐스팅을 쓰는 이유가 있습니다. ConvertFrom-Json이 JSON 값을 PowerShell 타입으로 변환할 때, 값이 숫자처럼 보이면 Int 타입으로 해석할 수 있습니다. [string]으로 명시적으로 문자열로 변환해야 ComfyUI가 올바르게 받습니다.
negative_prompt가 없을 때 기본값 "blurry, low quality, watermark"를 씁니다. job.json에 negative_prompt 필드가 없거나 빈 문자열이면 이 기본값이 들어갑니다.
블록 6: API 호출 (3회 재시도)
$body = @{ prompt = $wf } | ConvertTo-Json -Depth 100 -Compress
$q = $null
for ($try=1; $try -le 3; $try++) {
try {
$q = Invoke-RestMethod -Method Post -Uri "$COMFY/prompt" -ContentType "application/json" -Body $body
break
} catch {
if ($try -eq 3) { throw }
Start-Sleep -Seconds (2 * $try)
}
}
$promptId = [string]$q.prompt_id
Write-Host "[INFO] promptId=$promptId"
ConvertTo-Json -Depth 100의 Depth 값이 핵심입니다. 기본값은 2입니다. 워크플로우 JSON은 여러 겹으로 중첩되어 있습니다. Depth가 작으면 깊은 곳의 값이 잘리고 "System.Object"라는 문자열로 대체됩니다. ComfyUI가 이 값을 받으면 에러가 납니다. 100처럼 충분히 크게 설정합니다.
-Compress는 JSON에서 공백과 줄바꿈을 제거해서 크기를 줄입니다. HTTP Body를 최소화하는 것이 좋습니다.
재시도 로직은 3번 시도합니다. 실패 후 대기 시간이 2 * $try초입니다. 1번째 실패 후 2초, 2번째 실패 후 4초를 기다립니다. 이를 지수 백오프(exponential backoff)라고 합니다. 서버가 잠시 바쁠 때 바로 재시도하면 더 부하가 걸립니다. 조금 기다렸다가 재시도하는 것이 좋습니다. 3번 모두 실패하면 throw로 에러를 다시 던집니다.
성공하면 응답에서 prompt_id를 꺼냅니다. 이 UUID로 다음 단계에서 생성 결과를 조회합니다.
블록 7: 생성 완료 폴링 (최대 600초)
$imageMeta = $null
for ($i=0; $i -lt 300; $i++) {
Start-Sleep -Seconds 2
$h = Invoke-RestMethod -Method Get -Uri "$COMFY/history/$promptId"
if ($h.PSObject.Properties.Name -contains $promptId) {
$outputs = $h.$promptId.outputs
foreach ($p in $outputs.PSObject.Properties) {
if ($p.Value.images -and $p.Value.images.Count -gt 0) {
$imageMeta = $p.Value.images[0]
break
}
}
}
if ($imageMeta) { break }
}
300번 반복 x 2초 = 최대 600초(10분)를 기다립니다. z_image_turbo는 steps=15이므로 보통 10~30초 안에 끝납니다.
$h.PSObject.Properties.Name -contains $promptId 구문이 낯설게 보입니다. PowerShell에서 동적 프로퍼티 이름(변수에 담긴 문자열)이 객체에 있는지 확인하는 방법입니다. 일반적인 $h.$promptId는 프로퍼티가 없으면 null을 반환하는데, 이 방식으로 먼저 존재를 확인합니다.
$outputs.PSObject.Properties로 outputs 안의 모든 노드를 순회합니다. SaveImage 노드(여기서는 노드 "9")의 출력에 images 배열이 있으면 생성이 완료된 것입니다. 첫 번째 이미지의 메타데이터(filename, subfolder, type)를 $imageMeta에 담습니다.
타임아웃이 되면 status: "error", message: "timeout"을 result.json에 쓰고 exit 1로 종료합니다.
블록 8: URI 인코딩과 이미지 다운로드
$fn = [uri]::EscapeDataString([string]$imageMeta.filename)
$sf = [uri]::EscapeDataString([string]$imageMeta.subfolder)
$tp = [uri]::EscapeDataString([string]$imageMeta.type)
$viewUrl = "$COMFY/view?filename=$fn&subfolder=$sf&type=$tp"
if (!(Test-Path $OUT_DIR)) { New-Item -ItemType Directory -Path $OUT_DIR | Out-Null }
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
$outPath = Join-Path $OUT_DIR ("result_" + $ts + ".jpg")
Invoke-WebRequest -Uri $viewUrl -OutFile $outPath -UseBasicParsing
EscapeDataString으로 파일명의 특수문자를 URL 안전 형태로 변환합니다. 이 프로젝트의 파일명에는 /가 포함됩니다(Z_Image_Turbo/27-02-2026/image_00001_.png). URL 쿼리 파라미터에 /를 그대로 넣으면 경로로 해석됩니다. %2F로 인코딩해야 합니다. EscapeDataString이 이것을 처리합니다.
Invoke-WebRequest로 /view 엔드포인트에서 이미지 바이너리를 받아서 파일로 저장합니다. -UseBasicParsing은 HTML 파싱 없이 바이너리를 그대로 받겠다는 옵션입니다. 이미지 파일을 받을 때 필요합니다.
타임스탬프 파일명(result_20260322_105758.jpg)으로 저장하므로 여러 번 실행해도 덮어씌워지지 않습니다.
블록 9: result.json 작성
@{
status = "ok"
prompt_id = $promptId
output_image = $outPath
} | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 $RES_PATH
Write-Host "[OK] saved: $outPath"
Set-Content -Encoding UTF8로 저장합니다. PowerShell에서 기본 인코딩은 시스템 설정에 따라 다릅니다. 명시적으로 UTF8을 지정하면 Linux에서 읽을 때 한글이 깨지지 않습니다.
마지막 줄 Write-Host "[OK] saved: $outPath"는 SSH로 실행했을 때 터미널에 보이는 성공 메시지입니다.
실행 방법
PowerShell 터미널에서 직접 실행합니다.
powershell -NoProfile -ExecutionPolicy Bypass -File "D:\016_CardNewscriptsworker.ps1"
-NoProfile은 PowerShell 프로필 스크립트를 로드하지 않습니다. 불필요한 시간을 줄이고 환경 오염을 막습니다. -ExecutionPolicy Bypass는 스크립트 실행 권한을 임시로 허용합니다. Windows 기본 설정에서는 외부 스크립트 실행이 막혀있기 때문입니다.
정상 실행 시 출력
[INFO] promptId=626d4d7c-6955-4577-a2b6-80d4cd80c172
[OK] saved: D:\016_CardNewoutput
esult_20260322_105758.jpg
이 두 줄이 나오면 성공입니다. result_날짜시간.jpg 파일이 output 폴더에 생겼는지 확인합니다.
worker.py — Python 대안
같은 역할을 Python으로 구현한 버전도 있습니다(D:\016_CardNewscriptsworker.py). 주요 차이점입니다.
폴링 횟수가 120회(x2초 = 최대 240초)입니다. ps1보다 짧습니다. 결과 이미지를 output/result.jpg 고정 파일명으로 저장합니다. 타임스탬프 없이 덮어씁니다. BASE 경로가 D:\015. Autothreads로 되어있습니다. 실제 사용 전에 D:\016_CardNew로 수정해야 합니다. .venv를 활성화해야 requests 패키지를 쓸 수 있습니다.
PowerShell이 없는 환경이거나, Python 코드로 파이프라인을 통합하고 싶을 때 worker.py를 씁니다. 일반적인 Windows 자동화에는 worker.ps1이 더 편리합니다.
