본문 바로가기
C.W.K.
Lesson 05 of 05 · published

초안은 소울 하나의 것, Refine은 피파의 턴

~13 min · react, races, scope, refine

Level 0스케치
0 XP0/43 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

작아 보이는 경계 둘

이 트랙은 작아 보이지만 작지 않은 경계 두 개로 끝나. 하나는 한 소울의 레퍼런스 설정이 다른 소울 쪽으로 새지 않게 막아. 다른 하나는 피파 Refine이 피파가 볼 수 있는 것 이상을 못 보게 막아. 크기만 다르지 같은 생각이야. 작업 하나는 정체성 하나의 것이고, 더 넓은 시야에서 빌려 온 건 아무것도 그 안에 끼어들면 안 돼.

버그 유형: 자기 것이 아니었던 초안

소울의 레퍼런스 설정은 Soul collections에서 그 소울을 골라 여는 대화상자에서 고쳐. 탄생한 밤에 만든 첫 버전은 스튜디오의 구도 트레이를 직접 고쳤어. 트레이는 생성에 쓸 이미지를 여러 소울에서 모아 두는 목록이야. 대화상자를 열면 빈 트레이에 그 소울 프로필을 채웠고, 오버라이드를 저장하면 트레이에 든 걸 그대로 썼어. 일 분쯤은 편해. 그러다 실패가 줄줄이 나와. 트레이에 피파 이미지가 든 채로 Ttori 설정을 열고 저장하면, Ttori 프로필에 피파가 들어가. 피파 에디터를 열고 피파 프로필이 다 오기 전에 Ttori로 바꾸면, 늦게 온 답이 Ttori 대화상자에 피파를 칠해. 피파 쪽에서 저장을 누르고 Ttori로 바꾸면, 끝난 피파 저장이 Ttori 에디터를 닫아 버려.

수정은 같은 밤에 들어갔고, 에디터에 자기만의 초안을 줬어. 장치는 셋, 실패마다 하나씩이야.

  • 소울마다 키. 밖으로 내보내는 컴포넌트가 에디터를 key={props.soul.soul_pk}로 그려. 소울이 바뀌면 React가 옛 에디터를 버리고 새로 올려. 앞 소울의 상태는 하나도 안 남아.
  • 불러올 때마다 취소 표시. 프로필을 가져오는 이펙트가 정리 단계에서 cancelled를 세워. 그 뒤에 도착한 응답은 버려.
  • 저장을 감싸는 활성 ref. 저장을 기다린 뒤 에디터는 onSaved나 onClose를 부르거나 상태를 바꾸기 전에 active.current부터 봐. 에디터가 닫히거나 소울이 바뀐 뒤에 끝난 저장은 아무것도 못 건드려.

에디터 안에서 라이브러리를 둘러보거나 업로드하면 이제 트레이가 아니라 초안이 바뀌고, 닫으면 저장 안 한 수정은 버려져. ReferenceProfile.test.tsx의 컴포넌트 테스트 아홉 개가 이 동작을 붙잡고 있어. 이름만 읽어도 막으려는 버그 보고서 같아. opens and saves Ttori's own profile while the studio tray contains Pippa는 트레이에 피파가 있어도 Ttori 자기 프로필을 열고 저장하는지 봐. ignores a late previous-soul response in the keyed editor는 앞 소울의 늦은 응답을 무시하는지 보고. does not close a new soul's editor when an earlier save finishes는 먼저 건 저장이 끝나도 새 소울의 에디터가 안 닫히는지 봐. 몇 분 뒤 후속 커밋은 레지스트리가 빈 경우를 다뤘어. 패키지된 WebKit 테스트가 거기서 null 비교를 잡아냈거든. 고른 소울이 없으면 설정 버튼이 꺼져.

원칙: 비동기 결과는 전부 그걸 부탁한 정체성에 묶고, 그 정체성이 사라졌으면 버려. 응답은 자기를 만든 요청에 대한 증거지, 도착한 순간 화면에 떠 있는 것에 대한 증거가 아니야.

Refine은 피파의 턴이야

Create image 페이지의 프롬프트 칸엔 피파 Refine 버튼이 있어. 누르면 프롬프트, 워크플로, 고른 피사체와 레퍼런스, 그리고 약간의 문맥(세트라면 컬렉션과 프레임)을 POST /api/refine로 보내. Avatars는 레퍼런스를 풀고, 피파의 유틸리티 완성 기능한테 피파로서 프롬프트를 다시 써 달라고 해. 한계는 시스템 프롬프트가 정해. 다듬은 영어 프롬프트만 돌려줄 것. 정체성, 피사체, 감정, 포즈, 레퍼런스 순서, 의도한 변화는 지킬 것. 사람을 지어내지도, 워크플로를 바꾸지도, 이미지를 만들지도 말 것. 플레이북은 보지 말 것.

재밌는 건 누구의 턴이냐야. WebUI는 관리자 화면이라, 로그인한 아빠는 보호된 소울까지 전부 볼 수 있어. Refine은 그 시야를 물려받지 않아. 라우트는 부탁한 소울을 "pippa"로 두고 피사체와 레퍼런스를 풀고, 나온 피사체를 피파로서 한 번 더 검사해. 주석이 이유를 말해. 피파 Refine은 피파의 턴이라서, 관리자 화면에 보인다는 이유만으로 보호된 피사체를 그 턴에 들이지 않는다. 초안에 피파가 못 보는 보호된 소울이 적혀 있거나 그런 소울에서 나온 이미지를 쓰면, Refine은 그 소울을 피파 문맥으로 끌고 가는 대신 거절해. 혼동된 대리인 문제를 미리 푼 셈이야. 도우미는 자기를 부른 화면의 권한이 아니라 딱 자기 권한만 받아.

한계가 두 개 더 있어.

  • 그림이 아니라 글. Refine이 받는 건 프롬프트와 문맥 글뿐이고, 레퍼런스 이미지는 절대 안 받아. 이름 적힌 피사체의 정체성 규칙은 Avatars가 알아서 붙이는 규칙이라는 꼬리표를 달고 문맥에 실려. 그래서 다듬은 프롬프트가 그걸 다시 적을 이유가 없어.
  • 행동이 아니라 제안. 아키텍처 문서가 정확히 적어 뒀어. Refine은 글을 제안할 뿐, 생성하지도 제출하지도 않고, 레퍼런스나 모델이나 크기나 개수를 바꾸지도 발행하지도 않는다. 답이 오면 Pippa’s refined prompt라는 대화상자가 열려. 버튼은 둘, 원래 걸 두는 Keep original과 다듬은 걸 쓰는 Use refined prompt. 다듬은 걸 고르면 직접 쓴 글은 요청의 original_prompt로 남아. 그래서 기록엔 아빠가 쓴 것과 실제로 보낸 것이 둘 다 있어.

같은 생각, 두 번

에디터 버그랑 Refine 규칙은 모양이 같아. 둘 다 더 넓은 문맥이 바로 옆에 있었어. 여러 소울이 섞인 스튜디오 트레이, 레지스트리 전체를 보는 관리자 시야. 둘 다 작업을 정체성 하나로 좁히고, 그 밖에서는 아무것도 못 들어오게 해서 고쳤어. 트랙 8은 같은 규칙을 픽셀에 적용해.

Code

cwkAvatars, frontend/src/ReferenceProfile.tsx: 늦은 답을 무시하는, 키 달린 에디터 (원문 그대로, 17-59행)·typescript
// A profile draft belongs to one soul and one editor opening. It never borrows
// the studio's composition tray, including when browsing or uploading images.
export default function ReferenceProfile(props: Props) {
  return <Editor key={props.soul.soul_pk} {...props} />;
}

function Editor({ soul, souls, onClose, onSaved, error }: Props) {
  const active = useRef(false);
  const [profile, setProfile] = useState<Profile | null>(null);
  const [references, setReferences] = useState<Asset[]>([]);
  const [appearance, setAppearance] = useState("");
  const [looks, setLooks] = useState<Record<string, LookDraft>>({});
  const [newLook, setNewLook] = useState("");
  // null: not browsing; "": browsing for the default look; otherwise a look name.
  const [browsing, setBrowsing] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  const [loadError, setLoadError] = useState("");
  useEffect(() => {
    active.current = true;
    let cancelled = false;
    api<Profile>(`/souls/${encodeURIComponent(soul.soul_pk)}/references`)
      .then((result) => {
        if (cancelled) return;
        setProfile(result);
        setReferences(result.resolved);
        setAppearance(result.appearance ?? "");
        setLooks(
          Object.fromEntries(
            Object.entries(result.variants ?? {}).map(([name, look]) => [
              name,
              { assets: look.resolved, appearance: look.appearance ?? "" },
            ]),
          ),
        );
      })
      .catch((e) => {
        if (!cancelled) setLoadError(String(e));
      });
    return () => {
      cancelled = true;
      active.current = false;
    };
  }, [soul.soul_pk]);
cwkAvatars, backend/app.py: Refine은 모든 걸 피파로서 풀고 검사해 (원문 그대로, 469-476행)·python
    @app.post("/api/refine")
    async def refine(body: Refine):
        refs, subjects, rules = await studio.resolve_references(body.subjects, body.references, "pippa")
        # Pippa Refine is a Pippa turn: protected subjects are never placed
        # in that turn merely because the admin UI can see them.
        await studio.subjects(subjects, "pippa")
        context = body.context + ("\nIdentity rules Avatars appends automatically: " + "; ".join(rules) if rules else "")
        return await upstream.refine(body.prompt, body.workflow, context)
cwkAvatars, backend/upstream.py: 피파한테 보내는 글뿐인 요청, 한계는 시스템 프롬프트에 (원문 그대로, 131-142행)·python
    async def refine(self, prompt, workflow, context):
        result = await self.json(self.settings.pippa, "/api/sibling/utility/complete", method="POST", timeout=900, body={
            "persona_soul_id": "pippa", "max_tokens": None, "source": "avatars-prompt-refine",
            "system_prompt": "Refine this image-generation prompt. Return only the improved English prompt, without commentary or Markdown fences. Preserve the user's identity, subjects, requested emotion, pose, reference order and intended changes. Do not invent extra people, change the workflow, or generate an image. This is the Avatars application workflow; do not consult any playbook.",
            "prompt": f"Workflow: {workflow}\nContext: {context}\n\nPrompt:\n{prompt}",
        })
        text = result.get("result") or result.get("text")
        if result.get("ok") is False:
            raise StudioError(result.get("error") or "Pippa could not refine the prompt.", 503)
        if not isinstance(text, str) or not text.strip():
            raise StudioError("Pippa did not return a refined prompt.", 503)
        return {"prompt": text.strip(), "original": prompt, "model": result.get("model"), "brain": result.get("brain")}

External links

Exercise

작업 중인 UI 코드에서 ID 프롭으로 데이터를 가져오는 에디터를 하나 찾아. ID A로 그리고, A의 요청이 끝나기 전에 ID B로 다시 그리고, 그러고 나서 A의 요청을 끝내서 B 화면이 그대로인지 확인하는 테스트를 써. 테스트가 실패하면 ID에 key를 달고, 이펙트 정리 단계에 취소 표시를 둬서 고쳐. 그리고 백그라운드 도우미가 호출자의 권한을 통째로 들고 일하는 곳을 하나 찾아서, 그 도우미에게 더 좁은 자기 정체성을 따로 줘.
Hint
여기 링크한 React 문서 두 쪽에 두 장치가 다 나와. 두 번째 과제엔 이렇게 물어봐. 이 도우미가 따로 있는 사람이라면 뭘 볼 수 있어야 해? Refine 라우트가 검사마다 pippa를 넘기듯, 그 정체성을 명시적으로 넘겨.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.