livepilot 1.15.0-beta → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +206 -3
- package/README.md +11 -11
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/device_atlas.json +91219 -7161
- package/mcp_server/atlas/enrichments/audio_effects/pitch_hack.yaml +61 -0
- package/mcp_server/atlas/enrichments/audio_effects/pitchloop89.yaml +111 -0
- package/mcp_server/atlas/enrichments/audio_effects/re_enveloper.yaml +51 -0
- package/mcp_server/atlas/enrichments/audio_effects/snipper.yaml +36 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_blur.yaml +64 -0
- package/mcp_server/atlas/enrichments/instruments/bell_tower.yaml +37 -0
- package/mcp_server/atlas/enrichments/instruments/granulator_iii.yaml +124 -0
- package/mcp_server/atlas/enrichments/instruments/harmonic_drone_generator.yaml +83 -0
- package/mcp_server/atlas/enrichments/instruments/impulse.yaml +47 -0
- package/mcp_server/atlas/enrichments/instruments/sting_iftah.yaml +44 -0
- package/mcp_server/atlas/enrichments/midi_effects/expressive_chords.yaml +38 -0
- package/mcp_server/atlas/enrichments/midi_effects/filler.yaml +32 -0
- package/mcp_server/atlas/enrichments/midi_effects/microtuner.yaml +83 -0
- package/mcp_server/atlas/enrichments/midi_effects/patterns_iftah.yaml +38 -0
- package/mcp_server/atlas/enrichments/midi_effects/phase_pattern.yaml +51 -0
- package/mcp_server/atlas/enrichments/midi_effects/polyrhythm.yaml +46 -0
- package/mcp_server/atlas/enrichments/midi_effects/retrigger.yaml +40 -0
- package/mcp_server/atlas/enrichments/midi_effects/slice_shuffler.yaml +39 -0
- package/mcp_server/atlas/enrichments/midi_effects/sq_sequencer.yaml +39 -0
- package/mcp_server/atlas/enrichments/midi_effects/stages.yaml +38 -0
- package/mcp_server/atlas/enrichments/utility/arrangement_looper.yaml +31 -0
- package/mcp_server/atlas/enrichments/utility/cv_clock_in.yaml +25 -0
- package/mcp_server/atlas/enrichments/utility/cv_clock_out.yaml +25 -0
- package/mcp_server/atlas/enrichments/utility/cv_envelope_follower.yaml +38 -0
- package/mcp_server/atlas/enrichments/utility/cv_in.yaml +26 -0
- package/mcp_server/atlas/enrichments/utility/cv_instrument.yaml +34 -0
- package/mcp_server/atlas/enrichments/utility/cv_lfo.yaml +38 -0
- package/mcp_server/atlas/enrichments/utility/cv_shaper.yaml +35 -0
- package/mcp_server/atlas/enrichments/utility/cv_triggers.yaml +26 -0
- package/mcp_server/atlas/enrichments/utility/cv_utility.yaml +37 -0
- package/mcp_server/atlas/enrichments/utility/performer.yaml +36 -0
- package/mcp_server/atlas/enrichments/utility/prearranger.yaml +36 -0
- package/mcp_server/atlas/enrichments/utility/rotating_rhythm_generator.yaml +35 -0
- package/mcp_server/atlas/enrichments/utility/surround_panner.yaml +40 -0
- package/mcp_server/atlas/enrichments/utility/variations.yaml +40 -0
- package/mcp_server/atlas/enrichments/utility/vector_map.yaml +36 -0
- package/mcp_server/atlas/tools.py +30 -2
- package/mcp_server/runtime/remote_commands.py +3 -0
- package/mcp_server/sample_engine/tools.py +738 -60
- package/mcp_server/server.py +18 -6
- package/mcp_server/splice_client/client.py +583 -65
- package/mcp_server/splice_client/http_bridge.py +434 -0
- package/mcp_server/splice_client/models.py +278 -2
- package/mcp_server/splice_client/quota.py +229 -0
- package/mcp_server/tools/_analyzer_engine/__init__.py +4 -0
- package/mcp_server/tools/_analyzer_engine/sample.py +73 -0
- package/mcp_server/tools/analyzer.py +730 -29
- package/mcp_server/tools/browser.py +164 -13
- package/mcp_server/tools/devices.py +56 -11
- package/mcp_server/tools/mixing.py +64 -15
- package/mcp_server/tools/scales.py +18 -6
- package/mcp_server/tools/tracks.py +92 -4
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/_clip_helpers.py +86 -0
- package/remote_script/LivePilot/_drum_helpers.py +40 -0
- package/remote_script/LivePilot/_scale_helpers.py +87 -0
- package/remote_script/LivePilot/arrangement.py +44 -15
- package/remote_script/LivePilot/clips.py +182 -2
- package/remote_script/LivePilot/devices.py +82 -2
- package/remote_script/LivePilot/notes.py +17 -2
- package/remote_script/LivePilot/scales.py +31 -16
- package/remote_script/LivePilot/simpler_sample.py +105 -17
- package/server.json +3 -3
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""HTTPS bridge for Splice plugin-exclusive features.
|
|
2
|
+
|
|
3
|
+
The Splice Sounds Plugin (beta) ships two capabilities that are NOT on the
|
|
4
|
+
local gRPC service:
|
|
5
|
+
- **Describe a Sound** — natural-language search ("dark ambient pad
|
|
6
|
+
with shimmer")
|
|
7
|
+
- **Variations** — generate unique re-keyed / re-tempo'd versions of
|
|
8
|
+
any sample
|
|
9
|
+
|
|
10
|
+
Both call `api.splice.com` over HTTPS, authenticated with the session
|
|
11
|
+
token we can read from the local gRPC `GetSession` RPC.
|
|
12
|
+
|
|
13
|
+
This module is *scaffolding* — it builds the auth flow, endpoint URLs,
|
|
14
|
+
response parsing, and retry/timeout plumbing so that capturing the real
|
|
15
|
+
endpoint shapes (via mitmproxy against the running plugin) is a matter
|
|
16
|
+
of updating the URL templates rather than rebuilding infrastructure.
|
|
17
|
+
|
|
18
|
+
## How to go from scaffolding to working tool
|
|
19
|
+
|
|
20
|
+
1. Run mitmproxy in transparent mode against the Splice Sounds Plugin
|
|
21
|
+
while it makes a Describe a Sound or Variations request.
|
|
22
|
+
2. Capture the real endpoint URL, request body shape, and response body.
|
|
23
|
+
3. Drop the values into `SpliceHTTPConfig` defaults or via env vars:
|
|
24
|
+
- `SPLICE_API_BASE_URL` (default: https://api.splice.com)
|
|
25
|
+
- `SPLICE_DESCRIBE_ENDPOINT` (default: /v1/describe)
|
|
26
|
+
- `SPLICE_VARIATION_ENDPOINT` (default: /v1/variations/{file_hash})
|
|
27
|
+
4. Run `splice_describe_sound("dark pad")` — done.
|
|
28
|
+
|
|
29
|
+
Until step 4 is complete, the MCP tools return a clear, actionable error
|
|
30
|
+
rather than pretending to work. Zero cheats.
|
|
31
|
+
|
|
32
|
+
## Why token-based instead of embedding the plugin
|
|
33
|
+
|
|
34
|
+
The plugin's authentication flow uses Splice's OAuth session tokens.
|
|
35
|
+
These rotate periodically — hardcoding them wouldn't work. Reading from
|
|
36
|
+
`GetSession` RPC means we always use the current session, tied to the
|
|
37
|
+
user's currently-logged-in Splice desktop app.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import asyncio
|
|
43
|
+
import json
|
|
44
|
+
import logging
|
|
45
|
+
import os
|
|
46
|
+
import ssl
|
|
47
|
+
import urllib.error
|
|
48
|
+
import urllib.request
|
|
49
|
+
from dataclasses import dataclass, field
|
|
50
|
+
from typing import Any, Optional
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── Configuration ─────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_DEFAULT_CONFIG_PATH = os.path.expanduser("~/.livepilot/splice.json")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class SpliceHTTPConfig:
|
|
63
|
+
"""Endpoint configuration for the HTTPS bridge.
|
|
64
|
+
|
|
65
|
+
Three sources, checked in order of precedence:
|
|
66
|
+
1. Env vars (highest — useful for one-off tests / CI)
|
|
67
|
+
2. JSON config file at `~/.livepilot/splice.json` (persistent user config)
|
|
68
|
+
3. Built-in defaults (unverified guesses — WILL need updating when
|
|
69
|
+
we capture real traffic)
|
|
70
|
+
|
|
71
|
+
JSON config shape:
|
|
72
|
+
{
|
|
73
|
+
"base_url": "https://api.splice.com",
|
|
74
|
+
"describe_endpoint": "/v1/...",
|
|
75
|
+
"variation_endpoint": "/v1/variations/{file_hash}",
|
|
76
|
+
"search_with_sound_endpoint": "/v1/...",
|
|
77
|
+
"timeout_sec": 30.0,
|
|
78
|
+
"max_retries": 2,
|
|
79
|
+
"allow_unverified_endpoints": false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Any subset of keys is allowed; omitted keys fall through to defaults.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
base_url: str = "https://api.splice.com"
|
|
86
|
+
describe_endpoint: str = "/v1/describe"
|
|
87
|
+
variation_endpoint: str = "/v1/variations/{file_hash}"
|
|
88
|
+
search_with_sound_endpoint: str = "/v1/search-with-sound"
|
|
89
|
+
timeout_sec: float = 30.0
|
|
90
|
+
max_retries: int = 2
|
|
91
|
+
# Whether any of the above values came from user config (file or env)
|
|
92
|
+
# rather than the built-in defaults. Used by `is_user_configured`.
|
|
93
|
+
_user_configured: bool = False
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_env(cls, config_path: Optional[str] = None) -> "SpliceHTTPConfig":
|
|
97
|
+
"""Load config: defaults → JSON file → env vars.
|
|
98
|
+
|
|
99
|
+
`config_path` override is test-only. Production always uses
|
|
100
|
+
~/.livepilot/splice.json (or skips the file silently if absent).
|
|
101
|
+
"""
|
|
102
|
+
instance = cls()
|
|
103
|
+
loaded_from_file = False
|
|
104
|
+
|
|
105
|
+
# Layer 1: JSON file (persistent user config)
|
|
106
|
+
path = config_path or _DEFAULT_CONFIG_PATH
|
|
107
|
+
if os.path.isfile(path):
|
|
108
|
+
try:
|
|
109
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
110
|
+
data = json.load(f)
|
|
111
|
+
if isinstance(data, dict):
|
|
112
|
+
for key in (
|
|
113
|
+
"base_url", "describe_endpoint", "variation_endpoint",
|
|
114
|
+
"search_with_sound_endpoint",
|
|
115
|
+
):
|
|
116
|
+
if key in data and isinstance(data[key], str):
|
|
117
|
+
setattr(instance, key, data[key])
|
|
118
|
+
loaded_from_file = True
|
|
119
|
+
for key in ("timeout_sec",):
|
|
120
|
+
if key in data:
|
|
121
|
+
try:
|
|
122
|
+
setattr(instance, key, float(data[key]))
|
|
123
|
+
loaded_from_file = True
|
|
124
|
+
except (TypeError, ValueError):
|
|
125
|
+
logger.warning(
|
|
126
|
+
"splice.json: %s must be a number", key,
|
|
127
|
+
)
|
|
128
|
+
for key in ("max_retries",):
|
|
129
|
+
if key in data:
|
|
130
|
+
try:
|
|
131
|
+
setattr(instance, key, int(data[key]))
|
|
132
|
+
loaded_from_file = True
|
|
133
|
+
except (TypeError, ValueError):
|
|
134
|
+
logger.warning(
|
|
135
|
+
"splice.json: %s must be an integer", key,
|
|
136
|
+
)
|
|
137
|
+
if data.get("allow_unverified_endpoints"):
|
|
138
|
+
loaded_from_file = True
|
|
139
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
140
|
+
logger.warning(
|
|
141
|
+
"Could not load %s: %s — falling back to defaults/env",
|
|
142
|
+
path, exc,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Layer 2: env vars (override file/defaults)
|
|
146
|
+
env_keys = (
|
|
147
|
+
("SPLICE_API_BASE_URL", "base_url", str),
|
|
148
|
+
("SPLICE_DESCRIBE_ENDPOINT", "describe_endpoint", str),
|
|
149
|
+
("SPLICE_VARIATION_ENDPOINT", "variation_endpoint", str),
|
|
150
|
+
("SPLICE_SEARCH_WITH_SOUND_ENDPOINT", "search_with_sound_endpoint", str),
|
|
151
|
+
("SPLICE_HTTP_TIMEOUT", "timeout_sec", float),
|
|
152
|
+
("SPLICE_HTTP_RETRIES", "max_retries", int),
|
|
153
|
+
)
|
|
154
|
+
env_configured = False
|
|
155
|
+
for env_name, attr, cast in env_keys:
|
|
156
|
+
if env_name in os.environ:
|
|
157
|
+
try:
|
|
158
|
+
setattr(instance, attr, cast(os.environ[env_name]))
|
|
159
|
+
env_configured = True
|
|
160
|
+
except (TypeError, ValueError) as exc:
|
|
161
|
+
logger.warning(
|
|
162
|
+
"Env %s has invalid value: %s", env_name, exc,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
instance._user_configured = (
|
|
166
|
+
loaded_from_file
|
|
167
|
+
or env_configured
|
|
168
|
+
or os.environ.get("SPLICE_ALLOW_UNVERIFIED_ENDPOINTS") == "1"
|
|
169
|
+
)
|
|
170
|
+
return instance
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def is_user_configured(self) -> bool:
|
|
174
|
+
"""True when at least one endpoint URL has been overridden by the
|
|
175
|
+
user (JSON config file or env var).
|
|
176
|
+
|
|
177
|
+
Defaults are unverified guesses; callers check this before making
|
|
178
|
+
requests so we don't silently hit non-existent endpoints.
|
|
179
|
+
"""
|
|
180
|
+
return self._user_configured
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ── Auth token fetch ─────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def fetch_session_token(grpc_client) -> Optional[str]:
|
|
187
|
+
"""Fetch the current Splice session token from the local gRPC.
|
|
188
|
+
|
|
189
|
+
The `GetSession` RPC returns an `Auth` object with a `Token` field —
|
|
190
|
+
this is the bearer we attach to `api.splice.com` requests. The token
|
|
191
|
+
rotates periodically so we always fetch fresh rather than caching.
|
|
192
|
+
"""
|
|
193
|
+
if not grpc_client or not getattr(grpc_client, "connected", False):
|
|
194
|
+
return None
|
|
195
|
+
pb2 = getattr(grpc_client, "_pb2", None)
|
|
196
|
+
if pb2 is None:
|
|
197
|
+
return None
|
|
198
|
+
try:
|
|
199
|
+
response = await grpc_client.stub.GetSession(
|
|
200
|
+
pb2.GetSessionRequest(), timeout=5.0,
|
|
201
|
+
)
|
|
202
|
+
return str(response.Auth.Token) if response.Auth else None
|
|
203
|
+
except Exception as exc:
|
|
204
|
+
logger.warning("GetSession RPC failed: %s", exc)
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ── HTTP client ───────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass
|
|
212
|
+
class SpliceHTTPError(Exception):
|
|
213
|
+
"""Structured error for HTTPS-bridge calls."""
|
|
214
|
+
|
|
215
|
+
code: str
|
|
216
|
+
message: str
|
|
217
|
+
endpoint: str = ""
|
|
218
|
+
status_code: int = 0
|
|
219
|
+
|
|
220
|
+
def __str__(self) -> str:
|
|
221
|
+
return f"[{self.code}] {self.message} ({self.endpoint})"
|
|
222
|
+
|
|
223
|
+
def to_dict(self) -> dict:
|
|
224
|
+
return {
|
|
225
|
+
"ok": False,
|
|
226
|
+
"error": self.message,
|
|
227
|
+
"code": self.code,
|
|
228
|
+
"endpoint": self.endpoint,
|
|
229
|
+
"status_code": self.status_code,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class SpliceHTTPBridge:
|
|
234
|
+
"""Low-level HTTPS client for Splice cloud APIs.
|
|
235
|
+
|
|
236
|
+
Attaches the bearer token, retries on 5xx, applies a total timeout.
|
|
237
|
+
Thread-safe — each request builds its own opener. Synchronous network
|
|
238
|
+
calls run in an executor from the async wrappers.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
config: Optional[SpliceHTTPConfig] = None,
|
|
244
|
+
grpc_client=None,
|
|
245
|
+
):
|
|
246
|
+
self.config = config or SpliceHTTPConfig.from_env()
|
|
247
|
+
self.grpc_client = grpc_client
|
|
248
|
+
|
|
249
|
+
async def _request(
|
|
250
|
+
self,
|
|
251
|
+
method: str,
|
|
252
|
+
path: str,
|
|
253
|
+
body: Optional[dict] = None,
|
|
254
|
+
query: Optional[dict] = None,
|
|
255
|
+
) -> Any:
|
|
256
|
+
token = await fetch_session_token(self.grpc_client)
|
|
257
|
+
if token is None:
|
|
258
|
+
raise SpliceHTTPError(
|
|
259
|
+
code="NO_AUTH",
|
|
260
|
+
message=(
|
|
261
|
+
"Could not fetch Splice session token via GetSession RPC. "
|
|
262
|
+
"Is the Splice desktop app running and logged in?"
|
|
263
|
+
),
|
|
264
|
+
endpoint=path,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
url = self.config.base_url.rstrip("/") + path
|
|
268
|
+
if query:
|
|
269
|
+
import urllib.parse
|
|
270
|
+
qs = urllib.parse.urlencode(query)
|
|
271
|
+
url = f"{url}?{qs}"
|
|
272
|
+
|
|
273
|
+
data_bytes = None
|
|
274
|
+
headers = {
|
|
275
|
+
"Authorization": f"Bearer {token}",
|
|
276
|
+
"Accept": "application/json",
|
|
277
|
+
"User-Agent": "LivePilot/1.15 (+splice-http-bridge)",
|
|
278
|
+
}
|
|
279
|
+
if body is not None:
|
|
280
|
+
data_bytes = json.dumps(body).encode("utf-8")
|
|
281
|
+
headers["Content-Type"] = "application/json"
|
|
282
|
+
|
|
283
|
+
loop = asyncio.get_running_loop()
|
|
284
|
+
last_err = None
|
|
285
|
+
for attempt in range(1 + max(0, self.config.max_retries)):
|
|
286
|
+
try:
|
|
287
|
+
return await loop.run_in_executor(
|
|
288
|
+
None,
|
|
289
|
+
self._perform_sync_request,
|
|
290
|
+
url, method, data_bytes, headers,
|
|
291
|
+
)
|
|
292
|
+
except SpliceHTTPError as exc:
|
|
293
|
+
last_err = exc
|
|
294
|
+
# Retry only on 5xx / network. 4xx is terminal.
|
|
295
|
+
if exc.status_code and exc.status_code < 500:
|
|
296
|
+
raise
|
|
297
|
+
await asyncio.sleep(min(2 ** attempt, 5))
|
|
298
|
+
assert last_err is not None
|
|
299
|
+
raise last_err
|
|
300
|
+
|
|
301
|
+
def _perform_sync_request(self, url, method, data_bytes, headers):
|
|
302
|
+
try:
|
|
303
|
+
req = urllib.request.Request(
|
|
304
|
+
url, data=data_bytes, headers=headers, method=method,
|
|
305
|
+
)
|
|
306
|
+
context = ssl.create_default_context()
|
|
307
|
+
with urllib.request.urlopen(
|
|
308
|
+
req, timeout=self.config.timeout_sec, context=context,
|
|
309
|
+
) as resp:
|
|
310
|
+
raw = resp.read()
|
|
311
|
+
content_type = resp.headers.get("Content-Type", "")
|
|
312
|
+
if "application/json" in content_type:
|
|
313
|
+
return json.loads(raw.decode("utf-8"))
|
|
314
|
+
return {"raw": raw.decode("utf-8", errors="replace")}
|
|
315
|
+
except urllib.error.HTTPError as exc:
|
|
316
|
+
raise SpliceHTTPError(
|
|
317
|
+
code="HTTP_ERROR",
|
|
318
|
+
message=f"HTTP {exc.code}: {exc.reason}",
|
|
319
|
+
endpoint=url,
|
|
320
|
+
status_code=exc.code,
|
|
321
|
+
)
|
|
322
|
+
except urllib.error.URLError as exc:
|
|
323
|
+
raise SpliceHTTPError(
|
|
324
|
+
code="NETWORK_ERROR",
|
|
325
|
+
message=f"Network error: {exc.reason}",
|
|
326
|
+
endpoint=url,
|
|
327
|
+
status_code=0,
|
|
328
|
+
)
|
|
329
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
330
|
+
raise SpliceHTTPError(
|
|
331
|
+
code="DECODE_ERROR",
|
|
332
|
+
message=f"Response decode failed: {exc}",
|
|
333
|
+
endpoint=url,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# ── Tool-facing helpers ──────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
async def describe_sound(
|
|
339
|
+
self,
|
|
340
|
+
description: str,
|
|
341
|
+
bpm: Optional[int] = None,
|
|
342
|
+
key: Optional[str] = None,
|
|
343
|
+
limit: int = 20,
|
|
344
|
+
) -> dict:
|
|
345
|
+
"""Natural-language sample search.
|
|
346
|
+
|
|
347
|
+
Returns a dict with keys: `samples` (list of sample metadata),
|
|
348
|
+
`total_hits`, plus whatever Splice echoes back. Shape is best-effort
|
|
349
|
+
until we capture real traffic — see module docstring.
|
|
350
|
+
"""
|
|
351
|
+
if not self.config.is_user_configured:
|
|
352
|
+
raise SpliceHTTPError(
|
|
353
|
+
code="ENDPOINT_NOT_CONFIGURED",
|
|
354
|
+
message=(
|
|
355
|
+
"Describe a Sound endpoint is unverified. Set "
|
|
356
|
+
"SPLICE_DESCRIBE_ENDPOINT (or SPLICE_ALLOW_UNVERIFIED_"
|
|
357
|
+
"ENDPOINTS=1) once you've captured the real URL via "
|
|
358
|
+
"mitmproxy against the Sounds Plugin."
|
|
359
|
+
),
|
|
360
|
+
endpoint=self.config.describe_endpoint,
|
|
361
|
+
)
|
|
362
|
+
body = {
|
|
363
|
+
"description": description,
|
|
364
|
+
"limit": int(limit),
|
|
365
|
+
}
|
|
366
|
+
if bpm is not None:
|
|
367
|
+
body["bpm"] = int(bpm)
|
|
368
|
+
if key:
|
|
369
|
+
body["key"] = key
|
|
370
|
+
return await self._request("POST", self.config.describe_endpoint, body=body)
|
|
371
|
+
|
|
372
|
+
async def generate_variation(
|
|
373
|
+
self,
|
|
374
|
+
file_hash: str,
|
|
375
|
+
target_key: Optional[str] = None,
|
|
376
|
+
target_bpm: Optional[int] = None,
|
|
377
|
+
count: int = 1,
|
|
378
|
+
) -> dict:
|
|
379
|
+
"""Generate AI variations of a sample.
|
|
380
|
+
|
|
381
|
+
Returns a dict with keys: `variations` (list), `credits_spent`.
|
|
382
|
+
Shape is best-effort until captured — see module docstring.
|
|
383
|
+
"""
|
|
384
|
+
if not self.config.is_user_configured:
|
|
385
|
+
raise SpliceHTTPError(
|
|
386
|
+
code="ENDPOINT_NOT_CONFIGURED",
|
|
387
|
+
message=(
|
|
388
|
+
"Variations endpoint is unverified. Set "
|
|
389
|
+
"SPLICE_VARIATION_ENDPOINT (or SPLICE_ALLOW_UNVERIFIED_"
|
|
390
|
+
"ENDPOINTS=1) once you've captured the real URL via "
|
|
391
|
+
"mitmproxy against the Sounds Plugin."
|
|
392
|
+
),
|
|
393
|
+
endpoint=self.config.variation_endpoint,
|
|
394
|
+
)
|
|
395
|
+
path = self.config.variation_endpoint.format(file_hash=file_hash)
|
|
396
|
+
body: dict = {"count": max(1, int(count))}
|
|
397
|
+
if target_key:
|
|
398
|
+
body["target_key"] = target_key
|
|
399
|
+
if target_bpm is not None:
|
|
400
|
+
body["target_bpm"] = int(target_bpm)
|
|
401
|
+
return await self._request("POST", path, body=body)
|
|
402
|
+
|
|
403
|
+
async def search_with_sound(
|
|
404
|
+
self,
|
|
405
|
+
audio_path: str,
|
|
406
|
+
limit: int = 20,
|
|
407
|
+
) -> dict:
|
|
408
|
+
"""Sample-reference search — find catalog samples similar to a file.
|
|
409
|
+
|
|
410
|
+
Encodes the file as a multipart POST. Wiring waits on a real
|
|
411
|
+
endpoint capture; the upload shape is the most uncertain part
|
|
412
|
+
of the bridge.
|
|
413
|
+
"""
|
|
414
|
+
if not self.config.is_user_configured:
|
|
415
|
+
raise SpliceHTTPError(
|
|
416
|
+
code="ENDPOINT_NOT_CONFIGURED",
|
|
417
|
+
message=(
|
|
418
|
+
"Search with Sound endpoint is unverified. Set "
|
|
419
|
+
"SPLICE_SEARCH_WITH_SOUND_ENDPOINT (or SPLICE_ALLOW_"
|
|
420
|
+
"UNVERIFIED_ENDPOINTS=1) once you've captured the real "
|
|
421
|
+
"URL via mitmproxy against the Sounds Plugin."
|
|
422
|
+
),
|
|
423
|
+
endpoint=self.config.search_with_sound_endpoint,
|
|
424
|
+
)
|
|
425
|
+
# Multipart upload — reserved for the real-capture wiring.
|
|
426
|
+
raise SpliceHTTPError(
|
|
427
|
+
code="NOT_YET_IMPLEMENTED",
|
|
428
|
+
message=(
|
|
429
|
+
"search_with_sound multipart upload wiring is pending real-"
|
|
430
|
+
"endpoint capture. File a follow-up when the Describe a "
|
|
431
|
+
"Sound endpoint has been mapped — similar shape is likely."
|
|
432
|
+
),
|
|
433
|
+
endpoint=self.config.search_with_sound_endpoint,
|
|
434
|
+
)
|