fluncle 0.88.0 → 0.90.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 +89 -12
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -538,7 +538,7 @@ function parseVersion(version) {
538
538
  var currentVersion;
539
539
  var init_version = __esm(() => {
540
540
  init_output();
541
- currentVersion = "0.88.0".trim() ? "0.88.0".trim() : "0.1.0";
541
+ currentVersion = "0.90.0".trim() ? "0.90.0".trim() : "0.1.0";
542
542
  });
543
543
 
544
544
  // src/update-notifier.ts
@@ -2168,7 +2168,7 @@ function parseVersion2(version) {
2168
2168
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2169
2169
  var init_version2 = __esm(() => {
2170
2170
  init_output();
2171
- currentVersion2 = "0.88.0".trim() ? "0.88.0".trim() : "0.1.0";
2171
+ currentVersion2 = "0.90.0".trim() ? "0.90.0".trim() : "0.1.0";
2172
2172
  });
2173
2173
 
2174
2174
  // ../../packages/registry/src/index.ts
@@ -2894,16 +2894,58 @@ __export(exports_track, {
2894
2894
  trackNoteCommand: () => trackNoteCommand,
2895
2895
  trackGetCommand: () => trackGetCommand,
2896
2896
  trackDraftCommand: () => trackDraftCommand,
2897
- trackContextCommand: () => trackContextCommand
2897
+ trackContextCommand: () => trackContextCommand,
2898
+ isPlatesOnlyUpload: () => isPlatesOnlyUpload,
2899
+ checkBundleCompleteness: () => checkBundleCompleteness
2898
2900
  });
2899
2901
  async function trackGetCommand(idOrLogId) {
2900
2902
  return publicApiGet(`/api/tracks/${encodeURIComponent(idOrLogId)}`);
2901
2903
  }
2902
- async function trackVideoCommand(idOrLogId, files, onProgress) {
2904
+ function isPlatesOnlyUpload(files) {
2905
+ const hasPlate = PLATE_FIELDS.some((option) => Boolean(files[option]));
2906
+ if (!hasPlate) {
2907
+ return false;
2908
+ }
2909
+ return Object.keys(files).every((option) => !files[option] || PLATE_FIELDS.includes(option) || NON_FILE_OPTIONS.includes(option));
2910
+ }
2911
+ function checkBundleCompleteness(files) {
2912
+ const uploadingFootage = FOOTAGE_FIELDS.some((option) => Boolean(files[option]));
2913
+ const missingFrom = (specs) => uploadingFootage ? specs.filter((spec) => !files[spec.option]).map((spec) => spec.file) : [];
2914
+ const plateWarnings = [];
2915
+ if (files.plateBackground && !files.plate) {
2916
+ plateWarnings.push("plate.background.png without plate.png — the background is the parallax layer OF a plate; pass --plate (or drop it in the --dir) too");
2917
+ }
2918
+ return {
2919
+ missingAdvisory: missingFrom(RERENDER_ADVISORY_FIELDS),
2920
+ missingContract: missingFrom(RERENDER_CONTRACT_FIELDS),
2921
+ plateWarnings,
2922
+ uploadingFootage
2923
+ };
2924
+ }
2925
+ async function trackVideoCommand(idOrLogId, files, onProgress, options = {}) {
2926
+ const completeness = checkBundleCompleteness(files);
2927
+ if (completeness.uploadingFootage && completeness.missingContract.length > 0 && !options.allowPartial) {
2928
+ throw new CliError2("bundle_incomplete", `Refusing to upload a PARTIAL bundle: footage is being uploaded but the re-render contract is missing ${completeness.missingContract.join(", ")}. ` + `A footage-only upload leaves composition.tsx/props.json/render.json stale on R2 and desyncs the render.json from the DB ledger. ` + `Ship the complete bundle (re-run \`ship\` and upload with --dir), or pass --allow-partial for a deliberate partial refresh (e.g. poster-only).`);
2929
+ }
2930
+ if (completeness.uploadingFootage) {
2931
+ if (completeness.missingContract.length > 0) {
2932
+ onProgress?.(`warning: --allow-partial — uploading WITHOUT the re-render contract (${completeness.missingContract.join(", ")}); the R2 bundle will NOT be re-renderable`);
2933
+ }
2934
+ for (const missing of completeness.missingAdvisory) {
2935
+ onProgress?.(`warning: ${missing} missing (provenance/eval only) — shipping without it`);
2936
+ }
2937
+ }
2938
+ for (const warning of completeness.plateWarnings) {
2939
+ onProgress?.(`warning: ${warning}`);
2940
+ }
2903
2941
  const present = VIDEO_FIELDS.map((spec) => ({
2904
2942
  field: spec.field,
2905
2943
  path: files[spec.option]
2906
2944
  })).filter((spec) => Boolean(spec.path));
2945
+ if (present.length === 0) {
2946
+ throw new CliError2("nothing_to_upload", "No bundle files resolved to upload (pass --dir <bundle> or explicit file flags).");
2947
+ }
2948
+ const platesOnly = isPlatesOnlyUpload(files);
2907
2949
  const presign = await adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/video/uploads`, { fields: present.map((spec) => spec.field) });
2908
2950
  const byField = new Map(presign.uploads.map((upload) => [upload.field, upload]));
2909
2951
  const urls = {};
@@ -2924,6 +2966,10 @@ async function trackVideoCommand(idOrLogId, files, onProgress) {
2924
2966
  }
2925
2967
  urls[spec.field] = `${FOUND_BASE}/${upload.key}`;
2926
2968
  }
2969
+ if (platesOnly) {
2970
+ onProgress?.(`plate pre-upload complete — compose against ${FOUND_BASE}/${presign.logId}/plate.png; finalize is deferred to the footage ship`);
2971
+ return { logId: presign.logId, ok: true, trackId: presign.trackId, urls };
2972
+ }
2927
2973
  const manifest = files.render ? await readManifestFields(files.render) : {};
2928
2974
  const videoModel = files.model?.trim().slice(0, 120) || manifest.model || DEFAULT_VIDEO_MODEL;
2929
2975
  const videoModelReasoning = files.reasoning?.trim().slice(0, 120) || manifest.reasoning || DEFAULT_VIDEO_REASONING;
@@ -3033,7 +3079,7 @@ async function trackNoteCommand(idOrLogId, options) {
3033
3079
  const body = { note: options.note };
3034
3080
  return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/note`, body);
3035
3081
  }
3036
- var DEFAULT_VIDEO_MODEL = "anthropic/claude-opus-4-8", DEFAULT_VIDEO_REASONING = "high", FOUND_BASE = "https://found.fluncle.com", VIDEO_FIELDS;
3082
+ var DEFAULT_VIDEO_MODEL = "anthropic/claude-opus-4-8", DEFAULT_VIDEO_REASONING = "high", FOUND_BASE = "https://found.fluncle.com", VIDEO_FIELDS, RERENDER_CONTRACT_FIELDS, RERENDER_ADVISORY_FIELDS, FOOTAGE_FIELDS, PLATE_FIELDS, NON_FILE_OPTIONS;
3037
3083
  var init_track = __esm(() => {
3038
3084
  init_api();
3039
3085
  init_output();
@@ -3045,13 +3091,35 @@ var init_track = __esm(() => {
3045
3091
  { field: "footage-landscape-social", option: "footageLandscapeSocial" },
3046
3092
  { field: "poster", option: "poster" },
3047
3093
  { field: "cover", option: "cover" },
3094
+ { field: "plate", option: "plate" },
3095
+ { field: "plate-background", option: "plateBackground" },
3048
3096
  { field: "note", option: "note" },
3049
3097
  { field: "composition", option: "composition" },
3050
3098
  { field: "props", option: "props" },
3051
3099
  { field: "render", option: "render" },
3052
3100
  { field: "intent", option: "intent" },
3053
- { field: "metrics", option: "metrics" }
3101
+ { field: "metrics", option: "metrics" },
3102
+ { field: "scene", option: "scene" }
3054
3103
  ];
3104
+ RERENDER_CONTRACT_FIELDS = [
3105
+ { file: "composition.tsx", option: "composition" },
3106
+ { file: "props.json", option: "props" },
3107
+ { file: "render.json", option: "render" }
3108
+ ];
3109
+ RERENDER_ADVISORY_FIELDS = [
3110
+ { file: "intent.json", option: "intent" },
3111
+ { file: "metrics.json", option: "metrics" },
3112
+ { file: "scene.json", option: "scene" }
3113
+ ];
3114
+ FOOTAGE_FIELDS = [
3115
+ "footage",
3116
+ "footageSocial",
3117
+ "footageNotext",
3118
+ "footageLandscape",
3119
+ "footageLandscapeSocial"
3120
+ ];
3121
+ PLATE_FIELDS = ["plate", "plateBackground"];
3122
+ NON_FILE_OPTIONS = ["model", "reasoning"];
3055
3123
  });
3056
3124
 
3057
3125
  // src/commands/add.ts
@@ -7229,7 +7297,7 @@ function addAdminCommands(program2) {
7229
7297
  const { trackUpdateCommand: trackUpdateCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7230
7298
  await runTrackUpdate(trackId, options, trackUpdateCommand2);
7231
7299
  });
7232
- adminTrack.command("video").description("Upload a track's video bundle to R2 and link it").argument("[idOrLogId]").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("--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").allowExcessArguments().action(async (idOrLogId, options) => {
7300
+ 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) => {
7233
7301
  const { trackVideoCommand: trackVideoCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7234
7302
  await runTrackVideo(idOrLogId, options, trackVideoCommand2);
7235
7303
  });
@@ -7654,7 +7722,7 @@ async function runBackfillDiscogs(options, backfillDiscogsCommand2) {
7654
7722
  }
7655
7723
  async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
7656
7724
  if (!idOrLogId) {
7657
- throw new Error("Missing id. Usage: fluncle admin tracks video <track_id|log_id> (--dir <dir> | --footage <file> [--footage-social <file>] [--footage-notext <file>] [--footage-landscape <file>] [--footage-landscape-social <file>] [--poster <file>] [--cover <file>] [--note <file>] [--composition <file>] [--props <file>] [--render <file>] [--intent <file>] [--metrics <file>])");
7725
+ throw new Error("Missing id. Usage: fluncle admin tracks video <track_id|log_id> (--dir <dir> | --footage <file> [--footage-social <file>] [--footage-notext <file>] [--footage-landscape <file>] [--footage-landscape-social <file>] [--poster <file>] [--cover <file>] [--note <file>] [--composition <file>] [--props <file>] [--render <file>] [--intent <file>] [--metrics <file>] [--scene <file>] | --plate <file> [--plate-background <file>]) [--allow-partial]");
7658
7726
  }
7659
7727
  const dir = options.dir ? path2.resolve(process.cwd(), options.dir) : undefined;
7660
7728
  const fromDir = (name) => {
@@ -7682,16 +7750,22 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
7682
7750
  metrics: resolveFile(options.metrics, "metrics.json"),
7683
7751
  model: options.model,
7684
7752
  note: resolveFile(options.note, "note.txt"),
7753
+ plate: resolveFile(options.plate, "plate.png"),
7754
+ plateBackground: resolveFile(options.plateBackground, "plate.background.png"),
7685
7755
  poster: resolveFile(options.poster, "poster.jpg"),
7686
7756
  props: resolveFile(options.props, "props.json"),
7687
7757
  reasoning: options.reasoning,
7688
- render: resolveFile(options.render, "render.json")
7758
+ render: resolveFile(options.render, "render.json"),
7759
+ scene: resolveFile(options.scene, "scene.json")
7689
7760
  };
7690
- if (!files.footage) {
7691
- throw new Error("A footage cut is required (--footage <file>, or --dir containing footage.mp4)");
7761
+ const { isPlatesOnlyUpload: isPlatesOnlyUpload2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7762
+ if (!files.footage && !options.allowPartial && !isPlatesOnlyUpload2(files)) {
7763
+ throw new Error("A footage cut is required (--footage <file>, or --dir containing footage.mp4). Pass --allow-partial for a deliberate partial refresh (e.g. poster-only), or upload plates alone (--plate/--plate-background) for the plate-lane pre-upload.");
7692
7764
  }
7693
7765
  const onProgress = options.json ? undefined : (message) => console.log(message);
7694
- const result = await trackVideoCommand2(idOrLogId, files, onProgress);
7766
+ const result = await trackVideoCommand2(idOrLogId, files, onProgress, {
7767
+ allowPartial: options.allowPartial
7768
+ });
7695
7769
  if (options.json) {
7696
7770
  printJson(result);
7697
7771
  return;
@@ -8485,12 +8559,15 @@ var stringOptions = new Set([
8485
8559
  "--metrics",
8486
8560
  "--mime",
8487
8561
  "--note",
8562
+ "--plate",
8563
+ "--plate-background",
8488
8564
  "--platform",
8489
8565
  "--poster",
8490
8566
  "--props",
8491
8567
  "--query",
8492
8568
  "--recorded-at",
8493
8569
  "--render",
8570
+ "--scene",
8494
8571
  "--scheduled-for",
8495
8572
  "--soundcloud-url",
8496
8573
  "--source",
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.88.0"
34
+ "version": "0.90.0"
35
35
  }