mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 10:12:13 +00:00
Delete assets/js/app.js
This commit is contained in:
337
assets/js/app.js
337
assets/js/app.js
@@ -1,337 +0,0 @@
|
|||||||
(function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
/* ── Config ── */
|
|
||||||
const STORAGE_KEY = "vibegif_openrouter_api_key";
|
|
||||||
|
|
||||||
const MASTER_PROMPT =
|
|
||||||
"minimal black and white line doodle, single stroke, white background, kawaii style";
|
|
||||||
|
|
||||||
const MODELS = [
|
|
||||||
{
|
|
||||||
id: "google/gemini-3.1-flash-image-preview",
|
|
||||||
label: "google/gemini-3.1-flash-image-preview",
|
|
||||||
supportsHalfK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "bytedance-seed/seedream-4.5",
|
|
||||||
label: "bytedance-seed/seedream-4.5",
|
|
||||||
supportsHalfK: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ASPECT_RATIOS = [
|
|
||||||
"1:1", "2:3", "3:2", "3:4", "4:3",
|
|
||||||
"4:5", "5:4", "9:16", "16:9", "21:9",
|
|
||||||
];
|
|
||||||
|
|
||||||
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
||||||
|
|
||||||
const GIF_WORKER_URL =
|
|
||||||
"https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.worker.js";
|
|
||||||
|
|
||||||
/* ── Helpers ── */
|
|
||||||
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, Number(n) || lo));
|
|
||||||
const modelById = (id) => MODELS.find((m) => m.id === id) || MODELS[0];
|
|
||||||
|
|
||||||
function refreshIcons() {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (window.lucide) window.lucide.createIcons();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── OpenRouter ── */
|
|
||||||
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 buildPromptText(userPrompt, masterPrompt, 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(
|
|
||||||
"Imagine we are trying to create a " + frameCount +
|
|
||||||
" frame gif. Generate the next meaningful frame."
|
|
||||||
);
|
|
||||||
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(text, prevFrames) {
|
|
||||||
var parts = [{ type: "text", text: text }];
|
|
||||||
var tail = prevFrames.slice(-2);
|
|
||||||
for (var i = 0; i < tail.length; i++) {
|
|
||||||
parts.push({
|
|
||||||
type: "image_url",
|
|
||||||
image_url: { url: tail[i] },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractImageUrl(json) {
|
|
||||||
var msg = json && json.choices && json.choices[0] && json.choices[0].message;
|
|
||||||
if (!msg) return "";
|
|
||||||
// OpenRouter image gen returns base64 URL inside content array
|
|
||||||
if (Array.isArray(msg.content)) {
|
|
||||||
for (var i = 0; i < msg.content.length; i++) {
|
|
||||||
var part = msg.content[i];
|
|
||||||
if (part.type === "image_url") {
|
|
||||||
var url = part.image_url && (part.image_url.url || part.image_url);
|
|
||||||
if (url) return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// fallback: images array
|
|
||||||
if (Array.isArray(msg.images) && msg.images.length) {
|
|
||||||
var img = msg.images[0];
|
|
||||||
return img.image_url?.url || img.imageUrl?.url || img.url || "";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestFrame(opts) {
|
|
||||||
var body = {
|
|
||||||
model: opts.model,
|
|
||||||
stream: false,
|
|
||||||
modalities: ["image"],
|
|
||||||
messages: [
|
|
||||||
{ role: "system", content: SYSTEM_PROMPT },
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: buildUserContent(
|
|
||||||
buildPromptText(opts.userPrompt, opts.masterPrompt, opts.frameCount, opts.frameIndex),
|
|
||||||
opts.previousFrames
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
image_config: {
|
|
||||||
image_size: opts.imageSize,
|
|
||||||
aspect_ratio: opts.aspectRatio,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var headers = {
|
|
||||||
Authorization: "Bearer " + opts.apiKey,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"HTTP-Referer": window.location.origin || "https://vibegif.lol",
|
|
||||||
"X-OpenRouter-Title": "vibegif.lol",
|
|
||||||
};
|
|
||||||
|
|
||||||
var res = await fetch(OPENROUTER_URL, {
|
|
||||||
method: "POST",
|
|
||||||
headers: headers,
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
var raw = await res.text();
|
|
||||||
var errMsg;
|
|
||||||
try {
|
|
||||||
var parsed = JSON.parse(raw);
|
|
||||||
errMsg = (parsed.error && parsed.error.message) || parsed.message || raw;
|
|
||||||
} catch (_) {
|
|
||||||
errMsg = raw || "Request failed (" + res.status + ")";
|
|
||||||
}
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = await res.json();
|
|
||||||
var url = extractImageUrl(json);
|
|
||||||
if (!url) throw new Error("No image returned. Try a simpler prompt or fewer frames.");
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── GIF Builder ── */
|
|
||||||
function loadImage(src) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
var img = new Image();
|
|
||||||
img.crossOrigin = "anonymous";
|
|
||||||
img.onload = function () { resolve(img); };
|
|
||||||
img.onerror = function () { reject(new Error("Failed to load frame image.")); };
|
|
||||||
img.src = src;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createGif(frameUrls, fps) {
|
|
||||||
if (!frameUrls.length) throw new Error("No frames to encode.");
|
|
||||||
if (typeof window.GIF !== "function") throw new Error("GIF encoder not loaded.");
|
|
||||||
|
|
||||||
var images = await Promise.all(frameUrls.map(loadImage));
|
|
||||||
var first = images[0];
|
|
||||||
var delay = Math.max(20, Math.round(1000 / clamp(fps, 1, 24)));
|
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
var gif = new window.GIF({
|
|
||||||
workers: 2,
|
|
||||||
quality: 10,
|
|
||||||
repeat: 0,
|
|
||||||
width: first.naturalWidth || first.width,
|
|
||||||
height: first.naturalHeight || first.height,
|
|
||||||
workerScript: GIF_WORKER_URL,
|
|
||||||
});
|
|
||||||
for (var i = 0; i < images.length; i++) {
|
|
||||||
gif.addFrame(images[i], { delay: delay });
|
|
||||||
}
|
|
||||||
gif.on("finished", function (blob) { resolve(blob); });
|
|
||||||
gif.on("abort", function () { reject(new Error("GIF rendering aborted.")); });
|
|
||||||
try { gif.render(); } catch (e) { reject(e); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Alpine Component ── */
|
|
||||||
document.addEventListener("alpine:init", function () {
|
|
||||||
Alpine.data("vibeGifApp", function () {
|
|
||||||
return {
|
|
||||||
apiKey: "",
|
|
||||||
apiKeyInput: "",
|
|
||||||
showSettings: false,
|
|
||||||
model: MODELS[0].id,
|
|
||||||
imageSize: "1K",
|
|
||||||
aspectRatio: "1:1",
|
|
||||||
frameCount: 4,
|
|
||||||
fps: 4,
|
|
||||||
userPrompt: "rolling cat",
|
|
||||||
masterPrompt: MASTER_PROMPT,
|
|
||||||
frames: [],
|
|
||||||
gifUrl: "",
|
|
||||||
gifBlob: null,
|
|
||||||
isGenerating: false,
|
|
||||||
progressText: "",
|
|
||||||
errorText: "",
|
|
||||||
|
|
||||||
init() {
|
|
||||||
var saved = localStorage.getItem(STORAGE_KEY) || "";
|
|
||||||
this.apiKey = saved;
|
|
||||||
this.apiKeyInput = saved;
|
|
||||||
if (!saved) this.showSettings = true;
|
|
||||||
this.normalizeSelections();
|
|
||||||
refreshIcons();
|
|
||||||
},
|
|
||||||
|
|
||||||
get modelOptions() { return MODELS; },
|
|
||||||
get selectedModel() { return modelById(this.model); },
|
|
||||||
|
|
||||||
get imageSizeOptions() {
|
|
||||||
return this.selectedModel.supportsHalfK ? ["1K", "0.5K"] : ["1K"];
|
|
||||||
},
|
|
||||||
|
|
||||||
get aspectRatioOptions() { return ASPECT_RATIOS; },
|
|
||||||
get hasApiKey() { return !!this.apiKey; },
|
|
||||||
|
|
||||||
get canGenerate() {
|
|
||||||
return this.hasApiKey && !this.isGenerating && this.userPrompt.trim().length > 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
normalizeSelections() {
|
|
||||||
if (this.imageSizeOptions.indexOf(this.imageSize) === -1) {
|
|
||||||
this.imageSize = "1K";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
openSettings() {
|
|
||||||
this.apiKeyInput = this.apiKey;
|
|
||||||
this.showSettings = true;
|
|
||||||
this.$nextTick(refreshIcons);
|
|
||||||
},
|
|
||||||
|
|
||||||
closeSettings() { this.showSettings = false; },
|
|
||||||
|
|
||||||
saveApiKey() {
|
|
||||||
var key = (this.apiKeyInput || "").trim();
|
|
||||||
this.apiKey = key;
|
|
||||||
if (key) localStorage.setItem(STORAGE_KEY, key);
|
|
||||||
else localStorage.removeItem(STORAGE_KEY);
|
|
||||||
this.showSettings = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
clearApiKey() {
|
|
||||||
this.apiKey = "";
|
|
||||||
this.apiKeyInput = "";
|
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
|
||||||
},
|
|
||||||
|
|
||||||
resetOutput() {
|
|
||||||
this.frames = [];
|
|
||||||
this.errorText = "";
|
|
||||||
this.progressText = "";
|
|
||||||
this.gifBlob = null;
|
|
||||||
if (this.gifUrl) {
|
|
||||||
URL.revokeObjectURL(this.gifUrl);
|
|
||||||
this.gifUrl = "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async generateGif() {
|
|
||||||
if (!this.canGenerate) {
|
|
||||||
if (!this.hasApiKey) this.openSettings();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fc = clamp(this.frameCount, 2, 24);
|
|
||||||
var fpsVal = clamp(this.fps, 1, 24);
|
|
||||||
var prompt = this.userPrompt.trim();
|
|
||||||
this.frameCount = fc;
|
|
||||||
this.fps = fpsVal;
|
|
||||||
this.resetOutput();
|
|
||||||
this.isGenerating = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (var i = 1; i <= fc; i++) {
|
|
||||||
this.progressText = "Generating frame " + i + "/" + fc + "…";
|
|
||||||
|
|
||||||
var imgUrl = await requestFrame({
|
|
||||||
apiKey: this.apiKey,
|
|
||||||
model: this.model,
|
|
||||||
userPrompt: prompt,
|
|
||||||
masterPrompt: this.masterPrompt,
|
|
||||||
frameCount: fc,
|
|
||||||
frameIndex: i,
|
|
||||||
previousFrames: this.frames.slice(-2),
|
|
||||||
imageSize: this.imageSize,
|
|
||||||
aspectRatio: this.aspectRatio,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.frames.push(imgUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.progressText = "Rendering GIF…";
|
|
||||||
this.gifBlob = await createGif(this.frames, fpsVal);
|
|
||||||
this.gifUrl = URL.createObjectURL(this.gifBlob);
|
|
||||||
this.progressText = "Done ✨";
|
|
||||||
this.$nextTick(refreshIcons);
|
|
||||||
} catch (err) {
|
|
||||||
this.errorText = err.message || "Generation failed.";
|
|
||||||
} finally {
|
|
||||||
this.isGenerating = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
downloadGif() {
|
|
||||||
if (!this.gifUrl) return;
|
|
||||||
var a = document.createElement("a");
|
|
||||||
a.href = this.gifUrl;
|
|
||||||
a.download = "vibegif-" + Date.now() + ".gif";
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
Reference in New Issue
Block a user