아빠가 스튜디오에서 Review generation을 눌러도 이미지는 한 장도 안 만들어져. 스튜디오는 plan() 함수 하나를 부르고, 돌아온 걸 보여줄 뿐이야. 이어서 Generate images를 누르면 스튜디오는 똑같은 요청 본문을 같은 요청 id 그대로 다시 보내. 엔진은 뭔가 저장하기 전에 plan()을 한 번 더 돌려. 미리보기와 제출은 같은 코드 길을 타는 거야. POST /api/generation/preview는 계획에서 Ember 카탈로그를 빼고, 첫 결과물에 들어갈 프롬프트와 파일을 붙여서 돌려줘. POST /api/generation은 generate() 안에서 plan()을 돌리고 배치를 담아 202로 답해. 피파 쪽 관문인 /internal/generation도 같은 함수를 지나.
계획은 돈을 쓰기 전에 요청 전체를 글로 펼쳐 놓은 거야. 작업 하나가 이미지 한 장이고, 프레임 하나, 후보 번호 하나, 생성기 하나로 정해져. 작업마다 확정된 공급자와 출력 크기, Ember가 그 줄에 적어둔 과금 종류, 조립된 프롬프트가 들어가. 레퍼런스 이미지 전부의 id와 SHA-256, 그려질 피사체, 요청한 소울도 같이 담겨. 자동 생성기를 골랐다면 Ember의 경로 결정까지 붙어. 워커가 나중에 알아서 판단할 몫은 하나도 안 남겨.
배치가 생기기 전에 이유를 대고 거절해
plan()의 대부분은 거절이야. 거절마다 사람이 읽고 바로 고칠 수 있는 문장을 담은 StudioError가 나오고, 앱은 그걸 422로 돌려줘. 공급자 연결이 막혀 있으면 Ember가 준 메시지 그대로 503이 와. 코드에서 걸리는 순서대로 대강 훑어보면 이래.
이미지 요청에 붙은 클립 설정. 클립은 따로 계획하고, 그건 Clips Are Paid 트랙에서 다뤄.
Ember 어휘에 없는 화면비.
같은 생성기를 두 번 고른 요청. 반복은 후보 개수로 하라고 알려줘.
세트가 필요한 작업(create, regenerate, remix)인데 세트가 없는 경우, 그리고 프레임 이름이 겹치거나 규칙에 안 맞거나 세트에 없는 경우.
이미지 레퍼런스 자리에 들어온 클립.
Ember 목록에서 사라진 생성기, 막힌 연결, 그 줄이 안 주는 크기나 품질.
고른 생성기의 자체 목록에 없는 화면비.
원본이 연결 안 된 리믹스 프레임, 그리고 모델이 받겠다고 밝힌 것보다 많은 레퍼런스.
요청한 소울이 그릴 수 없는 피사체. 이건 피파한테 물어보고 판단해.
배치 상한을 넘는 작업 수.
이 검사는 전부 행 하나 쓰기 전에 끝나. 그래서 거절된 요청은 배치도, 작업도, 잡도 안 남겨. 화면비 테스트가 거절 직후에 바로 그걸 확인해. 배치 목록과 작업 목록이 둘 다 비어 있어야 통과야.
오타는 검토에서 멈춰야 해
요청 계약은 생성기 하나당 후보 수를 1에서 50 사이로 묶고, 스튜디오 숫자 칸도 같은 범위로 잘라. 근데 그걸로 배치 크기까지 묶이진 않아. 후보는 곱해지거든. 아키텍처 문서에 나오는 예가 딱 그래. 15프레임 컬렉션 두 개, 생성기 두 개, 후보 다섯이면 이미지 300장이야. 근데 요청 하나는 세트를 하나만 가리킬 수 있어. 그래서 지금 계획기로는 컬렉션마다 150장짜리 배치 두 개가 돼. 그중 하나에서 5 대신 50을 치면 1,500장이 계획돼. 그래서 곱한 결과에 두 번째 상한이 걸려 있어. max_batch_tasks, 기본값 600이고 AVATARS_MAX_BATCH_TASKS에서 읽어. 옆 주석이 이유를 한 줄로 말해. 후보 수 오타는 검토 단계에서 멈춰야지, 공급자 청구서에서 멈추면 안 된다는 거야.
이 검사는 계획을 짜는 반복문 안에 있어. 그래서 601번째 작업이 나오는 순간 멈추고, 나머지는 아예 안 만들어. 거절 문장은 뭘 바꾸면 되는지도 알려줘. 후보나 프레임이나 생성기를 줄이든지, 배치를 여러 개로 나누라고. 테스트는 이미지 20장짜리 리믹스 요청에 상한을 10으로 낮춰 걸고, 10장을 넘는다는 거절이 나오는지 봐. 후보 수 500은 계획까지 가지도 못한다는 것도 보여줘. 요청을 해석하는 단계에서 계약이 먼저 거절하거든.
피사체 조합마다 한 번만 확인해
어떤 검사는 다른 서비스한테 물어야 해. 요청한 소울이 어떤 피사체를 그려도 되는지는 피파의 정책이라서, 계획은 피파한테 물어. 다만 피사체 조합 하나에 한 번만 묻고, 그 답을 계획 끝까지 들고 가. 같은 테스트가 호출 수도 세는데, 작업 20개짜리 계획에서 피파한테 피사체를 묻는 건 많아야 여섯 번이야. 작업마다 네트워크를 한 번씩 오가는 미리보기라면 너무 느려서 아무도 안 쓸 거야. 기다리기 싫은 검사는 결국 다들 건너뛰게 되거든.
계획에서 의도로, 트랜잭션 하나로
generate()는 먼저 스튜디오의 의도 잠금을 잡고 이 요청 id를 본 적이 있는지 확인해. 그리고 계획을 다시 짜고, 배치와 작업 전부를 요청 id와 함께 SQLite 트랜잭션 하나로 써. 트리 전체가 있거나, 아무것도 없거나 둘 중 하나야. 테스트가 삽입 도중에 일부러 실패를 일으키면 배치도 작업도 요청 id도 하나도 안 남아. 같은 요청 id에 같은 입력이 오면 이미 만든 배치를 그대로 돌려줘. 그래서 Generate images를 두 번 눌러도 배치는 하나야. 같은 id에 다른 입력이 오면 충돌로 거절해. 이 저장 구조는 트랙 2에서 다뤄.
여기서 중요한 건 계획이 고스란히 의도가 된다는 점이야. 작업은 전부 queued 상태에 빈 시도 목록을 달고 저장돼. 워커는 계획이 얼려둔 것만 읽어. 레퍼런스 이미지 해시도 마찬가지라서, Ember에 한 바이트라도 보내기 전에 그 해시를 다시 확인해.
원칙: 배치가 생기기 전에 거절하면 문장 하나로 끝나. 같은 거절이 제출 뒤에 나오면 실패한 잡이 하나 남고, 때로는 청구서까지 날아와.
Code
cwkAvatars, backend/studio.py: plan()의 첫 거절들 (원문 그대로, 373-394행)·python
async def plan(self, body: Generation, actor=None):
if body.workflow == "clip":
return await self.plan_clip(body, actor)
if body.clip is not None:
raise StudioError("Clip settings belong to the clip workflow.")
catalog = await self.upstream.catalog()
providers = {p["id"]: p for p in catalog["providers"]}
ratios = aspect_ids(catalog.get("aspect_ratios"))
if ratios and body.aspect_ratio not in ratios:
raise StudioError("This aspect ratio is not available in Ember.")
if len({p.id for p in body.providers}) != len(body.providers):
raise StudioError("Choose each provider once; use the candidate count for repetitions.")
collection = await self.io("get", body.set_id, "set") if body.set_id else None
if collection:
await self.subjects([collection["soul_pk"]], actor)
if body.workflow in {"create", "regenerate", "remix"} and not collection:
raise StudioError("Choose an avatar set for this workflow.")
frames = body.frames or (collection["frames"] if collection else ["image"])
if len(set(frames)) != len(frames) or any(not FRAME.fullmatch(f) for f in frames):
raise StudioError("Frame names must be unique and valid.")
if collection and not set(frames) <= set(collection["frames"]):
raise StudioError("A requested frame does not belong to this set.")
cwkAvatars, backend/studio.py: generate()가 계획을 의도 하나로 바꾸는 부분 (원문 그대로, 548-562행)·python
async def generate(self, body, actor=None):
payload = {**body.model_dump(), "actor_soul_id": actor}
async with self.intent_lock:
existing = await self.io("request", body.request_id, payload)
if existing:
return existing
plan = await self.plan(body, actor)
spending = plan.get("spending")
if spending:
# Nothing is queued without the consent Ember's policy asks for.
if spending["blocked"]:
raise StudioError(spending["blocked"], 409)
if spending["requires_confirmation"] and not spending["confirmed"]:
raise StudioError("This clip needs confirmation before it is paid for. " + " ".join(spending["reasons"]), 409)
return await self.io("create_intent", body.request_id, payload, "batch", {"title": body.title, "request": payload, "source_version": plan["source_version"], "frames": plan["frames"], "image_count": plan["image_count"], "set_count": plan["set_count"], "catalog": plan["catalog"], "archived": False}, parent=body.set_id or "", children=[("task", {**item, "state": "queued", "phase": "Queued", "attempts": [], "job_id": None, "asset_id": None, "error": None}) for item in plan["tasks"]])
cwkAvatars, backend/settings.py: 배치 상한과 그 이유 (원문 그대로, 35-37행)·python
# One request plans at most this many metered images; a typo in the
# candidate count must stop at review, never at the provider's invoice.
max_batch_tasks: int = field(default_factory=lambda: max(1, int(os.getenv("AVATARS_MAX_BATCH_TASKS", "600"))))
만들고 있는 소프트웨어에서 네트워크 건너편에 돈이나 자원을 쓰는 동작 하나를 골라봐. 유료 API 호출이든, 메일 한 묶음이든, 클라우드 잡 여러 개든 좋아. 그 동작에서 plan()을 떼어내. plan()은 시작할 단위 전부를 각 단위가 쓸 입력과 함께 목록으로 돌려주고, 잘못된 요청은 문제를 짚는 문장으로 거절해야 해. 미리보기와 제출이 둘 다 plan()을 부르게 만들어. 단위 수엔 상한을 두되 설정에서 읽고, 왜 있는지 주석으로 남겨. 끝으로 일부러 숫자를 잘못 친 요청을 제출해서, 저장된 것도 나간 것도 없는지 확인해.
Hint
거절부터 쓰고 정상 경로는 나중에 써. 상한 검사는 단위를 만드는 반복문 안에 둬. 그래야 거대한 요청이 개수를 세느라 만 개를 다 만들기 전에 일찍 멈춰. 다른 서비스에 물어야 하는 검사라면 서로 다른 입력마다 한 번만 묻고, 답은 계획 끝까지 들고 가.
Progress
Progress is local-only — sign in to sync across devices.