3d5c34491c
Per sub-project-2 plan Task 7 / spec §7. HeuristicProposer seeds the brain plane from archive priors and the mood plane from title/description keyword nudges, clamps to 0..4, and emits a one-line rationale. Deterministic, no I/O, no ML (honors design §11) — only a DRAFT a human reviews. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
2.8 KiB
Python
91 lines
2.8 KiB
Python
"""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)
|