video-accessibility/backend/tests/unit/test_video_renderer_buffers.py
Vadym Samoilenko 2f4925353a feat(pause-insert): adaptive buffer, forward-snap, timeline drag + share link fix
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>
2026-05-01 16:09:09 +01:00

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)