feat: integrate Sensei Spook mascot in 4 stances, speech bubbles, and style updates
This commit is contained in:
commit
e04672db2a
104 files changed
+8262
No files matched your search
@@ -0,0 +1,133 @@
|
||||
export interface OpenRouterGenerateOptions {
|
||||
model: string;
|
||||
prompt: string;
|
||||
negativePrompt?: string;
|
||||
imageBase64: string;
|
||||
mimeType: string;
|
||||
timeoutMs: number;
|
||||
}
|
||||
|
||||
export interface OpenRouterResult {
|
||||
imageUrl: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export async function generateWithOpenRouter(
|
||||
options: OpenRouterGenerateOptions
|
||||
): Promise<OpenRouterResult> {
|
||||
const apiKey = process.env.OPENROUTER_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("OPENROUTER_API_KEY is not configured in environment variables");
|
||||
}
|
||||
|
||||
const cleanBase64 = options.imageBase64.startsWith("data:")
|
||||
? options.imageBase64
|
||||
: `data:${options.mimeType || "image/jpeg"};base64,${options.imageBase64}`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://photobooth.local",
|
||||
"X-Title": "AI PhotoBooth",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: options.model,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `${options.prompt}\n\nNegative prompt: ${options.negativePrompt || "none"}`,
|
||||
},
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: cleanBase64,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`OpenRouter API error [${response.status}]: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const choice = data.choices?.[0];
|
||||
const message = choice?.message;
|
||||
|
||||
// 1. Check OpenRouter image output attachments (message.images: [{ image_url: { url: ... } }])
|
||||
if (Array.isArray(message?.images) && message.images.length > 0) {
|
||||
for (const img of message.images) {
|
||||
const url = img?.image_url?.url || img?.url;
|
||||
if (url) {
|
||||
return {
|
||||
imageUrl: url,
|
||||
provider: "openrouter",
|
||||
model: options.model,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = message?.content;
|
||||
|
||||
if (!content) {
|
||||
throw new Error("OpenRouter returned an empty completion");
|
||||
}
|
||||
|
||||
// Check if content is or contains an image URL or base64 data
|
||||
const base64Match = content.match(/data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+/);
|
||||
if (base64Match) {
|
||||
return {
|
||||
imageUrl: base64Match[0],
|
||||
provider: "openrouter",
|
||||
model: options.model,
|
||||
};
|
||||
}
|
||||
|
||||
const urlMatch = content.match(/https?:\/\/[^\s"']+\.(?:png|jpg|jpeg|webp)/i);
|
||||
if (urlMatch) {
|
||||
return {
|
||||
imageUrl: urlMatch[0],
|
||||
provider: "openrouter",
|
||||
model: options.model,
|
||||
};
|
||||
}
|
||||
|
||||
// Some models return markdown image: 
|
||||
const mdMatch = content.match(/!\[.*?\]\((https?:\/\/.*?)\)/);
|
||||
if (mdMatch) {
|
||||
return {
|
||||
imageUrl: mdMatch[1],
|
||||
provider: "openrouter",
|
||||
model: options.model,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`OpenRouter response did not contain an image. Output received: ${content.slice(0, 120)}...`
|
||||
);
|
||||
} catch (error: any) {
|
||||
clearTimeout(timeoutId);
|
||||
if (error.name === "AbortError") {
|
||||
throw new Error(`OpenRouter request timed out after ${options.timeoutMs}ms`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user