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:
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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/"):
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user