feat(music-lab): 영상 인코딩을 Windows GPU 서버로 오프로드

- pipeline/video.py 재작성: subprocess.run 제거, httpx로 Windows /encode_video 호출
- Windows 서버 다운 시 즉시 VideoGenerationError (NAS 로컬 폴백 X — 의도적 결정)
- /app/data/* → /volume1/docker/webpage/data/* 경로 변환 (_container_to_nas)
- 테스트는 respx mock 기반으로 교체 (6개)
This commit is contained in:
2026-05-09 02:01:34 +09:00
parent bb0b0dff25
commit 240bd38541
2 changed files with 157 additions and 59 deletions

View File

@@ -3,6 +3,10 @@ import pytest
from unittest.mock import patch, MagicMock
from app.pipeline import video, thumb, storage
import respx
import httpx
from httpx import Response
@pytest.fixture
def tmp_storage(monkeypatch, tmp_path):
@@ -17,31 +21,6 @@ def tmp_storage(monkeypatch, tmp_path):
return tmp_path
@patch("subprocess.run")
def test_generate_video_calls_ffmpeg(mock_run, tmp_storage):
mock_run.return_value = MagicMock(returncode=0, stderr="")
out = video.generate(pipeline_id=50, audio_path=str(tmp_storage / "audio.mp3"),
cover_path=str(tmp_storage / "50" / "cover.jpg"),
genre="lo-fi", duration_sec=120, resolution="1920x1080",
style="visualizer")
assert out["url"].endswith("/50/video.mp4")
assert out["used_fallback"] is False
args = mock_run.call_args[0][0]
assert args[0] == "ffmpeg"
assert "-i" in args
assert "showwaves" in " ".join(args)
@patch("subprocess.run")
def test_generate_video_failure_marks_failed(mock_run, tmp_storage):
mock_run.return_value = MagicMock(returncode=1, stderr="bad codec")
with pytest.raises(video.VideoGenerationError):
video.generate(pipeline_id=51, audio_path=str(tmp_storage / "audio.mp3"),
cover_path=str(tmp_storage / "50" / "cover.jpg"),
genre="lo-fi", duration_sec=120, resolution="1920x1080",
style="visualizer")
@patch("subprocess.run")
def test_thumb_extracts_frame(mock_run, tmp_storage):
mock_run.return_value = MagicMock(returncode=0, stderr="")
@@ -64,3 +43,90 @@ def test_thumb_failure_raises(mock_run, tmp_storage):
with pytest.raises(thumb.ThumbGenerationError):
thumb.generate(pipeline_id=61, video_path=str(video_path),
track_title="X", overlay_text=False)
# ===== Video tests (replacing the FFmpeg-based tests) =====
@pytest.fixture
def encoder_env(monkeypatch):
monkeypatch.setattr(video, "ENCODER_URL", "http://192.168.45.59:8765")
monkeypatch.setattr(video, "NAS_VIDEOS_ROOT", "/volume1/docker/webpage/data/videos")
monkeypatch.setattr(video, "NAS_MUSIC_ROOT", "/volume1/docker/webpage/data/music")
@respx.mock
def test_generate_video_calls_remote_encoder(encoder_env, tmp_path, monkeypatch):
monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path))
respx.post("http://192.168.45.59:8765/encode_video").mock(
return_value=Response(200, json={
"ok": True, "duration_ms": 12000,
"output_path_nas": "/volume1/docker/webpage/data/videos/3/video.mp4",
"output_bytes": 28000000,
"encoder": "h264_nvenc", "preset": "p4",
})
)
out = video.generate(
pipeline_id=3,
audio_path="/app/data/1c695df3.mp3",
cover_path="/app/data/videos/3/cover.jpg",
genre="lo-fi", duration_sec=120, resolution="1920x1080",
style="visualizer",
)
assert out["url"].endswith("/3/video.mp4")
assert out["used_fallback"] is False
assert out["encode_duration_ms"] == 12000
@respx.mock
def test_generate_video_raises_on_connection_error(encoder_env, monkeypatch, tmp_path):
monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path))
respx.post("http://192.168.45.59:8765/encode_video").mock(
side_effect=httpx.ConnectError("Connection refused")
)
with pytest.raises(video.VideoGenerationError) as exc:
video.generate(
pipeline_id=4,
audio_path="/app/data/x.mp3", cover_path="/app/data/videos/4/cover.jpg",
genre="lo-fi", duration_sec=120, resolution="1920x1080",
)
assert "연결 실패" in str(exc.value) or "Connection" in str(exc.value)
@respx.mock
def test_generate_video_raises_on_500(encoder_env, monkeypatch, tmp_path):
monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path))
respx.post("http://192.168.45.59:8765/encode_video").mock(
return_value=Response(500, json={"detail": {"ok": False, "stage": "ffmpeg", "error": "bad codec"}})
)
with pytest.raises(video.VideoGenerationError) as exc:
video.generate(
pipeline_id=5,
audio_path="/app/data/x.mp3", cover_path="/app/data/videos/5/cover.jpg",
genre="lo-fi", duration_sec=120, resolution="1920x1080",
)
assert "Windows 인코더 오류" in str(exc.value)
assert "ffmpeg" in str(exc.value)
def test_generate_video_no_url_configured(monkeypatch, tmp_path):
monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path))
monkeypatch.setattr(video, "ENCODER_URL", "")
with pytest.raises(video.VideoGenerationError) as exc:
video.generate(
pipeline_id=6,
audio_path="/app/data/x.mp3", cover_path="/app/data/videos/6/cover.jpg",
genre="lo-fi", duration_sec=120, resolution="1920x1080",
)
assert "WINDOWS_VIDEO_ENCODER_URL" in str(exc.value)
def test_container_to_nas_videos_path(monkeypatch):
monkeypatch.setattr(video, "NAS_VIDEOS_ROOT", "/volume1/docker/webpage/data/videos")
monkeypatch.setattr(video, "NAS_MUSIC_ROOT", "/volume1/docker/webpage/data/music")
assert video._container_to_nas("/app/data/videos/3/cover.jpg") == "/volume1/docker/webpage/data/videos/3/cover.jpg"
def test_container_to_nas_music_path(monkeypatch):
monkeypatch.setattr(video, "NAS_VIDEOS_ROOT", "/volume1/docker/webpage/data/videos")
monkeypatch.setattr(video, "NAS_MUSIC_ROOT", "/volume1/docker/webpage/data/music")
assert video._container_to_nas("/app/data/abc.mp3") == "/volume1/docker/webpage/data/music/abc.mp3"