livepilot 1.9.23 → 1.9.24
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/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +46 -0
- package/README.md +94 -0
- package/livepilot/.Codex-plugin/plugin.json +1 -1
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/skills/livepilot-core/references/overview.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +14 -0
- package/livepilot.mcpb +0 -0
- package/manifest.json +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/hook_hunter/analyzer.py +23 -0
- package/mcp_server/hook_hunter/models.py +1 -0
- package/mcp_server/hook_hunter/tools.py +4 -2
- package/mcp_server/memory/taste_graph.py +68 -1
- package/mcp_server/memory/tools.py +15 -4
- package/mcp_server/musical_intelligence/detectors.py +14 -1
- package/mcp_server/musical_intelligence/tools.py +11 -8
- package/mcp_server/persistence/__init__.py +1 -0
- package/mcp_server/persistence/base_store.py +82 -0
- package/mcp_server/persistence/project_store.py +106 -0
- package/mcp_server/persistence/taste_store.py +122 -0
- package/mcp_server/preview_studio/models.py +1 -0
- package/mcp_server/preview_studio/tools.py +56 -13
- package/mcp_server/runtime/capability.py +66 -0
- package/mcp_server/runtime/capability_probe.py +118 -0
- package/mcp_server/runtime/execution_router.py +139 -0
- package/mcp_server/runtime/remote_commands.py +82 -0
- package/mcp_server/semantic_moves/mix_moves.py +41 -41
- package/mcp_server/semantic_moves/performance_moves.py +13 -13
- package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
- package/mcp_server/semantic_moves/tools.py +18 -17
- package/mcp_server/semantic_moves/transition_moves.py +16 -16
- package/mcp_server/services/__init__.py +1 -0
- package/mcp_server/services/motif_service.py +67 -0
- package/mcp_server/session_continuity/tracker.py +29 -1
- package/mcp_server/song_brain/builder.py +28 -1
- package/mcp_server/song_brain/models.py +4 -0
- package/mcp_server/song_brain/tools.py +20 -2
- package/mcp_server/wonder_mode/tools.py +6 -1
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +1 -1
- package/scripts/sync_metadata.py +132 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"name": "livepilot",
|
|
12
12
|
"description": "Agentic production system for Ableton Live 12 — 293 tools, 39 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
13
|
-
"version": "1.9.
|
|
13
|
+
"version": "1.9.24",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Pilot Studio"
|
|
16
16
|
},
|
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# LivePilot v1.9.
|
|
1
|
+
# LivePilot v1.9.24 — Ableton Live 12
|
|
2
2
|
|
|
3
3
|
## Project
|
|
4
4
|
- **Repo:** This directory (LivePilot)
|
|
@@ -43,4 +43,4 @@ When modifying .amxd attributes that Max editor won't persist (e.g., `openinpres
|
|
|
43
43
|
4. Structure: 24-byte `ampf` header + `ptch` chunk + `mx@c` header + JSON patcher + frozen deps
|
|
44
44
|
|
|
45
45
|
## Tool Count
|
|
46
|
-
Currently
|
|
46
|
+
Currently 293 tools. If adding/removing tools, update: README.md, package.json description, livepilot/.Codex-plugin/plugin.json, livepilot/.claude-plugin/plugin.json, server.json, livepilot/skills/livepilot-core/SKILL.md, livepilot/skills/livepilot-core/references/overview.md, AGENTS.md, CLAUDE.md, CHANGELOG.md, tests/test_tools_contract.py, docs/manual/index.md, docs/manual/tool-reference.md
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.24 — Stability & Intelligence Upgrade (April 2026)
|
|
4
|
+
|
|
5
|
+
### Truth and Boundaries (Wave 1)
|
|
6
|
+
- **feat(runtime):** Capability contract — every advanced tool reports `full/fallback/analytical_only/unavailable` with confidence scores
|
|
7
|
+
- **feat(runtime):** Command boundary audit — CI catches any `send_command()` targeting a non-existent Remote Script command
|
|
8
|
+
- **fix(song_brain):** `get_motif_graph` now uses pure-Python engine instead of invalid TCP call
|
|
9
|
+
- **fix(hook_hunter):** Same motif routing fix
|
|
10
|
+
- **fix(musical_intelligence):** Same motif routing fix + `analyze_phrase_arc` now calls perception engine directly
|
|
11
|
+
- **fix(memory):** `record_positive_preference` actually updates taste dimensions (was a silent no-op due to key mismatch)
|
|
12
|
+
- **fix(metadata):** AGENTS.md synced to v1.9.23/293 tools, test docstring corrected
|
|
13
|
+
|
|
14
|
+
### Unified Execution Layer (Wave 2)
|
|
15
|
+
- **feat(runtime):** Execution router — classifies steps as `remote_command/bridge_command/mcp_tool/unknown`, dispatches correctly
|
|
16
|
+
- **feat(semantic_moves):** `apply_semantic_move` explore mode uses execution router
|
|
17
|
+
- **feat(preview_studio):** `render_preview_variant` uses execution router
|
|
18
|
+
|
|
19
|
+
### Persistent Memory (Waves 2-3)
|
|
20
|
+
- **feat(persistence):** Base persistent JSON store (atomic write, corruption recovery, thread-safe)
|
|
21
|
+
- **feat(persistence):** Taste store at `~/.livepilot/taste.json` — move outcomes, novelty band, device affinity, anti-preferences survive restart
|
|
22
|
+
- **feat(persistence):** Project store at `~/.livepilot/projects/<hash>/state.json` — threads, turns, Wonder outcomes per song
|
|
23
|
+
- **feat(memory):** TasteGraph.record_move_outcome writes to persistent backing
|
|
24
|
+
- **feat(session_continuity):** tracker flushes threads and turns to project store on write
|
|
25
|
+
|
|
26
|
+
### Move Annotations (Wave 3)
|
|
27
|
+
- **feat(semantic_moves):** All 20 moves annotated with explicit `backend` per compile_plan step
|
|
28
|
+
- **test:** Static audit verifies all annotations match the execution router classifier
|
|
29
|
+
|
|
30
|
+
### Intelligence Upgrade (Waves 3-4)
|
|
31
|
+
- **feat(services):** Shared motif service — one entry point consumed by SongBrain, HookHunter, musical_intelligence
|
|
32
|
+
- **feat(song_brain):** Evidence-weighted identity confidence (motif=0.4, composition=0.2, roles=0.15, scenes=0.15, moves=0.1)
|
|
33
|
+
- **feat(song_brain):** `evidence_breakdown` field shows per-source contributions
|
|
34
|
+
- **feat(hook_hunter):** Hooks carry `evidence_sources` (motif_recurrence, track_name, clip_reuse)
|
|
35
|
+
- **feat(hook_hunter):** Section-placement analysis boosts hooks recurring across sections
|
|
36
|
+
- **feat(detectors):** Motif appearing in >60% of sections triggers fatigue signal
|
|
37
|
+
|
|
38
|
+
### Preview and Doctor (Wave 4)
|
|
39
|
+
- **feat(preview_studio):** Three explicit preview modes: `audible_preview` (M4L+spectrum), `metadata_only_preview`, `analytical_preview`
|
|
40
|
+
- **feat(preview_studio):** `bars` parameter used for audible preview playback duration
|
|
41
|
+
- **feat(preview_studio):** `preview_mode` field in response — no ambiguity about what was measured
|
|
42
|
+
- **feat(runtime):** Capability probe — 6-area runtime detection (Ableton, Remote Script, M4L, numpy, persistence, tier)
|
|
43
|
+
|
|
44
|
+
### Release Infrastructure (Wave 5)
|
|
45
|
+
- **feat(scripts):** `sync_metadata.py` — single source of truth for version and tool count, CI-checkable
|
|
46
|
+
- **docs:** README Intelligence Layer section with all 12 engines described
|
|
47
|
+
- **docs:** Manual index rewritten with three-layer architecture and 39-domain map
|
|
48
|
+
|
|
3
49
|
## 1.9.23-wonder-v1.5 — Wonder Mode V1.5: Stuck-Rescue Workflow (April 2026)
|
|
4
50
|
|
|
5
51
|
### Wonder Mode Redesign (292->293 tools)
|
package/README.md
CHANGED
|
@@ -77,6 +77,77 @@ All three feed into 293 deterministic tools that execute on Ableton's main threa
|
|
|
77
77
|
|
|
78
78
|
---
|
|
79
79
|
|
|
80
|
+
## The Intelligence Layer
|
|
81
|
+
|
|
82
|
+
Most MCP servers are tool collections — they execute commands. LivePilot is an **agentic production system** — it understands what a song is becoming, diagnoses when a session is stuck, generates real creative options, learns from your decisions, and tracks its own impact.
|
|
83
|
+
|
|
84
|
+
This is the V2 intelligence layer: 12 engines that sit on top of the 293 tools and give the AI musical judgment, not just musical execution.
|
|
85
|
+
|
|
86
|
+
### SongBrain — What the Song Is
|
|
87
|
+
|
|
88
|
+
SongBrain builds a real-time model of the current session: what the defining idea is (identity core), what elements must not be casually damaged (sacred elements), what each section is trying to do emotionally (section purposes), and where the energy arc is heading. It answers the question every producer asks: *"What is this track?"*
|
|
89
|
+
|
|
90
|
+
It detects when the song's identity is drifting — when recent edits are pulling the track away from what made it work. When identity confidence is high, the system makes bolder suggestions. When it's fragile, it protects what's there.
|
|
91
|
+
|
|
92
|
+
### Taste Graph — What You Like
|
|
93
|
+
|
|
94
|
+
The Taste Graph learns your production preferences across sessions. Not just "prefers reverb" — it tracks which move families you keep vs. undo (mix moves? arrangement moves?), which devices you gravitate toward, how experimental you want suggestions to be (your novelty band), and which dimensions you actively avoid.
|
|
95
|
+
|
|
96
|
+
Every time you accept or reject a suggestion, the graph updates. Over time, it personalizes which creative options are offered and how they're ranked. Two producers using the same tools get different recommendations.
|
|
97
|
+
|
|
98
|
+
### Semantic Moves — Musical Actions, Not Parameters
|
|
99
|
+
|
|
100
|
+
A semantic move is a high-level musical intent — "add contrast," "tighten the low end," "build tension toward the chorus" — that compiles into a specific sequence of tool calls. The system has 20 moves across 4 families (mix, arrangement, transition, sound design), each with an executable plan.
|
|
101
|
+
|
|
102
|
+
Moves carry risk levels, target dimensions, and protection thresholds. "Add a filter sweep build" targets energy and tension while protecting clarity. The AI doesn't just know what to do — it knows what it's risking.
|
|
103
|
+
|
|
104
|
+
### Wonder Mode — Stuck-Rescue Workflow
|
|
105
|
+
|
|
106
|
+
When a session is stuck — too many undos, polishing the same loop, no structural progress — Wonder Mode activates. It's not "surprise me." It's a structured diagnosis-and-rescue workflow:
|
|
107
|
+
|
|
108
|
+
1. **Diagnose** — Why is the session stuck? Repeated undos? Overpolished loop? Missing contrast? Identity unclear? The stuckness detector analyzes the action history and classifies the problem.
|
|
109
|
+
|
|
110
|
+
2. **Generate** — Based on the diagnosis, Wonder searches for semantic moves that address the specific problem. It enforces real distinctness — each variant must differ by move family or execution approach. If only one real option exists, it says so honestly instead of relabeling the same idea three times.
|
|
111
|
+
|
|
112
|
+
3. **Preview** — Each executable variant can be applied, captured, and undone using Ableton's undo system. You hear what each option would actually sound like before committing.
|
|
113
|
+
|
|
114
|
+
4. **Commit or Reject** — Choose one, and the system records it into taste and session continuity. Reject all, and the creative thread stays open for another attempt. No fake outcomes are recorded.
|
|
115
|
+
|
|
116
|
+
### Creative Engines
|
|
117
|
+
|
|
118
|
+
Six specialized engines handle different aspects of production intelligence:
|
|
119
|
+
|
|
120
|
+
| Engine | What it does |
|
|
121
|
+
|--------|-------------|
|
|
122
|
+
| **Mix Engine** | Critic-driven mix analysis. Identifies masking, headroom issues, stereo problems. Plans corrective moves with before/after evaluation. |
|
|
123
|
+
| **Sound Design Engine** | Analyzes patches for static timbre, missing modulation, weak transients. Suggests parameter moves and evaluates the result. |
|
|
124
|
+
| **Transition Engine** | Classifies transition types (drop, build, breakdown). Scores transition quality and plans improvements using archetypes. |
|
|
125
|
+
| **Composition Engine** | Analyzes song structure, detects motifs, infers section purposes, scores emotional arcs. Plans arrangement moves. |
|
|
126
|
+
| **Performance Engine** | Safety-constrained suggestions for live performance. Knows which moves are safe during playback and which risk audio dropouts. |
|
|
127
|
+
| **Reference Engine** | Distills principles from reference tracks. Maps those principles to your current session as concrete, actionable moves. |
|
|
128
|
+
|
|
129
|
+
### Hook Hunter — Finding What Matters
|
|
130
|
+
|
|
131
|
+
The Hook Hunter identifies the most salient musical idea in a session — the element listeners would remember. It ranks candidates by rhythmic distinctiveness, melodic contour, and repetition. Then it tracks whether hooks are being developed, neglected, or undermined by arrangement choices.
|
|
132
|
+
|
|
133
|
+
When the hook is strong but underused, it flags it. When a transition fails to deliver the expected payoff, it diagnoses why.
|
|
134
|
+
|
|
135
|
+
### Session Continuity — The Story of Your Session
|
|
136
|
+
|
|
137
|
+
Session Continuity tracks what happened, what changed, and what's still unresolved. It maintains creative threads (open questions like "the chorus needs more lift") and records turn resolutions (what you tried, whether you kept it, how it affected identity).
|
|
138
|
+
|
|
139
|
+
When you return to a project, the session story tells the AI: *"Last time, you were working on making the bridge darker. You tried three approaches and kept the filter sweep. The chorus lift thread is still open."*
|
|
140
|
+
|
|
141
|
+
### Evaluation Loop — Verify Before Claiming Success
|
|
142
|
+
|
|
143
|
+
Every creative engine follows the same discipline: **measure before, act, measure after, compare**. The evaluation system captures session state snapshots, runs the change, captures again, and scores the difference. If the change made things worse — more masking, lost headroom, identity drift — the system flags it before you move on.
|
|
144
|
+
|
|
145
|
+
This closes the gap between "the AI did something" and "the AI did something that actually helped."
|
|
146
|
+
|
|
147
|
+
<br>
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
80
151
|
## Tools
|
|
81
152
|
|
|
82
153
|
293 tools across 39 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
|
|
@@ -322,6 +393,29 @@ read_audio_metadata Format, duration, sample rate, tags
|
|
|
322
393
|
|
|
323
394
|
<br>
|
|
324
395
|
|
|
396
|
+
### Agentic Intelligence — 83 tools
|
|
397
|
+
|
|
398
|
+
The V2 intelligence layer. These tools don't just execute commands — they analyze, diagnose, plan, evaluate, and learn.
|
|
399
|
+
|
|
400
|
+
| Domain | # | What it does |
|
|
401
|
+
|--------|:-:|-------------|
|
|
402
|
+
| Agent OS | 8 | session kernel, action ledger, capability state, routing, turn budget |
|
|
403
|
+
| Composition | 9 | section analysis, motif detection, emotional arc, form planning, section transforms |
|
|
404
|
+
| Evaluation | 1 | before/after evaluation with structured scoring |
|
|
405
|
+
| Mix Engine | 6 | critic-driven mix analysis, issue detection, move planning, masking reports |
|
|
406
|
+
| Sound Design | 5 | patch analysis, modulation planning, timbre scoring |
|
|
407
|
+
| Transition Engine | 5 | transition classification, scoring, archetype-based planning |
|
|
408
|
+
| Reference Engine | 5 | reference profiling, principle distillation, gap analysis, move mapping |
|
|
409
|
+
| Translation Engine | 3 | cross-domain translation, issue detection |
|
|
410
|
+
| Performance Engine | 5 | safety-constrained suggestions, safe move lists, scene handoff planning |
|
|
411
|
+
| Song Brain | 4 | identity inference, sacred element detection, drift monitoring, section purposes |
|
|
412
|
+
| Hook Hunter | 9 | hook detection, salience scoring, development strategies, neglect detection, phrase impact |
|
|
413
|
+
| Stuckness Detector | 3 | momentum analysis, rescue classification, structured rescue workflows |
|
|
414
|
+
| Wonder Mode | 3 | diagnosis-driven variant generation, taste-aware ranking, session discard |
|
|
415
|
+
| Session Continuity | 7 | creative threads, turn resolution, taste vs identity ranking, session story |
|
|
416
|
+
| Creative Constraints | 5 | constraint activation, reference-inspired variants, constrained generation |
|
|
417
|
+
| Preview Studio | 5 | variant creation, preview rendering, comparison, commit, discard |
|
|
418
|
+
|
|
325
419
|
> **[View all 293 tools →](docs/manual/tool-catalog.md)**
|
|
326
420
|
|
|
327
421
|
<br>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.24",
|
|
4
4
|
"description": "Agentic production system for Ableton Live 12 — 293 tools, 39 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pilot Studio"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.24",
|
|
4
4
|
"description": "Agentic production system for Ableton Live 12 — 293 tools, 39 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pilot Studio"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# LivePilot v1.9.
|
|
1
|
+
# LivePilot v1.9.24 — Architecture & Tool Reference
|
|
2
2
|
|
|
3
3
|
Agentic production system for Ableton Live 12. 293 tools across 39 domains. Device atlas (280+ devices), spectral perception (M4L analyzer), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O.
|
|
4
4
|
|
|
@@ -105,6 +105,20 @@ Current: **39 domains**: transport, tracks, clips, notes, devices, scenes, mixin
|
|
|
105
105
|
- [ ] Remote script version matches MCP server version
|
|
106
106
|
- [ ] All tests pass: `python3 -m pytest tests/ -v`
|
|
107
107
|
|
|
108
|
+
## 11. Automated Checks
|
|
109
|
+
|
|
110
|
+
- [ ] `python scripts/sync_metadata.py --check` — all metadata in sync
|
|
111
|
+
- [ ] `python -m pytest tests/test_command_boundary_audit.py` — no invalid TCP targets
|
|
112
|
+
- [ ] `python -m pytest tests/test_move_annotations.py` — all moves annotated
|
|
113
|
+
- [ ] `python -m pytest tests/test_capability.py` — capability contract works
|
|
114
|
+
- [ ] `python -m pytest tests/test_capability_probe.py` — doctor probe works
|
|
115
|
+
|
|
116
|
+
## 12. Release Smoke Board
|
|
117
|
+
|
|
118
|
+
- [ ] Run through `docs/manual/release-smoke-board.md` scenarios against real Ableton session
|
|
119
|
+
- [ ] All preview modes correctly labeled (audible/metadata/analytical)
|
|
120
|
+
- [ ] Persistence survives server restart
|
|
121
|
+
|
|
108
122
|
## Quick Verify Command
|
|
109
123
|
|
|
110
124
|
```bash
|
package/livepilot.mcpb
CHANGED
|
Binary file
|
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "livepilot",
|
|
4
4
|
"display_name": "LivePilot — AI for Ableton Live",
|
|
5
|
-
"version": "1.9.
|
|
5
|
+
"version": "1.9.24",
|
|
6
6
|
"description": "Agentic production system for Ableton Live 12. Make beats, mix tracks, design sounds, and arrange songs with 293 AI-powered tools.",
|
|
7
7
|
"long_description": "LivePilot is an AI production assistant that connects directly to Ableton Live 12. It can create drum patterns, program basslines, write chord progressions, design sounds, mix your tracks, analyze your audio, and arrange full songs — all through natural language.\n\n**What it does:**\n- Creates MIDI clips with notes, chords, and rhythms\n- Loads instruments and effects from Ableton's browser\n- Shapes sounds by adjusting device parameters\n- Mixes with volume, panning, sends, and automation\n- Analyzes your mix with real-time spectral data\n- Remembers your production style across sessions\n\n**How it works:**\nLivePilot installs a Remote Script in Ableton that communicates with the AI over a local TCP connection. Everything runs on your machine — no audio leaves your computer.",
|
|
8
8
|
"author": {
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.9.
|
|
2
|
+
__version__ = "1.9.24"
|
|
@@ -82,9 +82,32 @@ def find_hook_candidates(
|
|
|
82
82
|
development_potential=0.5,
|
|
83
83
|
))
|
|
84
84
|
|
|
85
|
+
# 4. Section-placement analysis: boost hooks that appear in payoff sections
|
|
86
|
+
payoff_sections = {
|
|
87
|
+
s.get("label", "").lower()
|
|
88
|
+
for s in (composition.get("sections", []) if composition else [])
|
|
89
|
+
if s.get("is_payoff")
|
|
90
|
+
} or {"chorus", "drop", "hook"}
|
|
91
|
+
|
|
92
|
+
for c in candidates:
|
|
93
|
+
# Check if hook is present in payoff sections (via motif locations)
|
|
94
|
+
if c.hook_type == "melodic" and motif_data:
|
|
95
|
+
for motif in motif_data.get("motifs", []):
|
|
96
|
+
if motif.get("name", "") in c.hook_id:
|
|
97
|
+
# Motif with high recurrence across sections = stronger hook
|
|
98
|
+
c.memorability = min(1.0, c.memorability + motif.get("recurrence", 0) * 0.2)
|
|
99
|
+
|
|
85
100
|
# Score all candidates
|
|
86
101
|
for c in candidates:
|
|
87
102
|
c.salience = _compute_salience(c)
|
|
103
|
+
# Add evidence sources
|
|
104
|
+
c.evidence_sources = []
|
|
105
|
+
if "motif_" in c.hook_id:
|
|
106
|
+
c.evidence_sources.append("motif_recurrence")
|
|
107
|
+
if "track_" in c.hook_id:
|
|
108
|
+
c.evidence_sources.append("track_name")
|
|
109
|
+
if "groove" in c.hook_id:
|
|
110
|
+
c.evidence_sources.append("clip_reuse")
|
|
88
111
|
|
|
89
112
|
# Sort by salience
|
|
90
113
|
candidates.sort(key=lambda c: c.salience, reverse=True)
|
|
@@ -19,6 +19,7 @@ class HookCandidate:
|
|
|
19
19
|
contrast_potential: float = 0.0 # 0-1 how well it stands out
|
|
20
20
|
development_potential: float = 0.0 # 0-1 how much room to develop
|
|
21
21
|
salience: float = 0.0 # composite score
|
|
22
|
+
evidence_sources: list[str] = field(default_factory=list) # what data informed this
|
|
22
23
|
|
|
23
24
|
def to_dict(self) -> dict:
|
|
24
25
|
return asdict(self)
|
|
@@ -53,9 +53,11 @@ def _fetch_tracks_and_scenes(ctx: Context) -> tuple[list[dict], list[dict], dict
|
|
|
53
53
|
except Exception:
|
|
54
54
|
pass
|
|
55
55
|
|
|
56
|
-
# Fetch motif data
|
|
56
|
+
# Fetch motif data — via shared motif service
|
|
57
57
|
try:
|
|
58
|
-
|
|
58
|
+
from ..services.motif_service import get_motif_data, fetch_notes_from_ableton
|
|
59
|
+
notes_by_track = fetch_notes_from_ableton(ableton, tracks)
|
|
60
|
+
motif_data = get_motif_data(notes_by_track)
|
|
59
61
|
except Exception:
|
|
60
62
|
pass # Motif graph requires notes in clips; empty dict is valid fallback
|
|
61
63
|
|
|
@@ -99,6 +99,9 @@ class TasteGraph:
|
|
|
99
99
|
|
|
100
100
|
# ── Update methods ───────────────────────────────────────────────
|
|
101
101
|
|
|
102
|
+
# Persistent store reference (set by build_taste_graph when available)
|
|
103
|
+
_persistent_store: object = None
|
|
104
|
+
|
|
102
105
|
def record_move_outcome(
|
|
103
106
|
self, move_id: str, family: str, kept: bool, score: float = 0.0
|
|
104
107
|
) -> None:
|
|
@@ -120,6 +123,13 @@ class TasteGraph:
|
|
|
120
123
|
self.evidence_count += 1
|
|
121
124
|
self.last_updated_ms = now
|
|
122
125
|
|
|
126
|
+
# Write-back to persistent store
|
|
127
|
+
if self._persistent_store is not None:
|
|
128
|
+
try:
|
|
129
|
+
self._persistent_store.record_move_outcome(move_id, family, kept, score)
|
|
130
|
+
except Exception:
|
|
131
|
+
pass # persistence is best-effort
|
|
132
|
+
|
|
123
133
|
def record_device_use(self, device_name: str, positive: bool = True) -> None:
|
|
124
134
|
"""Update device affinity from usage."""
|
|
125
135
|
now = int(time.time() * 1000)
|
|
@@ -245,10 +255,17 @@ class TasteGraph:
|
|
|
245
255
|
def build_taste_graph(
|
|
246
256
|
taste_store=None, # TasteMemoryStore
|
|
247
257
|
anti_store=None, # AntiMemoryStore
|
|
258
|
+
persistent_store=None, # PersistentTasteStore (optional)
|
|
248
259
|
) -> TasteGraph:
|
|
249
|
-
"""Build a TasteGraph from existing memory stores.
|
|
260
|
+
"""Build a TasteGraph from existing memory stores.
|
|
261
|
+
|
|
262
|
+
When persistent_store is provided, hydrates move_family_scores,
|
|
263
|
+
device_affinities, and novelty_band from disk — these survive
|
|
264
|
+
server restart.
|
|
265
|
+
"""
|
|
250
266
|
graph = TasteGraph()
|
|
251
267
|
|
|
268
|
+
# Session-scoped dimensions (in-memory)
|
|
252
269
|
if taste_store:
|
|
253
270
|
for dim in taste_store.get_taste_dimensions():
|
|
254
271
|
if dim.evidence_count > 0:
|
|
@@ -258,4 +275,54 @@ def build_taste_graph(
|
|
|
258
275
|
for pref in anti_store.get_anti_preferences():
|
|
259
276
|
graph.dimension_avoidances[pref.dimension] = pref.direction
|
|
260
277
|
|
|
278
|
+
# Persistent state (from disk)
|
|
279
|
+
if persistent_store is not None:
|
|
280
|
+
persisted = persistent_store.get_all()
|
|
281
|
+
|
|
282
|
+
# Move family scores
|
|
283
|
+
for move_id, outcome in persisted.get("move_outcomes", {}).items():
|
|
284
|
+
family = outcome.get("family", "")
|
|
285
|
+
if family and family not in graph.move_family_scores:
|
|
286
|
+
from .taste_graph import MoveFamilyScore
|
|
287
|
+
graph.move_family_scores[family] = MoveFamilyScore(family=family)
|
|
288
|
+
if family:
|
|
289
|
+
fam = graph.move_family_scores[family]
|
|
290
|
+
fam.kept_count += outcome.get("kept_count", 0)
|
|
291
|
+
fam.undone_count += outcome.get("undone_count", 0)
|
|
292
|
+
total = fam.kept_count + fam.undone_count
|
|
293
|
+
if total > 0:
|
|
294
|
+
fam.score = round((fam.kept_count - fam.undone_count) / total, 3)
|
|
295
|
+
|
|
296
|
+
# Novelty band
|
|
297
|
+
graph.novelty_band = persisted.get("novelty_band", 0.5)
|
|
298
|
+
|
|
299
|
+
# Device affinities
|
|
300
|
+
for dev_name, dev_data in persisted.get("device_affinities", {}).items():
|
|
301
|
+
from .taste_graph import DeviceAffinity
|
|
302
|
+
graph.device_affinities[dev_name] = DeviceAffinity(
|
|
303
|
+
device_name=dev_name,
|
|
304
|
+
affinity=dev_data.get("affinity", 0.0),
|
|
305
|
+
use_count=dev_data.get("use_count", 0),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Evidence count
|
|
309
|
+
graph.evidence_count = max(
|
|
310
|
+
graph.evidence_count, persisted.get("evidence_count", 0)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Dimension weights from persistent store (merged, session takes precedence)
|
|
314
|
+
for dim, val in persisted.get("dimension_weights", {}).items():
|
|
315
|
+
if dim not in graph.dimension_weights:
|
|
316
|
+
graph.dimension_weights[dim] = val
|
|
317
|
+
|
|
318
|
+
# Anti-preferences from persistent store
|
|
319
|
+
for anti in persisted.get("anti_preferences", []):
|
|
320
|
+
dim = anti.get("dimension", "")
|
|
321
|
+
direction = anti.get("direction", "")
|
|
322
|
+
if dim and dim not in graph.dimension_avoidances:
|
|
323
|
+
graph.dimension_avoidances[dim] = direction
|
|
324
|
+
|
|
325
|
+
# Attach persistent store for write-back
|
|
326
|
+
graph._persistent_store = persistent_store
|
|
327
|
+
|
|
261
328
|
return graph
|
|
@@ -189,12 +189,23 @@ def record_positive_preference(
|
|
|
189
189
|
not just what they dislike.
|
|
190
190
|
"""
|
|
191
191
|
taste_store = _get_taste_memory(ctx)
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
# Find matching outcome signals for this dimension+direction
|
|
193
|
+
from ..memory.taste_memory import _OUTCOME_SIGNALS
|
|
194
|
+
matching_signals = []
|
|
195
|
+
dim_signals = _OUTCOME_SIGNALS.get(dimension, {})
|
|
196
|
+
for sig_name, adjustment in dim_signals.items():
|
|
197
|
+
# "increase" preference → match positive-adjustment signals (kept)
|
|
198
|
+
# "decrease" preference → match negative-adjustment signals (undone/less)
|
|
199
|
+
if direction == "increase" and adjustment > 0:
|
|
200
|
+
matching_signals.append(sig_name)
|
|
201
|
+
elif direction == "decrease" and adjustment < 0:
|
|
202
|
+
matching_signals.append(sig_name)
|
|
203
|
+
if matching_signals:
|
|
204
|
+
taste_store.update_from_outcome({"signals": matching_signals})
|
|
195
205
|
return {
|
|
196
|
-
"recorded":
|
|
206
|
+
"recorded": bool(matching_signals),
|
|
197
207
|
"dimension": dimension,
|
|
198
208
|
"direction": direction,
|
|
209
|
+
"signals_matched": matching_signals,
|
|
199
210
|
"evidence": evidence,
|
|
200
211
|
}
|
|
@@ -96,9 +96,22 @@ def detect_repetition_fatigue(
|
|
|
96
96
|
# 3. Motif fatigue from motif_graph
|
|
97
97
|
if motif_graph:
|
|
98
98
|
motifs = motif_graph.get("motifs", [])
|
|
99
|
+
num_sections = max(1, len(scenes))
|
|
99
100
|
for motif in motifs:
|
|
100
101
|
fatigue_risk = motif.get("fatigue_risk", 0)
|
|
101
|
-
|
|
102
|
+
recurrence = motif.get("recurrence", 0)
|
|
103
|
+
|
|
104
|
+
# Motif appearing in >60% of sections = fatigue signal
|
|
105
|
+
if recurrence > 0.6 and num_sections >= 3:
|
|
106
|
+
adjusted_fatigue = max(fatigue_risk, recurrence * 0.8)
|
|
107
|
+
report.issues.append({
|
|
108
|
+
"type": "motif_overuse",
|
|
109
|
+
"severity": round(adjusted_fatigue, 3),
|
|
110
|
+
"detail": f"Motif {motif.get('name', motif.get('motif_id', '?'))} appears in {recurrence:.0%} of sections",
|
|
111
|
+
"motif_id": motif.get("motif_id", motif.get("name", "")),
|
|
112
|
+
"evidence": "motif_recurrence",
|
|
113
|
+
})
|
|
114
|
+
elif fatigue_risk > 0.6:
|
|
102
115
|
report.issues.append({
|
|
103
116
|
"type": "motif_overuse",
|
|
104
117
|
"severity": fatigue_risk,
|
|
@@ -45,10 +45,14 @@ def detect_repetition_fatigue(ctx: Context) -> dict:
|
|
|
45
45
|
"clips": row,
|
|
46
46
|
})
|
|
47
47
|
|
|
48
|
-
#
|
|
48
|
+
# Motif data — via shared motif service
|
|
49
49
|
motif_graph = None
|
|
50
50
|
try:
|
|
51
|
-
|
|
51
|
+
from ..services.motif_service import get_motif_data, fetch_notes_from_ableton
|
|
52
|
+
session_info = ableton.send_command("get_session_info", {})
|
|
53
|
+
track_list = session_info.get("tracks", [])
|
|
54
|
+
notes_by_track = fetch_notes_from_ableton(ableton, track_list)
|
|
55
|
+
motif_graph = get_motif_data(notes_by_track)
|
|
52
56
|
except Exception:
|
|
53
57
|
pass
|
|
54
58
|
|
|
@@ -171,17 +175,16 @@ def analyze_phrase_arc(
|
|
|
171
175
|
loudness_data = None
|
|
172
176
|
spectrum_data = None
|
|
173
177
|
|
|
178
|
+
# Direct Python calls to perception engine — not TCP
|
|
174
179
|
try:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
})
|
|
180
|
+
from ..tools._perception_engine import compute_loudness
|
|
181
|
+
loudness_data = compute_loudness(file_path, detail="full")
|
|
178
182
|
except Exception:
|
|
179
183
|
pass
|
|
180
184
|
|
|
181
185
|
try:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
})
|
|
186
|
+
from ..tools._perception_engine import compute_spectral
|
|
187
|
+
spectrum_data = compute_spectral(file_path)
|
|
185
188
|
except Exception:
|
|
186
189
|
pass
|
|
187
190
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Persistent storage for LivePilot state that survives server restart."""
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Persistent JSON store with atomic writes and corruption recovery.
|
|
2
|
+
|
|
3
|
+
Follows the TechniqueStore pattern: lazy init, atomic tmp+rename,
|
|
4
|
+
fsync to disk, corruption recovery via .corrupt rename.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import threading
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PersistentJsonStore:
|
|
16
|
+
"""Thread-safe, crash-safe JSON file store."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, path: Path):
|
|
19
|
+
self._path = Path(path)
|
|
20
|
+
self._lock = threading.RLock()
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def path(self) -> Path:
|
|
24
|
+
return self._path
|
|
25
|
+
|
|
26
|
+
def read(self) -> dict:
|
|
27
|
+
"""Read the store. Returns {} if missing or corrupt."""
|
|
28
|
+
with self._lock:
|
|
29
|
+
if not self._path.exists():
|
|
30
|
+
return {}
|
|
31
|
+
try:
|
|
32
|
+
return json.loads(self._path.read_text(encoding="utf-8"))
|
|
33
|
+
except (json.JSONDecodeError, OSError):
|
|
34
|
+
corrupt = self._path.with_suffix(self._path.suffix + ".corrupt")
|
|
35
|
+
try:
|
|
36
|
+
self._path.rename(corrupt)
|
|
37
|
+
except OSError:
|
|
38
|
+
pass
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
def write(self, data: dict) -> None:
|
|
42
|
+
"""Atomically write data to disk."""
|
|
43
|
+
with self._lock:
|
|
44
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
tmp = self._path.with_suffix(".tmp")
|
|
46
|
+
try:
|
|
47
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
48
|
+
json.dump(data, f, indent=2, default=str)
|
|
49
|
+
f.flush()
|
|
50
|
+
os.fsync(f.fileno())
|
|
51
|
+
os.replace(str(tmp), str(self._path))
|
|
52
|
+
except OSError:
|
|
53
|
+
try:
|
|
54
|
+
tmp.unlink(missing_ok=True)
|
|
55
|
+
except OSError:
|
|
56
|
+
pass
|
|
57
|
+
raise
|
|
58
|
+
|
|
59
|
+
def update(self, updater) -> dict:
|
|
60
|
+
"""Read-modify-write atomically. updater(data) -> modified data."""
|
|
61
|
+
with self._lock:
|
|
62
|
+
data = self._read_unlocked()
|
|
63
|
+
data = updater(data)
|
|
64
|
+
self._write_unlocked(data)
|
|
65
|
+
return data
|
|
66
|
+
|
|
67
|
+
def _read_unlocked(self) -> dict:
|
|
68
|
+
if not self._path.exists():
|
|
69
|
+
return {}
|
|
70
|
+
try:
|
|
71
|
+
return json.loads(self._path.read_text(encoding="utf-8"))
|
|
72
|
+
except (json.JSONDecodeError, OSError):
|
|
73
|
+
return {}
|
|
74
|
+
|
|
75
|
+
def _write_unlocked(self, data: dict) -> None:
|
|
76
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
tmp = self._path.with_suffix(".tmp")
|
|
78
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
79
|
+
json.dump(data, f, indent=2, default=str)
|
|
80
|
+
f.flush()
|
|
81
|
+
os.fsync(f.fileno())
|
|
82
|
+
os.replace(str(tmp), str(self._path))
|