| @@ -15,14 +15,39 @@ func WantsClass(values []string, class string) bool { | |||||
| } | } | ||||
| func CandidatePriorityBoost(policy Policy, hint string) float64 { | func CandidatePriorityBoost(policy Policy, hint string) float64 { | ||||
| boost := hintMatchBoost(policy.SignalPriorities, hint, 3.0) | |||||
| boost += hintMatchBoost(policy.AutoRecordClasses, hint, 1.5) | |||||
| boost += hintMatchBoost(policy.AutoDecodeClasses, hint, 1.0) | |||||
| return boost | |||||
| } | |||||
| func DecisionPriorityBoost(policy Policy, hint string, class string, queue string) float64 { | |||||
| tag := strings.TrimSpace(hint) | |||||
| if tag == "" { | |||||
| tag = strings.TrimSpace(class) | |||||
| } | |||||
| boost := CandidatePriorityBoost(policy, tag) | |||||
| switch strings.ToLower(strings.TrimSpace(queue)) { | |||||
| case "record": | |||||
| boost += hintMatchBoost(policy.AutoRecordClasses, tag, 3.0) | |||||
| case "decode": | |||||
| boost += hintMatchBoost(policy.AutoDecodeClasses, tag, 3.0) | |||||
| } | |||||
| return boost | |||||
| } | |||||
| func hintMatchBoost(values []string, hint string, weight float64) float64 { | |||||
| h := strings.ToLower(strings.TrimSpace(hint)) | h := strings.ToLower(strings.TrimSpace(hint)) | ||||
| for i, want := range policy.SignalPriorities { | |||||
| if h == "" || len(values) == 0 { | |||||
| return 0 | |||||
| } | |||||
| for i, want := range values { | |||||
| w := strings.ToLower(strings.TrimSpace(want)) | w := strings.ToLower(strings.TrimSpace(want)) | ||||
| if w == "" { | if w == "" { | ||||
| continue | continue | ||||
| } | } | ||||
| if strings.Contains(h, w) || strings.Contains(w, h) { | if strings.Contains(h, w) || strings.Contains(w, h) { | ||||
| return float64(len(policy.SignalPriorities)-i) * 3.0 | |||||
| return float64(len(values)-i) * weight | |||||
| } | } | ||||
| } | } | ||||
| return 0 | return 0 | ||||
| @@ -50,3 +50,8 @@ type RefinementResult struct { | |||||
| Decisions []SignalDecision `json:"decisions,omitempty"` | Decisions []SignalDecision `json:"decisions,omitempty"` | ||||
| Candidates []Candidate `json:"candidates,omitempty"` | Candidates []Candidate `json:"candidates,omitempty"` | ||||
| } | } | ||||
| type RefinementStep struct { | |||||
| Input RefinementInput `json:"input"` | |||||
| Result RefinementResult `json:"result"` | |||||
| } | |||||
| @@ -15,6 +15,7 @@ type Policy struct { | |||||
| SurveillanceFPS int `json:"surveillance_fps"` | SurveillanceFPS int `json:"surveillance_fps"` | ||||
| DisplayBins int `json:"display_bins"` | DisplayBins int `json:"display_bins"` | ||||
| DisplayFPS int `json:"display_fps"` | DisplayFPS int `json:"display_fps"` | ||||
| SurveillanceStrategy string `json:"surveillance_strategy"` | |||||
| RefinementEnabled bool `json:"refinement_enabled"` | RefinementEnabled bool `json:"refinement_enabled"` | ||||
| MaxRefinementJobs int `json:"max_refinement_jobs"` | MaxRefinementJobs int `json:"max_refinement_jobs"` | ||||
| RefinementMaxConcurrent int `json:"refinement_max_concurrent"` | RefinementMaxConcurrent int `json:"refinement_max_concurrent"` | ||||
| @@ -41,6 +42,7 @@ func PolicyFromConfig(cfg config.Config) Policy { | |||||
| SurveillanceFPS: cfg.Surveillance.FrameRate, | SurveillanceFPS: cfg.Surveillance.FrameRate, | ||||
| DisplayBins: cfg.Surveillance.DisplayBins, | DisplayBins: cfg.Surveillance.DisplayBins, | ||||
| DisplayFPS: cfg.Surveillance.DisplayFPS, | DisplayFPS: cfg.Surveillance.DisplayFPS, | ||||
| SurveillanceStrategy: cfg.Surveillance.Strategy, | |||||
| RefinementEnabled: cfg.Refinement.Enabled, | RefinementEnabled: cfg.Refinement.Enabled, | ||||
| MaxRefinementJobs: cfg.Resources.MaxRefinementJobs, | MaxRefinementJobs: cfg.Resources.MaxRefinementJobs, | ||||
| RefinementMaxConcurrent: cfg.Refinement.MaxConcurrent, | RefinementMaxConcurrent: cfg.Refinement.MaxConcurrent, | ||||
| @@ -28,3 +28,30 @@ func AutoSpanForHint(hint string) (float64, string) { | |||||
| return 0, "" | return 0, "" | ||||
| } | } | ||||
| } | } | ||||
| // RefinementWindowForCandidate applies policy-aware span rules to a candidate. | |||||
| func RefinementWindowForCandidate(policy Policy, candidate Candidate) RefinementWindow { | |||||
| span := candidate.BandwidthHz | |||||
| windowSource := "candidate" | |||||
| if policy.RefinementAutoSpan && (span <= 0 || span < 2000 || span > 400000) { | |||||
| autoSpan, autoSource := AutoSpanForHint(candidate.Hint) | |||||
| if autoSpan > 0 { | |||||
| span = autoSpan | |||||
| windowSource = autoSource | |||||
| } | |||||
| } | |||||
| if policy.RefinementMinSpanHz > 0 && span < policy.RefinementMinSpanHz { | |||||
| span = policy.RefinementMinSpanHz | |||||
| } | |||||
| if policy.RefinementMaxSpanHz > 0 && span > policy.RefinementMaxSpanHz { | |||||
| span = policy.RefinementMaxSpanHz | |||||
| } | |||||
| if span <= 0 { | |||||
| span = 12000 | |||||
| } | |||||
| return RefinementWindow{ | |||||
| CenterHz: candidate.CenterHz, | |||||
| SpanHz: span, | |||||
| Source: windowSource, | |||||
| } | |||||
| } | |||||