Skip to content

Commit 6d51b60

Browse files
committed
docs(logging): add rejection summary for silence candidates
- Show counts of rejected silence candidates grouped by reason - Extract rejection reason from TransientWarning field - Display summary in analysis output and diagnostic report - Classify reasons: digital silence, crosstalk, transient contamination, too loud Signed-off-by: Martin Wimpress <code@wimpress.io>
1 parent 657d232 commit 6d51b60

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

internal/logging/analysis_display.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ func DisplayAnalysisResults(w io.Writer, inputPath string, metadata *audio.Metad
136136
fmt.Fprintln(w)
137137
}
138138
}
139+
140+
// Rejection summary: count zero-scored candidates by reason
141+
writeSilenceRejectionSummary(w, measurements.SilenceCandidates)
139142
} else if measurements.NoiseProfile != nil {
140143
fmt.Fprintf(w, " Sample: %.1fs at %s\n",
141144
measurements.NoiseProfile.Duration.Seconds(), formatTimestamp(measurements.NoiseProfile.Start))
@@ -298,6 +301,55 @@ func writeSilenceCandidateMetrics(w io.Writer, c processor.SilenceCandidateMetri
298301
fmt.Fprintf(w, " Centroid: %.0f Hz\n", c.Spectral.Centroid)
299302
}
300303

304+
// writeSilenceRejectionSummary outputs a compact summary of rejected silence candidates.
305+
// Groups zero-scored candidates by rejection reason extracted from TransientWarning.
306+
func writeSilenceRejectionSummary(w io.Writer, candidates []processor.SilenceCandidateMetrics) {
307+
reasonCounts := make(map[string]int)
308+
for _, c := range candidates {
309+
if c.Score != 0.0 {
310+
continue
311+
}
312+
reason := classifyRejectionReason(c.TransientWarning)
313+
reasonCounts[reason]++
314+
}
315+
316+
if len(reasonCounts) == 0 {
317+
return
318+
}
319+
320+
// Build summary parts in a stable order
321+
order := []string{"digital silence", "crosstalk", "transient contamination", "too loud"}
322+
var parts []string
323+
for _, reason := range order {
324+
if count, ok := reasonCounts[reason]; ok {
325+
parts = append(parts, fmt.Sprintf("%d %s", count, reason))
326+
delete(reasonCounts, reason)
327+
}
328+
}
329+
// Any unexpected reasons
330+
for reason, count := range reasonCounts {
331+
parts = append(parts, fmt.Sprintf("%d %s", count, reason))
332+
}
333+
334+
fmt.Fprintf(w, " Rejected: %s\n", strings.Join(parts, ", "))
335+
}
336+
337+
// classifyRejectionReason maps a TransientWarning string to a short label.
338+
func classifyRejectionReason(warning string) string {
339+
switch {
340+
case strings.Contains(warning, "digital silence"):
341+
return "digital silence"
342+
case strings.Contains(warning, "crosstalk"):
343+
return "crosstalk"
344+
case strings.Contains(warning, "transient contamination"):
345+
return "transient contamination"
346+
case warning == "":
347+
return "too loud"
348+
default:
349+
return "too loud"
350+
}
351+
}
352+
301353
// writeAnalysisSection writes a section header for analysis output.
302354
func writeAnalysisSection(w io.Writer, title string) {
303355
fmt.Fprintln(w, title)

internal/logging/report.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,9 @@ func writeDiagnosticSilence(f *os.File, measurements *processor.AudioMeasurement
16591659
i+1, c.Region.Duration.Seconds(), c.Region.Start.Seconds(), c.Score, c.RMSLevel)
16601660
}
16611661
}
1662+
1663+
// Rejection summary for zero-scored candidates
1664+
writeReportRejectionSummary(f, measurements.SilenceCandidates)
16621665
} else if measurements.NoiseProfile != nil {
16631666
fmt.Fprintf(f, "Silence Sample: %.1fs at %.1fs\n",
16641667
measurements.NoiseProfile.Duration.Seconds(),
@@ -1678,6 +1681,36 @@ func writeDiagnosticSilence(f *os.File, measurements *processor.AudioMeasurement
16781681
fmt.Fprintln(f, "")
16791682
}
16801683

1684+
// writeReportRejectionSummary outputs a compact summary of rejected silence candidates to the report file.
1685+
func writeReportRejectionSummary(f *os.File, candidates []processor.SilenceCandidateMetrics) {
1686+
reasonCounts := make(map[string]int)
1687+
for _, c := range candidates {
1688+
if c.Score != 0.0 {
1689+
continue
1690+
}
1691+
reason := classifyRejectionReason(c.TransientWarning)
1692+
reasonCounts[reason]++
1693+
}
1694+
1695+
if len(reasonCounts) == 0 {
1696+
return
1697+
}
1698+
1699+
order := []string{"digital silence", "crosstalk", "transient contamination", "too loud"}
1700+
var parts []string
1701+
for _, reason := range order {
1702+
if count, ok := reasonCounts[reason]; ok {
1703+
parts = append(parts, fmt.Sprintf("%d %s", count, reason))
1704+
delete(reasonCounts, reason)
1705+
}
1706+
}
1707+
for reason, count := range reasonCounts {
1708+
parts = append(parts, fmt.Sprintf("%d %s", count, reason))
1709+
}
1710+
1711+
fmt.Fprintf(f, "Rejected: %s\n", strings.Join(parts, ", "))
1712+
}
1713+
16811714
// writeDiagnosticSpeech outputs detailed speech detection diagnostics.
16821715
func writeDiagnosticSpeech(f *os.File, measurements *processor.AudioMeasurements) {
16831716
if measurements == nil {

internal/processor/analyzer_candidates.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,10 @@ func scoreSilenceCandidate(m *SilenceCandidateMetrics) float64 {
11171117
m.Region.Start.Seconds(), m.CrestFactor, isCrosstalk)
11181118
if isCrosstalk {
11191119
debugLog("scoreSilenceCandidate: REJECTING candidate at %.3fs (returning score=0.0)", m.Region.Start.Seconds())
1120+
m.TransientWarning = fmt.Sprintf(
1121+
"rejected: crosstalk detected (crest %.1f dB, centroid %.0f Hz)",
1122+
m.CrestFactor, m.Spectral.Centroid,
1123+
)
11201124
return 0.0 // Reject this candidate
11211125
}
11221126

0 commit comments

Comments
 (0)