@shop-os/modelkit 0.4.4 → 0.4.6

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/dist/index.cjs CHANGED
@@ -3770,6 +3770,11 @@ var arkAdapter = {
3770
3770
  async generateVideo(req, ctx) {
3771
3771
  throwIfAborted(ctx.signal);
3772
3772
  return generateSeedanceVideo(req, ctx);
3773
+ },
3774
+ resumeVideo(req, jobId, ctx) {
3775
+ throwIfAborted(ctx.signal);
3776
+ const key = arkKey(ctx);
3777
+ return seedanceJob(ctx, key, jobId, seedanceUsage(ctx, req, seedanceInitialDuration(req)));
3773
3778
  }
3774
3779
  };
3775
3780
  function arkKey(ctx) {
@@ -3939,6 +3944,9 @@ function seedanceUsage(ctx, req, duration) {
3939
3944
  if (req.resolution !== void 0) usage2.resolution = req.resolution;
3940
3945
  return usage2;
3941
3946
  }
3947
+ function seedanceInitialDuration(req) {
3948
+ return req.durationSeconds ?? 5;
3949
+ }
3942
3950
  function seedanceErrorMessage(error, status) {
3943
3951
  const asString = import_zod7.z.string().safeParse(error);
3944
3952
  if (asString.success) return asString.data;
@@ -3946,6 +3954,78 @@ function seedanceErrorMessage(error, status) {
3946
3954
  if (asObject.success && asObject.data.message !== void 0) return asObject.data.message;
3947
3955
  return `Seedance task ${status}`;
3948
3956
  }
3957
+ function seedanceJob(ctx, key, jobId, usage2) {
3958
+ const modelId = ctx.model.id;
3959
+ const fetchStatus = async () => {
3960
+ const res = await arkFetch(ctx, `${arkBaseUrl(ctx)}${SUBMIT_PATH}/${jobId}`, {
3961
+ headers: { Authorization: `Bearer ${key}` }
3962
+ });
3963
+ if (!res.ok) {
3964
+ if (ctx.call.maxRetries !== 0 && (res.status === 429 || res.status >= 500))
3965
+ return void 0;
3966
+ const text2 = await res.text().catch(() => "");
3967
+ throwArkError(res.status, text2, modelId);
3968
+ }
3969
+ const parsed = SeedanceResponseSchema.safeParse(await res.json());
3970
+ if (!parsed.success) return {};
3971
+ return parsed.data;
3972
+ };
3973
+ return {
3974
+ jobId,
3975
+ async poll() {
3976
+ const polled = await fetchStatus();
3977
+ if (!polled) return { status: "running" };
3978
+ const status = (polled.status ?? "queued").toLowerCase();
3979
+ if (status === "succeeded") return { status: "succeeded", raw: polled };
3980
+ if (status === "failed" || status === "expired" || status === "cancelled") {
3981
+ return { status: "failed", raw: polled };
3982
+ }
3983
+ if (status === "queued" || status === "pending")
3984
+ return { status: "pending", raw: polled };
3985
+ return { status: "running", raw: polled };
3986
+ },
3987
+ async result(opts) {
3988
+ const optSignal = opts?.signal ?? ctx.signal;
3989
+ const pollOpts = {
3990
+ intervalMs: opts?.pollIntervalMs ?? SEEDANCE_POLL_INTERVAL_MS,
3991
+ timeoutMs: opts?.timeoutMs ?? RESULT_TIMEOUT_MS
3992
+ };
3993
+ if (optSignal !== void 0) pollOpts.signal = optSignal;
3994
+ return pollUntil(async () => {
3995
+ const polled = await fetchStatus();
3996
+ if (!polled) return void 0;
3997
+ const status = (polled.status ?? "queued").toLowerCase();
3998
+ if (status === "succeeded") {
3999
+ const url = polled.content?.video_url;
4000
+ if (!url)
4001
+ throwArkError(
4002
+ void 0,
4003
+ "Seedance task succeeded but returned no video_url",
4004
+ modelId
4005
+ );
4006
+ if (polled.duration !== void 0 && polled.duration > 0) {
4007
+ usage2.quantity = polled.duration;
4008
+ usage2.durationSeconds = polled.duration;
4009
+ }
4010
+ const tokens = polled.usage;
4011
+ if (tokens?.total_tokens !== void 0 && tokens.completion_tokens !== void 0) {
4012
+ usage2.inputTokens = tokens.total_tokens - tokens.completion_tokens;
4013
+ usage2.outputTokens = tokens.completion_tokens;
4014
+ }
4015
+ return {
4016
+ video: { url, expiresAt: new Date(Date.now() + VIDEO_URL_TTL_MS) },
4017
+ usage: usage2,
4018
+ raw: polled
4019
+ };
4020
+ }
4021
+ if (status === "failed" || status === "expired" || status === "cancelled") {
4022
+ throwArkError(void 0, seedanceErrorMessage(polled.error, status), modelId);
4023
+ }
4024
+ return void 0;
4025
+ }, pollOpts);
4026
+ }
4027
+ };
4028
+ }
3949
4029
  async function generateSeedanceVideo(req, ctx) {
3950
4030
  const modelId = ctx.model.id;
3951
4031
  const taskType = req.taskType;
@@ -3960,7 +4040,7 @@ async function generateSeedanceVideo(req, ctx) {
3960
4040
  });
3961
4041
  }
3962
4042
  const key = arkKey(ctx);
3963
- const requestedDuration = req.durationSeconds ?? 5;
4043
+ const requestedDuration = seedanceInitialDuration(req);
3964
4044
  let duration = requestedDuration;
3965
4045
  if (taskType === "edit") duration = -1;
3966
4046
  const resolution = req.resolution ?? "720p";
@@ -4041,75 +4121,7 @@ async function generateSeedanceVideo(req, ctx) {
4041
4121
  if (!submit.id) throwArkError(void 0, "Seedance task create returned no id", modelId);
4042
4122
  const jobId = submit.id;
4043
4123
  const usage2 = seedanceUsage(ctx, req, taskType === "edit" ? requestedDuration : duration);
4044
- const fetchStatus = async () => {
4045
- const res = await arkFetch(ctx, `${arkBaseUrl(ctx)}${SUBMIT_PATH}/${jobId}`, {
4046
- headers: { Authorization: `Bearer ${key}` }
4047
- });
4048
- if (!res.ok) {
4049
- if (ctx.call.maxRetries !== 0 && (res.status === 429 || res.status >= 500))
4050
- return void 0;
4051
- const text2 = await res.text().catch(() => "");
4052
- throwArkError(res.status, text2, modelId);
4053
- }
4054
- const parsed = SeedanceResponseSchema.safeParse(await res.json());
4055
- if (!parsed.success) return {};
4056
- return parsed.data;
4057
- };
4058
- return {
4059
- jobId,
4060
- async poll() {
4061
- const polled = await fetchStatus();
4062
- if (!polled) return { status: "running" };
4063
- const status = (polled.status ?? "queued").toLowerCase();
4064
- if (status === "succeeded") return { status: "succeeded", raw: polled };
4065
- if (status === "failed" || status === "expired" || status === "cancelled") {
4066
- return { status: "failed", raw: polled };
4067
- }
4068
- if (status === "queued" || status === "pending")
4069
- return { status: "pending", raw: polled };
4070
- return { status: "running", raw: polled };
4071
- },
4072
- async result(opts) {
4073
- const optSignal = opts?.signal ?? ctx.signal;
4074
- const pollOpts = {
4075
- intervalMs: opts?.pollIntervalMs ?? SEEDANCE_POLL_INTERVAL_MS,
4076
- timeoutMs: opts?.timeoutMs ?? RESULT_TIMEOUT_MS
4077
- };
4078
- if (optSignal !== void 0) pollOpts.signal = optSignal;
4079
- return pollUntil(async () => {
4080
- const polled = await fetchStatus();
4081
- if (!polled) return void 0;
4082
- const status = (polled.status ?? "queued").toLowerCase();
4083
- if (status === "succeeded") {
4084
- const url = polled.content?.video_url;
4085
- if (!url)
4086
- throwArkError(
4087
- void 0,
4088
- "Seedance task succeeded but returned no video_url",
4089
- modelId
4090
- );
4091
- if (polled.duration !== void 0 && polled.duration > 0) {
4092
- usage2.quantity = polled.duration;
4093
- usage2.durationSeconds = polled.duration;
4094
- }
4095
- const tokens = polled.usage;
4096
- if (tokens?.total_tokens !== void 0 && tokens.completion_tokens !== void 0) {
4097
- usage2.inputTokens = tokens.total_tokens - tokens.completion_tokens;
4098
- usage2.outputTokens = tokens.completion_tokens;
4099
- }
4100
- return {
4101
- video: { url, expiresAt: new Date(Date.now() + VIDEO_URL_TTL_MS) },
4102
- usage: usage2,
4103
- raw: polled
4104
- };
4105
- }
4106
- if (status === "failed" || status === "expired" || status === "cancelled") {
4107
- throwArkError(void 0, seedanceErrorMessage(polled.error, status), modelId);
4108
- }
4109
- return void 0;
4110
- }, pollOpts);
4111
- }
4112
- };
4124
+ return seedanceJob(ctx, key, jobId, usage2);
4113
4125
  }
4114
4126
 
4115
4127
  // src/adapters/fal.ts
@@ -4251,6 +4263,20 @@ var import_zod11 = require("zod");
4251
4263
 
4252
4264
  // src/catalog/video-tools-spec.ts
4253
4265
  var import_zod10 = require("zod");
4266
+
4267
+ // src/video-length.ts
4268
+ function tenths(seconds2) {
4269
+ return Math.round(seconds2 * 10) / 10;
4270
+ }
4271
+ function overMax(seconds2, max) {
4272
+ return seconds2 > max && tenths(seconds2) > max;
4273
+ }
4274
+ function overTotal(lengths, total) {
4275
+ const sum = (values) => values.reduce((acc, len) => acc + len, 0);
4276
+ return sum(lengths) > total && tenths(sum(lengths.map(tenths))) > total;
4277
+ }
4278
+
4279
+ // src/catalog/video-tools-spec.ts
4254
4280
  var RangeSchema2 = import_zod10.z.object({ min: import_zod10.z.number(), max: import_zod10.z.number() });
4255
4281
  var ToolSchema = import_zod10.z.object({
4256
4282
  kind: import_zod10.z.enum(["upscale", "interpolate", "hdr"]),
@@ -4294,7 +4320,7 @@ function planVideoTool(row, req) {
4294
4320
  return fail("The source video's width, height and duration are required");
4295
4321
  }
4296
4322
  const maxSeconds = row.constraints.sourceVideoSeconds?.max;
4297
- if (maxSeconds !== void 0 && durationSeconds > maxSeconds) {
4323
+ if (maxSeconds !== void 0 && overMax(durationSeconds, maxSeconds)) {
4298
4324
  return fail(`${row.label} takes source videos up to ${maxSeconds} seconds.`);
4299
4325
  }
4300
4326
  const base = {
@@ -8696,7 +8722,7 @@ function videoLengthViolation(model, input) {
8696
8722
  const lengths = (input.referenceVideos ?? []).map(knownLength);
8697
8723
  if (limits?.max !== void 0) {
8698
8724
  const max = limits.max;
8699
- const i = lengths.findIndex((len) => len !== void 0 && len > max);
8725
+ const i = lengths.findIndex((len) => len !== void 0 && overMax(len, max));
8700
8726
  if (i >= 0)
8701
8727
  return `${model.label} takes reference videos up to ${String(max)} s each; video ${i + 1} is ${seconds(lengths[i], max)} s.`;
8702
8728
  }
@@ -8708,14 +8734,17 @@ function videoLengthViolation(model, input) {
8708
8734
  const source = knownLength(input.sourceVideo);
8709
8735
  const sourceLimits = model.constraints.sourceVideoSeconds;
8710
8736
  if (limits?.total !== void 0) {
8711
- const refSum = lengths.reduce((acc, len) => acc + (len ?? 0), 0);
8712
- const sum = refSum + (sourceLimits?.inReferenceTotal ? source ?? 0 : 0);
8713
- if (sum > limits.total)
8737
+ const counted = [
8738
+ ...lengths.filter((len) => len !== void 0),
8739
+ ...sourceLimits?.inReferenceTotal && source !== void 0 ? [source] : []
8740
+ ];
8741
+ const sum = counted.reduce((acc, len) => acc + len, 0);
8742
+ if (overTotal(counted, limits.total))
8714
8743
  return `${model.label} takes reference videos up to ${String(limits.total)} s combined; these add up to ${seconds(sum, limits.total)} s.`;
8715
8744
  }
8716
8745
  if (source === void 0) return void 0;
8717
8746
  const sourceMin = effectiveMin(sourceLimits, input.taskType);
8718
- if (sourceLimits?.max !== void 0 && source > sourceLimits.max)
8747
+ if (sourceLimits?.max !== void 0 && overMax(source, sourceLimits.max))
8719
8748
  return `${model.label} takes a source video up to ${String(sourceLimits.max)} s; this one is ${seconds(source, sourceLimits.max)} s.`;
8720
8749
  if (sourceMin !== void 0 && source < sourceMin)
8721
8750
  return `${model.label} takes a source video of at least ${String(sourceMin)} s; this one is ${seconds(source, sourceMin)} s.`;
@@ -9672,6 +9701,33 @@ function createModelKit(config, adapters) {
9672
9701
  result: () => done
9673
9702
  };
9674
9703
  }
9704
+ function wrapVideoJob(model, job, warnings, startedAt) {
9705
+ const result = job.result.bind(job);
9706
+ let settled;
9707
+ return {
9708
+ ...job,
9709
+ warnings,
9710
+ result(opts) {
9711
+ if (settled) return settled;
9712
+ const pending = result(opts).then(
9713
+ async (out) => {
9714
+ const usage2 = timed(
9715
+ { ...out.usage, model: model.id, unit: model.usage.unit },
9716
+ startedAt
9717
+ );
9718
+ await emit(usage2);
9719
+ return { ...out, usage: usage2 };
9720
+ },
9721
+ (cause) => {
9722
+ settled = void 0;
9723
+ throw cause;
9724
+ }
9725
+ );
9726
+ settled = pending;
9727
+ return pending;
9728
+ }
9729
+ };
9730
+ }
9675
9731
  return {
9676
9732
  catalog,
9677
9733
  describe: (id) => catalog.require(id),
@@ -9756,31 +9812,52 @@ function createModelKit(config, adapters) {
9756
9812
  });
9757
9813
  const startedAt = Date.now();
9758
9814
  const job = await adapter.generateVideo(request, ctx);
9759
- const result = job.result.bind(job);
9760
- let settled;
9761
- return {
9762
- ...job,
9763
- warnings,
9764
- result(opts) {
9765
- if (settled) return settled;
9766
- const pending = result(opts).then(
9767
- async (out) => {
9768
- const usage2 = timed(
9769
- { ...out.usage, model: model.id, unit: model.usage.unit },
9770
- startedAt
9771
- );
9772
- await emit(usage2);
9773
- return { ...out, usage: usage2 };
9774
- },
9775
- (cause) => {
9776
- settled = void 0;
9777
- throw cause;
9778
- }
9779
- );
9780
- settled = pending;
9781
- return pending;
9782
- }
9783
- };
9815
+ return wrapVideoJob(model, job, warnings, startedAt);
9816
+ },
9817
+ async resumeVideo(req, jobId, call = {}) {
9818
+ mediaRetryOptions(call);
9819
+ if (!jobId) {
9820
+ throw new ModelKitError({
9821
+ code: "missing_input",
9822
+ message: "resumeVideo needs a jobId",
9823
+ retryable: false
9824
+ });
9825
+ }
9826
+ if (!isCatalogId(req.model)) {
9827
+ throw new ModelKitError({
9828
+ code: "unsupported_option",
9829
+ message: "resumeVideo needs a catalog model id"
9830
+ });
9831
+ }
9832
+ const model = catalog.require(req.model);
9833
+ if (model.type !== "video")
9834
+ throw new ModelKitError({
9835
+ code: "unsupported_option",
9836
+ message: `${model.id} is not a video model`,
9837
+ model: model.id
9838
+ });
9839
+ const { request: input, warnings: inputWarnings } = videoRequestOf({
9840
+ ...req,
9841
+ model: req.model
9842
+ });
9843
+ const { request, warnings } = resolveVideoOptions(
9844
+ model,
9845
+ input,
9846
+ call.onUnsupported ?? "error"
9847
+ );
9848
+ warnings.push(...inputWarnings);
9849
+ logCoercions(logger, model.id, warnings);
9850
+ const signal = combineSignals(req.abortSignal, call.signal);
9851
+ const { adapter, ctx } = context(model, signal ? { ...call, signal } : call, warnings);
9852
+ if (!adapter.resumeVideo)
9853
+ throw new ModelKitError({
9854
+ code: "unsupported_option",
9855
+ message: `${adapter.id} cannot resume a video job`,
9856
+ model: model.id
9857
+ });
9858
+ const startedAt = Date.now();
9859
+ const job = adapter.resumeVideo(request, jobId, ctx);
9860
+ return wrapVideoJob(model, { ...job, jobId }, warnings, startedAt);
9784
9861
  },
9785
9862
  async generateAudio(req, call = {}) {
9786
9863
  mediaRetryOptions(call);