feat(asset-dedup): 素材去重机制6项优化 — 调参+警告+贪心分配 (#1611)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m44s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m15s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 36s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m41s
CI/CD Pipeline / Validate - Style (push) Successful in 5m55s
CI/CD Pipeline / Integration Tests (push) Successful in 6m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m51s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m45s
CI/CD Pipeline / Validate - Security (push) Successful in 10m34s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m36s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m13s
CI/CD Pipeline / Unit Tests (push) Failing after 14m3s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m44s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m15s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 36s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m41s
CI/CD Pipeline / Validate - Style (push) Successful in 5m55s
CI/CD Pipeline / Integration Tests (push) Successful in 6m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m51s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m45s
CI/CD Pipeline / Validate - Security (push) Successful in 10m34s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m36s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m13s
CI/CD Pipeline / Unit Tests (push) Failing after 14m3s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #1611.
This commit is contained in:
@@ -582,6 +582,47 @@ def _get_mediakit_recommendations(
|
||||
return {}
|
||||
|
||||
|
||||
def _calc_plan_internal_duplicate_rate(clips_data: list[dict]) -> float:
|
||||
"""估算单条成片内部重复率(%).
|
||||
|
||||
检查本条成片中同一素材是否有重叠的时间区间。
|
||||
重叠时长 / 成片总时长 * 100 = 内部重复率。
|
||||
这是一个轻量估算,不依赖视频指纹;完整查重由 worker 异步完成。
|
||||
"""
|
||||
if not clips_data:
|
||||
return 0.0
|
||||
|
||||
# 按素材分组
|
||||
by_asset: dict[str, list[tuple[float, float]]] = {}
|
||||
total_duration = 0.0
|
||||
for c in clips_data:
|
||||
aid = c.get("asset_id", "")
|
||||
if not aid:
|
||||
continue
|
||||
start = c.get("start_time", 0.0)
|
||||
end = start + c.get("duration", 0.0)
|
||||
by_asset.setdefault(aid, []).append((start, end))
|
||||
total_duration += c.get("duration", 0.0)
|
||||
|
||||
if total_duration <= 0:
|
||||
return 0.0
|
||||
|
||||
# 检查同素材内的区间重叠
|
||||
overlap_duration = 0.0
|
||||
for segments in by_asset.values():
|
||||
if len(segments) < 2:
|
||||
continue
|
||||
segments_sorted = sorted(segments, key=lambda s: s[0])
|
||||
last_end = segments_sorted[0][1]
|
||||
for start, end in segments_sorted[1:]:
|
||||
overlap = max(0.0, min(end, last_end) - start)
|
||||
if overlap > 0:
|
||||
overlap_duration += overlap
|
||||
last_end = max(last_end, end)
|
||||
|
||||
return round(overlap_duration / total_duration * 100, 1)
|
||||
|
||||
|
||||
@router.post("/clips/from-assets", response_model=ClipsFromAssetsResponse)
|
||||
def create_clips_from_assets_editor(
|
||||
template_id: str,
|
||||
@@ -662,19 +703,31 @@ def create_clips_from_assets_editor(
|
||||
return False
|
||||
return reused_durations.get(aid, 0.0) / assigned > REUSE_RATIO_LIMIT
|
||||
|
||||
# 素材耗尽标志:某轮循环中所有素材均被跳过时为 True
|
||||
all_assets_exhausted = False
|
||||
|
||||
for i, (_seg_order, dur_min, dur_max) in enumerate(segments):
|
||||
# 在 segment 的 duration_min ~ duration_max 之间随机取值(保留一位小数)
|
||||
raw_duration = random.uniform(dur_min, dur_max)
|
||||
|
||||
# 轮询分配素材:跳过时长缺失、复用占比已超 15% 阈值的素材;
|
||||
# 贪心分配素材:按"已使用次数"升序排列候选素材(使用最少的优先),
|
||||
# 同次数随机打散,避免"A-B-C-D"的固定组合反复出现。
|
||||
# 跳过时长缺失、复用占比已超 10% 阈值的素材;
|
||||
# 选中后计算起点,若该素材可用区间耗尽且复用被闸门拒绝(calc 返回 None),
|
||||
# 继续轮询下一个素材
|
||||
# 继续尝试下一个素材
|
||||
asset_id = ""
|
||||
clip_duration = 0.0
|
||||
start_time: float | None = None
|
||||
n_assets = len(asset_ids)
|
||||
for offset in range(n_assets):
|
||||
candidate = asset_ids[(i + offset) % n_assets]
|
||||
# 动态按使用次数排序:优先选使用最少的素材,同次数随机打散
|
||||
asset_use_counts = {
|
||||
aid: len(used_segments.get(aid, []))
|
||||
for aid in asset_ids
|
||||
}
|
||||
sorted_candidates = sorted(
|
||||
asset_ids,
|
||||
key=lambda aid: (asset_use_counts.get(aid, 0), random.random()),
|
||||
)
|
||||
for candidate in sorted_candidates:
|
||||
candidate_total = asset_durations.get(candidate, 0.0)
|
||||
if candidate_total <= 0:
|
||||
continue
|
||||
@@ -690,7 +743,7 @@ def create_clips_from_assets_editor(
|
||||
continue
|
||||
# 随机起始时间(不调用 MediaKit,保证接口快速返回);100 次避不开
|
||||
# 历史区间时走受控复用回调(复用片段累加 reused_durations,回调内部
|
||||
# 预判复用后占比超 15% 则拒绝并返回 None)
|
||||
# 预判复用后占比超 10% 则拒绝并返回 None)
|
||||
candidate_start = _calc_random_start_time(
|
||||
candidate,
|
||||
candidate_duration,
|
||||
@@ -712,6 +765,7 @@ def create_clips_from_assets_editor(
|
||||
|
||||
if not asset_id or start_time is None:
|
||||
# 所有素材时长缺失、复用占比超阈值,或区间耗尽且复用被拒 → 素材可切区间不足
|
||||
all_assets_exhausted = True
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="素材可切区间不足,请补充新素材",
|
||||
@@ -754,11 +808,31 @@ def create_clips_from_assets_editor(
|
||||
unique_asset_ids,
|
||||
)
|
||||
|
||||
# 6. 立即返回响应
|
||||
# 6. 估算成片内部重复率(本条成片中同一素材的重叠片段时长占比)
|
||||
dup_rate = _calc_plan_internal_duplicate_rate(clips_data)
|
||||
duplicate_warning = None
|
||||
if dup_rate > 50:
|
||||
duplicate_warning = f"查重率 {dup_rate:.1f}% 超过50%,建议更换素材或模板"
|
||||
logger.warning(
|
||||
"from-assets 成片查重率超标: plan_id=%s dup_rate=%.1f%%",
|
||||
plan_id, dup_rate,
|
||||
)
|
||||
|
||||
# 7. 素材耗尽提示
|
||||
exhaustion_warning = None
|
||||
if all_assets_exhausted and created_count < len(segments):
|
||||
exhaustion_warning = (
|
||||
"素材可切区间不足,部分片段使用了复用素材。"
|
||||
"建议:1) 补充更多素材到素材库 2) 使用不同的素材组合生成"
|
||||
)
|
||||
|
||||
# 8. 立即返回响应
|
||||
return ClipsFromAssetsResponse(
|
||||
created_count=created_count,
|
||||
plan_id=plan_id,
|
||||
clip_ids=[],
|
||||
duplicate_warning=duplicate_warning,
|
||||
exhaustion_warning=exhaustion_warning,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -189,6 +189,8 @@ class ClipsFromAssetsResponse(BaseModel):
|
||||
plan_id: str = ""
|
||||
message: str = ""
|
||||
clip_ids: List[str] = Field(default_factory=list, description="创建的片段ID列表")
|
||||
duplicate_warning: Optional[str] = Field(default=None, description="查重率超标警告")
|
||||
exhaustion_warning: Optional[str] = Field(default=None, description="素材耗尽警告")
|
||||
|
||||
|
||||
# ── 封面配置 ────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user