Fix backend port handling, generation progress sync, and restore img2img UI

This commit is contained in:
techjarves committed 2026-06-07 10:45:47 +05:30
1 parent 992a642b40
commit c78960c6f4
9 files changed
+259 -257

No files matched your search

+78 -3
View File
@@ -1,5 +1,5 @@
import React, { memo, useState, useRef, useCallback } from "react";
import { Sparkles, Download, Copy, RefreshCw, Check, Sliders, Trash2 } from "lucide-react";
import { Sparkles, Download, Copy, RefreshCw, Check, Sliders, Trash2, ImagePlus } from "lucide-react";
import { generateImage, startServer, stopServer, waitForServerReady, getBackendStatus, getGenerationProgress, saveGeneratedOutput, deleteGeneratedOutputs } from "../services/api";
const GalleryItem = memo(({ img, idx, isSelected, onClick }) => {
@@ -55,10 +55,27 @@ function Generator({
const [errorMsg, setErrorMsg] = useState(null);
const [isDecoding, setIsDecoding] = useState(false);
const [selectedGalleryIndexes, setSelectedGalleryIndexes] = useState([]);
const [baseImage, setBaseImage] = useState(null);
const timerRef = useRef(null);
const abortControllerRef = useRef(null);
const hasRealGenerationStepRef = useRef(false);
const handleBaseImageSelect = (e) => {
const file = e.target.files?.[0];
e.target.value = "";
if (!file) return;
if (!file.type.startsWith("image/")) {
setErrorMsg("Please choose an image file (PNG, JPG, or WEBP).");
return;
}
const reader = new FileReader();
reader.onload = () => setBaseImage(reader.result);
reader.onerror = () => setErrorMsg("Could not read the selected image file.");
reader.readAsDataURL(file);
};
const handleClearBaseImage = () => setBaseImage(null);
// Trigger main image generation process
const handleGenerate = async () => {
if (!prompt.trim()) return;
@@ -299,7 +316,7 @@ function Generator({
negativePrompt,
constraints,
activeModel,
null,
baseImage,
(prog) => setGenerationProgress((prev) => Math.max(prev, prog)),
abortControllerRef.current.signal
);
@@ -320,6 +337,8 @@ function Generator({
height: constraints.height,
sampler: constraints.sampler,
model: activeModel,
mode: baseImage ? "img2img" : "txt2img",
denoisingStrength: baseImage ? constraints.denoisingStrength : null,
duration_sec: result.duration_sec,
timestamp: new Date().toLocaleTimeString(),
};
@@ -588,6 +607,62 @@ function Generator({
/>
</div>
<div className="m3-text-field">
<label className="m3-text-field-label">Base Image (Optional)</label>
{baseImage ? (
<div style={{ display: "flex", gap: "12px", alignItems: "center", padding: "12px", background: "var(--md-sys-color-surface-variant)", borderRadius: "var(--md-shape-corner-medium)", border: "1px solid var(--md-sys-color-outline-variant)" }}>
<img src={baseImage} alt="Base" style={{ width: "64px", height: "64px", objectFit: "cover", borderRadius: "8px", flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: "0.85rem" }}>Base image ready</div>
<div style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)" }}>
The AI will redraw this image guided by your prompt.
</div>
</div>
<button type="button" className="m3-btn m3-btn-error" style={{ height: "34px", flexShrink: 0 }} onClick={handleClearBaseImage} disabled={isGenerating}>
<Trash2 size={14} />
<span>Remove</span>
</button>
</div>
) : (
<label className="import-box" style={{ margin: 0, padding: "16px", cursor: isGenerating ? "not-allowed" : "pointer" }}>
<input
type="file"
accept="image/png,image/jpeg,image/webp"
style={{ display: "none" }}
onChange={handleBaseImageSelect}
disabled={isGenerating}
/>
<ImagePlus className="import-icon" />
<span style={{ fontWeight: 600 }}>Upload a base image</span>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", textAlign: "center" }}>
Optional. Generate a new image based on one of your own.
</span>
</label>
)}
{baseImage && (
<div style={{ marginTop: "12px" }}>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.8rem", marginBottom: "4px" }}>
<span>Transformation strength</span>
<span>{constraints.denoisingStrength.toFixed(2)}</span>
</div>
<input
type="range"
min="0.1"
max="1"
step="0.05"
value={constraints.denoisingStrength}
onChange={(e) => setConstraints((prev) => ({ ...prev, denoisingStrength: parseFloat(e.target.value) }))}
disabled={isGenerating}
style={{ width: "100%" }}
/>
<div style={{ fontSize: "0.72rem", color: "var(--md-sys-color-outline)", marginTop: "2px" }}>
Lower = closer to your image; higher = more creative freedom. 0.7 is a good start.
</div>
</div>
)}
</div>
{/* Configuration Status Chips (Material 3 style) */}
<div className="m3-text-field">
<label className="m3-text-field-label">Active Image Constraints</label>
@@ -618,7 +693,7 @@ function Generator({
disabled={isGenerating || !prompt.trim() || !activeModel}
>
<Sparkles size={18} />
<span>{!activeModel ? "Select Model to Generate" : "Generate Local Image"}</span>
<span>{!activeModel ? "Select Model to Generate" : baseImage ? "Generate from Base Image" : "Generate Local Image"}</span>
</button>
</div>
</div>
+23 -6
View File
@@ -528,11 +528,11 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM
};
};
// stable-diffusion.cpp server only exposes /v1/images/generations (OpenAI-compat)
// Steps, cfg_scale, seed, sample_method are passed in the body and read by the server
// txt2img uses /v1/images/generations; img2img uses /sdapi/v1/img2img.
const isImg2Img = !!payload.image;
let endpoint = `${baseUrl}/v1/images/generations`;
const genBody = {
let genBody = {
prompt: payload.prompt,
negative_prompt: payload.negative_prompt || "",
n: 1,
@@ -547,13 +547,30 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM
// img2img extra fields
if (isImg2Img) {
genBody.init_images = [payload.image];
genBody.denoising_strength = payload.denoising_strength || 0.7;
const initBase64 = String(payload.image).replace(/^data:[^;]+;base64,/, "");
endpoint = `${baseUrl}/sdapi/v1/img2img`;
genBody = {
init_images: [initBase64],
prompt: payload.prompt,
negative_prompt: payload.negative_prompt || "",
denoising_strength: payload.denoising_strength || 0.7,
steps: payload.steps,
cfg_scale: payload.cfg_scale,
seed: payload.seed,
width: payload.width,
height: payload.height,
sampler_name: payload.sampler || "euler_a",
sample_method: payload.sampler || "euler_a",
batch_size: 1,
n_iter: 1,
send_images: true,
save_images: false,
};
}
// Attempt real HTTP call
try {
const response = await fetch(`${baseUrl}/v1/images/generations`, {
const response = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: signal,