1. video.py _container_to_nas, orchestrator.py _local_path에서 path 변환 전 ?쿼리 strip — 이전 commit 20c5268의 cache-buster ?v=...가 Windows path로 그대로 전달되어 input_validation 실패하던 문제 픽스 2. cover.py _generate_with_dalle가 background_keyword를 prompt에 포함 — 사용자가 PipelineStartModal에서 '배경 키워드' 입력 시 처음부터 원하는 분위기 cover 생성
170 lines
6.9 KiB
Python
170 lines
6.9 KiB
Python
import os
|
|
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):
|
|
monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path))
|
|
# 더미 입력 파일들
|
|
audio = tmp_path / "audio.mp3"
|
|
audio.write_bytes(b"\x00" * 100)
|
|
cover_dir = tmp_path / "50"
|
|
cover_dir.mkdir()
|
|
cover = cover_dir / "cover.jpg"
|
|
cover.write_bytes(b"\x00" * 100)
|
|
return tmp_path
|
|
|
|
|
|
@patch("subprocess.run")
|
|
def test_thumb_extracts_frame(mock_run, tmp_storage):
|
|
mock_run.return_value = MagicMock(returncode=0, stderr="")
|
|
video_path = tmp_storage / "60" / "video.mp4"
|
|
video_path.parent.mkdir()
|
|
video_path.write_bytes(b"\x00" * 100)
|
|
out = thumb.generate(pipeline_id=60, video_path=str(video_path),
|
|
track_title="Midnight Drive", overlay_text=False)
|
|
assert out["url"].endswith("/60/thumbnail.jpg")
|
|
args = mock_run.call_args[0][0]
|
|
assert args[0] == "ffmpeg"
|
|
|
|
|
|
@patch("subprocess.run")
|
|
def test_thumb_failure_raises(mock_run, tmp_storage):
|
|
mock_run.return_value = MagicMock(returncode=1, stderr="bad input")
|
|
video_path = tmp_storage / "61" / "video.mp4"
|
|
video_path.parent.mkdir()
|
|
video_path.write_bytes(b"\x00" * 100)
|
|
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"
|
|
|
|
|
|
def test_container_to_nas_strips_cache_buster(monkeypatch):
|
|
monkeypatch.setattr(video, "NAS_VIDEOS_ROOT", "/volume1/docker/webpage/data/videos")
|
|
monkeypatch.setattr(video, "NAS_MUSIC_ROOT", "/volume1/docker/webpage/data/music")
|
|
# cache-busted path → strip ?v=... before NAS conversion
|
|
assert video._container_to_nas("/app/data/videos/3/cover.jpg?v=20260510065642") == "/volume1/docker/webpage/data/videos/3/cover.jpg"
|
|
|
|
|
|
@respx.mock
|
|
def test_generate_video_passes_essential_params(encoder_env, tmp_path, monkeypatch):
|
|
monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path))
|
|
captured = {}
|
|
|
|
def hook(req):
|
|
import json as _json
|
|
captured["body"] = _json.loads(req.content)
|
|
return Response(200, json={"ok": True, "duration_ms": 5000,
|
|
"output_path_nas": "/v/3/video.mp4",
|
|
"output_bytes": 10_000_000,
|
|
"encoder": "h264_nvenc", "preset": "p4"})
|
|
|
|
respx.post("http://192.168.45.59:8765/encode_video").mock(side_effect=hook)
|
|
out = video.generate(
|
|
pipeline_id=3, audio_path="/app/data/x.mp3",
|
|
cover_path="/app/data/videos/3/cover.jpg",
|
|
genre="mix", duration_sec=3600, resolution="1920x1080",
|
|
style="essential", background_mode="video_loop",
|
|
background_path="/app/data/videos/3/loop.mp4",
|
|
tracks=[{"id": 1, "title": "T1", "start_offset_sec": 0}],
|
|
)
|
|
body = captured["body"]
|
|
assert body["style"] == "essential"
|
|
assert body["background_mode"] == "video_loop"
|
|
assert body["background_path_nas"] == "/volume1/docker/webpage/data/videos/3/loop.mp4"
|
|
assert body["tracks"][0]["title"] == "T1"
|
|
assert out["url"].endswith("/3/video.mp4")
|