mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 02:12:12 +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