Add 'manual' source: a hand-marked ad is ground truth (0.18.0)
Lint Python / lint (push) Canceled after 0s
Test Python / test (push) Canceled after 0s

manual joins GROUND_TRUTH_SOURCES (seeds both libraries, confirms a
campaign) and CUT_SOURCES (cut). A human marking a span as an ad is the
strongest evidence there is; hark's UI writes these when an operator marks
or corrects an ad by hand.
This commit is contained in:
flan
2026-07-24 18:59:40 +00:00
parent ca3df3f471
commit 2e09824c20
7 changed files with 29 additions and 8 deletions
+10
View File
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.18.0] - 2026-07-24
### Added
- **`manual` source — a hand-marked ad is ground truth.** Added to `repeats.GROUND_TRUTH_SOURCES`
(so it seeds both the text-repeat and fingerprint libraries and confirms a campaign) and to
`cut.CUT_SOURCES` (so it's cut). A human marking a span as an ad is the strongest evidence there
is; it ranks with `llm`/`chapter`. hark's UI writes these when an operator marks/corrects an ad
by hand.
## [0.17.0] - 2026-07-24
### Added
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "adscrub"
version = "0.17.0"
version = "0.18.0"
description = "Self-hosted podcast ad-detection and removal proxy"
readme = "README.md"
requires-python = ">=3.12"
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.17.0"
__version__ = "0.18.0"
+1 -1
View File
@@ -40,7 +40,7 @@ from .db import utcnow
# recur cold-start self-recurrence; roughly 1 flagged region in 10 is not an ad at all
# Both stay valuable for SEEDING and DISCOVERY, which is a different job from cutting. Opt in
# deliberately (`--sources`) if you want them cut anyway.
CUT_SOURCES = ("chapter", "llm", "repeat", "fpmatch")
CUT_SOURCES = ("chapter", "llm", "repeat", "fpmatch", "manual")
# A cut that removes more than this fraction of an episode is almost certainly wrong — a
# false-positive tier eating editorial content, not an ad load. Even ad-heavy shows run ~10-30%
+4 -1
View File
@@ -101,7 +101,10 @@ def _ad_segment_indices(transcript: list[dict], spans: Iterable[tuple[float, flo
# 958 spans to 993, because each pass's guesses became the next pass's evidence, matched
# more loosely, and drifted — a detector slowly hallucinating a larger and larger idea of
# what an ad sounds like. Evidence in, inference out; never the reverse.
GROUND_TRUTH_SOURCES = ("llm", "chapter")
# `manual` = a human marked this span an ad by hand — the STRONGEST evidence there is, so it
# ranks with llm/chapter: it seeds both libraries and confirms a campaign. (A human correction is
# also how a false positive gets removed — see the cut path.)
GROUND_TRUTH_SOURCES = ("llm", "chapter", "manual")
def build_library(
+11 -3
View File
@@ -8,6 +8,13 @@ from adscrub import cut, db
AUDIO_URL = "https://example.com/audio/ep1.mp3"
def test_manual_marks_are_ground_truth_and_cuttable():
"""A hand-marked ad ('manual') is the strongest evidence: cut, and it seeds both libraries."""
from adscrub import repeats
assert "manual" in cut.CUT_SOURCES
assert "manual" in repeats.GROUND_TRUTH_SOURCES
def test_is_anomalous_cut_flags_implausible_fraction():
assert cut.is_anomalous_cut(400, 1000) is True # 40% of the episode -> flag
assert cut.is_anomalous_cut(349, 1000) is False # just under 35% -> fine
@@ -253,9 +260,10 @@ def test_untrusted_spans_are_not_removed_from_audio(tmp_path):
conn.execute("INSERT INTO ad_segments (episode_id, start_second, end_second, source) "
"VALUES (?, 30.0, 40.0, 'recur')", (eid,))
conn.commit()
ph = ",".join("?" * len(cut.CUT_SOURCES))
rows = conn.execute(
"SELECT start_second, end_second FROM ad_segments WHERE episode_id=? AND source IN "
"(?,?,?,?)", (eid, *cut.CUT_SOURCES)).fetchall()
f"SELECT start_second, end_second FROM ad_segments WHERE episode_id=? AND source IN ({ph})",
(eid, *cut.CUT_SOURCES)).fetchall()
assert [(r["start_second"], r["end_second"]) for r in rows] == [(10.0, 20.0)]
# 30-40 survives in the audio because nothing trusted vouches for those edges
keep = cut.compute_keep_spans([(r["start_second"], r["end_second"]) for r in rows], 60.0)
@@ -264,7 +272,7 @@ def test_untrusted_spans_are_not_removed_from_audio(tmp_path):
def test_cut_sources_excludes_the_edge_unsafe_tiers():
assert "dai" not in cut.CUT_SOURCES and "recur" not in cut.CUT_SOURCES
assert {"chapter", "llm", "repeat", "fpmatch"} == set(cut.CUT_SOURCES)
assert {"chapter", "llm", "repeat", "fpmatch", "manual"} == set(cut.CUT_SOURCES)
# --- snapping cut edges to silence ---
Generated
+1 -1
View File
@@ -4,7 +4,7 @@ requires-python = ">=3.12"
[[package]]
name = "adscrub"
version = "0.16.0"
version = "0.17.0"
source = { editable = "." }
dependencies = [
{ name = "anthropic" },