livepilot 1.25.0 → 1.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +9 -9
- package/installer/codex.js +87 -9
- package/livepilot/.Codex-plugin/plugin.json +8 -0
- package/livepilot/.claude-plugin/plugin.json +8 -0
- package/livepilot/.mcp.json +8 -0
- package/livepilot/agents/livepilot-producer/AGENT.md +314 -0
- package/livepilot/commands/arrange.md +47 -0
- package/livepilot/commands/beat.md +81 -0
- package/livepilot/commands/evaluate.md +49 -0
- package/livepilot/commands/memory.md +22 -0
- package/livepilot/commands/mix.md +47 -0
- package/livepilot/commands/perform.md +42 -0
- package/livepilot/commands/session.md +13 -0
- package/livepilot/commands/sounddesign.md +58 -0
- package/livepilot/rubrics/default_preset_check.md +82 -0
- package/livepilot/rubrics/layer_accumulation.md +79 -0
- package/livepilot/rubrics/layer_precision.md +79 -0
- package/livepilot/rubrics/modulation_presence.md +63 -0
- package/livepilot/rubrics/sound_design_depth.md +40 -0
- package/livepilot/skills/livepilot-arrangement/SKILL.md +164 -0
- package/livepilot/skills/livepilot-composition-engine/SKILL.md +151 -0
- package/livepilot/skills/livepilot-composition-engine/references/form-patterns.md +97 -0
- package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +102 -0
- package/livepilot/skills/livepilot-core/SKILL.md +261 -0
- package/livepilot/skills/livepilot-core/references/ableton-workflow-patterns.md +831 -0
- package/livepilot/skills/livepilot-core/references/affordances/_schema.md +160 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/auto-filter.yaml +133 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/chorus-ensemble.yaml +91 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/compressor.yaml +98 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/convolution-reverb.yaml +113 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/corpus.yaml +84 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/drift.yaml +105 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/echo.yaml +108 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/eq-eight.yaml +95 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/glue-compressor.yaml +88 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/granulator-iii.yaml +104 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/hybrid-reverb.yaml +83 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/operator.yaml +98 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/ping-pong-delay.yaml +104 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/poli.yaml +98 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/saturator.yaml +98 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/shifter.yaml +77 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/simpler.yaml +113 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/utility.yaml +95 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/vinyl-distortion.yaml +92 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/wavetable.yaml +98 -0
- package/livepilot/skills/livepilot-core/references/artist-vocabularies.md +389 -0
- package/livepilot/skills/livepilot-core/references/automation-atlas.md +272 -0
- package/livepilot/skills/livepilot-core/references/concepts/_schema.md +158 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/akufen.yaml +116 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/aphex-twin.yaml +133 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/arca-sophie.yaml +131 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/autechre.yaml +130 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/basic-channel.yaml +140 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/basinski.yaml +126 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/boards-of-canada.yaml +124 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/burial.yaml +127 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/com-truise-tycho.yaml +121 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/daft-punk.yaml +117 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/dj-premier-rza.yaml +119 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/gas.yaml +134 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/hawtin.yaml +127 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/isolee-luomo.yaml +130 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/j-dilla.yaml +133 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/jeff-mills.yaml +120 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/johannsson-richter.yaml +132 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/madlib.yaml +124 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/moodymann-theo-parrish.yaml +121 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/oneohtrix-point-never.yaml +126 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/photek-source-direct.yaml +120 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/rashad-spinn-traxman.yaml +122 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/robert-henke.yaml +113 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/shackleton.yaml +124 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/skream-mala.yaml +119 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/stars-of-the-lid.yaml +119 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/tim-hecker.yaml +122 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/villalobos.yaml +135 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/ambient.yaml +137 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/boom_bap.yaml +124 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/deep-minimal.yaml +130 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/deep_house.yaml +130 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/detroit_techno.yaml +116 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/disco.yaml +123 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/downtempo.yaml +129 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/drone.yaml +133 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/drum-and-bass.yaml +119 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/dub-techno.yaml +132 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/dub.yaml +129 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/dubstep.yaml +120 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/experimental.yaml +136 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/footwork.yaml +119 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/hip-hop.yaml +132 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/house.yaml +126 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/hyperpop.yaml +128 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/idm.yaml +134 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/lo_fi.yaml +129 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/microhouse.yaml +138 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/minimal-techno.yaml +116 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/modern-classical.yaml +123 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/soul.yaml +125 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/synthwave.yaml +123 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/techno.yaml +123 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/trap.yaml +120 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/uk-garage.yaml +121 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/00-index.md +110 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +687 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +753 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +525 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +402 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +963 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +874 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +571 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +714 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/synths-native.md +953 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
- package/livepilot/skills/livepilot-core/references/genre-vocabularies.md +382 -0
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +352 -0
- package/livepilot/skills/livepilot-core/references/memory-guide.md +178 -0
- package/livepilot/skills/livepilot-core/references/midi-recipes.md +402 -0
- package/livepilot/skills/livepilot-core/references/mixing-patterns.md +578 -0
- package/livepilot/skills/livepilot-core/references/overview.md +300 -0
- package/livepilot/skills/livepilot-core/references/pack-knowledge.md +319 -0
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
- package/livepilot/skills/livepilot-core/references/sound-design.md +393 -0
- package/livepilot/skills/livepilot-corpus-builder/SKILL.md +379 -0
- package/livepilot/skills/livepilot-creative-director/SKILL.md +455 -0
- package/livepilot/skills/livepilot-creative-director/references/anti-repetition-rules.md +214 -0
- package/livepilot/skills/livepilot-creative-director/references/creative-brief-template.md +222 -0
- package/livepilot/skills/livepilot-creative-director/references/hybrid-compilation.md +185 -0
- package/livepilot/skills/livepilot-creative-director/references/move-family-diversity-rule.md +258 -0
- package/livepilot/skills/livepilot-creative-director/references/phase-6-execution.md +409 -0
- package/livepilot/skills/livepilot-creative-director/references/the-four-move-rule.md +192 -0
- package/livepilot/skills/livepilot-devices/SKILL.md +213 -0
- package/livepilot/skills/livepilot-devices/references/load_browser_item-uri-grammar.md +82 -0
- package/livepilot/skills/livepilot-evaluation/SKILL.md +195 -0
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +176 -0
- package/livepilot/skills/livepilot-evaluation/references/evaluation-contracts.md +121 -0
- package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +110 -0
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +136 -0
- package/livepilot/skills/livepilot-mix-engine/references/mix-critics.md +143 -0
- package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +105 -0
- package/livepilot/skills/livepilot-mixing/SKILL.md +157 -0
- package/livepilot/skills/livepilot-notes/SKILL.md +130 -0
- package/livepilot/skills/livepilot-performance-engine/SKILL.md +122 -0
- package/livepilot/skills/livepilot-performance-engine/references/performance-safety.md +98 -0
- package/livepilot/skills/livepilot-release/SKILL.md +151 -0
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +117 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +225 -0
- package/livepilot/skills/livepilot-sound-design-engine/references/patch-model.md +119 -0
- package/livepilot/skills/livepilot-sound-design-engine/references/sound-design-critics.md +118 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +143 -0
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/LivePilot_Elektron.amxd +0 -0
- package/m4l_device/LivePilot_Elektron.maxpat +758 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/m4l_device/livepilot_elektron_bridge.js +82 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/tools.py +63 -12
- package/mcp_server/audit/checks.py +167 -6
- package/mcp_server/audit/state.py +88 -0
- package/mcp_server/audit/tools.py +6 -69
- package/mcp_server/composer/develop/apply.py +2 -2
- package/mcp_server/composer/full/apply.py +32 -6
- package/mcp_server/grader/__init__.py +17 -0
- package/mcp_server/grader/client.py +647 -0
- package/mcp_server/grader/iterator.py +57 -0
- package/mcp_server/grader/tools.py +263 -0
- package/mcp_server/m4l_bridge.py +5 -0
- package/mcp_server/runtime/execution_router.py +6 -0
- package/mcp_server/runtime/mcp_dispatch.py +18 -0
- package/mcp_server/runtime/remote_commands.py +2 -0
- package/mcp_server/server.py +12 -7
- package/mcp_server/tools/browser.py +68 -0
- package/package.json +20 -5
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/server.py +63 -2
- package/requirements.txt +24 -3
- package/server.json +3 -3
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"""Mechanical rubric grader.
|
|
2
|
+
|
|
3
|
+
Phase 1 — §7.3 layer accumulation only. Each check is a pure function of
|
|
4
|
+
session state, returning the same shape used by `mcp_server/audit/checks.py`:
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
"id": str,
|
|
8
|
+
"passed": bool,
|
|
9
|
+
"severity": "pass" | "warn" | "fail" | "n/a",
|
|
10
|
+
"summary": str,
|
|
11
|
+
"issues": [{"code": str, "detail": str, "track_index": int | None}, ...],
|
|
12
|
+
"evidence": {...},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
`evaluate(rubric_id, state)` aggregates per-criterion results into a Verdict.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Callable, Iterable
|
|
21
|
+
|
|
22
|
+
from mcp_server.audit import checks as audit_checks
|
|
23
|
+
from mcp_server.audit.checks import infer_role
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_TRACK_COUNT_WARN = 8
|
|
27
|
+
_TRACK_COUNT_FAIL = 12
|
|
28
|
+
|
|
29
|
+
_BURIED_THRESHOLD = 0.15
|
|
30
|
+
_GHOST_KEYWORDS: tuple[str, ...] = ("ghost", "_g ", "_g_", " gh ", "gh_")
|
|
31
|
+
|
|
32
|
+
_ROLE_VOLUME_BANDS: dict[str, tuple[float, float]] = {
|
|
33
|
+
"kick": (0.60, 0.85),
|
|
34
|
+
"bass": (0.60, 0.85),
|
|
35
|
+
"snare": (0.55, 0.80),
|
|
36
|
+
"hat": (0.40, 0.70),
|
|
37
|
+
"perc": (0.40, 0.65),
|
|
38
|
+
"lead": (0.50, 0.80),
|
|
39
|
+
"vox": (0.55, 0.85),
|
|
40
|
+
"pad": (0.25, 0.50),
|
|
41
|
+
"atmos": (0.25, 0.45),
|
|
42
|
+
"fx": (0.30, 0.70),
|
|
43
|
+
"unknown": (0.30, 0.80),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Banned-default detection uses (class_name, name) FINGERPRINTS rather than
|
|
47
|
+
# class names alone. Live's runtime class taxonomy doesn't match user-facing
|
|
48
|
+
# brand names — surveyed live in 2026-05-08 with load_browser_item:
|
|
49
|
+
# Drift → class="Drift", name="Drift" (native)
|
|
50
|
+
# Analog → class="UltraAnalog", name="Analog" (native)
|
|
51
|
+
# Meld → class="InstrumentMeld", name="Meld" (native)
|
|
52
|
+
# Poli → class="MxDeviceInstrument", name="Poli" (M4L wrapper)
|
|
53
|
+
# The fingerprint approach catches all four; the previous flat-set approach
|
|
54
|
+
# only caught Drift.
|
|
55
|
+
_BANNED_DEFAULT_FINGERPRINTS: frozenset[tuple[str, str]] = frozenset({
|
|
56
|
+
("drift", "drift"),
|
|
57
|
+
("ultraanalog", "analog"),
|
|
58
|
+
("instrumentmeld", "meld"),
|
|
59
|
+
("mxdeviceinstrument", "poli"),
|
|
60
|
+
})
|
|
61
|
+
_BANNED_DEFAULT_ROLES: frozenset[str] = frozenset({"bass", "pad", "lead"})
|
|
62
|
+
_SUBTRACTIVE_EXCEPTION_KEYWORDS: tuple[str, ...] = ("subtractive", "analog sub", "_sub_synth")
|
|
63
|
+
|
|
64
|
+
# Instrument-class set used by the modulation-presence guard (Fix #2).
|
|
65
|
+
# Includes Live's actual runtime class names, not user-facing brand names.
|
|
66
|
+
_INSTRUMENT_CLASSES: frozenset[str] = frozenset({
|
|
67
|
+
"operator", "wavetable", "drift",
|
|
68
|
+
"ultraanalog", # Analog
|
|
69
|
+
"instrumentmeld", # Meld
|
|
70
|
+
"mxdeviceinstrument", # Poli + every other M4L instrument
|
|
71
|
+
"tension", "collision",
|
|
72
|
+
"simpler", "originalsimpler", "sampler", "multisampler",
|
|
73
|
+
"electric", "loungelizard", # Electric → LoungeLizard runtime class
|
|
74
|
+
"drumgroup", "drumrack", "drum rack", "drumgroupdevice",
|
|
75
|
+
"instrumentgroupdevice", "instrumentrack",
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
_MODULATION_REQUIRED_ROLES: frozenset[str] = frozenset({"bass", "pad", "lead", "vox", "atmos"})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _is_ghost(name: str) -> bool:
|
|
82
|
+
n = (name or "").lower()
|
|
83
|
+
return any(kw in n for kw in _GHOST_KEYWORDS)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _track_volume(track: dict) -> float | None:
|
|
87
|
+
mixer = track.get("mixer") or {}
|
|
88
|
+
vol = mixer.get("volume")
|
|
89
|
+
return float(vol) if vol is not None else None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _check_track_count_within_limit(state: dict) -> dict:
|
|
93
|
+
tracks = list(state.get("tracks") or [])
|
|
94
|
+
n = len(tracks)
|
|
95
|
+
if n <= _TRACK_COUNT_WARN:
|
|
96
|
+
severity = "pass"
|
|
97
|
+
summary = f"{n} tracks — within sustainable range (≤{_TRACK_COUNT_WARN})"
|
|
98
|
+
issues: list[dict] = []
|
|
99
|
+
elif n < _TRACK_COUNT_FAIL:
|
|
100
|
+
severity = "warn"
|
|
101
|
+
summary = f"{n} tracks — approaching §7.3 ceiling (warn at >{_TRACK_COUNT_WARN}, fail at ≥{_TRACK_COUNT_FAIL})"
|
|
102
|
+
issues = [{
|
|
103
|
+
"code": "track_count_high",
|
|
104
|
+
"detail": f"{n} tracks present. Consider deleting 1–{n - _TRACK_COUNT_WARN} weakest layers before adding more.",
|
|
105
|
+
"track_index": None,
|
|
106
|
+
}]
|
|
107
|
+
else:
|
|
108
|
+
severity = "fail"
|
|
109
|
+
summary = f"{n} tracks — exceeds §7.3 ceiling (≥{_TRACK_COUNT_FAIL}). 5–6 great layers > {n} mediocre."
|
|
110
|
+
issues = [{
|
|
111
|
+
"code": "track_count_exceeded",
|
|
112
|
+
"detail": f"{n} tracks present. §7.3 demands fewer, better layers — delete the weakest until ≤{_TRACK_COUNT_WARN}.",
|
|
113
|
+
"track_index": None,
|
|
114
|
+
}]
|
|
115
|
+
return {
|
|
116
|
+
"id": "track_count_within_limit",
|
|
117
|
+
"passed": severity in ("pass", "warn"),
|
|
118
|
+
"severity": severity,
|
|
119
|
+
"summary": summary,
|
|
120
|
+
"issues": issues,
|
|
121
|
+
"evidence": {"track_count": n, "warn_threshold": _TRACK_COUNT_WARN, "fail_threshold": _TRACK_COUNT_FAIL},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _check_no_extreme_buried_track(state: dict) -> dict:
|
|
126
|
+
tracks = list(state.get("tracks") or [])
|
|
127
|
+
buried_non_ghost: list[dict] = []
|
|
128
|
+
buried_ghost: list[dict] = []
|
|
129
|
+
for t in tracks:
|
|
130
|
+
vol = _track_volume(t)
|
|
131
|
+
if vol is None or vol >= _BURIED_THRESHOLD:
|
|
132
|
+
continue
|
|
133
|
+
entry = {"index": t.get("index"), "name": t.get("name"), "volume": round(vol, 3)}
|
|
134
|
+
if _is_ghost(t.get("name") or ""):
|
|
135
|
+
buried_ghost.append(entry)
|
|
136
|
+
else:
|
|
137
|
+
buried_non_ghost.append(entry)
|
|
138
|
+
|
|
139
|
+
if not buried_non_ghost:
|
|
140
|
+
return {
|
|
141
|
+
"id": "no_extreme_buried_track",
|
|
142
|
+
"passed": True,
|
|
143
|
+
"severity": "pass",
|
|
144
|
+
"summary": (
|
|
145
|
+
"No buried tracks below 0.15"
|
|
146
|
+
if not buried_ghost
|
|
147
|
+
else f"{len(buried_ghost)} buried track(s) all ghost-tagged — OK"
|
|
148
|
+
),
|
|
149
|
+
"issues": [],
|
|
150
|
+
"evidence": {"buried_non_ghost": [], "buried_ghost": buried_ghost},
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"id": "no_extreme_buried_track",
|
|
155
|
+
"passed": False,
|
|
156
|
+
"severity": "fail",
|
|
157
|
+
"summary": f"{len(buried_non_ghost)} non-ghost track(s) at volume < {_BURIED_THRESHOLD} — delete or feature them",
|
|
158
|
+
"issues": [
|
|
159
|
+
{
|
|
160
|
+
"code": "extreme_buried_track",
|
|
161
|
+
"detail": f"Track '{e['name']}' at volume {e['volume']}. §7.3: delete it or feature it, don't bury it.",
|
|
162
|
+
"track_index": e["index"],
|
|
163
|
+
}
|
|
164
|
+
for e in buried_non_ghost
|
|
165
|
+
],
|
|
166
|
+
"evidence": {"buried_non_ghost": buried_non_ghost, "buried_ghost": buried_ghost, "threshold": _BURIED_THRESHOLD},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _check_role_volume_hierarchy(state: dict) -> dict:
|
|
171
|
+
tracks = list(state.get("tracks") or [])
|
|
172
|
+
out_of_band: list[dict] = []
|
|
173
|
+
in_band_count = 0
|
|
174
|
+
skipped_unknown = 0
|
|
175
|
+
for t in tracks:
|
|
176
|
+
vol = _track_volume(t)
|
|
177
|
+
if vol is None:
|
|
178
|
+
continue
|
|
179
|
+
role = infer_role(t.get("name") or "", t.get("devices") or [])
|
|
180
|
+
# No role inferred → no expected band → skip. Live's default fader
|
|
181
|
+
# is 0.85 (unity); applying any band to unrecognised tracks fires
|
|
182
|
+
# false positives on every fresh project.
|
|
183
|
+
if role == "unknown":
|
|
184
|
+
skipped_unknown += 1
|
|
185
|
+
continue
|
|
186
|
+
band = _ROLE_VOLUME_BANDS.get(role) or _ROLE_VOLUME_BANDS["unknown"]
|
|
187
|
+
if band[0] <= vol <= band[1]:
|
|
188
|
+
in_band_count += 1
|
|
189
|
+
continue
|
|
190
|
+
out_of_band.append({
|
|
191
|
+
"index": t.get("index"),
|
|
192
|
+
"name": t.get("name"),
|
|
193
|
+
"role": role,
|
|
194
|
+
"volume": round(vol, 3),
|
|
195
|
+
"band": [band[0], band[1]],
|
|
196
|
+
"direction": "above" if vol > band[1] else "below",
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
if not out_of_band:
|
|
200
|
+
if in_band_count == 0 and skipped_unknown > 0:
|
|
201
|
+
summary = f"No role-tagged tracks to check ({skipped_unknown} skipped as unknown role)"
|
|
202
|
+
else:
|
|
203
|
+
summary = f"All {in_band_count} role-tagged track(s) within role volume band"
|
|
204
|
+
if skipped_unknown:
|
|
205
|
+
summary += f" ({skipped_unknown} unknown-role skipped)"
|
|
206
|
+
return {
|
|
207
|
+
"id": "role_volume_hierarchy",
|
|
208
|
+
"passed": True,
|
|
209
|
+
"severity": "pass",
|
|
210
|
+
"summary": summary,
|
|
211
|
+
"issues": [],
|
|
212
|
+
"evidence": {
|
|
213
|
+
"in_band": in_band_count,
|
|
214
|
+
"out_of_band": [],
|
|
215
|
+
"skipped_unknown": skipped_unknown,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"id": "role_volume_hierarchy",
|
|
221
|
+
"passed": True, # advisory — flag, don't block
|
|
222
|
+
"severity": "warn",
|
|
223
|
+
"summary": (
|
|
224
|
+
f"{len(out_of_band)} role-tagged track(s) outside role volume band (advisory)"
|
|
225
|
+
+ (f" ({skipped_unknown} unknown-role skipped)" if skipped_unknown else "")
|
|
226
|
+
),
|
|
227
|
+
"issues": [
|
|
228
|
+
{
|
|
229
|
+
"code": "role_volume_off_band",
|
|
230
|
+
"detail": (
|
|
231
|
+
f"Track '{e['name']}' (role={e['role']}) at volume {e['volume']} — "
|
|
232
|
+
f"{e['direction']} expected band {e['band']}. "
|
|
233
|
+
f"{'Pad/atmos shouldn’t dominate.' if e['direction'] == 'above' and e['role'] in ('pad', 'atmos') else ''}"
|
|
234
|
+
f"{'Anchor role too quiet — should carry.' if e['direction'] == 'below' and e['role'] in ('kick', 'bass', 'vox') else ''}"
|
|
235
|
+
).strip(),
|
|
236
|
+
"track_index": e["index"],
|
|
237
|
+
}
|
|
238
|
+
for e in out_of_band
|
|
239
|
+
],
|
|
240
|
+
"evidence": {
|
|
241
|
+
"in_band": in_band_count,
|
|
242
|
+
"out_of_band": out_of_band,
|
|
243
|
+
"skipped_unknown": skipped_unknown,
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _first_instrument_device(devices: list[dict]) -> dict | None:
|
|
249
|
+
for d in devices or []:
|
|
250
|
+
cls = (d.get("class_name") or "").lower()
|
|
251
|
+
if cls in _INSTRUMENT_CLASSES:
|
|
252
|
+
return d
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _is_banned_default_fingerprint(device: dict) -> bool:
|
|
257
|
+
"""Match (class_name, name) against the banned-default fingerprint set.
|
|
258
|
+
|
|
259
|
+
Fires only when both class AND device-display-name match a banned synth's
|
|
260
|
+
default-loaded state. A preset-applied device has device.name set to the
|
|
261
|
+
preset stem, so it falls out of the fingerprint set automatically.
|
|
262
|
+
"""
|
|
263
|
+
cls = (device.get("class_name") or "").strip().lower()
|
|
264
|
+
name = (device.get("name") or "").strip().lower()
|
|
265
|
+
return (cls, name) in _BANNED_DEFAULT_FINGERPRINTS
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _is_subtractive_exception(track_name: str) -> bool:
|
|
269
|
+
n = (track_name or "").lower()
|
|
270
|
+
return any(kw in n for kw in _SUBTRACTIVE_EXCEPTION_KEYWORDS)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _check_no_banned_default_instruments(state: dict) -> dict:
|
|
274
|
+
tracks = list(state.get("tracks") or [])
|
|
275
|
+
violations: list[dict] = []
|
|
276
|
+
skipped_exceptions: list[dict] = []
|
|
277
|
+
for t in tracks:
|
|
278
|
+
role = infer_role(t.get("name") or "", t.get("devices") or [])
|
|
279
|
+
if role not in _BANNED_DEFAULT_ROLES:
|
|
280
|
+
continue
|
|
281
|
+
instr = _first_instrument_device(t.get("devices") or [])
|
|
282
|
+
if not instr:
|
|
283
|
+
continue
|
|
284
|
+
if not _is_banned_default_fingerprint(instr):
|
|
285
|
+
continue
|
|
286
|
+
entry = {
|
|
287
|
+
"index": t.get("index"),
|
|
288
|
+
"name": t.get("name"),
|
|
289
|
+
"role": role,
|
|
290
|
+
"class_name": instr.get("class_name"),
|
|
291
|
+
"device_name": instr.get("name"),
|
|
292
|
+
}
|
|
293
|
+
if _is_subtractive_exception(t.get("name") or ""):
|
|
294
|
+
skipped_exceptions.append(entry)
|
|
295
|
+
else:
|
|
296
|
+
violations.append(entry)
|
|
297
|
+
|
|
298
|
+
if not violations:
|
|
299
|
+
return {
|
|
300
|
+
"id": "no_banned_default_instruments",
|
|
301
|
+
"passed": True,
|
|
302
|
+
"severity": "pass",
|
|
303
|
+
"summary": (
|
|
304
|
+
"No banned-default synths on melodic-role tracks"
|
|
305
|
+
if not skipped_exceptions
|
|
306
|
+
else f"All banned-default loads explicitly tagged as subtractive ({len(skipped_exceptions)} skipped)"
|
|
307
|
+
),
|
|
308
|
+
"issues": [],
|
|
309
|
+
"evidence": {"violations": [], "subtractive_exceptions": skipped_exceptions},
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
"id": "no_banned_default_instruments",
|
|
314
|
+
"passed": False,
|
|
315
|
+
"severity": "fail",
|
|
316
|
+
"summary": f"{len(violations)} melodic-role track(s) using banned-default synth (§1)",
|
|
317
|
+
"issues": [
|
|
318
|
+
{
|
|
319
|
+
"code": "banned_default_instrument",
|
|
320
|
+
"detail": (
|
|
321
|
+
f"Track '{v['name']}' (role={v['role']}) starts with default-loaded "
|
|
322
|
+
f"{v['class_name']}. §1: hunt the library — atlas_search, search_browser, "
|
|
323
|
+
"or sample-based / granular / physical-modeling source. "
|
|
324
|
+
"Tag track name with 'subtractive' if this is a deliberate analog choice."
|
|
325
|
+
),
|
|
326
|
+
"track_index": v["index"],
|
|
327
|
+
}
|
|
328
|
+
for v in violations
|
|
329
|
+
],
|
|
330
|
+
"evidence": {"violations": violations, "subtractive_exceptions": skipped_exceptions},
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _check_melodic_layers_have_motion(state: dict) -> dict:
|
|
335
|
+
"""§4 — every melodic/harmonic layer should have ≥1 form of motion.
|
|
336
|
+
|
|
337
|
+
State must populate per track:
|
|
338
|
+
- modulation_count: int (sum of mod-matrix non-zero entries)
|
|
339
|
+
- has_clip_automation: bool
|
|
340
|
+
|
|
341
|
+
Tracks missing both keys are reported as `unknown` and the check
|
|
342
|
+
degrades to n/a if no track has either signal.
|
|
343
|
+
"""
|
|
344
|
+
tracks = list(state.get("tracks") or [])
|
|
345
|
+
static_tracks: list[dict] = []
|
|
346
|
+
moving_tracks: list[dict] = []
|
|
347
|
+
unknown_tracks: list[dict] = []
|
|
348
|
+
|
|
349
|
+
for t in tracks:
|
|
350
|
+
role = infer_role(t.get("name") or "", t.get("devices") or [])
|
|
351
|
+
if role not in _MODULATION_REQUIRED_ROLES:
|
|
352
|
+
continue
|
|
353
|
+
mod_count = t.get("modulation_count")
|
|
354
|
+
has_auto = t.get("has_clip_automation")
|
|
355
|
+
if mod_count is None and has_auto is None:
|
|
356
|
+
unknown_tracks.append({
|
|
357
|
+
"index": t.get("index"), "name": t.get("name"), "role": role,
|
|
358
|
+
})
|
|
359
|
+
continue
|
|
360
|
+
has_motion = (mod_count or 0) > 0 or bool(has_auto)
|
|
361
|
+
entry = {
|
|
362
|
+
"index": t.get("index"),
|
|
363
|
+
"name": t.get("name"),
|
|
364
|
+
"role": role,
|
|
365
|
+
"modulation_count": mod_count or 0,
|
|
366
|
+
"has_clip_automation": bool(has_auto),
|
|
367
|
+
}
|
|
368
|
+
(moving_tracks if has_motion else static_tracks).append(entry)
|
|
369
|
+
|
|
370
|
+
n_checked = len(moving_tracks) + len(static_tracks)
|
|
371
|
+
|
|
372
|
+
if n_checked == 0:
|
|
373
|
+
return {
|
|
374
|
+
"id": "melodic_layers_have_motion",
|
|
375
|
+
"passed": True,
|
|
376
|
+
"severity": "n/a",
|
|
377
|
+
"summary": (
|
|
378
|
+
"No melodic-role tracks present"
|
|
379
|
+
if not unknown_tracks
|
|
380
|
+
else f"Modulation data missing for {len(unknown_tracks)} melodic-role track(s)"
|
|
381
|
+
),
|
|
382
|
+
"issues": [],
|
|
383
|
+
"evidence": {"moving": [], "static": [], "unknown": unknown_tracks},
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if not static_tracks:
|
|
387
|
+
return {
|
|
388
|
+
"id": "melodic_layers_have_motion",
|
|
389
|
+
"passed": True,
|
|
390
|
+
"severity": "pass",
|
|
391
|
+
"summary": f"All {n_checked} melodic-role layer(s) have modulation or automation",
|
|
392
|
+
"issues": [],
|
|
393
|
+
"evidence": {"moving": moving_tracks, "static": [], "unknown": unknown_tracks},
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
"id": "melodic_layers_have_motion",
|
|
398
|
+
"passed": True,
|
|
399
|
+
"severity": "warn",
|
|
400
|
+
"summary": (
|
|
401
|
+
f"{len(static_tracks)} of {n_checked} melodic-role layer(s) static "
|
|
402
|
+
"(no modulation routings, no clip automation)"
|
|
403
|
+
),
|
|
404
|
+
"issues": [
|
|
405
|
+
{
|
|
406
|
+
"code": "static_melodic_layer",
|
|
407
|
+
"detail": (
|
|
408
|
+
f"Track '{t['name']}' (role={t['role']}) has 0 modulation routings "
|
|
409
|
+
"and no clip automation. §4: add LFO routing, mod-matrix entry, or "
|
|
410
|
+
"automation curve. Static MIDI at default velocity ≈ 'didn't try'."
|
|
411
|
+
),
|
|
412
|
+
"track_index": t["index"],
|
|
413
|
+
}
|
|
414
|
+
for t in static_tracks
|
|
415
|
+
],
|
|
416
|
+
"evidence": {"moving": moving_tracks, "static": static_tracks, "unknown": unknown_tracks},
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _aggregate_per_track(
|
|
421
|
+
*,
|
|
422
|
+
criterion_id: str,
|
|
423
|
+
state: dict,
|
|
424
|
+
args_for_track: Callable[[dict, dict, str], tuple | None],
|
|
425
|
+
check_fn: Callable[..., dict],
|
|
426
|
+
pass_summary: str,
|
|
427
|
+
) -> dict:
|
|
428
|
+
"""Run an audit check function per track, aggregate into one verdict.
|
|
429
|
+
|
|
430
|
+
args_for_track(state, track, role) returns the args tuple for check_fn,
|
|
431
|
+
or None to skip the track entirely. The audit check's own n/a returns
|
|
432
|
+
are filtered out at aggregation time so they don't drag down the result.
|
|
433
|
+
"""
|
|
434
|
+
tracks = list(state.get("tracks") or [])
|
|
435
|
+
per_track: list[dict] = []
|
|
436
|
+
for t in tracks:
|
|
437
|
+
role = infer_role(t.get("name") or "", t.get("devices") or [])
|
|
438
|
+
args = args_for_track(state, t, role)
|
|
439
|
+
if args is None:
|
|
440
|
+
continue
|
|
441
|
+
try:
|
|
442
|
+
result = check_fn(*args)
|
|
443
|
+
except Exception as exc:
|
|
444
|
+
per_track.append({
|
|
445
|
+
"track_index": t.get("index"),
|
|
446
|
+
"name": t.get("name"),
|
|
447
|
+
"role": role,
|
|
448
|
+
"severity": "n/a",
|
|
449
|
+
"summary": f"check failed: {type(exc).__name__}",
|
|
450
|
+
"issues": [],
|
|
451
|
+
"evidence": {},
|
|
452
|
+
})
|
|
453
|
+
continue
|
|
454
|
+
per_track.append({
|
|
455
|
+
"track_index": t.get("index"),
|
|
456
|
+
"name": t.get("name"),
|
|
457
|
+
"role": role,
|
|
458
|
+
**result,
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
actionable = [r for r in per_track if r["severity"] != "n/a"]
|
|
462
|
+
|
|
463
|
+
if not actionable:
|
|
464
|
+
return {
|
|
465
|
+
"id": criterion_id,
|
|
466
|
+
"passed": True,
|
|
467
|
+
"severity": "n/a",
|
|
468
|
+
"summary": "No checkable tracks (data missing or no applicable role)",
|
|
469
|
+
"issues": [],
|
|
470
|
+
"evidence": {"per_track": per_track},
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
has_fail = any(r["severity"] == "fail" for r in actionable)
|
|
474
|
+
has_warn = any(r["severity"] == "warn" for r in actionable)
|
|
475
|
+
|
|
476
|
+
if has_fail:
|
|
477
|
+
rubric_severity, passed = "fail", False
|
|
478
|
+
elif has_warn:
|
|
479
|
+
rubric_severity, passed = "warn", True
|
|
480
|
+
else:
|
|
481
|
+
rubric_severity, passed = "pass", True
|
|
482
|
+
|
|
483
|
+
issues: list[dict] = []
|
|
484
|
+
for r in actionable:
|
|
485
|
+
if r["severity"] in ("warn", "fail"):
|
|
486
|
+
for issue in r.get("issues") or []:
|
|
487
|
+
issues.append({
|
|
488
|
+
"code": issue.get("code", ""),
|
|
489
|
+
"detail": f"Track '{r['name']}' (role={r['role']}): {issue.get('detail', '')}",
|
|
490
|
+
"track_index": r["track_index"],
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
n_pass = sum(1 for r in actionable if r["severity"] == "pass")
|
|
494
|
+
n_warn = sum(1 for r in actionable if r["severity"] == "warn")
|
|
495
|
+
n_fail = sum(1 for r in actionable if r["severity"] == "fail")
|
|
496
|
+
summary = (
|
|
497
|
+
pass_summary
|
|
498
|
+
if rubric_severity == "pass"
|
|
499
|
+
else f"{len(actionable)} checked: {n_pass} pass, {n_warn} warn, {n_fail} fail"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
"id": criterion_id,
|
|
504
|
+
"passed": passed,
|
|
505
|
+
"severity": rubric_severity,
|
|
506
|
+
"summary": summary,
|
|
507
|
+
"issues": issues,
|
|
508
|
+
"evidence": {"per_track": per_track},
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _check_timbre_per_track(state: dict) -> dict:
|
|
513
|
+
return _aggregate_per_track(
|
|
514
|
+
criterion_id="timbre_per_track",
|
|
515
|
+
state=state,
|
|
516
|
+
args_for_track=lambda s, t, role: (role, t.get("fingerprint")),
|
|
517
|
+
check_fn=audit_checks.check_timbre,
|
|
518
|
+
pass_summary="All checked tracks have role-appropriate spectral shape",
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _check_sequence_per_track(state: dict) -> dict:
|
|
523
|
+
return _aggregate_per_track(
|
|
524
|
+
criterion_id="sequence_per_track",
|
|
525
|
+
state=state,
|
|
526
|
+
args_for_track=lambda s, t, role: (role, t.get("notes_per_clip") or []),
|
|
527
|
+
check_fn=audit_checks.check_sequence,
|
|
528
|
+
pass_summary="All MIDI tracks meet sequence bar (humanization + ghosts + variation)",
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _check_stereo_per_track(state: dict) -> dict:
|
|
533
|
+
return _aggregate_per_track(
|
|
534
|
+
criterion_id="stereo_per_track",
|
|
535
|
+
state=state,
|
|
536
|
+
args_for_track=lambda s, t, role: (role, t),
|
|
537
|
+
check_fn=audit_checks.check_stereo,
|
|
538
|
+
pass_summary="No anti-pattern panning detected",
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _check_masking_per_track(state: dict) -> dict:
|
|
543
|
+
masking_report = state.get("masking_report")
|
|
544
|
+
return _aggregate_per_track(
|
|
545
|
+
criterion_id="masking_per_track",
|
|
546
|
+
state=state,
|
|
547
|
+
args_for_track=lambda s, t, role: (t.get("index"), masking_report) if masking_report else None,
|
|
548
|
+
check_fn=audit_checks.check_masking,
|
|
549
|
+
pass_summary="No detected cross-track masking collisions",
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _modulation_args(state: dict, track: dict, role: str) -> tuple | None:
|
|
554
|
+
"""Skip tracks that have no instrument-class device.
|
|
555
|
+
|
|
556
|
+
`audit_checks.check_modulation` returns 'no_movement' on empty-device
|
|
557
|
+
tracks because routings=0 — but there's nothing on the track to
|
|
558
|
+
modulate. Audio tracks, FX-only buses, and fresh empty tracks are
|
|
559
|
+
not candidates for §4. Pre-filter here.
|
|
560
|
+
"""
|
|
561
|
+
devices = track.get("devices") or []
|
|
562
|
+
if _first_instrument_device(devices) is None:
|
|
563
|
+
return None
|
|
564
|
+
return (
|
|
565
|
+
role,
|
|
566
|
+
devices,
|
|
567
|
+
bool(track.get("has_clip_automation")),
|
|
568
|
+
int(track.get("wavetable_mod_routings", 0)),
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _check_modulation_per_track(state: dict) -> dict:
|
|
573
|
+
return _aggregate_per_track(
|
|
574
|
+
criterion_id="modulation_per_track",
|
|
575
|
+
state=state,
|
|
576
|
+
args_for_track=_modulation_args,
|
|
577
|
+
check_fn=audit_checks.check_modulation,
|
|
578
|
+
pass_summary="All instrument tracks have ≥1 modulation routing or automation",
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _check_params_per_track(state: dict) -> dict:
|
|
583
|
+
return _aggregate_per_track(
|
|
584
|
+
criterion_id="params_per_track",
|
|
585
|
+
state=state,
|
|
586
|
+
args_for_track=lambda s, t, role: (role, t.get("devices") or []),
|
|
587
|
+
check_fn=audit_checks.check_params,
|
|
588
|
+
pass_summary="All instrument tracks show evidence of parameter programming (§2)",
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _check_effects_per_track(state: dict) -> dict:
|
|
593
|
+
return _aggregate_per_track(
|
|
594
|
+
criterion_id="effects_per_track",
|
|
595
|
+
state=state,
|
|
596
|
+
args_for_track=lambda s, t, role: (role, t.get("devices") or []),
|
|
597
|
+
check_fn=audit_checks.check_effects,
|
|
598
|
+
pass_summary="Required effects categories present per role",
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
_RUBRICS: dict[str, list[Callable[[dict], dict]]] = {
|
|
603
|
+
"layer_accumulation": [
|
|
604
|
+
_check_track_count_within_limit,
|
|
605
|
+
_check_no_extreme_buried_track,
|
|
606
|
+
_check_role_volume_hierarchy,
|
|
607
|
+
],
|
|
608
|
+
"default_preset_check": [
|
|
609
|
+
_check_no_banned_default_instruments,
|
|
610
|
+
],
|
|
611
|
+
"modulation_presence": [
|
|
612
|
+
_check_melodic_layers_have_motion,
|
|
613
|
+
],
|
|
614
|
+
"layer_precision": [
|
|
615
|
+
_check_timbre_per_track,
|
|
616
|
+
_check_sequence_per_track,
|
|
617
|
+
_check_stereo_per_track,
|
|
618
|
+
_check_masking_per_track,
|
|
619
|
+
_check_modulation_per_track,
|
|
620
|
+
_check_params_per_track,
|
|
621
|
+
_check_effects_per_track,
|
|
622
|
+
],
|
|
623
|
+
"sound_design_depth": [
|
|
624
|
+
_check_params_per_track,
|
|
625
|
+
],
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def evaluate(rubric_id: str, state: dict[str, Any]) -> dict[str, Any]:
|
|
630
|
+
"""Run all checks for a rubric, return aggregated verdict.
|
|
631
|
+
|
|
632
|
+
Raises KeyError if rubric_id is unknown.
|
|
633
|
+
"""
|
|
634
|
+
checks = _RUBRICS[rubric_id]
|
|
635
|
+
results = [check(state) for check in checks]
|
|
636
|
+
blocking_failed = any(
|
|
637
|
+
not r["passed"] and r["severity"] == "fail" for r in results
|
|
638
|
+
)
|
|
639
|
+
return {
|
|
640
|
+
"rubric_id": rubric_id,
|
|
641
|
+
"passed": not blocking_failed,
|
|
642
|
+
"criteria": results,
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def list_rubrics() -> list[str]:
|
|
647
|
+
return list(_RUBRICS.keys())
|