fluncle 0.113.0 → 0.115.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.
Files changed (2) hide show
  1. package/bin/fluncle.mjs +179 -3
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -557,7 +557,7 @@ function parseVersion(version) {
557
557
  var currentVersion;
558
558
  var init_version = __esm(() => {
559
559
  init_output();
560
- currentVersion = "0.113.0".trim() ? "0.113.0".trim() : "0.1.0";
560
+ currentVersion = "0.115.0".trim() ? "0.115.0".trim() : "0.1.0";
561
561
  });
562
562
 
563
563
  // src/update-notifier.ts
@@ -1042,6 +1042,48 @@ var init_artists = __esm(() => {
1042
1042
  init_api();
1043
1043
  });
1044
1044
 
1045
+ // src/commands/galaxies.ts
1046
+ var exports_galaxies = {};
1047
+ __export(exports_galaxies, {
1048
+ galaxyMapWriteCommand: () => galaxyMapWriteCommand,
1049
+ galaxyMapReadCommand: () => galaxyMapReadCommand,
1050
+ galaxyGetCommand: () => galaxyGetCommand,
1051
+ galaxyEmbeddingsCommand: () => galaxyEmbeddingsCommand,
1052
+ galaxiesListCommand: () => galaxiesListCommand
1053
+ });
1054
+ async function galaxiesListCommand() {
1055
+ const response = await publicApiGet("/api/v1/galaxies");
1056
+ return response.galaxies;
1057
+ }
1058
+ async function galaxyGetCommand(slug) {
1059
+ const response = await publicApiGet(`/api/v1/galaxies/${encodeURIComponent(slug)}`);
1060
+ return { findings: response.findings, galaxy: response.galaxy };
1061
+ }
1062
+ async function galaxyMapReadCommand() {
1063
+ const response = await adminApiGet("/api/admin/galaxies");
1064
+ return response.galaxies;
1065
+ }
1066
+ async function galaxyEmbeddingsCommand(options) {
1067
+ const params = new URLSearchParams;
1068
+ if (options.cursor) {
1069
+ params.set("cursor", options.cursor);
1070
+ }
1071
+ if (options.limit) {
1072
+ params.set("limit", options.limit);
1073
+ }
1074
+ const query = params.toString();
1075
+ return adminApiGet(`/api/admin/tracks/embeddings${query ? `?${query}` : ""}`);
1076
+ }
1077
+ async function galaxyMapWriteCommand(clusters) {
1078
+ const response = await adminApiPut("/api/admin/galaxies/map", {
1079
+ clusters
1080
+ });
1081
+ return response.galaxies;
1082
+ }
1083
+ var init_galaxies = __esm(() => {
1084
+ init_api();
1085
+ });
1086
+
1045
1087
  // src/commands/mixtape-api.ts
1046
1088
  async function mixtapeListCommand() {
1047
1089
  const response = await adminApiGet("/api/admin/mixtapes");
@@ -2209,7 +2251,7 @@ function parseVersion2(version) {
2209
2251
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2210
2252
  var init_version2 = __esm(() => {
2211
2253
  init_output();
2212
- currentVersion2 = "0.113.0".trim() ? "0.113.0".trim() : "0.1.0";
2254
+ currentVersion2 = "0.115.0".trim() ? "0.115.0".trim() : "0.1.0";
2213
2255
  });
2214
2256
 
2215
2257
  // ../../packages/registry/src/index.ts
@@ -2265,6 +2307,18 @@ var init_src = __esm(() => {
2265
2307
  url: `${SITE}/mixtapes`,
2266
2308
  weights: { ssh: "secondary", web: "primary" }
2267
2309
  },
2310
+ {
2311
+ exposedContent: [
2312
+ "the mixability set-builder — chain a set from Fluncle's findings that mixes clean, share it as a link"
2313
+ ],
2314
+ kind: "web_route",
2315
+ name: "web.mix",
2316
+ operatorNotes: "ADMIN-GATED at launch (RFC mixability-engine, Decision 1): the full /mix set-builder ships behind admin auth until the archive clears the ~250-finding floor. Registered here as `pending` (dark to every public consumer — the dev-row, llms.txt, the sitemap, /status) so going public is one flip: remove `pending` + set a web weight + announce. The `list_mixable_tracks` API op already ships at its final public tier, so the flip needs no rebuild. No probeConfig — the admin gate 302s a bare GET to /admin/login, which would read as a false 'down'.",
2317
+ pending: true,
2318
+ route: "/mix",
2319
+ url: `${SITE}/mix`,
2320
+ weights: { web: "secondary" }
2321
+ },
2268
2322
  {
2269
2323
  exposedContent: [
2270
2324
  "the feed-first Stories surface — full-bleed findings",
@@ -3258,6 +3312,9 @@ async function trackUpdateCommand(trackId, options) {
3258
3312
  if (options.embedding !== undefined) {
3259
3313
  body.embedding = options.embedding;
3260
3314
  }
3315
+ if (options.galaxyId !== undefined) {
3316
+ body.galaxyId = options.galaxyId;
3317
+ }
3261
3318
  if (options.note !== undefined) {
3262
3319
  body.note = options.note;
3263
3320
  }
@@ -3450,6 +3507,9 @@ async function trackUpdateCommand2(trackId, options) {
3450
3507
  if (options.embedding !== undefined) {
3451
3508
  body.embedding = options.embedding;
3452
3509
  }
3510
+ if (options.galaxyId !== undefined) {
3511
+ body.galaxyId = options.galaxyId;
3512
+ }
3453
3513
  if (options.note !== undefined) {
3454
3514
  body.note = options.note;
3455
3515
  }
@@ -3486,6 +3546,7 @@ __export(exports_admin_tracks, {
3486
3546
  queueCommand: () => queueCommand,
3487
3547
  observeQueueCommand: () => observeQueueCommand,
3488
3548
  noteQueueCommand: () => noteQueueCommand,
3549
+ mixableOrderCommand: () => mixableOrderCommand,
3489
3550
  listCommand: () => listCommand,
3490
3551
  enrichQueueCommand: () => enrichQueueCommand,
3491
3552
  embedQueueCommand: () => embedQueueCommand,
@@ -3665,6 +3726,13 @@ async function vehiclesCommand(limit) {
3665
3726
  vehicle: track.videoVehicle
3666
3727
  }));
3667
3728
  }
3729
+ async function mixableOrderCommand(logIds, seed) {
3730
+ const params = new URLSearchParams({ ids: logIds.join(",") });
3731
+ if (seed) {
3732
+ params.set("seed", seed);
3733
+ }
3734
+ return adminApiGet(`/api/admin/tracks/mixable-order?${params.toString()}`);
3735
+ }
3668
3736
  var pageSize3 = 48;
3669
3737
  var init_admin_tracks = __esm(() => {
3670
3738
  init_api();
@@ -7748,6 +7816,10 @@ function addListenCommands(program2) {
7748
7816
  const { artistsListCommand: artistsListCommand2, artistsGetCommand: artistsGetCommand2 } = await Promise.resolve().then(() => (init_artists(), exports_artists));
7749
7817
  await runArtists(slug, options, { artistsGetCommand: artistsGetCommand2, artistsListCommand: artistsListCommand2 });
7750
7818
  });
7819
+ program2.command("galaxies").description("Wander Fluncle's sonic galaxies").argument("[slug]", "Galaxy slug (omit for the full map)").option("--json", "Print JSON", false).action(async (slug, options) => {
7820
+ const { galaxiesListCommand: galaxiesListCommand2, galaxyGetCommand: galaxyGetCommand2 } = await Promise.resolve().then(() => (init_galaxies(), exports_galaxies));
7821
+ await runGalaxies(slug, options, { galaxiesListCommand: galaxiesListCommand2, galaxyGetCommand: galaxyGetCommand2 });
7822
+ });
7751
7823
  program2.command("mixtapes").description("Fluncle's checkpoint sets").option("--json", "Print JSON", false).action(async (options) => {
7752
7824
  const { mixtapesCommand: mixtapesCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
7753
7825
  await runMixtapes(options, mixtapesCommand2);
@@ -7877,7 +7949,11 @@ function addAdminCommands(program2) {
7877
7949
  const { trackGetAdminCommand: trackGetAdminCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7878
7950
  await runTrackGetAdmin(idOrLogId, options, trackGetAdminCommand2);
7879
7951
  });
7880
- adminTrack.command("update").description("Certify a track into the archive").argument("[trackId]").option("--analyzed-at <iso>", "Analysis-write ISO timestamp (BPM/key provenance)").option("--analyzed-from <class>", "Audio class BPM/key were analyzed from: full or preview").option("--bpm <number>", "Track BPM").option("--bpm-confidence <number>", "Analyzer confidence in the BPM (0..1)").option("--bpm-source <source>", "Where the BPM came from (analyzer bpmSource)").option("--embedding <json>", "MuQ audio embedding as a JSON array of 1024 floats").option("--embedding-file <file>", "Read the MuQ embedding JSON array from a file").option("--features <json>", "Audio feature JSON").option("--json", "Print JSON", false).option("--key <key>", "Musical key").option("--key-confidence <number>", "Analyzer confidence in the key (0..1)").option("--key-source <source>", "Where the key came from (analyzer keySource)").option("--note <text>", "Operator note").option("--status <status>", "Enrichment status").option("--video-url <url>", "Rendered video URL").allowExcessArguments().action(async (trackId, options) => {
7952
+ adminTrack.command("mixable-order").description("Order findings into a smooth proposed mix (2\u201364 Log IDs)").argument("[logIds...]", "The findings to order, by Log ID").option("--seed <logId>", "Pin the first stop").option("--json", "Print JSON", false).allowExcessArguments().action(async (logIds, options) => {
7953
+ const { mixableOrderCommand: mixableOrderCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
7954
+ await runMixableOrder(logIds, options, mixableOrderCommand2);
7955
+ });
7956
+ adminTrack.command("update").description("Certify a track into the archive").argument("[trackId]").option("--analyzed-at <iso>", "Analysis-write ISO timestamp (BPM/key provenance)").option("--analyzed-from <class>", "Audio class BPM/key were analyzed from: full or preview").option("--bpm <number>", "Track BPM").option("--bpm-confidence <number>", "Analyzer confidence in the BPM (0..1)").option("--bpm-source <source>", "Where the BPM came from (analyzer bpmSource)").option("--embedding <json>", "MuQ audio embedding as a JSON array of 1024 floats").option("--embedding-file <file>", "Read the MuQ embedding JSON array from a file").option("--features <json>", "Audio feature JSON").option("--galaxy-id <id>", "Sonic galaxy assignment (empty string clears it)").option("--json", "Print JSON", false).option("--key <key>", "Musical key").option("--key-confidence <number>", "Analyzer confidence in the key (0..1)").option("--key-source <source>", "Where the key came from (analyzer keySource)").option("--note <text>", "Operator note").option("--status <status>", "Enrichment status").option("--video-url <url>", "Rendered video URL").allowExcessArguments().action(async (trackId, options) => {
7881
7957
  const { trackUpdateCommand: trackUpdateCommand3 } = await Promise.resolve().then(() => (init_track(), exports_track));
7882
7958
  await runTrackUpdate(trackId, options, trackUpdateCommand3);
7883
7959
  });
@@ -8119,6 +8195,22 @@ function addAdminCommands(program2) {
8119
8195
  artists.action(() => {
8120
8196
  artists.outputHelp();
8121
8197
  });
8198
+ const galaxies = configureCommand(admin.command("galaxies").description("Sonic galaxy map (cluster sweep) commands"));
8199
+ galaxies.action(() => {
8200
+ galaxies.outputHelp();
8201
+ });
8202
+ galaxies.command("map").description("Read the full galaxy map (named + unnamed + retired, with centroids)").option("--json", "Print JSON", false).action(async (options) => {
8203
+ const { galaxyMapReadCommand: galaxyMapReadCommand2 } = await Promise.resolve().then(() => (init_galaxies(), exports_galaxies));
8204
+ await runGalaxyMapRead(options, galaxyMapReadCommand2);
8205
+ });
8206
+ galaxies.command("embeddings").description("Read a cursor page of the embedded corpus (the cluster engine's input)").option("--cursor <cursor>", "Opaque cursor from a prior page's nextCursor").option("--limit <limit>", "Page size (default 200, clamped to 500)").option("--json", "Print JSON", false).action(async (options) => {
8207
+ const { galaxyEmbeddingsCommand: galaxyEmbeddingsCommand2 } = await Promise.resolve().then(() => (init_galaxies(), exports_galaxies));
8208
+ await runGalaxyEmbeddings(options, galaxyEmbeddingsCommand2);
8209
+ });
8210
+ galaxies.command("set-map").description("Write the galaxy map transactionally (mint new ids, upsert centroids, retire)").requiredOption("--file <file>", "Path to the clusters JSON ({ clusters: [...] } or a bare array)").option("--json", "Print JSON", false).action(async (options) => {
8211
+ const { galaxyMapWriteCommand: galaxyMapWriteCommand2 } = await Promise.resolve().then(() => (init_galaxies(), exports_galaxies));
8212
+ await runGalaxyMapWrite(options, galaxyMapWriteCommand2);
8213
+ });
8122
8214
  const backfill = configureCommand(admin.command("backfills").description("Backfill operator-only archives"));
8123
8215
  backfill.action(() => {
8124
8216
  backfill.outputHelp();
@@ -8684,6 +8776,24 @@ async function runTrackGet(idOrLogId, options, trackGetCommand2) {
8684
8776
  console.log(`Log: ${t.logPageUrl}`);
8685
8777
  }
8686
8778
  }
8779
+ async function runMixableOrder(logIds, options, mixableOrderCommand2) {
8780
+ if (logIds.length < 2) {
8781
+ throw new Error("Give 2 or more Log IDs. Usage: fluncle admin tracks mixable-order <logId...> [--seed <logId>] [--json]");
8782
+ }
8783
+ const result = await mixableOrderCommand2(logIds, options.seed);
8784
+ if (options.json) {
8785
+ printJson(result);
8786
+ return;
8787
+ }
8788
+ console.log(`Proposed mix \u2014 a smooth chain (${result.algorithm}), not an energy-shaped set. Copy into Rekordbox as advisory.
8789
+ `);
8790
+ result.order.forEach((stop, index) => {
8791
+ const into = index === 0 ? "start" : stop.flagged ? "sparse join" : stop.transitionReason?.relationship.replace(/_/g, " ") ?? "\u2014";
8792
+ const meta = [stop.key, stop.bpm ? `${Math.round(stop.bpm)} bpm` : undefined].filter(Boolean).join(" \xB7 ");
8793
+ console.log(`${String(index + 1).padStart(2)}. ${stop.logId} ${stop.artists.join(", ")} \u2014 ${stop.title}`);
8794
+ console.log(` ${[into, meta].filter(Boolean).join(" \xB7 ")}`);
8795
+ });
8796
+ }
8687
8797
  async function runTrackGetAdmin(idOrLogId, options, trackGetAdminCommand2) {
8688
8798
  if (!idOrLogId) {
8689
8799
  throw new Error("Missing id. Usage: fluncle admin tracks get <track_id|log_id> [--json]");
@@ -8750,6 +8860,7 @@ async function runTrackUpdate(trackId, options, trackUpdateCommand3) {
8750
8860
  bpmSource: options.bpmSource,
8751
8861
  embedding,
8752
8862
  features: options.features,
8863
+ galaxyId: options.galaxyId,
8753
8864
  key: options.key,
8754
8865
  keyConfidence,
8755
8866
  keySource: options.keySource,
@@ -9113,6 +9224,71 @@ async function runArtists(slug, options, commands) {
9113
9224
  console.log(`${a.name.padEnd(40)} ${String(a.findingCount).padStart(3)} finding${a.findingCount === 1 ? "" : "s"}`);
9114
9225
  }
9115
9226
  }
9227
+ async function runGalaxies(slug, options, commands) {
9228
+ if (slug) {
9229
+ const { findings, galaxy } = await commands.galaxyGetCommand(slug);
9230
+ if (options.json) {
9231
+ printJson({ findings, galaxy, ok: true });
9232
+ return;
9233
+ }
9234
+ console.log(`${galaxy.name} (${galaxy.slug})`);
9235
+ console.log(`${galaxy.memberCount} finding${galaxy.memberCount === 1 ? "" : "s"}`);
9236
+ for (const finding of findings) {
9237
+ const coordinate3 = finding.logId ? `${finding.logId} ` : "";
9238
+ console.log(` ${coordinate3}${finding.artists.join(", ")} \u2014 ${finding.title}`);
9239
+ }
9240
+ return;
9241
+ }
9242
+ const galaxies = await commands.galaxiesListCommand();
9243
+ if (options.json) {
9244
+ printJson({ galaxies, ok: true });
9245
+ return;
9246
+ }
9247
+ if (galaxies.length === 0) {
9248
+ console.log("No galaxies named yet. Quiet map tonight.");
9249
+ return;
9250
+ }
9251
+ for (const galaxy of galaxies) {
9252
+ console.log(`${galaxy.name.padEnd(40)} ${String(galaxy.memberCount).padStart(3)} finding${galaxy.memberCount === 1 ? "" : "s"}`);
9253
+ }
9254
+ }
9255
+ async function runGalaxyMapRead(options, galaxyMapReadCommand2) {
9256
+ const galaxies = await galaxyMapReadCommand2();
9257
+ if (options.json) {
9258
+ printJson({ galaxies, ok: true });
9259
+ return;
9260
+ }
9261
+ if (galaxies.length === 0) {
9262
+ console.log("No galaxies on the map yet.");
9263
+ return;
9264
+ }
9265
+ for (const galaxy of galaxies) {
9266
+ const name = galaxy.name ?? "(unnamed)";
9267
+ const retired = galaxy.retiredAt ? " [retired]" : "";
9268
+ console.log(`${galaxy.id} ${galaxy.handle.padEnd(30)} ${name.padEnd(24)} ${String(galaxy.memberCount).padStart(4)} members${retired}`);
9269
+ }
9270
+ }
9271
+ async function runGalaxyEmbeddings(options, galaxyEmbeddingsCommand2) {
9272
+ const result = await galaxyEmbeddingsCommand2({ cursor: options.cursor, limit: options.limit });
9273
+ if (options.json) {
9274
+ printJson(result);
9275
+ return;
9276
+ }
9277
+ console.log(`${result.embeddings.length} embedding(s)${result.nextCursor ? ` \xB7 nextCursor: ${result.nextCursor}` : " \xB7 end of corpus"}`);
9278
+ }
9279
+ async function runGalaxyMapWrite(options, galaxyMapWriteCommand2) {
9280
+ const raw = JSON.parse(readFileSync6(options.file, "utf8"));
9281
+ const clusters = Array.isArray(raw) || raw === null ? raw : raw.clusters;
9282
+ if (!Array.isArray(clusters)) {
9283
+ throw new Error("Invalid --file: expected { clusters: [...] } or a JSON array of cluster rows");
9284
+ }
9285
+ const galaxies = await galaxyMapWriteCommand2(clusters);
9286
+ if (options.json) {
9287
+ printJson({ galaxies, ok: true });
9288
+ return;
9289
+ }
9290
+ console.log(`Wrote ${clusters.length} cluster(s); the map now holds ${galaxies.length} galaxy(ies).`);
9291
+ }
9116
9292
  async function runMixtapes(options, mixtapesCommand2) {
9117
9293
  const mixtapes = await mixtapesCommand2();
9118
9294
  if (options.json) {
package/package.json CHANGED
@@ -31,5 +31,5 @@
31
31
  "url": "git+https://github.com/mauricekleine/fluncle.git"
32
32
  },
33
33
  "type": "module",
34
- "version": "0.113.0"
34
+ "version": "0.115.0"
35
35
  }