mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 02:12:12 +00:00
Feat: OpenRouter image generation API client
This commit is contained in:
88
api.js
Normal file
88
api.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { getApiKey } from './storage.js';
|
||||||
|
|
||||||
|
const BASE = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
const MASTER_PROMPT = 'minimal black and white line doodle, single stroke, white background, kawaii style';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a single image frame via OpenRouter.
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {string} opts.model
|
||||||
|
* @param {Array} opts.messages - chat history messages
|
||||||
|
* @param {string} opts.imageSize - "1K" or "0.5K"
|
||||||
|
* @param {string} opts.aspectRatio - e.g. "1:1"
|
||||||
|
* @returns {Promise<{base64: string, assistantMsg: Object}>}
|
||||||
|
*/
|
||||||
|
export async function generateFrame({ model, messages, imageSize, aspectRatio }) {
|
||||||
|
const isGemini = model.startsWith('google/');
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
model,
|
||||||
|
messages,
|
||||||
|
modalities: ['image'],
|
||||||
|
image_config: {
|
||||||
|
aspect_ratio: aspectRatio,
|
||||||
|
image_size: imageSize,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isGemini && imageSize === '0.5K') {
|
||||||
|
body.image_config.image_size = '1K';
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(BASE, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${getApiKey()}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://vibegif.lol',
|
||||||
|
'X-Title': 'vibegif.lol',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(err?.error?.message || `API error ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const choice = data.choices?.[0];
|
||||||
|
if (!choice) throw new Error('No response from model');
|
||||||
|
|
||||||
|
const images = choice.message?.images;
|
||||||
|
if (!images?.length) throw new Error('No image in response. The model may have refused or returned text only.');
|
||||||
|
|
||||||
|
const url = images[0].image_url?.url || images[0].url;
|
||||||
|
if (!url) throw new Error('Could not parse image from response');
|
||||||
|
|
||||||
|
const base64 = url.startsWith('data:') ? url : `data:image/png;base64,${url}`;
|
||||||
|
|
||||||
|
const assistantMsg = {
|
||||||
|
role: 'assistant',
|
||||||
|
content: [
|
||||||
|
{ type: 'image_url', image_url: { url: base64 } }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return { base64, assistantMsg };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the first user message (frame 1).
|
||||||
|
*/
|
||||||
|
export function buildFirstMessage(userPrompt) {
|
||||||
|
return {
|
||||||
|
role: 'user',
|
||||||
|
content: `${MASTER_PROMPT}, ${userPrompt}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build follow-up user message for subsequent frames.
|
||||||
|
*/
|
||||||
|
export function buildNextMessage(frameIndex, frameCount) {
|
||||||
|
return {
|
||||||
|
role: 'user',
|
||||||
|
content: `imagine we are trying to create a ${frameCount} frame gif. generate the next meaningful frame (frame ${frameIndex} of ${frameCount})`
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user