요청 네 가지를 넣는다.
1) 자막 배경 여백(padding)
배경 상자를 창 전체가 아니라 **글자를 감싸는 크기**로 그리도록 바꿨다.
창 전체를 칠하던 동안에는 여백을 키워도 글자만 안으로 밀릴 뿐 배경
크기는 그대로여서, 설정 항목을 만들어도 의미가 없었다.
2) 평소 잠금 + '자막 위치 조정'
locked 기본값을 True 로 바꾼다. 게임 중에 자막을 실수로 끌고 다니는
사고를 막는 쪽이 기본이어야 한다. 자막 화면의 버튼으로 조정 모드를
열면 그때만 이동·네 모서리 크기 조절·휠이 열린다. 조정 중에는 클릭
통과를 강제로 끈다 — 켜둔 채로는 마우스가 창을 지나가 잡을 수 없다.
크기를 바꾸면 글자 크기가 높이 비율을 그대로 따라간다. 상자만 커지고
글자가 그대로면 여백만 늘어나 '자막을 키우는' 조작이 되지 않는다.
조정 모드는 저장하지 않는다. 다음 실행은 다시 잠긴 상태로 시작한다.
3) GPU 가 없을 때 CPU 추천
티어 표의 recommended 는 GPU 가 넉넉할 때를 전제한 값이라, 그대로
쓰면 GPU 없는 사람에게 10GB 짜리를 권하게 된다. 환경을 보고 고르는
recommended_tier() 를 만들고 배지도 그 결과로 붙인다. GPU 가 없으면
CTranslate2 계열만 후보에 두고 가장 가벼운 쪽을 권한다 — LLM 계열은
CPU 에서 한 문장에 수십 초가 걸려 실시간 자막이 되지 않는다.
'추천대로 맞추기' 버튼과, 실제로는 CPU 로 도는데 화면은 GPU 라고
표시하던 것도 같이 고친다. nvidia-smi 실패 문구가 명령줄을 통째로
토해내던 것도 사람이 읽을 문장으로 바꿨다.
4) 마이크 등 입력 장치 선택
InputDeviceCapture 를 추가하고 홈 화면에 목록 전환 버튼을 둔다.
두 목록을 한 번에 섞지 않는다 — 다 합치면 정작 찾으려던 게임이
라인입력들 사이에 파묻힌다. 버튼 글자는 '지금 상태'가 아니라
'누르면 가는 곳'을 적는다. 고른 쪽은 설정에 기억한다.
"아무 창이나 뜨면 통과" 로 둔 탓에, 자막 오버레이 창(제목이 앱 이름뿐이다)이
잡혀서 본 화면이 안 떠도 통과할 수 있었다. 지난 두 번의 오보가 전부 같은
형태였다 — 느슨한 통과 조건. 어떤 창을 기다리는지 호출하는 쪽이 지정하게
바꾼다. 첫 실행은 저장 위치 화면, 두 번째 실행은 본 화면 제목에만 있는
표어를 기다린다. 찾은 창 목록을 전부 기록해 나중에 되짚을 수 있게 한다.
onefile 의 부모는 압축을 푸는 부트로더일 뿐이고 창과 로그 파일을 쥐고 있는
것은 자식이다. 부모만 terminate 하니 자식이 livesub.log 를 계속 잡고 있어
임시 APPDATA 를 지울 때 PermissionError 가 났다. exe 의 문제가 아니라
점검이 뒷정리를 못 한 것인데 exe 기동 실패로 보고됐다.
- 프로세스 트리를 통째로 정리한다
- 임시폴더 삭제는 실패해도 점검을 실패시키지 않는다
- 두 번째 실행에서 저장 위치 화면이 또 뜨면 실패로 본다. "기억한다"는
약속이 깨진 것이므로 본 화면이 아닌 창은 통과가 아니다
포터블 exe 를 실행하면 창 대신 오류창만 떴다.
File "__main__.py", line 1, in <module>
ImportError: attempted relative import with no known parent package
PyInstaller 는 진입 스크립트를 **패키지 없이** 최상위로 실행한다
(__package__ 가 비어 있다). 그래서 src/livesub/__main__.py 첫 줄의
`from .app import main` 이 그 자리에서 죽는다. 절대 임포트로 바꾼다.
`python -m livesub` 는 둘 다 동작하므로 잃는 것이 없다.
왜 못 잡았나 — exe 점검이 "8초 뒤에도 프로세스가 살아 있으면 통과" 였다.
PyInstaller 오류창은 프로세스를 그대로 띄워둔 채 서 있으므로 통과로
보고됐다. 살아 있는 것은 떴다는 뜻이 아니다. 고친다:
- 프로세스 생존이 아니라 **실제 창**을 본다. onefile 은 자식 프로세스가
창을 띄우므로 프로세스 트리 전체를 훑는다.
- "Unhandled exception" / "Failed to execute script" 오류창을 보면
그 자리에서 실패로 판정한다.
- APPDATA 를 빈 폴더로 돌려 첫 실행(저장 위치 화면)과 두 번째 실행
(본 화면)을 각각 확인한다. 둘 다 실제 창 제목으로 확인한다.
- 같은 회귀를 리눅스에서도 잡도록 runpy 로 패키지 없는 실행을 재현하는
테스트를 추가한다. 되돌려보면 실제로 실패하는 것을 확인했다.
모델이 티어에 따라 2~17GB 를 차지하는데 %APPDATA% 에 박혀 있어서 C 드라이브가
좁은 사람은 손쓸 방법이 없었다. 처음 켤 때 한 번 묻고, 그 선택을 기억하고,
설정에서 바꿀 수 있게 한다.
닭과 달걀 문제가 하나 있다 — "어디에 둘지" 는 사용자가 정하는데 그 선택 자체는
고정된 곳에 적어둬야 다음 실행에 찾을 수 있다. 그래서 OS 표준 폴더에는 위치를
가리키는 쪽지(location.json) 한 장만 두고, 내용물은 전부 고른 폴더에 둔다.
- storage.py 신설: 쪽지 읽기/쓰기, 경로 검증, 여유 공간, 데이터 이동
- config.py 의 CONFIG_PATH 를 config_path() 로 바꾼다. import 시점 상수라
위치를 바꿔도 옛 경로를 계속 가리켰다
- 위치가 정해지기 전에는 파일 로그를 열지 않는다. 열면 고르지도 않은 기본
폴더가 만들어지고 거기에 로그가 남는다
- 이전 버전 사용자에게는 묻지 않는다. 기본 위치에 쓰던 흔적이 있으면 그대로
쓴다 — 같은 자리를 다시 고르는 헛수고를 시킬 이유가 없다
- 고른 디스크가 빠져도 죽지 않고 기본 위치로 물러난다
- 위치를 바꿀 때 받아둔 모델을 옮길지 물어본다. 수 GB 라 창이 얼어붙지
않도록 별도 스레드에서 옮긴다
- windows_smoke 에 이 화면 렌더링 점검을 추가하고, 스크린샷을 호스트로
회수한다. 경로 표기와 디스크 조회는 OS 마다 다르게 도는 부분이다
native\process_loopback\main.cpp 이 MSVC 에서 C2275 로 죽는다. WRL 의
FtmBase 는 플래그가 아니라 섞어 넣는 클래스인데 RuntimeClassFlags 안에
`ClassicCom | FtmBase` 로 넣어 타입을 값 자리에 썼다. 인터페이스 목록
쪽으로 옮긴다 (ApplicationLoopback 샘플과 같은 형태).
이 코드는 지금까지 한 번도 빌드된 적이 없다. 리눅스에서는 컴파일할
방법이 없었고, CI 의 네이티브 빌드 단계는 continue-on-error 였다.
win-ci VM 에 MSVC 를 깔고 나서야 드러났다.
livesub_capture.exe 가 없으면 포터블 빌드는 그대로 성공하되 프로그램별
캡처가 장치 전체 캡처로 조용히 폴백된다. 핵심 기능이 빠진 exe 가 나오는데
빌드는 PASS 로 보이는 조합이라 그대로 릴리스될 뻔했다.
- win-ci VM 에 VS 2022 Build Tools(C++)를 깐다. WASAPI 프로세스 루프백
헤더와 WRL 은 Windows SDK + MSVC 에만 있어서 cmake 만으로는 못 만든다.
- 검증 순서에 native_capture 단계를 windows_smoke 앞에 넣는다. 뒤에 두면
smoke 와 spec 이 둘 다 "없음"으로 통과해 버린다.
- pyinstaller 단계가 거짓 PASS 를 내던 것을 고친다. 앞 회차의 exe 가
실행 중이면 PermissionError 로 빌드가 죽는데, 남아 있는 옛 exe 때문에
Test-Path 검사는 통과했다. 빌드 전에 프로세스를 죽이고 build/dist 를
지우며 PyInstaller 종료코드도 확인한다.
- 통과한 exe 를 호스트로 회수한다. serve-host.py 는 32MB 상한이라 exe 를
413 으로 튕겼다. 상한을 올리고 조각으로 받아 디스크에 바로 쓰며,
.part 로 받아 완료 후 옮겨 반쪽 파일이 결과물로 남지 않게 한다.
- .gitignore 의 네이티브 산출물 경로가 예전 이름(hearo)이라 빌드한
exe 가 무시되지 않던 것을 livesub 로 고친다.
[Console]::OutputEncoding 을 UTF-8 로 두지 않으면 PowerShell 이 자식
프로세스 stdout 을 콘솔 코드페이지(LTSC en-US = 437)로 읽는다. python 쪽은
PYTHONUTF8=1 로 UTF-8 을 내보내니 한글이 전부 깨지고, 회수한 증거 파일의
실패 메시지를 읽을 수 없었다(실제로 첫 수집본의 한글 트레이스백이 그랬다).
docs/WINDOWS-TESTING.md 에 증거 채널(POST /artifact -> results/)과
sync.sh <sha> 재검증 절차를 적었다.
게스트 agent 는 stage2.ps1 의 해시가 바뀔 때만 다시 돈다. 그래서 커밋을
푸시해도 재검증이 안 걸렸고, 지금까지는 주석에 "rev2" 같은 문구를 넣어
해시를 억지로 바꿨다. 무엇을 검증한 결과인지도 모호했다.
stage2.ps1 에 $TargetRev 를 두고 `./sync.sh <sha>` 가 그 값을 박아서
배포한다. 검증 요청(= 리비전 지정)이 그대로 재실행 트리거가 되고,
움직이는 origin/main 대신 지정한 커밋을 reset --hard 로 고정해 본다.
VM 을 만든 이유가 "리눅스에서 통과한 게 Windows 에서 깨지는 것을 재현
가능하게 본다" 인데, 정작 실패 트레이스백이 호스트에 한 줄도 안 남았다.
로그 채널은 마지막 몇 줄만 흘렸고(Select-Object -Last 30), serial.log 는
VM 이 뜰 때마다 truncate 되고, guest.log 는 stage2 가 다시 돌 때만 채워진다.
그래서 실패 내용을 화면에서 읽어 옮겨 적는 상태였고 — 고친 뒤에 고쳐졌다는
것을 증명할 기준선이 없었다.
- serve-host.py: POST /artifact/<name> 추가. results/<name> 에 파일로 저장
(경로는 basename 으로만 취하고 32MB 상한)
- stage2.ps1: Step 이 각 단계의 출력 전문을 StreamWriter(AutoFlush)로 받아
호스트로 업로드한다. 중간에 예외로 죽어도 거기까지는 남는다.
출력 자르기를 없애고 pytest 는 --tb=long, 검증 리비전을 박은
summary.log 도 함께 올린다
- sync.sh: 스크립트만 /home/claude/win-ci 로 반영한다. build.sh 를 다시
돌리면 지워둔 Windows ISO 4.8GB 를 다시 내려받는다
첫 수집 결과(rev 162540b)로 실제 원인 두 개를 특정했다.
.gitignore 가 src/livesub/models 를 통째로 무시하던 동안 이 8개 파일
1,674줄은 커밋에서만 빠진 게 아니라 ruff 검사 범위에서도 빠져 있었다
(ruff 는 기본적으로 .gitignore 를 존중한다). 추적 대상이 되면서 처음
검사에 들어와 나온 2건이다.
같은 파일들에 대해 확인한 것: 프로젝트 룰셋 통과, 전체 룰(--select ALL)
감사에서 정확성 계열(F/E9/PLE) 위반 없음 — 남는 건 프로젝트가 켜지 않은
문서화/스타일 룰뿐, 7개 모듈 import 스모크 전부 통과, tests 10개 파일이
이미 models 를 임포트해 실행 범위에는 들어 있었다.
Windows 포터블 빌드가 dist\LiveSub.exe 를 만들지 못했다. 원인은
pyinstaller-hooks-contrib 의 hook-webrtcvad.py 가 copy_metadata('webrtcvad')
를 부르는 것이다. 우리가 설치하는 배포 이름은 webrtcvad-wheels 라
(원본 webrtcvad 는 Windows 휠이 없어 MSVC 컴파일이 필요하다)
PackageNotFoundError 가 나고, PyInstaller 는 훅 import 실패를
ImportErrorWhenRunningHook 으로 올려 빌드를 통째로 중단한다.
packaging/pyinstaller-hooks/hook-webrtcvad.py 로 같은 이름의 훅을 덮어써
실제 배포 이름으로 메타데이터를 수집한다. spec 의 hookspath 에 등록했으므로
build-portable.ps1, .gitea 워크플로, VM 자체 검증이 모두 같은 경로를 탄다.
리눅스에서 최소 재현으로 확인: 훅 없이는 같은 ImportErrorWhenRunningHook,
훅을 넣으면 "Processing standard module hook ... from packaging/pyinstaller-hooks"
로 우리 훅이 선택되고 빌드 성공, 만들어진 바이너리에서 webrtcvad.Vad(2) 생성까지 통과.
Windows CI 에서 test_segmenter.py 5개가 실패하고 로컬에서는 통과했다.
플랫폼 차이가 아니라 선택 의존성 차이였다.
테스트 입력은 220Hz 순음이다. `_is_speech` 는 에너지 판정과 webrtcvad 를
AND 로 묶는데, webrtcvad 는 사람 목소리로 학습돼 있어 순음을 발화로 보지
않는다. 포터블 exe 의 requirements 에 webrtcvad-wheels 가 들어 있으니
Windows CI 는 게이트가 켜진 환경이고, 개발 venv 에는 없어서 안 드러났다.
(개발 venv 에 webrtcvad-wheels 를 넣으면 리눅스에서도 5개가 똑같이,
같은 수치로 깨지는 것을 확인했다.)
- 이 파일이 검증하려는 건 분할 상태 기계이므로 autouse fixture 로
선택 의존성을 끊어 환경과 무관하게 같은 경로를 시험한다
- 게이트 동작 자체는 test_webrtcvad_gate_filters_pure_tone 이 따로 본다
(webrtcvad 없으면 skip)
- dev extras 에 webrtcvad-wheels 를 넣어 같은 환경 드리프트가 재발하지
않게 한다
리뷰에서 나온 두 가지 혼선을 문서로 못 박는다.
1) systemd-run 트랜지언트로 띄우면 /run 에만 남아 재부팅에 사라진다.
is-enabled 가 'transient' 로 보이면 영구 유닛이 아니라는 뜻이므로,
FragmentPath 가 ~/.config/... 를 가리키는지 확인하는 절차를 넣었다.
2) 설치 후 무엇을 지우는지 기준을 표로 박았다. 판단 기준은 'VM 이 도는 데
계속 필요한가' 하나다. Windows ISO(4.8GB)만 지우고, VM 에 물려 있는
virtio-win.iso 와 unattend.iso 는 남긴다.
같은 ISO 를 버전별로 여러 개 남기지 말라는 주의도 넣었다. 어느 것이 물려
있는지 헷갈리면 고친 줄 알았던 스크립트가 반영되지 않는다.
트랜지언트 유닛은 재부팅에 안 남는다. 러너 호스트가 재부팅마다 사라지면
CI 가 조용히 대기 상태가 되므로 영구 user 유닛으로 박는다 (linger=yes 라
로그인 없이도 올라온다). cgroup/CPU 상한은 트랜지언트 때와 동일하다.
stage2 에는 러너 등록 토큰 없이도 워크플로와 같은 단계를 VM 안에서 직접
돌리는 검증을 넣었다. 첫 실행에서 실제로 결함 하나를 잡았다.
`models/` 처럼 슬래시 없이 쓴 규칙은 깊이에 상관없이 같은 이름의 폴더를 전부
무시한다. 의도는 루트의 모델 캐시였는데 소스 패키지까지 같이 걸렸고, 그래서
asr/translator/manager/tiers/glossary/packs/speech_level 8개 파일이 한 번도
커밋된 적이 없다.
증상: Gitea 에서 clone 하면 `ModuleNotFoundError: No module named
'livesub.models'` 로 앱이 아예 임포트되지 않는다. 로컬 작업 트리에는 파일이
있으니 개발 중에는 절대 안 보인다. 방금 붙인 Windows CI VM 이 첫 검증에서
바로 잡아냈다.
/models/, /adapters/, /data/ 로 루트에 고정하고 빠진 파일을 추가한다.
실제 설치에서 드러난 것들이다. 셋 다 증상이 원인과 전혀 안 닮아서 기록해 둔다.
1) provision.ps1 이 1행부터 파싱 실패 - 'Missing closing }'
Windows PowerShell 5.1 은 BOM 없는 .ps1 을 ANSI(CP1252)로 읽는다.
한글 주석의 UTF-8 바이트가 CP1252 에서 스마트따옴표(0x93/0x94 = " ")로
변하고, 파서가 그걸 문자열 시작으로 보고 뒤를 전부 삼킨다.
-> build.sh 가 ISO 에 넣을 때 UTF-8 BOM 을 붙인다.
2) stage2.ps1 의 1행이 주석으로 인식되지 않음
agent 가 받은 본문에 BOM 이 있으면 Set-Content -Encoding UTF8 이 BOM 을
한 번 더 붙여 BOM 이 두 개가 된다. PowerShell 은 첫 개만 벗겨내고 남은
U+FEFF 때문에 '#' 이 줄 맨 앞이 아니게 된다.
-> agent 가 TrimStart(U+FEFF), 서버는 charset=utf-8 을 명시한다.
3) Git 설치가 'Cannot convert System.Object[] to System.String' 로 실패
Log 가 Write-Output 이라 함수 안에서 부르면 그 함수의 반환값에 로그가
섞인다. Get-File 이 배열을 돌려주니 Start-Process 가 죽었다.
-> Log 를 Write-Host 로 바꿨다.
덤으로 시리얼/HTTP 로그의 한글이 '?' 로 깨지던 것도 고쳤다 (SerialPort 기본
인코딩이 ASCII). 그리고 러너 토큰이 없어도 워크플로와 같은 단계를 VM 안에서
직접 돌리는 검증 절차를 stage2 에 붙였다.
Windows 설치 화면을 사람이 클릭할 수 없으므로 전 과정을 무인으로 짰다.
autounattend.xml 이 설치와 OOBE 를 끝내고, provision.ps1 이 virtio 드라이버를
오프라인으로 넣어 네트워크를 살리고, 상주 agent.ps1 이 호스트 HTTP 에서
stage2.ps1 을 받아 Git/pwsh7/Python/Node/CMake/act_runner 를 설치한다.
설치 내용을 ISO 에 박지 않고 호스트가 HTTP 로 내려주는 구조로 한 이유는
ISO 를 한 번 구우면 못 고쳐서, 한 줄 고칠 때마다 Windows 재설치가 되기
때문이다. stage2.ps1 만 고치면 게스트가 20초 안에 반영한다.
- 시스템 디스크는 SATA. virtio-blk 로 하면 Setup 이 디스크를 못 봐서
WinPE 드라이버 주입이 필요해진다
- VM 은 systemd --user 트랜지언트 유닛으로 띄워 봇 cgroup 밖에 둔다
(MemoryHigh=16G 안에 8G VM 이 들어가면 봇이 OOM 으로 죽는 전례)
- Node 포함. act_runner host 모드가 JS 액션을 PATH 의 node 로 돌린다
"VM 이 실제 윈도우와 다른가, 100% 부하에서도 자원이 괜찮은가" 에 대한 답.
앞선 측정은 한가할 때 값이라 부하를 걸어 다시 쟀고, 결론이 하나 바뀌었다.
이전에 "여유용으로 vCPU 12까지 가능" 이라고 적었는데 틀렸다. 늘리면 .5 의
운영 컨테이너 19개가 느려진다. 물리 호스트가 i7-11700 16스레드뿐이라
.9 의 지분이 커지는 만큼 .5 가 밀린다. .5 응답 속도 실측:
.9 한가할 때 616~628ms 기준
.9 가 4코어 사용 657~678ms +7% <- VM 100% 인 실제 상황, 무시 가능
.9 가 8코어 전부 1487~1491ms +140% <- 봇까지 동시에 바쁠 때
VM 4 vCPU 는 안전하다. .9 내부 영향도 작다 (파이프라인 9.8 -> 11.0초).
최악(8코어 전부)을 막으려면 CPUQuota=400% + CPUWeight=50 으로 띄운다.
CPUQuota=200% 로 재봤을 때 .5 응답이 649~653ms 로 기준선과 같았다.
RAM 은 최악을 다 더해도 21.6GB / 30GB 로 8.4GB 여유가 있다.
QEMU 오버헤드는 -m 8192 기준 VmSize 8.22GB 로 약 0.2GB.
VM 의 Windows 가 실제와 다른지도 정리했다. 같은 바이너리이고 WASAPI,
RegisterHotKey, 모니터 열거, PyInstaller 모두 코드 경로가 동일하다.
다른 건 GPU 없음, 실제 게임 소리 없음, 속도/지연 세 가지뿐이다.
부수 수확: virtio-vga,max_outputs=2 가 KVM 으로 기동되는 것을 확인했다.
--vga std 는 화면이 1개라 모니터 선택 기능을 검증할 수 없는데, 이걸 쓰면
화면 2개를 줄 수 있어 사용자가 요청한 모니터 선택 기능도 자동 검증 가능하다
(게스트에 virtio-gpu 드라이버 필요).
결론: CPU·RAM 모두 증설 없이 진행. RAM 만 원하면 38GB 까지 가능. CPU 는 금지.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
"VM 만들기 전에 CPU/RAM 을 얼마나 늘려줄까" 에 대한 답. 감이 아니라 재봤다.
코어는 4개에서 포화한다. 더 줘도 빨라지지 않는다.
파이프라인(음성->자막) 2코어 13초 / 4코어 10초 / 8코어 11초
테스트 190개 4코어 5.9초 / 8코어 5.8초
CI 작업 대부분이 단일 스레드고, 병렬인 음성인식도 4개에서 멈춘다.
물리 호스트는 i7-11700(8코어/16스레드) / RAM 62.6GB.
호스트 스펙은 Proxmox 에 SSH 가 안 되어 .5 LXC 를 통해 확인했다.
/proc/meminfo 는 lxcfs 가 컨테이너 한도로 가려서 16GB 로 보이지만
/sys/devices/system/node/node0/meminfo 는 가려지지 않아 실제 값이 나온다.
.9 는 그중 8 vCPU / 30GB 를 쓰고 있고, 실측상 증설이 거의 불필요하다.
ejclaw 최대 7.2GB (상한 16GB 는 여유분) + VM 8GB = 15.2GB / 30GB
코어 8개면 VM 4 + 봇 4 로 충분
결론: VM 은 vCPU 4 / RAM 8GB / 디스크 80GB. 호스트는 그대로 둬도 되고,
여유를 두려면 vCPU 12 / RAM 38GB 가 상한이다 (호스트 16스레드 중 .5 가
4개를 쓰고, 호스트 여유 RAM 이 페이지 캐시 포함 약 24GB 이므로).
.5 에서 운영 컨테이너 19개가 도는 점도 같이 적어뒀다.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
실제 GPU 로 검증을 돌리다 발견한 버그다. tier_availability() 가 torch 유무만
봐서, RTX 5050(7.5GB)에서도 정밀·극한 티어를 "사용 가능"이라고 답했다.
정밀(AWQ Int4) autoawq 가 있어야 로드된다. requirements.txt 에 없는
선택 의존성이라 대개 없다.
극한(Qwen3-8B) min_vram_gb=16. 7.5GB 카드에서는 영영 불가능하다.
둘 다 "고를 수 있다"고 해놓고 로드에서 죽는다. 사용자는 원인을 알 수 없다.
UI 에 min_vram_gb 경고 배지는 있었지만 경고만 하고 선택은 막지 않았다.
- tier_availability(tier, gpu=None) 에 autoawq·VRAM 검사 추가.
새 필드를 만들지 않고 이미 있던 min_vram_gb 를 쓴다.
free 가 아니라 total 로 본다 — free 로 막으면 다른 프로그램 때문에
"아까는 되던 티어가 지금은 안 보인다"가 된다.
CT2 티어(1~3)는 조기 반환이라 detect_gpu 를 아예 부르지 않는다.
(포터블에서 torch 도 GPU 도 없이 돌아야 하므로)
- GpuInfo 를 인자로 받아 루프에서 nvidia-smi 반복 호출을 막았다.
models_page 는 티어 카드 5개를 만들며 매번 조회하던 것을 __init__ 에서
한 번만 하도록 바꿨다. 빌더 함수의 부수효과에 의존하던 것도 제거.
- 점검기의 판정 기준도 고쳤다. 기존엔 "4·5티어가 전부 열려야 통과"였는데
VRAM 작은 GPU 에서 막히는 건 정상이라 오탐이었다. 이제 게이트는 판정
없이 보고만 하고, "게이트가 된다고 한 티어가 실제로 올라가는지" 가
판정한다. 게이트가 전부 막았으면 검증 대상이 없다는 사실을 남기고
통과시킨다 — 하드웨어 한계이지 회귀가 아니다.
- scripts/gpu-check.sh 추가. CTranslate2 가 libcublas.so.12 를 직접 찾는데
torch 의 nvidia 패키지 안에 있어 LD_LIBRARY_PATH 가 필요하다. 없으면
GPU 번역만 조용히 실패한다. 스크립트가 경로를 자동으로 잡는다.
검증: pytest 190 passed (신규 4개 중 3개는 torch 필요 -> gpu-venv 에서
실제로 실행해 9 passed 확인), ruff clean,
RTX 5050 에서 scripts/gpu-check.sh 6/6 통과 (종료코드 0)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
"Proxmox 말고 .9 안에서 Windows VM 을 돌릴 수 없나" 에 대한 답.
가능하다. 주장이 아니라 실제로 띄워서 확인했다.
/dev/kvm 존재 / kvm_intel nested=Y
qemu -enable-kvm -> "kvm support: enabled"
여유 RAM 25GB, 디스크 294GB (8G VM 수용 가능)
가장 큰 이점은 성능이 아니라 **사람 손이 안 간다는 것**이다. .9 안이라
에이전트가 VM 생성부터 러너 등록까지 직접 할 수 있고 Proxmox 자격증명이
필요 없다. 지금까지 막혀 있던 지점이 정확히 그거였다.
다만 GPU 문제는 전혀 해결되지 않는다.
IOMMU 그룹 0개, 커널 cmdline 에 iommu 옵션 없음 -> 중첩 패스스루 불가
되게 하려면 Proxmox 가 vIOMMU 를 열고 .9 커널에 intel_iommu=on 을 넣고
GPU 를 vfio-pci 에 묶어야 하는데, 그러면 .9 가 GPU 를 잃는다. 결국
"GPU 를 누가 가질 것인가" 로 되돌아온다.
운영상 함정 하나를 실측으로 확인해 문서에 박아뒀다. 에이전트 턴에서 그냥
qemu 를 띄우면 VM 이 ejclaw.service cgroup(MemoryHigh=16G, 평시 5G 사용)
안에서 돌아 8G VM 이 상한을 치고 봇을 OOM 으로 죽인다(전례 있음).
systemd-run --user --scope 를 쓰면 app.slice 형제 scope 로 빠지는 것을
확인했다 — 독립 상한이 실제로 적용됨.
A(.9 중첩) vs B(Proxmox) 비교표 추가. CI 타임아웃 30분에 파이프라인이
27초라 중첩 오버헤드는 문제되지 않으므로 A 를 먼저 권한다.
부수 변경(이 저장소 밖): .9 에 qemu-system-x86/ovmf 설치, claude 계정을
kvm 그룹에 추가. /dev/kvm 이 root:kvm 0660 이라 권한이 없으면 qemu 가
"Could not access KVM kernel module: Permission denied" 로 죽는다.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
러너를 여러 프로젝트가 공유한다는 전제에서 두 가지 구멍이 있었다.
1) 동시 실행 — GPU 는 나눠 쓸 수 없다. 두 작업이 같이 VRAM 을 잡으면
둘 다 OOM 이거나, 더 나쁘게는 하나가 조용히 느려져 원인을 못 찾는다.
concurrency(같은 저장소) + 러너 capacity:1(저장소 무관, 진짜 보장)을
두 겹으로 두고, 문서에 capacity 확인을 필수 절차로 넣었다.
한 대에 러너 프로세스를 여러 개 띄우면 안 된다는 것도 명시.
2) 티어 검증이 주장보다 얕았다 — "4·5티어 검증"이라 해놓고 실제로는
tier_availability() 만 봤고, 그 함수는 torch 유무만 본다. 즉
- 4티어(Seed-X AWQ Int4)는 autoawq 가 없어도 "사용 가능"
- 5티어(Qwen3-8B)는 VRAM 11GB 가 필요한데 8GB 카드에서도 "사용 가능"
게이트가 통과인데 실제로는 못 올리는 상태를 CI 가 못 잡고 있었다.
그래서 게이트 검사는 얕은 검사임을 이름과 주석에 명시하고, 그 옆에
**진짜로 모델을 올려 번역까지 하는** 항목을 새로 뒀다. 러너 GPU 가
무엇일지 모르므로 여유 VRAM 과 autoawq 유무로 후보를 거른 뒤 올릴 수
있는 것 중 가장 무거운 티어를 고르고, 건너뛴 티어와 그 이유를 결과에
남긴다. 하나도 못 올리면 실패다 (조용히 통과하지 않는다).
게이트는 통과인데 실제 로드가 실패하면 그게 찾아야 할 버그다 — 앱이
사용자에게 "쓸 수 있다"고 하고선 못 올리는 상황. 러너가 붙으면 실제
증거를 보고 tier_availability() 를 고칠지 판단한다.
- autoawq 설치 단계 추가(4티어 전용, Windows 휠이 없을 때가 있어 선택)
- 1~3티어 CUDA 경로 번역 검사는 유지하고 unload 를 finally 로 보장
검증: pytest 189개 통과, ruff clean, 워크플로 파싱·concurrency 확인,
--gpu 를 torch 없는 환경에서 실행해 6개 중 5개가 의도대로 실패
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GPU 는 한 VM 만 독점한다 — 컨슈머 NVIDIA 는 vGPU/SR-IOV 를 지원하지 않아
쪼개 쓸 방법이 없고, 지금 RTX 5050 은 .9(리눅스)에 물려 있다. 따라서 GPU
없는 상시 CI VM 에서는 CUDA·VRAM·4~5티어를 영영 확인할 수 없다.
코드로 우회할 수 없는 제약이므로, 대신 **나중에 GPU 머신이 생겼을 때 그대로
붙도록** 구조를 지금 잡아뒀다. 이 러너를 다른 프로젝트 GPU 테스트에도 쓸
계획이라면 이 구분이 그쪽에도 그대로 적용된다.
windows:host GPU 없는 서버 VM main push 마다 자동
테스트·Windows 경로·오디오→자막·포터블 exe
windows-gpu:host GPU 달린 머신 수동 실행만
CUDA 인식·VRAM 상한·4·5티어·GPU 번역
- windows_smoke.py --gpu
GPU 있는 머신에서만 의미 있는 항목만 모았다. CUDA 가용성, 앱의 GPU 인식,
VRAM 상한이 실제로 걸리는지, torch 필요 티어(4·5)가 열리는지, GPU 로 실제
번역까지. 다른 모드와 달리 **GPU 부재를 치명적 실패로 처리**한다 —
GPU 를 확인하겠다고 부른 작업이 조용히 통과하면 안 되기 때문이다.
- .gitea/workflows/windows-gpu.yml
수동 실행 전용. 러너가 없으면 큐에 쌓이기만 하고 아무 일도 없다.
포터블 CI 와 달리 torch CUDA 빌드를 설치하고, 설치 직후 CPU 빌드가
깔리지 않았는지 확인한다. nvidia-smi 가 없으면 그 자리에서 멈춘다.
- 문서에 두 러너 구조와 GPU 머신 확보 3가지 방안(서버에 GPU 추가 /
.9 에서 옮기기 / 필요할 때만 실물 PC) + 슬롯 확인 명령 추가.
검증: pytest 189개 통과, ruff clean, 두 워크플로 파싱·라벨·트리거 확인,
--gpu 를 torch 없는 환경에서 실행해 의도대로 실패(종료코드 1)하는지 확인
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
RTX 5050 은 PCIe 패스스루로 이 VM(.9)이 독점하므로 Proxmox 에 Windows VM 을
띄워도 GPU 를 못 준다. 그래서 "Windows 검증에 GPU 가 정말 필요한가"를 실제로
재봤다 — GPU 를 완전히 가린 채 점검기를 돌린 결과, 실패한 건 'GPU 인식' 하나뿐이고
그건 이미 경고 처리라 빌드를 막지 않는다. 나머지(단축키·모니터·오디오 열거·
exe 빌드·기동·오버레이 렌더링)는 전부 통과한다. 즉 GPU 없는 서버 VM 으로 충분하고,
CUDA/VRAM 관련은 GPU 가 물려 있는 .9 에서 이미 검증 중이다.
- tests/fixtures/en_callout_16k.wav (139KB)
MeloTTS 로 만든 4.4초 영어 콜아웃. CI 가 실제 음성으로 검증할 수 있게 커밋.
- windows_smoke.py --pipeline
오디오 파일 -> 음성인식 -> 번역 -> 한국어 자막까지 끝까지 확인한다.
부품이 다 통과해도 이어 붙이면 안 되는 경우를 잡기 위한 것.
CPU + 가장 가벼운 티어로 돌려 GPU 없는 VM 에서도 그대로 된다.
실측: 27초, 'Enemy coming from the left, fall back now.'
-> '적 왼쪽에서 오는, 지금 뒤로 물러서.'
음성인식 결과에 핵심 단어가 있는지, 자막에 한글이 있는지까지 검사한다.
- 워크플로에 해당 단계 추가 (30분 타임아웃, 첫 회만 모델 다운로드)
- docs/WINDOWS-TESTING.md 에 서버 VM 구축 절차
Proxmox qm create 예시와 함께, 빠뜨리면 조용히 실패하는 두 가지를 명시:
(1) --audio0 없으면 오디오 장치가 안 잡혀 WASAPI 점검이 실패
(2) 자동 로그온이 없으면 데스크톱 세션이 없어 GUI/스크린샷이 전부 실패
(러너를 서비스로 돌리면 안 되는 실질적 이유이기도 하다)
검증: pytest 189개 통과, ruff clean, 워크플로 11스텝 파싱 확인(bash 0개),
GPU 숨긴 조건에서 점검기 실행, --pipeline 실제 통과
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Windows 호스트 러너에서 bash.exe 는 기본 PATH 에 없다 (Git for Windows 는
Git\cmd 만 PATH 에 등록하고 bash.exe 는 Git\bin 에 있다). OS 확인 단계가
첫 스텝이라 러너를 붙이자마자 "bash not found" 로 죽는다. pwsh 로 바꿨다.
같은 이유로 문서 설치 목록에 Git.Git 과 Microsoft.PowerShell 을 추가했다.
나머지 7개 스텝이 전부 pwsh(7.x) 를 쓰는데 Windows 기본 탑재는 5.1 뿐이라
pwsh 없이는 단계마다 실패한다.
트리거 설명도 실제 on: 설정(main push / workflow_dispatch)에 맞춰 고쳤다.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
리뷰 지적을 확인해보니 두 건은 실제 결함이었다.
1) 점검기가 화면 없는 환경에서 그냥 죽음 (SIGABRT, 종료코드 134)
QT_QPA_PLATFORM 을 "windows" 로 하드코딩해 둔 탓에, 표시할 화면이 없으면
첫 QApplication 생성에서 플러그인 로드에 실패하며 아무 결과도 못 남기고
abort 했다. 점검기가 그렇게 죽으면 존재 이유가 없다.
- 화면이 없으면 offscreen 으로 떨어지게 하고, 이 판단을 Qt import 전에 한다
- QApplication 생성을 _qt_app() 한 곳으로 모음
- 실행 플랫폼을 출력해 리눅스에서 돌아간 경우가 바로 보이게 함
이제 환경변수 없이 돌려도 항목별 판정과 종료코드가 정상으로 나온다.
2) 보안 설명이 과장돼 있었음 — 가장 중요한 수정
"커밋된 워크플로만 실행하니 임의 명령은 불가"라고 썼는데, 나는 이 저장소
main 에 push 할 수 있으므로 워크플로를 고쳐 커밋하면 그 PC에서 원하는
명령을 돌릴 수 있다. 개인 PC를 내주는 판단에 쓰일 설명을 틀리게 준 것이라
그대로 바로잡았다. 실제로 보장되는 것(숨길 수 없음·언제든 끌 수 있음·
계정 권한으로만 동작)만 적고, VM 또는 전용 계정을 권하도록 바꿨다.
서비스 등록(SYSTEM 권한) 안내도 로그온 작업(계정 권한)으로 교체 —
"관리자로 띄우지 말라"는 권고와 모순됐다.
3) 라벨 오타 안전장치 (공식 문서 확인 중 발견)
Gitea 는 runs-on 이 어떤 라벨과도 안 맞으면 실패시키지 않고 기본 우분투
컨테이너에서 돌린다. Windows 검증이 조용히 리눅스에서 돌아가 "통과"로
보일 수 있어, 워크플로 첫 단계에서 RUNNER_OS 를 확인해 막는다.
문서의 등록 라벨도 windows:host 로 명시 (생략 시 기본값이 host 이지만
명시가 낫다 — docs.gitea.com/runner/labels).
4) 트리거 설명 정정: "아무거나 push" -> main push 또는 수동 실행.
검증: pytest 189개 통과, ruff clean, 워크플로 트리거/스텝 파싱 확인,
QT_QPA_PLATFORM 없이 점검기 실행해 abort 없이 종료코드 1 확인
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
개발이 리눅스에서 이뤄지다 보니 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>
4티어(Seed-X-PPO-7B) 번역 경로가 모델 카드 요구사항을 어기고 있었다.
- 프롬프트 끝의 `<ko>` 등 대상 언어 태그가 빠져 있었다. PPO 학습에 쓰인
신호라 없으면 번역 품질이 흔들린다.
- Seed-X 는 chat template 없는 번역 전용 completion 모델인데 "구어체로
자연스럽게" 같은 지시문 래퍼를 씌우고 있었다. 학습 분포를 벗어난다.
- 그 결과 MTSpec.supports_prompt_glossary=True 가 사실과 달랐다.
Seed-X 는 용어집 지시문을 이해하지 못하므로 플레이스홀더 치환을 써야 한다.
수정
- PromptStyle(NONE/SEEDX/INSTRUCT) 도입, supports_prompt_glossary 를
prompt_style 에서 파생시켜 둘이 어긋날 수 없게 함
- build_seedx_prompt() 로 모델 카드 형식을 분리 (지시문 주입 불가)
- INSTRUCT 경로는 chat template 사용, Qwen3 thinking 모드는 끔
- LANGUAGES 에 seedx 태그 명시
- finetune_mt.py 가 티어의 prompt_style 을 따라가게 해 학습/추론 프롬프트 일치
- AWQ Int4 로드 실패 시 autoawq 설치 안내를 담은 오류 메시지
검증: pytest 52개 통과 (프롬프트 회귀 테스트 13개 추가), ruff clean
근거: https://huggingface.co/ByteDance-Seed/Seed-X-PPO-7B
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>