diff --git a/.gitea/workflows/windows.yml b/.gitea/workflows/windows.yml index 466e46b..d7405de 100644 --- a/.gitea/workflows/windows.yml +++ b/.gitea/workflows/windows.yml @@ -75,6 +75,16 @@ jobs: PYTHONPATH: src run: .\.venv-ci\Scripts\python packaging\windows_smoke.py + # 부품이 다 통과해도 이어 붙이면 안 되는 경우가 있다. 실제 음성 파일을 + # 넣어 한국어 자막이 나오는지 끝까지 확인한다. CPU 로 돌므로 GPU 없는 + # 서버 VM 에서도 그대로 된다. 모델은 처음 한 번만 받고 이후 캐시를 쓴다. + - name: 전 파이프라인 (오디오 -> 자막) + shell: pwsh + timeout-minutes: 30 + env: + PYTHONPATH: src + run: .\.venv-ci\Scripts\python packaging\windows_smoke.py --pipeline + - name: 프로그램별 캡처 보조 프로그램 빌드 shell: pwsh continue-on-error: true # VS Build Tools 가 없는 러너도 있을 수 있다 diff --git a/docs/WINDOWS-TESTING.md b/docs/WINDOWS-TESTING.md index 245d0e4..2c83e0b 100644 --- a/docs/WINDOWS-TESTING.md +++ b/docs/WINDOWS-TESTING.md @@ -7,6 +7,62 @@ Windows 러너를 하나 붙이면 `main` 에 push할 때(또는 Actions 탭에서 수동 실행할 때) 이 경로들이 자동으로 검증됩니다. +## 어디에 올릴까 — 서버 Windows VM 권장 + +개인 PC를 안 쓰고 서버에 올려 완전 자동화하는 게 가장 깔끔합니다. +**GPU가 없어도 됩니다.** 실제로 확인해봤습니다. + +| 검증 항목 | GPU 필요? | GPU 없는 서버 VM | +|---|---|---| +| 단위 테스트 189개 | ❌ | ✅ | +| WASAPI 장치·프로그램 열거 | ❌ | ✅ | +| 전역 단축키 | ❌ | ✅ | +| 모니터 열거·자막 배치 | ❌ | ✅ | +| 포터블 exe 빌드·기동 | ❌ | ✅ | +| **오디오 → 한국어 자막 (전 과정)** | ❌ CPU로 됨 | ✅ | +| CUDA 인식·VRAM 상한 | ✅ | ⚠️ 경고만 (빌드 통과) | + +GPU가 필요한 건 마지막 한 줄뿐이고, 그건 **이 서버(.9, 리눅스)에 RTX 5050이 +물려 있어 이미 거기서 검증**하고 있습니다. GPU는 한 VM만 독점할 수 있으니 +Windows VM에는 안 주는 게 맞습니다. + +### Proxmox에 Windows VM 만들기 + +```bash +# Proxmox 호스트에서 (VMID 는 비어 있는 번호로) +qm create 920 --name win-ci --memory 8192 --cores 4 --net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-single --scsi0 local-lvm:80 --ostype win11 --machine q35 --bios ovmf --efidisk0 local-lvm:1,efitype=4m,pre-enrolled-keys=1 --tpmstate0 local-lvm:1,version=v2.0 --ide2 local:iso/Win11.iso,media=cdrom --ide0 local:iso/virtio-win.iso,media=cdrom --audio0 device=ich9-intel-hda,driver=none --vga std +``` + +RAM 8GB / 디스크 80GB면 충분합니다(모델 캐시 포함). 아래 둘을 꼭 넣으세요. + +- `--audio0 ...` — **없으면 오디오 장치가 하나도 안 잡혀** WASAPI 점검이 실패합니다 +- `--vga std` — 화면이 있어야 자막 오버레이를 그리고 스크린샷을 찍습니다 + +### VM 안에서 꼭 해야 하는 두 가지 + +**1. 자동 로그온** — 러너가 서비스로 돌면 데스크톱 세션이 없어 GUI 테스트와 +스크린샷이 전부 실패합니다. 자동 로그온 + 로그온 시 실행이어야 합니다. + +```powershell +# 관리자 PowerShell (VM 안이므로 부담 적음) +$k = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" +Set-ItemProperty $k AutoAdminLogon 1 +Set-ItemProperty $k DefaultUserName "ci" +Set-ItemProperty $k DefaultPassword "<비밀번호>" +``` + +**2. 화면 꺼짐·잠금 끄기** — 잠기면 GUI 테스트가 실패합니다. + +```powershell +powercfg /change monitor-timeout-ac 0 +powercfg /change standby-timeout-ac 0 +``` + +그 다음은 아래 일반 설치 절차와 같습니다. 러너는 **서비스가 아니라 로그온 +예약 작업**으로 등록하세요(아래 "항상 켜두고 싶다면" 참고). + +--- + ## 무엇이 자동으로 확인되나 | 항목 | 리눅스에서 | Windows 러너에서 | diff --git a/packaging/windows_smoke.py b/packaging/windows_smoke.py index 2253304..251c5ab 100644 --- a/packaging/windows_smoke.py +++ b/packaging/windows_smoke.py @@ -160,6 +160,76 @@ def run_source_checks() -> None: return "스크린샷 2장 저장 (artifacts/)" +# --- 전 파이프라인 점검 ----------------------------------------------------- + + +def run_pipeline_check(timeout_s: int = 900) -> None: + """오디오 파일 하나를 넣어 한국어 자막이 나오는지 끝까지 본다. + + 개별 부품이 다 통과해도 이어 붙이면 안 되는 경우가 있다. 이 점검이 + 통과하면 "음성인식 -> 번역 -> 자막" 이 실제로 동작한다는 뜻이다. + + GPU 없이 CPU 로 돌린다. 서버의 GPU 없는 Windows VM 에서도 그대로 된다. + 모델은 처음 한 번만 받고 그 뒤로는 캐시를 쓴다. + """ + print(f"\n=== 전 파이프라인 (오디오 -> 자막), 플랫폼: {sys.platform} ===") + + @check("오디오 파일 -> 한국어 자막") + def _pipeline(): + from livesub.config import AppConfig + from livesub.core.engine import TranslationEngine + + clip = ROOT / "tests" / "fixtures" / "en_callout_16k.wav" + if not clip.is_file(): + raise RuntimeError(f"테스트 음성이 없습니다: {clip}") + + cfg = AppConfig() + cfg.models.tier = "lightning" # CI 에서는 가장 가벼운 티어로 충분하다 + cfg.models.device = "cpu" # GPU 없는 VM 에서도 돌아야 한다 + cfg.models.preload_on_start = True + cfg.audio.backend = "file" + cfg.audio.target_process_name = str(clip) + cfg.audio.silence_ms = 400 + cfg.performance.game_mode = False + + lines: list = [] + errors: list[str] = [] + + def on_status(status) -> None: + if status.state.value == "error": + errors.append(status.message) + + engine = TranslationEngine(cfg, on_line=lines.append, on_status=on_status) + engine.start() + deadline = time.time() + timeout_s + try: + while time.time() < deadline and not errors: + if any(line.is_final for line in lines): + break + time.sleep(0.5) + finally: + engine.shutdown() + + if errors: + raise RuntimeError(f"엔진 오류: {errors[0]}") + finals = [line for line in lines if line.is_final] + if not finals: + raise RuntimeError(f"{timeout_s}초 안에 자막이 나오지 않았습니다") + + line = finals[0] + if "enemy" not in line.source_text.lower(): + raise RuntimeError(f"음성인식이 빗나갔습니다: {line.source_text!r}") + if not any("\uac00" <= ch <= "\ud7a3" for ch in line.translated_text): + raise RuntimeError(f"한국어 자막이 아닙니다: {line.translated_text!r}") + + ARTIFACTS.mkdir(exist_ok=True) + (ARTIFACTS / "pipeline.txt").write_text( + f"원문: {line.source_text}\n자막: {line.translated_text}\n", + encoding="utf-8", + ) + return f"{line.source_text!r} -> {line.translated_text!r}" + + # --- 빌드된 exe 점검 -------------------------------------------------------- @@ -195,10 +265,16 @@ def run_exe_check(exe: Path) -> None: def main() -> int: parser = argparse.ArgumentParser(description="Windows 전용 점검") parser.add_argument("--exe", type=Path, help="빌드된 exe 를 점검") + parser.add_argument( + "--pipeline", action="store_true", + help="오디오 파일로 전 파이프라인 점검 (모델을 내려받으므로 느립니다)", + ) args = parser.parse_args() if args.exe: run_exe_check(args.exe) + elif args.pipeline: + run_pipeline_check() else: run_source_checks() diff --git a/tests/fixtures/en_callout_16k.wav b/tests/fixtures/en_callout_16k.wav new file mode 100644 index 0000000..1ab912f Binary files /dev/null and b/tests/fixtures/en_callout_16k.wav differ