Chub.ai 또는 RisuAI에 업로드하기 전 캐릭터 카드 검증 방법.
Tavern 카드를 올리기 전의 의식 —— Chub.ai, RisuAI, SillyTavern 업로드를 조용히 망치는 10가지 실수, 그것들을 잡아내는 수동 체크리스트, 그리고 카드 수가 늘었을 때 자동화해야 할 lint 워크플로.
출항 전 마지막 점검
주요 카드 호스트의 지원 게시판을 열어보면 같은 질문이 반복된다. 업로드는 됐는데 인사말이 비어 있다, 로어북이 안 따라온다, 이미지 미리보기가 깨졌다. 스레드의 결말도 비슷하다 —— 조용히 수정, 재 export, 재업로드. 카드 자체는 거의 다 맞았던 셈이다.
검증은 이 왕복을 막아주는 값싼 습관이다. Chub.ai 로 push 하거나 RisuAI 로 import 하기 전, 2분만 필드 목록을 훑어보자. 동일한 체크리스트가 확장된다 —— 10장은 눈으로, 100장은 스크립트로. 이 글은 그 양면을 다룬다: 사람이 봐야 할 것, linter가 대신해야 할 것.
업로드를 망치는 10가지 실수
커뮤니티 bug 스레에서 등장하는 빈도순으로 대략:
- first_mes 누락 또는 빈 mes_example. 호스트는 카드를 받지만 채팅 창은 비어 있다. RisuAI는 특히 mes_example 로 톤을 잡으므로 빈 문자열은 합리적 기본값이 아니다.
- 너무 긴 description 이 토큰 예산을 터뜨림. 업로드는 깔끔하지만 첫 생성에서 prompt가 페르소나로 가득 차 응답 공간이 없다. 토큰 한도는 호스트마다 다르지만 원칙적으로 상주 블록(description + personality + scenario + system_prompt)은 대상 모델의 context window에 여유를 남겨야 한다.
- creator_notes 와 system_prompt 혼동. 전자는 사람용 —— 사용 팁, 경고, 크레딧. 후자는 매 턴마다 모델에 전송된다. 바뀌면 메타 코멘트가 대화에 새거나, jailbreak이 모델에 닿지 않는다.
- PNG tEXt 청크 손상 또는 잘못된 인코딩. 관례는 keyword =chara 의 tEXt에 Base64로 인코딩된 UTF-8 JSON. 원시 UTF-8을 그대로 쓰거나 non-ASCII를 떼어내거나 잘못된 keyword를 쓰는 도구는 한 호스트에서는 열리지만 다음 호스트에서는 조용히 망가지는 PNG를 만든다.
- character_book 필드 타입 오류. entry는 특정 형태를 기대한다: keys 는 문자열 배열이지 쉼표로 구분된 문자열이 아니다. insertion_order 는 숫자, enabled 는 불리언. 손으로 편집한 로어북일수록 숫자를 문자열로, 배열을 단일 문자열로 적기 쉽고, importer는 경고 없이 entry를 버린다.
- extensions 아래 대상 플랫폼이 모르는 커스텀 키. SillyTavern이 적은 Regex pipeline 설정은 Chub 미리보기 렌더러에서는 무의미. 망가지지는 않지만 로컬에서 확인한 동작이 이동하는 순간 사라진다.
- V3 전용 필드가 V2 카드에 섞임. nickname, group_only_greetings, creator_notes_multilingual 은 V3 추가. 카드의 spec 가 여전히 chara_card_v2 이면 규격 준수 클라이언트는 이들을 무시할 권리가 있다.
- tags 형태 오류. 이는 「문자열 배열」. "tags": "fantasy, royalty" 는 문자열이며 거부되거나 「fantasy, royalty」라는 이상한 단일 태그로 색인된다.
- alternate_greetings 안의 빈 문자열. 보통 지우지 못한 초안 잔재. 빈 선택지로 렌더하는 클라이언트도 있고 예외를 던지는 것도 있다. 어느 쪽이든 노이즈 버그.
- 호스트 정책 —— 레이트 리밋, 이미지 크기 상한, NSFW 필터. 스키마 문제는 아니고 로컬 linter로 다 잡을 수는 없지만, 위 항목들 못지않게 업로드를 실패시킨다. 새벽에 일괄 업로드 돌리기 전에 각 호스트 규칙을 한 번씩 짚고 가자.
수동 체크리스트
카드가 적을 때는 직접 한 바퀴 도는 것이 어떤 도구보다 빠르다. 업로드 누르기 전에 다음을 체크:
- [ ] spec 와 spec_version 일치 (V2는 2.0, V3는 3.0).
- [ ] name, description, first_mes 가 모두 존재하고 비어 있지 않음.
- [ ] 상주 prompt가 대상 context window에 여유를 남김.
- [ ] creator_notes 에 모델용 지시가 섞이지 않음.
- [ ] PNG가 열리고 미리보기 정상, hex에서 keyword chara 의 tEXt 청크 확인.
- [ ] 모든 character_book.entries[*].keys 가 문자열 배열.
- [ ] tags 가 배열; alternate_greetings 에 빈 문자열 없음.
- [ ] V2 카드에 V3 전용 키 없음(반대도 마찬가지).
- [ ] extensions 아래 각 키가 문서화돼 있고 어느 호스트용인지 파악함.
- [ ] 대상 호스트의 업로드 규칙(이미지 크기, NSFW, 레이트 리밋) 확인 완료.
linter가 대신해야 할 일
수동 체크리스트는 30장쯤에서 재미가 사라진다. 그 다음은 자동화. 쓸 만한 카드 linter는 세 가지 일을 한다:
- 스키마 검증. PNG를 파싱하고 tEXt 를 디코드한 뒤 선언된 spec 에 대해 JSON을 검증한다. 필수 필드, 타입, 열거, V2 카드에 섞인 V3 키 등.
- 호스트 호환성 매트릭스. 카드의 각 키에 대해 어느 호스트가 실제로 소비하는지 보고. SillyTavern만 읽는 필드라고 「깨졌다」는 뜻은 아니다 —— 단지 이동 못 한다는 사실을 미리 알아야 가치가 있다.
- 토큰 예산 추정. 자주 쓰는 tokenizer로 상주 prompt를 합산하고 대상 모델의 context를 위협하는 임계값을 넘으면 경고한다.
바로 tavernai.cards 가 채우려는 자리: 수동으로 할 검사를 실제 발행 대상 호스트들에 대해 돌려주는 업로드 전 워크벤치.
업로드 기록에서 깨진 카드를 발견하는 일은 그만하자. 발행 전에 V2 / V3 카드를 실제 호스트의 특이사항에 맞춰 lint 하고, 읽을 수 있는 diff로 스펙 간 마이그레이션을 수행한다.