fluncle 0.74.0 → 0.76.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 +134 -21
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -462,6 +462,26 @@ function versionMatches(findingTitle, candidateTitle) {
462
462
  }
463
463
  return !candidateIsRemix;
464
464
  }
465
+ function trackLabel(artists, title) {
466
+ const joined = artists.join(", ");
467
+ return joined && title ? `${joined} — ${title}` : joined || title;
468
+ }
469
+ function resolveClipTracks(options) {
470
+ const { inMs, members, outMs, setDurationMs } = options;
471
+ const cued = members.filter((member) => member.startMs != null).sort((a, b) => a.startMs - b.startMs);
472
+ if (cued.length === 0) {
473
+ return [];
474
+ }
475
+ const lastIndex = cued.length - 1;
476
+ return cued.filter((member, index) => {
477
+ const lo = index === 0 ? Math.min(member.startMs, inMs) : member.startMs;
478
+ const hi = index === lastIndex ? Math.max(setDurationMs, outMs) : cued[index + 1]?.startMs ?? Number.POSITIVE_INFINITY;
479
+ return lo < outMs && inMs < hi;
480
+ }).map((member) => ({
481
+ label: trackLabel(member.artists, member.title),
482
+ startMs: member.startMs
483
+ }));
484
+ }
465
485
  function baseTitleMatches(findingTitle, candidateTitle) {
466
486
  const want = new Set(tokenize(stripVersionSuffix(findingTitle)));
467
487
  const have = new Set(tokenize(stripVersionSuffix(candidateTitle)));
@@ -524,7 +544,7 @@ function parseVersion(version) {
524
544
  var currentVersion;
525
545
  var init_version = __esm(() => {
526
546
  init_output();
527
- currentVersion = "0.74.0".trim() ? "0.74.0".trim() : "0.1.0";
547
+ currentVersion = "0.76.0".trim() ? "0.76.0".trim() : "0.1.0";
528
548
  });
529
549
 
530
550
  // src/update-notifier.ts
@@ -2381,7 +2401,7 @@ function parseVersion2(version) {
2381
2401
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2382
2402
  var init_version2 = __esm(() => {
2383
2403
  init_output();
2384
- currentVersion2 = "0.74.0".trim() ? "0.74.0".trim() : "0.1.0";
2404
+ currentVersion2 = "0.76.0".trim() ? "0.76.0".trim() : "0.1.0";
2385
2405
  });
2386
2406
 
2387
2407
  // ../../packages/registry/src/index.ts
@@ -3001,7 +3021,7 @@ var init_src = __esm(() => {
3001
3021
  ],
3002
3022
  kind: "cron",
3003
3023
  name: "cron.healthcheck",
3004
- operatorNotes: "every 10m. Pure probing, zero LLM tokens. POSTs to the agent-tier record_health op that /status reads.",
3024
+ operatorNotes: "every 10m, run by a rave-02 host systemd timer (docs/agents/hermes/healthcheck-timer/) — decoupled from the Hermes cron gateway so the prober isn't starved by the scheduler it monitors. Pure probing, zero LLM tokens. POSTs to the agent-tier record_health op that /status reads.",
3005
3025
  probeConfig: { cadenceMs: 10 * MINUTE_MS, cronName: "fluncle-healthcheck", kind: "cron" },
3006
3026
  weights: { status: "hidden" }
3007
3027
  },
@@ -3975,47 +3995,132 @@ var init_mixtapes2 = __esm(() => {
3975
3995
  var exports_clips = {};
3976
3996
  __export(exports_clips, {
3977
3997
  setVideoUrl: () => setVideoUrl,
3998
+ resolveClipSansFontFile: () => resolveClipSansFontFile,
3999
+ resolveClipFontFile: () => resolveClipFontFile,
3978
4000
  escapeDrawtextValue: () => escapeDrawtextValue,
3979
4001
  clipsListCommand: () => clipsListCommand,
3980
4002
  clipFootageKey: () => clipFootageKey,
3981
- clipCutVideoFilter: () => clipCutVideoFilter,
4003
+ clipCutFilterComplex: () => clipCutFilterComplex,
3982
4004
  clipCutFfmpegArgs: () => clipCutFfmpegArgs,
3983
4005
  clipCutCommand: () => clipCutCommand,
4006
+ brandDrawtext: () => brandDrawtext,
3984
4007
  MAX_CLIP_BYTES: () => MAX_CLIP_BYTES,
3985
4008
  CLIP_WIDTH: () => CLIP_WIDTH,
4009
+ CLIP_TITLE_SIZE: () => CLIP_TITLE_SIZE,
4010
+ CLIP_TITLE_COLOR: () => CLIP_TITLE_COLOR,
4011
+ CLIP_SHARP_BORDERW: () => CLIP_SHARP_BORDERW,
4012
+ CLIP_SAFE_BOTTOM: () => CLIP_SAFE_BOTTOM,
3986
4013
  CLIP_MAXRATE: () => CLIP_MAXRATE,
4014
+ CLIP_MARGIN_X: () => CLIP_MARGIN_X,
4015
+ CLIP_LINE_GAP: () => CLIP_LINE_GAP,
3987
4016
  CLIP_HEIGHT: () => CLIP_HEIGHT,
4017
+ CLIP_HALO_FEATHER_SIGMA: () => CLIP_HALO_FEATHER_SIGMA,
4018
+ CLIP_HALO_CORE_SIGMA: () => CLIP_HALO_CORE_SIGMA,
4019
+ CLIP_HALO_COLOR: () => CLIP_HALO_COLOR,
3988
4020
  CLIP_CRF: () => CLIP_CRF,
4021
+ CLIP_COORDINATE_SIZE: () => CLIP_COORDINATE_SIZE,
4022
+ CLIP_COORDINATE_COLOR: () => CLIP_COORDINATE_COLOR,
3989
4023
  CLIP_BUFSIZE: () => CLIP_BUFSIZE,
3990
- CLIP_AUDIO_BITRATE: () => CLIP_AUDIO_BITRATE
4024
+ CLIP_AUDIO_BITRATE: () => CLIP_AUDIO_BITRATE,
4025
+ BOX_CLIP_SANS_FONT_FILE: () => BOX_CLIP_SANS_FONT_FILE,
4026
+ BOX_CLIP_FONT_FILE: () => BOX_CLIP_FONT_FILE
3991
4027
  });
3992
4028
  import { randomUUID as randomUUID2 } from "node:crypto";
3993
- import { rmSync as rmSync3, statSync as statSync4 } from "node:fs";
4029
+ import { existsSync as existsSync4, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
3994
4030
  import { tmpdir as tmpdir2 } from "node:os";
3995
4031
  import { join as join5 } from "node:path";
3996
4032
  function clipFootageKey(clipId) {
3997
4033
  return `${clipId}/footage.mp4`;
3998
4034
  }
4035
+ function resolveFontFile(envVar, bakedPath) {
4036
+ const explicit = process.env[envVar];
4037
+ if (explicit) {
4038
+ return explicit;
4039
+ }
4040
+ return existsSync4(bakedPath) ? bakedPath : undefined;
4041
+ }
4042
+ function resolveClipFontFile() {
4043
+ return resolveFontFile("CLIP_FONT_FILE", BOX_CLIP_FONT_FILE);
4044
+ }
4045
+ function resolveClipSansFontFile() {
4046
+ return resolveFontFile("CLIP_SANS_FONT_FILE", BOX_CLIP_SANS_FONT_FILE);
4047
+ }
3999
4048
  function setVideoUrl(logId) {
4000
4049
  return `${FOUND_BASE3}/${encodeURIComponent(logId)}/set.mp4`;
4001
4050
  }
4002
4051
  function escapeDrawtextValue(text) {
4003
4052
  return text.replace(/\\/g, "\\\\").replace(/:/g, "\\:").replace(/,/g, "\\,").replace(/%/g, "\\%").replace(/'/g, "'\\''");
4004
4053
  }
4005
- function clipCutVideoFilter(options) {
4054
+ function brandDrawtext(options) {
4055
+ const value = escapeDrawtextValue(options.text);
4056
+ const font = options.fontFile ? `:fontfile='${options.fontFile}'` : "";
4057
+ const border = options.borderw && options.borderw > 0 ? `:borderw=${options.borderw}:bordercolor=${CLIP_HALO_COLOR}` : "";
4058
+ const enable = options.enable ? `:enable='${options.enable}'` : "";
4059
+ return `drawtext=text='${value}'${font}:` + `fontcolor=${options.color}:fontsize=${options.size}:` + `x=${options.x}:y=${options.y}${border}${enable}`;
4060
+ }
4061
+ function resolveTitleLines(options, y) {
4062
+ const inMs = options.inMs ?? 0;
4063
+ const outMs = options.outMs ?? 0;
4064
+ const clipDur = Math.max(0, (outMs - inMs) / 1000);
4065
+ const base = { fontFile: options.sansFontFile, size: CLIP_TITLE_SIZE, x: CLIP_MARGIN_X, y };
4066
+ const resolved = options.members && options.members.length > 0 ? resolveClipTracks({
4067
+ inMs,
4068
+ members: options.members,
4069
+ outMs,
4070
+ setDurationMs: options.setDurationMs ?? 0
4071
+ }) : [];
4072
+ if (resolved.length === 0) {
4073
+ return [{ ...base, text: options.title }];
4074
+ }
4075
+ const clamp = (seconds) => Math.min(Math.max(seconds, 0), clipDur);
4076
+ return resolved.map((track, index) => {
4077
+ const relStart = clamp((track.startMs - inMs) / 1000);
4078
+ const next = resolved[index + 1];
4079
+ const relEnd = next ? clamp((next.startMs - inMs) / 1000) : clipDur;
4080
+ return {
4081
+ ...base,
4082
+ enable: `between(t,${relStart.toFixed(3)},${relEnd.toFixed(3)})`,
4083
+ text: track.label
4084
+ };
4085
+ });
4086
+ }
4087
+ function clipCutFilterComplex(options) {
4006
4088
  const xOffset = Math.max(0, Math.round(options.xOffset));
4007
4089
  const crop = `crop=ih*9/16:ih:${xOffset}:0,scale=${CLIP_WIDTH}:${CLIP_HEIGHT}`;
4008
- const font = options.fontFile ? `:fontfile='${options.fontFile}'` : "";
4009
- const title = escapeDrawtextValue(options.title);
4010
- const coordinate2 = escapeDrawtextValue(`fluncle://${options.logId}`);
4011
- const titleDraw = `drawtext=text='${title}'${font}:fontcolor=white:fontsize=46:` + `x=56:y=h-208:box=1:boxcolor=black@0.55:boxborderw=18`;
4012
- const coordinateDraw = `drawtext=text='${coordinate2}'${font}:fontcolor=0xF4EAD7:fontsize=30:` + `x=56:y=h-132:box=1:boxcolor=black@0.55:boxborderw=12`;
4013
- return `${crop},${titleDraw},${coordinateDraw}`;
4090
+ const coordinateBox = Math.round(CLIP_COORDINATE_SIZE * 1.18);
4091
+ const titleBottomOffset = CLIP_SAFE_BOTTOM + coordinateBox + CLIP_LINE_GAP;
4092
+ const titleY = `h-${titleBottomOffset}-th`;
4093
+ const titleLines = resolveTitleLines(options, titleY);
4094
+ const coordinate2 = {
4095
+ fontFile: options.oxaniumFontFile,
4096
+ size: CLIP_COORDINATE_SIZE,
4097
+ text: `fluncle://${options.logId}`,
4098
+ x: CLIP_MARGIN_X,
4099
+ y: `h-${CLIP_SAFE_BOTTOM}-th`
4100
+ };
4101
+ const haloGlyphs = [
4102
+ ...titleLines.map((line) => brandDrawtext({ ...line, color: CLIP_HALO_COLOR })),
4103
+ brandDrawtext({ ...coordinate2, color: CLIP_HALO_COLOR })
4104
+ ].join(",");
4105
+ const sharpGlyphs = [
4106
+ ...titleLines.map((line) => brandDrawtext({ ...line, borderw: CLIP_SHARP_BORDERW, color: CLIP_TITLE_COLOR })),
4107
+ brandDrawtext({ ...coordinate2, borderw: CLIP_SHARP_BORDERW, color: CLIP_COORDINATE_COLOR })
4108
+ ].join(",");
4109
+ return [
4110
+ `[0:v]${crop},setsar=1[base]`,
4111
+ `color=c=black@0:s=${CLIP_WIDTH}x${CLIP_HEIGHT},format=rgba,${haloGlyphs}[ink]`,
4112
+ `[ink]split[ink1][ink2]`,
4113
+ `[ink1]gblur=sigma=${CLIP_HALO_CORE_SIGMA}[core]`,
4114
+ `[ink2]gblur=sigma=${CLIP_HALO_FEATHER_SIGMA}[feather]`,
4115
+ `[base][feather]overlay=0:0[b1]`,
4116
+ `[b1][core]overlay=0:0[b2]`,
4117
+ `[b2]${sharpGlyphs}[out]`
4118
+ ].join(";");
4014
4119
  }
4015
4120
  function clipCutFfmpegArgs(options) {
4016
4121
  const inSeconds = (options.inMs / 1000).toFixed(3);
4017
4122
  const durationSeconds = ((options.outMs - options.inMs) / 1000).toFixed(3);
4018
- const filter = clipCutVideoFilter(options);
4123
+ const filter = clipCutFilterComplex(options);
4019
4124
  return [
4020
4125
  "-y",
4021
4126
  "-ss",
@@ -4024,8 +4129,12 @@ function clipCutFfmpegArgs(options) {
4024
4129
  options.setUrl,
4025
4130
  "-t",
4026
4131
  durationSeconds,
4027
- "-vf",
4132
+ "-filter_complex",
4028
4133
  filter,
4134
+ "-map",
4135
+ "[out]",
4136
+ "-map",
4137
+ "0:a?",
4029
4138
  "-c:v",
4030
4139
  "libx264",
4031
4140
  "-preset",
@@ -4079,11 +4188,14 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
4079
4188
  try {
4080
4189
  onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms] from ${mixtape.logId}…`);
4081
4190
  await runClipCut({
4082
- fontFile: process.env.CLIP_FONT_FILE,
4083
4191
  inMs: clip.inMs,
4084
4192
  logId: mixtape.logId,
4193
+ members: mixtape.members,
4085
4194
  outMs: clip.outMs,
4086
4195
  outputPath,
4196
+ oxaniumFontFile: resolveClipFontFile(),
4197
+ sansFontFile: resolveClipSansFontFile(),
4198
+ setDurationMs: mixtape.durationMs ?? 0,
4087
4199
  setUrl,
4088
4200
  title: mixtape.title,
4089
4201
  xOffset: clip.xOffset
@@ -4144,8 +4256,9 @@ async function runClipCut(options) {
4144
4256
  throw new CliError2("ffmpeg_failed", `ffmpeg failed to cut the clip${detail ? `: ${detail}` : ""}`);
4145
4257
  }
4146
4258
  }
4147
- var FOUND_BASE3 = "https://found.fluncle.com", CLIP_WIDTH = 1080, CLIP_HEIGHT = 1920, CLIP_CRF = 21, CLIP_MAXRATE = "10M", CLIP_BUFSIZE = "20M", CLIP_AUDIO_BITRATE = "192k", MAX_CLIP_BYTES;
4259
+ var FOUND_BASE3 = "https://found.fluncle.com", CLIP_WIDTH = 1080, CLIP_HEIGHT = 1920, CLIP_CRF = 21, CLIP_MAXRATE = "10M", CLIP_BUFSIZE = "20M", CLIP_AUDIO_BITRATE = "192k", MAX_CLIP_BYTES, CLIP_TITLE_COLOR = "0xf4ead7", CLIP_COORDINATE_COLOR = "0xb7ab95", CLIP_HALO_COLOR = "0x090a0b", CLIP_HALO_CORE_SIGMA = 3, CLIP_HALO_FEATHER_SIGMA = 9, CLIP_SHARP_BORDERW = 1, CLIP_MARGIN_X = 96, CLIP_SAFE_BOTTOM = 230, CLIP_LINE_GAP = 10, CLIP_TITLE_SIZE = 40, CLIP_COORDINATE_SIZE = 22, BOX_CLIP_FONT_FILE = "/opt/fonts/Oxanium-SemiBold.ttf", BOX_CLIP_SANS_FONT_FILE = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf";
4148
4260
  var init_clips = __esm(() => {
4261
+ init_util();
4149
4262
  init_api();
4150
4263
  init_output();
4151
4264
  MAX_CLIP_BYTES = 100 * 1024 * 1024;
@@ -4160,7 +4273,7 @@ __export(exports_newsletter, {
4160
4273
  newsletterDraftCommand: () => newsletterDraftCommand,
4161
4274
  newsletterDeleteCommand: () => newsletterDeleteCommand
4162
4275
  });
4163
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
4276
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
4164
4277
  function buildBody3(options, { requireContent }) {
4165
4278
  const body = {};
4166
4279
  if (options.contentFile !== undefined) {
@@ -4180,7 +4293,7 @@ function buildBody3(options, { requireContent }) {
4180
4293
  return body;
4181
4294
  }
4182
4295
  function readContentFile(filePath) {
4183
- if (!existsSync4(filePath)) {
4296
+ if (!existsSync5(filePath)) {
4184
4297
  throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
4185
4298
  }
4186
4299
  const text = readFileSync4(filePath, "utf-8");
@@ -4822,7 +4935,7 @@ var init_format2 = __esm(() => {
4822
4935
  });
4823
4936
 
4824
4937
  // src/cli.ts
4825
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
4938
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
4826
4939
  import path2 from "path";
4827
4940
 
4828
4941
  // ../../node_modules/commander/lib/error.js
@@ -7499,7 +7612,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
7499
7612
  return;
7500
7613
  }
7501
7614
  const candidate = path2.join(dir, name);
7502
- return existsSync5(candidate) ? candidate : undefined;
7615
+ return existsSync6(candidate) ? candidate : undefined;
7503
7616
  };
7504
7617
  const resolveFile = (explicit, name) => {
7505
7618
  if (explicit) {
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.74.0"
34
+ "version": "0.76.0"
35
35
  }