diff --git a/assets/js/openrouter.js b/assets/js/openrouter.js deleted file mode 100644 index 162cf31..0000000 --- a/assets/js/openrouter.js +++ /dev/null @@ -1,137 +0,0 @@ -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; -}