Backend (Phase A): - A1: Adaptive silence buffer — natural_gap_ms persisted per cue; renderer computes per-cue silence_before/silence_after instead of fixed 500ms; per-cue silence files - A2: Forward-preferred snap — snap_pause_point prefers boundaries up to 4s ahead over boundaries within 1.5s behind, reducing mid-scene cuts - A3: Min-gap validation — pause points with < 200ms gap trigger forward search to the next acceptable gap - natural_gap_ms added to PausePointData model and api.ts type - New config fields: whisper_snap_forward_window, whisper_snap_backward_window, ad_silence_buffer_default, ad_silence_buffer_min_after, ad_min_acceptable_gap - Tests: test_whisper_snap.py (13 tests), test_video_renderer_buffers.py Frontend (Phase B): - B1: Drag pause-point markers — pointer state machine with 3px move threshold, clamp to min/max bounds, click-without-move still opens PausePointEditor - B2: Drag freeze blocks — orange blocks translate with linked pause point - B3: Time tooltip visible during drag, hidden on release - Tests: TimelinePreview.drag.test.tsx (10 tests) Fixes: - Share link pointed to ai-sandbox.oliver.solutions — added app_url to Settings with correct optical-dev.oliver.solutions default; share_url now configurable via APP_URL env var - Removed all ai-sandbox.oliver.solutions references from docker-compose, apache config, docs, and scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
90 lines
3.7 KiB
Python
90 lines
3.7 KiB
Python
"""Tests for adaptive silence buffer formula in video_renderer.py (A1).
|
|
|
|
The renderer lives behind heavy GCP + FFmpeg deps only available in Docker.
|
|
These tests cover the pure arithmetic used inside _render_pause_insert_method;
|
|
they do not import VideoRendererService to stay runnable locally via pytest.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
# ── Pure formula tests (no FFmpeg, no GCS) ───────────────────────────────────
|
|
#
|
|
# Mirrors the exact formula in _render_pause_insert_method:
|
|
# natural_gap = natural_gap_ms / 1000.0
|
|
# silence_before = max(0.05, default_buf - natural_gap * 0.5)
|
|
# silence_after = max(min_after, default_buf - natural_gap * 0.3)
|
|
|
|
def _buffers(
|
|
natural_gap_ms: float,
|
|
default_buf: float = 0.5,
|
|
min_after: float = 0.1,
|
|
) -> tuple[float, float]:
|
|
natural_gap = natural_gap_ms / 1000.0
|
|
silence_before = max(0.05, default_buf - natural_gap * 0.5)
|
|
silence_after = max(min_after, default_buf - natural_gap * 0.3)
|
|
return silence_before, silence_after
|
|
|
|
|
|
@pytest.mark.parametrize("natural_gap_ms,exp_before,exp_after", [
|
|
# No natural gap → full default buffers
|
|
(0, 0.50, 0.50),
|
|
# 200 ms gap: before = 0.5 - 0.1 = 0.40; after = 0.5 - 0.06 = 0.44
|
|
(200, 0.40, 0.44),
|
|
# 500 ms gap: before = 0.5 - 0.25 = 0.25; after = 0.5 - 0.15 = 0.35
|
|
(500, 0.25, 0.35),
|
|
# 1000 ms gap: before = max(0.05, 0.5-0.5)=0.05; after = max(0.1, 0.5-0.3)=0.20
|
|
(1000, 0.05, 0.20),
|
|
# 1500 ms gap: before=0.05 (floor); after = max(0.1, 0.5-0.45)=0.10 (floor)
|
|
(1500, 0.05, 0.10),
|
|
# 2000 ms gap: both at their floors
|
|
(2000, 0.05, 0.10),
|
|
])
|
|
def test_buffer_formula(natural_gap_ms, exp_before, exp_after):
|
|
before, after = _buffers(natural_gap_ms)
|
|
assert before == pytest.approx(exp_before, abs=0.001)
|
|
assert after == pytest.approx(exp_after, abs=0.001)
|
|
|
|
|
|
def test_total_freeze_duration_uses_adaptive_buffers():
|
|
"""total_freeze_duration = ad_duration + silence_before + silence_after."""
|
|
ad_duration = 5.0
|
|
natural_gap_ms = 800.0 # 800ms natural gap
|
|
|
|
before, after = _buffers(natural_gap_ms)
|
|
total = ad_duration + before + after
|
|
|
|
# before = max(0.05, 0.5 - 0.4) = 0.10; after = max(0.1, 0.5 - 0.24) = 0.26
|
|
assert total == pytest.approx(ad_duration + before + after, abs=0.001)
|
|
# Sanity: less than the old constant 1.0s overhead when there's a natural gap
|
|
assert (before + after) < 1.0
|
|
|
|
|
|
def test_buffers_never_below_floor():
|
|
"""silence_before never < 0.05, silence_after never < 0.10, regardless of gap size."""
|
|
for gap_ms in [0, 100, 500, 1000, 5000, 10000]:
|
|
before, after = _buffers(gap_ms)
|
|
assert before >= 0.05, f"silence_before={before} below floor for gap={gap_ms}ms"
|
|
assert after >= 0.10, f"silence_after={after} below floor for gap={gap_ms}ms"
|
|
|
|
|
|
def test_large_natural_gap_has_less_total_overhead_than_small_gap():
|
|
"""Larger natural gap → smaller combined silence overhead."""
|
|
before_small, after_small = _buffers(100)
|
|
before_large, after_large = _buffers(900)
|
|
|
|
assert (before_small + after_small) > (before_large + after_large)
|
|
|
|
|
|
def test_renderer_config_defaults_match_formula():
|
|
"""The config defaults used in the formula match the expected values."""
|
|
# These must stay in sync with config.py defaults:
|
|
# ad_silence_buffer_default: float = 0.5
|
|
# ad_silence_buffer_min_after: float = 0.1
|
|
DEFAULT_BUF = 0.5
|
|
MIN_AFTER = 0.1
|
|
assert DEFAULT_BUF == pytest.approx(0.5)
|
|
assert MIN_AFTER == pytest.approx(0.1)
|
|
# Verify floors are derived from these values
|
|
_, after = _buffers(10_000, DEFAULT_BUF, MIN_AFTER) # saturated gap
|
|
assert after == pytest.approx(MIN_AFTER)
|