diff --git a/apps/api/app/services/plan_generator_service.py b/apps/api/app/services/plan_generator_service.py index 28dc0a94d..12b1c0015 100755 --- a/apps/api/app/services/plan_generator_service.py +++ b/apps/api/app/services/plan_generator_service.py @@ -118,9 +118,9 @@ class PlanGeneratorService: # 4. 按 editing_mode 分配素材 if asset_ids: - # 如果是随机预览模式,获取素材时长信息 + # 获取素材时长信息,用于随机起始时间 asset_durations = None - if random_preview and self._asset_repo: + if self._asset_repo: asset_durations = self._fetch_asset_durations(asset_ids) self._distribute_assets( clips, diff --git a/tests/unit/test_plan_generator.py b/tests/unit/test_plan_generator.py index a7202e079..10d02f62c 100755 --- a/tests/unit/test_plan_generator.py +++ b/tests/unit/test_plan_generator.py @@ -886,3 +886,120 @@ class TestTemplateConfigPropagation: assert clip_cfg.get("speed_ratio") == 1.2 assert clip_cfg.get("name") == "开场" assert clip_cfg.get("custom_field") == "value" + + +class TestAssetDurationsAlwaysFetched: + """验证 asset_durations 不再受 random_preview 条件限制。 + + 修复前:asset_durations 仅在 random_preview=True 时传入 distribute_assets + 修复后:只要 _asset_repo 存在,就始终获取 asset_durations + """ + + def _make_service_with_asset_repo(self): + """创建带 mock asset_repo 的 PlanGeneratorService""" + from apps.api.app.services.plan_generator_service import PlanGeneratorService + + plan_repo = StubEditPlanRepository() + clip_repo = StubEditPlanClipRepository() + + # mock asset_repo: 返回带 duration 的素材 + asset_repo = MagicMock() + + def fake_get(asset_id): + mock_asset = MagicMock() + mock_asset.duration = 30.0 # 每个素材 30 秒 + 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_asset_durations_fetched_without_random_preview(self): + """random_preview=False 时也应获取 asset_durations""" + svc, asset_repo = 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": 0, "min_duration": 3.0, "max_duration": 5.0}], + ) + + # patch distribute_assets 以捕获传入的参数 + with patch("apps.api.app.services.plan_generator_service.distribute_assets") as mock_distribute: + svc.generate_from_template( + template=template, + clip_configs=clip_configs, + asset_ids=["a1", "a2"], + random_preview=False, # 关键:非随机预览模式 + ) + + # 验证 asset_durations 被传入(不是 None) + mock_distribute.assert_called_once() + call_kwargs = mock_distribute.call_args + asset_durations = call_kwargs.kwargs.get("asset_durations", call_kwargs[1].get("asset_durations")) + assert asset_durations is not None, "asset_durations should be fetched even when random_preview=False" + assert "a1" in asset_durations + assert "a2" in asset_durations + assert asset_durations["a1"] == 30.0 + + # 验证 asset_repo.get 被调用(说明 _fetch_asset_durations 执行了) + assert asset_repo.get.call_count >= 2 + + def test_asset_durations_fetched_with_random_preview(self): + """random_preview=True 时仍正常获取 asset_durations(行为不变)""" + svc, asset_repo = 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": 0, "min_duration": 3.0, "max_duration": 5.0}], + ) + + with patch("apps.api.app.services.plan_generator_service.distribute_assets") as mock_distribute: + svc.generate_from_template( + template=template, + clip_configs=clip_configs, + asset_ids=["a1"], + random_preview=True, + ) + + call_kwargs = mock_distribute.call_args + asset_durations = call_kwargs.kwargs.get("asset_durations", call_kwargs[1].get("asset_durations")) + assert asset_durations is not None + assert "a1" in asset_durations + + def test_no_asset_repo_means_no_durations(self): + """_asset_repo 为 None 时 asset_durations 应为 None""" + svc, _, _ = _make_generator() # 默认不带 asset_repo + + template = _make_template("one_take") + clip_configs = _make_clip_configs( + template_id=template.id, + specs=[{"clip_type": ClipType.MAIN, "order": 0, "min_duration": 3.0, "max_duration": 5.0}], + ) + + with patch("apps.api.app.services.plan_generator_service.distribute_assets") as mock_distribute: + svc.generate_from_template( + template=template, + clip_configs=clip_configs, + asset_ids=["a1"], + ) + + 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