@mux/ai 0.6.0 → 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/README.md +27 -23
- package/dist/{index-B6ro59tn.d.ts → index-BMqnP1RV.d.ts} +7 -4
- package/dist/{index-DAlX4SNJ.d.ts → index-DZlygsvb.d.ts} +113 -3
- package/dist/index.d.ts +30 -6
- package/dist/index.js +858 -282
- package/dist/index.js.map +1 -1
- package/dist/primitives/index.d.ts +2 -2
- package/dist/primitives/index.js +189 -21
- package/dist/primitives/index.js.map +1 -1
- package/dist/{types-CqjLMB84.d.ts → types-BQVi_wnh.d.ts} +49 -18
- package/dist/workflows/index.d.ts +2 -2
- package/dist/workflows/index.js +617 -214
- package/dist/workflows/index.js.map +1 -1
- package/package.json +11 -5
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// package.json
|
|
8
|
-
var version = "0.
|
|
8
|
+
var version = "0.7.3";
|
|
9
9
|
|
|
10
10
|
// src/env.ts
|
|
11
11
|
import { z } from "zod";
|
|
@@ -60,6 +60,9 @@ var EnvSchema = z.object({
|
|
|
60
60
|
S3_BUCKET: optionalString("Bucket used for caption and audio uploads.", "S3 bucket"),
|
|
61
61
|
S3_ACCESS_KEY_ID: optionalString("Access key ID for S3-compatible uploads.", "S3 access key id"),
|
|
62
62
|
S3_SECRET_ACCESS_KEY: optionalString("Secret access key for S3-compatible uploads.", "S3 secret access key"),
|
|
63
|
+
S3_ALLOWED_ENDPOINT_HOSTS: optionalString(
|
|
64
|
+
"Comma-separated S3 endpoint allowlist (supports exact hosts and *.suffix patterns)."
|
|
65
|
+
),
|
|
63
66
|
EVALITE_RESULTS_ENDPOINT: optionalString(
|
|
64
67
|
"Full URL for posting Evalite results (e.g., https://example.com/api/evalite-results).",
|
|
65
68
|
"Evalite results endpoint"
|
|
@@ -135,11 +138,15 @@ function bytesToBase64(bytes) {
|
|
|
135
138
|
}
|
|
136
139
|
return output;
|
|
137
140
|
}
|
|
141
|
+
function normalizeBase64Input(value) {
|
|
142
|
+
const cleaned = value.replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/");
|
|
143
|
+
return cleaned?.length % 4 === 0 ? cleaned : cleaned + "=".repeat(4 - cleaned.length % 4);
|
|
144
|
+
}
|
|
138
145
|
function base64ToBytes(value, label) {
|
|
139
146
|
if (!value) {
|
|
140
147
|
throw new Error(`${label} is missing`);
|
|
141
148
|
}
|
|
142
|
-
const normalized =
|
|
149
|
+
const normalized = normalizeBase64Input(value);
|
|
143
150
|
const atobImpl = typeof globalThis.atob === "function" ? globalThis.atob.bind(globalThis) : void 0;
|
|
144
151
|
if (atobImpl) {
|
|
145
152
|
let binary;
|
|
@@ -157,19 +164,18 @@ function base64ToBytes(value, label) {
|
|
|
157
164
|
}
|
|
158
165
|
return bytes2;
|
|
159
166
|
}
|
|
160
|
-
|
|
161
|
-
if (cleaned.length % 4 !== 0) {
|
|
167
|
+
if (normalized.length % 4 !== 0) {
|
|
162
168
|
throw new Error(`${label} is not valid base64`);
|
|
163
169
|
}
|
|
164
|
-
const padding =
|
|
165
|
-
const outputLength =
|
|
170
|
+
const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0;
|
|
171
|
+
const outputLength = normalized.length / 4 * 3 - padding;
|
|
166
172
|
const bytes = new Uint8Array(outputLength);
|
|
167
173
|
let offset = 0;
|
|
168
|
-
for (let i = 0; i <
|
|
169
|
-
const c0 =
|
|
170
|
-
const c1 =
|
|
171
|
-
const c2 =
|
|
172
|
-
const c3 =
|
|
174
|
+
for (let i = 0; i < normalized.length; i += 4) {
|
|
175
|
+
const c0 = normalized.charCodeAt(i);
|
|
176
|
+
const c1 = normalized.charCodeAt(i + 1);
|
|
177
|
+
const c2 = normalized.charCodeAt(i + 2);
|
|
178
|
+
const c3 = normalized.charCodeAt(i + 3);
|
|
173
179
|
if (c0 === 61 || c1 === 61) {
|
|
174
180
|
throw new Error(`${label} is not valid base64`);
|
|
175
181
|
}
|
|
@@ -318,7 +324,9 @@ async function shouldEnforceEncryptedCredentials() {
|
|
|
318
324
|
function getWorkflowSecretKeyFromEnv() {
|
|
319
325
|
const key = env_default.MUX_AI_WORKFLOW_SECRET_KEY;
|
|
320
326
|
if (!key) {
|
|
321
|
-
throw new Error(
|
|
327
|
+
throw new Error(
|
|
328
|
+
"Workflow secret key is required. Set MUX_AI_WORKFLOW_SECRET_KEY environment variable."
|
|
329
|
+
);
|
|
322
330
|
}
|
|
323
331
|
return key;
|
|
324
332
|
}
|
|
@@ -349,36 +357,92 @@ async function resolveWorkflowCredentials(credentials) {
|
|
|
349
357
|
);
|
|
350
358
|
return { ...resolved, ...decrypted };
|
|
351
359
|
} catch (error) {
|
|
352
|
-
const detail = error instanceof Error ? error.message :
|
|
360
|
+
const detail = error instanceof Error ? error.message : "Unknown error.";
|
|
353
361
|
throw new Error(`Failed to decrypt workflow credentials. ${detail}`);
|
|
354
362
|
}
|
|
355
363
|
}
|
|
356
364
|
if (await shouldEnforceEncryptedCredentials()) {
|
|
357
365
|
throw new Error(
|
|
358
|
-
"Plaintext workflow credentials are not allowed when using Workflow Dev Kit.Pass encrypted credentials (encryptForWorkflow) or resolve secrets via environment variables."
|
|
366
|
+
"Plaintext workflow credentials are not allowed when using Workflow Dev Kit. Pass encrypted credentials (encryptForWorkflow) or resolve secrets via environment variables."
|
|
359
367
|
);
|
|
360
368
|
}
|
|
361
369
|
return { ...resolved, ...credentials };
|
|
362
370
|
}
|
|
363
|
-
|
|
371
|
+
function readString(record, key) {
|
|
372
|
+
const value = record?.[key];
|
|
373
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
374
|
+
}
|
|
375
|
+
function resolveDirectMuxCredentials(record) {
|
|
376
|
+
const tokenId = readString(record, "muxTokenId");
|
|
377
|
+
const tokenSecret = readString(record, "muxTokenSecret");
|
|
378
|
+
const signingKey = readString(record, "muxSigningKey");
|
|
379
|
+
const privateKey = readString(record, "muxPrivateKey");
|
|
380
|
+
if (!tokenId && !tokenSecret && !signingKey && !privateKey) {
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
if (!tokenId || !tokenSecret) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
"Both muxTokenId and muxTokenSecret are required when passing direct Mux workflow credentials."
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
tokenId,
|
|
390
|
+
tokenSecret,
|
|
391
|
+
signingKey,
|
|
392
|
+
privateKey
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function createWorkflowMuxClient(options) {
|
|
396
|
+
return {
|
|
397
|
+
async createClient() {
|
|
398
|
+
const { default: MuxClient } = await import("@mux/mux-node");
|
|
399
|
+
return new MuxClient({
|
|
400
|
+
tokenId: options.tokenId,
|
|
401
|
+
tokenSecret: options.tokenSecret
|
|
402
|
+
});
|
|
403
|
+
},
|
|
404
|
+
getSigningKey() {
|
|
405
|
+
return options.signingKey;
|
|
406
|
+
},
|
|
407
|
+
getPrivateKey() {
|
|
408
|
+
return options.privateKey;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
async function resolveMuxClient(credentials) {
|
|
364
413
|
const resolved = await resolveWorkflowCredentials(credentials);
|
|
365
|
-
const
|
|
366
|
-
const
|
|
414
|
+
const resolvedRecord = resolved;
|
|
415
|
+
const resolvedMuxCredentials = resolveDirectMuxCredentials(resolvedRecord);
|
|
416
|
+
if (resolvedMuxCredentials) {
|
|
417
|
+
return createWorkflowMuxClient(resolvedMuxCredentials);
|
|
418
|
+
}
|
|
419
|
+
const muxTokenId = env_default.MUX_TOKEN_ID;
|
|
420
|
+
const muxTokenSecret = env_default.MUX_TOKEN_SECRET;
|
|
367
421
|
if (!muxTokenId || !muxTokenSecret) {
|
|
368
422
|
throw new Error(
|
|
369
|
-
"Mux credentials are required. Provide
|
|
423
|
+
"Mux credentials are required. Provide muxTokenId/muxTokenSecret via workflow credentials, or set MUX_TOKEN_ID and MUX_TOKEN_SECRET environment variables."
|
|
370
424
|
);
|
|
371
425
|
}
|
|
372
|
-
return {
|
|
426
|
+
return createWorkflowMuxClient({
|
|
427
|
+
tokenId: muxTokenId,
|
|
428
|
+
tokenSecret: muxTokenSecret,
|
|
429
|
+
signingKey: env_default.MUX_SIGNING_KEY,
|
|
430
|
+
privateKey: env_default.MUX_PRIVATE_KEY
|
|
431
|
+
});
|
|
373
432
|
}
|
|
374
|
-
|
|
375
|
-
const
|
|
433
|
+
function resolveProviderApiKeyFromCredentials(provider, resolved) {
|
|
434
|
+
const record = resolved;
|
|
435
|
+
const openaiApiKey = readString(record, "openaiApiKey");
|
|
436
|
+
const anthropicApiKey = readString(record, "anthropicApiKey");
|
|
437
|
+
const googleApiKey = readString(record, "googleApiKey");
|
|
438
|
+
const hiveApiKey = readString(record, "hiveApiKey");
|
|
439
|
+
const elevenLabsApiKey = readString(record, "elevenLabsApiKey");
|
|
376
440
|
const apiKeyMap = {
|
|
377
|
-
openai:
|
|
378
|
-
anthropic:
|
|
379
|
-
google:
|
|
380
|
-
hive:
|
|
381
|
-
elevenlabs:
|
|
441
|
+
openai: openaiApiKey ?? env_default.OPENAI_API_KEY,
|
|
442
|
+
anthropic: anthropicApiKey ?? env_default.ANTHROPIC_API_KEY,
|
|
443
|
+
google: googleApiKey ?? env_default.GOOGLE_GENERATIVE_AI_API_KEY,
|
|
444
|
+
hive: hiveApiKey ?? env_default.HIVE_API_KEY,
|
|
445
|
+
elevenlabs: elevenLabsApiKey ?? env_default.ELEVENLABS_API_KEY
|
|
382
446
|
};
|
|
383
447
|
const apiKey = apiKeyMap[provider];
|
|
384
448
|
if (!apiKey) {
|
|
@@ -390,21 +454,320 @@ async function resolveProviderApiKey(provider, credentials) {
|
|
|
390
454
|
elevenlabs: "ELEVENLABS_API_KEY"
|
|
391
455
|
};
|
|
392
456
|
throw new Error(
|
|
393
|
-
`${provider} API key is required. Provide
|
|
457
|
+
`${provider} API key is required. Provide ${provider} credentials via workflow credentials or set ${envVarNames[provider]} environment variable.`
|
|
394
458
|
);
|
|
395
459
|
}
|
|
396
460
|
return apiKey;
|
|
397
461
|
}
|
|
462
|
+
async function resolveProviderApiKey(provider, credentials) {
|
|
463
|
+
const resolved = await resolveWorkflowCredentials(credentials);
|
|
464
|
+
return resolveProviderApiKeyFromCredentials(provider, resolved);
|
|
465
|
+
}
|
|
398
466
|
async function resolveMuxSigningContext(credentials) {
|
|
399
467
|
const resolved = await resolveWorkflowCredentials(credentials);
|
|
400
|
-
const
|
|
401
|
-
const
|
|
468
|
+
const resolvedRecord = resolved;
|
|
469
|
+
const keyId = readString(resolvedRecord, "muxSigningKey") ?? env_default.MUX_SIGNING_KEY;
|
|
470
|
+
const keySecret = readString(resolvedRecord, "muxPrivateKey") ?? env_default.MUX_PRIVATE_KEY;
|
|
402
471
|
if (!keyId || !keySecret) {
|
|
403
472
|
return void 0;
|
|
404
473
|
}
|
|
405
474
|
return { keyId, keySecret };
|
|
406
475
|
}
|
|
407
476
|
|
|
477
|
+
// node_modules/@workflow/serde/dist/index.js
|
|
478
|
+
var WORKFLOW_SERIALIZE = /* @__PURE__ */ Symbol.for("workflow-serialize");
|
|
479
|
+
var WORKFLOW_DESERIALIZE = /* @__PURE__ */ Symbol.for("workflow-deserialize");
|
|
480
|
+
|
|
481
|
+
// src/lib/s3-sigv4.ts
|
|
482
|
+
var AWS4_ALGORITHM = "AWS4-HMAC-SHA256";
|
|
483
|
+
var AWS4_REQUEST_TERMINATOR = "aws4_request";
|
|
484
|
+
var AWS4_SERVICE = "s3";
|
|
485
|
+
var S3_ALLOWED_ENDPOINT_PATTERNS = parseEndpointAllowlist(
|
|
486
|
+
env_default.S3_ALLOWED_ENDPOINT_HOSTS
|
|
487
|
+
);
|
|
488
|
+
function getCrypto() {
|
|
489
|
+
const webCrypto = globalThis.crypto;
|
|
490
|
+
if (!webCrypto?.subtle) {
|
|
491
|
+
throw new Error("Web Crypto API is required for S3 signing.");
|
|
492
|
+
}
|
|
493
|
+
return webCrypto;
|
|
494
|
+
}
|
|
495
|
+
var textEncoder = new TextEncoder();
|
|
496
|
+
function toBytes(value) {
|
|
497
|
+
return typeof value === "string" ? textEncoder.encode(value) : value;
|
|
498
|
+
}
|
|
499
|
+
function bytesToHex(bytes) {
|
|
500
|
+
return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
501
|
+
}
|
|
502
|
+
async function sha256Hex(value) {
|
|
503
|
+
const digest = await getCrypto().subtle.digest("SHA-256", toBytes(value));
|
|
504
|
+
return bytesToHex(new Uint8Array(digest));
|
|
505
|
+
}
|
|
506
|
+
async function hmacSha256Raw(key, value) {
|
|
507
|
+
const cryptoKey = await getCrypto().subtle.importKey(
|
|
508
|
+
"raw",
|
|
509
|
+
key,
|
|
510
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
511
|
+
false,
|
|
512
|
+
["sign"]
|
|
513
|
+
);
|
|
514
|
+
const signature = await getCrypto().subtle.sign("HMAC", cryptoKey, textEncoder.encode(value));
|
|
515
|
+
return new Uint8Array(signature);
|
|
516
|
+
}
|
|
517
|
+
async function deriveSigningKey(secretAccessKey, shortDate, region) {
|
|
518
|
+
const kDate = await hmacSha256Raw(textEncoder.encode(`AWS4${secretAccessKey}`), shortDate);
|
|
519
|
+
const kRegion = await hmacSha256Raw(kDate, region);
|
|
520
|
+
const kService = await hmacSha256Raw(kRegion, AWS4_SERVICE);
|
|
521
|
+
return hmacSha256Raw(kService, AWS4_REQUEST_TERMINATOR);
|
|
522
|
+
}
|
|
523
|
+
function formatAmzDate(date = /* @__PURE__ */ new Date()) {
|
|
524
|
+
const iso = date.toISOString();
|
|
525
|
+
const shortDate = iso.slice(0, 10).replace(/-/g, "");
|
|
526
|
+
const amzDate = `${iso.slice(0, 19).replace(/[-:]/g, "")}Z`;
|
|
527
|
+
return { amzDate, shortDate };
|
|
528
|
+
}
|
|
529
|
+
function encodeRFC3986(value) {
|
|
530
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
531
|
+
}
|
|
532
|
+
function encodePath(path) {
|
|
533
|
+
return path.split("/").map((segment) => encodeRFC3986(segment)).join("/");
|
|
534
|
+
}
|
|
535
|
+
function normalizeEndpoint(endpoint) {
|
|
536
|
+
let url;
|
|
537
|
+
try {
|
|
538
|
+
url = new URL(endpoint);
|
|
539
|
+
} catch {
|
|
540
|
+
throw new Error(`Invalid S3 endpoint: ${endpoint}`);
|
|
541
|
+
}
|
|
542
|
+
if (url.search || url.hash) {
|
|
543
|
+
throw new Error("S3 endpoint must not include query params or hash fragments.");
|
|
544
|
+
}
|
|
545
|
+
enforceEndpointPolicy(url);
|
|
546
|
+
return url;
|
|
547
|
+
}
|
|
548
|
+
function parseEndpointAllowlist(allowlist) {
|
|
549
|
+
if (!allowlist) {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
return allowlist.split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
553
|
+
}
|
|
554
|
+
function hostnameMatchesPattern(hostname, pattern) {
|
|
555
|
+
if (pattern.startsWith("*.")) {
|
|
556
|
+
const suffix = pattern.slice(1);
|
|
557
|
+
return hostname.endsWith(suffix) && hostname.length > suffix.length;
|
|
558
|
+
}
|
|
559
|
+
return hostname === pattern;
|
|
560
|
+
}
|
|
561
|
+
function enforceEndpointPolicy(url) {
|
|
562
|
+
const hostname = url.hostname.toLowerCase();
|
|
563
|
+
if (url.protocol !== "https:") {
|
|
564
|
+
throw new Error(
|
|
565
|
+
`Insecure S3 endpoint protocol "${url.protocol}" is not allowed. Use HTTPS.`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
if (S3_ALLOWED_ENDPOINT_PATTERNS.length > 0 && !S3_ALLOWED_ENDPOINT_PATTERNS.some((pattern) => hostnameMatchesPattern(hostname, pattern))) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
`S3 endpoint host "${hostname}" is not in S3_ALLOWED_ENDPOINT_HOSTS.`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function buildCanonicalUri(endpoint, bucket, key) {
|
|
575
|
+
const endpointPath = endpoint.pathname === "/" ? "" : encodePath(endpoint.pathname.replace(/\/+$/, ""));
|
|
576
|
+
const encodedBucket = encodeRFC3986(bucket);
|
|
577
|
+
const encodedKey = encodePath(key);
|
|
578
|
+
return `${endpointPath}/${encodedBucket}/${encodedKey}`;
|
|
579
|
+
}
|
|
580
|
+
function buildCanonicalQuery(params) {
|
|
581
|
+
return Object.entries(params).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${encodeRFC3986(key)}=${encodeRFC3986(value)}`).join("&");
|
|
582
|
+
}
|
|
583
|
+
async function signString(secretAccessKey, shortDate, region, value) {
|
|
584
|
+
const signingKey = await deriveSigningKey(secretAccessKey, shortDate, region);
|
|
585
|
+
const signatureBytes = await hmacSha256Raw(signingKey, value);
|
|
586
|
+
return bytesToHex(signatureBytes);
|
|
587
|
+
}
|
|
588
|
+
function buildCredentialScope(shortDate, region) {
|
|
589
|
+
return `${shortDate}/${region}/${AWS4_SERVICE}/${AWS4_REQUEST_TERMINATOR}`;
|
|
590
|
+
}
|
|
591
|
+
async function putObjectToS3({
|
|
592
|
+
accessKeyId,
|
|
593
|
+
secretAccessKey,
|
|
594
|
+
endpoint,
|
|
595
|
+
region,
|
|
596
|
+
bucket,
|
|
597
|
+
key,
|
|
598
|
+
body,
|
|
599
|
+
contentType
|
|
600
|
+
}) {
|
|
601
|
+
const resolvedEndpoint = normalizeEndpoint(endpoint);
|
|
602
|
+
const canonicalUri = buildCanonicalUri(resolvedEndpoint, bucket, key);
|
|
603
|
+
const host = resolvedEndpoint.host;
|
|
604
|
+
const normalizedContentType = contentType?.trim();
|
|
605
|
+
const { amzDate, shortDate } = formatAmzDate();
|
|
606
|
+
const payloadHash = await sha256Hex(body);
|
|
607
|
+
const signingHeaders = [
|
|
608
|
+
["host", host],
|
|
609
|
+
["x-amz-content-sha256", payloadHash],
|
|
610
|
+
["x-amz-date", amzDate],
|
|
611
|
+
...normalizedContentType ? [["content-type", normalizedContentType]] : []
|
|
612
|
+
].sort(([a], [b]) => a.localeCompare(b));
|
|
613
|
+
const canonicalHeaders = signingHeaders.map(([name, value]) => `${name}:${value}`).join("\n");
|
|
614
|
+
const signedHeaders = signingHeaders.map(([name]) => name).join(";");
|
|
615
|
+
const canonicalRequest = [
|
|
616
|
+
"PUT",
|
|
617
|
+
canonicalUri,
|
|
618
|
+
"",
|
|
619
|
+
`${canonicalHeaders}
|
|
620
|
+
`,
|
|
621
|
+
signedHeaders,
|
|
622
|
+
payloadHash
|
|
623
|
+
].join("\n");
|
|
624
|
+
const credentialScope = buildCredentialScope(shortDate, region);
|
|
625
|
+
const stringToSign = [
|
|
626
|
+
AWS4_ALGORITHM,
|
|
627
|
+
amzDate,
|
|
628
|
+
credentialScope,
|
|
629
|
+
await sha256Hex(canonicalRequest)
|
|
630
|
+
].join("\n");
|
|
631
|
+
const signature = await signString(secretAccessKey, shortDate, region, stringToSign);
|
|
632
|
+
const authorization = `${AWS4_ALGORITHM} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
633
|
+
const requestUrl = `${resolvedEndpoint.origin}${canonicalUri}`;
|
|
634
|
+
const response = await fetch(requestUrl, {
|
|
635
|
+
method: "PUT",
|
|
636
|
+
headers: {
|
|
637
|
+
"Authorization": authorization,
|
|
638
|
+
"x-amz-content-sha256": payloadHash,
|
|
639
|
+
"x-amz-date": amzDate,
|
|
640
|
+
...normalizedContentType ? { "content-type": normalizedContentType } : {}
|
|
641
|
+
},
|
|
642
|
+
body
|
|
643
|
+
});
|
|
644
|
+
if (!response.ok) {
|
|
645
|
+
const errorBody = await response.text().catch(() => "");
|
|
646
|
+
const detail = errorBody ? ` ${errorBody}` : "";
|
|
647
|
+
throw new Error(`S3 PUT failed (${response.status} ${response.statusText}).${detail}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async function createPresignedGetUrl({
|
|
651
|
+
accessKeyId,
|
|
652
|
+
secretAccessKey,
|
|
653
|
+
endpoint,
|
|
654
|
+
region,
|
|
655
|
+
bucket,
|
|
656
|
+
key,
|
|
657
|
+
expiresInSeconds = 3600
|
|
658
|
+
}) {
|
|
659
|
+
const resolvedEndpoint = normalizeEndpoint(endpoint);
|
|
660
|
+
const canonicalUri = buildCanonicalUri(resolvedEndpoint, bucket, key);
|
|
661
|
+
const host = resolvedEndpoint.host;
|
|
662
|
+
const { amzDate, shortDate } = formatAmzDate();
|
|
663
|
+
const credentialScope = buildCredentialScope(shortDate, region);
|
|
664
|
+
const signedHeaders = "host";
|
|
665
|
+
const queryParams = {
|
|
666
|
+
"X-Amz-Algorithm": AWS4_ALGORITHM,
|
|
667
|
+
"X-Amz-Credential": `${accessKeyId}/${credentialScope}`,
|
|
668
|
+
"X-Amz-Date": amzDate,
|
|
669
|
+
"X-Amz-Expires": `${expiresInSeconds}`,
|
|
670
|
+
"X-Amz-SignedHeaders": signedHeaders
|
|
671
|
+
};
|
|
672
|
+
const canonicalQuery = buildCanonicalQuery(queryParams);
|
|
673
|
+
const canonicalRequest = [
|
|
674
|
+
"GET",
|
|
675
|
+
canonicalUri,
|
|
676
|
+
canonicalQuery,
|
|
677
|
+
`host:${host}
|
|
678
|
+
`,
|
|
679
|
+
signedHeaders,
|
|
680
|
+
"UNSIGNED-PAYLOAD"
|
|
681
|
+
].join("\n");
|
|
682
|
+
const stringToSign = [
|
|
683
|
+
AWS4_ALGORITHM,
|
|
684
|
+
amzDate,
|
|
685
|
+
credentialScope,
|
|
686
|
+
await sha256Hex(canonicalRequest)
|
|
687
|
+
].join("\n");
|
|
688
|
+
const signature = await signString(secretAccessKey, shortDate, region, stringToSign);
|
|
689
|
+
const queryWithSignature = `${canonicalQuery}&X-Amz-Signature=${signature}`;
|
|
690
|
+
return `${resolvedEndpoint.origin}${canonicalUri}?${queryWithSignature}`;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/lib/workflow-serialization.ts
|
|
694
|
+
var WORKFLOW_CLASS_REGISTRY = /* @__PURE__ */ Symbol.for("workflow-class-registry");
|
|
695
|
+
function registerWorkflowSerializationClass(classId, cls) {
|
|
696
|
+
const globalObject = globalThis;
|
|
697
|
+
let registry = globalObject[WORKFLOW_CLASS_REGISTRY];
|
|
698
|
+
if (!registry) {
|
|
699
|
+
registry = /* @__PURE__ */ new Map();
|
|
700
|
+
globalObject[WORKFLOW_CLASS_REGISTRY] = registry;
|
|
701
|
+
}
|
|
702
|
+
registry.set(classId, cls);
|
|
703
|
+
const serializableClass = cls;
|
|
704
|
+
if (serializableClass.classId !== classId) {
|
|
705
|
+
Object.defineProperty(cls, "classId", {
|
|
706
|
+
value: classId,
|
|
707
|
+
writable: false,
|
|
708
|
+
enumerable: false,
|
|
709
|
+
configurable: false
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/lib/workflow-storage-client.ts
|
|
715
|
+
var WorkflowStorageClient = class {
|
|
716
|
+
constructor(options = {}) {
|
|
717
|
+
this.accessKeyId = options.accessKeyId;
|
|
718
|
+
this.secretAccessKey = options.secretAccessKey;
|
|
719
|
+
}
|
|
720
|
+
resolveCredentials(input) {
|
|
721
|
+
const accessKeyId = input.accessKeyId ?? this.accessKeyId;
|
|
722
|
+
const secretAccessKey = input.secretAccessKey ?? this.secretAccessKey;
|
|
723
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
"Storage credentials are required. Provide accessKeyId/secretAccessKey in WorkflowStorageClient options or in the storage operation input."
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
return { accessKeyId, secretAccessKey };
|
|
729
|
+
}
|
|
730
|
+
async putObject(input) {
|
|
731
|
+
const credentials = this.resolveCredentials(input);
|
|
732
|
+
await putObjectToS3({
|
|
733
|
+
accessKeyId: credentials.accessKeyId,
|
|
734
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
735
|
+
endpoint: input.endpoint,
|
|
736
|
+
region: input.region,
|
|
737
|
+
bucket: input.bucket,
|
|
738
|
+
key: input.key,
|
|
739
|
+
body: input.body,
|
|
740
|
+
contentType: input.contentType
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
async createPresignedGetUrl(input) {
|
|
744
|
+
const credentials = this.resolveCredentials(input);
|
|
745
|
+
return createPresignedGetUrl({
|
|
746
|
+
accessKeyId: credentials.accessKeyId,
|
|
747
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
748
|
+
endpoint: input.endpoint,
|
|
749
|
+
region: input.region,
|
|
750
|
+
bucket: input.bucket,
|
|
751
|
+
key: input.key,
|
|
752
|
+
expiresInSeconds: input.expiresInSeconds
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
static [WORKFLOW_SERIALIZE](instance) {
|
|
756
|
+
return {
|
|
757
|
+
accessKeyId: instance.accessKeyId,
|
|
758
|
+
secretAccessKey: instance.secretAccessKey
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
static [WORKFLOW_DESERIALIZE](value) {
|
|
762
|
+
return new this(value);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
WorkflowStorageClient.classId = "WorkflowStorageClient";
|
|
766
|
+
registerWorkflowSerializationClass(WorkflowStorageClient.classId, WorkflowStorageClient);
|
|
767
|
+
function createWorkflowStorageClient(options = {}) {
|
|
768
|
+
return new WorkflowStorageClient(options);
|
|
769
|
+
}
|
|
770
|
+
|
|
408
771
|
// src/primitives/index.ts
|
|
409
772
|
var primitives_exports = {};
|
|
410
773
|
__export(primitives_exports, {
|
|
@@ -418,6 +781,12 @@ __export(primitives_exports, {
|
|
|
418
781
|
extractTimestampedTranscript: () => extractTimestampedTranscript,
|
|
419
782
|
fetchTranscriptForAsset: () => fetchTranscriptForAsset,
|
|
420
783
|
findCaptionTrack: () => findCaptionTrack,
|
|
784
|
+
getHeatmapForAsset: () => getHeatmapForAsset,
|
|
785
|
+
getHeatmapForPlaybackId: () => getHeatmapForPlaybackId,
|
|
786
|
+
getHeatmapForVideo: () => getHeatmapForVideo,
|
|
787
|
+
getHotspotsForAsset: () => getHotspotsForAsset,
|
|
788
|
+
getHotspotsForPlaybackId: () => getHotspotsForPlaybackId,
|
|
789
|
+
getHotspotsForVideo: () => getHotspotsForVideo,
|
|
421
790
|
getReadyTextTracks: () => getReadyTextTracks,
|
|
422
791
|
getStoryboardUrl: () => getStoryboardUrl,
|
|
423
792
|
getThumbnailUrls: () => getThumbnailUrls,
|
|
@@ -426,10 +795,166 @@ __export(primitives_exports, {
|
|
|
426
795
|
vttTimestampToSeconds: () => vttTimestampToSeconds
|
|
427
796
|
});
|
|
428
797
|
|
|
798
|
+
// src/lib/providers.ts
|
|
799
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
800
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
801
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
802
|
+
var DEFAULT_LANGUAGE_MODELS = {
|
|
803
|
+
openai: "gpt-5.1",
|
|
804
|
+
anthropic: "claude-sonnet-4-5",
|
|
805
|
+
google: "gemini-3-flash-preview"
|
|
806
|
+
};
|
|
807
|
+
var DEFAULT_EMBEDDING_MODELS = {
|
|
808
|
+
openai: "text-embedding-3-small",
|
|
809
|
+
google: "gemini-embedding-001"
|
|
810
|
+
};
|
|
811
|
+
function resolveLanguageModelConfig(options = {}) {
|
|
812
|
+
const provider = options.provider || "openai";
|
|
813
|
+
const modelId = options.model || DEFAULT_LANGUAGE_MODELS[provider];
|
|
814
|
+
return { provider, modelId };
|
|
815
|
+
}
|
|
816
|
+
function resolveEmbeddingModelConfig(options = {}) {
|
|
817
|
+
const provider = options.provider || "openai";
|
|
818
|
+
const modelId = options.model || DEFAULT_EMBEDDING_MODELS[provider];
|
|
819
|
+
return { provider, modelId };
|
|
820
|
+
}
|
|
821
|
+
async function createLanguageModelFromConfig(provider, modelId, credentials) {
|
|
822
|
+
switch (provider) {
|
|
823
|
+
case "openai": {
|
|
824
|
+
const apiKey = await resolveProviderApiKey("openai", credentials);
|
|
825
|
+
const openai = createOpenAI({ apiKey });
|
|
826
|
+
return openai(modelId);
|
|
827
|
+
}
|
|
828
|
+
case "anthropic": {
|
|
829
|
+
const apiKey = await resolveProviderApiKey("anthropic", credentials);
|
|
830
|
+
const anthropic = createAnthropic({ apiKey });
|
|
831
|
+
return anthropic(modelId);
|
|
832
|
+
}
|
|
833
|
+
case "google": {
|
|
834
|
+
const apiKey = await resolveProviderApiKey("google", credentials);
|
|
835
|
+
const google = createGoogleGenerativeAI({ apiKey });
|
|
836
|
+
return google(modelId);
|
|
837
|
+
}
|
|
838
|
+
default: {
|
|
839
|
+
const exhaustiveCheck = provider;
|
|
840
|
+
throw new Error(`Unsupported provider: ${exhaustiveCheck}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
async function createEmbeddingModelFromConfig(provider, modelId, credentials) {
|
|
845
|
+
switch (provider) {
|
|
846
|
+
case "openai": {
|
|
847
|
+
const apiKey = await resolveProviderApiKey("openai", credentials);
|
|
848
|
+
const openai = createOpenAI({ apiKey });
|
|
849
|
+
return openai.embedding(modelId);
|
|
850
|
+
}
|
|
851
|
+
case "google": {
|
|
852
|
+
const apiKey = await resolveProviderApiKey("google", credentials);
|
|
853
|
+
const google = createGoogleGenerativeAI({ apiKey });
|
|
854
|
+
return google.textEmbeddingModel(modelId);
|
|
855
|
+
}
|
|
856
|
+
default: {
|
|
857
|
+
const exhaustiveCheck = provider;
|
|
858
|
+
throw new Error(`Unsupported embedding provider: ${exhaustiveCheck}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/lib/client-factory.ts
|
|
864
|
+
async function getMuxClientFromEnv(credentials) {
|
|
865
|
+
return resolveMuxClient(credentials);
|
|
866
|
+
}
|
|
867
|
+
async function getApiKeyFromEnv(provider, credentials) {
|
|
868
|
+
return resolveProviderApiKey(provider, credentials);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/primitives/heatmap.ts
|
|
872
|
+
async function getHeatmapForAsset(assetId, options = {}) {
|
|
873
|
+
"use step";
|
|
874
|
+
return fetchHeatmap("assets", assetId, options);
|
|
875
|
+
}
|
|
876
|
+
async function getHeatmapForVideo(videoId, options = {}) {
|
|
877
|
+
"use step";
|
|
878
|
+
return fetchHeatmap("videos", videoId, options);
|
|
879
|
+
}
|
|
880
|
+
async function getHeatmapForPlaybackId(playbackId, options = {}) {
|
|
881
|
+
"use step";
|
|
882
|
+
return fetchHeatmap("playback-ids", playbackId, options);
|
|
883
|
+
}
|
|
884
|
+
function transformHeatmapResponse(response) {
|
|
885
|
+
return {
|
|
886
|
+
assetId: response.asset_id,
|
|
887
|
+
videoId: response.video_id,
|
|
888
|
+
playbackId: response.playback_id,
|
|
889
|
+
heatmap: response.heatmap,
|
|
890
|
+
timeframe: response.timeframe
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
async function fetchHeatmap(identifierType, id, options) {
|
|
894
|
+
"use step";
|
|
895
|
+
const { timeframe = "[24:hours]", credentials } = options;
|
|
896
|
+
const muxClient = await getMuxClientFromEnv(credentials);
|
|
897
|
+
const mux = await muxClient.createClient();
|
|
898
|
+
const queryParams = new URLSearchParams();
|
|
899
|
+
queryParams.append("timeframe[]", timeframe);
|
|
900
|
+
const path = `/data/v1/engagement/${identifierType}/${id}/heatmap?${queryParams.toString()}`;
|
|
901
|
+
const response = await mux.get(path);
|
|
902
|
+
return transformHeatmapResponse(response);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/primitives/hotspots.ts
|
|
906
|
+
async function getHotspotsForAsset(assetId, options = {}) {
|
|
907
|
+
"use step";
|
|
908
|
+
const response = await fetchHotspots("assets", assetId, options);
|
|
909
|
+
return response.hotspots;
|
|
910
|
+
}
|
|
911
|
+
async function getHotspotsForVideo(videoId, options = {}) {
|
|
912
|
+
"use step";
|
|
913
|
+
const response = await fetchHotspots("videos", videoId, options);
|
|
914
|
+
return response.hotspots;
|
|
915
|
+
}
|
|
916
|
+
async function getHotspotsForPlaybackId(playbackId, options = {}) {
|
|
917
|
+
"use step";
|
|
918
|
+
const response = await fetchHotspots("playback-ids", playbackId, options);
|
|
919
|
+
return response.hotspots;
|
|
920
|
+
}
|
|
921
|
+
function transformHotspotResponse(response) {
|
|
922
|
+
return {
|
|
923
|
+
assetId: response.data.asset_id,
|
|
924
|
+
videoId: response.data.video_id,
|
|
925
|
+
playbackId: response.data.playback_id,
|
|
926
|
+
hotspots: response.data.hotspots.map((h) => ({
|
|
927
|
+
startMs: h.start_ms,
|
|
928
|
+
endMs: h.end_ms,
|
|
929
|
+
score: h.score
|
|
930
|
+
}))
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
async function fetchHotspots(identifierType, id, options) {
|
|
934
|
+
"use step";
|
|
935
|
+
const {
|
|
936
|
+
limit = 5,
|
|
937
|
+
orderDirection = "desc",
|
|
938
|
+
orderBy = "score",
|
|
939
|
+
timeframe = "[24:hours]",
|
|
940
|
+
credentials
|
|
941
|
+
} = options;
|
|
942
|
+
const muxClient = await getMuxClientFromEnv(credentials);
|
|
943
|
+
const mux = await muxClient.createClient();
|
|
944
|
+
const queryParams = new URLSearchParams();
|
|
945
|
+
queryParams.append("limit", String(limit));
|
|
946
|
+
queryParams.append("order_direction", orderDirection);
|
|
947
|
+
queryParams.append("order_by", orderBy);
|
|
948
|
+
queryParams.append("timeframe[]", timeframe);
|
|
949
|
+
const path = `/data/v1/engagement/${identifierType}/${id}/hotspots?${queryParams.toString()}`;
|
|
950
|
+
const response = await mux.get(path);
|
|
951
|
+
return transformHotspotResponse(response);
|
|
952
|
+
}
|
|
953
|
+
|
|
429
954
|
// src/lib/url-signing.ts
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return new
|
|
955
|
+
async function createSigningClient(context) {
|
|
956
|
+
const { default: MuxClient } = await import("@mux/mux-node");
|
|
957
|
+
return new MuxClient({
|
|
433
958
|
// These are not needed for signing, but the SDK requires them
|
|
434
959
|
// Using empty strings as we only need the jwt functionality
|
|
435
960
|
tokenId: env_default.MUX_TOKEN_ID || "",
|
|
@@ -440,7 +965,7 @@ function createSigningClient(context) {
|
|
|
440
965
|
}
|
|
441
966
|
async function signPlaybackId(playbackId, context, type = "video", params) {
|
|
442
967
|
"use step";
|
|
443
|
-
const client = createSigningClient(context);
|
|
968
|
+
const client = await createSigningClient(context);
|
|
444
969
|
const stringParams = params ? Object.fromEntries(
|
|
445
970
|
Object.entries(params).map(([key, value]) => [key, String(value)])
|
|
446
971
|
) : void 0;
|
|
@@ -455,7 +980,7 @@ async function signUrl(url, playbackId, context, type = "video", params, credent
|
|
|
455
980
|
const resolvedContext = context ?? await resolveMuxSigningContext(credentials);
|
|
456
981
|
if (!resolvedContext) {
|
|
457
982
|
throw new Error(
|
|
458
|
-
"Signed playback ID requires signing credentials. Provide muxSigningKey and muxPrivateKey
|
|
983
|
+
"Signed playback ID requires signing credentials. Provide muxSigningKey and muxPrivateKey via workflow credentials or set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
|
|
459
984
|
);
|
|
460
985
|
}
|
|
461
986
|
const token = await signPlaybackId(playbackId, resolvedContext, type, params);
|
|
@@ -906,82 +1431,6 @@ async function downloadImagesAsBase64(urls, options = {}, maxConcurrent = 5) {
|
|
|
906
1431
|
return results;
|
|
907
1432
|
}
|
|
908
1433
|
|
|
909
|
-
// src/lib/mux-assets.ts
|
|
910
|
-
import Mux2 from "@mux/mux-node";
|
|
911
|
-
|
|
912
|
-
// src/lib/providers.ts
|
|
913
|
-
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
914
|
-
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
915
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
916
|
-
var DEFAULT_LANGUAGE_MODELS = {
|
|
917
|
-
openai: "gpt-5.1",
|
|
918
|
-
anthropic: "claude-sonnet-4-5",
|
|
919
|
-
google: "gemini-3-flash-preview"
|
|
920
|
-
};
|
|
921
|
-
var DEFAULT_EMBEDDING_MODELS = {
|
|
922
|
-
openai: "text-embedding-3-small",
|
|
923
|
-
google: "gemini-embedding-001"
|
|
924
|
-
};
|
|
925
|
-
function resolveLanguageModelConfig(options = {}) {
|
|
926
|
-
const provider = options.provider || "openai";
|
|
927
|
-
const modelId = options.model || DEFAULT_LANGUAGE_MODELS[provider];
|
|
928
|
-
return { provider, modelId };
|
|
929
|
-
}
|
|
930
|
-
function resolveEmbeddingModelConfig(options = {}) {
|
|
931
|
-
const provider = options.provider || "openai";
|
|
932
|
-
const modelId = options.model || DEFAULT_EMBEDDING_MODELS[provider];
|
|
933
|
-
return { provider, modelId };
|
|
934
|
-
}
|
|
935
|
-
async function createLanguageModelFromConfig(provider, modelId, credentials) {
|
|
936
|
-
switch (provider) {
|
|
937
|
-
case "openai": {
|
|
938
|
-
const apiKey = await resolveProviderApiKey("openai", credentials);
|
|
939
|
-
const openai = createOpenAI({ apiKey });
|
|
940
|
-
return openai(modelId);
|
|
941
|
-
}
|
|
942
|
-
case "anthropic": {
|
|
943
|
-
const apiKey = await resolveProviderApiKey("anthropic", credentials);
|
|
944
|
-
const anthropic = createAnthropic({ apiKey });
|
|
945
|
-
return anthropic(modelId);
|
|
946
|
-
}
|
|
947
|
-
case "google": {
|
|
948
|
-
const apiKey = await resolveProviderApiKey("google", credentials);
|
|
949
|
-
const google = createGoogleGenerativeAI({ apiKey });
|
|
950
|
-
return google(modelId);
|
|
951
|
-
}
|
|
952
|
-
default: {
|
|
953
|
-
const exhaustiveCheck = provider;
|
|
954
|
-
throw new Error(`Unsupported provider: ${exhaustiveCheck}`);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
async function createEmbeddingModelFromConfig(provider, modelId, credentials) {
|
|
959
|
-
switch (provider) {
|
|
960
|
-
case "openai": {
|
|
961
|
-
const apiKey = await resolveProviderApiKey("openai", credentials);
|
|
962
|
-
const openai = createOpenAI({ apiKey });
|
|
963
|
-
return openai.embedding(modelId);
|
|
964
|
-
}
|
|
965
|
-
case "google": {
|
|
966
|
-
const apiKey = await resolveProviderApiKey("google", credentials);
|
|
967
|
-
const google = createGoogleGenerativeAI({ apiKey });
|
|
968
|
-
return google.textEmbeddingModel(modelId);
|
|
969
|
-
}
|
|
970
|
-
default: {
|
|
971
|
-
const exhaustiveCheck = provider;
|
|
972
|
-
throw new Error(`Unsupported embedding provider: ${exhaustiveCheck}`);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// src/lib/client-factory.ts
|
|
978
|
-
async function getMuxCredentialsFromEnv(credentials) {
|
|
979
|
-
return resolveMuxCredentials(credentials);
|
|
980
|
-
}
|
|
981
|
-
async function getApiKeyFromEnv(provider, credentials) {
|
|
982
|
-
return resolveProviderApiKey(provider, credentials);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
1434
|
// src/lib/mux-assets.ts
|
|
986
1435
|
function getPlaybackId(asset) {
|
|
987
1436
|
const playbackIds = asset.playback_ids || [];
|
|
@@ -1002,25 +1451,35 @@ function isAudioOnlyAsset(asset) {
|
|
|
1002
1451
|
const hasVideoTrack = asset.tracks?.some((track) => track.type === "video") ?? false;
|
|
1003
1452
|
return hasAudioTrack && !hasVideoTrack;
|
|
1004
1453
|
}
|
|
1454
|
+
function toPlaybackAsset(asset) {
|
|
1455
|
+
const { id: playbackId, policy } = getPlaybackId(asset);
|
|
1456
|
+
return { asset, playbackId, policy };
|
|
1457
|
+
}
|
|
1005
1458
|
async function getPlaybackIdForAsset(assetId, credentials) {
|
|
1006
1459
|
"use step";
|
|
1007
1460
|
const asset = await getMuxAsset(assetId, credentials);
|
|
1008
|
-
|
|
1009
|
-
return { asset, playbackId, policy };
|
|
1461
|
+
return toPlaybackAsset(asset);
|
|
1010
1462
|
}
|
|
1011
1463
|
async function getMuxAsset(assetId, credentials) {
|
|
1012
1464
|
"use step";
|
|
1013
|
-
const
|
|
1014
|
-
const mux =
|
|
1015
|
-
tokenId: muxTokenId,
|
|
1016
|
-
tokenSecret: muxTokenSecret
|
|
1017
|
-
});
|
|
1465
|
+
const muxClient = await getMuxClientFromEnv(credentials);
|
|
1466
|
+
const mux = await muxClient.createClient();
|
|
1018
1467
|
return mux.video.assets.retrieve(assetId);
|
|
1019
1468
|
}
|
|
1020
1469
|
function getAssetDurationSecondsFromAsset(asset) {
|
|
1021
1470
|
const duration = asset.duration;
|
|
1022
1471
|
return typeof duration === "number" && Number.isFinite(duration) ? duration : void 0;
|
|
1023
1472
|
}
|
|
1473
|
+
function getVideoTrackDurationSecondsFromAsset(asset) {
|
|
1474
|
+
const videoTrack = asset.tracks?.find((track) => track.type === "video");
|
|
1475
|
+
const duration = videoTrack?.duration;
|
|
1476
|
+
return typeof duration === "number" && Number.isFinite(duration) ? duration : void 0;
|
|
1477
|
+
}
|
|
1478
|
+
function getVideoTrackMaxFrameRateFromAsset(asset) {
|
|
1479
|
+
const videoTrack = asset.tracks?.find((track) => track.type === "video");
|
|
1480
|
+
const maxFrameRate = videoTrack?.max_frame_rate;
|
|
1481
|
+
return typeof maxFrameRate === "number" && Number.isFinite(maxFrameRate) && maxFrameRate > 0 ? maxFrameRate : void 0;
|
|
1482
|
+
}
|
|
1024
1483
|
|
|
1025
1484
|
// src/lib/prompt-builder.ts
|
|
1026
1485
|
function renderSection(section) {
|
|
@@ -1560,10 +2019,7 @@ async function hasBurnedInCaptions(assetId, options = {}) {
|
|
|
1560
2019
|
model,
|
|
1561
2020
|
provider
|
|
1562
2021
|
});
|
|
1563
|
-
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(
|
|
1564
|
-
assetId,
|
|
1565
|
-
credentials
|
|
1566
|
-
);
|
|
2022
|
+
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
|
|
1567
2023
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
|
|
1568
2024
|
const imageUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", credentials);
|
|
1569
2025
|
let analysisResponse;
|
|
@@ -1761,10 +2217,7 @@ async function generateChapters(assetId, languageCode, options = {}) {
|
|
|
1761
2217
|
model,
|
|
1762
2218
|
provider
|
|
1763
2219
|
});
|
|
1764
|
-
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(
|
|
1765
|
-
assetId,
|
|
1766
|
-
credentials
|
|
1767
|
-
);
|
|
2220
|
+
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
|
|
1768
2221
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
|
|
1769
2222
|
const isAudioOnly = isAudioOnlyAsset(assetData);
|
|
1770
2223
|
const signingContext = await resolveMuxSigningContext(credentials);
|
|
@@ -2003,6 +2456,66 @@ async function generateVideoEmbeddings(assetId, options = {}) {
|
|
|
2003
2456
|
return generateEmbeddingsInternal(assetId, options);
|
|
2004
2457
|
}
|
|
2005
2458
|
|
|
2459
|
+
// src/lib/sampling-plan.ts
|
|
2460
|
+
var DEFAULT_FPS = 30;
|
|
2461
|
+
function roundToNearestFrameMs(tsMs, fps = DEFAULT_FPS) {
|
|
2462
|
+
const frameMs = 1e3 / fps;
|
|
2463
|
+
return Math.round(Math.round(tsMs / frameMs) * frameMs * 100) / 100;
|
|
2464
|
+
}
|
|
2465
|
+
function planSamplingTimestamps(options) {
|
|
2466
|
+
const DEFAULT_MIN_CANDIDATES = 10;
|
|
2467
|
+
const DEFAULT_MAX_CANDIDATES = 30;
|
|
2468
|
+
const {
|
|
2469
|
+
duration_sec,
|
|
2470
|
+
min_candidates = DEFAULT_MIN_CANDIDATES,
|
|
2471
|
+
max_candidates = DEFAULT_MAX_CANDIDATES,
|
|
2472
|
+
trim_start_sec = 1,
|
|
2473
|
+
trim_end_sec = 1,
|
|
2474
|
+
fps = DEFAULT_FPS,
|
|
2475
|
+
base_cadence_hz,
|
|
2476
|
+
anchor_percents = [0.2, 0.5, 0.8],
|
|
2477
|
+
anchor_window_sec = 1.5
|
|
2478
|
+
} = options;
|
|
2479
|
+
const usableSec = Math.max(0, duration_sec - (trim_start_sec + trim_end_sec));
|
|
2480
|
+
if (usableSec <= 0)
|
|
2481
|
+
return [];
|
|
2482
|
+
const cadenceHz = base_cadence_hz ?? (duration_sec < 15 ? 3 : duration_sec < 60 ? 2 : duration_sec < 180 ? 1.5 : 1);
|
|
2483
|
+
let target = Math.round(usableSec * cadenceHz);
|
|
2484
|
+
target = Math.max(min_candidates, Math.min(max_candidates, target));
|
|
2485
|
+
const stepSec = usableSec / target;
|
|
2486
|
+
const t0 = trim_start_sec;
|
|
2487
|
+
const base = [];
|
|
2488
|
+
for (let i = 0; i < target; i++) {
|
|
2489
|
+
const tsSec = t0 + (i + 0.5) * stepSec;
|
|
2490
|
+
base.push(tsSec * 1e3);
|
|
2491
|
+
}
|
|
2492
|
+
const slack = Math.max(0, max_candidates - base.length);
|
|
2493
|
+
const extra = [];
|
|
2494
|
+
if (slack > 0 && anchor_percents.length > 0) {
|
|
2495
|
+
const perAnchor = Math.max(1, Math.min(5, Math.floor(slack / anchor_percents.length)));
|
|
2496
|
+
for (const p of anchor_percents) {
|
|
2497
|
+
const centerSec = Math.min(
|
|
2498
|
+
t0 + usableSec - 1e-3,
|
|
2499
|
+
// nudge just inside the end bound
|
|
2500
|
+
Math.max(t0 + 1e-3, duration_sec * p)
|
|
2501
|
+
// nudge just inside the start bound
|
|
2502
|
+
);
|
|
2503
|
+
const startSec = Math.max(t0, centerSec - anchor_window_sec / 2);
|
|
2504
|
+
const endSec = Math.min(t0 + usableSec, centerSec + anchor_window_sec / 2);
|
|
2505
|
+
if (endSec <= startSec)
|
|
2506
|
+
continue;
|
|
2507
|
+
const wStep = (endSec - startSec) / perAnchor;
|
|
2508
|
+
for (let i = 0; i < perAnchor; i++) {
|
|
2509
|
+
const tsSec = startSec + (i + 0.5) * wStep;
|
|
2510
|
+
extra.push(tsSec * 1e3);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
const all = base.concat(extra).map((ms) => roundToNearestFrameMs(ms, fps)).filter((ms) => ms >= trim_start_sec * 1e3 && ms <= (duration_sec - trim_end_sec) * 1e3);
|
|
2515
|
+
const uniqSorted = Array.from(new Set(all)).sort((a, b) => a - b);
|
|
2516
|
+
return uniqSorted.slice(0, max_candidates);
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2006
2519
|
// src/workflows/moderation.ts
|
|
2007
2520
|
var DEFAULT_THRESHOLDS = {
|
|
2008
2521
|
sexual: 0.7,
|
|
@@ -2014,31 +2527,19 @@ var HIVE_SEXUAL_CATEGORIES = [
|
|
|
2014
2527
|
"general_nsfw",
|
|
2015
2528
|
"general_suggestive",
|
|
2016
2529
|
"yes_sexual_activity",
|
|
2017
|
-
"female_underwear",
|
|
2018
|
-
"male_underwear",
|
|
2019
|
-
"bra",
|
|
2020
|
-
"panties",
|
|
2021
2530
|
"sex_toys",
|
|
2022
2531
|
"nudity_female",
|
|
2023
|
-
"nudity_male"
|
|
2024
|
-
"cleavage",
|
|
2025
|
-
"swimwear"
|
|
2532
|
+
"nudity_male"
|
|
2026
2533
|
];
|
|
2027
2534
|
var HIVE_VIOLENCE_CATEGORIES = [
|
|
2028
2535
|
"gun_in_hand",
|
|
2029
2536
|
"gun_not_in_hand",
|
|
2030
|
-
"animated_gun",
|
|
2031
2537
|
"knife_in_hand",
|
|
2032
|
-
"knife_not_in_hand",
|
|
2033
|
-
"culinary_knife_not_in_hand",
|
|
2034
|
-
"culinary_knife_in_hand",
|
|
2035
2538
|
"very_bloody",
|
|
2036
|
-
"a_little_bloody",
|
|
2037
2539
|
"other_blood",
|
|
2038
2540
|
"hanging",
|
|
2039
2541
|
"noose",
|
|
2040
2542
|
"human_corpse",
|
|
2041
|
-
"animated_corpse",
|
|
2042
2543
|
"emaciated_body",
|
|
2043
2544
|
"self_harm",
|
|
2044
2545
|
"animal_abuse",
|
|
@@ -2097,7 +2598,8 @@ async function moderateImageWithOpenAI(entry) {
|
|
|
2097
2598
|
url: entry.url,
|
|
2098
2599
|
sexual: 0,
|
|
2099
2600
|
violence: 0,
|
|
2100
|
-
error: true
|
|
2601
|
+
error: true,
|
|
2602
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2101
2603
|
};
|
|
2102
2604
|
}
|
|
2103
2605
|
}
|
|
@@ -2142,7 +2644,8 @@ async function requestOpenAITextModeration(text, model, url, credentials) {
|
|
|
2142
2644
|
url,
|
|
2143
2645
|
sexual: 0,
|
|
2144
2646
|
violence: 0,
|
|
2145
|
-
error: true
|
|
2647
|
+
error: true,
|
|
2648
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2146
2649
|
};
|
|
2147
2650
|
}
|
|
2148
2651
|
}
|
|
@@ -2167,7 +2670,7 @@ async function requestOpenAITranscriptModeration(transcriptText, model, maxConcu
|
|
|
2167
2670
|
const chunks = chunkTextByUtf16CodeUnits(transcriptText, 1e4);
|
|
2168
2671
|
if (!chunks.length) {
|
|
2169
2672
|
return [
|
|
2170
|
-
{ url: "transcript:0", sexual: 0, violence: 0, error: true }
|
|
2673
|
+
{ url: "transcript:0", sexual: 0, violence: 0, error: true, errorMessage: "No transcript chunks to moderate" }
|
|
2171
2674
|
];
|
|
2172
2675
|
}
|
|
2173
2676
|
const targets = chunks.map((chunk, idx) => ({
|
|
@@ -2201,34 +2704,59 @@ async function moderateImageWithHive(entry) {
|
|
|
2201
2704
|
});
|
|
2202
2705
|
formData.append("media", blob, `thumbnail.${extension}`);
|
|
2203
2706
|
}
|
|
2204
|
-
const
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2707
|
+
const controller = new AbortController();
|
|
2708
|
+
const timeout = setTimeout(() => controller.abort(), 15e3);
|
|
2709
|
+
let res;
|
|
2710
|
+
try {
|
|
2711
|
+
res = await fetch(HIVE_ENDPOINT, {
|
|
2712
|
+
method: "POST",
|
|
2713
|
+
headers: {
|
|
2714
|
+
Accept: "application/json",
|
|
2715
|
+
Authorization: `Token ${apiKey}`
|
|
2716
|
+
},
|
|
2717
|
+
body: formData,
|
|
2718
|
+
signal: controller.signal
|
|
2719
|
+
});
|
|
2720
|
+
} catch (err) {
|
|
2721
|
+
if (err?.name === "AbortError") {
|
|
2722
|
+
throw new Error("Hive request timed out after 15s");
|
|
2723
|
+
}
|
|
2724
|
+
throw err;
|
|
2725
|
+
} finally {
|
|
2726
|
+
clearTimeout(timeout);
|
|
2727
|
+
}
|
|
2212
2728
|
const json = await res.json().catch(() => void 0);
|
|
2213
2729
|
if (!res.ok) {
|
|
2214
2730
|
throw new Error(
|
|
2215
2731
|
`Hive moderation error: ${res.status} ${res.statusText} - ${JSON.stringify(json)}`
|
|
2216
2732
|
);
|
|
2217
2733
|
}
|
|
2218
|
-
|
|
2734
|
+
if (json?.return_code != null && json.return_code !== 0) {
|
|
2735
|
+
throw new Error(
|
|
2736
|
+
`Hive API error (return_code ${json.return_code}): ${json.message || "Unknown error"}`
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
const classes = json?.status?.[0]?.response?.output?.[0]?.classes;
|
|
2740
|
+
if (!Array.isArray(classes)) {
|
|
2741
|
+
throw new TypeError(
|
|
2742
|
+
`Unexpected Hive response structure: ${JSON.stringify(json)}`
|
|
2743
|
+
);
|
|
2744
|
+
}
|
|
2745
|
+
const sexual = getHiveCategoryScores(classes, HIVE_SEXUAL_CATEGORIES);
|
|
2746
|
+
const violence = getHiveCategoryScores(classes, HIVE_VIOLENCE_CATEGORIES);
|
|
2219
2747
|
return {
|
|
2220
2748
|
url: entry.url,
|
|
2221
|
-
sexual
|
|
2222
|
-
violence
|
|
2749
|
+
sexual,
|
|
2750
|
+
violence,
|
|
2223
2751
|
error: false
|
|
2224
2752
|
};
|
|
2225
2753
|
} catch (error) {
|
|
2226
|
-
console.error("Hive moderation failed:", error);
|
|
2227
2754
|
return {
|
|
2228
2755
|
url: entry.url,
|
|
2229
2756
|
sexual: 0,
|
|
2230
2757
|
violence: 0,
|
|
2231
|
-
error: true
|
|
2758
|
+
error: true,
|
|
2759
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2232
2760
|
};
|
|
2233
2761
|
}
|
|
2234
2762
|
}
|
|
@@ -2247,7 +2775,20 @@ async function requestHiveModeration(imageUrls, maxConcurrent = 5, submissionMod
|
|
|
2247
2775
|
source: { kind: "url", value: url },
|
|
2248
2776
|
credentials
|
|
2249
2777
|
}));
|
|
2250
|
-
return processConcurrently(targets, moderateImageWithHive, maxConcurrent);
|
|
2778
|
+
return await processConcurrently(targets, moderateImageWithHive, maxConcurrent);
|
|
2779
|
+
}
|
|
2780
|
+
async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options) {
|
|
2781
|
+
"use step";
|
|
2782
|
+
const { width, shouldSign, credentials } = options;
|
|
2783
|
+
const baseUrl = `https://image.mux.com/${playbackId}/thumbnail.png`;
|
|
2784
|
+
const urlPromises = timestampsMs.map(async (tsMs) => {
|
|
2785
|
+
const time = Number((tsMs / 1e3).toFixed(2));
|
|
2786
|
+
if (shouldSign) {
|
|
2787
|
+
return signUrl(baseUrl, playbackId, void 0, "thumbnail", { time, width }, credentials);
|
|
2788
|
+
}
|
|
2789
|
+
return `${baseUrl}?time=${time}&width=${width}`;
|
|
2790
|
+
});
|
|
2791
|
+
return Promise.all(urlPromises);
|
|
2251
2792
|
}
|
|
2252
2793
|
async function getModerationScores(assetId, options = {}) {
|
|
2253
2794
|
"use workflow";
|
|
@@ -2262,11 +2803,17 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
2262
2803
|
maxConcurrent = 5,
|
|
2263
2804
|
imageSubmissionMode = "url",
|
|
2264
2805
|
imageDownloadOptions,
|
|
2265
|
-
credentials
|
|
2806
|
+
credentials: providedCredentials
|
|
2266
2807
|
} = options;
|
|
2808
|
+
const credentials = providedCredentials;
|
|
2267
2809
|
const { asset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
|
|
2810
|
+
const videoTrackDurationSeconds = getVideoTrackDurationSecondsFromAsset(asset);
|
|
2811
|
+
const videoTrackFps = getVideoTrackMaxFrameRateFromAsset(asset);
|
|
2268
2812
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(asset);
|
|
2269
|
-
const
|
|
2813
|
+
const candidateDurations = [videoTrackDurationSeconds, assetDurationSeconds].filter(
|
|
2814
|
+
(d) => d != null
|
|
2815
|
+
);
|
|
2816
|
+
const duration = candidateDurations.length > 0 ? Math.min(...candidateDurations) : 0;
|
|
2270
2817
|
const isAudioOnly = isAudioOnlyAsset(asset);
|
|
2271
2818
|
const signingContext = await resolveMuxSigningContext(credentials);
|
|
2272
2819
|
if (policy === "signed" && !signingContext) {
|
|
@@ -2308,13 +2855,35 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
2308
2855
|
throw new Error(`Unsupported moderation provider: ${provider}`);
|
|
2309
2856
|
}
|
|
2310
2857
|
} else {
|
|
2311
|
-
const thumbnailUrls =
|
|
2312
|
-
interval
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2858
|
+
const thumbnailUrls = maxSamples === void 0 ? (
|
|
2859
|
+
// Generate thumbnail URLs (signed if needed) using existing interval-based logic.
|
|
2860
|
+
await getThumbnailUrls(playbackId, duration, {
|
|
2861
|
+
interval: thumbnailInterval,
|
|
2862
|
+
width: thumbnailWidth,
|
|
2863
|
+
shouldSign: policy === "signed",
|
|
2864
|
+
credentials
|
|
2865
|
+
})
|
|
2866
|
+
) : (
|
|
2867
|
+
// In maxSamples mode, sample valid timestamps over the trimmed usable span.
|
|
2868
|
+
// Use proportional trims (≈ duration/6, capped at 5s) to stay well inside the
|
|
2869
|
+
// renderable range — Mux can't always serve thumbnails at the very edges.
|
|
2870
|
+
await getThumbnailUrlsFromTimestamps(
|
|
2871
|
+
playbackId,
|
|
2872
|
+
planSamplingTimestamps({
|
|
2873
|
+
duration_sec: duration,
|
|
2874
|
+
max_candidates: maxSamples,
|
|
2875
|
+
trim_start_sec: duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
|
|
2876
|
+
trim_end_sec: duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
|
|
2877
|
+
fps: videoTrackFps,
|
|
2878
|
+
base_cadence_hz: thumbnailInterval > 0 ? 1 / thumbnailInterval : void 0
|
|
2879
|
+
}),
|
|
2880
|
+
{
|
|
2881
|
+
width: thumbnailWidth,
|
|
2882
|
+
shouldSign: policy === "signed",
|
|
2883
|
+
credentials
|
|
2884
|
+
}
|
|
2885
|
+
)
|
|
2886
|
+
);
|
|
2318
2887
|
thumbnailCount = thumbnailUrls.length;
|
|
2319
2888
|
if (provider === "openai") {
|
|
2320
2889
|
thumbnailScores = await requestOpenAIModeration(
|
|
@@ -2337,6 +2906,13 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
2337
2906
|
throw new Error(`Unsupported moderation provider: ${provider}`);
|
|
2338
2907
|
}
|
|
2339
2908
|
}
|
|
2909
|
+
const failed = thumbnailScores.filter((s) => s.error);
|
|
2910
|
+
if (failed.length > 0) {
|
|
2911
|
+
const details = failed.map((s) => `${s.url}: ${s.errorMessage || "Unknown error"}`).join("; ");
|
|
2912
|
+
throw new Error(
|
|
2913
|
+
`Moderation failed for ${failed.length}/${thumbnailScores.length} thumbnail(s): ${details}`
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2340
2916
|
const maxSexual = Math.max(...thumbnailScores.map((s) => s.sexual));
|
|
2341
2917
|
const maxViolence = Math.max(...thumbnailScores.map((s) => s.violence));
|
|
2342
2918
|
const finalThresholds = { ...DEFAULT_THRESHOLDS, ...thresholds };
|
|
@@ -2347,7 +2923,7 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
2347
2923
|
thumbnailScores,
|
|
2348
2924
|
usage: {
|
|
2349
2925
|
metadata: {
|
|
2350
|
-
assetDurationSeconds,
|
|
2926
|
+
assetDurationSeconds: duration,
|
|
2351
2927
|
...thumbnailCount === void 0 ? {} : { thumbnailCount }
|
|
2352
2928
|
}
|
|
2353
2929
|
},
|
|
@@ -2708,7 +3284,8 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2708
3284
|
model,
|
|
2709
3285
|
provider
|
|
2710
3286
|
});
|
|
2711
|
-
const
|
|
3287
|
+
const workflowCredentials = credentials;
|
|
3288
|
+
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, workflowCredentials);
|
|
2712
3289
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
|
|
2713
3290
|
const isAudioOnly = isAudioOnlyAsset(assetData);
|
|
2714
3291
|
if (isAudioOnly && !includeTranscript) {
|
|
@@ -2716,7 +3293,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2716
3293
|
"Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles)."
|
|
2717
3294
|
);
|
|
2718
3295
|
}
|
|
2719
|
-
const signingContext = await resolveMuxSigningContext(
|
|
3296
|
+
const signingContext = await resolveMuxSigningContext(workflowCredentials);
|
|
2720
3297
|
if (policy === "signed" && !signingContext) {
|
|
2721
3298
|
throw new Error(
|
|
2722
3299
|
"Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
|
|
@@ -2725,7 +3302,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2725
3302
|
const transcriptText = includeTranscript ? (await fetchTranscriptForAsset(assetData, playbackId, {
|
|
2726
3303
|
cleanTranscript,
|
|
2727
3304
|
shouldSign: policy === "signed",
|
|
2728
|
-
credentials,
|
|
3305
|
+
credentials: workflowCredentials,
|
|
2729
3306
|
required: isAudioOnly
|
|
2730
3307
|
})).transcriptText : "";
|
|
2731
3308
|
const userPrompt = buildUserPrompt4({
|
|
@@ -2745,10 +3322,10 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2745
3322
|
modelConfig.modelId,
|
|
2746
3323
|
userPrompt,
|
|
2747
3324
|
systemPrompt,
|
|
2748
|
-
|
|
3325
|
+
workflowCredentials
|
|
2749
3326
|
);
|
|
2750
3327
|
} else {
|
|
2751
|
-
const storyboardUrl = await getStoryboardUrl(playbackId, 640, policy === "signed",
|
|
3328
|
+
const storyboardUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", workflowCredentials);
|
|
2752
3329
|
imageUrl = storyboardUrl;
|
|
2753
3330
|
if (imageSubmissionMode === "base64") {
|
|
2754
3331
|
const downloadResult = await downloadImageAsBase64(storyboardUrl, imageDownloadOptions);
|
|
@@ -2758,7 +3335,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2758
3335
|
modelConfig.modelId,
|
|
2759
3336
|
userPrompt,
|
|
2760
3337
|
systemPrompt,
|
|
2761
|
-
|
|
3338
|
+
workflowCredentials
|
|
2762
3339
|
);
|
|
2763
3340
|
} else {
|
|
2764
3341
|
analysisResponse = await withRetry(() => analyzeStoryboard2(
|
|
@@ -2767,7 +3344,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2767
3344
|
modelConfig.modelId,
|
|
2768
3345
|
userPrompt,
|
|
2769
3346
|
systemPrompt,
|
|
2770
|
-
|
|
3347
|
+
workflowCredentials
|
|
2771
3348
|
));
|
|
2772
3349
|
}
|
|
2773
3350
|
}
|
|
@@ -2803,9 +3380,6 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2803
3380
|
};
|
|
2804
3381
|
}
|
|
2805
3382
|
|
|
2806
|
-
// src/workflows/translate-audio.ts
|
|
2807
|
-
import Mux3 from "@mux/mux-node";
|
|
2808
|
-
|
|
2809
3383
|
// src/lib/language-codes.ts
|
|
2810
3384
|
var ISO639_1_TO_3 = {
|
|
2811
3385
|
// Major world languages
|
|
@@ -2964,6 +3538,48 @@ function getLanguageName(code) {
|
|
|
2964
3538
|
}
|
|
2965
3539
|
}
|
|
2966
3540
|
|
|
3541
|
+
// src/lib/storage-adapter.ts
|
|
3542
|
+
function requireCredentials(accessKeyId, secretAccessKey) {
|
|
3543
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
3544
|
+
throw new Error(
|
|
3545
|
+
"S3 credentials are required for default storage operations. Provide S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY or pass options.storageAdapter."
|
|
3546
|
+
);
|
|
3547
|
+
}
|
|
3548
|
+
return { accessKeyId, secretAccessKey };
|
|
3549
|
+
}
|
|
3550
|
+
async function putObjectWithStorageAdapter(input, adapter) {
|
|
3551
|
+
if (adapter) {
|
|
3552
|
+
await adapter.putObject(input);
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
const credentials = requireCredentials(input.accessKeyId, input.secretAccessKey);
|
|
3556
|
+
await putObjectToS3({
|
|
3557
|
+
accessKeyId: credentials.accessKeyId,
|
|
3558
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
3559
|
+
endpoint: input.endpoint,
|
|
3560
|
+
region: input.region,
|
|
3561
|
+
bucket: input.bucket,
|
|
3562
|
+
key: input.key,
|
|
3563
|
+
body: input.body,
|
|
3564
|
+
contentType: input.contentType
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
async function createPresignedGetUrlWithStorageAdapter(input, adapter) {
|
|
3568
|
+
if (adapter) {
|
|
3569
|
+
return adapter.createPresignedGetUrl(input);
|
|
3570
|
+
}
|
|
3571
|
+
const credentials = requireCredentials(input.accessKeyId, input.secretAccessKey);
|
|
3572
|
+
return createPresignedGetUrl({
|
|
3573
|
+
accessKeyId: credentials.accessKeyId,
|
|
3574
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
3575
|
+
endpoint: input.endpoint,
|
|
3576
|
+
region: input.region,
|
|
3577
|
+
bucket: input.bucket,
|
|
3578
|
+
key: input.key,
|
|
3579
|
+
expiresInSeconds: input.expiresInSeconds
|
|
3580
|
+
});
|
|
3581
|
+
}
|
|
3582
|
+
|
|
2967
3583
|
// src/workflows/translate-audio.ts
|
|
2968
3584
|
var STATIC_RENDITION_POLL_INTERVAL_MS = 5e3;
|
|
2969
3585
|
var STATIC_RENDITION_MAX_ATTEMPTS = 36;
|
|
@@ -2983,11 +3599,8 @@ function getReadyAudioStaticRendition(asset) {
|
|
|
2983
3599
|
var hasReadyAudioStaticRendition = (asset) => Boolean(getReadyAudioStaticRendition(asset));
|
|
2984
3600
|
async function requestStaticRenditionCreation(assetId, credentials) {
|
|
2985
3601
|
"use step";
|
|
2986
|
-
const
|
|
2987
|
-
const mux =
|
|
2988
|
-
tokenId: muxTokenId,
|
|
2989
|
-
tokenSecret: muxTokenSecret
|
|
2990
|
-
});
|
|
3602
|
+
const muxClient = await resolveMuxClient(credentials);
|
|
3603
|
+
const mux = await muxClient.createClient();
|
|
2991
3604
|
try {
|
|
2992
3605
|
await mux.video.assets.createStaticRendition(assetId, {
|
|
2993
3606
|
resolution: "audio-only"
|
|
@@ -3009,11 +3622,8 @@ async function waitForAudioStaticRendition({
|
|
|
3009
3622
|
credentials
|
|
3010
3623
|
}) {
|
|
3011
3624
|
"use step";
|
|
3012
|
-
const
|
|
3013
|
-
const mux =
|
|
3014
|
-
tokenId: muxTokenId,
|
|
3015
|
-
tokenSecret: muxTokenSecret
|
|
3016
|
-
});
|
|
3625
|
+
const muxClient = await resolveMuxClient(credentials);
|
|
3626
|
+
const mux = await muxClient.createClient();
|
|
3017
3627
|
let currentAsset = initialAsset;
|
|
3018
3628
|
if (hasReadyAudioStaticRendition(currentAsset)) {
|
|
3019
3629
|
return currentAsset;
|
|
@@ -3126,53 +3736,40 @@ async function uploadDubbedAudioToS3({
|
|
|
3126
3736
|
toLanguageCode,
|
|
3127
3737
|
s3Endpoint,
|
|
3128
3738
|
s3Region,
|
|
3129
|
-
s3Bucket
|
|
3739
|
+
s3Bucket,
|
|
3740
|
+
storageAdapter
|
|
3130
3741
|
}) {
|
|
3131
3742
|
"use step";
|
|
3132
|
-
const { S3Client, GetObjectCommand } = await import("@aws-sdk/client-s3");
|
|
3133
|
-
const { Upload } = await import("@aws-sdk/lib-storage");
|
|
3134
|
-
const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
|
|
3135
3743
|
const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
|
|
3136
3744
|
const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
|
|
3137
|
-
const
|
|
3745
|
+
const audioKey = `audio-translations/${assetId}/auto-to-${toLanguageCode}-${Date.now()}.m4a`;
|
|
3746
|
+
await putObjectWithStorageAdapter({
|
|
3747
|
+
accessKeyId: s3AccessKeyId,
|
|
3748
|
+
secretAccessKey: s3SecretAccessKey,
|
|
3749
|
+
endpoint: s3Endpoint,
|
|
3138
3750
|
region: s3Region,
|
|
3751
|
+
bucket: s3Bucket,
|
|
3752
|
+
key: audioKey,
|
|
3753
|
+
body: new Uint8Array(dubbedAudioBuffer),
|
|
3754
|
+
contentType: "audio/mp4"
|
|
3755
|
+
}, storageAdapter);
|
|
3756
|
+
const presignedUrl = await createPresignedGetUrlWithStorageAdapter({
|
|
3757
|
+
accessKeyId: s3AccessKeyId,
|
|
3758
|
+
secretAccessKey: s3SecretAccessKey,
|
|
3139
3759
|
endpoint: s3Endpoint,
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
});
|
|
3146
|
-
const audioKey = `audio-translations/${assetId}/auto-to-${toLanguageCode}-${Date.now()}.m4a`;
|
|
3147
|
-
const upload = new Upload({
|
|
3148
|
-
client: s3Client,
|
|
3149
|
-
params: {
|
|
3150
|
-
Bucket: s3Bucket,
|
|
3151
|
-
Key: audioKey,
|
|
3152
|
-
Body: new Uint8Array(dubbedAudioBuffer),
|
|
3153
|
-
ContentType: "audio/mp4"
|
|
3154
|
-
}
|
|
3155
|
-
});
|
|
3156
|
-
await upload.done();
|
|
3157
|
-
const getObjectCommand = new GetObjectCommand({
|
|
3158
|
-
Bucket: s3Bucket,
|
|
3159
|
-
Key: audioKey
|
|
3160
|
-
});
|
|
3161
|
-
const presignedUrl = await getSignedUrl(s3Client, getObjectCommand, {
|
|
3162
|
-
expiresIn: 3600
|
|
3163
|
-
// 1 hour
|
|
3164
|
-
});
|
|
3760
|
+
region: s3Region,
|
|
3761
|
+
bucket: s3Bucket,
|
|
3762
|
+
key: audioKey,
|
|
3763
|
+
expiresInSeconds: 3600
|
|
3764
|
+
}, storageAdapter);
|
|
3165
3765
|
console.warn(`\u2705 Audio uploaded successfully to: ${audioKey}`);
|
|
3166
3766
|
console.warn(`\u{1F517} Generated presigned URL (expires in 1 hour)`);
|
|
3167
3767
|
return presignedUrl;
|
|
3168
3768
|
}
|
|
3169
3769
|
async function createAudioTrackOnMux(assetId, languageCode, presignedUrl, credentials) {
|
|
3170
3770
|
"use step";
|
|
3171
|
-
const
|
|
3172
|
-
const mux =
|
|
3173
|
-
tokenId: muxTokenId,
|
|
3174
|
-
tokenSecret: muxTokenSecret
|
|
3175
|
-
});
|
|
3771
|
+
const muxClient = await resolveMuxClient(credentials);
|
|
3772
|
+
const mux = await muxClient.createClient();
|
|
3176
3773
|
const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(languageCode) || languageCode.toUpperCase();
|
|
3177
3774
|
const trackName = `${languageName} (auto-dubbed)`;
|
|
3178
3775
|
const trackResponse = await mux.video.assets.createTrack(assetId, {
|
|
@@ -3192,34 +3789,24 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
3192
3789
|
provider = "elevenlabs",
|
|
3193
3790
|
numSpeakers = 0,
|
|
3194
3791
|
// 0 = auto-detect
|
|
3195
|
-
elevenLabsApiKey,
|
|
3196
3792
|
uploadToMux = true,
|
|
3793
|
+
storageAdapter,
|
|
3197
3794
|
credentials: providedCredentials
|
|
3198
3795
|
} = options;
|
|
3199
3796
|
if (provider !== "elevenlabs") {
|
|
3200
3797
|
throw new Error("Only ElevenLabs provider is currently supported for audio translation");
|
|
3201
3798
|
}
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
credentials = providedCredentials;
|
|
3205
|
-
} else if (providedCredentials || elevenLabsApiKey) {
|
|
3206
|
-
credentials = {
|
|
3207
|
-
...providedCredentials ?? {},
|
|
3208
|
-
...elevenLabsApiKey ? { elevenLabsApiKey } : {}
|
|
3209
|
-
};
|
|
3210
|
-
}
|
|
3799
|
+
const credentials = providedCredentials;
|
|
3800
|
+
const effectiveStorageAdapter = storageAdapter;
|
|
3211
3801
|
const s3Endpoint = options.s3Endpoint ?? env_default.S3_ENDPOINT;
|
|
3212
3802
|
const s3Region = options.s3Region ?? env_default.S3_REGION ?? "auto";
|
|
3213
3803
|
const s3Bucket = options.s3Bucket ?? env_default.S3_BUCKET;
|
|
3214
3804
|
const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
|
|
3215
3805
|
const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
|
|
3216
|
-
if (uploadToMux && (!s3Endpoint || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey)) {
|
|
3217
|
-
throw new Error("
|
|
3806
|
+
if (uploadToMux && (!s3Endpoint || !s3Bucket || !effectiveStorageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
|
|
3807
|
+
throw new Error("Storage configuration is required for uploading to Mux. Provide s3Endpoint and s3Bucket. If no storageAdapter is supplied, also provide s3AccessKeyId and s3SecretAccessKey in options or set S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY environment variables.");
|
|
3218
3808
|
}
|
|
3219
|
-
const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(
|
|
3220
|
-
assetId,
|
|
3221
|
-
credentials
|
|
3222
|
-
);
|
|
3809
|
+
const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
|
|
3223
3810
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(initialAsset);
|
|
3224
3811
|
let currentAsset = initialAsset;
|
|
3225
3812
|
if (!hasReadyAudioStaticRendition(currentAsset)) {
|
|
@@ -3333,7 +3920,8 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
3333
3920
|
toLanguageCode,
|
|
3334
3921
|
s3Endpoint,
|
|
3335
3922
|
s3Region,
|
|
3336
|
-
s3Bucket
|
|
3923
|
+
s3Bucket,
|
|
3924
|
+
storageAdapter: effectiveStorageAdapter
|
|
3337
3925
|
});
|
|
3338
3926
|
} catch (error) {
|
|
3339
3927
|
throw new Error(`Failed to upload audio to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -3369,7 +3957,6 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
3369
3957
|
}
|
|
3370
3958
|
|
|
3371
3959
|
// src/workflows/translate-captions.ts
|
|
3372
|
-
import Mux4 from "@mux/mux-node";
|
|
3373
3960
|
import { generateText as generateText5, Output as Output5 } from "ai";
|
|
3374
3961
|
import { z as z6 } from "zod";
|
|
3375
3962
|
var translationSchema = z6.object({
|
|
@@ -3423,51 +4010,37 @@ async function uploadVttToS3({
|
|
|
3423
4010
|
toLanguageCode,
|
|
3424
4011
|
s3Endpoint,
|
|
3425
4012
|
s3Region,
|
|
3426
|
-
s3Bucket
|
|
4013
|
+
s3Bucket,
|
|
4014
|
+
storageAdapter
|
|
3427
4015
|
}) {
|
|
3428
4016
|
"use step";
|
|
3429
|
-
const { S3Client, GetObjectCommand } = await import("@aws-sdk/client-s3");
|
|
3430
|
-
const { Upload } = await import("@aws-sdk/lib-storage");
|
|
3431
|
-
const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
|
|
3432
4017
|
const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
|
|
3433
4018
|
const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
|
|
3434
|
-
const
|
|
4019
|
+
const vttKey = `translations/${assetId}/${fromLanguageCode}-to-${toLanguageCode}-${Date.now()}.vtt`;
|
|
4020
|
+
await putObjectWithStorageAdapter({
|
|
4021
|
+
accessKeyId: s3AccessKeyId,
|
|
4022
|
+
secretAccessKey: s3SecretAccessKey,
|
|
4023
|
+
endpoint: s3Endpoint,
|
|
3435
4024
|
region: s3Region,
|
|
4025
|
+
bucket: s3Bucket,
|
|
4026
|
+
key: vttKey,
|
|
4027
|
+
body: translatedVtt,
|
|
4028
|
+
contentType: "text/vtt"
|
|
4029
|
+
}, storageAdapter);
|
|
4030
|
+
return createPresignedGetUrlWithStorageAdapter({
|
|
4031
|
+
accessKeyId: s3AccessKeyId,
|
|
4032
|
+
secretAccessKey: s3SecretAccessKey,
|
|
3436
4033
|
endpoint: s3Endpoint,
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
});
|
|
3443
|
-
const vttKey = `translations/${assetId}/${fromLanguageCode}-to-${toLanguageCode}-${Date.now()}.vtt`;
|
|
3444
|
-
const upload = new Upload({
|
|
3445
|
-
client: s3Client,
|
|
3446
|
-
params: {
|
|
3447
|
-
Bucket: s3Bucket,
|
|
3448
|
-
Key: vttKey,
|
|
3449
|
-
Body: translatedVtt,
|
|
3450
|
-
ContentType: "text/vtt"
|
|
3451
|
-
}
|
|
3452
|
-
});
|
|
3453
|
-
await upload.done();
|
|
3454
|
-
const getObjectCommand = new GetObjectCommand({
|
|
3455
|
-
Bucket: s3Bucket,
|
|
3456
|
-
Key: vttKey
|
|
3457
|
-
});
|
|
3458
|
-
const presignedUrl = await getSignedUrl(s3Client, getObjectCommand, {
|
|
3459
|
-
expiresIn: 3600
|
|
3460
|
-
// 1 hour
|
|
3461
|
-
});
|
|
3462
|
-
return presignedUrl;
|
|
4034
|
+
region: s3Region,
|
|
4035
|
+
bucket: s3Bucket,
|
|
4036
|
+
key: vttKey,
|
|
4037
|
+
expiresInSeconds: 3600
|
|
4038
|
+
}, storageAdapter);
|
|
3463
4039
|
}
|
|
3464
4040
|
async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials) {
|
|
3465
4041
|
"use step";
|
|
3466
|
-
const
|
|
3467
|
-
const mux =
|
|
3468
|
-
tokenId: muxTokenId,
|
|
3469
|
-
tokenSecret: muxTokenSecret
|
|
3470
|
-
});
|
|
4042
|
+
const muxClient = await resolveMuxClient(credentials);
|
|
4043
|
+
const mux = await muxClient.createClient();
|
|
3471
4044
|
const trackResponse = await mux.video.assets.createTrack(assetId, {
|
|
3472
4045
|
type: "text",
|
|
3473
4046
|
text_type: "subtitles",
|
|
@@ -3489,8 +4062,11 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
|
|
|
3489
4062
|
s3Region: providedS3Region,
|
|
3490
4063
|
s3Bucket: providedS3Bucket,
|
|
3491
4064
|
uploadToMux: uploadToMuxOption,
|
|
3492
|
-
|
|
4065
|
+
storageAdapter,
|
|
4066
|
+
credentials: providedCredentials
|
|
3493
4067
|
} = options;
|
|
4068
|
+
const credentials = providedCredentials;
|
|
4069
|
+
const effectiveStorageAdapter = storageAdapter;
|
|
3494
4070
|
const s3Endpoint = providedS3Endpoint ?? env_default.S3_ENDPOINT;
|
|
3495
4071
|
const s3Region = providedS3Region ?? env_default.S3_REGION ?? "auto";
|
|
3496
4072
|
const s3Bucket = providedS3Bucket ?? env_default.S3_BUCKET;
|
|
@@ -3502,13 +4078,10 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
|
|
|
3502
4078
|
model,
|
|
3503
4079
|
provider
|
|
3504
4080
|
});
|
|
3505
|
-
if (uploadToMux && (!s3Endpoint || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey)) {
|
|
3506
|
-
throw new Error("
|
|
4081
|
+
if (uploadToMux && (!s3Endpoint || !s3Bucket || !effectiveStorageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
|
|
4082
|
+
throw new Error("Storage configuration is required for uploading to Mux. Provide s3Endpoint and s3Bucket. If no storageAdapter is supplied, also provide s3AccessKeyId and s3SecretAccessKey in options or set S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY environment variables.");
|
|
3507
4083
|
}
|
|
3508
|
-
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(
|
|
3509
|
-
assetId,
|
|
3510
|
-
credentials
|
|
3511
|
-
);
|
|
4084
|
+
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
|
|
3512
4085
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
|
|
3513
4086
|
const isAudioOnly = isAudioOnlyAsset(assetData);
|
|
3514
4087
|
const signingContext = await resolveMuxSigningContext(credentials);
|
|
@@ -3593,7 +4166,8 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
|
|
|
3593
4166
|
toLanguageCode,
|
|
3594
4167
|
s3Endpoint,
|
|
3595
4168
|
s3Region,
|
|
3596
|
-
s3Bucket
|
|
4169
|
+
s3Bucket,
|
|
4170
|
+
storageAdapter: effectiveStorageAdapter
|
|
3597
4171
|
});
|
|
3598
4172
|
} catch (error) {
|
|
3599
4173
|
throw new Error(`Failed to upload VTT to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -3626,6 +4200,8 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
|
|
|
3626
4200
|
};
|
|
3627
4201
|
}
|
|
3628
4202
|
export {
|
|
4203
|
+
WorkflowStorageClient,
|
|
4204
|
+
createWorkflowStorageClient,
|
|
3629
4205
|
decryptFromWorkflow,
|
|
3630
4206
|
encryptForWorkflow,
|
|
3631
4207
|
primitives_exports as primitives,
|