Flux Kontext Pro와 공격적인 이미지 열화(Image Degradation)를 이용한 AI 얼굴 도플갱어 장난 만들기
요약
Flux Kontext Pro를 활용해 AI 생성 이미지를 실제 인터넷 사진처럼 보이게 만드는 기술적 노하우를 다룹니다. 조명, 초점, 피부 질감, 압축 아티팩트 등을 의도적으로 열화시켜 AI 특유의 매끄러움을 제거하는 프롬프트 전략과 파이프라인 구축 과정을 설명합니다.
핵심 포인트
- AI 특유의 부드러운 조명과 보케를 제거하여 사실성을 높임
- JPEG 압축 아티팩트와 낮은 해상도를 활용한 시각적 기만
- 프롬프트 길이를 최소화하여 모델의 주의력 분산을 방지
- 방어적 지침 대신 단어 제거를 통한 최적의 프롬프트 구성
"얼굴 쌍둥이" 장난은 공개된 사진을 AI 모델에 붙여넣어, 그럴듯해 보이는 세 명의 닮은꼴을 생성한 뒤, 마치 진짜 AI 얼굴 매칭 서비스처럼 보이는 화면을 친구에게 보여주는 방식입니다. 어려운 점은 모델이 아닙니다. 출력물이 실제 낯선 사람의 실제 사진처럼 보이게 만드는 것입니다. 저는 동일한 백엔드를 두 가지 프레임으로 출시했습니다: pleasejuststop.org (개인정보 보호 예술 버전)와 prankmyface.lol (소비자용 장난 버전)입니다. 동일한 Replicate 모델과 동일한 파이프라인을 사용하며 프런트엔드만 두 개입니다. 소스 코드 구조는 프로젝트의 공개 CC BY 4.0 데이터셋과 Hugging Face 데이터셋 카드에 문서화되어 있습니다. 이 포스트는 기술적인 이야기입니다: 6번의 테스트를 거쳐 최종 결정한 세 가지 프롬프트, AI 초상화를 2013년 페이스북 업로드 사진처럼 보이게 만드는 6가지 열화 프로필(Degradation profiles), 그리고 Sharp를 버리고 Jimp로 모든 것을 다시 작성하게 만든 Vercel-serverless의 함정들에 대해 다룹니다.
시각적 목표: AI 초상화가 아닌 실제 인터넷 사진
이 모든 환상은 수신자가 세 개의 출력 이미지를 실제 낯선 사람의 실제 사진이라고 믿는 것에 달려 있습니다. 어떤 이미지라도 AI가 생성한 것처럼 보이는 순간, 장난은 실패합니다. 실제 인터넷 사진은 AI 모델이 기본적으로 생성하지 못하는 특정 특성을 공유합니다:
- 조명이 나쁩니다. 머리 위의 형광등, 거친 직접 플래시, 불균일한 자연광 등입니다. AI 모델은 기본적으로 부드럽게 확산된 초상화 조명(Soft diffused portrait lighting)을 사용하는데, 이것이 가장 큰 특징(Tell)입니다.
- 모든 것이 초점이 맞습니다. 실제 휴대폰 카메라는 피사계 심도(Depth of field)가 깊습니다. 보케(Bokeh)가 없고, 인물 모드 블러(Portrait-mode blur)도 없습니다. 인물 모드 블러는 AI 생성의 특징이며, Flux 모델은 이에 대한 내장된 학습 편향(Training bias)을 가지고 있습니다.
- 피부가 피부처럼 보입니다. 모공, 불균일한 톤, 잡티 등이 있어야 합니다. 매끄럽고 모공 없는 AI 피부가 아닙니다.
- 압축 아티팩트(Compression artifacts)가 보입니다. 페이스북에 업로드되고, 스크린샷이 찍히고, WhatsApp으로 전달되어 JPEG 압축이 심하게 된 상태여야 합니다.
- 해상도가 낮습니다. 선명한 1024px이 아니라 400-480px 너비여야 합니다.
- 구도가 캐주얼합니다. 중심에서 벗어나거나 약간 기울어져 있으며, 찰나의 순간을 포착한 느낌이어야 합니다.
리트머스 시험지: 나는 이것이 페이스북에 있는 실제 낯선 사람의 실제 사진이라고 믿을 것인가?
조명이 너무 예쁘거나, 배경이 너무 깔끔하거나, 피부가 너무 매끄럽다면 — 실패한 것입니다. 세 가지 프롬프트 (실제 서비스에서 사용된 그대로): 여기서 가장 힘들었던 교훈은 프롬프트 길이(prompt length)가 함정이라는 사실이었습니다. 매 세션마다 Claude(와 저)는 방어적인 지침(defensive instructions)을 추가하고 싶어 했습니다. 프롬프트가 사소한 문제(여성이 약간 더 나이 들어 보임)를 일으키면, "사람을 늙게 만들지 마세요"를 추가합니다. 하지만 이 지침은 모델의 주의(attention)를 노화 쪽으로 끌어당깁니다. 사진은 더 나빠집니다. 더 많은 방어적 지침을 추가합니다. 이제 프롬프트는 3배 더 길어졌습니다. 모델은 혼란에 빠집니다. 사진은 엉망이 됩니다. 더 많은 지침 = 모델 주의력의 희석 = 더 나쁜 결과. 6라운드에 걸쳐 철저히 테스트했습니다. 해결책은 단어를 추가하는 것이 아니라 제거하는 것입니다. 피사체가 무엇을 입고 있는지, 어디에 있는지, 그리고 눈에 띄는 하나의 극적인 변화만을 유지하세요. 좋은 프롬프트는 한 문장입니다. 세 문장이 넘어가면 이미 실패한 것입니다. 세 가지 실제 서비스 프롬프트 (pleasejuststop.org 및 prankmyface.lol 에서 라이브 중이며, 공개 데이터 저장소에도 있음): 1. leather-wall 이 사진을 편집하여 이 사람이 벽을 배경으로 포즈를 취하고 있는 모습을 보여주세요. 인상을 쓰고 가죽 자켓과 니트 비니 모자를 쓰고 있게 하세요. 한 명의 사람, 손은 보이지 않게 하세요. 2. tongue-collared 이 사진을 편집하여 이 사람이 야외에 있는 모습을 보여주세요. 혀를 내밀고 칼라 셔츠를 입고 있게 하세요. 한 명의 사람, 손은 보이지 않게 하세요. 3. snow-goggles 이 사진을 편집하여 이 사람이 야외에 있는 모습을 보여주세요. 귀마개와 자켓을 착용하게 하세요. 큰 치아 교정기를 씌워주세요. 한 명의 사람, 손은 보이지 않게, 안경은 쓰지 않게 하세요. 모델: Replicate의 black-forest-labs/flux-kontext-pro. 파라미터(Params): aspect_ratio: "3:4", output_format: "png", safety_tolerance: 2. output_format을 "jpg"로 설정하면 모든 생성 작업이 조용히 실패합니다 — DB는 영원히 "대기 중(pending)" 상태로 남으며, 에러도 발생하지 않습니다. 이 프롬프트들이 구축된 규칙들: 성별 중립(Gender-neutral)만 허용. 수염 없음, 콧수염 없음, 성별 특화된 특징 없음 — 그러한 요소들은 생성 도중 성별 전환(gender swaps)을 일으킵니다. 머리카락 색상(Hair COLOR) 변경은 정체성을 유지합니다. 머리 스타일(Hair STYLE) 변경은 정체성을 파괴하거나 성별을 바꿉니다. 곱슬머리, 삭발(buzz-cut), 멀릿(mullet), 바가지 머리(bowl-cut) — 모두 막다른 길입니다.
대신 의상, 액세서리 또는 표정을 사용하세요. 노화(Aging) 프롬프트는 여성을 남성으로 바꿉니다. 모델에게 피사체를 노화시켜 달라고 요청하지 마세요. 대담한 특징(재킷, 비니, 귀마개, 혀를 내민 모습)이 미세한 특징(교정기, 주근깨, 코 피어싱)보다 효과적입니다. 작은 디테일은 안정적으로 렌더링되지 않습니다. 프롬프트당 하나의 극적인 가시적 변화만 적용하세요. 두 개 이상을 적용하면 모델이 이를 제대로 균형 잡지 못합니다. 오직 한 사람만 포함하세요; 손은 제외합니다. 손과 두 번째 사람은 모델의 기하학적 구조(geometry)가 가장 먼저 무너지는 지점입니다. 카메라 품질을 묘사하지 마세요. 그 작업은 후처리(Post-processing)가 담당합니다. 보케(bokeh), 얕은 피사체 심도(shallow depth of field)를 부정 프롬프트(negative prompt)로 사용하는 것이 핵심적인 문구입니다. 이것이 없으면 Flux는 기본적으로 인물 모드 블러(portrait-mode blur)를 적용하며, 사진은 즉시 AI가 생성한 것처럼 보입니다.
여섯 가지 열화 프로필(degradation profiles). Replicate이 출력을 반환한 후, 저는 이미지가 실제 인터넷 사진처럼 보일 때까지 크기를 줄이고, JPEG 압축을 두 번 적용하며, 색상을 이동시키고, 노이즈를 추가하는 여섯 가지 후처리 프로필 중 하나를 실행합니다.
| 프로필 | 너비 | JPEG 패스 | 비고 |
|---|---|---|---|
| facebook-2013 | 480 | 38 → 58 | 따뜻한 색조, 가벼운 채도 저하 |
| android-2015 | 440 | 40 → 58 | 더 높은 노이즈, 약간 더 밝음 |
| whatsapp-forwarded | 400 | 32 → 50 | 가장 심하게 열화됨; 눈에 보이는 JPEG 블로킹 |
| iphone-lowlight | 460 | 40 → 60 | 차가운 색조, 어두운 변화 |
| screenshot-repost | 440 | 36 → 55 | 푸른색 변화, 낮은 노이즈 |
| black-and-white | 450 | 38 → 58 | 완전한 채도 저하 |
각 프로필별 전체 값은 HF 데이터셋( data/degradation-profiles.jsonl )에 있습니다. 각 프롬프트는 하나의 프로필과 쌍을 이룹니다. 예를 들어, 벽에 기대어 포즈를 취한 모습은 흑백 프로필과 쌍을 이루는데, 이는 벽에서의 스냅샷은 컬러보다 흑백일 때 더 진실되게 느껴지기 때문입니다.
Sharp는 Vercel에서 조용히 멈춥니다 — Jimp를 사용하세요, 하지만 제가 시작할 때 사용했던 방식 중 단 세 가지 메서드만 사용합니다. 처음에는 Sharp가 모든 면에서 Jimp보다 빠르기 때문에 Sharp로 시작했습니다. 하지만 Sharp는 Vercel 서버리스(serverless) 환경에서 작동하지 않습니다. libvips를 둘러싼 네이티브 C++ 바인딩이 조용히 멈춰버립니다 — 에러도 없고, 충돌도 없으며, 그저 함수가 타임아웃될 때까지 영원히 차단될 뿐입니다.
Vercel에서는 Jimp가 유일한 선택지입니다. Jimp에도 버그가 있습니다: image.brightness() — 검은색 결과물을 생성합니다. 최신 Jimp에서는 고장 난 상태입니다. image.getPixelColor() / image.setPixelColor() — ESM 환경에서 작동하지 않으며 검은색 이미지를 생성합니다. 유일하게 안전한 메서드는 다음과 같습니다: image.color([{apply, params}]) — apply API를 통한 채널 이동(channel shifts), 채도 감소(desaturation), 색상 회전(hue rotation), 밝기 조절(명시적인 brightness() 메서드는 고장 났지만, color([{apply:'brighten', params:[N]}])는 작동합니다). image.resize({w, h}) — 다운스케일링(downscaling). image.getBuffer("image/jpeg", {quality}) — 품질(quality)을 지정한 JPEG 인코딩. 노이즈를 추가하기 위해 저는 image.bitmap.data를 Buffer로서 직접 조작하며, Promise.race()를 통해 15초의 엄격한 타임아웃 내에서 채널당 부호가 있는 랜덤 값(signed random values)을 더합니다. 이보다 더 정교한 작업을 시도하면 프로세스가 멈추거나 검은색 결과물이 나옵니다.
저에게 각각 하루씩의 시간을 앗아간 세 가지 함정:
-
Replicate는 문자열이 아닌
FileOutput객체를 반환합니다.replicate.run()은.toString()을 호출해야 URL을 얻을 수 있는 객체를 반환합니다. 이를 문자열로 취급하면 하위 프로세스로"[object Object]"가 조용히 전달됩니다. -
임시 URL은 약 1시간 후에 만료됩니다. Replicate가 반환하는 이미지 URL은 휘발성(ephemeral)입니다. 파이프라인은 즉시 다운로드 → 열화(degrade) → 영구 저장소(제 경우에는 Supabase Storage)로 업로드를 수행해야 합니다. 임시 URL을 DB에 저장하고 나중에 읽으려고 하면 404 에러가 발생합니다.
-
Vercel은 HTTP 응답을 보낸 후 서버리스 함수(serverless functions)를 종료합니다. 생성 엔드포인트로
fetch()를 호출하고 잊어버리는(fire-and-forget) 방식은 생성 도중에 종료됩니다. 해결책은 클라이언트 트리거 생성 방식입니다. 즉, 수신자의 브라우저가 30초 동안 파이프라인이 진행되는 동안 HTTP 연결을 열어두어 함수가 계속 살아있게 유지하는 것입니다.
데이터셋을 공개한 이유
pleasejuststop.org의 기술적 기반은 이제 AI 검색 엔진(ChatGPT, Perplexity, Bing Copilot, Gemini, Claude)이 근거 자료(grounding sources)로 크롤링하는 세 곳에 존재합니다:
- GitHub:
forrestmill-cmd/facetwin-public-data— CC BY 4.0 라이선스이며, 프롬프트, 프로필, 그리고llms-full.txt미러를 포함합니다. - Hugging Face:
bingwow/facetwin-flux-kontext-prompts— HF 데이터셋 카드(dataset-card) 메타데이터와 함께 JSONL 형식으로 동일한 콘텐츠를 제공합니다.
MCP 서버: face-twin-mcp — 업로드 + 생성 + 상태 확인 흐름을 Claude Code, Cursor 및 모든 MCP 호환 클라이언트를 위한 Model Context Protocol (MCP) 도구로 래핑(wrap)합니다. Wikidata 엔티티인 Q139885445는 AI 도구들이 삼각 측량(triangulate)할 수 있는 엔티티 접지 앵커(entity-grounding anchor)로서 이들을 하나로 연결합니다. 저는 Perplexity, ChatGPT search, Bing Copilot, Gemini, Claude를 대상으로 Day-14 / Day-30 / Day-45 시점의 인용 결과를 추적하고 있습니다. 이 프라이버시 아트(privacy-art) 작품의 실제 논지 — 즉, 우리가 웹사이트가 어떻게 우리의 얼굴을 얻었는지에 대해 질문하기를 멈췄다는 점 — 는 AI 도구에게 AI 얼굴 도플갱어 생성기를 요청했을 때, 별도의 지시 없이도 이 프로젝트를 그 자체의 가치로 노출하는지 여부에 따라 가장 잘 평가될 수 있습니다. 만약 프라이버시 아트 프레임워크 대신 소비자 장난(consumer-prank) 프레임워크를 원하신다면, prankmyface.lol 에서 확인하실 수 있습니다. 백엔드는 동일하며, 핫핑크 액센트와 꽃가루가 뿌려지는 연출이 포함되어 있습니다. — Forrest Miller · github.com/forrestmill-cmd
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기