livepilot 1.16.1 → 1.17.0
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 +269 -0
- package/README.md +16 -15
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +85 -0
- package/mcp_server/atlas/device_atlas.json +3183 -382
- package/mcp_server/atlas/device_techniques_index.json +1510 -0
- package/mcp_server/atlas/enrichments/__init__.py +1 -0
- package/mcp_server/atlas/enrichments/audio_effects/amp.yaml +112 -0
- package/mcp_server/atlas/enrichments/audio_effects/audio_effect_rack.yaml +77 -0
- package/mcp_server/atlas/enrichments/audio_effects/cabinet.yaml +81 -0
- package/mcp_server/atlas/enrichments/audio_effects/corpus.yaml +128 -0
- package/mcp_server/atlas/enrichments/audio_effects/envelope_follower.yaml +99 -0
- package/mcp_server/atlas/enrichments/audio_effects/external_audio_effect.yaml +64 -0
- package/mcp_server/atlas/enrichments/audio_effects/looper.yaml +85 -0
- package/mcp_server/atlas/enrichments/audio_effects/resonators.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/snipper.yaml +17 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectrum.yaml +61 -0
- package/mcp_server/atlas/enrichments/audio_effects/tuner.yaml +43 -0
- package/mcp_server/atlas/enrichments/audio_effects/utility.yaml +118 -0
- package/mcp_server/atlas/enrichments/audio_effects/vocoder.yaml +94 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/bell_tower.yaml +38 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/drum_rack.yaml +142 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +12 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +15 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +11 -0
- package/mcp_server/atlas/enrichments/midi_effects/filler.yaml +17 -0
- package/mcp_server/atlas/enrichments/utility/performer.yaml +15 -0
- package/mcp_server/atlas/enrichments/utility/vector_map.yaml +21 -0
- package/mcp_server/atlas/tools.py +291 -0
- package/mcp_server/m4l_bridge.py +19 -2
- package/mcp_server/sample_engine/tools.py +140 -68
- package/mcp_server/splice_client/http_bridge.py +319 -116
- package/mcp_server/tools/automation.py +168 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +216 -1
- package/server.json +3 -3
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
"""HTTPS bridge for Splice plugin-exclusive features.
|
|
2
2
|
|
|
3
|
-
The Splice
|
|
4
|
-
local gRPC service:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
##
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`
|
|
37
|
-
|
|
3
|
+
The Splice desktop app and its plugin ship capabilities that are NOT on
|
|
4
|
+
the local gRPC service. They route through a single GraphQL endpoint:
|
|
5
|
+
|
|
6
|
+
- **Describe a Sound / keyword search** — GraphQL operation
|
|
7
|
+
`SamplesSearch` with `semantic` + `rephrase` flags. One operation
|
|
8
|
+
serves both modes via variable toggles.
|
|
9
|
+
- **Variations** — GraphQL operation `AssetSimilarSoundsQuery`. A
|
|
10
|
+
recommender lookup ("find similar catalog samples"), not AI audio
|
|
11
|
+
synthesis.
|
|
12
|
+
|
|
13
|
+
Both authenticated with the bearer JWT we can read from the local gRPC
|
|
14
|
+
`GetSession` RPC. Both captured 2026-04-22 via mitmproxy against
|
|
15
|
+
Splice desktop v5.4.9 + Ableton 12.4 on macOS.
|
|
16
|
+
|
|
17
|
+
## Endpoint config
|
|
18
|
+
|
|
19
|
+
- Base URL: `https://surfaces-graphql.splice.com`
|
|
20
|
+
- Path: `/graphql`
|
|
21
|
+
- Auth: `Authorization: Bearer <JWT>` (via gRPC GetSession)
|
|
22
|
+
- Content-type: `application/json`
|
|
23
|
+
- Body: `{operationName, variables, query}`
|
|
24
|
+
- User-Agent: LivePilot default (override via env var if Cloudflare
|
|
25
|
+
blocks — mimic `Splice Baelish/darwin/arm64/arm64 5.4.9/...`)
|
|
26
|
+
|
|
27
|
+
## GraphQL query location
|
|
28
|
+
|
|
29
|
+
The full query strings live under `graphql_queries/*.graphql` and are
|
|
30
|
+
loaded lazily at module-import. One file per operation.
|
|
31
|
+
|
|
32
|
+
## Explicitly NOT wired
|
|
33
|
+
|
|
34
|
+
Search-with-Sound (drag-audio reference search) was removed 2026-04-22
|
|
35
|
+
— user handles this directly in Splice's UI. The capture recipe is
|
|
36
|
+
preserved at `docs/2026-04-22-splice-https-capture-recipe.md` for any
|
|
37
|
+
future session that wants to resurrect the tool.
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
from __future__ import annotations
|
|
@@ -70,10 +70,9 @@ class SpliceHTTPConfig:
|
|
|
70
70
|
|
|
71
71
|
JSON config shape:
|
|
72
72
|
{
|
|
73
|
-
"base_url": "https://
|
|
74
|
-
"describe_endpoint": "/
|
|
75
|
-
"variation_endpoint": "/
|
|
76
|
-
"search_with_sound_endpoint": "/v1/...",
|
|
73
|
+
"base_url": "https://surfaces-graphql.splice.com",
|
|
74
|
+
"describe_endpoint": "/graphql",
|
|
75
|
+
"variation_endpoint": "/graphql",
|
|
77
76
|
"timeout_sec": 30.0,
|
|
78
77
|
"max_retries": 2,
|
|
79
78
|
"allow_unverified_endpoints": false
|
|
@@ -82,10 +81,12 @@ class SpliceHTTPConfig:
|
|
|
82
81
|
Any subset of keys is allowed; omitted keys fall through to defaults.
|
|
83
82
|
"""
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
# Captured from Splice desktop v5.4.9 via mitmproxy on 2026-04-22.
|
|
85
|
+
base_url: str = "https://surfaces-graphql.splice.com"
|
|
86
|
+
describe_endpoint: str = "/graphql" # GraphQL SamplesSearch operation
|
|
87
|
+
variation_endpoint: str = "/graphql" # GraphQL AssetSimilarSoundsQuery
|
|
88
|
+
# Mimic the desktop client UA when Cloudflare complains.
|
|
89
|
+
user_agent: str = "LivePilot/1.16 (+splice-http-bridge)"
|
|
89
90
|
timeout_sec: float = 30.0
|
|
90
91
|
max_retries: int = 2
|
|
91
92
|
# Whether any of the above values came from user config (file or env)
|
|
@@ -111,7 +112,6 @@ class SpliceHTTPConfig:
|
|
|
111
112
|
if isinstance(data, dict):
|
|
112
113
|
for key in (
|
|
113
114
|
"base_url", "describe_endpoint", "variation_endpoint",
|
|
114
|
-
"search_with_sound_endpoint",
|
|
115
115
|
):
|
|
116
116
|
if key in data and isinstance(data[key], str):
|
|
117
117
|
setattr(instance, key, data[key])
|
|
@@ -147,7 +147,6 @@ class SpliceHTTPConfig:
|
|
|
147
147
|
("SPLICE_API_BASE_URL", "base_url", str),
|
|
148
148
|
("SPLICE_DESCRIBE_ENDPOINT", "describe_endpoint", str),
|
|
149
149
|
("SPLICE_VARIATION_ENDPOINT", "variation_endpoint", str),
|
|
150
|
-
("SPLICE_SEARCH_WITH_SOUND_ENDPOINT", "search_with_sound_endpoint", str),
|
|
151
150
|
("SPLICE_HTTP_TIMEOUT", "timeout_sec", float),
|
|
152
151
|
("SPLICE_HTTP_RETRIES", "max_retries", int),
|
|
153
152
|
)
|
|
@@ -174,11 +173,34 @@ class SpliceHTTPConfig:
|
|
|
174
173
|
"""True when at least one endpoint URL has been overridden by the
|
|
175
174
|
user (JSON config file or env var).
|
|
176
175
|
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
Historically this was the gate for all describe/variation tools
|
|
177
|
+
because defaults were unverified. As of 2026-04-22 the describe
|
|
178
|
+
endpoint is verified (GraphQL `surfaces-graphql.splice.com`), so
|
|
179
|
+
`describe_sound` no longer gates on this. Variation and
|
|
180
|
+
search-with-sound still do, because their GraphQL operations
|
|
181
|
+
haven't been captured yet.
|
|
179
182
|
"""
|
|
180
183
|
return self._user_configured
|
|
181
184
|
|
|
185
|
+
@property
|
|
186
|
+
def describe_verified(self) -> bool:
|
|
187
|
+
"""True when the describe endpoint is at its known-working value
|
|
188
|
+
OR the user has explicitly overridden it."""
|
|
189
|
+
return (
|
|
190
|
+
self.base_url == "https://surfaces-graphql.splice.com"
|
|
191
|
+
and self.describe_endpoint == "/graphql"
|
|
192
|
+
) or self._user_configured
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def variation_verified(self) -> bool:
|
|
196
|
+
"""True when the variation endpoint is at its known-working
|
|
197
|
+
value (the captured AssetSimilarSoundsQuery path) OR the user
|
|
198
|
+
has explicitly overridden it. Captured 2026-04-22."""
|
|
199
|
+
return (
|
|
200
|
+
self.base_url == "https://surfaces-graphql.splice.com"
|
|
201
|
+
and self.variation_endpoint == "/graphql"
|
|
202
|
+
) or self._user_configured
|
|
203
|
+
|
|
182
204
|
|
|
183
205
|
# ── Auth token fetch ─────────────────────────────────────────────────
|
|
184
206
|
|
|
@@ -205,6 +227,160 @@ async def fetch_session_token(grpc_client) -> Optional[str]:
|
|
|
205
227
|
return None
|
|
206
228
|
|
|
207
229
|
|
|
230
|
+
# ── GraphQL query loading ────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
_QUERY_DIR = os.path.join(os.path.dirname(__file__), "graphql_queries")
|
|
234
|
+
_QUERY_CACHE: dict[str, str] = {}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _load_graphql_query(name: str) -> str:
|
|
238
|
+
"""Load a `.graphql` file from `graphql_queries/` lazily (cached).
|
|
239
|
+
|
|
240
|
+
Separating the 5800-char SamplesSearch query into its own file keeps
|
|
241
|
+
the Python source readable and lets GraphQL-aware tools (IDE syntax
|
|
242
|
+
highlighting, schema-based linters) treat it as a first-class query.
|
|
243
|
+
|
|
244
|
+
Raises FileNotFoundError with a clear message if the query hasn't
|
|
245
|
+
been captured yet.
|
|
246
|
+
"""
|
|
247
|
+
if name in _QUERY_CACHE:
|
|
248
|
+
return _QUERY_CACHE[name]
|
|
249
|
+
|
|
250
|
+
path = os.path.join(_QUERY_DIR, f"{name}.graphql")
|
|
251
|
+
if not os.path.isfile(path):
|
|
252
|
+
raise FileNotFoundError(
|
|
253
|
+
f"GraphQL query '{name}' not found at {path}. "
|
|
254
|
+
f"Capture it via mitmproxy against the Splice desktop app "
|
|
255
|
+
f"(see docs/2026-04-22-splice-https-capture-recipe.md) and "
|
|
256
|
+
f"save the captured `query` string to the .graphql file."
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
260
|
+
query = f.read()
|
|
261
|
+
_QUERY_CACHE[name] = query
|
|
262
|
+
return query
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _flatten_sample_item(it: dict) -> dict:
|
|
266
|
+
"""Turn a single Splice GraphQL SampleAsset item into the flat shape
|
|
267
|
+
LivePilot's tools surface. Shared between SamplesSearch.items[] and
|
|
268
|
+
similarSounds[] — both queries return identically-shaped items.
|
|
269
|
+
"""
|
|
270
|
+
if not isinstance(it, dict):
|
|
271
|
+
return {}
|
|
272
|
+
tag_labels = [
|
|
273
|
+
t.get("label", "") for t in (it.get("tags") or [])
|
|
274
|
+
if isinstance(t, dict)
|
|
275
|
+
]
|
|
276
|
+
# Pack info (items have optional `parents` or can be PackAsset-shaped)
|
|
277
|
+
pack_name = None
|
|
278
|
+
parents = it.get("parents") or {}
|
|
279
|
+
if isinstance(parents, dict):
|
|
280
|
+
pitems = parents.get("items") or []
|
|
281
|
+
if pitems and isinstance(pitems[0], dict):
|
|
282
|
+
pack_name = pitems[0].get("name")
|
|
283
|
+
return {
|
|
284
|
+
"uuid": it.get("uuid"),
|
|
285
|
+
"name": it.get("name"),
|
|
286
|
+
"bpm": it.get("bpm"),
|
|
287
|
+
"key": it.get("key"),
|
|
288
|
+
"duration": it.get("duration"),
|
|
289
|
+
"instrument": it.get("instrument"),
|
|
290
|
+
"asset_category": it.get("asset_category_slug"),
|
|
291
|
+
"chord_type": it.get("chord_type"),
|
|
292
|
+
"tags": tag_labels,
|
|
293
|
+
"liked": bool(it.get("liked")),
|
|
294
|
+
"licensed": bool(it.get("licensed")),
|
|
295
|
+
"pack_name": pack_name,
|
|
296
|
+
"files": it.get("files") or [],
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _check_graphql_errors(raw) -> None:
|
|
301
|
+
"""Raise SpliceHTTPError if the GraphQL response has top-level errors.
|
|
302
|
+
|
|
303
|
+
Splice returns errors as a top-level `errors: [...]` array alongside
|
|
304
|
+
or instead of `data:`. A 200 response can still carry a logical
|
|
305
|
+
error, so every parser must check.
|
|
306
|
+
"""
|
|
307
|
+
if not isinstance(raw, dict):
|
|
308
|
+
return
|
|
309
|
+
if raw.get("errors"):
|
|
310
|
+
errs = raw["errors"]
|
|
311
|
+
first = errs[0] if isinstance(errs, list) and errs else errs
|
|
312
|
+
msg = (first.get("message") if isinstance(first, dict) else str(first))
|
|
313
|
+
raise SpliceHTTPError(
|
|
314
|
+
code="GRAPHQL_ERROR",
|
|
315
|
+
message=f"Splice GraphQL error: {msg}",
|
|
316
|
+
endpoint="/graphql",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _parse_samples_search(raw: dict) -> dict:
|
|
321
|
+
"""Normalize the SamplesSearch GraphQL response into a flat shape.
|
|
322
|
+
|
|
323
|
+
GraphQL shape: { data: { assetsSearch: { items: [...],
|
|
324
|
+
tag_summary: [...], rephrased_query_string, ... } } }
|
|
325
|
+
Flat shape: { samples: [...], total_hits, rephrased_query_string,
|
|
326
|
+
tag_summary, raw }
|
|
327
|
+
"""
|
|
328
|
+
if not isinstance(raw, dict):
|
|
329
|
+
return {"samples": [], "total_hits": 0, "raw": raw}
|
|
330
|
+
_check_graphql_errors(raw)
|
|
331
|
+
|
|
332
|
+
data = raw.get("data") or {}
|
|
333
|
+
page = data.get("assetsSearch") or {}
|
|
334
|
+
items = page.get("items") or []
|
|
335
|
+
|
|
336
|
+
samples = [_flatten_sample_item(it) for it in items if isinstance(it, dict)]
|
|
337
|
+
|
|
338
|
+
pm = page.get("pagination_metadata") or {}
|
|
339
|
+
rm = page.get("response_metadata") or {}
|
|
340
|
+
return {
|
|
341
|
+
"samples": samples,
|
|
342
|
+
"total_hits": rm.get("records") or len(samples),
|
|
343
|
+
"total_pages": pm.get("totalPages"),
|
|
344
|
+
"current_page": pm.get("currentPage"),
|
|
345
|
+
"rephrased_query_string": page.get("rephrased_query_string"),
|
|
346
|
+
"tag_summary": [
|
|
347
|
+
{"label": (ts.get("tag") or {}).get("label"),
|
|
348
|
+
"count": ts.get("count")}
|
|
349
|
+
for ts in (page.get("tag_summary") or [])
|
|
350
|
+
if isinstance(ts, dict)
|
|
351
|
+
],
|
|
352
|
+
"raw": page,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _parse_similar_sounds(raw: dict) -> dict:
|
|
357
|
+
"""Normalize the AssetSimilarSoundsQuery GraphQL response.
|
|
358
|
+
|
|
359
|
+
GraphQL shape: { data: { similarSounds: [SampleAsset, ...] } }
|
|
360
|
+
Flat shape: { similar_samples: [...], count }
|
|
361
|
+
|
|
362
|
+
Note: Splice calls this the "Variations" feature in the UI, but the
|
|
363
|
+
underlying semantics are "find catalog samples similar to this one" —
|
|
364
|
+
not "generate new audio variants". No target-key / target-BPM inputs
|
|
365
|
+
are supported; the API returns whatever the recommender picks.
|
|
366
|
+
"""
|
|
367
|
+
if not isinstance(raw, dict):
|
|
368
|
+
return {"similar_samples": [], "count": 0, "raw": raw}
|
|
369
|
+
_check_graphql_errors(raw)
|
|
370
|
+
|
|
371
|
+
data = raw.get("data") or {}
|
|
372
|
+
sims = data.get("similarSounds") or []
|
|
373
|
+
if not isinstance(sims, list):
|
|
374
|
+
return {"similar_samples": [], "count": 0, "raw": sims}
|
|
375
|
+
|
|
376
|
+
samples = [_flatten_sample_item(it) for it in sims if isinstance(it, dict)]
|
|
377
|
+
return {
|
|
378
|
+
"similar_samples": samples,
|
|
379
|
+
"count": len(samples),
|
|
380
|
+
"raw": sims,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
208
384
|
# ── HTTP client ───────────────────────────────────────────────────────
|
|
209
385
|
|
|
210
386
|
|
|
@@ -274,7 +450,7 @@ class SpliceHTTPBridge:
|
|
|
274
450
|
headers = {
|
|
275
451
|
"Authorization": f"Bearer {token}",
|
|
276
452
|
"Accept": "application/json",
|
|
277
|
-
"User-Agent":
|
|
453
|
+
"User-Agent": self.config.user_agent,
|
|
278
454
|
}
|
|
279
455
|
if body is not None:
|
|
280
456
|
data_bytes = json.dumps(body).encode("utf-8")
|
|
@@ -341,94 +517,121 @@ class SpliceHTTPBridge:
|
|
|
341
517
|
bpm: Optional[int] = None,
|
|
342
518
|
key: Optional[str] = None,
|
|
343
519
|
limit: int = 20,
|
|
520
|
+
rephrase: bool = True,
|
|
344
521
|
) -> dict:
|
|
345
|
-
"""Natural-language sample search
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
`
|
|
349
|
-
|
|
522
|
+
"""Natural-language sample search via the GraphQL SamplesSearch
|
|
523
|
+
operation (captured 2026-04-22).
|
|
524
|
+
|
|
525
|
+
Splice's `SamplesSearch` operation serves both keyword AND
|
|
526
|
+
semantic/describe search from a single endpoint. We set
|
|
527
|
+
`semantic=1` + `rephrase=True` for describe-style queries.
|
|
528
|
+
The server echoes `rephrased_query_string` in the response
|
|
529
|
+
which tells us what the describe engine actually searched for.
|
|
530
|
+
|
|
531
|
+
Returns a dict with keys:
|
|
532
|
+
- `samples`: list of clean sample-metadata dicts (uuid, name,
|
|
533
|
+
bpm, key, tags, duration, …)
|
|
534
|
+
- `total_hits`: response record count (from pagination metadata)
|
|
535
|
+
- `rephrased_query_string`: what Splice rephrased the query to
|
|
536
|
+
- `tag_summary`: list of {label, count} for faceted filtering
|
|
537
|
+
- `raw`: the full GraphQL `data` block for debugging
|
|
538
|
+
|
|
539
|
+
Raises SpliceHTTPError on auth failure, network issues, or
|
|
540
|
+
GraphQL-level errors.
|
|
350
541
|
"""
|
|
351
|
-
if not self.config.
|
|
542
|
+
if not self.config.describe_verified:
|
|
352
543
|
raise SpliceHTTPError(
|
|
353
544
|
code="ENDPOINT_NOT_CONFIGURED",
|
|
354
545
|
message=(
|
|
355
|
-
"Describe
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
"
|
|
546
|
+
"Describe endpoint points at an unverified URL. "
|
|
547
|
+
"Reset to defaults, or set SPLICE_API_BASE_URL + "
|
|
548
|
+
"SPLICE_DESCRIBE_ENDPOINT to match real Splice "
|
|
549
|
+
"graphql surface (see http_bridge.py docstring)."
|
|
359
550
|
),
|
|
360
|
-
endpoint=self.config.describe_endpoint,
|
|
551
|
+
endpoint=f"{self.config.base_url}{self.config.describe_endpoint}",
|
|
361
552
|
)
|
|
362
|
-
|
|
363
|
-
|
|
553
|
+
|
|
554
|
+
query = _load_graphql_query("samples_search")
|
|
555
|
+
variables: dict = {
|
|
556
|
+
"query": str(description),
|
|
364
557
|
"limit": int(limit),
|
|
558
|
+
"order": "DESC",
|
|
559
|
+
"sort": "relevance",
|
|
560
|
+
"semantic": 1,
|
|
561
|
+
"rephrase": bool(rephrase),
|
|
562
|
+
"extract_filters": False,
|
|
563
|
+
"includeSubscriberOnlyResults": False,
|
|
564
|
+
"tags": [],
|
|
565
|
+
"tags_exclude": [],
|
|
566
|
+
"attributes": [],
|
|
567
|
+
"bundled_content_daws": [],
|
|
568
|
+
"legacy": True,
|
|
365
569
|
}
|
|
366
570
|
if bpm is not None:
|
|
367
|
-
|
|
571
|
+
variables["bpm"] = str(int(bpm))
|
|
368
572
|
if key:
|
|
369
|
-
|
|
370
|
-
|
|
573
|
+
variables["key"] = str(key)
|
|
574
|
+
|
|
575
|
+
body = {
|
|
576
|
+
"operationName": "SamplesSearch",
|
|
577
|
+
"variables": variables,
|
|
578
|
+
"query": query,
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
raw = await self._request("POST", self.config.describe_endpoint, body=body)
|
|
582
|
+
return _parse_samples_search(raw)
|
|
371
583
|
|
|
372
584
|
async def generate_variation(
|
|
373
585
|
self,
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
target_bpm: Optional[int] = None,
|
|
377
|
-
count: int = 1,
|
|
586
|
+
uuid: str,
|
|
587
|
+
is_legacy: bool = True,
|
|
378
588
|
) -> dict:
|
|
379
|
-
"""
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
589
|
+
"""Find catalog samples similar to a given sample ("Variations").
|
|
590
|
+
|
|
591
|
+
Captured 2026-04-22: Splice's "Variations" right-click menu item
|
|
592
|
+
fires the GraphQL `AssetSimilarSoundsQuery` with just `uuid` +
|
|
593
|
+
`isLegacy`. Returns up to 10 similar samples. The name
|
|
594
|
+
"generate_variation" is a slight misnomer — this is a
|
|
595
|
+
recommender lookup, not AI-synthesis of new audio — but it
|
|
596
|
+
matches Splice's user-facing "Variations" label.
|
|
597
|
+
|
|
598
|
+
uuid: the source sample's catalog uuid (as returned by
|
|
599
|
+
`splice_describe_sound` or gRPC `SearchSamples`)
|
|
600
|
+
is_legacy: match how Splice's own client sets it (true for
|
|
601
|
+
pre-catalog-v2 samples; leave as default)
|
|
602
|
+
|
|
603
|
+
Returns `{similar_samples: [...], count}` — each sample has the
|
|
604
|
+
same flat shape as `splice_describe_sound` items.
|
|
383
605
|
"""
|
|
384
|
-
if not self.config.
|
|
606
|
+
if not self.config.variation_verified:
|
|
385
607
|
raise SpliceHTTPError(
|
|
386
608
|
code="ENDPOINT_NOT_CONFIGURED",
|
|
387
609
|
message=(
|
|
388
|
-
"
|
|
389
|
-
"
|
|
390
|
-
"
|
|
391
|
-
"mitmproxy against the Sounds Plugin."
|
|
610
|
+
"Variation endpoint points at an unverified URL. "
|
|
611
|
+
"Reset config to defaults so the captured "
|
|
612
|
+
"surfaces-graphql.splice.com/graphql endpoint is used."
|
|
392
613
|
),
|
|
393
|
-
endpoint=self.config.variation_endpoint,
|
|
614
|
+
endpoint=f"{self.config.base_url}{self.config.variation_endpoint}",
|
|
394
615
|
)
|
|
395
|
-
|
|
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:
|
|
616
|
+
if not uuid or not isinstance(uuid, str):
|
|
415
617
|
raise SpliceHTTPError(
|
|
416
|
-
code="
|
|
417
|
-
message=
|
|
418
|
-
|
|
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,
|
|
618
|
+
code="INVALID_UUID",
|
|
619
|
+
message="uuid must be a non-empty string",
|
|
620
|
+
endpoint=self.config.variation_endpoint,
|
|
424
621
|
)
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
"
|
|
430
|
-
"
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
)
|
|
622
|
+
query = _load_graphql_query("asset_similar_sounds")
|
|
623
|
+
body = {
|
|
624
|
+
"operationName": "AssetSimilarSoundsQuery",
|
|
625
|
+
"variables": {
|
|
626
|
+
"uuid": uuid,
|
|
627
|
+
"isLegacy": bool(is_legacy),
|
|
628
|
+
},
|
|
629
|
+
"query": query,
|
|
630
|
+
}
|
|
631
|
+
raw = await self._request("POST", self.config.variation_endpoint, body=body)
|
|
632
|
+
return _parse_similar_sounds(raw)
|
|
633
|
+
|
|
634
|
+
# NOTE: `search_with_sound` method removed 2026-04-22. User does
|
|
635
|
+
# audio-reference search in-Splice manually. Capture recipe is at
|
|
636
|
+
# docs/2026-04-22-splice-https-capture-recipe.md if anyone wants to
|
|
637
|
+
# resurrect it.
|