@juspay/neurolink 9.7.0 → 9.9.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/CHANGELOG.md +12 -0
- package/dist/adapters/video/videoAnalyzer.d.ts +26 -0
- package/dist/adapters/video/videoAnalyzer.js +222 -0
- package/dist/core/baseProvider.js +20 -0
- package/dist/lib/adapters/video/videoAnalyzer.d.ts +26 -0
- package/dist/lib/adapters/video/videoAnalyzer.js +223 -0
- package/dist/lib/core/baseProvider.js +20 -0
- package/dist/lib/processors/media/VideoProcessor.js +9 -12
- package/dist/lib/types/generateTypes.d.ts +1 -0
- package/dist/lib/utils/videoAnalysisProcessor.d.ts +30 -0
- package/dist/lib/utils/videoAnalysisProcessor.js +59 -0
- package/dist/processors/media/VideoProcessor.js +9 -12
- package/dist/types/generateTypes.d.ts +1 -0
- package/dist/utils/videoAnalysisProcessor.d.ts +30 -0
- package/dist/utils/videoAnalysisProcessor.js +58 -0
- package/package.json +26 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [9.9.0](https://github.com/juspay/neurolink/compare/v9.8.0...v9.9.0) (2026-02-17)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **(video-analysis):** add video-analysis support in neurolink ([c35f8a8](https://github.com/juspay/neurolink/commit/c35f8a8d52cc1366e10b8701285e1bec52e27d98))
|
|
6
|
+
|
|
7
|
+
## [9.8.0](https://github.com/juspay/neurolink/compare/v9.7.0...v9.8.0) (2026-02-17)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **(scripts):** migrate peripheral JS/CJS/MJS files to TypeScript with tsx runner ([25f17e8](https://github.com/juspay/neurolink/commit/25f17e81c09d6184a4bf2ff15e4427c302fde55e))
|
|
12
|
+
|
|
1
13
|
## [9.7.0](https://github.com/juspay/neurolink/compare/v9.6.0...v9.7.0) (2026-02-16)
|
|
2
14
|
|
|
3
15
|
### Features
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Analysis Handler
|
|
3
|
+
*
|
|
4
|
+
* Provides video analysis using Google's Gemini 2.0 Flash model.
|
|
5
|
+
* Supports both Vertex AI and Gemini API providers.
|
|
6
|
+
*
|
|
7
|
+
* @module adapters/video/geminiVideoAnalyzer
|
|
8
|
+
*/
|
|
9
|
+
import { AIProviderName } from "../../constants/enums.js";
|
|
10
|
+
import type { CoreMessage } from "ai";
|
|
11
|
+
export declare function analyzeVideoWithVertexAI(frames: CoreMessage, options?: {
|
|
12
|
+
project?: string;
|
|
13
|
+
location?: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
export declare function analyzeVideoWithGeminiAPI(frames: CoreMessage, options?: {
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
}): Promise<string>;
|
|
20
|
+
export declare function analyzeVideo(frames: CoreMessage, options?: {
|
|
21
|
+
provider?: AIProviderName;
|
|
22
|
+
project?: string;
|
|
23
|
+
location?: string;
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
model?: string;
|
|
26
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Analysis Handler
|
|
3
|
+
*
|
|
4
|
+
* Provides video analysis using Google's Gemini 2.0 Flash model.
|
|
5
|
+
* Supports both Vertex AI and Gemini API providers.
|
|
6
|
+
*
|
|
7
|
+
* @module adapters/video/geminiVideoAnalyzer
|
|
8
|
+
*/
|
|
9
|
+
import { AIProviderName, ErrorSeverity, ErrorCategory, } from "../../constants/enums.js";
|
|
10
|
+
import { logger } from "../../utils/logger.js";
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import { NeuroLinkError } from "../../utils/errorHandling.js";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Shared config
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const DEFAULT_MODEL = "gemini-2.0-flash";
|
|
17
|
+
const DEFAULT_LOCATION = "us-central1";
|
|
18
|
+
/**
|
|
19
|
+
* Convert CoreMessage content array to Gemini parts format
|
|
20
|
+
*
|
|
21
|
+
* @param contentArray - Array of content items from CoreMessage
|
|
22
|
+
* @returns Array of parts in Gemini API format
|
|
23
|
+
*/
|
|
24
|
+
function buildContentParts(frames) {
|
|
25
|
+
const contentArray = Array.isArray(frames.content) ? frames.content : [];
|
|
26
|
+
return contentArray.map((item) => {
|
|
27
|
+
if (item.type === "text" && item.text) {
|
|
28
|
+
return { text: item.text };
|
|
29
|
+
}
|
|
30
|
+
else if (item.type === "image" && item.image) {
|
|
31
|
+
let base64Data;
|
|
32
|
+
// Handle Buffer or Uint8Array
|
|
33
|
+
if (Buffer.isBuffer(item.image) || item.image instanceof Uint8Array) {
|
|
34
|
+
base64Data = Buffer.from(item.image).toString("base64");
|
|
35
|
+
}
|
|
36
|
+
else if (typeof item.image === "string") {
|
|
37
|
+
// Strip data URI prefix if present (e.g., "data:image/jpeg;base64,")
|
|
38
|
+
base64Data = item.image.replace(/^data:image\/[a-z]+;base64,/, "");
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
throw new Error(`Invalid image data type: expected string, Buffer, or Uint8Array, got ${typeof item.image}`);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
inlineData: {
|
|
45
|
+
mimeType: "image/jpeg",
|
|
46
|
+
data: base64Data,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Invalid content type: ${item.type}`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Configuration for video frame analysis.
|
|
55
|
+
* Generic prompt that handles both general content and technical bug reporting.
|
|
56
|
+
*/
|
|
57
|
+
function buildConfig() {
|
|
58
|
+
return {
|
|
59
|
+
systemInstruction: `You are a Visual Analysis Assistant.
|
|
60
|
+
Your task is to analyze images or video frames provided by the user and extract structured visual features. The user may or may not provide an issue description. Your role is to understand the visual content, optionally correlate it with the provided issue, and produce a structured output that can be directly consumed by another LLM for analysis, debugging, or decision-making.
|
|
61
|
+
|
|
62
|
+
Follow these rules strictly:
|
|
63
|
+
- The analysis must be generic and applicable to any domain (UI, dashboards, video frames, animations, charts, documents, etc.).
|
|
64
|
+
- Support both images and videos (single frame or multiple frames).
|
|
65
|
+
- Extract only what is visually observable; do not assume backend behavior unless supported by visuals.
|
|
66
|
+
- The JSON must be structured, consistent, and machine-readable.
|
|
67
|
+
- Logs are optional and should only be included if explicitly provided.
|
|
68
|
+
- The final output must be clear, concise, and actionable for an LLM.
|
|
69
|
+
|
|
70
|
+
Always produce the output in the following format:
|
|
71
|
+
|
|
72
|
+
Issue:
|
|
73
|
+
<Refined issue description if provided, otherwise a clear description of the observed visual situation>
|
|
74
|
+
|
|
75
|
+
Image/Video Patterns:
|
|
76
|
+
<Structured JSON describing extracted visual features and anomalies>
|
|
77
|
+
|
|
78
|
+
Steps to Reproduce:
|
|
79
|
+
<Ordered steps that reliably reproduce the issue based on the visual context>
|
|
80
|
+
|
|
81
|
+
[Logs: Include ONLY if provided by the user]
|
|
82
|
+
|
|
83
|
+
Proof:
|
|
84
|
+
<Visual evidence explaining how the image/video confirms the issue>
|
|
85
|
+
Ensure the final response is fully self-sufficient and does not reference external context.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Vertex AI
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
export async function analyzeVideoWithVertexAI(frames, options = {}) {
|
|
92
|
+
const startTime = Date.now();
|
|
93
|
+
const { GoogleGenAI } = await import("@google/genai");
|
|
94
|
+
// Get default config and merge with provided options
|
|
95
|
+
const config = await getVertexConfig();
|
|
96
|
+
const project = options.project ?? config.project;
|
|
97
|
+
const location = options.location ?? config.location;
|
|
98
|
+
const model = options.model || DEFAULT_MODEL;
|
|
99
|
+
// Extract content array from CoreMessage
|
|
100
|
+
const contentArray = Array.isArray(frames.content) ? frames.content : [];
|
|
101
|
+
const frameCount = contentArray.filter((item) => item.type === "image").length;
|
|
102
|
+
logger.debug("[GeminiVideoAnalyzer] Analyzing video with Vertex AI", {
|
|
103
|
+
project,
|
|
104
|
+
location,
|
|
105
|
+
model,
|
|
106
|
+
frameCount,
|
|
107
|
+
});
|
|
108
|
+
const ai = new GoogleGenAI({ vertexai: true, project, location });
|
|
109
|
+
// Convert frames content to parts array for Gemini
|
|
110
|
+
const parts = buildContentParts(frames);
|
|
111
|
+
const response = await ai.models.generateContent({
|
|
112
|
+
model,
|
|
113
|
+
config: buildConfig(),
|
|
114
|
+
contents: [
|
|
115
|
+
{
|
|
116
|
+
role: "user",
|
|
117
|
+
parts,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
const responseText = response.text || "";
|
|
122
|
+
const processingTime = Date.now() - startTime;
|
|
123
|
+
logger.debug("[GeminiVideoAnalyzer] Vertex response received", {
|
|
124
|
+
responseLength: responseText.length,
|
|
125
|
+
processingTime,
|
|
126
|
+
});
|
|
127
|
+
return responseText;
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Gemini API (Google AI)
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
export async function analyzeVideoWithGeminiAPI(frames, options = {}) {
|
|
133
|
+
const startTime = Date.now();
|
|
134
|
+
const { GoogleGenAI } = await import("@google/genai");
|
|
135
|
+
const apiKey = options.apiKey || process.env.GOOGLE_AI_API_KEY;
|
|
136
|
+
const model = options.model || DEFAULT_MODEL;
|
|
137
|
+
if (!apiKey) {
|
|
138
|
+
throw new Error("GOOGLE_AI_API_KEY environment variable is required for Gemini API video analysis");
|
|
139
|
+
}
|
|
140
|
+
// Extract content array from CoreMessage
|
|
141
|
+
const contentArray = Array.isArray(frames.content) ? frames.content : [];
|
|
142
|
+
const frameCount = contentArray.filter((item) => item.type === "image").length;
|
|
143
|
+
logger.debug("[GeminiVideoAnalyzer] Analyzing video with Gemini API", {
|
|
144
|
+
model,
|
|
145
|
+
frameCount,
|
|
146
|
+
});
|
|
147
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
148
|
+
// Convert frames content to parts array for Gemini
|
|
149
|
+
const parts = buildContentParts(frames);
|
|
150
|
+
logger.debug("[GeminiVideoAnalyzer] Generating analysis with frames");
|
|
151
|
+
const response = await ai.models.generateContent({
|
|
152
|
+
model,
|
|
153
|
+
config: buildConfig(),
|
|
154
|
+
contents: [
|
|
155
|
+
{
|
|
156
|
+
role: "user",
|
|
157
|
+
parts,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
const responseText = response.text || "";
|
|
162
|
+
const processingTime = Date.now() - startTime;
|
|
163
|
+
logger.debug("[GeminiVideoAnalyzer] Gemini API response received", {
|
|
164
|
+
responseLength: responseText.length,
|
|
165
|
+
processingTime,
|
|
166
|
+
});
|
|
167
|
+
return responseText;
|
|
168
|
+
}
|
|
169
|
+
async function getVertexConfig() {
|
|
170
|
+
const location = process.env.GOOGLE_VERTEX_LOCATION || DEFAULT_LOCATION;
|
|
171
|
+
// Try environment variables first
|
|
172
|
+
let project = process.env.GOOGLE_VERTEX_PROJECT ||
|
|
173
|
+
process.env.GOOGLE_CLOUD_PROJECT ||
|
|
174
|
+
process.env.GOOGLE_CLOUD_PROJECT_ID ||
|
|
175
|
+
process.env.VERTEX_PROJECT_ID;
|
|
176
|
+
// Fallback: read from ADC credentials file
|
|
177
|
+
if (!project && process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
178
|
+
try {
|
|
179
|
+
const credData = JSON.parse(await readFile(process.env.GOOGLE_APPLICATION_CREDENTIALS, "utf-8"));
|
|
180
|
+
project = credData.quota_project_id || credData.project_id;
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
// Ignore read errors, will throw below if project still not found
|
|
184
|
+
logger.debug("Failed to read project from credentials file", {
|
|
185
|
+
error: e instanceof Error ? e.message : String(e),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!project) {
|
|
190
|
+
throw new NeuroLinkError({
|
|
191
|
+
code: "PROVIDER_NOT_CONFIGURED",
|
|
192
|
+
message: "Google Cloud project not found. Set GOOGLE_VERTEX_PROJECT or GOOGLE_CLOUD_PROJECT environment variable, or ensure ADC credentials contain project_id",
|
|
193
|
+
category: ErrorCategory.CONFIGURATION,
|
|
194
|
+
severity: ErrorSeverity.HIGH,
|
|
195
|
+
retriable: false,
|
|
196
|
+
context: {
|
|
197
|
+
missingVar: "GOOGLE_VERTEX_PROJECT",
|
|
198
|
+
feature: "video-generation",
|
|
199
|
+
checkedEnvVars: [
|
|
200
|
+
"GOOGLE_VERTEX_PROJECT",
|
|
201
|
+
"GOOGLE_CLOUD_PROJECT",
|
|
202
|
+
"GOOGLE_CLOUD_PROJECT_ID",
|
|
203
|
+
"VERTEX_PROJECT_ID",
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return { project, location };
|
|
209
|
+
}
|
|
210
|
+
export async function analyzeVideo(frames, options = {}) {
|
|
211
|
+
const provider = options.provider || AIProviderName.AUTO;
|
|
212
|
+
// Vertex — only when GOOGLE_VERTEX_PROJECT is explicitly set
|
|
213
|
+
if (provider === AIProviderName.VERTEX || provider === AIProviderName.AUTO) {
|
|
214
|
+
return analyzeVideoWithVertexAI(frames, options);
|
|
215
|
+
}
|
|
216
|
+
// Gemini API — when GOOGLE_AI_API_KEY is set
|
|
217
|
+
if (provider === AIProviderName.GOOGLE_AI && process.env.GOOGLE_AI_API_KEY) {
|
|
218
|
+
return analyzeVideoWithGeminiAPI(frames, options);
|
|
219
|
+
}
|
|
220
|
+
throw new Error("No valid provider configuration found. " +
|
|
221
|
+
"Set GOOGLE_VERTEX_PROJECT for Vertex AI or GOOGLE_AI_API_KEY for Gemini API.");
|
|
222
|
+
}
|
|
@@ -7,6 +7,7 @@ import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../
|
|
|
7
7
|
import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
|
|
8
8
|
import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
|
|
9
9
|
import { TTSProcessor } from "../utils/ttsProcessor.js";
|
|
10
|
+
import { hasVideoFrames, executeVideoAnalysis, } from "../utils/videoAnalysisProcessor.js";
|
|
10
11
|
import { GenerationHandler } from "./modules/GenerationHandler.js";
|
|
11
12
|
// Import modules for composition
|
|
12
13
|
import { MessageBuilder } from "./modules/MessageBuilder.js";
|
|
@@ -473,6 +474,25 @@ export class BaseProvider {
|
|
|
473
474
|
// ===== Normal AI Generation Flow =====
|
|
474
475
|
const { tools, model } = await this.prepareGenerationContext(options);
|
|
475
476
|
const messages = await this.buildMessages(options);
|
|
477
|
+
// ===== VIDEO ANALYSIS FROM MESSAGES CONTENT =====
|
|
478
|
+
// Check if video files are present in messages content array
|
|
479
|
+
// If video analysis is needed, perform it and return early to avoid running generation
|
|
480
|
+
if (hasVideoFrames(messages)) {
|
|
481
|
+
const videoAnalysisResult = await executeVideoAnalysis(messages, {
|
|
482
|
+
provider: options.provider,
|
|
483
|
+
providerName: this.providerName,
|
|
484
|
+
region: options.region,
|
|
485
|
+
model: options.model,
|
|
486
|
+
});
|
|
487
|
+
// Return video analysis result directly without running generation
|
|
488
|
+
const videoResult = {
|
|
489
|
+
content: videoAnalysisResult,
|
|
490
|
+
provider: options.provider ?? this.providerName,
|
|
491
|
+
model: this.modelName,
|
|
492
|
+
usage: { input: 0, output: 0, total: 0 }, // Video analysis doesn't use standard token counting
|
|
493
|
+
};
|
|
494
|
+
return await this.enhanceResult(videoResult, options, startTime);
|
|
495
|
+
}
|
|
476
496
|
// Compose timeout signal with user-provided abort signal (mirrors stream path)
|
|
477
497
|
const timeoutController = createTimeoutController(options.timeout, this.providerName, "generate");
|
|
478
498
|
const composedSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Analysis Handler
|
|
3
|
+
*
|
|
4
|
+
* Provides video analysis using Google's Gemini 2.0 Flash model.
|
|
5
|
+
* Supports both Vertex AI and Gemini API providers.
|
|
6
|
+
*
|
|
7
|
+
* @module adapters/video/geminiVideoAnalyzer
|
|
8
|
+
*/
|
|
9
|
+
import { AIProviderName } from "../../constants/enums.js";
|
|
10
|
+
import type { CoreMessage } from "ai";
|
|
11
|
+
export declare function analyzeVideoWithVertexAI(frames: CoreMessage, options?: {
|
|
12
|
+
project?: string;
|
|
13
|
+
location?: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
export declare function analyzeVideoWithGeminiAPI(frames: CoreMessage, options?: {
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
}): Promise<string>;
|
|
20
|
+
export declare function analyzeVideo(frames: CoreMessage, options?: {
|
|
21
|
+
provider?: AIProviderName;
|
|
22
|
+
project?: string;
|
|
23
|
+
location?: string;
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
model?: string;
|
|
26
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Analysis Handler
|
|
3
|
+
*
|
|
4
|
+
* Provides video analysis using Google's Gemini 2.0 Flash model.
|
|
5
|
+
* Supports both Vertex AI and Gemini API providers.
|
|
6
|
+
*
|
|
7
|
+
* @module adapters/video/geminiVideoAnalyzer
|
|
8
|
+
*/
|
|
9
|
+
import { AIProviderName, ErrorSeverity, ErrorCategory, } from "../../constants/enums.js";
|
|
10
|
+
import { logger } from "../../utils/logger.js";
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import { NeuroLinkError } from "../../utils/errorHandling.js";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Shared config
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const DEFAULT_MODEL = "gemini-2.0-flash";
|
|
17
|
+
const DEFAULT_LOCATION = "us-central1";
|
|
18
|
+
/**
|
|
19
|
+
* Convert CoreMessage content array to Gemini parts format
|
|
20
|
+
*
|
|
21
|
+
* @param contentArray - Array of content items from CoreMessage
|
|
22
|
+
* @returns Array of parts in Gemini API format
|
|
23
|
+
*/
|
|
24
|
+
function buildContentParts(frames) {
|
|
25
|
+
const contentArray = Array.isArray(frames.content) ? frames.content : [];
|
|
26
|
+
return contentArray.map((item) => {
|
|
27
|
+
if (item.type === "text" && item.text) {
|
|
28
|
+
return { text: item.text };
|
|
29
|
+
}
|
|
30
|
+
else if (item.type === "image" && item.image) {
|
|
31
|
+
let base64Data;
|
|
32
|
+
// Handle Buffer or Uint8Array
|
|
33
|
+
if (Buffer.isBuffer(item.image) || item.image instanceof Uint8Array) {
|
|
34
|
+
base64Data = Buffer.from(item.image).toString("base64");
|
|
35
|
+
}
|
|
36
|
+
else if (typeof item.image === "string") {
|
|
37
|
+
// Strip data URI prefix if present (e.g., "data:image/jpeg;base64,")
|
|
38
|
+
base64Data = item.image.replace(/^data:image\/[a-z]+;base64,/, "");
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
throw new Error(`Invalid image data type: expected string, Buffer, or Uint8Array, got ${typeof item.image}`);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
inlineData: {
|
|
45
|
+
mimeType: "image/jpeg",
|
|
46
|
+
data: base64Data,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Invalid content type: ${item.type}`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Configuration for video frame analysis.
|
|
55
|
+
* Generic prompt that handles both general content and technical bug reporting.
|
|
56
|
+
*/
|
|
57
|
+
function buildConfig() {
|
|
58
|
+
return {
|
|
59
|
+
systemInstruction: `You are a Visual Analysis Assistant.
|
|
60
|
+
Your task is to analyze images or video frames provided by the user and extract structured visual features. The user may or may not provide an issue description. Your role is to understand the visual content, optionally correlate it with the provided issue, and produce a structured output that can be directly consumed by another LLM for analysis, debugging, or decision-making.
|
|
61
|
+
|
|
62
|
+
Follow these rules strictly:
|
|
63
|
+
- The analysis must be generic and applicable to any domain (UI, dashboards, video frames, animations, charts, documents, etc.).
|
|
64
|
+
- Support both images and videos (single frame or multiple frames).
|
|
65
|
+
- Extract only what is visually observable; do not assume backend behavior unless supported by visuals.
|
|
66
|
+
- The JSON must be structured, consistent, and machine-readable.
|
|
67
|
+
- Logs are optional and should only be included if explicitly provided.
|
|
68
|
+
- The final output must be clear, concise, and actionable for an LLM.
|
|
69
|
+
|
|
70
|
+
Always produce the output in the following format:
|
|
71
|
+
|
|
72
|
+
Issue:
|
|
73
|
+
<Refined issue description if provided, otherwise a clear description of the observed visual situation>
|
|
74
|
+
|
|
75
|
+
Image/Video Patterns:
|
|
76
|
+
<Structured JSON describing extracted visual features and anomalies>
|
|
77
|
+
|
|
78
|
+
Steps to Reproduce:
|
|
79
|
+
<Ordered steps that reliably reproduce the issue based on the visual context>
|
|
80
|
+
|
|
81
|
+
[Logs: Include ONLY if provided by the user]
|
|
82
|
+
|
|
83
|
+
Proof:
|
|
84
|
+
<Visual evidence explaining how the image/video confirms the issue>
|
|
85
|
+
Ensure the final response is fully self-sufficient and does not reference external context.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Vertex AI
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
export async function analyzeVideoWithVertexAI(frames, options = {}) {
|
|
92
|
+
const startTime = Date.now();
|
|
93
|
+
const { GoogleGenAI } = await import("@google/genai");
|
|
94
|
+
// Get default config and merge with provided options
|
|
95
|
+
const config = await getVertexConfig();
|
|
96
|
+
const project = options.project ?? config.project;
|
|
97
|
+
const location = options.location ?? config.location;
|
|
98
|
+
const model = options.model || DEFAULT_MODEL;
|
|
99
|
+
// Extract content array from CoreMessage
|
|
100
|
+
const contentArray = Array.isArray(frames.content) ? frames.content : [];
|
|
101
|
+
const frameCount = contentArray.filter((item) => item.type === "image").length;
|
|
102
|
+
logger.debug("[GeminiVideoAnalyzer] Analyzing video with Vertex AI", {
|
|
103
|
+
project,
|
|
104
|
+
location,
|
|
105
|
+
model,
|
|
106
|
+
frameCount,
|
|
107
|
+
});
|
|
108
|
+
const ai = new GoogleGenAI({ vertexai: true, project, location });
|
|
109
|
+
// Convert frames content to parts array for Gemini
|
|
110
|
+
const parts = buildContentParts(frames);
|
|
111
|
+
const response = await ai.models.generateContent({
|
|
112
|
+
model,
|
|
113
|
+
config: buildConfig(),
|
|
114
|
+
contents: [
|
|
115
|
+
{
|
|
116
|
+
role: "user",
|
|
117
|
+
parts,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
const responseText = response.text || "";
|
|
122
|
+
const processingTime = Date.now() - startTime;
|
|
123
|
+
logger.debug("[GeminiVideoAnalyzer] Vertex response received", {
|
|
124
|
+
responseLength: responseText.length,
|
|
125
|
+
processingTime,
|
|
126
|
+
});
|
|
127
|
+
return responseText;
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Gemini API (Google AI)
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
export async function analyzeVideoWithGeminiAPI(frames, options = {}) {
|
|
133
|
+
const startTime = Date.now();
|
|
134
|
+
const { GoogleGenAI } = await import("@google/genai");
|
|
135
|
+
const apiKey = options.apiKey || process.env.GOOGLE_AI_API_KEY;
|
|
136
|
+
const model = options.model || DEFAULT_MODEL;
|
|
137
|
+
if (!apiKey) {
|
|
138
|
+
throw new Error("GOOGLE_AI_API_KEY environment variable is required for Gemini API video analysis");
|
|
139
|
+
}
|
|
140
|
+
// Extract content array from CoreMessage
|
|
141
|
+
const contentArray = Array.isArray(frames.content) ? frames.content : [];
|
|
142
|
+
const frameCount = contentArray.filter((item) => item.type === "image").length;
|
|
143
|
+
logger.debug("[GeminiVideoAnalyzer] Analyzing video with Gemini API", {
|
|
144
|
+
model,
|
|
145
|
+
frameCount,
|
|
146
|
+
});
|
|
147
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
148
|
+
// Convert frames content to parts array for Gemini
|
|
149
|
+
const parts = buildContentParts(frames);
|
|
150
|
+
logger.debug("[GeminiVideoAnalyzer] Generating analysis with frames");
|
|
151
|
+
const response = await ai.models.generateContent({
|
|
152
|
+
model,
|
|
153
|
+
config: buildConfig(),
|
|
154
|
+
contents: [
|
|
155
|
+
{
|
|
156
|
+
role: "user",
|
|
157
|
+
parts,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
const responseText = response.text || "";
|
|
162
|
+
const processingTime = Date.now() - startTime;
|
|
163
|
+
logger.debug("[GeminiVideoAnalyzer] Gemini API response received", {
|
|
164
|
+
responseLength: responseText.length,
|
|
165
|
+
processingTime,
|
|
166
|
+
});
|
|
167
|
+
return responseText;
|
|
168
|
+
}
|
|
169
|
+
async function getVertexConfig() {
|
|
170
|
+
const location = process.env.GOOGLE_VERTEX_LOCATION || DEFAULT_LOCATION;
|
|
171
|
+
// Try environment variables first
|
|
172
|
+
let project = process.env.GOOGLE_VERTEX_PROJECT ||
|
|
173
|
+
process.env.GOOGLE_CLOUD_PROJECT ||
|
|
174
|
+
process.env.GOOGLE_CLOUD_PROJECT_ID ||
|
|
175
|
+
process.env.VERTEX_PROJECT_ID;
|
|
176
|
+
// Fallback: read from ADC credentials file
|
|
177
|
+
if (!project && process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
178
|
+
try {
|
|
179
|
+
const credData = JSON.parse(await readFile(process.env.GOOGLE_APPLICATION_CREDENTIALS, "utf-8"));
|
|
180
|
+
project = credData.quota_project_id || credData.project_id;
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
// Ignore read errors, will throw below if project still not found
|
|
184
|
+
logger.debug("Failed to read project from credentials file", {
|
|
185
|
+
error: e instanceof Error ? e.message : String(e),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!project) {
|
|
190
|
+
throw new NeuroLinkError({
|
|
191
|
+
code: "PROVIDER_NOT_CONFIGURED",
|
|
192
|
+
message: "Google Cloud project not found. Set GOOGLE_VERTEX_PROJECT or GOOGLE_CLOUD_PROJECT environment variable, or ensure ADC credentials contain project_id",
|
|
193
|
+
category: ErrorCategory.CONFIGURATION,
|
|
194
|
+
severity: ErrorSeverity.HIGH,
|
|
195
|
+
retriable: false,
|
|
196
|
+
context: {
|
|
197
|
+
missingVar: "GOOGLE_VERTEX_PROJECT",
|
|
198
|
+
feature: "video-generation",
|
|
199
|
+
checkedEnvVars: [
|
|
200
|
+
"GOOGLE_VERTEX_PROJECT",
|
|
201
|
+
"GOOGLE_CLOUD_PROJECT",
|
|
202
|
+
"GOOGLE_CLOUD_PROJECT_ID",
|
|
203
|
+
"VERTEX_PROJECT_ID",
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return { project, location };
|
|
209
|
+
}
|
|
210
|
+
export async function analyzeVideo(frames, options = {}) {
|
|
211
|
+
const provider = options.provider || AIProviderName.AUTO;
|
|
212
|
+
// Vertex — only when GOOGLE_VERTEX_PROJECT is explicitly set
|
|
213
|
+
if (provider === AIProviderName.VERTEX || provider === AIProviderName.AUTO) {
|
|
214
|
+
return analyzeVideoWithVertexAI(frames, options);
|
|
215
|
+
}
|
|
216
|
+
// Gemini API — when GOOGLE_AI_API_KEY is set
|
|
217
|
+
if (provider === AIProviderName.GOOGLE_AI && process.env.GOOGLE_AI_API_KEY) {
|
|
218
|
+
return analyzeVideoWithGeminiAPI(frames, options);
|
|
219
|
+
}
|
|
220
|
+
throw new Error("No valid provider configuration found. " +
|
|
221
|
+
"Set GOOGLE_VERTEX_PROJECT for Vertex AI or GOOGLE_AI_API_KEY for Gemini API.");
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=videoAnalyzer.js.map
|
|
@@ -7,6 +7,7 @@ import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../
|
|
|
7
7
|
import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
|
|
8
8
|
import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
|
|
9
9
|
import { TTSProcessor } from "../utils/ttsProcessor.js";
|
|
10
|
+
import { hasVideoFrames, executeVideoAnalysis, } from "../utils/videoAnalysisProcessor.js";
|
|
10
11
|
import { GenerationHandler } from "./modules/GenerationHandler.js";
|
|
11
12
|
// Import modules for composition
|
|
12
13
|
import { MessageBuilder } from "./modules/MessageBuilder.js";
|
|
@@ -473,6 +474,25 @@ export class BaseProvider {
|
|
|
473
474
|
// ===== Normal AI Generation Flow =====
|
|
474
475
|
const { tools, model } = await this.prepareGenerationContext(options);
|
|
475
476
|
const messages = await this.buildMessages(options);
|
|
477
|
+
// ===== VIDEO ANALYSIS FROM MESSAGES CONTENT =====
|
|
478
|
+
// Check if video files are present in messages content array
|
|
479
|
+
// If video analysis is needed, perform it and return early to avoid running generation
|
|
480
|
+
if (hasVideoFrames(messages)) {
|
|
481
|
+
const videoAnalysisResult = await executeVideoAnalysis(messages, {
|
|
482
|
+
provider: options.provider,
|
|
483
|
+
providerName: this.providerName,
|
|
484
|
+
region: options.region,
|
|
485
|
+
model: options.model,
|
|
486
|
+
});
|
|
487
|
+
// Return video analysis result directly without running generation
|
|
488
|
+
const videoResult = {
|
|
489
|
+
content: videoAnalysisResult,
|
|
490
|
+
provider: options.provider ?? this.providerName,
|
|
491
|
+
model: this.modelName,
|
|
492
|
+
usage: { input: 0, output: 0, total: 0 }, // Video analysis doesn't use standard token counting
|
|
493
|
+
};
|
|
494
|
+
return await this.enhanceResult(videoResult, options, startTime);
|
|
495
|
+
}
|
|
476
496
|
// Compose timeout signal with user-provided abort signal (mirrors stream path)
|
|
477
497
|
const timeoutController = createTimeoutController(options.timeout, this.providerName, "generate");
|
|
478
498
|
const composedSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
|
|
@@ -529,7 +529,7 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
529
529
|
// Extract frames using ffmpeg
|
|
530
530
|
const framesDir = join(tempDir, "frames");
|
|
531
531
|
await fs.mkdir(framesDir, { recursive: true });
|
|
532
|
-
await this.runFfmpegFrameExtraction(videoPath, framesDir, timestamps);
|
|
532
|
+
await this.runFfmpegFrameExtraction(videoPath, framesDir, timestamps, intervalSec);
|
|
533
533
|
// Read extracted frames and resize with sharp
|
|
534
534
|
const keyframes = [];
|
|
535
535
|
for (let i = 0; i < timestamps.length; i++) {
|
|
@@ -563,15 +563,11 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
563
563
|
* @param outputDir - Directory to write frame files
|
|
564
564
|
* @param timestamps - Array of timestamps in seconds
|
|
565
565
|
*/
|
|
566
|
-
runFfmpegFrameExtraction(videoPath, outputDir, timestamps) {
|
|
566
|
+
runFfmpegFrameExtraction(videoPath, outputDir, timestamps, intervalSec) {
|
|
567
567
|
return new Promise((resolve, reject) => {
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
// Build timestamp-based filter
|
|
572
|
-
const selectExpr = timestamps
|
|
573
|
-
.map((t) => `gte(t\\,${t})*lt(t\\,${t + 0.5})`)
|
|
574
|
-
.join("+");
|
|
568
|
+
// Improved select expression to pick exactly one frame per interval
|
|
569
|
+
// instead of multiple frames within a 0.5s window.
|
|
570
|
+
const selectExpr = `isnan(prev_selected_t)+gte(t-prev_selected_t,${intervalSec}-0.001)`;
|
|
575
571
|
const timeoutId = setTimeout(() => {
|
|
576
572
|
reject(new Error(`ffmpeg frame extraction timed out after ${VIDEO_CONFIG.FFMPEG_TIMEOUT_MS}ms`));
|
|
577
573
|
}, VIDEO_CONFIG.FFMPEG_TIMEOUT_MS);
|
|
@@ -861,19 +857,20 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
861
857
|
}
|
|
862
858
|
const clampedCount = Math.min(frameCount, VIDEO_CONFIG.MAX_FRAMES);
|
|
863
859
|
const timestamps = [];
|
|
860
|
+
let interval = duration;
|
|
864
861
|
if (clampedCount === 1) {
|
|
865
862
|
timestamps.push(startSec);
|
|
866
863
|
}
|
|
867
864
|
else {
|
|
868
|
-
|
|
865
|
+
interval = duration / (clampedCount - 1);
|
|
869
866
|
for (let i = 0; i < clampedCount; i++) {
|
|
870
|
-
timestamps.push(startSec +
|
|
867
|
+
timestamps.push(startSec + interval * i);
|
|
871
868
|
}
|
|
872
869
|
}
|
|
873
870
|
// Extract frames
|
|
874
871
|
const framesDir = join(tempDir, "frames");
|
|
875
872
|
await fs.mkdir(framesDir, { recursive: true });
|
|
876
|
-
await this.runFfmpegFrameExtraction(tempVideoPath, framesDir, timestamps);
|
|
873
|
+
await this.runFfmpegFrameExtraction(tempVideoPath, framesDir, timestamps, interval);
|
|
877
874
|
// Read and resize frames
|
|
878
875
|
const keyframes = [];
|
|
879
876
|
for (let i = 0; i < timestamps.length; i++) {
|
|
@@ -521,6 +521,7 @@ export type TextGenerationOptions = {
|
|
|
521
521
|
*/
|
|
522
522
|
images?: Array<Buffer | string | import("./content.js").ImageWithAltText>;
|
|
523
523
|
pdfFiles?: Array<Buffer | string>;
|
|
524
|
+
files?: Array<Buffer | string | import("./fileTypes.js").FileWithMetadata>;
|
|
524
525
|
};
|
|
525
526
|
provider?: AIProviderName;
|
|
526
527
|
model?: string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Analysis Processor
|
|
3
|
+
*
|
|
4
|
+
* Formats video analysis results into human-readable text
|
|
5
|
+
*
|
|
6
|
+
* @module utils/videoAnalysisProcessor
|
|
7
|
+
*/
|
|
8
|
+
import type { CoreMessage } from "ai";
|
|
9
|
+
import { AIProviderName } from "../constants/enums.js";
|
|
10
|
+
/**
|
|
11
|
+
* Check if messages contain video frames (images)
|
|
12
|
+
*
|
|
13
|
+
* @param messages - Array of CoreMessage objects
|
|
14
|
+
* @returns true if video frames are present
|
|
15
|
+
*/
|
|
16
|
+
export declare function hasVideoFrames(messages: CoreMessage[]): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Execute video analysis on messages containing video frames
|
|
19
|
+
*
|
|
20
|
+
* @param messages - Array of CoreMessage objects with video frames
|
|
21
|
+
* @param options - Video analysis options
|
|
22
|
+
* @returns Video analysis text result
|
|
23
|
+
* @throws Error if analysis fails
|
|
24
|
+
*/
|
|
25
|
+
export declare function executeVideoAnalysis(messages: CoreMessage[], options: {
|
|
26
|
+
provider?: AIProviderName | string;
|
|
27
|
+
providerName?: AIProviderName;
|
|
28
|
+
region?: string;
|
|
29
|
+
model?: string;
|
|
30
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Analysis Processor
|
|
3
|
+
*
|
|
4
|
+
* Formats video analysis results into human-readable text
|
|
5
|
+
*
|
|
6
|
+
* @module utils/videoAnalysisProcessor
|
|
7
|
+
*/
|
|
8
|
+
import { AIProviderName } from "../constants/enums.js";
|
|
9
|
+
import { logger } from "./logger.js";
|
|
10
|
+
/**
|
|
11
|
+
* Check if messages contain video frames (images)
|
|
12
|
+
*
|
|
13
|
+
* @param messages - Array of CoreMessage objects
|
|
14
|
+
* @returns true if video frames are present
|
|
15
|
+
*/
|
|
16
|
+
export function hasVideoFrames(messages) {
|
|
17
|
+
return messages.some((msg) => {
|
|
18
|
+
if (Array.isArray(msg.content)) {
|
|
19
|
+
return msg.content.some((part) => typeof part === "object" &&
|
|
20
|
+
part !== null &&
|
|
21
|
+
"type" in part &&
|
|
22
|
+
part.type === "image");
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Execute video analysis on messages containing video frames
|
|
29
|
+
*
|
|
30
|
+
* @param messages - Array of CoreMessage objects with video frames
|
|
31
|
+
* @param options - Video analysis options
|
|
32
|
+
* @returns Video analysis text result
|
|
33
|
+
* @throws Error if analysis fails
|
|
34
|
+
*/
|
|
35
|
+
export async function executeVideoAnalysis(messages, options) {
|
|
36
|
+
logger.debug("[VideoAnalysisProcessor] Video frames detected, triggering analysis");
|
|
37
|
+
const { analyzeVideo } = await import("../adapters/video/videoAnalyzer.js");
|
|
38
|
+
const provider = options.provider === AIProviderName.GOOGLE_AI ||
|
|
39
|
+
(options.provider === AIProviderName.AUTO && process.env.GOOGLE_AI_API_KEY)
|
|
40
|
+
? AIProviderName.GOOGLE_AI
|
|
41
|
+
: options.provider === AIProviderName.VERTEX ||
|
|
42
|
+
options.providerName === AIProviderName.VERTEX
|
|
43
|
+
? AIProviderName.VERTEX
|
|
44
|
+
: AIProviderName.AUTO;
|
|
45
|
+
const videoAnalysisText = await analyzeVideo(messages[0], {
|
|
46
|
+
provider: provider,
|
|
47
|
+
project: options.region
|
|
48
|
+
? undefined
|
|
49
|
+
: process.env.GOOGLE_VERTEX_PROJECT || process.env.GOOGLE_CLOUD_PROJECT,
|
|
50
|
+
location: options.region || process.env.GOOGLE_VERTEX_LOCATION,
|
|
51
|
+
model: options.model || "gemini-2.0-flash",
|
|
52
|
+
});
|
|
53
|
+
logger.debug("[VideoAnalysisProcessor] Video analysis completed", {
|
|
54
|
+
hasResult: !!videoAnalysisText,
|
|
55
|
+
resultLength: videoAnalysisText?.length,
|
|
56
|
+
});
|
|
57
|
+
return videoAnalysisText;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=videoAnalysisProcessor.js.map
|
|
@@ -529,7 +529,7 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
529
529
|
// Extract frames using ffmpeg
|
|
530
530
|
const framesDir = join(tempDir, "frames");
|
|
531
531
|
await fs.mkdir(framesDir, { recursive: true });
|
|
532
|
-
await this.runFfmpegFrameExtraction(videoPath, framesDir, timestamps);
|
|
532
|
+
await this.runFfmpegFrameExtraction(videoPath, framesDir, timestamps, intervalSec);
|
|
533
533
|
// Read extracted frames and resize with sharp
|
|
534
534
|
const keyframes = [];
|
|
535
535
|
for (let i = 0; i < timestamps.length; i++) {
|
|
@@ -563,15 +563,11 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
563
563
|
* @param outputDir - Directory to write frame files
|
|
564
564
|
* @param timestamps - Array of timestamps in seconds
|
|
565
565
|
*/
|
|
566
|
-
runFfmpegFrameExtraction(videoPath, outputDir, timestamps) {
|
|
566
|
+
runFfmpegFrameExtraction(videoPath, outputDir, timestamps, intervalSec) {
|
|
567
567
|
return new Promise((resolve, reject) => {
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
// Build timestamp-based filter
|
|
572
|
-
const selectExpr = timestamps
|
|
573
|
-
.map((t) => `gte(t\\,${t})*lt(t\\,${t + 0.5})`)
|
|
574
|
-
.join("+");
|
|
568
|
+
// Improved select expression to pick exactly one frame per interval
|
|
569
|
+
// instead of multiple frames within a 0.5s window.
|
|
570
|
+
const selectExpr = `isnan(prev_selected_t)+gte(t-prev_selected_t,${intervalSec}-0.001)`;
|
|
575
571
|
const timeoutId = setTimeout(() => {
|
|
576
572
|
reject(new Error(`ffmpeg frame extraction timed out after ${VIDEO_CONFIG.FFMPEG_TIMEOUT_MS}ms`));
|
|
577
573
|
}, VIDEO_CONFIG.FFMPEG_TIMEOUT_MS);
|
|
@@ -861,19 +857,20 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
861
857
|
}
|
|
862
858
|
const clampedCount = Math.min(frameCount, VIDEO_CONFIG.MAX_FRAMES);
|
|
863
859
|
const timestamps = [];
|
|
860
|
+
let interval = duration;
|
|
864
861
|
if (clampedCount === 1) {
|
|
865
862
|
timestamps.push(startSec);
|
|
866
863
|
}
|
|
867
864
|
else {
|
|
868
|
-
|
|
865
|
+
interval = duration / (clampedCount - 1);
|
|
869
866
|
for (let i = 0; i < clampedCount; i++) {
|
|
870
|
-
timestamps.push(startSec +
|
|
867
|
+
timestamps.push(startSec + interval * i);
|
|
871
868
|
}
|
|
872
869
|
}
|
|
873
870
|
// Extract frames
|
|
874
871
|
const framesDir = join(tempDir, "frames");
|
|
875
872
|
await fs.mkdir(framesDir, { recursive: true });
|
|
876
|
-
await this.runFfmpegFrameExtraction(tempVideoPath, framesDir, timestamps);
|
|
873
|
+
await this.runFfmpegFrameExtraction(tempVideoPath, framesDir, timestamps, interval);
|
|
877
874
|
// Read and resize frames
|
|
878
875
|
const keyframes = [];
|
|
879
876
|
for (let i = 0; i < timestamps.length; i++) {
|
|
@@ -521,6 +521,7 @@ export type TextGenerationOptions = {
|
|
|
521
521
|
*/
|
|
522
522
|
images?: Array<Buffer | string | import("./content.js").ImageWithAltText>;
|
|
523
523
|
pdfFiles?: Array<Buffer | string>;
|
|
524
|
+
files?: Array<Buffer | string | import("./fileTypes.js").FileWithMetadata>;
|
|
524
525
|
};
|
|
525
526
|
provider?: AIProviderName;
|
|
526
527
|
model?: string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Analysis Processor
|
|
3
|
+
*
|
|
4
|
+
* Formats video analysis results into human-readable text
|
|
5
|
+
*
|
|
6
|
+
* @module utils/videoAnalysisProcessor
|
|
7
|
+
*/
|
|
8
|
+
import type { CoreMessage } from "ai";
|
|
9
|
+
import { AIProviderName } from "../constants/enums.js";
|
|
10
|
+
/**
|
|
11
|
+
* Check if messages contain video frames (images)
|
|
12
|
+
*
|
|
13
|
+
* @param messages - Array of CoreMessage objects
|
|
14
|
+
* @returns true if video frames are present
|
|
15
|
+
*/
|
|
16
|
+
export declare function hasVideoFrames(messages: CoreMessage[]): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Execute video analysis on messages containing video frames
|
|
19
|
+
*
|
|
20
|
+
* @param messages - Array of CoreMessage objects with video frames
|
|
21
|
+
* @param options - Video analysis options
|
|
22
|
+
* @returns Video analysis text result
|
|
23
|
+
* @throws Error if analysis fails
|
|
24
|
+
*/
|
|
25
|
+
export declare function executeVideoAnalysis(messages: CoreMessage[], options: {
|
|
26
|
+
provider?: AIProviderName | string;
|
|
27
|
+
providerName?: AIProviderName;
|
|
28
|
+
region?: string;
|
|
29
|
+
model?: string;
|
|
30
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Analysis Processor
|
|
3
|
+
*
|
|
4
|
+
* Formats video analysis results into human-readable text
|
|
5
|
+
*
|
|
6
|
+
* @module utils/videoAnalysisProcessor
|
|
7
|
+
*/
|
|
8
|
+
import { AIProviderName } from "../constants/enums.js";
|
|
9
|
+
import { logger } from "./logger.js";
|
|
10
|
+
/**
|
|
11
|
+
* Check if messages contain video frames (images)
|
|
12
|
+
*
|
|
13
|
+
* @param messages - Array of CoreMessage objects
|
|
14
|
+
* @returns true if video frames are present
|
|
15
|
+
*/
|
|
16
|
+
export function hasVideoFrames(messages) {
|
|
17
|
+
return messages.some((msg) => {
|
|
18
|
+
if (Array.isArray(msg.content)) {
|
|
19
|
+
return msg.content.some((part) => typeof part === "object" &&
|
|
20
|
+
part !== null &&
|
|
21
|
+
"type" in part &&
|
|
22
|
+
part.type === "image");
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Execute video analysis on messages containing video frames
|
|
29
|
+
*
|
|
30
|
+
* @param messages - Array of CoreMessage objects with video frames
|
|
31
|
+
* @param options - Video analysis options
|
|
32
|
+
* @returns Video analysis text result
|
|
33
|
+
* @throws Error if analysis fails
|
|
34
|
+
*/
|
|
35
|
+
export async function executeVideoAnalysis(messages, options) {
|
|
36
|
+
logger.debug("[VideoAnalysisProcessor] Video frames detected, triggering analysis");
|
|
37
|
+
const { analyzeVideo } = await import("../adapters/video/videoAnalyzer.js");
|
|
38
|
+
const provider = options.provider === AIProviderName.GOOGLE_AI ||
|
|
39
|
+
(options.provider === AIProviderName.AUTO && process.env.GOOGLE_AI_API_KEY)
|
|
40
|
+
? AIProviderName.GOOGLE_AI
|
|
41
|
+
: options.provider === AIProviderName.VERTEX ||
|
|
42
|
+
options.providerName === AIProviderName.VERTEX
|
|
43
|
+
? AIProviderName.VERTEX
|
|
44
|
+
: AIProviderName.AUTO;
|
|
45
|
+
const videoAnalysisText = await analyzeVideo(messages[0], {
|
|
46
|
+
provider: provider,
|
|
47
|
+
project: options.region
|
|
48
|
+
? undefined
|
|
49
|
+
: process.env.GOOGLE_VERTEX_PROJECT || process.env.GOOGLE_CLOUD_PROJECT,
|
|
50
|
+
location: options.region || process.env.GOOGLE_VERTEX_LOCATION,
|
|
51
|
+
model: options.model || "gemini-2.0-flash",
|
|
52
|
+
});
|
|
53
|
+
logger.debug("[VideoAnalysisProcessor] Video analysis completed", {
|
|
54
|
+
hasResult: !!videoAnalysisText,
|
|
55
|
+
resultLength: videoAnalysisText?.length,
|
|
56
|
+
});
|
|
57
|
+
return videoAnalysisText;
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.9.0",
|
|
4
4
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Juspay Technologies",
|
|
@@ -37,28 +37,28 @@
|
|
|
37
37
|
"prepack": "svelte-kit sync && svelte-package && pnpm run build:cli && publint",
|
|
38
38
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit --strict",
|
|
39
39
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
40
|
-
"modelServer": "
|
|
40
|
+
"modelServer": "tsx scripts/modelServer.ts",
|
|
41
41
|
"lint": "prettier --check . && eslint .",
|
|
42
42
|
"format": "prettier --write .",
|
|
43
43
|
"format:check": "prettier --check .",
|
|
44
|
-
"format:staged": "
|
|
44
|
+
"format:staged": "tsx scripts/format-staged.ts",
|
|
45
45
|
"changeset": "changeset",
|
|
46
46
|
"changeset:version": "changeset version && git add --all",
|
|
47
|
-
"format:changelog": "
|
|
47
|
+
"format:changelog": "tsx scripts/format-changelog.ts",
|
|
48
48
|
"// ===== NEUROLINK DEVELOPER EXPERIENCE ENHANCEMENT 2.0 =====": "",
|
|
49
49
|
"// Environment & Setup (pnpm-first)": "",
|
|
50
|
-
"setup": "pnpm install &&
|
|
50
|
+
"setup": "pnpm install && tsx tools/setup.ts",
|
|
51
51
|
"setup:complete": "pnpm run setup && pnpm run project:organize && pnpm run env:validate",
|
|
52
|
-
"env:setup": "
|
|
53
|
-
"env:validate": "
|
|
54
|
-
"env:backup": "
|
|
55
|
-
"env:list-backups": "
|
|
52
|
+
"env:setup": "tsx tools/automation/environmentManager.ts",
|
|
53
|
+
"env:validate": "tsx tools/automation/environmentManager.ts --validate",
|
|
54
|
+
"env:backup": "tsx tools/automation/environmentManager.ts --backup",
|
|
55
|
+
"env:list-backups": "tsx tools/automation/environmentManager.ts --list-backups",
|
|
56
56
|
"// Project Management & Analysis": "",
|
|
57
|
-
"project:organize": "
|
|
58
|
-
"project:health": "
|
|
57
|
+
"project:organize": "tsx tools/automation/projectOrganizer.ts",
|
|
58
|
+
"project:health": "tsx tools/development/healthMonitor.ts",
|
|
59
59
|
"// Shell Script Conversion": "",
|
|
60
|
-
"convert:shell-scripts": "
|
|
61
|
-
"convert:specific": "
|
|
60
|
+
"convert:shell-scripts": "tsx tools/automation/shellConverter.ts",
|
|
61
|
+
"convert:specific": "tsx tools/automation/shellConverter.ts --specific",
|
|
62
62
|
"// Testing (Enhanced Vitest Framework)": "",
|
|
63
63
|
"test": "vitest run",
|
|
64
64
|
"test:watch": "vitest",
|
|
@@ -71,13 +71,13 @@
|
|
|
71
71
|
"test:e2e": "vitest run test/e2e",
|
|
72
72
|
"test:ci": "vitest run --coverage --reporter=junit --reporter=verbose",
|
|
73
73
|
"test:debug": "vitest --inspect-brk",
|
|
74
|
-
"test:performance": "
|
|
74
|
+
"test:performance": "tsx tools/testing/performanceMonitor.ts",
|
|
75
75
|
"// Legacy Testing Support (during transition)": "",
|
|
76
76
|
"test:legacy": "npx tsx test/continuous-test-suite.ts",
|
|
77
77
|
"test:comparison": "pnpm run test && pnpm run test:legacy",
|
|
78
78
|
"// Content Generation (Cross-platform JS)": "",
|
|
79
|
-
"content:videos": "
|
|
80
|
-
"content:cleanup": "
|
|
79
|
+
"content:videos": "tsx tools/converted-scripts/generateAllVideos.ts",
|
|
80
|
+
"content:cleanup": "tsx tools/converted-scripts/cleanupHashNamedVideos.ts",
|
|
81
81
|
"content:all": "pnpm run content:videos",
|
|
82
82
|
"// Documentation Automation (Legacy MkDocs)": "",
|
|
83
83
|
"docs:api": "typedoc",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"docs:build:mkdocs": "pnpm run docs:api && bash scripts/sync-readme.sh && mkdocs build --strict --clean",
|
|
86
86
|
"docs:serve:mkdocs": "bash scripts/sync-readme.sh && mkdocs serve",
|
|
87
87
|
"docs:gh-deploy:mkdocs": "bash scripts/sync-readme.sh && mkdocs gh-deploy --force",
|
|
88
|
-
"docs:validate": "
|
|
88
|
+
"docs:validate": "tsx tools/content/documentationSync.ts --validate",
|
|
89
89
|
"docs:generate": "pnpm run docs:validate",
|
|
90
90
|
"// Documentation (Docusaurus)": "",
|
|
91
91
|
"docs:start": "pnpm --filter ./docs-site start",
|
|
@@ -93,11 +93,11 @@
|
|
|
93
93
|
"docs:serve": "pnpm --filter ./docs-site serve",
|
|
94
94
|
"docs:clear": "pnpm --filter ./docs-site clear",
|
|
95
95
|
"// Development & Monitoring": "",
|
|
96
|
-
"dev:health": "
|
|
96
|
+
"dev:health": "tsx tools/development/healthMonitor.ts",
|
|
97
97
|
"dev:demo": "concurrently \"pnpm run dev\" \"node neurolink-demo/complete-enhanced-server.js\"",
|
|
98
|
-
"demo:voice": "pnpm build &&
|
|
98
|
+
"demo:voice": "pnpm build && tsx examples/voice-demo/server.ts",
|
|
99
99
|
"// Build & Deploy (Complete Pipeline)": "",
|
|
100
|
-
"build:complete": "
|
|
100
|
+
"build:complete": "tsx tools/automation/buildSystem.ts",
|
|
101
101
|
"// Quality & Maintenance": "",
|
|
102
102
|
"quality:all": "pnpm run lint && pnpm run format && pnpm run test:ci",
|
|
103
103
|
"clean": "pnpm run content:cleanup && rm -rf dist .svelte-kit node_modules/.cache",
|
|
@@ -108,12 +108,12 @@
|
|
|
108
108
|
"test:semantic-release": "node scripts/test-semantic-release.js",
|
|
109
109
|
"release:dry-run": "npx semantic-release --dry-run",
|
|
110
110
|
"// Build Rule Enforcement Scripts": "",
|
|
111
|
-
"validate": "
|
|
112
|
-
"validate:env": "
|
|
113
|
-
"validate:security": "
|
|
111
|
+
"validate": "tsx scripts/build-validations.ts",
|
|
112
|
+
"validate:env": "tsx scripts/env-validation.ts",
|
|
113
|
+
"validate:security": "tsx scripts/security-check.ts",
|
|
114
114
|
"validate:all": "pnpm run validate && pnpm run validate:env && pnpm run validate:security",
|
|
115
|
-
"validate:commit": "
|
|
116
|
-
"quality:metrics": "
|
|
115
|
+
"validate:commit": "tsx scripts/commit-validation.ts",
|
|
116
|
+
"quality:metrics": "tsx scripts/quality-metrics.ts",
|
|
117
117
|
"quality:report": "pnpm run quality:metrics && echo 'Quality metrics saved to quality-metrics.json'",
|
|
118
118
|
"pre-commit": "lint-staged",
|
|
119
119
|
"pre-push": "pnpm run validate:commit && pnpm run validate:env && pnpm run validate && pnpm run test",
|
|
@@ -299,6 +299,7 @@
|
|
|
299
299
|
"svelte": "^5.0.0",
|
|
300
300
|
"svelte-check": "^4.0.0",
|
|
301
301
|
"tslib": "^2.4.1",
|
|
302
|
+
"tsx": "^4.21.0",
|
|
302
303
|
"typedoc": "^0.28.15",
|
|
303
304
|
"typedoc-plugin-markdown": "^4.9.0",
|
|
304
305
|
"typescript": "^5.0.0",
|