@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
158 lines • 6.42 kB
JavaScript
/**
* Replicate Video Handler
*
* Routes video generation through the universal Replicate prediction
* lifecycle. Default model is Wan-Alpha (RGBA video); callers can specify
* any image-to-video model on Replicate via `options.model`.
*
* @module adapters/video/replicateVideoHandler
* @see https://replicate.com/atonamy/wan-alpha
*/
import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
import { VIDEO_ERROR_CODES } from "../../constants/videoErrors.js";
import { logger } from "../../utils/logger.js";
import { VideoError } from "../../utils/videoProcessor.js";
import { getReplicateAuth } from "../replicate/auth.js";
import { downloadPredictionOutput, predict, } from "../replicate/predictionLifecycle.js";
const DEFAULT_MODEL = "atonamy/wan-alpha";
/**
* Replicate Video Handler.
*
* Capabilities depend on the specific Replicate model — this handler
* advertises conservative bounds (any provider-supported aspect ratio /
* resolution; up to 10s typical for Wan-Alpha).
*/
export class ReplicateVideoHandler {
maxDurationSeconds = 10;
supportedAspectRatios = ["9:16", "16:9", "1:1"];
supportedResolutions = [
"720p",
"1080p",
];
instanceCredentials;
constructor(credentials) {
this.instanceCredentials = credentials;
}
isConfigured() {
return getReplicateAuth(this.instanceCredentials) !== null;
}
async generate(image, prompt, options) {
const perCallCreds = options.credentials?.replicate;
const auth = getReplicateAuth(perCallCreds ?? this.instanceCredentials);
if (!auth) {
throw new VideoError({
code: VIDEO_ERROR_CODES.PROVIDER_NOT_CONFIGURED,
message: "REPLICATE_API_TOKEN not configured",
category: ErrorCategory.CONFIGURATION,
severity: ErrorSeverity.HIGH,
retriable: false,
});
}
const startTime = Date.now();
const model = options.model ?? DEFAULT_MODEL;
const dataUri = `data:image/${this.detectImageType(image)};base64,${image.toString("base64")}`;
// Wan-Alpha + most image-to-video models accept this shape; specific
// models may require provider-specific extras passed through
// VideoOutputOptions.[unknown key].
//
// `resolution` is forwarded as the `resolution` input parameter.
// Wan-Alpha and several other Replicate image-to-video models accept it
// (e.g. "720p", "1080p"). Models that do not recognise it will silently
// ignore the field — the Replicate API does not reject unknown input keys.
// `calculateDimensions` still populates the metadata `dimensions` field
// so downstream consumers always receive correct width/height regardless
// of whether the model honoured the resolution hint.
const inputPayload = {
image: dataUri,
prompt,
num_frames: (options.length ?? 4) * 24, // Assume 24 fps
fps: 24,
aspect_ratio: options.aspectRatio,
...(options.resolution !== undefined
? { resolution: options.resolution }
: {}),
};
let prediction;
try {
prediction = await predict(auth, { model, input: inputPayload }, { abortSignal: options.abortSignal });
}
catch (err) {
throw new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: `Replicate video generation failed: ${err instanceof Error ? err.message : String(err)}`,
category: ErrorCategory.EXECUTION,
severity: ErrorSeverity.HIGH,
retriable: true,
context: { model, options },
originalError: err instanceof Error ? err : undefined,
});
}
let videoBuffer;
try {
videoBuffer = await downloadPredictionOutput(prediction);
}
catch (err) {
throw new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: `Replicate video download failed: ${err instanceof Error ? err.message : String(err)}`,
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.MEDIUM,
retriable: true,
context: { predictionId: prediction.id },
originalError: err instanceof Error ? err : undefined,
});
}
const processingTime = Date.now() - startTime;
logger.info(`[ReplicateVideoHandler] Generated ${videoBuffer.length} bytes in ${processingTime}ms — model ${model}`);
return {
data: videoBuffer,
mediaType: "video/mp4",
metadata: {
duration: options.length ?? 4,
dimensions: this.calculateDimensions(options),
model,
provider: "replicate",
aspectRatio: options.aspectRatio ?? "16:9",
audioEnabled: false, // Most Replicate video models are silent
processingTime,
},
};
}
detectImageType(buffer) {
if (buffer.length < 4) {
return "jpeg";
}
if (buffer[0] === 0x89 && buffer[1] === 0x50) {
return "png";
}
if (buffer[0] === 0xff && buffer[1] === 0xd8) {
return "jpeg";
}
if (buffer[0] === 0x52 &&
buffer[1] === 0x49 &&
buffer[2] === 0x46 &&
buffer[3] === 0x46) {
return "webp";
}
return "jpeg";
}
calculateDimensions(options) {
const aspectRatio = options.aspectRatio ?? "16:9";
const resolution = options.resolution ?? "720p";
if (resolution === "1080p") {
if (aspectRatio === "1:1") {
return { width: 1080, height: 1080 };
}
return aspectRatio === "9:16"
? { width: 1080, height: 1920 }
: { width: 1920, height: 1080 };
}
if (aspectRatio === "1:1") {
return { width: 720, height: 720 };
}
return aspectRatio === "9:16"
? { width: 720, height: 1280 }
: { width: 1280, height: 720 };
}
}
//# sourceMappingURL=replicateVideoHandler.js.map