fix(music-lab): xfade offset 누적 오차 수정 + 테스트 보강
- _build_slideshow_cmd: offset 공식을 `duration_per_image * i - xd * i`로 수정 (누적 전환 오차 제거) - _generate_metadata: genre 빈 문자열일 때 yt_tags에 빈 문자열 삽입 방지 - test: VIDEO_DATA_DIR 패치를 monkeypatch로 교체 (자동 복원 보장) - test: xfade offset 값 검증 테스트 추가 (29.00, 58.00) - test: 미사용 import 제거 (pytest, sqlite3) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -84,7 +84,7 @@ def _build_slideshow_cmd(
|
|||||||
filter_str = ";".join(filter_parts)
|
filter_str = ";".join(filter_parts)
|
||||||
prev = "v0"
|
prev = "v0"
|
||||||
for i in range(1, n):
|
for i in range(1, n):
|
||||||
offset = max(0.0, duration_per_image * i - xd)
|
offset = max(0.0, duration_per_image * i - xd * i)
|
||||||
nxt = "out" if i == n - 1 else f"xf{i}"
|
nxt = "out" if i == n - 1 else f"xf{i}"
|
||||||
filter_str += (
|
filter_str += (
|
||||||
f";[{prev}][v{i}]xfade=transition=fade:"
|
f";[{prev}][v{i}]xfade=transition=fade:"
|
||||||
@@ -159,7 +159,7 @@ def _generate_metadata(genre: str, moods: list, lyrics: str, target_countries: l
|
|||||||
start, end = text.find("{"), text.rfind("}") + 1
|
start, end = text.find("{"), text.rfind("}") + 1
|
||||||
return json.loads(text[start:end])
|
return json.loads(text[start:end])
|
||||||
except Exception:
|
except Exception:
|
||||||
return {"yt_title": f"{genre or 'Music'} - Chill Beats", "yt_description": "", "yt_tags": [genre]}
|
return {"yt_title": f"{genre or 'Music'} - Chill Beats", "yt_description": "", "yt_tags": [genre] if genre else []}
|
||||||
|
|
||||||
|
|
||||||
def _render_visualizer(track: dict, proj: dict, output_path: str) -> None:
|
def _render_visualizer(track: dict, proj: dict, output_path: str) -> None:
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# music-lab/tests/test_video_producer.py
|
# music-lab/tests/test_video_producer.py
|
||||||
import os
|
import os
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_visualizer_cmd():
|
def test_build_visualizer_cmd():
|
||||||
@@ -59,10 +58,9 @@ def test_build_slideshow_cmd_multiple_images():
|
|||||||
assert "/tmp/out.mp4" in cmd
|
assert "/tmp/out.mp4" in cmd
|
||||||
|
|
||||||
|
|
||||||
def test_produce_video_visualizer_calls_ffmpeg(tmp_db, tmp_path):
|
def test_produce_video_visualizer_calls_ffmpeg(tmp_db, tmp_path, monkeypatch):
|
||||||
"""produce_video가 visualizer 포맷으로 FFmpeg를 호출하는지 확인."""
|
"""produce_video가 visualizer 포맷으로 FFmpeg를 호출하는지 확인."""
|
||||||
from app.db import init_db, create_video_project
|
from app.db import init_db, create_video_project
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
init_db()
|
init_db()
|
||||||
|
|
||||||
@@ -82,7 +80,7 @@ def test_produce_video_visualizer_calls_ffmpeg(tmp_db, tmp_path):
|
|||||||
create_video_project({"track_id": 1, "format": "visualizer", "target_countries": ["BR"]})
|
create_video_project({"track_id": 1, "format": "visualizer", "target_countries": ["BR"]})
|
||||||
|
|
||||||
import app.video_producer as vp
|
import app.video_producer as vp
|
||||||
vp.VIDEO_DATA_DIR = str(tmp_path / "videos")
|
monkeypatch.setattr("app.video_producer.VIDEO_DATA_DIR", str(tmp_path / "videos"))
|
||||||
|
|
||||||
with patch("app.video_producer.subprocess.run") as mock_run, \
|
with patch("app.video_producer.subprocess.run") as mock_run, \
|
||||||
patch("app.video_producer._generate_metadata", return_value={
|
patch("app.video_producer._generate_metadata", return_value={
|
||||||
@@ -96,3 +94,18 @@ def test_produce_video_visualizer_calls_ffmpeg(tmp_db, tmp_path):
|
|||||||
proj = get_video_project(1)
|
proj = get_video_project(1)
|
||||||
assert proj["status"] == "done"
|
assert proj["status"] == "done"
|
||||||
assert mock_run.called
|
assert mock_run.called
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_slideshow_cmd_offset_calculation():
|
||||||
|
from app.video_producer import _build_slideshow_cmd
|
||||||
|
imgs = ["/tmp/img0.jpg", "/tmp/img1.jpg", "/tmp/img2.jpg"]
|
||||||
|
cmd = _build_slideshow_cmd(imgs, "/tmp/audio.mp3", "/tmp/out.mp4", duration_per_image=30.0)
|
||||||
|
# filter_complex 문자열 추출
|
||||||
|
fc_idx = cmd.index("-filter_complex")
|
||||||
|
fc = cmd[fc_idx + 1]
|
||||||
|
# xfade이 2번 등장해야 함 (이미지 3개 → 전환 2번)
|
||||||
|
assert fc.count("xfade") == 2
|
||||||
|
# 첫 번째 xfade offset: 30*1 - 1*1 = 29.0
|
||||||
|
assert "offset=29.00" in fc
|
||||||
|
# 두 번째 xfade offset: 30*2 - 1*2 = 58.0
|
||||||
|
assert "offset=58.00" in fc
|
||||||
|
|||||||
Reference in New Issue
Block a user