@juspay/neurolink 12.29.1 → 12.30.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 +3 -3
- package/dist/browser/neurolink.min.js +376 -374
- package/dist/cli/factories/commandFactory.js +2 -1
- package/dist/core/baseProvider.js +1 -0
- package/dist/core/modules/MessageBuilder.js +8 -0
- package/dist/processors/media/VideoProcessor.d.ts +56 -4
- package/dist/processors/media/VideoProcessor.js +263 -19
- package/dist/providers/catalog/deepseek.json +2 -1
- package/dist/providers/catalog/loader.js +3 -0
- package/dist/providers/catalog/provider-catalog.schema.json +4 -0
- package/dist/providers/catalog/schema.d.ts +1 -0
- package/dist/providers/catalog/schema.js +1 -0
- package/dist/providers/configuredOpenAICompat.d.ts +1 -0
- package/dist/providers/configuredOpenAICompat.js +3 -0
- package/dist/providers/openaiChatCompletionsBase.d.ts +8 -0
- package/dist/providers/openaiChatCompletionsBase.js +16 -2
- package/dist/providers/openaiChatCompletionsClient.d.ts +1 -1
- package/dist/providers/openaiChatCompletionsClient.js +13 -2
- package/dist/types/file.d.ts +20 -2
- package/dist/types/generate.d.ts +20 -26
- package/dist/types/openaiCompatible.d.ts +3 -0
- package/dist/types/processor.d.ts +43 -0
- package/dist/types/providerCatalog.d.ts +5 -0
- package/dist/types/providers.d.ts +3 -0
- package/dist/types/stream.d.ts +11 -11
- package/dist/utils/fileDetector.d.ts +15 -0
- package/dist/utils/fileDetector.js +40 -7
- package/dist/utils/mediaDuration.d.ts +19 -0
- package/dist/utils/mediaDuration.js +23 -0
- package/dist/utils/messageBuilder.js +4 -15
- package/dist/utils/multimodalOptionsBuilder.d.ts +2 -0
- package/dist/utils/multimodalOptionsBuilder.js +7 -0
- package/docs-site/static/search-index.json +6 -6
- package/package.json +3 -2
|
@@ -262,6 +262,16 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
262
262
|
useStreamingWireForGenerate() {
|
|
263
263
|
return false;
|
|
264
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* When true, an assistant turn's reasoning goes back to the vendor as
|
|
267
|
+
* `reasoning_content` on every later request of the conversation. DeepSeek
|
|
268
|
+
* documents this as required once tools are in play and answers 400
|
|
269
|
+
* without it. Default false: strict OpenAI-compatible backends reject a
|
|
270
|
+
* field they don't know. Read per request, never during construction.
|
|
271
|
+
*/
|
|
272
|
+
replayReasoningContent() {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
265
275
|
/**
|
|
266
276
|
* Hook to adjust the fully-built wire request body before it is sent, on
|
|
267
277
|
* both the streaming and non-streaming paths. Default identity. Override for
|
|
@@ -542,6 +552,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
542
552
|
const resolveWireMaxTokens = this.resolveWireMaxTokens.bind(this);
|
|
543
553
|
const suppressResponseFormatWithTools = this.suppressResponseFormatWithTools.bind(this);
|
|
544
554
|
const useStreamingWireForGenerate = this.useStreamingWireForGenerate.bind(this);
|
|
555
|
+
const replayReasoningContent = this.replayReasoningContent.bind(this);
|
|
545
556
|
const getTimeoutForOptions = (opts) => this.getTimeout((opts ?? {}));
|
|
546
557
|
return {
|
|
547
558
|
specificationVersion: "v3",
|
|
@@ -555,7 +566,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
555
566
|
const wireNameMaps = buildWireToolNameMaps((options.tools ?? [])
|
|
556
567
|
.filter((t) => t.type === "function")
|
|
557
568
|
.map((t) => t.name));
|
|
558
|
-
const baseMessages = messageBuilderToOpenAI(options.prompt, wireNameMaps?.toWire);
|
|
569
|
+
const baseMessages = messageBuilderToOpenAI(options.prompt, wireNameMaps?.toWire, replayReasoningContent());
|
|
559
570
|
const hasTools = Array.isArray(options.tools) && options.tools.length > 0;
|
|
560
571
|
const responseFormat = options.responseFormat &&
|
|
561
572
|
!(hasTools && suppressResponseFormatWithTools())
|
|
@@ -1657,7 +1668,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1657
1668
|
const transformedPrompt = Array.isArray(params?.prompt)
|
|
1658
1669
|
? params.prompt
|
|
1659
1670
|
: promptMessages;
|
|
1660
|
-
const conversation = messageBuilderToOpenAI(transformedPrompt, wireNameMaps?.toWire);
|
|
1671
|
+
const conversation = messageBuilderToOpenAI(transformedPrompt, wireNameMaps?.toWire, this.replayReasoningContent());
|
|
1661
1672
|
loopConversation = conversation;
|
|
1662
1673
|
const sampled = {
|
|
1663
1674
|
...options,
|
|
@@ -2171,6 +2182,9 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
2171
2182
|
role: "assistant",
|
|
2172
2183
|
content: stepResult.text.length > 0 ? stepResult.text : null,
|
|
2173
2184
|
tool_calls: toolCallsForMessage,
|
|
2185
|
+
...(this.replayReasoningContent() && stepResult.reasoning
|
|
2186
|
+
? { reasoning_content: stepResult.reasoning }
|
|
2187
|
+
: {}),
|
|
2174
2188
|
});
|
|
2175
2189
|
for (const [, t] of stepResult.toolCalls) {
|
|
2176
2190
|
const startedAt = new Date();
|
|
@@ -48,7 +48,7 @@ export declare const stringifyToolInput: (input: unknown) => string;
|
|
|
48
48
|
export declare const stringifyToolOutput: (output: unknown) => string;
|
|
49
49
|
export declare const imageDataToURL: (data: unknown, mediaType?: string) => string | undefined;
|
|
50
50
|
export declare const convertContentForOpenAI: (content: unknown) => string | OpenAICompatMessageContent[];
|
|
51
|
-
export declare const messageBuilderToOpenAI: (messages: ReadonlyArray<OpenAICompatMessage>, toolNameToWire?: Map<string, string
|
|
51
|
+
export declare const messageBuilderToOpenAI: (messages: ReadonlyArray<OpenAICompatMessage>, toolNameToWire?: Map<string, string>, replayReasoning?: boolean) => OpenAICompatChatMessage[];
|
|
52
52
|
export declare const buildToolsForOpenAI: (tools: Record<string, Tool>, toolNameToWire?: Map<string, string>) => OpenAICompatChatTool[] | undefined;
|
|
53
53
|
export declare const v3ToolsToOpenAI: (tools: OpenAICompatV3CallTools | undefined, toolNameToWire?: Map<string, string>) => OpenAICompatChatTool[] | undefined;
|
|
54
54
|
export declare const v3ToolChoiceToOpenAI: (choice: OpenAICompatV3CallToolChoice, toolNameToWire?: Map<string, string>) => OpenAICompatToolChoiceWire | undefined;
|
|
@@ -96,6 +96,11 @@ export const estimateWireTokens = (messages, tools, provider) => {
|
|
|
96
96
|
if (toolCalls) {
|
|
97
97
|
total += estimateTokens(safeStringify(toolCalls), provider);
|
|
98
98
|
}
|
|
99
|
+
const reasoning = message
|
|
100
|
+
.reasoning_content;
|
|
101
|
+
if (reasoning) {
|
|
102
|
+
total += estimateTokens(reasoning, provider);
|
|
103
|
+
}
|
|
99
104
|
}
|
|
100
105
|
if (tools && tools.length > 0) {
|
|
101
106
|
total += estimateTokens(safeStringify(tools), provider);
|
|
@@ -242,7 +247,7 @@ export const convertContentForOpenAI = (content) => {
|
|
|
242
247
|
}
|
|
243
248
|
return out;
|
|
244
249
|
};
|
|
245
|
-
export const messageBuilderToOpenAI = (messages, toolNameToWire) => {
|
|
250
|
+
export const messageBuilderToOpenAI = (messages, toolNameToWire, replayReasoning = false) => {
|
|
246
251
|
const out = [];
|
|
247
252
|
for (const msg of messages) {
|
|
248
253
|
switch (msg.role) {
|
|
@@ -264,10 +269,14 @@ export const messageBuilderToOpenAI = (messages, toolNameToWire) => {
|
|
|
264
269
|
const parts = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
265
270
|
const text = [];
|
|
266
271
|
const toolCalls = [];
|
|
272
|
+
const reasoning = [];
|
|
267
273
|
for (const part of parts) {
|
|
268
274
|
if (part && typeof part === "object") {
|
|
269
275
|
const p = part;
|
|
270
|
-
if (p.type === "
|
|
276
|
+
if (p.type === "reasoning") {
|
|
277
|
+
reasoning.push(part.text ?? "");
|
|
278
|
+
}
|
|
279
|
+
else if (p.type === "text") {
|
|
271
280
|
text.push({
|
|
272
281
|
type: "text",
|
|
273
282
|
text: part.text ?? "",
|
|
@@ -297,10 +306,12 @@ export const messageBuilderToOpenAI = (messages, toolNameToWire) => {
|
|
|
297
306
|
: text.length === 1 && text[0].type === "text"
|
|
298
307
|
? text[0].text
|
|
299
308
|
: text;
|
|
309
|
+
const reasoningContent = replayReasoning ? reasoning.join("") : "";
|
|
300
310
|
out.push({
|
|
301
311
|
role: "assistant",
|
|
302
312
|
content: flat,
|
|
303
313
|
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
314
|
+
...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
|
|
304
315
|
});
|
|
305
316
|
break;
|
|
306
317
|
}
|
package/dist/types/file.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File detection and processing types for unified file handling
|
|
3
3
|
*/
|
|
4
|
+
import type { ImageWithAltText } from "./multimodal.js";
|
|
4
5
|
/**
|
|
5
6
|
* Supported file types for multimodal input
|
|
6
7
|
*/
|
|
@@ -276,8 +277,15 @@ export type FileProcessingResult = {
|
|
|
276
277
|
type: FileType;
|
|
277
278
|
content: string | Buffer;
|
|
278
279
|
mimeType: string;
|
|
279
|
-
/**
|
|
280
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Additional images extracted from the file (e.g., video keyframes, audio
|
|
282
|
+
* cover art).
|
|
283
|
+
*
|
|
284
|
+
* An entry may carry alt text, which is how a video keyframe states the
|
|
285
|
+
* timestamp it was sampled at. Bare bytes still work — the field was
|
|
286
|
+
* widened, not changed.
|
|
287
|
+
*/
|
|
288
|
+
images?: Array<Buffer | string | ImageWithAltText>;
|
|
281
289
|
metadata: {
|
|
282
290
|
confidence: number;
|
|
283
291
|
size?: number;
|
|
@@ -549,6 +557,16 @@ export type VideoProcessorOptions = {
|
|
|
549
557
|
quality?: number;
|
|
550
558
|
/** Frame encoding. Defaults to jpeg. */
|
|
551
559
|
format?: "jpeg" | "png";
|
|
560
|
+
/**
|
|
561
|
+
* Transcribe the clip's spoken audio (#433). Off by default: it costs an
|
|
562
|
+
* ffmpeg pass plus a Whisper call, and most attached video is silent
|
|
563
|
+
* screen capture.
|
|
564
|
+
*
|
|
565
|
+
* Best-effort — a clip with no audio track, no `OPENAI_API_KEY`, or a
|
|
566
|
+
* failed call still processes, and the reason lands in
|
|
567
|
+
* `ProcessedVideo.transcriptionSkippedReason`.
|
|
568
|
+
*/
|
|
569
|
+
transcribeAudio?: boolean;
|
|
552
570
|
};
|
|
553
571
|
/**
|
|
554
572
|
* Office processor options for Word, PowerPoint, and Excel documents
|
package/dist/types/generate.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { AvatarOptions, AvatarResult } from "./avatar.js";
|
|
|
18
18
|
import type { MusicOptions, MusicResult } from "./music.js";
|
|
19
19
|
import type { StandardRecord, ValidationSchema, ZodUnknownSchema } from "./aliases.js";
|
|
20
20
|
import type { NeurolinkCredentials } from "./providers.js";
|
|
21
|
-
import type { CSVProcessorOptions, FileWithMetadata, MultimodalAudioEntry, MultimodalVideoEntry, VisionImageOutputFormat } from "./file.js";
|
|
21
|
+
import type { CSVProcessorOptions, VideoProcessorOptions, FileWithMetadata, MultimodalAudioEntry, MultimodalVideoEntry, VisionImageOutputFormat } from "./file.js";
|
|
22
22
|
import type { WorkflowConfig } from "./workflow.js";
|
|
23
23
|
import type { Schema, Tool, ToolChoice } from "./tools.js";
|
|
24
24
|
import type { StepResult, LanguageModel } from "./providers.js";
|
|
@@ -180,16 +180,15 @@ export type GenerateOptions = {
|
|
|
180
180
|
*/
|
|
181
181
|
outputFormat?: VisionImageOutputFormat;
|
|
182
182
|
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
};
|
|
183
|
+
/**
|
|
184
|
+
* Video processing options — keyframe budget, encoder settings, and
|
|
185
|
+
* whether to transcribe the clip's spoken audio.
|
|
186
|
+
*
|
|
187
|
+
* The canonical shape, rather than a third structurally-identical copy of
|
|
188
|
+
* it: `StreamOptions` and `TextGenerationOptions` declare the same field,
|
|
189
|
+
* and the inline copies had already drifted apart in what they documented.
|
|
190
|
+
*/
|
|
191
|
+
videoOptions?: VideoProcessorOptions;
|
|
193
192
|
/**
|
|
194
193
|
* Text-to-Speech (TTS) configuration
|
|
195
194
|
*
|
|
@@ -1428,6 +1427,16 @@ export type TextGenerationOptions = {
|
|
|
1428
1427
|
expectedOutcome?: string;
|
|
1429
1428
|
evaluationCriteria?: string[];
|
|
1430
1429
|
csvOptions?: CSVProcessorOptions;
|
|
1430
|
+
/**
|
|
1431
|
+
* Video processing options (#478, #433).
|
|
1432
|
+
*
|
|
1433
|
+
* Reconstructed option objects have to carry this the same way they carry
|
|
1434
|
+
* `csvOptions` and `pdfOptions`: the message builder hands it to the
|
|
1435
|
+
* detector, which hands it to `VideoProcessor`, and anything that rebuilds
|
|
1436
|
+
* the options along the way and omits it restores the defaults without
|
|
1437
|
+
* saying so.
|
|
1438
|
+
*/
|
|
1439
|
+
videoOptions?: VideoProcessorOptions;
|
|
1431
1440
|
/** PDF processing options (#258). */
|
|
1432
1441
|
pdfOptions?: {
|
|
1433
1442
|
/** Password for an encrypted PDF (image-conversion fallback path). */
|
|
@@ -1453,21 +1462,6 @@ export type TextGenerationOptions = {
|
|
|
1453
1462
|
*/
|
|
1454
1463
|
outputFormat?: VisionImageOutputFormat;
|
|
1455
1464
|
};
|
|
1456
|
-
/**
|
|
1457
|
-
* Video processing options (mirrors `GenerateOptions.videoOptions`). Never
|
|
1458
|
-
* declared here before, so `buildGenerateTextOptions` had nowhere to
|
|
1459
|
-
* forward the caller's setting even once the allowlist itself named it.
|
|
1460
|
-
*/
|
|
1461
|
-
videoOptions?: {
|
|
1462
|
-
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
1463
|
-
frames?: number;
|
|
1464
|
-
/** Frame encoder quality, clamped to 1-100. Default 80. */
|
|
1465
|
-
quality?: number;
|
|
1466
|
-
/** Frame encoding. Default jpeg. */
|
|
1467
|
-
format?: "jpeg" | "png";
|
|
1468
|
-
/** Not implemented yet (#433) — warns rather than silently doing nothing. */
|
|
1469
|
-
transcribeAudio?: boolean;
|
|
1470
|
-
};
|
|
1471
1465
|
enableSummarization?: boolean;
|
|
1472
1466
|
/**
|
|
1473
1467
|
* Skip injecting tool schemas into the system prompt.
|
|
@@ -34,6 +34,9 @@ export type OpenAICompatChatMessage = {
|
|
|
34
34
|
role: "assistant";
|
|
35
35
|
content?: string | OpenAICompatMessageContent[] | null;
|
|
36
36
|
tool_calls?: OpenAICompatToolCallWire[];
|
|
37
|
+
/** The turn's reasoning, sent back only where the catalog opts in
|
|
38
|
+
* (DeepSeek's replayReasoningContent quirk). */
|
|
39
|
+
reasoning_content?: string;
|
|
37
40
|
} | {
|
|
38
41
|
role: "tool";
|
|
39
42
|
content: string;
|
|
@@ -711,6 +711,20 @@ export type ProcessedAudio = ProcessedFileBase & {
|
|
|
711
711
|
transcriptionSkippedReason?: string;
|
|
712
712
|
coverArt?: Buffer;
|
|
713
713
|
};
|
|
714
|
+
/**
|
|
715
|
+
* One extracted keyframe and the moment it came from.
|
|
716
|
+
*
|
|
717
|
+
* Internal to extraction: `ProcessedVideo` splits this back into the parallel
|
|
718
|
+
* `keyframes` / `keyframeTimestampsSec` arrays its existing consumers expect.
|
|
719
|
+
* Keeping the pair together while frames are being read and encoded is what
|
|
720
|
+
* makes a dropped frame impossible to mislabel — the alternative is
|
|
721
|
+
* reconstructing timestamps from an interval after the fact, which is wrong
|
|
722
|
+
* for every frame following a failed encode.
|
|
723
|
+
*/
|
|
724
|
+
export type VideoKeyframe = {
|
|
725
|
+
readonly buffer: Buffer;
|
|
726
|
+
readonly timestampSec: number;
|
|
727
|
+
};
|
|
714
728
|
/**
|
|
715
729
|
* Processed video result.
|
|
716
730
|
* Extends ProcessedFileBase with video-specific fields including metadata,
|
|
@@ -720,6 +734,19 @@ export type ProcessedAudio = ProcessedFileBase & {
|
|
|
720
734
|
export type ProcessedVideo = ProcessedFileBase & {
|
|
721
735
|
textContent: string;
|
|
722
736
|
keyframes: Buffer[];
|
|
737
|
+
/**
|
|
738
|
+
* When each entry of `keyframes` was sampled, in seconds from the start of
|
|
739
|
+
* the clip. Same length, same order, ascending.
|
|
740
|
+
*
|
|
741
|
+
* A parallel array rather than a richer `keyframes` element type: that
|
|
742
|
+
* field is read by `fileReferenceRegistry` and by the detector as a plain
|
|
743
|
+
* `Buffer[]`, and reshaping it would break them for a value they do not
|
|
744
|
+
* need. Extraction can also drop an individual frame when its encode
|
|
745
|
+
* fails, so the timestamps are recorded as frames are kept — never
|
|
746
|
+
* reconstructed from an interval, which would silently mislabel every
|
|
747
|
+
* frame after a dropped one.
|
|
748
|
+
*/
|
|
749
|
+
keyframeTimestampsSec: number[];
|
|
723
750
|
metadata: {
|
|
724
751
|
duration: number;
|
|
725
752
|
durationFormatted: string;
|
|
@@ -735,6 +762,22 @@ export type ProcessedVideo = ProcessedFileBase & {
|
|
|
735
762
|
fileSize: number;
|
|
736
763
|
};
|
|
737
764
|
subtitleText?: string;
|
|
765
|
+
/**
|
|
766
|
+
* Speech transcribed from the clip's audio track, when
|
|
767
|
+
* `VideoProcessorOptions.transcribeAudio` asked for it and it worked.
|
|
768
|
+
*
|
|
769
|
+
* Distinct from `subtitleText`, which is an embedded subtitle stream the
|
|
770
|
+
* file already carried. A recording can have one, both or neither.
|
|
771
|
+
*/
|
|
772
|
+
transcript?: string;
|
|
773
|
+
hasTranscript: boolean;
|
|
774
|
+
/**
|
|
775
|
+
* Why no transcript was produced. Follows `ProcessedAudio`'s precedent:
|
|
776
|
+
* "nobody asked", "there is no audio track", "no API key", "the call
|
|
777
|
+
* failed" and "nobody was speaking" are five different situations that
|
|
778
|
+
* otherwise all present as an absent transcript.
|
|
779
|
+
*/
|
|
780
|
+
transcriptionSkippedReason?: string;
|
|
738
781
|
hasKeyframes: boolean;
|
|
739
782
|
frameCount: number;
|
|
740
783
|
};
|
|
@@ -73,6 +73,11 @@ export type CatalogQuirks = {
|
|
|
73
73
|
* ConfiguredOpenAICompatProvider so `generate({ schema })` keeps working
|
|
74
74
|
* (mirrors the pre-catalog `supportsStructuredOutputs: false` behavior). */
|
|
75
75
|
responseFormatDowngrade?: "json-schema-to-json-object";
|
|
76
|
+
/** Vendor wants each assistant turn's `reasoning_content` sent back on
|
|
77
|
+
* every later request of the conversation (DeepSeek documents a 400
|
|
78
|
+
* without it once tools are in play). ConfiguredOpenAICompatProvider
|
|
79
|
+
* turns on the replay; every other provider leaves the field off. */
|
|
80
|
+
replayReasoningContent?: boolean;
|
|
76
81
|
};
|
|
77
82
|
export type CatalogBillingPolicy = "free-tier" | "free-with-card" | "no-free-tier";
|
|
78
83
|
export type CatalogSetup = {
|
|
@@ -798,6 +798,9 @@ export type OpenAICompatCatalogEntry = {
|
|
|
798
798
|
* `response_format: { type: "json_schema" }` but accepts
|
|
799
799
|
* `{ type: "json_object" }`. */
|
|
800
800
|
responseFormatDowngrade?: "json-schema-to-json-object";
|
|
801
|
+
/** See CatalogQuirks.replayReasoningContent — a vendor that wants each
|
|
802
|
+
* assistant turn's `reasoning_content` sent back on later requests. */
|
|
803
|
+
replayReasoningContent?: boolean;
|
|
801
804
|
};
|
|
802
805
|
/** The subset of OpenAICompatCatalogEntry that resolveOpenAICompatConfig()
|
|
803
806
|
* needs — lets call sites pass a minimal object without the full catalog
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { AIModelProviderConfig, NeurolinkCredentials } from "./providers.js
|
|
|
16
16
|
import type { TTSChunk, TTSOptions, TTSResult } from "./tts.js";
|
|
17
17
|
import type { STTOptions, STTResult } from "./stt.js";
|
|
18
18
|
import type { StandardRecord, ValidationSchema } from "./aliases.js";
|
|
19
|
-
import type { CSVProcessorOptions, FileWithMetadata, VisionImageOutputFormat } from "./file.js";
|
|
19
|
+
import type { CSVProcessorOptions, FileWithMetadata, VideoProcessorOptions, VisionImageOutputFormat } from "./file.js";
|
|
20
20
|
import type { WorkflowConfig } from "./workflow.js";
|
|
21
21
|
import type { LanguageModel, StepResult } from "./providers.js";
|
|
22
22
|
import type { Tool, ToolChoice } from "./tools.js";
|
|
@@ -356,6 +356,16 @@ export type StreamOptions = {
|
|
|
356
356
|
};
|
|
357
357
|
};
|
|
358
358
|
csvOptions?: CSVProcessorOptions;
|
|
359
|
+
/**
|
|
360
|
+
* Video processing options (#478, #433).
|
|
361
|
+
*
|
|
362
|
+
* Reconstructed option objects have to carry this the same way they carry
|
|
363
|
+
* `csvOptions` and `pdfOptions`: the message builder hands it to the
|
|
364
|
+
* detector, which hands it to `VideoProcessor`, and anything that rebuilds
|
|
365
|
+
* the options along the way and omits it restores the defaults without
|
|
366
|
+
* saying so.
|
|
367
|
+
*/
|
|
368
|
+
videoOptions?: VideoProcessorOptions;
|
|
359
369
|
/** PDF processing options (#258). */
|
|
360
370
|
pdfOptions?: {
|
|
361
371
|
/** Password for an encrypted PDF (image-conversion fallback path). */
|
|
@@ -386,16 +396,6 @@ export type StreamOptions = {
|
|
|
386
396
|
*/
|
|
387
397
|
outputFormat?: VisionImageOutputFormat;
|
|
388
398
|
};
|
|
389
|
-
videoOptions?: {
|
|
390
|
-
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
391
|
-
frames?: number;
|
|
392
|
-
/** Frame encoder quality, clamped to 1-100. Default 80. */
|
|
393
|
-
quality?: number;
|
|
394
|
-
/** Frame encoding. Default jpeg. */
|
|
395
|
-
format?: "jpeg" | "png";
|
|
396
|
-
/** Not implemented yet (#433) — warns rather than silently doing nothing. */
|
|
397
|
-
transcribeAudio?: boolean;
|
|
398
|
-
};
|
|
399
399
|
/**
|
|
400
400
|
* Text-to-Speech (TTS) configuration for streaming
|
|
401
401
|
*
|
|
@@ -114,6 +114,21 @@ export declare class FileDetector {
|
|
|
114
114
|
/**
|
|
115
115
|
* SDK-8: Format an informative placeholder when a file processor fails.
|
|
116
116
|
* Instead of bare "[Video file: name]" strings, include size, format, and
|
|
117
|
+
* Describe one keyframe well enough for a model to place it in time.
|
|
118
|
+
*
|
|
119
|
+
* The clip's total length rides along because the eighth frame of a
|
|
120
|
+
* recording means nothing without knowing how long the recording is.
|
|
121
|
+
*
|
|
122
|
+
* Explicit units rather than a clock — see `formatKeyframeTimestamp`. An
|
|
123
|
+
* earlier draft used "0:03" here and the model reported the frame as
|
|
124
|
+
* appearing at "3:00".
|
|
125
|
+
*
|
|
126
|
+
* A timestamp can be missing if extraction and its schedule ever disagree
|
|
127
|
+
* in length. That should not happen, but an unlabelled frame is better
|
|
128
|
+
* than one labelled "NaN".
|
|
129
|
+
*/
|
|
130
|
+
private static describeKeyframe;
|
|
131
|
+
/**
|
|
117
132
|
* the reason for failure so the LLM can acknowledge the attachment.
|
|
118
133
|
*/
|
|
119
134
|
private static formatInformativePlaceholder;
|
|
@@ -37,6 +37,7 @@ import { withTimeout } from "./errorHandling.js";
|
|
|
37
37
|
import { normalizeUrlForCache, redactUrlForError, sanitizeErrorCause, } from "./logSanitize.js";
|
|
38
38
|
import { mimeHintToExtension, mimeHintToFileType, normalizeMimeHint, } from "./mimeTypeHints.js";
|
|
39
39
|
import { PDFProcessor } from "./pdfProcessor.js";
|
|
40
|
+
import { formatKeyframeTimestamp } from "./mediaDuration.js";
|
|
40
41
|
/**
|
|
41
42
|
* Short-TTL cache of URL → Content-Type (#323). A URL is commonly detected more
|
|
42
43
|
* than once (repeated multimodal prompts reuse the same asset URL); caching the
|
|
@@ -797,6 +798,26 @@ export class FileDetector {
|
|
|
797
798
|
/**
|
|
798
799
|
* SDK-8: Format an informative placeholder when a file processor fails.
|
|
799
800
|
* Instead of bare "[Video file: name]" strings, include size, format, and
|
|
801
|
+
* Describe one keyframe well enough for a model to place it in time.
|
|
802
|
+
*
|
|
803
|
+
* The clip's total length rides along because the eighth frame of a
|
|
804
|
+
* recording means nothing without knowing how long the recording is.
|
|
805
|
+
*
|
|
806
|
+
* Explicit units rather than a clock — see `formatKeyframeTimestamp`. An
|
|
807
|
+
* earlier draft used "0:03" here and the model reported the frame as
|
|
808
|
+
* appearing at "3:00".
|
|
809
|
+
*
|
|
810
|
+
* A timestamp can be missing if extraction and its schedule ever disagree
|
|
811
|
+
* in length. That should not happen, but an unlabelled frame is better
|
|
812
|
+
* than one labelled "NaN".
|
|
813
|
+
*/
|
|
814
|
+
static describeKeyframe(timestampSec, durationFormatted) {
|
|
815
|
+
if (typeof timestampSec !== "number" || !Number.isFinite(timestampSec)) {
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
return `Video keyframe at ${formatKeyframeTimestamp(timestampSec)} into a ${durationFormatted} video`;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
800
821
|
* the reason for failure so the LLM can acknowledge the attachment.
|
|
801
822
|
*/
|
|
802
823
|
static formatInformativePlaceholder(typeName, filename, content, detection, error) {
|
|
@@ -1120,25 +1141,37 @@ export class FileDetector {
|
|
|
1120
1141
|
// the processor; previously these stopped at the CLI layer.
|
|
1121
1142
|
videoOptions);
|
|
1122
1143
|
if (videoResult.success && videoResult.data) {
|
|
1144
|
+
// Bound to a local because the keyframe map below is a closure, and
|
|
1145
|
+
// TypeScript drops the `videoResult.data` narrowing across one.
|
|
1146
|
+
const video = videoResult.data;
|
|
1123
1147
|
return {
|
|
1124
1148
|
type: "video",
|
|
1125
|
-
content:
|
|
1149
|
+
content: video.textContent ||
|
|
1126
1150
|
FileDetector.formatInformativePlaceholder("Video", videoFilename, content, detection),
|
|
1127
1151
|
mimeType: detection.mimeType,
|
|
1128
|
-
|
|
1129
|
-
|
|
1152
|
+
// Each keyframe carries the moment it was sampled at. Alt text is
|
|
1153
|
+
// the only per-image channel that survives to the model — the
|
|
1154
|
+
// message builder folds it into the prompt as
|
|
1155
|
+
// "[Image N: <alt>]" — so without it the frames arrive as an
|
|
1156
|
+
// unlabelled pile and "what happens at 0:30?" has no answer even
|
|
1157
|
+
// though the frame at 0:30 is right there.
|
|
1158
|
+
images: video.keyframes && video.keyframes.length > 0
|
|
1159
|
+
? video.keyframes.map((frame, index) => ({
|
|
1160
|
+
data: frame,
|
|
1161
|
+
altText: FileDetector.describeKeyframe(video.keyframeTimestampsSec[index], video.metadata.durationFormatted),
|
|
1162
|
+
}))
|
|
1130
1163
|
: undefined,
|
|
1131
1164
|
metadata: {
|
|
1132
1165
|
...detection.metadata,
|
|
1133
|
-
frameCount:
|
|
1134
|
-
hasKeyframes:
|
|
1166
|
+
frameCount: video.frameCount,
|
|
1167
|
+
hasKeyframes: video.hasKeyframes,
|
|
1135
1168
|
// Carried so native delivery can apply its duration ceiling
|
|
1136
1169
|
// without opening the container a second time. The processor
|
|
1137
1170
|
// reports 0 when nothing could probe it; that is "unknown", not a
|
|
1138
1171
|
// zero-length clip, so it is left off rather than passed through
|
|
1139
1172
|
// as a number a ceiling check would happily accept.
|
|
1140
|
-
...(
|
|
1141
|
-
? { durationSec:
|
|
1173
|
+
...(video.metadata.duration > 0
|
|
1174
|
+
? { durationSec: video.metadata.duration }
|
|
1142
1175
|
: {}),
|
|
1143
1176
|
},
|
|
1144
1177
|
};
|
|
@@ -21,3 +21,22 @@
|
|
|
21
21
|
* fabricated number would be worse than an obvious zero.
|
|
22
22
|
*/
|
|
23
23
|
export declare function formatMediaDuration(seconds: number): string;
|
|
24
|
+
/**
|
|
25
|
+
* Format the moment a video keyframe was sampled at — "3.00s",
|
|
26
|
+
* "1234.00s (20m 34s)".
|
|
27
|
+
*
|
|
28
|
+
* Deliberately not the clock form, for the reason in this module's header:
|
|
29
|
+
* the string is read by a language model, and "0:03" is ambiguous. That is
|
|
30
|
+
* not theoretical here — an early draft labelled frames "0:03" and the model
|
|
31
|
+
* reported the green frame as appearing at "3:00".
|
|
32
|
+
*
|
|
33
|
+
* Deliberately not `formatMediaDuration` alone either. That rounds to whole
|
|
34
|
+
* seconds, and an explicit `videoOptions.frames` spreads frames evenly rather
|
|
35
|
+
* than on the duration tier: sixteen frames across a 3.6s clip are 0.225s
|
|
36
|
+
* apart, and every one of them would render as "0s". The precise value is
|
|
37
|
+
* therefore always present, with the readable form added only past a minute,
|
|
38
|
+
* where "1234.00s" stops meaning anything to a reader.
|
|
39
|
+
*
|
|
40
|
+
* Non-finite and negative inputs clamp to zero rather than emitting "NaNs".
|
|
41
|
+
*/
|
|
42
|
+
export declare function formatKeyframeTimestamp(seconds: number): string;
|
|
@@ -42,3 +42,26 @@ export function formatMediaDuration(seconds) {
|
|
|
42
42
|
}
|
|
43
43
|
return parts.join(" ");
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Format the moment a video keyframe was sampled at — "3.00s",
|
|
47
|
+
* "1234.00s (20m 34s)".
|
|
48
|
+
*
|
|
49
|
+
* Deliberately not the clock form, for the reason in this module's header:
|
|
50
|
+
* the string is read by a language model, and "0:03" is ambiguous. That is
|
|
51
|
+
* not theoretical here — an early draft labelled frames "0:03" and the model
|
|
52
|
+
* reported the green frame as appearing at "3:00".
|
|
53
|
+
*
|
|
54
|
+
* Deliberately not `formatMediaDuration` alone either. That rounds to whole
|
|
55
|
+
* seconds, and an explicit `videoOptions.frames` spreads frames evenly rather
|
|
56
|
+
* than on the duration tier: sixteen frames across a 3.6s clip are 0.225s
|
|
57
|
+
* apart, and every one of them would render as "0s". The precise value is
|
|
58
|
+
* therefore always present, with the readable form added only past a minute,
|
|
59
|
+
* where "1234.00s" stops meaning anything to a reader.
|
|
60
|
+
*
|
|
61
|
+
* Non-finite and negative inputs clamp to zero rather than emitting "NaNs".
|
|
62
|
+
*/
|
|
63
|
+
export function formatKeyframeTimestamp(seconds) {
|
|
64
|
+
const safe = Number.isFinite(seconds) && seconds > 0 ? seconds : 0;
|
|
65
|
+
const precise = `${safe.toFixed(2)}s`;
|
|
66
|
+
return safe >= 60 ? `${precise} (${formatMediaDuration(safe)})` : precise;
|
|
67
|
+
}
|
|
@@ -832,20 +832,6 @@ export function mergeMediaFileAliases(input) {
|
|
|
832
832
|
input.audioFiles = undefined;
|
|
833
833
|
input.videoFiles = undefined;
|
|
834
834
|
}
|
|
835
|
-
/**
|
|
836
|
-
* #478: `transcribeAudio` (CLI `--transcribe-audio`) is accepted by the options
|
|
837
|
-
* surface but no video-audio transcription exists yet — VideoProcessor extracts
|
|
838
|
-
* keyframes and embedded subtitle tracks only, and the transcription step is
|
|
839
|
-
* still open as #433. Say so once per request rather than letting the caller
|
|
840
|
-
* believe a transcript was produced and silently omitted.
|
|
841
|
-
*/
|
|
842
|
-
function warnIfVideoTranscriptionRequested(videoOptions) {
|
|
843
|
-
if (videoOptions?.transcribeAudio) {
|
|
844
|
-
logger.warn("[NEUROLINK] Video audio transcription was requested but is not implemented yet " +
|
|
845
|
-
"(tracked as #433). Keyframes and any embedded subtitle tracks are still extracted; " +
|
|
846
|
-
"spoken audio will not be transcribed.");
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
835
|
/**
|
|
850
836
|
* Process the unified files array with auto-detection.
|
|
851
837
|
* Handles lazy file registration, full processing, and preview injection.
|
|
@@ -916,6 +902,10 @@ async function detectFileForUnifiedArray(file, options, provider, genericFileMax
|
|
|
916
902
|
frames: options.videoOptions.frames,
|
|
917
903
|
quality: options.videoOptions.quality,
|
|
918
904
|
format: options.videoOptions.format,
|
|
905
|
+
// #433: `--transcribe-audio` stopped here until now — the flag was
|
|
906
|
+
// accepted, warned about, and never handed to the processor that
|
|
907
|
+
// would have to act on it.
|
|
908
|
+
transcribeAudio: options.videoOptions.transcribeAudio,
|
|
919
909
|
}
|
|
920
910
|
: undefined,
|
|
921
911
|
provider: provider,
|
|
@@ -952,7 +942,6 @@ export async function processUnifiedFilesArray(options, maxSize, provider) {
|
|
|
952
942
|
}
|
|
953
943
|
const totalFiles = pending.length;
|
|
954
944
|
const files = pending;
|
|
955
|
-
warnIfVideoTranscriptionRequested(options.videoOptions);
|
|
956
945
|
return withSpan({
|
|
957
946
|
name: "neurolink.file.process_all",
|
|
958
947
|
tracer: tracers.file,
|
|
@@ -15,6 +15,7 @@ import type { StreamOptions } from "../types/index.js";
|
|
|
15
15
|
* - input.audioFiles: Audio files (Buffer | string paths)
|
|
16
16
|
* - input.videoFiles: Video files (Buffer | string paths)
|
|
17
17
|
* - csvOptions: CSV parsing options
|
|
18
|
+
* - videoOptions: Video frame/quality/format/transcription options
|
|
18
19
|
* - systemPrompt: System-level instructions
|
|
19
20
|
* - conversationMessages: Chat history
|
|
20
21
|
* - temperature: Model temperature (0-1)
|
|
@@ -64,6 +65,7 @@ export declare function buildMultimodalOptions(options: StreamOptions, providerN
|
|
|
64
65
|
imageOptions: {
|
|
65
66
|
outputFormat?: import("../index.js").VisionImageOutputFormat;
|
|
66
67
|
} | undefined;
|
|
68
|
+
videoOptions: import("../index.js").VideoProcessorOptions | undefined;
|
|
67
69
|
systemPrompt: string | undefined;
|
|
68
70
|
conversationHistory: import("../index.js").ChatMessage[] | undefined;
|
|
69
71
|
provider: string;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* - input.audioFiles: Audio files (Buffer | string paths)
|
|
15
15
|
* - input.videoFiles: Video files (Buffer | string paths)
|
|
16
16
|
* - csvOptions: CSV parsing options
|
|
17
|
+
* - videoOptions: Video frame/quality/format/transcription options
|
|
17
18
|
* - systemPrompt: System-level instructions
|
|
18
19
|
* - conversationMessages: Chat history
|
|
19
20
|
* - temperature: Model temperature (0-1)
|
|
@@ -60,6 +61,12 @@ export function buildMultimodalOptions(options, providerName, modelName) {
|
|
|
60
61
|
csvOptions: options.csvOptions,
|
|
61
62
|
pdfOptions: options.pdfOptions,
|
|
62
63
|
imageOptions: options.imageOptions,
|
|
64
|
+
// Same whitelist hazard as above, one level up: `videoFiles` reaching
|
|
65
|
+
// the processor is not enough on its own — without `videoOptions` too,
|
|
66
|
+
// the frame/quality/format/transcribeAudio knobs a caller set on
|
|
67
|
+
// `StreamOptions` never reach VideoProcessor on the Bedrock branch, and
|
|
68
|
+
// it silently falls back to the duration-tier default.
|
|
69
|
+
videoOptions: options.videoOptions,
|
|
63
70
|
systemPrompt: options.systemPrompt,
|
|
64
71
|
conversationHistory: options.conversationMessages,
|
|
65
72
|
provider: providerName,
|