Sub-project 2: ingest & tagging / review tools #1
@@ -0,0 +1,79 @@
|
||||
from hef.selection import Coordinate
|
||||
from tools.drafting import Draft, HeuristicProposer, Signals
|
||||
|
||||
|
||||
def _sig(**o):
|
||||
base = dict(
|
||||
title="",
|
||||
description="",
|
||||
source_archive="nasa",
|
||||
mode="video",
|
||||
duration_s=600,
|
||||
)
|
||||
base.update(o)
|
||||
return Signals(**base)
|
||||
|
||||
|
||||
def test_returns_draft_with_coordinate_and_rationale():
|
||||
d = HeuristicProposer().propose(_sig())
|
||||
assert isinstance(d, Draft)
|
||||
assert isinstance(d.coordinate, Coordinate)
|
||||
assert d.rationale and "\n" not in d.rationale
|
||||
|
||||
|
||||
def test_librivox_seeds_left():
|
||||
d = HeuristicProposer().propose(
|
||||
_sig(title="Meditations", source_archive="librivox", mode="audio")
|
||||
)
|
||||
assert d.coordinate.left >= 3 and d.rationale
|
||||
|
||||
|
||||
def test_music_archives_seed_right():
|
||||
for arch in ("musopen", "fma", "nasa", "freesound"):
|
||||
d = HeuristicProposer().propose(_sig(source_archive=arch, mode="audio"))
|
||||
assert d.coordinate.right >= 3, arch
|
||||
|
||||
|
||||
def test_internet_archive_seeds_left():
|
||||
d = HeuristicProposer().propose(_sig(source_archive="internet_archive"))
|
||||
assert d.coordinate.left >= 3
|
||||
|
||||
|
||||
def test_storm_seeds_dark():
|
||||
d = HeuristicProposer().propose(
|
||||
_sig(title="Thunderstorm at Night", source_archive="nasa")
|
||||
)
|
||||
assert d.coordinate.dark >= 2
|
||||
|
||||
|
||||
def test_sunrise_seeds_light():
|
||||
d = HeuristicProposer().propose(
|
||||
_sig(title="Sunrise over the Garden", source_archive="nasa")
|
||||
)
|
||||
assert d.coordinate.light >= 2
|
||||
|
||||
|
||||
def test_coordinates_clamped_to_0_4():
|
||||
d = HeuristicProposer().propose(
|
||||
_sig(
|
||||
title="storm night war funeral decay requiem minor",
|
||||
description="noir death grief",
|
||||
source_archive="librivox",
|
||||
)
|
||||
)
|
||||
for v in (d.coordinate.left, d.coordinate.right, d.coordinate.dark, d.coordinate.light):
|
||||
assert 0 <= v <= 4
|
||||
|
||||
|
||||
def test_rationale_cites_a_signal():
|
||||
d = HeuristicProposer().propose(
|
||||
_sig(title="Storm", source_archive="librivox", mode="audio")
|
||||
)
|
||||
assert "librivox" in d.rationale.lower()
|
||||
|
||||
|
||||
def test_deterministic():
|
||||
s = _sig(title="Storm at Dawn", source_archive="nasa")
|
||||
a = HeuristicProposer().propose(s)
|
||||
b = HeuristicProposer().propose(s)
|
||||
assert a == b
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Heuristic coordinate proposer — a deterministic, rule-based DRAFT seed.
|
||||
|
||||
The four curatorial coordinates are a human act (design §11: no automatic ML
|
||||
coordinate tagging). This proposer only suggests a starting point written as
|
||||
review_status='proposed'; a human blesses every coordinate before selection.
|
||||
There is no ML model here — just archive priors (brain plane) and keyword
|
||||
nudges (mood plane), grounded in the §8 sourcing→axis map.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
from hef.selection import Coordinate
|
||||
|
||||
COORD_MIN, COORD_MAX = 0, 4
|
||||
|
||||
# Brain plane (left = verbal/spoken/educational, right = music/wordless/abstract).
|
||||
_ARCHIVE_PRIORS = {
|
||||
"librivox": (4, 0), # spoken readings
|
||||
"internet_archive": (3, 1), # educational / industrial / Prelinger
|
||||
"musopen": (0, 4), # classical music
|
||||
"fma": (0, 4), # music
|
||||
"nasa": (0, 4), # wordless awe
|
||||
"freesound": (0, 4), # abstract field recordings
|
||||
}
|
||||
_ARCHIVE_DEFAULT = (2, 2)
|
||||
|
||||
# Mood plane keywords (substring match so "thunderstorm" trips "storm").
|
||||
_DARK_WORDS = (
|
||||
"storm", "night", "noir", "requiem", "minor", "war", "funeral",
|
||||
"decay", "death", "grief", "dusk", "winter", "mourning", "shadow",
|
||||
)
|
||||
_LIGHT_WORDS = (
|
||||
"sunrise", "dawn", "garden", "spring", "joy", "hope", "major",
|
||||
"bright", "bloom", "summer", "smile", "light", "morning",
|
||||
)
|
||||
|
||||
|
||||
def _clamp(v: int) -> int:
|
||||
return max(COORD_MIN, min(COORD_MAX, v))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Signals:
|
||||
title: str
|
||||
description: str
|
||||
source_archive: str
|
||||
mode: str
|
||||
duration_s: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Draft:
|
||||
coordinate: Coordinate
|
||||
rationale: str
|
||||
|
||||
|
||||
class Proposer(Protocol):
|
||||
def propose(self, signals: Signals) -> Draft: ...
|
||||
|
||||
|
||||
class HeuristicProposer:
|
||||
def propose(self, signals: Signals) -> Draft:
|
||||
left, right = _ARCHIVE_PRIORS.get(
|
||||
signals.source_archive, _ARCHIVE_DEFAULT
|
||||
)
|
||||
|
||||
text = f"{signals.title} {signals.description}".lower()
|
||||
dark_hits = [w for w in _DARK_WORDS if w in text]
|
||||
light_hits = [w for w in _LIGHT_WORDS if w in text]
|
||||
dark = _clamp(len(dark_hits))
|
||||
light = _clamp(len(light_hits))
|
||||
|
||||
coordinate = Coordinate(_clamp(left), _clamp(right), dark, light)
|
||||
|
||||
# Rationale: name the dominant brain prior + any mood keywords.
|
||||
brain = (
|
||||
f"{signals.source_archive} → "
|
||||
+ ("strong left" if left >= right else "strong right")
|
||||
)
|
||||
mood_bits = []
|
||||
if dark_hits:
|
||||
mood_bits.append(f"{dark_hits[0]!r} → dark")
|
||||
if light_hits:
|
||||
mood_bits.append(f"{light_hits[0]!r} → light")
|
||||
rationale = "; ".join([brain, *mood_bits])
|
||||
|
||||
return Draft(coordinate=coordinate, rationale=rationale)
|
||||
Reference in New Issue
Block a user