Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate images via OpenAI, Google, OpenRouter, DashScope, Jimeng, Seedream, and Replicate APIs with batch support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/providers/replicate.ts
1import path from "node:path";2import { readFile } from "node:fs/promises";3import type { CliArgs } from "../types";45const DEFAULT_MODEL = "google/nano-banana-pro";6const SYNC_WAIT_SECONDS = 60;7const POLL_INTERVAL_MS = 2000;8const MAX_POLL_MS = 300_000;910export function getDefaultModel(): string {11return process.env.REPLICATE_IMAGE_MODEL || DEFAULT_MODEL;12}1314function getApiToken(): string | null {15return process.env.REPLICATE_API_TOKEN || null;16}1718function getBaseUrl(): string {19const base = process.env.REPLICATE_BASE_URL || "https://api.replicate.com";20return base.replace(/\/+$/g, "");21}2223export function parseModelId(model: string): { owner: string; name: string; version: string | null } {24const [ownerName, version] = model.split(":");25const parts = ownerName!.split("/");26if (parts.length !== 2 || !parts[0] || !parts[1]) {27throw new Error(28`Invalid Replicate model format: "${model}". Expected "owner/name" or "owner/name:version".`29);30}31return { owner: parts[0], name: parts[1], version: version || null };32}3334export function buildInput(prompt: string, args: CliArgs, referenceImages: string[]): Record<string, unknown> {35const input: Record<string, unknown> = { prompt };3637if (args.aspectRatio) {38input.aspect_ratio = args.aspectRatio;39} else if (referenceImages.length > 0) {40input.aspect_ratio = "match_input_image";41}4243if (args.n > 1) {44input.number_of_images = args.n;45}4647if (args.quality === "normal") {48input.resolution = "1K";49} else if (args.quality === "2k") {50input.resolution = "2K";51}5253input.output_format = "png";5455if (referenceImages.length > 0) {56input.image_input = referenceImages;57}5859return input;60}6162async function readImageAsDataUrl(p: string): Promise<string> {63const buf = await readFile(p);64const ext = path.extname(p).toLowerCase();65let mimeType = "image/png";66if (ext === ".jpg" || ext === ".jpeg") mimeType = "image/jpeg";67else if (ext === ".gif") mimeType = "image/gif";68else if (ext === ".webp") mimeType = "image/webp";69return `data:${mimeType};base64,${buf.toString("base64")}`;70}7172type PredictionResponse = {73id: string;74status: string;75output: unknown;76error: string | null;77urls?: { get?: string };78};7980async function createPrediction(81apiToken: string,82model: { owner: string; name: string; version: string | null },83input: Record<string, unknown>,84sync: boolean85): Promise<PredictionResponse> {86const baseUrl = getBaseUrl();8788let url: string;89const body: Record<string, unknown> = { input };9091if (model.version) {92url = `${baseUrl}/v1/predictions`;93body.version = model.version;94} else {95url = `${baseUrl}/v1/models/${model.owner}/${model.name}/predictions`;96}9798const headers: Record<string, string> = {99Authorization: `Bearer ${apiToken}`,100"Content-Type": "application/json",101};102103if (sync) {104headers["Prefer"] = `wait=${SYNC_WAIT_SECONDS}`;105}106107const res = await fetch(url, {108method: "POST",109headers,110body: JSON.stringify(body),111});112113if (!res.ok) {114const err = await res.text();115throw new Error(`Replicate API error (${res.status}): ${err}`);116}117118return (await res.json()) as PredictionResponse;119}120121async function pollPrediction(apiToken: string, getUrl: string): Promise<PredictionResponse> {122const start = Date.now();123124while (Date.now() - start < MAX_POLL_MS) {125const res = await fetch(getUrl, {126headers: { Authorization: `Bearer ${apiToken}` },127});128129if (!res.ok) {130const err = await res.text();131throw new Error(`Replicate poll error (${res.status}): ${err}`);132}133134const prediction = (await res.json()) as PredictionResponse;135136if (prediction.status === "succeeded") return prediction;137if (prediction.status === "failed" || prediction.status === "canceled") {138throw new Error(`Replicate prediction ${prediction.status}: ${prediction.error || "unknown error"}`);139}140141await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));142}143144throw new Error(`Replicate prediction timed out after ${MAX_POLL_MS / 1000}s`);145}146147export function extractOutputUrl(prediction: PredictionResponse): string {148const output = prediction.output;149150if (typeof output === "string") return output;151152if (Array.isArray(output)) {153const first = output[0];154if (typeof first === "string") return first;155}156157if (output && typeof output === "object" && "url" in output) {158const url = (output as Record<string, unknown>).url;159if (typeof url === "string") return url;160}161162throw new Error(`Unexpected Replicate output format: ${JSON.stringify(output)}`);163}164165async function downloadImage(url: string): Promise<Uint8Array> {166const res = await fetch(url);167if (!res.ok) throw new Error(`Failed to download image from Replicate: ${res.status}`);168const buf = await res.arrayBuffer();169return new Uint8Array(buf);170}171172export async function generateImage(173prompt: string,174model: string,175args: CliArgs176): Promise<Uint8Array> {177const apiToken = getApiToken();178if (!apiToken) throw new Error("REPLICATE_API_TOKEN is required. Get one at https://replicate.com/account/api-tokens");179180const parsedModel = parseModelId(model);181182const refDataUrls: string[] = [];183for (const refPath of args.referenceImages) {184refDataUrls.push(await readImageAsDataUrl(refPath));185}186187const input = buildInput(prompt, args, refDataUrls);188189console.log(`Generating image with Replicate (${model})...`);190191let prediction = await createPrediction(apiToken, parsedModel, input, true);192193if (prediction.status !== "succeeded") {194if (!prediction.urls?.get) {195throw new Error("Replicate prediction did not return a poll URL");196}197console.log("Waiting for prediction to complete...");198prediction = await pollPrediction(apiToken, prediction.urls.get);199}200201console.log("Generation completed.");202203const outputUrl = extractOutputUrl(prediction);204return downloadImage(outputUrl);205}206