test: add provisional adaptive sync calibration
This commit is contained in:
parent
160cbffbd4
commit
eec490b407
20
AGENTS.md
20
AGENTS.md
@ -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
6
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.17.19"
|
||||
version = "0.17.20"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.17.19"
|
||||
version = "0.17.20"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -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")),
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.17.19"
|
||||
version = "0.17.20"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -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\"",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user