@shop-os/modelkit 0.4.3 → 0.4.5

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 CHANGED
@@ -9,6 +9,18 @@ header in the release PR.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.4.5] - Unreleased
13
+
14
+ ### Fixed
15
+ - Video length maximums and combined totals forgive ffprobe's container over-read. ffprobe reads a clip a few hundredths over its nominal length (a 10 s clip reads 10.04 s), so a clip at the limit was refused. A length is now over a maximum only when it and its tenth are both over: a source that reads 10.04 s passes Gemini Omni's 10 s cap, one that reads 10.05 s or more is still refused, and a limit that is not a whole tenth (Kling O1's 10.05 s) still takes a clip at it. A combined total adds each clip's tenth, so three 5.04 s references fit a 15 s total. The rule is never stricter than the old raw comparison. It applies to the reference maximum and total and the source maximum in `videoLengthViolation`, and to the source cap in `planVideoTool`. Minimums stay raw: the over-read makes a clip that reads short shorter still, so 3.96 s is still under a 4 s minimum.
16
+
17
+ ## [0.4.4] - Unreleased
18
+
19
+ ### Added
20
+ - `inputSpec.dimensions` on image rows: `{ field, areas, maxSide?, multipleOf? }` sends one `{ width, height }` object in `field`, computed from the request's aspect ratio and size. `areas` is the target pixel count per size tier; the long side is capped at `maxSide` and both sides round down to `multipleOf`. Nothing is sent when the ratio is missing or `adaptive`, or the size has no area, so the provider default applies (on an edit endpoint, the first reference's size). A spec with `dimensions` cannot also map `aspectRatio` or `size` in `fields`, and `field` cannot be a name another mapping already sends. This lets a fal row whose endpoint only takes `image_size` presets or `{ width, height }` (FLUX 3 Image Klein) offer 2K and 4K as data.
21
+
22
+ Add a row that carries `inputSpec.dimensions` only after every app that will call it runs 0.4.4. An older kit rejects the unknown key, so the row is quarantined there.
23
+
12
24
  ## [0.4.3] - Unreleased
13
25
 
14
26
  ### Fixed
package/README.md CHANGED
@@ -224,6 +224,7 @@ last. Video rows use `VIDEO_INPUT_SPEC_KEYS` (`prompt`, `durationSeconds`,
224
224
  with `values`); audio rows use
225
225
  `AUDIO_INPUT_SPEC_KEYS` (`prompt`, `durationSeconds`, `seed`, `negativePrompt`,
226
226
  `references`, `sourceVideo`, `instrumental`). `aspectRatio: "adaptive"` is omitted. A fal image row has no kind and builds its whole input from the spec; the optional `editEndpoint` (fal rows only) is the fal endpoint called instead of `providerModelId` when the request has references. fal rows without an `inputSpec` keep the Muse-shaped request and Ideogram keeps its own path; an `inputSpec` on an Ideogram row replaces that path. `editEndpoint` needs a `references` entry in `fields`. `inputSpec` on fal video rows is not supported.
227
+ `dimensions` (image rows only, from 0.4.4) sends one `{ width, height }` object for a provider that takes pixel sizes instead of a ratio and a tier: `dimensions: { field: "image_size", areas: { "1K": 1048576, "2K": 4194304, "4K": 16777216 }, maxSide: 4096, multipleOf: 16 }`. Each `areas` entry is the target pixel count for that size; the long side is capped at `maxSide`, and both sides round down to `multipleOf`. With no ratio, `adaptive`, or a size with no area, the field is omitted. A spec with `dimensions` maps neither `aspectRatio` nor `size` in `fields`.
227
228
  `arrays` joins several image inputs into one provider array field, in the listed order: `arrays: { images: ["firstFrame", "referenceImages", "lastFrame"] }` (video rows: `firstFrame`, `referenceImages`, `lastFrame`; image rows: `references`). A key is mapped once, in `fields` or in one array, and the field is omitted when none of its inputs is set. A row that maps `sourceVideo` also needs `capabilities.acceptsSourceVideo`; set `capabilities.sourceVideoExcludesFrames` when the provider rejects a source video next to images.
228
229
  `validateInputSpec` is the shared contract the write schema, `dataOnlyCapable`, and
229
230
  the Replicate mapper all check. `dataOnlyCapable(row)` reports whether such a row would
package/dist/catalog.cjs CHANGED
@@ -2563,16 +2563,26 @@ var ArraysSchema = import_zod2.z.record(import_zod2.z.string().min(1), import_zo
2563
2563
  }
2564
2564
  }
2565
2565
  });
2566
+ var DimensionsSchema = import_zod2.z.strictObject({
2567
+ field: import_zod2.z.string().min(1),
2568
+ areas: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.number().int().positive()).refine((a) => Object.keys(a).length > 0, {
2569
+ message: "inputSpec.dimensions.areas must name at least one size"
2570
+ }),
2571
+ maxSide: import_zod2.z.number().int().positive().optional(),
2572
+ multipleOf: import_zod2.z.number().int().positive().optional()
2573
+ });
2566
2574
  var InputSpecSchema = import_zod2.z.strictObject({
2567
2575
  fields: FieldsSchema,
2568
2576
  values: ValuesSchema.optional(),
2569
2577
  static: StaticSchema.optional(),
2570
2578
  arrays: ArraysSchema.optional(),
2571
- editEndpoint: import_zod2.z.string().min(1).optional()
2579
+ editEndpoint: import_zod2.z.string().min(1).optional(),
2580
+ dimensions: DimensionsSchema.optional()
2572
2581
  }).superRefine((spec, ctx) => {
2573
2582
  const mappedNames = /* @__PURE__ */ new Set([
2574
2583
  ...Object.values(spec.fields),
2575
- ...Object.keys(spec.arrays ?? {})
2584
+ ...Object.keys(spec.arrays ?? {}),
2585
+ ...spec.dimensions ? [spec.dimensions.field] : []
2576
2586
  ]);
2577
2587
  for (const key of Object.keys(spec.static ?? {})) {
2578
2588
  if (mappedNames.has(key)) {
@@ -2601,7 +2611,10 @@ function validateInputSpec(raw, row) {
2601
2611
  return {
2602
2612
  ok: false,
2603
2613
  issues: [
2604
- { path: [], message: "inputSpec is only supported on replicate and fal rows" }
2614
+ {
2615
+ path: [],
2616
+ message: "inputSpec is only supported on replicate and fal rows"
2617
+ }
2605
2618
  ]
2606
2619
  };
2607
2620
  }
@@ -2694,6 +2707,29 @@ function validateInputSpec(raw, row) {
2694
2707
  }
2695
2708
  }
2696
2709
  }
2710
+ if (spec.dimensions !== void 0) {
2711
+ if (row.type !== "image") {
2712
+ issues.push({
2713
+ path: ["dimensions"],
2714
+ message: "inputSpec.dimensions is only supported on image rows"
2715
+ });
2716
+ }
2717
+ for (const key of ["aspectRatio", "size"]) {
2718
+ if (key in spec.fields) {
2719
+ issues.push({
2720
+ path: ["fields", key],
2721
+ message: `inputSpec.fields "${key}" cannot be set with inputSpec.dimensions, which sends it`
2722
+ });
2723
+ }
2724
+ }
2725
+ const owner = seenNames.get(spec.dimensions.field);
2726
+ if (owner !== void 0 || spec.dimensions.field in (spec.arrays ?? {})) {
2727
+ issues.push({
2728
+ path: ["dimensions", "field"],
2729
+ message: `inputSpec.dimensions.field "${spec.dimensions.field}" is already a mapped provider field`
2730
+ });
2731
+ }
2732
+ }
2697
2733
  const supports = row.constraints.supports ?? [];
2698
2734
  for (const key of INPUT_SPEC_SUPPORTS_KEYS) {
2699
2735
  if (supports.includes(key)) continue;
@@ -3288,6 +3324,20 @@ function resetLiveCatalog() {
3288
3324
 
3289
3325
  // src/catalog/video-tools-spec.ts
3290
3326
  var import_zod6 = require("zod");
3327
+
3328
+ // src/video-length.ts
3329
+ function tenths(seconds2) {
3330
+ return Math.round(seconds2 * 10) / 10;
3331
+ }
3332
+ function overMax(seconds2, max) {
3333
+ return seconds2 > max && tenths(seconds2) > max;
3334
+ }
3335
+ function overTotal(lengths, total) {
3336
+ const sum = (values) => values.reduce((acc, len) => acc + len, 0);
3337
+ return sum(lengths) > total && tenths(sum(lengths.map(tenths))) > total;
3338
+ }
3339
+
3340
+ // src/catalog/video-tools-spec.ts
3291
3341
  var RangeSchema2 = import_zod6.z.object({ min: import_zod6.z.number(), max: import_zod6.z.number() });
3292
3342
  var ToolSchema = import_zod6.z.object({
3293
3343
  kind: import_zod6.z.enum(["upscale", "interpolate", "hdr"]),
@@ -3331,7 +3381,7 @@ function planVideoTool(row, req) {
3331
3381
  return fail("The source video's width, height and duration are required");
3332
3382
  }
3333
3383
  const maxSeconds = row.constraints.sourceVideoSeconds?.max;
3334
- if (maxSeconds !== void 0 && durationSeconds > maxSeconds) {
3384
+ if (maxSeconds !== void 0 && overMax(durationSeconds, maxSeconds)) {
3335
3385
  return fail(`${row.label} takes source videos up to ${maxSeconds} seconds.`);
3336
3386
  }
3337
3387
  const base = {
@@ -3528,7 +3578,7 @@ function videoLengthViolation(model, input) {
3528
3578
  const lengths = (input.referenceVideos ?? []).map(knownLength);
3529
3579
  if (limits?.max !== void 0) {
3530
3580
  const max = limits.max;
3531
- const i = lengths.findIndex((len) => len !== void 0 && len > max);
3581
+ const i = lengths.findIndex((len) => len !== void 0 && overMax(len, max));
3532
3582
  if (i >= 0)
3533
3583
  return `${model.label} takes reference videos up to ${String(max)} s each; video ${i + 1} is ${seconds(lengths[i], max)} s.`;
3534
3584
  }
@@ -3540,14 +3590,17 @@ function videoLengthViolation(model, input) {
3540
3590
  const source = knownLength(input.sourceVideo);
3541
3591
  const sourceLimits = model.constraints.sourceVideoSeconds;
3542
3592
  if (limits?.total !== void 0) {
3543
- const refSum = lengths.reduce((acc, len) => acc + (len ?? 0), 0);
3544
- const sum = refSum + (sourceLimits?.inReferenceTotal ? source ?? 0 : 0);
3545
- if (sum > limits.total)
3593
+ const counted = [
3594
+ ...lengths.filter((len) => len !== void 0),
3595
+ ...sourceLimits?.inReferenceTotal && source !== void 0 ? [source] : []
3596
+ ];
3597
+ const sum = counted.reduce((acc, len) => acc + len, 0);
3598
+ if (overTotal(counted, limits.total))
3546
3599
  return `${model.label} takes reference videos up to ${String(limits.total)} s combined; these add up to ${seconds(sum, limits.total)} s.`;
3547
3600
  }
3548
3601
  if (source === void 0) return void 0;
3549
3602
  const sourceMin = effectiveMin(sourceLimits, input.taskType);
3550
- if (sourceLimits?.max !== void 0 && source > sourceLimits.max)
3603
+ if (sourceLimits?.max !== void 0 && overMax(source, sourceLimits.max))
3551
3604
  return `${model.label} takes a source video up to ${String(sourceLimits.max)} s; this one is ${seconds(source, sourceLimits.max)} s.`;
3552
3605
  if (sourceMin !== void 0 && source < sourceMin)
3553
3606
  return `${model.label} takes a source video of at least ${String(sourceMin)} s; this one is ${seconds(source, sourceMin)} s.`;