fluncle 0.110.0 → 0.111.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 +219 -8
  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.110.0".trim() ? "0.110.0".trim() : "0.1.0";
560
+ currentVersion = "0.111.0".trim() ? "0.111.0".trim() : "0.1.0";
561
561
  });
562
562
 
563
563
  // src/update-notifier.ts
@@ -961,6 +961,7 @@ function mapTrack(track) {
961
961
  addedToSpotify: track.addedToSpotify,
962
962
  album: track.album,
963
963
  albumImageUrl: track.albumImageUrl,
964
+ analyzedFrom: track.analyzedFrom,
964
965
  artists: track.artists,
965
966
  bpm: track.bpm,
966
967
  durationMs: track.durationMs,
@@ -1673,6 +1674,7 @@ function mapTrack2(track) {
1673
1674
  addedToSpotify: track.addedToSpotify,
1674
1675
  album: track.album,
1675
1676
  albumImageUrl: track.albumImageUrl,
1677
+ analyzedFrom: track.analyzedFrom,
1676
1678
  artists: track.artists,
1677
1679
  bpm: track.bpm,
1678
1680
  durationMs: track.durationMs,
@@ -2207,7 +2209,7 @@ function parseVersion2(version) {
2207
2209
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2208
2210
  var init_version2 = __esm(() => {
2209
2211
  init_output();
2210
- currentVersion2 = "0.110.0".trim() ? "0.110.0".trim() : "0.1.0";
2212
+ currentVersion2 = "0.111.0".trim() ? "0.111.0".trim() : "0.1.0";
2211
2213
  });
2212
2214
 
2213
2215
  // ../../packages/registry/src/index.ts
@@ -2650,11 +2652,26 @@ var init_src = __esm(() => {
2650
2652
  url: `${SITE}/.well-known/agent-skills/index.json`,
2651
2653
  weights: { web: "tertiary" }
2652
2654
  },
2655
+ {
2656
+ apiFormat: "application/json+oembed",
2657
+ exposedContent: [
2658
+ "the oEmbed 1.0 provider — a pasted /log, /mixtapes, or /artist link unfurls as a rich finding card (Discord/Notion/WordPress/Ghost/…)",
2659
+ "html iframes the self-contained /embed/<logId> card; thumbnail is the finding's OG image"
2660
+ ],
2661
+ kind: "discovery",
2662
+ name: "discovery.oembed",
2663
+ operatorNotes: 'GET /oembed?url=<a fluncle.com /log|/mixtapes|/artist URL>&format=json. Discovered via the `<link rel="alternate" type="application/json+oembed">` on the /log, mixtape, and /artist heads. XML → 501 (JSON only). No probeConfig — a bare GET without a valid `url` param is a 404, so there is no fixed URL to health-probe (like web.artist). The `rich` html frames /embed/<logId> (apps/web/src/routes/embed.$logId.ts), a standalone dark card served with a permissive `frame-ancestors *` CSP scoped to that route. Source: apps/web/src/routes/oembed.ts + src/lib/oembed.ts.',
2664
+ route: "/oembed",
2665
+ url: `${SITE}/oembed`,
2666
+ weights: { web: "tertiary" }
2667
+ },
2653
2668
  {
2654
2669
  apiFormat: "application/json",
2655
2670
  discoveryUrl: `${SITE}/.well-known/mcp/server-card.json`,
2656
2671
  exposedContent: [
2657
- "the archive as MCP tools (Streamable HTTP, no auth): list_tracks, get_random_track, get_status, search_tracks, submit_track, subscribe_newsletter"
2672
+ "the archive as MCP tools (Streamable HTTP, no auth): list_tracks, get_track, get_random_track, get_status, search_tracks, submit_track, subscribe_newsletter",
2673
+ "the archive as MCP resources: each finding/mixtape at fluncle://finding/<logId> or fluncle://mixtape/<logId> (its public record)",
2674
+ "Fluncle-voiced MCP prompts: recommend_finding, walk_recent_night, decode_coordinate"
2658
2675
  ],
2659
2676
  kind: "mcp",
2660
2677
  name: "mcp.server",
@@ -3216,6 +3233,24 @@ async function trackUpdateCommand(trackId, options) {
3216
3233
  if (options.note !== undefined) {
3217
3234
  body.note = options.note;
3218
3235
  }
3236
+ if (options.bpmSource !== undefined) {
3237
+ body.bpmSource = options.bpmSource;
3238
+ }
3239
+ if (options.bpmConfidence !== undefined) {
3240
+ body.bpmConfidence = options.bpmConfidence;
3241
+ }
3242
+ if (options.keySource !== undefined) {
3243
+ body.keySource = options.keySource;
3244
+ }
3245
+ if (options.keyConfidence !== undefined) {
3246
+ body.keyConfidence = options.keyConfidence;
3247
+ }
3248
+ if (options.analyzedFrom !== undefined) {
3249
+ body.analyzedFrom = options.analyzedFrom;
3250
+ }
3251
+ if (options.analyzedAt !== undefined) {
3252
+ body.analyzedAt = options.analyzedAt;
3253
+ }
3219
3254
  return adminApiPatch(`/api/admin/tracks/${encodeURIComponent(trackId)}`, body);
3220
3255
  }
3221
3256
  async function trackRequeueVideoCommand(idOrLogId) {
@@ -3326,10 +3361,60 @@ var init_add = __esm(() => {
3326
3361
  init_api();
3327
3362
  });
3328
3363
 
3364
+ // src/commands/track.ts
3365
+ async function trackUpdateCommand2(trackId, options) {
3366
+ const body = {};
3367
+ if (options.bpm !== undefined) {
3368
+ body.bpm = options.bpm;
3369
+ }
3370
+ if (options.key !== undefined) {
3371
+ body.key = options.key;
3372
+ }
3373
+ if (options.videoUrl !== undefined) {
3374
+ body.videoUrl = options.videoUrl;
3375
+ }
3376
+ if (options.status !== undefined) {
3377
+ body.enrichmentStatus = options.status;
3378
+ }
3379
+ if (options.features !== undefined) {
3380
+ body.features = options.features;
3381
+ }
3382
+ if (options.embedding !== undefined) {
3383
+ body.embedding = options.embedding;
3384
+ }
3385
+ if (options.note !== undefined) {
3386
+ body.note = options.note;
3387
+ }
3388
+ if (options.bpmSource !== undefined) {
3389
+ body.bpmSource = options.bpmSource;
3390
+ }
3391
+ if (options.bpmConfidence !== undefined) {
3392
+ body.bpmConfidence = options.bpmConfidence;
3393
+ }
3394
+ if (options.keySource !== undefined) {
3395
+ body.keySource = options.keySource;
3396
+ }
3397
+ if (options.keyConfidence !== undefined) {
3398
+ body.keyConfidence = options.keyConfidence;
3399
+ }
3400
+ if (options.analyzedFrom !== undefined) {
3401
+ body.analyzedFrom = options.analyzedFrom;
3402
+ }
3403
+ if (options.analyzedAt !== undefined) {
3404
+ body.analyzedAt = options.analyzedAt;
3405
+ }
3406
+ return adminApiPatch(`/api/admin/tracks/${encodeURIComponent(trackId)}`, body);
3407
+ }
3408
+ var init_track2 = __esm(() => {
3409
+ init_api();
3410
+ init_output();
3411
+ });
3412
+
3329
3413
  // src/commands/admin-tracks.ts
3330
3414
  var exports_admin_tracks = {};
3331
3415
  __export(exports_admin_tracks, {
3332
3416
  vehiclesCommand: () => vehiclesCommand,
3417
+ requeueAnalysisCommand: () => requeueAnalysisCommand,
3333
3418
  queueCommand: () => queueCommand,
3334
3419
  observeQueueCommand: () => observeQueueCommand,
3335
3420
  noteQueueCommand: () => noteQueueCommand,
@@ -3429,6 +3514,44 @@ async function embedQueueCommand(limit) {
3429
3514
  async function captureQueueCommand(limit) {
3430
3515
  return fetchAdminTracks({ captureQueue: true, max: limit, order: "desc" });
3431
3516
  }
3517
+ async function requeueAnalysisCommand(options) {
3518
+ const findings = await fetchAdminTracks({ max: options.max, order: "asc" });
3519
+ const stale = findings.filter((track) => track.analyzedFrom !== "full");
3520
+ const rows = stale.map((track) => ({
3521
+ analyzedFrom: track.analyzedFrom,
3522
+ bpm: track.bpm,
3523
+ hasSourceAudio: Boolean(track.sourceAudioKey),
3524
+ key: track.key,
3525
+ logId: track.logId,
3526
+ title: track.title,
3527
+ trackId: track.trackId
3528
+ }));
3529
+ const withSourceAudio = rows.filter((row) => row.hasSourceAudio);
3530
+ const withoutSourceAudio = rows.filter((row) => !row.hasSourceAudio);
3531
+ const requeued = [];
3532
+ const failed = [];
3533
+ if (options.apply) {
3534
+ for (const row of rows) {
3535
+ try {
3536
+ await trackUpdateCommand2(row.trackId, { status: "pending" });
3537
+ requeued.push(row.trackId);
3538
+ } catch (error) {
3539
+ failed.push({
3540
+ error: error instanceof Error ? error.message : String(error),
3541
+ trackId: row.trackId
3542
+ });
3543
+ }
3544
+ }
3545
+ }
3546
+ return {
3547
+ applied: options.apply,
3548
+ failed,
3549
+ requeued,
3550
+ scanned: findings.length,
3551
+ withSourceAudio,
3552
+ withoutSourceAudio
3553
+ };
3554
+ }
3432
3555
  async function contextQueueCommand(limit, retryEmptyContext = false) {
3433
3556
  return fetchAdminTracks({ hasContext: false, max: limit, order: "asc", retryEmptyContext });
3434
3557
  }
@@ -3478,6 +3601,7 @@ var pageSize3 = 48;
3478
3601
  var init_admin_tracks = __esm(() => {
3479
3602
  init_api();
3480
3603
  init_recent2();
3604
+ init_track2();
3481
3605
  });
3482
3606
 
3483
3607
  // src/commands/preview-archive.ts
@@ -7611,6 +7735,10 @@ function addAdminCommands(program2) {
7611
7735
  const { captureQueueCommand: captureQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
7612
7736
  await runAdminCaptureQueue(options, captureQueueCommand2);
7613
7737
  });
7738
+ adminTracks.command("requeue-analysis").description("Re-queue findings whose BPM/key are preview-grade (dry-run; --apply to flip)").option("--apply", "Actually flip statuses (default is a dry-run preview)", false).option("--limit <limit>", "Cap the archive walk (default: the whole archive)").option("--json", "Print JSON", false).action(async (options) => {
7739
+ const { requeueAnalysisCommand: requeueAnalysisCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
7740
+ await runAdminRequeueAnalysis(options, requeueAnalysisCommand2);
7741
+ });
7614
7742
  adminTracks.command("vehicles").description("Recent video vehicles, newest first (the style ledger for diversity)").option("--limit <limit>", "Number of vehicles to show", "10").option("--json", "Print JSON", false).action(async (options) => {
7615
7743
  const { vehiclesCommand: vehiclesCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
7616
7744
  await runAdminVehicles(options, vehiclesCommand2);
@@ -7620,9 +7748,9 @@ function addAdminCommands(program2) {
7620
7748
  const { trackGetAdminCommand: trackGetAdminCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7621
7749
  await runTrackGetAdmin(idOrLogId, options, trackGetAdminCommand2);
7622
7750
  });
7623
- adminTrack.command("update").description("Certify a track into the archive").argument("[trackId]").option("--bpm <number>", "Track BPM").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("--note <text>", "Operator note").option("--status <status>", "Enrichment status").option("--video-url <url>", "Rendered video URL").allowExcessArguments().action(async (trackId, options) => {
7624
- const { trackUpdateCommand: trackUpdateCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7625
- await runTrackUpdate(trackId, options, trackUpdateCommand2);
7751
+ 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) => {
7752
+ const { trackUpdateCommand: trackUpdateCommand3 } = await Promise.resolve().then(() => (init_track(), exports_track));
7753
+ await runTrackUpdate(trackId, options, trackUpdateCommand3);
7626
7754
  });
7627
7755
  adminTrack.command("video").description("Upload a track's video bundle to R2 and link it").argument("[idOrLogId]").option("--allow-partial", "Allow an intentionally partial upload (skip the re-render-contract check; e.g. poster-only)", false).option("--composition <file>", "Composition source file").option("--cover <file>", "Cover image").option("--dir <dir>", "Bundle directory").option("--footage <file>", "Video footage (square crop source)").option("--footage-landscape <file>", "Clean landscape cut (optional escape hatch)").option("--footage-landscape-social <file>", "Landscape cut with baked text (optional)").option("--footage-notext <file>", "Portrait cut without the type layer (optional)").option("--footage-social <file>", "Portrait social cut (baked text)").option("--intent <file>", "Render-intent JSON (optional)").option("--json", "Print JSON", false).option("--metrics <file>", "Gate metrics JSON (optional)").option("--model <model>", "Authoring AI model (<provider>/<model>)").option("--note <file>", "Note file").option("--plate <file>", "Plate-lane photographic plate (plate.png; uploadable pre-render)").option("--plate-background <file>", "The plate's subject-removed background (plate.background.png, optional)").option("--poster <file>", "Poster image").option("--props <file>", "Render props JSON").option("--reasoning <level>", "Authoring model reasoning effort (e.g. high)").option("--render <file>", "Render metadata JSON").option("--scene <file>", "Scene replay manifest JSON (fluncle.scene/1, optional)").allowExcessArguments().action(async (idOrLogId, options) => {
7628
7756
  const { trackVideoCommand: trackVideoCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
@@ -8437,7 +8565,7 @@ function parseEmbeddingArg(raw) {
8437
8565
  }
8438
8566
  return parsed;
8439
8567
  }
8440
- async function runTrackUpdate(trackId, options, trackUpdateCommand2) {
8568
+ async function runTrackUpdate(trackId, options, trackUpdateCommand3) {
8441
8569
  if (!trackId) {
8442
8570
  throw new Error("Missing track id. Usage: fluncle admin tracks update <track_id>");
8443
8571
  }
@@ -8445,13 +8573,30 @@ async function runTrackUpdate(trackId, options, trackUpdateCommand2) {
8445
8573
  if (bpm !== undefined && !Number.isFinite(bpm)) {
8446
8574
  throw new Error(`Invalid --bpm: ${options.bpm}`);
8447
8575
  }
8576
+ const bpmConfidence = options.bpmConfidence === undefined ? undefined : Number(options.bpmConfidence);
8577
+ if (bpmConfidence !== undefined && !Number.isFinite(bpmConfidence)) {
8578
+ throw new Error(`Invalid --bpm-confidence: ${options.bpmConfidence}`);
8579
+ }
8580
+ const keyConfidence = options.keyConfidence === undefined ? undefined : Number(options.keyConfidence);
8581
+ if (keyConfidence !== undefined && !Number.isFinite(keyConfidence)) {
8582
+ throw new Error(`Invalid --key-confidence: ${options.keyConfidence}`);
8583
+ }
8584
+ if (options.analyzedFrom !== undefined && options.analyzedFrom !== "full" && options.analyzedFrom !== "preview") {
8585
+ throw new Error(`Invalid --analyzed-from: ${options.analyzedFrom} (expected full or preview)`);
8586
+ }
8448
8587
  const embeddingRaw = options.embeddingFile ? readFileSync5(options.embeddingFile, "utf8") : options.embedding;
8449
8588
  const embedding = parseEmbeddingArg(embeddingRaw);
8450
- const result = await trackUpdateCommand2(trackId, {
8589
+ const result = await trackUpdateCommand3(trackId, {
8590
+ analyzedAt: options.analyzedAt,
8591
+ analyzedFrom: options.analyzedFrom,
8451
8592
  bpm,
8593
+ bpmConfidence,
8594
+ bpmSource: options.bpmSource,
8452
8595
  embedding,
8453
8596
  features: options.features,
8454
8597
  key: options.key,
8598
+ keyConfidence,
8599
+ keySource: options.keySource,
8455
8600
  note: options.note,
8456
8601
  status: options.status,
8457
8602
  videoUrl: options.videoUrl
@@ -8979,6 +9124,66 @@ async function runAdminCaptureQueue(options, captureQueueCommand2) {
8979
9124
  console.log(trackRows2(tracks).join(`
8980
9125
  `));
8981
9126
  }
9127
+ var REQUEUE_ANALYSIS_ARCHIVE_CAP = 1e6;
9128
+ function parseRequeueAnalysisLimit(value) {
9129
+ if (value === undefined) {
9130
+ return REQUEUE_ANALYSIS_ARCHIVE_CAP;
9131
+ }
9132
+ const limit = Number.parseInt(value, 10);
9133
+ if (!Number.isInteger(limit) || limit < 1) {
9134
+ throw new Error("Limit must be a positive integer");
9135
+ }
9136
+ return limit;
9137
+ }
9138
+ async function runAdminRequeueAnalysis(options, requeueAnalysisCommand2) {
9139
+ const max = parseRequeueAnalysisLimit(options.limit);
9140
+ const result = await requeueAnalysisCommand2({ apply: options.apply === true, max });
9141
+ if (options.json) {
9142
+ printJson({ ok: true, ...result });
9143
+ return;
9144
+ }
9145
+ const { coordinate: coordinate3 } = await Promise.resolve().then(() => (init_format2(), exports_format));
9146
+ const staleCount = result.withSourceAudio.length + result.withoutSourceAudio.length;
9147
+ if (staleCount === 0) {
9148
+ console.log("Nothing to re-queue. Every finding's BPM/key was analyzed from full audio.");
9149
+ return;
9150
+ }
9151
+ const describe = (row) => {
9152
+ const coord = coordinate3({ logId: row.logId });
9153
+ const bpm = row.bpm ? `${row.bpm} bpm` : "no bpm";
9154
+ const key = row.key ?? "no key";
9155
+ const from = row.analyzedFrom ?? "legacy/null";
9156
+ return ` ${coord} ${row.trackId} ${row.title} \u2014 ${bpm}, ${key} (from ${from})`;
9157
+ };
9158
+ console.log(`${staleCount} finding${staleCount === 1 ? "" : "s"} with preview-grade BPM/key (scanned ${result.scanned}):`);
9159
+ if (result.withSourceAudio.length > 0) {
9160
+ console.log(`
9161
+ Captured full song on file \u2014 re-derives from full audio (${result.withSourceAudio.length}):`);
9162
+ console.log(result.withSourceAudio.map(describe).join(`
9163
+ `));
9164
+ }
9165
+ if (result.withoutSourceAudio.length > 0) {
9166
+ console.log(`
9167
+ No captured full song \u2014 re-derives from the 30s preview, still an upgrade with the fixed estimator (${result.withoutSourceAudio.length}):`);
9168
+ console.log(result.withoutSourceAudio.map(describe).join(`
9169
+ `));
9170
+ }
9171
+ console.log(`
9172
+ Caveat: keys backfilled from Rekordbox are indistinguishable from DSP keys (no legacy` + " provenance) and a re-enrichment may overwrite them. This dry-run diff is the gate \u2014" + " review it before applying.");
9173
+ if (!result.applied) {
9174
+ console.log(`
9175
+ Dry-run \u2014 nothing flipped. Re-run with --apply to re-queue all ${staleCount}.`);
9176
+ return;
9177
+ }
9178
+ console.log(`
9179
+ Applied \u2014 re-queued ${result.requeued.length} of ${staleCount}.`);
9180
+ if (result.failed.length > 0) {
9181
+ console.log(`${result.failed.length} failed:`);
9182
+ console.log(result.failed.map((entry) => ` ${entry.trackId}: ${entry.error}`).join(`
9183
+ `));
9184
+ process.exitCode = 1;
9185
+ }
9186
+ }
8982
9187
  async function runAdminVehicles(options, vehiclesCommand2) {
8983
9188
  const limit = parseListLimit(options.limit);
8984
9189
  const vehicles = await vehiclesCommand2(limit);
@@ -9203,7 +9408,11 @@ function normalizeCommanderError(error) {
9203
9408
  return new Error(message);
9204
9409
  }
9205
9410
  var stringOptions = new Set([
9411
+ "--analyzed-at",
9412
+ "--analyzed-from",
9206
9413
  "--bpm",
9414
+ "--bpm-confidence",
9415
+ "--bpm-source",
9207
9416
  "--composition",
9208
9417
  "--cover",
9209
9418
  "--dir",
@@ -9218,6 +9427,8 @@ var stringOptions = new Set([
9218
9427
  "--from",
9219
9428
  "--intent",
9220
9429
  "--key",
9430
+ "--key-confidence",
9431
+ "--key-source",
9221
9432
  "--limit",
9222
9433
  "--metrics",
9223
9434
  "--mime",
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.110.0"
34
+ "version": "0.111.0"
35
35
  }