mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 10:12:13 +00:00
Feat: OpenRouter image frame generation
This commit is contained in:
137
assets/js/openrouter.js
Normal file
137
assets/js/openrouter.js
Normal file
@@ -0,0 +1,137 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user