Skip to content
Pink-SpiderPublic

About

주식 수익률 변동을 세로형 라인 레이스 애니메이션으로 만들어 YouTube Shorts로 발행하는 로컬 툴 (Python 수집기 + Remotion 렌더러)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

hindsight

TypeScript React Vite Hono Remotion Docker pnpm License: MIT

주식 수익률 변동을 애니메이션 영상으로 만들어 YouTube Shorts로 발행하는 개인용 로컬 툴.

티커 몇 개와 기간을 넣으면 "2020년, 1,000만원을 넣었다면?" 같은 세로형(1080×1920, 60fps, 약 40초) 라인 레이스 영상이 나온다. 훅 → 레이스(₩ 환산 카운터·종목 간 격차·완급 연출) → 결과 요약 정지 프레임.

  • 한국/미국 주식·ETF 지원 (지수는 라이선스 문제로 추종 ETF 사용: S&P500→SPY, 나스닥100→QQQ)
  • 수정주가(adjusted close) 기준 가격수익률 — 화면에 명시
  • 한/미 휴장일 차이는 union 거래일 축 + forward-fill로 정렬, 리베이스 100

아키텍처

컴포넌트는 기능이 아니라 계약 3개로 자른다. 계약이 안정되면 양쪽 구현은 독립적으로 교체 가능하다.

flowchart LR
    ADP["Python collector<br/>KR·US·FX 어댑터"] -->|"계약 ① 쓰기"| DB[("SQLite<br/>일간 OHLCV·환율")]
    DB -->|"읽기 전용"| SB["spec-builder<br/>정규화·리베이스·다운샘플"]
    SB -->|"계약 ② Scene Spec JSON"| TPL["templates<br/>(frame, spec) → 화면 상태<br/>순수 함수"]
    TPL -->|"계약 ③"| DRAW["renderer-canvas<br/>단일 drawScene"]
    DRAW --> REM["Remotion 셸<br/>→ mp4 (+BGM 먹싱)"]
    DRAW --> WEB["웹 스튜디오<br/>실시간 미리보기"]
Loading
  • 계약 ① SQLite — Python(collector)은 쓰기만, TypeScript는 읽기만. 언어 경계가 DB 파일 하나다.
  • 계약 ② Scene Spec — specVersion·template·공유 time 배열을 가진 JSON. 렌더러와의 유일한 계약.
  • 계약 ③ 템플릿 인터페이스 — 연출 로직(easing·완급·카운터)은 전부 templates의 순수 함수에 있고, Remotion(mp4)과 웹 미리보기는 같은 drawScene(Canvas 2D) 하나를 공유한다. 미리보기에서 본 것과 영상이 어긋날 수 없는 구조.

모노레포

경로 역할
data/collector/ Python — 시장 어댑터(FDR 1차, yfinance 폴백) + SQLite 증분 적재
packages/scene-spec 계약 ②: zod 스키마 + 검증기
packages/spec-builder 정규화 → Scene Spec 생성 (순수 함수, I/O 없음)
packages/templates 계약 ③: 템플릿 = 프레임 → 화면 상태
packages/renderer-canvas 단일 페인터 drawScene + 실시간 플레이어
packages/renderer-remotion Remotion 셸 (drawScene을 감싸 mp4로)
apps/server 로컬 API(:4600) — Spec 생성, 렌더 잡 큐, BGM 목록
apps/worker 잡 큐 소비 → Remotion 렌더 → ffmpeg BGM 먹싱
apps/web 제작 스튜디오(:5173) — 입력 폼 + 실시간 미리보기

시작하기

요구사항: Node 22+, pnpm 10(corepack), Python 3.11+, ffmpeg

pnpm install
python3 -m venv .venv
.venv/bin/pip install finance-datareader yfinance pandas
.venv/bin/python3 scripts/make-placeholder-bgm.py   # (선택) 검증용 BGM 트랙 생성

개발 모드

pnpm server   # API 서버 :4600
pnpm queue    # 렌더 워커 (잡 큐 폴링)
pnpm web      # 웹 스튜디오 :5173 (/api는 서버로 프록시)

http://localhost:5173 에서 종목·기간·타이틀·맥락 한 줄을 넣고 미리보기 → 렌더 (mp4). 부족한 시세 데이터는 서버가 collector를 호출해 자동 적재한다. 렌더 결과는 out/job_<id>.mp4에 쌓이고, 스튜디오의 "mp4 다운로드" 링크로 받는다. 첫 렌더 때는 Remotion이 Chrome Headless Shell을 내려받으므로 한 번은 오래 걸린다.

docker

docker compose up   # server(:4600, 웹 포함) + worker

data/(시세·잡 DB), out/(결과물), assets/(BGM)는 호스트 볼륨으로 공유한다. worker 이미지는 Chrome Headless Shell 런타임 라이브러리·Noto CJK 폰트·ffmpeg를 포함해 server보다 크다.

CLI

.venv/bin/python3 data/collector/ingest.py 005930 NVDA USD/KRW       # 시세 적재 (증분)
.venv/bin/python3 data/collector/ingest.py NVDA --start 2010-01-01           # 과거 구간 백필
.venv/bin/python3 data/collector/ingest.py 005930 --full                     # 전체 재수집
pnpm render specs/golden_synthetic.json [out.mp4]       # Spec → mp4 (기본 out/<spec>_remotion.mp4)
pnpm studio                                             # Remotion Studio
pnpm --filter @hindsight/worker exec tsx src/still.ts specs/golden_synthetic.json 600 "$PWD/out/frame600.png"   # 특정 프레임 PNG

증분 모드는 캐시의 최신일 − 7일부터만 다시 받는다. --start가 지금까지 요청해 본 범위(ingest_meta.covered_from)보다 앞서면 그 날부터 다시 받아 과거를 채운다(백필). 상장일 이전을 요청해도 한 번만 시도하고 기억한다. 액면분할·배당락 뒤 수정주가를 갱신하려면 --full(--start 기본 2010-01-01)로 전체를 다시 받는다.

웹 UI 없이 서버 API만으로도 Spec을 만들 수 있다 (pnpm server가 떠 있어야 한다):

curl -s localhost:4600/api/specs -H 'content-type: application/json' -d '{
  "tickers": [{"id": "005930", "name": "삼성전자"}, {"id": "SPY", "name": "S&P 500"}],
  "start": "2020-01-01", "end": "2024-12-31",
  "title": "2020년, 1,000만원을 넣었다면?",
  "contextLine": "국민주와 미국 시장 — 같은 5년, 다른 결말",
  "seedKrw": 10000000
}' > specs/samsung_spy.json
pnpm render specs/samsung_spy.json

사용법

티커 표기

형태 분류 소스 예
숫자 6자리 한국 주식·ETF FinanceDataReader(네이버) — Close가 이미 수정주가 005930, 069500
그 외 영문 미국 주식·ETF FinanceDataReader(야후 Adj Close) → 실패 시 yfinance NVDA, SPY, QQQ
A/B 환율 FinanceDataReader USD/KRW

표시 이름(name)을 주면 화면에는 그 이름이 나온다. 한국 종목 코드는 라인 헤드 배지에도 이름을 쓴다. 웹 스튜디오는 종목을 1~5개까지 받는다. 기간 중간에 상장한 종목이 섞이면 모든 종목에 값이 생기는 첫 날부터 리베이스한다.

Scene Spec

계약 ②. 데이터와 연출 파라미터를 담은 JSON 한 장이며, 렌더러는 이것만 읽는다. 스키마는 packages/scene-spec의 zod가 단일 소스다.

{
  "specVersion": 1,
  "template": "line-race",
  "targetFps": 60,
  "maxPoints": 600,
  "timeline": { "hookSec": 1.5, "raceSec": 35.5, "endSec": 3.0, "warp": 0.6 },
  "meta": {
    "title": "2020년, 1,000만원을 넣었다면?",
    "contextLine": "국민주와 미국 시장 — 같은 5년, 다른 결말",
    "returnType": "price",
    "seedKrw": 10000000,
    "dataSource": "FinanceDataReader"
  },
  "bgm": { "track": "placeholder_ambient.m4a", "gainDb": -6 },
  "axis": { "time": ["2020-01-02", "2020-01-06", "..."] },
  "series": [
    { "id": "005930", "name": "삼성전자", "color": "#76B900", "values": [100, 101.2, "..."] },
    { "id": "SPY", "name": "S&P 500", "color": "#5B8DEF", "values": [100, 99.4, "..."] }
  ]
}
필드 설명
timeline 훅 + 레이스 + 엔딩(초). 합이 영상 길이. warp 0=선형, 1=ease-out(초반 빠르고 후반 느림)
meta.contextLine 필수. 엔딩 패널 제목이자 유튜브 반복 콘텐츠 정책 대응
meta.returnType price(가격수익률) / total(총수익률). 푸터에 표기된다
meta.seedKrw 선택. 있으면 ₩ 환산 카운터를 그린다
bgm 선택. track은 assets/bgm/의 파일명, gainDb 기본 −6
axis.time 공유 날짜축. 길이 ≤ maxPoints
series[].values 리베이스 100 기준. 모든 시리즈가 axis.time과 같은 길이여야 한다

서버 API

메서드 경로 역할
POST /api/specs tickers, start, end, title, contextLine, seedKrw?, targetFps?, maxPoints?, bgm? → Scene Spec. 부족한 시세는 자동 적재
POST /api/renders { spec } → { id }. 렌더 잡을 큐에 넣는다
GET /api/renders/:id { status, error, hasFile }. status는 queued / running / done / error
GET /api/renders/:id/file 완료된 mp4 다운로드
GET /api/bgm assets/bgm/의 트랙 목록

환경 변수: PORT(기본 4600), PYTHON(collector를 실행할 파이썬, 기본 .venv/bin/python3).

BGM

assets/bgm/에 음원 파일(mp3/m4a/wav)을 넣으면 웹 스튜디오 드롭다운에 나타난다. 워커가 렌더 후 ffmpeg로 루프·페이드(인 0.8s / 아웃 2.5s) 먹싱한다. 발행용 음원은 YouTube 오디오 보관함(수익화 안전) 사용을 권장하며, 음원은 저장소에 커밋하지 않는다.

개발

  • 테스트 러너·린터는 없다. 타입체크는 pnpm -r exec tsc --noEmit.
  • 스모크 테스트는 pnpm render specs/golden_synthetic.json. 시드 고정 합성 랜덤워크라 실제 시세 없이 파이프라인 전체를 돈다. 재생성: .venv/bin/python3 scripts/make-synthetic-fixture.py
  • 연출을 바꿨으면 still.ts로 프레임을 뽑아 눈으로 확인한다. 연출 로직은 packages/templates, 픽셀 그리기는 packages/renderer-canvas/src/draw.ts에만 있다.
  • 워커는 Remotion 번들을 프로세스당 한 번만 만든다. packages를 고쳤으면 pnpm queue를 재시작한다.
  • 템플릿 추가 절차와 계약별 불변 조건은 CLAUDE.md에 있다.

문제 해결

증상 확인할 것
렌더 잡이 queued에서 안 움직임 pnpm queue(또는 docker worker)가 떠 있는지
해당 기간 데이터 없음 티커 표기·상장일 확인. ingest.py <티커> --start <날짜>로 수동 적재
수익률이 이상함(분할·배당락 의심) ingest.py <티커> --full로 재수집. 수정주가는 과거 전체가 바뀐다
리눅스에서 한글이 네모로 나옴 Noto CJK 등 한글 폰트 설치 (docker worker 이미지에는 포함)
BGM 먹싱 실패 ffmpeg가 PATH에 있는지, 트랙 파일이 assets/bgm/에 있는지

로드맵 (현재: P2 완료)

단계 범위 상태
P0 matplotlib PoC — 연출 감각 확보 ✅
P1 Scene Spec v1 + Remotion 파이프라인 ✅
P2 collector + 웹 스튜디오 + 잡 큐 + docker ✅
P3 템플릿 3종(단일/라인 레이스/바 차트 레이스) + 메타 생성 예정
P4 YouTube 업로드 자동화 (OAuth 검증 통과 후) 예정
P5 모바일 뷰어 앱 + 공개 뷰어 API 선택

설계 결정 기록(ADR)·단계별 실행 계획은 비공개 노션에서 관리한다.

주의

  • 시세 데이터는 FinanceDataReader/yfinance 등 무료·비공식 소스다. 수익화 규모가 커지면 상업 이용이 허용된 유료 소스로 전환한다 (어댑터 뒤에 숨겨둔 이유).
  • Remotion은 개인·직원 3인 이하 조직은 상업 이용 포함 무료. 메이저 버전 업그레이드 시 라이선스 재확인.
  • YouTube는 반복적·대량생산 콘텐츠를 수익화 불가로 분류한다 — 영상마다 맥락 한 줄(contextLine)이 계약 수준에서 필수인 이유.
  • 실제 시세로 만든 specs/*.json·out/*.mp4·data/*.db는 커밋하지 않는다(gitignore). 저장소에 들어가는 Spec은 합성 픽스처 specs/golden_synthetic.json 하나뿐이다.

라이선스

MIT — LICENSE 참고.

About

주식 수익률 변동을 세로형 라인 레이스 애니메이션으로 만들어 YouTube Shorts로 발행하는 로컬 툴 (Python 수집기 + Remotion 렌더러)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages