test: add provisional adaptive sync calibration

This commit is contained in:
Brad Stein 2026-05-02 15:51:11 -03:00
parent 160cbffbd4
commit eec490b407
7 changed files with 166 additions and 19 deletions

View File

@ -409,3 +409,23 @@ not degrade into camera-only evidence.
- [x] Run shell syntax checks, focused contract tests, and package checks.
- [x] Push clean semver `0.17.19` for installed client/server testing.
- [ ] Re-run only after `LESAVKA_MIC_SOURCE` is listed by the local audio stack.
## 0.17.20 Provisional Adaptive Calibration Checklist
Context: the 0.17.19 adaptive run reached live media and produced browser-visible sync evidence,
but the probe still did not calibrate. Each segment either had only 3 coded pairs or an analyzer
failure, so the existing `calibration.ready=true` gate refused every adjustment. That was safe for
saved defaults, but wrong for the user-directed adaptive probe goal: live segments should be allowed
to make bounded provisional corrections from imperfect-but-usable evidence, then let the next segment
judge whether the correction helped.
- [x] Treat the 0.17.19 run as a measurement-only run, not a calibration run.
- [x] Keep permanent saved calibration gated by analyzer-ready evidence.
- [x] Add provisional adaptive calibration mode enabled by adaptive runs by default.
- [x] Allow provisional corrections from low-but-usable paired-pulse reports when p95 and drift are bounded.
- [x] Use median skew as the provisional correction source and default to video offset adjustment.
- [x] Clamp provisional correction gain and max step so one noisy segment cannot swing the site offset wildly.
- [x] Record decision mode and provisional recommendations in segment metrics.
- [x] Update manual probe contract tests for provisional calibration controls and output.
- [x] Run shell syntax checks, focused contract tests, and package checks.
- [ ] Push clean semver `0.17.20` for installed client/server testing.

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lesavka_client"
version = "0.17.19"
version = "0.17.20"
dependencies = [
"anyhow",
"async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]]
name = "lesavka_common"
version = "0.17.19"
version = "0.17.20"
dependencies = [
"anyhow",
"base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]]
name = "lesavka_server"
version = "0.17.19"
version = "0.17.20"
dependencies = [
"anyhow",
"base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.17.19"
version = "0.17.20"
edition = "2024"
[dependencies]

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.17.19"
version = "0.17.20"
edition = "2024"
build = "build.rs"

View File

@ -32,6 +32,12 @@ LESAVKA_SYNC_CALIBRATION_SEGMENTS=${LESAVKA_SYNC_CALIBRATION_SEGMENTS:-1}
LESAVKA_SYNC_CONTINUOUS_BROWSER=${LESAVKA_SYNC_CONTINUOUS_BROWSER:-${LESAVKA_SYNC_ADAPTIVE_CALIBRATION}}
LESAVKA_SYNC_CONTINUE_ON_ANALYSIS_FAILURE=${LESAVKA_SYNC_CONTINUE_ON_ANALYSIS_FAILURE:-${LESAVKA_SYNC_ADAPTIVE_CALIBRATION}}
LESAVKA_SYNC_SEGMENT_SETTLE_SECONDS=${LESAVKA_SYNC_SEGMENT_SETTLE_SECONDS:-3}
LESAVKA_SYNC_PROVISIONAL_CALIBRATION=${LESAVKA_SYNC_PROVISIONAL_CALIBRATION:-${LESAVKA_SYNC_ADAPTIVE_CALIBRATION}}
LESAVKA_SYNC_PROVISIONAL_MIN_PAIRS=${LESAVKA_SYNC_PROVISIONAL_MIN_PAIRS:-3}
LESAVKA_SYNC_PROVISIONAL_MAX_P95_MS=${LESAVKA_SYNC_PROVISIONAL_MAX_P95_MS:-350}
LESAVKA_SYNC_PROVISIONAL_MAX_DRIFT_MS=${LESAVKA_SYNC_PROVISIONAL_MAX_DRIFT_MS:-250}
LESAVKA_SYNC_PROVISIONAL_GAIN=${LESAVKA_SYNC_PROVISIONAL_GAIN:-0.5}
LESAVKA_SYNC_PROVISIONAL_MAX_STEP_US=${LESAVKA_SYNC_PROVISIONAL_MAX_STEP_US:-150000}
STIMULUS_PORT=${STIMULUS_PORT:-18444}
STIMULUS_SETTLE_SECONDS=${STIMULUS_SETTLE_SECONDS:-10}
LOCAL_OUTPUT_DIR=${LOCAL_OUTPUT_DIR:-"${REPO_ROOT}/tmp"}
@ -289,6 +295,8 @@ maybe_apply_probe_calibration() {
local summary
if ! summary="$(python3 - "${report_json}" "${LESAVKA_SYNC_CALIBRATION_TARGET}" <<'PY'
import json
import math
import os
import shlex
import sys
@ -302,25 +310,115 @@ verdict = report.get("verdict", {})
if target not in {"audio", "video"}:
target = "video"
audio_recommendation = int(cal.get("recommended_audio_offset_adjust_us") or 0)
video_recommendation = int(cal.get("recommended_video_offset_adjust_us") or 0)
def env_bool(name, default=False):
raw = os.environ.get(name)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
def env_int(name, default):
try:
return int(os.environ.get(name, str(default)))
except ValueError:
return default
def env_float(name, default):
try:
return float(os.environ.get(name, str(default)))
except ValueError:
return default
def as_float(value, default=0.0):
try:
parsed = float(value)
except (TypeError, ValueError):
return default
return parsed if math.isfinite(parsed) else default
def clamp(value, limit):
limit = abs(int(limit))
return max(-limit, min(limit, int(round(value))))
ready = bool(cal.get("ready"))
paired_pulses = int(report.get("paired_event_count", 0) or 0)
median_skew_ms = as_float(report.get("median_skew_ms", 0.0))
p95_abs_skew_ms = as_float(verdict.get("p95_abs_skew_ms", 0.0))
drift_ms = as_float(report.get("drift_ms", 0.0))
provisional_enabled = env_bool("LESAVKA_SYNC_PROVISIONAL_CALIBRATION", False)
provisional_min_pairs = env_int("LESAVKA_SYNC_PROVISIONAL_MIN_PAIRS", 3)
provisional_max_p95_ms = env_float("LESAVKA_SYNC_PROVISIONAL_MAX_P95_MS", 350.0)
provisional_max_drift_ms = env_float("LESAVKA_SYNC_PROVISIONAL_MAX_DRIFT_MS", 250.0)
provisional_gain = env_float("LESAVKA_SYNC_PROVISIONAL_GAIN", 0.5)
provisional_max_step_us = env_int("LESAVKA_SYNC_PROVISIONAL_MAX_STEP_US", 150000)
ready_audio_recommendation = int(cal.get("recommended_audio_offset_adjust_us") or 0)
ready_video_recommendation = int(cal.get("recommended_video_offset_adjust_us") or 0)
provisional_audio_recommendation = int(round(-median_skew_ms * 1000.0))
provisional_video_recommendation = int(round(median_skew_ms * 1000.0))
audio_recommendation = ready_audio_recommendation
video_recommendation = ready_video_recommendation
audio_delta = audio_recommendation if target == "audio" else 0
video_delta = video_recommendation if target == "video" else 0
decision_mode = "ready" if ready else "refused"
decision_note = "analyzer marked this report calibration-ready" if ready else "analyzer did not mark this report calibration-ready"
if not ready and provisional_enabled:
refusal_reasons = []
if paired_pulses < provisional_min_pairs:
refusal_reasons.append(f"paired_pulses {paired_pulses} < {provisional_min_pairs}")
if p95_abs_skew_ms > provisional_max_p95_ms:
refusal_reasons.append(f"p95_abs_skew_ms {p95_abs_skew_ms:.1f} > {provisional_max_p95_ms:.1f}")
if abs(drift_ms) > provisional_max_drift_ms:
refusal_reasons.append(f"abs(drift_ms) {abs(drift_ms):.1f} > {provisional_max_drift_ms:.1f}")
if refusal_reasons:
decision_note = "provisional calibration refused: " + "; ".join(refusal_reasons)
else:
audio_recommendation = provisional_audio_recommendation
video_recommendation = provisional_video_recommendation
if target == "audio":
audio_delta = clamp(audio_recommendation * provisional_gain, provisional_max_step_us)
video_delta = 0
else:
audio_delta = 0
video_delta = clamp(video_recommendation * provisional_gain, provisional_max_step_us)
if audio_delta == 0 and video_delta == 0:
decision_note = "provisional calibration skipped: rounded correction was zero"
else:
decision_mode = "provisional"
decision_note = (
"bounded provisional correction from median skew; "
"not safe to save until a confirming ready report"
)
fields = {
"report_json": report_path,
"calibration_ready": str(bool(cal.get("ready"))).lower(),
"calibration_ready": str(ready).lower(),
"calibration_target": target,
"calibration_decision_mode": decision_mode,
"calibration_decision_note": decision_note,
"calibration_audio_recommendation_us": audio_recommendation,
"calibration_video_recommendation_us": video_recommendation,
"calibration_ready_audio_recommendation_us": ready_audio_recommendation,
"calibration_ready_video_recommendation_us": ready_video_recommendation,
"calibration_provisional_audio_recommendation_us": provisional_audio_recommendation,
"calibration_provisional_video_recommendation_us": provisional_video_recommendation,
"calibration_apply_audio_delta_us": audio_delta,
"calibration_apply_video_delta_us": video_delta,
"calibration_note": cal.get("note", ""),
"provisional_calibration_enabled": str(provisional_enabled).lower(),
"provisional_min_pairs": provisional_min_pairs,
"provisional_max_p95_ms": f"{provisional_max_p95_ms:.1f}",
"provisional_max_drift_ms": f"{provisional_max_drift_ms:.1f}",
"provisional_gain": f"{provisional_gain:.3f}",
"provisional_max_step_us": provisional_max_step_us,
"verdict_status": verdict.get("status", ""),
"paired_pulses": report.get("paired_event_count", 0),
"median_skew_ms": f"{float(report.get('median_skew_ms', 0.0)):+.1f}",
"p95_abs_skew_ms": f"{float(verdict.get('p95_abs_skew_ms', 0.0)):.1f}",
"drift_ms": f"{float(report.get('drift_ms', 0.0)):+.1f}",
"paired_pulses": paired_pulses,
"median_skew_ms": f"{median_skew_ms:+.1f}",
"p95_abs_skew_ms": f"{p95_abs_skew_ms:.1f}",
"drift_ms": f"{drift_ms:+.1f}",
}
for key, value in fields.items():
print(f"{key}={shlex.quote(str(value))}")
@ -340,16 +438,28 @@ PY
echo " ↪ drift_ms=${drift_ms}"
echo " ↪ calibration_ready=${calibration_ready}"
echo " ↪ calibration_target=${calibration_target}"
echo " ↪ calibration_decision_mode=${calibration_decision_mode}"
echo " ↪ recommended_audio_offset_adjust_us=${calibration_audio_recommendation_us}"
echo " ↪ recommended_video_offset_adjust_us=${calibration_video_recommendation_us}"
echo " ↪ ready_audio_offset_adjust_us=${calibration_ready_audio_recommendation_us}"
echo " ↪ ready_video_offset_adjust_us=${calibration_ready_video_recommendation_us}"
echo " ↪ provisional_audio_offset_adjust_us=${calibration_provisional_audio_recommendation_us}"
echo " ↪ provisional_video_offset_adjust_us=${calibration_provisional_video_recommendation_us}"
echo " ↪ provisional_calibration_enabled=${provisional_calibration_enabled}"
echo " ↪ provisional_min_pairs=${provisional_min_pairs}"
echo " ↪ provisional_max_p95_ms=${provisional_max_p95_ms}"
echo " ↪ provisional_max_drift_ms=${provisional_max_drift_ms}"
echo " ↪ provisional_gain=${provisional_gain}"
echo " ↪ provisional_max_step_us=${provisional_max_step_us}"
echo " ↪ calibration_note=${calibration_note}"
echo " ↪ calibration_decision_note=${calibration_decision_note}"
if [[ "${LESAVKA_SYNC_APPLY_CALIBRATION}" != "1" ]]; then
echo " ↪ calibration apply disabled; set LESAVKA_SYNC_APPLY_CALIBRATION=1 to apply a ready recommendation"
echo " ↪ calibration apply disabled; set LESAVKA_SYNC_APPLY_CALIBRATION=1 to apply ready or provisional recommendations"
return 0
fi
if [[ "${calibration_ready}" != "true" ]]; then
echo " ↪ calibration apply refused: analyzer did not mark this report calibration-ready"
if [[ "${calibration_decision_mode}" == "refused" ]]; then
echo " ↪ calibration apply refused: ${calibration_decision_note}"
return 0
fi
if [[ "${calibration_apply_audio_delta_us}" == "0" && "${calibration_apply_video_delta_us}" == "0" ]]; then
@ -357,7 +467,7 @@ PY
return 0
fi
local note="mirrored probe ${STAMP} ${label}: target=${calibration_target}, median=${median_skew_ms}ms, p95=${p95_abs_skew_ms}ms, pairs=${paired_pulses}"
local note="mirrored probe ${STAMP} ${label}: mode=${calibration_decision_mode}, target=${calibration_target}, median=${median_skew_ms}ms, p95=${p95_abs_skew_ms}ms, pairs=${paired_pulses}"
echo " ↪ applying calibration: audio_delta_us=${calibration_apply_audio_delta_us}, video_delta_us=${calibration_apply_video_delta_us}"
LESAVKA_TLS_DOMAIN="${LESAVKA_TLS_DOMAIN}" \
"${REPO_ROOT}/target/debug/lesavka-relayctl" \
@ -365,13 +475,15 @@ PY
calibrate "${calibration_apply_audio_delta_us}" "${calibration_apply_video_delta_us}" "${note}" \
| sed 's/^/ ↪ /'
if [[ "${LESAVKA_SYNC_SAVE_CALIBRATION}" == "1" ]]; then
if [[ "${LESAVKA_SYNC_SAVE_CALIBRATION}" == "1" && "${calibration_decision_mode}" == "ready" ]]; then
echo " ↪ saving active calibration as site default"
LESAVKA_TLS_DOMAIN="${LESAVKA_TLS_DOMAIN}" \
"${REPO_ROOT}/target/debug/lesavka-relayctl" \
--server "${RESOLVED_LESAVKA_SERVER_ADDR}" \
calibration-save-default \
| sed 's/^/ ↪ /'
elif [[ "${LESAVKA_SYNC_SAVE_CALIBRATION}" == "1" ]]; then
echo " ↪ provisional calibration not saved; require a confirming ready rerun before saving defaults"
else
echo " ↪ active calibration not saved; set LESAVKA_SYNC_SAVE_CALIBRATION=1 after a confirming rerun"
fi
@ -595,8 +707,12 @@ for segment in range(1, segment_count + 1):
"analysis_raw_first_audio_activity_s": as_float(str(failure.get("raw_first_audio_activity_s", ""))),
"calibration_ready": bool(calibration.get("ready", False)),
"calibration_note": calibration.get("note", ""),
"decision_mode": decision.get("calibration_decision_mode", ""),
"decision_note": decision.get("calibration_decision_note", ""),
"decision_video_delta_us": as_float(decision.get("calibration_apply_video_delta_us")),
"decision_audio_delta_us": as_float(decision.get("calibration_apply_audio_delta_us")),
"decision_provisional_video_recommendation_us": as_float(decision.get("calibration_provisional_video_recommendation_us")),
"decision_provisional_audio_recommendation_us": as_float(decision.get("calibration_provisional_audio_recommendation_us")),
"planner_phase_before": planner_before.get("planner_phase", ""),
"planner_phase_after": planner_after.get("planner_phase", ""),
"planner_live_lag_ms_before": as_float(planner_before.get("planner_live_lag_ms")),

View File

@ -10,7 +10,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.17.19"
version = "0.17.20"
edition = "2024"
autobins = false

View File

@ -127,6 +127,12 @@ fn mirrored_sync_script_uses_real_client_capture_path() {
"LESAVKA_SYNC_CONTINUOUS_BROWSER=${LESAVKA_SYNC_CONTINUOUS_BROWSER:-${LESAVKA_SYNC_ADAPTIVE_CALIBRATION}}",
"LESAVKA_SYNC_CONTINUE_ON_ANALYSIS_FAILURE=${LESAVKA_SYNC_CONTINUE_ON_ANALYSIS_FAILURE:-${LESAVKA_SYNC_ADAPTIVE_CALIBRATION}}",
"LESAVKA_SYNC_SEGMENT_SETTLE_SECONDS=${LESAVKA_SYNC_SEGMENT_SETTLE_SECONDS:-3}",
"LESAVKA_SYNC_PROVISIONAL_CALIBRATION=${LESAVKA_SYNC_PROVISIONAL_CALIBRATION:-${LESAVKA_SYNC_ADAPTIVE_CALIBRATION}}",
"LESAVKA_SYNC_PROVISIONAL_MIN_PAIRS=${LESAVKA_SYNC_PROVISIONAL_MIN_PAIRS:-3}",
"LESAVKA_SYNC_PROVISIONAL_MAX_P95_MS=${LESAVKA_SYNC_PROVISIONAL_MAX_P95_MS:-350}",
"LESAVKA_SYNC_PROVISIONAL_MAX_DRIFT_MS=${LESAVKA_SYNC_PROVISIONAL_MAX_DRIFT_MS:-250}",
"LESAVKA_SYNC_PROVISIONAL_GAIN=${LESAVKA_SYNC_PROVISIONAL_GAIN:-0.5}",
"LESAVKA_SYNC_PROVISIONAL_MAX_STEP_US=${LESAVKA_SYNC_PROVISIONAL_MAX_STEP_US:-150000}",
"LESAVKA_SYNC_ADAPTIVE_CALIBRATION",
"LESAVKA_SYNC_CALIBRATION_SEGMENTS=4",
"browser_consumer_reuse_session=${reuse_browser_session}",
@ -147,13 +153,18 @@ fn mirrored_sync_script_uses_real_client_capture_path() {
"probe_activity_start_delta_ms",
"blind-targets.json",
"no segment produced a passing probe verdict; refusing to invent blind targets",
"decision_mode",
"decision_provisional_video_recommendation_us",
"planner_live_lag_ms_after",
"probe_p95_abs_skew_ms",
"settling ${LESAVKA_SYNC_SEGMENT_SETTLE_SECONDS}s before next calibration segment",
"print_upstream_calibration_state \"before mirrored run\"",
"maybe_apply_probe_calibration",
"calibration_ready=${calibration_ready}",
"calibration apply refused: analyzer did not mark this report calibration-ready",
"calibration_decision_mode=${calibration_decision_mode}",
"bounded provisional correction from median skew",
"provisional calibration not saved",
"calibration apply refused: ${calibration_decision_note}",
"calibrate \"${calibration_apply_audio_delta_us}\" \"${calibration_apply_video_delta_us}\"",
"calibration-save-default",
"print_upstream_sync_state \"after mirrored run\"",