마리오 카트 자동 집계 Bot 설계에서 고안한 점
요약
본 기사는 마리오 카트 팀전 점수 자동 집계 시스템을 설계하고 개선한 과정을 다룹니다. OBS 화면에서 실시간으로 스코어를 추출하여 오버레이에 표시하는 애플리케이션의 핵심 기술과 구조를 설명합니다. 특히, OCR 정밀도를 높이기 위해 Gemini 2.5 Flash Lite 모델 채택, 결과 영역 크롭 및 대비(Contrast) 조절 기법을 적용했습니다. 나아가, 사람이 수동으로 확인하고 승인한 플레이어 이름(Hint)을 다음 회차의 OCR 프롬프트에 피드백 루프 형태로 제공하여 시스템의 안정성과 정확도를 획기적으로 개선했습니다.
핵심 포인트
- Gemini 2.5 Flash Lite를 채택하여 저렴한 비용, 빠른 응답 속도, 실용적인 일본어 Mii 네임 인식 정밀도를 확보했다.
- OCR 입력 효율을 높이기 위해 결과 영역만 크롭(Crop)하고 휘도 변환(휘도 대비 증가)을 적용하여 정보량과 정밀도를 동시에 개선했다.
- 수동으로 확인된 플레이어 이름(Hint)을 다음 OCR 프롬프트에 피드백 루프로 제공함으로써, 시스템의 오독률을 낮추고 안정적인 데이터 처리를 구현했다.
- OpenCV 대신 `@techstark/opencv-js`를 사용하여 Next.js 환경에서 OBS 연동부터 OCR까지 Node.js 프로세스 하나로 통합하여 셋업 복잡도를 줄였다.
시작하며
마리오 카트 8DX의 팀전(라운지)에서는 12명이 12개의 레이스를 달리고, 팀별 합계 점수를 겨룹니다. 레이스가 하나 끝날 때마다 순위를 확인하고 팀별로 점수를 집계하는 것은 꽤 번거로운 일입니다.
그래서 OBS 화면을 읽어 들여 자동으로 팀별 스코어를 집계하고, 방송 오버레이(Overlay)에 실시간으로 표시하는 애플리케이션을 만들었습니다. 구현은 아래의 두 Zenn 기사를 참고했습니다.
방송 중 자동 집계라는 발상으로부터, OBS WebSocket · 템플릿 매칭(Template Matching) · Vision API를 이용한 OCR · 순위의 점수화까지의 설계를 공개해 주셨습니다. 덕분에 제 애플리케이션을 망설임 없이 만들기 시작할 수 있었습니다. 저자분께 이 자리를 빌려 진심으로 감사의 말씀을 드립니다.
한 가지 구현상의 차이점으로서, 참고 기사에서는 템플릿 매칭 부분에 Python (OpenCV)이 사용되었습니다. 제 애플리케이션은 @techstark/opencv-js를 채택하여, Next.js 애플리케이션만으로 완결되는 구성으로 만들었습니다. OBS 연동부터 OCR, 오버레이까지 Node.js 프로세스 하나로 동작하기 때문에 셋업이 심플해집니다.
본 기사에서는 그 토대 위에 추가로 구현한 다음 두 가지 점에 집중하여 작성하겠습니다.
- OCR의 정밀도를 높이기 위한 고안
- OCR이 틀려도 결과를 보정할 수 있는 구조
OCR의 정밀도를 높이기 위한 고안
Vision API 모델로 Gemini 2.5 Flash Lite를 채택했다
참고 기사에서는 ChatGPT를 사용하고 있었습니다. 제 구현에서도 처음에는 마찬가지로 ChatGPT를 사용했으나, 최종적으로는 Gemini 2.5 Flash Lite로 결정했습니다. 이유는 다음 세 가지입니다.
1. 요금이 압도적으로 저렴하다
ChatGPT 계열의 모델과 비교했을 때, Gemini Flash Lite는 토큰(Token) 단가가 한 자릿수 이상 저렴합니다. 레이스당 비용이 거의 무시할 수 있는 수준이 되었습니다.
2. 응답이 빠르다
flash 계열은 속도 중시 모델이며, 그중에서도 lite는 특히 토큰 생성이 빠릅니다. "결과 화면이 나온 후 오버레이에 스코어가 반영되기까지"의 타임랙(Time lag)이 체감상 짧아졌습니다. 방송 연출로서 이 지연은 그대로 "집계의 느림"으로 시청자에게 전달되기 때문에 무시할 수 없는 지표입니다.
3. 일본어 Mii 네임의 정밀도가 실용 수준이다
기호(★・♪・♥)가 섞여 있거나, 히라가나·가타나·영문이 혼재된 Mii 네임이라도, 후술할 전처리와 힌트 기능과 결합하면 실용적인 정밀도를 낼 수 있었습니다. Lite 모델이라도 이미지를 잘라낸(Crop) 후 프롬프트(Prompt)로 유도하면, 이른바 경량 모델임을 의식하지 않고 사용할 수 있습니다.
다만 "Gemini가 항상 베스트"라는 주장은 아닙니다. 코스나 방송 해상도, 참가자의 이름 경향에 따라 베스트 프로바이더(Provider)는 바뀐다고 느끼고 있습니다. 그래서 OpenAI / Gemini / Claude를 전략 패턴(Strategy Pattern)으로 교체 가능하게 해두고, .env.local의 한 줄로 전환할 수 있도록 했습니다. 기본값을 Gemini 2.5 Flash Lite로 설정해 두었다는 이야기입니다.
결과 영역만 크롭하여 대비(Contrast)를 높인다
참고 기사에서는 "그레이스케일(Grayscale)화나 이진화(Binarization)는 코스에 따라 정밀도를 떨어뜨린다"라고 적혀 있었습니다. 동감입니다. 저도 처음에 시도해 보고 같은 결론에 도달했습니다. 최종적으로 남은 전처리는 다음 두 가지뿐입니다.
const RESULT_CROP_REGION = {left: 840, top: 80, width: 580, height: 920,}
```as const;
...
1920x1080에서 결과가 표시되는 영역인 580x920으로 크롭(Crop)하면, API에 전달되는 정보량은 약 1/4이 됩니다. 토큰 사용량과 정밀도가 동시에 올라가므로, 처음에 이 부분부터 착수하면 효과적입니다.
linear(1.3, -30)은 y = 1.3x - 30의 휘도 변환으로, 문자와 배경의 대비를 가볍게 넓히는 정도입니다. 과하게 하면 문자가 뭉개지므로 계수는 절제하는 것이 효과적입니다.
실패 시 원본 이미지를 그대로 반환하는 점도 미묘한 포인트입니다. OBS 소스의 해상도가 바뀌어 크롭에서 예외가 발생하는 경우에도, OCR까지 중단하지 않고 원본 이미지로 계속 진행할 수 있습니다.
힌트 승인을 통해 OCR에 피드백 루프를 돌린다
OCR의 정밀도는 완벽해질 수 없습니다. 특히 Mii 네임은 기호나 반각·전각의 혼재, 닮은 형태의 히라가나·가타카나("ニ"와 "二" 등)가 많아서, 단 한 번에 모든 인원을 올바르게 읽어내는 경우가 드뭅니다.
그래서 조작 화면에 「힌트 승인 패널」을 두었습니다. OCR이 읽은 플레이어 이름을 사람이 확인하고, 올바르다고 판단하면 승인합니다. 승인된 이름은 내부 스토어에 저장되어, 다음 회차 이후의 OCR 프롬프트(Prompt)에 "이 이름의 플레이어가 참여하고 있을 것"이라는 힌트로 섞여 들어갑니다.
export function buildPrompt(hints?: HintInfo | null): string {
if (!hints || hints.players.length === 0) {
return BASE_PROMPT;
...
포인트는 "힌트를 정답으로 강제하지 않는 것"입니다. 프롬프트에서 "이미지를 제대로 보고, 표기의 참고용으로만 사용하라"고 명시합니다. 이를 작성하지 않으면, 다른 사람이 해당 팀 슬롯에 중도 참여했을 때 힌트에 끌려 오독(Misreading)이 발생합니다.
이 피드백 루프(Feedback Loop) 덕분에, 1 레이스는 수동으로 몇 건 수정하더라도 2 레이스 이후부터는 동일한 플레이어가 안정적으로 읽힙니다.
OCR이 틀려도 결과를 보정할 수 있는 구조
OCR의 정밀도는 아무리 높여도 100%에 도달할 수 없습니다. 실용적인 관점에서는 "OCR이 틀리더라도 사람이 보고 즉시 고칠 수 있는 것"이 훨씬 중요합니다. UI는 그것을 전제로 만들어졌습니다.
조작 화면의 보정 패널은 다음과 같은 모습입니다.
패널에서 조작할 수 있는 항목을 세 가지 관점으로 나누어 설명하겠습니다.
스코어 정합성을 자동으로 체크하기
MK8DX의 라운지전은 12명이 참가하며, 1 레이스의 합계 점수는 반드시 82점(15 + 12 + 10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1)이 됩니다. 이를 이용해 집계 결과가 부정확할 경우 조작 화면에서 경고를 띄웁니다.
판정 로직 자체는 매우 단순합니다.
const POINTS_PER_RACE = 82;
const validTeams = teams.filter((t) => !t.isInvalid);
const actualTotal = validTeams.reduce(
...
무효화된 팀은 합계에서 제외하고, 수동 보정값(scoreAdjustment)은 가산한 뒤, 82 × 레이스 수와 일치하지 않으면 경고를 보냅니다. 앞서 스크린샷 최상단에 있는 주황색 경고 바("합계 점수가 일치하지 않습니다~")가 바로 그것이며, OCR의 누락이나 보정 실수를 여기서 사용자에게 인지시킵니다.
오검출을 「무효화」하는 설계 (삭제가 아닌)
OCR은 가끔 원래 같은 팀인 플레이어를 다른 팀으로 취급하는 실수를 합니다.
이러한 오검출에 대한 조작으로, 팀을 삭제하는 것이 아니라 무효화하는 버튼만을 마련했습니다. 스크린샷 오른쪽 끝 「무효화」 열에 있는 「유효」 버튼이 그것입니다. 누르면 무효화 상태로 전환되어, 대상 팀을 스코어 집계와 표시에서 제외합니다. 단, 데이터 자체는 남아 있으므로 실수로 눌렀더라도 다시 누르면 즉시 원래대로 되돌릴 수 있습니다.
OCR이 놓친 팀을 수동으로 추가하기
반대로, 아무도 OCR에 검출되지 않은 팀을 수동으로 추가하는 조작도 마련되어 있습니다. 스크린샷 맨 아래에 있는 「팀 태그」 입력란과 초기 스코어란, 「팀 추가」 버튼이 그것입니다. 팀 식별자와 현재 합계 점수를 입력하고 누르면 이후의 집계에 포함됩니다.
추가 후의 합계 점수는 앞서 언급한 정합성 체크가 잡아내므로, 경고 바를 보면서 "앞으로 몇 점이 더 필요한지" 확인하며 보정할 수 있습니다.
요약
OCR 정밀도 측면의 고안
- Vision API 모델로 Gemini 2.5 Flash Lite를 채택 (비용·속도·일본어 정밀도의 균형) - 프로바이더(Provider)는 전략 패턴(Strategy Pattern)으로 교체 가능하게 구성
- 결과 영역의 크롭(Crop)과 대비(Contrast) 강화에 집중한 전처리 (복잡한 처리는 오히려 정밀도를 떨어뜨림)
- 힌트 승인을 통해 "수동 수정 = 다음 회차 정밀도 개선"이라는 피드백 루프를 구축
결과 보정 측면의 고안
- 1 레이스 82점 규칙으로 스코어 정합성을 자동 체크
- 오검출은 「무효화」로 처리 (삭제하지 않음 = 되돌릴 수 있음)
- 수동 팀 추가로 OCR의 누락을 보완
동일하게 자동화 도구를 만드는 분들에게 참고가 된다면 기쁘겠습니다.
마지막으로, 베이스를 공개해 주신 참고 기사의 저자분께 진심으로 감사드립니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기