Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .cursor/rules/live-tuning-ui.mdc

Large diffs are not rendered by default.

27 changes: 13 additions & 14 deletions .cursor/rules/preset-scan-rotation-set.mdc
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
---
description: Keep cleave scan preset derivation aligned with preset_switching_rotation_set
globs: cleave/config_schema.py,cleave/viz/preset_switching.py,cleave/preset_scan*.py,cleave/cli.py
description: Keep cleave scan targets aligned with live preset switching lists
globs:
- cleave/preset_scan*.py
- cleave/viz/preset_switching.py
- cleave/config_schema.py
alwaysApply: false
---

# Preset scan and rotation set
# Preset scan and switching list

When changing `PresetSwitchingRotationSet`, `PRESET_SWITCHING_ROTATION_SETS`, or how [cleave/viz/preset_switching.py](cleave/viz/preset_switching.py) builds the projectM playlist (`add_path` recurse flag, directory vs user-defined list), also update **scan set derivation** for `cleave scan`.

Plan: [docs/legacy-plans/presets-scan-plan.md](docs/legacy-plans/presets-scan-plan.md).
When changing how [cleave/viz/preset_switching.py](cleave/viz/preset_switching.py) builds the live switching list, also update **scan set derivation** for `cleave scan`.

## Experimental / low trust

`cleave scan` and `cleave scan-golden` are **experimental**. Classifier thresholds are tuned on the golden set only; treat project and bulk scan results as best-effort until validated on a pack. CLI prints a stderr warning on every `scan` / `scan-golden` invocation.

## Current contract

- Mode (`preset_switching`): `none` | `projectm` | `timeline`.
- Rotation set (`preset_switching_rotation_set`): `directory` | `user_defined` (used when mode is `projectm` or `timeline`).
- **directory** live rotation: immediate `*.milk` siblings in `playlist.current_dir`, **non-recursive** (`recurse=False`), then `sort()`.
- **user_defined** live rotation: `preset_switching_presets` via `add_presets(..., allow_duplicates=True)` (no sort).
- Project scan includes the anchor directory for **every** layer (even `preset_switching: none` or `enabled: false`), plus `preset_switching_presets` when `preset_switching_rotation_set` is `user_defined`, plus non-recursive `*.milk` in each `preset_root/roles/<role>/` pool (`bed`, `pulse`, `lead`, `accent`). Role pools are part of the live rotation surface: timeline cues with a `role` cast from those directories via `role_rotations` in [cleave/viz/preset_switching.py](cleave/viz/preset_switching.py), so scan must enumerate the same files.
- Mode (`preset_switching`): `off` | `on`.
- Trigger (`preset_switching_trigger`): `timer` | `projectm` | `timeline`.
- Live switching always uses the explicit ordered `preset_switching_list` (session `preset_list`).
- Project scan includes the anchor directory for **every** layer, plus every path in `preset_switching_list`, plus non-recursive `*.milk` in each `preset_root/roles/<role>/` pool (`bed`, `pulse`, `lead`, `accent`). Role pools feed populate-from-role actions; scan should still enumerate them.
- Bulk mode uses `--presets-dir` with optional `--recursive`; project mode does not recurse into subfolders.

## When rotation set expands
## When the live list source changes

1. Mirror the new live rule in the scan-target builder (same module family as [cleave/preset_scan.py](cleave/preset_scan.py)).
2. Extend JSON report `layers` / metadata if attribution changes.
3. Add or adjust unit tests for target enumeration (no GL required).
4. Update [docs/legacy-plans/presets-scan-plan.md](docs/legacy-plans/presets-scan-plan.md) scan set section.

Do not let play and scan disagree on which presets are in rotation.
Do not let play and scan disagree on which presets are in the switching surface.
29 changes: 11 additions & 18 deletions cleave/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
_YAML_DUMP_WIDTH = 2**31 - 1

from cleave.blend_modes import BlendMode
from cleave.cue_roles import CueRole
from cleave.effects.constants import clamp_effect_pct
from cleave.extract import StemSource
from cleave.config_schema import (
Expand All @@ -25,12 +24,8 @@
BEAT_SENSITIVITY_MIN,
DEFAULT_BEAT_SENSITIVITY,
DEFAULT_LAYER_Z_ORDER,
DEFAULT_CAST_ROLES_DEFAULT_ROLE,
DEFAULT_CAST_ROLES_TIMELINE_BEHAVIOUR,
DEFAULT_PRESET_SWITCHING,
DEFAULT_PRESET_SWITCHING_ROTATION_SET,
DEFAULT_PRESET_SWITCHING_SHUFFLE,
DEFAULT_PRESET_SWITCHING_SHUFFLE_SALT,
DEFAULT_PRESET_SWITCHING_TRIGGER,
DEFAULT_PRESET_DURATION,
DEFAULT_SOFT_CUT_DURATION,
DEFAULT_HARD_CUT_DURATION,
Expand All @@ -39,9 +34,8 @@
DEFAULT_EASTER_EGG,
DEFAULT_PRESET_START_CLEAN,
DEFAULT_PRESET_ROOT,
CastRolesTimelineBehaviour,
PresetSwitchingMode,
PresetSwitchingRotationSet,
PresetSwitchingTrigger,
DEFAULT_RENDER_OVERLAY_BACKGROUND_COLOUR,
DEFAULT_RENDER_OVERLAY_BACKGROUND_MARGIN,
DEFAULT_RENDER_OVERLAY_BACKGROUND_OPACITY,
Expand Down Expand Up @@ -72,6 +66,7 @@
DEFAULT_TIMELINE_PLACEMENT_SNAP,
DEFAULT_VISUAL_LIMITER_ENABLED,
DEFAULT_VISUAL_LIMITER_THRESHOLD,
DEFAULT_VISUAL_LIMITER_RATIO,
DEFAULT_VISUAL_LIMITER_RELEASE,
TimelinePlacementSnap,
DEFAULT_HDR_COMPOSITING,
Expand Down Expand Up @@ -128,7 +123,10 @@
DEFAULT_TIMELINE_PRESET_DENSITY,
TimelinePresetDensity,
)
from cleave.timeline_presets.reshuffle import DEFAULT_TIMELINE_PRESET_RESHUFFLE
from cleave.timeline_presets.repopulate import (
DEFAULT_TIMELINE_PRESET_REPOPULATE,
TimelinePresetRepopulate,
)
from cleave.timeline_presets.song_marker_snap import (
DEFAULT_TIMELINE_PRESET_SONG_MARKER_SNAP,
TimelinePresetSongMarkerSnap,
Expand Down Expand Up @@ -158,21 +156,15 @@ class LayerConfig:
blend_mode: BlendMode = "black-key"
locked: bool = False
preset_switching: PresetSwitchingMode = DEFAULT_PRESET_SWITCHING
preset_switching_rotation_set: PresetSwitchingRotationSet = DEFAULT_PRESET_SWITCHING_ROTATION_SET
cast_roles_timeline_behaviour: CastRolesTimelineBehaviour = (
DEFAULT_CAST_ROLES_TIMELINE_BEHAVIOUR
)
cast_roles_default_role: CueRole = DEFAULT_CAST_ROLES_DEFAULT_ROLE
preset_switching_shuffle: bool = DEFAULT_PRESET_SWITCHING_SHUFFLE
preset_switching_shuffle_salt: int = DEFAULT_PRESET_SWITCHING_SHUFFLE_SALT
preset_switching_trigger: PresetSwitchingTrigger = DEFAULT_PRESET_SWITCHING_TRIGGER
preset_duration: float = DEFAULT_PRESET_DURATION
soft_cut_duration: float = DEFAULT_SOFT_CUT_DURATION
hard_cut_duration: float = DEFAULT_HARD_CUT_DURATION
hard_cut_sensitivity: float = DEFAULT_HARD_CUT_SENSITIVITY
hard_cut_enabled: bool = DEFAULT_HARD_CUT_ENABLED
easter_egg: float = DEFAULT_EASTER_EGG
preset_start_clean: bool = DEFAULT_PRESET_START_CLEAN
preset_switching_presets: list[Path] = field(default_factory=list)
preset_switching_list: list[Path] = field(default_factory=list)


@dataclass(frozen=True)
Expand Down Expand Up @@ -318,7 +310,7 @@ class TimelinePresetConfig:
DEFAULT_TIMELINE_PRESET_SONG_MARKER_SNAP
)
timeline_cuts: TimelinePresetTimelineCuts = DEFAULT_TIMELINE_PRESET_TIMELINE_CUTS
reshuffle: bool = DEFAULT_TIMELINE_PRESET_RESHUFFLE
repopulate: TimelinePresetRepopulate = DEFAULT_TIMELINE_PRESET_REPOPULATE
conductor: bool = DEFAULT_TIMELINE_PRESET_CONDUCTOR


Expand All @@ -328,6 +320,7 @@ class TimelineLimiterConfig:

enabled: bool = DEFAULT_VISUAL_LIMITER_ENABLED
threshold: float = DEFAULT_VISUAL_LIMITER_THRESHOLD
ratio: float = DEFAULT_VISUAL_LIMITER_RATIO
release: float = DEFAULT_VISUAL_LIMITER_RELEASE


Expand Down
Loading
Loading