drum-sample-extractor / supervised_state.py
ChatGPT
feat: add spleeter and selected card exports
fa35534
Raw
History Blame Contribute Delete
44.8 kB
#!/usr/bin/env python3
"""Persistent supervised-editing state for interactive extraction jobs.
The extraction pipeline produces immutable audio artifacts and a batch manifest.
This module layers replayable semantic state on top of that manifest: hits,
clusters, constraints, events, suggestions, confidence, and undo snapshots.
Supervised edits are cheap, explicit, inspectable, and reproducible. A
separate supervised export step renders the mutable state into edited WAV/MIDI/ZIP
artifacts without mutating the original batch manifest.
"""
from __future__ import annotations
import copy
import json
import math
import time
import uuid
from pathlib import Path
from typing import Any, Callable
STATE_VERSION = "interactive-state-v1"
STATE_FILENAME = "supervision_state.json"
MAX_UNDO = 30
def now() -> float:
return round(time.time(), 6)
def state_path(output_dir: str | Path) -> Path:
return Path(output_dir) / STATE_FILENAME
def manifest_path(output_dir: str | Path) -> Path:
return Path(output_dir) / "manifest.json"
def load_manifest(output_dir: str | Path) -> dict[str, Any]:
path = manifest_path(output_dir)
if not path.exists():
raise FileNotFoundError(f"manifest.json not found in {Path(output_dir)}")
return json.loads(path.read_text(encoding="utf-8"))
def _hit_id(hit: dict[str, Any]) -> str:
return f"hit:{int(hit.get('index', 0)):05d}"
def _cluster_id(raw: Any) -> str:
return f"cluster:{raw}"
def _base_label(label: str) -> str:
text = str(label or "other")
return text.rsplit("_", 1)[0] if "_" in text else text
def _new_id(prefix: str) -> str:
return f"{prefix}:{uuid.uuid4().hex[:10]}"
def _safe_float(value: Any, default: float = 0.0) -> float:
try:
out = float(value)
if math.isfinite(out):
return out
except Exception:
pass
return default
def _safe_int(value: Any, default: int = 0) -> int:
try:
return int(value)
except Exception:
return default
def _safe_file_component(value: str) -> str:
import re
text = str(value or "hit").strip().lower()
text = re.sub(r"[^a-z0-9._-]+", "_", text)
text = re.sub(r"_+", "_", text).strip("._-")
return text or "hit"
def _snapshot(state: dict[str, Any]) -> dict[str, Any]:
snap = copy.deepcopy(state)
snap["undo_stack"] = []
return snap
def _push_undo(state: dict[str, Any]) -> None:
stack = list(state.get("undo_stack") or [])
stack.append(_snapshot(state))
del stack[:-MAX_UNDO]
state["undo_stack"] = stack
def _event(state: dict[str, Any], event_type: str, payload: dict[str, Any] | None = None, source: str = "system") -> dict[str, Any]:
event = {
"id": _new_id("event"),
"type": event_type,
"source": source,
"created_at": now(),
"payload": payload or {},
}
state.setdefault("events", []).append(event)
return event
def _constraint(state: dict[str, Any], constraint_type: str, payload: dict[str, Any], source: str = "user") -> dict[str, Any]:
constraint = {
"id": _new_id("constraint"),
"type": constraint_type,
"source": source,
"created_at": now(),
**payload,
}
state.setdefault("constraints", []).append(constraint)
_event(state, "constraint.created", {"constraint_id": constraint["id"], "type": constraint_type}, source=source)
return constraint
def _write_state(output_dir: str | Path, state: dict[str, Any]) -> dict[str, Any]:
state["updated_at"] = now()
path = state_path(output_dir)
path.write_text(json.dumps(state, indent=2, sort_keys=True), encoding="utf-8")
return state
def _cluster_label_for_hit(hit: dict[str, Any]) -> str:
return str(hit.get("cluster_label") or f"{hit.get('label', 'other')}_{hit.get('cluster_id', '0')}")
def build_initial_state(job_id: str, manifest: dict[str, Any]) -> dict[str, Any]:
hits_by_id: dict[str, dict[str, Any]] = {}
clusters: dict[str, dict[str, Any]] = {}
raw_hits = list(manifest.get("hits") or [])
if not raw_hits:
# Older manifests may only contain samples. Keep state valid even then.
raw_hits = []
for hit in raw_hits:
hid = _hit_id(hit)
cid = _cluster_id(hit.get("cluster_id", "unclustered"))
cluster_label = _cluster_label_for_hit(hit)
hits_by_id[hid] = {
"id": hid,
"index": int(hit.get("index", len(hits_by_id))),
"label": str(hit.get("label") or "other"),
"cluster_id": cid,
"original_cluster_id": cid,
"cluster_label": cluster_label,
"onset_sec": _safe_float(hit.get("onset_sec")),
"duration_ms": _safe_float(hit.get("duration_ms")),
"rms_energy": _safe_float(hit.get("rms_energy")),
"spectral_centroid_hz": _safe_float(hit.get("spectral_centroid_hz")),
"file": hit.get("file"),
"is_representative": bool(hit.get("is_representative")),
"source": "detected",
"suppressed": False,
"favorite": False,
"review_status": "unreviewed",
"confidence": 0.0,
"confidence_reasons": [],
"explicit": False,
}
clusters.setdefault(
cid,
{
"id": cid,
"label": cluster_label,
"classification": _base_label(cluster_label),
"hit_ids": [],
"representative_hit_id": None,
"locked": False,
"user_named": False,
"confidence": 0.0,
"confidence_reasons": [],
"suppressed_count": 0,
"original_id": cid,
},
)["hit_ids"].append(hid)
if bool(hit.get("is_representative")):
clusters[cid]["representative_hit_id"] = hid
for cid, cluster in clusters.items():
if cluster["representative_hit_id"] is None and cluster["hit_ids"]:
cluster["representative_hit_id"] = cluster["hit_ids"][0]
state = {
"version": STATE_VERSION,
"job_id": job_id,
"created_at": now(),
"updated_at": now(),
"manifest_fingerprint": _manifest_fingerprint(manifest),
"hits": hits_by_id,
"clusters": clusters,
"constraints": [],
"events": [],
"suggestions": [],
"undo_stack": [],
"counters": {"user_clusters": 0},
}
recompute_scores(state)
_event(
state,
"job.state.created",
{
"hit_count": len(hits_by_id),
"cluster_count": len(clusters),
"manifest_fingerprint": state["manifest_fingerprint"],
},
)
return state
def _manifest_fingerprint(manifest: dict[str, Any]) -> str:
import hashlib
payload = {
"params": manifest.get("params"),
"hit_count": manifest.get("hit_count"),
"cluster_count": manifest.get("cluster_count"),
"files": manifest.get("files"),
"hits": [
{
"index": h.get("index"),
"cluster_id": h.get("cluster_id"),
"file": h.get("file"),
"onset_sec": h.get("onset_sec"),
}
for h in manifest.get("hits", [])
],
}
return hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
def load_or_create_state(job_id: str, output_dir: str | Path) -> dict[str, Any]:
path = state_path(output_dir)
if path.exists():
state = json.loads(path.read_text(encoding="utf-8"))
if state.get("version") != STATE_VERSION:
raise ValueError(f"Unsupported supervision state version: {state.get('version')}")
return state
manifest = load_manifest(output_dir)
state = build_initial_state(job_id, manifest)
return _write_state(output_dir, state)
def _active_hits(state: dict[str, Any], cluster: dict[str, Any]) -> list[dict[str, Any]]:
hits = state.get("hits", {})
return [hits[hid] for hid in cluster.get("hit_ids", []) if hid in hits and not hits[hid].get("suppressed")]
def recompute_scores(state: dict[str, Any]) -> None:
hits = state.get("hits", {})
clusters = state.get("clusters", {})
energies = sorted(_safe_float(hit.get("rms_energy")) for hit in hits.values())
def energy_rank(value: float) -> float:
if not energies:
return 0.5
less = sum(1 for item in energies if item <= value)
return less / max(1, len(energies))
for cluster in clusters.values():
members = [hits[hid] for hid in cluster.get("hit_ids", []) if hid in hits]
active = [hit for hit in members if not hit.get("suppressed")]
if not members:
confidence = 0.15
reasons = ["empty cluster"]
else:
labels: dict[str, int] = {}
for hit in active:
labels[hit.get("label", "other")] = labels.get(hit.get("label", "other"), 0) + 1
majority = max(labels.values()) if labels else 0
purity = majority / max(1, len(active))
size_score = min(1.0, math.log2(len(active) + 1) / 4.0)
representative_bonus = 0.12 if cluster.get("representative_hit_id") in cluster.get("hit_ids", []) else 0.0
lock_bonus = 0.12 if cluster.get("locked") else 0.0
confidence = (0.42 * purity) + (0.34 * size_score) + representative_bonus + lock_bonus
reasons = []
if len(active) <= 1:
reasons.append("singleton cluster")
if purity < 0.75:
reasons.append("mixed labels")
if cluster.get("locked"):
reasons.append("user locked")
if representative_bonus:
reasons.append("has representative")
cluster["confidence"] = round(max(0.0, min(1.0, confidence)), 4)
cluster["confidence_reasons"] = reasons or ["cohesive cluster"]
cluster["suppressed_count"] = sum(1 for hit in members if hit.get("suppressed"))
for hit in hits.values():
cluster = clusters.get(hit.get("cluster_id"), {})
active_count = len(_active_hits(state, cluster)) if cluster else 0
label_match = _base_label(str(cluster.get("label", ""))) == str(hit.get("label", ""))
energy = energy_rank(_safe_float(hit.get("rms_energy")))
duration_ms = _safe_float(hit.get("duration_ms"))
duration_score = 0.65 if duration_ms <= 0 else max(0.0, min(1.0, 1.0 - abs(duration_ms - 180.0) / 700.0))
cluster_conf = _safe_float(cluster.get("confidence"), 0.2)
confidence = (0.42 * cluster_conf) + (0.18 * min(1.0, active_count / 4.0)) + (0.18 if label_match else 0.0) + (0.12 * energy) + (0.10 * duration_score)
reasons = []
if active_count <= 1:
reasons.append("singleton")
if not label_match:
reasons.append("label differs from cluster")
if energy < 0.2:
reasons.append("low energy")
if hit.get("is_representative"):
confidence += 0.08
reasons.append("representative")
if hit.get("explicit"):
confidence += 0.10
reasons.append("explicit user assignment")
if hit.get("suppressed"):
confidence = min(confidence, 0.25)
reasons.append("suppressed")
hit["confidence"] = round(max(0.0, min(1.0, confidence)), 4)
hit["confidence_reasons"] = reasons or ["consistent assignment"]
hit["cluster_label"] = cluster.get("label", hit.get("cluster_label", "unclustered"))
def review_queue(state: dict[str, Any], limit: int = 30) -> list[dict[str, Any]]:
rows = []
clusters = state.get("clusters", {})
for hit in state.get("hits", {}).values():
cluster = clusters.get(hit.get("cluster_id"), {})
score = 1.0 - _safe_float(hit.get("confidence"), 0.0)
if len(cluster.get("hit_ids", [])) <= 1:
score += 0.15
if hit.get("suppressed"):
score -= 0.35
if hit.get("review_status") == "accepted":
score -= 0.25
rows.append(
{
"hit_id": hit["id"],
"hit_index": hit.get("index"),
"label": hit.get("label"),
"cluster_id": hit.get("cluster_id"),
"cluster_label": cluster.get("label"),
"confidence": hit.get("confidence", 0.0),
"priority": round(max(0.0, score), 4),
"reasons": hit.get("confidence_reasons", []),
"suppressed": bool(hit.get("suppressed")),
"file": hit.get("file"),
}
)
rows.sort(key=lambda item: (-item["priority"], item["hit_index"] or 0))
return rows[: max(1, min(int(limit), 200))]
def _find_similar_hits(state: dict[str, Any], hit_id: str, *, exclude_cluster: str | None = None, include_suppressed: bool = False, limit: int = 12) -> list[tuple[dict[str, Any], float]]:
hits = state.get("hits", {})
src = hits[hit_id]
src_centroid = _safe_float(src.get("spectral_centroid_hz"))
src_energy = _safe_float(src.get("rms_energy"))
scored: list[tuple[dict[str, Any], float]] = []
for candidate in hits.values():
if candidate["id"] == hit_id:
continue
if exclude_cluster and candidate.get("cluster_id") == exclude_cluster:
continue
if candidate.get("suppressed") and not include_suppressed:
continue
label_score = 1.0 if candidate.get("label") == src.get("label") else 0.35
centroid_delta = abs(_safe_float(candidate.get("spectral_centroid_hz")) - src_centroid)
centroid_score = max(0.0, 1.0 - centroid_delta / 6000.0)
energy_delta = abs(_safe_float(candidate.get("rms_energy")) - src_energy)
energy_score = max(0.0, 1.0 - energy_delta / max(src_energy, 1e-4, _safe_float(candidate.get("rms_energy"))))
score = (0.48 * label_score) + (0.34 * centroid_score) + (0.18 * energy_score)
if score >= 0.62:
scored.append((candidate, round(score, 4)))
scored.sort(key=lambda item: (-item[1], item[0].get("index", 0)))
return scored[:limit]
def suggestion_diff(state: dict[str, Any], suggestion: dict[str, Any]) -> dict[str, Any]:
"""Build an exact before/after preview for a suggestion against current state."""
hits = state.get("hits", {})
clusters = state.get("clusters", {})
stype = suggestion.get("type")
hit_ids = [hid for hid in suggestion.get("hit_ids", []) if hid in hits]
def cluster_snapshot(cluster_id: str | None) -> dict[str, Any]:
cluster = clusters.get(cluster_id or "", {})
members = [hid for hid in cluster.get("hit_ids", []) if hid in hits]
active = [hid for hid in members if not hits[hid].get("suppressed")]
return {
"cluster_id": cluster_id,
"label": cluster.get("label", cluster_id),
"active_count": len(active),
"total_count": len(members),
"suppressed_count": sum(1 for hid in members if hits[hid].get("suppressed")),
}
rows = []
cluster_ids: set[str] = set()
for hid in hit_ids:
hit = hits[hid]
source_cluster_id = hit.get("cluster_id")
target_cluster_id = suggestion.get("target_cluster_id") if stype in {"move-hits", "split-hits"} else source_cluster_id
cluster_ids.add(str(source_cluster_id))
if target_cluster_id:
cluster_ids.add(str(target_cluster_id))
rows.append(
{
"hit_id": hid,
"hit_index": hit.get("index"),
"label": hit.get("label"),
"from_cluster_id": source_cluster_id,
"from_cluster_label": clusters.get(source_cluster_id, {}).get("label"),
"to_cluster_id": target_cluster_id,
"to_cluster_label": clusters.get(target_cluster_id, {}).get("label") if target_cluster_id else None,
"before_suppressed": bool(hit.get("suppressed")),
"after_suppressed": bool(hit.get("suppressed")) or stype == "suppress-hits",
"confidence": hit.get("confidence"),
}
)
before = {cid: cluster_snapshot(cid) for cid in sorted(cluster_ids)}
after = copy.deepcopy(before)
if stype in {"move-hits", "split-hits"}:
target = suggestion.get("target_cluster_id")
for row in rows:
source = row.get("from_cluster_id")
if source in after and source != target:
after[source]["active_count"] = max(0, after[source]["active_count"] - 1)
after[source]["total_count"] = max(0, after[source]["total_count"] - 1)
if target in after and source != target:
after[target]["active_count"] += 1
after[target]["total_count"] += 1
elif stype == "suppress-hits":
for row in rows:
source = row.get("from_cluster_id")
if source in after and not row.get("before_suppressed"):
after[source]["active_count"] = max(0, after[source]["active_count"] - 1)
after[source]["suppressed_count"] += 1
return {
"type": stype,
"affected_hit_count": len(rows),
"hits": rows,
"clusters_before": before,
"clusters_after": after,
}
def _add_suggestion(state: dict[str, Any], suggestion_type: str, payload: dict[str, Any], confidence: float, reason: str) -> dict[str, Any]:
suggestion = {
"id": _new_id("suggestion"),
"type": suggestion_type,
"status": "open",
"created_at": now(),
"confidence": round(max(0.0, min(1.0, confidence)), 4),
"reason": reason,
**payload,
}
suggestion["diff"] = suggestion_diff(state, suggestion)
state.setdefault("suggestions", []).append(suggestion)
_event(state, "suggestion.created", {"suggestion_id": suggestion["id"], "type": suggestion_type, "reason": reason})
return suggestion
def _rebuild_cluster_labels(state: dict[str, Any]) -> None:
hits = state.get("hits", {})
for cluster in state.get("clusters", {}).values():
for hid in cluster.get("hit_ids", []):
if hid in hits:
hits[hid]["cluster_label"] = cluster.get("label", "unclustered")
def move_hit(output_dir: str | Path, job_id: str, hit_id: str, target_cluster_id: str, source: str = "user") -> dict[str, Any]:
state = load_or_create_state(job_id, output_dir)
hits = state.get("hits", {})
clusters = state.get("clusters", {})
if hit_id not in hits:
raise KeyError(f"Unknown hit: {hit_id}")
if target_cluster_id not in clusters:
raise KeyError(f"Unknown cluster: {target_cluster_id}")
hit = hits[hit_id]
source_cluster_id = hit.get("cluster_id")
if source_cluster_id == target_cluster_id:
hit["review_status"] = "accepted"
recompute_scores(state)
return _write_state(output_dir, state)
_push_undo(state)
if source_cluster_id in clusters:
clusters[source_cluster_id]["hit_ids"] = [hid for hid in clusters[source_cluster_id].get("hit_ids", []) if hid != hit_id]
clusters[target_cluster_id].setdefault("hit_ids", [])
if hit_id not in clusters[target_cluster_id]["hit_ids"]:
clusters[target_cluster_id]["hit_ids"].append(hit_id)
hit["cluster_id"] = target_cluster_id
hit["cluster_label"] = clusters[target_cluster_id].get("label", target_cluster_id)
hit["explicit"] = True
hit["review_status"] = "accepted"
target_rep = clusters[target_cluster_id].get("representative_hit_id")
_constraint(state, "force-cluster", {"hit_id": hit_id, "cluster_id": target_cluster_id}, source=source)
if target_rep and target_rep != hit_id:
_constraint(state, "must-link", {"a": hit_id, "b": target_rep}, source=source)
_event(state, "hit.moved", {"hit_id": hit_id, "from_cluster_id": source_cluster_id, "to_cluster_id": target_cluster_id}, source=source)
similar = _find_similar_hits(state, hit_id, exclude_cluster=target_cluster_id, limit=10)
suggested_ids = [item[0]["id"] for item in similar if item[1] >= 0.72]
if suggested_ids:
avg = sum(score for _, score in similar if _["id"] in suggested_ids) / len(suggested_ids)
_add_suggestion(
state,
"move-hits",
{"hit_ids": suggested_ids, "target_cluster_id": target_cluster_id, "preview_count": len(suggested_ids)},
avg,
f"Similar label/spectral/energy profile to {hit_id}",
)
_rebuild_cluster_labels(state)
recompute_scores(state)
return _write_state(output_dir, state)
def pull_hit_to_new_cluster(output_dir: str | Path, job_id: str, hit_id: str, label: str | None = None, source: str = "user") -> dict[str, Any]:
state = load_or_create_state(job_id, output_dir)
hits = state.get("hits", {})
clusters = state.get("clusters", {})
if hit_id not in hits:
raise KeyError(f"Unknown hit: {hit_id}")
hit = hits[hit_id]
source_cluster_id = hit.get("cluster_id")
source_rep = clusters.get(source_cluster_id, {}).get("representative_hit_id")
_push_undo(state)
state.setdefault("counters", {})["user_clusters"] = int(state.get("counters", {}).get("user_clusters", 0)) + 1
base = label or f"{hit.get('label', 'hit')}_user_{state['counters']['user_clusters']}"
new_cluster_id = _new_id("cluster:user")
if source_cluster_id in clusters:
clusters[source_cluster_id]["hit_ids"] = [hid for hid in clusters[source_cluster_id].get("hit_ids", []) if hid != hit_id]
clusters[new_cluster_id] = {
"id": new_cluster_id,
"label": base,
"classification": _base_label(base),
"hit_ids": [hit_id],
"representative_hit_id": hit_id,
"locked": False,
"user_named": bool(label),
"confidence": 0.0,
"confidence_reasons": [],
"suppressed_count": 0,
"original_id": None,
}
hit["cluster_id"] = new_cluster_id
hit["cluster_label"] = base
hit["explicit"] = True
hit["review_status"] = "accepted"
if source_rep and source_rep != hit_id:
_constraint(state, "cannot-link", {"a": hit_id, "b": source_rep}, source=source)
_constraint(state, "force-cluster", {"hit_id": hit_id, "cluster_id": new_cluster_id}, source=source)
_event(state, "hit.pulled_out", {"hit_id": hit_id, "from_cluster_id": source_cluster_id, "to_cluster_id": new_cluster_id}, source=source)
similar = _find_similar_hits(state, hit_id, exclude_cluster=new_cluster_id, limit=8)
split_ids = [item[0]["id"] for item in similar if item[0].get("cluster_id") == source_cluster_id and item[1] >= 0.70]
if split_ids:
_add_suggestion(
state,
"split-hits",
{"hit_ids": split_ids, "source_cluster_id": source_cluster_id, "target_cluster_id": new_cluster_id, "preview_count": len(split_ids)},
0.76,
f"Similar to pulled-out hit {hit_id}; preview split from original cluster",
)
_rebuild_cluster_labels(state)
recompute_scores(state)
return _write_state(output_dir, state)
def lock_cluster(output_dir: str | Path, job_id: str, cluster_id: str, locked: bool = True, source: str = "user") -> dict[str, Any]:
state = load_or_create_state(job_id, output_dir)
clusters = state.get("clusters", {})
if cluster_id not in clusters:
raise KeyError(f"Unknown cluster: {cluster_id}")
_push_undo(state)
clusters[cluster_id]["locked"] = bool(locked)
_constraint(state, "lock-cluster", {"cluster_id": cluster_id, "locked": bool(locked)}, source=source)
_event(state, "cluster.locked" if locked else "cluster.unlocked", {"cluster_id": cluster_id}, source=source)
recompute_scores(state)
return _write_state(output_dir, state)
def suppress_hit(output_dir: str | Path, job_id: str, hit_id: str, reason: str = "bleed", source: str = "user") -> dict[str, Any]:
state = load_or_create_state(job_id, output_dir)
hits = state.get("hits", {})
if hit_id not in hits:
raise KeyError(f"Unknown hit: {hit_id}")
_push_undo(state)
hit = hits[hit_id]
hit["suppressed"] = True
hit["review_status"] = "suppressed"
hit["explicit"] = True
_constraint(state, "suppress-pattern", {"example_hit_id": hit_id, "reason": reason}, source=source)
_event(state, "hit.suppressed", {"hit_id": hit_id, "reason": reason}, source=source)
similar = _find_similar_hits(state, hit_id, include_suppressed=False, limit=16)
suggested_ids = [item[0]["id"] for item in similar if item[1] >= 0.72 and _safe_float(item[0].get("rms_energy")) <= _safe_float(hit.get("rms_energy")) * 1.35]
if suggested_ids:
_add_suggestion(
state,
"suppress-hits",
{"hit_ids": suggested_ids, "reason_code": reason, "preview_count": len(suggested_ids)},
0.74,
f"Similar low-energy profile to suppressed {reason} example {hit_id}",
)
recompute_scores(state)
return _write_state(output_dir, state)
def restore_hit(output_dir: str | Path, job_id: str, hit_id: str, source: str = "user") -> dict[str, Any]:
state = load_or_create_state(job_id, output_dir)
hits = state.get("hits", {})
if hit_id not in hits:
raise KeyError(f"Unknown hit: {hit_id}")
_push_undo(state)
hit = hits[hit_id]
hit["suppressed"] = False
hit["review_status"] = "unreviewed" if hit.get("review_status") == "suppressed" else hit.get("review_status", "unreviewed")
hit["explicit"] = True
_constraint(state, "restore-hit", {"hit_id": hit_id}, source=source)
_event(state, "hit.restored", {"hit_id": hit_id}, source=source)
recompute_scores(state)
return _write_state(output_dir, state)
def force_onset(
output_dir: str | Path,
job_id: str,
onset_sec: float,
*,
duration_ms: float | None = None,
label: str | None = None,
target_cluster_id: str | None = None,
pre_pad_sec: float = 0.003,
source: str = "user",
) -> dict[str, Any]:
"""Create a user-forced hit from ``stem.wav`` and add it to semantic state."""
import librosa
import numpy as np
import soundfile as sf
from sample_extractor import Hit as AudioHit, classify_hit
out = Path(output_dir)
stem_path = out / "stem.wav"
if not stem_path.exists():
raise FileNotFoundError("stem.wav is required before forcing onsets")
state = load_or_create_state(job_id, out)
hits = state.setdefault("hits", {})
clusters = state.setdefault("clusters", {})
onset = max(0.0, _safe_float(onset_sec))
audio, sr = sf.read(stem_path, dtype="float32", always_2d=False)
if audio.ndim > 1:
audio = audio.mean(axis=1)
audio = np.asarray(audio, dtype=np.float32)
duration = (_safe_float(duration_ms, 0.0) / 1000.0) if duration_ms else 0.0
if duration <= 0:
future_onsets = sorted(
_safe_float(hit.get("onset_sec"))
for hit in hits.values()
if not hit.get("suppressed") and _safe_float(hit.get("onset_sec")) > onset + 0.01
)
next_onset = future_onsets[0] if future_onsets else None
duration = min(1.5, max(0.08, (next_onset - onset) if next_onset is not None else 0.45))
duration = max(0.02, min(10.0, duration))
start = max(0, int((onset - max(0.0, pre_pad_sec)) * sr))
end = min(len(audio), start + int(duration * sr))
if end <= start:
raise ValueError("Forced onset is outside the available stem audio")
segment = audio[start:end].copy()
fade_len = min(int(0.003 * sr), len(segment) // 4)
if fade_len > 0:
segment[-fade_len:] *= np.linspace(1, 0, fade_len)
rms = float(np.sqrt(np.mean(segment**2))) if len(segment) else 0.0
spectral_centroid = float(librosa.feature.spectral_centroid(y=segment, sr=sr).mean()) if len(segment) >= 32 else 0.0
index = max((_safe_int(hit.get("index"), -1) for hit in hits.values()), default=-1) + 1
tmp_hit = AudioHit(audio=segment, sr=sr, onset_time=onset, duration=len(segment) / sr, index=index, rms_energy=rms, spectral_centroid=spectral_centroid)
inferred_label = label or classify_hit(tmp_hit)
tmp_hit.label = inferred_label
hit_id = _hit_id({"index": index})
safe_label = _safe_file_component(inferred_label or "forced")
rel_file = f"review/hits/hit_{index:05d}_{safe_label}_forced.wav"
full_path = out / rel_file
full_path.parent.mkdir(parents=True, exist_ok=True)
sf.write(full_path, segment, sr, subtype="PCM_24")
_push_undo(state)
if target_cluster_id and target_cluster_id not in clusters:
raise KeyError(f"Unknown cluster: {target_cluster_id}")
if not target_cluster_id:
state.setdefault("counters", {})["user_clusters"] = int(state.get("counters", {}).get("user_clusters", 0)) + 1
target_cluster_id = _new_id("cluster:user")
cluster_label = f"{_safe_file_component(inferred_label)}_forced_{state['counters']['user_clusters']}"
clusters[target_cluster_id] = {
"id": target_cluster_id,
"label": cluster_label,
"classification": _base_label(cluster_label),
"hit_ids": [],
"representative_hit_id": hit_id,
"locked": False,
"user_named": bool(label),
"confidence": 0.0,
"confidence_reasons": [],
"suppressed_count": 0,
"original_id": None,
}
cluster_label = clusters[target_cluster_id].get("label", target_cluster_id)
hits[hit_id] = {
"id": hit_id,
"index": index,
"label": str(inferred_label or "other"),
"cluster_id": target_cluster_id,
"original_cluster_id": None,
"cluster_label": cluster_label,
"onset_sec": round(onset, 6),
"duration_ms": round((len(segment) / sr) * 1000.0, 1),
"rms_energy": round(rms, 6),
"spectral_centroid_hz": round(spectral_centroid, 1),
"file": rel_file,
"is_representative": False,
"source": "forced",
"suppressed": False,
"favorite": False,
"review_status": "accepted",
"confidence": 0.0,
"confidence_reasons": [],
"explicit": True,
}
clusters[target_cluster_id].setdefault("hit_ids", [])
if hit_id not in clusters[target_cluster_id]["hit_ids"]:
clusters[target_cluster_id]["hit_ids"].append(hit_id)
if not clusters[target_cluster_id].get("representative_hit_id"):
clusters[target_cluster_id]["representative_hit_id"] = hit_id
_constraint(state, "force-onset", {"hit_id": hit_id, "onset_sec": round(onset, 6)}, source=source)
_constraint(state, "force-cluster", {"hit_id": hit_id, "cluster_id": target_cluster_id}, source=source)
_event(state, "hit.force_onset", {"hit_id": hit_id, "onset_sec": round(onset, 6), "cluster_id": target_cluster_id}, source=source)
_rebuild_cluster_labels(state)
recompute_scores(state)
return _write_state(out, state)
def set_hit_review_status(output_dir: str | Path, job_id: str, hit_id: str, status: str = "accepted", source: str = "user") -> dict[str, Any]:
if status not in {"unreviewed", "accepted", "favorite"}:
raise ValueError("status must be unreviewed, accepted, or favorite")
state = load_or_create_state(job_id, output_dir)
if hit_id not in state.get("hits", {}):
raise KeyError(f"Unknown hit: {hit_id}")
_push_undo(state)
hit = state["hits"][hit_id]
hit["review_status"] = status
if status == "favorite":
hit["favorite"] = True
cid = hit.get("cluster_id")
if cid in state.get("clusters", {}):
state["clusters"][cid]["representative_hit_id"] = hit_id
_constraint(state, "pin-representative", {"hit_id": hit_id, "cluster_id": cid}, source=source)
_event(state, "hit.reviewed", {"hit_id": hit_id, "status": status}, source=source)
recompute_scores(state)
return _write_state(output_dir, state)
def accept_suggestion(output_dir: str | Path, job_id: str, suggestion_id: str) -> dict[str, Any]:
state = load_or_create_state(job_id, output_dir)
suggestion = next((s for s in state.get("suggestions", []) if s.get("id") == suggestion_id), None)
if not suggestion:
raise KeyError(f"Unknown suggestion: {suggestion_id}")
if suggestion.get("status") != "open":
return state
_push_undo(state)
stype = suggestion.get("type")
if stype in {"move-hits", "split-hits"}:
target = suggestion.get("target_cluster_id")
for hid in suggestion.get("hit_ids", []):
if hid in state.get("hits", {}) and target in state.get("clusters", {}):
current = state["hits"][hid].get("cluster_id")
if current in state["clusters"]:
state["clusters"][current]["hit_ids"] = [x for x in state["clusters"][current].get("hit_ids", []) if x != hid]
state["clusters"][target].setdefault("hit_ids", [])
if hid not in state["clusters"][target]["hit_ids"]:
state["clusters"][target]["hit_ids"].append(hid)
state["hits"][hid]["cluster_id"] = target
state["hits"][hid]["explicit"] = True
_constraint(state, "force-cluster", {"hit_id": hid, "cluster_id": target}, source="accepted-suggestion")
elif stype == "suppress-hits":
for hid in suggestion.get("hit_ids", []):
if hid in state.get("hits", {}):
state["hits"][hid]["suppressed"] = True
state["hits"][hid]["review_status"] = "suppressed"
_constraint(state, "suppress-pattern", {"example_hit_id": hid, "reason": suggestion.get("reason_code", "bleed")}, source="accepted-suggestion")
else:
raise ValueError(f"Unsupported suggestion type: {stype}")
suggestion["status"] = "accepted"
suggestion["resolved_at"] = now()
_event(state, "suggestion.accepted", {"suggestion_id": suggestion_id, "type": stype}, source="user")
_rebuild_cluster_labels(state)
recompute_scores(state)
return _write_state(output_dir, state)
def reject_suggestion(output_dir: str | Path, job_id: str, suggestion_id: str) -> dict[str, Any]:
state = load_or_create_state(job_id, output_dir)
suggestion = next((s for s in state.get("suggestions", []) if s.get("id") == suggestion_id), None)
if not suggestion:
raise KeyError(f"Unknown suggestion: {suggestion_id}")
_push_undo(state)
suggestion["status"] = "rejected"
suggestion["resolved_at"] = now()
_event(state, "suggestion.rejected", {"suggestion_id": suggestion_id, "type": suggestion.get("type")}, source="user")
return _write_state(output_dir, state)
def undo_last(output_dir: str | Path, job_id: str) -> dict[str, Any]:
state = load_or_create_state(job_id, output_dir)
stack = list(state.get("undo_stack") or [])
if not stack:
return state
restored = stack.pop()
restored["undo_stack"] = stack
_event(restored, "state.undo", {"restored_for_job_id": job_id}, source="user")
recompute_scores(restored)
return _write_state(output_dir, restored)
def explain_cluster(state: dict[str, Any], cluster_id: str) -> dict[str, Any]:
clusters = state.get("clusters", {})
hits = state.get("hits", {})
if cluster_id not in clusters:
raise KeyError(f"Unknown cluster: {cluster_id}")
cluster = clusters[cluster_id]
members = [hits[hid] for hid in cluster.get("hit_ids", []) if hid in hits]
active = [h for h in members if not h.get("suppressed")]
constraints = [c for c in state.get("constraints", []) if c.get("cluster_id") == cluster_id or c.get("hit_id") in cluster.get("hit_ids", []) or c.get("a") in cluster.get("hit_ids", []) or c.get("b") in cluster.get("hit_ids", [])]
outliers = sorted(active, key=lambda h: h.get("confidence", 0.0))[:8]
labels: dict[str, int] = {}
for hit in active:
labels[hit.get("label", "other")] = labels.get(hit.get("label", "other"), 0) + 1
return {
"cluster_id": cluster_id,
"label": cluster.get("label"),
"locked": bool(cluster.get("locked")),
"confidence": cluster.get("confidence"),
"confidence_reasons": cluster.get("confidence_reasons", []),
"representative_hit_id": cluster.get("representative_hit_id"),
"hit_count": len(members),
"active_hit_count": len(active),
"suppressed_count": sum(1 for hit in members if hit.get("suppressed")),
"label_distribution": labels,
"outliers": [{"hit_id": h["id"], "hit_index": h.get("index"), "confidence": h.get("confidence"), "reasons": h.get("confidence_reasons", [])} for h in outliers],
"constraints": constraints[-20:],
"summary": f"{cluster.get('label')} has {len(active)} active hits, confidence {cluster.get('confidence')}, and {len(constraints)} relevant constraints.",
}
def public_state(state: dict[str, Any], url_for: Callable[[str], str] | None = None, review_limit: int = 30) -> dict[str, Any]:
recompute_scores(state)
hits = copy.deepcopy(list(state.get("hits", {}).values()))
clusters = copy.deepcopy(list(state.get("clusters", {}).values()))
for hit in hits:
if url_for and hit.get("file"):
hit["url"] = url_for(hit["file"])
clusters.sort(key=lambda c: (-len(c.get("hit_ids", [])), c.get("label", "")))
hits.sort(key=lambda h: h.get("index", 0))
open_suggestions = [copy.deepcopy(s) for s in state.get("suggestions", []) if s.get("status") == "open"]
for suggestion in open_suggestions:
suggestion["diff"] = suggestion.get("diff") or suggestion_diff(state, suggestion)
open_suggestions.sort(key=lambda s: (-_safe_float(s.get("confidence")), s.get("created_at", 0)))
latest_export = copy.deepcopy(state.get("latest_export"))
if latest_export and url_for and latest_export.get("path"):
latest_export["url"] = url_for(latest_export["path"])
return {
"version": state.get("version"),
"job_id": state.get("job_id"),
"created_at": state.get("created_at"),
"updated_at": state.get("updated_at"),
"summary": {
"hit_count": len(hits),
"cluster_count": len(clusters),
"constraint_count": len(state.get("constraints", [])),
"event_count": len(state.get("events", [])),
"open_suggestion_count": len(open_suggestions),
"suppressed_hit_count": sum(1 for h in hits if h.get("suppressed")),
"locked_cluster_count": sum(1 for c in clusters if c.get("locked")),
"undo_available": bool(state.get("undo_stack")),
"forced_hit_count": sum(1 for h in hits if h.get("source") == "forced"),
"latest_export": latest_export,
},
"hits": hits,
"clusters": clusters,
"constraints": state.get("constraints", [])[-100:],
"events": state.get("events", [])[-120:],
"suggestions": open_suggestions[:50],
"review_queue": review_queue(state, review_limit),
}
def pin_representative(output_dir: str | Path, job_id: str, cluster_id: str, hit_id: str, source: str = "user") -> dict[str, Any]:
"""Persistently choose a representative hit for a cluster/card."""
state = load_or_create_state(job_id, output_dir)
clusters = state.get("clusters", {})
hits = state.get("hits", {})
if cluster_id not in clusters:
raise KeyError(f"Unknown cluster: {cluster_id}")
if hit_id not in hits:
raise KeyError(f"Unknown hit: {hit_id}")
if hit_id not in clusters[cluster_id].get("hit_ids", []):
raise ValueError(f"Hit {hit_id} is not a member of {cluster_id}")
_push_undo(state)
for hid in clusters[cluster_id].get("hit_ids", []):
if hid in hits:
hits[hid]["is_representative"] = (hid == hit_id)
clusters[cluster_id]["representative_hit_id"] = hit_id
hits[hit_id]["favorite"] = True
hits[hit_id]["review_status"] = "favorite"
hits[hit_id]["explicit"] = True
_constraint(state, "pin-representative", {"hit_id": hit_id, "cluster_id": cluster_id}, source=source)
_event(state, "cluster.representative_pinned", {"hit_id": hit_id, "cluster_id": cluster_id}, source=source)
recompute_scores(state)
return _write_state(output_dir, state)
def draw_next_representative(output_dir: str | Path, job_id: str, cluster_id: str, source: str = "user") -> dict[str, Any]:
"""Cycle a cluster/card to the next available non-suppressed candidate."""
state = load_or_create_state(job_id, output_dir)
clusters = state.get("clusters", {})
hits = state.get("hits", {})
if cluster_id not in clusters:
raise KeyError(f"Unknown cluster: {cluster_id}")
cluster = clusters[cluster_id]
active_ids = [hid for hid in cluster.get("hit_ids", []) if hid in hits and not hits[hid].get("suppressed")]
if not active_ids:
raise ValueError(f"Cluster {cluster_id} has no active hits")
current = cluster.get("representative_hit_id")
if current in active_ids:
next_id = active_ids[(active_ids.index(current) + 1) % len(active_ids)]
else:
next_id = active_ids[0]
return pin_representative(output_dir, job_id, cluster_id, next_id, source=source)
def edit_hit_timing(
output_dir: str | Path,
job_id: str,
hit_id: str,
*,
start_offset_ms: float = 0.0,
tail_offset_ms: float = 0.0,
source: str = "user",
) -> dict[str, Any]:
"""Rewrite one hit preview from stem.wav and persist the timing edit.
``start_offset_ms`` trims from the front when positive and extends earlier when
negative. ``tail_offset_ms`` extends when positive and trims the tail when
negative. The selected hit's file path is replaced so cards and supervised
exports immediately use the edited audio.
"""
import numpy as np
import soundfile as sf
import librosa
out = Path(output_dir)
state = load_or_create_state(job_id, out)
hits = state.get("hits", {})
if hit_id not in hits:
raise KeyError(f"Unknown hit: {hit_id}")
hit = hits[hit_id]
stem_path = out / "stem.wav"
if not stem_path.exists():
raise FileNotFoundError("stem.wav is required for timing edits")
audio, sr = sf.read(stem_path, dtype="float32", always_2d=False)
if audio.ndim > 1:
audio = audio.mean(axis=1)
audio = np.asarray(audio, dtype=np.float32)
original_onset = _safe_float(hit.get("onset_sec"))
original_duration = max(0.02, _safe_float(hit.get("duration_ms"), 100.0) / 1000.0)
start_offset = _safe_float(start_offset_ms) / 1000.0
tail_offset = _safe_float(tail_offset_ms) / 1000.0
new_onset = max(0.0, original_onset + start_offset)
new_duration = max(0.02, original_duration - start_offset + tail_offset)
start = max(0, int(round(new_onset * sr)))
end = min(len(audio), start + int(round(new_duration * sr)))
if end <= start:
raise ValueError("Edited sample range is outside the available stem audio")
segment = audio[start:end].copy()
fade_len = min(int(0.003 * sr), len(segment) // 4)
if fade_len > 0:
segment[-fade_len:] *= np.linspace(1, 0, fade_len)
rms = float(np.sqrt(np.mean(segment**2))) if len(segment) else 0.0
spectral_centroid = float(librosa.feature.spectral_centroid(y=segment, sr=sr).mean()) if len(segment) >= 32 else 0.0
safe_label = _safe_file_component(hit.get("label") or "edited")
rel_file = f"overrides/hits/hit_{_safe_int(hit.get('index')):05d}_{safe_label}_edited.wav"
full_path = out / rel_file
full_path.parent.mkdir(parents=True, exist_ok=True)
sf.write(full_path, segment, sr, subtype="PCM_24")
_push_undo(state)
hit["onset_sec"] = round(new_onset, 6)
hit["duration_ms"] = round((len(segment) / sr) * 1000.0, 1)
hit["rms_energy"] = round(rms, 6)
hit["spectral_centroid_hz"] = round(spectral_centroid, 1)
hit["file"] = rel_file
hit["explicit"] = True
hit["review_status"] = "accepted"
_constraint(state, "edit-hit-timing", {"hit_id": hit_id, "start_offset_ms": round(_safe_float(start_offset_ms), 3), "tail_offset_ms": round(_safe_float(tail_offset_ms), 3)}, source=source)
_event(state, "hit.timing_edited", {"hit_id": hit_id, "file": rel_file, "onset_sec": hit["onset_sec"], "duration_ms": hit["duration_ms"]}, source=source)
recompute_scores(state)
return _write_state(out, state)