mcp-video-analyzer 0.2.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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/dist/adapters/adapter.interface.d.ts +15 -0
  4. package/dist/adapters/adapter.interface.js +17 -0
  5. package/dist/adapters/adapter.interface.js.map +1 -0
  6. package/dist/adapters/direct.adapter.d.ts +13 -0
  7. package/dist/adapters/direct.adapter.js +67 -0
  8. package/dist/adapters/direct.adapter.js.map +1 -0
  9. package/dist/adapters/loom.adapter.d.ts +13 -0
  10. package/dist/adapters/loom.adapter.js +183 -0
  11. package/dist/adapters/loom.adapter.js.map +1 -0
  12. package/dist/config/detail-levels.d.ts +18 -0
  13. package/dist/config/detail-levels.js +30 -0
  14. package/dist/config/detail-levels.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +5 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/processors/annotated-timeline.d.ts +25 -0
  19. package/dist/processors/annotated-timeline.js +83 -0
  20. package/dist/processors/annotated-timeline.js.map +1 -0
  21. package/dist/processors/audio-transcriber.d.ts +16 -0
  22. package/dist/processors/audio-transcriber.js +191 -0
  23. package/dist/processors/audio-transcriber.js.map +1 -0
  24. package/dist/processors/browser-frame-extractor.d.ts +27 -0
  25. package/dist/processors/browser-frame-extractor.js +132 -0
  26. package/dist/processors/browser-frame-extractor.js.map +1 -0
  27. package/dist/processors/frame-dedup.d.ts +23 -0
  28. package/dist/processors/frame-dedup.js +76 -0
  29. package/dist/processors/frame-dedup.js.map +1 -0
  30. package/dist/processors/frame-extractor.d.ts +19 -0
  31. package/dist/processors/frame-extractor.js +201 -0
  32. package/dist/processors/frame-extractor.js.map +1 -0
  33. package/dist/processors/frame-ocr.d.ts +13 -0
  34. package/dist/processors/frame-ocr.js +45 -0
  35. package/dist/processors/frame-ocr.js.map +1 -0
  36. package/dist/processors/image-optimizer.d.ts +7 -0
  37. package/dist/processors/image-optimizer.js +21 -0
  38. package/dist/processors/image-optimizer.js.map +1 -0
  39. package/dist/server.d.ts +2 -0
  40. package/dist/server.js +67 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/tools/analyze-moment.d.ts +2 -0
  43. package/dist/tools/analyze-moment.js +145 -0
  44. package/dist/tools/analyze-moment.js.map +1 -0
  45. package/dist/tools/analyze-video.d.ts +2 -0
  46. package/dist/tools/analyze-video.js +320 -0
  47. package/dist/tools/analyze-video.js.map +1 -0
  48. package/dist/tools/get-frame-at.d.ts +2 -0
  49. package/dist/tools/get-frame-at.js +88 -0
  50. package/dist/tools/get-frame-at.js.map +1 -0
  51. package/dist/tools/get-frame-burst.d.ts +2 -0
  52. package/dist/tools/get-frame-burst.js +106 -0
  53. package/dist/tools/get-frame-burst.js.map +1 -0
  54. package/dist/tools/get-frames.d.ts +2 -0
  55. package/dist/tools/get-frames.js +143 -0
  56. package/dist/tools/get-frames.js.map +1 -0
  57. package/dist/tools/get-metadata.d.ts +2 -0
  58. package/dist/tools/get-metadata.js +65 -0
  59. package/dist/tools/get-metadata.js.map +1 -0
  60. package/dist/tools/get-transcript.d.ts +2 -0
  61. package/dist/tools/get-transcript.js +82 -0
  62. package/dist/tools/get-transcript.js.map +1 -0
  63. package/dist/types.d.ts +62 -0
  64. package/dist/types.js +2 -0
  65. package/dist/types.js.map +1 -0
  66. package/dist/utils/cache.d.ts +31 -0
  67. package/dist/utils/cache.js +87 -0
  68. package/dist/utils/cache.js.map +1 -0
  69. package/dist/utils/field-filter.d.ts +10 -0
  70. package/dist/utils/field-filter.js +32 -0
  71. package/dist/utils/field-filter.js.map +1 -0
  72. package/dist/utils/temp-files.d.ts +3 -0
  73. package/dist/utils/temp-files.js +28 -0
  74. package/dist/utils/temp-files.js.map +1 -0
  75. package/dist/utils/url-detector.d.ts +4 -0
  76. package/dist/utils/url-detector.js +33 -0
  77. package/dist/utils/url-detector.js.map +1 -0
  78. package/dist/utils/vtt-parser.d.ts +2 -0
  79. package/dist/utils/vtt-parser.js +85 -0
  80. package/dist/utils/vtt-parser.js.map +1 -0
  81. package/package.json +78 -0
@@ -0,0 +1,320 @@
1
+ import { imageContent, UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getAdapter } from '../adapters/adapter.interface.js';
4
+ import { extractSceneFrames, extractDenseFrames, probeVideoDuration, formatTimestamp, } from '../processors/frame-extractor.js';
5
+ import { extractBrowserFrames, generateTimestamps } from '../processors/browser-frame-extractor.js';
6
+ import { deduplicateFrames } from '../processors/frame-dedup.js';
7
+ import { extractTextFromFrames } from '../processors/frame-ocr.js';
8
+ import { buildAnnotatedTimeline } from '../processors/annotated-timeline.js';
9
+ import { optimizeFrames } from '../processors/image-optimizer.js';
10
+ import { extractAudioTrack, transcribeAudio } from '../processors/audio-transcriber.js';
11
+ import { createTempDir, cleanupTempDir } from '../utils/temp-files.js';
12
+ import { AnalysisCache, cacheKey } from '../utils/cache.js';
13
+ import { getDetailConfig } from '../config/detail-levels.js';
14
+ import { filterAnalysisResult } from '../utils/field-filter.js';
15
+ const cache = new AnalysisCache();
16
+ const ANALYSIS_FIELDS = [
17
+ 'metadata',
18
+ 'transcript',
19
+ 'frames',
20
+ 'comments',
21
+ 'chapters',
22
+ 'ocrResults',
23
+ 'timeline',
24
+ 'aiSummary',
25
+ ];
26
+ const AnalyzeOptionsSchema = z
27
+ .object({
28
+ maxFrames: z
29
+ .number()
30
+ .min(1)
31
+ .max(60)
32
+ .optional()
33
+ .describe('Maximum number of key frames to extract (default depends on detail level)'),
34
+ threshold: z
35
+ .number()
36
+ .min(0)
37
+ .max(1)
38
+ .default(0.1)
39
+ .optional()
40
+ .describe('Scene-change sensitivity 0.0-1.0 (lower = more frames, default: 0.1). Use 0.1 for screencasts/demos, 0.3 for live-action video.'),
41
+ returnBase64: z
42
+ .boolean()
43
+ .default(false)
44
+ .optional()
45
+ .describe('Return frames as base64 inline instead of file paths'),
46
+ skipFrames: z
47
+ .boolean()
48
+ .default(false)
49
+ .optional()
50
+ .describe('Skip frame extraction (transcript + metadata only)'),
51
+ detail: z
52
+ .enum(['brief', 'standard', 'detailed'])
53
+ .default('standard')
54
+ .optional()
55
+ .describe('Analysis depth: "brief" (metadata + truncated transcript, no frames), "standard" (default), "detailed" (dense sampling, more frames)'),
56
+ fields: z
57
+ .array(z.enum(ANALYSIS_FIELDS))
58
+ .optional()
59
+ .describe('Specific fields to return (default: all). E.g., ["metadata", "transcript"] returns only those fields.'),
60
+ forceRefresh: z
61
+ .boolean()
62
+ .default(false)
63
+ .optional()
64
+ .describe('Bypass cache and re-analyze the video'),
65
+ })
66
+ .optional();
67
+ const AnalyzeVideoSchema = z.object({
68
+ url: z.string().url().describe('Video URL (Loom share link or direct mp4/webm URL)'),
69
+ options: AnalyzeOptionsSchema.describe('Analysis options'),
70
+ });
71
+ export function registerAnalyzeVideo(server) {
72
+ server.addTool({
73
+ name: 'analyze_video',
74
+ description: `Analyze a video URL to extract transcript, key frames, metadata, comments, OCR text, and annotated timeline.
75
+
76
+ Returns structured data about the video content:
77
+ - Transcript with timestamps and speakers
78
+ - Key frames extracted via scene-change detection (deduplicated, as images)
79
+ - OCR text extracted from frames (code, error messages, UI text visible on screen)
80
+ - Annotated timeline merging transcript + frames + OCR into a unified chronological view
81
+ - Metadata (title, duration, platform)
82
+ - Comments from viewers (if available)
83
+
84
+ Supports: Loom (loom.com/share/...) and direct video URLs (.mp4, .webm, .mov).
85
+
86
+ Detail levels:
87
+ - "brief": metadata + truncated transcript only (fast, no video download)
88
+ - "standard": full analysis with scene-change frames (default)
89
+ - "detailed": dense sampling (1 frame/sec), more frames, full OCR
90
+
91
+ Use options.fields to request only specific data (e.g., ["metadata", "transcript"]).
92
+ Use options.forceRefresh to bypass the cache.`,
93
+ parameters: AnalyzeVideoSchema,
94
+ annotations: {
95
+ title: 'Analyze Video',
96
+ readOnlyHint: true,
97
+ destructiveHint: false,
98
+ idempotentHint: true,
99
+ openWorldHint: true,
100
+ },
101
+ execute: async (args, { reportProgress }) => {
102
+ const { url, options } = args;
103
+ const detail = options?.detail ?? 'standard';
104
+ const forceRefresh = options?.forceRefresh ?? false;
105
+ const fields = options?.fields;
106
+ const threshold = options?.threshold ?? 0.1;
107
+ // Resolve detail config
108
+ const config = getDetailConfig(detail);
109
+ const maxFrames = options?.maxFrames ?? config.maxFrames;
110
+ const skipFrames = options?.skipFrames ?? !config.includeFrames;
111
+ // Cache check
112
+ const key = cacheKey(url, { detail, maxFrames, threshold });
113
+ if (!forceRefresh) {
114
+ const cached = cache.get(key);
115
+ if (cached) {
116
+ const filtered = filterAnalysisResult(cached, fields);
117
+ const textData = { ...filtered, frameCount: cached.frames.length };
118
+ const content = [{ type: 'text', text: JSON.stringify(textData, null, 2) }];
119
+ // Re-add frame images if included
120
+ if (!fields || fields.includes('frames')) {
121
+ for (const frame of cached.frames) {
122
+ try {
123
+ content.push(await imageContent({ path: frame.filePath }));
124
+ }
125
+ catch {
126
+ // Frame file may have been cleaned up
127
+ }
128
+ }
129
+ }
130
+ return { content };
131
+ }
132
+ }
133
+ let adapter;
134
+ try {
135
+ adapter = getAdapter(url);
136
+ }
137
+ catch (error) {
138
+ if (error instanceof UserError)
139
+ throw error;
140
+ throw new UserError(`Failed to detect video platform for URL: ${url}`);
141
+ }
142
+ const warnings = [];
143
+ let tempDir = null;
144
+ try {
145
+ await reportProgress({ progress: 0, total: 100 });
146
+ // Fetch metadata, transcript, comments in parallel
147
+ const [metadata, transcript, comments, chapters, aiSummary] = await Promise.all([
148
+ adapter.getMetadata(url).catch((e) => {
149
+ warnings.push(`Failed to fetch metadata: ${e instanceof Error ? e.message : String(e)}`);
150
+ return {
151
+ platform: adapter.name,
152
+ title: 'Unknown',
153
+ duration: 0,
154
+ durationFormatted: '0:00',
155
+ url,
156
+ };
157
+ }),
158
+ adapter.getTranscript(url).catch((e) => {
159
+ warnings.push(`Failed to fetch transcript: ${e instanceof Error ? e.message : String(e)}`);
160
+ return [];
161
+ }),
162
+ adapter.getComments(url).catch((e) => {
163
+ warnings.push(`Failed to fetch comments: ${e instanceof Error ? e.message : String(e)}`);
164
+ return [];
165
+ }),
166
+ adapter.getChapters(url).catch(() => []),
167
+ adapter.getAiSummary(url).catch(() => null),
168
+ ]);
169
+ await reportProgress({ progress: 40, total: 100 });
170
+ // Apply transcript limit for brief mode
171
+ const limitedTranscript = config.transcriptMaxEntries !== null
172
+ ? transcript.slice(0, config.transcriptMaxEntries)
173
+ : transcript;
174
+ // Frame extraction (if not skipped)
175
+ const result = {
176
+ metadata,
177
+ transcript: limitedTranscript,
178
+ frames: [],
179
+ comments,
180
+ chapters,
181
+ ocrResults: [],
182
+ timeline: [],
183
+ aiSummary: aiSummary ?? undefined,
184
+ warnings,
185
+ };
186
+ let videoPath = null;
187
+ if (!skipFrames) {
188
+ tempDir = await createTempDir();
189
+ let framesExtracted = false;
190
+ // Strategy 1: yt-dlp download + ffmpeg frame extraction
191
+ if (adapter.capabilities.videoDownload) {
192
+ videoPath = await adapter.downloadVideo(url, tempDir);
193
+ if (videoPath) {
194
+ await reportProgress({ progress: 60, total: 100 });
195
+ // Probe duration if metadata didn't provide it
196
+ if (metadata.duration === 0) {
197
+ const duration = await probeVideoDuration(videoPath).catch(() => 0);
198
+ metadata.duration = duration;
199
+ metadata.durationFormatted = formatTimestamp(Math.floor(duration));
200
+ }
201
+ // Extract frames: dense or scene-based
202
+ const rawFrames = config.denseSampling
203
+ ? await extractDenseFrames(videoPath, tempDir, { maxFrames }).catch((e) => {
204
+ warnings.push(`Dense frame extraction failed: ${e instanceof Error ? e.message : String(e)}`);
205
+ return [];
206
+ })
207
+ : await extractSceneFrames(videoPath, tempDir, {
208
+ threshold,
209
+ maxFrames,
210
+ }).catch((e) => {
211
+ warnings.push(`Frame extraction failed: ${e instanceof Error ? e.message : String(e)}`);
212
+ return [];
213
+ });
214
+ await reportProgress({ progress: 80, total: 100 });
215
+ if (rawFrames.length > 0) {
216
+ const optimizedPaths = await optimizeFrames(rawFrames.map((f) => f.filePath), tempDir).catch((e) => {
217
+ warnings.push(`Frame optimization failed: ${e instanceof Error ? e.message : String(e)}`);
218
+ return rawFrames.map((f) => f.filePath);
219
+ });
220
+ result.frames = rawFrames.map((frame, i) => ({
221
+ ...frame,
222
+ filePath: optimizedPaths[i] ?? frame.filePath,
223
+ }));
224
+ framesExtracted = true;
225
+ }
226
+ }
227
+ }
228
+ // Strategy 2: Browser-based extraction (fallback)
229
+ if (!framesExtracted && metadata.duration > 0) {
230
+ await reportProgress({ progress: 60, total: 100 });
231
+ const timestamps = generateTimestamps(metadata.duration, maxFrames);
232
+ const browserFrames = await extractBrowserFrames(url, tempDir, {
233
+ timestamps,
234
+ }).catch((e) => {
235
+ warnings.push(`Browser frame extraction failed: ${e instanceof Error ? e.message : String(e)}`);
236
+ return [];
237
+ });
238
+ await reportProgress({ progress: 80, total: 100 });
239
+ if (browserFrames.length > 0) {
240
+ result.frames = browserFrames;
241
+ framesExtracted = true;
242
+ }
243
+ }
244
+ if (!framesExtracted) {
245
+ warnings.push('Frame extraction not available — returning transcript and metadata only. Install yt-dlp or Chrome/Chromium for frame extraction.');
246
+ }
247
+ // Post-processing: dedup, OCR, timeline
248
+ if (result.frames.length > 0) {
249
+ const beforeDedup = result.frames.length;
250
+ result.frames = await deduplicateFrames(result.frames).catch((e) => {
251
+ warnings.push(`Frame dedup failed: ${e instanceof Error ? e.message : String(e)}`);
252
+ return result.frames;
253
+ });
254
+ if (result.frames.length < beforeDedup) {
255
+ warnings.push(`Removed ${beforeDedup - result.frames.length} near-duplicate frames (${beforeDedup} → ${result.frames.length})`);
256
+ }
257
+ await reportProgress({ progress: 85, total: 100 });
258
+ // OCR: extract text visible on screen
259
+ if (config.includeOcr) {
260
+ result.ocrResults = await extractTextFromFrames(result.frames).catch((e) => {
261
+ warnings.push(`OCR failed: ${e instanceof Error ? e.message : String(e)}`);
262
+ return [];
263
+ });
264
+ }
265
+ await reportProgress({ progress: 95, total: 100 });
266
+ }
267
+ // Build annotated timeline
268
+ if (config.includeTimeline) {
269
+ result.timeline = buildAnnotatedTimeline(result.transcript, result.frames, result.ocrResults);
270
+ }
271
+ }
272
+ else {
273
+ // Even without frames, try to get the video for whisper fallback
274
+ if (result.transcript.length === 0 && adapter.capabilities.videoDownload) {
275
+ tempDir = tempDir ?? (await createTempDir());
276
+ videoPath = await adapter.downloadVideo(url, tempDir).catch(() => null);
277
+ }
278
+ }
279
+ // Whisper fallback: if no transcript and we have a video file
280
+ if (result.transcript.length === 0 && videoPath) {
281
+ try {
282
+ const audioPath = await extractAudioTrack(videoPath, tempDir ?? '');
283
+ const whisperTranscript = await transcribeAudio(audioPath);
284
+ if (whisperTranscript.length > 0) {
285
+ result.transcript = whisperTranscript;
286
+ warnings.push('Transcript generated via Whisper fallback (no native transcript available).');
287
+ }
288
+ }
289
+ catch {
290
+ // Audio extraction or transcription failed — not critical
291
+ }
292
+ }
293
+ await reportProgress({ progress: 100, total: 100 });
294
+ // Cache the full result
295
+ cache.set(key, result);
296
+ // Apply field filter
297
+ const filtered = filterAnalysisResult(result, fields);
298
+ // Build response content
299
+ const textData = {
300
+ ...filtered,
301
+ frameCount: result.frames.length,
302
+ };
303
+ const content = [{ type: 'text', text: JSON.stringify(textData, null, 2) }];
304
+ // Add frame images (if not filtered out)
305
+ if (!fields || fields.includes('frames')) {
306
+ for (const frame of result.frames) {
307
+ content.push(await imageContent({ path: frame.filePath }));
308
+ }
309
+ }
310
+ return { content };
311
+ }
312
+ finally {
313
+ if (tempDir && skipFrames) {
314
+ await cleanupTempDir(tempDir).catch(() => undefined);
315
+ }
316
+ }
317
+ },
318
+ });
319
+ }
320
+ //# sourceMappingURL=analyze-video.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-video.js","sourceRoot":"","sources":["../../src/tools/analyze-video.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { FastMCP } from 'fastmcp';
2
+ export declare function registerGetFrameAt(server: FastMCP): void;
@@ -0,0 +1,88 @@
1
+ import { imageContent, UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getAdapter } from '../adapters/adapter.interface.js';
4
+ import { extractFrameAt, parseTimestamp } from '../processors/frame-extractor.js';
5
+ import { extractBrowserFrames } from '../processors/browser-frame-extractor.js';
6
+ import { optimizeFrame } from '../processors/image-optimizer.js';
7
+ import { createTempDir } from '../utils/temp-files.js';
8
+ import { getTempFilePath } from '../utils/temp-files.js';
9
+ const GetFrameAtSchema = z.object({
10
+ url: z.string().url().describe('Video URL (Loom share link or direct mp4/webm URL)'),
11
+ timestamp: z
12
+ .string()
13
+ .describe('Timestamp to extract frame at (e.g., "1:23", "0:05", "01:23:45")'),
14
+ returnBase64: z
15
+ .boolean()
16
+ .default(false)
17
+ .optional()
18
+ .describe('Return frame as base64 inline instead of file path'),
19
+ });
20
+ export function registerGetFrameAt(server) {
21
+ server.addTool({
22
+ name: 'get_frame_at',
23
+ description: `Extract a single video frame at a specific timestamp.
24
+
25
+ Useful for inspecting what's on screen at a particular moment. The AI reads the transcript,
26
+ identifies a critical moment, and requests the exact frame at that timestamp.
27
+
28
+ Supports: Loom (loom.com/share/...) and direct video URLs (.mp4, .webm, .mov).
29
+ Requires video download capability — direct URLs work best.
30
+
31
+ Args:
32
+ - url: Video URL
33
+ - timestamp: Time position (e.g., "1:23", "0:05", "01:23:45")
34
+
35
+ Returns: A single image of the video frame at the specified timestamp.`,
36
+ parameters: GetFrameAtSchema,
37
+ annotations: {
38
+ title: 'Get Frame at Timestamp',
39
+ readOnlyHint: true,
40
+ destructiveHint: false,
41
+ idempotentHint: true,
42
+ openWorldHint: true,
43
+ },
44
+ execute: async (args, { reportProgress }) => {
45
+ const { url, timestamp } = args;
46
+ const adapter = getAdapter(url);
47
+ await reportProgress({ progress: 0, total: 100 });
48
+ const tempDir = await createTempDir();
49
+ // Strategy 1: Download video + ffmpeg extraction
50
+ if (adapter.capabilities.videoDownload) {
51
+ const videoPath = await adapter.downloadVideo(url, tempDir);
52
+ if (videoPath) {
53
+ await reportProgress({ progress: 50, total: 100 });
54
+ const frame = await extractFrameAt(videoPath, tempDir, timestamp);
55
+ const optimizedPath = getTempFilePath(tempDir, `opt_frame_at.jpg`);
56
+ await optimizeFrame(frame.filePath, optimizedPath);
57
+ await reportProgress({ progress: 100, total: 100 });
58
+ return {
59
+ content: [
60
+ { type: 'text', text: `Frame extracted at ${timestamp}` },
61
+ await imageContent({ path: optimizedPath }),
62
+ ],
63
+ };
64
+ }
65
+ }
66
+ // Strategy 2: Browser-based extraction (fallback)
67
+ await reportProgress({ progress: 30, total: 100 });
68
+ const seconds = parseTimestamp(timestamp);
69
+ const browserFrames = await extractBrowserFrames(url, tempDir, {
70
+ timestamps: [seconds],
71
+ });
72
+ if (browserFrames.length > 0) {
73
+ await reportProgress({ progress: 100, total: 100 });
74
+ return {
75
+ content: [
76
+ {
77
+ type: 'text',
78
+ text: `Frame extracted at ${timestamp} (via browser)`,
79
+ },
80
+ await imageContent({ path: browserFrames[0].filePath }),
81
+ ],
82
+ };
83
+ }
84
+ throw new UserError('Failed to extract frame. Install yt-dlp or Chrome/Chromium for frame extraction.');
85
+ },
86
+ });
87
+ }
88
+ //# sourceMappingURL=get-frame-at.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-frame-at.js","sourceRoot":"","sources":["../../src/tools/get-frame-at.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { FastMCP } from 'fastmcp';
2
+ export declare function registerGetFrameBurst(server: FastMCP): void;
@@ -0,0 +1,106 @@
1
+ import { imageContent, UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getAdapter } from '../adapters/adapter.interface.js';
4
+ import { extractFrameBurst, parseTimestamp } from '../processors/frame-extractor.js';
5
+ import { extractBrowserFrames } from '../processors/browser-frame-extractor.js';
6
+ import { optimizeFrames } from '../processors/image-optimizer.js';
7
+ import { createTempDir } from '../utils/temp-files.js';
8
+ const GetFrameBurstSchema = z.object({
9
+ url: z.string().url().describe('Video URL (Loom share link or direct mp4/webm URL)'),
10
+ from: z.string().describe('Start timestamp (e.g., "0:15")'),
11
+ to: z.string().describe('End timestamp (e.g., "0:17")'),
12
+ count: z
13
+ .number()
14
+ .min(2)
15
+ .max(30)
16
+ .default(5)
17
+ .optional()
18
+ .describe('Number of frames to extract (default: 5)'),
19
+ returnBase64: z
20
+ .boolean()
21
+ .default(false)
22
+ .optional()
23
+ .describe('Return frames as base64 inline instead of file paths'),
24
+ });
25
+ export function registerGetFrameBurst(server) {
26
+ server.addTool({
27
+ name: 'get_frame_burst',
28
+ description: `Extract multiple frames evenly distributed across a time range.
29
+
30
+ Designed for motion and vibration analysis where scene-change detection fails because
31
+ the "scene" doesn't change — only the position/state of objects does.
32
+
33
+ Example: get_frame_burst(url, "0:15", "0:17", 10) → 10 frames in 2 seconds
34
+ - AI sees the object in different positions across frames → understands the vibration
35
+ - Works for: shaking, flickering, animations, fast scrolling, loading spinners
36
+
37
+ Supports: Loom (loom.com/share/...) and direct video URLs (.mp4, .webm, .mov).
38
+ Requires video download capability — direct URLs work best.
39
+
40
+ Args:
41
+ - url: Video URL
42
+ - from: Start timestamp (e.g., "0:15")
43
+ - to: End timestamp (e.g., "0:17")
44
+ - count: Number of frames (default: 5, max: 30)
45
+
46
+ Returns: N images evenly distributed between the from and to timestamps.`,
47
+ parameters: GetFrameBurstSchema,
48
+ annotations: {
49
+ title: 'Get Frame Burst',
50
+ readOnlyHint: true,
51
+ destructiveHint: false,
52
+ idempotentHint: true,
53
+ openWorldHint: true,
54
+ },
55
+ execute: async (args, { reportProgress }) => {
56
+ const { url, from, to, count } = args;
57
+ const frameCount = count ?? 5;
58
+ const adapter = getAdapter(url);
59
+ await reportProgress({ progress: 0, total: 100 });
60
+ const tempDir = await createTempDir();
61
+ // Strategy 1: Download video + ffmpeg burst extraction
62
+ if (adapter.capabilities.videoDownload) {
63
+ const videoPath = await adapter.downloadVideo(url, tempDir);
64
+ if (videoPath) {
65
+ await reportProgress({ progress: 40, total: 100 });
66
+ const frames = await extractFrameBurst(videoPath, tempDir, from, to, frameCount);
67
+ await reportProgress({ progress: 70, total: 100 });
68
+ const optimizedPaths = await optimizeFrames(frames.map((f) => f.filePath), tempDir);
69
+ await reportProgress({ progress: 100, total: 100 });
70
+ const content = [
71
+ {
72
+ type: 'text',
73
+ text: `Extracted ${optimizedPaths.length} frames from ${from} to ${to}`,
74
+ },
75
+ ];
76
+ for (const path of optimizedPaths) {
77
+ content.push(await imageContent({ path }));
78
+ }
79
+ return { content };
80
+ }
81
+ }
82
+ // Strategy 2: Browser-based extraction (fallback)
83
+ await reportProgress({ progress: 30, total: 100 });
84
+ const fromSeconds = parseTimestamp(from);
85
+ const toSeconds = parseTimestamp(to);
86
+ const interval = (toSeconds - fromSeconds) / Math.max(frameCount - 1, 1);
87
+ const timestamps = Array.from({ length: frameCount }, (_, i) => Math.round(fromSeconds + i * interval));
88
+ const browserFrames = await extractBrowserFrames(url, tempDir, { timestamps });
89
+ if (browserFrames.length > 0) {
90
+ await reportProgress({ progress: 100, total: 100 });
91
+ const content = [
92
+ {
93
+ type: 'text',
94
+ text: `Extracted ${browserFrames.length} frames from ${from} to ${to} (via browser)`,
95
+ },
96
+ ];
97
+ for (const frame of browserFrames) {
98
+ content.push(await imageContent({ path: frame.filePath }));
99
+ }
100
+ return { content };
101
+ }
102
+ throw new UserError('Failed to extract frames. Install yt-dlp or Chrome/Chromium for frame extraction.');
103
+ },
104
+ });
105
+ }
106
+ //# sourceMappingURL=get-frame-burst.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-frame-burst.js","sourceRoot":"","sources":["../../src/tools/get-frame-burst.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { FastMCP } from 'fastmcp';
2
+ export declare function registerGetFrames(server: FastMCP): void;