|
|
|
|
@ -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")),
|
|
|
|
|
|