Add 'manual' source: a hand-marked ad is ground truth (0.18.0)
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:
@@ -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
@@ -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 @@
|
||||
__version__ = "0.17.0"
|
||||
__version__ = "0.18.0"
|
||||
|
||||
+1
-1
@@ -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%
|
||||
|
||||
@@ -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
@@ -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 ---
|
||||
|
||||
Reference in New Issue
Block a user