Files

199 lines
6.3 KiB
Go
Raw Permalink Normal View History

package markers
import (
"strings"
"github.com/Silo-Server/silo-server/internal/models"
)
// CanWriteMarker reports whether a new marker write should be accepted given
// the existing source/confidence. A strictly higher-priority source always
// wins; an equal-priority source wins only if its confidence is strictly
// higher than what's already stored. Unknown/empty existing source is treated
// as priority zero so any defined source can replace it.
//
// A manual write is the exception: an explicit human edit is authoritative and
// always applies at equal-or-higher priority (last-writer-wins). Without this,
// correcting a previously hand-set marker — whose confidence is fixed at 1.0 —
// would be silently rejected by the strictly-higher-confidence rule, leaving
// users unable to fix their own (or each other's) edits.
func CanWriteMarker(existingSource *string, existingConfidence *float64, newSource string, newConfidence *float64) bool {
currentSource := ""
if existingSource != nil {
currentSource = *existingSource
}
existingPriority := models.MarkerSourcePriority(currentSource)
newPriority := models.MarkerSourcePriority(newSource)
if newPriority > existingPriority {
return true
}
if newPriority < existingPriority {
return false
}
if newSource == models.MarkerSourceManual {
return true
}
if existingConfidence != nil && newConfidence != nil {
return *newConfidence > *existingConfidence
}
return false
}
// SegmentPayload is the storage-agnostic per-segment marker write: its bounds
// plus the provenance (provider/confidence/algorithm) of the marker that
// produced it. A merged multi-provider result carries a different provider per
// segment; a single-source result repeats the same provider across segments.
type SegmentPayload struct {
Start, End *float64
Source string
Provider *string
Confidence *float64
Algorithm string
}
// Present reports whether the segment carries a bound to write.
func (s SegmentPayload) Present() bool { return s.Start != nil || s.End != nil }
// MarkerUpdatePayload is the storage-agnostic shape produced from a provider
// Result. Repositories convert it into their concrete write column set. Source
// is the shared source class (online/scanner/manual/...); each SegmentPayload
// carries its own provider/confidence/algorithm so a merged multi-provider
// result records correct per-segment provenance.
type MarkerUpdatePayload struct {
Intro SegmentPayload
Credits SegmentPayload
Recap SegmentPayload
Preview SegmentPayload
Source string
}
// HasAnySegment reports whether the payload carries at least one segment
// range. Callers can short-circuit empty writes without touching the DB.
func (p MarkerUpdatePayload) HasAnySegment() bool {
return p.Intro.Present() || p.Credits.Present() || p.Recap.Present() || p.Preview.Present()
}
// SummaryConfidence returns the highest per-segment confidence present, for the
// legacy shared markers_confidence column. Returns nil when no segment carries
// a confidence.
func (p MarkerUpdatePayload) SummaryConfidence() *float64 {
var max float64
found := false
for _, s := range []SegmentPayload{p.Intro, p.Credits, p.Recap, p.Preview} {
if s.Confidence != nil && (!found || *s.Confidence > max) {
max = *s.Confidence
found = true
}
}
if !found {
return nil
}
return &max
}
// SummarySource returns the highest-priority source class present in the
// per-segment payloads, using confidence as the tie-breaker. This keeps the
// legacy shared markers_source column coherent even when individual segment
// provenance differs.
func (p MarkerUpdatePayload) SummarySource() string {
source := strings.TrimSpace(p.Source)
confidence := p.SummaryConfidence()
found := source != ""
for _, s := range []SegmentPayload{p.Intro, p.Credits, p.Recap, p.Preview} {
if !s.Present() || strings.TrimSpace(s.Source) == "" {
continue
}
if !found ||
models.MarkerSourcePriority(s.Source) > models.MarkerSourcePriority(source) ||
(models.MarkerSourcePriority(s.Source) == models.MarkerSourcePriority(source) && confidenceGreater(s.Confidence, confidence)) {
source = s.Source
confidence = s.Confidence
found = true
}
}
return source
}
// BuildUpdatePayload converts a provider Result into the storage payload. Each
// marker maps to its segment with its own provenance: ProviderID/Algorithm fall
// back to the Result-level values (used for single-provider results), and the
// algorithm finally falls back to external:<source> so every write carries an
// algorithm tag.
func BuildUpdatePayload(result Result) MarkerUpdatePayload {
payload := MarkerUpdatePayload{Source: result.SourceClass}
for _, m := range result.Markers {
start := m.Start.Seconds()
end := m.End.Seconds()
if end <= start {
continue
}
startPtr, endPtr := start, end
source := markerSource(m, result)
seg := SegmentPayload{
Start: &startPtr,
End: &endPtr,
Source: source,
Provider: markerProvider(m, result),
Algorithm: markerAlgorithm(m, result.Algorithm, source),
}
if m.Confidence > 0 {
conf := m.Confidence
seg.Confidence = &conf
}
switch m.Kind {
case MarkerKindIntro:
payload.Intro = seg
case MarkerKindCredits:
payload.Credits = seg
case MarkerKindRecap:
payload.Recap = seg
case MarkerKindPreview:
payload.Preview = seg
}
}
return payload
}
func markerSource(m Marker, result Result) string {
source := strings.TrimSpace(m.SourceClass)
if source == "" {
source = strings.TrimSpace(result.SourceClass)
}
return source
}
// markerProvider returns the per-marker provider, falling back to the
// Result-level provider (single-provider results), or nil when neither is set.
func markerProvider(m Marker, result Result) *string {
provider := strings.TrimSpace(m.ProviderID)
if provider == "" {
provider = strings.TrimSpace(result.ProviderID)
}
if provider == "" {
return nil
}
return &provider
}
// markerAlgorithm returns the per-marker algorithm, falling back to the
// already-resolved Result-level algorithm.
func markerAlgorithm(m Marker, resultAlgorithm, source string) string {
if a := strings.TrimSpace(m.Algorithm); a != "" {
return a
}
if strings.TrimSpace(resultAlgorithm) == "" && strings.TrimSpace(source) != "" {
return "external:" + source
}
return resultAlgorithm
}
func confidenceGreater(a, b *float64) bool {
if a == nil {
return false
}
if b == nil {
return true
}
return *a > *b
}