@idk500/video-vision-mcp 1.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.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 pickstar-2002
4
+ Copyright (c) 2025 idk500 (Bigmodel glm-4.6v-flash backend fork)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # 🎬 Video Vision MCP
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
6
+
7
+ > 🚀 基于 Model Context Protocol (MCP) 的视频分析与拍摄脚本生成工具,使用 OpenAI 兼容的多模态视觉模型(默认:智谱 Bigmodel glm-4.6v-flash,免费可用)
8
+
9
+ ## ✨ 简介
10
+
11
+ Video Vision MCP 是一个专业的视频分析和脚本生成工具,通过 MCP 协议为 AI 助手提供强大的视频处理能力。它可以从视频中提取关键帧,使用多模态视觉模型进行智能内容分析,并生成专业的拍摄脚本。
12
+
13
+ > **Fork 自 [pickstar-2002/video-capture-script-mcp](https://github.com/pickstar-2002/video-capture-script-mcp)**,主要改动:将视觉后端从腾讯混元替换为 OpenAI 兼容接口(默认智谱 Bigmodel `glm-4.6v-flash`,免费可用),无需腾讯云密钥。感谢原作者的开源贡献。
14
+
15
+ ## 🎯 主要功能
16
+
17
+ - 🖼️ **智能帧提取**: 支持多种策略提取视频关键帧
18
+ - 均匀间隔提取 (uniform)
19
+ - 关键帧提取 (keyframe)
20
+ - 场景变化检测 (scene_change)
21
+ - 🤖 **AI 内容分析**: 使用多模态视觉模型分析视频/图片内容
22
+ - 🎬 **拍摄脚本生成**: 基于视频分析结果,生成专业拍摄脚本
23
+ - 支持多种类型:商业广告、纪录片、教学视频、叙事视频
24
+ - 可自定义目标受众、拍摄风格、时长要求
25
+ - 📊 **批量图片分析**: 批量分析多张图片内容
26
+ - 📹 **视频信息获取**: 获取视频时长、分辨率、帧率等元数据
27
+
28
+ ## 📦 安装
29
+
30
+ ### 在 MCP 兼容工具中配置
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "video-vision-mcp": {
36
+ "command": "npx",
37
+ "args": ["-y", "@idk500/video-vision-mcp@latest"],
38
+ "env": {
39
+ "VISION_API_KEY": "your_api_key_here"
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### 本地开发
47
+
48
+ ```bash
49
+ git clone https://github.com/idk500/video-capture-script-mcp.git
50
+ cd video-capture-script-mcp
51
+ npm install
52
+ npm run build
53
+ ```
54
+
55
+ ## 🔑 配置(视觉模型 API Key)
56
+
57
+ 视觉分析功能需要一个支持图片输入的多模态模型 API Key。**默认使用智谱 Bigmodel `glm-4.6v-flash`(免费)**。
58
+
59
+ ### 获取 Key
60
+
61
+ 1. 访问 https://open.bigmodel.cn/usercenter/apikeys
62
+ 2. 注册/登录智谱开放平台
63
+ 3. 创建 API Key(格式:`xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx`)
64
+
65
+ ### 配置方式(任选其一)
66
+
67
+ **环境变量(推荐):**
68
+
69
+ ```bash
70
+ export VISION_API_KEY=your_api_key_here
71
+ # 可选:自定义端点和模型
72
+ export VISION_ENDPOINT=https://open.bigmodel.cn/api/paas/v4
73
+ export VISION_MODEL=glm-4.6v-flash
74
+ export TEXT_MODEL=glm-4.6
75
+ ```
76
+
77
+ **启动参数:**
78
+
79
+ ```bash
80
+ node dist/index.js --secret-id your_api_key_here
81
+ ```
82
+
83
+ ### 切换其他 OpenAI 兼容后端
84
+
85
+ 只要接口兼容 OpenAI Chat Completions(支持 `image_url` 内容),都可以用:
86
+
87
+ ```bash
88
+ export VISION_ENDPOINT=https://your-openai-compatible-endpoint/v1
89
+ export VISION_MODEL=your-vision-model
90
+ export TEXT_MODEL=your-text-model
91
+ export VISION_API_KEY=your-key
92
+ ```
93
+
94
+ ## 🛠️ 可用工具
95
+
96
+ | 工具 | 说明 | 需要视觉模型 |
97
+ |------|------|:---:|
98
+ | `extract_video_frames` | 从视频提取关键帧(本地,依赖 FFmpeg) | 否 |
99
+ | `get_video_info` | 获取视频时长/分辨率/帧率等元信息 | 否 |
100
+ | `analyze_video_content` | 抽帧后送入视觉模型,总结视频内容 | 是 |
101
+ | `analyze_image_batch` | 批量分析图片内容 | 是 |
102
+ | `generate_video_script` | 抽帧→视觉理解→生成专业拍摄脚本 | 是 |
103
+ | `generate_image_script` | 基于多张图片生成拍摄脚本 | 是 |
104
+
105
+ **脚本类型**:`commercial`(商业广告)/ `documentary`(纪录片)/ `tutorial`(教学)/ `narrative`(叙事)/ `custom`(自定义)
106
+
107
+ ## 📋 系统要求与依赖
108
+
109
+ - **Node.js** >= 18.0.0
110
+ - **FFmpeg**(帧提取与视频信息)
111
+ - Windows: `choco install ffmpeg`
112
+ - macOS: `brew install ffmpeg`
113
+ - Linux: `sudo apt install ffmpeg`
114
+
115
+ ## 📝 开发
116
+
117
+ ```bash
118
+ npm run dev # 开发模式(tsx)
119
+ npm run build # 编译到 dist/
120
+ npm run lint # 代码检查
121
+ ```
122
+
123
+ ## 📄 许可证
124
+
125
+ [MIT](LICENSE) License
126
+
127
+ Copyright (c) 2025 pickstar-2002(原作者)
128
+ Copyright (c) 2025 idk500(Bigmodel 后端改造)
129
+
130
+ ## 🙏 致谢
131
+
132
+ 本项目 Fork 自 [pickstar-2002/video-capture-script-mcp](https://github.com/pickstar-2002/video-capture-script-mcp),感谢原作者的完整实现。Fork 改动点:将腾讯混元视觉后端替换为 OpenAI 兼容接口(默认智谱 Bigmodel glm-4.6v-flash)。
133
+
134
+ ## 🐛 问题反馈
135
+
136
+ 请在 [GitHub Issues](https://github.com/idk500/video-capture-script-mcp/issues) 中反馈问题或建议。
@@ -0,0 +1,28 @@
1
+ export interface FrameExtractionOptions {
2
+ maxFrames: number;
3
+ outputDir?: string;
4
+ strategy: 'uniform' | 'keyframe' | 'scene_change';
5
+ quality?: number;
6
+ }
7
+ export interface VideoInfo {
8
+ duration: number;
9
+ width: number;
10
+ height: number;
11
+ frameRate: number;
12
+ frameCount: number;
13
+ format: string;
14
+ }
15
+ export declare class FrameExtractor {
16
+ private defaultOutputDir;
17
+ constructor();
18
+ private ensureOutputDir;
19
+ getVideoInfo(videoPath: string): Promise<VideoInfo>;
20
+ private parseFrameRate;
21
+ extractFrames(videoPath: string, options: FrameExtractionOptions): Promise<string[]>;
22
+ private calculateTimestamps;
23
+ private generateUniformTimestamps;
24
+ private detectKeyframes;
25
+ private detectSceneChanges;
26
+ private extractFrameAtTimestamp;
27
+ cleanupFrames(framePaths: string[]): Promise<void>;
28
+ }
@@ -0,0 +1,246 @@
1
+ import ffmpeg from 'fluent-ffmpeg';
2
+ import path from 'path';
3
+ import { promises as fs } from 'fs';
4
+ export class FrameExtractor {
5
+ defaultOutputDir = './temp_frames';
6
+ constructor() {
7
+ // 确保临时目录存在
8
+ this.ensureOutputDir(this.defaultOutputDir);
9
+ }
10
+ async ensureOutputDir(dir) {
11
+ try {
12
+ await fs.access(dir);
13
+ }
14
+ catch {
15
+ await fs.mkdir(dir, { recursive: true });
16
+ }
17
+ }
18
+ async getVideoInfo(videoPath) {
19
+ return new Promise((resolve, reject) => {
20
+ console.error(`分析视频文件: ${videoPath}`);
21
+ ffmpeg.ffprobe(videoPath, (err, metadata) => {
22
+ if (err) {
23
+ let errorMessage = `获取视频信息失败: ${err.message}`;
24
+ if (err.message.includes('No such file')) {
25
+ errorMessage = `视频文件不存在: ${videoPath}`;
26
+ }
27
+ else if (err.message.includes('Invalid data')) {
28
+ errorMessage = `视频文件格式无效或损坏: ${videoPath}`;
29
+ }
30
+ else if (err.message.includes('Permission denied')) {
31
+ errorMessage = `无权限访问视频文件: ${videoPath}`;
32
+ }
33
+ else if (err.message.includes('ffprobe')) {
34
+ errorMessage = `FFmpeg未正确安装或配置。请确保FFmpeg已正确安装并可在命令行中使用。\n原始错误: ${err.message}`;
35
+ }
36
+ reject(new Error(errorMessage));
37
+ return;
38
+ }
39
+ if (!metadata || !metadata.streams) {
40
+ reject(new Error(`视频文件元数据获取失败: ${videoPath}`));
41
+ return;
42
+ }
43
+ const videoStream = metadata.streams.find(s => s.codec_type === 'video');
44
+ if (!videoStream) {
45
+ reject(new Error(`视频文件中未找到视频流,可能是纯音频文件: ${videoPath}`));
46
+ return;
47
+ }
48
+ const duration = metadata.format.duration || 0;
49
+ if (duration <= 0) {
50
+ reject(new Error(`视频时长无效 (${duration}秒),可能是损坏的视频文件: ${videoPath}`));
51
+ return;
52
+ }
53
+ const frameRate = this.parseFrameRate(videoStream.r_frame_rate || '25/1');
54
+ const frameCount = Math.floor(duration * frameRate);
55
+ const videoInfo = {
56
+ duration,
57
+ width: videoStream.width || 0,
58
+ height: videoStream.height || 0,
59
+ frameRate,
60
+ frameCount,
61
+ format: metadata.format.format_name || 'unknown',
62
+ };
63
+ console.error(`视频信息获取成功 - 时长: ${duration.toFixed(2)}s, 分辨率: ${videoInfo.width}x${videoInfo.height}, 帧率: ${frameRate.toFixed(2)}fps`);
64
+ resolve(videoInfo);
65
+ });
66
+ });
67
+ }
68
+ parseFrameRate(frameRateStr) {
69
+ const parts = frameRateStr.split('/');
70
+ if (parts.length === 2) {
71
+ return parseInt(parts[0]) / parseInt(parts[1]);
72
+ }
73
+ return parseFloat(frameRateStr) || 25;
74
+ }
75
+ async extractFrames(videoPath, options) {
76
+ try {
77
+ const outputDir = options.outputDir || this.defaultOutputDir;
78
+ await this.ensureOutputDir(outputDir);
79
+ console.error(`开始提取视频帧 - 输出目录: ${outputDir}`);
80
+ const videoInfo = await this.getVideoInfo(videoPath);
81
+ // 验证提取参数
82
+ if (options.maxFrames <= 0) {
83
+ throw new Error(`最大帧数必须大于0,当前值: ${options.maxFrames}`);
84
+ }
85
+ if (options.maxFrames > 100) {
86
+ console.warn(`警告: 请求提取大量帧 (${options.maxFrames}),这可能会消耗大量存储空间和处理时间`);
87
+ }
88
+ const timestamps = await this.calculateTimestamps(videoPath, videoInfo, options);
89
+ if (timestamps.length === 0) {
90
+ throw new Error('无法计算有效的时间戳,可能是视频太短或参数设置有误');
91
+ }
92
+ console.error(`计算出 ${timestamps.length} 个提取时间点: ${timestamps.map(t => t.toFixed(2)).join(', ')}s`);
93
+ const framePaths = [];
94
+ const videoName = path.basename(videoPath, path.extname(videoPath));
95
+ let successCount = 0;
96
+ let failureCount = 0;
97
+ for (let i = 0; i < timestamps.length; i++) {
98
+ const timestamp = timestamps[i];
99
+ const framePath = path.join(outputDir, `${videoName}_frame_${i + 1}_${timestamp.toFixed(2)}s.jpg`);
100
+ try {
101
+ console.error(`提取第${i + 1}/${timestamps.length}帧 - 时间: ${timestamp.toFixed(2)}s`);
102
+ await this.extractFrameAtTimestamp(videoPath, timestamp, framePath, options.quality || 90);
103
+ framePaths.push(framePath);
104
+ successCount++;
105
+ console.error(`第${i + 1}帧提取成功: ${framePath}`);
106
+ }
107
+ catch (error) {
108
+ failureCount++;
109
+ console.error(`第${i + 1}帧提取失败 (时间: ${timestamp.toFixed(2)}s):`, error);
110
+ // 如果连续失败太多,停止提取
111
+ if (failureCount >= 3 && successCount === 0) {
112
+ throw new Error(`连续多帧提取失败,停止处理。可能原因:\n1. FFmpeg配置问题\n2. 视频文件损坏\n3. 磁盘空间不足\n4. 输出目录权限问题`);
113
+ }
114
+ }
115
+ }
116
+ console.error(`帧提取完成 - 成功: ${successCount}, 失败: ${failureCount}`);
117
+ if (framePaths.length === 0) {
118
+ throw new Error('所有帧提取都失败了,请检查视频文件和FFmpeg配置');
119
+ }
120
+ return framePaths;
121
+ }
122
+ catch (error) {
123
+ console.error(`视频帧提取过程失败:`, error);
124
+ throw error;
125
+ }
126
+ }
127
+ async calculateTimestamps(videoPath, videoInfo, options) {
128
+ const { duration } = videoInfo;
129
+ const { maxFrames, strategy } = options;
130
+ switch (strategy) {
131
+ case 'uniform':
132
+ return this.generateUniformTimestamps(duration, maxFrames);
133
+ case 'keyframe':
134
+ return await this.detectKeyframes(videoPath, maxFrames);
135
+ case 'scene_change':
136
+ return await this.detectSceneChanges(videoPath, maxFrames);
137
+ default:
138
+ return this.generateUniformTimestamps(duration, maxFrames);
139
+ }
140
+ }
141
+ generateUniformTimestamps(duration, maxFrames) {
142
+ const timestamps = [];
143
+ const interval = duration / (maxFrames + 1);
144
+ for (let i = 1; i <= maxFrames; i++) {
145
+ timestamps.push(interval * i);
146
+ }
147
+ return timestamps;
148
+ }
149
+ async detectKeyframes(videoPath, maxFrames) {
150
+ return new Promise((resolve, reject) => {
151
+ // 简化方法:直接回退到均匀采样,避免复杂的FFmpeg参数问题
152
+ console.warn('Using uniform sampling instead of keyframe detection for better compatibility');
153
+ // 首先获取视频时长
154
+ ffmpeg.ffprobe(videoPath, (err, metadata) => {
155
+ if (err) {
156
+ console.warn('Could not get video duration, using default 60s');
157
+ resolve(this.generateUniformTimestamps(60, maxFrames));
158
+ return;
159
+ }
160
+ const duration = metadata.format.duration || 60;
161
+ resolve(this.generateUniformTimestamps(duration, maxFrames));
162
+ });
163
+ });
164
+ }
165
+ async detectSceneChanges(videoPath, maxFrames) {
166
+ return new Promise((resolve, reject) => {
167
+ // 简化方法:直接回退到均匀采样,避免复杂的FFmpeg参数问题
168
+ console.warn('Using uniform sampling instead of scene detection for better compatibility');
169
+ // 首先获取视频时长
170
+ ffmpeg.ffprobe(videoPath, (err, metadata) => {
171
+ if (err) {
172
+ console.warn('Could not get video duration, using default 60s');
173
+ resolve(this.generateUniformTimestamps(60, maxFrames));
174
+ return;
175
+ }
176
+ const duration = metadata.format.duration || 60;
177
+ resolve(this.generateUniformTimestamps(duration, maxFrames));
178
+ });
179
+ });
180
+ }
181
+ async extractFrameAtTimestamp(videoPath, timestamp, outputPath, quality = 90) {
182
+ return new Promise((resolve, reject) => {
183
+ // 验证参数
184
+ if (timestamp < 0) {
185
+ reject(new Error(`时间戳不能为负数: ${timestamp}`));
186
+ return;
187
+ }
188
+ if (quality < 1 || quality > 100) {
189
+ console.warn(`质量参数超出范围 (${quality}),将使用默认值90`);
190
+ quality = 90;
191
+ }
192
+ const timeoutId = setTimeout(() => {
193
+ reject(new Error(`帧提取超时 (${timestamp}s) - 可能是视频文件问题或FFmpeg响应慢`));
194
+ }, 30000); // 30秒超时
195
+ ffmpeg(videoPath)
196
+ .seekInput(timestamp)
197
+ .frames(1)
198
+ .outputOptions([`-q:v ${Math.floor((100 - quality) / 10)}`]) // 转换质量参数
199
+ .output(outputPath)
200
+ .on('start', (commandLine) => {
201
+ console.error(`FFmpeg命令: ${commandLine}`);
202
+ })
203
+ .on('end', () => {
204
+ clearTimeout(timeoutId);
205
+ resolve();
206
+ })
207
+ .on('error', (err) => {
208
+ clearTimeout(timeoutId);
209
+ let errorMessage = `帧提取失败 (时间: ${timestamp.toFixed(2)}s): ${err.message}`;
210
+ if (err.message.includes('Invalid data')) {
211
+ errorMessage = `在时间点 ${timestamp.toFixed(2)}s 处的视频数据无效,可能超出视频时长或视频损坏`;
212
+ }
213
+ else if (err.message.includes('No such file')) {
214
+ errorMessage = `视频文件在处理过程中丢失: ${videoPath}`;
215
+ }
216
+ else if (err.message.includes('Permission denied')) {
217
+ errorMessage = `无权限写入输出文件: ${outputPath}`;
218
+ }
219
+ else if (err.message.includes('No space left')) {
220
+ errorMessage = `磁盘空间不足,无法保存帧文件: ${outputPath}`;
221
+ }
222
+ reject(new Error(errorMessage));
223
+ })
224
+ .run();
225
+ });
226
+ }
227
+ async cleanupFrames(framePaths) {
228
+ let successCount = 0;
229
+ let failureCount = 0;
230
+ console.error(`开始清理 ${framePaths.length} 个临时帧文件...`);
231
+ for (const framePath of framePaths) {
232
+ try {
233
+ await fs.unlink(framePath);
234
+ successCount++;
235
+ }
236
+ catch (error) {
237
+ failureCount++;
238
+ console.warn(`清理文件失败 ${framePath}:`, error);
239
+ }
240
+ }
241
+ console.error(`文件清理完成 - 成功: ${successCount}, 失败: ${failureCount}`);
242
+ if (failureCount > 0) {
243
+ console.warn(`部分临时文件清理失败,可能需要手动删除`);
244
+ }
245
+ }
246
+ }
@@ -0,0 +1,95 @@
1
+ export interface HunyuanConfig {
2
+ /** OpenAI-compatible API key (sent as `Authorization: Bearer <key>`). */
3
+ secretId?: string;
4
+ /** Kept for config-shape compatibility; ignored (auth uses secretId only). */
5
+ secretKey?: string;
6
+ /** Unused for OpenAI-compatible auth; kept for config-shape compatibility. */
7
+ region?: string;
8
+ /** Base URL, e.g. `https://open.bigmodel.cn/api/paas/v4`. */
9
+ endpoint?: string;
10
+ /** Vision model name. */
11
+ visionModel?: string;
12
+ /** Text model name. */
13
+ textModel?: string;
14
+ }
15
+ export interface ImageAnalysisResult {
16
+ content: string;
17
+ usage: {
18
+ promptTokens: number;
19
+ completionTokens: number;
20
+ totalTokens: number;
21
+ };
22
+ }
23
+ export interface TextGenerationResult {
24
+ content: string;
25
+ usage: {
26
+ promptTokens: number;
27
+ completionTokens: number;
28
+ totalTokens: number;
29
+ };
30
+ }
31
+ export interface Message {
32
+ Role: string;
33
+ Contents?: Array<{
34
+ Type: string;
35
+ Text?: string;
36
+ ImageUrl?: {
37
+ Url: string;
38
+ };
39
+ }>;
40
+ Content?: string;
41
+ }
42
+ export declare class HunyuanClient {
43
+ private apiKey?;
44
+ private endpoint;
45
+ private visionModel;
46
+ private textModel;
47
+ constructor(config?: HunyuanConfig);
48
+ /**
49
+ * Set credentials at runtime. For the OpenAI-compatible backend only the
50
+ * `secretId` (API key) is meaningful; `secretKey` is accepted for
51
+ * signature compatibility and ignored.
52
+ */
53
+ setCredentials(secretId: string, _secretKey?: string): void;
54
+ private resolveApiKey;
55
+ private resolveEndpoint;
56
+ private resolveVisionModel;
57
+ private resolveTextModel;
58
+ /** Map a file extension to its MIME type, defaulting to jpeg. */
59
+ private mimeFromExt;
60
+ /**
61
+ * Read an image and return a base64 string suitable for `image_url.url`.
62
+ * Returns a bare base64 string (no `data:` prefix) — this is what the
63
+ * Zhipu Bigmodel endpoint expects; an explicit `data:` URL is *not* added
64
+ * here because the backend rejects it for the glm-4.6v family.
65
+ */
66
+ private imageToBase64;
67
+ /** Sleep helper for retry backoff. */
68
+ private sleep;
69
+ /**
70
+ * POST to /chat/completions with retry on 429/5xx. The Zhipu free vision
71
+ * model throttles aggressively under load, so callers benefit from backoff.
72
+ */
73
+ private chatCompletions;
74
+ /**
75
+ * Analyze a single image. The backend is the OpenAI-compatible Chat
76
+ * Completions API with an `image_url` content part.
77
+ */
78
+ analyzeImage(imagePath: string, prompt?: string, apiKeyOverride?: string): Promise<ImageAnalysisResult>;
79
+ /**
80
+ * Analyze each image in sequence. Serial processing keeps us under the free
81
+ * model's QPS limit; failed images do not abort the batch.
82
+ */
83
+ analyzeImageBatch(imagePaths: string[], prompt?: string, apiKeyOverride?: string): Promise<ImageAnalysisResult[]>;
84
+ /**
85
+ * Analyze multiple images in a single request. Useful for video frames so
86
+ * the model can reason about sequence/context. Limited to 4 images to keep
87
+ * request size and cost bounded (matches the original behavior).
88
+ */
89
+ analyzeImagesInSingleRequest(imagePaths: string[], prompt?: string, apiKeyOverride?: string): Promise<ImageAnalysisResult>;
90
+ /**
91
+ * Generate text from a prompt. Used for the second pass of script
92
+ * generation, where we don't need vision — a plain text model is cheaper.
93
+ */
94
+ generateText(prompt: string, modelOverride?: string, apiKeyOverride?: string): Promise<TextGenerationResult>;
95
+ }