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

픽셀 말고 역할을 골라

~14 min · typography, localization, inheritance, evolution

Level 0불 꺼진 작업장
0 XP0/58 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

카드 하나를 대여섯 번 그린 일

더빙은 주인의 마스터를 그림으로 그대로 써(이 트랙 여섯 번째 강의). 새 언어로 다시 그리는 건 말이 실린 글자뿐이야. 피파 자신의 방송 카드, 그리고 모바일 영상 더빙에선 화자 이름, 합성 음성 표시, 자막, 오프닝 안내, 장 제목 글자까지. 나머지 픽셀은 전부 주인의 마스터야. 모바일 영상을 더빙하기 시작하고 첫 두 주 동안, 카드 영역마다 글자 크기, 굵기, 행간, 여백이 따로 붙어 있었어. 그때그때 번역하던 세션이 적어 넣은 값이고, 렌더러는 받은 대로 그렸지. 하나하나의 선택이 틀린 건 아니었어. 문제는 세션마다 그걸 전부 처음부터 다시 정했다는 거야. 한 더빙의 중국어, 일본어 카드는 다섯 번, 여섯 번 그려졌어. 다음 더빙은 언어마다 세 번씩 그려졌고, 주인이 세션 로그를 찍은 스크린샷과 함께 렌더 도중에 멈춰 세웠어. 스크린샷 속 세션은 금색 제목보다 커져 버린 자막을 보고 서열을 되돌리는 중이었어. 영어 시트까지 다시 채워서 이미 올라간 영어 영화의 영역이 밀릴 판이었고, 또 한 번 렌더하려고 글자를 50 px 로 키우는 중이었지.

주인의 판정은 카드 하나가 아니라 빠진 규칙을 겨냥했어.

한번 룰로 정하면? 저렇게 계속 실수를 하는 건 룰이 없다는 건데. 세션마다 처음부터 다시 한다는 거거든 — 아빠

정본은 하나, 다형성 세금은 자식이 내

판정은 모양까지 같이 왔어. 주인의 루트 클래스 원칙이야. 정본은 하나 두고, 다른 게 필요한 자식은 꼭 필요한 것만 재정의해서 그 값을 치르고, 나머지는 물려받아. beacon/card_type.py 는 더빙 캔버스 크기 1920×1080 에 맞춘 뿌리 척도 Latin 을 정의해. 역할은 여덟 개야. headline, lead, subtitle, body, cell-head, cell, label, caption. 역할마다 서열, 굵기, 명목 크기(그려질 수 있는 최대 크기), 줄어들 수 있는 바닥, 행간, 여백, 기본 잉크가 붙어. English 는 아무것도 안 바꾸는 자식이야. CJK 는 딱 하나, caption 의 바닥만 바꾸는 자식이고, Chinese, Japanese, Korean 은 그걸 물려받아. 언어를 하나 더하는 건 코드 붙은 클래스 하나를 더하는 거야. 숫자도 모듈 안에서 지어낸 게 아냐. 2026-10-08 부터 2026-10-19 까지 올라간 더빙에서 주인이 이미 받아들인 카드들의 값을 읽어 왔어. 세션끼리 엇갈린 곳에선 가운데를 잡고, 서열 사이에 눈에 보이는 간격을 남겼고.

글자를 키워온 측정

이 모듈에서 제일 배울 게 많은 건 중국어, 일본어 자식이 안 바꾸는 거야. 크기와 행간은 뿌리 그대로야. 판정 전 더빙들에서 중국어, 일본어 글자를 키운 건 전부 같은 원인이었고, 그 원인은 읽는 사람의 필요가 아니었어. 렌더가 끝나면 작업장은 자기 프레임을 다시 읽어서, 그린 잉크와 인코딩된 영상 속 잉크를 견줘(트랙 6 에 새로 붙은 강의 '어떤 글자도 네모로 타지 않아'가 그 계기 얘기야). 제목 크기의 한자는 멀쩡히 있는데도 엄격한 선 14 를 살짝 넘겼어. 어느 제목 서체는 14.3, 37 px 보통 굵기 한자는 15.4 에서 16.6, 42 px 굵은 한자는 14.03 에서 15.02. 정말로 빠진 글자는 44 에서 77, 네모는 51 에서 135 가 나와. 세션들은 멀쩡한 글자가 높게 읽히자 엄격한 선을 통과할 때까지 글자를 키우는 걸로 답했어. 카드가 서열을 잃은 경로가 그거야. 고친 건 계기 쪽이야. 획이 빽빽한 문자는 화면 높이의 56/1080 까지 느슨한 작은 글자 기준을 써.

그렇다고 그 선이 그냥 틀렸던 것도 아냐. 그래서 배울 거리야. 2026-10-18 에 가는 중국어 획이 14.6, 15.3 으로 그 선에 걸리면서 진짜 버그 하나가 드러났어. 굵은 글자용 서체 번호가 글꼴 묶음 안의 보통 굵기를 가리키고 있어서, 굵은 한자 줄이 전부 굵은 숫자, 굵은 가나 옆에 보통 굵기로 그려지고 있었던 거야. 문턱 가까이 앉은 계기는 진짜 결함과 멀쩡한 작업을 같이 잡아. 사람들이 그 값을 보고 제품을 바꾸기 시작하면, 먼저 그 값이 어느 쪽인지부터 물어.

크기는 기계가 정해

이제 영역은 마스터가 정한 것만 말해. 원래 잉크가 어디 앉아 있는지(bounds), 바탕과 잉크 색, 마스터 자체의 글자 안쪽 여백, 줄바꿈, 정렬, 글머리표. 그리고 role 을 이름으로 불러. font_size, weight, line_height, padding, paragraph_gap 을 직접 적은 영역은 거절돼. 재서 확인한 예외는 reason 을 단 override 에 살고, 실행할 때마다 출력되고 카드 잠금에 남아. 그다음 배치가 더빙 전체에 대해 크기를 한 번에 정해. 영역마다 자기 역할 범위 안에서 글이 들어가는 가장 큰 크기를 가져. 먼저 한 줄로, 안 되면 줄을 바꿔서. 바닥 밑으로 내려가야 하면 멈추고, 영역, 상자, 역할, 바닥을 짚으면서 '번역을 줄여'라고 말해. 한 카드 안에서 같은 역할은 한 크기로 그려. 그 역할의 영역들이 필요로 하는 크기 중 가장 작은 걸로. 서열은 불변 조건이야. 같은 카드에서 높은 서열은 낮은 서열보다 최소 2 px 커야 하고, 낮은 쪽이 양보해. 양보하다 자기 바닥 밑으로 내려가야 하면 실행은 멈추고, 위쪽 줄을 줄이라고 말해.

절차는 이걸 한 줄로 적어. 글자를 키우지 말고 번역을 줄여. 카드엔 마스터가 정한 방이 있고, 말이 그 방에 들어가야 해. 여섯 번째 강의가 말소리 한 줄 한 줄에 건 제약이랑 똑같아.

경계는 얼고, 올라간 언어는 다시 안 그려

언어를 처음 렌더하면 dub/<language>/card-lock.json 이 생겨. 영역마다 경계, 역할, 크기, 그리고 이유를 단 예외 전부. 나중 실행이 이미 그린 영역의 경계를 옮기면 멈춰. 경계는 마스터의 기하 구조고, 글자를 맞추는 수단이 절대 아니니까. 잘못 잰 영역은 거절 로그에 이유를 적고 잠금을 지워야만 풀려. 업로드 기록이 있는 언어는 아예 다시 안 그려. 거길 고치는 건 개정이야(트랙 4 에 새로 붙은 강의 '고친 판은 갈아 끼우지 않고 뒤를 이어'). 스크린샷 속 세션이 영어 시트를 다시 채우던 그 일이 이제는 그대로 거절돼.

문자 고유의 규칙도 따라왔어

척도를 정하고 나니, 아무도 고르지 않았는데 CJK 글자가 틀려 있던 다른 길들이 드러났어. 줄바꿈은 금칙을 따라. 닫는 부호나 작은 가나로는 줄을 시작하지 않아(보드는 2026-10-17, 자기만의 탐욕적 줄바꿈을 따로 쓰던 오프닝 안내는 2026-10-26). 2026-10-26 부터는 닫는 부호가 여럿 이어지면 앞 글자랑 통째로 같이 내려가고, 그 글자 바로 앞에 여는 괄호가 있으면 그것도 같이 내려가. 다만 여는 괄호 검사는 이렇게 끌어내릴 때만 돌아. 평범한 글자 앞에서 줄이 끊기면 여는 괄호가 줄 끝에 그대로 남을 수 있어. 그 변경의 커밋 메시지는 줄 끝에 여는 괄호를 절대 안 남긴다고 적었지만, 그 말은 이 경우에만 맞아. CJK 자막에서 숫자는 통째로 남고, 구분 기호에서 안 끊겨(2026-10-13). 그리고 글리프 관문은 이제 소리 내 읽는 자막만이 아니라 더빙이 태우는 모든 면을 봐(2026-10-17). 이름, 목소리 표시, 안내, 장 카드, 영역 하나하나까지. 전부 측정으로 찾았고, 모든 언어가 거쳐 그리는 코드 한 곳에서 한 번 고쳤어.

판정이 요구한 증명은 손보지 않은 렌더였어. 예전 더빙 둘을 중국어, 일본어로 다시 렌더했어. 카드 3 장에 영역 18 개, 카드 12 장에 영역 24 개. 한 번씩만 돌렸고, 되읽기 실패는 없었어. 자막이 제목보다 컸던 그 카드는 모든 언어에서 제목 38 위에 자막 33 으로 나왔고.

픽셀 말고 역할을 불러. 저자마다 눈대중으로 값을 고르면, 저자마다 0 에서 시작하고 시스템엔 규칙 대신 짐작의 역사만 남아. 정본 값은 뿌리 하나에 두고, 변형마다 정말 필요한 것만 재정의하게 하고, 맞추기는 기계가 하게 하고, 실패는 뭘 바꿔야 하는지 이름을 대게 하고, 이미 나간 건 얼려.

Code

beacon/card_type.py 원문 그대로, 2026-10-27 기준·python
class Latin:
    """The root scale: Latin script at the 1920x1080 dub canvas."""
    code = None
    ROLES = {
        'headline':  Role(8, 'bold',     38, 30, 1.25, 8,  INK),
        'lead':      Role(7, 'regular',  35, 20, 1.48, 12, INK),
        'subtitle':  Role(6, 'bold',     33, 26, 1.3,  8,  INK),
        'body':      Role(5, 'regular',  30, 20, 1.42, 6,  INK),
        'cell-head': Role(4, 'bold',     26, 18, 1.3,  2,  GOLD),
        'cell':      Role(3, 'regular',  24, 16, 1.3,  2,  INK),
        'label':     Role(2, 'semibold', 21, 14, 1.2,  4,  GOLD),
        'caption':   Role(1, 'regular',  19, 12, 1.3,  4,  MUTED),
    }
    #: What this script changes, per role, as {role: {field: value}}.
    TAX: dict[str, dict] = {}

    @classmethod
    def role(cls, name: str) -> Role:
        if name not in Latin.ROLES:
            raise ValueError(f'unknown card role {name!r}; one of {", ".join(Latin.ROLES)}')
        role = Latin.ROLES[name]
        for klass in reversed(cls.__mro__[:-1]):          # root first, the child last
            role = replace(role, **klass.__dict__.get('TAX', {}).get(name, {}))
        return role


class English(Latin):
    code = 'en'


class CJK(Latin):
    """Han, kana and Hangul fill the em with dense strokes.

    The sizes and leading stay the root's. A Han glyph already fills more of
    the em than a Latin capital, and the accepted Chinese and Japanese cards
    used the English leading throughout; every CJK size bump before #558 was
    the glyph readback misreading present dense strokes (beacon.glyphs
    DENSE_GLYPH), not a reader's need. What the script does need: the
    smallest captions stay at a size where a stroke-dense character is still
    a character, and line breaks follow kinsoku (authored_dub.NO_LINE_START,
    applied by character class, so a mixed Latin/Han line obeys it too)."""
    TAX = {
        'caption': {'min': 14},
    }


class Chinese(CJK):
    code = 'zh'


class Japanese(CJK):
    code = 'ja'


class Korean(CJK):
    code = 'ko'
판정 전과 후의 영역, 그리고 남는 잠금 (스케치)·text
// before 2026-10-20: every session wrote its own type
{ "key": "c2-title", "bounds": [212, 140, 1708, 236],
  "font_size": 52, "weight": "bold", "translated": "<the line>" }

// now: the region states the master's geometry and names a role
{ "key": "c2-title", "card": "c2", "role": "headline",
  "bounds": [212, 140, 1708, 236], "background": [10, 12, 18, 255],
  "translated": "<the line>" }

// a measured exception is explicit, and logged
{ "key": "c2-foot", "card": "c2", "role": "caption",
  "bounds": [212, 980, 1708, 1012], "translated": "<the line>",
  "override": { "padding": 2, "reason": "<what was measured>" } }

// dub/ja/card-lock.json after the first render
{ "schema": "beacon.card-lock.v1", "language": "ja",
  "regions": { "c2-title": { "bounds": [212, 140, 1708, 236],
                             "role": "headline", "size": 38 } },
  "overrides": [ { "key": "c2-foot", "padding": 2, "reason": "<...>" } ] }

External links

Exercise

네 작업에서 저자가 매번 눈대중으로 고르는 값을 하나 찾아봐. 슬라이드 글자 크기, 서비스 호출 타임아웃, 재시도 횟수, 로그 수준, UI 간격 같은 거. 지난 몇 달 동안 사람들이 실제로 고른 값을 모아서 흩어진 정도를 봐. 그다음 정본 뿌리를 설계해. 이름 붙인 역할들, 받아들여진 사례에서 가져온 값, 변형(플랫폼, 언어, 등급)이 재정의해도 되는 것과 꼭 물려받아야 하는 것, 들어맞지 않을 때 시스템이 내는 메시지. 이미 나간 사례에선 뭐가 얼어야 하는지도 정해.
Hint
제일 쓸모 있는 건 실패 메시지야. 정본 값을 바꿔야만 들어맞는다면 뿌리가 틀린 거고, 내용을 바꿔서 맞출 수 있다면 메시지가 어느 내용을 얼마나 바꿔야 하는지 말해야 해. 그리고 흔들림 뒤에 숨은 계기를 찾아봐. 슬금슬금 올라간 값은 아무도 의심 안 한 측정에 대한 답인 경우가 많아.

Progress

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

댓글 0

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

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