@shop-os/modelkit 0.4.5 → 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/CHANGELOG.md +5 -0
- package/README.md +4 -0
- package/dist/index.cjs +155 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +155 -95
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,11 @@ header in the release PR.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.4.6] - Unreleased
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `ModelKit.resumeVideo(req, jobId)` and `Adapter.resumeVideo` (ark/Seedance only): reconnect to a video job `generateVideo` submitted earlier, by its `jobId`, without submitting a new job. Other adapters throw `unsupported_option`.
|
|
16
|
+
|
|
12
17
|
## [0.4.5] - Unreleased
|
|
13
18
|
|
|
14
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -51,6 +51,10 @@ await kit.generateText({
|
|
|
51
51
|
await clip.cleanup?.();
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
A video job that outlives its caller can be rejoined with `resumeVideo`: pass the same
|
|
55
|
+
request and the `jobId` from `generateVideo` and await `job.result()` as usual. Only the
|
|
56
|
+
ark adapter (Seedance) supports it today; every other adapter throws `unsupported_option`.
|
|
57
|
+
|
|
54
58
|
### Video tools
|
|
55
59
|
|
|
56
60
|
`video-tool` rows (Topaz upscale, frame interpolation, SDR to HDR on fal) take a clip and
|
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
|
|
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
|
-
|
|
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
|
|
@@ -9689,6 +9701,33 @@ function createModelKit(config, adapters) {
|
|
|
9689
9701
|
result: () => done
|
|
9690
9702
|
};
|
|
9691
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
|
+
}
|
|
9692
9731
|
return {
|
|
9693
9732
|
catalog,
|
|
9694
9733
|
describe: (id) => catalog.require(id),
|
|
@@ -9773,31 +9812,52 @@ function createModelKit(config, adapters) {
|
|
|
9773
9812
|
});
|
|
9774
9813
|
const startedAt = Date.now();
|
|
9775
9814
|
const job = await adapter.generateVideo(request, ctx);
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
}
|
|
9800
|
-
}
|
|
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);
|
|
9801
9861
|
},
|
|
9802
9862
|
async generateAudio(req, call = {}) {
|
|
9803
9863
|
mediaRetryOptions(call);
|