livepilot 1.10.6 → 1.10.8
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 +168 -0
- package/README.md +12 -10
- package/bin/livepilot.js +168 -30
- package/installer/install.js +117 -11
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +215 -3
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +132 -33
- package/mcp_server/atlas/tools.py +56 -15
- package/mcp_server/composer/layer_planner.py +27 -0
- package/mcp_server/composer/prompt_parser.py +15 -6
- package/mcp_server/connection.py +11 -3
- package/mcp_server/corpus/__init__.py +14 -4
- package/mcp_server/creative_constraints/tools.py +206 -33
- package/mcp_server/experiment/engine.py +7 -9
- package/mcp_server/hook_hunter/analyzer.py +62 -9
- package/mcp_server/hook_hunter/tools.py +60 -9
- package/mcp_server/m4l_bridge.py +68 -12
- package/mcp_server/musical_intelligence/detectors.py +32 -0
- package/mcp_server/performance_engine/tools.py +112 -29
- package/mcp_server/preview_studio/engine.py +89 -8
- package/mcp_server/preview_studio/tools.py +22 -6
- package/mcp_server/project_brain/automation_graph.py +71 -19
- package/mcp_server/project_brain/builder.py +2 -0
- package/mcp_server/project_brain/tools.py +55 -5
- package/mcp_server/reference_engine/profile_builder.py +129 -3
- package/mcp_server/reference_engine/tools.py +47 -6
- package/mcp_server/runtime/execution_router.py +66 -2
- package/mcp_server/runtime/mcp_dispatch.py +75 -3
- package/mcp_server/runtime/remote_commands.py +10 -2
- package/mcp_server/sample_engine/analyzer.py +131 -4
- package/mcp_server/sample_engine/critics.py +29 -8
- package/mcp_server/sample_engine/models.py +42 -4
- package/mcp_server/sample_engine/tools.py +48 -14
- package/mcp_server/semantic_moves/__init__.py +1 -0
- package/mcp_server/semantic_moves/compiler.py +9 -1
- package/mcp_server/semantic_moves/device_creation_compilers.py +47 -0
- package/mcp_server/semantic_moves/mix_compilers.py +170 -0
- package/mcp_server/semantic_moves/mix_moves.py +1 -1
- package/mcp_server/semantic_moves/models.py +5 -0
- package/mcp_server/semantic_moves/sound_design_compilers.py +22 -59
- package/mcp_server/semantic_moves/tools.py +15 -4
- package/mcp_server/semantic_moves/transition_compilers.py +12 -19
- package/mcp_server/server.py +75 -5
- package/mcp_server/services/singletons.py +68 -0
- package/mcp_server/session_continuity/models.py +4 -0
- package/mcp_server/session_continuity/tracker.py +14 -1
- package/mcp_server/song_brain/builder.py +110 -12
- package/mcp_server/song_brain/tools.py +77 -13
- package/mcp_server/sound_design/tools.py +112 -1
- package/mcp_server/splice_client/client.py +29 -8
- package/mcp_server/stuckness_detector/detector.py +90 -0
- package/mcp_server/stuckness_detector/tools.py +41 -0
- package/mcp_server/tools/_agent_os_engine/critics.py +24 -0
- package/mcp_server/tools/_composition_engine/__init__.py +2 -2
- package/mcp_server/tools/_composition_engine/harmony.py +90 -0
- package/mcp_server/tools/_composition_engine/sections.py +47 -4
- package/mcp_server/tools/_harmony_engine.py +52 -8
- package/mcp_server/tools/_research_engine.py +98 -19
- package/mcp_server/tools/_theory_engine.py +138 -9
- package/mcp_server/tools/agent_os.py +20 -3
- package/mcp_server/tools/analyzer.py +105 -6
- package/mcp_server/tools/clips.py +46 -1
- package/mcp_server/tools/composition.py +66 -23
- package/mcp_server/tools/devices.py +22 -1
- package/mcp_server/tools/harmony.py +115 -14
- package/mcp_server/tools/midi_io.py +23 -1
- package/mcp_server/tools/mixing.py +35 -1
- package/mcp_server/tools/motif.py +49 -3
- package/mcp_server/tools/research.py +24 -0
- package/mcp_server/tools/theory.py +108 -16
- package/mcp_server/tools/tracks.py +1 -1
- package/mcp_server/tools/transport.py +1 -1
- package/mcp_server/transition_engine/critics.py +18 -11
- package/mcp_server/translation_engine/tools.py +8 -4
- package/package.json +25 -3
- package/remote_script/LivePilot/__init__.py +77 -2
- package/remote_script/LivePilot/arrangement.py +12 -2
- package/remote_script/LivePilot/browser.py +16 -6
- package/remote_script/LivePilot/clips.py +69 -0
- package/remote_script/LivePilot/devices.py +10 -5
- package/remote_script/LivePilot/mixing.py +117 -0
- package/remote_script/LivePilot/notes.py +13 -2
- package/remote_script/LivePilot/router.py +13 -1
- package/remote_script/LivePilot/server.py +51 -13
- package/remote_script/LivePilot/version_detect.py +7 -4
- package/server.json +20 -0
- package/.claude-plugin/marketplace.json +0 -21
- package/.mcpbignore +0 -57
- package/AGENTS.md +0 -46
- package/CODE_OF_CONDUCT.md +0 -27
- package/CONTRIBUTING.md +0 -131
- package/SECURITY.md +0 -48
- package/livepilot/.Codex-plugin/plugin.json +0 -8
- package/livepilot/.claude-plugin/plugin.json +0 -8
- package/livepilot/agents/livepilot-producer/AGENT.md +0 -313
- package/livepilot/commands/arrange.md +0 -47
- package/livepilot/commands/beat.md +0 -77
- package/livepilot/commands/evaluate.md +0 -49
- package/livepilot/commands/memory.md +0 -22
- package/livepilot/commands/mix.md +0 -44
- package/livepilot/commands/perform.md +0 -42
- package/livepilot/commands/session.md +0 -13
- package/livepilot/commands/sounddesign.md +0 -43
- package/livepilot/skills/livepilot-arrangement/SKILL.md +0 -155
- package/livepilot/skills/livepilot-composition-engine/SKILL.md +0 -107
- package/livepilot/skills/livepilot-composition-engine/references/form-patterns.md +0 -97
- package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +0 -102
- package/livepilot/skills/livepilot-core/SKILL.md +0 -184
- package/livepilot/skills/livepilot-core/references/ableton-workflow-patterns.md +0 -831
- package/livepilot/skills/livepilot-core/references/automation-atlas.md +0 -272
- package/livepilot/skills/livepilot-core/references/device-atlas/00-index.md +0 -110
- package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +0 -687
- package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +0 -753
- package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +0 -525
- package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +0 -402
- package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +0 -963
- package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +0 -874
- package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +0 -571
- package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +0 -714
- package/livepilot/skills/livepilot-core/references/device-atlas/synths-native.md +0 -953
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +0 -34
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +0 -204
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +0 -173
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +0 -211
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +0 -188
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +0 -162
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +0 -229
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +0 -243
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +0 -352
- package/livepilot/skills/livepilot-core/references/memory-guide.md +0 -107
- package/livepilot/skills/livepilot-core/references/midi-recipes.md +0 -402
- package/livepilot/skills/livepilot-core/references/mixing-patterns.md +0 -578
- package/livepilot/skills/livepilot-core/references/overview.md +0 -290
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +0 -724
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +0 -140
- package/livepilot/skills/livepilot-core/references/sound-design.md +0 -393
- package/livepilot/skills/livepilot-devices/SKILL.md +0 -169
- package/livepilot/skills/livepilot-evaluation/SKILL.md +0 -156
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +0 -118
- package/livepilot/skills/livepilot-evaluation/references/evaluation-contracts.md +0 -121
- package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +0 -110
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +0 -123
- package/livepilot/skills/livepilot-mix-engine/references/mix-critics.md +0 -143
- package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +0 -105
- package/livepilot/skills/livepilot-mixing/SKILL.md +0 -157
- package/livepilot/skills/livepilot-notes/SKILL.md +0 -130
- package/livepilot/skills/livepilot-performance-engine/SKILL.md +0 -122
- package/livepilot/skills/livepilot-performance-engine/references/performance-safety.md +0 -98
- package/livepilot/skills/livepilot-release/SKILL.md +0 -130
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +0 -105
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +0 -87
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +0 -51
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +0 -131
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +0 -168
- package/livepilot/skills/livepilot-sound-design-engine/references/patch-model.md +0 -119
- package/livepilot/skills/livepilot-sound-design-engine/references/sound-design-critics.md +0 -118
- package/livepilot/skills/livepilot-wonder/SKILL.md +0 -79
- package/m4l_device/LivePilot_Analyzer.maxpat +0 -2705
- package/manifest.json +0 -91
- package/mcp_server/splice_client/protos/app_pb2.pyi +0 -1153
- package/scripts/generate_tool_catalog.py +0 -131
- package/scripts/sync_metadata.py +0 -132
|
@@ -245,7 +245,15 @@ def suggest_next_chord(
|
|
|
245
245
|
candidates = style_map[k]
|
|
246
246
|
break
|
|
247
247
|
if not candidates:
|
|
248
|
-
|
|
248
|
+
# BUG-B23: the old fallback hard-coded ["IV", "V"] — uppercase
|
|
249
|
+
# literals that lie about chord quality in a minor key (where
|
|
250
|
+
# both iv and v are minor triads). Pick the tonic key ("i" in
|
|
251
|
+
# minor, "I" in major) and let the style_map's own conventions
|
|
252
|
+
# populate a minor-appropriate default when available.
|
|
253
|
+
tonic_key = "i" if is_minor else "I"
|
|
254
|
+
candidates = style_map.get(tonic_key)
|
|
255
|
+
if not candidates:
|
|
256
|
+
candidates = ["iv", "v"] if is_minor else ["IV", "V"]
|
|
249
257
|
|
|
250
258
|
# Build concrete suggestions with MIDI pitches
|
|
251
259
|
suggestions = []
|
|
@@ -450,11 +458,24 @@ def harmonize_melody(
|
|
|
450
458
|
) -> dict:
|
|
451
459
|
"""Generate a multi-voice harmonization of a melody from a MIDI clip.
|
|
452
460
|
|
|
453
|
-
|
|
454
|
-
|
|
461
|
+
Hymn-style SATB convention: the original melody IS the soprano voice.
|
|
462
|
+
The algorithm finds diatonic chords containing each melody note and
|
|
463
|
+
voices them below the melody (bass + tenor + alto for 4-voice mode;
|
|
464
|
+
just bass for 2-voice mode).
|
|
455
465
|
|
|
456
466
|
voices: 2 (melody + bass) or 4 (SATB). Default 4.
|
|
457
|
-
|
|
467
|
+
|
|
468
|
+
Response keys:
|
|
469
|
+
- melody: the input melody as passed in (identical pitches to soprano)
|
|
470
|
+
- soprano: same as melody (hymn-style convention)
|
|
471
|
+
- alto / tenor: inner voices (4-voice only)
|
|
472
|
+
- bass: root-aware bass line — BUG-B26 fix prevents tonic pedal
|
|
473
|
+
- chord_sequence: the chord chosen per melody note (degree + name)
|
|
474
|
+
|
|
475
|
+
BUG-B27: `soprano` and `melody` are intentionally identical — the
|
|
476
|
+
tool's job is to add harmony UNDER an existing melody, not replace
|
|
477
|
+
it. Both fields are returned so callers can pipe whichever makes
|
|
478
|
+
their downstream code cleaner.
|
|
458
479
|
|
|
459
480
|
Processing time: 2-5s.
|
|
460
481
|
"""
|
|
@@ -477,6 +498,39 @@ def harmonize_melody(
|
|
|
477
498
|
result_voices["tenor"] = []
|
|
478
499
|
|
|
479
500
|
prev_bass_midi = None
|
|
501
|
+
prev_degree: Optional[int] = None
|
|
502
|
+
|
|
503
|
+
# BUG-B26 fix: the old chord-selection walked scale degrees in a fixed
|
|
504
|
+
# preference order [I, IV, V, vi, ii, iii, vii] and picked the FIRST
|
|
505
|
+
# one containing the melody note — in practice this meant I (tonic)
|
|
506
|
+
# won whenever the melody hit a chord tone of I, so the bass pedaled
|
|
507
|
+
# on the tonic. We now build ALL candidate diatonic chords that
|
|
508
|
+
# contain the note, then score them to prefer harmonic motion:
|
|
509
|
+
# - strong penalty for picking the same chord twice in a row
|
|
510
|
+
# (breaks the pedal pattern)
|
|
511
|
+
# - mild preference for functional moves (I→IV, IV→V, V→I etc)
|
|
512
|
+
# - fallback to the original [I, IV, V, …] order on ties
|
|
513
|
+
_DEGREE_ORDER = [0, 3, 4, 5, 1, 2, 6] # I, IV, V, vi, ii, iii, vii
|
|
514
|
+
|
|
515
|
+
def _score_chord(degree: int, prev: Optional[int]) -> float:
|
|
516
|
+
score = 10.0 - _DEGREE_ORDER.index(degree) * 0.5
|
|
517
|
+
if prev is None:
|
|
518
|
+
return score
|
|
519
|
+
if degree == prev:
|
|
520
|
+
# Avoid repeating the same chord — the big lever for B26
|
|
521
|
+
score -= 20.0
|
|
522
|
+
# Functional pair bonuses (classical/pop voice-leading)
|
|
523
|
+
functional_pairs = {
|
|
524
|
+
(0, 3), (0, 4), (0, 5), # I → IV, V, vi
|
|
525
|
+
(3, 4), (3, 0), (3, 1), # IV → V, I, ii
|
|
526
|
+
(4, 0), (4, 5), # V → I, vi
|
|
527
|
+
(5, 1), (5, 3), # vi → ii, IV
|
|
528
|
+
(1, 4), (1, 6), # ii → V, vii°
|
|
529
|
+
(6, 0), (6, 2), # vii° → I, iii
|
|
530
|
+
}
|
|
531
|
+
if (prev, degree) in functional_pairs:
|
|
532
|
+
score += 5.0
|
|
533
|
+
return score
|
|
480
534
|
|
|
481
535
|
for n in melody:
|
|
482
536
|
melody_pitch = n["pitch"]
|
|
@@ -484,16 +538,23 @@ def harmonize_melody(
|
|
|
484
538
|
dur = n["duration"]
|
|
485
539
|
mel_pc = melody_pitch % 12
|
|
486
540
|
|
|
487
|
-
#
|
|
488
|
-
|
|
489
|
-
for degree in
|
|
541
|
+
# Collect every diatonic chord that contains this melody note
|
|
542
|
+
candidates: list[tuple[int, dict]] = []
|
|
543
|
+
for degree in _DEGREE_ORDER:
|
|
490
544
|
chord = engine.build_chord(degree, tonic, mode)
|
|
491
545
|
if mel_pc in chord["pitch_classes"]:
|
|
492
|
-
|
|
493
|
-
break
|
|
546
|
+
candidates.append((degree, chord))
|
|
494
547
|
|
|
495
|
-
if
|
|
496
|
-
|
|
548
|
+
if not candidates:
|
|
549
|
+
candidates = [(0, engine.build_chord(0, tonic, mode))]
|
|
550
|
+
|
|
551
|
+
# Pick the highest-scoring candidate — break ties by preference order
|
|
552
|
+
candidates.sort(
|
|
553
|
+
key=lambda pair: (-_score_chord(pair[0], prev_degree),
|
|
554
|
+
_DEGREE_ORDER.index(pair[0]))
|
|
555
|
+
)
|
|
556
|
+
best_degree, best_chord = candidates[0]
|
|
557
|
+
prev_degree = best_degree
|
|
497
558
|
|
|
498
559
|
# Build MIDI pitches for the chord
|
|
499
560
|
chord_midis = sorted([
|
|
@@ -553,10 +614,13 @@ def harmonize_melody(
|
|
|
553
614
|
"duration": dur, "velocity": int(vel * 0.7),
|
|
554
615
|
})
|
|
555
616
|
|
|
617
|
+
# BUG-B27: expose the input melody under `melody` (alias of soprano)
|
|
618
|
+
# so downstream code doesn't have to guess which field carries it.
|
|
556
619
|
result = {
|
|
557
620
|
"key": _key_display(key_info),
|
|
558
621
|
"voices": voices,
|
|
559
622
|
"melody_notes": len(melody),
|
|
623
|
+
"melody": list(result_voices["soprano"]),
|
|
560
624
|
}
|
|
561
625
|
for voice_name, voice_notes in result_voices.items():
|
|
562
626
|
if voice_notes:
|
|
@@ -608,9 +672,15 @@ def generate_countermelody(
|
|
|
608
672
|
|
|
609
673
|
# Consonant intervals (semitones mod 12): P1, m3, M3, P4, P5, m6, M6, P8
|
|
610
674
|
consonant = {0, 3, 4, 5, 7, 8, 9}
|
|
675
|
+
# BUG-B28: "imperfect" consonances (thirds + sixths) are more
|
|
676
|
+
# colorful than perfect ones (unison, 4th, 5th, octave). Prefer them
|
|
677
|
+
# so the countermelody doesn't collapse onto a static pedal of 1/4/5/8s.
|
|
678
|
+
imperfect_consonant = {3, 4, 8, 9} # m3, M3, m6, M6
|
|
611
679
|
|
|
612
680
|
counter_notes = []
|
|
613
681
|
prev_cp = None
|
|
682
|
+
# Track recently-used pitches to reward range exploration (B28)
|
|
683
|
+
recent_pitches: list[int] = []
|
|
614
684
|
|
|
615
685
|
for i, n in enumerate(melody):
|
|
616
686
|
mel_pitch = n["pitch"]
|
|
@@ -626,29 +696,47 @@ def generate_countermelody(
|
|
|
626
696
|
|
|
627
697
|
score = 0.0
|
|
628
698
|
|
|
629
|
-
#
|
|
699
|
+
# Prefer imperfect consonances (thirds + sixths) — more
|
|
700
|
+
# colorful than unison/4th/5th/octave. Without this the
|
|
701
|
+
# counter collapses onto perfect intervals (B28).
|
|
702
|
+
if iv in imperfect_consonant:
|
|
703
|
+
score += 4.0
|
|
704
|
+
|
|
705
|
+
# Contrary motion bonus (amplified for B28)
|
|
630
706
|
if prev_cp is not None and i > 0:
|
|
631
707
|
mel_dir = mel_pitch - melody[i - 1]["pitch"]
|
|
632
708
|
cp_dir = cp - prev_cp
|
|
633
709
|
if (mel_dir > 0 and cp_dir < 0) or (mel_dir < 0 and cp_dir > 0):
|
|
634
|
-
score += 10
|
|
710
|
+
score += 15 # was 10 — make contrary motion dominant
|
|
635
711
|
# Penalize parallel perfect intervals
|
|
636
712
|
prev_iv = abs(prev_cp - melody[i - 1]["pitch"]) % 12
|
|
637
713
|
if prev_iv == iv and iv in (0, 7):
|
|
638
714
|
score -= 50
|
|
639
715
|
|
|
640
|
-
# Stepwise motion bonus
|
|
716
|
+
# Stepwise motion bonus — but don't let it pin us to one pitch
|
|
641
717
|
if prev_cp is not None:
|
|
642
718
|
step = abs(cp - prev_cp)
|
|
643
|
-
if step
|
|
719
|
+
if step == 0:
|
|
720
|
+
# Penalize exact repetition (was implicit-neutral)
|
|
721
|
+
score -= 6
|
|
722
|
+
elif step <= 2:
|
|
644
723
|
score += 5
|
|
645
724
|
elif step <= 4:
|
|
646
|
-
score +=
|
|
725
|
+
score += 3
|
|
647
726
|
elif step > 7:
|
|
648
727
|
score -= 3
|
|
649
728
|
else:
|
|
650
729
|
score += 3
|
|
651
730
|
|
|
731
|
+
# BUG-B28: range-exploration bonus. Penalize pitches used
|
|
732
|
+
# in the last 4 notes; this forces the counter to walk
|
|
733
|
+
# through the pool instead of hovering on 2-3 pitches.
|
|
734
|
+
if cp in recent_pitches:
|
|
735
|
+
score -= 4
|
|
736
|
+
# Modest bonus for pitches we haven't visited recently
|
|
737
|
+
if cp not in set(recent_pitches):
|
|
738
|
+
score += 1
|
|
739
|
+
|
|
652
740
|
score += random.uniform(0, 2)
|
|
653
741
|
scored.append((cp, score))
|
|
654
742
|
|
|
@@ -657,6 +745,10 @@ def generate_countermelody(
|
|
|
657
745
|
|
|
658
746
|
scored.sort(key=lambda x: -x[1])
|
|
659
747
|
chosen = scored[0][0]
|
|
748
|
+
# Maintain a sliding window of the last 4 counter pitches
|
|
749
|
+
recent_pitches.append(chosen)
|
|
750
|
+
if len(recent_pitches) > 4:
|
|
751
|
+
recent_pitches.pop(0)
|
|
660
752
|
|
|
661
753
|
counter_notes.append({
|
|
662
754
|
"pitch": chosen,
|
|
@@ -106,7 +106,7 @@ def duplicate_track(ctx: Context, track_index: int) -> dict:
|
|
|
106
106
|
|
|
107
107
|
@mcp.tool()
|
|
108
108
|
def set_track_name(ctx: Context, track_index: int, name: str) -> dict:
|
|
109
|
-
"""Rename a track."""
|
|
109
|
+
"""Rename a track. The new name appears in both the Session and Arrangement views and survives session save."""
|
|
110
110
|
_validate_track_index(track_index)
|
|
111
111
|
if not name.strip():
|
|
112
112
|
raise ValueError("Track name cannot be empty")
|
|
@@ -62,7 +62,7 @@ def start_playback(ctx: Context) -> dict:
|
|
|
62
62
|
|
|
63
63
|
@mcp.tool()
|
|
64
64
|
def stop_playback(ctx: Context) -> dict:
|
|
65
|
-
"""Stop playback."""
|
|
65
|
+
"""Stop playback — halts the session transport and the arrangement cursor returns to its last position."""
|
|
66
66
|
return _get_ableton(ctx).send_command("stop_playback")
|
|
67
67
|
|
|
68
68
|
|
|
@@ -270,17 +270,24 @@ def run_gesture_fit_critic(
|
|
|
270
270
|
boundary = plan.boundary
|
|
271
271
|
archetype = plan.archetype
|
|
272
272
|
|
|
273
|
-
# Check if the section pair matches any of the archetype's use cases
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
273
|
+
# Check if the section pair matches any of the archetype's use cases.
|
|
274
|
+
# BUG-B15: "any_section_change" is a wildcard that matches any transition —
|
|
275
|
+
# honor it explicitly so archetypes documented as universal don't get
|
|
276
|
+
# flagged as mismatched on specific pairs like intro→build.
|
|
277
|
+
WILDCARDS = {"any_section_change", "any"}
|
|
278
|
+
if any(uc in WILDCARDS for uc in archetype.use_cases):
|
|
279
|
+
use_case_match = True
|
|
280
|
+
else:
|
|
281
|
+
pair_tags = {
|
|
282
|
+
f"{boundary.from_type}_to_{boundary.to_type}",
|
|
283
|
+
boundary.from_type,
|
|
284
|
+
boundary.to_type,
|
|
285
|
+
}
|
|
286
|
+
use_case_match = any(
|
|
287
|
+
tag in uc or uc in tag
|
|
288
|
+
for tag in pair_tags
|
|
289
|
+
for uc in archetype.use_cases
|
|
290
|
+
)
|
|
284
291
|
|
|
285
292
|
if not use_case_match:
|
|
286
293
|
issues.append(TransitionIssue(
|
|
@@ -6,11 +6,19 @@ then delegates to pure-computation critics.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import logging
|
|
10
|
+
|
|
9
11
|
from fastmcp import Context
|
|
10
12
|
|
|
11
13
|
from ..server import mcp
|
|
12
14
|
from .critics import build_translation_report, run_all_translation_critics
|
|
13
15
|
|
|
16
|
+
# Logger defined at module top: _fetch_translation_data below calls
|
|
17
|
+
# logger.debug on an exception path. The previous version buried the logger
|
|
18
|
+
# definition inside a docstring mid-file, which meant the name was never
|
|
19
|
+
# actually bound at module level — any exception path would raise NameError.
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
14
22
|
|
|
15
23
|
# ── Helpers ─────────────────────────────────────────────────────────
|
|
16
24
|
|
|
@@ -97,10 +105,6 @@ def get_translation_issues(ctx: Context) -> dict:
|
|
|
97
105
|
|
|
98
106
|
Lighter than check_translation — returns only detected issues
|
|
99
107
|
from the 5 playback robustness critics.
|
|
100
|
-
import logging
|
|
101
|
-
|
|
102
|
-
logger = logging.getLogger(__name__)
|
|
103
|
-
|
|
104
108
|
"""
|
|
105
109
|
mix_snapshot = _fetch_translation_data(ctx)
|
|
106
110
|
issues = run_all_translation_critics(mix_snapshot)
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.8",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12 —
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 — 324 tools, 45 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "BSL-1.1",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -43,5 +43,27 @@
|
|
|
43
43
|
],
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=18.0.0"
|
|
46
|
-
}
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"bin/**/*.js",
|
|
49
|
+
"installer/**/*.js",
|
|
50
|
+
"mcp_server/**/*.py",
|
|
51
|
+
"mcp_server/**/*.json",
|
|
52
|
+
"mcp_server/**/*.yaml",
|
|
53
|
+
"mcp_server/**/*.md",
|
|
54
|
+
"mcp_server/**/*.db",
|
|
55
|
+
"remote_script/**/*.py",
|
|
56
|
+
"m4l_device/LivePilot_Analyzer.amxd",
|
|
57
|
+
"m4l_device/LivePilot_Analyzer.adv",
|
|
58
|
+
"m4l_device/livepilot_bridge.js",
|
|
59
|
+
"m4l_device/BUILD_GUIDE.md",
|
|
60
|
+
"requirements.txt",
|
|
61
|
+
"README.md",
|
|
62
|
+
"LICENSE",
|
|
63
|
+
"CHANGELOG.md",
|
|
64
|
+
"server.json",
|
|
65
|
+
"!**/__pycache__/**",
|
|
66
|
+
"!**/*.pyc",
|
|
67
|
+
"!**/.DS_Store"
|
|
68
|
+
]
|
|
47
69
|
}
|
|
@@ -5,9 +5,10 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
|
|
|
5
5
|
when this script is selected in Preferences > Link, Tempo & MIDI.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.10.
|
|
8
|
+
__version__ = "1.10.8"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
|
+
from . import router
|
|
11
12
|
from .server import LivePilotServer
|
|
12
13
|
from . import transport # noqa: F401 — registers transport handlers
|
|
13
14
|
from . import tracks # noqa: F401 — registers track handlers
|
|
@@ -23,8 +24,82 @@ from . import clip_automation # noqa: F401 — registers clip automation hand
|
|
|
23
24
|
from . import version_detect # noqa: F401 — version detection
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
# ── Reload plumbing (BUG-B-reload, Batch 20) ──────────────────────────────
|
|
28
|
+
# Ableton keeps `sys.modules["LivePilot.*"]` cached across Control Surface
|
|
29
|
+
# toggles. Without intervention, edits to handler files don't take effect
|
|
30
|
+
# until a full Ableton restart — the toggle just re-instantiates the
|
|
31
|
+
# ControlSurface class without re-importing submodules.
|
|
32
|
+
#
|
|
33
|
+
# Fix: track whether this is the first create_instance call per
|
|
34
|
+
# interpreter lifetime. On subsequent calls, force-reload the router
|
|
35
|
+
# (which clears _handlers) and every handler module (which re-fires
|
|
36
|
+
# @register decorators with the updated code). Result: a Control Surface
|
|
37
|
+
# toggle now behaves like a fresh module reload, so live-editing mixing.py
|
|
38
|
+
# / devices.py / etc. and re-toggling is enough — no Ableton restart.
|
|
39
|
+
|
|
40
|
+
_FIRST_CREATE_INSTANCE = True
|
|
41
|
+
|
|
42
|
+
_HANDLER_MODULES = (
|
|
43
|
+
transport, tracks, clips, notes, devices, scenes,
|
|
44
|
+
mixing, browser, arrangement, diagnostics,
|
|
45
|
+
clip_automation, version_detect,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _force_reload_handlers(cs=None):
|
|
50
|
+
"""Force Python to re-read the handler modules from disk.
|
|
51
|
+
|
|
52
|
+
Called on every create_instance() except the first, so edits to
|
|
53
|
+
handler files take effect via Control Surface toggle without
|
|
54
|
+
restarting Ableton. Order matters: router first (clears _handlers),
|
|
55
|
+
then each handler module (re-registers its @register decorators).
|
|
56
|
+
|
|
57
|
+
When ``cs`` is provided, reload exceptions are logged through the
|
|
58
|
+
ControlSurface so a SyntaxError / NameError in an edited handler is
|
|
59
|
+
surfaced in Live's status log instead of silently swallowed. The
|
|
60
|
+
previous ``except Exception: pass`` turned any bad handler into a
|
|
61
|
+
silent NOT_FOUND at dispatch time with no hint that reload had failed.
|
|
62
|
+
"""
|
|
63
|
+
import importlib
|
|
64
|
+
def _log(msg):
|
|
65
|
+
if cs is None:
|
|
66
|
+
return
|
|
67
|
+
try:
|
|
68
|
+
cs.log_message("[LivePilot] " + msg)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
importlib.reload(router)
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
_log("reload(router) FAILED — %s: %s. Handlers will be "
|
|
76
|
+
"stale until Ableton restart." % (type(exc).__name__, exc))
|
|
77
|
+
for mod in _HANDLER_MODULES:
|
|
78
|
+
try:
|
|
79
|
+
importlib.reload(mod)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
# Don't block Ableton startup on a single bad reload, but do
|
|
82
|
+
# tell the user what happened — the stale handler will keep
|
|
83
|
+
# serving the OLD code until a full restart.
|
|
84
|
+
_log("reload(%s) FAILED — %s: %s. Handler is stale." % (
|
|
85
|
+
getattr(mod, "__name__", "?"),
|
|
86
|
+
type(exc).__name__,
|
|
87
|
+
exc,
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
|
|
26
91
|
def create_instance(c_instance):
|
|
27
|
-
"""Factory function called by Ableton Live.
|
|
92
|
+
"""Factory function called by Ableton Live.
|
|
93
|
+
|
|
94
|
+
Called once on initial Control Surface enable, AND every time the
|
|
95
|
+
user toggles the Control Surface off and on. The reload path below
|
|
96
|
+
makes the toggle behave like a fresh import — crucial for dev
|
|
97
|
+
ergonomics when iterating on mixing.py / devices.py / etc.
|
|
98
|
+
"""
|
|
99
|
+
global _FIRST_CREATE_INSTANCE
|
|
100
|
+
if not _FIRST_CREATE_INSTANCE:
|
|
101
|
+
_force_reload_handlers(cs=c_instance)
|
|
102
|
+
_FIRST_CREATE_INSTANCE = False
|
|
28
103
|
return LivePilot(c_instance)
|
|
29
104
|
|
|
30
105
|
|
|
@@ -407,11 +407,21 @@ def modify_arrangement_notes(song, params):
|
|
|
407
407
|
for note in all_notes:
|
|
408
408
|
note_map[note.note_id] = note
|
|
409
409
|
|
|
410
|
+
# Two-pass: validate all note_ids BEFORE mutating any notes. See the
|
|
411
|
+
# identical fix in notes.py:modify_notes — partial mid-loop mutation on
|
|
412
|
+
# the C++ NoteVector was leaving the clip in a half-modified state that
|
|
413
|
+
# never got committed.
|
|
414
|
+
missing = [int(mod["note_id"]) for mod in modifications
|
|
415
|
+
if int(mod["note_id"]) not in note_map]
|
|
416
|
+
if missing:
|
|
417
|
+
raise ValueError(
|
|
418
|
+
"Note IDs not found in arrangement clip: %s. "
|
|
419
|
+
"No modifications applied." % missing
|
|
420
|
+
)
|
|
421
|
+
|
|
410
422
|
modified_count = 0
|
|
411
423
|
for mod in modifications:
|
|
412
424
|
note_id = int(mod["note_id"])
|
|
413
|
-
if note_id not in note_map:
|
|
414
|
-
raise ValueError("Note ID %d not found in clip" % note_id)
|
|
415
425
|
note = note_map[note_id]
|
|
416
426
|
if "pitch" in mod:
|
|
417
427
|
note.pitch = int(mod["pitch"])
|
|
@@ -287,11 +287,20 @@ def load_browser_item(song, params):
|
|
|
287
287
|
# and use the subcategory or the full fragment for matching
|
|
288
288
|
if "FileId_" in device_name:
|
|
289
289
|
# URI contains an internal file ID — name-based search won't work.
|
|
290
|
-
#
|
|
290
|
+
# We fall back to one URI walk, but with a TIGHT iteration budget:
|
|
291
|
+
# this runs synchronously on Ableton's audio/main thread, and the
|
|
292
|
+
# previous 200 000-node walk could stall audio and GUI for several
|
|
293
|
+
# seconds on large libraries (documented in CLAUDE.md).
|
|
294
|
+
#
|
|
295
|
+
# If the item isn't found inside the budget, we return a clean
|
|
296
|
+
# STATE_ERROR pointing the caller at search_browser(), which does
|
|
297
|
+
# the same walk lazily from a cached Python-side index without
|
|
298
|
+
# hogging the audio thread.
|
|
291
299
|
_iterations[0] = 0
|
|
292
|
-
DEEP_MAX =
|
|
300
|
+
DEEP_MAX = 20000 # was 200_000 — 10x reduction
|
|
301
|
+
DEEP_DEPTH_MAX = 8 # was 12 — shallower depth is usually enough
|
|
293
302
|
def find_by_uri_deep(parent, target_uri, depth=0):
|
|
294
|
-
if depth >
|
|
303
|
+
if depth > DEEP_DEPTH_MAX or _iterations[0] > DEEP_MAX:
|
|
295
304
|
return None
|
|
296
305
|
try:
|
|
297
306
|
children = list(parent.children)
|
|
@@ -326,9 +335,10 @@ def load_browser_item(song, params):
|
|
|
326
335
|
}
|
|
327
336
|
|
|
328
337
|
raise ValueError(
|
|
329
|
-
"Item '%s' not found
|
|
330
|
-
"search_browser to
|
|
331
|
-
"
|
|
338
|
+
"Item '%s' not found inside deep-scan budget (FileId URI). "
|
|
339
|
+
"Use search_browser(query=...) to locate it without stalling "
|
|
340
|
+
"Ableton's audio thread, then call load_browser_item with the "
|
|
341
|
+
"returned URI." % uri
|
|
332
342
|
)
|
|
333
343
|
|
|
334
344
|
for sep in (":", "/"):
|
|
@@ -36,6 +36,16 @@ def get_clip_info(song, params):
|
|
|
36
36
|
if clip.is_audio_clip:
|
|
37
37
|
result["warping"] = clip.warping
|
|
38
38
|
result["warp_mode"] = clip.warp_mode
|
|
39
|
+
# BUG-A4: expose pitch/gain so callers can reason about sample
|
|
40
|
+
# transposition vs session key. Pitch is in semitones (int, -48..+48),
|
|
41
|
+
# pitch_fine in cents (-50..+50), gain linear (normalized 0..1 in Live).
|
|
42
|
+
# Some Live builds expose these as None on freshly-recorded clips
|
|
43
|
+
# before a warp pass; wrap in try/except for safety.
|
|
44
|
+
for attr in ("pitch_coarse", "pitch_fine", "gain"):
|
|
45
|
+
try:
|
|
46
|
+
result[attr] = getattr(clip, attr)
|
|
47
|
+
except AttributeError:
|
|
48
|
+
pass
|
|
39
49
|
|
|
40
50
|
return result
|
|
41
51
|
|
|
@@ -202,6 +212,65 @@ def set_clip_launch(song, params):
|
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
|
|
215
|
+
@register("set_clip_pitch")
|
|
216
|
+
def set_clip_pitch(song, params):
|
|
217
|
+
"""Set pitch/gain on an audio clip (BUG-A5).
|
|
218
|
+
|
|
219
|
+
Audio clips only — MIDI clips raise ValueError.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
track_index, clip_index : int
|
|
224
|
+
coarse : int, optional -- semitones, -48..+48
|
|
225
|
+
fine : float, optional -- cents, -50.0..+50.0
|
|
226
|
+
gain : float, optional -- linear normalized (0..1 in Live)
|
|
227
|
+
|
|
228
|
+
At least one of (coarse, fine, gain) must be provided.
|
|
229
|
+
"""
|
|
230
|
+
track_index = int(params["track_index"])
|
|
231
|
+
clip_index = int(params["clip_index"])
|
|
232
|
+
clip = get_clip(song, track_index, clip_index)
|
|
233
|
+
|
|
234
|
+
if clip.is_midi_clip:
|
|
235
|
+
raise ValueError("set_clip_pitch only works on audio clips")
|
|
236
|
+
|
|
237
|
+
coarse = params.get("coarse")
|
|
238
|
+
fine = params.get("fine")
|
|
239
|
+
gain = params.get("gain")
|
|
240
|
+
|
|
241
|
+
if coarse is None and fine is None and gain is None:
|
|
242
|
+
raise ValueError(
|
|
243
|
+
"Provide at least one of: coarse (semitones), fine (cents), gain (0-1)"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if coarse is not None:
|
|
247
|
+
c = int(coarse)
|
|
248
|
+
if c < -48 or c > 48:
|
|
249
|
+
raise ValueError("coarse must be in -48..+48 semitones")
|
|
250
|
+
clip.pitch_coarse = c
|
|
251
|
+
if fine is not None:
|
|
252
|
+
f = float(fine)
|
|
253
|
+
if f < -50.0 or f > 50.0:
|
|
254
|
+
raise ValueError("fine must be in -50..+50 cents")
|
|
255
|
+
clip.pitch_fine = f
|
|
256
|
+
if gain is not None:
|
|
257
|
+
g = float(gain)
|
|
258
|
+
if g < 0.0 or g > 1.0:
|
|
259
|
+
raise ValueError("gain must be in 0..1")
|
|
260
|
+
clip.gain = g
|
|
261
|
+
|
|
262
|
+
result = {
|
|
263
|
+
"track_index": track_index,
|
|
264
|
+
"clip_index": clip_index,
|
|
265
|
+
}
|
|
266
|
+
for attr in ("pitch_coarse", "pitch_fine", "gain"):
|
|
267
|
+
try:
|
|
268
|
+
result[attr] = getattr(clip, attr)
|
|
269
|
+
except AttributeError:
|
|
270
|
+
pass
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
|
|
205
274
|
@register("set_clip_warp_mode")
|
|
206
275
|
def set_clip_warp_mode(song, params):
|
|
207
276
|
"""Set warp mode for an audio clip."""
|
|
@@ -199,17 +199,22 @@ def toggle_device(song, params):
|
|
|
199
199
|
track = get_track(song, track_index)
|
|
200
200
|
device = get_device(track, device_index)
|
|
201
201
|
|
|
202
|
-
# Find the "Device On" parameter by name
|
|
202
|
+
# Find the "Device On" parameter by name — the previous fallback
|
|
203
|
+
# blindly assumed parameters[0] was an on/off switch, which for many
|
|
204
|
+
# devices is actually "Filter Frequency", "Gain", or similar. The
|
|
205
|
+
# fallback silently mutated an arbitrary parameter while reporting
|
|
206
|
+
# is_active as if toggling had worked. Now refuse to guess.
|
|
203
207
|
on_param = None
|
|
204
208
|
for p in device.parameters:
|
|
205
209
|
if p.name == "Device On":
|
|
206
210
|
on_param = p
|
|
207
211
|
break
|
|
208
212
|
if on_param is None:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
raise ValueError(
|
|
214
|
+
"Device '%s' exposes no 'Device On' parameter and cannot be "
|
|
215
|
+
"toggled programmatically. Use delete_device or disable it "
|
|
216
|
+
"through the UI." % device.name
|
|
217
|
+
)
|
|
213
218
|
|
|
214
219
|
on_param.value = 1.0 if active else 0.0
|
|
215
220
|
return {"name": device.name, "is_active": on_param.value > 0.5}
|