Files
vibegif.lol/assets/js/openrouter.js

138 lines
3.3 KiB
JavaScript

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;
}