@mux/ai 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +516 -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 +654 -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,594 @@ 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
|
+
throw new Error(
|
|
3740
|
+
`No engagement data available for asset ${assetId} in timeframe ${timeframe}. Video may not have been viewed yet or engagement tracking is not enabled.`
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
let transcriptText = "";
|
|
3744
|
+
if (transcriptResult.status === "fulfilled") {
|
|
3745
|
+
transcriptText = transcriptResult.value.transcriptText || "";
|
|
3746
|
+
}
|
|
3747
|
+
if (!transcriptText.trim()) {
|
|
3748
|
+
console.warn(
|
|
3749
|
+
`No transcript available for asset ${assetId}. Insights will be based solely on visual analysis and engagement patterns.`
|
|
3750
|
+
);
|
|
3751
|
+
}
|
|
3752
|
+
const transcriptSegments = extractTranscriptSegmentsForHotspots(transcriptText, hotspots);
|
|
3753
|
+
let imageUrls = [];
|
|
3754
|
+
if (audioOnly) {
|
|
3755
|
+
console.warn(
|
|
3756
|
+
`Asset ${assetId} is audio-only. Insights will be based solely on transcript and engagement data without visual analysis.`
|
|
3757
|
+
);
|
|
3758
|
+
} else {
|
|
3759
|
+
if (shotsResult.status === "fulfilled" && shotsResult.value?.status === "completed") {
|
|
3760
|
+
const shotImageUrls = mapShotsToHotspots(shotsResult.value.shots, hotspots);
|
|
3761
|
+
if (shotImageUrls.length > 0) {
|
|
3762
|
+
imageUrls = shotImageUrls;
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
if (imageUrls.length === 0) {
|
|
3766
|
+
if (shouldFetchShots && shotsResult.status === "fulfilled" && shotsResult.value?.status !== "completed") {
|
|
3767
|
+
console.warn(
|
|
3768
|
+
`Shots not ready for asset ${assetId} (status: ${shotsResult.value?.status}). Falling back to thumbnails.`
|
|
3769
|
+
);
|
|
3770
|
+
} else if (shouldFetchShots && shotsResult.status === "rejected") {
|
|
3771
|
+
console.warn(
|
|
3772
|
+
`Shots fetch failed for asset ${assetId}. Falling back to thumbnails.`
|
|
3773
|
+
);
|
|
3774
|
+
}
|
|
3775
|
+
imageUrls = await getThumbnailUrlsForHotspots(playbackId, hotspots, {
|
|
3776
|
+
shouldSign,
|
|
3777
|
+
credentials
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
if (storyboardResult.status === "fulfilled" && storyboardResult.value) {
|
|
3781
|
+
imageUrls.push(storyboardResult.value);
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
const systemPrompt = audioOnly ? AUDIO_ONLY_SYSTEM_PROMPT : SYSTEM_PROMPT4;
|
|
3785
|
+
const userPrompt = buildUserPrompt4(
|
|
3786
|
+
hotspots,
|
|
3787
|
+
transcriptSegments,
|
|
3788
|
+
heatmap.heatmap,
|
|
3789
|
+
audioOnly
|
|
3790
|
+
);
|
|
3791
|
+
const { result: aiInsights, usage } = await generateInsightsWithAI(
|
|
3792
|
+
modelConfig.provider,
|
|
3793
|
+
modelConfig.modelId,
|
|
3794
|
+
systemPrompt,
|
|
3795
|
+
userPrompt,
|
|
3796
|
+
imageUrls,
|
|
3797
|
+
credentials
|
|
3798
|
+
);
|
|
3799
|
+
if (!aiInsights.momentInsights || aiInsights.momentInsights.length === 0) {
|
|
3800
|
+
throw new Error("Failed to generate insights from AI response");
|
|
3801
|
+
}
|
|
3802
|
+
const momentInsights = [];
|
|
3803
|
+
for (const aiMoment of aiInsights.momentInsights) {
|
|
3804
|
+
const idx = aiMoment.hotspotIndex;
|
|
3805
|
+
if (idx < 0 || idx >= hotspots.length) {
|
|
3806
|
+
console.warn(
|
|
3807
|
+
`AI returned hotspotIndex ${idx} which is out of range (0-${hotspots.length - 1}). Skipping.`
|
|
3808
|
+
);
|
|
3809
|
+
continue;
|
|
3810
|
+
}
|
|
3811
|
+
const hotspot = hotspots[idx];
|
|
3812
|
+
momentInsights.push({
|
|
3813
|
+
startMs: hotspot.startMs,
|
|
3814
|
+
endMs: hotspot.endMs,
|
|
3815
|
+
timestamp: secondsToTimestamp(hotspot.startMs / 1e3),
|
|
3816
|
+
engagementScore: hotspot.score,
|
|
3817
|
+
insight: aiMoment.insight
|
|
3818
|
+
});
|
|
3819
|
+
}
|
|
3820
|
+
if (momentInsights.length === 0) {
|
|
3821
|
+
throw new Error(
|
|
3822
|
+
"AI returned insights but none matched valid hotspot indices. This may indicate a prompt or model issue."
|
|
3823
|
+
);
|
|
3824
|
+
}
|
|
3825
|
+
const usageWithMetadata = {
|
|
3826
|
+
...usage,
|
|
3827
|
+
metadata: {
|
|
3828
|
+
assetDurationSeconds,
|
|
3829
|
+
thumbnailCount: imageUrls.length
|
|
3830
|
+
}
|
|
3831
|
+
};
|
|
3832
|
+
return {
|
|
3833
|
+
assetId,
|
|
3834
|
+
momentInsights,
|
|
3835
|
+
overallInsight: {
|
|
3836
|
+
summary: aiInsights.overallInsight.summary,
|
|
3837
|
+
trends: aiInsights.overallInsight.trends
|
|
3838
|
+
},
|
|
3839
|
+
usage: usageWithMetadata
|
|
3840
|
+
};
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3239
3843
|
// src/lib/sampling-plan.ts
|
|
3240
3844
|
var DEFAULT_FPS = 30;
|
|
3241
3845
|
function roundToNearestFrameMs(tsMs, fps = DEFAULT_FPS) {
|
|
@@ -3763,18 +4367,18 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
3763
4367
|
}
|
|
3764
4368
|
|
|
3765
4369
|
// src/workflows/summarization.ts
|
|
3766
|
-
import { generateText as
|
|
3767
|
-
import
|
|
3768
|
-
import { z as
|
|
4370
|
+
import { generateText as generateText6, Output as Output6 } from "ai";
|
|
4371
|
+
import dedent6 from "dedent";
|
|
4372
|
+
import { z as z7 } from "zod";
|
|
3769
4373
|
var DEFAULT_SUMMARY_KEYWORD_LIMIT = 10;
|
|
3770
4374
|
var DEFAULT_TITLE_LENGTH = 10;
|
|
3771
4375
|
var DEFAULT_DESCRIPTION_LENGTH = 50;
|
|
3772
|
-
var summarySchema =
|
|
3773
|
-
keywords:
|
|
3774
|
-
title:
|
|
3775
|
-
description:
|
|
4376
|
+
var summarySchema = z7.object({
|
|
4377
|
+
keywords: z7.array(z7.string()),
|
|
4378
|
+
title: z7.string(),
|
|
4379
|
+
description: z7.string()
|
|
3776
4380
|
}).strict();
|
|
3777
|
-
var SUMMARY_OUTPUT =
|
|
4381
|
+
var SUMMARY_OUTPUT = Output6.object({
|
|
3778
4382
|
name: "summary_metadata",
|
|
3779
4383
|
description: "Structured summary with title, description, and keywords.",
|
|
3780
4384
|
schema: summarySchema
|
|
@@ -3790,36 +4394,36 @@ var DESCRIPTION_LENGTH_THRESHOLD_LARGE = 100;
|
|
|
3790
4394
|
function buildDescriptionGuidance(wordCount, contentType) {
|
|
3791
4395
|
if (wordCount < DESCRIPTION_LENGTH_THRESHOLD_SMALL) {
|
|
3792
4396
|
if (contentType === "video") {
|
|
3793
|
-
return
|
|
4397
|
+
return dedent6`A brief summary of the video in no more than ${wordCount} words. Shorter is fine.
|
|
3794
4398
|
Focus on the single most important subject or action.
|
|
3795
4399
|
Write in present tense.`;
|
|
3796
4400
|
}
|
|
3797
|
-
return
|
|
4401
|
+
return dedent6`A brief summary of the audio content in no more than ${wordCount} words. Shorter is fine.
|
|
3798
4402
|
Focus on the single most important topic or theme.
|
|
3799
4403
|
Write in present tense.`;
|
|
3800
4404
|
}
|
|
3801
4405
|
if (wordCount > DESCRIPTION_LENGTH_THRESHOLD_LARGE) {
|
|
3802
4406
|
if (contentType === "video") {
|
|
3803
|
-
return
|
|
4407
|
+
return dedent6`A detailed summary that describes what happens across the video.
|
|
3804
4408
|
Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
|
|
3805
4409
|
Be thorough: cover subjects, actions, setting, progression, and any notable details visible across frames.
|
|
3806
4410
|
Write in present tense. Be specific about observable details rather than making assumptions.
|
|
3807
4411
|
If the transcript provides dialogue or narration, incorporate key points but prioritize visual content.`;
|
|
3808
4412
|
}
|
|
3809
|
-
return
|
|
4413
|
+
return dedent6`A detailed summary that describes the audio content.
|
|
3810
4414
|
Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
|
|
3811
4415
|
Be thorough: cover topics, speakers, themes, progression, and any notable insights.
|
|
3812
4416
|
Write in present tense. Be specific about what is discussed or presented rather than making assumptions.
|
|
3813
4417
|
Focus on the spoken content and any key insights, dialogue, or narrative elements.`;
|
|
3814
4418
|
}
|
|
3815
4419
|
if (contentType === "video") {
|
|
3816
|
-
return
|
|
4420
|
+
return dedent6`A summary that describes what happens across the video.
|
|
3817
4421
|
Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
|
|
3818
4422
|
Cover the main subjects, actions, setting, and any notable progression visible across frames.
|
|
3819
4423
|
Write in present tense. Be specific about observable details rather than making assumptions.
|
|
3820
4424
|
If the transcript provides dialogue or narration, incorporate key points but prioritize visual content.`;
|
|
3821
4425
|
}
|
|
3822
|
-
return
|
|
4426
|
+
return dedent6`A summary that describes the audio content.
|
|
3823
4427
|
Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
|
|
3824
4428
|
Cover the main topics, speakers, themes, and any notable progression in the discussion or narration.
|
|
3825
4429
|
Write in present tense. Be specific about what is discussed or presented rather than making assumptions.
|
|
@@ -3836,7 +4440,7 @@ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount }
|
|
|
3836
4440
|
},
|
|
3837
4441
|
title: {
|
|
3838
4442
|
tag: "title_requirements",
|
|
3839
|
-
content:
|
|
4443
|
+
content: dedent6`
|
|
3840
4444
|
A concise, label-style title — not a sentence or description.
|
|
3841
4445
|
Never exceed ${titleLimit} words, but shorter is better.
|
|
3842
4446
|
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 +4453,7 @@ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount }
|
|
|
3849
4453
|
},
|
|
3850
4454
|
keywords: {
|
|
3851
4455
|
tag: "keywords_requirements",
|
|
3852
|
-
content:
|
|
4456
|
+
content: dedent6`
|
|
3853
4457
|
Specific, searchable terms (up to ${keywordLimit}) that capture:
|
|
3854
4458
|
- Primary subjects (people, animals, objects)
|
|
3855
4459
|
- Actions and activities being performed
|
|
@@ -3861,7 +4465,7 @@ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount }
|
|
|
3861
4465
|
},
|
|
3862
4466
|
qualityGuidelines: {
|
|
3863
4467
|
tag: "quality_guidelines",
|
|
3864
|
-
content:
|
|
4468
|
+
content: dedent6`
|
|
3865
4469
|
- Examine all frames to understand the full context and progression
|
|
3866
4470
|
- Be precise: "golden retriever" is better than "dog" when identifiable
|
|
3867
4471
|
- Capture the narrative: what begins, develops, and concludes
|
|
@@ -3882,7 +4486,7 @@ function createAudioOnlyBuilder({ titleLength, descriptionLength, tagCount } = {
|
|
|
3882
4486
|
},
|
|
3883
4487
|
title: {
|
|
3884
4488
|
tag: "title_requirements",
|
|
3885
|
-
content:
|
|
4489
|
+
content: dedent6`
|
|
3886
4490
|
A concise, label-style title — not a sentence or description.
|
|
3887
4491
|
Never exceed ${titleLimit} words, but shorter is better.
|
|
3888
4492
|
Think of how a podcast episode title or playlist entry would read — e.g. "Weekly News Roundup" or "Interview with Dr. Smith".
|
|
@@ -3895,7 +4499,7 @@ function createAudioOnlyBuilder({ titleLength, descriptionLength, tagCount } = {
|
|
|
3895
4499
|
},
|
|
3896
4500
|
keywords: {
|
|
3897
4501
|
tag: "keywords_requirements",
|
|
3898
|
-
content:
|
|
4502
|
+
content: dedent6`
|
|
3899
4503
|
Specific, searchable terms (up to ${keywordLimit}) that capture:
|
|
3900
4504
|
- Primary topics and themes
|
|
3901
4505
|
- Speakers or presenters (if named)
|
|
@@ -3907,7 +4511,7 @@ function createAudioOnlyBuilder({ titleLength, descriptionLength, tagCount } = {
|
|
|
3907
4511
|
},
|
|
3908
4512
|
qualityGuidelines: {
|
|
3909
4513
|
tag: "quality_guidelines",
|
|
3910
|
-
content:
|
|
4514
|
+
content: dedent6`
|
|
3911
4515
|
- Analyze the full transcript to understand context and themes
|
|
3912
4516
|
- Be precise: use specific terminology when mentioned
|
|
3913
4517
|
- Capture the narrative: what is introduced, discussed, and concluded
|
|
@@ -3917,7 +4521,7 @@ function createAudioOnlyBuilder({ titleLength, descriptionLength, tagCount } = {
|
|
|
3917
4521
|
sectionOrder: ["task", "title", "description", "keywords", "qualityGuidelines"]
|
|
3918
4522
|
});
|
|
3919
4523
|
}
|
|
3920
|
-
var
|
|
4524
|
+
var SYSTEM_PROMPT5 = dedent6`
|
|
3921
4525
|
<role>
|
|
3922
4526
|
You are a video content analyst specializing in storyboard interpretation and multimodal analysis.
|
|
3923
4527
|
</role>
|
|
@@ -3974,7 +4578,7 @@ var SYSTEM_PROMPT4 = dedent5`
|
|
|
3974
4578
|
|
|
3975
4579
|
Write as if describing reality, not describing a recording of reality.
|
|
3976
4580
|
</language_guidelines>`;
|
|
3977
|
-
var
|
|
4581
|
+
var AUDIO_ONLY_SYSTEM_PROMPT2 = dedent6`
|
|
3978
4582
|
<role>
|
|
3979
4583
|
You are an audio content analyst specializing in transcript analysis and metadata generation.
|
|
3980
4584
|
</role>
|
|
@@ -4028,7 +4632,7 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent5`
|
|
|
4028
4632
|
|
|
4029
4633
|
Write as if describing reality, not describing a recording of reality.
|
|
4030
4634
|
</language_guidelines>`;
|
|
4031
|
-
function
|
|
4635
|
+
function buildUserPrompt5({
|
|
4032
4636
|
tone,
|
|
4033
4637
|
transcriptText,
|
|
4034
4638
|
isCleanTranscript = true,
|
|
@@ -4059,7 +4663,7 @@ function buildUserPrompt4({
|
|
|
4059
4663
|
async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, systemPrompt, credentials) {
|
|
4060
4664
|
"use step";
|
|
4061
4665
|
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
4062
|
-
const response = await
|
|
4666
|
+
const response = await generateText6({
|
|
4063
4667
|
model,
|
|
4064
4668
|
output: SUMMARY_OUTPUT,
|
|
4065
4669
|
messages: [
|
|
@@ -4094,7 +4698,7 @@ async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, s
|
|
|
4094
4698
|
async function analyzeAudioOnly(provider, modelId, userPrompt, systemPrompt, credentials) {
|
|
4095
4699
|
"use step";
|
|
4096
4700
|
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
4097
|
-
const response = await
|
|
4701
|
+
const response = await generateText6({
|
|
4098
4702
|
model,
|
|
4099
4703
|
output: SUMMARY_OUTPUT,
|
|
4100
4704
|
messages: [
|
|
@@ -4197,7 +4801,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
4197
4801
|
const transcriptText = transcriptResult?.transcriptText ?? "";
|
|
4198
4802
|
const resolvedLanguageCode = outputLanguageCode && outputLanguageCode !== "auto" ? outputLanguageCode : transcriptResult?.track?.language_code ?? getReadyTextTracks(assetData)[0]?.language_code;
|
|
4199
4803
|
const languageName = resolvedLanguageCode ? getLanguageName(resolvedLanguageCode) : void 0;
|
|
4200
|
-
const userPrompt =
|
|
4804
|
+
const userPrompt = buildUserPrompt5({
|
|
4201
4805
|
tone,
|
|
4202
4806
|
transcriptText,
|
|
4203
4807
|
isCleanTranscript: cleanTranscript,
|
|
@@ -4210,7 +4814,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
4210
4814
|
});
|
|
4211
4815
|
let analysisResponse;
|
|
4212
4816
|
let imageUrl;
|
|
4213
|
-
const systemPrompt = isAudioOnly ?
|
|
4817
|
+
const systemPrompt = isAudioOnly ? AUDIO_ONLY_SYSTEM_PROMPT2 : SYSTEM_PROMPT5;
|
|
4214
4818
|
try {
|
|
4215
4819
|
if (isAudioOnly) {
|
|
4216
4820
|
analysisResponse = await analyzeAudioOnly(
|
|
@@ -4683,24 +5287,24 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
4683
5287
|
// src/workflows/translate-captions.ts
|
|
4684
5288
|
import {
|
|
4685
5289
|
APICallError,
|
|
4686
|
-
generateText as
|
|
5290
|
+
generateText as generateText7,
|
|
4687
5291
|
NoObjectGeneratedError,
|
|
4688
|
-
Output as
|
|
5292
|
+
Output as Output7,
|
|
4689
5293
|
RetryError,
|
|
4690
5294
|
TypeValidationError
|
|
4691
5295
|
} from "ai";
|
|
4692
|
-
import
|
|
4693
|
-
import { z as
|
|
4694
|
-
var translationSchema =
|
|
4695
|
-
translation:
|
|
5296
|
+
import dedent7 from "dedent";
|
|
5297
|
+
import { z as z8 } from "zod";
|
|
5298
|
+
var translationSchema = z8.object({
|
|
5299
|
+
translation: z8.string()
|
|
4696
5300
|
});
|
|
4697
|
-
var
|
|
5301
|
+
var SYSTEM_PROMPT6 = dedent7`
|
|
4698
5302
|
You are a subtitle translation expert. Translate VTT subtitle files to the target language specified by the user.
|
|
4699
5303
|
You may receive either a full VTT file or a chunk from a larger VTT.
|
|
4700
5304
|
Preserve all timestamps, cue ordering, and VTT formatting exactly as they appear.
|
|
4701
5305
|
Return JSON with a single key "translation" containing the translated VTT content.
|
|
4702
5306
|
`;
|
|
4703
|
-
var CUE_TRANSLATION_SYSTEM_PROMPT =
|
|
5307
|
+
var CUE_TRANSLATION_SYSTEM_PROMPT = dedent7`
|
|
4704
5308
|
You are a subtitle translation expert.
|
|
4705
5309
|
You will receive a sequence of subtitle cues extracted from a VTT file.
|
|
4706
5310
|
Translate the cues to the requested target language while preserving their original order.
|
|
@@ -4872,13 +5476,13 @@ async function translateVttWithAI({
|
|
|
4872
5476
|
}) {
|
|
4873
5477
|
"use step";
|
|
4874
5478
|
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
4875
|
-
const response = await
|
|
5479
|
+
const response = await generateText7({
|
|
4876
5480
|
model,
|
|
4877
|
-
output:
|
|
5481
|
+
output: Output7.object({ schema: translationSchema }),
|
|
4878
5482
|
messages: [
|
|
4879
5483
|
{
|
|
4880
5484
|
role: "system",
|
|
4881
|
-
content:
|
|
5485
|
+
content: SYSTEM_PROMPT6
|
|
4882
5486
|
},
|
|
4883
5487
|
{
|
|
4884
5488
|
role: "user",
|
|
@@ -4909,8 +5513,8 @@ async function translateCueChunkWithAI({
|
|
|
4909
5513
|
}) {
|
|
4910
5514
|
"use step";
|
|
4911
5515
|
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
4912
|
-
const schema =
|
|
4913
|
-
translations:
|
|
5516
|
+
const schema = z8.object({
|
|
5517
|
+
translations: z8.array(z8.string().min(1)).length(cues.length)
|
|
4914
5518
|
});
|
|
4915
5519
|
const cuePayload = cues.map((cue, index) => ({
|
|
4916
5520
|
index,
|
|
@@ -4918,9 +5522,9 @@ async function translateCueChunkWithAI({
|
|
|
4918
5522
|
endTime: cue.endTime,
|
|
4919
5523
|
text: cue.text
|
|
4920
5524
|
}));
|
|
4921
|
-
const response = await
|
|
5525
|
+
const response = await generateText7({
|
|
4922
5526
|
model,
|
|
4923
|
-
output:
|
|
5527
|
+
output: Output7.object({ schema }),
|
|
4924
5528
|
messages: [
|
|
4925
5529
|
{
|
|
4926
5530
|
role: "system",
|
|
@@ -5271,12 +5875,15 @@ export {
|
|
|
5271
5875
|
chaptersSchema,
|
|
5272
5876
|
createReplacer,
|
|
5273
5877
|
editCaptions,
|
|
5878
|
+
extractTranscriptSegmentsForHotspots,
|
|
5274
5879
|
generateChapters,
|
|
5275
5880
|
generateEmbeddings,
|
|
5881
|
+
generateEngagementInsights,
|
|
5276
5882
|
generateVideoEmbeddings,
|
|
5277
5883
|
getModerationScores,
|
|
5278
5884
|
getSummaryAndTags,
|
|
5279
5885
|
hasBurnedInCaptions,
|
|
5886
|
+
mapShotsToHotspots,
|
|
5280
5887
|
profanityDetectionSchema,
|
|
5281
5888
|
questionAnswerSchema,
|
|
5282
5889
|
shouldSplitChunkTranslationError,
|