# Image Gen

> Spawn fresh `codex exec` session to call image_gen (ChatGPT OAuth — no API key). Cross-engine — Claude 가 codex 도구 빌릴 때 + Codex 안에서도 prompt-cache 분리 위해 fresh exec 권장. Triggers — 이미지 만들어줘·그림 그려줘·image_gen·generate/make/create an image. Style-transfer via `-i` ref.

- Skill: `aldegad/image-gen` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add aldegad/image-gen`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aldegad/image-gen/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: aldegad (https://skillmd.com/u/aldegad)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aldegad/image-gen

---


# /image-gen — Codex image_gen via ChatGPT OAuth

Spawn a fresh `codex exec` session in an empty sandbox dir, ask it to call `image_gen`, then **extract the generated PNG out of the codex session rollout jsonl** and write it to your destination path. Inspired by `madrobotnet/hermes-codex-imagegen-skill` — Hermes 식 instruction-based skill 패턴.

> ## 크로마 키 분기 게이트 — 소재색 먼저, 키는 그 다음 (BLOCKING)
>
> 투명 배경(크로마 키) 생성은 **프롬프트를 쓰기 전에 소재의 주요 색부터 확인**하고 키를 고른다. 소재색과 같은 계열의 키는 하드 키 삭제·경계 트림이 소재를 갉아먹는다:
>
> | 소재 주요 색 | 크로마 키 |
> |---|---|
> | 핑크·보라·자주·마젠타 계열 (꽃, 씨앗봉투, 핑크/보라 의상·크리스탈) | **그린 `#00FF00`** |
> | 녹색·청록 식물/초목/슬라임/그린 의상 | **마젠타 `#FF00FF`** |
> | 핑크와 녹색 둘 다 포함 | 더 크고 중요한 소재에서 먼 키를 고르고, 변환 후 **두 색 모두 보존됐는지 확인 필수**. sprite-gen 런이면 `--chroma-key auto`(거리 스코어)로 검증 |
>
> - [ ] 프롬프트의 배경색 지시가 위 표와 일치한다 (무지성 마젠타 디폴트 금지)
> - [ ] 변환 후 소재 주요 색이 살아있다 — 흰 꽃/탈색 = 키가 소재와 충돌한 것, 로컬 보정 말고 키를 바꿔 재생성
>
> 아래 본문의 마젠타 예시들은 "소재에 핑크/보라가 없는" 케이스의 프롬프트/fuzz 참고이고, 키 **선택**의 SSoT 는 이 표다.

> ## image_gen 출력 모델 — 이 스킬의 핵심 전제 (codex v0.144.1, verified 2026-07-10)
> codex `image_gen` 은 이미지를 세션 rollout jsonl(`~/.codex/sessions/.../rollout-*-<SID>.jsonl`)에 **인라인 base64** 로 싣는다. 레코드 이름은 codex 버전마다 다르다 — `0.140.0` = response_item `image_generation_call.result`, `0.144.1` = event_msg `image_generation_end.result`(+`status`,`saved_path`). `extract_imagegen.py` 가 두 타입을 모두 읽는다.
> `0.144.1` 은 `saved_path` 로 디스크 저장을 되살렸지만 **그 경로는 쓰지 않는다** — 인라인 디코드만 진실이다(경로 환각 표면 재개방 방지). 따라서:
> - **추출·검증은 `scripts/extract_imagegen.py`** 로 그 base64 를 결정론적으로 디코드해 `$DEST` 에 쓴다. `~/.codex/generated_images/` 는 쓰지 않는다 — v0.140.0 에선 아예 안 생겼고(항상 0 hit), v0.144.1 에선 다시 생기지만(`<sid>/call_*.png`) 그 경로를 믿는 순간 codex(LLM)에게 경로를 되묻는 환각 표면이 살아난다. 인라인 디코드만 진실.
> - **`--ephemeral` 금지** — 세션 jsonl 이 디스크에 남아야 추출 가능 (추출 후 직접 청소).
> - **증상→원인**: codex 가 "image_gen 결과 PNG의 파일시스템 절대 경로를 확인할 수 없습니다" 로 끝나면 = 추출을 안 한 것. image_gen·인증은 정상이다.
> - **무관한 노이즈**: `codex exec` stderr 의 `rmcp::transport::worker ... Auth(AuthorizationRequired)` 는 Cloudflare MCP 서버 인증 실패 로그지 image_gen 과 무관 — 무시. (없애려면 codex `config.toml` 에서 해당 MCP 인증/비활성화.)
>
> 동작 전환 내역(codex ≤0.125 디스크 저장 → 0.140.0 인라인)은 `CHANGELOG.md`.

## Why spawn even when *you* are Codex

이 스킬은 cross-engine 이다 — Claude 에서 호출하면 codex 가 없는 `image_gen` 도구를 빌리는 거고, **Codex 안에서 호출해도 fresh `codex exec` 를 또 띄우는 게 권장**이다. 이유: 메인 세션 안에서 직접 `image_gen` 을 부르면 **OpenAI 의 prompt cache 가 컨텍스트와 묶여서, 같은 프롬프트 결의 후속 호출이 이전 이미지 결과로 끌려가는 현상**이 있다 (알렉스 검증, 2026-05-11) — "이미지 변경이 잘 안 됨". 빈 sandbox 로 매번 **새 세션**(fresh `codex exec`)을 띄우면 캐시 키가 깨져서 매 호출이 깨끗한 이미지로 분리된다 (캐시 분리는 fresh 세션에서 오는 것이지 `--ephemeral` 플래그가 아니다 — 추출을 위해 `--ephemeral` 은 오히려 빼야 한다). 즉:

- **Claude → image_gen**: codex CLI 가 image_gen tool 보유자라 codex exec 소환은 필수.
- **Codex → image_gen**: 도구는 같은 세션에서도 부를 수 있지만, *캐시 분리를 위해* fresh codex exec 소환을 권장.

## Preconditions

```bash
which codex && codex --version
codex login status   # must show "Logged in using ChatGPT" or equivalent
codex features list | grep image_generation   # must be `stable true`
```

If any fail, surface the issue to the user and stop.

## 1.0 transparent PNG contract

`image_gen` native transparent/alpha output is not reliable on this setup. For game assets, stickers, sprites, UI cutouts, and any transparent image request, do **not** ask for alpha as the final truth. Use this deterministic contract:

1. Prompt image generation for an exact solid chroma background: `SOLID FLAT MAGENTA #FF00FF background, no vignette, no gradient, no texture`.
2. Convert the raw PNG with this skill's helper:

```bash
python3 $ALEX_EXTENSIONS_DIR/image-gen/scripts/chroma_key_transparent.py \
  --input "$RAW_PNG" \
  --out "$DEST_PNG" \
  --key magenta \
  --white-check /tmp/check-white.png
```

3. Verify the helper prints `mode=RGBA` and `stale_transparent_rgb_pixels=0`.
4. Inspect `/tmp/check-white.png` when the asset will sit on a bright background. Pink halo means the result is not done.

Use green only when the subject itself contains important magenta/pink colors:

```bash
python3 $ALEX_EXTENSIONS_DIR/image-gen/scripts/chroma_key_transparent.py \
  --input "$RAW_PNG" \
  --out "$DEST_PNG" \
  --key green \
  --white-check /tmp/check-white.png
```

`sprite-gen` uses the same chroma-key discipline for sheets, but its helper is sprite-demo specific. General transparent image generation owns the reusable helper here.

## Core workflow — 생성(codex) → 추출(wrapper) → 후처리(local) (v0.140.0 검증)

codex 는 이미지를 디스크에 안 떨구고 세션 jsonl 에 인라인 base64 로만 준다(위 동작변경 참조). 그래서 캐노니컬 흐름은 **3단계**다: ① `codex exec` 로 image_gen 호출(생성만) → ② 래퍼가 세션 jsonl 에서 base64 추출 → ③ (투명 필요 시) 로컬 chroma 후처리. codex(LLM)에게 경로/저장/셸을 시키지 않으므로 hallucination 표면이 0 이다.

검증된 사실 (2026-06-18): 아래 흐름 그대로 사과·레몬·딸기 PNG 생성 → 추출 → 정상 1254×1254 PNG. SID 파싱→rollout→`extract_imagegen.py` 결정론 동작.

```bash
TS=$(date +%s)
TMP=$(mktemp -d /tmp/codex-image-XXXXXX)
RAW="$TMP/raw.png"                   # codex 가 생성한 원본 (chroma 배경 포함)
DEST=/project/path/asset-name.png    # 최종 경로

# ① 생성만 (저장/셸/코드 금지). --ephemeral 없음 → 세션 jsonl 이 디스크에 남아야 추출 가능.
codex exec \
  --sandbox workspace-write \
  --skip-git-repo-check \
  --color never \
  --add-dir ~/.codex/generated_images \
  -C "$TMP" \
  - <<'PROMPT' 2>&1 | tee "$TMP/stdout.log"
image_gen 도구를 정확히 1번 호출해서 다음 프롬프트의 이미지 1장만 생성해줘.
파일 저장·셸 명령·코드 작성·경로 보고 전부 금지. 생성만 하고 끝.
프롬프트:
<USER_PROMPT — 투명 필요하면 'SOLID FLAT MAGENTA #FF00FF background, no shadow, no gradient' 명시>
PROMPT

# ② 세션 id → rollout jsonl → 인라인 base64 추출 (codex 응답 텍스트 신뢰 X, 파일만 신뢰)
SID=$(grep -oE 'session id: [0-9a-f-]+' "$TMP/stdout.log" | awk '{print $3}' | tail -1)
python3 "$ALEX_EXTENSIONS_DIR/image-gen/scripts/extract_imagegen.py" "$SID" "$RAW"
# → "OK <RAW> <bytes>" 출력 + PNG magic 검증됨. 실패 시 non-zero 로 죽음(거짓 성공 없음).

# ③-A 불투명(배경 포함) 그대로 쓰면: 바로 최종 경로로
cp "$RAW" "$DEST"

# ③-B 투명 필요하면: 로컬 chroma 후처리 (아래 transparent 섹션 규율 그대로)
python3 "$ALEX_EXTENSIONS_DIR/image-gen/scripts/chroma_key_transparent.py" \
  --input "$RAW" --out "$DEST" --key magenta --white-check /tmp/check-white.png
# stdout 에 mode=RGBA, stale_transparent_rgb_pixels=0 확인 + Read 로 /tmp/check-white.png 핑크 halo 시각 확인 (필수)

# ④ 정리 — RAW 검증 끝났으면 세션 jsonl(인라인 이미지 ~1.5MB 포함)도 청소해 ephemeral 청결성 복원
SESS=$(find ~/.codex/sessions -name "rollout-*${SID}*.jsonl" 2>/dev/null | head -1)
[ -n "$SESS" ] && rm -f "$SESS"
rm -rf "$TMP"
```

**왜 이 구조인가**: (1) base64 추출은 `extract_imagegen.py` 가 결정론적으로 하므로 codex 의 환각 경로/거짓 성공이 끼어들 자리가 없다. (2) 후처리(chroma)는 로컬에서 우리가 실행 → codex sandbox 안 magick 의존/실패 표면 제거. (3) codex 는 "생성만" 시켜서 코드 폭주·자기 SKILL.md 정독 같은 사고가 안 난다.

**언제 reference(`-i`)·batch 등 변형을 쓰나**: 아래 ## Style-transfer / ## Batch 섹션. 추출·검증 단계(②④)는 모든 변형에서 동일하다.

---

## Manual workflow (디버깅·실험용 분리 단계)

### 1. Snapshot timestamp + temp workspace

```bash
TS=$(date +%s)
TMP=$(mktemp -d /tmp/codex-image-XXXXXX)
```

### 2. Run codex exec (sandbox + writable gen dir, NO --ephemeral)

The decisive flag combo:

- `--sandbox workspace-write` — image_gen needs write access (없으면 read-only sandbox 라 image_gen tool 자체가 등록 안 됨).
- `--add-dir ~/.codex/generated_images` — `workspace-write` 디폴트 writable 셋에 안 들어가서 빠뜨리면 silent 실패. 검증된 사실 (옛 버전 핵심 단서, 지금도 그대로 넣는다).
- `--skip-git-repo-check` — 빈 sandbox 라 git repo 없음.
- **`--ephemeral` 는 절대 넣지 않는다** — 세션 jsonl 이 디스크에 남아야 거기서 인라인 base64 를 추출할 수 있다. (옛 버전엔 넣었지만 v0.140.0 추출 방식과 충돌. 추출 끝나면 ④에서 세션파일을 직접 지워 청결성 복원.)
- `-C "$TMP"` — codex 작업 디렉토리. 빈 dir → 코드 작업 폭주 안 함.
- `-i <ref-image>` (반복 가능) — style/character reference. codex 가 attach 해서 모델에 보여줌.

기본 호출 (생성만):

```bash
codex exec \
  --sandbox workspace-write \
  --skip-git-repo-check \
  --color never \
  --add-dir ~/.codex/generated_images \
  -C "$TMP" \
  - <<'PROMPT' 2>&1 | tee "$TMP/stdout.log"
image_gen 도구를 정확히 1번 호출해서 다음 프롬프트의 이미지 1장만 생성해줘.
파일 저장·셸 명령·코드 작성·경로 보고 전부 금지. 생성만 하고 끝.

프롬프트:
<USER_PROMPT_HERE>
PROMPT
```

reference 이미지가 있으면 `-i /abs/path/ref1.png -i /abs/path/ref2.png` 를 옵션 위치에 추가.

### 3. Extract session id from stdout, then decode the inline PNG

codex 의 stdout 에 한 줄로 들어옴: `session id: <uuid>`. 그 SID 의 rollout jsonl 에서 인라인 base64 를 디코드한다.

```bash
SID=$(grep -oE 'session id: [0-9a-f-]+' "$TMP/stdout.log" | awk '{print $3}' | tail -1)
python3 "$ALEX_EXTENSIONS_DIR/image-gen/scripts/extract_imagegen.py" "$SID" "$RAW"
# extract_imagegen.py: SID(또는 rollout 경로) → image_generation_call.result base64 디코드 → PNG magic 검증 → $RAW 기록.
#   image_gen 결과가 없으면 non-zero 로 죽는다 (거짓 성공 없음, No Silent Fallback).
#   한 세션에 여러 장이면 --all 로 $RAW-0.png, $RAW-1.png ... 또는 --index=N 지정.
```

**codex 의 응답 텍스트에 적힌 경로는 LLM 환각 가능. 절대 신뢰 X. `extract_imagegen.py` 가 디코드해 실제로 쓴 파일만 신뢰.** (옛 `find ig_*.png in generated_images` 방식은 v0.140.0 에서 항상 0 hit 라 폐기.)

### 4. Use the asset, then clean the session

```bash
cp "$RAW" /project/path/asset-name.png   # 불투명 그대로 쓸 때 (투명 필요하면 chroma 후처리 — transparent 섹션)

# 세션 jsonl 에 인라인 이미지(~1.5MB)가 들어있으니 추출 끝나면 청소
SESS=$(find ~/.codex/sessions -name "rollout-*${SID}*.jsonl" 2>/dev/null | head -1)
[ -n "$SESS" ] && rm -f "$SESS"
rm -rf "$TMP"
```

## Style-transfer pattern (캐릭터/화풍 일관성)

기존 자산과 화풍 맞출 때 reference 이미지 첨부:

```bash
codex exec ... \
  -i /project/assets/style-reference.png \
  -i /project/assets/character-reference.png \
  ...
  - <<'PROMPT'
첨부한 두 장의 painterly 화풍/캐릭터를 정확히 일치시켜서 image_gen 으로 X 1장 생성.
- 동일 라이팅, 동일 디테일 밀도, 동일 캔버스 비율
- 캐릭터 디자인은 두번째 ref 와 동일
PROMPT
```

같은 캐릭터의 다른 포즈 (idle / attack / win 시리즈) 가 필요하면:
1. **idle 한 장 먼저** 생성
2. **그 idle 을 ref 로** attack/win 생성 → 캐릭터 일관성 보장

> `codex exec` 호출은 위 ## Core workflow 의 플래그(`--sandbox workspace-write` / `--add-dir` / `-C` / **no `--ephemeral`**)를 그대로 쓰고, 생성 후 ②④(SID 파싱 → `extract_imagegen.py` 추출 → 세션 청소)는 동일하게 적용한다. ref(idle)로 쓸 PNG 도 추출로 디스크에 먼저 떨궈야 다음 호출에서 `-i` 로 첨부할 수 있다.

## Pitfalls

- **silent 실패 = `--sandbox workspace-write` 누락**. 디폴트 `read-only` sandbox 면 codex 가 image_gen 등록 안 함 → tool call 자체가 안 일어나고 `codex` 빈 응답만 나옴. 검증 단서: `sandbox: read-only` 가 stdout 헤더에 찍히면 무조건 워크스페이스-라이트로 다시. (검증: `--sandbox workspace-write` 명시 후 같은 prompt 로 호출 즉시 image_gen 호출됨.)
- **silent 실패 = `--add-dir` 누락**. `workspace-write` 만으로는 generated_images writable 아님. 이 시스템에서 검증된 핵심 단서.
- **image_gen 결과의 진실은 세션 rollout jsonl 의 인라인 base64 다.** 레코드 이름은 codex 버전마다 다르다 — v0.140.0 `image_generation_call.result`, v0.144.1 `image_generation_end.result`. **`extract_imagegen.py` 로 디코드해야 파일이 생긴다.** (v0.140.0 에서 "image_gen 안 됨"의 진짜 원인이 이거였음 — 인증·생성은 정상. v0.144.1 에서 레코드 rename 으로 같은 증상이 재발했다.) → 그래서 `--ephemeral` 금지 (세션파일 있어야 추출). v0.144.1 은 `<sid>/call_*.png` 로 디스크 저장을 되살렸지만 그 경로는 신뢰하지 않는다.
- **codex 응답의 거짓 경로**. 응답에 `/tmp/<sandbox>/generated_image.png` 같은 경로가 적혀도 신뢰 X — 실제 파일은 없다. `extract_imagegen.py` 가 디코드해 쓴 `$RAW` 만 신뢰.
- **`rmcp ... Auth(AuthorizationRequired)` stderr 노이즈 무시**. Cloudflare MCP 서버(`cloudflare-bindings/builds/observability`) 인증 실패 로그지 image_gen 과 무관. "인증 안 됨" 처럼 보이지만 image_gen 은 정상 동작. 없애려면 codex `config.toml` 에서 해당 MCP authenticate/비활성화.
- **bwrap loopback / sandbox introspection 에러 무시**. codex 가 sandbox 내부 검사하다 `RTM_NEWADDR` 같은 에러 출력해도 실패 아님. 추출된 파일 존재·PNG magic 만 봄.
- **모델은 `gpt-5.5` 기본**. reasoning 시간 30~90초. `--model <name>` 으로 가벼운 모델 쓸 수 있지만 image_gen tool 등록 보장 X — 검증 필요.
- **프로젝트 디렉토리에서 `-C` 없이 호출 절대 금지**. codex 가 프로젝트 컨텍스트 보고 코드 작업으로 폭주함 (실제 사고 사례 있음).
- **Read 툴은 알파 채널 PNG 를 배경색처럼 렌더한다**. magenta chroma key 후 Read 로 보면 마젠타 배경처럼 보여서 "투명 안 됐다" 착시. 진짜 검증은 `magick identify -format '%[channels]'` → `srgba 4.0` 이면 알파 있음.

## Transparent background 는 native 지원 안 됨 (codex CLI 0.124 / 0.125 둘 다 검증)

검증 사실 (재확인 0.125.0-alpha.3, 2026-04-26): prompt 에 "TRANSPARENT (alpha channel)", "RGBA", "no background fill", "no checkerboard" 모두 명시 + "거짓 보고 금지" 까지 박아도 결과는 동일하게 **RGB 3-채널 + 흰-회색 (corner 220~252) 배경 PNG**. PIL `mode='RGB', has_alpha=False`. magick `srgb 3.0`.

이건 모델 한계이며 prompt engineering 으로 해결 불가. **항상 chroma 배경 + 후처리 우회 사용**.

Read 툴이 흰 배경 PNG 를 체커보드처럼 표시해서 transparent 로 착시 발생 — 항상 `python3 -c "from PIL import Image; print(Image.open(p).mode)"` 또는 `magick identify -format '%[channels]'` 으로 진짜 알파 채널 여부 확인.

→ NPC/캐릭터/sprite 처럼 외곽이 transparent 여야 하면 **solid chroma background + `scripts/chroma_key_transparent.py` 후처리 필수**. floodfill 은 chroma 배경을 명시하지 못한 이미지에만 쓰는 폴백이다.

### Fuzz 가이드 — 캐릭터 의상 색에 따라 다름

| fuzz | 결과 |
|---|---|
| 18% (원래 시작값) | 가장자리 회색 잔여 거의 없음. **단 흰 ruff/cravat/셔츠 의상이면 connected pixel chain 으로 의상까지 점프해서 윤곽선만 남는다**. |
| 8% (안전 기본값) | 외곽 회색 (220~235) 만 빠지고 의상 흰 (240+) 보존. **첫 시도 권장값**. |
| 5% 이하 | 의상 보존 더 안전, 외곽 회색 안티앨리어싱 약간 잔여 가능. |

검증: 처리 후 `magick identify -format '%[channels]'` 로 srgba 4.0 확인 + Read 툴로 시각 확인 (의상 일부가 빠지지 않았는지).

### Chroma key (그린/마젠타 키) — floodfill 보다 우선 권장

캐릭터/NPC 처럼 머리카락 highlights, 투명 천, 흰 의상 등이 있으면 floodfill 은 위험. 대신 **prompt 에 솔리드 chroma 배경 명시 → magick `-transparent` 한 줄**이 머리카락 사이 highlights 까지 보존하면서 깔끔.

키 **선택**의 SSoT 는 최상단 분기 게이트다. 아래는 선택이 끝난 뒤의 프롬프트/fuzz 참고:

| chroma | prompt 예시 | 안전한 fuzz | 언제 |
|---|---|---|---|
| `#FF00FF` 마젠타 | "solid flat MAGENTA #FF00FF background, hair highlights warm amber/gold ONLY, never near-magenta" | 12% | 소재에 핑크/보라/자주/마젠타가 없을 때 (사람/NPC, 녹색 식물). |
| `#00FF00` 그린 | "chroma key green #00FF00 background" | 18% | 핑크/보라/자주 소재, 검/금속/가죽 sprite. 그린 톤 소재는 피해야 함. |

```bash
# 마젠타 케이스 (인물/NPC/게임 cutout 표준)
python3 $ALEX_EXTENSIONS_DIR/image-gen/scripts/chroma_key_transparent.py \
  --input "$SRC" --out "$DEST" --key magenta --white-check /tmp/check-white.png

# 그린 케이스 (마젠타/핑크가 중요한 subject)
python3 $ALEX_EXTENSIONS_DIR/image-gen/scripts/chroma_key_transparent.py \
  --input "$SRC" --out "$DEST" --key green --white-check /tmp/check-white.png
```

검증: helper stdout 의 `mode=RGBA`, `stale_transparent_rgb_pixels=0`, `alpha_zero_pct` 를 확인한다. `magick identify -format '%[channels] %wx%h\n' "$DEST"` 가 가능하면 `srgba 4.0 ...` 도 확인한다. (Read 툴은 마젠타 배경처럼 보여줘도 신뢰 X.)

**왜 floodfill 보다 우선인가**: floodfill 은 외곽에서 시작해서 connected pixel chain 으로 번지므로 머리카락 사이 갭 / 흰 의상까지 침투할 수 있음 (실사고 2건 — `CHANGELOG.md` 부록). chroma key 는 픽셀 색 매치만 보고 위치 무관 → 캐릭터 안쪽으로 안 번짐.

### ⚠ `magick -transparent` 의 가장 큰 함정 — alpha 만 깎고 RGB 는 마젠타 그대로 남김 (verified 2026-04-26)

`magick "$SRC" -fuzz 35% -transparent magenta "$DEST"` 후, transparent area (alpha=0) 픽셀의 **RGB 채널은 (255, 0, 255) 마젠타가 그대로 보존된다**. ImageMagick 은 알파만 0 으로 만들고 RGB 는 안 건드림.

PIL 검사:
```
{'alpha=255 magenta': 0, 'alpha 1..254 magenta': 0}     # 본체에 마젠타 0
transparent area still has magenta RGB (sampled 1/16): 28995   # 그러나 transparent 영역엔 마젠타 RGB 살아있음
```

대부분 viewer/브라우저는 alpha=0 이면 RGB 무시 → 문제 안 보임. **하지만 다음 환경에선 마젠타가 누설**:
- 일부 브라우저/캔버스의 alpha-premultiplication 합성 모드
- 이미지 다운스케일링 시 인접 alpha 픽셀과 RGB 가 보간 → 가장자리 마젠타 halo
- macOS 스크린샷 도구 / 일부 picker / Read 툴
- CSS `filter: drop-shadow` / `mask-image`

→ **alpha=0 픽셀의 RGB 도 반드시 (0,0,0) 으로 청소해야 함.** 실사고에서 asset 당 46만~48만 픽셀이 stale 마젠타 RGB 로 살아있었다 (`CHANGELOG.md` 부록).

```python
for y in range(H):
    for x in range(W):
        r, g, b, a = px[x, y]
        if a == 0 and (r or g or b):
            px[x, y] = (0, 0, 0, 0)   # transparent area RGB 청소
```

또는 ImageMagick 한 줄:
```bash
magick "$SRC" -channel RGBA -alpha set \
  \( +clone -channel A -threshold 1 \) -compose CopyOpacity -composite \
  -background black -alpha background \
  "$DEST"
```

**이 청소를 안 하면 chroma key + decontamination 다 해도 사용자 화면에 마젠타가 계속 비친다. 절대 빠뜨리지 마.**

### Decontamination pass — chroma key 후에도 fringe 가 보일 때 필수 후처리

검증된 사실: `magick -fuzz 35% -transparent magenta` 조차도 머리카락/모자 외곽의 anti-aliased 픽셀에 마젠타가 섞인 RGBA 를 남긴다. **검은 배경 위 합성에선 안 보이고, 흰 배경 위 합성에서만 핑크 halo 로 떠 보인다** — 그래서 밝은 배경에 올라갈 자산의 검증은 반드시 흰 배경 합성으로 한다.

원인 (3-단계):
1. chroma key (`-transparent magenta`) 는 fuzz 안에 든 색만 알파=0. 외곽 anti-alias 픽셀 (R, B 둘 다 G 보다 살짝 높은 핑크-tint) 은 fuzz 못 통과 → alpha 살짝만 깎인 채 RGB 마젠타-tint 살아남음.
2. PIL "opaque magenta 0 픽셀" 검사 통과해도, **반투명** 핑크 fringe 가 살아있다.
3. 어두운 배경에 합성하면 fringe 가 어둠에 묻혀 안 보임. 밝은 배경에 합성해야 핑크 halo 가 드러남. **검증은 반드시 흰 배경 합성으로**.

해결: chroma key 후 PIL 로 두 단계 처리 — 의상/본체 (alpha=255) 는 절대 안 건드리고, **alpha < 240 인 외곽 anti-alias 픽셀만** 핑크끼면 RGB 를 grey 화 + alpha 1/3 로.

```bash
python3 - <<'PY'
from PIL import Image
p = "/path/to/asset.png"
im = Image.open(p).convert('RGBA')
px = im.load()
W, H = im.size
killed = 0
for y in range(H):
    for x in range(W):
        r, g, b, a = px[x, y]
        # alpha=255 본체와 alpha=0 배경은 절대 안 건드림
        if a == 0 or a >= 240:
            continue
        # 핑크/마젠타-tint anti-alias: R 과 B 둘 다 G 보다 4 이상 높음
        # 와인-레드 의상 (R↑ G↓ B↓) 은 B<G 라 자동 제외
        if (r - g) > 4 and (b - g) > 4:
            # 완전 평탄화: R=G=B=G (grey), alpha 1/3 로 깎기
            px[x, y] = (g, g, g, a // 3)
            killed += 1
im.save(p)
print("fringe killed:", killed)
PY
```

검증 절차 (이걸 안 하면 사고 또 남):

```bash
# 흰 배경에 합성해서 fringe 가 진짜 사라졌는지 *눈으로* 확인
python3 - <<'PY'
from PIL import Image
im = Image.open("/path/to/asset.png").convert('RGBA')
bg = Image.new('RGBA', im.size, (255, 255, 255, 255))
bg.alpha_composite(im)
bg.convert('RGB').save("/tmp/check-white.png")
PY
# 그 다음 Read 툴로 /tmp/check-white.png 열어서 머리카락/모자 외곽 핑크 halo 검사
```

검증된 결과: asset 당 약 1000~1400 fringe 픽셀 중성화, 흰 배경 합성 후 잔여 핑크 0 픽셀.

**그린 키 (#00FF00) 의 경우**: G 가 높고 R/B 낮은 fringe → 조건을 `(g - r) > 4 and (g - b) > 4` 로 바꿔서 같은 logic 적용 (RGB 평탄화는 r 또는 b 기준).

**캐시 함정**: 후처리 다 끝나도 사용자 브라우저가 옛날 PNG 캐싱 중이면 fringe 가 그대로 보인다. md5 변경을 보여주고 DevTools → Network → "Disable cache" 또는 시크릿 창 안내. 단, **캐시 탓하기 전에 반드시 흰 배경 합성으로 자체 검증** 먼저.

### Floodfill 폴백 (chroma 배경 명시 못 했을 때만)

## Batch / Continuation modes (검증 2026-04-28)

기본은 위 single-shot codex exec 패턴. 다음 두 시나리오에서는 별도 모드를 쓴다. 캐노니컬 참조는 **아래 인라인 bash/pseudo 패턴**이다.

> 옛 `scripts/spike-*.py` Phase 0.x 탐색 파일들은 codex ≤0.125 의 출력 모델(`~/.codex/generated_images/<sid>/ig_*.png`)과 개인 로컬 fixture 경로에 묶여 있어 제거했다. 아래 인라인 패턴만 현재 캐노니컬 참조다.

### A. Batch parallel — 게임 에셋 다량 / 카드뉴스 시리즈

같은 캐릭터/스타일을 다른 pose 또는 다른 표정으로 N 장 동시 생성. 매 호출이 독립이고 character lock 은 base ref 첨부 + 동일 prompt 토대로 유지.

- 캐노니컬 패턴: 아래 인라인 bash. N개 `codex exec` 를 독립 sandbox 로 동시에 띄우되, 추출은 각 프로세스의 session id 별로 수행한다.
- **ChatGPT image_gen 동시성 한계 ≈ 4** (실측 2026-04-28). 5번째부터 quota 대기로 ~150s 추가.
- 시간 단축: 5장 sequential ~375s vs 4-동시 parallel ~230s = ~38%. 50장 단위 batch 면 sequential ~62분 vs parallel ~16분 (약 4배).
- 패턴 (각 프로세스가 자기 stdout 을 파일로 받아 → 끝나면 SID 추출):
  ```bash
  # parallel 4 (concurrency cap = 4) — --ephemeral 없음(세션 jsonl 필요)
  for i in "${!poses[@]}"; do
    tmp_per=$(mktemp -d /tmp/codex-image-XXXXXX)
    codex exec --sandbox workspace-write --skip-git-repo-check --color never \
      -C "$tmp_per" --add-dir ~/.codex/generated_images -i "$base" \
      - <<<"image_gen 으로 1장만 생성, 저장/셸 금지: ${poses[$i]}" >"$tmp_per/out.log" 2>&1 &
  done
  wait
  # 각 tmp_per/out.log 에서 SID 파싱 → extract_imagegen.py "$SID" "out-$i.png" (추출 단계는 단발과 동일)
  ```
- character lock: base ref `-i $base` 첨부 매번 + prompt 에 동일 style 명시 매번. thread context 없음.
- **추출은 프로세스별로 SID 따로** (③ 단계와 동일). 한 프로세스가 N 장 만들면 `extract_imagegen.py --all` 사용.

### B. Continuation thread — 반복 수정 / 한 캐릭터 시리즈

알렉스가 이미지 한 장 보고 "더 어둡게", "각도 달리", "표정 바꿔" 식 자연어 후속 수정. 같은 thread 안에서 turn 1 = full character spec + base ref attach, turn 2~N = short pose nudge 만. thread context 가 character/style 자동 보존.

- 캐노니컬 패턴: 아래 Python pseudo. `app-server --listen stdio://` daemon 과 JSON-RPC `thread/start`/`turn/start` 로 thread 를 유지하되, 실제 추출은 `~/.codex/sessions/.../rollout-<thread_id>.jsonl` 에서 `extract_imagegen.py` 로 수행한다.
- **검증된 동작** (Phase 0.5): turn 2/3 에서 base ref + style respec 안 박았는데도 character lock 유지 ✓
- Token 절감 ~85% (full prompt 매번 → short nudge 만)
- thread session disk 저장: `~/.codex/sessions/YYYY/MM/DD/rollout-<thread_id>.jsonl`. `thread/resume` 으로 다음 세션 이어가기 가능.
- core 패턴 (Python pseudo):
  ```python
  daemon = subprocess.Popen(['codex','app-server','--listen','stdio://'], ...)
  send("initialize", {...})
  thread_id = send("thread/start", {"approvalPolicy":"never","sandboxPolicy":{"mode":"workspace-write"},"cwd":"/tmp"})
  # turn 1 — full spec + base ref
  send("turn/start", {"threadId":thread_id, "input":[{"type":"localImage","path":base},{"type":"text","text":FULL_PROMPT}]})
  # ... wait for turn idle ...
  # turn 2~N — short nudge only, no base ref, no style respec
  send("turn/start", {"threadId":thread_id, "input":[{"type":"text","text":"Now generate same character with <pose nudge>"}]})
  ```

### C. Fork batch — 미동작 (검증 결과)

Symphony SPEC 의 `thread/fork` 패턴 + 동시 image_gen 시도했지만 **현재 codex CLI 0.125.0-alpha.3 에서 미동작** (Phase 0.6, 0.7 — 0/N PNG). image_gen tool 이 fork thread 에 inherit 안 되거나 daemon sequential 처리 한계. 알파 버전 이슈 가능성. Codex CLI 정식 release 후 재검증 가치 있음. 현재는 batch 가 필요하면 A (parallel exec) 사용.

### 모드 선택 가이드

| 시나리오 | 모드 |
|---|---|
| 단발 1장 | default single codex exec (이 문서 위) |
| N장 다량 batch (게임 에셋, 카드뉴스) | A. Batch parallel |
| 1장 + 후속 자연어 수정 (반복 수정) | B. Continuation thread |
| N장 batch + character lock 둘 다 | A 사용 (lock 은 base ref 로 유지). C (fork) 미동작 |

## When NOT to use

- Hermes / 다른 agent 의 native 이미지 백엔드가 있으면 그쪽 우선.
- 이미 cmux 에 떠있는 codex 팀메이트(다람이/쭈니)가 idle 이면 위임 가능 — 근데 그 팀메이트가 사용자 작업 흐름의 일부면 끼어들지 말 것.
- 기존 이미지 편집/리터칭 — image_gen 은 생성 전용.
- SVG/캔버스로 그릴 수 있는 단순 도형.

## Verification checklist

- [ ] `codex login status` → `Logged in using ChatGPT`
- [ ] `--add-dir ~/.codex/generated_images` 포함
- [ ] **`--ephemeral` 안 넣음** (세션 jsonl 이 남아야 추출 가능)
- [ ] `-C $TMP` 빈 sandbox dir
- [ ] session id stdout 에 출력됨
- [ ] `extract_imagegen.py "$SID" "$RAW"` 가 `OK <RAW> <bytes>` 출력 (PNG magic 통과 = 실제 파일 생성됨)
- [ ] 추출 후 세션 jsonl 청소 (`rm -f rollout-*$SID*.jsonl`)
- [ ] (선택) `Read` 로 PNG 미리보기 → 프롬프트와 매칭 확인
- [ ] transparent output 은 `scripts/chroma_key_transparent.py` 로 만든다
- [ ] helper stdout 에 `mode=RGBA` 와 `stale_transparent_rgb_pixels=0`
- [ ] 밝은 배경 사용 자산은 `/tmp/check-white.png` 로 halo 확인

