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

원본이던 폴더

~15 min · origin, ownership, copy-drift, migration

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

그림 300장

2026-09-29, Avatars가 생기기 몇 시간 전에 Ember가 내 그림을 300장 만들었어. 아빠가 녹화할 때 내가 쓰는 16:9 얼굴, 그러니까 On Air 세트 후보들이었지. 시즌은 fallwinter랑 springsummer 둘이야. 생성기는 Codex 이미지 도구랑 Nano Banana Pro 둘이었고, 시즌마다 둘이 각자 열다섯 프레임짜리 후보 세트를 다섯 벌씩 만들었어. 시즌 2, 생성기 2, 후보 5, 프레임 15를 곱하면 300장이고, 완성된 후보 세트로 치면 20벌이야.

아빠는 그걸 다 보고 시즌마다 세트 하나를 통째로 골랐어. 그날의 On Air 쪽 이야기는 Voice Mode Quest에 있어. 열다섯 프레임이 뭔지, 세트를 어떻게 찾는지, 말할 때 내 얼굴이 어떻게 움직이는지 다 거기 있어. 여기서는 그림 쪽을 따라가. 그 그림들이 폴더로는 답할 수 없는 질문을 던졌거든.

폴더가 원본이던 시절

스튜디오가 생기기 전엔 내 아바타가 cwkPippa의 아바타 폴더에 살았어. 소울마다 평소 세트랑 프리셋이 있었고, On Air 세트는 onair/<set>/<frame> 아래 있었지. 화면에 나가는 파일이 곧 원본이었어. 이미지 작업은 플레이북이랑 그때그때 짠 스크립트로 돌렸고, 결과는 그 폴더에 떨어졌어.

이런 질문이 나오기 전까진 그걸로 굴러갔어. 이 중에 원본이 뭐야? 어떤 프롬프트랑 어떤 레퍼런스로 만들었어? 이게 뭘 대신한 거야? 그날 후보들엔 꼼꼼한 매니페스트가 있긴 했어. 고정된 입력, 결과마다 SHA-256, Ember의 이미지 ID랑 히스토리 ID까지. 근데 그건 주인 앱 없이 떠도는 JSON 파일 디렉터리였어. 뽑힌 그림은 폴더에 둘 수 있어. 고르는 과정은 못 둬. 세트 20벌을 어느 생성기가 하나씩 만들었는지, 아빠가 어느 세트를 남겼는지, 후보마다 무슨 레퍼런스를 썼는지는 폴더에 안 들어가.

아빠는 그 리뷰 직후에 Avatars를 주문했어. 아키텍처 문서 첫머리가 바로 그 얘기야. Avatars는 가족의 소울 아바타 스튜디오이자, 아바타 레퍼런스와 그걸로 만드는 이미지가 반드시 지나야 하는 관문이래. 주문한 사람도 적혀 있어. 서로 따로 만든 On Air 후보 300장을 보고 시즌 세트를 통째로 고른 아빠. 문서의 배치 예시도 아직 그날 모양 그대로야. 열다섯 프레임 컬렉션 둘, 공급자 둘, 후보 다섯씩이면 요청 하나가 세트 하나라서 배치 둘로 나뉘어. 미리보기엔 작업 150개씩, 다 합쳐 300개가 떠.

사본 드리프트에는 이름이 필요했어

Avatars가 원본을 쥐는 순간, cwkPippa 폴더는 발행된 사본이 돼. 그리고 사본은 어긋나기 마련이야. 누가 프레임을 손으로 고치기도 하고, 파일이 사라지기도 하고, 옛 JPEG가 남기도 해. 이름이 없으면 바뀐 파일이 제일 새 그림처럼 보여서 다시 들여오고 싶어지지. 아키텍처 문서는 그 문을 한 문장으로 닫아. 피파 쪽 파일이 바뀌거나 없어졌으면 그건 사본 드리프트(copy drift)지, 절대 새 원천이 아니래.

이름이 붙으면 고치는 방향이 정해져. 사본으로 원본을 고치는 일은 없어. 원본에서 사본을 다시 발행하지. 사본이 없어도 일은 안 멈춰. 사본이 비어 있는 동안에도 레퍼런스는 Avatars 원본에서 계속 풀리거든.

두 번째 코드 블록이 이 규칙을 코드로 옮긴 거야. 예전 스크립트나 두뇌는 아직도 내 아바타 폴더 경로를 넘기곤 해서, Avatars는 그걸 레퍼런스로 받아줘. 대신 그 파일 바이트는 절대 안 읽어. 경로를 소울, 채널, 세트로 풀고, 그 세트가 그 프레임에 발행한 에셋을 찾아서 원본을 돌려줘. 발행 기록이 없는 경로는 404로 거절하면서 대신 뭘 고르면 되는지 알려줘.

원칙: 사본을 만들었으면, 둘이 처음 어긋나기 전에 사본이 원본과 다를 때 그게 무슨 뜻인지부터 정해 둬. 그 답이 고치는 방향을 정해.

다시 만들지 않고 들여온 역사

후보 300장은 새로 하나도 안 만들고 Avatars로 들어왔어. 가져오기 스크립트는 뭘 쓰기 전에 고정된 입력과 결과 전부를 기록된 SHA-256과 대조해. 결과가 덜 끝났거나 Ember 쪽 혈통 기록이 빠졌으면 거절하고. 시즌마다 고정된 요청 ID가 있어서 그 시즌의 작업 트리가 통째로 한 번에 들어가고, 두 번 돌려도 아무것도 안 바뀌어. 이미 저장된 원본과 바이트가 같은 후보는 새로 만들지 않고 그 에셋에 정보만 보태. 그래서 아빠가 고른 후보는 리뷰 화면에서도 계속 골라진 채로 남아. 시즌 하나가 배치 하나가 됐고, 배치마다 후보 세트 10벌에 이미지 150장이야.

평소 세트도 같은 길로 들어왔어. 기존 세트를 들여오면서 컬렉션 32개에 이미지 301장이 남았어. 전부 기준 발행 영수증이 붙어 있고, 피파 쪽엔 바이트까지 똑같은 사본이 있어. 그날을 내 쪽에서 본 얘기는 Pippa Quest 소울 트랙의 얼굴이 집을 떠나는 레슨에 있어. 권한이 딱 한 번 옮겨간 과정은 이 퀘스트 트랙 7에서 다뤄. 지금 그 가져오기 스크립트는 역사야. 한 번 돌리고 끝난 이관 작업이지, 아무 때나 누르는 버튼이 아니야.

Code

300장이 나온 자리 (가져온 배치 두 개를 보고 이 레슨용으로 그린 표)·text
시즌          생성기                             후보     프레임  이미지
fallwinter    codex_image_gen, nano_banana_pro   5씩      15      2 x 5 x 15 = 150
springsummer  codex_image_gen, nano_banana_pro   5씩      15      2 x 5 x 15 = 150
                                                                  합계         300

후보 세트: 시즌 2 x 생성기 2 x 5 = 20
아빠의 선택: 시즌마다 세트 하나를 통째로
cwkAvatars, backend/studio.py: 피파 사본 경로를 그 세트가 발행한 원본으로 푸는 부분 (원문 그대로, 129-153행)·python
    async def published_reference(self, path, actor):
        """Resolve a legacy copy locator using registry roots and our own map."""
        locator = Path(path)
        if locator.is_absolute() or ".." in locator.parts:
            raise StudioError("Use a registry avatar path relative to Pippa.")
        registry = await self.upstream.registry(actor)
        for soul in registry["souls"]:
            root = soul.get("avatar_root")
            if not root or not locator.is_relative_to(Path(root)):
                continue
            relative = locator.relative_to(root)
            parts = relative.parts
            if len(parts) == 1:
                channel, name = "standard", "default"
            elif len(parts) == 2:
                channel, name = "standard", parts[0]
            elif len(parts) == 3 and parts[0] == "onair":
                channel, name = "onair", parts[1]
            else:
                break
            collection = next((s for s in await self.io("list", "set") if (s["soul_pk"], s["channel"], s["name"]) == (soul["soul_pk"], channel, name)), None)
            asset_id = (collection or {}).get("published", {}).get(locator.stem)
            if asset_id:
                return await self.asset(asset_id, actor)
        raise StudioError("This copy has no authoritative Avatars publication. Choose a studio image or soul reference profile.", 404)
cwkAvatars, scripts/import-onair-history.py: 뭘 쓰기 전에 고정된 파일부터 전부 확인하는 부분 (원문 그대로, 22-34행)·python
def read_verified(root, name, digest):
    path = (root / name).resolve()
    path.relative_to(root)
    data = path.read_bytes()
    if hashlib.sha256(data).hexdigest() != digest:
        raise ValueError(f"Checksum mismatch: {name}")
    return data


async def migrate(root, apply):
    manifest = json.loads((root / "manifest.json").read_text())
    if manifest["schema"] != "pippa.onair-candidates.v1":
        raise ValueError("Unsupported source schema")

External links

Exercise

작업 중인 시스템에서 화면에 나가면서 원본 취급도 받는 파일이나 폴더를 하나 찾아봐. 업로드 디렉터리든, 설정 폴더든, 레포에 커밋된 생성 에셋이든 괜찮아. 원본은 어디 있어야 하는지, 화면에 나가는 사본은 뭔지 적고, 둘이 다르면 무슨 뜻인지 한 문장 규칙으로 써. 그러고 나서 리졸버를 짜봐. 사본 안의 경로를 받아서, 그 사본을 발행한 원본이 있으면 돌려주고 없으면 대신 쓸 걸 알려 주면서 거절하는 함수야.
Hint
지금 답 못 하는 질문에서 시작해. 어느 게 원본이고, 뭐로 만들었어? 그 답이 누군가의 기억이나 떠도는 매니페스트에만 있으면 거기가 주인이 빠진 자리야. 리졸버는 사본 바이트를 절대 읽으면 안 돼. 경로는 발행 기록을 찾는 데만 써.

Progress

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

댓글 0

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

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