From 4266c6f9b0d437af524f71aa9c5b48ec3719071a Mon Sep 17 00:00:00 2001 From: saas-backend-agent Date: Tue, 1 Sep 2026 19:36:19 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(render):=20=E4=BF=AE=E5=A4=8D=E7=94=9F?= =?UTF-8?q?=E6=88=90=E8=A7=86=E9=A2=91=E6=97=B6=E9=95=BF=E7=9F=AD=E4=BA=8E?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E8=A6=81=E6=B1=82=E6=97=B6=E9=95=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因分析: 1. 素材实际时长 < 配置时长时,min(duration, actual) 截短 clip 2. xfade offset 计算错误,未考虑前序转场的累积时长缩减 修复: - _resolve_clips: 素材不足时自动降低 playback_speed 补偿 (如配置4s但素材仅3s → speed=0.75x,用满3s素材达到4s输出) - _clip_effective_duration: 减速场景返回配置的 duration - xfade_builder: offset = first_input_dur - td(正确公式) 修复后 4 clips × 4s + 3 × 0.5s xfade = 14.5s(正确) 修复前 = 14.5s 但 offset 分布不均,导致内容错位 - from-assets: 分配时加转场补偿 (n-1)*td/n 确保最终输出 = 模板设定总时长 新增 debug 日志: - 每个 clip 的 duration/actual_duration/effective_duration/speed - 各图层时长明细和 estimated video_duration - xfade 链的 durations 和 estimated_dur 同步修复 PR #1611 遗留的测试(SEGMENT_EDGE_GAP=1.5 适配) --- .../app/api/routes/templates_editor/clips.py | 14 +++ .../unified_render_service.py | 63 +++++++++++- packages/domain/xfade_builder.py | 7 +- tests/unit/test_asset_availability.py | 10 +- tests/unit/test_duration_compensation.py | 95 +++++++++++++++++++ tests/unit/test_editor_clips_random_start.py | 3 +- tests/unit/test_mediakit_conflicts.py | 14 +-- 7 files changed, 188 insertions(+), 18 deletions(-) create mode 100644 tests/unit/test_duration_compensation.py diff --git a/apps/api/app/api/routes/templates_editor/clips.py b/apps/api/app/api/routes/templates_editor/clips.py index 44dcdd16e..a0fd097f3 100755 --- a/apps/api/app/api/routes/templates_editor/clips.py +++ b/apps/api/app/api/routes/templates_editor/clips.py @@ -23,6 +23,9 @@ import re from app.auth import AuthenticatedUser, get_current_user from app.core.storage import get_storage_service from app.dependencies import get_asset_repository, get_db_session +# 默认转场时长(与 worker 端保持一致) +_DEFAULT_TRANSITION_DURATION = 0.5 + from app.services.asset_segment_tracker import ( REUSE_RATIO_LIMIT, SEGMENT_EDGE_GAP, @@ -706,9 +709,20 @@ def create_clips_from_assets_editor( # 素材耗尽标志:某轮循环中所有素材均被跳过时为 True all_assets_exhausted = False + # 计算转场重叠补偿:每个 clip 需要额外增加的时长 + # 目标:渲染后视频总时长 = 模板设定的各片段时长之和 + # 公式:每 clip 增加 (n_segments - 1) * td / n_segments + n_segments = len(segments) + if n_segments > 1: + transition_compensation = (n_segments - 1) * _DEFAULT_TRANSITION_DURATION / n_segments + else: + transition_compensation = 0.0 + for i, (_seg_order, dur_min, dur_max) in enumerate(segments): # 在 segment 的 duration_min ~ duration_max 之间随机取值(保留一位小数) raw_duration = random.uniform(dur_min, dur_max) + # 加上转场补偿,确保最终输出时长 = 模板设定总时长 + raw_duration += transition_compensation # 贪心分配素材:按"已使用次数"升序排列候选素材(使用最少的优先), # 同次数随机打散,避免"A-B-C-D"的固定组合反复出现。 diff --git a/apps/worker/video_processing/unified_render_service.py b/apps/worker/video_processing/unified_render_service.py index 3906869b0..c04f5633b 100755 --- a/apps/worker/video_processing/unified_render_service.py +++ b/apps/worker/video_processing/unified_render_service.py @@ -200,6 +200,19 @@ class UnifiedRenderService: # 3. 计算视频总时长(用于字幕显示时长) video_duration = self._estimate_total_duration(layers) + # Debug: 输出各图层时长明细 + for layer in layers: + layer_total = sum(UnifiedRenderService._clip_adjusted_duration(c) for c in layer.clips) + clip_details = [ + f"{c.clip_id}(dur={c.duration:.3f},actual={c.actual_duration:.3f},speed={getattr(c, 'playback_speed', 1.0):.4f})" + for c in layer.clips + ] + logger.info( + "[debug] layer=%s clips=%d total=%.3f transition_duration=%.3f details=%s", + layer.role, len(layer.clips), layer_total, self.transition_duration, + ", ".join(clip_details), + ) + logger.info("[debug] estimated video_duration=%.3f", video_duration) # 3.5 TTS 配音生成(如果配置了) self._maybe_add_voiceover_layer(layers, video_duration=video_duration) @@ -1422,6 +1435,7 @@ class UnifiedRenderService: if trim_segments and len(trim_segments) > 1: # 多段裁剪:展开为多个 clip resolved_segments = TrimEngine.resolve_segments(trim_segments, actual_duration) + configured_speed = getattr(clip, "playback_speed", 1.0) or 1.0 for i, seg in enumerate(resolved_segments): # 每个段生成一个独立的 ResolvedClip seg_clip_id = f"{clip.id}_seg_{seg.segment_id}" @@ -1429,6 +1443,15 @@ class UnifiedRenderService: seg_start = seg.trim.start_time seg_duration = seg.trim.duration + # 多段裁剪:如果段的时长超过素材实际时长,减速补偿 + seg_speed = configured_speed + if actual_duration > 0 and seg_duration > actual_duration + 0.05: + seg_speed = max(0.25, round(configured_speed * actual_duration / seg_duration, 4)) + logger.info( + "[debug] multi-seg clip=%s seg=%s duration=%.3f actual=%.3f → speed=%.4f", + clip.id, seg.segment_id, seg_duration, actual_duration, seg_speed, + ) + rc = ResolvedClip( clip_id=seg_clip_id, asset_id=asset_id, @@ -1439,7 +1462,7 @@ class UnifiedRenderService: duration=seg_duration, transition_effect=clip.transition_effect or "cut", transition_duration=getattr(clip, "transition_duration", 0.0) or 0.0, - playback_speed=getattr(clip, "playback_speed", 1.0) or 1.0, + playback_speed=seg_speed, config={**clip_config, "_segment_id": seg.segment_id}, actual_duration=actual_duration, trim_config=seg.trim, @@ -1461,6 +1484,7 @@ class UnifiedRenderService: effective_trim: TrimConfig | None = None final_start = clip.start_time final_duration = clip.duration + configured_speed = getattr(clip, "playback_speed", 1.0) or 1.0 if trim_config is not None and actual_duration > 0: effective_trim = trim_config.validate_and_resolve(actual_duration) @@ -1474,6 +1498,21 @@ class UnifiedRenderService: final_start = 0.0 final_duration = actual_duration + # 素材实际时长不足以覆盖配置的时长时,降低播放速度来补偿 + # 例如:配置4s但素材只有3s → speed=0.75x,用满3s素材达到4s输出 + if actual_duration > 0 and final_duration > actual_duration + 0.05: + compensated_speed = actual_duration / final_duration + # 保留用户设置的速度(如果已减速则叠加) + final_speed = configured_speed * compensated_speed + # 下限 0.25x + final_speed = max(0.25, round(final_speed, 4)) + logger.info( + "[debug] clip=%s duration=%.3f actual=%.3f → 减速补偿 speed=%.4f (configured=%.3f)", + clip.id, final_duration, actual_duration, final_speed, configured_speed, + ) + else: + final_speed = configured_speed + rc = ResolvedClip( clip_id=clip.id, asset_id=asset_id, @@ -1484,13 +1523,20 @@ class UnifiedRenderService: duration=final_duration, transition_effect=clip.transition_effect or "cut", transition_duration=getattr(clip, "transition_duration", 0.0) or 0.0, - playback_speed=getattr(clip, "playback_speed", 1.0) or 1.0, + playback_speed=final_speed, config=clip_config, actual_duration=actual_duration, trim_config=effective_trim, ) resolved.append(rc) + # Debug日志:记录每个clip的时长信息 + eff_dur = _clip_effective_duration_pure(final_duration, actual_duration) + logger.info( + "[debug] resolved clip=%s duration=%.3f actual=%.3f effective=%.3f speed=%.4f start=%.3f", + clip.id, final_duration, actual_duration, eff_dur, final_speed, final_start, + ) + # 按 order 排序 resolved.sort(key=lambda c: c.order) return resolved @@ -1683,7 +1729,7 @@ class UnifiedRenderService: if d > 0: layer_dur = d break - xfade_filter, _ = self._transition_engine.build_xfade_chain( + xfade_filter, xfade_estimated_dur = self._transition_engine.build_xfade_chain( clip_durations=layer_durations, clip_video_labels=layer_labels, transitions=layer_transitions, @@ -1692,6 +1738,11 @@ class UnifiedRenderService: ) if xfade_filter: filter_parts.append(xfade_filter) + logger.info( + "[unified-render] layer=%s xfade: clips=%d durations=%s estimated_dur=%.3f", + layer.role, len(layer_labels), [round(d, 3) for d in layer_durations], + xfade_estimated_dur, + ) layer_output_labels[layer.role] = out_label # Step 3: 合成各层 @@ -1915,8 +1966,14 @@ class UnifiedRenderService: def _clip_effective_duration(clip: ResolvedClip) -> float: """计算 clip 的有效时长(原速 trim 后时长)。 + 如果 playback_speed < 1(为补偿素材不足而减速),返回配置的 duration, + 而非 min(duration, actual_duration)。 实际实现移至 packages.domain.render_layer_utils.clip_effective_duration。 """ + speed = getattr(clip, "playback_speed", 1.0) or 1.0 + # 减速场景:duration 已通过降低 playback_speed 补偿,返回配置的 duration + if speed < 1.0 - 1e-6 and clip.duration > 0: + return clip.duration return _clip_effective_duration_pure(clip.duration, clip.actual_duration) # ── 画中画(PiP)相关方法 ────────────────────────────────────────────────── diff --git a/packages/domain/xfade_builder.py b/packages/domain/xfade_builder.py index c5ec2a505..a4345e7c3 100755 --- a/packages/domain/xfade_builder.py +++ b/packages/domain/xfade_builder.py @@ -150,8 +150,11 @@ def build_xfade_filter_chain( else: first_input_dur = cumulative - total_transition - # 原始 offset 计算 - offset = max(0.0, cumulative - transition_duration * i) + # 正确的 offset 计算:offset 应相对于累积输出时长 + # offset = 累积输出中,转场开始的时间点 + # = first_input_dur - transition_duration + # 这样每个转场之间的"纯内容"时长等于原始 clip 时长 + offset = max(0.0, first_input_dur - transition_duration) # 安全钳制:offset + td 不能超过第一个输入的时长 available = max(0.0, first_input_dur - offset) diff --git a/tests/unit/test_asset_availability.py b/tests/unit/test_asset_availability.py index c37874394..4192a5ca8 100755 --- a/tests/unit/test_asset_availability.py +++ b/tests/unit/test_asset_availability.py @@ -197,14 +197,14 @@ class TestComputeAssetAvailability: def test_large_gap_remains_usable(self): """区间之间留有 ≥3s 空闲段(扩边后仍 ≥3s)→ usable=True。""" - # [0,2] 扩边到 [0,2.3],[5.3,10] 扩边前为 [5,10] 扩边起 4.7;空闲 [2.3,4.7]=2.4s <3 - # 改用更大间隙:[0,2] 与 [6,10],扩边后空闲 [2.3,5.7]=3.4s ≥3 + # [0,2] 扩边到 [0,3.5],[9,10] 扩边到 [7.5,10];空闲 [3.5,7.5]=4.0s >=3 + # 使用 [0,2] 与 [9,10],扩边后空闲 [3.5,7.5]=4.0s ≥3 → usable info = compute_asset_availability( _make_asset( duration=10.0, ranges=[ _range(0.0, 2.0, use_count=MAX_RANGE_USE_COUNT), - _range(6.0, 10.0, use_count=MAX_RANGE_USE_COUNT), + _range(9.0, 10.0, use_count=MAX_RANGE_USE_COUNT), ], ) ) @@ -236,8 +236,8 @@ class TestComputeAssetAvailability: assert info["usable"] is True def test_segment_edge_gap_constant(self): - """边缘间隙常量为 0.3s(与 MediaKit 冲突检测同口径)。""" - assert SEGMENT_EDGE_GAP == 0.3 + """边缘间隙常量为 1.5s(与 MediaKit 冲突检测同口径)。""" + assert SEGMENT_EDGE_GAP == 1.5 def test_domain_entity_metadata_dict_form(self): """领域实体形态(metadata 为 dict,无 classification_result)也能读到区间。 diff --git a/tests/unit/test_duration_compensation.py b/tests/unit/test_duration_compensation.py new file mode 100644 index 000000000..0022c3644 --- /dev/null +++ b/tests/unit/test_duration_compensation.py @@ -0,0 +1,95 @@ +"""Tests for duration compensation when source video is shorter than configured duration.""" +import pytest +from apps.worker.video_processing.unified_render_service import UnifiedRenderService +from apps.worker.video_processing.unified_render_service import ResolvedClip +from pathlib import Path + + +class TestClipEffectiveDurationWithSpeedCompensation: + """Test _clip_effective_duration handles speed < 1 correctly.""" + + def test_normal_speed_returns_min(self): + """When speed=1.0, effective_duration = min(duration, actual_duration).""" + clip = ResolvedClip( + clip_id="c1", + asset_id="a1", + local_path=Path("/tmp/fake.mp4"), + clip_type="main", + order=0, + start_time=0.0, + duration=4.0, + actual_duration=3.0, # shorter than configured + playback_speed=1.0, + ) + # Without speed compensation, effective = min(4, 3) = 3 + assert UnifiedRenderService._clip_effective_duration(clip) == 3.0 + + def test_compensated_speed_returns_configured_duration(self): + """When speed < 1 (compensated), effective_duration = configured duration.""" + clip = ResolvedClip( + clip_id="c1", + asset_id="a1", + local_path=Path("/tmp/fake.mp4"), + clip_type="main", + order=0, + start_time=0.0, + duration=4.0, + actual_duration=3.0, # shorter than configured + playback_speed=0.75, # compensated: 3/4 = 0.75 + ) + # With speed compensation, effective = configured duration = 4.0 + assert UnifiedRenderService._clip_effective_duration(clip) == 4.0 + + def test_zero_actual_duration_returns_configured(self): + """When actual_duration=0, effective_duration = configured duration.""" + clip = ResolvedClip( + clip_id="c1", + asset_id="a1", + local_path=Path("/tmp/fake.mp4"), + clip_type="main", + order=0, + start_time=0.0, + duration=4.0, + actual_duration=0.0, + playback_speed=1.0, + ) + assert UnifiedRenderService._clip_effective_duration(clip) == 4.0 + + def test_compensated_speed_with_actual_zero(self): + """When speed < 1 and actual=0, still returns configured duration.""" + clip = ResolvedClip( + clip_id="c1", + asset_id="a1", + local_path=Path("/tmp/fake.mp4"), + clip_type="main", + order=0, + start_time=0.0, + duration=4.0, + actual_duration=0.0, + playback_speed=0.5, + ) + assert UnifiedRenderService._clip_effective_duration(clip) == 4.0 + + +class TestClipAdjustedDurationWithSpeedCompensation: + """Test _clip_adjusted_duration accounts for compensated speed.""" + + def test_adjusted_duration_with_compensation(self): + """Adjusted duration = min(duration, actual) / speed. + With compensation: min(4,3)/0.75 = 3/0.75 = 4.0 + This equals the configured duration, which is the goal. + """ + clip = ResolvedClip( + clip_id="c1", + asset_id="a1", + local_path=Path("/tmp/fake.mp4"), + clip_type="main", + order=0, + start_time=0.0, + duration=4.0, + actual_duration=3.0, + playback_speed=0.75, # compensated + ) + adjusted = UnifiedRenderService._clip_adjusted_duration(clip) + # min(4,3)/0.75 = 3/0.75 = 4.0 (matches configured duration) + assert abs(adjusted - 4.0) < 0.01 diff --git a/tests/unit/test_editor_clips_random_start.py b/tests/unit/test_editor_clips_random_start.py index d3fb9a1ee..ac92e52b0 100644 --- a/tests/unit/test_editor_clips_random_start.py +++ b/tests/unit/test_editor_clips_random_start.py @@ -632,7 +632,8 @@ class TestReuseRatioGate: clips_data = _get_clips_data_from_call(mock_plan_svc) assert len(clips_data) == 13 # 1 个复用片段,占比 1/13 ≈ 7.7% ≤ 15% - assert reused.get("a1", 0.0) == 5.0 + # 转场补偿: raw_duration = 5.0 + (13-1)*0.5/13 ≈ 5.462 → round(5.462,1) = 5.5 + assert abs(reused.get("a1", 0.0) - 5.5) < 0.1 @patch("app.api.routes.templates_editor.clips.get_storage_service") def test_reuse_ratio_exceeded_returns_400(self, mock_storage): diff --git a/tests/unit/test_mediakit_conflicts.py b/tests/unit/test_mediakit_conflicts.py index 5c5a7cf96..93df22ead 100755 --- a/tests/unit/test_mediakit_conflicts.py +++ b/tests/unit/test_mediakit_conflicts.py @@ -32,16 +32,16 @@ class TestRecommendedTimeConflicts: assert _recommended_time_conflicts(10.0, 5.0, [(10.0, 15.0)]) is True def test_touching_endpoint_conflicts_due_to_edge_gap(self): - """首尾紧贴(推荐 15 开始,已用 [10,15]):0.3s 扩边内 → 冲突。""" + """首尾紧贴(推荐 15 开始,已用 [10,15]):1.5s 扩边内 → 冲突。""" assert _recommended_time_conflicts(15.0, 5.0, [(10.0, 15.0)]) is True def test_gap_within_edge_gap_conflicts(self): - """间隔 0.2s(< 0.3s 边缘间隙)→ 冲突。""" + """间隔 0.2s(< 1.5s 边缘间隙)→ 冲突。""" assert _recommended_time_conflicts(15.2, 5.0, [(10.0, 15.0)]) is True def test_gap_beyond_edge_gap_no_conflict(self): - """间隔 0.5s(> 0.3s 边缘间隙)→ 不冲突。""" - assert _recommended_time_conflicts(15.5, 5.0, [(10.0, 15.0)]) is False + """间隔 2.0s(> 1.5s 边缘间隙)→ 不冲突。""" + assert _recommended_time_conflicts(17.0, 5.0, [(10.0, 15.0)]) is False def test_far_apart_no_conflict(self): """相隔很远 → 不冲突。""" @@ -54,7 +54,7 @@ class TestRecommendedTimeConflicts: """多个已用区间,任一冲突即返回 True。""" used = [(0.0, 5.0), (10.0, 15.0), (20.0, 25.0)] assert _recommended_time_conflicts(12.0, 2.0, used) is True - assert _recommended_time_conflicts(6.0, 2.0, used) is False + assert _recommended_time_conflicts(6.5, 2.0, used) is False # range [6.5,8.5], just outside all expanded used ranges def test_custom_edge_gap(self): """edge_gap 可配置:gap=0 时紧贴不冲突(端点相接不算重叠)。""" @@ -64,5 +64,5 @@ class TestRecommendedTimeConflicts: assert _recommended_time_conflicts(15.5, 5.0, [(10.0, 15.0)], edge_gap=1.0) is True def test_default_edge_gap_constant(self): - """默认边缘间隙常量为 0.3s(配置常量)。""" - assert SEGMENT_EDGE_GAP == 0.3 + """默认边缘间隙常量为 1.5s(配置常量)。""" + assert SEGMENT_EDGE_GAP == 1.5 -- 2.54.0 From c49fad83fde0d6f991b4cad7d54b514ea7000670 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Tue, 1 Sep 2026 11:47:13 +0000 Subject: [PATCH 2/2] style: auto-format with black + isort + prettier [skip ci-format-check] --- .../app/api/routes/templates_editor/clips.py | 1 + .../unified_render_service.py | 28 +++++++++++++++---- tests/unit/test_duration_compensation.py | 8 ++++-- tests/unit/test_mediakit_conflicts.py | 4 ++- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/apps/api/app/api/routes/templates_editor/clips.py b/apps/api/app/api/routes/templates_editor/clips.py index a0fd097f3..3243dee4d 100755 --- a/apps/api/app/api/routes/templates_editor/clips.py +++ b/apps/api/app/api/routes/templates_editor/clips.py @@ -23,6 +23,7 @@ import re from app.auth import AuthenticatedUser, get_current_user from app.core.storage import get_storage_service from app.dependencies import get_asset_repository, get_db_session + # 默认转场时长(与 worker 端保持一致) _DEFAULT_TRANSITION_DURATION = 0.5 diff --git a/apps/worker/video_processing/unified_render_service.py b/apps/worker/video_processing/unified_render_service.py index c04f5633b..4dd3323a4 100755 --- a/apps/worker/video_processing/unified_render_service.py +++ b/apps/worker/video_processing/unified_render_service.py @@ -209,7 +209,10 @@ class UnifiedRenderService: ] logger.info( "[debug] layer=%s clips=%d total=%.3f transition_duration=%.3f details=%s", - layer.role, len(layer.clips), layer_total, self.transition_duration, + layer.role, + len(layer.clips), + layer_total, + self.transition_duration, ", ".join(clip_details), ) logger.info("[debug] estimated video_duration=%.3f", video_duration) @@ -1449,7 +1452,11 @@ class UnifiedRenderService: seg_speed = max(0.25, round(configured_speed * actual_duration / seg_duration, 4)) logger.info( "[debug] multi-seg clip=%s seg=%s duration=%.3f actual=%.3f → speed=%.4f", - clip.id, seg.segment_id, seg_duration, actual_duration, seg_speed, + clip.id, + seg.segment_id, + seg_duration, + actual_duration, + seg_speed, ) rc = ResolvedClip( @@ -1508,7 +1515,11 @@ class UnifiedRenderService: final_speed = max(0.25, round(final_speed, 4)) logger.info( "[debug] clip=%s duration=%.3f actual=%.3f → 减速补偿 speed=%.4f (configured=%.3f)", - clip.id, final_duration, actual_duration, final_speed, configured_speed, + clip.id, + final_duration, + actual_duration, + final_speed, + configured_speed, ) else: final_speed = configured_speed @@ -1534,7 +1545,12 @@ class UnifiedRenderService: eff_dur = _clip_effective_duration_pure(final_duration, actual_duration) logger.info( "[debug] resolved clip=%s duration=%.3f actual=%.3f effective=%.3f speed=%.4f start=%.3f", - clip.id, final_duration, actual_duration, eff_dur, final_speed, final_start, + clip.id, + final_duration, + actual_duration, + eff_dur, + final_speed, + final_start, ) # 按 order 排序 @@ -1740,7 +1756,9 @@ class UnifiedRenderService: filter_parts.append(xfade_filter) logger.info( "[unified-render] layer=%s xfade: clips=%d durations=%s estimated_dur=%.3f", - layer.role, len(layer_labels), [round(d, 3) for d in layer_durations], + layer.role, + len(layer_labels), + [round(d, 3) for d in layer_durations], xfade_estimated_dur, ) layer_output_labels[layer.role] = out_label diff --git a/tests/unit/test_duration_compensation.py b/tests/unit/test_duration_compensation.py index 0022c3644..abd4c08da 100644 --- a/tests/unit/test_duration_compensation.py +++ b/tests/unit/test_duration_compensation.py @@ -1,9 +1,11 @@ """Tests for duration compensation when source video is shorter than configured duration.""" -import pytest -from apps.worker.video_processing.unified_render_service import UnifiedRenderService -from apps.worker.video_processing.unified_render_service import ResolvedClip + from pathlib import Path +import pytest + +from apps.worker.video_processing.unified_render_service import ResolvedClip, UnifiedRenderService + class TestClipEffectiveDurationWithSpeedCompensation: """Test _clip_effective_duration handles speed < 1 correctly.""" diff --git a/tests/unit/test_mediakit_conflicts.py b/tests/unit/test_mediakit_conflicts.py index 93df22ead..c9f257be7 100755 --- a/tests/unit/test_mediakit_conflicts.py +++ b/tests/unit/test_mediakit_conflicts.py @@ -54,7 +54,9 @@ class TestRecommendedTimeConflicts: """多个已用区间,任一冲突即返回 True。""" used = [(0.0, 5.0), (10.0, 15.0), (20.0, 25.0)] assert _recommended_time_conflicts(12.0, 2.0, used) is True - assert _recommended_time_conflicts(6.5, 2.0, used) is False # range [6.5,8.5], just outside all expanded used ranges + assert ( + _recommended_time_conflicts(6.5, 2.0, used) is False + ) # range [6.5,8.5], just outside all expanded used ranges def test_custom_edge_gap(self): """edge_gap 可配置:gap=0 时紧贴不冲突(端点相接不算重叠)。""" -- 2.54.0