feat(UI): image generation improvements (#7804)
* chore: drop mode from image generation(unused) Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(UI): improve image generation front-end Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(UI): only ref images. files is to be deprecated Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * do not override default steps Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
3f1631aa87
commit
797f27f09f
@@ -301,7 +301,6 @@ message TranscriptSegment {
|
||||
message GenerateImageRequest {
|
||||
int32 height = 1;
|
||||
int32 width = 2;
|
||||
int32 mode = 3;
|
||||
int32 step = 4;
|
||||
int32 seed = 5;
|
||||
string positive_prompt = 6;
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
model "github.com/mudler/LocalAI/pkg/model"
|
||||
)
|
||||
|
||||
func ImageGeneration(height, width, mode, step, seed int, positive_prompt, negative_prompt, src, dst string, loader *model.ModelLoader, modelConfig config.ModelConfig, appConfig *config.ApplicationConfig, refImages []string) (func() error, error) {
|
||||
func ImageGeneration(height, width, step, seed int, positive_prompt, negative_prompt, src, dst string, loader *model.ModelLoader, modelConfig config.ModelConfig, appConfig *config.ApplicationConfig, refImages []string) (func() error, error) {
|
||||
|
||||
opts := ModelOptions(modelConfig, appConfig)
|
||||
inferenceModel, err := loader.Load(
|
||||
@@ -23,7 +23,6 @@ func ImageGeneration(height, width, mode, step, seed int, positive_prompt, negat
|
||||
&proto.GenerateImageRequest{
|
||||
Height: int32(height),
|
||||
Width: int32(width),
|
||||
Mode: int32(mode),
|
||||
Step: int32(step),
|
||||
Seed: int32(seed),
|
||||
CLIPSkip: int32(modelConfig.Diffusers.ClipSkip),
|
||||
|
||||
@@ -157,16 +157,11 @@ func ImageEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfi
|
||||
negative_prompt = prompts[1]
|
||||
}
|
||||
|
||||
mode := 0
|
||||
step := config.Step
|
||||
if step == 0 {
|
||||
step = 15
|
||||
}
|
||||
|
||||
if input.Mode != 0 {
|
||||
mode = input.Mode
|
||||
}
|
||||
|
||||
if input.Step != 0 {
|
||||
step = input.Step
|
||||
}
|
||||
@@ -197,7 +192,7 @@ func ImageEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfi
|
||||
inputSrc = inputImages[0]
|
||||
}
|
||||
|
||||
fn, err := backend.ImageGeneration(height, width, mode, step, *config.Seed, positive_prompt, negative_prompt, inputSrc, output, ml, *config, appConfig, refImages)
|
||||
fn, err := backend.ImageGeneration(height, width, step, *config.Seed, positive_prompt, negative_prompt, inputSrc, output, ml, *config, appConfig, refImages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ func InpaintingEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, app
|
||||
// Note: ImageGenerationFunc will call into the loaded model's GenerateImage which expects src JSON
|
||||
// Also pass ref images (orig + mask) so backends that support ref images can use them.
|
||||
refImages := []string{origRef, maskRef}
|
||||
fn, err := backend.ImageGenerationFunc(height, width, 0, steps, 0, prompt, "", jsonPath, dst, ml, *cfg, appConfig, refImages)
|
||||
fn, err := backend.ImageGenerationFunc(height, width, steps, 0, prompt, "", jsonPath, dst, ml, *cfg, appConfig, refImages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAI/core/http/middleware"
|
||||
"github.com/mudler/LocalAI/core/backend"
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/core/http/middleware"
|
||||
model "github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -58,7 +58,7 @@ func TestInpainting_HappyPath(t *testing.T) {
|
||||
|
||||
// stub the backend.ImageGenerationFunc
|
||||
orig := backend.ImageGenerationFunc
|
||||
backend.ImageGenerationFunc = func(height, width, mode, step, seed int, positive_prompt, negative_prompt, src, dst string, loader *model.ModelLoader, modelConfig config.ModelConfig, appConfig *config.ApplicationConfig, refImages []string) (func() error, error) {
|
||||
backend.ImageGenerationFunc = func(height, width, step, seed int, positive_prompt, negative_prompt, src, dst string, loader *model.ModelLoader, modelConfig config.ModelConfig, appConfig *config.ApplicationConfig, refImages []string) (func() error, error) {
|
||||
fn := func() error {
|
||||
// write a fake png file to dst
|
||||
return os.WriteFile(dst, []byte("PNGDATA"), 0644)
|
||||
|
||||
@@ -1,61 +1,255 @@
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to read multiple files
|
||||
async function filesToBase64Array(fileList) {
|
||||
const base64Array = [];
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const base64 = await fileToBase64(fileList[i]);
|
||||
base64Array.push(base64);
|
||||
}
|
||||
return base64Array;
|
||||
}
|
||||
|
||||
function genImage(event) {
|
||||
event.preventDefault();
|
||||
const input = document.getElementById("input").value;
|
||||
|
||||
promptDallE(input);
|
||||
promptDallE();
|
||||
}
|
||||
|
||||
async function promptDallE(input) {
|
||||
document.getElementById("loader").style.display = "block";
|
||||
document.getElementById("input").value = "";
|
||||
document.getElementById("input").disabled = true;
|
||||
|
||||
const model = document.getElementById("image-model").value;
|
||||
const size = document.getElementById("image-size").value;
|
||||
const response = await fetch("v1/images/generations", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
steps: 10,
|
||||
prompt: input,
|
||||
n: 1,
|
||||
size: size,
|
||||
}),
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.error) {
|
||||
// Display error if there is one
|
||||
var div = document.getElementById('result'); // Get the div by its ID
|
||||
div.innerHTML = '<p style="color:red;">' + json.error.message + '</p>';
|
||||
async function promptDallE() {
|
||||
const loader = document.getElementById("loader");
|
||||
const input = document.getElementById("input");
|
||||
const generateBtn = document.getElementById("generate-btn");
|
||||
const resultDiv = document.getElementById("result");
|
||||
|
||||
// Show loader and disable form
|
||||
loader.style.display = "block";
|
||||
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.style.display = "none";
|
||||
input.disabled = false;
|
||||
generateBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
const url = json.data[0].url;
|
||||
|
||||
var div = document.getElementById('result'); // Get the div by its ID
|
||||
var img = document.createElement('img'); // Create a new img element
|
||||
img.src = url; // Set the source of the image
|
||||
img.alt = 'Generated image'; // Set the alt text of the image
|
||||
// Collect all form values
|
||||
const model = document.getElementById("image-model").value;
|
||||
const size = document.getElementById("image-size").value;
|
||||
const negativePrompt = document.getElementById("negative-prompt").value.trim();
|
||||
const n = parseInt(document.getElementById("image-count").value) || 1;
|
||||
const stepInput = document.getElementById("image-steps").value.trim();
|
||||
const step = stepInput ? parseInt(stepInput) : undefined;
|
||||
const seedInput = document.getElementById("image-seed").value.trim();
|
||||
const seed = seedInput ? parseInt(seedInput) : undefined;
|
||||
|
||||
div.innerHTML = ''; // Clear the existing content of the div
|
||||
div.appendChild(img); // Add the new img element to the div
|
||||
// Prepare request body
|
||||
// Combine prompt and negative prompt with "|" separator (backend expects this format)
|
||||
let combinedPrompt = prompt;
|
||||
if (negativePrompt) {
|
||||
combinedPrompt = prompt + "|" + negativePrompt;
|
||||
}
|
||||
|
||||
document.getElementById("loader").style.display = "none";
|
||||
document.getElementById("input").disabled = false;
|
||||
document.getElementById("input").focus();
|
||||
const requestBody = {
|
||||
model: model,
|
||||
prompt: combinedPrompt,
|
||||
n: n,
|
||||
size: size,
|
||||
};
|
||||
|
||||
if (step !== undefined) {
|
||||
requestBody.step = step;
|
||||
}
|
||||
|
||||
if (seed !== undefined) {
|
||||
requestBody.seed = seed;
|
||||
}
|
||||
|
||||
// Handle file inputs
|
||||
try {
|
||||
// Source image (single file for img2img)
|
||||
const sourceImageInput = document.getElementById("source-image");
|
||||
if (sourceImageInput.files.length > 0) {
|
||||
const base64 = await fileToBase64(sourceImageInput.files[0]);
|
||||
requestBody.file = base64;
|
||||
}
|
||||
|
||||
// Reference images (collect from all dynamic inputs)
|
||||
const refImageInputs = document.querySelectorAll('.reference-image-file');
|
||||
const refImageFiles = [];
|
||||
for (const input of refImageInputs) {
|
||||
if (input.files.length > 0) {
|
||||
refImageFiles.push(input.files[0]);
|
||||
}
|
||||
}
|
||||
if (refImageFiles.length > 0) {
|
||||
const base64Array = await filesToBase64Array(refImageFiles);
|
||||
requestBody.ref_images = base64Array;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing image files:", error);
|
||||
resultDiv.innerHTML = '<p class="text-red-500">Error processing image files: ' + error.message + '</p>';
|
||||
loader.style.display = "none";
|
||||
input.disabled = false;
|
||||
generateBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Make API request
|
||||
try {
|
||||
const response = await fetch("v1/images/generations", {
|
||||
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-red-500 p-4">Error: ' + json.error.message + '</p>';
|
||||
loader.style.display = "none";
|
||||
input.disabled = false;
|
||||
generateBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear result div
|
||||
resultDiv.innerHTML = '';
|
||||
|
||||
// Display all generated images
|
||||
if (json.data && json.data.length > 0) {
|
||||
json.data.forEach((item, index) => {
|
||||
const imageContainer = document.createElement("div");
|
||||
imageContainer.className = "mb-6 bg-[var(--color-bg-primary)]/50 border border-[#1E293B] rounded-xl p-4";
|
||||
|
||||
// Create image element
|
||||
const img = document.createElement("img");
|
||||
if (item.url) {
|
||||
img.src = item.url;
|
||||
} else if (item.b64_json) {
|
||||
img.src = "data:image/png;base64," + item.b64_json;
|
||||
} else {
|
||||
return; // Skip invalid items
|
||||
}
|
||||
img.alt = prompt;
|
||||
img.className = "w-full h-auto rounded-lg mb-3";
|
||||
imageContainer.appendChild(img);
|
||||
|
||||
// Create caption container
|
||||
const captionDiv = document.createElement("div");
|
||||
captionDiv.className = "mt-3 p-3 bg-[var(--color-bg-secondary)] rounded-lg";
|
||||
|
||||
// Prompt caption
|
||||
const promptCaption = document.createElement("p");
|
||||
promptCaption.className = "text-sm text-[var(--color-text-primary)] mb-2";
|
||||
promptCaption.innerHTML = '<strong>Prompt:</strong> ' + escapeHtml(prompt);
|
||||
captionDiv.appendChild(promptCaption);
|
||||
|
||||
// Negative prompt if provided
|
||||
if (negativePrompt) {
|
||||
const negativeCaption = document.createElement("p");
|
||||
negativeCaption.className = "text-sm text-[var(--color-text-secondary)] mb-2";
|
||||
negativeCaption.innerHTML = '<strong>Negative Prompt:</strong> ' + escapeHtml(negativePrompt);
|
||||
captionDiv.appendChild(negativeCaption);
|
||||
}
|
||||
|
||||
// Generation details
|
||||
const detailsDiv = document.createElement("div");
|
||||
detailsDiv.className = "flex flex-wrap gap-4 text-xs text-[var(--color-text-secondary)] mt-2";
|
||||
detailsDiv.innerHTML = `
|
||||
<span><strong>Size:</strong> ${size}</span>
|
||||
${step !== undefined ? `<span><strong>Steps:</strong> ${step}</span>` : ''}
|
||||
${seed !== undefined ? `<span><strong>Seed:</strong> ${seed}</span>` : ''}
|
||||
`;
|
||||
captionDiv.appendChild(detailsDiv);
|
||||
|
||||
// Copy prompt button
|
||||
const copyBtn = document.createElement("button");
|
||||
copyBtn.className = "mt-2 px-3 py-1 text-xs 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);
|
||||
});
|
||||
};
|
||||
captionDiv.appendChild(copyBtn);
|
||||
|
||||
imageContainer.appendChild(captionDiv);
|
||||
resultDiv.appendChild(imageContainer);
|
||||
});
|
||||
} else {
|
||||
resultDiv.innerHTML = '<p class="text-[var(--color-text-secondary)] p-4">No images were generated.</p>';
|
||||
}
|
||||
|
||||
// Preserve prompt in input field (don't clear it)
|
||||
// The prompt is already in the input field, so we don't need to restore it
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error generating image:", error);
|
||||
resultDiv.innerHTML = '<p class="text-red-500 p-4">Error: ' + error.message + '</p>';
|
||||
} finally {
|
||||
// Hide loader and re-enable form
|
||||
loader.style.display = "none";
|
||||
input.disabled = false;
|
||||
generateBtn.disabled = false;
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("input").focus();
|
||||
document.getElementById("genimage").addEventListener("submit", genImage);
|
||||
// Helper function to escape HTML
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Handle Enter key press in the prompt input
|
||||
document.getElementById("input").addEventListener("keypress", function(event) {
|
||||
if (event.key === "Enter") {
|
||||
// Initialize
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const input = document.getElementById("input");
|
||||
const form = document.getElementById("genimage");
|
||||
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
|
||||
if (form) {
|
||||
form.addEventListener("submit", genImage);
|
||||
}
|
||||
|
||||
// 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();
|
||||
genImage(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("loader").style.display = "none";
|
||||
// Hide loader initially
|
||||
const loader = document.getElementById("loader");
|
||||
if (loader) {
|
||||
loader.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
@@ -55,46 +55,176 @@
|
||||
<div class="relative">
|
||||
<input id="image-model" type="hidden" value="{{.Model}}">
|
||||
<form id="genimage" action="text2image/{{.Model}}" method="get" class="mb-8">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<i class="fas fa-magic text-[var(--color-primary)]"></i>
|
||||
<!-- Basic Settings -->
|
||||
<div class="space-y-4">
|
||||
<!-- Prompt -->
|
||||
<div class="relative">
|
||||
<label for="input" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-magic mr-2 text-[var(--color-primary)]"></i>Prompt:
|
||||
</label>
|
||||
<div class="relative">
|
||||
<textarea
|
||||
id="input"
|
||||
name="input"
|
||||
placeholder="Describe the image you want to generate..."
|
||||
autocomplete="off"
|
||||
rows="3"
|
||||
class="input w-full pr-12 py-4 text-lg resize-y"
|
||||
required
|
||||
></textarea>
|
||||
<span id="loader" class="loader absolute right-4 top-4 hidden" style="top: 1rem; right: 1rem;">
|
||||
<svg class="animate-spin h-6 w-6 text-[var(--color-primary)]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Negative Prompt -->
|
||||
<div>
|
||||
<label for="negative-prompt" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-ban mr-2 text-[var(--color-primary)]"></i>Negative Prompt (optional):
|
||||
</label>
|
||||
<textarea
|
||||
id="negative-prompt"
|
||||
name="negative-prompt"
|
||||
placeholder="Things to avoid in the image..."
|
||||
rows="2"
|
||||
class="input w-full py-2 resize-y"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Size Selection with Presets -->
|
||||
<div>
|
||||
<label for="image-size" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-expand-arrows-alt mr-2 text-[var(--color-primary)]"></i>Image Size:
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
<button type="button" class="size-preset px-3 py-1 text-sm rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="256x256">256×256</button>
|
||||
<button type="button" class="size-preset px-3 py-1 text-sm rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="512x512">512×512</button>
|
||||
<button type="button" class="size-preset px-3 py-1 text-sm rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="768x768">768×768</button>
|
||||
<button type="button" class="size-preset px-3 py-1 text-sm rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)]" data-size="1024x1024">1024×1024</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="image-size"
|
||||
value="512x512"
|
||||
placeholder="e.g., 256x256, 512x512, 1024x1024"
|
||||
class="input p-2.5 w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Number of Images -->
|
||||
<div>
|
||||
<label for="image-count" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-images mr-2 text-[var(--color-primary)]"></i>Number of Images:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="image-count"
|
||||
name="n"
|
||||
min="1"
|
||||
max="4"
|
||||
value="1"
|
||||
class="input p-2.5 w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="input"
|
||||
name="input"
|
||||
placeholder="Describe the image you want to generate..."
|
||||
autocomplete="off"
|
||||
class="input w-full pr-12 py-4 text-lg"
|
||||
style="padding-left: 3.5rem !important;"
|
||||
required
|
||||
/>
|
||||
<span id="loader" class="my-2 loader absolute right-4 top-4 hidden">
|
||||
<svg class="animate-spin h-6 w-6 text-[var(--color-primary)]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Size Selection -->
|
||||
<div class="mt-4">
|
||||
<label for="image-size" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-expand-arrows-alt mr-2 text-[var(--color-primary)]"></i>Image Size:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="image-size"
|
||||
value="256x256"
|
||||
placeholder="e.g., 256x256, 512x512, 1024x1024"
|
||||
class="input p-2.5 w-full max-w-xs"
|
||||
/>
|
||||
|
||||
<!-- Advanced Settings (Collapsible) -->
|
||||
<div class="mt-6">
|
||||
<button type="button" id="advanced-toggle" class="flex items-center justify-between w-full text-left text-sm font-medium text-[var(--color-text-primary)] hover:text-[var(--color-primary)]">
|
||||
<span><i class="fas fa-cog mr-2"></i>Advanced Settings</span>
|
||||
<i class="fas fa-chevron-down" id="advanced-chevron"></i>
|
||||
</button>
|
||||
<div id="advanced-settings" class="hidden mt-4 space-y-4">
|
||||
<!-- Steps -->
|
||||
<div>
|
||||
<label for="image-steps" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-step-forward mr-2 text-[var(--color-primary)]"></i>Steps:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="image-steps"
|
||||
name="step"
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="Leave empty for default"
|
||||
class="input p-2.5 w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Seed -->
|
||||
<div>
|
||||
<label for="image-seed" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-seedling mr-2 text-[var(--color-primary)]"></i>Seed (optional):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="image-seed"
|
||||
name="seed"
|
||||
min="0"
|
||||
placeholder="Leave empty for random"
|
||||
class="input p-2.5 w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Inputs (Collapsible) -->
|
||||
<div class="mt-6">
|
||||
<button type="button" id="image-inputs-toggle" class="flex items-center justify-between w-full text-left text-sm font-medium text-[var(--color-text-primary)] hover:text-[var(--color-primary)]">
|
||||
<span><i class="fas fa-image mr-2"></i>Image Inputs (for img2img/inpainting)</span>
|
||||
<i class="fas fa-chevron-down" id="image-inputs-chevron"></i>
|
||||
</button>
|
||||
<div id="image-inputs-settings" class="hidden mt-4 space-y-4">
|
||||
<!-- Source Image (img2img) -->
|
||||
<div>
|
||||
<label for="source-image" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||
<i class="fas fa-file-image mr-2 text-[var(--color-primary)]"></i>Source Image (img2img):
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="source-image"
|
||||
name="file"
|
||||
accept="image/*"
|
||||
class="input p-2.5 w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Reference Images (Dynamic) -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="block text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
<i class="fas fa-images mr-2 text-[var(--color-primary)]"></i>Multiple Input Images:
|
||||
</label>
|
||||
<button type="button" id="add-reference-image" class="px-3 py-1 text-sm bg-[var(--color-primary)] text-white rounded hover:opacity-80">
|
||||
<i class="fas fa-plus mr-1"></i>Add Image
|
||||
</button>
|
||||
</div>
|
||||
<div id="reference-images-container" class="space-y-2">
|
||||
<div class="reference-image-item flex items-center gap-2">
|
||||
<input
|
||||
type="file"
|
||||
class="reference-image-file input p-2.5 flex-1"
|
||||
accept="image/*"
|
||||
data-type="ref_images"
|
||||
/>
|
||||
<button type="button" class="remove-reference-image px-3 py-2 text-sm bg-red-500 text-white rounded hover:opacity-80 hidden">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
id="generate-btn"
|
||||
class="btn-primary w-full"
|
||||
>
|
||||
<i class="fas fa-magic mr-2"></i>Generate Image
|
||||
@@ -104,10 +234,10 @@
|
||||
|
||||
<!-- Image Results Container -->
|
||||
<div class="mt-6 border-t border-[#1E293B] pt-6">
|
||||
<h3 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4">Generated Image</h3>
|
||||
<div class="container mx-auto flex justify-center">
|
||||
<div id="result" class="mx-auto bg-[var(--color-bg-primary)]/50 border border-[#1E293B] rounded-xl p-4 min-h-[300px] w-full flex items-center justify-center">
|
||||
<p class="text-[var(--color-text-secondary)] italic">Your generated image will appear here</p>
|
||||
<h3 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4">Generated Images</h3>
|
||||
<div id="result" class="space-y-6">
|
||||
<div class="bg-[var(--color-bg-primary)]/50 border border-[#1E293B] rounded-xl p-4 min-h-[300px] flex items-center justify-center">
|
||||
<p class="text-[var(--color-text-secondary)] italic">Your generated images will appear here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,10 +249,89 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Show loader when form is submitted
|
||||
document.getElementById('genimage').addEventListener('submit', function() {
|
||||
document.getElementById('loader').classList.remove('hidden');
|
||||
// Collapsible sections
|
||||
document.getElementById('advanced-toggle').addEventListener('click', function() {
|
||||
const settings = document.getElementById('advanced-settings');
|
||||
const chevron = document.getElementById('advanced-chevron');
|
||||
settings.classList.toggle('hidden');
|
||||
chevron.classList.toggle('fa-chevron-down');
|
||||
chevron.classList.toggle('fa-chevron-up');
|
||||
});
|
||||
|
||||
document.getElementById('image-inputs-toggle').addEventListener('click', function() {
|
||||
const settings = document.getElementById('image-inputs-settings');
|
||||
const chevron = document.getElementById('image-inputs-chevron');
|
||||
settings.classList.toggle('hidden');
|
||||
chevron.classList.toggle('fa-chevron-down');
|
||||
chevron.classList.toggle('fa-chevron-up');
|
||||
});
|
||||
|
||||
// Size preset buttons
|
||||
document.querySelectorAll('.size-preset').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const size = this.getAttribute('data-size');
|
||||
document.getElementById('image-size').value = size;
|
||||
// Update active state
|
||||
document.querySelectorAll('.size-preset').forEach(btn => {
|
||||
btn.classList.remove('bg-[var(--color-primary)]', 'text-white');
|
||||
});
|
||||
this.classList.add('bg-[var(--color-primary)]', 'text-white');
|
||||
});
|
||||
});
|
||||
|
||||
// Set initial active size preset
|
||||
document.querySelector('.size-preset[data-size="512x512"]').classList.add('bg-[var(--color-primary)]', 'text-white');
|
||||
|
||||
// Dynamic image inputs for Reference Images
|
||||
function addReferenceImage() {
|
||||
const container = document.getElementById('reference-images-container');
|
||||
const newItem = document.createElement('div');
|
||||
newItem.className = 'reference-image-item flex items-center gap-2';
|
||||
newItem.innerHTML = `
|
||||
<input
|
||||
type="file"
|
||||
class="reference-image-file input p-2.5 flex-1"
|
||||
accept="image/*"
|
||||
data-type="ref_images"
|
||||
/>
|
||||
<button type="button" class="remove-reference-image px-3 py-2 text-sm bg-red-500 text-white rounded hover:opacity-80">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(newItem);
|
||||
updateRemoveButtons('reference-images-container', 'remove-reference-image');
|
||||
}
|
||||
|
||||
function removeReferenceImage(button) {
|
||||
const container = document.getElementById('reference-images-container');
|
||||
if (container.children.length > 1) {
|
||||
button.closest('.reference-image-item').remove();
|
||||
updateRemoveButtons('reference-images-container', 'remove-reference-image');
|
||||
}
|
||||
}
|
||||
|
||||
// Update remove button visibility (hide if only one item, show if multiple)
|
||||
function updateRemoveButtons(containerId, buttonClass) {
|
||||
const container = document.getElementById(containerId);
|
||||
const buttons = container.querySelectorAll('.' + buttonClass);
|
||||
if (container.children.length > 1) {
|
||||
buttons.forEach(btn => btn.classList.remove('hidden'));
|
||||
} else {
|
||||
buttons.forEach(btn => btn.classList.add('hidden'));
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners for dynamic inputs
|
||||
document.getElementById('add-reference-image').addEventListener('click', addReferenceImage);
|
||||
|
||||
document.getElementById('reference-images-container').addEventListener('click', function(e) {
|
||||
if (e.target.closest('.remove-reference-image')) {
|
||||
removeReferenceImage(e.target.closest('.remove-reference-image'));
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize remove button visibility
|
||||
updateRemoveButtons('reference-images-container', 'remove-reference-image');
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -160,7 +160,6 @@ type OpenAIRequest struct {
|
||||
Stream bool `json:"stream"`
|
||||
|
||||
// Image (not supported by OpenAI)
|
||||
Mode int `json:"mode"`
|
||||
Quality string `json:"quality"`
|
||||
Step int `json:"step"`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user