feat: 正式生成时片段随机重排(降重,默认开启无开关)#1663 #1668
@@ -234,6 +234,11 @@ class PlanGeneratorService:
|
||||
# 有缓存的素材片段起点从随机镜头段选取,无缓存走随机起点兜底
|
||||
asset_scene_points = self._fetch_asset_scene_points(asset_ids)
|
||||
|
||||
# 正式生成也随机重排片段顺序(降重,默认开启无开关)
|
||||
# smart_match 决定选哪些素材,shuffle 只改变分配到 clips 的顺序
|
||||
asset_ids = list(asset_ids) # 复制避免修改调用方原列表
|
||||
random.shuffle(asset_ids)
|
||||
|
||||
distribute_assets(
|
||||
clips,
|
||||
asset_ids,
|
||||
|
||||
@@ -1007,3 +1007,162 @@ class TestAssetDurationsAlwaysFetched:
|
||||
call_kwargs = mock_distribute.call_args
|
||||
asset_durations = call_kwargs.kwargs.get("asset_durations", call_kwargs[1].get("asset_durations"))
|
||||
assert asset_durations is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 测试:正式生成片段随机重排(Issue #1663)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFormalGenerationShuffle:
|
||||
"""验证正式生成时片段顺序随机化。
|
||||
|
||||
Issue #1663: 正式生成时 smart_match 排序后对 asset_ids 做 random.shuffle,
|
||||
使得同一批素材每次生成的视频片段顺序不同,有利于查重降重。
|
||||
"""
|
||||
|
||||
def _make_service_with_asset_repo(self):
|
||||
"""创建带 mock asset_repo 的 PlanGeneratorService(复用 TestAssetDurationsAlwaysFetched 模式)"""
|
||||
from apps.api.app.services.plan_generator_service import PlanGeneratorService
|
||||
|
||||
plan_repo = StubEditPlanRepository()
|
||||
clip_repo = StubEditPlanClipRepository()
|
||||
|
||||
asset_repo = MagicMock()
|
||||
|
||||
def fake_get(asset_id):
|
||||
mock_asset = MagicMock()
|
||||
mock_asset.duration = 30.0
|
||||
mock_asset.quality_score = None
|
||||
mock_asset.created_at = None
|
||||
mock_asset.metadata = {}
|
||||
return mock_asset
|
||||
|
||||
asset_repo.get = MagicMock(side_effect=fake_get)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"apps.api.app.services.plan_generator_service.SQLAlchemyEditPlanRepository",
|
||||
return_value=plan_repo,
|
||||
),
|
||||
patch(
|
||||
"apps.api.app.services.plan_generator_service.SQLAlchemyEditPlanClipRepository",
|
||||
return_value=clip_repo,
|
||||
),
|
||||
):
|
||||
db = MagicMock()
|
||||
svc = PlanGeneratorService(db, asset_repo=asset_repo)
|
||||
svc._plan_repo = plan_repo
|
||||
svc._clip_repo = clip_repo
|
||||
|
||||
return svc, asset_repo
|
||||
|
||||
def test_formal_generation_shuffles_asset_ids(self):
|
||||
"""正式生成路径下 asset_ids 应被打乱,多次调用顺序应不同"""
|
||||
svc, _ = self._make_service_with_asset_repo()
|
||||
|
||||
template = _make_template("one_take")
|
||||
# 6 个 clip 容纳 6 个素材
|
||||
clip_configs = _make_clip_configs(
|
||||
template_id=template.id,
|
||||
specs=[
|
||||
{"clip_type": ClipType.MAIN, "order": i, "min_duration": 3.0, "max_duration": 5.0} for i in range(6)
|
||||
],
|
||||
)
|
||||
|
||||
asset_ids = ["a1", "a2", "a3", "a4", "a5", "a6"]
|
||||
|
||||
# 收集多次调用中 distribute_assets 收到的 asset_ids 顺序
|
||||
captured_orders = []
|
||||
with patch(
|
||||
"apps.api.app.services.plan_generator_service.distribute_assets",
|
||||
side_effect=lambda clips, asset_ids, *a, **kw: captured_orders.append(list(asset_ids)),
|
||||
):
|
||||
# mock _sort_assets_by_smart_score 返回固定顺序,验证 shuffle 会打乱
|
||||
with patch.object(
|
||||
svc,
|
||||
"_sort_assets_by_smart_score",
|
||||
side_effect=lambda ids: list(ids), # 原样返回
|
||||
):
|
||||
with patch.object(
|
||||
svc,
|
||||
"_fetch_asset_scene_points",
|
||||
return_value={},
|
||||
):
|
||||
for _ in range(10):
|
||||
svc.generate_from_template(
|
||||
template=template,
|
||||
clip_configs=clip_configs,
|
||||
asset_ids=list(asset_ids), # 每次传新列表
|
||||
random_preview=False, # 正式生成
|
||||
)
|
||||
|
||||
assert len(captured_orders) == 10
|
||||
# 每次 order 应该是 asset_ids 的一个排列
|
||||
expected_set = set(asset_ids)
|
||||
for order in captured_orders:
|
||||
assert set(order) == expected_set
|
||||
|
||||
# 10 次调用中应至少出现 2 种不同顺序(概率 > 99.9%)
|
||||
unique_orders = set(tuple(o) for o in captured_orders)
|
||||
assert (
|
||||
len(unique_orders) >= 2
|
||||
), f"Expected shuffled orders to vary, but got only {len(unique_orders)} unique order(s): {unique_orders}"
|
||||
|
||||
def test_formal_generation_does_not_mutate_original_list(self):
|
||||
"""shuffle 不应修改调用方的原始 asset_ids 列表"""
|
||||
svc, _ = self._make_service_with_asset_repo()
|
||||
|
||||
template = _make_template("one_take")
|
||||
clip_configs = _make_clip_configs(
|
||||
template_id=template.id,
|
||||
specs=[
|
||||
{"clip_type": ClipType.MAIN, "order": i, "min_duration": 3.0, "max_duration": 5.0} for i in range(4)
|
||||
],
|
||||
)
|
||||
|
||||
original = ["a1", "a2", "a3", "a4"]
|
||||
original_copy = list(original)
|
||||
|
||||
with patch("apps.api.app.services.plan_generator_service.distribute_assets"):
|
||||
with patch.object(svc, "_sort_assets_by_smart_score", side_effect=lambda ids: list(ids)):
|
||||
with patch.object(svc, "_fetch_asset_scene_points", return_value={}):
|
||||
svc.generate_from_template(
|
||||
template=template,
|
||||
clip_configs=clip_configs,
|
||||
asset_ids=original,
|
||||
random_preview=False,
|
||||
)
|
||||
|
||||
assert original == original_copy, "Original asset_ids list should not be mutated"
|
||||
|
||||
def test_preview_random_mode_unaffected_by_shuffle(self):
|
||||
"""预览随机模式不走 shuffle 路径,行为不变"""
|
||||
svc, _ = self._make_service_with_asset_repo()
|
||||
|
||||
template = _make_template("one_take")
|
||||
clip_configs = _make_clip_configs(
|
||||
template_id=template.id,
|
||||
specs=[
|
||||
{"clip_type": ClipType.MAIN, "order": i, "min_duration": 3.0, "max_duration": 5.0} for i in range(4)
|
||||
],
|
||||
)
|
||||
|
||||
asset_ids = ["a1", "a2", "a3", "a4"]
|
||||
|
||||
captured_orders = []
|
||||
with patch(
|
||||
"apps.api.app.services.plan_generator_service.distribute_assets",
|
||||
side_effect=lambda clips, asset_ids, *a, **kw: captured_orders.append(list(asset_ids)),
|
||||
):
|
||||
for _ in range(5):
|
||||
svc.generate_from_template(
|
||||
template=template,
|
||||
clip_configs=clip_configs,
|
||||
asset_ids=list(asset_ids),
|
||||
random_preview=True, # 预览随机模式
|
||||
)
|
||||
|
||||
assert len(captured_orders) == 5
|
||||
# 预览模式下 random.shuffle 不应被调用(在 _distribute_assets 的 if not random_selection 块内)
|
||||
# 所以 asset_ids 应该保持调用方传入的顺序(可能已由上层 shuffle 过)
|
||||
|
||||
Reference in New Issue
Block a user