요청 네 가지를 넣는다.
1) 자막 배경 여백(padding)
배경 상자를 창 전체가 아니라 **글자를 감싸는 크기**로 그리도록 바꿨다.
창 전체를 칠하던 동안에는 여백을 키워도 글자만 안으로 밀릴 뿐 배경
크기는 그대로여서, 설정 항목을 만들어도 의미가 없었다.
2) 평소 잠금 + '자막 위치 조정'
locked 기본값을 True 로 바꾼다. 게임 중에 자막을 실수로 끌고 다니는
사고를 막는 쪽이 기본이어야 한다. 자막 화면의 버튼으로 조정 모드를
열면 그때만 이동·네 모서리 크기 조절·휠이 열린다. 조정 중에는 클릭
통과를 강제로 끈다 — 켜둔 채로는 마우스가 창을 지나가 잡을 수 없다.
크기를 바꾸면 글자 크기가 높이 비율을 그대로 따라간다. 상자만 커지고
글자가 그대로면 여백만 늘어나 '자막을 키우는' 조작이 되지 않는다.
조정 모드는 저장하지 않는다. 다음 실행은 다시 잠긴 상태로 시작한다.
3) GPU 가 없을 때 CPU 추천
티어 표의 recommended 는 GPU 가 넉넉할 때를 전제한 값이라, 그대로
쓰면 GPU 없는 사람에게 10GB 짜리를 권하게 된다. 환경을 보고 고르는
recommended_tier() 를 만들고 배지도 그 결과로 붙인다. GPU 가 없으면
CTranslate2 계열만 후보에 두고 가장 가벼운 쪽을 권한다 — LLM 계열은
CPU 에서 한 문장에 수십 초가 걸려 실시간 자막이 되지 않는다.
'추천대로 맞추기' 버튼과, 실제로는 CPU 로 도는데 화면은 GPU 라고
표시하던 것도 같이 고친다. nvidia-smi 실패 문구가 명령줄을 통째로
토해내던 것도 사람이 읽을 문장으로 바꿨다.
4) 마이크 등 입력 장치 선택
InputDeviceCapture 를 추가하고 홈 화면에 목록 전환 버튼을 둔다.
두 목록을 한 번에 섞지 않는다 — 다 합치면 정작 찾으려던 게임이
라인입력들 사이에 파묻힌다. 버튼 글자는 '지금 상태'가 아니라
'누르면 가는 곳'을 적는다. 고른 쪽은 설정에 기억한다.
모델이 티어에 따라 2~17GB 를 차지하는데 %APPDATA% 에 박혀 있어서 C 드라이브가
좁은 사람은 손쓸 방법이 없었다. 처음 켤 때 한 번 묻고, 그 선택을 기억하고,
설정에서 바꿀 수 있게 한다.
닭과 달걀 문제가 하나 있다 — "어디에 둘지" 는 사용자가 정하는데 그 선택 자체는
고정된 곳에 적어둬야 다음 실행에 찾을 수 있다. 그래서 OS 표준 폴더에는 위치를
가리키는 쪽지(location.json) 한 장만 두고, 내용물은 전부 고른 폴더에 둔다.
- storage.py 신설: 쪽지 읽기/쓰기, 경로 검증, 여유 공간, 데이터 이동
- config.py 의 CONFIG_PATH 를 config_path() 로 바꾼다. import 시점 상수라
위치를 바꿔도 옛 경로를 계속 가리켰다
- 위치가 정해지기 전에는 파일 로그를 열지 않는다. 열면 고르지도 않은 기본
폴더가 만들어지고 거기에 로그가 남는다
- 이전 버전 사용자에게는 묻지 않는다. 기본 위치에 쓰던 흔적이 있으면 그대로
쓴다 — 같은 자리를 다시 고르는 헛수고를 시킬 이유가 없다
- 고른 디스크가 빠져도 죽지 않고 기본 위치로 물러난다
- 위치를 바꿀 때 받아둔 모델을 옮길지 물어본다. 수 GB 라 창이 얼어붙지
않도록 별도 스레드에서 옮긴다
- windows_smoke 에 이 화면 렌더링 점검을 추가하고, 스크린샷을 호스트로
회수한다. 경로 표기와 디스크 조회는 OS 마다 다르게 도는 부분이다
개발이 리눅스에서 이뤄지다 보니 Windows 전용 경로는 한 번도 실행해 본 적이
없다. 오디오 캡처, 전역 단축키, 모니터 열거, exe 빌드가 전부 여기 해당하고,
"리눅스에서 다 통과했는데 윈도우에서 안 되더라"가 나올 수 있는 지점이다.
- .gitea/workflows/windows.yml
push 때마다 Windows 러너에서 테스트 -> 스모크 -> 포터블 exe 빌드 ->
빌드된 exe 기동 확인까지 하고 exe·스크린샷을 artifact 로 올린다.
의존성을 packaging/requirements-portable.txt 로 깔아 torch 없는 포터블
조건을 그대로 재현하고, torch 가 섞이면 바로 실패하게 했다.
- packaging/windows_smoke.py
리눅스에서 확인 불가능한 항목만 모은 점검기. WASAPI 장치 열거, 소리 내는
프로그램 열거, 전역 단축키 등록/해제, 실제 모니터 열거, GPU 인식,
사용 가능 티어, 자막 오버레이 실제 렌더링(스크린샷 저장).
한 항목이 실패해도 나머지를 계속 돌려 전체 그림을 한 번에 보여준다.
러너 환경에 따라 없을 수 있는 항목(오디오 장치·보조 exe·GPU)은 경고로만
두고, 코드 문제인 항목만 빌드를 실패시킨다.
- docs/WINDOWS-TESTING.md
러너 등록 절차와, 러너 없이 직접 돌려보는 방법.
러너가 없으면 워크플로는 대기만 하므로 지금 상태에 영향이 없다.
검증: pytest 189개 통과, ruff clean, 워크플로 YAML 파싱 확인,
스모크 스크립트를 리눅스에서 실행해 항목별 판정과 종료코드 확인
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
리뷰 지적(포터블에서 transformers 제외 -> NLLB 로드 실패)을 고치려고 실제
NLLB 를 받아 돌려봤고, 그 과정에서 용어집이 사실상 동작하지 않고 있었다는
것을 발견했다. 단위 테스트는 "모델이 자리표시자를 통과시킨다"는 틀린 전제
위에 서 있었다.
1) 포터블 패키징 (리뷰 지적)
transformers 를 제외 목록에서 빼고 hiddenimports 에 넣었다. NLLB
토크나이저가 AutoTokenizer 를 쓰기 때문이다. transformers 는 torch 가
없으면 토크나이저 전용 모드로 뜨며 그게 우리 용도와 정확히 맞는다.
torch 없는 환경에서 ctranslate2/transformers/faster-whisper import 와
앱 기동을 검증하는 test_portable.py 를 추가했다.
2) 자리표시자 형식 (실측으로 발견)
`⟦0⟧` 는 NLLB 가 괄호를 날려 생존률 0/3 이었다. 용어가 자막에서 그냥
사라지고 있었다 ("Third party incoming" -> "0 들어오는"). 후보 8종을
실제 모델로 비교해 `#0#` 로 교체 (3/3, 다중 4/5).
3) 소실 대비
모델이 문장 일부를 누락하면 자리표시자도 사라진다. 그대로 복원하면
용어가 증발하므로, 하나라도 없으면 보호 없이 재번역한다.
4) 서술어는 문장 전체일 때만 (whole_only)
절/서술어를 문장 중간에서 치환하면 문법이 무너진다.
before: "탄 필요해와 구급상자"
after : "탄약과 구급상자가 필요합니다"
해당 56개 항목을 whole_only 로 지정해 단독 발화일 때만 적용한다.
("Cover me!" -> "엄호해줘" 는 그대로 유지)
5) 조사 교정
역어 받침이 달라 "자기장를" 이 남던 것을 fix_particles() 로 고친다.
을/를, 이/가, 은/는, 과/와, (으)로 — 한글 코드에서 받침을 읽어 판정.
검증: pytest 189개 통과, ruff clean
실제 NLLB-600M(torch 없이 CPU)로 번역 품질 직접 확인
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
말투 (존댓말 기본, 반말 선택)
- 원문이 실제로 존댓말이면 반말 모드여도 존댓말을 지킨다. 상대가 정중하게
말했는데 자막이 반말이면 뉘앙스가 뒤집히기 때문.
- 영어·중국어는 문법적 높임이 없으므로 항상 고른 모드를 따른다.
"please" 를 존댓말 근거로 삼으면 오탐이 많아 쓰지 않았다.
- 낮춤 변환은 한글 자모를 분해해 실제 활용 규칙(모음조화 + 축약)을 구현했다.
오+아->와, 지+어->져, 하+어->해, 았/었 뒤는 항상 어, 치겠습니다->칠게.
어미를 나열하는 방식보다 훨씬 넓게 맞는다.
- 변환 방향은 존댓말->반말 한쪽만 한다. 번역 모델의 한국어 출력이 이미
격식체라 존댓말 모드는 손댈 필요가 없고, 반대 방향은 훨씬 자주 틀린다.
- 지시문을 이해하는 Qwen3 에는 프롬프트로도 전달한다. Seed-X 는 지시문을
못 알아듣는 모델이라 후처리로만 맞춘다.
포터블 exe
- PyInstaller 명세 + 빌드 스크립트. torch 를 의도적으로 제외했다.
torch+CUDA 만 2.5GB 라 onefile 로 묶으면 실행할 때마다 그걸 임시폴더에
푸느라 1분 넘게 걸려 쓸 수 없다.
- 음성인식(faster-whisper)도 번역(NLLB)도 CTranslate2 위에서 돌아 torch 가
필요 없다. 덕분에 1~3티어는 그대로 다 되고 exe 는 3GB -> 500MB 가 된다.
- 4~5티어는 못 쓰므로 tier_availability() 로 판정해 모델 화면에 '사용 불가'와
이유를 표시한다. torch 없는 환경에서 앱 전체가 뜨는 것을 확인했다.
AI 이미지
- SDXL-turbo 로 아이콘/배경 생성 (로컬 GPU, 피크 VRAM 1.9GB).
- 글자는 AI 가 제대로 못 쓰므로 아트만 AI 로 만들고 타이포그래피는
정확히 렌더링해 합성했다. icon.ico 는 16~256px 멀티해상도.
검증: pytest 163개 통과 (말투 53개 신규), ruff clean
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
리뷰 지적 2건과, 그것을 고치다 드러난 용어집 버그 1건을 함께 처리했다.
1) 저부하 모드 토글이 모델에 반영되지 않던 버그
ModelManager 는 생성 시점의 low_power 로 정밀도를 정하는데 설정에서
토글해도 갱신되지 않았고, stop() 은 모델을 내리지 않아 이전 정밀도
모델이 그대로 남았다. TranslationEngine.sync_performance() 를 추가해
값이 바뀌면 언로드 후 갱신하고, _bootstrap 과 재시작 경로에서 부른다.
2) 필수 게임 팩이 기본으로 꺼져 있던 문제
새 설치 기본값을 fps-common + 오버워치 + 배그 + R6 + 워독스로 켰다.
3) (1)(2)를 고치며 드러난 것: 팩 여러 개를 켜면 결과가 체크 순서에
휘둘렸다. knocked 가 FPS공통 "기절" / 배그 "눕혔다" 로 갈리는 식으로
19개 용어가 충돌하는데, 사용자는 그 순서를 볼 수도 바꿀 수도 없었다.
팩에 scope(generic/game) 를 두고 범용 -> 게임 전용 -> 사용자 항목
순으로 적용해 결정적으로 만들었다. 게임 팩끼리 갈리는 것은 사람이
판단할 문제라 find_conflicts() 로 뽑아 용어집 화면에 표시한다.
4) 용어 겹침 버그: "the blue"(조각)가 "blue zone" 보다 앞 위치라는
이유로 먼저 잡혀 "자기장 자기장" 이 됐다. 최장일치 정렬은 같은 시작
위치에서만 통한다. 조각 항목을 제거하고, 모든 팩을 켠 상태로 각
용어가 정확히 한 번만 치환되는지 검사하는 테스트를 추가했다.
검증: pytest 110개 통과 (저부하 6 + 팩 계층/충돌/겹침 12 신규), ruff clean
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
사용자가 필수로 지정한 게임 중 오버워치를 뺀 세 개가 빠져 있었다.
- 레인보우 식스 시즈 67개 — 보강·하드브리치·앵커/로머·디퓨저·런아웃
- 배틀그라운드 66개 — 자기장·어부·낙하·파밍·3렙헬멧·스쿼드 콜
- 워치독스 59개 — 데드섹·ctOS·블룸·알비온 등 조직명과 해킹/잠입 용어
- 에이펙스 레전드 45개 — 링·배너·리스폰 비컨 (등등 항목으로 추가)
워치독스는 멀티 콜아웃이 아니라 스토리 대사가 주라, 번역기가 특히 잘 틀리는
고유명사(조직명)와 해킹 용어 위주로 골랐다.
전용 팩이 생긴 만큼 'FPS 공통' 설명을 장르 공통 콜로 좁혔다.
필수 게임이 빠지면 바로 잡히도록 test_required_games_are_bundled 추가.
번들 용어 304개 → 541개 (10개 팩)
검증: pytest 92개 통과, ruff clean
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
이름을 Hearo → LiveSub 로 변경. 말장난보다 하는 일이 바로 보이는 쪽이 낫다.
자막 on/off
- 전역 단축키 Ctrl+Alt+S (자막) / Ctrl+Alt+D (번역) — Windows RegisterHotKey +
네이티브 이벤트 필터라 게임 창이 떠 있어도 동작. 다른 OS 에서는 no-op
- 단축키·체크박스·우클릭 메뉴가 모두 같은 경로를 타도록 통합
모니터 선택 + 디스코드식 배치
- placement.py: Qt 비의존 배치 계산 (모니터 목록 → 9분할 좌표)
- AnchorGrid 위젯으로 3x3 위치 선택, 드래그하면 자유 배치로 전환
- 모니터 구성이 바뀌어도 자막이 화면 밖으로 사라지지 않도록 클램프
GPU 저부하 모드 (기본 켜짐)
- 연산 정밀도 int8 강등, VRAM 상한 35%, 추론 후 60ms 양보,
300초 무음 시 모델 언로드, 중간 결과 비활성
- 기본 티어를 균형(6GB) → 신속(4GB) 으로 하향
- 8GB GPU 기준 자막 2.8GB / 게임 5.2GB
게임 용어집 번들 (304개)
- 롤 74 · 발로란트 59 · 오버워치2 49 · FPS공통 46 · 마크 38 · 방송 38
- 체크박스로 켜고 끄며, 사용자가 직접 등록한 항목이 항상 우선
검증: pytest 91개 통과 (배치 14 + 팩 12 + UI 6 신규), ruff clean
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
프로그램별 오디오를 캡처해 로컬 GPU에서 음성인식→번역하고 화면 위
자막으로 보여주는 데스크톱 앱. 한/영/일/중 4개 언어.
구성
- audio: WASAPI 프로그램별 캡처(C++ 보조 프로그램) + 장치 루프백 폴백,
적응형 VAD 발화 분할
- models: 속도~품질 5단계 티어, faster-whisper + CTranslate2/LLM 2백엔드,
용어집(플레이스홀더 보호 + 프롬프트 주입)
- core: Qt 비의존 파이프라인 엔진 (캡처/분할/추론 3스레드, 큐 연결)
- ui: 사이드바 5화면 + 무테두리 항상위 자막 오버레이, 자체 다크 테마
모델 선정 근거는 docs/MODELS.md, 추가학습 가능 여부와 방법은
docs/FINETUNING.md 참고.
검증: pytest 39개 통과 (GPU·오디오 장치 없이 실행), ruff clean
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>