@shop-os/modelkit 0.7.2 → 0.7.3
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 +16 -0
- package/dist/index.cjs +101 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +101 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,22 @@ header in the release PR.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.7.3] - Unreleased
|
|
13
|
+
|
|
14
|
+
ENG-3272 Luma code review follow-ups.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `ray-3.2` (generate) returns the output as `{ url, expiresAt }` instead of downloading it: its 5 s bill needs no bytes. Luma's URL lasts 1 hour; `expiresAt` is 55 minutes after the result. Edit and reframe still download the MP4 to read its length, and that download is now retried.
|
|
18
|
+
- A URL source for edit or reframe sends `media_type` from its extension (`.mov` is `video/quicktime`, `.webm` is `video/webm`), else `video/mp4`.
|
|
19
|
+
- Luma receives the `providerModelId` lowercased, the same spelling the row checks accept.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- An edit or reframe whose MP4 length cannot be read bills the caller's measured `durationSeconds` for an inline (`bytes`) source too, not only for a URL source. Before, it billed 5 s.
|
|
23
|
+
- The MP4 length reader reads boxes with a 64-bit size or a size of 0 (box to end of file), so a `moov` after a large `mdat` is found.
|
|
24
|
+
- A completed video takes only an output typed `video` (or untyped); an output of another type is no longer downloaded as the video.
|
|
25
|
+
- `result({ signal })` also cancels the poll requests and the download, not only the wait between polls.
|
|
26
|
+
- A 200 response that is not JSON is a `ModelKitError`: on submit `provider_rejected`, on a poll a transient `network` error that keeps polling.
|
|
27
|
+
|
|
12
28
|
## [0.7.2] - Unreleased
|
|
13
29
|
|
|
14
30
|
ENG-3272 review fixes. Merged as a second 0.7.1 after FlashVSR's 0.7.1 had published, so npm never got it; released as 0.7.2.
|
package/dist/index.cjs
CHANGED
|
@@ -13808,24 +13808,24 @@ var import_zod25 = require("zod");
|
|
|
13808
13808
|
function mp4DurationSeconds(bytes) {
|
|
13809
13809
|
const moov = findBox(bytes, 0, bytes.length, "moov");
|
|
13810
13810
|
if (!moov) return void 0;
|
|
13811
|
-
const mvhd = findBox(bytes, moov.
|
|
13811
|
+
const mvhd = findBox(bytes, moov.body, moov.end, "mvhd");
|
|
13812
13812
|
if (!mvhd) return void 0;
|
|
13813
13813
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
13814
|
-
const size = mvhd.end - mvhd.
|
|
13815
|
-
if (size <
|
|
13816
|
-
const version = view.getUint8(mvhd.
|
|
13814
|
+
const size = mvhd.end - mvhd.body;
|
|
13815
|
+
if (size < 4) return void 0;
|
|
13816
|
+
const version = view.getUint8(mvhd.body);
|
|
13817
13817
|
if (version === 0) {
|
|
13818
|
-
if (size <
|
|
13819
|
-
const timescale = view.getUint32(mvhd.
|
|
13820
|
-
const duration = view.getUint32(mvhd.
|
|
13818
|
+
if (size < 4 + 4 + 4 + 4 + 4) return void 0;
|
|
13819
|
+
const timescale = view.getUint32(mvhd.body + 4 + 4 + 4);
|
|
13820
|
+
const duration = view.getUint32(mvhd.body + 4 + 4 + 4 + 4);
|
|
13821
13821
|
if (timescale === 0) return void 0;
|
|
13822
13822
|
return duration / timescale;
|
|
13823
13823
|
}
|
|
13824
13824
|
if (version === 1) {
|
|
13825
|
-
if (size <
|
|
13826
|
-
const timescale = view.getUint32(mvhd.
|
|
13827
|
-
const high = view.getUint32(mvhd.
|
|
13828
|
-
const low = view.getUint32(mvhd.
|
|
13825
|
+
if (size < 4 + 8 + 8 + 4 + 8) return void 0;
|
|
13826
|
+
const timescale = view.getUint32(mvhd.body + 4 + 8 + 8);
|
|
13827
|
+
const high = view.getUint32(mvhd.body + 4 + 8 + 8 + 4);
|
|
13828
|
+
const low = view.getUint32(mvhd.body + 4 + 8 + 8 + 4 + 4);
|
|
13829
13829
|
if (timescale === 0) return void 0;
|
|
13830
13830
|
return (high * 2 ** 32 + low) / timescale;
|
|
13831
13831
|
}
|
|
@@ -13835,15 +13835,23 @@ function findBox(bytes, from, to, type) {
|
|
|
13835
13835
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
13836
13836
|
let offset = from;
|
|
13837
13837
|
while (offset + 8 <= to) {
|
|
13838
|
-
|
|
13838
|
+
let size = view.getUint32(offset);
|
|
13839
|
+
let header = 8;
|
|
13839
13840
|
const name = String.fromCharCode(
|
|
13840
13841
|
view.getUint8(offset + 4),
|
|
13841
13842
|
view.getUint8(offset + 5),
|
|
13842
13843
|
view.getUint8(offset + 6),
|
|
13843
13844
|
view.getUint8(offset + 7)
|
|
13844
13845
|
);
|
|
13845
|
-
if (size
|
|
13846
|
-
|
|
13846
|
+
if (size === 1) {
|
|
13847
|
+
if (offset + 16 > to) return void 0;
|
|
13848
|
+
size = view.getUint32(offset + 8) * 2 ** 32 + view.getUint32(offset + 12);
|
|
13849
|
+
header = 16;
|
|
13850
|
+
} else if (size === 0) {
|
|
13851
|
+
size = to - offset;
|
|
13852
|
+
}
|
|
13853
|
+
if (size < header || offset + size > to) return void 0;
|
|
13854
|
+
if (name === type) return { body: offset + header, end: offset + size };
|
|
13847
13855
|
offset += size;
|
|
13848
13856
|
}
|
|
13849
13857
|
return void 0;
|
|
@@ -13856,6 +13864,7 @@ var IMAGE_TIMEOUT_MS = 5 * 6e4;
|
|
|
13856
13864
|
var VIDEO_POLL_INTERVAL_MS = 5e3;
|
|
13857
13865
|
var VIDEO_TIMEOUT_MS = 20 * 6e4;
|
|
13858
13866
|
var PROMPT_MAX_CHARS = 6e3;
|
|
13867
|
+
var OUTPUT_URL_TTL_MS = 55 * 6e4;
|
|
13859
13868
|
var LUMA_IMAGE_MODEL_IDS = /* @__PURE__ */ new Set(["uni-1", "uni-1-max"]);
|
|
13860
13869
|
var LUMA_VIDEO_MODEL_ID = "ray-3.2";
|
|
13861
13870
|
var LUMA_VIDEO_MODES = /* @__PURE__ */ new Map([
|
|
@@ -13874,6 +13883,9 @@ function lumaRowRefusal(row2) {
|
|
|
13874
13883
|
}
|
|
13875
13884
|
return `luma has no ${row2.type} rows`;
|
|
13876
13885
|
}
|
|
13886
|
+
function lumaModel(ctx) {
|
|
13887
|
+
return ctx.model.providerModelId.toLowerCase();
|
|
13888
|
+
}
|
|
13877
13889
|
function lumaBase(ctx) {
|
|
13878
13890
|
return ctx.endpoints?.luma ?? DEFAULT_BASE_URL;
|
|
13879
13891
|
}
|
|
@@ -13916,8 +13928,23 @@ function imageRef(ref) {
|
|
|
13916
13928
|
if ("url" in ref) return { url: ref.url };
|
|
13917
13929
|
return { data: mediaRefToBase64(ref.bytes), media_type: ref.mime };
|
|
13918
13930
|
}
|
|
13931
|
+
var VIDEO_TYPES_BY_EXTENSION = /* @__PURE__ */ new Map([
|
|
13932
|
+
["mp4", "video/mp4"],
|
|
13933
|
+
["m4v", "video/mp4"],
|
|
13934
|
+
["mov", "video/quicktime"],
|
|
13935
|
+
["webm", "video/webm"]
|
|
13936
|
+
]);
|
|
13937
|
+
function videoUrlType(url) {
|
|
13938
|
+
let path = url;
|
|
13939
|
+
try {
|
|
13940
|
+
path = new URL(url).pathname;
|
|
13941
|
+
} catch {
|
|
13942
|
+
}
|
|
13943
|
+
const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
|
|
13944
|
+
return VIDEO_TYPES_BY_EXTENSION.get(extension) ?? "video/mp4";
|
|
13945
|
+
}
|
|
13919
13946
|
function videoRef(ref) {
|
|
13920
|
-
if ("url" in ref) return { url: ref.url, media_type:
|
|
13947
|
+
if ("url" in ref) return { url: ref.url, media_type: videoUrlType(ref.url) };
|
|
13921
13948
|
const mediaType = ref.mime.startsWith("video/") ? ref.mime : "video/mp4";
|
|
13922
13949
|
return { data: mediaRefToBase64(ref.bytes), media_type: mediaType };
|
|
13923
13950
|
}
|
|
@@ -14081,14 +14108,22 @@ async function lumaRequest(ctx, url, body) {
|
|
|
14081
14108
|
}
|
|
14082
14109
|
return res;
|
|
14083
14110
|
}
|
|
14111
|
+
async function parseGeneration(res) {
|
|
14112
|
+
try {
|
|
14113
|
+
const parsed = lumaGenerationSchema.safeParse(await res.json());
|
|
14114
|
+
return parsed.success ? parsed.data : void 0;
|
|
14115
|
+
} catch {
|
|
14116
|
+
return "not_json";
|
|
14117
|
+
}
|
|
14118
|
+
}
|
|
14084
14119
|
async function submitGeneration(ctx, body) {
|
|
14085
14120
|
const retryOptions = mediaRetryOptions(ctx.call, ctx.signal);
|
|
14086
14121
|
const res = await withRetry(
|
|
14087
14122
|
() => lumaRequest(ctx, `${lumaBase(ctx)}/v1/generations`, JSON.stringify(body)),
|
|
14088
14123
|
retryOptions
|
|
14089
14124
|
);
|
|
14090
|
-
const
|
|
14091
|
-
if (
|
|
14125
|
+
const generation = await parseGeneration(res);
|
|
14126
|
+
if (generation === void 0 || generation === "not_json") {
|
|
14092
14127
|
throw new ModelKitError({
|
|
14093
14128
|
code: "provider_rejected",
|
|
14094
14129
|
message: "Luma submit returned no generation id",
|
|
@@ -14097,13 +14132,20 @@ async function submitGeneration(ctx, body) {
|
|
|
14097
14132
|
retryable: false
|
|
14098
14133
|
});
|
|
14099
14134
|
}
|
|
14100
|
-
return
|
|
14135
|
+
return generation;
|
|
14101
14136
|
}
|
|
14102
14137
|
async function readGeneration(ctx, id) {
|
|
14103
14138
|
const res = await lumaRequest(ctx, `${lumaBase(ctx)}/v1/generations/${id}`);
|
|
14104
|
-
const
|
|
14105
|
-
|
|
14106
|
-
|
|
14139
|
+
const generation = await parseGeneration(res);
|
|
14140
|
+
if (generation === "not_json") {
|
|
14141
|
+
throw new ModelKitError({
|
|
14142
|
+
code: "network",
|
|
14143
|
+
message: "Luma returned a poll response that is not JSON",
|
|
14144
|
+
provider: "luma",
|
|
14145
|
+
model: ctx.model.id
|
|
14146
|
+
});
|
|
14147
|
+
}
|
|
14148
|
+
if (generation === void 0) {
|
|
14107
14149
|
throw new ModelKitError({
|
|
14108
14150
|
code: "provider_rejected",
|
|
14109
14151
|
message: "Luma returned an unreadable generation",
|
|
@@ -14112,14 +14154,12 @@ async function readGeneration(ctx, id) {
|
|
|
14112
14154
|
retryable: false
|
|
14113
14155
|
});
|
|
14114
14156
|
}
|
|
14115
|
-
return
|
|
14157
|
+
return generation;
|
|
14116
14158
|
}
|
|
14117
14159
|
function outputUrl(generation, type) {
|
|
14118
14160
|
const outputs = generation.output ?? [];
|
|
14119
|
-
const match = outputs.find((item) => item.type === type && item.url);
|
|
14120
|
-
|
|
14121
|
-
const fallback = outputs.find((item) => item.url);
|
|
14122
|
-
return fallback?.url;
|
|
14161
|
+
const match = outputs.find((item) => item.type === type && item.url) ?? outputs.find((item) => item.type === void 0 && item.url);
|
|
14162
|
+
return match?.url;
|
|
14123
14163
|
}
|
|
14124
14164
|
async function readGenerationOnce(ctx, id) {
|
|
14125
14165
|
try {
|
|
@@ -14158,21 +14198,22 @@ async function checkGeneration(ctx, id, type) {
|
|
|
14158
14198
|
return void 0;
|
|
14159
14199
|
}
|
|
14160
14200
|
async function downloadBytes(ctx, url, fallback) {
|
|
14161
|
-
|
|
14162
|
-
|
|
14163
|
-
|
|
14164
|
-
|
|
14165
|
-
|
|
14166
|
-
|
|
14167
|
-
|
|
14168
|
-
|
|
14169
|
-
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14201
|
+
const { bytes, mime: header } = await withRetry(
|
|
14202
|
+
async () => {
|
|
14203
|
+
try {
|
|
14204
|
+
return await mediaRefToBytes({ url }, ctx.fetch, {
|
|
14205
|
+
...ctx.signal !== void 0 && { signal: ctx.signal },
|
|
14206
|
+
provider: "luma",
|
|
14207
|
+
model: ctx.model.id
|
|
14208
|
+
});
|
|
14209
|
+
} catch (cause) {
|
|
14210
|
+
throw classifyThrown("luma", cause, ctx.model.id);
|
|
14211
|
+
}
|
|
14212
|
+
},
|
|
14213
|
+
mediaRetryOptions(ctx.call, ctx.signal)
|
|
14214
|
+
);
|
|
14174
14215
|
const kind = fallback.split("/")[0];
|
|
14175
|
-
const usable = header
|
|
14216
|
+
const usable = header.startsWith(`${kind}/`);
|
|
14176
14217
|
return { bytes, mime: usable ? header : fallback };
|
|
14177
14218
|
}
|
|
14178
14219
|
function imageUsage2(ctx) {
|
|
@@ -14187,15 +14228,12 @@ function imageUsage2(ctx) {
|
|
|
14187
14228
|
}
|
|
14188
14229
|
var GENERATE_SECONDS = 5;
|
|
14189
14230
|
var MAX_PLAUSIBLE_SECONDS = 60;
|
|
14190
|
-
function
|
|
14191
|
-
if (mode === "generate") return GENERATE_SECONDS;
|
|
14231
|
+
function editedSeconds(req, bytes) {
|
|
14192
14232
|
const measured = mp4DurationSeconds(bytes);
|
|
14193
|
-
const
|
|
14194
|
-
const length = measured !== void 0 && measured > 0 && measured <= MAX_PLAUSIBLE_SECONDS ? measured : source2 !== void 0 && "url" in source2 ? source2.durationSeconds : void 0;
|
|
14233
|
+
const length = measured !== void 0 && measured > 0 && measured <= MAX_PLAUSIBLE_SECONDS ? measured : req.sourceVideo?.durationSeconds;
|
|
14195
14234
|
return Math.max(length ?? 0, GENERATE_SECONDS);
|
|
14196
14235
|
}
|
|
14197
|
-
function videoUsage4(ctx, req, mode,
|
|
14198
|
-
const seconds3 = billedSeconds(req, mode, bytes);
|
|
14236
|
+
function videoUsage4(ctx, req, mode, seconds3) {
|
|
14199
14237
|
const event = {
|
|
14200
14238
|
model: ctx.model.id,
|
|
14201
14239
|
provider: "luma",
|
|
@@ -14210,7 +14248,7 @@ function videoUsage4(ctx, req, mode, bytes) {
|
|
|
14210
14248
|
}
|
|
14211
14249
|
function imageBody(req, ctx, outputFormat) {
|
|
14212
14250
|
const prompt = checkPrompt(req, ctx);
|
|
14213
|
-
const model = ctx
|
|
14251
|
+
const model = lumaModel(ctx);
|
|
14214
14252
|
const ratio = req.aspectRatio !== "adaptive" ? req.aspectRatio : void 0;
|
|
14215
14253
|
const [source2, ...rest] = req.references ?? [];
|
|
14216
14254
|
if (source2 === void 0) {
|
|
@@ -14238,7 +14276,7 @@ function imageBody(req, ctx, outputFormat) {
|
|
|
14238
14276
|
}
|
|
14239
14277
|
function videoBody(req, ctx, mode) {
|
|
14240
14278
|
const prompt = checkPrompt(req, ctx);
|
|
14241
|
-
const model = ctx
|
|
14279
|
+
const model = lumaModel(ctx);
|
|
14242
14280
|
const hasReferences = (req.referenceImages ?? []).length + (req.referenceVideos ?? []).length + (req.referenceAudios ?? []).length > 0;
|
|
14243
14281
|
if (hasReferences) {
|
|
14244
14282
|
refuseOrDrop(
|
|
@@ -14320,7 +14358,7 @@ function videoBody(req, ctx, mode) {
|
|
|
14320
14358
|
async function generateLumaImage(req, ctx) {
|
|
14321
14359
|
throwIfAborted(ctx.signal);
|
|
14322
14360
|
lumaKey(ctx);
|
|
14323
|
-
if (!LUMA_IMAGE_MODEL_IDS.has(ctx
|
|
14361
|
+
if (!LUMA_IMAGE_MODEL_IDS.has(lumaModel(ctx))) {
|
|
14324
14362
|
throw new ModelKitError({
|
|
14325
14363
|
code: "unsupported_option",
|
|
14326
14364
|
message: `luma has no image mapping for "${ctx.model.providerModelId}"`,
|
|
@@ -14353,16 +14391,25 @@ function lumaVideoJob(ctx, req, mode, id) {
|
|
|
14353
14391
|
return { status: "pending" };
|
|
14354
14392
|
},
|
|
14355
14393
|
async result(opts) {
|
|
14356
|
-
const signal = opts?.signal
|
|
14357
|
-
const
|
|
14394
|
+
const signal = combineSignals(ctx.signal, opts?.signal);
|
|
14395
|
+
const jobCtx = { ...ctx, ...signal !== void 0 && { signal } };
|
|
14396
|
+
const url = await pollUntil(checkGeneration.bind(null, jobCtx, id, "video"), {
|
|
14358
14397
|
intervalMs: opts?.pollIntervalMs ?? VIDEO_POLL_INTERVAL_MS,
|
|
14359
14398
|
timeoutMs: opts?.timeoutMs ?? VIDEO_TIMEOUT_MS,
|
|
14360
14399
|
...signal !== void 0 && { signal }
|
|
14361
14400
|
});
|
|
14362
|
-
|
|
14401
|
+
if (mode === "generate") {
|
|
14402
|
+
const result2 = {
|
|
14403
|
+
video: { url, expiresAt: new Date(Date.now() + OUTPUT_URL_TTL_MS) },
|
|
14404
|
+
usage: videoUsage4(jobCtx, req, mode, GENERATE_SECONDS),
|
|
14405
|
+
raw: { id }
|
|
14406
|
+
};
|
|
14407
|
+
return result2;
|
|
14408
|
+
}
|
|
14409
|
+
const video = await downloadBytes(jobCtx, url, "video/mp4");
|
|
14363
14410
|
const result = {
|
|
14364
14411
|
video,
|
|
14365
|
-
usage: videoUsage4(
|
|
14412
|
+
usage: videoUsage4(jobCtx, req, mode, editedSeconds(req, video.bytes)),
|
|
14366
14413
|
raw: { id }
|
|
14367
14414
|
};
|
|
14368
14415
|
return result;
|
|
@@ -14372,7 +14419,7 @@ function lumaVideoJob(ctx, req, mode, id) {
|
|
|
14372
14419
|
async function generateLumaVideo(req, ctx) {
|
|
14373
14420
|
throwIfAborted(ctx.signal);
|
|
14374
14421
|
lumaKey(ctx);
|
|
14375
|
-
if (ctx
|
|
14422
|
+
if (lumaModel(ctx) !== LUMA_VIDEO_MODEL_ID) {
|
|
14376
14423
|
throw new ModelKitError({
|
|
14377
14424
|
code: "unsupported_option",
|
|
14378
14425
|
message: `luma has no video mapping for "${ctx.model.providerModelId}"`,
|