@mediaruntime/cli 1.3.0 → 1.4.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.
- package/CHANGELOG.md +10 -0
- package/README.md +6 -1
- package/dist/cli.js +173 -6
- package/dist/cli.js.map +1 -1
- package/docs/CLI_SDD.md +10 -0
- package/docs/clipping.md +113 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.0 — 2026-09-13
|
|
4
|
+
|
|
5
|
+
- Add capability-discovered Clip Candidates and Video Clip commands, bounded ranges,
|
|
6
|
+
original or vertical blurred framing, keyword controls, and opt-in burned captions.
|
|
7
|
+
- Attach local JSON plans/segment arrays, SRT, or VTT with source timestamps; reuse
|
|
8
|
+
transcripts for analysis without another Whisper run. Validate bounded UTF-8 attachments
|
|
9
|
+
before submission and preserve the normal wait and verified ZIP download flow.
|
|
10
|
+
- Require `@mediaruntime/node` 1.4.0 for clipping serialization and report parity.
|
|
11
|
+
- Correct the executable version and regression-test it against the package manifest.
|
|
12
|
+
|
|
3
13
|
## 1.3.0 — 2026-08-31
|
|
4
14
|
|
|
5
15
|
- Add complete Hosted Sticker Runtime commands for collection lifecycle, pack binding,
|
package/README.md
CHANGED
|
@@ -4,7 +4,12 @@ Official command-line client for MediaRuntime. It submits media, waits for jobs,
|
|
|
4
4
|
the canonical ZIP output bundle, inspects account jobs, and sends correctly signed
|
|
5
5
|
synthetic webhooks to a local receiver.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Version `1.4.0` adds [clipping commands](docs/clipping.md) for analysis,
|
|
8
|
+
manual ranges, vertical blur-fill, and JSON/SRT/VTT transcript attachments. It requires
|
|
9
|
+
Node SDK 1.4.0 or newer and a gateway/engine deployment supporting the clipping
|
|
10
|
+
presets. The live capability catalog determines which presets are available.
|
|
11
|
+
|
|
12
|
+
The documented `1.x` command names, flags, JSON envelopes, exit
|
|
8
13
|
codes, and credential precedence follow semantic versioning. Breaking changes require a
|
|
9
14
|
new major version; additive commands and fields may ship in minor releases.
|
|
10
15
|
|
package/dist/cli.js
CHANGED
|
@@ -608,6 +608,93 @@ async function runJobsCommand(args, dependencies) {
|
|
|
608
608
|
throw new UsageError("Usage: mediaruntime jobs <list|get>");
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
+
// src/clipTranscript.ts
|
|
612
|
+
import { open } from "fs/promises";
|
|
613
|
+
var MAX_FILE_BYTES = 1024 * 1024;
|
|
614
|
+
function timestamp(value) {
|
|
615
|
+
const match = /^(?:(\d{2,}):)?(\d{2}):(\d{2})[.,](\d{3})$/.exec(value);
|
|
616
|
+
if (!match || Number(match[2]) > 59 || Number(match[3]) > 59) {
|
|
617
|
+
throw new UsageError("Transcript cue times must use HH:MM:SS,mmm (SRT) or MM:SS.mmm / HH:MM:SS.mmm (VTT)");
|
|
618
|
+
}
|
|
619
|
+
return Number(match[1] ?? 0) * 3600 + Number(match[2]) * 60 + Number(match[3]) + Number(match[4]) / 1e3;
|
|
620
|
+
}
|
|
621
|
+
function subtitleSegments(text, vtt) {
|
|
622
|
+
if (vtt && !/^WEBVTT(?:\s|$)/.test(text)) throw new UsageError("VTT transcripts must begin with WEBVTT");
|
|
623
|
+
const segments = [];
|
|
624
|
+
for (const block of text.split(/\n[ \t]*\n/)) {
|
|
625
|
+
if (vtt && /^(WEBVTT(?:\s|$)|NOTE(?:\s|$)|STYLE(?:\s|$)|REGION(?:\s|$))/.test(block)) continue;
|
|
626
|
+
const lines = block.split("\n");
|
|
627
|
+
const timing = lines[0]?.includes("-->") ? 0 : 1;
|
|
628
|
+
const match = /^(\S+)\s+-->\s+(\S+)(?:\s+.*)?$/.exec(lines[timing] ?? "");
|
|
629
|
+
if (!match) throw new UsageError("Transcript contains an invalid SRT/VTT cue");
|
|
630
|
+
const entities = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
|
|
631
|
+
const cue = lines.slice(timing + 1).join("\n").replace(/<[^>]*>/g, "").replace(/&(amp|lt|gt|quot|apos|nbsp);/g, (_, name) => entities[name]);
|
|
632
|
+
segments.push({ startTimeSec: timestamp(match[1]), endTimeSec: timestamp(match[2]), text: cue });
|
|
633
|
+
}
|
|
634
|
+
return segments;
|
|
635
|
+
}
|
|
636
|
+
async function readClipTranscript(path) {
|
|
637
|
+
if (!/\.(json|srt|vtt)$/i.test(path)) throw new UsageError("--clip-transcript accepts a local .json, .srt, or .vtt file");
|
|
638
|
+
let data;
|
|
639
|
+
try {
|
|
640
|
+
const file = await open(path, "r");
|
|
641
|
+
try {
|
|
642
|
+
if (!(await file.stat()).isFile()) throw new UsageError("Clip transcript must be a regular file");
|
|
643
|
+
const buffer = Buffer.alloc(MAX_FILE_BYTES + 1);
|
|
644
|
+
let size = 0;
|
|
645
|
+
while (size < buffer.length) {
|
|
646
|
+
const read = await file.read(buffer, size, buffer.length - size, null);
|
|
647
|
+
if (!read.bytesRead) break;
|
|
648
|
+
size += read.bytesRead;
|
|
649
|
+
}
|
|
650
|
+
if (size > MAX_FILE_BYTES) throw new UsageError("Clip transcript file must be at most 1 MiB");
|
|
651
|
+
data = buffer.subarray(0, size);
|
|
652
|
+
} finally {
|
|
653
|
+
await file.close();
|
|
654
|
+
}
|
|
655
|
+
} catch (error) {
|
|
656
|
+
if (error instanceof UsageError) throw error;
|
|
657
|
+
throw new UsageError("Cannot read clip transcript file; check its path and permissions");
|
|
658
|
+
}
|
|
659
|
+
let text;
|
|
660
|
+
try {
|
|
661
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(data).replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").trim();
|
|
662
|
+
} catch {
|
|
663
|
+
throw new UsageError("Clip transcript must be valid UTF-8 text");
|
|
664
|
+
}
|
|
665
|
+
let segments;
|
|
666
|
+
if (/\.json$/i.test(path)) {
|
|
667
|
+
let value;
|
|
668
|
+
try {
|
|
669
|
+
value = JSON.parse(text);
|
|
670
|
+
} catch {
|
|
671
|
+
throw new UsageError("Clip transcript must be valid JSON");
|
|
672
|
+
}
|
|
673
|
+
segments = Array.isArray(value) ? value : value && typeof value === "object" && "transcript" in value ? value.transcript : void 0;
|
|
674
|
+
} else segments = subtitleSegments(text, /\.vtt$/i.test(path));
|
|
675
|
+
if (!Array.isArray(segments) || !segments.length || segments.length > 2e3) {
|
|
676
|
+
throw new UsageError("Clip transcript must contain 1 to 2000 segments");
|
|
677
|
+
}
|
|
678
|
+
let previous = -1;
|
|
679
|
+
let totalBytes = 0;
|
|
680
|
+
return segments.map((value) => {
|
|
681
|
+
const segment = value && typeof value === "object" ? value : {};
|
|
682
|
+
const start = segment.start_time_sec ?? segment.startTimeSec;
|
|
683
|
+
const end = segment.end_time_sec ?? segment.endTimeSec;
|
|
684
|
+
const cue = segment.text;
|
|
685
|
+
if (typeof start !== "number" || !Number.isFinite(start) || typeof end !== "number" || !Number.isFinite(end) || start < 0 || end <= start || end > 604800 || start < previous) {
|
|
686
|
+
throw new UsageError("Transcript segments require ordered finite source times with end after start (within 604800 seconds)");
|
|
687
|
+
}
|
|
688
|
+
if (typeof cue !== "string" || !cue.trim() || Buffer.byteLength(cue, "utf8") > 2e3) {
|
|
689
|
+
throw new UsageError("Transcript segments require nonblank text of at most 2000 UTF-8 bytes");
|
|
690
|
+
}
|
|
691
|
+
previous = start;
|
|
692
|
+
totalBytes += Buffer.byteLength(cue, "utf8");
|
|
693
|
+
if (totalBytes > 256 * 1024) throw new UsageError("Transcript text exceeds 256 KiB");
|
|
694
|
+
return { startTimeSec: start, endTimeSec: end, text: cue.trim() };
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
611
698
|
// src/commands/run.ts
|
|
612
699
|
var UNSUCCESSFUL_TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["FAILED", "REJECTED", "PARTIAL"]);
|
|
613
700
|
var OUTPUT_ALIASES = /* @__PURE__ */ new Set([
|
|
@@ -665,6 +752,9 @@ function parseRunOptions(args) {
|
|
|
665
752
|
const contactSheet = {};
|
|
666
753
|
const image = {};
|
|
667
754
|
const audiogram = {};
|
|
755
|
+
const clip = {};
|
|
756
|
+
const clipAnalysis = {};
|
|
757
|
+
let clipTranscriptFile;
|
|
668
758
|
const privacyRedaction = {};
|
|
669
759
|
const privacyDetectors = [];
|
|
670
760
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -794,6 +884,36 @@ function parseRunOptions(args) {
|
|
|
794
884
|
} else if (argument === "--image-min-quality") {
|
|
795
885
|
image.minQuality = boundedNumber(args, index, argument, 1, 100, true);
|
|
796
886
|
index += 1;
|
|
887
|
+
} else if (argument === "--clip-start") {
|
|
888
|
+
clip.startTimeSec = boundedNumber(args, index, argument, 0, 604800);
|
|
889
|
+
index += 1;
|
|
890
|
+
} else if (argument === "--clip-duration") {
|
|
891
|
+
clip.durationSec = boundedNumber(args, index, argument, 0.1, 300);
|
|
892
|
+
index += 1;
|
|
893
|
+
} else if (argument === "--clip-layout") {
|
|
894
|
+
const value = optionValue2(args, index, argument);
|
|
895
|
+
if (value !== "original" && value !== "vertical_blur") {
|
|
896
|
+
throw new UsageError("--clip-layout must be original or vertical_blur");
|
|
897
|
+
}
|
|
898
|
+
clip.layout = value;
|
|
899
|
+
index += 1;
|
|
900
|
+
} else if (argument === "--clip-captions") {
|
|
901
|
+
clip.burnCaptions = true;
|
|
902
|
+
} else if (argument === "--clip-transcript") {
|
|
903
|
+
clipTranscriptFile = optionValue2(args, index, argument);
|
|
904
|
+
index += 1;
|
|
905
|
+
} else if (argument === "--clip-min-duration") {
|
|
906
|
+
clipAnalysis.minDurationSec = boundedNumber(args, index, argument, 1, 300);
|
|
907
|
+
index += 1;
|
|
908
|
+
} else if (argument === "--clip-max-duration") {
|
|
909
|
+
clipAnalysis.maxDurationSec = boundedNumber(args, index, argument, 1, 300);
|
|
910
|
+
index += 1;
|
|
911
|
+
} else if (argument === "--clip-count") {
|
|
912
|
+
clipAnalysis.maxCandidates = boundedNumber(args, index, argument, 1, 20, true);
|
|
913
|
+
index += 1;
|
|
914
|
+
} else if (argument === "--clip-keyword") {
|
|
915
|
+
(clipAnalysis.keywords ??= []).push(optionValue2(args, index, argument));
|
|
916
|
+
index += 1;
|
|
797
917
|
} else if (argument === "--audiogram-artwork") {
|
|
798
918
|
audiogram.artworkSource = optionValue2(args, index, argument);
|
|
799
919
|
index += 1;
|
|
@@ -994,6 +1114,33 @@ function parseRunOptions(args) {
|
|
|
994
1114
|
}
|
|
995
1115
|
}
|
|
996
1116
|
const hasPrivacyOptions = privacyDetectors.length > 0 || Object.keys(privacyRedaction).length > 0;
|
|
1117
|
+
const clipPreset = outputs.length === 1 && outputs[0]?.kind === "preset" ? outputs[0].value : void 0;
|
|
1118
|
+
const hasClip = Object.keys(clip).length > 0 || clipPreset === "video_clip_v1";
|
|
1119
|
+
const hasClipAnalysis = Object.keys(clipAnalysis).length > 0 || clipPreset === "clip_candidates_v1";
|
|
1120
|
+
if (hasClip && (recipe || clipPreset !== "video_clip_v1" || hasClipAnalysis)) {
|
|
1121
|
+
throw new UsageError("clip render options require exactly one video_clip_v1 --preset");
|
|
1122
|
+
}
|
|
1123
|
+
if (hasClip && (clip.startTimeSec === void 0 || clip.durationSec === void 0)) {
|
|
1124
|
+
throw new UsageError("video_clip_v1 requires --clip-start and --clip-duration");
|
|
1125
|
+
}
|
|
1126
|
+
if (hasClipAnalysis && (recipe || clipPreset !== "clip_candidates_v1")) {
|
|
1127
|
+
throw new UsageError("clip analysis options require exactly one clip_candidates_v1 --preset");
|
|
1128
|
+
}
|
|
1129
|
+
if ((clipAnalysis.minDurationSec ?? 15) > (clipAnalysis.maxDurationSec ?? 60)) {
|
|
1130
|
+
throw new UsageError("--clip-min-duration must not exceed --clip-max-duration");
|
|
1131
|
+
}
|
|
1132
|
+
if ((clipAnalysis.keywords?.length ?? 0) > 20) {
|
|
1133
|
+
throw new UsageError("At most 20 --clip-keyword values are supported");
|
|
1134
|
+
}
|
|
1135
|
+
if (clipAnalysis.keywords?.some((keyword) => !keyword.trim() || Buffer.byteLength(keyword, "utf8") > 100)) {
|
|
1136
|
+
throw new UsageError("Each --clip-keyword must contain 1 to 100 UTF-8 bytes of nonblank text");
|
|
1137
|
+
}
|
|
1138
|
+
if (clipTranscriptFile && !hasClip && !hasClipAnalysis) {
|
|
1139
|
+
throw new UsageError("--clip-transcript requires a clipping preset");
|
|
1140
|
+
}
|
|
1141
|
+
if (clip.burnCaptions && !clipTranscriptFile) {
|
|
1142
|
+
throw new UsageError("--clip-captions requires --clip-transcript with source timestamps");
|
|
1143
|
+
}
|
|
997
1144
|
if (hasPrivacyOptions) {
|
|
998
1145
|
if (recipe) throw new UsageError("privacy-redaction options cannot be combined with --recipe");
|
|
999
1146
|
if (privacyDetectors.length === 0) {
|
|
@@ -1021,6 +1168,9 @@ function parseRunOptions(args) {
|
|
|
1021
1168
|
...hasPlaceholderOptions ? { placeholders } : {},
|
|
1022
1169
|
...hasContactSheetOptions ? { contactSheet } : {},
|
|
1023
1170
|
...hasImageOptions ? { image } : {},
|
|
1171
|
+
...hasClip ? { clip } : {},
|
|
1172
|
+
...hasClipAnalysis ? { clipAnalysis } : {},
|
|
1173
|
+
...clipTranscriptFile ? { clipTranscriptFile } : {},
|
|
1024
1174
|
...hasAudiogramOptions || audiogramSelections.length > 0 ? { audiogram } : {},
|
|
1025
1175
|
...hasPrivacyOptions ? { privacyRedaction } : {}
|
|
1026
1176
|
};
|
|
@@ -1047,7 +1197,7 @@ async function resolveAudiogram(value, uploads) {
|
|
|
1047
1197
|
...captionsSource === void 0 ? {} : { captionsSource, burnCaptions: true }
|
|
1048
1198
|
};
|
|
1049
1199
|
}
|
|
1050
|
-
async function resolveOutputs(selections, capabilitiesClient, animation, placeholders, contactSheet, image, audiogram, privacyRedaction) {
|
|
1200
|
+
async function resolveOutputs(selections, capabilitiesClient, animation, placeholders, contactSheet, image, audiogram, privacyRedaction, clip, clipAnalysis) {
|
|
1051
1201
|
if (!privacyRedaction && !selections.some((selection) => selection.kind === "preset")) {
|
|
1052
1202
|
return selections.map((selection) => selection.value);
|
|
1053
1203
|
}
|
|
@@ -1084,6 +1234,8 @@ async function resolveOutputs(selections, capabilitiesClient, animation, placeho
|
|
|
1084
1234
|
}
|
|
1085
1235
|
if (audiogram) output3.audiogram = audiogram;
|
|
1086
1236
|
if (privacyRedaction) output3.privacyRedaction = privacyRedaction;
|
|
1237
|
+
if (clip) output3.clip = clip;
|
|
1238
|
+
if (clipAnalysis) output3.clipAnalysis = clipAnalysis;
|
|
1087
1239
|
return output3;
|
|
1088
1240
|
});
|
|
1089
1241
|
}
|
|
@@ -1146,6 +1298,14 @@ async function runCommand(args, dependencies) {
|
|
|
1146
1298
|
isLocalSource || hasLocalAudiogramAsset ? "Uploading local media and creating job\u2026" : "Creating job\u2026"
|
|
1147
1299
|
);
|
|
1148
1300
|
try {
|
|
1301
|
+
if ((options.clip || options.clipAnalysis) && !("getClipCandidates" in dependencies.jobs)) {
|
|
1302
|
+
throw new UsageError("Clipping requires the MediaRuntime Node SDK release with getClipCandidates support");
|
|
1303
|
+
}
|
|
1304
|
+
if (options.clipTranscriptFile) {
|
|
1305
|
+
const transcript = await readClipTranscript(options.clipTranscriptFile);
|
|
1306
|
+
if (options.clip) options.clip.transcript = transcript;
|
|
1307
|
+
if (options.clipAnalysis) options.clipAnalysis.transcript = transcript;
|
|
1308
|
+
}
|
|
1149
1309
|
const audiogram = options.audiogram === void 0 ? void 0 : await resolveAudiogram(options.audiogram, dependencies.uploads);
|
|
1150
1310
|
const outputs = options.recipe ? void 0 : await resolveOutputs(
|
|
1151
1311
|
options.outputs,
|
|
@@ -1155,7 +1315,9 @@ async function runCommand(args, dependencies) {
|
|
|
1155
1315
|
options.contactSheet,
|
|
1156
1316
|
options.image,
|
|
1157
1317
|
audiogram,
|
|
1158
|
-
options.privacyRedaction
|
|
1318
|
+
options.privacyRedaction,
|
|
1319
|
+
options.clip,
|
|
1320
|
+
options.clipAnalysis
|
|
1159
1321
|
);
|
|
1160
1322
|
const params = {
|
|
1161
1323
|
source: options.source,
|
|
@@ -2032,7 +2194,7 @@ async function runTriggerCommand(args, dependencies = {}) {
|
|
|
2032
2194
|
writeStdout: dependencies.writeStdout ?? ((text) => process.stdout.write(text))
|
|
2033
2195
|
};
|
|
2034
2196
|
const secret = await resolveSecret(parsed, deps);
|
|
2035
|
-
const
|
|
2197
|
+
const timestamp2 = Math.floor(deps.now() / 1e3);
|
|
2036
2198
|
const unique = deps.randomUUID();
|
|
2037
2199
|
const eventId = `evt_cli_${unique}`;
|
|
2038
2200
|
const jobId = parsed.jobId ?? `job_cli_${unique}`;
|
|
@@ -2041,9 +2203,9 @@ async function runTriggerCommand(args, dependencies = {}) {
|
|
|
2041
2203
|
eventId,
|
|
2042
2204
|
jobId,
|
|
2043
2205
|
accountId,
|
|
2044
|
-
timestamp
|
|
2206
|
+
timestamp: timestamp2
|
|
2045
2207
|
});
|
|
2046
|
-
const signed = signSyntheticWebhook(payload, { eventId, timestamp, secret });
|
|
2208
|
+
const signed = signSyntheticWebhook(payload, { eventId, timestamp: timestamp2, secret });
|
|
2047
2209
|
const response = await deps.fetch(parsed.destination, {
|
|
2048
2210
|
method: "POST",
|
|
2049
2211
|
headers: signed.headers,
|
|
@@ -2117,7 +2279,7 @@ function createActivityIndicator(write, enabled) {
|
|
|
2117
2279
|
}
|
|
2118
2280
|
|
|
2119
2281
|
// src/cli.ts
|
|
2120
|
-
var VERSION = "1.
|
|
2282
|
+
var VERSION = "1.4.0";
|
|
2121
2283
|
var HELP = `MediaRuntime CLI
|
|
2122
2284
|
|
|
2123
2285
|
Usage:
|
|
@@ -2133,6 +2295,11 @@ Usage:
|
|
|
2133
2295
|
[--image-width PX] [--image-height PX] [--image-mode fit|fill|cover|contain]
|
|
2134
2296
|
[--image-format jpg|png|webp|avif] [--image-quality N]
|
|
2135
2297
|
[--image-max-bytes N] [--image-min-quality N]
|
|
2298
|
+
Clip render: --preset video_clip_v1 --clip-start SEC --clip-duration SEC
|
|
2299
|
+
[--clip-layout original|vertical_blur] [--clip-transcript <plan.json|captions.srt|captions.vtt>] [--clip-captions]
|
|
2300
|
+
Clip suggestions: --preset clip_candidates_v1 [--clip-min-duration SEC]
|
|
2301
|
+
[--clip-max-duration SEC] [--clip-count 1..20] [--clip-keyword TEXT]
|
|
2302
|
+
[--clip-transcript <plan.json|captions.srt|captions.vtt>]
|
|
2136
2303
|
Audiogram preset: --audiogram-artwork <source> [--audiogram-captions <source>]
|
|
2137
2304
|
[--audiogram-layout square|portrait|landscape]
|
|
2138
2305
|
[--audiogram-fit contain|cover|blurred_background]
|