TRENDSHIFT · 2026.06.15

ASCILINE 딥다이브
— 영상을 "텍스트"로 스트리밍하는 실시간 ASCII 렌더링 엔진

ASCILINE은 일반 MP4 영상을 서버에서 디코딩해 픽셀을 ASCII 문자(또는 컬러 점)로 변환한 뒤, <video> 태그가 아니라 WebSocket으로 "문자 데이터"만 흘려보내 브라우저 Canvas에 그려 주는 실시간 영상 엔진이다. 핵심 주장은 도발적이다 — "웹은 원래 텍스트로 만들어졌으니, 영상도 텍스트로 보내자." 그 결과 영상에 CSS 필터(네온 글로우·그림자)를 입히고, 광고 차단기·자동재생 제한을 우회하며, 초저대역폭으로 IoT까지 스트리밍할 수 있는 "타이포그래피 캔버스"가 된다. (저장소: YusufB5/ASCILINE · Python(FastAPI)+Vanilla JS · MIT 라이선스 + 광고 금지 조항)

이 문서는 README를 베끼는 대신 stream_server.py·app.js·codec.py·codec.js 소스를 직접 뜯어, "실시간 영상-텍스트 변환 파이프라인과 적응형 코덱에서 무엇을 배울 수 있는가"에 초점을 둔 학습 해설이다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 시스템 / 환경 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

이 레포가 무엇을 하는 물건인가.

핵심 메시지

"영상을 GPU가 디코딩하는 압축 바이너리가 아니라,
WebSocket으로 흘러오는 '문자'로 만들어 Canvas에 그린다."

보통 웹 영상은 <video> 태그가 H.264/VP9 같은 코덱을 GPU로 풀어 화면에 뿌린다. ASCILINE은 이 흐름을 통째로 바꾼다. 서버(Python)가 영상을 한 번 디코딩해 각 픽셀을 밝기에 맞는 ASCII 문자 + RGB 색으로 매핑하고, 그 결과를 바이너리 프레임으로 WebSocket에 실어 보낸다. 브라우저는 받은 문자 데이터를 Canvas에 한 글자씩 그려서 영상처럼 보이게 한다.

핵심은 "재미있는 ASCII 아트"가 아니라 "영상의 실체가 텍스트/Canvas다"라는 점이다. 그래서 영상에 CSS 필터를 입히고, 글자를 드래그로 선택하고, 브라우저의 자동재생·광고 차단 정책을 우회할 수 있다 — 브라우저 입장에선 그냥 "JS가 Canvas를 갱신하는 것"이기 때문이다.

ASCILINE실시간 비디오-ASCII 렌더링 엔진이다. 백엔드는 FastAPI + OpenCV + NumPy로 영상을 그리드(grid)로 다운샘플해 문자/색 버퍼를 만들고, 프론트엔드는 순수 Vanilla JS가 WebSocket으로 받은 프레임을 지터 버퍼(jitter buffer)에 쌓아 requestAnimationFrame 루프로 그린다. 오디오 트랙을 마스터 클럭(master clock)으로 삼아 A/V 동기를 맞추고, 정적/저움직임 프레임에는 적응형 코덱(adaptive codec)으로 변한 셀만 보내 대역폭을 수백 배 아낀다. 웹 UI 외에 터미널에서 트루컬러 ANSI로 직접 재생하는 모드(ascii_video_player2.py)도 함께 들어 있다.

용어
ASCII 아트 렌더링 (pixel-to-character mapping)
이미지의 각 영역 밝기를 밝기 순으로 정렬된 문자 팔레트(예: 어두움 " .:-=+*#%@" 밝음)에 매핑해 글자로 그림을 그리는 기법. ASCILINE은 여기에 픽셀의 RGB 색을 함께 실어 "컬러 ASCII 영상"을 만든다. 한 글자가 영상 한 셀(cell)에 대응한다.
용어
지터 버퍼 (jitter buffer)
네트워크로 들어오는 프레임은 도착 간격이 들쭉날쭉하다(=지터). 이를 작은 큐에 잠깐 쌓아 두었다가 일정한 속도로 꺼내 재생하면 끊김이 줄어든다. ASCILINE의 frameBuffer(크기 약 4프레임)가 이 역할을 한다 — 실시간 스트리밍 영상·음성의 필수 부품.

2왜 주목받는가

"영상=텍스트"라는 발상이 푸는 네 가지 실전 문제.

단순히 "옛날 ASCII 무비 플레이어"였다면 트렌딩하지 못했을 것이다. ASCILINE이 주목받는 이유는 "영상의 실체를 텍스트로 바꾸면 생기는 부수효과"를 전면에 내세웠기 때문이다. README가 정리한 네 가지 "전략적 비전"은 그대로 차별점이 된다.

일반 웹 영상(<video> / HLS)ASCILINE
실체압축 바이너리(GPU 디코딩)HTML/Canvas 위의 순수 문자
스타일링영상 위 오버레이만 가능영상 자체에 CSS 필터·글로우·애니메이션
대역폭코덱이 고정(약함=버벅임)프레임당 수 KB, 적응형 코덱으로 가변
브라우저 제약자동재생 throttle·광고차단 표적"JS가 canvas 갱신" — 제약 우회
AI 친화성무거운 CV 모델 필요구조화된 문자열 → 경량 LLM이 의미 요약
IoT/약한 기기마이크로컨트롤러가 코덱 못 풂서버가 무거운 일 처리, 몇 KB만 전송
기존 방식의 한계
"영상은 코덱이 풀어 주는 블랙박스"라는 전제

일반 웹 영상은 GPU가 디코딩하는 불투명한 픽셀 덩어리다. 그래서 ① 영상 콘텐츠에 텍스트처럼 CSS 필터를 직접 못 입히고, ② 약한 네트워크/마이크로컨트롤러는 H.264를 풀 여력이 없으며, ③ 브라우저는 자동재생 영상을 적극적으로 throttle하고 광고차단기가 미디어 프레임을 막는다. "영상=무거운 바이너리"라는 전제 자체가 병목이다.

ASCILINE의 해결
"무거운 일은 서버에서 한 번, 클라이언트엔 가벼운 문자만"

ASCILINE은 픽셀→문자 변환이라는 무거운 연산을 백엔드에서 한 번만 수행한다. 클라이언트로는 그리드 크기만큼의 문자+색 버퍼(또는 적응형 압축본)만 흐른다. 정적 화면에선 변한 셀만 전송(DELTA)해 레거시 대비 0.3%(약 375배 절감)까지 줄어든다. 게다가 출력이 "텍스트/Canvas"이므로 CSS·DOM의 모든 무기를 그대로 쓸 수 있다 — 이것이 "타이포그래피 캔버스"라는 비전의 실체다.

비유

일반 영상 스트리밍이 "완성된 그림을 통째로 택배로 보내는 것"이라면, ASCILINE은 "색칠 도안의 '바뀐 칸 좌표와 색만' 우편엽서로 보내는 것"에 가깝다. 화면 대부분이 그대로면(슬라이드·정지화면) 보낼 게 거의 없고, 받는 쪽은 도안 위 그 칸만 다시 칠하면 된다. 그래서 정적 콘텐츠에서 대역폭이 극적으로 줄어든다.

3기술 스택 전체 지도

백엔드(변환 엔진)·프론트엔드(렌더러)·코덱/인프라 각각.

① 백엔드 — 영상 디코딩 & 픽셀→문자 변환

백엔드의 일은 "영상 프레임을 그리드로 줄여 문자/색 버퍼로 만들고, WebSocket으로 밀어내는 것"이다. CPU 연산(리사이즈·인덱싱)이 핵심이라 NumPy 벡터화가 성능의 열쇠다.

요소역할
Python 3.10+전체 백엔드 언어. 타입힌트·async/await 사용.
FastAPI + uvicornHTTP 라우트(/, /static, /audio)와 @app.websocket("/ws") 엔드포인트.
OpenCV (cv2)영상 디코딩·프레임 추출·그리드 리사이즈. cap.grab()로 디코딩 없이 프레임 스킵(FPS 데시메이션).
NumPy밝기→문자 인덱스 매핑(floor_divide+LUT), 컬러 양자화((rgb>>qb)<<qb), 델타 비교를 모두 벡터 연산으로.
websocketssend_bytes/send_text로 프레임 전송, ConnectionClosed 처리.
FFmpeg (subprocess)/audio 스트림 생성. --vol 배율을 volume 필터로 적용. --vol 0이면 아예 실행 안 함.
ascii_video_player2.py엔진 공용 모듈. VideoDecoder·AsciiMapper를 서버와 터미널 플레이어가 공유.

② 프론트엔드 — Vanilla JS Canvas 렌더러

프레임워크가 전혀 없다(React·Vue 무관). app.js 한 파일이 WebSocket 수신·지터 버퍼·A/V 동기·Canvas 그리기를 다 한다. "장식 애니메이션 0, 순수 성능"이 코드 주석의 선언이다.

요소역할
WebSocket (binary)binaryType='arraybuffer'. /ws?codec=adaptive로 접속해 적응형 프레임 수신.
Canvas 2D컬러 모드(2~5)는 fillText로 글자별 색 그리기. 픽셀 모드는 ImageData 제로카피 픽셀 채우기.
requestAnimationFrame렌더 루프. 마스터 클럭과 비교해 늦은 프레임은 버리고(catch-up), 미래 프레임은 대기.
<pre> 선택 레이어Canvas 위에 투명 <pre>를 겹쳐 "보이지 않는 텍스트"를 깔아, 영상 글자를 마우스로 드래그 선택·복사하게 함.
<audio> 마스터 클럭오디오 currentTime이 절대 기준 시계. 오디오가 없으면 performance.now() 폴백.
RLE 색 캐싱이전 셀과 색이 같으면 fillStyle을 다시 안 바꿔(prevPacked) 드로우 오버헤드 절감.

③ 코덱 · 인프라 · 보안

요소역할
적응형 코덱(codec.py/codec.js)프레임마다 RAW/ZLIB/DELTA 중 가장 작은 인코딩 선택, 1바이트 태그로 표시. Python 인코더 ↔ JS 디코더가 바이트 단위로 동일.
키프레임 리싱크KEYFRAME_INTERVAL=48마다 전체 프레임 강제 전송 — 패킷 유실·늦게 접속한 클라이언트 재동기.
손실 시간 델타(선택)--quality로 색 셀이 허용 오차(tolerance)를 넘을 때만 재전송. 문자 평면은 항상 정확, 기본은 무손실.
정적 파일 화이트리스트STATIC_WHITELIST = {app.js, style.css, codec.js} — 디렉토리 트래버설 차단.
Origin 검증_origin_allowed() — localhost·동일 출처만 WS 허용해 cross-site WebSocket hijacking 방지(LAN 모드 호환).
크로스 검증 테스트experiments/: Python 인코딩 벡터를 Node가 디코딩해 byte-exact 확인 + 라이브 adaptive↔legacy diff.
배포/실행CLI 직접 실행(python stream_server.py). --folder/--playlist로 큐 관리, --loop 무한 반복.
용어
제로카피 (zero-copy)
데이터를 중간 버퍼에 복사하지 않고 그대로 쓰는 최적화. ASCILINE 픽셀 모드는 서버가 보낸 BGR 바이트를 별도 변환 객체로 옮기지 않고, ImageData.data에 직접 R↔B만 스왑해 채운다(for(src,dst) 루프). 복사가 사라져 고해상도에서 CPU를 아낀다.

4아키텍처 심화 분석

영상 → 그리드 → 프레임 인코딩 → WS → Canvas, 그리고 A/V 동기.

전체 파이프라인 한눈에

입력은 MP4 한 개(또는 폴더/플레이리스트). 서버는 영상을 열어 폭(--cols)에 맞춰 그리드 크기(rows×cols)를 종횡비로 자동 계산하고, INIT 핸드셰이크로 FPS·모드·그리드를 클라이언트에 알린다. 이후 프레임마다 OpenCV로 읽어 NumPy로 문자/색 버퍼를 만들고, 적응형 코덱으로 인코딩해 WebSocket에 실어 보낸다. 동시에 /audio가 FFmpeg로 오디오를 스트림한다. 클라이언트는 프레임을 버퍼에 쌓고, 오디오 시계를 기준으로 정확한 타이밍에 Canvas에 그린다.

MP4 영상 1개 (예: clip.mp4 --mode 5 --cols 240) │ ┌───────────────▼─────────────────┐ │ build_queue() 단일/폴더/플레이리스트 │ │ get_video_dimensions() → 종횡비 │ │ calc_auto_rows(cols, w, h) │ │ ASCII: rows = cols / ratio / 2 │ ← 글자는 폭의 2배 높이 │ PIXEL: rows = cols / ratio │ └───────────────┬─────────────────┘ │ WS 접속: /ws?codec=adaptive ─────────── INIT 핸드셰이크 (text) ─────────── "INIT:{fps}:{mode}:{cols}:{rows}:{pixel}:{idx}" ──────────────────┬────────────────────────── ┌─────────────────────┐ │ ┌──────────────────────────────┐ │ SERVER (Python) │ │ │ CLIENT (Vanilla JS) │ │ while True: │ │ │ buildCanvas(cols, rows) │ │ grab() x (skip-1) │ │ │ codecDecoder = makeDecoder() │ │ next(VideoDecoder) │ │ │ │ │ ┌── ASCII(mode>1) ─┐│ │ │ ws.onmessage → │ │ │ gray→문자 인덱스 ││ │ │ binary → codec.decode() │ │ │ (NumPy LUT) ││ │ │ → frameBuffer.push() │ │ │ bgr→RGB, 양자화 ││ │ │ (지터 버퍼, 약 4프레임) │ │ │ [char,R,G,B]/cell ││ │ │ │ │ └──────────────────┘│ │ │ requestAnimationFrame: │ │ ┌── PIXEL ──────────┐│ │ │ masterClock = audio.curTime │ │ │ raw BGR 3B/cell ││ │ │ 늦은 프레임 drop / 미래 wait │ │ └──────────────────┘│ │ │ ┌ mode1 → pre.textContent │ │ encode_frame() │ │ │ ┌ pixel → putImageData │ │ send_bytes(msg)──────┼─┼─┼──→┌ color → fillText/cell │ │ /audio (FFmpeg) ──────┼─┼─┼──→ <audio> (마스터 클럭) │ └─────────────────────┘ │ └──────────────────────────────┘ │ ── 적응형 코덱 프레임 포맷 (한 메시지 = 한 프레임) ── [ 4B frame_index (BE) ][ 1B tag ][ payload ... ] tag 0 RAW : framebuffer 원본 (레거시 호환) tag 1 ZLIB : zlib(framebuffer) ← 일반 움직임 tag 2 DELTA : zlib(바뀐 셀 인덱스 ++ 값) ← 정적/저움직임

핵심 1 — 픽셀을 문자로: NumPy 벡터 매핑

변환의 심장은 "밝기값(0~255)을 문자 팔레트 인덱스로 바꾸는" 한 줄이다. AsciiMapper는 어두움→밝음 순으로 정렬된 92글자 팔레트를 들고, 파이썬 루프 없이 NumPy 인덱싱으로 그리드 전체를 한 번에 변환한다.

밝기 → 문자 인덱스 (stream_server.py / AsciiMapper)
# 팔레트(어두움→밝음). 길이 n=93
PALETTE = " `.-':_,^=;><+!rc*/z?s ... %&@"   # 끝일수록 밝다

# gray: (rows, cols) uint8 밝기 행렬
indices = np.floor_divide(gray, max(1, 256 // n))  # 0..n-1 구간으로
np.clip(indices, 0, n - 1, out=indices)
char_codes = char_byte_lut[indices]   # 인덱스 → 문자코드, 통째로

# 색: BGR → RGB 후 양자화(상위 비트만 남김 → 압축률↑)
rgb = bgr_frame[:, :, ::-1]
if qb > 0: rgb = (rgb >> qb) << qb   # mode별 qb: 5→0, 4→2, 3→3, 2→5

# 셀당 4바이트 버퍼: [문자, R, G, B]
frame_buf[:, :, 0] = char_codes
frame_buf[:, :, 1:] = rgb

여기서 qb(양자화 비트)는 모드의 정체다. mode 5(16M컬러)는 qb=0(무손실), mode 2(512컬러)는 qb=5로 색을 거칠게 깎아 같은 색 셀이 많아지게 만든다 — 그래야 다음 단계 ZLIB/RLE 압축이 잘 먹힌다. "화질 모드"가 사실은 "압축 친화도 모드"인 셈이다.

핵심 2 — 적응형 코덱: 프레임마다 최선의 인코딩 선택

원래 프로토콜은 매 프레임 그리드 전체를 다시 보냈다. codec.pyencode_frame()은 프레임마다 바뀐 셀 비율(frac)을 재고, 세 인코딩 중 가장 작은 것을 골라 1바이트 태그로 표시한다. 똑똑한 점은 "양 극단의 명백한 패자는 시도조차 안 한다"는 것이다.

태그인코딩유리한 상황레거시 대비(mode5,200×80)
0 RAWframebuffer 원본압축 불가 프레임(안전망)
1 ZLIBzlib(framebuffer)일반 움직임고움직임 63% (절대 손해 없음)
2 DELTA바뀐 셀 인덱스+값만 zlib정적/저움직임정지화면 0.3% (약 375×)
"명백한 패자 건너뛰기" 선택 로직 (codec.py)
_DELTA_MAX_FRAC = 0.60   # 이보다 많이 바뀌면 DELTA는 손해 → 안 만듦
_ZLIB_MIN_FRAC  = 0.10   # 이보다 적게 바뀌면 전체 ZLIB는 손해

frac = changed.mean()              # 바뀐 셀 비율
candidates = []
if frac < _DELTA_MAX_FRAC:         # 적게 변함 → DELTA 후보
    candidates.append((TAG_DELTA, zlib(idx + vals), delta_shown))
if frac >= _ZLIB_MIN_FRAC or not candidates:
    candidates.append((TAG_ZLIB, zlib(raw), frame))

tag, payload, shown = min(candidates, key=lambda c: len(c[1]))  # 가장 작은 것
if len(raw) < len(payload):        # zlib이 오히려 부풀면 RAW로
    tag, payload, shown = TAG_RAW, raw, frame

또 하나의 핵심은 문자 평면은 절대 손상하지 않는다는 규칙이다(tolerance는 색에만). 글자가 깨지면 ASCII 영상의 "구조"가 무너지므로, 손실 압축을 켜도 글자(채널 0)는 항상 정확히 보낸다. 48프레임마다 키프레임을 강제해 늦게 접속한 클라이언트·패킷 유실도 자동 복구한다.

핵심 3 — A/V 동기: 오디오가 절대 시계다

영상과 소리가 어긋나는(desync) 문제를 ASCILINE은 "오디오를 마스터 클럭으로 삼는다"는 한 원칙으로 푼다. 비디오 프레임은 각자 time = frameIndex / fps를 갖고 버퍼에 쌓이고, 렌더 루프는 오디오의 현재 재생 위치와 비교해 프레임을 버리거나 기다린다.

렌더 루프의 A/V 동기 로직 (app.js)
masterClock = (audio && !audio.paused)
  ? audio.currentTime               # 오디오가 절대 기준
  : (now - streamStartTime) / 1000; # 폴백: 벽시계

# 너무 뒤처진 프레임은 버리고 따라잡기(catch-up)
while (buf.length > 1 && buf[0].time < masterClock - 0.1) buf.shift();

# 아직 미래의 프레임이면 이번 틱은 대기
if (buf[0].time > masterClock + 0.05) return;

const frame = buf.shift().data;     # 지금 그릴 프레임

여기에 "오디오 준비 게이트"가 더해진다. 영상은 버퍼링해 두되 오디오의 playing 이벤트가 뜰 때까지 첫 렌더를 미룬다(0.5초 폴백 포함). 덕분에 시작 직후의 끊김(stutter)이 사라진다. 고FPS 소스(60fps)는 grab()으로 디코딩 없이 프레임을 건너뛰어(데시메이션) 30fps로 떨궈 CPU를 절반으로 줄인다.

핵심 설계 패턴 요약

패턴ASCILINE에서의 구현
벡터화 변환밝기→문자, 색 양자화를 파이썬 루프 없이 NumPy 통째 연산으로.
적응형 인코딩프레임 특성(frac)에 따라 RAW/ZLIB/DELTA를 per-frame 선택.
공유 디코더codec.js를 브라우저와 Node 테스트가 공유 → "배포 경로 = 테스트 경로".
마스터 클럭 동기오디오 currentTime 기준 drop/wait로 A/V 정렬.
지터 버퍼도착 지터를 작은 큐로 흡수해 끊김 완화.
하위호환 옵트인?codec=adaptive 없으면 레거시 프로토콜 그대로 — 기존 클라이언트 무영향.
제로카피 렌더픽셀 모드는 ImageData에 직접 채워 복사 제거.

5디렉토리 구조 해부

파일 10여 개로 끝나는 단출한 단일 폴더 — 그래서 읽기 좋다.

ASCILINE/ ├── stream_server.py ★★ 웹 서버 본체 (≈30KB) — FastAPI + /ws + /audio + CLI │ · websocket_endpoint(): 큐 루프·INIT·인코딩·전송 │ · calc_auto_rows(): 종횡비로 rows 자동 계산 │ · _origin_allowed(): WS 하이재킹 방지 │ · audio_stream(): FFmpeg 서브프로세스 오디오 ├── ascii_video_player2.py ★ 엔진 공용 모듈 + 터미널 플레이어 (≈14KB) │ · VideoDecoder: OpenCV 프레임 이터레이터·grab() │ · AsciiMapper: 밝기→문자 LUT + 트루컬러 RLE │ · TerminalRenderer: ANSI 무플리커 출력 ├── codec.py ★ 적응형 인코더 (Python) — RAW/ZLIB/DELTA 선택 ├── codec.js ★ 적응형 디코더 (브라우저 + Node 공유) ├── app.js ★★ 프론트엔드 전부 (≈17KB) — WS·버퍼·동기·Canvas ├── index.html "선언문" 블로그형 데모 페이지 (player-container) ├── style.css Matrix 그린 테마 (CSS 변수로 --accent-color 등) ├── playlist.json 예시 플레이리스트(영상별 mode/vol/cols 오버라이드) ├── (play.bat / serve.bat 없음 — CLI로 직접 실행: python stream_server.py) ├── videos/ 사용자가 영상을 넣는 폴더 (.gitkeep만 추적) ├── experiments/ ★ 크로스언어 검증 스위트 │ ├── gen_vectors.py Python으로 프레임 인코딩 → 벡터 덤프 │ ├── check_vectors.js Node가 codec.js로 디코딩 → byte-exact 확인 │ ├── test_e2e.js 라이브 adaptive↔legacy 프레임 diff │ └── make_test_clips.sh ffmpeg lavfi로 결정적 테스트 영상 생성 ├── .gitignore *.mp4 등 영상·캐시·실험 산출물 제외 ├── LICENSE MIT + 광고 금지(ANTI-ADVERTISEMENT) 조항 └── README.md 전략 비전·CLI·모드·코덱 성능표
읽는 순서 추천

index.html로 화면 구조(canvas + 투명 pre 선택레이어) 파악 → ② app.jsconnectWebSocket·renderFrame으로 수신·동기·그리기 흐름 → ③ ascii_video_player2.pyAsciiMapper로 변환 공식 → ④ stream_server.pywebsocket_endpoint로 서버 루프 → ⑤ codec.py+codec.js 한 쌍을 나란히 놓고 인코더/디코더 대응 확인. 핵심 통찰은 ②③⑤에 거의 다 있다.

6학습 포인트 (기술별)

실시간 스트리밍·코덱·Canvas 렌더링에서 무엇을 배우나.

A. NumPy 벡터화 — "루프를 지우면 빨라진다"

영상 한 프레임은 수만 개 픽셀이다. 이걸 파이썬 for로 돌면 실시간이 불가능하다. ASCILINE은 밝기→문자(floor_divide+LUT 인덱싱), 색 양자화((rgb>>qb)<<qb), 델타 비교(np.abs(a-b)>tol)를 모두 배열 통째 연산으로 처리한다. "파이썬 루프를 NumPy 연산으로 바꾸는" 사고법이 그대로 보이는 교본이다.

실습: 임의의 그레이스케일 이미지를 받아 (1) 파이썬 이중 for문 (2) NumPy 인덱싱 두 방식으로 ASCII 문자열로 변환하고, time으로 속도를 비교하라.

B. 적응형 코덱 — "상황마다 다른 압축을 고르기"

"무조건 zlib"이 정답이 아니다. 정지화면엔 델타가, 격변 프레임엔 전체 압축이 유리하다. ASCILINE은 바뀐 비율을 재서 후보를 만들고 가장 작은 걸 고른다. 게다가 "양 극단의 패자는 만들지도 않는" 가지치기로 CPU까지 아낀다. "정책을 데이터(프레임 특성)에 맡기는" 적응형 시스템 설계의 좋은 미니 사례다.

실습: 1바이트 태그 헤더로 RAW/RLE/DELTA를 고르는 200×80 그리드용 미니 코덱을 직접 짜고, 정지·저움직임·고움직임 세 케이스의 바이트 수를 표로 비교하라.

C. 크로스언어 byte-exact 테스트 — "보낸 것 = 받은 것" 증명

인코더(Python)와 디코더(JS)가 다른 언어면, "정말 똑같이 복원되나?"를 증명해야 한다. ASCILINE은 디코더 한 벌(codec.js)을 브라우저와 Node 테스트가 공유하고, Python이 만든 벡터를 Node가 풀어 바이트 단위 일치를 확인한다(check_vectors.js). "배포되는 코드가 곧 테스트되는 코드"라는 원칙은 그대로 베껴 쓸 가치가 있다.

실습: B에서 만든 코덱의 인코더를 Python, 디코더를 JS로 짜고, Python이 덤프한 바이너리를 Node가 디코딩해 원본과 byte-exact인지 검증하는 스크립트를 작성하라.

D. 실시간 A/V 동기 — "시계를 하나로 정하라"

영상·음성이 따로 흐르면 반드시 어긋난다. 해법은 "무엇이 절대 시계인지 먼저 정하는 것"이다. ASCILINE은 오디오를 시계로 삼고, 비디오 프레임을 그 시계에 맞춰 버리거나(catch-up) 기다린다(wait). 시작 끊김은 "오디오 준비 게이트"로, 고FPS 부하는 grab() 데시메이션으로 푼다. 영상 플레이어·게임 루프·메트로놈 앱 어디에나 옮길 수 있는 원리다.

실습: requestAnimationFrame 루프에서 가짜 프레임 큐를 audio.currentTime(또는 가짜 시계)에 맞춰 drop/wait로 소비하는 30줄짜리 동기 데모를 만들어, 일부러 프레임을 늦게 넣어 catch-up이 작동하는지 확인하라.

E. Canvas 위 "보이지 않는 선택 레이어"

Canvas에 그린 글자는 마우스로 드래그 선택이 안 된다(그림이니까). ASCILINE은 Canvas 위에 투명한 <pre>를 정확히 겹쳐(object-fit 위치까지 맞춤) 같은 텍스트를 깐다. 색은 transparent지만 DOM 텍스트라 선택·복사가 된다. "그래픽 위에 접근성/상호작용 레이어를 겹치는" 영리한 트릭이다.

실습: Canvas에 임의 텍스트를 그리고, 그 위에 동일 텍스트의 투명 <pre>transform: translate/scale로 정렬해 마우스 드래그 선택이 되게 만들어 보라.

7시스템 / 환경 요구사항

설치는 가볍지만, --cols 값이 곧 "하드웨어 부담 다이얼"이다.

항목요구사항
런타임Python 3.10+. pip install fastapi uvicorn opencv-python numpy websockets.
오디오(선택)FFmpeg 필수(서버측 볼륨 처리). winget/brew/apt 또는 exe를 프로젝트 폴더에 드롭.
브라우저WebSocket + DecompressionStream('deflate')(zlib 해제) 지원 필요 — 현대 크롬/엣지/파이어폭스.
CPU/메모리고정 요구는 없으나 --cols가 클수록 인코딩 부하 급증. 너무 높이면 서버가 프레임을 못 따라가 A/V desync.
권장 colsASCII: 200~240 / 픽셀: 600~900(near-HD, CPU·VRAM 의존). 미지정 시 ASCII 200·픽셀 450 기본.
네트워크로컬(127.0.0.1:8000) 기본. --host 0.0.0.0로 LAN 노출(동일출처 Origin만 허용).
영상 파일videos/에 직접 넣음(레포에는 미포함, .gitignore로 제외). experiments/make_test_clips.sh로 테스트 클립 생성 가능.
라이선스 주의 — MIT지만 "광고 금지" 조항이 붙는다
ANTI-ADVERTISEMENT RESTRICTION

ASCILINE은 MIT지만, 저자가 "광고 네트워크가 차단 불가능한 광고를 송출하는 용도"를 명시적으로 금지하는 조항을 LICENSE에 추가했다. 이 엔진이 광고차단기·브라우저 제약을 우회한다는 바로 그 특성이 악용될 수 있기 때문이다. 상업적 사용 전 LICENSE 전문을 반드시 확인할 것.

8직접 해볼 수 있는 실습 과제

난이도별 5단계 — 클론 후 본인 영상으로 바로 시도.

과제 1 난이도 ★☆☆☆☆

다섯 가지 모드로 같은 영상 비교

짧은 MP4 하나를 --mode 1(B&W)부터 --mode 5 --pixel까지 바꿔 가며 돌려 보고, 각 모드의 화질·체감 FPS·상태바의 버퍼 수를 비교한다. --cols를 200→400→800으로 올리며 언제 A/V desync가 시작되는지 관찰하라.

과제 2 난이도 ★★☆☆☆

테마 바꾸기 + 디버그 대역폭 읽기

style.css--accent-color·--bg-color를 바꿔 Matrix 그린을 본인 색으로 교체한다. 그다음 --debug 플래그로 서버를 띄워 터미널에 찍히는 RAW vs WIRE 바이트와 압축비를 정지화면·고움직임 영상에서 각각 읽어 보라.

과제 3 난이도 ★★★☆☆

AsciiMapper 팔레트·양자화 실험

ascii_video_player2.pyDEFAULT_PALETTE를 다른 문자셋으로 바꾸고, mode별 qb(양자화 비트)를 조절해 화질과 압축률의 트레이드오프를 직접 만든다. 터미널 플레이어(ascii_video_player2.py)로 트루컬러 ANSI 출력 차이도 확인하라.

과제 4 난이도 ★★★★☆

크로스언어 코덱 검증 재현

experiments/make_test_clips.sh로 테스트 영상을 만들고, gen_vectors.pycheck_vectors.js를 돌려 Python 인코딩을 Node가 byte-exact로 디코딩하는지 재현한다. 그다음 test_e2e.js로 라이브 adaptive↔legacy 프레임이 동일한지 + 대역폭 절감률을 측정하라.

과제 5 난이도 ★★★★★

새 코덱 태그 추가하기 (예: tag 3 = 행 RLE)

codec.py에 새 인코딩(예: 행 단위 RLE)을 TAG 3으로 추가하고, codec.js 디코더에 대응 분기를 넣는다. 키프레임/하위호환을 깨지 않도록 후보 선택 로직에 끼워 넣고, 과제 4의 검증 스위트로 여전히 byte-exact인지 증명하라. (인코더/디코더 한 쌍을 동시에 다루는 진짜 코덱 작업)

9관련 기술 심화 학습 로드맵

6주 코스 — NumPy 영상 처리부터 실시간 코덱·동기까지.

주차주제실습 · 참고
1주차OpenCV + NumPy 영상 처리 — 프레임 추출·리사이즈·벡터 인덱싱VideoDecoder·AsciiMapper 정독 · 과제 1·3
2주차WebSocket 실시간 스트리밍 — 바이너리 프레임·INIT 핸드셰이크stream_server.py websocket_endpoint · FastAPI WS 문서
3주차Canvas 2D 렌더링 — fillText/ImageData·제로카피·선택 레이어app.js renderFrame·buildCanvas · 과제 5(E)
4주차압축 기초 — zlib/deflate·RLE·델타 인코딩·키프레임codec.py 정독 · 미니 코덱 구현(과제 2)
5주차적응형 시스템 — 상황별 정책 선택·크로스언어 byte-exact 테스트codec.js + experiments/ 재현(과제 4)
6주차실시간 A/V 동기 — 마스터 클럭·지터 버퍼·FPS 데시메이션동기 데모 구현 · grab() 데시메이션 분석

10핵심 키워드 사전

본문·소스에 나온 용어 빠른 참조.

용어의미
그리드 (grid)영상을 줄인 문자 격자. cols×rows, 한 칸(cell)이 한 글자/픽셀에 대응.
AsciiMapper밝기→문자 LUT + 트루컬러 RLE를 담당하는 변환 클래스.
양자화 (qb)색 하위 비트를 깎아 같은 색 셀을 늘리는 것((rgb>>qb)<<qb). 압축률↑.
적응형 코덱프레임마다 RAW/ZLIB/DELTA 중 가장 작은 인코딩을 골라 1바이트 태그로 표시.
DELTA이전 프레임 대비 바뀐 셀의 인덱스+값만 보내는 인코딩. 정적 화면에 강함.
키프레임 (keyframe)48프레임마다 전체를 강제 전송 — 패킷 유실·늦은 접속 재동기용.
tolerance (손실 델타)색 셀이 이 오차를 넘을 때만 재전송. 0=무손실, 문자 평면은 항상 정확.
마스터 클럭A/V 동기 기준 시계. ASCILINE은 오디오 currentTime을 사용.
지터 버퍼도착 간격 불균일을 흡수하는 작은 프레임 큐(frameBuffer).
FPS 데시메이션고FPS 소스를 grab()로 프레임 스킵해 30fps로 떨궈 CPU 절감.
제로카피중간 복사 없이 ImageData에 직접 픽셀을 채우는 최적화(픽셀 모드).
선택 레이어Canvas 위 투명 <pre> — 그린 글자를 드래그 선택·복사 가능하게.
INIT 핸드셰이크접속 시 서버가 보내는 INIT:fps:mode:cols:rows:pixel:idx 메타.
Origin 검증localhost/동일출처만 WS 허용 — cross-site WebSocket hijacking 방지.
FFmpeg서버측 오디오 스트림 생성·볼륨 처리. --vol 0이면 미실행.

11참고 링크