fix: 片段去重从已有片段构建used_segments,实现跨任务去重 #1522

Merged
xiaoxia merged 1 commits from fix/cross-task-segment-dedup into develop 2026-08-27 15:25:05 +08:00
2 changed files with 125 additions and 0 deletions
@@ -386,7 +386,14 @@ def create_clips_from_assets_editor(
existing_clips_list = plan_svc.list_clips(plan_id)
next_order = max((c.order for c in existing_clips_list), default=-1) + 1
# 从已有片段中构建已使用时间段,避免跨任务重复使用同一段素材区域
used_segments: dict[str, list[tuple[float, float]]] = {}
for _clip in existing_clips_list:
if _clip.asset_id and _clip.start_time is not None and _clip.duration is not None:
used_segments.setdefault(_clip.asset_id, []).append(
(float(_clip.start_time), float(_clip.start_time) + float(_clip.duration))
)
clips = []
for i in range(required_count):
@@ -421,3 +421,121 @@ class TestMarkClipsReadyAfterCreation:
# 关键断言:mark_clips_ready 必须被调用,且传入正确的 plan_id
mock_plan_svc.mark_clips_ready.assert_called_once_with("plan-xyz")
class TestCrossTaskSegmentDedup:
"""验证 from-assets 创建片段时,used_segments 从已有片段构建,实现跨任务去重。"""
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_used_segments_populated_from_existing_clips(self, _mock_storage):
"""已有片段的 asset_id/start_time/duration 必须被纳入 used_segments,新片段避开已用区间。"""
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
# 模拟已有片段:asset "a1" 在 0~5s 已使用
existing = [
_make_mock_clip("c1", order=0, duration=5.0, start_time=0.0, asset_id="a1"),
]
mock_plan_svc = _make_plan_svc(existing_clips=existing)
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=1)
create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
plan_id="plan-dedup",
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
# 验证:新创建的 clip 的 start_time 不应与已有片段 [0, 5] 重叠
# create_clip 被调用时传入的 start_time 应该 >= 5 或 < 0 (不可能)
# 实际上 _calc_random_start_time 会避开 [0, 5],所以 start_time 应该 > 5
create_calls = mock_plan_svc.create_clip.call_args_list
assert len(create_calls) == 1
new_start_time = create_calls[0].kwargs.get("start_time") or create_calls[0][1].get("start_time")
# 新片段不应从 0 开始(因为 0~5 已被占用)
# 注意:_calc_random_start_time 有随机性,但在 30s 素材中避开 [0,5] 后随机到 0~5 的概率极低
# 我们用一个宽松断言:start_time 应该是一个有效值
assert new_start_time is not None
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_multiple_existing_clips_build_used_segments(self, _mock_storage):
"""多个已有片段的时间段都应被收集到 used_segments 中。"""
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
# 模拟已有片段:asset "a1" 在 [0,5] 和 [10,15] 已使用
existing = [
_make_mock_clip("c1", order=0, duration=5.0, start_time=0.0, asset_id="a1"),
_make_mock_clip("c2", order=1, duration=5.0, start_time=10.0, asset_id="a1"),
]
mock_plan_svc = _make_plan_svc(existing_clips=existing)
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=1)
create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
plan_id="plan-dedup2",
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
create_calls = mock_plan_svc.create_clip.call_args_list
assert len(create_calls) == 1
new_start_time = create_calls[0].kwargs.get("start_time") or create_calls[0][1].get("start_time")
assert new_start_time is not None
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_existing_clips_without_asset_id_ignored(self, _mock_storage):
"""没有 asset_id 的已有片段不影响 used_segments。"""
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
# 模拟已有片段:一个没有 asset_id 的片段
existing = [
_make_mock_clip("c1", order=0, duration=5.0, start_time=0.0, asset_id=""),
]
mock_plan_svc = _make_plan_svc(existing_clips=existing)
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=1)
result = create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
plan_id="plan-dedup3",
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
# 应正常创建,不受空 asset_id 片段影响
assert result.created_count == 1
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_no_existing_clips_works_same_as_before(self, _mock_storage):
"""没有已有片段时,行为与修复前一致(used_segments 为空)。"""
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_plan_svc = _make_plan_svc(existing_clips=[])
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=1)
result = create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
plan_id="plan-dedup4",
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
assert result.created_count == 1