fix(#1743): 批量变体独立选片——完整重跑单视频选片+批次20%重叠避让+查重超阈重渲+封面独立 #1745
Reference in New Issue
Block a user
Delete Branch "fix/batch-variant-independent-plans-1743"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
根因
Issue #1743(P0 批量生成降重失效):批量生成的变体 1..N 走
clone_plan_for_variant,只重算起点,素材/顺序/速度与源 plan 完全相同,导致 N 条成片片段同源(实测批次查重率 60.1%/33.3%,3 个任务共用同一 plana2989c05、同片段 asset)。修复原则
多视频 = 单视频逻辑 × N,只是 1 条和多条的区别。 宁可不生成也不出同源成片。
改动
1. 新增选片纯核心
packages/domain/variant_plan_selector.py_resolve_start_time(与单视频同一入口:镜头洗牌/随机起点/历史区间避让)2.
EditPlanService.reselect_plan_for_variantrecord_used_segments写回素材 metadata(跨变体/跨任务避让,N 变体串行选片天然互避)3. 正式生成
generation_tasks.py4. 批量预览
generation_preview.py5. worker 渲染
generation.py_upload_rendered_video(仅上传,重渲版 _v1 后缀不覆盖首版)/_record_video_and_dedupbatch_similarity > 20%→_reselect_plan_for_batch_retry重选 plan 回写任务后重渲一次(最多 1 次;重选失败保留首版)should_rerender_for_batch_dedup/pick_batch_cover_index(可单测)6.
dedup_helpers.pycreate_video_record_and_dedup返回 int → dict:{video_count, is_duplicate, batch_similarity, duplicate_of},支撑重渲判定测试
test_variant_plan_selector_1743(8):N=3 素材组合/顺序/起点显著不同、20% 重叠避让触发重选、空池/零时长 ValueError、非 main 片段保留骨架、跨变体 batch_segments 累加test_reselect_plan_variant_1743(5):独立 plan/骨架复制/片段差异、record_used_segments 每片段写回、源区间避让 ≤20%、无片段/不存在 ValueErrortest_batch_render_rerender_1743(11):重渲阈值判定全分支、封面哈希稳定/分散/公式一致test_batch_dedup_helpers_1743(6 含既有调整):批次查重 batch_similarity 透传、历史重复跳过批次查重、非批次 None、重选 plan 重试成功/失败前端配合
批量预览现在走后端真实变体独立选片(count>1 变体 1..N-1 各自 reselect),前端可直接读真实变体 plan 对齐 variantSeed 假预览(方案 A 所见即所得)。
验收
部署 staging 后请主人实测:N=3 批量生成 → 3 成片片段 asset 组合/顺序/封面不同、批次查重率全部 <20%;N=1 单视频零回归。
根因:批量生成的变体 1..N 走 clone_plan_for_variant 只重算起点, 素材/顺序/速度与源 plan 完全相同,导致 N 条成片片段同源(批次查重率 60.1%/33.3%)。 修复(多视频 = 单视频逻辑 × N): - 新增 packages/domain/variant_plan_selector.py 纯核心: 素材池 shuffle + main 片段顺序洗牌 + _resolve_start_time 起点重选 (镜头洗牌/随机起点/历史区间避让,与单视频同一入口)+ 批次内同素材 区间重叠 >20% 自动重选起点(最多 6 次) - EditPlanService.reselect_plan_for_variant:源 plan 骨架(clip_type/order/ duration/文案/转场/速度)保留,素材池=源素材∪批次素材,新片段区间 record_used_segments 写回(跨变体/跨任务避让),replace 事务落库 - 正式生成(generation_tasks):创建任务前预生成 N-1 个独立 plan, 变体0保留用户预览 plan;count>1 无源 plan 直接 400(严禁 N 任务兜底 共用同一 plan);任务循环兜底块加批量守卫(缺 plan 500 中断) - 批量预览(generation_preview):count>1 变体0用源 plan、变体1..N-1 reselect 真实选片(所见即所得);count==1 保持 clone 零回归 - worker 渲染:上传 OSS 与落库查重拆分(_upload_rendered_video/ _record_video_and_dedup);批次任务 batch_similarity>20% 重选 plan 重渲一次(最多1次,重选失败保留首版,首版_v1后缀不覆盖); 封面按 task_id md5 稳定哈希分散选候选帧(非批次仍取首帧) - dedup_helpers.create_video_record_and_dedup 返回 dict: {video_count,is_duplicate,batch_similarity,duplicate_of} 测试:新增 24 个单测(纯核心选片差异化/重叠避让/骨架保留、服务层 reselect、重渲阈值判定、封面哈希、批次查重 dict 契约),适配 6 个旧测试, 全量 14469 passed;diff coverage 63%。🚀 预览环境已部署
【阻塞级判定】
📊 审查概览
🔴 阻塞级问题(必须修复)
[apps/api/app/api/routes/generation_preview.py: 437行]
variant_plan_ids列表内容逻辑错误count > 1的分支中,代码执行了variant_plan_ids.append(source_plan_id),随后又循环添加新生成的变体 plan。这导致variant_plan_ids列表包含了源 plan 和所有变体 plan(共 N 个)。然而,对比generation_tasks.py中的逻辑(以及通常的批量任务关联逻辑),variant_plan_ids往往仅用于存储“变体” plan(即 N-1 个),任务 0 直接使用源 plan。generation_tasks.py一致(即if task_index > 0: effective_plan_id = variant_plan_ids[task_index - 1]),那么任务 1 将会错误地获取到源 plan,任务 2 获取到变体 1 的 plan,导致所有任务的剪辑计划错位。variant_plan_ids.append(source_plan_id)这一行,确保variant_plan_ids仅包含新生成的变体 plan,保持与generation_tasks.py的数据结构一致。[apps/api/app/services/edit_plan_service.py: 539-545行] 事务完整性风险导致元数据脏写
reselect_plan_for_variant方法中,record_used_segments(记录素材使用区间)在replace_all_clips_transactional(事务性替换片段)之前被调用。如果replace_all_clips_transactional执行失败并抛出异常回滚,前面写入的素材元数据(metadata)将无法回滚,导致系统认为该片段已被使用,但实际上对应的 Plan 并未创建成功。record_used_segments的调用移至replace_all_clips_transactional内部,或者确保record_used_segments在同一个数据库事务中执行,保证原子性。[apps/api/app/api/routes/generation_preview.py: 435-501行] 预览任务对源 Plan 的污染风险
count == 1时,代码确实克隆了源 plan。但在count > 1时,代码直接将source_plan_id放入variant_plan_ids供任务 0 使用。这意味着批量预览的第一个任务将直接引用并可能修改用户的原始编辑计划。count > 1时的任务 0 也应该克隆一份源 plan,而不是直接引用。或者,如果业务逻辑允许任务 0 引用源 plan,则需确认其是否为只读操作,并更新注释说明此处的行为差异。💡 改进建议(不阻塞合并)
create_video_record_and_dedup的返回值从int变更为dict。这是一个破坏性变更。虽然 diff 中未显示调用方代码,但必须确保所有调用方(如generation.py中的 Worker 任务)都已同步更新以处理字典返回值,否则会导致运行时崩溃。建议在合并前全库搜索该函数的调用点进行确认。✅ 良好实践
generation_tasks.py中增加了对批量生成时源 plan 缺失的严格校验(第 481 行),避免了 N 个任务共用同一 plan 导致内容重复的严重问题,逻辑严谨。reselect_plan_for_variant方法中实现了完善的素材池去重和分页加载逻辑,代码健壮性较好。🤖 由 AI 代码审查机器人自动生成 | 2026-09-06 09:56:37 | 模型:
🗑️ 预览环境已清理
PR #1745 已关闭或合并,对应的预览环境已被清理。