두 부모의 얼굴을 하나로 융합하기: Cloudflare Functions에서 정체성을 보존하는 AI 아기 생성기 구축
요약
Cloudflare Functions와 fal-ai의 PuLID 모델을 사용하여 두 부모의 얼굴 특징을 보존하며 AI 아기 이미지를 생성하는 파이프라인을 구축하는 방법을 설명합니다. 서버리스 환경의 시간 제한 문제를 해결하기 위해 클라이언트 측 폴링 루프와 단계별 호출 전략을 사용합니다.
핵심 포인트
- PuLID 모델을 활용해 텍스트 프롬프트의 한계를 넘어 정체성을 보존하는 얼굴 융합 구현
- SDXL-Lightning 모델과 업스케일러를 조합하여 생성 속도와 피부 질감 품질 최적화
- 서버리스 함수의 실행 시간 제한을 극복하기 위해 브라우저 중심의 비동기 큐 폴링 구조 채택
- 작업 실패 시 업스케일링 전 단계의 이미지를 반환하는 폴백(fallback) 로직 적용
두 부모의 얼굴을 하나로 융합하기: Cloudflare Functions에서 정체성을 보존하는 AI 아기 생성기 구축
BabyGlimpse는 각 부모의 사진을 받아 세 가지 연령 단계(신생아, 유아, 어린이)에 걸친 AI 아기 초상화를 생성합니다. 흥미로운 문제는 '이미지 모델 호출' 자체가 아니었습니다. 서버리스 함수의 시간 제한 내에서 두 개의 분리된 얼굴을 하나의 일관된 정체성으로 융합하는 것이었습니다.
모델: 일반적인 텍스트-투-이미지 호출이 아닌 PuLID
두 얼굴을 텍스트 프롬프트로 설명하는 것은 작동하지 않습니다. 텍스트는 어떤 충실도(fidelity)로도 얼굴 정체성을 담아낼 수 없기 때문입니다. 이 파이프라인은 실제 참조 이미지를 받아들이는 정체성 보존 확산 모델인 fal-ai/pulid를 사용합니다. 두 부모의 사진을 모두 reference_images로 넣고, id_mix를 통해 모델에게 두 정체성을 단순히 복사하는 대신 하나의 새로운 얼굴로 혼합하도록 지시합니다:
const submit = await fetch("https://queue.fal.run/fal-ai/pulid", {
method: "POST",
headers: { Authorization: "Key " + env.FAL_API_KEY, "Content-Type": "application/json" },
...
fal-ai/pulid는 적은 단계(4~8)에 최적화된 SDXL-Lightning에서 실행됩니다. 더 많은 단계를 거친다고 해서 Lightning 체크포인트의 성능이 향상되는 것이 아니라, 단순히 시간을 소모할 뿐입니다. id_scale: 0.8은 결과물이 두 참조 정체성과 텍스트 프롬프트에 얼마나 강하게 유사해야 하는지를 설정하며; mode: "fidelity"는 PuLID가 두 부모의 알아볼 수 있는 특징을 유지하는 방향으로 편향되도록 합니다.
마무리 단계: 두 번째 모델 호출로 'AI 플라스틱' 느낌 제거하기
8단계 Lightning 출력물은 정체성 면에서는 정확하지만 미세한 피부 질감이 부족합니다. 적은 단계에 최적화된 모델에 더 많은 단계를 밀어 넣기보다는, 이 출력물을 별도의 패스로 fal-ai/clarity-upscaler를 거치게 합니다. 이때 실제(비-Lightning) CFG를 사용하는데, 낮은 creativity와 높은 resemblance 설정을 통해 PuLID가 이미 고정시킨 정체성에서 벗어나지 않으면서 질감을 향상시킵니다.
만약 이 과정이 실패하거나 시간 초과가 발생하면, 코드는 업스케일링되지 않은 기본 이미지로 폴백(fallback)합니다. 유료 작업은 절대로 비어 돌아와서는 안 되기 때문입니다.
브라우저가 루프를 구동하는 이유 (Why the browser drives the loop, not the server)
fal.run은 비동기 큐(async queue)입니다. 작업을 제출하고 status_url/response_url 쌍을 받은 다음, status === "COMPLETED"가 될 때까지 폴링(polling)합니다. 전체 배치(세 가지 연령 단계, 각 단계마다 여러 이미지, 각각 업스케일링 과정 포함)는 몇 분이 걸릴 수 있습니다. 이는 단일 서버리스 호출(serverless invocation)이 처리하기에는 너무 긴 시간입니다.
따라서 클라이언트가 최상위 레벨의 루프를 소유합니다. 각 /api/generate 호출은 정확히 하나의 연령 단계만 처리하며, 브라우저는 이 호출을 순차적으로 세 번 수행합니다(신생아 → 유아 → 아동). 호출 사이사이에 진행률 표시줄(progress bar)이 업데이트됩니다. 각 Cloudflare Pages Function 호출은 오직 한 단계의 이미지 폴링 생존에만 집중할 수 있으며, 모든 요청을 거대한 다분(multi-minute) 요청 하나를 돌보는 대신 플랫폼의 CPU/시간 예산 내에 유지합니다.
결제: 데이터베이스 대신 서명된 권한 부여 (Payment: a signed grant instead of a database)
여기에는 데이터베이스가 없습니다. PayPal 캡처(capture)가 서버 측에서 성공하면, capture-order.js는 HMAC으로 서명된 권한 부여(grant)를 발행합니다. 이는 {o, sku, n, exp} 형태의 base64 JSON 페이로드와 Web Crypto API를 통한 SHA-256 서명을 포함합니다:
async function sign(secret, payload) {
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
...
/api/generate는 동일한 서명을 재파생하고, 일치하지 않거나 exp가 만료된 경우 요청을 거부합니다(마지막 6시간 동안 유효). 이것이 바로 측정 가능한 타사 이미지 모델을 호출하는 유료 API를 위한 전체 접근 제어 계층입니다. 세션 저장소나 주문 테이블 없이, 단지 브라우저가 요청 사이에 가지고 다니는 서명된 토큰만으로 작동합니다. 가격 책정 자체는 서버 측의 하나의 TIERS 객체에 존재하며, 클라이언트는 PayPal이 실제로 청구하는 금액에 영향을 미칠 수 없습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기