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.
Files changed (163) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/README.md +12 -10
  3. package/bin/livepilot.js +168 -30
  4. package/installer/install.js +117 -11
  5. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  6. package/m4l_device/livepilot_bridge.js +215 -3
  7. package/mcp_server/__init__.py +1 -1
  8. package/mcp_server/atlas/__init__.py +132 -33
  9. package/mcp_server/atlas/tools.py +56 -15
  10. package/mcp_server/composer/layer_planner.py +27 -0
  11. package/mcp_server/composer/prompt_parser.py +15 -6
  12. package/mcp_server/connection.py +11 -3
  13. package/mcp_server/corpus/__init__.py +14 -4
  14. package/mcp_server/creative_constraints/tools.py +206 -33
  15. package/mcp_server/experiment/engine.py +7 -9
  16. package/mcp_server/hook_hunter/analyzer.py +62 -9
  17. package/mcp_server/hook_hunter/tools.py +60 -9
  18. package/mcp_server/m4l_bridge.py +68 -12
  19. package/mcp_server/musical_intelligence/detectors.py +32 -0
  20. package/mcp_server/performance_engine/tools.py +112 -29
  21. package/mcp_server/preview_studio/engine.py +89 -8
  22. package/mcp_server/preview_studio/tools.py +22 -6
  23. package/mcp_server/project_brain/automation_graph.py +71 -19
  24. package/mcp_server/project_brain/builder.py +2 -0
  25. package/mcp_server/project_brain/tools.py +55 -5
  26. package/mcp_server/reference_engine/profile_builder.py +129 -3
  27. package/mcp_server/reference_engine/tools.py +47 -6
  28. package/mcp_server/runtime/execution_router.py +66 -2
  29. package/mcp_server/runtime/mcp_dispatch.py +75 -3
  30. package/mcp_server/runtime/remote_commands.py +10 -2
  31. package/mcp_server/sample_engine/analyzer.py +131 -4
  32. package/mcp_server/sample_engine/critics.py +29 -8
  33. package/mcp_server/sample_engine/models.py +42 -4
  34. package/mcp_server/sample_engine/tools.py +48 -14
  35. package/mcp_server/semantic_moves/__init__.py +1 -0
  36. package/mcp_server/semantic_moves/compiler.py +9 -1
  37. package/mcp_server/semantic_moves/device_creation_compilers.py +47 -0
  38. package/mcp_server/semantic_moves/mix_compilers.py +170 -0
  39. package/mcp_server/semantic_moves/mix_moves.py +1 -1
  40. package/mcp_server/semantic_moves/models.py +5 -0
  41. package/mcp_server/semantic_moves/sound_design_compilers.py +22 -59
  42. package/mcp_server/semantic_moves/tools.py +15 -4
  43. package/mcp_server/semantic_moves/transition_compilers.py +12 -19
  44. package/mcp_server/server.py +75 -5
  45. package/mcp_server/services/singletons.py +68 -0
  46. package/mcp_server/session_continuity/models.py +4 -0
  47. package/mcp_server/session_continuity/tracker.py +14 -1
  48. package/mcp_server/song_brain/builder.py +110 -12
  49. package/mcp_server/song_brain/tools.py +77 -13
  50. package/mcp_server/sound_design/tools.py +112 -1
  51. package/mcp_server/splice_client/client.py +29 -8
  52. package/mcp_server/stuckness_detector/detector.py +90 -0
  53. package/mcp_server/stuckness_detector/tools.py +41 -0
  54. package/mcp_server/tools/_agent_os_engine/critics.py +24 -0
  55. package/mcp_server/tools/_composition_engine/__init__.py +2 -2
  56. package/mcp_server/tools/_composition_engine/harmony.py +90 -0
  57. package/mcp_server/tools/_composition_engine/sections.py +47 -4
  58. package/mcp_server/tools/_harmony_engine.py +52 -8
  59. package/mcp_server/tools/_research_engine.py +98 -19
  60. package/mcp_server/tools/_theory_engine.py +138 -9
  61. package/mcp_server/tools/agent_os.py +20 -3
  62. package/mcp_server/tools/analyzer.py +105 -6
  63. package/mcp_server/tools/clips.py +46 -1
  64. package/mcp_server/tools/composition.py +66 -23
  65. package/mcp_server/tools/devices.py +22 -1
  66. package/mcp_server/tools/harmony.py +115 -14
  67. package/mcp_server/tools/midi_io.py +23 -1
  68. package/mcp_server/tools/mixing.py +35 -1
  69. package/mcp_server/tools/motif.py +49 -3
  70. package/mcp_server/tools/research.py +24 -0
  71. package/mcp_server/tools/theory.py +108 -16
  72. package/mcp_server/tools/tracks.py +1 -1
  73. package/mcp_server/tools/transport.py +1 -1
  74. package/mcp_server/transition_engine/critics.py +18 -11
  75. package/mcp_server/translation_engine/tools.py +8 -4
  76. package/package.json +25 -3
  77. package/remote_script/LivePilot/__init__.py +77 -2
  78. package/remote_script/LivePilot/arrangement.py +12 -2
  79. package/remote_script/LivePilot/browser.py +16 -6
  80. package/remote_script/LivePilot/clips.py +69 -0
  81. package/remote_script/LivePilot/devices.py +10 -5
  82. package/remote_script/LivePilot/mixing.py +117 -0
  83. package/remote_script/LivePilot/notes.py +13 -2
  84. package/remote_script/LivePilot/router.py +13 -1
  85. package/remote_script/LivePilot/server.py +51 -13
  86. package/remote_script/LivePilot/version_detect.py +7 -4
  87. package/server.json +20 -0
  88. package/.claude-plugin/marketplace.json +0 -21
  89. package/.mcpbignore +0 -57
  90. package/AGENTS.md +0 -46
  91. package/CODE_OF_CONDUCT.md +0 -27
  92. package/CONTRIBUTING.md +0 -131
  93. package/SECURITY.md +0 -48
  94. package/livepilot/.Codex-plugin/plugin.json +0 -8
  95. package/livepilot/.claude-plugin/plugin.json +0 -8
  96. package/livepilot/agents/livepilot-producer/AGENT.md +0 -313
  97. package/livepilot/commands/arrange.md +0 -47
  98. package/livepilot/commands/beat.md +0 -77
  99. package/livepilot/commands/evaluate.md +0 -49
  100. package/livepilot/commands/memory.md +0 -22
  101. package/livepilot/commands/mix.md +0 -44
  102. package/livepilot/commands/perform.md +0 -42
  103. package/livepilot/commands/session.md +0 -13
  104. package/livepilot/commands/sounddesign.md +0 -43
  105. package/livepilot/skills/livepilot-arrangement/SKILL.md +0 -155
  106. package/livepilot/skills/livepilot-composition-engine/SKILL.md +0 -107
  107. package/livepilot/skills/livepilot-composition-engine/references/form-patterns.md +0 -97
  108. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +0 -102
  109. package/livepilot/skills/livepilot-core/SKILL.md +0 -184
  110. package/livepilot/skills/livepilot-core/references/ableton-workflow-patterns.md +0 -831
  111. package/livepilot/skills/livepilot-core/references/automation-atlas.md +0 -272
  112. package/livepilot/skills/livepilot-core/references/device-atlas/00-index.md +0 -110
  113. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +0 -687
  114. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +0 -753
  115. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +0 -525
  116. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +0 -402
  117. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +0 -963
  118. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +0 -874
  119. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +0 -571
  120. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +0 -714
  121. package/livepilot/skills/livepilot-core/references/device-atlas/synths-native.md +0 -953
  122. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +0 -34
  123. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +0 -204
  124. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +0 -173
  125. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +0 -211
  126. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +0 -188
  127. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +0 -162
  128. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +0 -229
  129. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +0 -243
  130. package/livepilot/skills/livepilot-core/references/m4l-devices.md +0 -352
  131. package/livepilot/skills/livepilot-core/references/memory-guide.md +0 -107
  132. package/livepilot/skills/livepilot-core/references/midi-recipes.md +0 -402
  133. package/livepilot/skills/livepilot-core/references/mixing-patterns.md +0 -578
  134. package/livepilot/skills/livepilot-core/references/overview.md +0 -290
  135. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +0 -724
  136. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +0 -140
  137. package/livepilot/skills/livepilot-core/references/sound-design.md +0 -393
  138. package/livepilot/skills/livepilot-devices/SKILL.md +0 -169
  139. package/livepilot/skills/livepilot-evaluation/SKILL.md +0 -156
  140. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +0 -118
  141. package/livepilot/skills/livepilot-evaluation/references/evaluation-contracts.md +0 -121
  142. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +0 -110
  143. package/livepilot/skills/livepilot-mix-engine/SKILL.md +0 -123
  144. package/livepilot/skills/livepilot-mix-engine/references/mix-critics.md +0 -143
  145. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +0 -105
  146. package/livepilot/skills/livepilot-mixing/SKILL.md +0 -157
  147. package/livepilot/skills/livepilot-notes/SKILL.md +0 -130
  148. package/livepilot/skills/livepilot-performance-engine/SKILL.md +0 -122
  149. package/livepilot/skills/livepilot-performance-engine/references/performance-safety.md +0 -98
  150. package/livepilot/skills/livepilot-release/SKILL.md +0 -130
  151. package/livepilot/skills/livepilot-sample-engine/SKILL.md +0 -105
  152. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +0 -87
  153. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +0 -51
  154. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +0 -131
  155. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +0 -168
  156. package/livepilot/skills/livepilot-sound-design-engine/references/patch-model.md +0 -119
  157. package/livepilot/skills/livepilot-sound-design-engine/references/sound-design-critics.md +0 -118
  158. package/livepilot/skills/livepilot-wonder/SKILL.md +0 -79
  159. package/m4l_device/LivePilot_Analyzer.maxpat +0 -2705
  160. package/manifest.json +0 -91
  161. package/mcp_server/splice_client/protos/app_pb2.pyi +0 -1153
  162. package/scripts/generate_tool_catalog.py +0 -131
  163. 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
- candidates = style_map.get("I", ["IV", "V"])
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
- Finds diatonic chords containing each melody note and voices them
454
- following basic voice leading rules (smooth bass motion, no crossing).
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
- Returns note data ready for add_notes on new tracks.
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
- # Find the best diatonic chord containing this pitch
488
- best_chord = None
489
- for degree in [0, 3, 4, 5, 1, 2, 6]: # I, IV, V, vi, ii, iii, vii
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
- best_chord = chord
493
- break
546
+ candidates.append((degree, chord))
494
547
 
495
- if best_chord is None:
496
- best_chord = engine.build_chord(0, tonic, mode)
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
- # Contrary motion bonus
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 <= 2:
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 += 2
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
- pair_tags = {
275
- f"{boundary.from_type}_to_{boundary.to_type}",
276
- boundary.from_type,
277
- boundary.to_type,
278
- }
279
- use_case_match = any(
280
- tag in uc or uc in tag
281
- for tag in pair_tags
282
- for uc in archetype.use_cases
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.6",
3
+ "version": "1.10.8",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 — 320 tools, 43 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
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.6"
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
- # Try one more URI pass with a much higher iteration limit.
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 = 200000
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 > 12 or _iterations[0] > DEEP_MAX:
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 in browser (FileId URI — try "
330
- "search_browser to find the item, then use find_and_load_device "
331
- "with the exact name instead)" % uri
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 (safer than assuming index 0)
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
- # Fallback to parameter 0 for devices that don't use "Device On"
210
- if not list(device.parameters):
211
- raise ValueError("Device '%s' has no parameters to toggle" % device.name)
212
- on_param = device.parameters[0]
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}