feat(orchestration): patter-duration estimator + boundary-fit selection — land handoffs near the boundary (gh-#253, gh-#254) - #287
Merged
Conversation
…idence tiers (gh-#253) IPatterDurationEstimator + PatterDurationEstimate/PatterEstimateConfidence (GenWave.Abstractions): planning-time estimates for not-yet-rendered patter, tiered Exact / Historical / Heuristic so a consumer can widen tolerance as confidence drops. No behavior change — the seam only exposes numbers. RollingPatterDurationEstimator (GenWave.Orchestration), in-memory only: - Exact: StationId's cache-stable clip (deterministic template per station voice) replays verbatim, so its last measured duration IS the next one. - Historical: per-persona x per-kind rolling average (last 20 measured F66.1 durations, >=3 samples). NOT booth-log-backed: station.booth_log patter rows carry no duration column, so a 14-day DB average would need a schema migration AND an event change — the in-process ring warms within a few units instead; no migration added. - Heuristic: ~15 chars/s over the kind's expected copy, live Llm:MaxCopyChars (new ICopyBoundsProvider seam, Tts-side OptionsMonitor adapter) bounding the LLM worst case. <3 real samples use the average but report Heuristic. Orchestrator feeds the seam: every successful render's MEASURED DurationMs (null cue = nothing observed, never fabricated) flows back via ObserveRendered, keyed by the request's own kind/persona/voice.
…ff track near the boundary (gh-#254) As a schedule boundary enters the F74.3 lookahead window, the in-window music pick becomes a genuine duration FIT instead of a raw duration-vs-due comparison. BuildBoundaryFit accounts, in air order, for: - queued-ahead drift: new PlayoutContext.QueuedAheadMs (additive, null = unknown = zero), measured by the feeder from state it already holds — on-air remaining time + any still-queued pushed backlog, zero extra I/O; - this unit's own pre-music patter (back-announce / station-id trigger / lead-in), estimated via the gh-#253 seam under the unit's one cadence + identity snapshot (reads hoisted to GetNextAsync, still once per unit); - the candidate itself, minus a 5s expected crossfade trim (midpoint of the engine's GW_XFADE_MIN..MAX 2..8s defaults — judged constant, error well inside tolerance); - the break's pre-boundary patter: next unit's back-announce + the sign-off piece (persona/voice from the deferral's own captured HandoffContext; boundary recovered as Due + SignOffLeadTime). Sign-on airs on the far side of the boundary and never counts. Tolerance-by-confidence: +-30s is a WIN, widened to 45s (historical) / 60s (heuristic) by the worst contributing estimate tier. Degenerate-pick guard: the FIRST rotation-tiered random sample inside the tolerance is kept as-is (sampling stops — no closest-fit leaderboard converging on one track every hour); min-diff engages only when nothing lands, ranking only this pick's up-to-5 random samples, so even inevitable overshoot picks the least-late of a rotating handful. Soft preference throughout: same pool query, every predicate (posture/rotation/envelope/never-play) untouched, never a filter, never dead air. No pending in-window deferral = the single plain pick, byte-for-byte the pre-fit behavior. SpeechDeferralQueue gains read-only PeekNextDue() (same Due-asc, kind-tiebreak contract as TryDequeueDue) so the fit can see WHAT is coming, not just when.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #253, fixes #254 — the estimator seam, then the boundary fit that consumes it. Direct answer to the live 4:03 handover drift.
⏱️ gh-#253 — IPatterDurationEstimator, honest tiers
Llm:MaxCopyChars, 2s floor.station.booth_logpatter rows carry kind/summary/persona but no duration, andSegmentGeneratednever carried one. A DB-backed average needs a migration + event change, so per house rules none was added: the historical tier is in-process and warms within a few units after boot; cold tier covers restarts honestly. Restart-surviving history filed as a rider.🕓 gh-#254 — boundary-fit selection
QueuedAheadMs: on-air remaining + pending backlog, computed once per refill, zero extra I/O — this is the actual drift source from the 4:03 handover) − this unit's own pre-music patter − expected crossfade trim.✅ Verification
dotnet build0 warnings. Orchestration 186 (18 new specs), Core 140 (2 new), Host 1228, Tts 249 — all green. MediaLibrary suite untouched/not run (its fixture spins dockerized Postgres; no MediaLibrary code changed).🔭 Live-only
Watch the demo box across a few real boundaries: handover lateness (the 5–6 min repro should collapse toward ±tolerance), no repetitive-short-track feel near hours, estimator warm-up quality, and the 5s crossfade-trim constant vs the box's real GW_XFADE settings.