feat(music-lab): video.py — Windows에 style/background_mode/tracks 전달 + orchestrator 파라미터 wiring

This commit is contained in:
2026-05-09 13:17:49 +09:00
parent a347da075c
commit 8f859274c4
3 changed files with 54 additions and 3 deletions

View File

@@ -202,12 +202,24 @@ async def _run_video(p, ctx):
vd = setup["visual_defaults"] vd = setup["visual_defaults"]
audio_path = ctx["audio_path"] audio_path = ctx["audio_path"]
cover_path = _local_path(p["cover_url"]) cover_path = _local_path(p["cover_url"])
style = p.get("visual_style") or vd.get("default_visual_style", "essential")
bg_mode = p.get("background_mode") or vd.get("default_background_mode", "static")
bg_path = None
if bg_mode == "video_loop":
loop_local = os.path.join(storage.pipeline_dir(p["id"]), "loop.mp4")
bg_path = loop_local if os.path.isfile(loop_local) else None
out = await asyncio.to_thread( out = await asyncio.to_thread(
video.generate, video.generate,
pipeline_id=p["id"], audio_path=audio_path, cover_path=cover_path, pipeline_id=p["id"], audio_path=audio_path, cover_path=cover_path,
genre=ctx["genre"], genre=ctx["genre"],
duration_sec=ctx["duration_sec"], duration_sec=ctx["duration_sec"],
resolution=vd["resolution"], style=vd["style"], resolution=vd.get("resolution", "1920x1080"),
style=style,
background_mode=bg_mode,
background_path=bg_path,
tracks=ctx["tracks"] if len(ctx["tracks"]) > 1 else None,
) )
return {"next_state": "video_pending", "fields": {"video_url": out["url"]}} return {"next_state": "video_pending", "fields": {"video_url": out["url"]}}
@@ -230,6 +242,7 @@ async def _run_meta(p, ctx, feedback):
"duration_sec": ctx["duration_sec"], "moods": ctx["moods"]}, "duration_sec": ctx["duration_sec"], "moods": ctx["moods"]},
template=setup["metadata_template"], template=setup["metadata_template"],
trend_keywords=trend_top, feedback=feedback, trend_keywords=trend_top, feedback=feedback,
tracks=ctx["tracks"] if len(ctx["tracks"]) > 1 else None,
) )
return {"next_state": "meta_pending", return {"next_state": "meta_pending",
"fields": {"metadata_json": json.dumps(out, ensure_ascii=False)}} "fields": {"metadata_json": json.dumps(out, ensure_ascii=False)}}

View File

@@ -24,7 +24,10 @@ class VideoGenerationError(Exception):
def generate(*, pipeline_id: int, audio_path: str, cover_path: str, def generate(*, pipeline_id: int, audio_path: str, cover_path: str,
genre: str, duration_sec: int, resolution: str = "1920x1080", genre: str, duration_sec: int, resolution: str = "1920x1080",
style: str = "visualizer") -> dict: style: str = "essential",
background_mode: str = "static",
background_path: str | None = None,
tracks: list[dict] | None = None) -> dict:
"""원격 Windows GPU 서버 호출. 다운/실패 시 즉시 예외.""" """원격 Windows GPU 서버 호출. 다운/실패 시 즉시 예외."""
if not ENCODER_URL: if not ENCODER_URL:
raise VideoGenerationError( raise VideoGenerationError(
@@ -35,6 +38,7 @@ def generate(*, pipeline_id: int, audio_path: str, cover_path: str,
nas_audio = _container_to_nas(audio_path) nas_audio = _container_to_nas(audio_path)
nas_cover = _container_to_nas(cover_path) nas_cover = _container_to_nas(cover_path)
nas_output = _container_to_nas(out_path) nas_output = _container_to_nas(out_path)
nas_bg = _container_to_nas(background_path) if background_path else None
payload = { payload = {
"cover_path_nas": nas_cover, "cover_path_nas": nas_cover,
@@ -43,9 +47,13 @@ def generate(*, pipeline_id: int, audio_path: str, cover_path: str,
"resolution": resolution, "resolution": resolution,
"duration_sec": duration_sec, "duration_sec": duration_sec,
"style": style, "style": style,
"background_mode": background_mode,
"background_path_nas": nas_bg,
"tracks": tracks or [],
} }
logger.info("Windows 인코더 호출: pipeline=%d audio=%s", pipeline_id, audio_path) logger.info("Windows 인코더 호출: pipeline=%d audio=%s style=%s bg_mode=%s",
pipeline_id, audio_path, style, background_mode)
try: try:
with httpx.Client(timeout=ENCODER_TIMEOUT_S) as client: with httpx.Client(timeout=ENCODER_TIMEOUT_S) as client:
resp = client.post(f"{ENCODER_URL}/encode_video", json=payload) resp = client.post(f"{ENCODER_URL}/encode_video", json=payload)

View File

@@ -130,3 +130,33 @@ def test_container_to_nas_music_path(monkeypatch):
monkeypatch.setattr(video, "NAS_VIDEOS_ROOT", "/volume1/docker/webpage/data/videos") monkeypatch.setattr(video, "NAS_VIDEOS_ROOT", "/volume1/docker/webpage/data/videos")
monkeypatch.setattr(video, "NAS_MUSIC_ROOT", "/volume1/docker/webpage/data/music") 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" assert video._container_to_nas("/app/data/abc.mp3") == "/volume1/docker/webpage/data/music/abc.mp3"
@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")