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 마다 다르게 도는 부분이다
실제 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>
러너를 여러 프로젝트가 공유한다는 전제에서 두 가지 구멍이 있었다.
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>
리뷰 지적을 확인해보니 두 건은 실제 결함이었다.
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>