import { NEXT_FRAME_PROMPT_TEMPLATE } from "./config.js"; const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"; const SYSTEM_PROMPT = [ "You generate a single image per request for an animation pipeline.", "Keep style consistency across frames.", "No text overlays, no labels, no typography.", "Output only the image.", ].join(" "); function buildFramePrompt({ masterPrompt, userPrompt, frameCount, frameIndex, }) { const lines = [ `Create frame ${frameIndex} of ${frameCount} for an animated GIF.`, `Concept: ${userPrompt}.`, `Master style lock: ${masterPrompt}.`, "Use a white background and clean black line doodle style.", "Keep the subject identity and scene composition coherent between frames.", ]; if (frameIndex === 1) { lines.push("This is the first frame. Establish a clear starting pose."); } else { lines.push(NEXT_FRAME_PROMPT_TEMPLATE(frameCount)); lines.push( `This is frame ${frameIndex}. Advance the motion naturally from the prior frame references.` ); } lines.push("Return one meaningful image only."); return lines.join("\n"); } function buildUserContent(promptText, previousFrames = []) { const parts = [{ type: "text", text: promptText }]; for (const frame of previousFrames.slice(-2)) { parts.push({ type: "image_url", image_url: { url: frame }, }); } return parts; } function extractImageUrl(json) { const message = json?.choices?.[0]?.message; const image = message?.images?.[0]; return image?.image_url?.url || image?.imageUrl?.url || ""; } function extractErrorMessage(text, status, fallbackStatusText) { try { const parsed = JSON.parse(text); return ( parsed?.error?.message || parsed?.message || `Request failed (${status} ${fallbackStatusText})` ); } catch { return text || `Request failed (${status} ${fallbackStatusText})`; } } export async function requestFrameImage({ apiKey, model, userPrompt, masterPrompt, frameCount, frameIndex, previousFrames, imageSize, aspectRatio, }) { const promptText = buildFramePrompt({ masterPrompt, userPrompt, frameCount, frameIndex, }); const body = { model, stream: false, modalities: ["image"], messages: [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: buildUserContent(promptText, previousFrames), }, ], image_config: { image_size: imageSize, aspect_ratio: aspectRatio, }, }; const headers = { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }; if (typeof window !== "undefined" && window.location?.origin?.startsWith("http")) { headers["HTTP-Referer"] = window.location.origin; headers["X-OpenRouter-Title"] = "vibegif.lol"; } const response = await fetch(OPENROUTER_URL, { method: "POST", headers, body: JSON.stringify(body), }); if (!response.ok) { const raw = await response.text(); throw new Error( extractErrorMessage(raw, response.status, response.statusText) ); } const json = await response.json(); const imageUrl = extractImageUrl(json); if (!imageUrl) { throw new Error("No image returned by model. Try again with fewer frames or simpler prompt."); } return imageUrl; }