fix(music-lab): cache-buster query 제거 + DALL·E prompt에 background_keyword 활용

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 생성
This commit is contained in:
2026-05-10 16:12:21 +09:00
parent 20c5268def
commit 755dea63f4
6 changed files with 52 additions and 2 deletions

View File

@@ -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:

View File

@@ -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):

View File

@@ -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/"):

View File

@@ -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"]

View File

@@ -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"

View File

@@ -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))