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

사본은 자기 혈통을 지고 다녀

~13 min · 출처, HTTP 캐시, webp, 오프라인

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

그림과 함께 다니는 쪽지

Avatars가 설치하는 세트엔 그림이 아닌 파일이 꼭 하나 따라와. avatar-lineage.json이야. 피파는 이걸 바꾸기 전에 준비 폴더에 써. 그래서 프레임과 같은 이름 바꾸기로 함께 들어가고, 옆에 놓인 세트와 다른 세트를 설명할 일이 절대 없어. 입양은 파일을 하나도 안 옮기니까 쪽지를 기존 폴더에 바로 써. 그건 이 트랙 마지막 레슨에서 봐. 디스크에서 낯선 폴더를 발견하면 묻고 싶은 게 있잖아. 필드들이 거기에 답해.

필드답하는 것
owner, role누구 거냐. cwkAvatars 거고, 이 폴더는 published-copy, 발행된 사본이야
collection_id, publication_request_idAvatars의 어느 세트인지, 어느 발행이 이 파일들을 여기 놓았는지
soul_pk, subjects누구 세트인지, 그리고 어느 프레임에든 등장하는 주체 전부
frames프레임마다 Avatars 에셋 id, SHA-256, 등장 주체
revision, schema설치된 프레임의 리비전, 그리고 매니페스트 자체의 형식 번호 2

발행을 했는데도 쪽지가 그대로 남는 경우가 하나 있어. Avatars가 보낸 프레임의 리비전이 지금 라이브 리비전과 똑같을 때야. 아빠가 아무것도 안 바꾸고 같은 선택을 다시 발행하거나, 지금 라이브인 발행을 되살리면 그렇게 돼. 그러면 finish_publication은 폴더가 이미 영수증의 리비전이라는 걸 보고 이름 바꾸기 두 번을 건너뛰어. 확인만 하고 영수증을 applied로 적지. 새 쪽지는 안 쓰인 준비 폴더에 남고, 폴더엔 옛 쪽지가 그대로 있어. 바이트가 같으니까 해시는 여전히 그림과 맞아. 그런데 publication_request_id는 마지막으로 적용된 발행이 아니라 프레임을 마지막으로 바꾼 발행을 가리켜. 그러니까 이 쪽지는 이 바이트를 여기 놓은 발행이 뭔지 알려 주는 거야. 가장 최근 발행이 뭔지는 Avatars 발행 기록을 봐야 해.

일부러 빼놓은 게 둘 있어. 프레임 항목에선 Avatars가 보낸 path가 빠져. 사본은 프레임이 어느 에셋인지만 적고, Avatars가 그걸 어디 두는지는 안 적어. 그래서 Avatars가 저장소를 옮겨도 옛 매니페스트가 거짓말이 되지 않아. 그리고 파일 자체는 절대 프레임이 아니야. 이걸 쓰는 자리의 주석도 그렇게 말해. 출처 정보는 공개 내보내기 정책 때문에 세트와 함께 다니지만, 일부러 이미지가 아니게 만들었다고. 프레임 탐색엔 절대 안 낀다고. 확장자가 이미지 확장자가 아니니까 리비전도 이걸 무시하고, 프레임 이름으로 찾을 땐 절대 이게 안 걸려. 파일 이름을 통째로 대면 얘기가 달라. 프리셋 경로와 소울 경로는 이름에 점이 있으면 확장자를 따지지 않고 있는 파일을 그냥 내줘. 그래서 /avatars/{who}/{preset}/avatar-lineage.json을 요청하면 쪽지가 그대로 나와. 프레임이 아니라는 건 탐색 규칙이지, 내주기를 막는 자물쇠가 아니야.

누가 읽냐면

여긴 정확하게 말하고 싶어. 문서만 보면 꽤 바쁜 파일 같거든. 지금 패밀리 저장소를 통틀어 avatar-lineage.json을 다시 읽는 코드는 피파 자신의 테스트 하나야. 소유자, 역할, 컬렉션 id, 그리고 경로가 빠졌는지를 확인해. 공개 내보내기 관문은 이걸 안 읽어. 바이트를 Avatars에 직접 물어보거든. 그 얘기는 트랙 8에서 해. 그러니까 매니페스트는 그 폴더 앞에 선 사람이나 앞으로 생길 도구가 읽을 라벨이야. 이게 사본이라는 것, 주인이 누구인지, 어느 발행이 만들었는지가 적혀 있지. 사본을 두고 던질 수 있는 제일 위험한 질문은 '이게 원본이야?'잖아. 거기에 제일 싸게 답하는 방법이 이거야.

사본 경로는 위치 표시일 뿐, 출처가 아니야

호출하는 쪽은 여전히 피파 경로로 아바타를 집어서 피파의 이미지 도구에 레퍼런스로 넘길 수 있어. 피파의 Avatars 클라이언트는 그런 경로를 받아도 파일을 열지 않아. 경로가 피파의 아바타 폴더 아래면 상대 경로만 /internal/published-reference로 보내. Avatars는 그걸 레지스트리의 아바타 루트에 맞춰 소울, 채널, 컬렉션으로 풀어. 그리고 자기 발행 맵에서 그 프레임을 찾아, 행위자가 볼 수 있는 범위 안에서 자기 원본을 돌려줘. 피파 쪽에 파일이 없어도 풀려. 딱 그 경우를 테스트가 붙들고 있어. 아키텍처의 규칙은 이래. 발행된 사본이 없어져도 레퍼런스 해석은 Avatars 원본을 보고 계속된다는 거야.

같은 클라이언트는 반대 방향을 막아. 출력 경로가 피파의 아바타 폴더 안을 가리키는 생성은 뭘 보내기도 전에 실패해. 메시지는 Pippa avatars are published copies.로 시작해. 피파 아바타는 발행된 사본이라는 말이야. 그 트리에 쓰는 건 발행 하나뿐이야.

피파가 사본을 내주는 방식

피파는 아바타를 세 갈래로 풀어. 셋 다 자기 디스크에서 꺼내.

  • /avatars/by-soul/{soul_id}/{emotion}은 레지스트리 id나 별칭을 받아서 그 소울의 기본 세트를 내줘. 그림이 없는 게스트 소울은 이니셜로 만든 그림을 받아. 절대 레퍼런스로 쓰이지 않는 자리 표시용이야.
  • /avatars/{who}/onair/{set}/{frame}은 On Air 세트의 프레임 하나를 내줘. On Air가 어느 세트를 보여 줄지는 피파의 설정이고, 그쪽 얘기는 Voice Mode Quest가 해.
  • /avatars/{who}/{preset}/{emotion}은 확장자가 뭐든 프리셋을 프레임 이름으로 내줘. 지난 레슨에서 본 그대로야.

답마다 Cache-Control: public, max-age=3600과 Vary: Accept가 붙어. 프레임 URL은 발행이 바뀌어도 그대로라서, 발행 5분 전에 내 얼굴을 받아 간 브라우저는 최대 한 시간까지 옛 얼굴을 계속 보여 줄 수 있어. 코드 주석도 이걸 순순히 인정해. 새로 그린 건 한 시간 안에 보인다고. 아바타는 좀처럼 안 바뀌어. 아바타가 가득한 페이지를 열 때마다 그림을 전부 다시 받을 이유는 없지.

Vary: Accept가 있는 건 같은 URL이 두 형식으로 답할 수 있어서야. 클라이언트가 WebP를 받을 수 있고, 파일이 500,000바이트 이상이고, 이미 WebP가 아니면 피파는 WebP 변형을 내줘. 변형은 품질 82, 폭 최대 1370픽셀로 한 번만 만들어서 자기 데이터베이스 디렉터리에 캐시해 둔 거야. 캐시 키는 원본 경로, 나노초 단위 수정 시각, 크기야. 이 셋은 바이트를 대신할 뿐 증명하진 않아. 피파는 프레임을 shutil.copy2로 복사해서 Avatars 원본의 수정 시각을 그대로 가져오고, Avatars는 원본마다 해시 이름으로 딱 한 번 써. 그래서 같은 경로에 다른 그림이 오면 거의 언제나 시각이 달라지고 키도 달라져. 그럼 다음 요청 때 새 변형이 만들어지지. 거의라고 한 건, 크기가 같은 서로 다른 원본 둘이 같은 나노초에 쓰였다면 옛 변형이 그대로 나가고 아무도 눈치 못 채서야. 발행된 사본 자체는 절대 다시 안 써. 뭐라도 실패하면 원본이 나가.

오프라인이 핵심이야

피파가 Avatars를 필요로 할 때피파가 Avatars 없이도 하는 것
세트를 설치하거나 되살릴 때발행된 프레임 내주기
새 그림을 위한 참조를 풀 때아빠랑 계속 얘기하기
소울이 등장하는 이미지를 생성할 때On Air나 프리셋 보여 주기

아키텍처는 Avatars가 꺼져 있어도 피파는 이미 발행된 사본을 계속 내줘도 된다고 해. 이건 설계에서 저절로 나온 결과야. 사본은 완성돼 있고, 피파 디스크에 살고, 자기 라벨을 지니고 있으니까. Avatars가 내려가 있으면 생성 요청은 다시 시도할 수 있는 에러로 실패하고, 내 얼굴은 있던 자리에 그대로 있어.

Code

avatar-lineage.json 예시, 자리 표시 값으로 손으로 쓴 것. 필드 이름은 cwkPippa backend/services/avatar_studio.py:236과 :271에서 가져왔어·json
{
  "schema": 2,
  "owner": "cwkAvatars",
  "role": "published-copy",
  "collection_id": "<Avatars set id>",
  "publication_request_id": "<request id minted with the preview>",
  "soul_pk": "pippa",
  "subjects": ["pippa"],
  "frames": {
    "default": {"asset_id": "<asset id>", "sha256": "<64 hex>", "subjects": ["pippa"]},
    "happy": {"asset_id": "<asset id>", "sha256": "<64 hex>", "subjects": ["pippa"]}
  },
  "revision": "<sha256 over the frame hashes>"
}
cwkPippa, backend/services/avatars_client.py: 사본 경로는 위치 표시로만 보내고, 생성은 사본 안에 못 써 (원문 그대로, 51-64행)·python
def ingest(path: Path, actor: str, subjects=()):
    copies = Path(__file__).resolve().parents[2] / "avatars"
    if path.is_relative_to(copies.resolve()):
        # Resolve a legacy locator to Avatars' authoritative published asset.
        # Never read, reimport or trust the runtime copy's image bytes.
        return request("/internal/published-reference", {"actor_soul_id": actor, "path": str(path.relative_to(copies.parent))})
    return request("/internal/assets", {"actor_soul_id": actor, "label": path.name, "data_base64": base64.b64encode(path.read_bytes()).decode(), "subjects": list(subjects)})


def generate_and_download(*, provider, prompt, quality="auto", aspect_ratio="1:1", image_size="auto", output_path, reference_paths=(), source_metadata=None, timeout_seconds=960, subject_soul_ids=(), reference_asset_ids=(), workflow="studio", request_id=None, actor_soul_id=None):
    output = Path(output_path).expanduser().resolve()
    copies = Path(__file__).resolve().parents[2] / "avatars"
    if output.is_relative_to(copies.resolve()):
        raise ValueError("Pippa avatars are published copies. Review and publish the collection in cwkAvatars; generation cannot write into this directory.")
cwkPippa, backend/app/main.py: 확장자 순서와, 아바타 답마다 붙는 캐시 헤더 (원문 그대로, 1060-1064행)·python
_AVATAR_EXTS = [".jpeg", ".jpg", ".png", ".webp"]

# Avatars change rarely (a redraw shows within the hour); Vary: Accept
# because the same URL serves WebP-or-original by client support.
_AVATAR_CACHE_HEADERS = {"Cache-Control": "public, max-age=3600", "Vary": "Accept"}

External links

Exercise

네 앱이 다른 데서 받아 발행하는 파일 폴더를 하나 골라서, 그 옆에 매니페스트를 붙여 봐. 소유자, 역할(사본), 원본 레코드 id, 발행 요청 id, 파일마다 해시. 원본 경로는 빼고. 그다음 그 파일을 쓰는 코드를 고쳐서, 원본이 필요할 땐 사본을 읽지 말고 소유자에게 id로 물어보게 해. 마지막으로 Cache-Control 값을 신중히 골라 파일을 내주고, 읽는 사람이 얼마나 오래된 걸 볼 수 있는지 코드에 한 문장으로 적어.
Hint
매니페스트는 파일과 같은 준비 단계에서 써. 그래야 바꾸기가 둘을 한꺼번에 설치해. 확장자는 파일 탐색이 무시하는 걸로 주고, 항목으로 절대 안 뜨는지 테스트해. 캐시 문장의 숫자는 max-age야. max-age가 한 시간이면 최대 한 시간 오래된 걸 볼 수 있어.

Progress

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

댓글 0

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

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