diff --git a/music-lab/app/pipeline/cover.py b/music-lab/app/pipeline/cover.py index 35a9d35..e53c592 100644 --- a/music-lab/app/pipeline/cover.py +++ b/music-lab/app/pipeline/cover.py @@ -94,7 +94,8 @@ async def generate(*, pipeline_id: int, genre: str, prompt_template: str, if api_key: try: await _generate_with_dalle(prompt_template, mood, feedback, out_path, - api_key=api_key, model=model) + api_key=api_key, model=model, + background_keyword=background_keyword) except (httpx.HTTPError, httpx.TimeoutException, KeyError, ValueError, OSError) as e: logger.warning("DALL·E 실패 — 폴백: %s", e) error = str(e) @@ -114,8 +115,11 @@ async def generate(*, pipeline_id: int, genre: str, prompt_template: str, async def _generate_with_dalle(prompt_template: str, mood: str, feedback: str, out_path: str, - *, api_key: str, model: str) -> None: + *, api_key: str, model: str, + background_keyword: str = "") -> None: prompt = prompt_template + if background_keyword: + prompt = f"{prompt}, {background_keyword}" # 사용자 직접 지정 keyword 우선 적용 if mood: prompt = f"{prompt}, {mood} mood" if feedback: diff --git a/music-lab/app/pipeline/orchestrator.py b/music-lab/app/pipeline/orchestrator.py index b49957f..53622b1 100644 --- a/music-lab/app/pipeline/orchestrator.py +++ b/music-lab/app/pipeline/orchestrator.py @@ -284,6 +284,8 @@ def _local_path(media_url: str) -> str: """ if not media_url: return "" + # Strip query string (e.g., cache-buster ?v=...) + media_url = media_url.split("?", 1)[0] base_media = os.getenv("VIDEO_MEDIA_BASE", "/media/videos") base_data = os.getenv("VIDEO_DATA_DIR", "/app/data/videos") if media_url.startswith(base_media): diff --git a/music-lab/app/pipeline/video.py b/music-lab/app/pipeline/video.py index c7b858b..d1ceb44 100644 --- a/music-lab/app/pipeline/video.py +++ b/music-lab/app/pipeline/video.py @@ -94,6 +94,10 @@ def _container_to_nas(container_path: str) -> str: """ /app/data/videos/3/cover.jpg → /volume1/docker/webpage/data/videos/3/cover.jpg /app/data/abc.mp3 → /volume1/docker/webpage/data/music/abc.mp3 """ + if not container_path: + return "" + # Strip query string (e.g., cache-buster ?v=...) + container_path = container_path.split("?", 1)[0] if container_path.startswith("/app/data/videos/"): return container_path.replace("/app/data/videos/", NAS_VIDEOS_ROOT + "/", 1) if container_path.startswith("/app/data/"): diff --git a/music-lab/tests/test_cover_generation.py b/music-lab/tests/test_cover_generation.py index 8dea3b4..33f15cb 100644 --- a/music-lab/tests/test_cover_generation.py +++ b/music-lab/tests/test_cover_generation.py @@ -147,3 +147,30 @@ async def test_pexels_zero_results_falls_back(tmp_storage, monkeypatch): image_source="pexels", ) assert out["used_fallback"] is True + + +@pytest.mark.asyncio +@respx.mock +async def test_dalle_uses_background_keyword(tmp_storage, monkeypatch): + monkeypatch.setenv("OPENAI_API_KEY", "test-key") + captured = {} + def hook(req): + import json as _json + captured["body"] = _json.loads(req.content) + return Response(200, json={"data": [{"url": "https://x"}]}) + respx.post("https://api.openai.com/v1/images/generations").mock(side_effect=hook) + png_bytes = bytes.fromhex( + "89504e470d0a1a0a0000000d49484452000000010000000108020000009077" + "53de0000000c4944415478da6300010000050001" + "0d0a2db40000000049454e44ae426082" + ) + respx.get("https://x").mock(return_value=Response(200, content=png_bytes)) + await cover.generate( + pipeline_id=80, genre="lo-fi", + prompt_template="moody anime", + mood="chill", track_title="X", + image_source="ai", + background_keyword="skateboard park bright atmosphere", + ) + assert "skateboard" in captured["body"]["prompt"] + assert "bright" in captured["body"]["prompt"] diff --git a/music-lab/tests/test_orchestrator_resolve.py b/music-lab/tests/test_orchestrator_resolve.py index c08b7f5..61909e8 100644 --- a/music-lab/tests/test_orchestrator_resolve.py +++ b/music-lab/tests/test_orchestrator_resolve.py @@ -75,3 +75,9 @@ def test_resolve_input_compile_job_done_status(): result = _resolve_input(pipeline) assert result["audio_path"] == "/app/data/compiles/7.mp3" assert result["title"] == "Done Mix" + + +def test_local_path_strips_cache_buster(): + from app.pipeline.orchestrator import _local_path + # /media/videos/3/cover.jpg?v=... → /app/data/videos/3/cover.jpg + assert _local_path("/media/videos/3/cover.jpg?v=20260510065642") == "/app/data/videos/3/cover.jpg" diff --git a/music-lab/tests/test_video_thumb.py b/music-lab/tests/test_video_thumb.py index 266ea99..c4e09c0 100644 --- a/music-lab/tests/test_video_thumb.py +++ b/music-lab/tests/test_video_thumb.py @@ -132,6 +132,13 @@ def test_container_to_nas_music_path(monkeypatch): 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))