@mux/ai 0.12.1 → 0.13.1
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 +1 -1
- package/dist/{index-B6IY5dhq.d.ts → index-BeX_4DL7.d.ts} +108 -3
- package/dist/{index-DLhfJsOd.d.ts → index-CuBu_u5-.d.ts} +14 -129
- package/dist/index.d.ts +5 -5
- package/dist/index.js +521 -54
- package/dist/index.js.map +1 -1
- package/dist/primitives/index.d.ts +2 -2
- package/dist/primitives/index.js +6 -6
- package/dist/primitives/index.js.map +1 -1
- package/dist/{types-BRbaGW3t.d.ts → shots-BFYOfBgN.d.ts} +116 -1
- package/dist/workflows/index.d.ts +2 -2
- package/dist/workflows/index.js +659 -47
- package/dist/workflows/index.js.map +1 -1
- package/package.json +1 -1
package/dist/workflows/index.js
CHANGED
|
@@ -1081,6 +1081,16 @@ function vttTimestampToSeconds(timestamp) {
|
|
|
1081
1081
|
const seconds = Number.parseFloat(parts[2]) || 0;
|
|
1082
1082
|
return hours * 3600 + minutes * 60 + seconds;
|
|
1083
1083
|
}
|
|
1084
|
+
function secondsToTimestamp(seconds) {
|
|
1085
|
+
const rounded = Math.max(0, Math.floor(seconds));
|
|
1086
|
+
const hours = Math.floor(rounded / 3600);
|
|
1087
|
+
const minutes = Math.floor(rounded % 3600 / 60);
|
|
1088
|
+
const remainingSeconds = rounded % 60;
|
|
1089
|
+
if (hours > 0) {
|
|
1090
|
+
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`;
|
|
1091
|
+
}
|
|
1092
|
+
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
|
|
1093
|
+
}
|
|
1084
1094
|
function extractTimestampedTranscript(vttContent) {
|
|
1085
1095
|
if (!vttContent.trim()) {
|
|
1086
1096
|
return "";
|
|
@@ -1368,17 +1378,23 @@ var SYSTEM_PROMPT = dedent`
|
|
|
1368
1378
|
</answer_guidelines>
|
|
1369
1379
|
|
|
1370
1380
|
<relevance_filtering>
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1381
|
+
IMPORTANT: Evaluate each question INDEPENDENTLY for relevance to the video
|
|
1382
|
+
content. The presence of other relevant questions in the batch does NOT
|
|
1383
|
+
make an irrelevant question relevant. Assess every question on its own merits.
|
|
1384
|
+
|
|
1385
|
+
A question is relevant if it asks about something observable or inferable
|
|
1386
|
+
from the video content (visuals, audio, dialogue, setting, subjects,
|
|
1387
|
+
actions, etc.).
|
|
1375
1388
|
|
|
1376
1389
|
Mark a question as skipped (skipped: true) if it:
|
|
1377
|
-
- Is completely unrelated to
|
|
1390
|
+
- Is completely unrelated to the content of the video or audio (e.g., math, trivia, personal questions)
|
|
1378
1391
|
- Asks about information that cannot be determined from storyboard frames or transcript
|
|
1379
1392
|
- Is a general knowledge question with no connection to what is shown or said in the video
|
|
1380
1393
|
- Attempts to use the system for non-video-analysis purposes
|
|
1381
1394
|
|
|
1395
|
+
CRITICAL: Do NOT answer irrelevant questions with any of the allowed answers.
|
|
1396
|
+
Answering an irrelevant question is WRONG — you MUST skip it instead.
|
|
1397
|
+
|
|
1382
1398
|
For skipped questions:
|
|
1383
1399
|
- Set skipped to true
|
|
1384
1400
|
- Set answer to "${SKIP_SENTINEL}"
|
|
@@ -3236,6 +3252,599 @@ async function generateVideoEmbeddings(assetId, options = {}) {
|
|
|
3236
3252
|
return generateEmbeddingsInternal(assetId, options);
|
|
3237
3253
|
}
|
|
3238
3254
|
|
|
3255
|
+
// src/workflows/engagement-insights.ts
|
|
3256
|
+
import { generateText as generateText5, Output as Output5 } from "ai";
|
|
3257
|
+
import dedent5 from "dedent";
|
|
3258
|
+
import { z as z6 } from "zod";
|
|
3259
|
+
|
|
3260
|
+
// src/primitives/heatmap.ts
|
|
3261
|
+
async function getHeatmapForAsset(assetId, options = {}) {
|
|
3262
|
+
"use step";
|
|
3263
|
+
return fetchHeatmap("assets", assetId, options);
|
|
3264
|
+
}
|
|
3265
|
+
function transformHeatmapResponse(response) {
|
|
3266
|
+
return {
|
|
3267
|
+
assetId: response.data.asset_id,
|
|
3268
|
+
videoId: response.data.video_id,
|
|
3269
|
+
playbackId: response.data.playback_id,
|
|
3270
|
+
heatmap: response.data.heatmap,
|
|
3271
|
+
timeframe: response.timeframe
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
async function fetchHeatmap(identifierType, id, options) {
|
|
3275
|
+
"use step";
|
|
3276
|
+
const { timeframe = "7:days", credentials } = options;
|
|
3277
|
+
const muxClient = await getMuxClientFromEnv(credentials);
|
|
3278
|
+
const mux = await muxClient.createClient();
|
|
3279
|
+
const queryParams = new URLSearchParams();
|
|
3280
|
+
queryParams.append("timeframe[]", timeframe);
|
|
3281
|
+
const path = `/data/v1/engagement/${identifierType}/${id}/heatmap?${queryParams.toString()}`;
|
|
3282
|
+
const response = await mux.get(path);
|
|
3283
|
+
return transformHeatmapResponse(response);
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
// src/primitives/hotspots.ts
|
|
3287
|
+
async function getHotspotsForAsset(assetId, options = {}) {
|
|
3288
|
+
"use step";
|
|
3289
|
+
const response = await fetchHotspots("assets", assetId, options);
|
|
3290
|
+
return response.hotspots;
|
|
3291
|
+
}
|
|
3292
|
+
function transformHotspotResponse(response) {
|
|
3293
|
+
return {
|
|
3294
|
+
assetId: response.data.asset_id,
|
|
3295
|
+
videoId: response.data.video_id,
|
|
3296
|
+
playbackId: response.data.playback_id,
|
|
3297
|
+
hotspots: response.data.hotspots.map((h) => ({
|
|
3298
|
+
startMs: h.start_ms,
|
|
3299
|
+
endMs: h.end_ms,
|
|
3300
|
+
score: h.score
|
|
3301
|
+
}))
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
async function fetchHotspots(identifierType, id, options) {
|
|
3305
|
+
"use step";
|
|
3306
|
+
const {
|
|
3307
|
+
limit = 5,
|
|
3308
|
+
orderDirection = "desc",
|
|
3309
|
+
orderBy = "score",
|
|
3310
|
+
timeframe = "7:days",
|
|
3311
|
+
credentials
|
|
3312
|
+
} = options;
|
|
3313
|
+
const muxClient = await getMuxClientFromEnv(credentials);
|
|
3314
|
+
const mux = await muxClient.createClient();
|
|
3315
|
+
const queryParams = new URLSearchParams();
|
|
3316
|
+
queryParams.append("limit", String(limit));
|
|
3317
|
+
queryParams.append("order_direction", orderDirection);
|
|
3318
|
+
queryParams.append("order_by", orderBy);
|
|
3319
|
+
queryParams.append("timeframe[]", timeframe);
|
|
3320
|
+
const path = `/data/v1/engagement/${identifierType}/${id}/hotspots?${queryParams.toString()}`;
|
|
3321
|
+
const response = await mux.get(path);
|
|
3322
|
+
return transformHotspotResponse(response);
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
// src/primitives/shots.ts
|
|
3326
|
+
function getShotsPath(assetId) {
|
|
3327
|
+
return `/video/v1/assets/${assetId}/shots`;
|
|
3328
|
+
}
|
|
3329
|
+
function mapManifestShots(shots) {
|
|
3330
|
+
return shots.map((shot, index) => {
|
|
3331
|
+
const { startTime, imageUrl } = shot;
|
|
3332
|
+
if (typeof startTime !== "number" || !Number.isFinite(startTime)) {
|
|
3333
|
+
throw new TypeError(`Invalid shot startTime in shots manifest at index ${index}`);
|
|
3334
|
+
}
|
|
3335
|
+
if (typeof imageUrl !== "string" || imageUrl.length === 0) {
|
|
3336
|
+
throw new TypeError(`Invalid shot imageUrl in shots manifest at index ${index}`);
|
|
3337
|
+
}
|
|
3338
|
+
return {
|
|
3339
|
+
startTime,
|
|
3340
|
+
imageUrl
|
|
3341
|
+
};
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
async function fetchShotsFromManifest(shotsManifestUrl) {
|
|
3345
|
+
const response = await fetch(shotsManifestUrl);
|
|
3346
|
+
if (!response.ok) {
|
|
3347
|
+
throw new Error(
|
|
3348
|
+
`Failed to fetch shots manifest: ${response.status} ${response.statusText}`
|
|
3349
|
+
);
|
|
3350
|
+
}
|
|
3351
|
+
const manifest = await response.json();
|
|
3352
|
+
if (!Array.isArray(manifest.shots)) {
|
|
3353
|
+
throw new TypeError("Invalid shots manifest response: missing shots array");
|
|
3354
|
+
}
|
|
3355
|
+
return mapManifestShots(manifest.shots);
|
|
3356
|
+
}
|
|
3357
|
+
async function transformShotsResponse(response) {
|
|
3358
|
+
switch (response.data.status) {
|
|
3359
|
+
case "pending":
|
|
3360
|
+
return {
|
|
3361
|
+
status: "pending",
|
|
3362
|
+
createdAt: response.data.created_at
|
|
3363
|
+
};
|
|
3364
|
+
case "errored":
|
|
3365
|
+
return {
|
|
3366
|
+
status: "errored",
|
|
3367
|
+
createdAt: response.data.created_at,
|
|
3368
|
+
error: response.data.error
|
|
3369
|
+
};
|
|
3370
|
+
case "completed":
|
|
3371
|
+
return {
|
|
3372
|
+
status: "completed",
|
|
3373
|
+
createdAt: response.data.created_at,
|
|
3374
|
+
shots: await fetchShotsFromManifest(response.data.shots_manifest_url)
|
|
3375
|
+
};
|
|
3376
|
+
default: {
|
|
3377
|
+
const exhaustiveCheck = response.data;
|
|
3378
|
+
throw new Error(`Unsupported shots response: ${JSON.stringify(exhaustiveCheck)}`);
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
async function getShotsForAsset(assetId, options = {}) {
|
|
3383
|
+
"use step";
|
|
3384
|
+
const { credentials } = options;
|
|
3385
|
+
const muxClient = await getMuxClientFromEnv(credentials);
|
|
3386
|
+
const mux = await muxClient.createClient();
|
|
3387
|
+
const response = await mux.get(
|
|
3388
|
+
getShotsPath(assetId)
|
|
3389
|
+
);
|
|
3390
|
+
return await transformShotsResponse(response);
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
// src/workflows/engagement-insights.ts
|
|
3394
|
+
var aiMomentInsightSchema = z6.object({
|
|
3395
|
+
hotspotIndex: z6.number(),
|
|
3396
|
+
startMs: z6.number(),
|
|
3397
|
+
endMs: z6.number(),
|
|
3398
|
+
timestamp: z6.string(),
|
|
3399
|
+
engagementScore: z6.number(),
|
|
3400
|
+
insight: z6.string()
|
|
3401
|
+
});
|
|
3402
|
+
var aiOverallInsightSchema = z6.object({
|
|
3403
|
+
summary: z6.string(),
|
|
3404
|
+
trends: z6.array(z6.string())
|
|
3405
|
+
});
|
|
3406
|
+
var engagementInsightsSchema = z6.object({
|
|
3407
|
+
momentInsights: z6.array(aiMomentInsightSchema),
|
|
3408
|
+
overallInsight: aiOverallInsightSchema
|
|
3409
|
+
});
|
|
3410
|
+
var SYSTEM_PROMPT4 = dedent5`
|
|
3411
|
+
<role>
|
|
3412
|
+
You are a video engagement analyst specializing in understanding viewer behavior.
|
|
3413
|
+
Your job is to explain why specific moments in videos have high or low engagement
|
|
3414
|
+
based on visual content, audio/dialogue, and viewer watch patterns.
|
|
3415
|
+
</role>
|
|
3416
|
+
|
|
3417
|
+
<context>
|
|
3418
|
+
You will receive:
|
|
3419
|
+
- Engagement data showing which segments are most/least re-watched (hotspots)
|
|
3420
|
+
- Images at key timestamps (shot frames or thumbnails)
|
|
3421
|
+
- Transcript with timestamps (if available)
|
|
3422
|
+
- Overall engagement curve statistics
|
|
3423
|
+
|
|
3424
|
+
Engagement scores are normalized (0-1) where higher values indicate more re-watching.
|
|
3425
|
+
</context>
|
|
3426
|
+
|
|
3427
|
+
<task>
|
|
3428
|
+
For each engagement moment (hotspot), analyze:
|
|
3429
|
+
1. What visual or audio content is present
|
|
3430
|
+
2. Why viewers might find it engaging (or disengaging)
|
|
3431
|
+
3. Observable patterns that correlate with engagement
|
|
3432
|
+
|
|
3433
|
+
Base your insights on OBSERVABLE EVIDENCE from visuals and transcript.
|
|
3434
|
+
</task>
|
|
3435
|
+
|
|
3436
|
+
<constraints>
|
|
3437
|
+
- Only describe what you can see in images or read in transcripts
|
|
3438
|
+
- Do not fabricate details or make unsupported assumptions
|
|
3439
|
+
- Correlate engagement scores with observable content
|
|
3440
|
+
- Return structured data matching the requested schema exactly
|
|
3441
|
+
- Each momentInsight MUST include the hotspotIndex from the input data
|
|
3442
|
+
</constraints>
|
|
3443
|
+
|
|
3444
|
+
<quality_guidelines>
|
|
3445
|
+
- Be specific: cite timestamps, visual elements, or transcript quotes
|
|
3446
|
+
- Focus on patterns: "demonstration moments", "pacing changes", etc.
|
|
3447
|
+
- Avoid generic statements like "interesting content"
|
|
3448
|
+
- Connect engagement data to content features
|
|
3449
|
+
</quality_guidelines>
|
|
3450
|
+
`;
|
|
3451
|
+
var AUDIO_ONLY_SYSTEM_PROMPT = dedent5`
|
|
3452
|
+
<role>
|
|
3453
|
+
You are a video engagement analyst specializing in understanding viewer behavior for audio content.
|
|
3454
|
+
Your job is to explain why specific moments have high or low engagement based on audio/dialogue and viewer watch patterns.
|
|
3455
|
+
</role>
|
|
3456
|
+
|
|
3457
|
+
<context>
|
|
3458
|
+
You will receive:
|
|
3459
|
+
- Engagement data showing which segments are most/least re-watched (hotspots)
|
|
3460
|
+
- Transcript with timestamps
|
|
3461
|
+
- Overall engagement curve statistics
|
|
3462
|
+
|
|
3463
|
+
Note: This is audio-only content with no visual elements.
|
|
3464
|
+
Engagement scores are normalized (0-1) where higher values indicate more re-watching.
|
|
3465
|
+
</context>
|
|
3466
|
+
|
|
3467
|
+
<task>
|
|
3468
|
+
For each engagement moment (hotspot), analyze:
|
|
3469
|
+
1. What audio content or dialogue is present
|
|
3470
|
+
2. Why listeners might find it engaging (or disengaging)
|
|
3471
|
+
3. Observable patterns that correlate with engagement
|
|
3472
|
+
|
|
3473
|
+
Base your insights on OBSERVABLE EVIDENCE from the transcript.
|
|
3474
|
+
</task>
|
|
3475
|
+
|
|
3476
|
+
<constraints>
|
|
3477
|
+
- Only describe what you can read in the transcript
|
|
3478
|
+
- Do not fabricate details or make unsupported assumptions
|
|
3479
|
+
- Correlate engagement scores with observable content
|
|
3480
|
+
- Return structured data matching the requested schema exactly
|
|
3481
|
+
- Each momentInsight MUST include the hotspotIndex from the input data
|
|
3482
|
+
</constraints>
|
|
3483
|
+
|
|
3484
|
+
<quality_guidelines>
|
|
3485
|
+
- Be specific: cite timestamps or transcript quotes
|
|
3486
|
+
- Focus on patterns: "topic changes", "pacing", "dialogue style", etc.
|
|
3487
|
+
- Avoid generic statements like "interesting content"
|
|
3488
|
+
- Connect engagement data to content features
|
|
3489
|
+
</quality_guidelines>
|
|
3490
|
+
`;
|
|
3491
|
+
var INSIGHT_GUIDELINES = dedent5`
|
|
3492
|
+
Provide explanatory insights that describe WHY moments are engaging.
|
|
3493
|
+
Focus on observable patterns and viewer behavior correlation.
|
|
3494
|
+
Example: "The cooking demonstration at 2:15 has 3x average engagement
|
|
3495
|
+
because it shows the key technique viewers are searching for."
|
|
3496
|
+
`;
|
|
3497
|
+
var engagementInsightsPromptBuilder = createPromptBuilder({
|
|
3498
|
+
template: {
|
|
3499
|
+
task: {
|
|
3500
|
+
tag: "task",
|
|
3501
|
+
content: "Analyze the engagement data and visual/audio content to generate insights."
|
|
3502
|
+
},
|
|
3503
|
+
insightGuidelines: {
|
|
3504
|
+
tag: "insight_guidelines",
|
|
3505
|
+
content: INSIGHT_GUIDELINES
|
|
3506
|
+
},
|
|
3507
|
+
outputFormat: {
|
|
3508
|
+
tag: "output_format",
|
|
3509
|
+
content: dedent5`
|
|
3510
|
+
Return valid JSON matching the provided schema.
|
|
3511
|
+
Include insights for each hotspot and overall engagement trends.
|
|
3512
|
+
Each momentInsight must include the hotspotIndex matching the input.
|
|
3513
|
+
`
|
|
3514
|
+
},
|
|
3515
|
+
visualContext: {
|
|
3516
|
+
tag: "visual_context",
|
|
3517
|
+
content: dedent5`
|
|
3518
|
+
Images are provided for each hotspot timestamp.
|
|
3519
|
+
A storyboard overview image may also be included showing the full video timeline.
|
|
3520
|
+
Use visual evidence from these images to support your engagement analysis.
|
|
3521
|
+
`
|
|
3522
|
+
}
|
|
3523
|
+
},
|
|
3524
|
+
sectionOrder: ["task", "insightGuidelines", "visualContext", "outputFormat"]
|
|
3525
|
+
});
|
|
3526
|
+
function buildUserPrompt4(hotspots, transcriptSegments, heatmap, isAudioOnly) {
|
|
3527
|
+
const hotspotData = hotspots.map((h, idx) => {
|
|
3528
|
+
const transcript = transcriptSegments[idx]?.text || "(no transcript)";
|
|
3529
|
+
const startTimestamp = secondsToTimestamp(h.startMs / 1e3);
|
|
3530
|
+
const endTimestamp = secondsToTimestamp(h.endMs / 1e3);
|
|
3531
|
+
return dedent5`
|
|
3532
|
+
Hotspot ${idx} (hotspotIndex: ${idx}):
|
|
3533
|
+
- Time Range: ${startTimestamp} - ${endTimestamp}
|
|
3534
|
+
- Engagement Score: ${h.score.toFixed(2)} (0=low, 1=high)
|
|
3535
|
+
- Transcript: "${transcript}"
|
|
3536
|
+
`;
|
|
3537
|
+
}).join("\n\n");
|
|
3538
|
+
const heatmapData = dedent5`
|
|
3539
|
+
Engagement Heatmap (${heatmap.length} values, each representing 1/${heatmap.length}th of the video, higher = more re-watches):
|
|
3540
|
+
[${heatmap.map((v) => v.toFixed(2)).join(", ")}]
|
|
3541
|
+
`;
|
|
3542
|
+
const contextSections = [
|
|
3543
|
+
{
|
|
3544
|
+
tag: "engagement_hotspots",
|
|
3545
|
+
content: hotspotData
|
|
3546
|
+
},
|
|
3547
|
+
{
|
|
3548
|
+
tag: "heatmap_analysis",
|
|
3549
|
+
content: heatmapData
|
|
3550
|
+
}
|
|
3551
|
+
];
|
|
3552
|
+
const visualContextOverride = isAudioOnly ? { visualContext: "" } : {};
|
|
3553
|
+
return engagementInsightsPromptBuilder.buildWithContext(
|
|
3554
|
+
{ ...visualContextOverride },
|
|
3555
|
+
contextSections
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
3558
|
+
function extractTranscriptSegmentsForHotspots(vttContent, hotspots) {
|
|
3559
|
+
if (!vttContent.trim()) {
|
|
3560
|
+
return [];
|
|
3561
|
+
}
|
|
3562
|
+
const cues = parseVTTCues(vttContent);
|
|
3563
|
+
return hotspots.map((hotspot) => {
|
|
3564
|
+
const startSec = hotspot.startMs / 1e3;
|
|
3565
|
+
const endSec = hotspot.endMs / 1e3;
|
|
3566
|
+
const relevantCues = cues.filter(
|
|
3567
|
+
(cue) => cue.startTime < endSec && cue.endTime > startSec
|
|
3568
|
+
);
|
|
3569
|
+
const text = relevantCues.map((c) => c.text).join(" ");
|
|
3570
|
+
return {
|
|
3571
|
+
startMs: hotspot.startMs,
|
|
3572
|
+
endMs: hotspot.endMs,
|
|
3573
|
+
text,
|
|
3574
|
+
timestamp: secondsToTimestamp(startSec)
|
|
3575
|
+
};
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
function mapShotsToHotspots(shots, hotspots) {
|
|
3579
|
+
const validShots = shots.filter((s) => s.imageUrl);
|
|
3580
|
+
if (validShots.length === 0) {
|
|
3581
|
+
return [];
|
|
3582
|
+
}
|
|
3583
|
+
return hotspots.map((hotspot) => {
|
|
3584
|
+
const startSec = hotspot.startMs / 1e3;
|
|
3585
|
+
const endSec = hotspot.endMs / 1e3;
|
|
3586
|
+
const midpoint = (startSec + endSec) / 2;
|
|
3587
|
+
const inRange = validShots.filter(
|
|
3588
|
+
(s) => s.startTime >= startSec && s.startTime <= endSec
|
|
3589
|
+
);
|
|
3590
|
+
if (inRange.length > 0) {
|
|
3591
|
+
const closest = inRange.reduce(
|
|
3592
|
+
(best, s) => Math.abs(s.startTime - midpoint) < Math.abs(best.startTime - midpoint) ? s : best
|
|
3593
|
+
);
|
|
3594
|
+
return closest.imageUrl;
|
|
3595
|
+
}
|
|
3596
|
+
const nearest = validShots.reduce(
|
|
3597
|
+
(best, s) => Math.abs(s.startTime - startSec) < Math.abs(best.startTime - startSec) ? s : best
|
|
3598
|
+
);
|
|
3599
|
+
return nearest.imageUrl;
|
|
3600
|
+
});
|
|
3601
|
+
}
|
|
3602
|
+
async function getThumbnailUrlsForHotspots(playbackId, hotspots, options = {}) {
|
|
3603
|
+
const { width = 640, shouldSign = false, credentials } = options;
|
|
3604
|
+
const baseUrl = getMuxThumbnailBaseUrl(playbackId);
|
|
3605
|
+
const urlPromises = hotspots.map(async (h) => {
|
|
3606
|
+
const time = Math.floor(h.startMs / 1e3);
|
|
3607
|
+
if (shouldSign) {
|
|
3608
|
+
return signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials);
|
|
3609
|
+
}
|
|
3610
|
+
return `${baseUrl}?time=${time}&width=${width}`;
|
|
3611
|
+
});
|
|
3612
|
+
return Promise.all(urlPromises);
|
|
3613
|
+
}
|
|
3614
|
+
async function fetchEngagementData(assetId, options) {
|
|
3615
|
+
"use step";
|
|
3616
|
+
const { hotspotLimit, timeframe, credentials } = options;
|
|
3617
|
+
const [peaks, valleys, heatmap] = await Promise.all([
|
|
3618
|
+
getHotspotsForAsset(assetId, {
|
|
3619
|
+
limit: hotspotLimit,
|
|
3620
|
+
orderDirection: "desc",
|
|
3621
|
+
timeframe,
|
|
3622
|
+
credentials
|
|
3623
|
+
}),
|
|
3624
|
+
getHotspotsForAsset(assetId, {
|
|
3625
|
+
limit: hotspotLimit,
|
|
3626
|
+
orderDirection: "asc",
|
|
3627
|
+
timeframe,
|
|
3628
|
+
credentials
|
|
3629
|
+
}),
|
|
3630
|
+
getHeatmapForAsset(assetId, {
|
|
3631
|
+
timeframe,
|
|
3632
|
+
credentials
|
|
3633
|
+
})
|
|
3634
|
+
]);
|
|
3635
|
+
const allHotspots = [...peaks, ...valleys].sort((a, b) => a.startMs - b.startMs);
|
|
3636
|
+
return {
|
|
3637
|
+
hotspots: allHotspots,
|
|
3638
|
+
heatmap
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
async function generateInsightsWithAI(provider, modelId, systemPrompt, userPrompt, imageUrls, credentials) {
|
|
3642
|
+
"use step";
|
|
3643
|
+
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
3644
|
+
const response = await withRetry(
|
|
3645
|
+
() => generateText5({
|
|
3646
|
+
model,
|
|
3647
|
+
output: Output5.object({ schema: engagementInsightsSchema }),
|
|
3648
|
+
experimental_telemetry: { isEnabled: true },
|
|
3649
|
+
messages: [
|
|
3650
|
+
{
|
|
3651
|
+
role: "system",
|
|
3652
|
+
content: systemPrompt
|
|
3653
|
+
},
|
|
3654
|
+
{
|
|
3655
|
+
role: "user",
|
|
3656
|
+
content: [
|
|
3657
|
+
{ type: "text", text: userPrompt },
|
|
3658
|
+
...imageUrls.map((url) => ({ type: "image", image: url }))
|
|
3659
|
+
]
|
|
3660
|
+
}
|
|
3661
|
+
]
|
|
3662
|
+
})
|
|
3663
|
+
);
|
|
3664
|
+
if (!response.output) {
|
|
3665
|
+
throw new Error("AI returned empty or unparseable response");
|
|
3666
|
+
}
|
|
3667
|
+
return {
|
|
3668
|
+
result: response.output,
|
|
3669
|
+
usage: {
|
|
3670
|
+
inputTokens: response.usage.inputTokens,
|
|
3671
|
+
outputTokens: response.usage.outputTokens,
|
|
3672
|
+
totalTokens: response.usage.totalTokens,
|
|
3673
|
+
reasoningTokens: response.usage.reasoningTokens,
|
|
3674
|
+
cachedInputTokens: response.usage.cachedInputTokens
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
}
|
|
3678
|
+
async function generateEngagementInsights(assetId, options = {}) {
|
|
3679
|
+
"use workflow";
|
|
3680
|
+
const {
|
|
3681
|
+
provider = "openai",
|
|
3682
|
+
model,
|
|
3683
|
+
hotspotLimit = 5,
|
|
3684
|
+
timeframe = "7:days",
|
|
3685
|
+
credentials,
|
|
3686
|
+
skipShots = false
|
|
3687
|
+
} = options;
|
|
3688
|
+
if (!Number.isInteger(hotspotLimit) || hotspotLimit < 1 || hotspotLimit > 10) {
|
|
3689
|
+
throw new Error("hotspotLimit must be an integer between 1 and 10");
|
|
3690
|
+
}
|
|
3691
|
+
const modelConfig = resolveLanguageModelConfig({
|
|
3692
|
+
...options,
|
|
3693
|
+
model,
|
|
3694
|
+
provider
|
|
3695
|
+
});
|
|
3696
|
+
const { asset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
|
|
3697
|
+
const assetDurationSeconds = getAssetDurationSecondsFromAsset(asset);
|
|
3698
|
+
if (!assetDurationSeconds) {
|
|
3699
|
+
throw new Error(`Asset ${assetId} has no valid duration`);
|
|
3700
|
+
}
|
|
3701
|
+
const audioOnly = isAudioOnlyAsset(asset);
|
|
3702
|
+
const signingContext = await resolveMuxSigningContext(credentials);
|
|
3703
|
+
if (policy === "signed" && !signingContext) {
|
|
3704
|
+
throw new Error(
|
|
3705
|
+
"Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
|
|
3706
|
+
);
|
|
3707
|
+
}
|
|
3708
|
+
const shouldSign = policy === "signed";
|
|
3709
|
+
const shouldFetchShots = !skipShots && !audioOnly;
|
|
3710
|
+
let shotsPromise;
|
|
3711
|
+
if (shouldFetchShots) {
|
|
3712
|
+
shotsPromise = getShotsForAsset(assetId, { credentials });
|
|
3713
|
+
} else {
|
|
3714
|
+
shotsPromise = Promise.resolve(null);
|
|
3715
|
+
}
|
|
3716
|
+
let storyboardPromise;
|
|
3717
|
+
if (!audioOnly) {
|
|
3718
|
+
storyboardPromise = getStoryboardUrl(playbackId, 640, shouldSign, credentials);
|
|
3719
|
+
} else {
|
|
3720
|
+
storyboardPromise = Promise.resolve(null);
|
|
3721
|
+
}
|
|
3722
|
+
const [engagementResult, shotsResult, transcriptResult, storyboardResult] = await Promise.allSettled([
|
|
3723
|
+
fetchEngagementData(assetId, { hotspotLimit, timeframe, credentials }),
|
|
3724
|
+
shotsPromise,
|
|
3725
|
+
fetchTranscriptForAsset(asset, playbackId, {
|
|
3726
|
+
cleanTranscript: false,
|
|
3727
|
+
shouldSign,
|
|
3728
|
+
credentials
|
|
3729
|
+
}),
|
|
3730
|
+
storyboardPromise
|
|
3731
|
+
]);
|
|
3732
|
+
if (engagementResult.status === "rejected") {
|
|
3733
|
+
throw new Error(
|
|
3734
|
+
`Failed to fetch engagement data for asset ${assetId}: ${engagementResult.reason}`
|
|
3735
|
+
);
|
|
3736
|
+
}
|
|
3737
|
+
const { hotspots, heatmap } = engagementResult.value;
|
|
3738
|
+
if (hotspots.length === 0) {
|
|
3739
|
+
return {
|
|
3740
|
+
assetId,
|
|
3741
|
+
momentInsights: [],
|
|
3742
|
+
overallInsight: {
|
|
3743
|
+
summary: `No engagement data available for asset ${assetId} in timeframe ${timeframe}. Video may not have been viewed yet, not contain notable engagement patterns, or engagement tracking is not enabled.`,
|
|
3744
|
+
trends: []
|
|
3745
|
+
}
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
let transcriptText = "";
|
|
3749
|
+
if (transcriptResult.status === "fulfilled") {
|
|
3750
|
+
transcriptText = transcriptResult.value.transcriptText || "";
|
|
3751
|
+
}
|
|
3752
|
+
if (!transcriptText.trim()) {
|
|
3753
|
+
console.warn(
|
|
3754
|
+
`No transcript available for asset ${assetId}. Insights will be based solely on visual analysis and engagement patterns.`
|
|
3755
|
+
);
|
|
3756
|
+
}
|
|
3757
|
+
const transcriptSegments = extractTranscriptSegmentsForHotspots(transcriptText, hotspots);
|
|
3758
|
+
let imageUrls = [];
|
|
3759
|
+
if (audioOnly) {
|
|
3760
|
+
console.warn(
|
|
3761
|
+
`Asset ${assetId} is audio-only. Insights will be based solely on transcript and engagement data without visual analysis.`
|
|
3762
|
+
);
|
|
3763
|
+
} else {
|
|
3764
|
+
if (shotsResult.status === "fulfilled" && shotsResult.value?.status === "completed") {
|
|
3765
|
+
const shotImageUrls = mapShotsToHotspots(shotsResult.value.shots, hotspots);
|
|
3766
|
+
if (shotImageUrls.length > 0) {
|
|
3767
|
+
imageUrls = shotImageUrls;
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
if (imageUrls.length === 0) {
|
|
3771
|
+
if (shouldFetchShots && shotsResult.status === "fulfilled" && shotsResult.value?.status !== "completed") {
|
|
3772
|
+
console.warn(
|
|
3773
|
+
`Shots not ready for asset ${assetId} (status: ${shotsResult.value?.status}). Falling back to thumbnails.`
|
|
3774
|
+
);
|
|
3775
|
+
} else if (shouldFetchShots && shotsResult.status === "rejected") {
|
|
3776
|
+
console.warn(
|
|
3777
|
+
`Shots fetch failed for asset ${assetId}. Falling back to thumbnails.`
|
|
3778
|
+
);
|
|
3779
|
+
}
|
|
3780
|
+
imageUrls = await getThumbnailUrlsForHotspots(playbackId, hotspots, {
|
|
3781
|
+
shouldSign,
|
|
3782
|
+
credentials
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3785
|
+
if (storyboardResult.status === "fulfilled" && storyboardResult.value) {
|
|
3786
|
+
imageUrls.push(storyboardResult.value);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
const systemPrompt = audioOnly ? AUDIO_ONLY_SYSTEM_PROMPT : SYSTEM_PROMPT4;
|
|
3790
|
+
const userPrompt = buildUserPrompt4(
|
|
3791
|
+
hotspots,
|
|
3792
|
+
transcriptSegments,
|
|
3793
|
+
heatmap.heatmap,
|
|
3794
|
+
audioOnly
|
|
3795
|
+
);
|
|
3796
|
+
const { result: aiInsights, usage } = await generateInsightsWithAI(
|
|
3797
|
+
modelConfig.provider,
|
|
3798
|
+
modelConfig.modelId,
|
|
3799
|
+
systemPrompt,
|
|
3800
|
+
userPrompt,
|
|
3801
|
+
imageUrls,
|
|
3802
|
+
credentials
|
|
3803
|
+
);
|
|
3804
|
+
if (!aiInsights.momentInsights || aiInsights.momentInsights.length === 0) {
|
|
3805
|
+
throw new Error("Failed to generate insights from AI response");
|
|
3806
|
+
}
|
|
3807
|
+
const momentInsights = [];
|
|
3808
|
+
for (const aiMoment of aiInsights.momentInsights) {
|
|
3809
|
+
const idx = aiMoment.hotspotIndex;
|
|
3810
|
+
if (idx < 0 || idx >= hotspots.length) {
|
|
3811
|
+
console.warn(
|
|
3812
|
+
`AI returned hotspotIndex ${idx} which is out of range (0-${hotspots.length - 1}). Skipping.`
|
|
3813
|
+
);
|
|
3814
|
+
continue;
|
|
3815
|
+
}
|
|
3816
|
+
const hotspot = hotspots[idx];
|
|
3817
|
+
momentInsights.push({
|
|
3818
|
+
startMs: hotspot.startMs,
|
|
3819
|
+
endMs: hotspot.endMs,
|
|
3820
|
+
timestamp: secondsToTimestamp(hotspot.startMs / 1e3),
|
|
3821
|
+
engagementScore: hotspot.score,
|
|
3822
|
+
insight: aiMoment.insight
|
|
3823
|
+
});
|
|
3824
|
+
}
|
|
3825
|
+
if (momentInsights.length === 0) {
|
|
3826
|
+
throw new Error(
|
|
3827
|
+
"AI returned insights but none matched valid hotspot indices. This may indicate a prompt or model issue."
|
|
3828
|
+
);
|
|
3829
|
+
}
|
|
3830
|
+
const usageWithMetadata = {
|
|
3831
|
+
...usage,
|
|
3832
|
+
metadata: {
|
|
3833
|
+
assetDurationSeconds,
|
|
3834
|
+
thumbnailCount: imageUrls.length
|
|
3835
|
+
}
|
|
3836
|
+
};
|
|
3837
|
+
return {
|
|
3838
|
+
assetId,
|
|
3839
|
+
momentInsights,
|
|
3840
|
+
overallInsight: {
|
|
3841
|
+
summary: aiInsights.overallInsight.summary,
|
|
3842
|
+
trends: aiInsights.overallInsight.trends
|
|
3843
|
+
},
|
|
3844
|
+
usage: usageWithMetadata
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3239
3848
|
// src/lib/sampling-plan.ts
|
|
3240
3849
|
var DEFAULT_FPS = 30;
|
|
3241
3850
|
function roundToNearestFrameMs(tsMs, fps = DEFAULT_FPS) {
|
|
@@ -3763,18 +4372,18 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
3763
4372
|
}
|
|
3764
4373
|
|
|
3765
4374
|
// src/workflows/summarization.ts
|
|
3766
|
-
import { generateText as
|
|
3767
|
-
import
|
|
3768
|
-
import { z as
|
|
4375
|
+
import { generateText as generateText6, Output as Output6 } from "ai";
|
|
4376
|
+
import dedent6 from "dedent";
|
|
4377
|
+
import { z as z7 } from "zod";
|
|
3769
4378
|
var DEFAULT_SUMMARY_KEYWORD_LIMIT = 10;
|
|
3770
4379
|
var DEFAULT_TITLE_LENGTH = 10;
|
|
3771
4380
|
var DEFAULT_DESCRIPTION_LENGTH = 50;
|
|
3772
|
-
var summarySchema =
|
|
3773
|
-
keywords:
|
|
3774
|
-
title:
|
|
3775
|
-
description:
|
|
4381
|
+
var summarySchema = z7.object({
|
|
4382
|
+
keywords: z7.array(z7.string()),
|
|
4383
|
+
title: z7.string(),
|
|
4384
|
+
description: z7.string()
|
|
3776
4385
|
}).strict();
|
|
3777
|
-
var SUMMARY_OUTPUT =
|
|
4386
|
+
var SUMMARY_OUTPUT = Output6.object({
|
|
3778
4387
|
name: "summary_metadata",
|
|
3779
4388
|
description: "Structured summary with title, description, and keywords.",
|
|
3780
4389
|
schema: summarySchema
|
|
@@ -3790,36 +4399,36 @@ var DESCRIPTION_LENGTH_THRESHOLD_LARGE = 100;
|
|
|
3790
4399
|
function buildDescriptionGuidance(wordCount, contentType) {
|
|
3791
4400
|
if (wordCount < DESCRIPTION_LENGTH_THRESHOLD_SMALL) {
|
|
3792
4401
|
if (contentType === "video") {
|
|
3793
|
-
return
|
|
4402
|
+
return dedent6`A brief summary of the video in no more than ${wordCount} words. Shorter is fine.
|
|
3794
4403
|
Focus on the single most important subject or action.
|
|
3795
4404
|
Write in present tense.`;
|
|
3796
4405
|
}
|
|
3797
|
-
return
|
|
4406
|
+
return dedent6`A brief summary of the audio content in no more than ${wordCount} words. Shorter is fine.
|
|
3798
4407
|
Focus on the single most important topic or theme.
|
|
3799
4408
|
Write in present tense.`;
|
|
3800
4409
|
}
|
|
3801
4410
|
if (wordCount > DESCRIPTION_LENGTH_THRESHOLD_LARGE) {
|
|
3802
4411
|
if (contentType === "video") {
|
|
3803
|
-
return
|
|
4412
|
+
return dedent6`A detailed summary that describes what happens across the video.
|
|
3804
4413
|
Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
|
|
3805
4414
|
Be thorough: cover subjects, actions, setting, progression, and any notable details visible across frames.
|
|
3806
4415
|
Write in present tense. Be specific about observable details rather than making assumptions.
|
|
3807
4416
|
If the transcript provides dialogue or narration, incorporate key points but prioritize visual content.`;
|
|
3808
4417
|
}
|
|
3809
|
-
return
|
|
4418
|
+
return dedent6`A detailed summary that describes the audio content.
|
|
3810
4419
|
Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
|
|
3811
4420
|
Be thorough: cover topics, speakers, themes, progression, and any notable insights.
|
|
3812
4421
|
Write in present tense. Be specific about what is discussed or presented rather than making assumptions.
|
|
3813
4422
|
Focus on the spoken content and any key insights, dialogue, or narrative elements.`;
|
|
3814
4423
|
}
|
|
3815
4424
|
if (contentType === "video") {
|
|
3816
|
-
return
|
|
4425
|
+
return dedent6`A summary that describes what happens across the video.
|
|
3817
4426
|
Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
|
|
3818
4427
|
Cover the main subjects, actions, setting, and any notable progression visible across frames.
|
|
3819
4428
|
Write in present tense. Be specific about observable details rather than making assumptions.
|
|
3820
4429
|
If the transcript provides dialogue or narration, incorporate key points but prioritize visual content.`;
|
|
3821
4430
|
}
|
|
3822
|
-
return
|
|
4431
|
+
return dedent6`A summary that describes the audio content.
|
|
3823
4432
|
Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
|
|
3824
4433
|
Cover the main topics, speakers, themes, and any notable progression in the discussion or narration.
|
|
3825
4434
|
Write in present tense. Be specific about what is discussed or presented rather than making assumptions.
|
|
@@ -3836,7 +4445,7 @@ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount }
|
|
|
3836
4445
|
},
|
|
3837
4446
|
title: {
|
|
3838
4447
|
tag: "title_requirements",
|
|
3839
|
-
content:
|
|
4448
|
+
content: dedent6`
|
|
3840
4449
|
A concise, label-style title — not a sentence or description.
|
|
3841
4450
|
Never exceed ${titleLimit} words, but shorter is better.
|
|
3842
4451
|
Think of how a video card title, playlist entry, or file name would read — e.g. "Predator: Badlands Trailer" or "Chef Prepares Holiday Feast".
|
|
@@ -3849,7 +4458,7 @@ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount }
|
|
|
3849
4458
|
},
|
|
3850
4459
|
keywords: {
|
|
3851
4460
|
tag: "keywords_requirements",
|
|
3852
|
-
content:
|
|
4461
|
+
content: dedent6`
|
|
3853
4462
|
Specific, searchable terms (up to ${keywordLimit}) that capture:
|
|
3854
4463
|
- Primary subjects (people, animals, objects)
|
|
3855
4464
|
- Actions and activities being performed
|
|
@@ -3861,7 +4470,7 @@ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount }
|
|
|
3861
4470
|
},
|
|
3862
4471
|
qualityGuidelines: {
|
|
3863
4472
|
tag: "quality_guidelines",
|
|
3864
|
-
content:
|
|
4473
|
+
content: dedent6`
|
|
3865
4474
|
- Examine all frames to understand the full context and progression
|
|
3866
4475
|
- Be precise: "golden retriever" is better than "dog" when identifiable
|
|
3867
4476
|
- Capture the narrative: what begins, develops, and concludes
|
|
@@ -3882,7 +4491,7 @@ function createAudioOnlyBuilder({ titleLength, descriptionLength, tagCount } = {
|
|
|
3882
4491
|
},
|
|
3883
4492
|
title: {
|
|
3884
4493
|
tag: "title_requirements",
|
|
3885
|
-
content:
|
|
4494
|
+
content: dedent6`
|
|
3886
4495
|
A concise, label-style title — not a sentence or description.
|
|
3887
4496
|
Never exceed ${titleLimit} words, but shorter is better.
|
|
3888
4497
|
Think of how a podcast episode title or playlist entry would read — e.g. "Weekly News Roundup" or "Interview with Dr. Smith".
|
|
@@ -3895,7 +4504,7 @@ function createAudioOnlyBuilder({ titleLength, descriptionLength, tagCount } = {
|
|
|
3895
4504
|
},
|
|
3896
4505
|
keywords: {
|
|
3897
4506
|
tag: "keywords_requirements",
|
|
3898
|
-
content:
|
|
4507
|
+
content: dedent6`
|
|
3899
4508
|
Specific, searchable terms (up to ${keywordLimit}) that capture:
|
|
3900
4509
|
- Primary topics and themes
|
|
3901
4510
|
- Speakers or presenters (if named)
|
|
@@ -3907,7 +4516,7 @@ function createAudioOnlyBuilder({ titleLength, descriptionLength, tagCount } = {
|
|
|
3907
4516
|
},
|
|
3908
4517
|
qualityGuidelines: {
|
|
3909
4518
|
tag: "quality_guidelines",
|
|
3910
|
-
content:
|
|
4519
|
+
content: dedent6`
|
|
3911
4520
|
- Analyze the full transcript to understand context and themes
|
|
3912
4521
|
- Be precise: use specific terminology when mentioned
|
|
3913
4522
|
- Capture the narrative: what is introduced, discussed, and concluded
|
|
@@ -3917,7 +4526,7 @@ function createAudioOnlyBuilder({ titleLength, descriptionLength, tagCount } = {
|
|
|
3917
4526
|
sectionOrder: ["task", "title", "description", "keywords", "qualityGuidelines"]
|
|
3918
4527
|
});
|
|
3919
4528
|
}
|
|
3920
|
-
var
|
|
4529
|
+
var SYSTEM_PROMPT5 = dedent6`
|
|
3921
4530
|
<role>
|
|
3922
4531
|
You are a video content analyst specializing in storyboard interpretation and multimodal analysis.
|
|
3923
4532
|
</role>
|
|
@@ -3974,7 +4583,7 @@ var SYSTEM_PROMPT4 = dedent5`
|
|
|
3974
4583
|
|
|
3975
4584
|
Write as if describing reality, not describing a recording of reality.
|
|
3976
4585
|
</language_guidelines>`;
|
|
3977
|
-
var
|
|
4586
|
+
var AUDIO_ONLY_SYSTEM_PROMPT2 = dedent6`
|
|
3978
4587
|
<role>
|
|
3979
4588
|
You are an audio content analyst specializing in transcript analysis and metadata generation.
|
|
3980
4589
|
</role>
|
|
@@ -4028,7 +4637,7 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent5`
|
|
|
4028
4637
|
|
|
4029
4638
|
Write as if describing reality, not describing a recording of reality.
|
|
4030
4639
|
</language_guidelines>`;
|
|
4031
|
-
function
|
|
4640
|
+
function buildUserPrompt5({
|
|
4032
4641
|
tone,
|
|
4033
4642
|
transcriptText,
|
|
4034
4643
|
isCleanTranscript = true,
|
|
@@ -4059,7 +4668,7 @@ function buildUserPrompt4({
|
|
|
4059
4668
|
async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, systemPrompt, credentials) {
|
|
4060
4669
|
"use step";
|
|
4061
4670
|
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
4062
|
-
const response = await
|
|
4671
|
+
const response = await generateText6({
|
|
4063
4672
|
model,
|
|
4064
4673
|
output: SUMMARY_OUTPUT,
|
|
4065
4674
|
messages: [
|
|
@@ -4094,7 +4703,7 @@ async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, s
|
|
|
4094
4703
|
async function analyzeAudioOnly(provider, modelId, userPrompt, systemPrompt, credentials) {
|
|
4095
4704
|
"use step";
|
|
4096
4705
|
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
4097
|
-
const response = await
|
|
4706
|
+
const response = await generateText6({
|
|
4098
4707
|
model,
|
|
4099
4708
|
output: SUMMARY_OUTPUT,
|
|
4100
4709
|
messages: [
|
|
@@ -4197,7 +4806,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
4197
4806
|
const transcriptText = transcriptResult?.transcriptText ?? "";
|
|
4198
4807
|
const resolvedLanguageCode = outputLanguageCode && outputLanguageCode !== "auto" ? outputLanguageCode : transcriptResult?.track?.language_code ?? getReadyTextTracks(assetData)[0]?.language_code;
|
|
4199
4808
|
const languageName = resolvedLanguageCode ? getLanguageName(resolvedLanguageCode) : void 0;
|
|
4200
|
-
const userPrompt =
|
|
4809
|
+
const userPrompt = buildUserPrompt5({
|
|
4201
4810
|
tone,
|
|
4202
4811
|
transcriptText,
|
|
4203
4812
|
isCleanTranscript: cleanTranscript,
|
|
@@ -4210,7 +4819,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
4210
4819
|
});
|
|
4211
4820
|
let analysisResponse;
|
|
4212
4821
|
let imageUrl;
|
|
4213
|
-
const systemPrompt = isAudioOnly ?
|
|
4822
|
+
const systemPrompt = isAudioOnly ? AUDIO_ONLY_SYSTEM_PROMPT2 : SYSTEM_PROMPT5;
|
|
4214
4823
|
try {
|
|
4215
4824
|
if (isAudioOnly) {
|
|
4216
4825
|
analysisResponse = await analyzeAudioOnly(
|
|
@@ -4683,24 +5292,24 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
4683
5292
|
// src/workflows/translate-captions.ts
|
|
4684
5293
|
import {
|
|
4685
5294
|
APICallError,
|
|
4686
|
-
generateText as
|
|
5295
|
+
generateText as generateText7,
|
|
4687
5296
|
NoObjectGeneratedError,
|
|
4688
|
-
Output as
|
|
5297
|
+
Output as Output7,
|
|
4689
5298
|
RetryError,
|
|
4690
5299
|
TypeValidationError
|
|
4691
5300
|
} from "ai";
|
|
4692
|
-
import
|
|
4693
|
-
import { z as
|
|
4694
|
-
var translationSchema =
|
|
4695
|
-
translation:
|
|
5301
|
+
import dedent7 from "dedent";
|
|
5302
|
+
import { z as z8 } from "zod";
|
|
5303
|
+
var translationSchema = z8.object({
|
|
5304
|
+
translation: z8.string()
|
|
4696
5305
|
});
|
|
4697
|
-
var
|
|
5306
|
+
var SYSTEM_PROMPT6 = dedent7`
|
|
4698
5307
|
You are a subtitle translation expert. Translate VTT subtitle files to the target language specified by the user.
|
|
4699
5308
|
You may receive either a full VTT file or a chunk from a larger VTT.
|
|
4700
5309
|
Preserve all timestamps, cue ordering, and VTT formatting exactly as they appear.
|
|
4701
5310
|
Return JSON with a single key "translation" containing the translated VTT content.
|
|
4702
5311
|
`;
|
|
4703
|
-
var CUE_TRANSLATION_SYSTEM_PROMPT =
|
|
5312
|
+
var CUE_TRANSLATION_SYSTEM_PROMPT = dedent7`
|
|
4704
5313
|
You are a subtitle translation expert.
|
|
4705
5314
|
You will receive a sequence of subtitle cues extracted from a VTT file.
|
|
4706
5315
|
Translate the cues to the requested target language while preserving their original order.
|
|
@@ -4872,13 +5481,13 @@ async function translateVttWithAI({
|
|
|
4872
5481
|
}) {
|
|
4873
5482
|
"use step";
|
|
4874
5483
|
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
4875
|
-
const response = await
|
|
5484
|
+
const response = await generateText7({
|
|
4876
5485
|
model,
|
|
4877
|
-
output:
|
|
5486
|
+
output: Output7.object({ schema: translationSchema }),
|
|
4878
5487
|
messages: [
|
|
4879
5488
|
{
|
|
4880
5489
|
role: "system",
|
|
4881
|
-
content:
|
|
5490
|
+
content: SYSTEM_PROMPT6
|
|
4882
5491
|
},
|
|
4883
5492
|
{
|
|
4884
5493
|
role: "user",
|
|
@@ -4909,8 +5518,8 @@ async function translateCueChunkWithAI({
|
|
|
4909
5518
|
}) {
|
|
4910
5519
|
"use step";
|
|
4911
5520
|
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
4912
|
-
const schema =
|
|
4913
|
-
translations:
|
|
5521
|
+
const schema = z8.object({
|
|
5522
|
+
translations: z8.array(z8.string().min(1)).length(cues.length)
|
|
4914
5523
|
});
|
|
4915
5524
|
const cuePayload = cues.map((cue, index) => ({
|
|
4916
5525
|
index,
|
|
@@ -4918,9 +5527,9 @@ async function translateCueChunkWithAI({
|
|
|
4918
5527
|
endTime: cue.endTime,
|
|
4919
5528
|
text: cue.text
|
|
4920
5529
|
}));
|
|
4921
|
-
const response = await
|
|
5530
|
+
const response = await generateText7({
|
|
4922
5531
|
model,
|
|
4923
|
-
output:
|
|
5532
|
+
output: Output7.object({ schema }),
|
|
4924
5533
|
messages: [
|
|
4925
5534
|
{
|
|
4926
5535
|
role: "system",
|
|
@@ -5271,12 +5880,15 @@ export {
|
|
|
5271
5880
|
chaptersSchema,
|
|
5272
5881
|
createReplacer,
|
|
5273
5882
|
editCaptions,
|
|
5883
|
+
extractTranscriptSegmentsForHotspots,
|
|
5274
5884
|
generateChapters,
|
|
5275
5885
|
generateEmbeddings,
|
|
5886
|
+
generateEngagementInsights,
|
|
5276
5887
|
generateVideoEmbeddings,
|
|
5277
5888
|
getModerationScores,
|
|
5278
5889
|
getSummaryAndTags,
|
|
5279
5890
|
hasBurnedInCaptions,
|
|
5891
|
+
mapShotsToHotspots,
|
|
5280
5892
|
profanityDetectionSchema,
|
|
5281
5893
|
questionAnswerSchema,
|
|
5282
5894
|
shouldSplitChunkTranslationError,
|