Files
LocalAI/core/http/static/video.js
Ettore Di Giacinto 0fa0ac4797 fix(videogen): drop incomplete endpoint, add GGUF support for LTX-2 (#8160)
* Debug

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Drop openai video endpoint (is not complete)

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Add download button

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-01-22 14:09:20 +01:00

377 lines
12 KiB
JavaScript

// Helper function to convert file to base64
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// Remove data:image/...;base64, prefix if present
const base64 = reader.result.split(',')[1] || reader.result;
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
function genVideo(event) {
event.preventDefault();
promptVideo();
}
async function promptVideo() {
const loader = document.getElementById("loader");
const input = document.getElementById("input");
const generateBtn = document.getElementById("generate-btn");
const resultDiv = document.getElementById("result");
const resultPlaceholder = document.getElementById("result-placeholder");
// Show loader and disable form
loader.classList.remove("hidden");
if (resultPlaceholder) {
resultPlaceholder.style.display = "none";
}
input.disabled = true;
generateBtn.disabled = true;
// Store the prompt for later restoration
const prompt = input.value.trim();
if (!prompt) {
alert("Please enter a prompt");
loader.classList.add("hidden");
if (resultPlaceholder) {
resultPlaceholder.style.display = "flex";
}
input.disabled = false;
generateBtn.disabled = false;
return;
}
// Collect all form values
const model = document.getElementById("video-model").value;
const size = document.getElementById("video-size").value;
const negativePrompt = document.getElementById("negative-prompt").value.trim();
// Parse size into width and height
const sizeParts = size.split("x");
let width = 512;
let height = 512;
if (sizeParts.length === 2) {
width = parseInt(sizeParts[0]) || 512;
height = parseInt(sizeParts[1]) || 512;
}
// Video-specific parameters
const secondsInput = document.getElementById("video-seconds").value.trim();
const seconds = secondsInput ? secondsInput : undefined;
const fpsInput = document.getElementById("video-fps").value.trim();
const fps = fpsInput ? parseInt(fpsInput) : 16;
const framesInput = document.getElementById("video-frames").value.trim();
const numFrames = framesInput ? parseInt(framesInput) : undefined;
// Advanced parameters
const stepInput = document.getElementById("video-steps").value.trim();
const step = stepInput ? parseInt(stepInput) : undefined;
const seedInput = document.getElementById("video-seed").value.trim();
const seed = seedInput ? parseInt(seedInput) : undefined;
const cfgScaleInput = document.getElementById("video-cfg-scale").value.trim();
const cfgScale = cfgScaleInput ? parseFloat(cfgScaleInput) : undefined;
// Prepare request body
const requestBody = {
model: model,
prompt: prompt,
width: width,
height: height,
fps: fps,
};
if (negativePrompt) {
requestBody.negative_prompt = negativePrompt;
}
if (seconds !== undefined) {
requestBody.seconds = seconds;
}
if (numFrames !== undefined) {
requestBody.num_frames = numFrames;
}
if (step !== undefined) {
requestBody.step = step;
}
if (seed !== undefined) {
requestBody.seed = seed;
}
if (cfgScale !== undefined) {
requestBody.cfg_scale = cfgScale;
}
// Handle file inputs
try {
// Start image (for img2video)
const startImageInput = document.getElementById("start-image");
if (startImageInput.files.length > 0) {
const base64 = await fileToBase64(startImageInput.files[0]);
requestBody.start_image = base64;
}
// End image
const endImageInput = document.getElementById("end-image");
if (endImageInput.files.length > 0) {
const base64 = await fileToBase64(endImageInput.files[0]);
requestBody.end_image = base64;
}
} catch (error) {
console.error("Error processing image files:", error);
resultDiv.innerHTML = '<p class="text-xs text-red-500 p-2">Error processing image files: ' + error.message + '</p>';
loader.classList.add("hidden");
if (resultPlaceholder) {
resultPlaceholder.style.display = "none";
}
input.disabled = false;
generateBtn.disabled = false;
return;
}
// Make API request to LocalAI endpoint
try {
const response = await fetch("video", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
const json = await response.json();
if (json.error) {
// Display error
resultDiv.innerHTML = '<p class="text-xs text-red-500 p-2">Error: ' + json.error.message + '</p>';
loader.classList.add("hidden");
if (resultPlaceholder) {
resultPlaceholder.style.display = "none";
}
input.disabled = false;
generateBtn.disabled = false;
return;
}
// Clear result div and hide placeholder
resultDiv.innerHTML = '';
if (resultPlaceholder) {
resultPlaceholder.style.display = "none";
}
// Display generated video
if (json.data && json.data.length > 0) {
json.data.forEach((item, index) => {
const videoContainer = document.createElement("div");
videoContainer.className = "flex flex-col";
// Create video element
const video = document.createElement("video");
video.controls = true;
video.className = "w-full h-auto rounded-lg";
video.preload = "metadata";
if (item.url) {
video.src = item.url;
} else if (item.b64_json) {
video.src = "data:video/mp4;base64," + item.b64_json;
} else {
return; // Skip invalid items
}
videoContainer.appendChild(video);
// Create caption container
const captionDiv = document.createElement("div");
captionDiv.className = "mt-2 p-2 bg-[var(--color-bg-secondary)] rounded-lg text-xs";
// Prompt caption
const promptCaption = document.createElement("p");
promptCaption.className = "text-[var(--color-text-primary)] mb-1.5 break-words";
promptCaption.innerHTML = '<strong>Prompt:</strong> ' + escapeHtml(prompt);
captionDiv.appendChild(promptCaption);
// Negative prompt if provided
if (negativePrompt) {
const negativeCaption = document.createElement("p");
negativeCaption.className = "text-[var(--color-text-secondary)] mb-1.5 break-words";
negativeCaption.innerHTML = '<strong>Negative Prompt:</strong> ' + escapeHtml(negativePrompt);
captionDiv.appendChild(negativeCaption);
}
// Generation details
const detailsDiv = document.createElement("div");
detailsDiv.className = "flex flex-wrap gap-3 text-[10px] text-[var(--color-text-secondary)] mt-1.5";
detailsDiv.innerHTML = `
<span><strong>Size:</strong> ${width}x${height}</span>
${fps ? `<span><strong>FPS:</strong> ${fps}</span>` : ''}
${numFrames !== undefined ? `<span><strong>Frames:</strong> ${numFrames}</span>` : ''}
${seconds !== undefined ? `<span><strong>Duration:</strong> ${seconds}s</span>` : ''}
${step !== undefined ? `<span><strong>Steps:</strong> ${step}</span>` : ''}
${seed !== undefined ? `<span><strong>Seed:</strong> ${seed}</span>` : ''}
${cfgScale !== undefined ? `<span><strong>CFG Scale:</strong> ${cfgScale}</span>` : ''}
`;
captionDiv.appendChild(detailsDiv);
// Button container
const buttonContainer = document.createElement("div");
buttonContainer.className = "mt-1.5 flex gap-2";
// Copy prompt button
const copyBtn = document.createElement("button");
copyBtn.className = "px-2 py-0.5 text-[10px] bg-[var(--color-primary)] text-white rounded hover:opacity-80";
copyBtn.innerHTML = '<i class="fas fa-copy mr-1"></i>Copy Prompt';
copyBtn.onclick = () => {
navigator.clipboard.writeText(prompt).then(() => {
copyBtn.innerHTML = '<i class="fas fa-check mr-1"></i>Copied!';
setTimeout(() => {
copyBtn.innerHTML = '<i class="fas fa-copy mr-1"></i>Copy Prompt';
}, 2000);
});
};
buttonContainer.appendChild(copyBtn);
// Download video button
const downloadBtn = document.createElement("button");
downloadBtn.className = "px-2 py-0.5 text-[10px] bg-[var(--color-primary)] text-white rounded hover:opacity-80";
downloadBtn.innerHTML = '<i class="fas fa-download mr-1"></i>Download Video';
downloadBtn.onclick = () => {
downloadVideo(item, downloadBtn);
};
buttonContainer.appendChild(downloadBtn);
captionDiv.appendChild(buttonContainer);
videoContainer.appendChild(captionDiv);
resultDiv.appendChild(videoContainer);
});
// Hide placeholder when videos are displayed
if (resultPlaceholder) {
resultPlaceholder.style.display = "none";
}
} else {
resultDiv.innerHTML = '<p class="text-xs text-[var(--color-text-secondary)] p-2">No videos were generated.</p>';
if (resultPlaceholder) {
resultPlaceholder.style.display = "none";
}
}
} catch (error) {
console.error("Error generating video:", error);
resultDiv.innerHTML = '<p class="text-xs text-red-500 p-2">Error: ' + error.message + '</p>';
if (resultPlaceholder) {
resultPlaceholder.style.display = "none";
}
} finally {
// Hide loader and re-enable form
loader.classList.add("hidden");
input.disabled = false;
generateBtn.disabled = false;
input.focus();
}
}
// Helper function to escape HTML
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// Helper function to download video
function downloadVideo(item, button) {
try {
let videoUrl;
let filename = "generated-video.mp4";
if (item.url) {
// If we have a URL, use it directly
videoUrl = item.url;
// Extract filename from URL if possible
const urlParts = item.url.split("/");
if (urlParts.length > 0) {
const lastPart = urlParts[urlParts.length - 1];
if (lastPart && lastPart.includes(".")) {
filename = lastPart;
}
}
} else if (item.b64_json) {
// Convert base64 to blob
const byteCharacters = atob(item.b64_json);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: "video/mp4" });
videoUrl = URL.createObjectURL(blob);
} else {
console.error("No video data available for download");
return;
}
// Create a temporary anchor element to trigger download
const link = document.createElement("a");
link.href = videoUrl;
link.download = filename;
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up object URL if we created one
if (item.b64_json && videoUrl.startsWith("blob:")) {
setTimeout(() => URL.revokeObjectURL(videoUrl), 100);
}
// Show feedback
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-check mr-1"></i>Downloaded!';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
} catch (error) {
console.error("Error downloading video:", error);
button.innerHTML = '<i class="fas fa-exclamation-triangle mr-1"></i>Error';
setTimeout(() => {
button.innerHTML = '<i class="fas fa-download mr-1"></i>Download Video';
}, 2000);
}
}
// Initialize
document.addEventListener("DOMContentLoaded", function() {
const input = document.getElementById("input");
const form = document.getElementById("genvideo");
if (input) {
input.focus();
}
if (form) {
form.addEventListener("submit", genVideo);
}
// Handle Enter key press in the prompt input (but allow Shift+Enter for new lines)
if (input) {
input.addEventListener("keydown", function(event) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
genVideo(event);
}
});
}
// Hide loader initially
const loader = document.getElementById("loader");
if (loader) {
loader.classList.add("hidden");
}
});